feat: skip pool hops for .onion destinations
Onion addresses require Tor to resolve, so pool proxies after Tor would break connectivity. Detect .onion targets and use the static chain only (Tor), skipping pool selection and retries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -283,3 +283,74 @@ class TestBypassDirectConnect:
|
||||
await _close_server(s)
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
class TestOnionChainOnly:
|
||||
"""Onion target uses static chain only, pool hops skipped."""
|
||||
|
||||
def test_onion_skips_pool(self):
|
||||
async def _run():
|
||||
servers = []
|
||||
try:
|
||||
# mock socks5 acts as the "Tor" hop
|
||||
mock_host, mock_port, mock_srv = await start_mock_socks5()
|
||||
servers.append(mock_srv)
|
||||
|
||||
# fake pool that would add a dead hop if called
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
dead_port = free_port()
|
||||
fake_pool = MagicMock()
|
||||
fake_pool.alive_count = 1
|
||||
fake_pool.get = AsyncMock(
|
||||
return_value=ChainHop(
|
||||
proto="socks5", host="127.0.0.1", port=dead_port,
|
||||
),
|
||||
)
|
||||
|
||||
listener = ListenerConfig(
|
||||
listen_host="127.0.0.1",
|
||||
listen_port=free_port(),
|
||||
chain=[ChainHop(proto="socks5", host=mock_host, port=mock_port)],
|
||||
pool_seq=[["default"]],
|
||||
)
|
||||
s5p_srv = await asyncio.start_server(
|
||||
lambda r, w: _handle_client(
|
||||
r, w, listener, timeout=5.0, retries=1,
|
||||
pool_seq=[[fake_pool]],
|
||||
),
|
||||
listener.listen_host, listener.listen_port,
|
||||
)
|
||||
servers.append(s5p_srv)
|
||||
await s5p_srv.start_serving()
|
||||
|
||||
# connect with .onion target -- mock socks5 will fail to
|
||||
# resolve it, but the key assertion is pool.get NOT called
|
||||
reader, writer = await asyncio.open_connection(
|
||||
listener.listen_host, listener.listen_port,
|
||||
)
|
||||
writer.write(b"\x05\x01\x00")
|
||||
await writer.drain()
|
||||
await reader.readexactly(2)
|
||||
|
||||
atyp, addr_bytes = encode_address("fake.onion")
|
||||
writer.write(
|
||||
struct.pack("!BBB", 0x05, 0x01, 0x00)
|
||||
+ bytes([atyp])
|
||||
+ addr_bytes
|
||||
+ struct.pack("!H", 80)
|
||||
)
|
||||
await writer.drain()
|
||||
await asyncio.wait_for(reader.read(4096), timeout=3.0)
|
||||
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# pool.get must NOT have been called (onion skips pool)
|
||||
fake_pool.get.assert_not_called()
|
||||
|
||||
finally:
|
||||
for s in servers:
|
||||
await _close_server(s)
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
Reference in New Issue
Block a user