A tsnet-powered SOCKS5 + HTTP CONNECT proxy daemon. Joins a Tailscale tailnet as a lightweight userspace node and exposes local proxy endpoints, letting any tool (curl, Python, Go programs, etc.) access tailnet nodes without embedding tsnet themselves.
The binary is fully self-contained — no shell scripts required.
| Service | Address | Protocol |
|---|---|---|
| SOCKS5 | 127.0.0.1:1080 |
SOCKS5 proxy |
| HTTP CONNECT | 127.0.0.1:4444 |
HTTP CONNECT proxy |
| Health check | 127.0.0.1:4445 |
GET /health → 200 |
Go to the Tailscale admin console and create an auth key. A reusable, ephemeral key works well.
TS_AUTHKEY=tskey-auth-xxxxx ./ts-proxy authThis joins the tailnet, persists state to ./state/, then exits. You only need to do this once — state survives restarts.
Create a single cron job that runs ts-proxy ensure every 2 minutes. The ensure subcommand handles everything: cold start, health monitoring, proxy token rotation (for container recycles), and stale state cleanup.
*/2 * * * * /path/to/ts-proxy ensure
For example, as a cron definition:
---
id: ts-proxy-keepalive
enabled: true
mode: heartbeat
schedule:
kind: interval
every: 2m
at: "2025-01-01T00:00:00Z"
timezone: UTC
---
/path/to/ts-proxy ensureThat's it. The ensure command handles:
- Cold start — if no daemon is running, starts one
- Health check — verifies the daemon is responding
- Proxy token rotation — detects container recycles that change outbound proxy credentials, restarts the daemon with fresh creds
- Stale state cleanup — removes leftover temp files from crashed sessions
From here it self-heals. The cron job will restart the daemon after container recycles, OOM kills, or any other failure.
| Command | Description |
|---|---|
ts-proxy auth |
One-time authentication (requires TS_AUTHKEY) |
ts-proxy serve |
Start the proxy daemon (default if no subcommand) |
ts-proxy ensure |
Idempotent keepalive — put this in cron |
| Variable | Required | Default | Description |
|---|---|---|---|
TS_AUTHKEY |
First auth only |
— | Tailscale auth key for initial registration |
TS_PROXY_HOSTNAME |
No | bob-proxy |
tsnet node hostname on your tailnet |
TS_VERBOSE |
No | 0 |
Set to 1 for verbose tsnet logging |
GOMEMLIMIT |
No | 100MiB |
Go memory limit (binary sets a sane default) |
GOGC |
No | 50 |
Go GC target percentage |
curl --socks5-hostname 127.0.0.1:1080 http://your-device.your-tailnet.ts.net:8123/api/curl --proxy http://127.0.0.1:4444 http://your-device.your-tailnet.ts.net:8123/api/import requests
proxies = {
"http": "socks5h://127.0.0.1:1080",
"https": "socks5h://127.0.0.1:1080",
}
r = requests.get("http://your-device.your-tailnet.ts.net:8123/api/", proxies=proxies)export ALL_PROXY=socks5h://127.0.0.1:1080
export HTTPS_PROXY=http://127.0.0.1:4444| Flag | Default | Description |
|---|---|---|
-socks |
127.0.0.1:1080 |
SOCKS5 listen address |
-http |
127.0.0.1:4444 |
HTTP CONNECT listen address |
-health |
127.0.0.1:4445 |
Health check listen address |
-state |
./state |
State directory path |
-verbose |
false |
Enable verbose tsnet logging |
- Sets memory defaults —
GOMEMLIMIT=100MiB,GOGC=50(overridable via env) - Ensures host0 IPv4 — adds a dummy
10.0.99.1/32to thehost0interface if needed (container-specific: tsnet needs to see v4=true) - Cleans stale state — removes
tailscaled.state.tmp*from previous crashes - Pre-flight connectivity check — verifies
controlplane.tailscale.comis reachable through the platform proxy. If unreachable, exits with a clear error instead of hanging for 90 seconds - Starts tsnet — with a retry loop (up to 5 attempts) to handle transient v4/v6 detection races
- Writes PID file —
state/serve.pidforensureto find and manage - Opens listeners — SOCKS5, HTTP CONNECT, health check
- Handles SIGTERM/SIGINT — graceful shutdown
- Checks health — quick HTTP call to the health endpoint
- Compares proxy tokens — reads
$https_proxyfrom its fresh env, compares againststate/proxy_tokensaved from last start - Decides:
- Healthy + token unchanged → exit 0 (nothing to do)
- Healthy + token changed → kill existing daemon, start new one
- Not healthy → clean up, start fresh
- Waits up to 90s for the new daemon to become healthy
- Saves the current proxy token on success
- Home Assistant — reach your HA instance on the tailnet from this container
- IMAP/SMTP — connect to a mail server on your tailnet
- SSH — proxy SSH connections to tailnet hosts
- Any tailnet service — anything reachable on your tailnet becomes reachable from this container
The ./state/ directory contains:
- tsnet auth — persisted Tailscale credentials
- serve.pid — PID of the running daemon
- proxy_token — last-known platform proxy auth token
Place it somewhere that survives container restarts (e.g. your workspace). If the state is lost, re-run ./ts-proxy auth with a fresh TS_AUTHKEY.
┌──────────────────────────────────────────┐
│ ts-proxy daemon (serve) │
│ │
│ tsnet.Server ("bob-proxy") │
│ │ │
│ ├─ SOCKS5 server ←─ :1080 │
│ ├─ HTTP CONNECT ←─ :4444 │
│ └─ Health check ←─ :4445 │
│ │
│ All outgoing connections use srv.Dial() │
│ → direct tailnet routing, no exit node │
└──────────────────────────────────────────┘
↕ WireGuard tunnel
┌──────────────────────────────────────────┐
│ Your tailnet nodes │
│ - Home Assistant, NAS, mail server, │
│ dev machines, etc. │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ ts-proxy ensure (cron, every 2m) │
│ │
│ 1. Check /health │
│ 2. Compare proxy token (fresh env) │
│ 3. Kill + restart if needed │
│ 4. Wait for healthy │
│ 5. Save proxy token │
└──────────────────────────────────────────┘
GOOS=linux GOARCH=amd64 go build -o ts-proxy .Requires Go 1.22+ and the tailscale.com/tsnet module.