Skip to content

Commit

Permalink
Add -N (--auto-nets) option for auto-discovering subnets.
Browse files Browse the repository at this point in the history
Now if you do

	./sshuttle -Nr username@myservername

It'll automatically route the "local" subnets (ie., stuff in the routing
table) from myservername.  This is (hopefully a reasonable default setting
for most people.
  • Loading branch information
apenwarr committed May 8, 2010
1 parent 77935bd commit 7043195
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 22 deletions.
2 changes: 1 addition & 1 deletion assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
if name:
nbytes = int(sys.stdin.readline())
if verbosity >= 2:
sys.stderr.write('remote assembling %r (%d bytes)\n'
sys.stderr.write('server: assembling %r (%d bytes)\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
exec compile(content, name, "exec")
Expand Down
32 changes: 24 additions & 8 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def original_dst(sock):
class FirewallClient:
def __init__(self, port, subnets):
self.port = port
self.auto_nets = []
self.subnets = subnets
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)] + subnets_str)
['--firewall', str(port)])
argv_tries = [
['sudo'] + argvbase,
['su', '-c', ' '.join(argvbase)],
Expand Down Expand Up @@ -66,6 +66,9 @@ def check(self):
raise Fatal('%r returned %d' % (self.argv, rv))

def start(self):
self.pfile.write('ROUTES\n')
for (ip,width) in self.subnets+self.auto_nets:
self.pfile.write('%s,%d\n' % (ip, width))
self.pfile.write('GO\n')
self.pfile.flush()
line = self.pfile.readline()
Expand All @@ -80,7 +83,7 @@ def done(self):
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))


def _main(listener, fw, use_server, remotename):
def _main(listener, fw, use_server, remotename, auto_nets):
handlers = []
if use_server:
if helpers.verbose >= 1:
Expand All @@ -102,9 +105,22 @@ def _main(listener, fw, use_server, remotename):
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))

# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
fw.start()
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 onaccept():
sock,srcip = listener.accept()
Expand Down Expand Up @@ -149,7 +165,7 @@ def onaccept():
mux.check_fullness()


def main(listenip, use_server, remotename, subnets):
def main(listenip, use_server, remotename, auto_nets, subnets):
debug1('Starting sshuttle proxy.\n')
listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Expand Down Expand Up @@ -179,6 +195,6 @@ def main(listenip, use_server, remotename, subnets):
fw = FirewallClient(listenip[1], subnets)

try:
return _main(listener, fw, use_server, remotename)
return _main(listener, fw, use_server, remotename, auto_nets)
finally:
fw.done()
19 changes: 16 additions & 3 deletions firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def program_exists(name):
# 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, subnets):
def main(port):
assert(port > 0)
assert(port <= 65535)

Expand Down Expand Up @@ -173,8 +173,21 @@ def main(port, subnets):
line = sys.stdin.readline(128)
if not line:
return # parent died; nothing to do
if line != 'GO\n':
raise Fatal('firewall: expected GO but got %r' % line)

subnets = []
if line != 'ROUTES\n':
raise Fatal('firewall: expected ROUTES but got %r' % line)
while 1:
line = sys.stdin.readline(128)
if not line:
raise Fatal('firewall: expected route but got %r' % line)
elif line == 'GO\n':
break
try:
(ip,width) = line.strip().split(',', 1)
except:
raise Fatal('firewall: expected route or GO but got %r' % line)
subnets.append((ip, int(width)))
try:
if line:
debug1('firewall manager: starting transproxy.\n')
Expand Down
13 changes: 7 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def parse_ipport(s):
sshuttle --server
--
l,listen= transproxy to this ip address and port number [default=0]
N,auto-nets automatically determine subnets to route
r,remote= ssh hostname (and optional username) of remote sshuttle server
v,verbose increase debug message verbosity
noserver don't use a separate server process (mostly for debugging)
Expand All @@ -65,19 +66,19 @@ def parse_ipport(s):
if opt.server:
sys.exit(server.main())
elif opt.firewall:
if len(extra) < 1:
o.fatal('at least one argument expected')
sys.exit(firewall.main(int(extra[0]),
parse_subnets(extra[1:])))
if len(extra) != 1:
o.fatal('exactly one argument expected')
sys.exit(firewall.main(int(extra[0])))
else:
if len(extra) < 1:
o.fatal('at least one subnet expected')
if len(extra) < 1 and not opt.auto_nets:
o.fatal('at least one subnet (or -N) expected')
remotename = opt.remote
if remotename == '' or remotename == '-':
remotename = None
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
not opt.noserver,
remotename,
opt.auto_nets,
parse_subnets(extra)))
except Fatal, e:
log('fatal: %s\n' % e)
Expand Down
73 changes: 72 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,83 @@
import struct, socket, select
import re, struct, socket, select, subprocess
if not globals().get('skip_imports'):
import ssnet, helpers
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *


