mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-03-03 01:32:30 +01:00
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
This commit is contained in:
parent
7bd0efd57b
commit
096bbcc576
24
client.py
24
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:')
|
||||
|
125
iptables.py
125
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.
|
||||
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.
|
||||
#
|
||||
# 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").
|
||||
#
|
||||
# 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, [])
|
||||
|
6
ssnet.py
6
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))
|
||||
|
Loading…
Reference in New Issue
Block a user