mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-05 01:00:37 +02:00
Compare commits
14 Commits
sshuttle-0
...
sshuttle-0
Author | SHA1 | Date | |
---|---|---|---|
f1b33dab29 | |||
3a25f709e5 | |||
a8b3d69856 | |||
2d4f6a4308 | |||
d435ed837d | |||
2d77403a0b | |||
77cf37e0fa | |||
384d0e7c1d | |||
5a4a2ab7f9 | |||
33efa5ac62 | |||
a2ea5ab455 | |||
680941cb0c | |||
7043195043 | |||
77935bd110 |
@ -161,3 +161,6 @@ later. You're welcome.
|
||||
--
|
||||
Avery Pennarun <apenwarr@gmail.com>
|
||||
|
||||
Mailing list:
|
||||
Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
||||
List archives are at: http://groups.google.com/group/sshuttle
|
||||
|
@ -7,7 +7,7 @@ while 1:
|
||||
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")
|
||||
|
65
client.py
65
client.py
@ -1,4 +1,4 @@
|
||||
import struct, socket, select, subprocess, errno
|
||||
import struct, socket, select, subprocess, errno, re
|
||||
import helpers, ssnet, ssh
|
||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||
from helpers import *
|
||||
@ -20,13 +20,14 @@ def original_dst(sock):
|
||||
|
||||
|
||||
class FirewallClient:
|
||||
def __init__(self, port, subnets):
|
||||
def __init__(self, port, subnets_include, subnets_exclude):
|
||||
self.port = port
|
||||
self.subnets = subnets
|
||||
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
|
||||
self.auto_nets = []
|
||||
self.subnets_include = subnets_include
|
||||
self.subnets_exclude = subnets_exclude
|
||||
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)],
|
||||
@ -66,6 +67,11 @@ class FirewallClient:
|
||||
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()
|
||||
@ -73,6 +79,12 @@ class FirewallClient:
|
||||
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()
|
||||
@ -80,13 +92,14 @@ class FirewallClient:
|
||||
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
||||
|
||||
|
||||
def _main(listener, fw, use_server, remotename):
|
||||
def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
|
||||
handlers = []
|
||||
if use_server:
|
||||
if helpers.verbose >= 1:
|
||||
helpers.logprefix = 'c : '
|
||||
else:
|
||||
helpers.logprefix = 'client: '
|
||||
debug1('connecting to server...\n')
|
||||
(serverproc, serversock) = ssh.connect(remotename)
|
||||
mux = Mux(serversock, serversock)
|
||||
handlers.append(mux)
|
||||
@ -101,10 +114,32 @@ def _main(listener, fw, use_server, remotename):
|
||||
if initstring != expected:
|
||||
raise Fatal('expected server init string %r; got %r'
|
||||
% (expected, initstring))
|
||||
debug1('connected.\n')
|
||||
|
||||
# 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 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():
|
||||
sock,srcip = listener.accept()
|
||||
@ -123,6 +158,10 @@ def _main(listener, fw, use_server, remotename):
|
||||
outwrap = ssnet.connect_dst(dstip[0], dstip[1])
|
||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||
handlers.append(Handler([listener], onaccept))
|
||||
|
||||
if seed_hosts != None:
|
||||
debug1('seed_hosts: %r\n' % seed_hosts)
|
||||
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
||||
|
||||
while 1:
|
||||
if use_server:
|
||||
@ -149,7 +188,8 @@ def _main(listener, fw, use_server, remotename):
|
||||
mux.check_fullness()
|
||||
|
||||
|
||||
def main(listenip, use_server, remotename, subnets):
|
||||
def main(listenip, use_server, remotename, seed_hosts, auto_nets,
|
||||
subnets_include, subnets_exclude):
|
||||
debug1('Starting sshuttle proxy.\n')
|
||||
listener = socket.socket()
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
@ -176,9 +216,10 @@ def main(listenip, use_server, remotename, subnets):
|
||||
listenip = listener.getsockname()
|
||||
debug1('Listening on %r.\n' % (listenip,))
|
||||
|
||||
fw = FirewallClient(listenip[1], subnets)
|
||||
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
|
||||
|
||||
try:
|
||||
return _main(listener, fw, use_server, remotename)
|
||||
return _main(listener, fw, use_server, remotename,
|
||||
seed_hosts, auto_nets)
|
||||
finally:
|
||||
fw.done()
|
||||
|
129
firewall.py
129
firewall.py
@ -1,4 +1,4 @@
|
||||
import subprocess, re
|
||||
import subprocess, re, errno
|
||||
import helpers
|
||||
from helpers import *
|
||||
|
||||
@ -43,14 +43,23 @@ def do_iptables(port, subnets):
|
||||
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
||||
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
||||
|
||||
# create new subnet entries
|
||||
for snet,swidth in subnets:
|
||||
ipt('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/%s' % (snet,swidth),
|
||||
'-p', 'tcp',
|
||||
'--to-ports', str(port),
|
||||
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
||||
)
|
||||
# create new subnet entries. Note that we're sorting in a very
|
||||
# particular order: we need to go from most-specific (largest swidth)
|
||||
# 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):
|
||||
if sexclude:
|
||||
ipt('-A', chain, '-j', 'RETURN',
|
||||
'--dest', '%s/%s' % (snet,swidth),
|
||||
'-p', 'tcp')
|
||||
else:
|
||||
ipt('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/%s' % (snet,swidth),
|
||||
'-p', 'tcp',
|
||||
'--to-ports', str(port),
|
||||
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
|
||||
)
|
||||
|
||||
|
||||
def ipfw_rule_exists(n):
|
||||
@ -58,7 +67,7 @@ def ipfw_rule_exists(n):
|
||||
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
|
||||
for line in p.stdout:
|
||||
if line.startswith('%05d ' % n):
|
||||
if line[5:].find('ipttl 42') < 0:
|
||||
if line.find('ipttl 42') < 0 and line.find('established') < 0:
|
||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||
return True
|
||||
rv = p.wait()
|
||||
@ -103,6 +112,7 @@ def ipfw(*args):
|
||||
|
||||
def do_ipfw(port, subnets):
|
||||
sport = str(port)
|
||||
xsport = str(port+1)
|
||||
|
||||
# cleanup any existing rules
|
||||
if ipfw_rule_exists(port):
|
||||
@ -115,13 +125,21 @@ def do_ipfw(port, subnets):
|
||||
if subnets:
|
||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||
sysctl_set('net.inet.ip.forwarding', 1)
|
||||
|
||||
ipfw('add', sport, 'accept', 'ip',
|
||||
'from', 'any', 'to', 'any', 'established')
|
||||
|
||||
# create new subnet entries
|
||||
for snet,swidth in subnets:
|
||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||
'log', 'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||
'not', 'ipttl', '42')
|
||||
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||
if sexclude:
|
||||
ipfw('add', sport, 'skipto', xsport,
|
||||
'log', 'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
||||
else:
|
||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||
'log', 'tcp',
|
||||
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||
'not', 'ipttl', '42')
|
||||
|
||||
|
||||
def program_exists(name):
|
||||
@ -131,6 +149,47 @@ def program_exists(name):
|
||||
if os.path.exists(fn):
|
||||
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||
|
||||
hostmap = {}
|
||||
def rewrite_etc_hosts(port):
|
||||
HOSTSFILE='/etc/hosts'
|
||||
BAKFILE='%s.sbak' % HOSTSFILE
|
||||
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
|
||||
old_content = ''
|
||||
st = None
|
||||
try:
|
||||
old_content = open(HOSTSFILE).read()
|
||||
st = os.stat(HOSTSFILE)
|
||||
except IOError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||
os.link(HOSTSFILE, BAKFILE)
|
||||
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||
f = open(tmpname, 'w')
|
||||
for line in old_content.rstrip().split('\n'):
|
||||
if line.find(APPEND) >= 0:
|
||||
continue
|
||||
f.write('%s\n' % line)
|
||||
for (name,ip) in sorted(hostmap.items()):
|
||||
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND))
|
||||
f.close()
|
||||
|
||||
if st:
|
||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||
os.chmod(tmpname, st.st_mode)
|
||||
else:
|
||||
os.chown(tmpname, 0, 0)
|
||||
os.chmod(tmpname, 0644)
|
||||
os.rename(tmpname, HOSTSFILE)
|
||||
|
||||
|
||||
def restore_etc_hosts(port):
|
||||
global hostmap
|
||||
hostmap = {}
|
||||
rewrite_etc_hosts(port)
|
||||
|
||||
|
||||
# This is some voodoo for setting up the kernel's transparent
|
||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||
@ -140,7 +199,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)
|
||||
|
||||
@ -173,8 +232,22 @@ 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:
|
||||
(width,exclude,ip) = line.strip().split(',', 2)
|
||||
except:
|
||||
raise Fatal('firewall: expected route or GO but got %r' % line)
|
||||
subnets.append((int(width), bool(int(exclude)), ip))
|
||||
|
||||
try:
|
||||
if line:
|
||||
debug1('firewall manager: starting transproxy.\n')
|
||||
@ -183,20 +256,28 @@ def main(port, subnets):
|
||||
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
|
||||
# Now we wait until EOF or any other kind of exception. We need
|
||||
# to stay running so that we don't need a *second* password
|
||||
# authentication at shutdown time - that cleanup is important!
|
||||
while sys.stdin.readline(128):
|
||||
pass
|
||||
except IOError:
|
||||
# the parent process died for some reason; he's surely been loud
|
||||
# enough, so no reason to report another error
|
||||
return
|
||||
|
||||
# Now we wait until EOF or any other kind of exception. We need
|
||||
# to stay running so that we don't need a *second* password
|
||||
# authentication at shutdown time - that cleanup is important!
|
||||
while 1:
|
||||
line = sys.stdin.readline(128)
|
||||
if line.startswith('HOST '):
|
||||
(name,ip) = line[5:].strip().split(',', 1)
|
||||
hostmap[name] = ip
|
||||
rewrite_etc_hosts(port)
|
||||
elif line:
|
||||
raise Fatal('expected EOF, got %r' % line)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
do_it(port, [])
|
||||
restore_etc_hosts(port)
|
||||
|
15
helpers.py
15
helpers.py
@ -4,9 +4,14 @@ logprefix = ''
|
||||
verbose = 0
|
||||
|
||||
def log(s):
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write(logprefix + s)
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write(logprefix + s)
|
||||
sys.stderr.flush()
|
||||
except IOError:
|
||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||
# our tty closes. That sucks, but it's no reason to abort the program.
|
||||
pass
|
||||
|
||||
def debug1(s):
|
||||
if verbose >= 1:
|
||||
@ -16,6 +21,10 @@ def debug2(s):
|
||||
if verbose >= 2:
|
||||
log(s)
|
||||
|
||||
def debug3(s):
|
||||
if verbose >= 3:
|
||||
log(s)
|
||||
|
||||
|
||||
class Fatal(Exception):
|
||||
pass
|
||||
|
276
hostwatch.py
Normal file
276
hostwatch.py
Normal file
@ -0,0 +1,276 @@
|
||||
import subprocess, time, socket, re, select
|
||||
if not globals().get('skip_imports'):
|
||||
import helpers
|
||||
from helpers import *
|
||||
|
||||
POLL_TIME = 60*15
|
||||
NETSTAT_POLL_TIME = 30
|
||||
CACHEFILE=os.path.expanduser('~/.sshuttle.hosts')
|
||||
|
||||
|
||||
_nmb_ok = True
|
||||
_smb_ok = True
|
||||
hostnames = {}
|
||||
queue = {}
|
||||
null = open('/dev/null', 'rb+')
|
||||
|
||||
|
||||
def _is_ip(s):
|
||||
return re.match(r'\d+\.\d+\.\d+\.\d+$', s)
|
||||
|
||||
|
||||
def write_host_cache():
|
||||
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
||||
try:
|
||||
f = open(tmpname, 'wb')
|
||||
for name,ip in sorted(hostnames.items()):
|
||||
f.write('%s,%s\n' % (name, ip))
|
||||
f.close()
|
||||
os.rename(tmpname, CACHEFILE)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmpname)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def read_host_cache():
|
||||
try:
|
||||
f = open(CACHEFILE)
|
||||
except IOError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
return
|
||||
else:
|
||||
raise
|
||||
for line in f:
|
||||
words = line.strip().split(',')
|
||||
if len(words) == 2:
|
||||
(name,ip) = words
|
||||
name = re.sub(r'[^-\w]', '-', name).strip()
|
||||
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||
if name and ip:
|
||||
found_host(name, ip)
|
||||
|
||||
|
||||
def found_host(hostname, ip):
|
||||
hostname = re.sub(r'\..*', '', hostname)
|
||||
hostname = re.sub(r'[^-\w]', '_', hostname)
|
||||
if (ip.startswith('127.') or ip.startswith('255.')
|
||||
or hostname == 'localhost'):
|
||||
return
|
||||
oldip = hostnames.get(hostname)
|
||||
if oldip != ip:
|
||||
hostnames[hostname] = ip
|
||||
debug1('Found: %s: %s\n' % (hostname, ip))
|
||||
sys.stdout.write('%s,%s\n' % (hostname, ip))
|
||||
write_host_cache()
|
||||
|
||||
|
||||
def _check_etc_hosts():
|
||||
debug2(' > hosts\n')
|
||||
for line in open('/etc/hosts'):
|
||||
line = re.sub(r'#.*', '', line)
|
||||
words = line.strip().split()
|
||||
if not words:
|
||||
continue
|
||||
ip = words[0]
|
||||
names = words[1:]
|
||||
if _is_ip(ip):
|
||||
debug3('< %s %r\n' % (ip, names))
|
||||
for n in names:
|
||||
check_host(n)
|
||||
found_host(n, ip)
|
||||
|
||||
|
||||
def _check_revdns(ip):
|
||||
debug2(' > rev: %s\n' % ip)
|
||||
try:
|
||||
r = socket.gethostbyaddr(ip)
|
||||
debug3('< %s\n' % r[0])
|
||||
check_host(r[0])
|
||||
found_host(r[0], ip)
|
||||
except socket.herror, e:
|
||||
pass
|
||||
|
||||
|
||||
def _check_dns(hostname):
|
||||
debug2(' > dns: %s\n' % hostname)
|
||||
try:
|
||||
ip = socket.gethostbyname(hostname)
|
||||
debug3('< %s\n' % ip)
|
||||
check_host(ip)
|
||||
found_host(hostname, ip)
|
||||
except socket.gaierror, e:
|
||||
pass
|
||||
|
||||
|
||||
def _check_netstat():
|
||||
debug2(' > netstat\n')
|
||||
argv = ['netstat', '-n']
|
||||
try:
|
||||
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||
content = p.stdout.read()
|
||||
p.wait()
|
||||
except OSError, e:
|
||||
log('%r failed: %r\n' % (argv, e))
|
||||
return
|
||||
|
||||
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
||||
debug3('< %s\n' % ip)
|
||||
check_host(ip)
|
||||
|
||||
|
||||
def _check_smb(hostname):
|
||||
return
|
||||
global _smb_ok
|
||||
if not _smb_ok:
|
||||
return
|
||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||
debug2(' > smb: %s\n' % hostname)
|
||||
try:
|
||||
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||
lines = p.stdout.readlines()
|
||||
p.wait()
|
||||
except OSError, e:
|
||||
log('%r failed: %r\n' % (argv, e))
|
||||
_smb_ok = False
|
||||
return
|
||||
|
||||
lines.reverse()
|
||||
|
||||
# junk at top
|
||||
while lines:
|
||||
line = lines.pop().strip()
|
||||
if re.match(r'Server\s+', line):
|
||||
break
|
||||
|
||||
# server list section:
|
||||
# Server Comment
|
||||
# ------ -------
|
||||
while lines:
|
||||
line = lines.pop().strip()
|
||||
if not line or re.match(r'-+\s+-+', line):
|
||||
continue
|
||||
if re.match(r'Workgroup\s+Master', line):
|
||||
break
|
||||
words = line.split()
|
||||
hostname = words[0].lower()
|
||||
debug3('< %s\n' % hostname)
|
||||
check_host(hostname)
|
||||
|
||||
# workgroup list section:
|
||||
# Workgroup Master
|
||||
# --------- ------
|
||||
while lines:
|
||||
line = lines.pop().strip()
|
||||
if re.match(r'-+\s+', line):
|
||||
continue
|
||||
if not line:
|
||||
break
|
||||
words = line.split()
|
||||
(workgroup, hostname) = (words[0].lower(), words[1].lower())
|
||||
debug3('< group(%s) -> %s\n' % (workgroup, hostname))
|
||||
check_host(hostname)
|
||||
check_workgroup(workgroup)
|
||||
|
||||
if lines:
|
||||
assert(0)
|
||||
|
||||
|
||||
def _check_nmb(hostname, is_workgroup, is_master):
|
||||
return
|
||||
global _nmb_ok
|
||||
if not _nmb_ok:
|
||||
return
|
||||
argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname]
|
||||
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
||||
try:
|
||||
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=null)
|
||||
lines = p.stdout.readlines()
|
||||
rv = p.wait()
|
||||
except OSError, e:
|
||||
log('%r failed: %r\n' % (argv, e))
|
||||
_nmb_ok = False
|
||||
return
|
||||
if rv:
|
||||
log('%r returned %d\n' % (argv, rv))
|
||||
return
|
||||
for line in lines:
|
||||
m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line)
|
||||
if m:
|
||||
g = m.groups()
|
||||
(ip, name) = (g[0], g[1].lower())
|
||||
debug3('< %s -> %s\n' % (name, ip))
|
||||
if is_workgroup:
|
||||
_enqueue(_check_smb, ip)
|
||||
else:
|
||||
found_host(name, ip)
|
||||
check_host(name)
|
||||
|
||||
|
||||
def check_host(hostname):
|
||||
if _is_ip(hostname):
|
||||
_enqueue(_check_revdns, hostname)
|
||||
else:
|
||||
_enqueue(_check_dns, hostname)
|
||||
_enqueue(_check_smb, hostname)
|
||||
_enqueue(_check_nmb, hostname, False, False)
|
||||
|
||||
|
||||
def check_workgroup(hostname):
|
||||
_enqueue(_check_nmb, hostname, True, False)
|
||||
_enqueue(_check_nmb, hostname, True, True)
|
||||
|
||||
|
||||
def _enqueue(op, *args):
|
||||
t = (op,args)
|
||||
if queue.get(t) == None:
|
||||
queue[t] = 0
|
||||
|
||||
|
||||
def _stdin_still_ok(timeout):
|
||||
r,w,x = select.select([sys.stdin.fileno()], [], [], timeout)
|
||||
if r:
|
||||
b = os.read(sys.stdin.fileno(), 4096)
|
||||
if not b:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def hw_main(seed_hosts):
|
||||
if helpers.verbose >= 2:
|
||||
helpers.logprefix = 'HH: '
|
||||
else:
|
||||
helpers.logprefix = 'hostwatch: '
|
||||
|
||||
read_host_cache()
|
||||
|
||||
_enqueue(_check_etc_hosts)
|
||||
_enqueue(_check_netstat)
|
||||
check_host('localhost')
|
||||
check_host(socket.gethostname())
|
||||
check_workgroup('workgroup')
|
||||
check_workgroup('-')
|
||||
for h in seed_hosts:
|
||||
check_host(h)
|
||||
|
||||
while 1:
|
||||
now = time.time()
|
||||
for t,last_polled in queue.items():
|
||||
(op,args) = t
|
||||
if not _stdin_still_ok(0):
|
||||
break
|
||||
maxtime = POLL_TIME
|
||||
if op == _check_netstat:
|
||||
maxtime = NETSTAT_POLL_TIME
|
||||
if now - last_polled > maxtime:
|
||||
queue[t] = time.time()
|
||||
op(*args)
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except IOError:
|
||||
break
|
||||
|
||||
# FIXME: use a smarter timeout based on oldest last_polled
|
||||
if not _stdin_still_ok(1):
|
||||
break
|
40
main.py
40
main.py
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import sys, os, re
|
||||
import helpers, options, client, server, firewall
|
||||
import helpers, options, client, server, firewall, hostwatch
|
||||
from helpers import *
|
||||
|
||||
|
||||
@ -50,11 +50,16 @@ sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --server
|
||||
--
|
||||
l,listen= transproxy to this ip address and port number [default=0]
|
||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||
N,auto-nets automatically determine subnets to route
|
||||
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
|
||||
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
||||
noserver don't use a separate server process (mostly for debugging)
|
||||
server [internal use only]
|
||||
firewall [internal use only]
|
||||
hostwatch [internal use only]
|
||||
"""
|
||||
o = options.Options('sshuttle', optspec)
|
||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
||||
@ -63,22 +68,41 @@ helpers.verbose = opt.verbose
|
||||
|
||||
try:
|
||||
if opt.server:
|
||||
if len(extra) != 0:
|
||||
o.fatal('no arguments expected')
|
||||
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])))
|
||||
elif opt.hostwatch:
|
||||
sys.exit(hostwatch.hw_main(extra))
|
||||
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')
|
||||
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'),
|
||||
not opt.noserver,
|
||||
remotename,
|
||||
parse_subnets(extra)))
|
||||
sh,
|
||||
opt.auto_nets,
|
||||
parse_subnets(includes),
|
||||
parse_subnets(excludes)))
|
||||
except Fatal, e:
|
||||
log('fatal: %s\n' % e)
|
||||
sys.exit(99)
|
||||
|
130
server.py
130
server.py
@ -1,15 +1,115 @@
|
||||
import struct, socket, select
|
||||
import re, struct, socket, select, subprocess, traceback
|
||||
if not globals().get('skip_imports'):
|
||||
import ssnet, helpers
|
||||
import ssnet, helpers, hostwatch
|
||||
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 _exc_dump():
|
||||
exc_info = sys.exc_info()
|
||||
return ''.join(traceback.format_exception(*exc_info))
|
||||
|
||||
|
||||
def start_hostwatch(seed_hosts):
|
||||
s1,s2 = socket.socketpair()
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
# child
|
||||
rv = 99
|
||||
try:
|
||||
try:
|
||||
s2.close()
|
||||
os.dup2(s1.fileno(), 1)
|
||||
os.dup2(s1.fileno(), 0)
|
||||
s1.close()
|
||||
rv = hostwatch.hw_main(seed_hosts) or 0
|
||||
except Exception, e:
|
||||
log('%s\n' % _exc_dump())
|
||||
rv = 98
|
||||
finally:
|
||||
os._exit(rv)
|
||||
s1.close()
|
||||
return pid,s2
|
||||
|
||||
|
||||
class Hostwatch:
|
||||
def __init__(self):
|
||||
self.pid = 0
|
||||
self.sock = None
|
||||
|
||||
|
||||
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')
|
||||
@ -21,16 +121,40 @@ 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)
|
||||
|
||||
hw = Hostwatch()
|
||||
|
||||
def hostwatch_ready():
|
||||
assert(hw.pid)
|
||||
content = hw.sock.recv(4096)
|
||||
if content:
|
||||
mux.send(0, ssnet.CMD_HOST_LIST, content)
|
||||
else:
|
||||
raise Fatal('hostwatch process died')
|
||||
|
||||
def got_host_req(data):
|
||||
if not hw.pid:
|
||||
(hw.pid,hw.sock) = start_hostwatch(data.strip().split())
|
||||
handlers.append(Handler(socks = [hw.sock],
|
||||
callback = hostwatch_ready))
|
||||
mux.got_host_req = got_host_req
|
||||
|
||||
def new_channel(channel, data):
|
||||
(dstip,dstport) = data.split(',', 1)
|
||||
dstport = int(dstport)
|
||||
outwrap = ssnet.connect_dst(dstip,dstport)
|
||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||
|
||||
mux.new_channel = new_channel
|
||||
|
||||
while mux.ok:
|
||||
if hw.pid:
|
||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||
if rpid:
|
||||
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
||||
|
||||
r = set()
|
||||
w = set()
|
||||
x = set()
|
||||
|
9
ssh.py
9
ssh.py
@ -5,8 +5,12 @@ from helpers import *
|
||||
|
||||
def readfile(name):
|
||||
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
fullname = os.path.join(basedir, name)
|
||||
return open(fullname, 'rb').read()
|
||||
path = [basedir] + sys.path
|
||||
for d in path:
|
||||
fullname = os.path.join(d, name)
|
||||
if os.path.exists(fullname):
|
||||
return open(fullname, 'rb').read()
|
||||
raise Exception("can't find file %r in any of %r" % (name, path))
|
||||
|
||||
|
||||
def empackage(z, filename):
|
||||
@ -30,6 +34,7 @@ def connect(rhostport):
|
||||
content = readfile('assembler.py')
|
||||
content2 = (empackage(z, 'helpers.py') +
|
||||
empackage(z, 'ssnet.py') +
|
||||
empackage(z, 'hostwatch.py') +
|
||||
empackage(z, 'server.py') +
|
||||
"\n")
|
||||
|
||||
|
33
ssnet.py
33
ssnet.py
@ -12,6 +12,9 @@ CMD_CONNECT = 0x4203
|
||||
CMD_CLOSE = 0x4204
|
||||
CMD_EOF = 0x4205
|
||||
CMD_DATA = 0x4206
|
||||
CMD_ROUTES = 0x4207
|
||||
CMD_HOST_REQ = 0x4208
|
||||
CMD_HOST_LIST = 0x4209
|
||||
|
||||
cmd_to_name = {
|
||||
CMD_EXIT: 'EXIT',
|
||||
@ -21,6 +24,9 @@ cmd_to_name = {
|
||||
CMD_CLOSE: 'CLOSE',
|
||||
CMD_EOF: 'EOF',
|
||||
CMD_DATA: 'DATA',
|
||||
CMD_ROUTES: 'ROUTES',
|
||||
CMD_HOST_REQ: 'HOST_REQ',
|
||||
CMD_HOST_LIST: 'HOST_LIST',
|
||||
}
|
||||
|
||||
|
||||
@ -83,7 +89,9 @@ class SockWrapper:
|
||||
elif e.args[0] == errno.EISCONN:
|
||||
# connected successfully (BSD)
|
||||
self.connect_to = None
|
||||
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]:
|
||||
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EACCES, errno.EPERM]:
|
||||
# a "normal" kind of error
|
||||
self.connect_to = None
|
||||
self.seterr(e)
|
||||
@ -218,7 +226,8 @@ class Mux(Handler):
|
||||
Handler.__init__(self, [rsock, wsock])
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
self.new_channel = None
|
||||
self.new_channel = self.got_routes = None
|
||||
self.got_host_req = self.got_host_list = None
|
||||
self.channels = {}
|
||||
self.chani = 0
|
||||
self.want = 0
|
||||
@ -257,12 +266,13 @@ class Mux(Handler):
|
||||
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:
|
||||
@ -275,6 +285,21 @@ class Mux(Handler):
|
||||
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('got CMD_ROUTES without got_routes?')
|
||||
elif cmd == CMD_HOST_REQ:
|
||||
if self.got_host_req:
|
||||
self.got_host_req(data)
|
||||
else:
|
||||
raise Exception('got CMD_HOST_REQ without got_host_req?')
|
||||
elif cmd == CMD_HOST_LIST:
|
||||
if self.got_host_list:
|
||||
self.got_host_list(data)
|
||||
else:
|
||||
raise Exception('got CMD_HOST_LIST without got_host_list?')
|
||||
else:
|
||||
callback = self.channels[channel]
|
||||
callback(cmd, data)
|
||||
|
Reference in New Issue
Block a user