util: refactor Tor-detection to be async
- on my PC, with Tor Browser running (socks proxy on port 9150), detect_tor_socks_proxy took ~4.01 seconds - this was because we probed port 9050, 2 sec timeout, 9051, 2 sec timeout, 9150, ~few ms to succeed - instead we now probe all ports concurrently
This commit is contained in:
@@ -177,7 +177,7 @@ def is_valid_host(ph: str):
|
||||
class ProxySettings:
|
||||
MODES = ['socks4', 'socks5']
|
||||
|
||||
probe_thread = None
|
||||
probe_fut = None
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
@@ -255,20 +255,19 @@ class ProxySettings:
|
||||
|
||||
@classmethod
|
||||
def probe_tor(cls, on_finished: Callable[[str | None, int | None], None]):
|
||||
def detect_task(finished: Callable[[str | None, int | None], None]):
|
||||
net_addr = detect_tor_socks_proxy()
|
||||
async def detect_task(finished: Callable[[str | None, int | None], None]):
|
||||
net_addr = await detect_tor_socks_proxy()
|
||||
if net_addr is None:
|
||||
finished('', -1)
|
||||
else:
|
||||
host = net_addr[0]
|
||||
port = net_addr[1]
|
||||
finished(host, port)
|
||||
cls.probe_thread = None
|
||||
cls.probe_fut = None
|
||||
|
||||
if cls.probe_thread: # don't spam threads
|
||||
if cls.probe_fut: # one probe at a time
|
||||
return
|
||||
cls.probe_thread = threading.Thread(target=detect_task, args=[on_finished], daemon=True)
|
||||
cls.probe_thread.start()
|
||||
cls.probe_fut = asyncio.run_coroutine_threadsafe(detect_task(on_finished), util.get_asyncio_loop())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.enabled == other.enabled \
|
||||
@@ -721,9 +720,9 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
util.trigger_callback('proxy_set', self.proxy)
|
||||
|
||||
def _detect_if_proxy_is_tor(self) -> None:
|
||||
def tor_probe_task(p):
|
||||
async def tor_probe_task(p):
|
||||
assert p is not None
|
||||
is_tor = util.is_tor_socks_port(p.host, int(p.port))
|
||||
is_tor = await util.is_tor_socks_port(p.host, int(p.port))
|
||||
if self.proxy == p: # is this the proxy we probed?
|
||||
if self.is_proxy_tor != is_tor:
|
||||
self.logger.info(f'Proxy is {"" if is_tor else "not "}TOR')
|
||||
@@ -732,8 +731,8 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
|
||||
proxy = self.proxy
|
||||
if proxy and proxy.enabled and proxy.mode == 'socks5':
|
||||
t = threading.Thread(target=tor_probe_task, args=(proxy,), daemon=True)
|
||||
t.start()
|
||||
# FIXME GC issues? do we need to store the Future?
|
||||
asyncio.run_coroutine_threadsafe(tor_probe_task(proxy), self.asyncio_loop)
|
||||
|
||||
@log_exceptions
|
||||
async def set_parameters(self, net_params: NetworkParameters):
|
||||
|
||||
@@ -1547,33 +1547,51 @@ class NetworkJobOnDefaultServer(Logger, ABC):
|
||||
return s
|
||||
|
||||
|
||||
def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
|
||||
async def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
|
||||
# Probable ports for Tor to listen at
|
||||
candidates = [
|
||||
("127.0.0.1", 9050),
|
||||
("127.0.0.1", 9051),
|
||||
("127.0.0.1", 9150),
|
||||
]
|
||||
for net_addr in candidates:
|
||||
if is_tor_socks_port(*net_addr):
|
||||
return net_addr
|
||||
return None
|
||||
|
||||
proxy_addr = None
|
||||
async def test_net_addr(net_addr):
|
||||
is_tor = await is_tor_socks_port(*net_addr)
|
||||
# set result, and cancel remaining probes
|
||||
if is_tor:
|
||||
nonlocal proxy_addr
|
||||
proxy_addr = net_addr
|
||||
await group.cancel_remaining()
|
||||
|
||||
async with OldTaskGroup() as group:
|
||||
for net_addr in candidates:
|
||||
await group.spawn(test_net_addr(net_addr))
|
||||
return proxy_addr
|
||||
|
||||
|
||||
def is_tor_socks_port(host: str, port: int) -> bool:
|
||||
@log_exceptions
|
||||
async def is_tor_socks_port(host: str, port: int) -> bool:
|
||||
# mimic "tor-resolve 0.0.0.0".
|
||||
# see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
|
||||
# > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
|
||||
# > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
|
||||
# > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
|
||||
writer = None
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=10) as s:
|
||||
# mimic "tor-resolve 0.0.0.0".
|
||||
# see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
|
||||
# > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
|
||||
# > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
|
||||
# > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
|
||||
s.send(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
|
||||
if s.recv(1024) == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
|
||||
async with async_timeout(10):
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
writer.write(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
|
||||
await writer.drain()
|
||||
data = await reader.read(1024)
|
||||
if data == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
|
||||
return True
|
||||
except socket.error:
|
||||
pass
|
||||
return False
|
||||
return False
|
||||
except (OSError, asyncio.TimeoutError):
|
||||
return False
|
||||
finally:
|
||||
if writer:
|
||||
writer.close()
|
||||
|
||||
|
||||
AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False # used by unit tests
|
||||
|
||||
Reference in New Issue
Block a user