diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 6ee2181a0..9186e9a58 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "mime" + "net" "net/http" "net/http/httptest" "net/netip" @@ -32,6 +33,8 @@ import ( "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/ipn/store/mem" + "tailscale.com/net/netmon" + "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/tsd" "tailscale.com/tstest" @@ -107,6 +110,104 @@ func TestParseRedirectWithRedirectCode(t *testing.T) { } } +// Tests LocalBackend.tcpHandlerForServe, but only the TCP forwarding part, not +// any of the web handling code (ipn.TCPPortHandler.HTTP/S). +func TestTCPHandlerForServeTCPForward(t *testing.T) { + tests := []struct { + name string + // Starts the back listener (the one connections are forwarded to), and + // also computes the TCPForward value for this listener. + listen func() (l net.Listener, tcpFwd string, err error) + }{ + { + name: "tcp", + listen: func() (net.Listener, string, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, "", err + } + return l, "tcp://" + l.Addr().String(), nil + }, + }, + { + name: "unix", + listen: func() (net.Listener, string, error) { + // n.b. The socket file is removed by l.Close. + sockFile := filepath.Join(os.TempDir(), "tailscale-ipnlocal-testtcphandlerforserve.sock") + os.Remove(sockFile) + l, err := net.Listen("unix", sockFile) + if err != nil { + return nil, "", err + } + return l, "unix://" + sockFile, nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + const dstPort uint16 = 1234 // not actually dialed + const msg = "hello from the peer" + + l, tcpFwd, err := tt.listen() + if err != nil { + t.Fatal(err) + } + defer l.Close() + + // Avoid using tsdial.Dialer.sysDialForTest as we specifically want + // to test the way targets would be dialed in production. + logf := tstest.WhileTestRunningLogger(t) + d := &tsdial.Dialer{Logf: logf} + d.SetNetMon(netmon.NewStatic()) + b := &LocalBackend{ + dialer: d, + logf: logf, + serveConfig: (&ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{ + dstPort: {TCPForward: tcpFwd}, + }, + }).View(), + } + h := b.tcpHandlerForServe(dstPort, netip.AddrPort{}, nil) + + // We use net.Pipe and h (the TCP handler we are testing), to test + // that a connecting peer will be able to talk to the target (l in + // our case). The topology looks like: + // peer <-net.Pipe-> fromPeer <-h-> result of l.Accept + peer, fromPeer := net.Pipe() + defer peer.Close() + defer fromPeer.Close() + + go func() { + c, err := l.Accept() + if err != nil { + t.Log("accept error:", err) + t.Fail() + return + } + defer c.Close() + + // Echo back until the peer closes the connection. + io.Copy(c, c) + }() + + go h(fromPeer) + if _, err := peer.Write([]byte(msg)); err != nil { + t.Fatal(err) + } + buf := make([]byte, 1024) + n, err := peer.Read(buf) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != msg { + t.Fatalf("expected '%s', got '%s'", msg, string(buf[:n])) + } + }) + } +} + func TestGetServeHandler(t *testing.T) { const serverName = "example.ts.net" conf1 := &ipn.ServeConfig{ diff --git a/ipn/serve.go b/ipn/serve.go index 7477c3ba2..f9d1f709e 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -700,6 +700,7 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error { // - https-insecure://localhost:3000 // - https-insecure://localhost:3000/foo // - https://tailscale.com +// - unix://my-socket.sock func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultScheme string) (string, error) { const host = "127.0.0.1" diff --git a/net/netns/netns_darwin.go b/net/netns/netns_darwin.go index ff05a3f31..3cf30a5e3 100644 --- a/net/netns/netns_darwin.go +++ b/net/netns/netns_darwin.go @@ -45,6 +45,11 @@ func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address stri return nil } + // Unix sockets cannot be bound to an interface. + if network == "unix" { + return nil + } + idx, err := getInterfaceIndex(logf, netMon, address) if err != nil { // callee logged