The torproxy container includes an internal REST API server written in Python. It provides status monitoring and operational control over the local Tor daemon without requiring external consumers to implement the Tor Control Protocol.
- Default Port:
8080(configurable viaTORPROXY_API_PORT) - Default Host Access:
http://127.0.0.1:8080 - Docker Network Access:
http://torproxy:8080 - Content-Type:
application/json - CORS:
Access-Control-Allow-Origin: *is returned on successful read responses (/status,/ip,/health,/metrics, …). It is intentionally not sent on error responses (401,403,4xx/5xx) and never on mutations (/connect,/disconnect,/reconnect).OPTIONSpreflight requests are handled automatically (returning204 No Content); preflight for mutations omits the wildcard header so foreign origins cannot read or trigger state changes. - CSRF protection on mutations: Mutations (
/connect,/disconnect,/reconnect, viaGETorPOST) presented with a browserOriginorRefererheader whose host differs from the requestHostare rejected with403 Forbidden. Requests without these headers (curl, scripts, Docker healthchecks) are unaffected, preserving the legacyGET-mutation contract.POSTis still recommended. - Authentication: None by default (guaranteeing 100% backward compatibility). When configured via
TORPROXY_API_TOKEN(orTORPROXY_API_KEY), clients must authenticate using standard HTTP headers:Authorization: Bearer <token>X-API-Key: <token>Unauthenticated or invalid requests receive401 UnauthorizedwithWWW-Authenticate: Bearer realm="torproxy". WhenTORPROXY_API_ALLOW_ANONYMOUS_HEALTH=1is enabled,/health(and/v1/health) allows unauthenticated probing for orchestrators while protecting all other endpoints. Anonymous health responses are minimal (ISSUE-14): only{"status": "..."}— country, ports and bootstrap details require authentication.
- Correlation: Every response carries
X-Request-ID(echoed if sent by the client, otherwise generated). The server log includes the same ID with all paths redacted. - Method Policy: Both
GETandPOSTmethods are accepted by all endpoints.POSTis recommended for state mutations (/connect,/disconnect,/reconnect), butGETis permanently preserved for backward compatibility. Mutations carrying a foreign browserOrigin/Refererare rejected with403 Forbidden(CSRF protection); mutations without those headers (curl, scripts) keep working unchanged.
Returns the current connection state and verified egress IP.
{
"status": "connected",
"ip": "185.220.101.5"
}{
"status": "connecting",
"ip": null,
"detail": "Tor bootstrapping: Handshake"
}{
"status": "disconnected",
"ip": null
}{
"error": "Failed to query Tor: <message>"
}Returns the current public Tor egress IP address, or 503 Service Unavailable if the proxy is not ready.
{
"ip": "185.220.101.5"
}{
"error": "Could not retrieve IP. Proxy may be offline or connecting.",
"detail": "<error details>"
}Enables the Tor network connection (SETCONF DisableNetwork=0).
Parameters can be passed via query string (e.g. ?country=DE) or JSON body for POST:
country(stringorarray[string]): Comma-separated or array of 2-letter ISO 3166-1 alpha-2 country codes (e.g."DE","DE,AT",["DE", "AT"]). Pass""or"any"to remove country restriction.strict_nodes(booleanorinteger):1/trueor0/false. If true, circuits are refused if no exit nodes in the requested country are available.
{
"action": "connect",
"status": "success",
"exit_country": "DE",
"strict_nodes": true
}{
"error": "Invalid ISO country code: 'XYZ'. Must be 2-letter alpha code."
}{
"error": "Failed to enable Tor network: <message>"
}Disables the Tor network connection (SETCONF DisableNetwork=1) and clears the cached egress IP.
{
"action": "disconnect",
"status": "success"
}{
"error": "Failed to disable Tor network: <message>"
}Requests a new Tor circuit (SIGNAL NEWNYM) and invalidates the cached egress IP.
Parameters can be passed via query string (e.g. ?country=DE) or JSON body for POST:
country(stringorarray[string]): Comma-separated or array of 2-letter ISO 3166-1 alpha-2 country codes (e.g."DE","DE,AT",["DE", "AT"]). Pass""or"any"to remove country restriction.strict_nodes(booleanorinteger):1/trueor0/false.
{
"action": "reconnect",
"status": "success",
"exit_country": "DE",
"strict_nodes": true
}{
"error": "Invalid ISO country code: 'XYZ'. Must be 2-letter alpha code."
}{
"error": "Failed to trigger reconnect: <message>"
}Additive diagnostic endpoint providing detailed readiness and component states without modifying existing /status consumers.
{
"status": "healthy",
"tor": {
"network_enabled": true,
"bootstrapped": true,
"progress": 100,
"summary": "Done",
"exit_country": "DE",
"strict_nodes": true
},
"proxies": {
"socks5": true
},
"egress": {
"verified": true,
"ip": "185.220.101.5",
"error": null,
"stale": false
}
}- Time-to-Live (TTL): 30 seconds by default, tunable via
TORPROXY_CACHE_TTL. - Single-flight (ISSUE-10): concurrent
/ip//statuscalls share one upstream probe instead of stampeding the check service. - Stale signal:
/healthincludesegress.stale; when Tor is bootstrapped but egress verification fails,/healthreportsdegradedwith the reason.
Returns structured runtime diagnostics, process uptime, Tor circuit rebuild state, proxy readiness, and egress verification. Strictly omits any sensitive user data, client IPs, or Tor secrets.
{
"uptime_seconds": 142.5,
"started_at": "2026-10-18T15:00:00Z",
"last_reconnect_at": "2026-10-18T15:05:00Z",
"reconnect_count": 1,
"tor": {
"network_enabled": true,
"bootstrapped": true,
"progress": 100,
"summary": "Done",
"exit_country": "DE",
"strict_nodes": true
},
"proxies": {
"socks5_ready": true,
"socks_port": 9050,
"http_port": 8118
},
"egress": {
"verified": true,
"ip": "185.220.101.5",
"error": null
}
}Returns lightweight Prometheus exposition text metrics (text/plain; version=0.0.4).
# HELP torproxy_uptime_seconds Total container API uptime in seconds
# TYPE torproxy_uptime_seconds gauge
torproxy_uptime_seconds 142.5
# HELP torproxy_reconnect_total Total circuit reconnect requests triggered
# TYPE torproxy_reconnect_total counter
torproxy_reconnect_total 1
# HELP torproxy_tor_bootstrapped Tor bootstrap status (1=done, 0=in progress)
# TYPE torproxy_tor_bootstrapped gauge
torproxy_tor_bootstrapped 1
# HELP torproxy_tor_bootstrap_progress Tor bootstrap progress percentage
# TYPE torproxy_tor_bootstrap_progress gauge
torproxy_tor_bootstrap_progress 100
# HELP torproxy_socks5_ready SOCKS5 listener probe (1=ready, 0=down)
# TYPE torproxy_socks5_ready gauge
torproxy_socks5_ready 1
Any request to an unsupported path returns 404 Not Found.
{
"error": "Not Found",
"supported_paths": [
"/status",
"/ip",
"/connect",
"/disconnect",
"/reconnect",
"/health",
"/diagnostics",
"/metrics"
]
}- Time-to-Live (TTL): 30 seconds.
- Thread Safety: Protected via an internal
threading.Lockacross concurrent handler threads. - Cache Eviction: Automatically invalidated whenever
/connect,/disconnect, or/reconnectis executed. - Verification Source: Evaluated against
https://check.torproject.org/api/ipthrough the local Privoxy proxy (127.0.0.1:8118). - Egress Guarantee: Egress verification strictly requires
IsTor: truefrom the check service. Responses withIsTor: falseor missing verification fields are rejected and never cached or reported as connected. - Egress Failure Taxonomy (ISSUE-12, #76): Transport failures of the probe surface as stable, operator-safe strings in the
error/detailfields of/ip,/healthand/diagnostics(thestatusvalues and JSON shapes are unchanged):local HTTP proxy unreachable— connection refused to the local HTTP proxy on127.0.0.1:8118(e.g. Privoxy down; SOCKS5/Tor may still be healthy).Tor upstream unavailable via local HTTP proxy (HTTP <code>)— the local proxy answered with a 5xx (e.g. because no Tor circuit is ready), so Tor/upstream is the problem, not the check service.egress check service returned HTTP <code>— the check service itself answered with a 4xx through a healthy proxy (e.g. 429 rate-limit); this is not a Tor upstream failure. An open circuit breaker keeps the last cause and appends(circuit breaker open).egress check service unreachable— timeout or other transport failure after the local proxy, i.e. the check service itself could not be reached.Tor check service reported traffic NOT routed through Tor (IsTor=false)and the other validation errors (missingIsTor, oversized/invalid response) — the check service answered definitively and is not retried.