ipn: allow serve to forward TCP connections to Unix sockets

Incidentally, this also allows serve to forward TCP connections to UDP
listeners, though this is still prohibited by the CLI.

Updates tailscale/corp#27200
Signed-off-by: Harry Harpham <harry@tailscale.com>
hwh33/add-unix-sockets-to-serve
Harry Harpham 2 weeks ago
parent 22a815b6d2
commit d88e2b2cda
No known key found for this signature in database

@ -1048,7 +1048,7 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
}
output.WriteString(fmt.Sprintf("|--> tcp://%s\n\n", tcpHandler.TCPForward))
output.WriteString(fmt.Sprintf("|--> %s\n\n", tcpHandler.TCPForward))
}
if !forService && !e.bg.Value {
@ -1204,7 +1204,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
svcName := tailcfg.AsServiceName(dnsName)
targetURL, err := ipn.ExpandProxyTargetValue(target, []string{"tcp"}, "tcp")
targetURL, err := ipn.ExpandProxyTargetValue(target, []string{"tcp", "unix"}, "tcp")
if err != nil {
return fmt.Errorf("unable to expand target: %v", err)
}
@ -1219,7 +1219,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
return fmt.Errorf("cannot serve TCP; already serving web on %d for %s", srcPort, dnsName)
}
sc.SetTCPForwarding(srcPort, dstURL.Host, terminateTLS, proxyProtocol, dnsName)
sc.SetTCPForwarding(srcPort, dstURL.String(), terminateTLS, proxyProtocol, dnsName)
return nil
}

