Simple proxy program to allow exposing individual services to a Tailnet, and even to the Internet using Tailscale Funnel.
Unfortunately, the Tailscale daemon only allows exposing services using the current node domain, and you can't spawn (so far) nodes for services. With this you can!
On first run, each configured server (Tailscale node) may need to be authenticated
with your Tailscale account. You can supply an auth key via the environment (see
auth_key: "${TS_AUTHKEY}" in the config file) or follow the login URL printed
on first start.
After the initial login, Tailscale stores certificates and node state under the
state_dir configured in the YAML file (a subdirectory is created per server
slug). Subsequent starts normally require no interaction.
As the main build of Tailscale, you can get information about the user accessing the service using the following headers that get forwarded to the upstream service:
Tailscale-User-LoginTailscale-User-NameTailscale-User-Profile-Pic
And yeah, you can use Tailscale as a single sign on and have a public facing version! It's as safe and stable as tclip is because this proxy uses the exact same primitives.
Warning
You can count on the headers sent by ts-proxy as long as you follow the following conditions:
-
Anything that changes the headers name representation such as Apache with PHP could be cheated by passing the header TAILSCALE_USER_LOGIN, for example.
-
If some users can access your actual service directly without passing the traffic through ts-proxy they can change all the headers they want, including authentication ones.
-
If you don't use the header authentication for anything in a given service these issues will not be a problem for that service.
The CLI is lewkit x/cmd. Subcommands are server and config.
# Show commands and flags
ts-proxyd --help
# Start the proxy (all servers defined in the config)
ts-proxyd server --config /etc/ts-proxy/config.yaml
# Dry-run: authenticate the Tailscale nodes and print the resolved
# server structure, then exit without serving traffic.
ts-proxyd server --dry-run --config config.yaml
# Print the resolved configuration after defaults and env expansion.
ts-proxyd config --config config.yamlGlobal flags (available to all commands):
--config– path to the YAML config file. If omitted, ts-proxyd looks forts-proxy.yamlin the current directory, the user config directory (os.UserConfigDir/ts-proxy, which followsXDG_CONFIG_HOMEon Unix), and/etc/ts-proxy/.--state-dir– base directory for Tailscale state (overwrites the value in the config file).--stop-on-fail– if any server fails, stop the whole process (instead of restarting the failed one).--status-listen– status listener address. Default127.0.0.1:8090. A bare port listens on every interface (8090means:8090).
ts-proxyd server --help lists --dry-run. -h / --help prints help, --version prints the build version, and -v raises log verbosity. Those flags come from lewkit.
ts-proxyd server binds status_listen before it starts any Tailscale node. --dry-run does not bind it.
Open http://127.0.0.1:8090/ for a page that lists each server. The page shows the lifecycle state (init, starting, authenticating, running, failed, stopped), the hostname, forward when set, and each handler. Handler rows include a TCP dial result: up, down, or unchecked (UDP). The page is a templ component. It loads Sakura CSS with sakuracss.Load. Each state and each availability has an icon beside the word.
The page keeps a server-sent stream at /events. The stream pushes a lifecycle change at once. The stream checks handler availability every 5 seconds.
While a server still needs a user, the page shows that server's Tailscale login link. The link disappears after authentication.
GET /health returns JSON. The status is 200 when every server is running. Any other state returns 503. The body lists each server and handler either way.
curl -sS http://127.0.0.1:8090/healthKeep status_listen on a loopback address. The page shows upstream addresses. Set status_listen in YAML, or override it with --status-listen or TS_PROXY_STATUS_LISTEN.
Configuration is done via a YAML file (recommended), combined with a small number of command-line flags and environment variables (Traefik-style).
See the well-commented example-config.yaml in the root of this repository for a complete example covering:
- Multiple independent Tailscale nodes ("servers")
- Named auth tokens (1 token can be used by many servers)
- Both HTTP and raw TCP handlers
- TLS termination + Tailscale Funnel on selected handlers
- Environment variable expansion inside
auth_keyvalues (${TS_AUTHKEY}etc.)
A minimal example:
state_dir: /var/lib/ts-proxy
stop_on_fail: false
tokens:
prod:
auth_key: "${TS_AUTHKEY}"
servers:
web:
hostname: my-service
token: prod
handlers:
- type: http
listen: ":80"
upstream_address: "127.0.0.1:8080"
- type: http
listen: ":443"
upstream_address: "127.0.0.1:8080"
tls: true
funnel: true # expose publicly via Tailscale FunnelTo publish every TCP port on a host you can already dial (for example 127.0.0.2), set forward on the server. The node still appears as its hostname on the tailnet. A connection to gremio.example.ts.net:22 is dialed as 127.0.0.2:22.
servers:
gremio:
hostname: gremio
token: prod
forward: 127.0.0.2forward is a host without a port. IPv4, IPv6, and hostnames are valid. forward: 127.0.0.2:80 is rejected.
Handlers still bind first. forward is only the miss path: a handler on :80 owns :80; :22 with no handler goes to forward:22. Omit forward and unmatched TCP is rejected, as before.
A server with no handlers and no forward is valid. The node stays on the tailnet and rejects every connection. The process logs a warning.
forward is TCP only. UDP, ICMP (other than ping to the tsnet node), and Funnel are unchanged: Funnel still needs an explicit handler on 443, 8443, or 10000. The upstream sees the ts-proxy process as the client, not the tailnet peer.
- Server and token names must match
^[a-zA-Z0-9_]+$(letters, numbers, underscore). - Each server gets its own subdirectory under
state_dir/<server-name>. auth_keyvalues containing${VAR}are expanded at load time using the process environment.- The
configsubcommand shows you exactly what will be used after defaults are applied and variables expanded. - You can override
state_dir,stop_on_fail, andstatus_listenfrom the command line orTS_PROXY_STATE_DIR,TS_PROXY_STOP_ON_FAIL, andTS_PROXY_STATUS_LISTEN.
Version structure example: 0.7.10
- 0: major
- 7: minor
- 10: patch
Each week an automatic PR is sent to update the package dependencies. For each update there will be a patch release.
Bug fixes would be shipped in patch releases.
Anything that has breaking changes by changing something in this repository will be released as a minor release.
No plans for bumping the major versions yet.
- A way to expose a folder, maybe using single page application patterns, instead of only ports.
- Multi-server support via YAML configuration (and a single process) — implemented. See
example-config.yamland theserver/configsubcommands. -
forwardwraps a reachable host as one Tailscale node for unmatched TCP ports.
This project re-uses the same Tailscale tsnet + header primitives as
tclip.