ipn/ipnlocal,cmd/tailscale/cli: support unix socket targets for TCP serve

Allow `tailscale serve --tcp <port> unix:/path/to/socket` and `tailscale serve --tls-terminated-tcp <port> unix:/path/to/socket` to forward TCP connections to a Unix domain socket. Previously only host:port targets were supported for TCP serve mode.

Updates #20161

Signed-off-by: ayanamist <ayanamist@gmail.com>
This commit is contained in:
ayanamist
2026-07-16 09:13:38 -06:00
committed by Harry Harpham
parent 38345dce3d
commit 50f1c285ba
10 changed files with 494 additions and 61 deletions
+15 -2
View File
@@ -673,7 +673,14 @@ func (b *LocalBackend) tcpHandlerForServeTCP(tcph ipn.TCPPortHandlerView, dport
defer conn.Close()
conn = b.meteredConnForService(conn, forVIPService)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
var backConn net.Conn
var err error
if socketPath, ok := strings.CutPrefix(backDst, "unix:"); ok {
var d net.Dialer
backConn, err = d.DialContext(ctx, "unix", socketPath)
} else {
backConn, err = b.dialer.SystemDial(ctx, "tcp", backDst)
}
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
@@ -714,7 +721,13 @@ func (b *LocalBackend) tcpHandlerForServeTCP(tcph ipn.TCPPortHandlerView, dport
func (b *LocalBackend) forwardTCPWithProxyProtocol(conn, backConn net.Conn, proxyProtoVer int, srcAddr netip.AddrPort, dport uint16, backDst string) error {
var proxyHeader []byte
if proxyProtoVer > 0 {
backAddr := backConn.RemoteAddr().(*net.TCPAddr)
// PROXY protocol requires a valid TCP destination address.
// For Unix socket backends, RemoteAddr is *net.UnixAddr;
// the CLI rejects this combination, but guard here as well.
backAddr, ok := backConn.RemoteAddr().(*net.TCPAddr)
if !ok {
return fmt.Errorf("PROXY protocol is not supported with non-TCP backend %s", backDst)
}
// We always want to format the PROXY protocol header based on
// the IPv4 or IPv6-ness of the client. The SourceAddr and
+82
View File
@@ -12,12 +12,14 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"tailscale.com/ipn"
"tailscale.com/tstest"
)
@@ -238,3 +240,83 @@ func TestServeBlocksTailscaledSocket(t *testing.T) {
t.Error("expected valid handler for legitimate socket")
}
}
func TestTCPForwardUnixSocket(t *testing.T) {
tmpDir := t.TempDir()
socketPath := filepath.Join(tmpDir, "backend.sock")
// Create a Unix socket echo server
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatalf("failed to create unix socket listener: %v", err)
}
defer listener.Close()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func() {
defer conn.Close()
io.Copy(conn, conn) // echo
}()
}
}()
// Set up a LocalBackend with a ServeConfig that forwards TCP port 3128 to the unix socket
b := newTestBackend(t)
b.logf = tstest.WhileTestRunningLogger(t)
conf := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
3128: {TCPForward: "unix:" + socketPath},
},
}
if err := b.SetServeConfig(conf, ""); err != nil {
t.Fatal("setting serve config:", err)
}
// Get the handler from tcpHandlerForServe
srcAddr := netip.MustParseAddrPort("100.100.100.1:12345")
handler := b.tcpHandlerForServe(3128, srcAddr, nil)
if handler == nil {
t.Fatal("tcpHandlerForServe returned nil handler")
}
// Create a pipe to simulate an incoming connection
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
// Run the handler in a goroutine
handlerDone := make(chan error, 1)
go func() {
handlerDone <- handler(serverConn)
}()
// Write data through the "client" side and read the echo back
testData := []byte("hello via tcpHandlerForServe")
if _, err := clientConn.Write(testData); err != nil {
t.Fatalf("write failed: %v", err)
}
buf := make([]byte, len(testData))
if _, err := io.ReadFull(clientConn, buf); err != nil {
t.Fatalf("read failed: %v", err)
}
if string(buf) != string(testData) {
t.Fatalf("echo mismatch: got %q, want %q", buf, testData)
}
// Close client side, handler should finish
clientConn.Close()
select {
case err := <-handlerDone:
if err != nil {
t.Fatalf("handler returned unexpected error: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("handler did not finish in time")
}
}