tailscale-proxy
Container image that bridges a Tailscale tailnet service to the local container network, no TUN device and no privileges required.
Tailscale container proxying a service
tailscale-proxy is a container image that puts one Tailscale service on the
local container network. It runs tailscaled in userspace networking mode and
forwards a local port to the tailnet host through socat. Other containers
connect to the proxy and never learn that Tailscale exists.
41 commits | 1,062 insertions(+) | 291 deletions(-)
I use Tailscale not only for remote access to my
lab, and sometimes you
just need a simple, permission-less solution to access a tailscale service. The
obvious alternatives are both heavier than this simple solution. Tailscale on the
host shares one tailnet identity among every workload on the machine. Tailscale
in every container wants /dev/net/tun, privileges, an auth key and persisted
state apiece. Here one container joins the tailnet, and the rest of the network
opens plain TCP connections to it.
Two processes and a socat
tailscaled starts in userspace mode, which trades the kernel’s tun device
for a SOCKS5 proxy on localhost:1055. The container authenticates with
TS_AUTHKEY, or picks up the session persisted on its volume. Then socat
listens on TS_BIND_ADDR:LISTEN_PORT and forwards every connection to
TS_DEST_HOST:TS_DEST_PORT through that proxy:
socat --experimental \
"TCP-LISTEN:${LISTEN_PORT},bind=${TS_BIND_ADDR},reuseaddr,fork" \
"SOCKS5-CONNECT:${TS_SOCKS5_ADDR}:${TS_DEST_HOST}:${TS_DEST_PORT}"
That is the whole mechanism. The rest of the work went into the entrypoint around it. Variables, build and run instructions are in the README.
What it does not do
TCP only. tailscaled’s userspace SOCKS5 server carries TCP and nothing
else, so neither does the proxy. No DNS, no QUIC, no UDP anything.
Idle connections are on their own. Neither socat nor the SOCKS5 path sets
TCP keepalive, so a connection that goes silent can be dropped by a NAT gateway
or a firewall without either endpoint hearing about it. Applications that hold
connections open, database drivers and SSH among them, should heartbeat on
their own.
The listener takes no credentials. Whatever reaches the published port gets tunneled into the tailnet service. Publish it only where you would publish the service itself.
It is one Alpine image, two processes and a shell script. For one tailnet service that a container network needs to reach, that is enough.