diff --git a/sshuttle/client.py b/sshuttle/client.py index 08df6787a..53bbc80ff 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -591,7 +591,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, add_cmd_delimiter, remote_shell): + to_nameserver, add_cmd_delimiter, remote_shell, unresolved_subnets): helpers.logprefix = 'c : ' debug1('Starting client with Python version %s' @@ -753,6 +753,39 @@ def onroutes(routestr): # set --auto-nets, we might as well wait for the message first, then # ignore its contents. mux.got_routes = None + if unresolved_subnets: + unresolved_map = {} + for h, c, fp, lp in unresolved_subnets: + unresolved_map.setdefault(h, []).append((h, c, fp, lp)) + + resolved_entries = [] + + def onresolve(hostlist): + for line in hostlist.strip().split(): + if not line: + continue + name, ip = line.split(b',', 1) + name = name.decode('ascii') + ip = ip.decode('ascii') + for h, c, fp, lp in unresolved_map.get(name, []): + fam = socket.AF_INET6 if ':' in ip else socket.AF_INET + max_cidr = 128 if fam == socket.AF_INET6 else 32 + cidr_to_use = max_cidr if c is None else int(c) + resolved_entries.append((fam, ip, cidr_to_use, + int(fp or 0), + int(lp or fp or 0))) + mux.got_host_list = None + + mux.got_host_list = onresolve + mux.send(0, ssnet.CMD_HOST_REQ, + '\n'.join(unresolved_map.keys()).encode('ascii')) + end = time.time() + 10 + while mux.got_host_list and time.time() < end: + ssnet.runonce(handlers, mux) + if latency_control: + mux.check_fullness() + fw.auto_nets.extend(resolved_entries) + serverready() mux.got_routes = onroutes @@ -810,7 +843,8 @@ 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, group, sudo_pythonpath, add_cmd_delimiter, remote_shell, tmark): + user, group, sudo_pythonpath, add_cmd_delimiter, remote_shell, tmark, + unresolved_subnets): if not remotename: raise Fatal("You must use -r/--remote to specify a remote " @@ -1159,7 +1193,8 @@ def feature_status(label, enabled, available): 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, add_cmd_delimiter, remote_shell) + daemon, to_nameserver, add_cmd_delimiter, remote_shell, + unresolved_subnets) finally: try: if daemon: diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py index 94ee58272..e266bc505 100644 --- a/sshuttle/cmdline.py +++ b/sshuttle/cmdline.py @@ -8,7 +8,7 @@ import sshuttle.firewall as firewall import sshuttle.hostwatch as hostwatch import sshuttle.ssyslog as ssyslog -from sshuttle.options import parser, parse_ipport +from sshuttle.options import parser, parse_ipport, configure_dns_flag, get_unresolved_subnets from sshuttle.helpers import family_ip_tuple, log, Fatal from sshuttle.sudoers import sudoers from sshuttle.namespace import enter_namespace @@ -20,6 +20,7 @@ def main(): else: env_args = [] args = [*env_args, *sys.argv[1:]] + configure_dns_flag(args) opt = parser.parse_args(args) @@ -66,6 +67,8 @@ def main(): for item in sublist] excludes = [item for sublist in opt.exclude for item in sublist] + unresolved = get_unresolved_subnets() + if not includes and not opt.auto_nets: parser.error('at least one subnet, subnet file, ' 'or -N expected') @@ -128,7 +131,8 @@ def main(): opt.sudo_pythonpath, opt.add_cmd_delimiter, opt.remote_shell, - opt.tmark) + opt.tmark, + unresolved) if return_code == 0: log('Normal exit code, exiting...') diff --git a/sshuttle/options.py b/sshuttle/options.py index 433abdf64..08b5a8b00 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -3,6 +3,27 @@ import sys from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal +# When --dns is supplied on the command line, hostnames passed as +# subnets might only be resolvable on the remote host. During the +# argument parsing stage we therefore record such hostnames for later +# resolution instead of failing immediately. +DNS_IN_ARGS = False +UNRESOLVED_SUBNETS = [] + + +def configure_dns_flag(args): + """Set DNS_IN_ARGS based on parsed command line args list.""" + global DNS_IN_ARGS + DNS_IN_ARGS = "--dns" in args + + +def get_unresolved_subnets(): + """Return and clear the list of unresolved subnets.""" + global UNRESOLVED_SUBNETS + lst = UNRESOLVED_SUBNETS + UNRESOLVED_SUBNETS = [] + return lst + from sshuttle import __version__ @@ -54,6 +75,9 @@ def parse_subnetport(s): try: addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM) except socket.gaierror: + if DNS_IN_ARGS: + UNRESOLVED_SUBNETS.append((host, cidr, fport, lport)) + return [] raise Fatal('Unable to resolve address: %s' % host) # If the address is a domain with multiple IPs and a mask is also diff --git a/tests/client/test_options.py b/tests/client/test_options.py index 0bb6d79c9..6da122cf6 100644 --- a/tests/client/test_options.py +++ b/tests/client/test_options.py @@ -178,6 +178,14 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo): ]) +@patch('sshuttle.options.socket.getaddrinfo', side_effect=socket.gaierror) +def test_parse_subnetport_unresolved_with_dns(mock_getaddrinfo): + sshuttle.options.DNS_IN_ARGS = True + sshuttle.options.UNRESOLVED_SUBNETS = [] + assert sshuttle.options.parse_subnetport('no.resolve') == [] + assert ('no.resolve', None, None, None) in sshuttle.options.UNRESOLVED_SUBNETS + + def test_parse_namespace(): valid_namespaces = [ 'my_namespace',