No description
Find a file
Stefan Wasilewski 62b9925027 initial import
2026-05-11 19:12:13 +04:00
.dirac-symbol-index initial import 2026-05-11 19:12:13 +04:00
dev.tsproxy.plist initial import 2026-05-11 19:12:13 +04:00
go.mod initial import 2026-05-11 19:12:13 +04:00
go.sum initial import 2026-05-11 19:12:13 +04:00
LICENSE initial import 2026-05-11 19:12:13 +04:00
main.go initial import 2026-05-11 19:12:13 +04:00
README.md initial import 2026-05-11 19:12:13 +04:00
tsproxy initial import 2026-05-11 19:12:13 +04:00

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

  1. (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"] }
    ]
    
  2. 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.

  3. 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
    
  4. Subsequent runs don't need TS_AUTHKEY:

    tsproxy -f 127.0.0.1:8080=webhost:80 -f 127.0.0.1:5432=dbhost:5432
    

    Then curl http://127.0.0.1:8080 hits webhost:80 on 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 --forward rules 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 fresh TS_AUTHKEY.

License

MIT