From a7df12cd68334ca5e5b89adf7fba2e2f3ca9f382 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Thu, 27 May 2021 16:21:42 -0400 Subject: [PATCH 1/5] Fix --tmark option Even when --tmark was used, the iptables code always used '1' for the mark. This patch corrects the problem. Previously, it wasn't clear if the tmark should be supplied in hexadecimal or as an integer. This makes it use hexadecimal, checks that the input is hexadecimal, and updates the associated documentation. This patch also makes --ttl information get passed to the firewall in a way that matches how other information gets passed. The ttl and tmark information are passed next to each other in many places and this patch also makes the order consistent. --- docs/manpage.rst | 5 +++-- docs/tproxy.rst | 3 ++- sshuttle/client.py | 11 +++++----- sshuttle/cmdline.py | 7 ++++++ sshuttle/firewall.py | 10 +++++---- sshuttle/methods/__init__.py | 2 +- sshuttle/methods/ipfw.py | 2 +- sshuttle/methods/nat.py | 2 +- sshuttle/methods/nft.py | 2 +- sshuttle/methods/pf.py | 2 +- sshuttle/methods/tproxy.py | 22 +++++-------------- sshuttle/options.py | 5 +++-- tests/client/test_firewall.py | 6 ++--- tests/client/test_methods_nat.py | 6 ++--- tests/client/test_methods_pf.py | 18 +++++++-------- tests/client/test_methods_tproxy.py | 34 +++++++++++++++-------------- 16 files changed, 71 insertions(+), 66 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 33e3373..38f29ea 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -274,9 +274,10 @@ Options Set the file name for the sudoers.d file to be added. Default is "sshuttle_auto". Only works with --sudoers. -.. option:: -t, --tmark +.. option:: -t , --tmark= - Transproxy optional traffic mark with provided MARK value. + An option used by the tproxy method: Use the specified traffic + mark. The mark must be a hexadecimal value. Defaults to 0x01. .. option:: --version diff --git a/docs/tproxy.rst b/docs/tproxy.rst index 6a9dbc8..a805120 100644 --- a/docs/tproxy.rst +++ b/docs/tproxy.rst @@ -12,7 +12,8 @@ There are some things you need to consider for TPROXY to work: ip -6 route add local default dev lo table 100 ip -6 rule add fwmark {TMARK} lookup 100 - where {TMARK} is the identifier mark passed with -t or --tmark flag (default value is 1). + where {TMARK} is the identifier mark passed with -t or --tmark flag + as a hexadecimal string (default value is '0x01'). - The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6 routes manually. e.g. by adding ``'::/0'`` to the end of the command line. diff --git a/sshuttle/client.py b/sshuttle/client.py index 647fb27..96b1a88 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -205,8 +205,7 @@ class FirewallClient: argvbase = ([sys.executable, sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + - ['--firewall'] + - ['--ttl', str(ttl)]) + ['--firewall']) if ssyslog._p: argvbase += ['--syslog'] @@ -261,7 +260,7 @@ class FirewallClient: def setup(self, subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, - user, tmark, ttl): + user, ttl, tmark): self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.nslist = nslist @@ -311,7 +310,9 @@ class FirewallClient: else: user = b'%d' % self.user - self.pfile.write(b'GO %d %s\n' % (udp, user)) + self.pfile.write(b'GO %d %s %d %s\n' % + (udp, user, self.ttl, + bytes(self.tmark, 'ascii'))) self.pfile.flush() line = self.pfile.readline() @@ -1003,7 +1004,7 @@ 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, ttl) + required.udp, user, ttl, tmark) # start the client process try: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index d935d9e..e4361a7 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -85,6 +85,13 @@ def main(): ipport_v4 = "auto" # parse_ipport6('[::1]:0') ipport_v6 = "auto" if not opt.disable_ipv6 else None + try: + int(opt.tmark, 16) + except ValueError: + parser.error("--tmark must be a hexadecimal value") + opt.tmark = opt.tmark.lower() # make 'x' in 0x lowercase + if not opt.tmark.startswith("0x"): # accept without 0x prefix + opt.tmark = "0x%s" % opt.tmark if opt.syslog: ssyslog.start_syslog() ssyslog.close_stdin() diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 021ca96..031454c 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -223,11 +223,13 @@ def main(method_name, syslog, ttl): raise Fatal('expected GO but got %r' % line) _, _, args = line.partition(" ") - udp, user = args.strip().split(" ", 1) + udp, user, ttl, tmark = args.strip().split(" ", 3) udp = bool(int(udp)) if user == '-': user = None - debug2('Got udp: %r, user: %r' % (udp, user)) + ttl = int(ttl) + debug2('Got udp: %r, user: %r, ttl: %s, tmark: %s' % + (udp, user, ttl, tmark)) subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] @@ -242,14 +244,14 @@ def main(method_name, syslog, ttl): method.setup_firewall( port_v6, dnsport_v6, nslist_v6, socket.AF_INET6, subnets_v6, udp, - user, ttl) + user, ttl, tmark) 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, ttl) + user, ttl, tmark) flush_systemd_dns_cache() stdout.write('STARTED\n') diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 1882c3a..0e4c49d 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -91,7 +91,7 @@ class BaseMethod(object): (key, self.name)) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, - user): + user, ttl, tmark): raise NotImplementedError() def restore_firewall(self, port, family, udp, user): diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py index f93bdf4..bda8968 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, ttl): + user, ttl, tmark): # IPv6 not supported if family not in [socket.AF_INET]: raise Exception( diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 569593d..ac4c56a 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, ttl): + user, ttl, tmark): # only ipv4 supported with NAT if family != socket.AF_INET: raise Exception( diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py index 775fa51..8f54c86 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, ttl): + user, ttl, tmark): if udp: raise Exception("UDP not supported by nft") diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index 1bc67e7..be46be7 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, ttl): + user, ttl, tmark): 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 c1cccd5..eb337fe 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -151,17 +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, ttl): - if self.firewall is None: - tmark = '1' - else: - tmark = self.firewall.tmark - - self.setup_firewall_tproxy(port, dnsport, nslist, family, subnets, udp, - user, tmark) - - def setup_firewall_tproxy(self, port, dnsport, nslist, family, subnets, - udp, user, tmark): + user, ttl, tmark): if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by tproxy method' @@ -192,8 +182,8 @@ class Method(BaseMethod): _ipt('-F', divert_chain) _ipt('-N', tproxy_chain) _ipt('-F', tproxy_chain) - _ipt('-I', 'OUTPUT', tmark, '-j', mark_chain) - _ipt('-I', 'PREROUTING', tmark, '-j', tproxy_chain) + _ipt('-I', 'OUTPUT', '1', '-j', mark_chain) + _ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain) # Don't have packets sent to any of our local IP addresses go # through the tproxy or mark chains. @@ -224,7 +214,7 @@ class Method(BaseMethod): '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53') _ipt('-A', tproxy_chain, '-j', 'TPROXY', - '--tproxy-mark', '0x'+tmark+'/0x'+tmark, + '--tproxy-mark', tmark, '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', str(dnsport)) @@ -249,7 +239,7 @@ class Method(BaseMethod): '-m', 'tcp', *tcp_ports) _ipt('-A', tproxy_chain, '-j', 'TPROXY', - '--tproxy-mark', '0x'+tmark+'/0x'+tmark, + '--tproxy-mark', tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'tcp', *(tcp_ports + ('--on-port', str(port)))) @@ -273,7 +263,7 @@ class Method(BaseMethod): '-m', 'udp', *udp_ports) _ipt('-A', tproxy_chain, '-j', 'TPROXY', - '--tproxy-mark', '0x'+tmark+'/0x'+tmark, + '--tproxy-mark', tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'udp', *(udp_ports + ('--on-port', str(port)))) diff --git a/sshuttle/options.py b/sshuttle/options.py index 290e176..4a50d6b 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -445,8 +445,9 @@ parser.add_argument( parser.add_argument( "-t", "--tmark", metavar="[MARK]", - default="1", + default="0x01", help=""" - transproxy optional traffic mark with provided MARK value + tproxy optional traffic mark with provided MARK value in + hexadecimal (default '0x01') """ ) diff --git a/tests/client/test_firewall.py b/tests/client/test_firewall.py index 4a1d744..31618d9 100644 --- a/tests/client/test_firewall.py +++ b/tests/client/test_firewall.py @@ -15,7 +15,7 @@ NSLIST {inet},1.2.3.33 {inet6},2404:6800:4004:80c::33 PORTS 1024,1025,1026,1027 -GO 1 - +GO 1 - 63 0x01 HOST 1.2.3.3,existing """.format(inet=AF_INET, inet6=AF_INET6)) stdout = Mock() @@ -126,7 +126,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None, - 63), + 63, '0x01'), call().setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], @@ -135,7 +135,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None, - 63), + 63, '0x01'), 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 dbe19ff..e0bf2a9 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -101,7 +101,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None, - 63) + 63, '0x01') assert str(excinfo.value) \ == 'Address family "AF_INET6" unsupported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] @@ -117,7 +117,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None, - 63) + 63, '0x01') assert str(excinfo.value) == 'UDP not supported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_ttl.mock_calls == [] @@ -131,7 +131,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], False, None, - 63) + 63, '0x01') 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 d807b11..83f7537 100644 --- a/tests/client/test_methods_pf.py +++ b/tests/client/test_methods_pf.py @@ -187,7 +187,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, - 63) + 63, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), @@ -227,7 +227,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, - 63) + 63, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -241,7 +241,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, - 63) + 63, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), @@ -302,7 +302,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, - 63) + 63, '0x01') assert mock_pfctl.mock_calls == [ call('-s all'), @@ -335,7 +335,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, - 63) + 63, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -349,7 +349,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, - 63) + 63, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), @@ -408,7 +408,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None, - 63) + 63, '0x01') assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), @@ -445,7 +445,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, - 63) + 63, '0x01') assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] @@ -459,7 +459,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None, - 63) + 63, '0x01') 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 50c77e6..d3db207 100644 --- a/tests/client/test_methods_tproxy.py +++ b/tests/client/test_methods_tproxy.py @@ -109,7 +109,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], True, None, - 63) + 63, '0x01') assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', 'sshuttle-t-1024'), @@ -139,17 +139,17 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK', - '--set-mark', '1'), + '--set-mark', '0x01'), call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', - '--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32', + '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32', '-m', 'udp', '-p', 'udp', '--dport', '53'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', + '--tproxy-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32', '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', @@ -165,17 +165,19 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', - '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', + '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64', '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', + '--tproxy-mark', '0x01', '--dest', + u'2404:6800:4004:80c::/64', '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000', '--on-port', '1024'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', - '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', + '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', + '--tproxy-mark', '0x01', '--dest', + u'2404:6800:4004:80c::/64', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000', '--on-port', '1024') ] @@ -214,7 +216,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None, - 63) + 63, '0x01') assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'), @@ -244,17 +246,17 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', - '-j', 'MARK', '--set-mark', '1'), + '-j', 'MARK', '--set-mark', '0x01'), call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', - '--set-mark', '1', '--dest', u'1.2.3.33/32', + '--set-mark', '0x01', '--dest', u'1.2.3.33/32', '-m', 'udp', '-p', 'udp', '--dport', '53'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32', + '--tproxy-mark', '0x01', '--dest', u'1.2.3.33/32', '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', @@ -269,16 +271,16 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp', '--dport', '80:80'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', - '--set-mark', '1', '--dest', u'1.2.3.0/24', + '--set-mark', '0x01', '--dest', u'1.2.3.0/24', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', + '--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24', '-m', 'tcp', '-p', 'tcp', '--on-port', '1025'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', - '--set-mark', '1', '--dest', u'1.2.3.0/24', + '--set-mark', '0x01', '--dest', u'1.2.3.0/24', '-m', 'udp', '-p', 'udp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', + '--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24', '-m', 'udp', '-p', 'udp', '--on-port', '1025') ] mock_ipt_chain_exists.reset_mock() From 1dbec7252d1fb607c63640b0c89d05ffc6da8597 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Sat, 29 May 2021 20:48:33 -0400 Subject: [PATCH 2/5] Remove dead code in hostwatch.py 11 years ago in commit 384d0e7c1d637c4c36eb3e4d31d538bc9420d987, hostwatch was updated to use netstat to find hosts, and _check_smb()/_check_nmb() were edited to immediately return. This patch removes all of the unused code in these two functions. --- sshuttle/hostwatch.py | 103 ------------------------------------------ 1 file changed, 103 deletions(-) diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py index 683b6a7..7e4d3c5 100644 --- a/sshuttle/hostwatch.py +++ b/sshuttle/hostwatch.py @@ -16,8 +16,6 @@ NETSTAT_POLL_TIME = 30 CACHEFILE = os.path.expanduser('~/.sshuttle.hosts') -_nmb_ok = True -_smb_ok = True hostnames = {} queue = {} try: @@ -141,110 +139,11 @@ def _check_netstat(): check_host(ip) -def _check_smb(hostname): - return - global _smb_ok - if not _smb_ok: - return - debug2(' > smb: %s' % hostname) - argv = ['smbclient', '-U', '%', '-L', hostname] - try: - p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, - env=get_env()) - lines = p.stdout.readlines() - p.wait() - except OSError: - _, e = sys.exc_info()[:2] - log('%r failed: %r' % (argv, e)) - _smb_ok = False - return - - lines.reverse() - - # junk at top - while lines: - line = lines.pop().strip() - if re.match(r'Server\s+', line): - break - - # server list section: - # Server Comment - # ------ ------- - while lines: - line = lines.pop().strip() - if not line or re.match(r'-+\s+-+', line): - continue - if re.match(r'Workgroup\s+Master', line): - break - words = line.split() - hostname = words[0].lower() - debug3('< %s' % hostname) - check_host(hostname) - - # workgroup list section: - # Workgroup Master - # --------- ------ - while lines: - line = lines.pop().strip() - if re.match(r'-+\s+', line): - continue - if not line: - break - words = line.split() - (workgroup, hostname) = (words[0].lower(), words[1].lower()) - debug3('< group(%s) -> %s' % (workgroup, hostname)) - check_host(hostname) - check_workgroup(workgroup) - - if lines: - assert(0) - - -def _check_nmb(hostname, is_workgroup, is_master): - return - global _nmb_ok - if not _nmb_ok: - return - debug2(' > n%d%d: %s' % (is_workgroup, is_master, hostname)) - argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname] - try: - p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, - env=get_env) - lines = p.stdout.readlines() - rv = p.wait() - except OSError: - _, e = sys.exc_info()[:2] - log('%r failed: %r' % (argv, e)) - _nmb_ok = False - return - if rv: - log('%r returned %d' % (argv, rv)) - return - for line in lines: - m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) - if m: - g = m.groups() - (ip, name) = (g[0], g[1].lower()) - debug3('< %s -> %s' % (name, ip)) - if is_workgroup: - _enqueue(_check_smb, ip) - else: - found_host(name, ip) - check_host(name) - - def check_host(hostname): if _is_ip(hostname): _enqueue(_check_revdns, hostname) else: _enqueue(_check_dns, hostname) - _enqueue(_check_smb, hostname) - _enqueue(_check_nmb, hostname, False, False) - - -def check_workgroup(hostname): - _enqueue(_check_nmb, hostname, True, False) - _enqueue(_check_nmb, hostname, True, True) def _enqueue(op, *args): @@ -277,8 +176,6 @@ def hw_main(seed_hosts, auto_hosts): _enqueue(_check_netstat) check_host('localhost') check_host(socket.gethostname()) - check_workgroup('workgroup') - check_workgroup('-') while 1: now = time.time() From 851b26cb13080d0897561240302b84adad4a4071 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Wed, 21 Oct 2020 01:05:21 -0400 Subject: [PATCH 3/5] Update documentation The output in the examples provided in the man page hadn't been updated as sshuttle changed its output over time. The example of testing sshuttle without a remote host was removed. It was the first example previously and it is something that is unlikely users will wish to do. Also: - Update some --help messages. - Manpage: Fix a typo. - Manpage: Mention that host specified with -r can be an ssh alias. - Eliminate variable only used once. --- docs/manpage.rst | 160 ++++++++++++++++++++++++++++---------------- docs/usage.rst | 4 ++ sshuttle/client.py | 5 +- sshuttle/cmdline.py | 2 +- sshuttle/options.py | 5 +- 5 files changed, 114 insertions(+), 62 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 33e3373..255a326 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -10,8 +10,8 @@ Synopsis Description ----------- :program:`sshuttle` allows you to create a VPN connection from your -machine to any remote server that you can connect to via -ssh, as long as that server has python 3.6 or higher. +machine to any remote server that you can connect to via ssh, as long +as that server has a sufficiently new Python installation. To work, you must have root access on the local machine, but you can have a normal account on the server. @@ -31,22 +31,23 @@ Options .. option:: A list of subnets to route over the VPN, in the form - ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a - single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), - 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 - netmask), and 0/0 ('just route everything through the - VPN'). Any of the previous examples are also valid if you append - a port or a port range, so 1.2.3.4:8000 will only tunnel traffic - that has as the destination port 8000 of 1.2.3.4 and - 1.2.3.0/24:8000-9000 will tunnel traffic going to any port between - 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet. - A hostname can be provided instead of an IP address. If the - hostname resolves to multiple IPs, all of the IPs are included. - If a width is provided with a hostname that the width is applied - to all of the hostnames IPs (if they are all either IPv4 or IPv6). - Widths cannot be supplied to hostnames that resolve to both IPv4 - and IPv6. Valid examples are example.com, example.com:8000, - example.com/24, example.com/24:8000 and example.com:8000-9000. + ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a + single IP address) and 1.2.3.4/32 (equivalent to 1.2.3.4), + 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask). + Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match + all IPv6 addresses. Any of the previous examples are also valid if + you append a port or a port range, so 1.2.3.4:8000 will only + tunnel traffic that has as the destination port 8000 of 1.2.3.4 + and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port + between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 + subnet. A hostname can be provided instead of an IP address. If + the hostname resolves to multiple IPs, all of the IPs are + included. If a width is provided with a hostname, the width is + applied to all of the hostnames IPs (if they are all either IPv4 + or IPv6). Widths cannot be supplied to hostnames that resolve to + both IPv4 and IPv6. Valid examples are example.com, + example.com:8000, example.com/24, example.com/24:8000 and + example.com:8000-9000. .. option:: --method @@ -141,7 +142,10 @@ Options The remote hostname and optional username and ssh port number to use for connecting to the remote server. For example, example.com, testuser@example.com, - testuser@example.com:2222, or example.com:2244. + testuser@example.com:2222, or example.com:2244. This + hostname is passed to ssh, so it will recognize any + aliases and settings you may have configured in + ~/.ssh/config. .. option:: -x , --exclude= @@ -305,11 +309,8 @@ Arguments read from a file must be one per line, as shown below:: --option2 value2 -Comments in config file -....................... - -It's possible to add comments in the configuration file. This allows annotating the -various subnets with human-readable descriptions, like:: +The configuration file supports comments for human-readable +annotations. For example:: # company-internal API 8.8.8.8/32 @@ -319,51 +320,96 @@ various subnets with human-readable descriptions, like:: Examples -------- -Test locally by proxying all local connections, without using ssh:: - $ sshuttle -v 0/0 +Use the following command to route all IPv4 TCP traffic through remote +(-r) host example.com (and possibly other traffic too, depending on +the selected --method). The 0/0 subnet, short for 0.0.0.0/0, matches +all IPv4 addresses. The ::/0 subnet, matching all IPv6 addresses could +be added to the example. We also exclude (-x) example.com:22 so that +we can establish ssh connections from our local machine to the remote +host without them being routed through sshuttle. Excluding the remote +host may be necessary on some machines for sshuttle to work properly. +Press Ctrl+C to exit. To also route DNS queries through sshuttle, try +adding --dns. Add or remove -v options to see more or less +information:: - Starting sshuttle proxy. - Listening on ('0.0.0.0', 12300). - [local sudo] Password: - firewall manager ready. - c : connecting to server... - s: available routes: - s: 192.168.42.0/24 - c : connected. - firewall manager: starting transproxy. - c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139. - c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443. - ...etc... + $ sshuttle -r example.com -x example.com:22 0/0 + + Starting sshuttle proxy (version ...). + [local sudo] Password: + fw: Starting firewall with Python version 3.9.5 + fw: ready method name nat. + c : IPv6 disabled since it isn't supported by method nat. + c : Method: nat + c : IPv4: on + c : IPv6: off (not available with nat method) + c : UDP : off (not available with nat method) + c : DNS : off (available) + c : User: off (available) + c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort): + c : (, '0.0.0.0', 0, 0, 0) + c : Subnets to exclude from forwarding: + c : (, '...', 32, 22, 22) + c : (, '127.0.0.1', 32, 0, 0) + c : TCP redirector listening on ('127.0.0.1', 12299). + c : Starting client with Python version 3.9.5 + c : Connecting to server... + user@example.com's password: + s: Starting server with Python version 3.6.8 + s: latency control setting = True + s: auto-nets:False + c : Connected to server. + fw: setting up. + fw: iptables -w -t nat -N sshuttle-12299 + fw: iptables -w -t nat -F sshuttle-12299 + ... + Accept: 192.168.42.121:60554 -> 77.141.99.22:22. ^C - firewall manager: undoing changes. - KeyboardInterrupt c : Keyboard interrupt: exiting. - c : SW#8:192.168.42.121:47523: deleting - c : SW#6:192.168.42.106:50035: deleting + c : SW'unknown':Mux#1: deleting (1 remain) + c : SW#7:192.168.42.121:60554: deleting (0 remain) -Test connection to a remote server, with automatic hostname + +Connect to a remote server, with automatic hostname and subnet guessing:: - $ sshuttle -vNHr example.org - - Starting sshuttle proxy. - Listening on ('0.0.0.0', 12300). - firewall manager ready. - c : connecting to server... + $ sshuttle -vNHr example.com -x example.com:22 + Starting sshuttle proxy (version ...). + [local sudo] Password: + fw: Starting firewall with Python version 3.9.5 + fw: ready method name nat. + c : IPv6 disabled since it isn't supported by method nat. + c : Method: nat + c : IPv4: on + c : IPv6: off (not available with nat method) + c : UDP : off (not available with nat method) + c : DNS : off (available) + c : User: off (available) + c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort): + c : NOTE: Additional subnets to forward may be added below by --auto-nets. + c : Subnets to exclude from forwarding: + c : (, '...', 32, 22, 22) + c : (, '127.0.0.1', 32, 0, 0) + c : TCP redirector listening on ('127.0.0.1', 12300). + c : Starting client with Python version 3.9.5 + c : Connecting to server... + user@example.com's password: + s: Starting server with Python version 3.6.8 + s: latency control setting = True + s: auto-nets:True + c : Connected to server. + c : seed_hosts: [] s: available routes: s: 77.141.99.0/24 - c : connected. - c : seed_hosts: [] - firewall manager: starting transproxy. - hostwatch: Found: testbox1: 1.2.3.4 - hostwatch: Found: mytest2: 5.6.7.8 - hostwatch: Found: domaincontroller: 99.1.2.3 + fw: setting up. + fw: iptables -w -t nat -N sshuttle-12300 + fw: iptables -w -t nat -F sshuttle-12300 + ... c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22. ^C - firewall manager: undoing changes. c : Keyboard interrupt: exiting. - c : SW#6:192.168.42.121:60554: deleting + c : SW'unknown':Mux#1: deleting (1 remain) + c : SW#7:192.168.42.121:60554: deleting (0 remain) Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file:: diff --git a/docs/usage.rst b/docs/usage.rst index 646c968..3fd63a1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -11,6 +11,10 @@ Forward all traffic:: sshuttle -r username@sshserver 0.0.0.0/0 - Use the :option:`sshuttle -r` parameter to specify a remote server. + One some systems, you may also need to use the :option:`sshuttle -x` + parameter to exclude sshserver or sshserver:22 so that your local + machine can communicate directly to sshserver without it being + redirected by sshuttle. - By default sshuttle will automatically choose a method to use. Override with the :option:`sshuttle --method` parameter. diff --git a/sshuttle/client.py b/sshuttle/client.py index 647fb27..c8600d6 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -201,7 +201,7 @@ class FirewallClient: 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] + @@ -224,7 +224,8 @@ class FirewallClient: if sudo_pythonpath: elev_prefix += ['/usr/bin/env', - 'PYTHONPATH=%s' % python_path] + 'PYTHONPATH=%s' % + os.path.dirname(os.path.dirname(__file__))] argv_tries = [elev_prefix + argvbase, argvbase] # we can't use stdin/stdout=subprocess.PIPE here, as we normally would, diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index d935d9e..a0765d6 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -20,7 +20,7 @@ def main(): return 1 if not opt.sudoers_filename: - log('--sudoers-file must be set or omited.') + log('--sudoers-file must be set or omitted.') return 1 sudoers( diff --git a/sshuttle/options.py b/sshuttle/options.py index 290e176..5729012 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -132,6 +132,7 @@ def parse_ipport(s): def parse_list(lst): + """Parse a comma separated string into a list.""" return re.split(r'[\s,]+', lst.strip()) if lst else [] @@ -219,7 +220,7 @@ parser.add_argument( default=[], type=parse_list, help=""" - capture and forward DNS requests made to the following servers + capture and forward DNS requests made to the following servers (comma separated) """ ) parser.add_argument( @@ -280,7 +281,7 @@ parser.add_argument( action="count", default=0, help=""" - increase debug message verbosity + increase debug message verbosity (can be used more than once) """ ) parser.add_argument( From 843a729d649c8e01acf65146ad6112ce18b4f637 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Sat, 29 May 2021 21:58:56 -0400 Subject: [PATCH 4/5] flake8: fix long line --- sshuttle/options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sshuttle/options.py b/sshuttle/options.py index 5729012..b8b93f7 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -220,7 +220,8 @@ parser.add_argument( default=[], type=parse_list, help=""" - capture and forward DNS requests made to the following servers (comma separated) + capture and forward DNS requests made to the following servers + (comma separated) """ ) parser.add_argument( From 8c5ffc9e729aeedf25a6c6c26e62406b0c58e436 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Sun, 30 May 2021 17:03:03 -0400 Subject: [PATCH 5/5] Fix #637: File written by sudoers-add should always begin with /etc/sudoers.d/ The problem and patch was originally submitted aayla-secura. I made a minor improvement to the error message. --- bin/sudoers-add | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/sudoers-add b/bin/sudoers-add index 5bec3d1..e359d46 100755 --- a/bin/sudoers-add +++ b/bin/sudoers-add @@ -50,6 +50,14 @@ if [ "$FILE_NAME" == "" ]; then exit 1 fi +# Verify that the resulting file name begins with /etc/sudoers.d +FILE_NAME="$(realpath "/etc/sudoers.d/$FILE_NAME")" +if [[ "$FILE_NAME" != "/etc/sudoers.d/"* ]] ; then + echo -n "Invalid sudoers filename: Final sudoers file " + echo "location ($FILE_NAME) does not begin with /etc/sudoers.d" + exit 1 +fi + # Make a temp file to hold the sudoers config umask 077 TEMP_FILE=$(mktemp) @@ -62,9 +70,9 @@ visudo_code=$? rm "$TEMP_FILE" if [ $visudo_code -eq 0 ]; then - echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME" - chmod 0440 "/etc/sudoers.d/$FILE_NAME" - echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!" + echo "$CONTENT" > "$FILE_NAME" + chmod 0440 "$FILE_NAME" + echo "The sudoers file $FILE_NAME has been successfully created!" exit 0 else