def flush_systemd_dns_cache(): # If the user is using systemd-resolve for DNS resolution, it is # possible for the request to go through systemd-resolve before we # see it...and it may use a cached result instead of sending a # request that we can intercept. When sshuttle starts and stops, # this means that we should clear the cache! # # The command to do this was named systemd-resolve, but changed to # resolvectl in systemd 239. # https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816 p = None if helpers.which("resolvectl"): debug2("Flushing systemd's DNS resolver cache: " "resolvectl flush-caches") p = ssubprocess.Popen(["resolvectl", "flush-caches"], stdout=ssubprocess.PIPE, env=helpers.get_env()) elif helpers.which("systemd-resolve"): debug2("Flushing systemd's DNS resolver cache: " "systemd-resolve --flush-caches") p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"], stdout=ssubprocess.PIPE, env=helpers.get_env()) if p: # Wait so flush is finished and process doesn't show up as defunct. rv = p.wait() if rv != 0: log("Received non-zero return code %d when flushing DNS resolver " "cache." % rv)
def list_routes(): if which('ip'): routes = _list_routes(['ip', 'route'], _route_iproute) elif which('netstat'): routes = _list_routes(['netstat', '-rn'], _route_netstat) else: log('WARNING: Neither "ip" nor "netstat" were found on the server. ' '--auto-nets feature will not work.') routes = [] for (family, ip, width) in routes: if not ip.startswith('0.') and not ip.startswith('127.'): yield (family, ip, width)
def get_auto_method(): if which('iptables'): method_name = "nat" elif which('nft'): method_name = "nft" elif which('pfctl'): method_name = "pf" elif which('ipfw'): method_name = "ipfw" else: raise Fatal( "can't find either iptables, nft or pfctl; check your PATH") return get_method(method_name)
def __init__(self, method_name, sudo_pythonpath): self.auto_nets = [] argvbase = ([sys.executable, sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + ['--firewall']) if ssyslog._p: argvbase += ['--syslog'] # Determine how to prefix the command in order to elevate privileges. if platform.platform().startswith('OpenBSD'): elev_prefix = ['doas'] # OpenBSD uses built in `doas` else: elev_prefix = ['sudo', '-p', '[local sudo] Password: '******'/usr/bin/env', '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, # because stupid Linux 'su' requires that stdin be attached to a tty. # Instead, attach a *bidirectional* socket to its stdout, and use # that for talking in both directions. (s1, s2) = socket.socketpair() def setup(): # run in the child process s2.close() if os.getuid() == 0: argv_tries = argv_tries[-1:] # last entry only for argv in argv_tries: try: if argv[0] == 'su': sys.stderr.write('[local su] ') self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) # No env: Talking to `FirewallClient.start`, which has no i18n. break except OSError as e: log('Spawning firewall manager: %r' % argv) raise Fatal(e) self.argv = argv s1.close() self.pfile = s2.makefile('rwb') line = self.pfile.readline() self.check() if line[0:5] != b'READY': raise Fatal('%r expected READY, got %r' % (self.argv, line)) method_name = line[6:-1] self.method = get_method(method_name.decode("ASCII")) self.method.set_firewall(self)
def flush_systemd_dns_cache(): # If the user is using systemd-resolve for DNS resolution, it is # possible for the request to go through systemd-resolve before we # see it...and it may use a cached result instead of sending a # request that we can intercept. When sshuttle starts and stops, # this means that we should clear the cache! # # The command to do this was named systemd-resolve, but changed to # resolvectl in systemd 239. # https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816 if helpers.which("resolvectl"): debug2("Flushing systemd's DNS resolver cache: " "resolvectl flush-caches") ssubprocess.Popen(["resolvectl", "flush-caches"], stdout=ssubprocess.PIPE, env=helpers.get_env()) elif helpers.which("systemd-resolve"): debug2("Flushing systemd's DNS resolver cache: " "systemd-resolve --flush-caches") ssubprocess.Popen(["systemd-resolve", "--flush-caches"], stdout=ssubprocess.PIPE, env=helpers.get_env())
def is_supported(self): if which("ipfw"): return True debug2("ipfw method not supported because 'ipfw' command is " "missing.") return False
def connect(ssh_cmd, rhostport, python, stderr, options): username, password, port, host = parse_hostport(rhostport) if username: rhost = "{}@{}".format(username, host) else: rhost = host z = zlib.compressobj(1) content = get_module_source('sshuttle.assembler') optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items())) optdata = optdata.encode("UTF8") content2 = (empackage(z, 'sshuttle') + empackage(z, 'sshuttle.cmdline_options', optdata) + empackage(z, 'sshuttle.helpers') + empackage(z, 'sshuttle.ssnet') + empackage(z, 'sshuttle.hostwatch') + empackage(z, 'sshuttle.server') + b"\n") pyscript = r""" import sys, os; verbosity=%d; sys.stdin = os.fdopen(0, "rb"); exec(compile(sys.stdin.read(%d), "assembler.py", "exec")) """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) if not rhost: # ignore the --python argument when running locally; we already know # which python version works. argv = [sys.executable, '-c', pyscript] else: if ssh_cmd: sshl = shlex.split(ssh_cmd) else: sshl = ['ssh'] if port is not None: portl = ["-p", str(port)] else: portl = [] if python: pycmd = "'%s' -c '%s'" % (python, pyscript) else: pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s") % (os.devnull, quote(pyscript)) pycmd = ("/bin/sh -c {}".format(quote(pycmd))) if password is not None: os.environ['SSHPASS'] = str(password) argv = (["sshpass", "-e"] + sshl + portl + [rhost, '--', pycmd]) else: argv = (sshl + portl + [rhost, '--', pycmd]) # Our which() function searches for programs in get_path() # directories (which include PATH). This step isn't strictly # necessary if ssh is already in the user's PATH, but it makes the # error message friendlier if the user incorrectly passes in a # custom ssh command that we cannot find. abs_path = which(argv[0]) if abs_path is None: raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path())) argv[0] = abs_path (s1, s2) = socket.socketpair() def setup(): # runs in the child process s2.close() s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1.close() debug2('executing: %r\n' % argv) p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, close_fds=True, stderr=stderr) os.close(s1a) os.close(s1b) s2.sendall(content) s2.sendall(content2) return p, s2
def is_supported(self): if which("nft"): return True debug2("nft method not supported because 'nft' command is missing.") return False
def is_supported(self): if which("iptables"): return True debug2("nat method not supported because 'iptables' command " "is missing.") return False
def is_supported(self): if which("iptables") and which("ip6tables"): return True debug2("tproxy method not supported because 'iptables' " "or 'ip6tables' commands are missing.\n") return False
def connect(ssh_cmd, rhostport, python, stderr, options): username, password, port, host = parse_hostport(rhostport) if username: rhost = "{}@{}".format(username, host) else: rhost = host z = zlib.compressobj(1) content = get_module_source('sshuttle.assembler') optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items())) optdata = optdata.encode("UTF8") content2 = (empackage(z, 'sshuttle') + empackage(z, 'sshuttle.cmdline_options', optdata) + empackage(z, 'sshuttle.helpers') + empackage(z, 'sshuttle.ssnet') + empackage(z, 'sshuttle.hostwatch') + empackage(z, 'sshuttle.server') + b"\n") # If the exec() program calls sys.exit(), it should exit python # and the sys.exit(98) call won't be reached (so we try to only # exit that way in the server). However, if the code that we # exec() simply returns from main, then we will return from # exec(). If the server's python process dies, it should stop # executing and also won't reach sys.exit(98). # # So, we shouldn't reach sys.exit(98) and we certainly shouldn't # reach it immediately after trying to start the server. pyscript = r""" import sys, os; verbosity=%d; sys.stdin = os.fdopen(0, "rb"); exec(compile(sys.stdin.read(%d), "assembler.py", "exec")); sys.exit(98); """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) if not rhost: # ignore the --python argument when running locally; we already know # which python version works. argv = [sys.executable, '-c', pyscript] else: if ssh_cmd: sshl = shlex.split(ssh_cmd) else: sshl = ['ssh'] if port is not None: portl = ["-p", str(port)] else: portl = [] if python: pycmd = "'%s' -c '%s'" % (python, pyscript) else: # By default, we run the following code in a shell. # However, with restricted shells and other unusual # situations, there can be trouble. See the RESTRICTED # SHELL section in "man bash" for more information. The # code makes many assumptions: # # (1) That /bin/sh exists and that we can call it. # Restricted shells often do *not* allow you to run # programs specified with an absolute path like /bin/sh. # Either way, if there is trouble with this, it should # return error code 127. # # (2) python3 or python exists in the PATH and is # executable. If they aren't, then exec won't work (see (4) # below). # # (3) In /bin/sh, that we can redirect stderr in order to # hide the version that "python3 -V" might print (some # restricted shells don't allow redirection, see # RESTRICTED SHELL section in 'man bash'). However, if we # are in a restricted shell, we'd likely have trouble with # assumption (1) above. # # (4) The 'exec' command should work except if we failed # to exec python because it doesn't exist or isn't # executable OR if exec isn't allowed (some restricted # shells don't allow exec). If the exec succeeded, it will # not return and not get to the "exit 97" command. If exec # does return, we exit with code 97. # # Specifying the exact python program to run with --python # avoids many of the issues above. However, if # you have a restricted shell on remote, you may only be # able to run python if it is in your PATH (and you can't # run programs specified with an absolute path). In that # case, sshuttle might not work at all since it is not # possible to run python on the remote machine---even if # it is present. pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s; exit 97") % \ (os.devnull, quote(pyscript)) pycmd = ("/bin/sh -c {}".format(quote(pycmd))) if password is not None: os.environ['SSHPASS'] = str(password) argv = (["sshpass", "-e"] + sshl + portl + [rhost, '--', pycmd]) else: argv = (sshl + portl + [rhost, '--', pycmd]) # Our which() function searches for programs in get_path() # directories (which include PATH). This step isn't strictly # necessary if ssh is already in the user's PATH, but it makes the # error message friendlier if the user incorrectly passes in a # custom ssh command that we cannot find. abs_path = which(argv[0]) if abs_path is None: raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path())) argv[0] = abs_path (s1, s2) = socket.socketpair() def setup(): # runs in the child process s2.close() s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1.close() debug2('executing: %r' % argv) p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, close_fds=True, stderr=stderr) os.close(s1a) os.close(s1b) s2.sendall(content) s2.sendall(content2) return p, s2
def is_supported(self): if which("pfctl"): return True debug2("pf method not supported because 'pfctl' command is missing.") return False