You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Planned work, roughly in priority order. Each item is self-contained — read AGENTS.md and the named files before starting one.
Core — extend what the pool is for
Authentication — the API can restart instances and resize the pool and has none, which is
why 8080 has to stay on loopback. Token (and/or basic) auth in internal/server, covering the
whole mux; decide explicitly whether /health and /metrics stay open for probes and scrapers.
The dashboard needs a login screen, and the credential has to reach GET /api/stream too — EventSource cannot set headers, so that means a cookie or a query token.
Standalone local package (docker is best fit. dropping it)
API docs — Currently it lives in the repository only. show it in dashboard page as well.
Additional Tor network configuration — the torrc surface in internal/tor/torrc.go and internal/config covers exits, dirtiness, conflux and pinning. Still missing: entry guards, MiddleNodes, IPv6 exits (ClientUseIPv6), and a DNSPort per pool for clients that cannot do socks5h. Keep every new knob validated in internal/config and documented in .env.example and docs/configuration.md.
Bridges and pluggable transports — obfs4 / snowflake / meek, for hosts where direct Tor is
blocked. Note this is not only config: obfs4proxy and friends are not in the image, so docker/Dockerfile changes too. Bridge lines are secrets-ish — do not log them.
Show instance logs in dashboard — tor's own output is already captured as EventTorLog in internal/stats/events.go. Needs a per-instance filter on the ring, an API
endpoint (or a filter param on /api/events), and a log view in web/src/views. Decide the
retention: the ring is small and shared with the audit log, so tor at debug level will evict
everything else — a separate, larger ring for logs is probably right.
Per-session exit policy (country / relay group) — today TOR_EXIT_NODES is global and
boot-time, so no caller can ask for where it exits. Two viable designs: tag instances into
groups at spawn (POOL_GROUPS=us:2,de:2,any:1) and constrain pick() in internal/pool/picker.go
by a group taken from the session key prefix or an API hint; or SETCONF ExitNodes on one instance
at runtime (see the tor-control skill). Groups are simpler and do not fight the remediation
ladder. Either way pick() must still fall back rather than refuse to route when a group is empty.
Typed failure reasons — handleSessionFailure in internal/server/api.go takes reason as
opaque text and every report weighs the same. Type it (rate_limited | blocked | captcha | transport | other) and let internal/pool/health.go weigh them: a 429 says slow down and the
exit is fine, a captcha says the exit is burnt and should quarantine well before QUARANTINE_FAILURES. Keep a bodyless POST working — it is the documented minimum signal.
Per-target exit reputation — the pool records that an instance failed, never for whom.
Keep (target host, exit IP) burn pairs so a rotation for site X avoids exits already burnt on X
while site Y keeps using them. Fixes the "everything is quarantined" case in docs/operations.md where the instances are not bad, just bad for
one target. Needs the failure report to carry the host, bounded storage (untrusted input, same rule
as session keys), and expiry — a burn is not permanent.
Active exit probing — failures are only learned reactively, so an idle instance's exit_confirmed stays false until real traffic happens to hit it and a dead exit is discovered by
a user's request. Probe idle instances periodically through their own SOCKS port: confirms the exit
IP, gives a latency figure, catches wedged exits early, and catches two instances landing on the
same exit relay (fewer identities than POOL_SIZE claims). Must respect invariant 14 — probing
runs on its own goroutine and never blocks a request path — and probe failures must not be scored
the same as real traffic.
Weighted instance picking — pick() is fewest-sessions only, so a healthy-but-crawling
instance takes an equal share. Factor in latency and recent success rate once probing supplies
them. Keep the random tie-break; an all-equal pool at startup must not funnel every session onto
one instance.
Warm spare instances — every instance serves, so a rotation or wipe-restart shrinks capacity
exactly when it is needed. SPARE_COUNT instances, bootstrapped and holding circuits but never
assigned, make rotation and remediation free. The fleet already resizes at runtime, so this is
mostly accounting in internal/pool plus keeping spares out of pick().
Operational
Runtime config editing — every knob is env-only, so retuning a threshold for an aggressive
target costs a container restart and every bootstrapped circuit with it. PATCH /api/config over
the tunable subset — thresholds, windows, TTL, DRAIN_ON_ROTATE — never ports or pool identity,
plus a settings panel in the dashboard. Land this after authentication.
Persist stats, events and logs — the rings and the audit log are in memory and reset on
every upgrade, so the dashboard's history is only ever as old as the container. docs/architecture.md already notes this is contained behind internal/stats. SQLite or periodic JSON snapshots on the existing volume; session pinnings are
deliberately not included.
Close keep-alives on rotate — invariant 4 calls the pooled-connection bug the most common
integration bug and pushes the fix onto every client. For the HTTP proxy we can just fix it: mark a
session on rotate and inject Connection: close on the next response over its old connections
(internal/proxy/http.go). SOCKS cannot be helped, but this removes a third of the footgun.
Event webhooks — stats.Log already supports subscribers and the SSE stream uses them. A WEBHOOK_URL that POSTs quarantine / all-down / pool-degraded events means ops learn without
polling /metrics. Must be fire-and-forget with a timeout — a slow webhook cannot stall the event
loop.
Per-session rate and byte limits — anyone who can reach the proxy ports can burn our Tor
bandwidth, and MAX_SESSIONS bounds the session table but nothing bounds usage. Per-session
concurrency cap and byte quota, so one greedy session cannot starve the others sharing its
instance.
Planned work, roughly in priority order. Each item is self-contained — read
AGENTS.md and the named files before starting one.
Core — extend what the pool is for
Authentication — the API can restart instances and resize the pool and has none, which is
why
8080has to stay on loopback. Token (and/or basic) auth ininternal/server, covering thewhole mux; decide explicitly whether
/healthand/metricsstay open for probes and scrapers.The dashboard needs a login screen, and the credential has to reach
GET /api/streamtoo —EventSourcecannot set headers, so that means a cookie or a query token.Standalone local package (docker is best fit. dropping it)
API docs — Currently it lives in the repository only. show it in dashboard page as well.
Additional Tor network configuration — the torrc surface in
internal/tor/torrc.goandinternal/configcovers exits, dirtiness, conflux and pinning. Still missing: entry guards,MiddleNodes, IPv6 exits (ClientUseIPv6), and aDNSPortper pool for clients that cannot dosocks5h. Keep every new knob validated ininternal/configand documented in.env.exampleanddocs/configuration.md.Bridges and pluggable transports — obfs4 / snowflake / meek, for hosts where direct Tor is
blocked. Note this is not only config:
obfs4proxyand friends are not in the image, sodocker/Dockerfilechanges too. Bridge lines are secrets-ish — do not log them.Show instance logs in dashboard — tor's own output is already captured as
EventTorLogininternal/stats/events.go. Needs a per-instance filter on the ring, an APIendpoint (or a filter param on
/api/events), and a log view inweb/src/views. Decide theretention: the ring is small and shared with the audit log, so tor at debug level will evict
everything else — a separate, larger ring for logs is probably right.
Per-session exit policy (country / relay group) — today
TOR_EXIT_NODESis global andboot-time, so no caller can ask for where it exits. Two viable designs: tag instances into
groups at spawn (
POOL_GROUPS=us:2,de:2,any:1) and constrainpick()ininternal/pool/picker.goby a group taken from the session key prefix or an API hint; or
SETCONF ExitNodeson one instanceat runtime (see the
tor-controlskill). Groups are simpler and do not fight the remediationladder. Either way
pick()must still fall back rather than refuse to route when a group is empty.Typed failure reasons —
handleSessionFailureininternal/server/api.gotakesreasonasopaque text and every report weighs the same. Type it (
rate_limited|blocked|captcha|transport|other) and letinternal/pool/health.goweigh them: a 429 says slow down and theexit is fine, a captcha says the exit is burnt and should quarantine well before
QUARANTINE_FAILURES. Keep a bodyless POST working — it is the documented minimum signal.Per-target exit reputation — the pool records that an instance failed, never for whom.
Keep
(target host, exit IP)burn pairs so a rotation for site X avoids exits already burnt on Xwhile site Y keeps using them. Fixes the "everything is quarantined" case in
docs/operations.md where the instances are not bad, just bad for
one target. Needs the failure report to carry the host, bounded storage (untrusted input, same rule
as session keys), and expiry — a burn is not permanent.
Active exit probing — failures are only learned reactively, so an idle instance's
exit_confirmedstays false until real traffic happens to hit it and a dead exit is discovered bya user's request. Probe idle instances periodically through their own SOCKS port: confirms the exit
IP, gives a latency figure, catches wedged exits early, and catches two instances landing on the
same exit relay (fewer identities than
POOL_SIZEclaims). Must respect invariant 14 — probingruns on its own goroutine and never blocks a request path — and probe failures must not be scored
the same as real traffic.
Weighted instance picking —
pick()is fewest-sessions only, so a healthy-but-crawlinginstance takes an equal share. Factor in latency and recent success rate once probing supplies
them. Keep the random tie-break; an all-equal pool at startup must not funnel every session onto
one instance.
Warm spare instances — every instance serves, so a rotation or wipe-restart shrinks capacity
exactly when it is needed.
SPARE_COUNTinstances, bootstrapped and holding circuits but neverassigned, make rotation and remediation free. The fleet already resizes at runtime, so this is
mostly accounting in
internal/poolplus keeping spares out ofpick().Operational
Runtime config editing — every knob is env-only, so retuning a threshold for an aggressive
target costs a container restart and every bootstrapped circuit with it.
PATCH /api/configoverthe tunable subset — thresholds, windows, TTL,
DRAIN_ON_ROTATE— never ports or pool identity,plus a settings panel in the dashboard. Land this after authentication.
Persist stats, events and logs — the rings and the audit log are in memory and reset on
every upgrade, so the dashboard's history is only ever as old as the container.
docs/architecture.md already notes this is contained behind
internal/stats. SQLite or periodic JSON snapshots on the existing volume; session pinnings aredeliberately not included.
Close keep-alives on rotate — invariant 4 calls the pooled-connection bug the most common
integration bug and pushes the fix onto every client. For the HTTP proxy we can just fix it: mark a
session on rotate and inject
Connection: closeon the next response over its old connections(
internal/proxy/http.go). SOCKS cannot be helped, but this removes a third of the footgun.Event webhooks —
stats.Logalready supports subscribers and the SSE stream uses them. AWEBHOOK_URLthat POSTs quarantine / all-down / pool-degraded events means ops learn withoutpolling
/metrics. Must be fire-and-forget with a timeout — a slow webhook cannot stall the eventloop.
Per-session rate and byte limits — anyone who can reach the proxy ports can burn our Tor
bandwidth, and
MAX_SESSIONSbounds the session table but nothing bounds usage. Per-sessionconcurrency cap and byte quota, so one greedy session cannot starve the others sharing its
instance.