tsproxy/README.md

99 lines
3.1 KiB
Markdown
Raw Normal View History

2026-05-11 15:12:13 +00:00
# tsproxy
A tiny TCP forwarder that joins your tailnet via [tsnet](https://pkg.go.dev/tailscale.com/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
```sh
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):
```json
"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:
```sh
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`:
```sh
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](dev.tsproxy.plist). Install it per-user:
```sh
cp dev.tsproxy.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/dev.tsproxy.plist
```
Manage it:
```sh
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](LICENSE)