feat: multi-listener with configurable proxy chaining

Each listener binds to its own port with an independent chain.
The "pool" keyword in a chain appends a random alive proxy from
the shared pool; multiple pool entries = multiple hops.

  :1080 -> Tor only (0 pool hops)
  :1081 -> Tor + 1 pool proxy
  :1082 -> Tor + 2 pool proxies

Shared resources (ProxyPool, Tor, metrics, semaphore, API) are
reused across listeners. FirstHopPool is shared per unique first
hop. Backward compatible: old listen/chain format still works.
This commit is contained in:
user
2026-02-17 22:03:37 +01:00
parent ba60d087c0
commit 7dc3926f48
11 changed files with 495 additions and 62 deletions

View File

@@ -15,7 +15,7 @@ from s5p.api import (
_parse_request,
_route,
)
from s5p.config import ChainHop, Config, PoolSourceConfig, ProxyPoolConfig
from s5p.config import ChainHop, Config, ListenerConfig, PoolSourceConfig, ProxyPoolConfig
from s5p.metrics import Metrics
# -- request parsing ---------------------------------------------------------
@@ -118,11 +118,26 @@ class TestHandleStatus:
_, body = _handle_status(ctx)
assert body["pool"] == {"alive": 5, "total": 10}
def test_with_chain(self):
config = Config(chain=[ChainHop("socks5", "127.0.0.1", 9050)])
def test_with_listeners(self):
config = Config(
listeners=[
ListenerConfig(
listen_host="0.0.0.0", listen_port=1080,
chain=[ChainHop("socks5", "127.0.0.1", 9050)],
),
ListenerConfig(
listen_host="0.0.0.0", listen_port=1081,
chain=[ChainHop("socks5", "127.0.0.1", 9050)],
pool_hops=1,
),
],
)
ctx = _make_ctx(config=config)
_, body = _handle_status(ctx)
assert body["chain"] == ["socks5://127.0.0.1:9050"]
assert len(body["listeners"]) == 2
assert body["listeners"][0]["chain"] == ["socks5://127.0.0.1:9050"]
assert body["listeners"][0]["pool_hops"] == 0
assert body["listeners"][1]["pool_hops"] == 1
class TestHandleMetrics:
@@ -195,13 +210,18 @@ class TestHandleConfig:
"""Test GET /config handler."""
def test_basic(self):
config = Config(timeout=15.0, retries=5, log_level="debug")
config = Config(
timeout=15.0, retries=5, log_level="debug",
listeners=[ListenerConfig(listen_host="0.0.0.0", listen_port=1080)],
)
ctx = _make_ctx(config=config)
status, body = _handle_config(ctx)
assert status == 200
assert body["timeout"] == 15.0
assert body["retries"] == 5
assert body["log_level"] == "debug"
assert len(body["listeners"]) == 1
assert body["listeners"][0]["listen"] == "0.0.0.0:1080"
def test_with_proxy_pool(self):
pp = ProxyPoolConfig(
@@ -210,11 +230,18 @@ class TestHandleConfig:
test_interval=60.0,
max_fails=5,
)
config = Config(proxy_pool=pp)
config = Config(
proxy_pool=pp,
listeners=[ListenerConfig(
chain=[ChainHop("socks5", "127.0.0.1", 9050)],
pool_hops=1,
)],
)
ctx = _make_ctx(config=config)
_, body = _handle_config(ctx)
assert body["proxy_pool"]["refresh"] == 600.0
assert body["proxy_pool"]["sources"][0]["url"] == "http://api:8081/proxies"
assert body["listeners"][0]["pool_hops"] == 1
# -- routing -----------------------------------------------------------------