diff --git a/PROJECT.md b/PROJECT.md index 02af7c7..8cbf2af 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -53,3 +53,4 @@ All other functionality uses Python stdlib (`asyncio`, `socket`, `struct`). - **Graceful shutdown** -- SIGTERM/SIGINT handled in the event loop for clean container stops - **Config split** -- tracked example template, gitignored live config with real addresses - **Proxy pool** -- multi-source (API + file), health-tested, persistent, auto-cleaned +- **Weighted selection** -- recently-tested proxies preferred via recency decay weight diff --git a/README.md b/README.md index fe2357c..3ed7912 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ through configurable chains of SOCKS4, SOCKS5, and HTTP CONNECT proxies. - Per-hop authentication (username/password) - DNS leak prevention (domain names forwarded to proxies, never resolved locally) - Tor integration (Tor is just another SOCKS5 hop) -- Managed proxy pool: multiple sources (API + file), health-tested, auto-cleaned +- Managed proxy pool: multiple sources (API + file), health-tested, weighted selection - Connection retry with proxy rotation (configurable attempts) - Connection metrics (logged periodically and on shutdown) - Container-ready (Alpine-based, podman/docker) @@ -97,10 +97,11 @@ Options: ## How Chaining Works ``` -Client -> s5p -> [static chain] -> [random alive proxy from pool] -> Destination +Client -> s5p -> [static chain] -> [weighted alive proxy from pool] -> Destination ``` s5p connects to Hop1 via TCP, negotiates the hop protocol (SOCKS5/4/HTTP), then over that tunnel negotiates with Hop2, and so on. If a proxy pool is -configured, a random health-tested proxy is appended to the chain per-connection. -Each hop only sees its immediate neighbors. +configured, an alive proxy is appended per-connection, weighted toward those +with the most recent successful health test. Each hop only sees its immediate +neighbors. diff --git a/ROADMAP.md b/ROADMAP.md index 26a6a6b..eb4b713 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,6 +11,7 @@ - [x] Graceful SIGTERM/SIGINT shutdown - [x] cProfile support - [x] Dynamic proxy source API integration +- [x] Weighted proxy selection (recency-based) ## v0.2.0 diff --git a/TASKS.md b/TASKS.md index 0f4dc99..a90c57d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -19,6 +19,7 @@ - [x] Connection retry with proxy rotation - [x] Connection metrics (periodic + shutdown logging) - [x] Managed proxy pool (multi-source, health-tested, persistent) +- [x] Weighted proxy selection (prefer recently-tested proxies) ## Next - [ ] Integration tests with mock proxy server diff --git a/TODO.md b/TODO.md index 8ab7013..1c0503f 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ ## Features - SOCKS5 BIND and UDP ASSOCIATE commands -- Chain randomization modes (random, round-robin) +- Chain randomization modes (round-robin, sticky-per-destination) - Per-destination chain rules (bypass chain for local addresses) - Hot config reload on SIGHUP - Systemd socket activation diff --git a/docs/USAGE.md b/docs/USAGE.md index fffaa49..dcf72a6 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -89,7 +89,8 @@ into the container. Edit locally, restart to pick up changes. ## Proxy Pool Managed proxy pool with multiple sources, health testing, and persistence. -Appends a random alive proxy after the static chain on each connection. +Appends an alive proxy after the static chain on each connection, weighted +by recency of last successful health test. ```yaml proxy_pool: @@ -133,6 +134,20 @@ After `max_fails` consecutive failures, a proxy is evicted. Mass-failure guard: if >90% of tests fail in one cycle, eviction is skipped (likely the static chain is broken, not the proxies). +### Selection weight + +Alive proxies are selected with probability proportional to their recency +weight: `1 / (1 + age / 300)`, where `age` is seconds since the last +successful health test. This favors freshly-verified proxies over stale ones: + +| Age | Weight | +|-----|--------| +| 0 s (just tested) | ~1.0 | +| 5 min | ~0.5 | +| 10 min | ~0.33 | +| 30 min | ~0.1 | +| Never tested | 0.01 | + ### Persistence Pool state is saved to `state_file` (default: `~/.cache/s5p/pool.json`) after