feat: named proxy pools with per-listener assignment
Add proxy_pools: top-level config (dict of name -> pool config) so
listeners can draw from different proxy sources. Each pool has
independent sources, health testing, state persistence, and refresh
cycles.
- PoolSourceConfig gains mitm: bool|None for API ?mitm=0/1 filtering
- ListenerConfig gains pool_name for named pool assignment
- ProxyPool gains name param with prefixed log messages and
per-name state file derivation (pool-{name}.json)
- server.py replaces single proxy_pool with proxy_pools dict,
validates listener pool references at startup, per-listener closure
- API /pool merges all pools (with pool field on multi-pool entries),
/status and /config expose per-pool summaries
- Backward compat: singular proxy_pool: registers as "default"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,7 @@ class TestJsonResponse:
|
||||
def _make_ctx(
|
||||
config: Config | None = None,
|
||||
pool: MagicMock | None = None,
|
||||
pools: dict | None = None,
|
||||
tor: MagicMock | None = None,
|
||||
) -> dict:
|
||||
"""Build a mock context dict."""
|
||||
@@ -89,6 +90,7 @@ def _make_ctx(
|
||||
"config": config or Config(),
|
||||
"metrics": Metrics(),
|
||||
"pool": pool,
|
||||
"pools": pools,
|
||||
"hop_pool": None,
|
||||
"tor": tor,
|
||||
}
|
||||
@@ -149,6 +151,25 @@ class TestHandleStatus:
|
||||
assert body["listeners"][1]["latency"] is None
|
||||
|
||||
|
||||
class TestHandleStatusPools:
|
||||
"""Test GET /status with multiple named pools."""
|
||||
|
||||
def test_multi_pool_summary(self):
|
||||
pool_a = MagicMock()
|
||||
pool_a.alive_count = 5
|
||||
pool_a.count = 10
|
||||
pool_a.name = "clean"
|
||||
pool_b = MagicMock()
|
||||
pool_b.alive_count = 3
|
||||
pool_b.count = 8
|
||||
pool_b.name = "mitm"
|
||||
ctx = _make_ctx(pools={"clean": pool_a, "mitm": pool_b})
|
||||
_, body = _handle_status(ctx)
|
||||
assert body["pool"] == {"alive": 8, "total": 18}
|
||||
assert body["pools"]["clean"] == {"alive": 5, "total": 10}
|
||||
assert body["pools"]["mitm"] == {"alive": 3, "total": 8}
|
||||
|
||||
|
||||
class TestHandleMetrics:
|
||||
"""Test GET /metrics handler."""
|
||||
|
||||
@@ -218,6 +239,57 @@ class TestHandlePool:
|
||||
assert "socks5://1.2.3.4:1080" in body["proxies"]
|
||||
|
||||
|
||||
class TestHandlePoolMulti:
|
||||
"""Test GET /pool with multiple named pools."""
|
||||
|
||||
def test_merges_entries(self):
|
||||
pool_a = MagicMock()
|
||||
pool_a.alive_count = 1
|
||||
pool_a.count = 1
|
||||
pool_a.name = "clean"
|
||||
entry_a = MagicMock(
|
||||
alive=True, fails=0, tests=5,
|
||||
last_ok=100.0, last_test=100.0, last_seen=100.0,
|
||||
)
|
||||
pool_a._proxies = {"socks5://1.2.3.4:1080": entry_a}
|
||||
|
||||
pool_b = MagicMock()
|
||||
pool_b.alive_count = 1
|
||||
pool_b.count = 1
|
||||
pool_b.name = "mitm"
|
||||
entry_b = MagicMock(
|
||||
alive=True, fails=0, tests=3,
|
||||
last_ok=90.0, last_test=90.0, last_seen=90.0,
|
||||
)
|
||||
pool_b._proxies = {"socks5://5.6.7.8:1080": entry_b}
|
||||
|
||||
ctx = _make_ctx(pools={"clean": pool_a, "mitm": pool_b})
|
||||
_, body = _handle_pool(ctx)
|
||||
assert body["alive"] == 2
|
||||
assert body["total"] == 2
|
||||
assert len(body["proxies"]) == 2
|
||||
assert body["proxies"]["socks5://1.2.3.4:1080"]["pool"] == "clean"
|
||||
assert body["proxies"]["socks5://5.6.7.8:1080"]["pool"] == "mitm"
|
||||
assert "pools" in body
|
||||
assert body["pools"]["clean"] == {"alive": 1, "total": 1}
|
||||
|
||||
def test_single_pool_no_pool_field(self):
|
||||
"""Single pool: no 'pool' field on entries, no 'pools' summary."""
|
||||
pool = MagicMock()
|
||||
pool.alive_count = 1
|
||||
pool.count = 1
|
||||
pool.name = "default"
|
||||
entry = MagicMock(
|
||||
alive=True, fails=0, tests=5,
|
||||
last_ok=100.0, last_test=100.0, last_seen=100.0,
|
||||
)
|
||||
pool._proxies = {"socks5://1.2.3.4:1080": entry}
|
||||
ctx = _make_ctx(pools={"default": pool})
|
||||
_, body = _handle_pool(ctx)
|
||||
assert "pool" not in body["proxies"]["socks5://1.2.3.4:1080"]
|
||||
assert "pools" not in body
|
||||
|
||||
|
||||
class TestHandleConfig:
|
||||
"""Test GET /config handler."""
|
||||
|
||||
@@ -255,6 +327,34 @@ class TestHandleConfig:
|
||||
assert body["proxy_pool"]["sources"][0]["url"] == "http://api:8081/proxies"
|
||||
assert body["listeners"][0]["pool_hops"] == 1
|
||||
|
||||
def test_with_proxy_pools(self):
|
||||
pp_clean = ProxyPoolConfig(
|
||||
sources=[PoolSourceConfig(url="http://api:8081/proxies/all", mitm=False)],
|
||||
refresh=300.0,
|
||||
test_interval=120.0,
|
||||
max_fails=3,
|
||||
)
|
||||
pp_mitm = ProxyPoolConfig(
|
||||
sources=[PoolSourceConfig(url="http://api:8081/proxies/all", mitm=True)],
|
||||
refresh=300.0,
|
||||
test_interval=120.0,
|
||||
max_fails=3,
|
||||
)
|
||||
config = Config(
|
||||
proxy_pools={"clean": pp_clean, "mitm": pp_mitm},
|
||||
listeners=[ListenerConfig(
|
||||
listen_host="0.0.0.0", listen_port=1080,
|
||||
chain=[ChainHop("socks5", "127.0.0.1", 9050)],
|
||||
pool_hops=2, pool_name="clean",
|
||||
)],
|
||||
)
|
||||
ctx = _make_ctx(config=config)
|
||||
_, body = _handle_config(ctx)
|
||||
assert "proxy_pools" in body
|
||||
assert body["proxy_pools"]["clean"]["sources"][0]["mitm"] is False
|
||||
assert body["proxy_pools"]["mitm"]["sources"][0]["mitm"] is True
|
||||
assert body["listeners"][0]["pool"] == "clean"
|
||||
|
||||
def test_with_tor_nodes(self):
|
||||
config = Config(
|
||||
tor_nodes=[
|
||||
|
||||
Reference in New Issue
Block a user