def _ipmatch(ipstr):
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] == None:
ips += '.0.0.0'
width = min(width, 8)
elif g[2] == None:
ips += '.0.0'
width = min(width, 16)
elif g[3] == None:
ips += '.0'
width = min(width, 24)
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)


def _ipstr(ip, width):
if width >= 32:
return ip
else:
return "%s/%d" % (ip, width)


def _maskbits(netmask):
if not netmask:
return 32
for i in range(32):
if netmask[0] & (1<<i):
return 32-i
return 0


def _list_routes():
argv = ['netstat', '-rn']
p = subprocess.Popen(argv, stdout=subprocess.PIPE)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line)
ipw = _ipmatch(cols[0])
if not ipw:
continue # some lines won't be parseable; never mind
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
width = min(ipw[1], mask)
ip = ipw[0] & (((1<<width)-1) << (32-width))
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
if rv != 0:
raise Fatal('%r returned %d' % (argv, rv))
return routes


def list_routes():
for (ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
yield (ip,width)



def main():
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '

routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %s/%d\n' % r)

# synchronization header
sys.stdout.write('SSHUTTLE0001')
Expand All @@ -21,6 +89,9 @@ def main():
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux)
routepkt = ''.join('%s,%d\n' % r
for r in routes)
mux.send(0, ssnet.CMD_ROUTES, routepkt)

def new_channel(channel, data):
(dstip,dstport) = data.split(',', 1)
Expand Down
14 changes: 11 additions & 3 deletions ssnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CMD_CLOSE = 0x4204
CMD_EOF = 0x4205
CMD_DATA = 0x4206
CMD_ROUTES = 0x4207

cmd_to_name = {
CMD_EXIT: 'EXIT',
Expand All @@ -21,6 +22,7 @@
CMD_CLOSE: 'CLOSE',
CMD_EOF: 'EOF',
CMD_DATA: 'DATA',
CMD_ROUTES: 'ROUTES',
}


Expand Down Expand Up @@ -220,7 +222,7 @@ def __init__(self, rsock, wsock):
Handler.__init__(self, [rsock, wsock])
self.rsock = rsock
self.wsock = wsock
self.new_channel = None
self.new_channel = self.got_routes = None
self.channels = {}
self.chani = 0
self.want = 0
Expand Down Expand Up @@ -259,12 +261,13 @@ def send(self, channel, cmd, data):
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
% (channel, cmd_to_name[cmd], len(data), self.fullness))
% (channel, cmd_to_name.get(cmd,hex(cmd)),
len(data), self.fullness))
self.fullness += len(data)

def got_packet(self, channel, cmd, data):
debug2('< channel=%d cmd=%s len=%d\n'
% (channel, cmd_to_name[cmd], len(data)))
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data)))
if cmd == CMD_PING:
self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG:
Expand All @@ -277,6 +280,11 @@ def got_packet(self, channel, cmd, data):
assert(not self.channels.get(channel))
if self.new_channel:
self.new_channel(channel, data)
elif cmd == CMD_ROUTES:
if self.got_routes:
self.got_routes(data)
else:
raise Exception('weird: got CMD_ROUTES without got_routes?')
else:
callback = self.channels[channel]
callback(cmd, data)
Expand Down

0 comments on commit 7043195

Please sign in to comment.