@ -449,7 +449,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:5432",
TCPForward: "tcp://127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
},
@ -464,7 +464,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TCPForward: "tcp://localhost:5432",
TerminateTLS: "foo.test.ts.net",
},
},
@ -475,7 +475,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:8443",
TCPForward: "tcp://127.0.0.1:8443",
TerminateTLS: "foo.test.ts.net",
},
},
@ -491,7 +491,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:123",
TCPForward: "tcp://localhost:123",
TerminateTLS: "foo.test.ts.net",
},
},
@ -549,6 +549,35 @@ func TestServeDevConfigMutations(t *testing.T) {
},
},
},
{
name: "unix",
steps: []step{
{
command: cmd("serve --bg --tcp=80 unix://my-socket.sock"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
80: {TCPForward: "unix://my-socket.sock"},
},
},
},
},
},
{
name: "unix over TLS",
steps: []step{
{
command: cmd("serve --bg --tls-terminated-tcp=443 unix://my-socket.sock"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "unix://my-socket.sock",
TerminateTLS: "foo.test.ts.net",
},
},
},
},
},
},
{
name: "bad_path",
initialState: fakeLocalServeClient{
@ -664,7 +693,9 @@ func TestServeDevConfigMutations(t *testing.T) {
{ // start a tcp forwarder on 8443
command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "localhost:5432"}},
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
8443: {TCPForward: "tcp://localhost:5432"}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
@ -675,7 +706,7 @@ func TestServeDevConfigMutations(t *testing.T) {
{ // remove primary port http handler
command: cmd("serve off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "localhost:5432"}},
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "tcp://localhost:5432"}},
},
},
{ // remove tcp forwarder
@ -745,7 +776,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TCPForward: "tcp://localhost:5432",
TerminateTLS: "foo.test.ts.net",
},
},
@ -929,7 +960,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {
TCPForward: "localhost:5432",
TCPForward: "tcp://localhost:5432",
ProxyProtocol: 1,
},
},
@ -943,7 +974,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TCPForward: "tcp://localhost:5432",
TerminateTLS: "foo.test.ts.net",
ProxyProtocol: 2,
},
@ -958,7 +989,7 @@ func TestServeDevConfigMutations(t *testing.T) {
command: cmd("serve --tcp=8000 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {TCPForward: "localhost:5432"},
8000: {TCPForward: "tcp://localhost:5432"},
},
},
},
@ -967,7 +998,7 @@ func TestServeDevConfigMutations(t *testing.T) {
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {
TCPForward: "localhost:5432",
TCPForward: "tcp://localhost:5432",
ProxyProtocol: 1,
},
},
@ -1445,6 +1476,30 @@ func TestMessageForPort(t *testing.T) {
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
}, "\n"),
},
{
name: "serve unix",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
80: {
TCPForward: "unix://my-socket.sock",
},
},
},
status: &ipnstate.Status{CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}},
dnsName: "foo.test.ts.net",
srvType: serveTypeTCP,
srvPort: 80,
expected: strings.Join([]string{
msgServeAvailable,
"",
"|-- tcp://foo.test.ts.net:80 (TLS over TCP)",
"|--> unix://my-socket.sock",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableProxy, "serve", "tcp", 80),
}, "\n"),
},
{
name: "serve service http",
subcmd: serve,
@ -1582,7 +1637,7 @@ func TestMessageForPort(t *testing.T) {
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {TCPForward: "localhost:3000"},
2200: {TCPForward: "tcp://localhost:3000"},
},
},
},
@ -1612,6 +1667,43 @@ func TestMessageForPort(t *testing.T) {
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service unix",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {TCPForward: "unix://my-socket.sock"},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeTCP,
srvPort: 2200,
expected: strings.Join([]string{
msgServeAvailable,
"",
"|-- tcp://foo.test.ts.net:2200 (TLS over TCP)",
"|-- tcp://100.101.101.101:2200",
"|-- tcp://[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:2200",
"|--> unix://my-socket.sock",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "tcp", 2200),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service Tun",
subcmd: serve,
@ -1654,7 +1746,7 @@ func TestMessageForPort(t *testing.T) {
}
if actual != tt.expected {
t.Errorf("\nGot: %q\nExpected: %q", actual, tt.expected)
t.Errorf("\nGot:\n%s\n\nExpected:\n%s", actual, tt.expected)
}
})
}
@ -1863,7 +1955,7 @@ func TestSetServe(t *testing.T) {
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "tcp://127.0.0.1:3000"}},
},
},
},
@ -1885,7 +1977,7 @@ func TestSetServe(t *testing.T) {
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3001"}},
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "tcp://127.0.0.1:3001"}},
},
},
},
@ -2078,8 +2170,9 @@ func TestSetServe(t *testing.T) {
t.Fatalf("got no error; expected error.")
}
if !tt.expectErr && !reflect.DeepEqual(tt.cfg, tt.expected) {
svcName := tailcfg.ServiceName(tt.dnsName)
t.Fatalf("got: %v; expected: %v", tt.cfg.Services[svcName], tt.expected.Services[svcName])
gotbts, _ := json.MarshalIndent(tt.cfg, "", "\t")
wantbts, _ := json.MarshalIndent(tt.expected, "", "\t")
t.Fatalf("diff:\n%s", cmp.Diff(string(gotbts), string(wantbts)))
}
})
}

@ -561,8 +561,17 @@ func (b *LocalBackend) tcpHandlerForVIPService(dstAddr, srcAddr netip.AddrPort)
if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
// Support optional schemes like 'unix://socket-file'.
// For backwards compatibility with existing serve config, this
// needs to support schemeless destinations and assume TCP.
backNet, backAddr := "tcp", backDst
if i := strings.Index(backDst, "://"); i >= 0 {
backNet, backAddr = backDst[:i], backDst[i+len("://"):]
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
backConn, err := b.dialer.SystemDial(ctx, backNet, backAddr)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
@ -648,8 +657,17 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
// Support optional schemes like 'unix://socket-file'.
// For backwards compatibility with existing serve config, this
// needs to support schemeless destinations and assume TCP.
backNet, backAddr := "tcp", backDst
if i := strings.Index(backDst, "://"); i >= 0 {
backNet, backAddr = backDst[:i], backDst[i+len("://"):]
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
backConn, err := b.dialer.SystemDial(ctx, backNet, backAddr)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)

@ -731,6 +731,10 @@ func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultSch
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
}
if u.Scheme == "unix" {
return u.String(), nil
}
// validate port according to host.
if u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1" {
// require port for localhost targets

Loading…
Cancel
Save