diff options
author | Victor Kareh <vkareh@redhat.com> | 2021-01-18 15:28:52 -0500 |
---|---|---|
committer | Brian May <brian@linuxpenguins.xyz> | 2021-03-05 08:53:53 +1100 |
commit | 167a57e739bf62b83dcfb46e7d01a581981ef431 (patch) | |
tree | 9cb75c1c02ac0df67297b408cc4eda2ec130a08b | |
parent | 0e51da519fe5e277fd77ac7860a4f39b5f7e5e78 (diff) |
firewall: Allow overriding the TTL
In instances where a cluster pod in a local VM needs to access a server
that is sshuttle'd from the host, since the packets arriving at the host
already made a hop, their TTL is 63 and so get ignored by sshuttle.
Allowing an override of the firewall TTL rule allows the packets to go
through.
-rw-r--r-- | sshuttle/assembler.py | 2 | ||||
-rw-r--r-- | sshuttle/client.py | 21 | ||||
-rw-r--r-- | sshuttle/cmdline.py | 5 | ||||
-rw-r--r-- | sshuttle/firewall.py | 6 | ||||
-rw-r--r-- | sshuttle/methods/ipfw.py | 8 | ||||
-rw-r--r-- | sshuttle/methods/nat.py | 4 | ||||
-rw-r--r-- | sshuttle/methods/nft.py | 2 | ||||
-rw-r--r-- | sshuttle/methods/pf.py | 2 | ||||
-rw-r--r-- | sshuttle/methods/tproxy.py | 2 | ||||
-rw-r--r-- | sshuttle/options.py | 8 | ||||
-rw-r--r-- | sshuttle/server.py | 15 | ||||
-rw-r--r-- | tests/client/test_firewall.py | 8 | ||||
-rw-r--r-- | tests/client/test_methods_nat.py | 9 | ||||
-rw-r--r-- | tests/client/test_methods_pf.py | 27 | ||||
-rw-r--r-- | tests/client/test_methods_tproxy.py | 6 |
15 files changed, 77 insertions, 48 deletions
diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 011baa2..2d09a50 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -42,4 +42,4 @@ import sshuttle.cmdline_options as options # noqa: E402 from sshuttle.server import main # noqa: E402 main(options.latency_control, options.latency_buffer_size, options.auto_hosts, options.to_nameserver, - options.auto_nets) + options.auto_nets, options.ttl) diff --git a/sshuttle/client.py b/sshuttle/client.py index aa8d568..ae2ea0e 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -187,13 +187,14 @@ class MultiListener: class FirewallClient: - def __init__(self, method_name, sudo_pythonpath): + def __init__(self, method_name, sudo_pythonpath, ttl): self.auto_nets = [] python_path = os.path.dirname(os.path.dirname(__file__)) argvbase = ([sys.executable, sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + - ['--firewall']) + ['--firewall'] + + ['--ttl', ttl]) if ssyslog._p: argvbase += ['--syslog'] @@ -248,7 +249,7 @@ class FirewallClient: def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, - user, tmark): + user, tmark, ttl): self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.nslist = nslist @@ -259,6 +260,7 @@ class FirewallClient: self.udp = udp self.user = user self.tmark = tmark + self.ttl = ttl def check(self): rv = self.p.poll() @@ -442,7 +444,7 @@ def ondns(listener, method, mux, handlers): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, latency_buffer_size, dns_listener, seed_hosts, auto_hosts, auto_nets, daemon, - to_nameserver): + to_nameserver, ttl): helpers.logprefix = 'c : ' debug1('Starting client with Python version %s' @@ -461,7 +463,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, latency_buffer_size=latency_buffer_size, auto_hosts=auto_hosts, to_nameserver=to_nameserver, - auto_nets=auto_nets)) + auto_nets=auto_nets, + ttl=ttl)) except socket.error as e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -655,7 +658,7 @@ def main(listenip_v6, listenip_v4, latency_buffer_size, dns, nslist, method_name, seed_hosts, auto_hosts, auto_nets, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, - user, sudo_pythonpath, tmark): + user, sudo_pythonpath, tmark, ttl): if not remotename: print("WARNING: You must specify -r/--remote to securely route " @@ -671,7 +674,7 @@ def main(listenip_v6, listenip_v4, debug1('Starting sshuttle proxy (version %s).' % __version__) helpers.logprefix = 'c : ' - fw = FirewallClient(method_name, sudo_pythonpath) + fw = FirewallClient(method_name, sudo_pythonpath, ttl) # If --dns is used, store the IP addresses that the client # normally uses for DNS lookups in nslist. The firewall needs to @@ -981,14 +984,14 @@ def main(listenip_v6, listenip_v4, # start the firewall fw.setup(subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, - required.udp, user, tmark) + required.udp, user, tmark, ttl) # start the client process try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, latency_buffer_size, dns_listener, seed_hosts, auto_hosts, auto_nets, - daemon, to_nameserver) + daemon, to_nameserver, ttl) finally: try: if daemon: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index 19d4b8d..d935d9e 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -43,7 +43,7 @@ def main(): if opt.firewall: if opt.subnets or opt.subnets_file: parser.error('exactly zero arguments expected') - return firewall.main(opt.method, opt.syslog) + return firewall.main(opt.method, opt.syslog, opt.ttl) elif opt.hostwatch: return hostwatch.hw_main(opt.subnets, opt.auto_hosts) else: @@ -109,7 +109,8 @@ def main(): opt.pidfile, opt.user, opt.sudo_pythonpath, - opt.tmark) + opt.tmark, + opt.ttl) if return_code == 0: log('Normal exit code, exiting...') diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index ce767b1..db776c3 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -97,7 +97,7 @@ def subnet_weight(s): # 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(method_name, syslog): +def main(method_name, syslog, ttl): helpers.logprefix = 'fw: ' stdin, stdout = setup_daemon() hostmap = {} @@ -218,14 +218,14 @@ def main(method_name, syslog): method.setup_firewall( port_v6, dnsport_v6, nslist_v6, socket.AF_INET6, subnets_v6, udp, - user) + user, ttl) if subnets_v4 or nslist_v4: debug2('setting up IPv4.') method.setup_firewall( port_v4, dnsport_v4, nslist_v4, socket.AF_INET, subnets_v4, udp, - user) + user, ttl) stdout.write('STARTED\n') diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py index 34f71a2..f93bdf4 100644 --- a/sshuttle/methods/ipfw.py +++ b/sshuttle/methods/ipfw.py @@ -189,7 +189,7 @@ class Method(BaseMethod): # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl): # IPv6 not supported if family not in [socket.AF_INET]: raise Exception( @@ -216,7 +216,7 @@ class Method(BaseMethod): ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port, 'tcp', 'from', 'any', 'to', 'table(126)', - 'not', 'ipttl', '63', 'keep-state', 'setup') + 'not', 'ipttl', ttl, 'keep-state', 'setup') ipfw_noexit('table', '124', 'flush') dnscount = 0 @@ -227,11 +227,11 @@ class Method(BaseMethod): ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport, 'udp', 'from', 'any', 'to', 'table(124)', - 'not', 'ipttl', '63') + 'not', 'ipttl', ttl) ipfw('add', '1', 'allow', 'udp', 'from', 'any', 'to', 'any', - 'ipttl', '63') + 'ipttl', ttl) if subnets: # create new subnet entries diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 2245ddd..569593d 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl): # only ipv4 supported with NAT if family != socket.AF_INET: raise Exception( @@ -53,7 +53,7 @@ class Method(BaseMethod): # This TTL hack allows the client and server to run on the # same host. The connections the sshuttle server makes will # have TTL set to 63. - _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '63') + _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '%s' % ttl) # Redirect DNS traffic as requested. This includes routing traffic # to localhost DNS servers through sshuttle. diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py index 20b99b8..775fa51 100644 --- a/sshuttle/methods/nft.py +++ b/sshuttle/methods/nft.py @@ -13,7 +13,7 @@ class Method(BaseMethod): # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl): if udp: raise Exception("UDP not supported by nft") diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index 4119273..1bc67e7 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -444,7 +444,7 @@ class Method(BaseMethod): return sock.getsockname() def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 5c42794..c1cccd5 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -151,7 +151,7 @@ class Method(BaseMethod): udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl): if self.firewall is None: tmark = '1' else: diff --git a/sshuttle/options.py b/sshuttle/options.py index 42e3ffe..377a1fd 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -388,6 +388,14 @@ parser.add_argument( """ ) parser.add_argument( + "--ttl", + default="63", + help=""" + Override the TTL for the connections made by the sshuttle server. + Default is 63. + """ +) +parser.add_argument( "--hostwatch", action="store_true", help=""" diff --git a/sshuttle/server.py b/sshuttle/server.py index 2ce4767..645252b 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -152,7 +152,7 @@ class Hostwatch: class DnsProxy(Handler): - def __init__(self, mux, chan, request, to_nameserver): + def __init__(self, mux, chan, request, to_nameserver, ttl): Handler.__init__(self, []) self.timeout = time.time() + 30 self.mux = mux @@ -162,6 +162,7 @@ class DnsProxy(Handler): self.peers = {} self.to_ns_peer = None self.to_ns_port = None + self.ttl = ttl if to_nameserver is None: self.to_nameserver = None else: @@ -191,7 +192,7 @@ class DnsProxy(Handler): family, sockaddr = self._addrinfo(peer, port) sock = socket.socket(family, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) + sock.setsockopt(socket.SOL_IP, socket.IP_TTL, self.ttl) sock.connect(sockaddr) self.peers[sock] = peer @@ -240,7 +241,7 @@ class DnsProxy(Handler): class UdpProxy(Handler): - def __init__(self, mux, chan, family): + def __init__(self, mux, chan, family, ttl): sock = socket.socket(family, socket.SOCK_DGRAM) Handler.__init__(self, [sock]) self.timeout = time.time() + 30 @@ -248,7 +249,7 @@ class UdpProxy(Handler): self.chan = chan self.sock = sock if family == socket.AF_INET: - self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) + self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl) def send(self, dstip, data): debug2('UDP: sending to %r port %d' % dstip) @@ -272,7 +273,7 @@ class UdpProxy(Handler): def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver, - auto_nets): + auto_nets, ttl): try: helpers.logprefix = ' s: ' debug1('Starting server with Python version %s' @@ -349,7 +350,7 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver, def dns_req(channel, data): debug2('Incoming DNS request channel=%d.' % channel) - h = DnsProxy(mux, channel, data, to_nameserver) + h = DnsProxy(mux, channel, data, to_nameserver, ttl) handlers.append(h) dnshandlers[channel] = h mux.got_dns_req = dns_req @@ -380,7 +381,7 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver, raise Fatal('UDP connection channel %d already open' % channel) else: - h = UdpProxy(mux, channel, family) + h = UdpProxy(mux, channel, family, ttl) handlers.append(h) udphandlers[channel] = h mux.got_udp_open = udp_open diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 4a32904..4a1d744 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -100,7 +100,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): mock_get_method("not_auto").name = "test" mock_get_method.reset_mock() - sshuttle.firewall.main("not_auto", False) + sshuttle.firewall.main("not_auto", False, 63) assert mock_rewrite_etc_hosts.mock_calls == [ call({'1.2.3.3': 'existing'}, 1024), @@ -125,7 +125,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, - None), + None, + 63), call().setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], @@ -133,7 +134,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, - None), + None, + 63), call().restore_firewall(1024, AF_INET6, True, None), call().restore_firewall(1025, AF_INET, True, None), ] diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index e528a6b..dbe19ff 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -100,7 +100,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, - None) + None, + 63) assert str(excinfo.value) \ == 'Address family "AF_INET6" unsupported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] @@ -115,7 +116,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, - None) + None, + 63) assert str(excinfo.value) == 'UDP not supported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_ttl.mock_calls == [] @@ -128,7 +130,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], False, - None) + None, + 63) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'nat', 'sshuttle-1025') ] diff --git a/tests/client/test_methods_pf.py b/tests/client/test_methods_pf.py index c92e9f1..d807b11 100644 --- a/tests/client/test_methods_pf.py +++ b/tests/client/test_methods_pf.py @@ -186,7 +186,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, - None) + None, + 63) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), @@ -225,7 +226,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, - None) + None, + 63) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -238,7 +240,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, - None) + None, + 63) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), @@ -298,7 +301,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, - None) + None, + 63) assert mock_pfctl.mock_calls == [ call('-s all'), @@ -330,7 +334,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, - None) + None, + 63) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -343,7 +348,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, - None) + None, + 63) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), @@ -401,7 +407,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, - None) + None, + 63) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), @@ -437,7 +444,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, - None) + None, + 63) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -450,7 +458,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, - None) + None, + 63) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY), diff --git a/tests/client/test_methods_tproxy.py b/tests/client/test_methods_tproxy.py index 2375bc0..50c77e6 100644 --- a/tests/client/test_methods_tproxy.py +++ b/tests/client/test_methods_tproxy.py @@ -108,7 +108,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], True, - None) + None, + 63) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', 'sshuttle-t-1024'), @@ -212,7 +213,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, - None) + None, + 63) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'), |