2010-10-01 21:06:56 +02:00
|
|
|
import re, errno
|
|
|
|
import compat.ssubprocess as ssubprocess
|
2011-01-01 09:06:04 +01:00
|
|
|
import helpers, ssyslog
|
2010-05-02 03:30:59 +02:00
|
|
|
from helpers import *
|
2010-05-02 03:14:19 +02:00
|
|
|
|
2010-05-02 03:30:59 +02:00
|
|
|
|
2010-05-05 00:24:43 +02:00
|
|
|
def ipt_chain_exists(name):
|
2010-05-02 03:30:59 +02:00
|
|
|
argv = ['iptables', '-t', 'nat', '-nL']
|
2010-10-01 21:06:56 +02:00
|
|
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
2010-05-02 03:30:59 +02:00
|
|
|
for line in p.stdout:
|
|
|
|
if line.startswith('Chain %s ' % name):
|
|
|
|
return True
|
|
|
|
rv = p.wait()
|
|
|
|
if rv:
|
2010-05-03 02:54:10 +02:00
|
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
2010-05-02 03:30:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
def ipt(*args):
|
|
|
|
argv = ['iptables', '-t', 'nat'] + list(args)
|
2010-05-02 08:14:20 +02:00
|
|
|
debug1('>> %s\n' % ' '.join(argv))
|
2010-10-01 21:06:56 +02:00
|
|
|
rv = ssubprocess.call(argv)
|
2010-05-02 03:30:59 +02:00
|
|
|
if rv:
|
2010-05-03 02:54:10 +02:00
|
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
2010-05-02 03:30:59 +02:00
|
|
|
|
|
|
|
|
2010-05-05 00:24:43 +02:00
|
|
|
# We name the chain based on the transproxy port number so that it's possible
|
|
|
|
# to run multiple copies of sshuttle at the same time. Of course, the
|
|
|
|
# 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, subnets):
|
2010-05-02 03:30:59 +02:00
|
|
|
chain = 'sshuttle-%s' % port
|
|
|
|
|
|
|
|
# basic cleanup/setup of chains
|
2010-05-05 00:24:43 +02:00
|
|
|
if ipt_chain_exists(chain):
|
2010-05-02 03:30:59 +02:00
|
|
|
ipt('-D', 'OUTPUT', '-j', chain)
|
2010-05-02 08:00:19 +02:00
|
|
|
ipt('-D', 'PREROUTING', '-j', chain)
|
2010-05-02 03:30:59 +02:00
|
|
|
ipt('-F', chain)
|
|
|
|
ipt('-X', chain)
|
|
|
|
|
|
|
|
if subnets:
|
|
|
|
ipt('-N', chain)
|
|
|
|
ipt('-F', chain)
|
|
|
|
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
2010-05-02 08:00:19 +02:00
|
|
|
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
2010-05-02 03:30:59 +02:00
|
|
|
|
2010-07-15 20:07:01 +02:00
|
|
|
# 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
|
|
|
|
)
|
2010-05-03 01:29:03 +02:00
|
|
|
|
|
|
|
|
2010-05-05 00:24:43 +02:00
|
|
|
def ipfw_rule_exists(n):
|
|
|
|
argv = ['ipfw', 'list']
|
2010-10-01 21:06:56 +02:00
|
|
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
2010-10-01 09:00:45 +02:00
|
|
|
found = False
|
2010-05-05 00:24:43 +02:00
|
|
|
for line in p.stdout:
|
|
|
|
if line.startswith('%05d ' % n):
|
2010-10-01 09:00:45 +02:00
|
|
|
if not ('ipttl 42 setup keep-state' in line
|
|
|
|
or ('skipto %d' % (n+1)) in line
|
|
|
|
or 'check-state' in line):
|
|
|
|
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
2010-05-05 00:24:43 +02:00
|
|
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
2010-10-01 09:00:45 +02:00
|
|
|
found = True
|
2010-05-05 00:24:43 +02:00
|
|
|
rv = p.wait()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
2010-10-01 09:00:45 +02:00
|
|
|
return found
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
|
2010-10-17 04:11:30 +02:00
|
|
|
_oldctls = {}
|
|
|
|
def _fill_oldctls(prefix):
|
|
|
|
argv = ['sysctl', prefix]
|
2010-10-01 21:06:56 +02:00
|
|
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
2010-10-17 04:11:30 +02:00
|
|
|
for line in p.stdout:
|
|
|
|
assert(line[-1] == '\n')
|
|
|
|
(k,v) = line[:-1].split(': ', 1)
|
|
|
|
_oldctls[k] = v
|
2010-05-05 00:24:43 +02:00
|
|
|
rv = p.wait()
|
|
|
|
if rv:
|
|
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
|
|
if not line:
|
|
|
|
raise Fatal('%r returned no data' % (argv,))
|
|
|
|
|
|
|
|
|
|
|
|
def _sysctl_set(name, val):
|
|
|
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
|
|
|
debug1('>> %s\n' % ' '.join(argv))
|
2010-10-01 21:06:56 +02:00
|
|
|
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
|
2010-10-17 04:11:30 +02:00
|
|
|
_changedctls = []
|
2010-05-05 00:24:43 +02:00
|
|
|
def sysctl_set(name, val):
|
2010-10-17 04:11:30 +02:00
|
|
|
PREFIX = 'net.inet.ip'
|
|
|
|
assert(name.startswith(PREFIX + '.'))
|
|
|
|
val = str(val)
|
|
|
|
if not _oldctls:
|
|
|
|
_fill_oldctls(PREFIX)
|
|
|
|
if not (name in _oldctls):
|
|
|
|
debug1('>> No such sysctl: %r\n' % name)
|
|
|
|
return
|
|
|
|
oldval = _oldctls[name]
|
|
|
|
if val != oldval:
|
|
|
|
_changedctls.append(name)
|
2010-05-05 00:24:43 +02:00
|
|
|
return _sysctl_set(name, val)
|
|
|
|
|
|
|
|
|
|
|
|
def ipfw(*args):
|
|
|
|
argv = ['ipfw', '-q'] + list(args)
|
|
|
|
debug1('>> %s\n' % ' '.join(argv))
|
2010-10-01 21:06:56 +02:00
|
|
|
rv = ssubprocess.call(argv)
|
2010-05-05 00:24:43 +02:00
|
|
|
if rv:
|
|
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
|
|
|
|
|
|
|
|
|
|
def do_ipfw(port, subnets):
|
|
|
|
sport = str(port)
|
2010-07-15 20:07:01 +02:00
|
|
|
xsport = str(port+1)
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
# cleanup any existing rules
|
|
|
|
if ipfw_rule_exists(port):
|
2010-10-05 19:29:12 +02:00
|
|
|
ipfw('delete', sport)
|
2010-05-05 00:24:43 +02:00
|
|
|
|
2010-10-17 04:11:30 +02:00
|
|
|
while _changedctls:
|
|
|
|
name = _changedctls.pop()
|
|
|
|
oldval = _oldctls[name]
|
2010-05-05 00:24:43 +02:00
|
|
|
_sysctl_set(name, oldval)
|
|
|
|
|
|
|
|
if subnets:
|
|
|
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
2010-10-01 08:19:18 +02:00
|
|
|
sysctl_set('net.inet.ip.scopedroute', 0)
|
2010-05-08 02:06:26 +02:00
|
|
|
|
2010-10-01 09:00:45 +02:00
|
|
|
ipfw('add', sport, 'check-state', 'ip',
|
|
|
|
'from', 'any', 'to', 'any')
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
# create new subnet entries
|
2010-07-15 20:07:01 +02:00
|
|
|
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),
|
2010-10-01 09:00:45 +02:00
|
|
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
def program_exists(name):
|
|
|
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
|
|
|
for p in paths:
|
|
|
|
fn = '%s/%s' % (p, name)
|
|
|
|
if os.path.exists(fn):
|
|
|
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
|
|
|
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
hostmap = {}
|
|
|
|
def rewrite_etc_hosts(port):
|
|
|
|
HOSTSFILE='/etc/hosts'
|
|
|
|
BAKFILE='%s.sbak' % HOSTSFILE
|
|
|
|
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
|
|
|
|
old_content = ''
|
2010-05-09 17:22:05 +02:00
|
|
|
st = None
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
try:
|
|
|
|
old_content = open(HOSTSFILE).read()
|
2010-05-09 17:22:05 +02:00
|
|
|
st = os.stat(HOSTSFILE)
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
except IOError, e:
|
|
|
|
if e.errno == errno.ENOENT:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
if old_content.strip() and not os.path.exists(BAKFILE):
|
2010-05-09 17:22:05 +02:00
|
|
|
os.link(HOSTSFILE, BAKFILE)
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
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()
|
2010-05-09 17:22:05 +02:00
|
|
|
|
|
|
|
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)
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
os.rename(tmpname, HOSTSFILE)
|
|
|
|
|
|
|
|
|
|
|
|
def restore_etc_hosts(port):
|
|
|
|
global hostmap
|
|
|
|
hostmap = {}
|
|
|
|
rewrite_etc_hosts(port)
|
|
|
|
|
2010-05-05 00:24:43 +02:00
|
|
|
|
|
|
|
# This is some voodoo for setting up the kernel's transparent
|
|
|
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
|
|
|
# otherwise we delete it, then make them from scratch.
|
2010-05-03 01:29:03 +02:00
|
|
|
#
|
2010-05-05 00:24:43 +02:00
|
|
|
# This code is supposed to clean up after itself by deleting its rules on
|
2010-05-03 01:29:03 +02:00
|
|
|
# exit. In case that fails, it's not the end of the world; future runs will
|
2010-05-05 00:24:43 +02:00
|
|
|
# supercede it in the transproxy list, at least, so the leftover rules
|
|
|
|
# are hopefully harmless.
|
2011-01-01 09:06:04 +01:00
|
|
|
def main(port, syslog):
|
2010-05-03 01:29:03 +02:00
|
|
|
assert(port > 0)
|
|
|
|
assert(port <= 65535)
|
|
|
|
|
2010-05-03 02:54:10 +02:00
|
|
|
if os.getuid() != 0:
|
2010-05-05 00:24:43 +02:00
|
|
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
|
|
|
|
|
|
|
if program_exists('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")
|
2010-05-03 02:54:10 +02:00
|
|
|
|
|
|
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
|
|
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
|
|
|
# can read from it.
|
|
|
|
os.dup2(1, 0)
|
|
|
|
|
2011-01-01 09:06:04 +01:00
|
|
|
if syslog:
|
|
|
|
ssyslog.start_syslog()
|
|
|
|
ssyslog.stderr_to_syslog()
|
|
|
|
|
2010-05-05 00:24:43 +02:00
|
|
|
debug1('firewall manager ready.\n')
|
2010-05-03 01:29:03 +02:00
|
|
|
sys.stdout.write('READY\n')
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
|
|
|
# I'll die automatically.
|
|
|
|
os.setsid()
|
|
|
|
|
|
|
|
# we wait until we get some input before creating the rules. That way,
|
|
|
|
# sshuttle can launch us as early as possible (and get sudo password
|
|
|
|
# authentication as early in the startup process as possible).
|
2010-05-03 03:06:31 +02:00
|
|
|
line = sys.stdin.readline(128)
|
|
|
|
if not line:
|
|
|
|
return # parent died; nothing to do
|
2010-05-08 02:02:04 +02:00
|
|
|
|
|
|
|
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:
|
2010-07-15 20:07:01 +02:00
|
|
|
(width,exclude,ip) = line.strip().split(',', 2)
|
2010-05-08 02:02:04 +02:00
|
|
|
except:
|
|
|
|
raise Fatal('firewall: expected route or GO but got %r' % line)
|
2010-07-15 20:07:01 +02:00
|
|
|
subnets.append((int(width), bool(int(exclude)), ip))
|
|
|
|
|
2010-05-03 01:29:03 +02:00
|
|
|
try:
|
2010-05-03 03:06:31 +02:00
|
|
|
if line:
|
2010-05-05 00:24:43 +02:00
|
|
|
debug1('firewall manager: starting transproxy.\n')
|
2010-05-03 03:06:31 +02:00
|
|
|
do_it(port, subnets)
|
|
|
|
sys.stdout.write('STARTED\n')
|
2010-05-03 03:01:30 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
sys.stdout.flush()
|
|
|
|
except IOError:
|
|
|
|
# the parent process died for some reason; he's surely been loud
|
|
|
|
# enough, so no reason to report another error
|
|
|
|
return
|
2010-05-03 02:54:10 +02:00
|
|
|
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
# 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
|
2010-05-03 01:29:03 +02:00
|
|
|
finally:
|
2010-05-03 03:01:30 +02:00
|
|
|
try:
|
2010-05-05 00:24:43 +02:00
|
|
|
debug1('firewall manager: undoing changes.\n')
|
2010-05-03 03:01:30 +02:00
|
|
|
except:
|
|
|
|
pass
|
2010-05-03 01:29:03 +02:00
|
|
|
do_it(port, [])
|
Added new --auto-hosts and --seed-hosts options to the client.
Now if you use --auto-hosts (-H), the client will ask the server to spawn a
hostwatcher to add names. That, in turn, will send names back to the
server, which sends them back to the client, which sends them to the
firewall subprocess, which will write them to /etc/hosts. Whew!
Only the firewall process can write to /etc/hosts, of course, because only
he's running as root.
Since the name discovery process is kind of slow, we cache the names in
~/.sshuttle.hosts on the remote server.
Right now, most of the names are discovered using nmblookup and smbclient,
as well as by reading the existing entries in /etc/hosts. What would really
be nice would be to query active directory or mdns somehow... but I don't
really know how those work, so this is what you get for now :) It's pretty
neat, at least.
2010-05-08 09:03:12 +02:00
|
|
|
restore_etc_hosts(port)
|