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:
@@ -92,11 +92,16 @@ type Target struct {
|
||||
|
||||
// If Protocol is ProtoFile, then Destination is a file path.
|
||||
// If Protocol is ProtoTUN, then Destination is empty.
|
||||
// If Protocol is ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, or
|
||||
// ProtoTLSTerminatedTCP and Destination starts with "unix:", it is a Unix
|
||||
// socket path (e.g. "unix:/var/run/app.sock" or "unix:relative.sock").
|
||||
// Otherwise, it is a host.
|
||||
Destination string
|
||||
|
||||
// If Protocol is not ProtoFile or ProtoTUN, then DestinationPorts is the
|
||||
// set of ports on which to connect to the host referred to by Destination.
|
||||
// For unix socket targets (Destination starting with "unix:"),
|
||||
// DestinationPorts is unused and left at the zero value.
|
||||
DestinationPorts tailcfg.PortRange
|
||||
}
|
||||
|
||||
@@ -133,13 +138,22 @@ func (t *Target) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
t.Destination = target
|
||||
t.DestinationPorts = tailcfg.PortRange{}
|
||||
case ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, ProtoTLSTerminatedTCP:
|
||||
host, portRange, err := tailcfg.ParseHostPortRange(rest)
|
||||
if err != nil {
|
||||
return err
|
||||
if unixPath, ok := strings.CutPrefix(rest, "unix:"); ok {
|
||||
if unixPath == "" {
|
||||
return errors.New("unix socket path cannot be empty")
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = rest
|
||||
t.DestinationPorts = tailcfg.PortRange{}
|
||||
} else {
|
||||
host, portRange, err := tailcfg.ParseHostPortRange(rest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = host
|
||||
t.DestinationPorts = portRange
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = host
|
||||
t.DestinationPorts = portRange
|
||||
default:
|
||||
return errors.New("unsupported protocol")
|
||||
}
|
||||
@@ -155,7 +169,12 @@ func (t *Target) MarshalText() ([]byte, error) {
|
||||
case ProtoTUN:
|
||||
out = "TUN"
|
||||
case ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, ProtoTLSTerminatedTCP:
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, net.JoinHostPort(t.Destination, t.DestinationPorts.String()))
|
||||
if strings.HasPrefix(t.Destination, "unix:") {
|
||||
// Unix socket: serialize as e.g. "tcp://unix:/path/to/sock"
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, t.Destination)
|
||||
} else {
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, net.JoinHostPort(t.Destination, t.DestinationPorts.String()))
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
@@ -261,8 +280,19 @@ func loadConfigV0(json []byte, forService string) (*ServicesConfigFile, error) {
|
||||
}
|
||||
foundTUN = true
|
||||
} else {
|
||||
if ppr.Ports.Last-ppr.Ports.First != target.DestinationPorts.Last-target.DestinationPorts.First {
|
||||
return nil, fmt.Errorf("service %q: source and destination port ranges must be of equal size", svcName.String())
|
||||
// Unix socket targets (Destination starting with "unix:" on an
|
||||
// HTTP/HTTPS/HTTPSInsecure/TCP/TLSTerminatedTCP inbound protocol)
|
||||
// don't have a destination port range; skip the equality check.
|
||||
isUnixSocket := strings.HasPrefix(target.Destination, "unix:") &&
|
||||
(target.Protocol == ProtoHTTP ||
|
||||
target.Protocol == ProtoHTTPS ||
|
||||
target.Protocol == ProtoHTTPSInsecure ||
|
||||
target.Protocol == ProtoTCP ||
|
||||
target.Protocol == ProtoTLSTerminatedTCP)
|
||||
if !isUnixSocket {
|
||||
if ppr.Ports.Last-ppr.Ports.First != target.DestinationPorts.Last-target.DestinationPorts.First {
|
||||
return nil, fmt.Errorf("service %q: source and destination port ranges must be of equal size", svcName.String())
|
||||
}
|
||||
}
|
||||
foundNonTUN = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package conffile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestTargetUnixSocketRoundtrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serialized string
|
||||
want Target
|
||||
}{
|
||||
{
|
||||
name: "tcp_unix_socket",
|
||||
serialized: "tcp://unix:/var/run/app.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoTCP,
|
||||
Destination: "unix:/var/run/app.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tls_terminated_tcp_unix_socket",
|
||||
serialized: "tls-terminated-tcp://unix:/var/run/app.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoTLSTerminatedTCP,
|
||||
Destination: "unix:/var/run/app.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp_unix_socket_relative",
|
||||
serialized: "tcp://unix:relative.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoTCP,
|
||||
Destination: "unix:relative.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http_unix_socket",
|
||||
serialized: "http://unix:/var/run/app.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoHTTP,
|
||||
Destination: "unix:/var/run/app.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https_unix_socket",
|
||||
serialized: "https://unix:/var/run/app.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoHTTPS,
|
||||
Destination: "unix:/var/run/app.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https_insecure_unix_socket",
|
||||
serialized: "https+insecure://unix:/var/run/app.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoHTTPSInsecure,
|
||||
Destination: "unix:/var/run/app.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http_unix_socket_relative",
|
||||
serialized: "http://unix:relative.sock",
|
||||
want: Target{
|
||||
Protocol: ProtoHTTP,
|
||||
Destination: "unix:relative.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp_host_port",
|
||||
serialized: "tcp://localhost:5432",
|
||||
want: Target{
|
||||
Protocol: ProtoTCP,
|
||||
Destination: "localhost",
|
||||
DestinationPorts: tailcfg.PortRange{First: 5432, Last: 5432},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test unmarshal
|
||||
var got Target
|
||||
if err := got.UnmarshalJSON([]byte(`"` + tt.serialized + `"`)); err != nil {
|
||||
t.Fatalf("UnmarshalJSON(%q) failed: %v", tt.serialized, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("UnmarshalJSON(%q) = %+v, want %+v", tt.serialized, got, tt.want)
|
||||
}
|
||||
|
||||
// Test marshal roundtrip
|
||||
marshaled, err := tt.want.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalText() failed: %v", err)
|
||||
}
|
||||
if string(marshaled) != tt.serialized {
|
||||
t.Errorf("MarshalText() = %q, want %q", marshaled, tt.serialized)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user