diff --git a/README.md b/README.md
index 7b1eece..d24ebea 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,14 @@ just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen
again, even after a reboot.
+Required Software
+=================
+
+ - You need PyXAPI, available here:
+ http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
+ - You also need autossh, available in various package management systems
+ - Python 2.x, both locally and the remote system
+
sshuttle: where transparent proxy meets VPN meets ssh
=====================================================
@@ -51,19 +59,34 @@ Prerequisites
Linux.)
- If you use MacOS or BSD on your client machine:
- Your kernel needs to be compiled with IPFIREWALL_FORWARD
+ Your kernel needs to be compiled with `IPFIREWALL_FORWARD`
(MacOS has this by default) and you need to have ipfw
available. (The server doesn't need to be MacOS or BSD.)
-This is how you use it:
+Obtaining sshuttle
+------------------
+
+ - First, go get PyXAPI from the link above
+
+ - Clone github.com/jwyllie83/sshuttle/tree/local
+
+
+Usage on (Ubuntu) Linux
-----------------------
- - git clone git://github.com/apenwarr/sshuttle
- on your client machine. You'll need root or sudo
- access, and python needs to be installed.
+ - `cd packaging; ./make_deb`
+
+ - `sudo dpkg -i ./sshuttle-VERSION.deb`
+
+ - Check out the files in `/etc/sshuttle`; configure them so your tunnel works
+
+ - `sudo service sshuttle start`
+
+
+Usage on other Linuxes and OSes
+-------------------------------
- - The most basic use of sshuttle looks like:
./sshuttle -r username@sshserver 0.0.0.0/0 -vv
- There is a shortcut for 0.0.0.0/0 for those that value
@@ -83,6 +106,9 @@ then the remote ssh password. Or you might have sudo and ssh set
up to not require passwords, in which case you won't be
prompted at all.)
+Usage Notes
+-----------
+
That's it! Now your local machine can access the remote network as if you
were right there. And if your "client" machine is a router, everyone on
your local network can make connections to your remote network.
diff --git a/client.py b/client.py
deleted file mode 100644
index 449a75a..0000000
--- a/client.py
+++ /dev/null
@@ -1,394 +0,0 @@
-import struct, socket, select, errno, re, signal, time
-import compat.ssubprocess as ssubprocess
-import helpers, ssnet, ssh, ssyslog
-from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
-from helpers import *
-
-_extra_fd = os.open('/dev/null', os.O_RDONLY)
-
-def got_signal(signum, frame):
- log('exiting on signal %d\n' % signum)
- sys.exit(1)
-
-
-_pidname = None
-def check_daemon(pidfile):
- global _pidname
- _pidname = os.path.abspath(pidfile)
- try:
- oldpid = open(_pidname).read(1024)
- except IOError, e:
- if e.errno == errno.ENOENT:
- return # no pidfile, ok
- else:
- raise Fatal("can't read %s: %s" % (_pidname, e))
- if not oldpid:
- os.unlink(_pidname)
- return # invalid pidfile, ok
- oldpid = int(oldpid.strip() or 0)
- if oldpid <= 0:
- os.unlink(_pidname)
- return # invalid pidfile, ok
- try:
- os.kill(oldpid, 0)
- except OSError, e:
- if e.errno == errno.ESRCH:
- os.unlink(_pidname)
- return # outdated pidfile, ok
- elif e.errno == errno.EPERM:
- pass
- else:
- raise
- raise Fatal("%s: sshuttle is already running (pid=%d)"
- % (_pidname, oldpid))
-
-
-def daemonize():
- if os.fork():
- os._exit(0)
- os.setsid()
- if os.fork():
- os._exit(0)
-
- outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666)
- try:
- os.write(outfd, '%d\n' % os.getpid())
- finally:
- os.close(outfd)
- os.chdir("/")
-
- # Normal exit when killed, or try/finally won't work and the pidfile won't
- # be deleted.
- signal.signal(signal.SIGTERM, got_signal)
-
- si = open('/dev/null', 'r+')
- os.dup2(si.fileno(), 0)
- os.dup2(si.fileno(), 1)
- si.close()
-
- ssyslog.stderr_to_syslog()
-
-
-def daemon_cleanup():
- try:
- os.unlink(_pidname)
- except OSError, e:
- if e.errno == errno.ENOENT:
- pass
- else:
- raise
-
-
-def original_dst(sock):
- try:
- SO_ORIGINAL_DST = 80
- SOCKADDR_MIN = 16
- sockaddr_in = sock.getsockopt(socket.SOL_IP,
- SO_ORIGINAL_DST, SOCKADDR_MIN)
- (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
- assert(socket.htons(proto) == socket.AF_INET)
- ip = '%d.%d.%d.%d' % (a,b,c,d)
- return (ip,port)
- except socket.error, e:
- if e.args[0] == errno.ENOPROTOOPT:
- return sock.getsockname()
- raise
-
-
-class FirewallClient:
- def __init__(self, port, subnets_include, subnets_exclude, dnsport):
- self.port = port
- self.auto_nets = []
- self.subnets_include = subnets_include
- self.subnets_exclude = subnets_exclude
- self.dnsport = dnsport
- argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
- ['-v'] * (helpers.verbose or 0) +
- ['--firewall', str(port), str(dnsport)])
- if ssyslog._p:
- argvbase += ['--syslog']
- argv_tries = [
- ['sudo', '-p', '[local sudo] Password: '] + argvbase,
- ['su', '-c', ' '.join(argvbase)],
- argvbase
- ]
-
- # we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
- # because stupid Linux 'su' requires that stdin be attached to a tty.
- # Instead, attach a *bidirectional* socket to its stdout, and use
- # that for talking in both directions.
- (s1,s2) = socket.socketpair()
- def setup():
- # run in the child process
- s2.close()
- e = None
- if os.getuid() == 0:
- argv_tries = argv_tries[-1:] # last entry only
- for argv in argv_tries:
- try:
- if argv[0] == 'su':
- sys.stderr.write('[local su] ')
- self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
- e = None
- break
- except OSError, e:
- pass
- self.argv = argv
- s1.close()
- self.pfile = s2.makefile('wb+')
- if e:
- log('Spawning firewall manager: %r\n' % self.argv)
- raise Fatal(e)
- line = self.pfile.readline()
- self.check()
- if line != 'READY\n':
- raise Fatal('%r expected READY, got %r' % (self.argv, line))
-
- def check(self):
- rv = self.p.poll()
- if rv:
- raise Fatal('%r returned %d' % (self.argv, rv))
-
- def start(self):
- self.pfile.write('ROUTES\n')
- for (ip,width) in self.subnets_include+self.auto_nets:
- self.pfile.write('%d,0,%s\n' % (width, ip))
- for (ip,width) in self.subnets_exclude:
- self.pfile.write('%d,1,%s\n' % (width, ip))
- self.pfile.write('GO\n')
- self.pfile.flush()
- line = self.pfile.readline()
- self.check()
- if line != 'STARTED\n':
- raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
-
- def sethostip(self, hostname, ip):
- assert(not re.search(r'[^-\w]', hostname))
- assert(not re.search(r'[^0-9.]', ip))
- self.pfile.write('HOST %s,%s\n' % (hostname, ip))
- self.pfile.flush()
-
- def done(self):
- self.pfile.close()
- rv = self.p.wait()
- if rv:
- raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
-
-
-def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
- dnslistener, seed_hosts, auto_nets,
- syslog, daemon):
- handlers = []
- if helpers.verbose >= 1:
- helpers.logprefix = 'c : '
- else:
- helpers.logprefix = 'client: '
- debug1('connecting to server...\n')
-
- try:
- (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
- stderr=ssyslog._p and ssyslog._p.stdin,
- options=dict(latency_control=latency_control))
- except socket.error, e:
- if e.args[0] == errno.EPIPE:
- raise Fatal("failed to establish ssh session (1)")
- else:
- raise
- mux = Mux(serversock, serversock)
- handlers.append(mux)
-
- expected = 'SSHUTTLE0001'
-
- try:
- v = 'x'
- while v and v != '\0':
- v = serversock.recv(1)
- v = 'x'
- while v and v != '\0':
- v = serversock.recv(1)
- initstring = serversock.recv(len(expected))
- except socket.error, e:
- if e.args[0] == errno.ECONNRESET:
- raise Fatal("failed to establish ssh session (2)")
- else:
- raise
-
- rv = serverproc.poll()
- if rv:
- raise Fatal('server died with error code %d' % rv)
-
- if initstring != expected:
- raise Fatal('expected server init string %r; got %r'
- % (expected, initstring))
- debug1('connected.\n')
- print 'Connected.'
- sys.stdout.flush()
- if daemon:
- daemonize()
- log('daemonizing (%s).\n' % _pidname)
- elif syslog:
- debug1('switching to syslog.\n')
- ssyslog.stderr_to_syslog()
-
- def onroutes(routestr):
- if auto_nets:
- for line in routestr.strip().split('\n'):
- (ip,width) = line.split(',', 1)
- fw.auto_nets.append((ip,int(width)))
-
- # we definitely want to do this *after* starting ssh, or we might end
- # up intercepting the ssh connection!
- #
- # Moreover, now that we have the --auto-nets option, we have to wait
- # for the server to send us that message anyway. Even if we haven't
- # set --auto-nets, we might as well wait for the message first, then
- # ignore its contents.
- mux.got_routes = None
- fw.start()
- mux.got_routes = onroutes
-
- def onhostlist(hostlist):
- debug2('got host list: %r\n' % hostlist)
- for line in hostlist.strip().split():
- if line:
- name,ip = line.split(',', 1)
- fw.sethostip(name, ip)
- mux.got_host_list = onhostlist
-
- def onaccept():
- global _extra_fd
- try:
- sock,srcip = listener.accept()
- except socket.error, e:
- if e.args[0] in [errno.EMFILE, errno.ENFILE]:
- debug1('Rejected incoming connection: too many open files!\n')
- # free up an fd so we can eat the connection
- os.close(_extra_fd)
- try:
- sock,srcip = listener.accept()
- sock.close()
- finally:
- _extra_fd = os.open('/dev/null', os.O_RDONLY)
- return
- else:
- raise
- dstip = original_dst(sock)
- debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
- dstip[0],dstip[1]))
- if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
- debug1("-- ignored: that's my address!\n")
- sock.close()
- return
- chan = mux.next_channel()
- if not chan:
- log('warning: too many open channels. Discarded connection.\n')
- sock.close()
- return
- mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
- outwrap = MuxWrapper(mux, chan)
- handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
- handlers.append(Handler([listener], onaccept))
-
- dnsreqs = {}
- def dns_done(chan, data):
- peer,timeout = dnsreqs.get(chan) or (None,None)
- debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
- if peer:
- del dnsreqs[chan]
- debug3('doing sendto %r\n' % (peer,))
- dnslistener.sendto(data, peer)
- def ondns():
- pkt,peer = dnslistener.recvfrom(4096)
- now = time.time()
- if pkt:
- debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
- chan = mux.next_channel()
- dnsreqs[chan] = peer,now+30
- mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
- mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
- for chan,(peer,timeout) in dnsreqs.items():
- if timeout < now:
- del dnsreqs[chan]
- debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
- if dnslistener:
- handlers.append(Handler([dnslistener], ondns))
-
- if seed_hosts != None:
- debug1('seed_hosts: %r\n' % seed_hosts)
- mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
-
- while 1:
- rv = serverproc.poll()
- if rv:
- raise Fatal('server died with error code %d' % rv)
-
- ssnet.runonce(handlers, mux)
- if latency_control:
- mux.check_fullness()
- mux.callback()
-
-
-def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
- seed_hosts, auto_nets,
- subnets_include, subnets_exclude, syslog, daemon, pidfile):
- if syslog:
- ssyslog.start_syslog()
- if daemon:
- try:
- check_daemon(pidfile)
- except Fatal, e:
- log("%s\n" % e)
- return 5
- debug1('Starting sshuttle proxy.\n')
-
- if listenip[1]:
- ports = [listenip[1]]
- else:
- ports = xrange(12300,9000,-1)
- last_e = None
- bound = False
- debug2('Binding:')
- for port in ports:
- debug2(' %d' % port)
- listener = socket.socket()
- listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- try:
- listener.bind((listenip[0], port))
- dnslistener.bind((listenip[0], port))
- bound = True
- break
- except socket.error, e:
- last_e = e
- debug2('\n')
- if not bound:
- assert(last_e)
- raise last_e
- listener.listen(10)
- listenip = listener.getsockname()
- debug1('Listening on %r.\n' % (listenip,))
-
- if dns:
- dnsip = dnslistener.getsockname()
- debug1('DNS listening on %r.\n' % (dnsip,))
- dnsport = dnsip[1]
- else:
- dnsport = 0
- dnslistener = None
-
- fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
-
- try:
- return _main(listener, fw, ssh_cmd, remotename,
- python, latency_control, dnslistener,
- seed_hosts, auto_nets, syslog, daemon)
- finally:
- try:
- if daemon:
- # it's not our child anymore; can't waitpid
- fw.p.returncode = 0
- fw.done()
- finally:
- if daemon:
- daemon_cleanup()
diff --git a/main.py b/main.py
deleted file mode 100644
index 1cf00af..0000000
--- a/main.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import sys, os, re
-import helpers, options, client, server, firewall, hostwatch
-import compat.ssubprocess as ssubprocess
-from helpers import *
-
-
-# list of:
-# 1.2.3.4/5 or just 1.2.3.4
-def parse_subnets(subnets_str):
- subnets = []
- for s in subnets_str:
- m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
- if not m:
- raise Fatal('%r is not a valid IP subnet format' % s)
- (a,b,c,d,width) = m.groups()
- (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
- if width == None:
- width = 32
- else:
- width = int(width)
- if a > 255 or b > 255 or c > 255 or d > 255:
- raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
- if width > 32:
- raise Fatal('*/%d is greater than the maximum of 32' % width)
- subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width))
- return subnets
-
-
-# 1.2.3.4:567 or just 1.2.3.4 or just 567
-def parse_ipport(s):
- s = str(s)
- m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
- if not m:
- raise Fatal('%r is not a valid IP:port format' % s)
- (a,b,c,d,port) = m.groups()
- (a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
- int(port or 0))
- if a > 255 or b > 255 or c > 255 or d > 255:
- raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
- if port > 65535:
- raise Fatal('*:%d is greater than the maximum of 65535' % port)
- if a == None:
- a = b = c = d = 0
- return ('%d.%d.%d.%d' % (a,b,c,d), port)
-
-
-optspec = """
-sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]]
-sshuttle --server
-sshuttle --firewall
-sshuttle --hostwatch
---
-l,listen= transproxy to this ip address and port number [127.0.0.1:0]
-H,auto-hosts scan for remote hostnames and update local /etc/hosts
-N,auto-nets automatically determine subnets to route
-dns capture local DNS requests and forward to the remote DNS server
-python= path to python interpreter on the remote server
-r,remote= ssh hostname (and optional username) of remote sshuttle server
-x,exclude= exclude this subnet (can be used more than once)
-v,verbose increase debug message verbosity
-e,ssh-cmd= the command to use to connect to the remote [ssh]
-seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
-no-latency-control sacrifice latency to improve bandwidth benchmarks
-wrap= restart counting channel numbers after this number (for testing)
-D,daemon run in the background as a daemon
-syslog send log messages to syslog (default if you use --daemon)
-pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
-server (internal use only)
-firewall (internal use only)
-hostwatch (internal use only)
-"""
-o = options.Options(optspec)
-(opt, flags, extra) = o.parse(sys.argv[2:])
-
-if opt.daemon:
- opt.syslog = 1
-if opt.wrap:
- import ssnet
- ssnet.MAX_CHANNEL = int(opt.wrap)
-helpers.verbose = opt.verbose
-
-try:
- if opt.server:
- if len(extra) != 0:
- o.fatal('no arguments expected')
- server.latency_control = opt.latency_control
- sys.exit(server.main())
- elif opt.firewall:
- if len(extra) != 2:
- o.fatal('exactly two arguments expected')
- sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
- elif opt.hostwatch:
- sys.exit(hostwatch.hw_main(extra))
- else:
- if len(extra) < 1 and not opt.auto_nets:
- o.fatal('at least one subnet (or -N) expected')
- includes = extra
- excludes = ['127.0.0.0/8']
- for k,v in flags:
- if k in ('-x','--exclude'):
- excludes.append(v)
- remotename = opt.remote
- if remotename == '' or remotename == '-':
- remotename = None
- if opt.seed_hosts and not opt.auto_hosts:
- o.fatal('--seed-hosts only works if you also use -H')
- if opt.seed_hosts:
- sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
- elif opt.auto_hosts:
- sh = []
- else:
- sh = None
- sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
- opt.ssh_cmd,
- remotename,
- opt.python,
- opt.latency_control,
- opt.dns,
- sh,
- opt.auto_nets,
- parse_subnets(includes),
- parse_subnets(excludes),
- opt.syslog, opt.daemon, opt.pidfile))
-except Fatal, e:
- log('fatal: %s\n' % e)
- sys.exit(99)
-except KeyboardInterrupt:
- log('\n')
- log('Keyboard interrupt: exiting.\n')
- sys.exit(1)
diff --git a/packaging/control b/packaging/control
new file mode 100644
index 0000000..ff162eb
--- /dev/null
+++ b/packaging/control
@@ -0,0 +1,26 @@
+Package: sshuttle
+Version: 0.2
+Architecture: i386
+Maintainer: Jim Wyllie
+Depends: autossh, upstart, python (>=2.6)
+Section: utils
+Priority: optional
+Homepage: http://github.com/jwyllie83/sshuttle.udp
+Description: "Full-featured" VPN over an SSH tunnel, allowing full remote
+ access somewhere where all you have is an SSH connection. It works well if
+ you generally find yourself in the following situation:
+ .
+ - Your client machine (or router) is Linux, FreeBSD, or MacOS.
+ - You have access to a remote network via ssh.
+ - You don't necessarily have admin access on the remote network.
+ - You do not wish to, or can't, use other VPN software
+ - You don't want to create an ssh port forward for every
+ single host/port on the remote network.
+ - You hate openssh's port forwarding because it's randomly
+ slow and/or stupid.
+ - You can't use openssh's PermitTunnel feature because
+ it's disabled by default on openssh servers; plus it does
+ TCP-over-TCP, which has suboptimal performance
+ .
+ It also has hooks for more complicated setups (VPN-in-a-SSH-VPN, etc) to allow
+ you to set it up as you like.
diff --git a/packaging/make_deb b/packaging/make_deb
new file mode 100755
index 0000000..96fa589
--- /dev/null
+++ b/packaging/make_deb
@@ -0,0 +1,41 @@
+#!/bin/bash
+#
+# This script puts together a .deb package suitable for installing on an Ubuntu
+# system
+
+B="/tmp/sshuttle/build"
+
+if [ ! -x /usr/bin/dpkg ]; then
+ echo 'Unable to build: dpkg not found on system'
+ exit 1
+fi
+
+# Create the new directory structure
+mkdir -p ${B}/etc/sshuttle/pre-start.d
+mkdir -p ${B}/etc/sshuttle/post-stop.d
+mkdir -p ${B}/usr/share/sshuttle
+mkdir -p ${B}/usr/bin
+mkdir -p ${B}/etc/init
+mkdir -p ${B}/DEBIAN
+
+# Copy over all of the files
+cp -r ../src/* ${B}/usr/share/sshuttle
+cp ../src/sshuttle ${B}/usr/bin
+cp -r sshuttle.conf ${B}/etc/init
+cp prefixes.conf ${B}/etc/sshuttle
+cp tunnel.conf ${B}/etc/sshuttle
+
+# Copy the control file over, as well
+cp control ${B}/DEBIAN
+
+# Create the md5sum manifest
+if [ -x /usr/bin/md5sum ]; then
+ cd ${B}
+ find . -type f | egrep -v DEBIAN | sed -re 's/^..//' | xargs md5sum > ${B}/DEBIAN/md5sums
+ cd ${OLDPWD}
+fi
+
+# Build the debian package
+VERSION=$(egrep -e '^Version' control | sed -re 's/^[^:]*: //')
+dpkg --build ${B} ./sshuttle-${VERSION}.deb
+rm -rf ${B}
diff --git a/packaging/prefixes.conf b/packaging/prefixes.conf
new file mode 100644
index 0000000..ca472b9
--- /dev/null
+++ b/packaging/prefixes.conf
@@ -0,0 +1,5 @@
+# Output prefixes here, one per line. Prefix is in:
+# prefix/netmask format
+# Like this:
+# 192.168.0.0/16
+# 192.0.43.10/32
diff --git a/packaging/sshuttle.conf b/packaging/sshuttle.conf
new file mode 100644
index 0000000..0adff3f
--- /dev/null
+++ b/packaging/sshuttle.conf
@@ -0,0 +1,90 @@
+description "Create a transparent proxy over SSH"
+author "Jim Wyllie "
+
+manual
+nice -5
+
+# Edit this file with network prefixes that should be loaded through the SSH
+# tunnel.
+env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf
+
+# Routing table; defaults to 100
+env ROUTE_TABLE=100
+
+# fwmark; defaults to 1
+env FWMARK=1
+
+# SSH tunnel configuration file
+env SSHUTTLE_TUNNEL_FILE=/etc/sshuttle/tunnel.conf
+
+# File containing the tunnel proxy name / host / whatever
+env TUNNEL_PROXY="/etc/sshuttle/tunnel.conf"
+
+# Any other commands needed to run before or after loading the SSH tunnel.
+# This is where you can put any of your hacks to set up tunnels-in-tunnels,
+# etc. Scripts in this directory are executed in order.
+env MISC_START_DIR=/etc/sshuttle/pre-start.d
+env MISC_STOP_DIR=/etc/sshuttle/post-stop.d
+
+start on (local-filesystems and net-device-up IFACE!=lo)
+stop on stopping network-services
+
+#respawn
+
+pre-start script
+ # Make sure we have created the routes
+ sudo ip rule add fwmark ${FWMARK} lookup ${ROUTE_TABLE}
+ logger "Starting sshuttle..."
+
+ if [ -f "${PREFIX_LOCATION}" ]; then
+ cat "${PREFIX_LOCATION}" | while read ROUTE; do
+
+ # Skip comments
+ if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
+ continue
+ fi
+
+ # Skip empty lines
+ if [ -z "${ROUTE}" ]; then
+ continue
+ fi
+
+ logger "Adding route: ${ROUTE}"
+ ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}
+ done
+ fi
+
+ for RUNFILE in ${MISC_START_DIR}/*; do
+ logger "Executing ${RUNFILE}"
+ /bin/sh -c "${RUNFILE}"
+ done
+end script
+
+post-stop script
+ if [ -f "${PREFIX_LOCATION}" ]; then
+ cat "${PREFIX_LOCATION}" | while read ROUTE; do
+
+ # Skip comments
+ if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
+ continue
+ fi
+
+ # Skip empty lines
+ if [ -z "${ROUTE}" ]; then
+ continue
+ fi
+
+ logger "Deleting route: ${ROUTE}"
+ ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}
+ done
+ fi
+
+ ip rule del fwmark ${FWMARK}
+
+ for RUNFILE in "${MISC_STOP_DIR}/*"; do
+ logger "Executing ${RUNFILE}"
+ /bin/sh -c "${RUNFILE}"
+ done
+end script
+
+exec /usr/bin/sshuttle --dns --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}"
diff --git a/packaging/tunnel.conf b/packaging/tunnel.conf
new file mode 100644
index 0000000..64fb54e
--- /dev/null
+++ b/packaging/tunnel.conf
@@ -0,0 +1,19 @@
+# Here is where you can specify any SSH tunnel options See ssh_config(5) for
+# details. You need to leave the Host line intact, but everything else can
+# specify whatever you want
+Host sshuttle_tunnel
+
+# REQUIRED: Set this to be the host to which you would like to connect your
+# tunnel
+#Hostname localhost
+
+# REQUIRED: Set this to be the target SSH user on the remote system
+#User foo
+
+# ---------------------------------------------------------------------------
+# The rest are all optional; see ssh_config(5) for the full list of what can
+# be specified. Some very commonly needed ones are below.
+# ---------------------------------------------------------------------------
+
+# SSH key used for connecting
+#IdentityFile /path/to/key
diff --git a/Makefile b/src/Makefile
similarity index 100%
rename from Makefile
rename to src/Makefile
diff --git a/all.do b/src/all.do
similarity index 100%
rename from all.do
rename to src/all.do
diff --git a/assembler.py b/src/assembler.py
similarity index 100%
rename from assembler.py
rename to src/assembler.py
diff --git a/clean.do b/src/clean.do
similarity index 100%
rename from clean.do
rename to src/clean.do
diff --git a/src/client.py b/src/client.py
new file mode 100644
index 0000000..cb034cd
--- /dev/null
+++ b/src/client.py
@@ -0,0 +1,718 @@
+import struct, select, errno, re, signal, time
+import compat.ssubprocess as ssubprocess
+import helpers, ssnet, ssh, ssyslog
+from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
+from helpers import *
+
+recvmsg = None
+try:
+ # try getting recvmsg from python
+ import socket as pythonsocket
+ getattr(pythonsocket.socket,"recvmsg")
+ socket = pythonsocket
+ recvmsg = "python"
+except AttributeError:
+ # try getting recvmsg from socket_ext library
+ try:
+ import socket_ext
+ getattr(socket_ext.socket,"recvmsg")
+ socket = socket_ext
+ recvmsg = "socket_ext"
+ except ImportError:
+ import socket
+
+_extra_fd = os.open('/dev/null', os.O_RDONLY)
+
+def got_signal(signum, frame):
+ log('exiting on signal %d\n' % signum)
+ sys.exit(1)
+
+
+_pidname = None
+IP_TRANSPARENT = 19
+IP_ORIGDSTADDR = 20
+IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
+SOL_IPV6 = 41
+IPV6_ORIGDSTADDR = 74
+IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
+
+
+if recvmsg == "python":
+ def recv_udp(listener, bufsize):
+ debug3('Accept UDP python using recvmsg.\n')
+ data, ancdata, msg_flags, srcip = listener.recvmsg(4096,socket.CMSG_SPACE(24))
+ dstip = None
+ family = None
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
+ family,port = struct.unpack('=HH', cmsg_data[0:4])
+ port = socket.htons(port)
+ if family == socket.AF_INET:
+ start = 4
+ length = 4
+ else:
+ raise Fatal("Unsupported socket type '%s'"%family)
+ ip = socket.inet_ntop(family, cmsg_data[start:start+length])
+ dstip = (ip, port)
+ break
+ elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
+ family,port = struct.unpack('=HH', cmsg_data[0:4])
+ port = socket.htons(port)
+ if family == socket.AF_INET6:
+ start = 8
+ length = 16
+ else:
+ raise Fatal("Unsupported socket type '%s'"%family)
+ ip = socket.inet_ntop(family, cmsg_data[start:start+length])
+ dstip = (ip, port)
+ break
+ return (srcip, dstip, data)
+elif recvmsg == "socket_ext":
+ def recv_udp(listener, bufsize):
+ debug3('Accept UDP using socket_ext recvmsg.\n')
+ srcip, data, adata, flags = listener.recvmsg((bufsize,),socket.CMSG_SPACE(24))
+ dstip = None
+ family = None
+ for a in adata:
+ if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
+ family,port = struct.unpack('=HH', a.cmsg_data[0:4])
+ port = socket.htons(port)
+ if family == socket.AF_INET:
+ start = 4
+ length = 4
+ else:
+ raise Fatal("Unsupported socket type '%s'"%family)
+ ip = socket.inet_ntop(family, a.cmsg_data[start:start+length])
+ dstip = (ip, port)
+ break
+ elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
+ family,port = struct.unpack('=HH', a.cmsg_data[0:4])
+ port = socket.htons(port)
+ if family == socket.AF_INET6:
+ start = 8
+ length = 16
+ else:
+ raise Fatal("Unsupported socket type '%s'"%family)
+ ip = socket.inet_ntop(family, a.cmsg_data[start:start+length])
+ dstip = (ip, port)
+ break
+ return (srcip, dstip, data[0])
+else:
+ def recv_udp(listener, bufsize):
+ debug3('Accept UDP using recvfrom.\n')
+ data, srcip = listener.recvfrom(bufsize)
+ return (srcip, None, data)
+
+
+def check_daemon(pidfile):
+ global _pidname
+ _pidname = os.path.abspath(pidfile)
+ try:
+ oldpid = open(_pidname).read(1024)
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ return # no pidfile, ok
+ else:
+ raise Fatal("can't read %s: %s" % (_pidname, e))
+ if not oldpid:
+ os.unlink(_pidname)
+ return # invalid pidfile, ok
+ oldpid = int(oldpid.strip() or 0)
+ if oldpid <= 0:
+ os.unlink(_pidname)
+ return # invalid pidfile, ok
+ try:
+ os.kill(oldpid, 0)
+ except OSError, e:
+ if e.errno == errno.ESRCH:
+ os.unlink(_pidname)
+ return # outdated pidfile, ok
+ elif e.errno == errno.EPERM:
+ pass
+ else:
+ raise
+ raise Fatal("%s: sshuttle is already running (pid=%d)"
+ % (_pidname, oldpid))
+
+
+def daemonize():
+ if os.fork():
+ os._exit(0)
+ os.setsid()
+ if os.fork():
+ os._exit(0)
+
+ outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666)
+ try:
+ os.write(outfd, '%d\n' % os.getpid())
+ finally:
+ os.close(outfd)
+ os.chdir("/")
+
+ # Normal exit when killed, or try/finally won't work and the pidfile won't
+ # be deleted.
+ signal.signal(signal.SIGTERM, got_signal)
+
+ si = open('/dev/null', 'r+')
+ os.dup2(si.fileno(), 0)
+ os.dup2(si.fileno(), 1)
+ si.close()
+
+ ssyslog.stderr_to_syslog()
+
+
+def daemon_cleanup():
+ try:
+ os.unlink(_pidname)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ pass
+ else:
+ raise
+
+
+def original_dst(sock):
+ try:
+ SO_ORIGINAL_DST = 80
+ SOCKADDR_MIN = 16
+ sockaddr_in = sock.getsockopt(socket.SOL_IP,
+ SO_ORIGINAL_DST, SOCKADDR_MIN)
+ (proto, port, a,b,c,d) = struct.unpack('=HHBBBB', sockaddr_in[:8])
+ port = socket.htons(port)
+ assert(proto == socket.AF_INET)
+ ip = '%d.%d.%d.%d' % (a,b,c,d)
+ return (ip,port)
+ except socket.error, e:
+ if e.args[0] == errno.ENOPROTOOPT:
+ return sock.getsockname()
+ raise
+
+
+class MultiListener:
+
+ def __init__(self, type=socket.SOCK_STREAM, proto=0):
+ self.v6 = socket.socket(socket.AF_INET6, type, proto)
+ self.v4 = socket.socket(socket.AF_INET, type, proto)
+
+ def setsockopt(self, level, optname, value):
+ if self.v6:
+ self.v6.setsockopt(level, optname, value)
+ if self.v4:
+ self.v4.setsockopt(level, optname, value)
+
+ def add_handler(self, handlers, callback, method, mux):
+ if self.v6:
+ handlers.append(Handler([self.v6], lambda: callback(self.v6, method, mux, handlers)))
+ if self.v4:
+ handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers)))
+
+ def listen(self, backlog):
+ if self.v6:
+ self.v6.listen(backlog)
+ if self.v4:
+ try:
+ self.v4.listen(backlog)
+ except socket.error, e:
+ # on some systems v4 bind will fail if the v6 suceeded,
+ # in this case the v6 socket will receive v4 too.
+ if e.errno == errno.EADDRINUSE and self.v6:
+ self.v4 = None
+ else:
+ raise e
+
+ def bind(self, address_v6, address_v4):
+ if address_v6 and self.v6:
+ self.v6.bind(address_v6)
+ else:
+ self.v6 = None
+ if address_v4 and self.v4:
+ self.v4.bind(address_v4)
+ else:
+ self.v4 = None
+
+ def print_listening(self, what):
+ if self.v6:
+ listenip = self.v6.getsockname()
+ debug1('%s listening on %r.\n' % (what, listenip))
+ if self.v4:
+ listenip = self.v4.getsockname()
+ debug1('%s listening on %r.\n' % (what, listenip))
+
+
+class FirewallClient:
+ def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp):
+ self.auto_nets = []
+ self.subnets_include = subnets_include
+ self.subnets_exclude = subnets_exclude
+ argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
+ ['-v'] * (helpers.verbose or 0) +
+ ['--firewall', str(port_v6), str(port_v4),
+ str(dnsport_v6), str(dnsport_v4),
+ method, str(int(udp))])
+ if ssyslog._p:
+ argvbase += ['--syslog']
+ argv_tries = [
+ ['sudo', '-p', '[local sudo] Password: '] + argvbase,
+ ['su', '-c', ' '.join(argvbase)],
+ argvbase
+ ]
+
+ # we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
+ # because stupid Linux 'su' requires that stdin be attached to a tty.
+ # Instead, attach a *bidirectional* socket to its stdout, and use
+ # that for talking in both directions.
+ (s1,s2) = socket.socketpair()
+ def setup():
+ # run in the child process
+ s2.close()
+ e = None
+ if os.getuid() == 0:
+ argv_tries = argv_tries[-1:] # last entry only
+ for argv in argv_tries:
+ try:
+ if argv[0] == 'su':
+ sys.stderr.write('[local su] ')
+ self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
+ e = None
+ break
+ except OSError, e:
+ pass
+ self.argv = argv
+ s1.close()
+ self.pfile = s2.makefile('wb+')
+ if e:
+ log('Spawning firewall manager: %r\n' % self.argv)
+ raise Fatal(e)
+ line = self.pfile.readline()
+ self.check()
+ if line[0:5] != 'READY':
+ raise Fatal('%r expected READY, got %r' % (self.argv, line))
+ self.method = line[6:-1]
+
+ def check(self):
+ rv = self.p.poll()
+ if rv:
+ raise Fatal('%r returned %d' % (self.argv, rv))
+
+ def start(self):
+ self.pfile.write('ROUTES\n')
+ for (family,ip,width) in self.subnets_include+self.auto_nets:
+ self.pfile.write('%d,%d,0,%s\n' % (family, width, ip))
+ for (family,ip,width) in self.subnets_exclude:
+ self.pfile.write('%d,%d,1,%s\n' % (family, width, ip))
+ self.pfile.write('GO\n')
+ self.pfile.flush()
+ line = self.pfile.readline()
+ self.check()
+ if line != 'STARTED\n':
+ raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
+
+ def sethostip(self, hostname, ip):
+ assert(not re.search(r'[^-\w]', hostname))
+ assert(not re.search(r'[^0-9.]', ip))
+ self.pfile.write('HOST %s,%s\n' % (hostname, ip))
+ self.pfile.flush()
+
+ def done(self):
+ self.pfile.close()
+ rv = self.p.wait()
+ if rv:
+ raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
+
+
+dnsreqs = {}
+udp_by_src = {}
+def expire_connections(now, mux):
+ for chan,timeout in dnsreqs.items():
+ if timeout < now:
+ debug3('expiring dnsreqs channel=%d\n' % chan)
+ del mux.channels[chan]
+ del dnsreqs[chan]
+ debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
+ for peer,(chan,timeout) in udp_by_src.items():
+ if timeout < now:
+ debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
+ mux.send(chan, ssnet.CMD_UDP_CLOSE, '')
+ del mux.channels[chan]
+ del udp_by_src[peer]
+ debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
+
+
+def onaccept_tcp(listener, method, mux, handlers):
+ global _extra_fd
+ try:
+ sock,srcip = listener.accept()
+ except socket.error, e:
+ if e.args[0] in [errno.EMFILE, errno.ENFILE]:
+ debug1('Rejected incoming connection: too many open files!\n')
+ # free up an fd so we can eat the connection
+ os.close(_extra_fd)
+ try:
+ sock,srcip = listener.accept()
+ sock.close()
+ finally:
+ _extra_fd = os.open('/dev/null', os.O_RDONLY)
+ return
+ else:
+ raise
+ if method == "tproxy":
+ dstip = sock.getsockname();
+ else:
+ dstip = original_dst(sock)
+ debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
+ dstip[0],dstip[1]))
+ if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
+ debug1("-- ignored: that's my address!\n")
+ sock.close()
+ return
+ chan = mux.next_channel()
+ if not chan:
+ log('warning: too many open channels. Discarded connection.\n')
+ sock.close()
+ return
+ mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%s' % (sock.family, dstip[0], dstip[1]))
+ outwrap = MuxWrapper(mux, chan)
+ handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
+ expire_connections(time.time(), mux)
+
+
+def udp_done(chan, data, method, family, dstip):
+ (src,srcport,data) = data.split(",",2)
+ srcip = (src,int(srcport))
+ debug3('doing send from %r to %r\n' % (srcip,dstip,))
+
+ try:
+ sender = socket.socket(family, socket.SOCK_DGRAM)
+ sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
+ sender.bind(srcip)
+ sender.sendto(data, dstip)
+ sender.close()
+ except socket.error, e:
+ debug1('-- ignored socket error sending UDP data: %r\n'%e)
+
+
+def onaccept_udp(listener, method, mux, handlers):
+ now = time.time()
+ srcip, dstip, data = recv_udp(listener, 4096)
+ if not dstip:
+ debug1("-- ignored UDP from %r: couldn't determine destination IP address\n" % (srcip,))
+ return
+ debug1('Accept UDP: %r -> %r.\n' % (srcip,dstip,))
+ if srcip in udp_by_src:
+ chan,timeout = udp_by_src[srcip]
+ else:
+ chan = mux.next_channel()
+ mux.channels[chan] = lambda cmd,data: udp_done(chan, data, method, listener.family, dstip=srcip)
+ mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
+ udp_by_src[srcip] = chan,now+30
+
+ hdr = "%s,%r,"%(dstip[0], dstip[1])
+ mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data)
+
+ expire_connections(now, mux)
+
+
+def dns_done(chan, data, method, sock, srcip, dstip, mux):
+ debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan,srcip,dstip))
+ del mux.channels[chan]
+ del dnsreqs[chan]
+ if method == "tproxy":
+ debug3('doing send from %r to %r\n' % (srcip,dstip,))
+ sender = socket.socket(sock.family, socket.SOCK_DGRAM)
+ sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
+ sender.bind(srcip)
+ sender.sendto(data, dstip)
+ sender.close()
+ else:
+ debug3('doing sendto %r\n' % (dstip,))
+ sock.sendto(data, dstip)
+
+
+def ondns(listener, method, mux, handlers):
+ now = time.time()
+ srcip, dstip, data = recv_udp(listener, 4096)
+ if method == "tproxy" and not dstip:
+ debug1("-- ignored UDP from %r: couldn't determine destination IP address\n" % (srcip,))
+ return
+ debug1('DNS request from %r to %r: %d bytes\n' % (srcip,dstip,len(data)))
+ chan = mux.next_channel()
+ dnsreqs[chan] = now+30
+ mux.send(chan, ssnet.CMD_DNS_REQ, data)
+ mux.channels[chan] = lambda cmd,data: dns_done(chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
+ expire_connections(now, mux)
+
+
+def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control,
+ dns_listener, method, seed_hosts, auto_nets,
+ syslog, daemon):
+ handlers = []
+ if helpers.verbose >= 1:
+ helpers.logprefix = 'c : '
+ else:
+ helpers.logprefix = 'client: '
+ debug1('connecting to server...\n')
+
+ try:
+ (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
+ stderr=ssyslog._p and ssyslog._p.stdin,
+ options=dict(latency_control=latency_control, method=method))
+ except socket.error, e:
+ if e.args[0] == errno.EPIPE:
+ raise Fatal("failed to establish ssh session (1)")
+ else:
+ raise
+ mux = Mux(serversock, serversock)
+ handlers.append(mux)
+
+ expected = 'SSHUTTLE0001'
+
+ try:
+ v = 'x'
+ while v and v != '\0':
+ v = serversock.recv(1)
+ v = 'x'
+ while v and v != '\0':
+ v = serversock.recv(1)
+ initstring = serversock.recv(len(expected))
+ except socket.error, e:
+ if e.args[0] == errno.ECONNRESET:
+ raise Fatal("failed to establish ssh session (2)")
+ else:
+ raise
+
+ rv = serverproc.poll()
+ if rv:
+ raise Fatal('server died with error code %d' % rv)
+
+ if initstring != expected:
+ raise Fatal('expected server init string %r; got %r'
+ % (expected, initstring))
+ debug1('connected.\n')
+ print 'Connected.'
+ sys.stdout.flush()
+ if daemon:
+ daemonize()
+ log('daemonizing (%s).\n' % _pidname)
+ elif syslog:
+ debug1('switching to syslog.\n')
+ ssyslog.stderr_to_syslog()
+
+ def onroutes(routestr):
+ if auto_nets:
+ for line in routestr.strip().split('\n'):
+ (family,ip,width) = line.split(',', 2)
+ fw.auto_nets.append((family,ip,int(width)))
+
+ # we definitely want to do this *after* starting ssh, or we might end
+ # up intercepting the ssh connection!
+ #
+ # Moreover, now that we have the --auto-nets option, we have to wait
+ # for the server to send us that message anyway. Even if we haven't
+ # set --auto-nets, we might as well wait for the message first, then
+ # ignore its contents.
+ mux.got_routes = None
+ fw.start()
+ mux.got_routes = onroutes
+
+ def onhostlist(hostlist):
+ debug2('got host list: %r\n' % hostlist)
+ for line in hostlist.strip().split():
+ if line:
+ name,ip = line.split(',', 1)
+ fw.sethostip(name, ip)
+ mux.got_host_list = onhostlist
+
+ tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
+
+ if udp_listener:
+ udp_listener.add_handler(handlers, onaccept_udp, method, mux)
+
+ if dns_listener:
+ dns_listener.add_handler(handlers, ondns, method, mux)
+
+ if seed_hosts != None:
+ debug1('seed_hosts: %r\n' % seed_hosts)
+ mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
+
+ while 1:
+ rv = serverproc.poll()
+ if rv:
+ raise Fatal('server died with error code %d' % rv)
+
+ ssnet.runonce(handlers, mux)
+ if latency_control:
+ mux.check_fullness()
+ mux.callback()
+
+
+def main(listenip_v6, listenip_v4,
+ ssh_cmd, remotename, python, latency_control, dns,
+ method, seed_hosts, auto_nets,
+ subnets_include, subnets_exclude, syslog, daemon, pidfile):
+
+ if syslog:
+ ssyslog.start_syslog()
+ if daemon:
+ try:
+ check_daemon(pidfile)
+ except Fatal, e:
+ log("%s\n" % e)
+ return 5
+ debug1('Starting sshuttle proxy.\n')
+
+ if recvmsg is not None:
+ debug1("recvmsg %s support enabled.\n"%recvmsg)
+
+ if method == "tproxy":
+ if recvmsg is not None:
+ debug1("tproxy UDP support enabled.\n")
+ udp = True
+ else:
+ debug1("tproxy UDP support requires recvmsg function.\n")
+ udp = False
+ if dns and recvmsg is None:
+ debug1("tproxy DNS support requires recvmsg function.\n")
+ dns = False
+ else:
+ debug1("UDP support requires tproxy; disabling UDP.\n")
+ udp = False
+
+ if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
+ # if both ports given, no need to search for a spare port
+ ports = [ 0, ]
+ else:
+ # if at least one port missing, we have to search
+ ports = xrange(12300,9000,-1)
+
+ # search for free ports and try to bind
+ last_e = None
+ redirectport_v6 = 0
+ redirectport_v4 = 0
+ bound = False
+ debug2('Binding redirector:')
+ for port in ports:
+ debug2(' %d' % port)
+ tcp_listener = MultiListener()
+ tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ if udp:
+ udp_listener = MultiListener(socket.SOCK_DGRAM)
+ udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ else:
+ udp_listener = None
+
+ if listenip_v6 and listenip_v6[1]:
+ lv6 = listenip_v6
+ redirectport_v6 = lv6[1]
+ elif listenip_v6:
+ lv6 = (listenip_v6[0],port)
+ redirectport_v6 = port
+ else:
+ lv6 = None
+ redirectport_v6 = 0
+
+ if listenip_v4 and listenip_v4[1]:
+ lv4 = listenip_v4
+ redirectport_v4 = lv4[1]
+ elif listenip_v4:
+ lv4 = (listenip_v4[0],port)
+ redirectport_v4 = port
+ else:
+ lv4 = None
+ redirectport_v4 = 0
+
+ try:
+ tcp_listener.bind(lv6, lv4)
+ if udp_listener:
+ udp_listener.bind(lv6, lv4)
+ bound = True
+ break
+ except socket.error, e:
+ if e.errno == errno.EADDRINUSE:
+ last_e = e
+ else:
+ raise e
+ debug2('\n')
+ if not bound:
+ assert(last_e)
+ raise last_e
+ tcp_listener.listen(10)
+ tcp_listener.print_listening("TCP redirector")
+ if udp_listener:
+ udp_listener.print_listening("UDP redirector")
+
+ bound = False
+ if dns:
+ # search for spare port for DNS
+ debug2('Binding DNS:')
+ ports = xrange(12300,9000,-1)
+ for port in ports:
+ debug2(' %d' % port)
+ dns_listener = MultiListener(socket.SOCK_DGRAM)
+
+ if listenip_v6:
+ lv6 = (listenip_v6[0],port)
+ dnsport_v6 = port
+ else:
+ lv6 = None
+ dnsport_v6 = 0
+
+ if listenip_v4:
+ lv4 = (listenip_v4[0],port)
+ dnsport_v4 = port
+ else:
+ lv4 = None
+ dnsport_v4 = 0
+
+ try:
+ dns_listener.bind(lv6, lv4)
+ bound = True
+ break
+ except socket.error, e:
+ if e.errno == errno.EADDRINUSE:
+ last_e = e
+ else:
+ raise e
+ debug2('\n')
+ dns_listener.print_listening("DNS")
+ if not bound:
+ assert(last_e)
+ raise last_e
+ else:
+ dnsport_v6 = 0
+ dnsport_v4 = 0
+ dns_listener = None
+
+ fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp)
+
+ if fw.method == "tproxy":
+ tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
+ if udp_listener:
+ udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
+ if udp_listener.v4 is not None:
+ udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
+ if udp_listener.v6 is not None:
+ udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
+ if dns_listener:
+ dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
+ if dns_listener.v4 is not None:
+ dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
+ if dns_listener.v6 is not None:
+ dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
+
+ try:
+ return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
+ python, latency_control, dns_listener,
+ fw.method, seed_hosts, auto_nets, syslog,
+ daemon)
+ finally:
+ try:
+ if daemon:
+ # it's not our child anymore; can't waitpid
+ fw.p.returncode = 0
+ fw.done()
+ finally:
+ if daemon:
+ daemon_cleanup()
diff --git a/compat/__init__.py b/src/compat/__init__.py
similarity index 100%
rename from compat/__init__.py
rename to src/compat/__init__.py
diff --git a/compat/ssubprocess.py b/src/compat/ssubprocess.py
similarity index 100%
rename from compat/ssubprocess.py
rename to src/compat/ssubprocess.py
diff --git a/default.8.do b/src/default.8.do
similarity index 100%
rename from default.8.do
rename to src/default.8.do
diff --git a/do b/src/do
similarity index 100%
rename from do
rename to src/do
diff --git a/firewall.py b/src/firewall.py
similarity index 66%
rename from firewall.py
rename to src/firewall.py
index 4fd8c79..1e233c3 100644
--- a/firewall.py
+++ b/src/firewall.py
@@ -14,8 +14,14 @@ def nonfatal(func, *args):
log('error: %s\n' % e)
-def ipt_chain_exists(name):
- argv = ['iptables', '-t', 'nat', '-nL']
+def ipt_chain_exists(family, table, name):
+ if family == socket.AF_INET6:
+ cmd = 'ip6tables'
+ elif family == socket.AF_INET:
+ cmd = 'iptables'
+ else:
+ raise Exception('Unsupported family "%s"'%family_to_string(family))
+ argv = [cmd, '-t', table, '-nL']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
for line in p.stdout:
if line.startswith('Chain %s ' % name):
@@ -25,8 +31,13 @@ def ipt_chain_exists(name):
raise Fatal('%r returned %d' % (argv, rv))
-def ipt(*args):
- argv = ['iptables', '-t', 'nat'] + list(args)
+def _ipt(family, table, *args):
+ if family == socket.AF_INET6:
+ argv = ['ip6tables', '-t', table] + list(args)
+ elif family == socket.AF_INET:
+ argv = ['iptables', '-t', table] + list(args)
+ else:
+ raise Exception('Unsupported family "%s"'%family_to_string(family))
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
@@ -34,7 +45,7 @@ def ipt(*args):
_no_ttl_module = False
-def ipt_ttl(*args):
+def _ipt_ttl(family, *args):
global _no_ttl_module
if not _no_ttl_module:
# we avoid infinite loops by generating server-side connections
@@ -42,16 +53,15 @@ def ipt_ttl(*args):
# connections, in case client == server.
try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
- ipt(*argsplus)
+ _ipt(family, *argsplus)
except Fatal:
- ipt(*args)
+ _ipt(family, *args)
# we only get here if the non-ttl attempt succeeds
log('sshuttle: warning: your iptables is missing '
'the ttl module.\n')
_no_ttl_module = True
else:
- ipt(*args)
-
+ _ipt(family, *args)
# We name the chain based on the transproxy port number so that it's possible
@@ -59,11 +69,23 @@ def ipt_ttl(*args):
# multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
-def do_iptables(port, dnsport, subnets):
+def do_iptables_nat(port, dnsport, family, subnets, udp):
+ # only ipv4 supported with NAT
+ if family != socket.AF_INET:
+ raise Exception('Address family "%s" unsupported by nat method'%family_to_string(family))
+ if udp:
+ raise Exception("UDP not supported by nat method")
+
+ table = "nat"
+ def ipt(*args):
+ return _ipt(family, table, *args)
+ def ipt_ttl(*args):
+ return _ipt_ttl(family, table, *args)
+
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
- if ipt_chain_exists(chain):
+ if ipt_chain_exists(family, table, chain):
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
nonfatal(ipt, '-F', chain)
@@ -81,7 +103,7 @@ def do_iptables(port, dnsport, subnets):
# to least-specific, and at any given level of specificity, we want
# excludes to come first. That's why the columns are in such a non-
# intuitive order.
- for swidth,sexclude,snet in sorted(subnets, reverse=True):
+ for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet,swidth),
@@ -91,10 +113,10 @@ def do_iptables(port, dnsport, subnets):
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp',
'--to-ports', str(port))
-
+
if dnsport:
nslist = resolvconf_nameservers()
- for ip in nslist:
+ for f,ip in filter(lambda i: i[0]==family, nslist):
ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
@@ -102,6 +124,98 @@ def do_iptables(port, dnsport, subnets):
'--to-ports', str(dnsport))
+def do_iptables_tproxy(port, dnsport, family, subnets, udp):
+ if family not in [socket.AF_INET, socket.AF_INET6]:
+ raise Exception('Address family "%s" unsupported by tproxy method'%family_to_string(family))
+
+ table = "mangle"
+ def ipt(*args):
+ return _ipt(family, table, *args)
+ def ipt_ttl(*args):
+ return _ipt_ttl(family, table, *args)
+
+ mark_chain = 'sshuttle-m-%s' % port
+ tproxy_chain = 'sshuttle-t-%s' % port
+ divert_chain = 'sshuttle-d-%s' % port
+
+ # basic cleanup/setup of chains
+ if ipt_chain_exists(family, table, mark_chain):
+ ipt('-D', 'OUTPUT', '-j', mark_chain)
+ ipt('-F', mark_chain)
+ ipt('-X', mark_chain)
+
+ if ipt_chain_exists(family, table, tproxy_chain):
+ ipt('-D', 'PREROUTING', '-j', tproxy_chain)
+ ipt('-F', tproxy_chain)
+ ipt('-X', tproxy_chain)
+
+ if ipt_chain_exists(family, table, divert_chain):
+ ipt('-F', divert_chain)
+ ipt('-X', divert_chain)
+
+ if subnets or dnsport:
+ ipt('-N', mark_chain)
+ ipt('-F', mark_chain)
+ ipt('-N', divert_chain)
+ ipt('-F', divert_chain)
+ ipt('-N', tproxy_chain)
+ ipt('-F', tproxy_chain)
+ ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
+ ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
+ ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
+ ipt('-A', divert_chain, '-j', 'ACCEPT')
+ ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
+ '-m', 'tcp', '-p', 'tcp')
+ if subnets and udp:
+ ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
+ '-m', 'udp', '-p', 'udp')
+
+ if dnsport:
+ nslist = resolvconf_nameservers()
+ for f,ip in filter(lambda i: i[0]==family, nslist):
+ ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
+ '--dest', '%s/32' % ip,
+ '-m', 'udp', '-p', 'udp', '--dport', '53')
+ ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
+ '--dest', '%s/32' % ip,
+ '-m', 'udp', '-p', 'udp', '--dport', '53',
+ '--on-port', str(dnsport))
+
+ if subnets:
+ for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True):
+ if sexclude:
+ ipt('-A', mark_chain, '-j', 'RETURN',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'tcp', '-p', 'tcp')
+ ipt('-A', tproxy_chain, '-j', 'RETURN',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'tcp', '-p', 'tcp')
+ else:
+ ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'tcp', '-p', 'tcp')
+ ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'tcp', '-p', 'tcp',
+ '--on-port', str(port))
+
+ if sexclude and udp:
+ ipt('-A', mark_chain, '-j', 'RETURN',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'udp', '-p', 'udp')
+ ipt('-A', tproxy_chain, '-j', 'RETURN',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'udp', '-p', 'udp')
+ elif udp:
+ ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'udp', '-p', 'udp')
+ ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-m', 'udp', '-p', 'udp',
+ '--on-port', str(port))
+
+
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
@@ -207,7 +321,13 @@ def ipfw(*args):
raise Fatal('%r returned %d' % (argv, rv))
-def do_ipfw(port, dnsport, subnets):
+def do_ipfw(port, dnsport, family, subnets, udp):
+ # IPv6 not supported
+ if family not in [socket.AF_INET, ]:
+ raise Exception('Address family "%s" unsupported by ipfw method'%family_to_string(family))
+ if udp:
+ raise Exception("UDP not supported by ipfw method")
+
sport = str(port)
xsport = str(port+1)
@@ -240,7 +360,7 @@ def do_ipfw(port, dnsport, subnets):
if subnets:
# create new subnet entries
- for swidth,sexclude,snet in sorted(subnets, reverse=True):
+ for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipfw('add', sport, 'skipto', xsport,
'log', 'tcp',
@@ -286,7 +406,7 @@ def do_ipfw(port, dnsport, subnets):
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
nslist = resolvconf_nameservers()
- for ip in nslist:
+ for f,ip in filter(lambda i: i[0]==family, nslist):
# relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport,
'log', 'udp',
@@ -369,21 +489,35 @@ def restore_etc_hosts(port):
# exit. In case that fails, it's not the end of the world; future runs will
# supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
-def main(port, dnsport, syslog):
- assert(port > 0)
- assert(port <= 65535)
- assert(dnsport >= 0)
- assert(dnsport <= 65535)
+def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
+ assert(port_v6 >= 0)
+ assert(port_v6 <= 65535)
+ assert(port_v4 >= 0)
+ assert(port_v4 <= 65535)
+ assert(dnsport_v6 >= 0)
+ assert(dnsport_v6 <= 65535)
+ assert(dnsport_v4 >= 0)
+ assert(dnsport_v4 <= 65535)
if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
- if program_exists('ipfw'):
+ if method == "auto":
+ if program_exists('ipfw'):
+ method = "ipfw"
+ elif program_exists('iptables'):
+ method = "nat"
+ else:
+ raise Fatal("can't find either ipfw or iptables; check your PATH")
+
+ if method == "nat":
+ do_it = do_iptables_nat
+ elif method == "tproxy":
+ do_it = do_iptables_tproxy
+ elif method == "ipfw":
do_it = do_ipfw
- elif program_exists('iptables'):
- do_it = do_iptables
else:
- raise Fatal("can't find either ipfw or iptables; check your PATH")
+ raise Exception('Unknown method "%s"'%method)
# because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we
@@ -394,8 +528,8 @@ def main(port, dnsport, syslog):
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
- debug1('firewall manager ready.\n')
- sys.stdout.write('READY\n')
+ debug1('firewall manager ready method %s.\n'%method)
+ sys.stdout.write('READY %s\n'%method)
sys.stdout.flush()
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
@@ -419,15 +553,27 @@ def main(port, dnsport, syslog):
elif line == 'GO\n':
break
try:
- (width,exclude,ip) = line.strip().split(',', 2)
+ (family,width,exclude,ip) = line.strip().split(',', 3)
except:
raise Fatal('firewall: expected route or GO but got %r' % line)
- subnets.append((int(width), bool(int(exclude)), ip))
+ subnets.append((int(family), int(width), bool(int(exclude)), ip))
try:
if line:
debug1('firewall manager: starting transproxy.\n')
- do_wait = do_it(port, dnsport, subnets)
+
+ subnets_v6 = filter(lambda i: i[0]==socket.AF_INET6, subnets)
+ if port_v6:
+ do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp)
+ elif len(subnets_v6) > 0:
+ debug1("IPv6 subnets defined but IPv6 disabled\n")
+
+ subnets_v4 = filter(lambda i: i[0]==socket.AF_INET, subnets)
+ if port_v4:
+ do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp)
+ elif len(subnets_v4) > 0:
+ debug1('IPv4 subnets defined but IPv4 disabled\n')
+
sys.stdout.write('STARTED\n')
try:
@@ -446,7 +592,7 @@ def main(port, dnsport, syslog):
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
- rewrite_etc_hosts(port)
+ rewrite_etc_hosts(port_v6 or port_v4)
elif line:
raise Fatal('expected EOF, got %r' % line)
else:
@@ -456,5 +602,8 @@ def main(port, dnsport, syslog):
debug1('firewall manager: undoing changes.\n')
except:
pass
- do_it(port, 0, [])
- restore_etc_hosts(port)
+ if port_v6:
+ do_it(port_v6, 0, socket.AF_INET6, [], udp)
+ if port_v4:
+ do_it(port_v4, 0, socket.AF_INET, [], udp)
+ restore_etc_hosts(port_v6 or port_v4)
diff --git a/helpers.py b/src/helpers.py
similarity index 70%
rename from helpers.py
rename to src/helpers.py
index c169d0c..f08b731 100644
--- a/helpers.py
+++ b/src/helpers.py
@@ -1,4 +1,4 @@
-import sys, os, socket
+import sys, os, socket, errno
logprefix = ''
verbose = 0
@@ -42,7 +42,10 @@ def resolvconf_nameservers():
for line in open('/etc/resolv.conf'):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
- l.append(words[1])
+ if ':' in words[1]:
+ l.append((socket.AF_INET6,words[1]))
+ else:
+ l.append((socket.AF_INET,words[1]))
return l
@@ -55,11 +58,11 @@ def resolvconf_random_nameserver():
random.shuffle(l)
return l[0]
else:
- return '127.0.0.1'
+ return (socket.AF_INET,'127.0.0.1')
-def islocal(ip):
- sock = socket.socket()
+def islocal(ip,family):
+ sock = socket.socket(family)
try:
try:
sock.bind((ip, 0))
@@ -73,3 +76,18 @@ def islocal(ip):
return True # it's a local IP, or there would have been an error
+def guess_address_family(ip):
+ if ':' in ip:
+ return socket.AF_INET6
+ else:
+ return socket.AF_INET
+
+
+def family_to_string(family):
+ if family == socket.AF_INET6:
+ return "AF_INET6"
+ elif family == socket.AF_INET:
+ return "AF_INET"
+ else:
+ return str(family)
+
diff --git a/hostwatch.py b/src/hostwatch.py
similarity index 100%
rename from hostwatch.py
rename to src/hostwatch.py
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..8d277c9
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,221 @@
+import sys, os, re, socket
+import helpers, options, client, server, firewall, hostwatch
+import compat.ssubprocess as ssubprocess
+from helpers import *
+
+
+# 1.2.3.4/5 or just 1.2.3.4
+def parse_subnet4(s):
+ m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
+ if not m:
+ raise Fatal('%r is not a valid IP subnet format' % s)
+ (a,b,c,d,width) = m.groups()
+ (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
+ if width == None:
+ width = 32
+ else:
+ width = int(width)
+ if a > 255 or b > 255 or c > 255 or d > 255:
+ raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
+ if width > 32:
+ raise Fatal('*/%d is greater than the maximum of 32' % width)
+ return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width)
+
+
+# 1:2::3/64 or just 1:2::3
+def parse_subnet6(s):
+ m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
+ if not m:
+ raise Fatal('%r is not a valid IP subnet format' % s)
+ (net,width) = m.groups()
+ if width == None:
+ width = 128
+ else:
+ width = int(width)
+ if width > 128:
+ raise Fatal('*/%d is greater than the maximum of 128' % width)
+ return(socket.AF_INET6, net, width)
+
+
+# Subnet file, supporting empty lines and hash-started comment lines
+def parse_subnet_file(s):
+ try:
+ handle = open(s, 'r')
+ except OSError, e:
+ raise Fatal('Unable to open subnet file: %s' % s)
+
+ raw_config_lines = handle.readlines()
+ config_lines = []
+ for line_no, line in enumerate(raw_config_lines):
+ line = line.strip()
+ if len(line) == 0:
+ continue
+ if line[0] == '#':
+ continue
+ config_lines.append(line)
+
+ return config_lines
+
+
+# list of:
+# 1.2.3.4/5 or just 1.2.3.4
+# 1:2::3/64 or just 1:2::3
+def parse_subnets(subnets_str):
+ subnets = []
+ for s in subnets_str:
+ if ':' in s:
+ subnet = parse_subnet6(s)
+ else:
+ subnet = parse_subnet4(s)
+ subnets.append(subnet)
+ return subnets
+
+
+# 1.2.3.4:567 or just 1.2.3.4 or just 567
+def parse_ipport4(s):
+ s = str(s)
+ m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
+ if not m:
+ raise Fatal('%r is not a valid IP:port format' % s)
+ (a,b,c,d,port) = m.groups()
+ (a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
+ int(port or 0))
+ if a > 255 or b > 255 or c > 255 or d > 255:
+ raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
+ if port > 65535:
+ raise Fatal('*:%d is greater than the maximum of 65535' % port)
+ if a == None:
+ a = b = c = d = 0
+ return ('%d.%d.%d.%d' % (a,b,c,d), port)
+
+
+# [1:2::3]:456 or [1:2::3] or 456
+def parse_ipport6(s):
+ s = str(s)
+ m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
+ if not m:
+ raise Fatal('%s is not a valid IP:port format' % s)
+ (ip,port) = m.groups()
+ (ip,port) = (ip or '::', int(port or 0))
+ return (ip, port)
+
+
+optspec = """
+sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]]
+sshuttle --server
+sshuttle --firewall
+sshuttle --hostwatch
+--
+l,listen= transproxy to this ip address and port number
+H,auto-hosts scan for remote hostnames and update local /etc/hosts
+N,auto-nets automatically determine subnets to route
+dns capture local DNS requests and forward to the remote DNS server
+method= auto, nat, tproxy, or ipfw
+python= path to python interpreter on the remote server
+r,remote= ssh hostname (and optional username) of remote sshuttle server
+x,exclude= exclude this subnet (can be used more than once)
+v,verbose increase debug message verbosity
+e,ssh-cmd= the command to use to connect to the remote [ssh]
+seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
+no-latency-control sacrifice latency to improve bandwidth benchmarks
+wrap= restart counting channel numbers after this number (for testing)
+D,daemon run in the background as a daemon
+s,subnets= file where the subnets are stored, instead of on the command line
+syslog send log messages to syslog (default if you use --daemon)
+pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
+server (internal use only)
+firewall (internal use only)
+hostwatch (internal use only)
+"""
+o = options.Options(optspec)
+(opt, flags, extra) = o.parse(sys.argv[2:])
+
+if opt.daemon:
+ opt.syslog = 1
+if opt.wrap:
+ import ssnet
+ ssnet.MAX_CHANNEL = int(opt.wrap)
+helpers.verbose = opt.verbose
+
+try:
+ if opt.server:
+ if len(extra) != 0:
+ o.fatal('no arguments expected')
+ server.latency_control = opt.latency_control
+ sys.exit(server.main())
+ elif opt.firewall:
+ if len(extra) != 6:
+ o.fatal('exactly six arguments expected')
+ sys.exit(firewall.main(int(extra[0]), int(extra[1]),
+ int(extra[2]), int(extra[3]),
+ extra[4], int(extra[5]), opt.syslog))
+ elif opt.hostwatch:
+ sys.exit(hostwatch.hw_main(extra))
+ else:
+ if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
+ o.fatal('at least one subnet, subnet file, or -N expected')
+ includes = extra
+ excludes = ['127.0.0.0/8']
+ for k,v in flags:
+ if k in ('-x','--exclude'):
+ excludes.append(v)
+ remotename = opt.remote
+ if remotename == '' or remotename == '-':
+ remotename = None
+ if opt.seed_hosts and not opt.auto_hosts:
+ o.fatal('--seed-hosts only works if you also use -H')
+ if opt.seed_hosts:
+ sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
+ elif opt.auto_hosts:
+ sh = []
+ else:
+ sh = None
+ if opt.subnets:
+ includes = parse_subnet_file(opt.subnets)
+ if not opt.method:
+ method = "auto"
+ elif opt.method in [ "auto", "nat", "tproxy", "ipfw" ]:
+ method = opt.method
+ else:
+ o.fatal("method %s not supported"%opt.method)
+ if not opt.listen:
+ if opt.method == "tproxy":
+ ipport_v6 = parse_ipport6('[::1]:0')
+ else:
+ ipport_v6 = None
+ ipport_v4 = parse_ipport4('127.0.0.1:0')
+ else:
+ ipport_v6 = None
+ ipport_v4 = None
+ list = opt.listen.split(",")
+ for ip in list:
+ if '[' in ip and ']' in ip and opt.method == "tproxy":
+ ipport_v6 = parse_ipport6(ip)
+ else:
+ ipport_v4 = parse_ipport4(ip)
+ return_code = client.main(ipport_v6, ipport_v4,
+ opt.ssh_cmd,
+ remotename,
+ opt.python,
+ opt.latency_control,
+ opt.dns,
+ method,
+ sh,
+ opt.auto_nets,
+ parse_subnets(includes),
+ parse_subnets(excludes),
+ opt.syslog, opt.daemon, opt.pidfile)
+
+ if return_code == 0:
+ log('Normal exit code, exiting...')
+ else:
+ log('Abnormal exit code detected, failing...' % return_code)
+ sys.exit(return_code)
+
+except Fatal, e:
+ log('fatal: %s\n' % e)
+ sys.exit(99)
+except KeyboardInterrupt:
+ log('\n')
+ log('Keyboard interrupt: exiting.\n')
+ sys.exit(1)
diff --git a/options.py b/src/options.py
similarity index 100%
rename from options.py
rename to src/options.py
diff --git a/server.py b/src/server.py
similarity index 69%
rename from server.py
rename to src/server.py
index e1b327d..bc0bea8 100644
--- a/server.py
+++ b/src/server.py
@@ -59,7 +59,7 @@ def _list_routes():
mask = _maskbits(maskw) # returns 32 if maskw is null
width = min(ipw[1], mask)
ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width)
- routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
+ routes.append((socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv))
@@ -68,9 +68,9 @@ def _list_routes():
def list_routes():
- for (ip,width) in _list_routes():
+ for (family, ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
- yield (ip,width)
+ yield (family, ip,width)
def _exc_dump():
@@ -108,6 +108,7 @@ def __init__(self):
class DnsProxy(Handler):
def __init__(self, mux, chan, request):
+ # FIXME! IPv4 specific
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Handler.__init__(self, [sock])
self.timeout = time.time()+30
@@ -117,6 +118,7 @@ def __init__(self, mux, chan, request):
self.peer = None
self.request = request
self.sock = sock
+ # FIXME! IPv4 specific
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.try_send()
@@ -124,7 +126,8 @@ def try_send(self):
if self.tries >= 3:
return
self.tries += 1
- self.peer = resolvconf_random_nameserver()
+ # FIXME! Support IPv6 nameservers
+ self.peer = resolvconf_random_nameserver()[1]
self.sock.connect((self.peer, 53))
debug2('DNS: sending to %r\n' % self.peer)
try:
@@ -160,6 +163,35 @@ def callback(self):
self.ok = False
+class UdpProxy(Handler):
+ def __init__(self, mux, chan, family):
+ sock = socket.socket(family, socket.SOCK_DGRAM)
+ Handler.__init__(self, [sock])
+ self.timeout = time.time()+30
+ self.mux = mux
+ self.chan = chan
+ self.sock = sock
+ if family == socket.AF_INET:
+ self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
+
+ def send(self, dstip, data):
+ debug2('UDP: sending to %r port %d\n' % dstip)
+ try:
+ self.sock.sendto(data,dstip)
+ except socket.error, e:
+ log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
+ return
+
+ def callback(self):
+ try:
+ data,peer = self.sock.recvfrom(4096)
+ except socket.error, e:
+ log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
+ return
+ debug2('UDP response: %d bytes\n' % len(data))
+ hdr = "%s,%r,"%(peer[0], peer[1])
+ self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr+data)
+
def main():
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
@@ -170,7 +202,7 @@ def main():
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
- debug1(' %s/%d\n' % r)
+ debug1(' %d/%s/%d\n' % r)
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
@@ -184,7 +216,7 @@ def main():
handlers.append(mux)
routepkt = ''
for r in routes:
- routepkt += '%s,%d\n' % r
+ routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch()
@@ -213,20 +245,49 @@ def got_host_req(data):
mux.got_host_req = got_host_req
def new_channel(channel, data):
- (dstip,dstport) = data.split(',', 1)
+ (family,dstip,dstport) = data.split(',', 2)
+ family = int(family)
dstport = int(dstport)
- outwrap = ssnet.connect_dst(dstip,dstport)
+ outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
- debug2('Incoming DNS request.\n')
+ debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
+ udphandlers = {}
+ def udp_req(channel, cmd, data):
+ debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel,cmd))
+ if cmd == ssnet.CMD_UDP_DATA:
+ (dstip,dstport,data) = data.split(",",2)
+ dstport = int(dstport)
+ debug2('is incoming UDP data. %r %d.\n' % (dstip,dstport))
+ h = udphandlers[channel]
+ h.send((dstip,dstport),data)
+ elif cmd == ssnet.CMD_UDP_CLOSE:
+ debug2('is incoming UDP close\n')
+ h = udphandlers[channel]
+ h.ok = False
+ del mux.channels[channel]
+
+ def udp_open(channel, data):
+ debug2('Incoming UDP open.\n')
+ family = int(data)
+ mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
+ if channel in udphandlers:
+ raise Fatal('UDP connection channel %d already open'%channel)
+ else:
+ h = UdpProxy(mux, channel, family)
+ handlers.append(h)
+ udphandlers[channel] = h
+ mux.got_udp_open = udp_open
+
+
while mux.ok:
if hw.pid:
assert(hw.pid > 0)
@@ -243,5 +304,13 @@ def dns_req(channel, data):
now = time.time()
for channel,h in dnshandlers.items():
if h.timeout < now or not h.ok:
+ debug3('expiring dnsreqs channel=%d\n' % channel)
del dnshandlers[channel]
+ h.sock.close()
+ h.ok = False
+ for channel,h in udphandlers.items():
+ if not h.ok:
+ debug3('expiring UDP channel=%d\n' % channel)
+ del udphandlers[channel]
+ h.sock.close()
h.ok = False
diff --git a/ssh.py b/src/ssh.py
similarity index 95%
rename from ssh.py
rename to src/ssh.py
index c4bf06e..85d96b4 100644
--- a/ssh.py
+++ b/src/ssh.py
@@ -27,9 +27,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
main_exe = sys.argv[0]
portl = []
- rhostIsIPv6 = False
if (rhostport or '').count(':') > 1:
- rhostIsIPv6 = True
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
@@ -48,10 +46,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
if rhost == '-':
rhost = None
- ipv6flag = []
- if rhostIsIPv6:
- ipv6flag = ['-6']
-
z = zlib.compressobj(1)
content = readfile('assembler.py')
optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items())
@@ -88,7 +82,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
"exec \"$P\" -c '%s'") % pyscript
argv = (sshl +
portl +
- ipv6flag +
[rhost, '--', pycmd])
(s1,s2) = socket.socketpair()
def setup():
diff --git a/sshuttle b/src/sshuttle
similarity index 82%
rename from sshuttle
rename to src/sshuttle
index 2d234d5..6ee47f1 100755
--- a/sshuttle
+++ b/src/sshuttle
@@ -4,7 +4,8 @@ for i in 1 2 3 4 5 6 7 8 9 10; do
[ -L "$EXE" ] || break
EXE=$(readlink "$EXE")
done
-DIR=$(dirname "$EXE")
+#DIR=$(dirname "$EXE")
+DIR=/usr/share/sshuttle
if python2 -V 2>/dev/null; then
exec python2 "$DIR/main.py" python2 "$@"
else
diff --git a/sshuttle.md b/src/sshuttle.md
similarity index 100%
rename from sshuttle.md
rename to src/sshuttle.md
diff --git a/ssnet.py b/src/ssnet.py
similarity index 90%
rename from ssnet.py
rename to src/ssnet.py
index b6d73c2..67747a4 100644
--- a/ssnet.py
+++ b/src/ssnet.py
@@ -16,29 +16,35 @@
CMD_EXIT = 0x4200
CMD_PING = 0x4201
CMD_PONG = 0x4202
-CMD_CONNECT = 0x4203
-CMD_STOP_SENDING = 0x4204
-CMD_EOF = 0x4205
-CMD_DATA = 0x4206
+CMD_TCP_CONNECT = 0x4203
+CMD_TCP_STOP_SENDING = 0x4204
+CMD_TCP_EOF = 0x4205
+CMD_TCP_DATA = 0x4206
CMD_ROUTES = 0x4207
CMD_HOST_REQ = 0x4208
CMD_HOST_LIST = 0x4209
CMD_DNS_REQ = 0x420a
CMD_DNS_RESPONSE = 0x420b
+CMD_UDP_OPEN = 0x420c
+CMD_UDP_DATA = 0x420d
+CMD_UDP_CLOSE = 0x420e
cmd_to_name = {
CMD_EXIT: 'EXIT',
CMD_PING: 'PING',
CMD_PONG: 'PONG',
- CMD_CONNECT: 'CONNECT',
- CMD_STOP_SENDING: 'STOP_SENDING',
- CMD_EOF: 'EOF',
- CMD_DATA: 'DATA',
+ CMD_TCP_CONNECT: 'TCP_CONNECT',
+ CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING',
+ CMD_TCP_EOF: 'TCP_EOF',
+ CMD_TCP_DATA: 'TCP_DATA',
CMD_ROUTES: 'ROUTES',
CMD_HOST_REQ: 'HOST_REQ',
CMD_HOST_LIST: 'HOST_LIST',
CMD_DNS_REQ: 'DNS_REQ',
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
+ CMD_UDP_OPEN: 'UDP_OPEN',
+ CMD_UDP_DATA: 'UDP_DATA',
+ CMD_UDP_CLOSE: 'UDP_CLOSE',
}
@@ -128,7 +134,8 @@ def try_connect(self):
return # already connected
self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
- if socket.inet_aton(self.connect_to[0])[0] == '\0':
+ family = self.rsock.family
+ if family==socket.AF_INET and socket.inet_pton(family, self.connect_to[0])[0] == '\0':
self.seterr(Exception("Can't connect to %r: "
"IP address starts with zero\n"
% (self.connect_to,)))
@@ -152,6 +159,17 @@ def try_connect(self):
debug3('%r: fixed connect result: %s\n' % (self, e))
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet
+ elif e.args[0] == 0:
+ # connected successfully (weird Linux bug?)
+ # Sometimes Linux seems to return EINVAL when it isn't
+ # invalid. This *may* be caused by a race condition
+ # between connect() and getsockopt(SO_ERROR) (ie. it
+ # finishes connecting in between the two, so there is no
+ # longer an error). However, I'm not sure of that.
+ #
+ # I did get at least one report that the problem went away
+ # when we added this, however.
+ self.connect_to = None
elif e.args[0] == errno.EISCONN:
# connected successfully (BSD)
self.connect_to = None
@@ -306,6 +324,7 @@ def __init__(self, rsock, wsock):
self.rsock = rsock
self.wsock = wsock
self.new_channel = self.got_dns_req = self.got_routes = None
+ self.got_udp_open = self.got_udp_data = self.got_udp_close = None
self.got_host_req = self.got_host_list = None
self.channels = {}
self.chani = 0
@@ -363,7 +382,7 @@ def got_packet(self, channel, cmd, data):
self.fullness = 0
elif cmd == CMD_EXIT:
self.ok = False
- elif cmd == CMD_CONNECT:
+ elif cmd == CMD_TCP_CONNECT:
assert(not self.channels.get(channel))
if self.new_channel:
self.new_channel(channel, data)
@@ -371,6 +390,10 @@ def got_packet(self, channel, cmd, data):
assert(not self.channels.get(channel))
if self.got_dns_req:
self.got_dns_req(channel, data)
+ elif cmd == CMD_UDP_OPEN:
+ assert(not self.channels.get(channel))
+ if self.got_udp_open:
+ self.got_udp_open(channel, data)
elif cmd == CMD_ROUTES:
if self.got_routes:
self.got_routes(data)
@@ -467,13 +490,13 @@ def __repr__(self):
def noread(self):
if not self.shut_read:
self.shut_read = True
- self.mux.send(self.channel, CMD_STOP_SENDING, '')
+ self.mux.send(self.channel, CMD_TCP_STOP_SENDING, '')
self.maybe_close()
def nowrite(self):
if not self.shut_write:
self.shut_write = True
- self.mux.send(self.channel, CMD_EOF, '')
+ self.mux.send(self.channel, CMD_TCP_EOF, '')
self.maybe_close()
def maybe_close(self):
@@ -490,7 +513,7 @@ def uwrite(self, buf):
return 0 # too much already enqueued
if len(buf) > 2048:
buf = buf[:2048]
- self.mux.send(self.channel, CMD_DATA, buf)
+ self.mux.send(self.channel, CMD_TCP_DATA, buf)
return len(buf)
def uread(self):
@@ -500,20 +523,20 @@ def uread(self):
return None # no data available right now
def got_packet(self, cmd, data):
- if cmd == CMD_EOF:
+ if cmd == CMD_TCP_EOF:
self.noread()
- elif cmd == CMD_STOP_SENDING:
+ elif cmd == CMD_TCP_STOP_SENDING:
self.nowrite()
- elif cmd == CMD_DATA:
+ elif cmd == CMD_TCP_DATA:
self.buf.append(data)
else:
raise Exception('unknown command %d (%d bytes)'
% (cmd, len(data)))
-def connect_dst(ip, port):
+def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
- outsock = socket.socket()
+ outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock,
connect_to = (ip,port),
diff --git a/ssyslog.py b/src/ssyslog.py
similarity index 100%
rename from ssyslog.py
rename to src/ssyslog.py
diff --git a/stresstest.py b/src/stresstest.py
old mode 100755
new mode 100644
similarity index 100%
rename from stresstest.py
rename to src/stresstest.py
diff --git a/ui-macos/.gitignore b/src/ui-macos/.gitignore
similarity index 100%
rename from ui-macos/.gitignore
rename to src/ui-macos/.gitignore
diff --git a/ui-macos/Info.plist b/src/ui-macos/Info.plist
similarity index 100%
rename from ui-macos/Info.plist
rename to src/ui-macos/Info.plist
diff --git a/ui-macos/MainMenu.xib b/src/ui-macos/MainMenu.xib
similarity index 100%
rename from ui-macos/MainMenu.xib
rename to src/ui-macos/MainMenu.xib
diff --git a/ui-macos/UserDefaults.plist b/src/ui-macos/UserDefaults.plist
similarity index 100%
rename from ui-macos/UserDefaults.plist
rename to src/ui-macos/UserDefaults.plist
diff --git a/ui-macos/all.do b/src/ui-macos/all.do
similarity index 100%
rename from ui-macos/all.do
rename to src/ui-macos/all.do
diff --git a/ui-macos/app.icns b/src/ui-macos/app.icns
similarity index 100%
rename from ui-macos/app.icns
rename to src/ui-macos/app.icns
diff --git a/ui-macos/askpass.py b/src/ui-macos/askpass.py
similarity index 100%
rename from ui-macos/askpass.py
rename to src/ui-macos/askpass.py
diff --git a/ui-macos/bits/.gitignore b/src/ui-macos/bits/.gitignore
similarity index 100%
rename from ui-macos/bits/.gitignore
rename to src/ui-macos/bits/.gitignore
diff --git a/ui-macos/bits/PkgInfo b/src/ui-macos/bits/PkgInfo
similarity index 100%
rename from ui-macos/bits/PkgInfo
rename to src/ui-macos/bits/PkgInfo
diff --git a/ui-macos/bits/runpython.c b/src/ui-macos/bits/runpython.c
similarity index 100%
rename from ui-macos/bits/runpython.c
rename to src/ui-macos/bits/runpython.c
diff --git a/ui-macos/bits/runpython.do b/src/ui-macos/bits/runpython.do
similarity index 100%
rename from ui-macos/bits/runpython.do
rename to src/ui-macos/bits/runpython.do
diff --git a/ui-macos/chicken-tiny-bw.png b/src/ui-macos/chicken-tiny-bw.png
similarity index 100%
rename from ui-macos/chicken-tiny-bw.png
rename to src/ui-macos/chicken-tiny-bw.png
diff --git a/ui-macos/chicken-tiny-err.png b/src/ui-macos/chicken-tiny-err.png
similarity index 100%
rename from ui-macos/chicken-tiny-err.png
rename to src/ui-macos/chicken-tiny-err.png
diff --git a/ui-macos/chicken-tiny.png b/src/ui-macos/chicken-tiny.png
similarity index 100%
rename from ui-macos/chicken-tiny.png
rename to src/ui-macos/chicken-tiny.png
diff --git a/ui-macos/clean.do b/src/ui-macos/clean.do
similarity index 100%
rename from ui-macos/clean.do
rename to src/ui-macos/clean.do
diff --git a/ui-macos/debug.app.do b/src/ui-macos/debug.app.do
similarity index 100%
rename from ui-macos/debug.app.do
rename to src/ui-macos/debug.app.do
diff --git a/ui-macos/default.app.do b/src/ui-macos/default.app.do
similarity index 100%
rename from ui-macos/default.app.do
rename to src/ui-macos/default.app.do
diff --git a/ui-macos/default.app.tar.gz.do b/src/ui-macos/default.app.tar.gz.do
similarity index 100%
rename from ui-macos/default.app.tar.gz.do
rename to src/ui-macos/default.app.tar.gz.do
diff --git a/ui-macos/default.app.zip.do b/src/ui-macos/default.app.zip.do
similarity index 100%
rename from ui-macos/default.app.zip.do
rename to src/ui-macos/default.app.zip.do
diff --git a/ui-macos/default.nib.do b/src/ui-macos/default.nib.do
similarity index 100%
rename from ui-macos/default.nib.do
rename to src/ui-macos/default.nib.do
diff --git a/ui-macos/dist.do b/src/ui-macos/dist.do
similarity index 100%
rename from ui-macos/dist.do
rename to src/ui-macos/dist.do
diff --git a/ui-macos/git-export.do b/src/ui-macos/git-export.do
similarity index 100%
rename from ui-macos/git-export.do
rename to src/ui-macos/git-export.do
diff --git a/ui-macos/main.py b/src/ui-macos/main.py
similarity index 100%
rename from ui-macos/main.py
rename to src/ui-macos/main.py
diff --git a/ui-macos/models.py b/src/ui-macos/models.py
similarity index 100%
rename from ui-macos/models.py
rename to src/ui-macos/models.py
diff --git a/ui-macos/my.py b/src/ui-macos/my.py
similarity index 100%
rename from ui-macos/my.py
rename to src/ui-macos/my.py
diff --git a/ui-macos/run.do b/src/ui-macos/run.do
similarity index 100%
rename from ui-macos/run.do
rename to src/ui-macos/run.do
diff --git a/ui-macos/sources.list.do b/src/ui-macos/sources.list.do
similarity index 100%
rename from ui-macos/sources.list.do
rename to src/ui-macos/sources.list.do
diff --git a/ui-macos/sshuttle b/src/ui-macos/sshuttle
similarity index 100%
rename from ui-macos/sshuttle
rename to src/ui-macos/sshuttle