mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-07 08:44:38 +00:00
ipn/ipnlocal: allow connecting to local web client
The local web client has the same characteristic as tailscale serve, in that it needs a local listener to allow for connections from the local machine itself when running in kernel networking mode. This change renames and adapts the existing serveListener to allow it to be used by the web client as well. Updates tailscale/corp#14335 Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
parent
d36a0d42aa
commit
09de240934
@ -209,7 +209,6 @@ type LocalBackend struct {
|
|||||||
ccGen clientGen // function for producing controlclient; lazily populated
|
ccGen clientGen // function for producing controlclient; lazily populated
|
||||||
sshServer SSHServer // or nil, initialized lazily.
|
sshServer SSHServer // or nil, initialized lazily.
|
||||||
appConnector *appc.AppConnector // or nil, initialized when configured.
|
appConnector *appc.AppConnector // or nil, initialized when configured.
|
||||||
webClient webClient
|
|
||||||
notify func(ipn.Notify)
|
notify func(ipn.Notify)
|
||||||
cc controlclient.Client
|
cc controlclient.Client
|
||||||
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
||||||
@ -273,7 +272,10 @@ type LocalBackend struct {
|
|||||||
serveConfig ipn.ServeConfigView // or !Valid if none
|
serveConfig ipn.ServeConfigView // or !Valid if none
|
||||||
activeWatchSessions set.Set[string] // of WatchIPN SessionID
|
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
|
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
||||||
|
|
||||||
// statusLock must be held before calling statusChanged.Wait() or
|
// statusLock must be held before calling statusChanged.Wait() or
|
||||||
@ -4491,6 +4493,11 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
|||||||
}
|
}
|
||||||
if b.ShouldRunWebClient() {
|
if b.ShouldRunWebClient() {
|
||||||
handlePorts = append(handlePorts, webClientPort)
|
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)
|
b.reloadServeConfigLocked(prefs)
|
||||||
|
@ -56,16 +56,17 @@ type serveHTTPContext struct {
|
|||||||
DestPort uint16
|
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,
|
// 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.
|
// then there will be six of these active and looping in their Run method.
|
||||||
//
|
//
|
||||||
// This is not used in userspace-networking mode.
|
// This is not used in userspace-networking mode.
|
||||||
//
|
//
|
||||||
// Most serve traffic is intercepted by netstack. This exists purely for connections
|
// localListener is used by tailscale serve (TCP only) as well as the built-in web client.
|
||||||
// from the machine itself, as that goes via the kernel, so we need to be in the
|
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
|
||||||
// kernel's listening/routing tables.
|
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
|
||||||
type serveListener struct {
|
// so we need to be in the kernel's listening/routing tables.
|
||||||
|
type localListener struct {
|
||||||
b *LocalBackend
|
b *LocalBackend
|
||||||
ap netip.AddrPort
|
ap netip.AddrPort
|
||||||
ctx context.Context // valid while listener is desired
|
ctx context.Context // valid while listener is desired
|
||||||
@ -73,24 +74,35 @@ type serveListener struct {
|
|||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
bo *backoff.Backoff // for retrying failed Listen calls
|
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
|
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)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
return &serveListener{
|
return &localListener{
|
||||||
b: b,
|
b: b,
|
||||||
ap: ap,
|
ap: ap,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
logf: logf,
|
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),
|
bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close cancels the context and closes the listener, if any.
|
// Close cancels the context and closes the listener, if any.
|
||||||
func (s *serveListener) Close() error {
|
func (s *localListener) Close() error {
|
||||||
s.cancel()
|
s.cancel()
|
||||||
if close, ok := s.closeListener.LoadOk(); ok {
|
if close, ok := s.closeListener.LoadOk(); ok {
|
||||||
s.closeListener.Store(nil)
|
s.closeListener.Store(nil)
|
||||||
@ -99,10 +111,10 @@ func (s *serveListener) Close() error {
|
|||||||
return nil
|
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.
|
// If unable to listen, it retries with exponential backoff.
|
||||||
// Listen is retried until the context is canceled.
|
// Listen is retried until the context is canceled.
|
||||||
func (s *serveListener) Run() {
|
func (s *localListener) Run() {
|
||||||
for {
|
for {
|
||||||
ip := s.ap.Addr()
|
ip := s.ap.Addr()
|
||||||
ipStr := ip.String()
|
ipStr := ip.String()
|
||||||
@ -115,7 +127,7 @@ func (s *serveListener) Run() {
|
|||||||
// a specific interface. Without this hook, the system
|
// a specific interface. Without this hook, the system
|
||||||
// chooses a default interface to bind to.
|
// chooses a default interface to bind to.
|
||||||
if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil {
|
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)
|
s.bo.BackOff(s.ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -138,26 +150,26 @@ func (s *serveListener) Run() {
|
|||||||
ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
|
ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s.shouldWarnAboutListenError(err) {
|
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)
|
s.bo.BackOff(s.ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.closeListener.Store(ln.Close)
|
s.closeListener.Store(ln.Close)
|
||||||
|
|
||||||
s.logf("serve listening on %v", s.ap)
|
s.logf("listening on %v", s.ap)
|
||||||
err = s.handleServeListenersAccept(ln)
|
err = s.handleListenersAccept(ln)
|
||||||
if s.ctx.Err() != nil {
|
if s.ctx.Err() != nil {
|
||||||
// context canceled, we're done
|
// context canceled, we're done
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
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()) {
|
if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) {
|
||||||
// Machine likely doesn't have IPv6 enabled (or the IP is still being
|
// Machine likely doesn't have IPv6 enabled (or the IP is still being
|
||||||
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
|
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
|
||||||
@ -168,23 +180,17 @@ func (s *serveListener) shouldWarnAboutListenError(err error) bool {
|
|||||||
return true
|
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
|
// handler in a new goroutine for each accepted connection. This is used to
|
||||||
// handle local "tailscale serve" traffic originating from the machine itself.
|
// handle local "tailscale serve" and web client traffic originating from the
|
||||||
func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
// machine itself.
|
||||||
|
func (s *localListener) handleListenersAccept(ln net.Listener) error {
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
|
go s.handler(conn)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,14 +6,20 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/client/web"
|
"tailscale.com/client/web"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
const webClientPort = web.ListenPort
|
const webClientPort = web.ListenPort
|
||||||
@ -73,6 +79,9 @@ func (b *LocalBackend) WebClientShutdown() {
|
|||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
server := b.webClient.server
|
server := b.webClient.server
|
||||||
b.webClient.server = nil
|
b.webClient.server = nil
|
||||||
|
for _, ln := range b.webClientListeners {
|
||||||
|
ln.Close()
|
||||||
|
}
|
||||||
b.mu.Unlock() // release lock before shutdown
|
b.mu.Unlock() // release lock before shutdown
|
||||||
if server != nil {
|
if server != nil {
|
||||||
server.Shutdown()
|
server.Shutdown()
|
||||||
@ -88,3 +97,41 @@ func (b *LocalBackend) handleWebClientConn(c net.Conn) error {
|
|||||||
s := http.Server{Handler: b.webClient.server}
|
s := http.Server{Handler: b.webClient.server}
|
||||||
return s.Serve(netutil.NewOneConnListener(c, nil))
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,3 +27,4 @@ func (b *LocalBackend) WebClientShutdown() {}
|
|||||||
func (b *LocalBackend) handleWebClientConn(c net.Conn) error {
|
func (b *LocalBackend) handleWebClientConn(c net.Conn) error {
|
||||||
return errors.New("not implemented")
|
return errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
func (b *LocalBackend) updateWebClientListenersLocked() {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user