Minimal multi-device proxy selector that benchmarks V2Ray/Xray configs from the
user's real network, ranks them per traffic type, and emits a v2rayNG
subscription of the winners. See docs/DESIGN.md and docs/adr/.
Phase 2 (Linux CLI) — working. Collects the Epodonios subscription, drops dead configs via a parallel connectivity funnel, probes survivors against 5 traffic categories, ranks per category, and emits top-3 × 5. No server yet (Phase 3).
Verified run (this box, full 4,753 configs, --workers 8 --connect-timeout 2):
- 4,753 supported configs (SSR dropped per ADR-004)
- connectivity funnel: 57/4,753 connected, ~487s (8 workers is the bottleneck —
raise
--workersto ~32 to cut this to ~150s) - full per-category probe on the 57 candidates
- emitted
out/subscription.txt= 7–9 distinct configs, 3 per category, each namedPS-<n><CATS] <host>(see "Subscription naming") - total wall-clock: ~921s at 8 workers
A smaller 800-config run at --workers 32 finished in ~243s with a 5-config
subscription. Worker fan-out dominates cost at full scale, not per-probe time.
src/proxy_selector/
config.py config + subscription URL (env-overridable)
collector.py fetch + decode Epodonios subscription -> URIs (SSR dropped, ADR-004)
uri_to_xray.py URI -> xray outbound JSON (vmess/vless/trojan/tuic/ss)
xray_runner.py per-config xray launch + connectivity + full per-category probe
categories.py 5 categories + provisional weights (ADR-003)
ranking.py per-category min-max normalize + gates + top-N (ADR-003)
__main__.py Phase 2 parallel funnel orchestrator
tools/xray downloaded xray-core binary
docs/ DESIGN.md + ADRs
uv sync # creates .venv, installs httpx[socks]
# xray binary (download once, or point --xray at it):
curl -sSL -o tools/xray.zip https://github.com/XTLS/Xray-core/releases/download/v26.3.27/Xray-linux-64.zip
unzip -o tools/xray.zip -d tools/ && chmod +x tools/xray# probe N configs (parallel connectivity funnel), full-probe the best candidates,
# emit a subscription with top-3 per category
uv run python -m proxy_selector --limit 800 --workers 32 --budget 40 \
--out out/subscription.txt
# import out/subscription.txt into v2rayNG as a normal subscriptionFlags:
--limit Ncap how many subscription configs to consider (default: all)--workers Nparallel xray worker count (default 8; 32 used in the run above)--budget Nmax candidates to fully probe (default 40)--xray PATHpath to the xray binary (default:tools/xray)--out PATHsubscription output path (default:out/subscription.txt)--connect-timeout STCP/TLS connect timeout per config in seconds; dead configs fail fast (default 2.0)
Env: PROXY_SELECTOR_SUBSCRIPTION_URL overrides the source.
Every emitted config is renamed so it is identifiable in v2rayNG. The display
label (URI fragment #..., and vmess ps inside the base64 payload) is:
PS-<n><CATS] <host>
<n>— 1-based index in the emitted subscription (dedup order)<CATS>— sorted tags of the categories this config won:T=Telegram,X=X,Y=YouTube,D=Developer,g=General<host>— the config's server host (so the same host with different credentials shows twice, clearly)
A config that wins multiple categories keeps all its tags in one name, so
overlaps are visible — e.g. PS-1TXYDg] 140.174.184.5 is one Shadowsocks config
at 140.174.184.5 that won Telegram+X+YouTube+Developer+General. The ] is
URL-encoded as %5D in the URI but v2rayNG decodes it on import.
- System proxy: if
HTTP_PROXY/HTTPS_PROXY/ALL_PROXYare set (e.g. a local v2rayNG/clash on127.0.0.1:10808), the benchmark clients must bypass them — they talk to localhost xray and to the real destinations directly. The code usestrust_env=Falseon all measurement/localhost httpx clients; only the SOCKS measurement client pins its own proxy. Don't "fix" this by removing the bypass. - xray API is gRPC, not HTTP JSON-RPC. The earlier "one persistent xray, swap outbound via the runtime API" design does not work over httpx. The current model launches one short-lived xray per config (DESIGN §6) and fans them across the worker pool — simple and proven.
- Secrets: Developer probes are reachability-only (no API keys sent). Never route authenticated traffic (GitHub tokens, LLM keys) through these anonymous proxies. See ADR-004.
- One xray process per config, parallelized across
--workers(process startup is the dominant cost; fan-out hides it). Batching through one long-running instance would need xray's gRPC API — deferred (DESIGN §6). - Throughput is a 200KB speed sample (ranking only, not a bandwidth benchmark). The sample is capped at a 4s transfer budget so a free proxy that connects but throttles the download can't stall the candidate stage.
- Weights in
ranking.pyare PROVISIONAL (ADR-003) — pending real run data. - SSR is dropped (ADR-004).
- Free-config yield is low and churny; Phase 4's 30-min cache + explore/exploit cycle keeps winners fresh across runs.