diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 550c26a36..3654faa87 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -209,7 +209,6 @@ type LocalBackend struct { ccGen clientGen // function for producing controlclient; lazily populated sshServer SSHServer // or nil, initialized lazily. appConnector *appc.AppConnector // or nil, initialized when configured. - webClient webClient notify func(ipn.Notify) cc controlclient.Client ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto @@ -273,7 +272,10 @@ type LocalBackend struct { serveConfig ipn.ServeConfigView // or !Valid if none activeWatchSessions set.Set[string] // of WatchIPN SessionID - serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener + webClient webClient + webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic + + serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy // statusLock must be held before calling statusChanged.Wait() or @@ -4491,6 +4493,11 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. } if b.ShouldRunWebClient() { handlePorts = append(handlePorts, webClientPort) + + // don't listen on netmap addresses if we're in userspace mode + if !b.sys.IsNetstack() { + b.updateWebClientListenersLocked() + } } b.reloadServeConfigLocked(prefs) diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 60dbaeff4..95407a3a8 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -56,16 +56,17 @@ type serveHTTPContext struct { DestPort uint16 } -// serveListener is the state of host-level net.Listen for a specific (Tailscale IP, serve port) +// localListener is the state of host-level net.Listen for a specific (Tailscale IP, port) // combination. If there are two TailscaleIPs (v4 and v6) and three ports being served, // then there will be six of these active and looping in their Run method. // // This is not used in userspace-networking mode. // -// Most serve traffic is intercepted by netstack. This exists purely for connections -// from the machine itself, as that goes via the kernel, so we need to be in the -// kernel's listening/routing tables. -type serveListener struct { +// localListener is used by tailscale serve (TCP only) as well as the built-in web client. +// Most serve traffic and peer traffic for the web client are intercepted by netstack. +// This listener exists purely for connections from the machine itself, as that goes via the kernel, +// so we need to be in the kernel's listening/routing tables. +type localListener struct { b *LocalBackend ap netip.AddrPort ctx context.Context // valid while listener is desired @@ -73,24 +74,35 @@ type serveListener struct { logf logger.Logf bo *backoff.Backoff // for retrying failed Listen calls + handler func(net.Conn) error // handler for inbound connections closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any } -func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *serveListener { +func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener { ctx, cancel := context.WithCancel(ctx) - return &serveListener{ + return &localListener{ b: b, ap: ap, ctx: ctx, cancel: cancel, logf: logf, + handler: func(conn net.Conn) error { + srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort() + handler := b.tcpHandlerForServe(ap.Port(), srcAddr) + if handler == nil { + b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, ap.Port()) + conn.Close() + } + return nil + }, bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second), } + } // Close cancels the context and closes the listener, if any. -func (s *serveListener) Close() error { +func (s *localListener) Close() error { s.cancel() if close, ok := s.closeListener.LoadOk(); ok { s.closeListener.Store(nil) @@ -99,10 +111,10 @@ func (s *serveListener) Close() error { return nil } -// Run starts a net.Listen for the serveListener's address and port. +// Run starts a net.Listen for the localListener's address and port. // If unable to listen, it retries with exponential backoff. // Listen is retried until the context is canceled. -func (s *serveListener) Run() { +func (s *localListener) Run() { for { ip := s.ap.Addr() ipStr := ip.String() @@ -115,7 +127,7 @@ func (s *serveListener) Run() { // a specific interface. Without this hook, the system // chooses a default interface to bind to. if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil { - s.logf("serve failed to init listen config %v, backing off: %v", s.ap, err) + s.logf("localListener failed to init listen config %v, backing off: %v", s.ap, err) s.bo.BackOff(s.ctx, err) continue } @@ -138,26 +150,26 @@ func (s *serveListener) Run() { ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port()))) if err != nil { if s.shouldWarnAboutListenError(err) { - s.logf("serve failed to listen on %v, backing off: %v", s.ap, err) + s.logf("localListener failed to listen on %v, backing off: %v", s.ap, err) } s.bo.BackOff(s.ctx, err) continue } s.closeListener.Store(ln.Close) - s.logf("serve listening on %v", s.ap) - err = s.handleServeListenersAccept(ln) + s.logf("listening on %v", s.ap) + err = s.handleListenersAccept(ln) if s.ctx.Err() != nil { // context canceled, we're done return } if err != nil { - s.logf("serve listener accept error, retrying: %v", err) + s.logf("localListener accept error, retrying: %v", err) } } } -func (s *serveListener) shouldWarnAboutListenError(err error) bool { +func (s *localListener) shouldWarnAboutListenError(err error) bool { if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) { // Machine likely doesn't have IPv6 enabled (or the IP is still being // assigned). No need to warn. Notably, WSL2 (Issue 6303). @@ -168,23 +180,17 @@ func (s *serveListener) shouldWarnAboutListenError(err error) bool { return true } -// handleServeListenersAccept accepts connections for the Listener. It calls the +// handleListenersAccept accepts connections for the Listener. It calls the // handler in a new goroutine for each accepted connection. This is used to -// handle local "tailscale serve" traffic originating from the machine itself. -func (s *serveListener) handleServeListenersAccept(ln net.Listener) error { +// handle local "tailscale serve" and web client traffic originating from the +// machine itself. +func (s *localListener) handleListenersAccept(ln net.Listener) error { for { conn, err := ln.Accept() if err != nil { return err } - srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort() - handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr) - if handler == nil { - s.b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, s.ap.Port()) - conn.Close() - continue - } - go handler(conn) + go s.handler(conn) } } diff --git a/ipn/ipnlocal/web_client.go b/ipn/ipnlocal/web_client.go index 4cb25174e..8180f929c 100644 --- a/ipn/ipnlocal/web_client.go +++ b/ipn/ipnlocal/web_client.go @@ -6,14 +6,20 @@ package ipnlocal import ( + "context" "errors" "fmt" "net" "net/http" + "net/netip" + "time" "tailscale.com/client/tailscale" "tailscale.com/client/web" + "tailscale.com/logtail/backoff" "tailscale.com/net/netutil" + "tailscale.com/types/logger" + "tailscale.com/util/mak" ) const webClientPort = web.ListenPort @@ -73,6 +79,9 @@ func (b *LocalBackend) WebClientShutdown() { b.mu.Lock() server := b.webClient.server b.webClient.server = nil + for _, ln := range b.webClientListeners { + ln.Close() + } b.mu.Unlock() // release lock before shutdown if server != nil { server.Shutdown() @@ -88,3 +97,41 @@ func (b *LocalBackend) handleWebClientConn(c net.Conn) error { s := http.Server{Handler: b.webClient.server} return s.Serve(netutil.NewOneConnListener(c, nil)) } + +// updateWebClientListenersLocked creates listeners on the web client port (5252) +// for each of the local device's Tailscale IP addresses. This is needed to properly +// route local traffic when using kernel networking mode. +func (b *LocalBackend) updateWebClientListenersLocked() { + if b.netMap == nil { + return + } + + addrs := b.netMap.GetAddresses() + for i := range addrs.LenIter() { + addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort) + if _, ok := b.webClientListeners[addrPort]; ok { + continue // already listening + } + + sl := b.newWebClientListener(context.Background(), addrPort, b.logf) + mak.Set(&b.webClientListeners, addrPort, sl) + + go sl.Run() + } +} + +// newWebClientListener returns a listener for local connections to the built-in web client +// used to manage this Tailscale instance. +func (b *LocalBackend) newWebClientListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener { + ctx, cancel := context.WithCancel(ctx) + return &localListener{ + b: b, + ap: ap, + ctx: ctx, + cancel: cancel, + logf: logf, + + handler: b.handleWebClientConn, + bo: backoff.NewBackoff("webclient-listener", logf, 30*time.Second), + } +} diff --git a/ipn/ipnlocal/web_client_stub.go b/ipn/ipnlocal/web_client_stub.go index b9e8ce919..8bfba1d0c 100644 --- a/ipn/ipnlocal/web_client_stub.go +++ b/ipn/ipnlocal/web_client_stub.go @@ -27,3 +27,4 @@ func (b *LocalBackend) WebClientShutdown() {} func (b *LocalBackend) handleWebClientConn(c net.Conn) error { return errors.New("not implemented") } +func (b *LocalBackend) updateWebClientListenersLocked() {}