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:
@@ -151,13 +151,17 @@ async def _handle_client(
|
||||
target_host, target_port = await read_socks5_address(client_reader)
|
||||
logger.info("[%s] connect %s:%d", tag, target_host, target_port)
|
||||
|
||||
# -- bypass check --
|
||||
# -- bypass / onion check --
|
||||
bypass = bool(listener.bypass and _bypass_match(listener.bypass, target_host))
|
||||
onion = target_host.endswith(".onion")
|
||||
skip_pool = bypass or onion
|
||||
if bypass:
|
||||
logger.debug("[%s] bypass %s:%d", tag, target_host, target_port)
|
||||
elif onion:
|
||||
logger.debug("[%s] onion %s:%d (chain only)", tag, target_host, target_port)
|
||||
|
||||
# -- build chain (with retry) --
|
||||
attempts = retries if pool_seq and not bypass else 1
|
||||
attempts = retries if pool_seq and not skip_pool else 1
|
||||
last_err: Exception | None = None
|
||||
|
||||
for attempt in range(attempts):
|
||||
@@ -174,7 +178,7 @@ async def _handle_client(
|
||||
fhp = hop_pools.get((node.host, node.port))
|
||||
|
||||
pool_hops: list[tuple[ChainHop, ProxyPool]] = []
|
||||
if pool_seq and not bypass:
|
||||
if pool_seq and not skip_pool:
|
||||
for candidates in pool_seq:
|
||||
weights = [max(pp.alive_count, 1) for pp in candidates]
|
||||
pp = random.choices(candidates, weights=weights)[0]
|
||||
|
||||
@@ -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