mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-04-25 19:58:56 +02:00
Add -N (--auto-nets) option for auto-discovering subnets.
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.
This commit is contained in:
parent
77935bd110
commit
7043195043
@ -7,7 +7,7 @@ while 1:
|
|||||||
if name:
|
if name:
|
||||||
nbytes = int(sys.stdin.readline())
|
nbytes = int(sys.stdin.readline())
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
sys.stderr.write('remote assembling %r (%d bytes)\n'
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
% (name, nbytes))
|
% (name, nbytes))
|
||||||
content = z.decompress(sys.stdin.read(nbytes))
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
exec compile(content, name, "exec")
|
exec compile(content, name, "exec")
|
||||||
|
32
client.py
32
client.py
@ -22,11 +22,11 @@ def original_dst(sock):
|
|||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
def __init__(self, port, subnets):
|
def __init__(self, port, subnets):
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.auto_nets = []
|
||||||
self.subnets = subnets
|
self.subnets = subnets
|
||||||
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
|
|
||||||
argvbase = ([sys.argv[0]] +
|
argvbase = ([sys.argv[0]] +
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--firewall', str(port)] + subnets_str)
|
['--firewall', str(port)])
|
||||||
argv_tries = [
|
argv_tries = [
|
||||||
['sudo'] + argvbase,
|
['sudo'] + argvbase,
|
||||||
['su', '-c', ' '.join(argvbase)],
|
['su', '-c', ' '.join(argvbase)],
|
||||||
@ -66,6 +66,9 @@ class FirewallClient:
|
|||||||
raise Fatal('%r returned %d' % (self.argv, rv))
|
raise Fatal('%r returned %d' % (self.argv, rv))
|
||||||
|
|
||||||
def start(self):
|
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.write('GO\n')
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
line = self.pfile.readline()
|
line = self.pfile.readline()
|
||||||
@ -80,7 +83,7 @@ class FirewallClient:
|
|||||||
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
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 = []
|
handlers = []
|
||||||
if use_server:
|
if use_server:
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
@ -102,9 +105,22 @@ def _main(listener, fw, use_server, remotename):
|
|||||||
raise Fatal('expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
|
|
||||||
# we definitely want to do this *after* starting ssh, or we might end
|
def onroutes(routestr):
|
||||||
# up intercepting the ssh connection!
|
if auto_nets:
|
||||||
fw.start()
|
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():
|
def onaccept():
|
||||||
sock,srcip = listener.accept()
|
sock,srcip = listener.accept()
|
||||||
@ -149,7 +165,7 @@ def _main(listener, fw, use_server, remotename):
|
|||||||
mux.check_fullness()
|
mux.check_fullness()
|
||||||
|
|
||||||
|
|
||||||
def main(listenip, use_server, remotename, subnets):
|
def main(listenip, use_server, remotename, auto_nets, subnets):
|
||||||
debug1('Starting sshuttle proxy.\n')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
listener = socket.socket()
|
listener = socket.socket()
|
||||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
@ -179,6 +195,6 @@ def main(listenip, use_server, remotename, subnets):
|
|||||||
fw = FirewallClient(listenip[1], subnets)
|
fw = FirewallClient(listenip[1], subnets)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _main(listener, fw, use_server, remotename)
|
return _main(listener, fw, use_server, remotename, auto_nets)
|
||||||
finally:
|
finally:
|
||||||
fw.done()
|
fw.done()
|
||||||
|
19
firewall.py
19
firewall.py
@ -140,7 +140,7 @@ def program_exists(name):
|
|||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
# 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
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
# are hopefully harmless.
|
# are hopefully harmless.
|
||||||
def main(port, subnets):
|
def main(port):
|
||||||
assert(port > 0)
|
assert(port > 0)
|
||||||
assert(port <= 65535)
|
assert(port <= 65535)
|
||||||
|
|
||||||
@ -173,8 +173,21 @@ def main(port, subnets):
|
|||||||
line = sys.stdin.readline(128)
|
line = sys.stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
return # parent died; nothing to do
|
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:
|
try:
|
||||||
if line:
|
if line:
|
||||||
debug1('firewall manager: starting transproxy.\n')
|
debug1('firewall manager: starting transproxy.\n')
|
||||||
|
13
main.py
13
main.py
@ -50,6 +50,7 @@ sshuttle --firewall <port> <subnets...>
|
|||||||
sshuttle --server
|
sshuttle --server
|
||||||
--
|
--
|
||||||
l,listen= transproxy to this ip address and port number [default=0]
|
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
|
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||||
v,verbose increase debug message verbosity
|
v,verbose increase debug message verbosity
|
||||||
noserver don't use a separate server process (mostly for debugging)
|
noserver don't use a separate server process (mostly for debugging)
|
||||||
@ -65,19 +66,19 @@ try:
|
|||||||
if opt.server:
|
if opt.server:
|
||||||
sys.exit(server.main())
|
sys.exit(server.main())
|
||||||
elif opt.firewall:
|
elif opt.firewall:
|
||||||
if len(extra) < 1:
|
if len(extra) != 1:
|
||||||
o.fatal('at least one argument expected')
|
o.fatal('exactly one argument expected')
|
||||||
sys.exit(firewall.main(int(extra[0]),
|
sys.exit(firewall.main(int(extra[0])))
|
||||||
parse_subnets(extra[1:])))
|
|
||||||
else:
|
else:
|
||||||
if len(extra) < 1:
|
if len(extra) < 1 and not opt.auto_nets:
|
||||||
o.fatal('at least one subnet expected')
|
o.fatal('at least one subnet (or -N) expected')
|
||||||
remotename = opt.remote
|
remotename = opt.remote
|
||||||
if remotename == '' or remotename == '-':
|
if remotename == '' or remotename == '-':
|
||||||
remotename = None
|
remotename = None
|
||||||
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
|
||||||
not opt.noserver,
|
not opt.noserver,
|
||||||
remotename,
|
remotename,
|
||||||
|
opt.auto_nets,
|
||||||
parse_subnets(extra)))
|
parse_subnets(extra)))
|
||||||
except Fatal, e:
|
except Fatal, e:
|
||||||
log('fatal: %s\n' % e)
|
log('fatal: %s\n' % e)
|
||||||
|
73
server.py
73
server.py
@ -1,15 +1,83 @@
|
|||||||
import struct, socket, select
|
import re, struct, socket, select, subprocess
|
||||||
if not globals().get('skip_imports'):
|
if not globals().get('skip_imports'):
|
||||||
import ssnet, helpers
|
import ssnet, helpers
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from helpers import *
|
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():
|
def main():
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
else:
|
else:
|
||||||
helpers.logprefix = 'server: '
|
helpers.logprefix = 'server: '
|
||||||
|
|
||||||
|
routes = list(list_routes())
|
||||||
|
debug1('available routes:\n')
|
||||||
|
for r in routes:
|
||||||
|
debug1(' %s/%d\n' % r)
|
||||||
|
|
||||||
# synchronization header
|
# synchronization header
|
||||||
sys.stdout.write('SSHUTTLE0001')
|
sys.stdout.write('SSHUTTLE0001')
|
||||||
@ -21,6 +89,9 @@ def main():
|
|||||||
socket.fromfd(sys.stdout.fileno(),
|
socket.fromfd(sys.stdout.fileno(),
|
||||||
socket.AF_INET, socket.SOCK_STREAM))
|
socket.AF_INET, socket.SOCK_STREAM))
|
||||||
handlers.append(mux)
|
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):
|
def new_channel(channel, data):
|
||||||
(dstip,dstport) = data.split(',', 1)
|
(dstip,dstport) = data.split(',', 1)
|
||||||
|
14
ssnet.py
14
ssnet.py
@ -12,6 +12,7 @@ CMD_CONNECT = 0x4203
|
|||||||
CMD_CLOSE = 0x4204
|
CMD_CLOSE = 0x4204
|
||||||
CMD_EOF = 0x4205
|
CMD_EOF = 0x4205
|
||||||
CMD_DATA = 0x4206
|
CMD_DATA = 0x4206
|
||||||
|
CMD_ROUTES = 0x4207
|
||||||
|
|
||||||
cmd_to_name = {
|
cmd_to_name = {
|
||||||
CMD_EXIT: 'EXIT',
|
CMD_EXIT: 'EXIT',
|
||||||
@ -21,6 +22,7 @@ cmd_to_name = {
|
|||||||
CMD_CLOSE: 'CLOSE',
|
CMD_CLOSE: 'CLOSE',
|
||||||
CMD_EOF: 'EOF',
|
CMD_EOF: 'EOF',
|
||||||
CMD_DATA: 'DATA',
|
CMD_DATA: 'DATA',
|
||||||
|
CMD_ROUTES: 'ROUTES',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -220,7 +222,7 @@ class Mux(Handler):
|
|||||||
Handler.__init__(self, [rsock, wsock])
|
Handler.__init__(self, [rsock, wsock])
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
self.new_channel = None
|
self.new_channel = self.got_routes = None
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
self.want = 0
|
self.want = 0
|
||||||
@ -259,12 +261,13 @@ class Mux(Handler):
|
|||||||
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
|
p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
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)
|
self.fullness += len(data)
|
||||||
|
|
||||||
def got_packet(self, channel, cmd, data):
|
def got_packet(self, channel, cmd, data):
|
||||||
debug2('< channel=%d cmd=%s len=%d\n'
|
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:
|
if cmd == CMD_PING:
|
||||||
self.send(0, CMD_PONG, data)
|
self.send(0, CMD_PONG, data)
|
||||||
elif cmd == CMD_PONG:
|
elif cmd == CMD_PONG:
|
||||||
@ -277,6 +280,11 @@ class Mux(Handler):
|
|||||||
assert(not self.channels.get(channel))
|
assert(not self.channels.get(channel))
|
||||||
if self.new_channel:
|
if self.new_channel:
|
||||||
self.new_channel(channel, data)
|
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:
|
else:
|
||||||
callback = self.channels[channel]
|
callback = self.channels[channel]
|
||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
|
Loading…
Reference in New Issue
Block a user