A tiny, self-contained TCP proxy that embeds Tailscale's tsnet. It joins a Tailnet as its own Tailscale node, listens on one or more Tailnet ports plus matching local ports, and forwards each connection to a configured service inside the Tailnet.
ts-proxy creates a Tailscale node using tsnet, then starts one Tailscale TCP listener and one local TCP listener for each configured mapping.
[Client in Tailnet] -> [ts-proxy:5432] -> [postgres.tailnet.ts.net:5432]
[Local machine] -> [localhost:5432] -> [postgres.tailnet.ts.net:5432]
[Client in Tailnet] -> [ts-proxy:8080] -> [100.64.0.10:80]
The proxy dials targets through tsnet, so target hosts can be Tailnet DNS names, MagicDNS names, or Tailscale IPs.
Configuration is via environment variables:
TS_PROXY_MAPPINGSis required. UselistenPort=targetHost:targetPortentries separated by commas, semicolons, or newlines.TS_PROXY_HOSTNAMEcontrols the Tailscale node name. Defaults tots-proxy.TS_PROXY_STATE_DIRcontrols where tsnet stores its node identity. Defaults to./tsnet-state.TS_PROXY_LOCAL_ADDRcontrols the local bind address for matching local listeners. Defaults to127.0.0.1; use0.0.0.0to expose on all local network interfaces.TS_PROXY_ACCEPT_ROUTEScontrols whether the proxy accepts subnet routes advertised by other Tailnet nodes. Defaults totrue.TS_PROXY_AUTH_KEYorTS_AUTHKEYis optional. This can be a Tailscale auth key or OAuth client secret. When omitted, first run prints a Tailscale login URL.TS_PROXY_ADVERTISE_TAGSis optional. Use comma, semicolon, or newline-separated tags such astag:ci,tag:proxy.TS_PROXY_AUTH_EPHEMERALis optional. When set, appends the OAuth auth-key parameterephemeral=true|false.TS_PROXY_AUTH_PREAUTHORIZEDis optional. When set, appends the OAuth auth-key parameterpreauthorized=true|false.TS_PROXY_AUTH_BASE_URLis optional. When set, appends the OAuth auth-key parameterbaseURL=....
Example:
export TS_PROXY_HOSTNAME=client-services-proxy
export TS_PROXY_MAPPINGS="5432=postgres.tailnet.ts.net:5432,8080=100.64.0.10:80"
go run .Then connect from another Tailnet machine:
psql "postgres://user:pass@client-services-proxy:5432/db"
curl http://client-services-proxy:8080Or connect locally on the machine running the proxy:
psql "postgres://user:pass@localhost:5432/db"
curl http://localhost:8080For unattended deployments, create a reusable or ephemeral auth key in Tailscale and set:
export TS_PROXY_AUTH_KEY=tskey-auth-...OAuth client secrets are also supported. The OAuth client must have the auth_keys scope and must be allowed to advertise the tag or tags passed in TS_PROXY_ADVERTISE_TAGS:
export TS_PROXY_AUTH_KEY=tskey-client-...
export TS_PROXY_ADVERTISE_TAGS=tag:ci
go run .You can pass OAuth auth-key parameters directly:
export TS_PROXY_AUTH_KEY="tskey-client-...?ephemeral=false&preauthorized=true"
export TS_PROXY_ADVERTISE_TAGS=tag:ci
go run .Or let ts-proxy append them from env vars:
export TS_PROXY_AUTH_KEY=tskey-client-...
export TS_PROXY_ADVERTISE_TAGS=tag:ci
export TS_PROXY_AUTH_EPHEMERAL=false
export TS_PROXY_AUTH_PREAUTHORIZED=true
go run .Route acceptance is enabled by default so targets behind approved subnet routers are reachable. To disable it:
export TS_PROXY_ACCEPT_ROUTES=falsePre-built binaries are created for each release:
# Linux
wget https://github.com/pipelabs/ts-proxy/releases/latest/download/ts-proxy-VERSION-linux-amd64.tar.gz
tar -xzf ts-proxy-VERSION-linux-amd64.tar.gz
# macOS Apple Silicon
wget https://github.com/pipelabs/ts-proxy/releases/latest/download/ts-proxy-VERSION-mac-arm64.tar.gz
tar -xzf ts-proxy-VERSION-mac-arm64.tar.gz
# macOS Intel
wget https://github.com/pipelabs/ts-proxy/releases/latest/download/ts-proxy-VERSION-mac-amd64.tar.gz
tar -xzf ts-proxy-VERSION-mac-amd64.tar.gz# Current platform
go build -o ts-proxy .
# Release-style binaries in dist/
./build.sh- Copy the binary to the machine that will run the proxy.
- Set
TS_PROXY_HOSTNAMEandTS_PROXY_MAPPINGS. - Optionally set
TS_AUTHKEYfor non-interactive authentication. - Run
./ts-proxy.
On first run without TS_AUTHKEY, the process prints a Tailscale authentication URL.
The tsnet-state directory should be persisted across restarts so the proxy keeps the same Tailscale identity.
PostgreSQL:
export TS_PROXY_MAPPINGS="5432=postgres.tailnet.ts.net:5432"Multiple services:
export TS_PROXY_MAPPINGS="5432=postgres.tailnet.ts.net:5432,6379=redis.tailnet.ts.net:6379,8080=100.64.0.10:80"Newline-separated mappings:
export TS_PROXY_MAPPINGS="
5432=postgres.tailnet.ts.net:5432
6379=redis.tailnet.ts.net:6379
"./build.sh creates:
dist/ts-proxy-{version}-linux-amd64dist/ts-proxy-{version}-mac-arm64dist/ts-proxy-{version}-mac-amd64dist/ts-proxy-{version}-windows-amd64.exe
Release builds embed version information that is printed on startup:
ts-proxy
Version: v1.2.3
Build Time: 2026-05-06_01:23:45
Git Commit: abc1234
Module: github.com/pipelabs/ts-proxy
go test ./...
go build ./...- Check the login URL printed by the process.
- Verify
TS_AUTHKEYis valid if running unattended. - Make sure the Tailscale account has room for another node.
- Confirm the mapping listen port is present in
TS_PROXY_MAPPINGS. - Confirm the target service is reachable from inside the Tailnet.
- Check Tailscale ACLs allow the client to reach the proxy port.
Persist TS_PROXY_STATE_DIR between restarts. Deleting the state directory creates a new tsnet identity.
Releases are automated via GitHub Actions on pushes to main. See RELEASING.md for details.