Small Go web service that proxies outbound GET requests through an upstream proxy with TLS client fingerprinting. Two upstream transports are supported, each with its own endpoint:
GET /socks5(also available asGET /proxy) — SOCKS5 upstream, local DNS resolutionGET /https— HTTP(S) CONNECT upstream, remote DNS at the proxy exit (optional route)
and one technical endpoint:
GET /health
/socks5 accepts a target URL via query params, enforces Bearer token auth, forwards the request through SOCKS5, and returns upstream response as-is.
Flow:
- Validate
Authorizationheader:Bearer <token>. - Read query params:
url(required iftargetnot set): full HTTPS URL used as-istarget(required ifurlnot set): host/path without protocol, service prependshttps://method(required): onlygetsupported (case-insensitive)data_type(optional): currently ignored, kept for compatibility
- Only HTTPS targets are allowed. HTTP URLs are rejected with
400. - If
TOKENBEL_PROXY_ALLOWED_DOMAINSis configured, only requests to listed domains (including subdomains) are allowed. - Execute request through SOCKS5 proxy with per-request timeout.
- Return upstream status code, headers, and body.
Both proxy routes share the same contract (query params, auth, allowlist, error mapping). They differ only in upstream transport:
/socks5, /proxy |
/https |
|
|---|---|---|
| Upstream proxy URL | TOKENBEL_PROXY_SOCKS5_URL (socks5/socks5h) |
TOKENBEL_PROXY_HTTPS_URL (http/https) |
| DNS resolution | local — service resolves IPv4 and sends the IP through SOCKS | remote — CONNECT carries the hostname, DNS resolves at the proxy exit |
Caller User-Agent |
dropped | forwarded verbatim; profile-matched Chrome UA when the caller sends none |
| Typical use | gov.by JSON APIs | anti-bot-protected sites (e.g. pravo.by) that reject datacenter DNS resolution and non-browser UAs |
/https requires TOKENBEL_PROXY_HTTPS_URL. Without it the route returns 503 {"error":"endpoint_not_configured"} while /socks5 and /proxy keep working.
Error behavior:
- Missing/invalid Bearer token ->
401 - Missing
url/target->400 - HTTP (non-HTTPS) target ->
400 - Disallowed domain ->
403 - Invalid
method->400 - Rate limit exceeded ->
429 - Upstream timeout ->
504 - Proxy/upstream network error ->
502
Error response shape:
{
"error": "bad_gateway",
"message": "proxy connection failed",
"details": "connection refused"
}Configuration is read from environment variables:
| Variable | Required | Description |
|---|---|---|
TOKENBEL_PROXY_BEARER_TOKEN |
Yes | Bearer token for endpoint authentication |
TOKENBEL_PROXY_SOCKS5_URL |
Yes | SOCKS5 proxy URL (e.g., socks5://user:pass@host:port) |
TOKENBEL_PROXY_HTTPS_URL |
No | HTTP(S) CONNECT proxy URL (e.g., http://user:pass@geo.iproyal.com:12321); enables the /https route |
TOKENBEL_PROXY_PORT |
No | Server port (default: 8080) |
TOKENBEL_PROXY_TIMEOUT |
No | Upstream request timeout in seconds (default: 120) |
TOKENBEL_PROXY_CLIENT_PROFILE |
No | TLS client profile: chrome_120 or firefox_117 (default: chrome_120) |
TOKENBEL_PROXY_INSECURE_TLS |
No | Skip TLS verification: true or false (default: true) |
TOKENBEL_PROXY_ALLOWED_DOMAINS |
No | Comma-separated domain allowlist (default: empty = all allowed) |
TOKENBEL_PROXY_RATE_LIMIT |
No | Requests per second per IP (default: 0 = unlimited) |
TOKENBEL_PROXY_RATE_BURST |
No | Burst size for rate limiter (default: same as rate limit) |
export TOKENBEL_PROXY_BEARER_TOKEN="your-secret-token"
export TOKENBEL_PROXY_SOCKS5_URL="socks5://user:password@geo.iproyal.com:12321"
export TOKENBEL_PROXY_HTTPS_URL="http://user:password@geo.iproyal.com:12321" # optional, enables /https
export TOKENBEL_PROXY_ALLOWED_DOMAINS="egr.gov.by,example.com"
export TOKENBEL_PROXY_RATE_LIMIT=5The service will exit at startup if required variables are missing.
GET /health
Checks TCP reachability of every configured proxy (SOCKS5 always; HTTPS when TOKENBEL_PROXY_HTTPS_URL is set). Returns 200 if all are reachable, 503 otherwise.
Success response:
{ "status": "ok" }Unhealthy response:
{ "status": "unhealthy", "details": "proxy unreachable" }GET /socks5?url=<https-url>&method=get
GET /socks5?target=<host/path>&data_type=json&method=get
Required header:
Authorization: Bearer $TOKENBEL_PROXY_BEARER_TOKENA X-Request-ID header is set on every response. If the incoming request includes one, it is forwarded; otherwise a random ID is generated.
Example:
curl -i \
-H "Authorization: Bearer $TOKENBEL_PROXY_BEARER_TOKEN" \
"http://localhost:8080/socks5?target=egr.gov.by/api/v2/egr/getAllJurNamesByRegNum/191179355&data_type=json&method=get"GET /https?url=<https-url>&method=get
GET /https?target=<host/path>&method=get
Same contract as /socks5 (auth header, query params, allowlist, error mapping), but the request is tunneled through the HTTP(S) CONNECT proxy configured via TOKENBEL_PROXY_HTTPS_URL. Two behavioral differences:
- the target hostname travels in the CONNECT request, so DNS resolves at the proxy exit (socks5h-style remote resolution);
- the caller's
User-Agentis forwarded upstream (anti-bot front-ends reject Go/curl defaults); when the caller sends none, a Chrome UA matching the default TLS profile is applied.
Without TOKENBEL_PROXY_HTTPS_URL the route returns 503 {"error":"endpoint_not_configured","details":"set TOKENBEL_PROXY_HTTPS_URL"}.
Example:
curl -i \
-H "Authorization: Bearer $TOKENBEL_PROXY_BEARER_TOKEN" \
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" \
-H "Accept-Language: ru" \
"http://localhost:8080/https?target=pravo.by/ofitsialnoe-opublikovanie/novye-postupleniya/&method=get"From project root:
cd src
go mod tidy
go build -o proxy .
./proxyThe service starts on :8080.
Quick checks:
curl -s http://localhost:8080/healthcurl -i "http://localhost:8080/socks5?target=example.com&method=get"
# expected: 401 (missing auth header)Run all checks:
cd src
go test ./...
go vet ./...
go build ./...Current unit tests are in:
handler/proxy_test.gohandler/health_test.gohandler/notconfigured_test.gohandler/ratelimit_test.gohandler/requestid_test.goclient/client_test.goconfig/config_test.go
Covered scenarios:
- unauthorized requests
- missing target
- invalid method
- HTTPS-only enforcement
- domain allowlist (allowed, blocked, subdomains)
- per-request timeout (504)
- request ID generation and forwarding
- rate limiting (under/over limit, separate IPs)
- health check (proxy reachable, unreachable, no proxy configured, multiple proxies)
- client constructor scheme validation (socks5 vs http/https)
- User-Agent forwarding rules (dropped on /socks5, forwarded/defaulted on /https)
- not-configured route stub (503)
- timeout mapping
- bad gateway mapping
- success response forwarding
- config env var parsing
Service includes a multi-stage Dockerfile:
- Builder:
golang:alpine - Runtime:
alpine:latest
From repository root:
docker build -t tokenbel-proxy:latest ./srcdocker run --rm -p 8080:8080 \
-e TOKENBEL_PROXY_BEARER_TOKEN="$TOKENBEL_PROXY_BEARER_TOKEN" \
-e TOKENBEL_PROXY_SOCKS5_URL="$TOKENBEL_PROXY_SOCKS5_URL" \
-e TOKENBEL_PROXY_HTTPS_URL="$TOKENBEL_PROXY_HTTPS_URL" \
tokenbel-proxy:latestcurl -s http://localhost:8080/healthFor production-like deployment, at minimum:
- Restrict inbound access (private network, gateway, ACLs).
- Set
TOKENBEL_PROXY_ALLOWED_DOMAINSto limit upstream targets. - Enable rate limiting via
TOKENBEL_PROXY_RATE_LIMIT. - Set
TOKENBEL_PROXY_INSECURE_TLS=falseif upstream certificates are valid. - Monitor logs (structured JSON via
log/slog) and HTTP status distribution.