mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-01-23 14:28:40 +01:00
TProxy IPv6 support.
This commit is contained in:
parent
f41c6b62e5
commit
20254bab57
51
client.py
51
client.py
@ -99,41 +99,55 @@ def original_dst(sock):
|
||||
class MultiListener:
|
||||
|
||||
def __init__(self, type=socket.SOCK_STREAM, proto=0):
|
||||
self.v6 = socket.socket(socket.AF_INET6, type, proto)
|
||||
self.v4 = socket.socket(socket.AF_INET, type, proto)
|
||||
|
||||
def setsockopt(self, level, optname, value):
|
||||
if self.v6:
|
||||
self.v6.setsockopt(level, optname, value)
|
||||
if self.v4:
|
||||
self.v4.setsockopt(level, optname, value)
|
||||
|
||||
def add_handler(self, handlers, callback, method, mux):
|
||||
if self.v6:
|
||||
handlers.append(Handler([self.v6], lambda: callback(self.v6, method, mux, handlers)))
|
||||
if self.v4:
|
||||
handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers)))
|
||||
|
||||
def listen(self, backlog):
|
||||
if self.v6:
|
||||
self.v6.listen(backlog)
|
||||
if self.v4:
|
||||
self.v4.listen(backlog)
|
||||
|
||||
def bind(self, address_v4):
|
||||
def bind(self, address_v6, address_v4):
|
||||
if address_v6 and self.v6:
|
||||
self.v6.bind(address_v6)
|
||||
else:
|
||||
self.v6 = None
|
||||
if address_v4 and self.v4:
|
||||
self.v4.bind(address_v4)
|
||||
else:
|
||||
self.v4 = None
|
||||
|
||||
def print_listening(self, what):
|
||||
if self.v6:
|
||||
listenip = self.v6.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
if self.v4:
|
||||
listenip = self.v4.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
|
||||
|
||||
class FirewallClient:
|
||||
def __init__(self, port_v4, subnets_include, subnets_exclude, dnsport_v4, method):
|
||||
def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method):
|
||||
self.auto_nets = []
|
||||
self.subnets_include = subnets_include
|
||||
self.subnets_exclude = subnets_exclude
|
||||
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
|
||||
['-v'] * (helpers.verbose or 0) +
|
||||
['--firewall', str(port_v4),
|
||||
str(dnsport_v4),
|
||||
['--firewall', str(port_v6), str(port_v4),
|
||||
str(dnsport_v6), str(dnsport_v4),
|
||||
method])
|
||||
if ssyslog._p:
|
||||
argvbase += ['--syslog']
|
||||
@ -376,7 +390,7 @@ def _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control,
|
||||
mux.callback()
|
||||
|
||||
|
||||
def main(listenip_v4,
|
||||
def main(listenip_v6, listenip_v4,
|
||||
ssh_cmd, remotename, python, latency_control, dns,
|
||||
method, seed_hosts, auto_nets,
|
||||
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
||||
@ -390,7 +404,7 @@ def main(listenip_v4,
|
||||
return 5
|
||||
debug1('Starting sshuttle proxy.\n')
|
||||
|
||||
if listenip_v4 and listenip_v4[1]:
|
||||
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
||||
# if both ports given, no need to search for a spare port
|
||||
ports = [ 0, ]
|
||||
else:
|
||||
@ -399,6 +413,7 @@ def main(listenip_v4,
|
||||
|
||||
# search for free ports and try to bind
|
||||
last_e = None
|
||||
redirectport_v6 = 0
|
||||
redirectport_v4 = 0
|
||||
bound = False
|
||||
debug2('Binding redirector:')
|
||||
@ -407,6 +422,16 @@ def main(listenip_v4,
|
||||
tcp_listener = MultiListener()
|
||||
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
if listenip_v6 and listenip_v6[1]:
|
||||
lv6 = listenip_v6
|
||||
redirectport_v6 = lv6[1]
|
||||
elif listenip_v6:
|
||||
lv6 = (listenip_v6[0],port)
|
||||
redirectport_v6 = port
|
||||
else:
|
||||
lv6 = None
|
||||
redirectport_v6 = 0
|
||||
|
||||
if listenip_v4 and listenip_v4[1]:
|
||||
lv4 = listenip_v4
|
||||
redirectport_v4 = lv4[1]
|
||||
@ -418,7 +443,7 @@ def main(listenip_v4,
|
||||
redirectport_v4 = 0
|
||||
|
||||
try:
|
||||
tcp_listener.bind(lv4)
|
||||
tcp_listener.bind(lv6, lv4)
|
||||
bound = True
|
||||
break
|
||||
except socket.error, e:
|
||||
@ -443,6 +468,13 @@ def main(listenip_v4,
|
||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||
dns_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
if listenip_v6:
|
||||
lv6 = (listenip_v6[0],port)
|
||||
dnsport_v6 = port
|
||||
else:
|
||||
lv6 = None
|
||||
dnsport_v6 = 0
|
||||
|
||||
if listenip_v4:
|
||||
lv4 = (listenip_v4[0],port)
|
||||
dnsport_v4 = port
|
||||
@ -451,7 +483,7 @@ def main(listenip_v4,
|
||||
dnsport_v4 = 0
|
||||
|
||||
try:
|
||||
dns_listener.bind(lv4)
|
||||
dns_listener.bind(lv6, lv4)
|
||||
bound = True
|
||||
break
|
||||
except socket.error, e:
|
||||
@ -465,10 +497,11 @@ def main(listenip_v4,
|
||||
assert(last_e)
|
||||
raise last_e
|
||||
else:
|
||||
dnsport_v6 = 0
|
||||
dnsport_v4 = 0
|
||||
dns_listener = None
|
||||
|
||||
fw = FirewallClient(redirectport_v4, subnets_include, subnets_exclude, dnsport_v4, method)
|
||||
fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method)
|
||||
|
||||
if fw.method == "tproxy":
|
||||
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
|
32
firewall.py
32
firewall.py
@ -15,7 +15,9 @@ def nonfatal(func, *args):
|
||||
|
||||
|
||||
def ipt_chain_exists(family, table, name):
|
||||
if family == socket.AF_INET:
|
||||
if family == socket.AF_INET6:
|
||||
cmd = 'ip6tables'
|
||||
elif family == socket.AF_INET:
|
||||
cmd = 'iptables'
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"'%family_to_string(family))
|
||||
@ -30,7 +32,9 @@ def ipt_chain_exists(family, table, name):
|
||||
|
||||
|
||||
def _ipt(family, table, *args):
|
||||
if family == socket.AF_INET:
|
||||
if family == socket.AF_INET6:
|
||||
argv = ['ip6tables', '-t', table] + list(args)
|
||||
elif family == socket.AF_INET:
|
||||
argv = ['iptables', '-t', table] + list(args)
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"'%family_to_string(family))
|
||||
@ -110,7 +114,7 @@ def do_iptables_nat(port, dnsport, family, subnets):
|
||||
|
||||
if dnsport:
|
||||
nslist = resolvconf_nameservers()
|
||||
for ip in nslist:
|
||||
for f,ip in filter(lambda i: i[0]==family, nslist):
|
||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/32' % ip,
|
||||
'-p', 'udp',
|
||||
@ -119,7 +123,7 @@ def do_iptables_nat(port, dnsport, family, subnets):
|
||||
|
||||
|
||||
def do_iptables_tproxy(port, dnsport, family, subnets):
|
||||
if family not in [socket.AF_INET]:
|
||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||
raise Exception('Address family "%s" unsupported by tproxy method'%family_to_string(family))
|
||||
|
||||
table = "mangle"
|
||||
@ -367,7 +371,7 @@ def do_ipfw(port, dnsport, family, subnets):
|
||||
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
|
||||
|
||||
nslist = resolvconf_nameservers()
|
||||
for ip in nslist:
|
||||
for f,ip in filter(lambda i: i[0]==family, nslist):
|
||||
# relabel and then catch outgoing DNS requests
|
||||
ipfw('add', sport, 'divert', sport,
|
||||
'log', 'udp',
|
||||
@ -450,9 +454,13 @@ def restore_etc_hosts(port):
|
||||
# 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_v4, dnsport_v4, method, syslog):
|
||||
assert(port_v4 > 0)
|
||||
def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, syslog):
|
||||
assert(port_v6 >= 0)
|
||||
assert(port_v6 <= 65535)
|
||||
assert(port_v4 >= 0)
|
||||
assert(port_v4 <= 65535)
|
||||
assert(dnsport_v6 >= 0)
|
||||
assert(dnsport_v6 <= 65535)
|
||||
assert(dnsport_v4 >= 0)
|
||||
assert(dnsport_v4 <= 65535)
|
||||
|
||||
@ -519,6 +527,12 @@ def main(port_v4, dnsport_v4, method, syslog):
|
||||
if line:
|
||||
debug1('firewall manager: starting transproxy.\n')
|
||||
|
||||
subnets_v6 = filter(lambda i: i[0]==socket.AF_INET6, subnets)
|
||||
if port_v6:
|
||||
do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets_v6)
|
||||
elif len(subnets_v6) > 0:
|
||||
debug1("IPv6 subnets defined but IPv6 disabled\n")
|
||||
|
||||
subnets_v4 = filter(lambda i: i[0]==socket.AF_INET, subnets)
|
||||
if port_v4:
|
||||
do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4)
|
||||
@ -543,7 +557,7 @@ def main(port_v4, dnsport_v4, method, syslog):
|
||||
if line.startswith('HOST '):
|
||||
(name,ip) = line[5:].strip().split(',', 1)
|
||||
hostmap[name] = ip
|
||||
rewrite_etc_hosts(port_v4)
|
||||
rewrite_etc_hosts(port_v6 or port_v4)
|
||||
elif line:
|
||||
raise Fatal('expected EOF, got %r' % line)
|
||||
else:
|
||||
@ -555,4 +569,4 @@ def main(port_v4, dnsport_v4, method, syslog):
|
||||
pass
|
||||
if port_v4:
|
||||
do_it(port_v4, 0, socket.AF_INET, [])
|
||||
restore_etc_hosts(port_v4)
|
||||
restore_etc_hosts(port_v6 or port_v4)
|
||||
|
@ -42,7 +42,10 @@ def resolvconf_nameservers():
|
||||
for line in open('/etc/resolv.conf'):
|
||||
words = line.lower().split()
|
||||
if len(words) >= 2 and words[0] == 'nameserver':
|
||||
l.append(words[1])
|
||||
if ':' in words[1]:
|
||||
l.append((socket.AF_INET6,words[1]))
|
||||
else:
|
||||
l.append((socket.AF_INET,words[1]))
|
||||
return l
|
||||
|
||||
|
||||
@ -55,7 +58,7 @@ def resolvconf_random_nameserver():
|
||||
random.shuffle(l)
|
||||
return l[0]
|
||||
else:
|
||||
return '127.0.0.1'
|
||||
return (socket.AF_INET,'127.0.0.1')
|
||||
|
||||
|
||||
def islocal(ip,family):
|
||||
|
59
main.py
59
main.py
@ -22,12 +22,31 @@ def parse_subnet4(s):
|
||||
return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width)
|
||||
|
||||
|
||||
# 1:2::3/64 or just 1:2::3
|
||||
def parse_subnet6(s):
|
||||
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
||||
(net,width) = m.groups()
|
||||
if width == None:
|
||||
width = 128
|
||||
else:
|
||||
width = int(width)
|
||||
if width > 128:
|
||||
raise Fatal('*/%d is greater than the maximum of 128' % width)
|
||||
return(socket.AF_INET6, net, width)
|
||||
|
||||
|
||||
# list of:
|
||||
# 1.2.3.4/5 or just 1.2.3.4
|
||||
# 1:2::3/64 or just 1:2::3
|
||||
def parse_subnets(subnets_str):
|
||||
subnets = []
|
||||
for s in subnets_str:
|
||||
subnet = parse_subnet4(s)
|
||||
if ':' in s:
|
||||
subnet = parse_subnet6(s)
|
||||
else:
|
||||
subnet = parse_subnet4(s)
|
||||
subnets.append(subnet)
|
||||
return subnets
|
||||
|
||||
@ -50,13 +69,24 @@ def parse_ipport4(s):
|
||||
return ('%d.%d.%d.%d' % (a,b,c,d), port)
|
||||
|
||||
|
||||
# [1:2::3]:456 or [1:2::3] or 456
|
||||
def parse_ipport6(s):
|
||||
s = str(s)
|
||||
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
|
||||
if not m:
|
||||
raise Fatal('%s is not a valid IP:port format' % s)
|
||||
(ip,port) = m.groups()
|
||||
(ip,port) = (ip or '::', int(port or 0))
|
||||
return (ip, port)
|
||||
|
||||
|
||||
optspec = """
|
||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||
sshuttle --server
|
||||
sshuttle --firewall <port> <subnets...>
|
||||
sshuttle --hostwatch
|
||||
--
|
||||
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
|
||||
l,listen= transproxy to this ip address and port number
|
||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||
N,auto-nets automatically determine subnets to route
|
||||
dns capture local DNS requests and forward to the remote DNS server
|
||||
@ -93,10 +123,11 @@ try:
|
||||
server.latency_control = opt.latency_control
|
||||
sys.exit(server.main())
|
||||
elif opt.firewall:
|
||||
if len(extra) != 3:
|
||||
o.fatal('exactly three arguments expected')
|
||||
if len(extra) != 5:
|
||||
o.fatal('exactly five arguments expected')
|
||||
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
|
||||
extra[2], opt.syslog))
|
||||
int(extra[2]), int(extra[3]),
|
||||
extra[4], opt.syslog))
|
||||
elif opt.hostwatch:
|
||||
sys.exit(hostwatch.hw_main(extra))
|
||||
else:
|
||||
@ -124,8 +155,22 @@ try:
|
||||
method = opt.method
|
||||
else:
|
||||
o.fatal("method %s not supported"%opt.method)
|
||||
ipport_v4 = parse_ipport4(opt.listen or '0.0.0.0:0')
|
||||
sys.exit(client.main(ipport_v4,
|
||||
if not opt.listen:
|
||||
if opt.method == "tproxy":
|
||||
ipport_v6 = parse_ipport6('[::1]:0')
|
||||
else:
|
||||
ipport_v6 = None
|
||||
ipport_v4 = parse_ipport4('127.0.0.1:0')
|
||||
else:
|
||||
ipport_v6 = None
|
||||
ipport_v4 = None
|
||||
list = opt.listen.split(",")
|
||||
for ip in list:
|
||||
if '[' in ip and ']' in ip and opt.method == "tproxy":
|
||||
ipport_v6 = parse_ipport6(ip)
|
||||
else:
|
||||
ipport_v4 = parse_ipport4(ip)
|
||||
sys.exit(client.main(ipport_v6, ipport_v4,
|
||||
opt.ssh_cmd,
|
||||
remotename,
|
||||
opt.python,
|
||||
|
@ -108,6 +108,7 @@ class Hostwatch:
|
||||
|
||||
class DnsProxy(Handler):
|
||||
def __init__(self, mux, chan, request):
|
||||
# FIXME! IPv4 specific
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
Handler.__init__(self, [sock])
|
||||
self.timeout = time.time()+30
|
||||
@ -117,6 +118,7 @@ class DnsProxy(Handler):
|
||||
self.peer = None
|
||||
self.request = request
|
||||
self.sock = sock
|
||||
# FIXME! IPv4 specific
|
||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
self.try_send()
|
||||
|
||||
@ -124,7 +126,8 @@ class DnsProxy(Handler):
|
||||
if self.tries >= 3:
|
||||
return
|
||||
self.tries += 1
|
||||
self.peer = resolvconf_random_nameserver()
|
||||
# FIXME! Support IPv6 nameservers
|
||||
self.peer = resolvconf_random_nameserver()[1]
|
||||
self.sock.connect((self.peer, 53))
|
||||
debug2('DNS: sending to %r\n' % self.peer)
|
||||
try:
|
||||
|
Loading…
Reference in New Issue
Block a user