| .dirac-symbol-index | ||
| dev.tsproxy.plist | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
| tsproxy | ||
tsproxy
A tiny TCP forwarder that joins your tailnet via tsnet
and exposes one or more tailnet services as local ports — without running
tailscaled on the host.
Useful when you want to reach a single service on your tailnet from a machine you'd rather not fully enroll (work laptop, ephemeral VM, etc). The proxy itself becomes a tailnet node, so it can be tightly scoped with tags and ACLs.
Install
go install github.com/smw/tsproxy@latest # or build locally:
go build -o /usr/local/bin/tsproxy .
Quick start
-
(Optional) In your tailnet policy, add a tag for the proxy and an ACL that limits what it can reach. Example (adapt to your hosts):
"tagOwners": { "tag:tsproxy": ["you@example.com"] }, "acls": [ { "action": "accept", "src": ["autogroup:members"], "dst": ["*:*"] }, { "action": "accept", "src": ["tag:tsproxy"], "dst": ["webhost:80", "dbhost:5432"] } ] -
Mint a one-shot auth key at https://login.tailscale.com/admin/settings/keys: not reusable, not ephemeral, tagged
tag:tsproxy. The key is only needed for the first run — tsnet persists its own credentials after that. -
First run:
TS_AUTHKEY=tskey-auth-... tsproxy \ --name tsproxy \ --forward 127.0.0.1:8080=webhost:80 \ --forward 127.0.0.1:5432=dbhost:5432 -
Subsequent runs don't need
TS_AUTHKEY:tsproxy -f 127.0.0.1:8080=webhost:80 -f 127.0.0.1:5432=dbhost:5432Then
curl http://127.0.0.1:8080hitswebhost:80on your tailnet.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--forward |
-f |
(required, repeatable) | Forward rule LOCAL=TARGET, e.g. 127.0.0.1:8080=myhost:80. |
--name |
-n |
tsproxy |
Hostname advertised on the tailnet. |
--dir |
~/.config/tsproxy/<name> |
State directory (node identity, keys). | |
--verbose |
-v |
false |
Verbose tsnet logging. |
Target can be any MagicDNS name, short hostname, or tailnet IP.
Running under launchd (macOS)
An example LaunchAgent plist is included as dev.tsproxy.plist. Install it per-user:
cp dev.tsproxy.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/dev.tsproxy.plist
Manage it:
launchctl list | grep tsproxy
launchctl kickstart -k gui/$(id -u)/dev.tsproxy # restart after edits
launchctl unload ~/Library/LaunchAgents/dev.tsproxy.plist
tail -f /tmp/tsproxy.log
On first run, add the auth key via an EnvironmentVariables block in the
plist, then remove it once the state directory has been populated.
Notes
- One node, many forwards. All
--forwardrules share a single tsnet identity, so you get one device in the admin console and one ACL subject. - Startup is strict. If any local listener fails to bind, the process exits — partial success is confusing under a supervisor.
- State directory matters. Losing
~/.config/tsproxy/<name>/means the node re-registers on next launch and will need a freshTS_AUTHKEY.