diff --git a/sshuttle/client.py b/sshuttle/client.py index 0523fde..20e5e41 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -295,10 +295,10 @@ class FirewallClient: else: # In windows, if client/firewall processes is running as admin user, stdio can be used for communication. - # But if firewall process is run with elevated mode, access to stdio is lost. + # But if firewall process is run with elevated mode, access to stdio is lost. # So we have to use a socketpair (as in unix). - # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows - can_use_stdio = is_admin_user() + # But socket need to be "shared" to child process as it can't be directly set as stdio in Windows + can_use_stdio = is_admin_user() pstdout = ssubprocess.PIPE if can_use_stdio else None pstdin = ssubprocess.PIPE preexec_fn = None @@ -306,25 +306,27 @@ class FirewallClient: penv['PYTHONPATH'] = os.path.dirname(os.path.dirname(__file__)) def get_pfile(): - if can_use_stdio: - import io + if can_use_stdio: self.p.stdin.write(b'STDIO:\n') self.p.stdin.flush() + class RWPair: def __init__(self, r, w): - self.r = r + self.r = r self.w = w self.read = r.read self.readline = r.readline self.write = w.write self.flush = w.flush + def close(self): for f in self.r, self.w: try: f.close() - except: + except Exception: pass return RWPair(self.p.stdout, self.p.stdin) + # import io # return io.BufferedRWPair(self.p.stdout, self.p.stdin, 1) else: import base64 @@ -885,7 +887,11 @@ def main(listenip_v6, listenip_v4, # listenip_v4 contains user specified value or it is set to "auto". if listenip_v4 == "auto": - listenip_v4 = ('127.0.0.1', 0) + if sys.platform == 'win32': + listenip_v4 = ('0.0.0.0', 0) # windivert method won't work with loopback interface + else: + listenip_v4 = ('127.0.0.1', 0) + debug1("Using default IPv4 listen address " + listenip_v4[0]) # listenip_v6 is... # None when IPv6 is disabled. @@ -895,8 +901,11 @@ def main(listenip_v6, listenip_v4, debug1("IPv6 disabled by --disable-ipv6") if listenip_v6 == "auto": if avail.ipv6: - debug1("IPv6 enabled: Using default IPv6 listen address ::1") - listenip_v6 = ('::1', 0) + if sys.platform == 'win32': + listenip_v6 = ('::', 0) # windivert method won't work with loopback interface + else: + listenip_v6 = ('::1', 0) + debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0]) else: debug1("IPv6 disabled since it isn't supported by method " "%s." % fw.method.name) diff --git a/sshuttle/methods/windivert.py b/sshuttle/methods/windivert.py index 72f74e1..da8c6bc 100644 --- a/sshuttle/methods/windivert.py +++ b/sshuttle/methods/windivert.py @@ -4,6 +4,8 @@ import ipaddress import threading from collections import namedtuple import socket +import subprocess +import re from multiprocessing import shared_memory import struct from functools import wraps @@ -270,6 +272,18 @@ class Method(BaseMethod): def __init__(self, name): super().__init__(name) + def _get_local_proxy_listen_addr(self, port, family): + proto = "TCPv6" if family.version == 6 else "TCP" + for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): + try: + _, local_addr, _, state, *_ = re.split(r"\s+", line.strip()) + except ValueError: + continue + port_suffix = ":" + str(port) + if state == "LISTENING" and local_addr.endswith(port_suffix): + return ipaddress.ip_address(local_addr[:-len(port_suffix)].strip("[]")) + raise Fatal("Could not find listening address for {}/{}".format(port, proto)) + def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, tmark): log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") @@ -279,16 +293,20 @@ class Method(BaseMethod): family = IPFamily(family) # using loopback proxy address never worked. + # >>> self.proxy_addr[family] = family.loopback_addr # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) # As a workaround we use another interface ip instead. - # self.proxy_addr[family] = family.loopback_addr + + local_addr = self._get_local_proxy_listen_addr(port, family) for addr in (ipaddress.ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): if addr.is_loopback or addr.version != family.version: continue - self.proxy_addr[family] = str(addr) - break + if local_addr.is_unspecified or local_addr == addr: + debug2("Found non loopback address to connect to proxy: " + str(addr)) + self.proxy_addr[family] = str(addr) + break else: - raise Fatal(f"Could not find a non loopback proxy address for {family.name}") + raise Fatal("Windivert method requires proxy to listen on non loopback address") self.proxy_port = port @@ -423,6 +441,8 @@ class Method(BaseMethod): else: # ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this ip_filters.append(f"ipv6.SrcAddr=={addr}") + if not ip_filters: + raise Fatal("At least ipv4 or ipv6 address is expected") filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}" debug2(f"[INGRESS] {filter=}") with pydivert.WinDivert(filter) as w: diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index 3ffb045..ee5a47e 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -12,7 +12,7 @@ import ipaddress from urllib.parse import urlparse import sshuttle.helpers as helpers -from sshuttle.helpers import debug2, debug3, which, get_path, Fatal +from sshuttle.helpers import debug2, which, get_path, Fatal def get_module_source(name):