summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAvery Pennarun <apenwarr@gmail.com>2010-05-04 18:24:43 -0400
committerAvery Pennarun <apenwarr@gmail.com>2010-05-04 22:06:22 -0400
commit096bbcc57641445f0b3bbf4f463b9e6909bd2601 (patch)
tree9a7df73b2f44613bfbdc6b4f07a79b175a32c63b
parent7bd0efd57bdfd4e809a335af7d1eea184b1d6721 (diff)
Client "almost" works on MacOS and maybe FreeBSD.
Basic forwarding now works on MacOS, assuming you set up ipfw correctly (ha ha). I wasted a few hours today trying to figure this out, and I'm *so very close*, but unfortunately it just didn't work. Think you can figure it out? Related changes: - don't die if iptables is unavailable - BSD uses getsockname() instead of SO_ORIGINAL_DST - non-blocking connect() returns EISCONN once it's connected - you can't setsockopt IP_TTL more than once
-rw-r--r--client.py24
-rw-r--r--iptables.py125
-rw-r--r--ssnet.py6
3 files changed, 125 insertions, 30 deletions
diff --git a/client.py b/client.py
index 88b9c67..aa977c2 100644
--- a/client.py
+++ b/client.py
@@ -4,13 +4,19 @@ from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
def original_dst(sock):
- 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)
+ 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 IPTables:
@@ -105,7 +111,7 @@ def _main(listener, ipt, use_server, remotename):
dstip = original_dst(sock)
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1]))
- if dstip == sock.getsockname():
+ if dstip == listener.getsockname():
debug1("-- ignored: that's my address!\n")
sock.close()
return
@@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets):
if listenip[1]:
ports = [listenip[1]]
else:
- ports = xrange(12300,65536)
+ ports = xrange(12300,9000,-1)
last_e = None
bound = False
debug2('Binding:')
diff --git a/iptables.py b/iptables.py
index 6ad216f..b4bef1f 100644
--- a/iptables.py
+++ b/iptables.py
@@ -3,7 +3,7 @@ import helpers
from helpers import *
-def chain_exists(name):
+def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
@@ -22,11 +22,16 @@ def ipt(*args):
raise Fatal('%r returned %d' % (argv, rv))
-def do_it(port, subnets):
+# 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):
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
- if chain_exists(chain):
+ if ipt_chain_exists(chain):
ipt('-D', 'OUTPUT', '-j', chain)
ipt('-D', 'PREROUTING', '-j', chain)
ipt('-F', chain)
@@ -48,33 +53,113 @@ def do_it(port, subnets):
)
-# This is some iptables voodoo for setting up the Linux kernel's transparent
-# proxying stuff. If subnets is empty, we just delete our sshuttle chain;
-# otherwise we delete it, then make it from scratch.
-#
-# 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 ipfw_rule_exists(n):
+ argv = ['ipfw', 'list']
+ p = subprocess.Popen(argv, stdout = subprocess.PIPE)
+ for line in p.stdout:
+ if line.startswith('%05d ' % n):
+ if line[5:].find('ipttl 42') < 0:
+ raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
+ return True
+ rv = p.wait()
+ if rv:
+ raise Fatal('%r returned %d' % (argv, rv))
+
+
+def sysctl_get(name):
+ argv = ['sysctl', '-n', name]
+ p = subprocess.Popen(argv, stdout = subprocess.PIPE)
+ line = p.stdout.readline()
+ rv = p.wait()
+ if rv:
+ raise Fatal('%r returned %d' % (argv, rv))
+ if not line:
+ raise Fatal('%r returned no data' % (argv,))
+ assert(line[-1] == '\n')
+ return line[:-1]
+
+
+def _sysctl_set(name, val):
+ argv = ['sysctl', '-w', '%s=%s' % (name, val)]
+ debug1('>> %s\n' % ' '.join(argv))
+ rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
+
+
+_oldctls = []
+def sysctl_set(name, val):
+ oldval = sysctl_get(name)
+ if str(val) != str(oldval):
+ _oldctls.append((name, oldval))
+ return _sysctl_set(name, val)
+
+
+def ipfw(*args):
+ argv = ['ipfw', '-q'] + list(args)
+ debug1('>> %s\n' % ' '.join(argv))
+ rv = subprocess.call(argv)
+ if rv:
+ raise Fatal('%r returned %d' % (argv, rv))
+
+
+def do_ipfw(port, subnets):
+ sport = str(port)
+
+ # cleanup any existing rules
+ if ipfw_rule_exists(port):
+ ipfw('del', sport)
+
+ while _oldctls:
+ (name,oldval) = _oldctls.pop()
+ _sysctl_set(name, oldval)
+
+ if subnets:
+ sysctl_set('net.inet.ip.fw.enable', 1)
+ sysctl_set('net.inet.ip.forwarding', 1)
+
+ # 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')
+
+
+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)
+
+
+# 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.
#
-# This code is supposed to clean up after itself by deleting extra chains on
+# This code is supposed to clean up after itself by deleting its rules on
# 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 iptables
-# chains are mostly harmless.
+# supercede it in the transproxy list, at least, so the leftover rules
+# are hopefully harmless.
def main(port, subnets):
assert(port > 0)
assert(port <= 65535)
if os.getuid() != 0:
- raise Fatal('you must be root (or enable su/sudo) to set up iptables')
+ 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")
# 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)
- debug1('iptables manager ready.\n')
+ debug1('firewall manager ready.\n')
sys.stdout.write('READY\n')
sys.stdout.flush()
@@ -89,10 +174,10 @@ def main(port, subnets):
if not line:
return # parent died; nothing to do
if line != 'GO\n':
- raise Fatal('iptables: expected GO but got %r' % line)
+ raise Fatal('firewall: expected GO but got %r' % line)
try:
if line:
- debug1('iptables manager: starting transproxy.\n')
+ debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets)
sys.stdout.write('STARTED\n')
@@ -111,7 +196,7 @@ def main(port, subnets):
finally:
try:
- debug1('iptables manager: undoing changes.\n')
+ debug1('firewall manager: undoing changes.\n')
except:
pass
do_it(port, [])
diff --git a/ssnet.py b/ssnet.py
index d9cfae5..c7d78f9 100644
--- a/ssnet.py
+++ b/ssnet.py
@@ -71,14 +71,17 @@ class SockWrapper:
def try_connect(self):
if not self.connect_to:
return # already connected
- self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.rsock.setblocking(False)
try:
self.rsock.connect(self.connect_to)
+ # connected successfully (Linux)
self.connect_to = None
except socket.error, e:
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet
+ elif e.args[0] == errno.EISCONN:
+ # connected successfully (BSD)
+ self.connect_to = None
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]:
# a "normal" kind of error
self.connect_to = None
@@ -387,6 +390,7 @@ class MuxWrapper(SockWrapper):
def connect_dst(ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket()
+ outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock,
connect_to = (ip,port),
peername = '%s:%d' % (ip,port))