cd /home/lczerner Lukáš Czerner - home
Lukáš Czerner
1: ~/projects/tailscale-proxy.md markdown

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.


NORMAL ~/projects/tailscale-proxy.md utf-8 markdown 352W 2min All