Allow listeners to mix named pools in a single chain using pool:name
syntax. Bare "pool" continues to use the listener's default pool.
Replaces pool_hops field with pool_seq list; pool_hops is now a
backward-compatible property. Each hop draws from its own pool and
failure reporting targets the correct source pool.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
New top-level tor_nodes list distributes traffic across multiple Tor
SOCKS proxies. First hop is replaced at connection time by round-robin
selection; health tests also rotate across all nodes. FirstHopPools
are created for each node when pool_size > 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-scale test concurrency to ~10% of proxy count, capped by
test_concurrency config ceiling (default raised from 5 to 25).
Prevents saturating upstream Tor when pool size varies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each listener now tracks chain setup latency independently via a
dict[str, LatencyTracker] on Metrics. The global aggregate stays for
summary output. /status embeds per-listener latency on each listener
entry; /metrics includes a listener_latency map keyed by host:port.
Add RateTracker (rolling deque, events/sec) and LatencyTracker (circular
buffer, p50/p95/p99 in ms) to the Metrics class. Both are recorded in
_handle_client and exposed via summary(), to_dict(), /status, and /metrics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Proxies that pass the TLS handshake are now immediately added to the
alive list instead of waiting for the entire batch to complete. On
cold start with large pools, this means proxies become available
within seconds rather than waiting 30+ minutes.
Cold start with large pools blocked the server for the entire test
duration. Now start() always defers health testing so the server
listens immediately. Proxies become available as tests complete.
Replace _http_check (HTTP GET to httpbin.org) with _tls_check that
performs a TLS handshake through the proxy chain. Multiple targets
(google, cloudflare, amazon) rotated round-robin eliminate the single
point of failure. Lighter, faster, harder to block than HTTP.
- Add test_targets config field (replaces test_url)
- Backward compat: legacy test_url extracts hostname automatically
- Add ssl.create_default_context() and round-robin index to ProxyPool
- Update docs (example.yaml, USAGE.md, CHEATSHEET.md)
TimeoutError.__str__() returns '' in Python, causing truncated log
lines like "source ... failed: ". Fall back to the class name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses Python's built-in tracemalloc module to show top N memory
allocators on exit. Orthogonal to --cprofile; both can run together.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Tor control port section to USAGE.md covering config,
auth modes, rate limiting, and API endpoints. Updates README
feature line and config example, CHEATSHEET with tor snippets,
and marks the feature complete in TASKS.md and ROADMAP.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Start/stop TorController in serve() lifecycle when tor: config
is present. Adds GET /tor (status) and POST /tor/newnym (signal)
endpoints to the control API. Logs control address at startup.
Adds tor: section and api_listen to example config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Async TCP client for the Tor control protocol (port 9051).
Supports password, cookie, and bare authentication. Provides
NEWNYM signaling with client-side 10s rate limiting and
optional periodic timer. Auto-reconnects on disconnect.
Adds TorConfig dataclass and YAML parsing to config module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add API section to README (features, CLI, config), PROJECT (architecture),
USAGE (full endpoint reference with examples), CHEATSHEET (curl one-liners).
Update TASKS and ROADMAP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
29 tests covering request parsing, JSON response format, all GET/POST
handlers with mock context, 404/405 error routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add api_host/api_port to Config dataclass, parse api_listen key in
load_config(), add --api [HOST:]PORT CLI flag. Start/stop API server
in serve() alongside the SOCKS5 listener.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lightweight asyncio HTTP handler for runtime inspection and management.
Endpoints: /status, /metrics, /pool, /pool/alive, /config (GET) and
/reload, /pool/test, /pool/refresh (POST). Raw HTTP/1.1 parsing, JSON
responses, no new dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove source.py from architecture (deleted)
- Add metrics.py to module list
- Update warm start: trusts cached state, instant startup
- Update signal handling: registered before startup
- Add refactoring tasks to TASKS.md
- Remove stale troubleshooting entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move SIGTERM/SIGINT handler registration to the top of serve()
so signals are never ignored during slow startup phases (cold
start health tests, source fetching). Previously, signals sent
before pool.start() returned had no handler, causing podman to
escalate to SIGKILL after the stop timeout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On warm start, trust persisted alive state and serve immediately.
Health tests run in background. Cold start behavior unchanged.
Previously warm start blocked on testing all cached-alive proxies
before binding the listen port, causing multi-minute delays when
the chain or proxies were slow to respond.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete source.py, ProxySourceConfig, and Config.proxy_source.
ProxyPool fully supersedes ProxySource. The YAML backward-compat
conversion in load_config is preserved so old configs still work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the modern asyncio.create_task() in pool.py and server.py.
Replace redundant list comprehension with list() in evict_keys copy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The server is pure asyncio (single-threaded). The threading.Lock
was never contended. Use a float accumulator in _human_bytes to
avoid the int-to-float type: ignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract _http_check() to deduplicate identical HTTP GET + status
parsing between _test_proxy and _test_chain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move VALID_PROTOS to config.py as single source of truth.
Add parse_api_proxies() to eliminate duplicate API response
parsing in pool.py and source.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update USAGE, CHEATSHEET, README, PROJECT, and TASKS for the three
performance improvements: max_connections semaphore, async HTTP
source fetching, and first-hop TCP connection pool.
Add FirstHopPool that maintains a deque of pre-established TCP
connections to chain[0]. Connections idle beyond pool_max_idle are
evicted; a background task refills to pool_size. build_chain() tries
the pool first, falls back to open_connection. Enabled with
pool_size > 0 in config. Only pools the TCP handshake -- SOCKS/HTTP
tunnels are consumed, not returned.
Replace blocking urllib with a minimal async HTTP/1.1 client (http.py)
using asyncio streams. Pool source fetches now run in parallel via
asyncio.gather. Dead proxy reporting uses async POST. Handles
Content-Length, chunked transfer-encoding, and connection-close bodies.
No new dependencies.
Add max_connections config (default 256) with -m/--max-connections CLI
flag. Server wraps on_client in asyncio.Semaphore to prevent fd
exhaustion under load. Value reloads on SIGHUP; active connections
drain normally. Also adds pool_size/pool_max_idle config fields and
first_hop_pool wiring in server.py (used by next commits), and fixes
asyncio.TimeoutError -> TimeoutError lint warnings.
Mount ~/.cache/s5p as /data for pool state and profile output.
Enable cProfile by default, dumping to /data/s5p.prof.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
basicConfig creates a StreamHandler with its own level filter.
Changing only the logger levels left the handler filtering at the
original level, so debug messages were silently dropped after reload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add hot reload section to USAGE with reloadable settings table.
Add dead proxy reporting section with report_url config and payload
format. Update example.yaml, ROADMAP, TASKS, TODO, CHEATSHEET.