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:
@@ -222,6 +222,90 @@ class TestConfig:
|
||||
]
|
||||
|
||||
|
||||
class TestProxyPools:
|
||||
"""Test named proxy_pools config parsing."""
|
||||
|
||||
def test_proxy_pools_from_yaml(self, tmp_path):
|
||||
cfg_file = tmp_path / "test.yaml"
|
||||
cfg_file.write_text(
|
||||
"proxy_pools:\n"
|
||||
" clean:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/proxies/all\n"
|
||||
" mitm: false\n"
|
||||
" refresh: 300\n"
|
||||
" state_file: /data/pool-clean.json\n"
|
||||
" mitm:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/proxies/all\n"
|
||||
" mitm: true\n"
|
||||
" state_file: /data/pool-mitm.json\n"
|
||||
)
|
||||
c = load_config(cfg_file)
|
||||
assert "clean" in c.proxy_pools
|
||||
assert "mitm" in c.proxy_pools
|
||||
assert c.proxy_pools["clean"].sources[0].mitm is False
|
||||
assert c.proxy_pools["mitm"].sources[0].mitm is True
|
||||
assert c.proxy_pools["clean"].state_file == "/data/pool-clean.json"
|
||||
assert c.proxy_pools["mitm"].state_file == "/data/pool-mitm.json"
|
||||
|
||||
def test_mitm_none_when_absent(self, tmp_path):
|
||||
cfg_file = tmp_path / "test.yaml"
|
||||
cfg_file.write_text(
|
||||
"proxy_pool:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/proxies\n"
|
||||
)
|
||||
c = load_config(cfg_file)
|
||||
assert c.proxy_pool is not None
|
||||
assert c.proxy_pool.sources[0].mitm is None
|
||||
|
||||
def test_singular_becomes_default(self, tmp_path):
|
||||
cfg_file = tmp_path / "test.yaml"
|
||||
cfg_file.write_text(
|
||||
"proxy_pool:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/proxies\n"
|
||||
)
|
||||
c = load_config(cfg_file)
|
||||
assert "default" in c.proxy_pools
|
||||
assert c.proxy_pools["default"] is c.proxy_pool
|
||||
|
||||
def test_proxy_pools_wins_over_singular(self, tmp_path):
|
||||
cfg_file = tmp_path / "test.yaml"
|
||||
cfg_file.write_text(
|
||||
"proxy_pools:\n"
|
||||
" default:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/pools-default\n"
|
||||
"proxy_pool:\n"
|
||||
" sources:\n"
|
||||
" - url: http://api:8081/singular\n"
|
||||
)
|
||||
c = load_config(cfg_file)
|
||||
# proxy_pools "default" wins, singular does not overwrite
|
||||
assert c.proxy_pools["default"].sources[0].url == "http://api:8081/pools-default"
|
||||
|
||||
def test_listener_pool_name(self, tmp_path):
|
||||
cfg_file = tmp_path / "test.yaml"
|
||||
cfg_file.write_text(
|
||||
"listeners:\n"
|
||||
" - listen: 0.0.0.0:1080\n"
|
||||
" pool: clean\n"
|
||||
" chain:\n"
|
||||
" - socks5://127.0.0.1:9050\n"
|
||||
" - pool\n"
|
||||
" - listen: 0.0.0.0:1081\n"
|
||||
" chain:\n"
|
||||
" - socks5://127.0.0.1:9050\n"
|
||||
)
|
||||
c = load_config(cfg_file)
|
||||
assert c.listeners[0].pool_name == "clean"
|
||||
assert c.listeners[0].pool_hops == 1
|
||||
assert c.listeners[1].pool_name == ""
|
||||
assert c.listeners[1].pool_hops == 0
|
||||
|
||||
|
||||
class TestTorNodes:
|
||||
"""Test tor_nodes config parsing."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user