From 7330aa593e3701cb193ceff0bc26eda3bc62df83 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Mon, 17 Apr 2023 16:01:41 -0700 Subject: [PATCH] all: avoid repeated default interface lookups On some platforms (notably macOS and iOS) we look up the default interface to bind outgoing connections to. This is both duplicated work and results in logspam when the default interface is not available (i.e. when a phone has no connectivity, we log an error and thus cause more things that we will try to upload and fail). Fixed by passing around a netmon.Monitor to more places, so that we can use its cached interface state. Fixes #7850 Updates #7621 Signed-off-by: Mihai Parparita --- cmd/derper/depaware.txt | 2 +- cmd/tailscale/cli/netcheck.go | 8 +++++- cmd/tailscale/depaware.txt | 2 +- cmd/tailscaled/debug.go | 4 +-- cmd/tailscaled/tailscaled.go | 25 +++++++++-------- cmd/tailscaled/tailscaled_windows.go | 8 +++++- cmd/tsconnect/wasm/wasm_js.go | 2 +- control/controlclient/direct.go | 5 ++-- control/controlclient/noise.go | 12 +++++++- control/controlclient/noise_test.go | 2 +- control/controlhttp/client.go | 4 ++- control/controlhttp/constants.go | 3 ++ derp/derphttp/derphttp_client.go | 10 +++++-- ipn/ipnlocal/local.go | 2 +- ipn/ipnserver/proxyconnect.go | 3 +- ipn/ipnserver/server.go | 9 ++++-- ipn/localapi/debugderp.go | 4 +-- ipn/localapi/localapi.go | 9 ++++-- log/sockstatlog/logger.go | 6 ++-- log/sockstatlog/logger_test.go | 2 +- logpolicy/logpolicy.go | 35 +++++++++++++++-------- net/dns/manager.go | 1 + net/dns/resolver/forwarder.go | 3 +- net/dns/resolver/macios_ext.go | 4 +-- net/dns/resolver/tsdns_test.go | 2 +- net/dnscache/dnscache.go | 6 ++++ net/dnsfallback/dnsfallback.go | 16 +++++++---- net/netcheck/netcheck.go | 32 +++++++++++++++------ net/netns/netns.go | 16 +++++++---- net/netns/netns_android.go | 3 +- net/netns/netns_darwin.go | 42 ++++++++++++++++++---------- net/netns/netns_darwin_test.go | 4 +-- net/netns/netns_default.go | 3 +- net/netns/netns_linux.go | 3 +- net/netns/netns_test.go | 2 +- net/netns/netns_windows.go | 3 +- net/ping/ping.go | 10 ++++--- net/portmapper/igd_test.go | 2 +- net/portmapper/portmapper.go | 10 +++++-- net/portmapper/portmapper_test.go | 6 ++-- net/portmapper/upnp.go | 2 +- net/sockstats/sockstats.go | 4 +-- net/sockstats/sockstats_noop.go | 2 +- net/sockstats/sockstats_tsgo.go | 6 ++-- net/tsdial/tsdial.go | 6 ++-- prober/derp.go | 2 +- tsnet/tsnet.go | 6 ++-- wgengine/magicsock/magicsock.go | 7 +++-- wgengine/netlog/logger.go | 6 ++-- wgengine/userspace.go | 2 +- 50 files changed, 242 insertions(+), 126 deletions(-) diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 7e82305d1..b40ca360a 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -86,7 +86,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa 💣 tailscale.com/net/interfaces from tailscale.com/net/netns+ tailscale.com/net/netaddr from tailscale.com/ipn+ tailscale.com/net/netknob from tailscale.com/net/netns - tailscale.com/net/netmon from tailscale.com/net/sockstats + tailscale.com/net/netmon from tailscale.com/net/sockstats+ tailscale.com/net/netns from tailscale.com/derp/derphttp tailscale.com/net/netutil from tailscale.com/client/tailscale tailscale.com/net/packet from tailscale.com/wgengine/filter diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index 0c2f7acd1..bac5a0a48 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -19,6 +19,7 @@ import ( "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/net/netcheck" + "tailscale.com/net/netmon" "tailscale.com/net/portmapper" "tailscale.com/tailcfg" "tailscale.com/types/logger" @@ -45,9 +46,14 @@ var netcheckArgs struct { } func runNetcheck(ctx context.Context, args []string) error { + logf := logger.WithPrefix(log.Printf, "portmap: ") + netMon, err := netmon.New(logf) + if err != nil { + return err + } c := &netcheck.Client{ UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"), - PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil, nil), + PortMapper: portmapper.NewClient(logf, netMon, nil, nil), UseDNSCache: false, // always resolve, don't cache } if netcheckArgs.verbose { diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index b146ec567..5b644992e 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -74,7 +74,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli tailscale.com/net/neterror from tailscale.com/net/netcheck+ tailscale.com/net/netknob from tailscale.com/net/netns - tailscale.com/net/netmon from tailscale.com/net/sockstats + tailscale.com/net/netmon from tailscale.com/net/sockstats+ tailscale.com/net/netns from tailscale.com/derp/derphttp+ tailscale.com/net/netutil from tailscale.com/client/tailscale+ tailscale.com/net/packet from tailscale.com/wgengine/filter+ diff --git a/cmd/tailscaled/debug.go b/cmd/tailscaled/debug.go index 8c1ad88b1..b6a95ba49 100644 --- a/cmd/tailscaled/debug.go +++ b/cmd/tailscaled/debug.go @@ -193,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) { priv1 := key.NewNode() priv2 := key.NewNode() - c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion) - c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion) + c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion) + c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion) defer func() { if err != nil { c1.Close() diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 3bae560a2..d2c28de99 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -329,7 +329,15 @@ var logPol *logpolicy.Policy var debugMux *http.ServeMux func run() error { - pol := logpolicy.New(logtail.CollectionNode) + var logf logger.Logf = log.Printf + netMon, err := netmon.New(func(format string, args ...any) { + logf(format, args...) + }) + if err != nil { + return fmt.Errorf("netmon.New: %w", err) + } + + pol := logpolicy.New(logtail.CollectionNode, netMon) pol.SetVerbosityLevel(args.verbose) logPol = pol defer func() { @@ -353,7 +361,6 @@ func run() error { return nil } - var logf logger.Logf = log.Printf if envknob.Bool("TS_DEBUG_MEMORY") { logf = logger.RusagePrefixLog(logf) } @@ -379,10 +386,10 @@ func run() error { debugMux = newDebugMux() } - return startIPNServer(context.Background(), logf, pol.PublicID) + return startIPNServer(context.Background(), logf, pol.PublicID, netMon) } -func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) error { +func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) error { ln, err := safesocket.Listen(args.socketpath) if err != nil { return fmt.Errorf("safesocket.Listen: %v", err) @@ -408,7 +415,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) } }() - srv := ipnserver.New(logf, logID) + srv := ipnserver.New(logf, logID, netMon) if debugMux != nil { debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus) } @@ -426,7 +433,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) return } } - lb, err := getLocalBackend(ctx, logf, logID) + lb, err := getLocalBackend(ctx, logf, logID, netMon) if err == nil { logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond)) srv.SetLocalBackend(lb) @@ -450,11 +457,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) return nil } -func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID) (_ *ipnlocal.LocalBackend, retErr error) { - netMon, err := netmon.New(logf) - if err != nil { - return nil, fmt.Errorf("netmon.New: %w", err) - } +func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (_ *ipnlocal.LocalBackend, retErr error) { if logPol != nil { logPol.Logtail.SetNetMon(netMon) } diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index a654dadf4..077d3f99f 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -45,6 +45,7 @@ import ( "tailscale.com/logpolicy" "tailscale.com/logtail/backoff" "tailscale.com/net/dns" + "tailscale.com/net/netmon" "tailscale.com/net/tstun" "tailscale.com/types/logger" "tailscale.com/types/logid" @@ -291,8 +292,13 @@ func beWindowsSubprocess() bool { } }() + netMon, err := netmon.New(log.Printf) + if err != nil { + log.Printf("Could not create netMon: %v", err) + netMon = nil + } publicLogID, _ := logid.ParsePublicID(logID) - err := startIPNServer(ctx, log.Printf, publicLogID) + err = startIPNServer(ctx, log.Printf, publicLogID, netMon) if err != nil { log.Fatalf("ipnserver: %v", err) } diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index f94b850a2..a1b0fd95e 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -123,7 +123,7 @@ func newIPN(jsConfig js.Value) map[string]any { } logid := lpc.PublicID - srv := ipnserver.New(logf, logid) + srv := ipnserver.New(logf, logid, nil /* no netMon */) lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral) if err != nil { log.Fatalf("ipnlocal.NewLocalBackend: %v", err) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 85bd7f07b..cf6fd9987 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -211,8 +211,9 @@ func NewDirect(opts Options) (*Direct, error) { dnsCache := &dnscache.Resolver{ Forward: dnscache.Get().Forward, // use default cache's forwarder UseLastGood: true, - LookupIPFallback: dnsfallback.Lookup(opts.Logf), + LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, opts.NetMon), Logf: opts.Logf, + NetMon: opts.NetMon, } tr := http.DefaultTransport.(*http.Transport).Clone() tr.Proxy = tshttpproxy.ProxyFromEnvironment @@ -1508,7 +1509,7 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) { return nil, err } c.logf("creating new noise client") - nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp) + nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, c.logf, c.netMon, dp) if err != nil { return nil, err } diff --git a/control/controlclient/noise.go b/control/controlclient/noise.go index 826b0c249..61c472a35 100644 --- a/control/controlclient/noise.go +++ b/control/controlclient/noise.go @@ -19,9 +19,11 @@ import ( "golang.org/x/net/http2" "tailscale.com/control/controlbase" "tailscale.com/control/controlhttp" + "tailscale.com/net/netmon" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/types/key" + "tailscale.com/types/logger" "tailscale.com/util/mak" "tailscale.com/util/multierr" "tailscale.com/util/singleflight" @@ -167,6 +169,9 @@ type NoiseClient struct { // be nil. dialPlan func() *tailcfg.ControlDialPlan + logf logger.Logf + netMon *netmon.Monitor + // mu only protects the following variables. mu sync.Mutex last *noiseConn // or nil @@ -177,8 +182,9 @@ type NoiseClient struct { // NewNoiseClient returns a new noiseClient for the provided server and machine key. // serverURL is of the form https://: (no trailing slash). // +// netMon may be nil, if non-nil it's used to do faster interface lookups. // dialPlan may be nil -func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) { +func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, logf logger.Logf, netMon *netmon.Monitor, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) { u, err := url.Parse(serverURL) if err != nil { return nil, err @@ -207,6 +213,8 @@ func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, httpsPort: httpsPort, dialer: dialer, dialPlan: dialPlan, + logf: logf, + netMon: netMon, } // Create the HTTP/2 Transport using a net/http.Transport @@ -366,6 +374,8 @@ func (nc *NoiseClient) dial() (*noiseConn, error) { ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion), Dialer: nc.dialer.SystemDial, DialPlan: dialPlan, + Logf: nc.logf, + NetMon: nc.netMon, }).Dial(ctx) if err != nil { return nil, err diff --git a/control/controlclient/noise_test.go b/control/controlclient/noise_test.go index 049e260fe..11e35f0af 100644 --- a/control/controlclient/noise_test.go +++ b/control/controlclient/noise_test.go @@ -74,7 +74,7 @@ func (tt noiseClientTest) run(t *testing.T) { defer hs.Close() dialer := new(tsdial.Dialer) - nc, err := NewNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil) + nc, err := NewNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil, nil, nil) if err != nil { t.Fatal(err) } diff --git a/control/controlhttp/client.go b/control/controlhttp/client.go index 02b5a7821..d04aac518 100644 --- a/control/controlhttp/client.go +++ b/control/controlhttp/client.go @@ -389,13 +389,15 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, SingleHostStaticResult: []netip.Addr{addr}, SingleHost: u.Hostname(), Logf: a.Logf, // not a.logf method; we want to propagate nil-ness + NetMon: a.NetMon, } } else { dns = &dnscache.Resolver{ Forward: dnscache.Get().Forward, - LookupIPFallback: dnsfallback.Lookup(a.logf), + LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon), UseLastGood: true, Logf: a.Logf, // not a.logf method; we want to propagate nil-ness + NetMon: a.NetMon, } } diff --git a/control/controlhttp/constants.go b/control/controlhttp/constants.go index 045eeba7f..a58ee5374 100644 --- a/control/controlhttp/constants.go +++ b/control/controlhttp/constants.go @@ -9,6 +9,7 @@ import ( "time" "tailscale.com/net/dnscache" + "tailscale.com/net/netmon" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -70,6 +71,8 @@ type Dialer struct { // dropped. Logf logger.Logf + NetMon *netmon.Monitor + // DialPlan, if set, contains instructions from the control server on // how to connect to it. If present, we will try the methods in this // plan before falling back to DNS. diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index 349838dfa..2ea86b686 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -31,6 +31,7 @@ import ( "tailscale.com/derp" "tailscale.com/envknob" "tailscale.com/net/dnscache" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/sockstats" "tailscale.com/net/tlsdial" @@ -55,6 +56,7 @@ type Client struct { privateKey key.NodePrivate logf logger.Logf + netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand dialer func(ctx context.Context, network, addr string) (net.Conn, error) // Either url or getRegion is non-nil: @@ -88,11 +90,13 @@ func (c *Client) String() string { // NewRegionClient returns a new DERP-over-HTTP client. It connects lazily. // To trigger a connection, use Connect. -func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client { ctx, cancel := context.WithCancel(context.Background()) c := &Client{ privateKey: privateKey, logf: logf, + netMon: netMon, getRegion: getRegion, ctx: ctx, cancelCtx: cancel, @@ -492,7 +496,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) { return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url))) } hostOrIP := host - dialer := netns.NewDialer(c.logf) + dialer := netns.NewDialer(c.logf, c.netMon) if c.DNSCache != nil { ip, _, _, err := c.DNSCache.LookupIP(ctx, host) @@ -587,7 +591,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl } func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) { - return netns.NewDialer(c.logf).DialContext(ctx, proto, addr) + return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr) } // shouldDialProto reports whether an explicitly provided IPv4 or IPv6 diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 532e68d19..d1aaf6b89 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -313,7 +313,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor loginFlags: loginFlags, } - b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID) + b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, e.GetNetMon()) if err != nil { log.Printf("error setting up sockstat logger: %v", err) } diff --git a/ipn/ipnserver/proxyconnect.go b/ipn/ipnserver/proxyconnect.go index 8f330add1..5ac7e89db 100644 --- a/ipn/ipnserver/proxyconnect.go +++ b/ipn/ipnserver/proxyconnect.go @@ -37,7 +37,8 @@ func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) return } - back, err := logpolicy.DialContext(ctx, "tcp", hostPort) + dialContext := logpolicy.MakeDialFunc(s.netMon) + back, err := dialContext(ctx, "tcp", hostPort) if err != nil { s.logf("error CONNECT dialing %v: %v", hostPort, err) http.Error(w, "Connect failure", http.StatusBadGateway) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 0123d9e9e..3abfa9364 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -24,6 +24,7 @@ import ( "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/localapi" + "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/util/mak" @@ -36,6 +37,7 @@ import ( type Server struct { lb atomic.Pointer[ipnlocal.LocalBackend] logf logger.Logf + netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand backendLogID logid.PublicID // resetOnZero is whether to call bs.Reset on transition from // 1->0 active HTTP requests. That is, this is whether the backend is @@ -197,7 +199,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { defer onDone() if strings.HasPrefix(r.URL.Path, "/localapi/") { - lah := localapi.NewHandler(lb, s.logf, s.backendLogID) + lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID) lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci) lah.PermitCert = s.connCanFetchCerts(ci) lah.ServeHTTP(w, r) @@ -408,15 +410,18 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit } // New returns a new Server. +// The netMon parameter is optional; if non-nil it's used to do faster interface +// lookups. // // To start it, use the Server.Run method. // // At some point, either before or after Run, the Server's SetLocalBackend // method must also be called before Server can do anything useful. -func New(logf logger.Logf, logID logid.PublicID) *Server { +func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server { return &Server{ backendLogID: logID, logf: logf, + netMon: netMon, resetOnZero: envknob.GOOS() == "windows", } } diff --git a/ipn/localapi/debugderp.go b/ipn/localapi/debugderp.go index f7ae88787..af2527ee8 100644 --- a/ipn/localapi/debugderp.go +++ b/ipn/localapi/debugderp.go @@ -140,7 +140,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) { } checkSTUN4 := func(derpNode *tailcfg.DERPNode) { - u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf)).ListenPacket(ctx, "udp4", ":0") + u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf, h.netMon)).ListenPacket(ctx, "udp4", ":0") if err != nil { st.Errors = append(st.Errors, fmt.Sprintf("Error creating IPv4 STUN listener: %v", err)) return @@ -249,7 +249,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) { serverPubKeys := make(map[key.NodePublic]bool) for i := 0; i < 5; i++ { func() { - rc := derphttp.NewRegionClient(fakePrivKey, h.logf, func() *tailcfg.DERPRegion { + rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.netMon, func() *tailcfg.DERPRegion { return &tailcfg.DERPRegion{ RegionID: reg.RegionID, RegionCode: reg.RegionCode, diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index be15cbe08..28371a45d 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -125,8 +125,10 @@ var ( metrics = map[string]*clientmetric.Metric{} ) -func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID logid.PublicID) *Handler { - return &Handler{b: b, logf: logf, backendLogID: logID} +// NewHandler creates a new LocalAPI HTTP handler. All parameters except netMon +// are required (if non-nil it's used to do faster interface lookups). +func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, netMon *netmon.Monitor, logID logid.PublicID) *Handler { + return &Handler{b: b, logf: logf, netMon: netMon, backendLogID: logID} } type Handler struct { @@ -150,6 +152,7 @@ type Handler struct { b *ipnlocal.LocalBackend logf logger.Logf + netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand backendLogID logid.PublicID } @@ -679,7 +682,7 @@ func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) { done := make(chan bool, 1) var c *portmapper.Client - c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), debugKnobs, func() { + c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), h.netMon, debugKnobs, func() { logf("portmapping changed.") logf("have mapping: %v", c.HaveMapping()) diff --git a/log/sockstatlog/logger.go b/log/sockstatlog/logger.go index 683342a08..4f522f17d 100644 --- a/log/sockstatlog/logger.go +++ b/log/sockstatlog/logger.go @@ -20,6 +20,7 @@ import ( "tailscale.com/logpolicy" "tailscale.com/logtail" "tailscale.com/logtail/filch" + "tailscale.com/net/netmon" "tailscale.com/net/sockstats" "tailscale.com/smallzstd" "tailscale.com/types/logger" @@ -92,7 +93,8 @@ func SockstatLogID(logID logid.PublicID) logid.PrivateID { // On platforms that do not support sockstat logging, a nil Logger will be returned. // The returned Logger is not yet enabled, and must be shut down with Shutdown when it is no longer needed. // Logs will be uploaded to the log server using a new log ID derived from the provided backend logID. -func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID) (*Logger, error) { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (*Logger, error) { if !sockstats.IsAvailable { return nil, nil } @@ -112,7 +114,7 @@ func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID) (*Logger, logger := &Logger{ logf: logf, filch: filch, - tr: logpolicy.NewLogtailTransport(logtail.DefaultHost), + tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon), } logger.logger = logtail.NewLogger(logtail.Config{ BaseURL: logpolicy.LogURL(), diff --git a/log/sockstatlog/logger_test.go b/log/sockstatlog/logger_test.go index e64a5de8b..1e2d67d97 100644 --- a/log/sockstatlog/logger_test.go +++ b/log/sockstatlog/logger_test.go @@ -23,7 +23,7 @@ func TestResourceCleanup(t *testing.T) { if err != nil { t.Fatal(err) } - lg, err := NewLogger(td, logger.Discard, id.Public()) + lg, err := NewLogger(td, logger.Discard, id.Public(), nil) if err != nil { t.Fatal(err) } diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index 84da5f31a..7becc934f 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -37,6 +37,7 @@ import ( "tailscale.com/net/dnscache" "tailscale.com/net/dnsfallback" "tailscale.com/net/netknob" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/tlsdial" "tailscale.com/net/tshttpproxy" @@ -450,14 +451,15 @@ func tryFixLogStateLocation(dir, cmdname string) { // New returns a new log policy (a logger and its instance ID) for a // given collection name. -func New(collection string) *Policy { - return NewWithConfigPath(collection, "", "") +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func New(collection string, netMon *netmon.Monitor) *Policy { + return NewWithConfigPath(collection, "", "", netMon) } // NewWithConfigPath is identical to New, // but uses the specified directory and command name. // If either is empty, it derives them automatically. -func NewWithConfigPath(collection, dir, cmdName string) *Policy { +func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor) *Policy { var lflags int if term.IsTerminal(2) || runtime.GOOS == "windows" { lflags = 0 @@ -554,7 +556,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy { } return w }, - HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost)}, + HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon)}, } if collection == logtail.CollectionNode { conf.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta @@ -569,7 +571,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy { log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.") conf.BaseURL = val u, _ := url.Parse(val) - conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host)} + conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon)} } filchOptions := filch.Options{ @@ -670,13 +672,22 @@ func (p *Policy) Shutdown(ctx context.Context) error { return nil } -// DialContext is a net.Dialer.DialContext specialized for use by logtail. +// MakeDialFunc creates a net.Dialer.DialContext function specialized for use +// by logtail. // It does the following: // - If DNS lookup fails, consults the bootstrap DNS list of Tailscale hostnames. // - If TLS connection fails, try again using LetsEncrypt's built-in root certificate, // for the benefit of older OS platforms which might not include it. -func DialContext(ctx context.Context, netw, addr string) (net.Conn, error) { - nd := netns.FromDialer(log.Printf, &net.Dialer{ +// +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func MakeDialFunc(netMon *netmon.Monitor) func(ctx context.Context, netw, addr string) (net.Conn, error) { + return func(ctx context.Context, netw, addr string) (net.Conn, error) { + return dialContext(ctx, netw, addr, netMon) + } +} + +func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor) (net.Conn, error) { + nd := netns.FromDialer(log.Printf, netMon, &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: netknob.PlatformTCPKeepAlive(), }) @@ -711,7 +722,8 @@ func DialContext(ctx context.Context, netw, addr string) (net.Conn, error) { dnsCache := &dnscache.Resolver{ Forward: dnscache.Get().Forward, // use default cache's forwarder UseLastGood: true, - LookupIPFallback: dnsfallback.Lookup(log.Printf), + LookupIPFallback: dnsfallback.MakeLookupFunc(log.Printf, netMon), + NetMon: netMon, } dialer := dnscache.Dialer(nd.DialContext, dnsCache) c, err = dialer(ctx, netw, addr) @@ -723,7 +735,8 @@ func DialContext(ctx context.Context, netw, addr string) (net.Conn, error) { // NewLogtailTransport returns an HTTP Transport particularly suited to uploading // logs to the given host name. See DialContext for details on how it works. -func NewLogtailTransport(host string) http.RoundTripper { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func NewLogtailTransport(host string, netMon *netmon.Monitor) http.RoundTripper { if inTest() { return noopPretendSuccessTransport{} } @@ -739,7 +752,7 @@ func NewLogtailTransport(host string) http.RoundTripper { tr.DisableCompression = true // Log whenever we dial: - tr.DialContext = DialContext + tr.DialContext = MakeDialFunc(netMon) // We're contacting exactly 1 hostname, so the default's 100 // max idle conns is very high for our needs. Even 2 is diff --git a/net/dns/manager.go b/net/dns/manager.go index e909cbe5d..d1aa73ca6 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -64,6 +64,7 @@ type Manager struct { } // NewManagers created a new manager from the given config. +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. func NewManager(logf logger.Logf, oscfg OSConfigurator, netMon *netmon.Monitor, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager { if dialer == nil { panic("nil Dialer") diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index 3373ffcf1..85670e1d6 100644 --- a/net/dns/resolver/forwarder.go +++ b/net/dns/resolver/forwarder.go @@ -380,11 +380,12 @@ func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client if err != nil { return nil, false } - nsDialer := netns.NewDialer(f.logf) + nsDialer := netns.NewDialer(f.logf, f.netMon) dialer := dnscache.Dialer(nsDialer.DialContext, &dnscache.Resolver{ SingleHost: dohURL.Hostname(), SingleHostStaticResult: allIPs, Logf: f.logf, + NetMon: f.netMon, }) c = &http.Client{ Transport: &http.Transport{ diff --git a/net/dns/resolver/macios_ext.go b/net/dns/resolver/macios_ext.go index 895b8714f..e3f979c19 100644 --- a/net/dns/resolver/macios_ext.go +++ b/net/dns/resolver/macios_ext.go @@ -17,8 +17,8 @@ func init() { initListenConfig = initListenConfigNetworkExtension } -func initListenConfigNetworkExtension(nc *net.ListenConfig, mon *netmon.Monitor, tunName string) error { - nif, ok := mon.InterfaceState().Interface[tunName] +func initListenConfigNetworkExtension(nc *net.ListenConfig, netMon *netmon.Monitor, tunName string) error { + nif, ok := netMon.InterfaceState().Interface[tunName] if !ok { return errors.New("utun not found") } diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go index ad3d36c99..8db97f49a 100644 --- a/net/dns/resolver/tsdns_test.go +++ b/net/dns/resolver/tsdns_test.go @@ -997,7 +997,7 @@ func TestMarshalResponseFormatError(t *testing.T) { func TestForwardLinkSelection(t *testing.T) { configCall := make(chan string, 1) - tstest.Replace(t, &initListenConfig, func(nc *net.ListenConfig, mon *netmon.Monitor, tunName string) error { + tstest.Replace(t, &initListenConfig, func(nc *net.ListenConfig, netMon *netmon.Monitor, tunName string) error { select { case configCall <- tunName: return nil diff --git a/net/dnscache/dnscache.go b/net/dnscache/dnscache.go index 5a5978f5f..0983ae875 100644 --- a/net/dnscache/dnscache.go +++ b/net/dnscache/dnscache.go @@ -21,6 +21,7 @@ import ( "time" "tailscale.com/envknob" + "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/util/cloudenv" "tailscale.com/util/singleflight" @@ -91,6 +92,11 @@ type Resolver struct { // be added to all log messages printed with this logger. Logf logger.Logf + // NetMon optionally provides a netmon.Monitor to use to get the current + // (cached) network interface. + // If nil, the interface will be looked up dynamically. + NetMon *netmon.Monitor + sf singleflight.Group[string, ipRes] mu sync.Mutex diff --git a/net/dnsfallback/dnsfallback.go b/net/dnsfallback/dnsfallback.go index 9ac22b9dc..5e80c79c9 100644 --- a/net/dnsfallback/dnsfallback.go +++ b/net/dnsfallback/dnsfallback.go @@ -23,6 +23,7 @@ import ( "time" "tailscale.com/atomicfile" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/tlsdial" "tailscale.com/net/tshttpproxy" @@ -31,13 +32,16 @@ import ( "tailscale.com/util/slicesx" ) -func Lookup(logf logger.Logf) func(ctx context.Context, host string) ([]netip.Addr, error) { +// MakeLookupFunc creates a function that can be used to resolve hostnames +// (e.g. as a LookupIPFallback from dnscache.Resolver). +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.Context, host string) ([]netip.Addr, error) { return func(ctx context.Context, host string) ([]netip.Addr, error) { - return lookup(ctx, host, logf) + return lookup(ctx, host, logf, netMon) } } -func lookup(ctx context.Context, host string, logf logger.Logf) ([]netip.Addr, error) { +func lookup(ctx context.Context, host string, logf logger.Logf, netMon *netmon.Monitor) ([]netip.Addr, error) { if ip, err := netip.ParseAddr(host); err == nil && ip.IsValid() { return []netip.Addr{ip}, nil } @@ -85,7 +89,7 @@ func lookup(ctx context.Context, host string, logf logger.Logf) ([]netip.Addr, e logf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host) ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() - dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host, logf) + dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host, logf, netMon) if err != nil { logf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err) continue @@ -104,8 +108,8 @@ func lookup(ctx context.Context, host string, logf logger.Logf) ([]netip.Addr, e // serverName and serverIP of are, say, "derpN.tailscale.com". // queryName is the name being sought (e.g. "controlplane.tailscale.com"), passed as hint. -func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf) (dnsMap, error) { - dialer := netns.NewDialer(logf) +func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, netMon *netmon.Monitor) (dnsMap, error) { + dialer := netns.NewDialer(logf, netMon) tr := http.DefaultTransport.(*http.Transport).Clone() tr.Proxy = tshttpproxy.ProxyFromEnvironment tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) { diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index 687aa6a8b..a61343ebe 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -29,6 +29,7 @@ import ( "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" "tailscale.com/net/neterror" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/ping" "tailscale.com/net/portmapper" @@ -158,6 +159,11 @@ type Client struct { // If nil, log.Printf is used. Logf logger.Logf + // NetMon optionally provides a netmon.Monitor to use to get the current + // (cached) network interface. + // If nil, the interface will be looked up dynamically. + NetMon *netmon.Monitor + // TimeNow, if non-nil, is used instead of time.Now. TimeNow func() time.Time @@ -864,22 +870,29 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, return c.finishAndStoreReport(rs, dm), nil } - ifState, err := interfaces.GetState() - if err != nil { - c.logf("[v1] interfaces: %v", err) - return nil, err + var ifState *interfaces.State + if c.NetMon == nil { + directState, err := interfaces.GetState() + if err != nil { + c.logf("[v1] interfaces: %v", err) + return nil, err + } else { + ifState = directState + } + } else { + ifState = c.NetMon.InterfaceState() } // See if IPv6 works at all, or if it's been hard disabled at the // OS level. - v6udp, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf)).ListenPacket(ctx, "udp6", "[::1]:0") + v6udp, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, c.NetMon)).ListenPacket(ctx, "udp6", "[::1]:0") if err == nil { rs.report.OSHasIPv6 = true v6udp.Close() } // Create a UDP4 socket used for sending to our discovered IPv4 address. - rs.pc4Hair, err = nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf)).ListenPacket(ctx, "udp4", ":0") + rs.pc4Hair, err = nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, c.NetMon)).ListenPacket(ctx, "udp4", ":0") if err != nil { c.logf("udp4: %v", err) return nil, err @@ -909,7 +922,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, if f := c.GetSTUNConn4; f != nil { rs.pc4 = f() } else { - u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf)).ListenPacket(ctx, "udp4", c.udpBindAddr()) + u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, nil)).ListenPacket(ctx, "udp4", c.udpBindAddr()) if err != nil { c.logf("udp4: %v", err) return nil, err @@ -922,7 +935,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report, if f := c.GetSTUNConn6; f != nil { rs.pc6 = f() } else { - u6, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf)).ListenPacket(ctx, "udp6", c.udpBindAddr()) + u6, err := nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, nil)).ListenPacket(ctx, "udp6", c.udpBindAddr()) if err != nil { c.logf("udp6: %v", err) } else { @@ -1297,7 +1310,7 @@ func (c *Client) measureAllICMPLatency(ctx context.Context, rs *reportState, nee ctx, done := context.WithTimeout(ctx, icmpProbeTimeout) defer done() - p, err := ping.New(ctx, c.logf) + p, err := ping.New(ctx, c.logf, c.NetMon) if err != nil { return err } @@ -1635,6 +1648,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP Forward: net.DefaultResolver, UseLastGood: true, Logf: c.logf, + NetMon: c.NetMon, } } resolver := c.resolver diff --git a/net/netns/netns.go b/net/netns/netns.go index 86aafcac2..2acb15129 100644 --- a/net/netns/netns.go +++ b/net/netns/netns.go @@ -20,6 +20,7 @@ import ( "sync/atomic" "tailscale.com/net/netknob" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -55,19 +56,21 @@ func SetDisableBindConnToInterface(v bool) { // Listener returns a new net.Listener with its Control hook func // initialized as necessary to run in logical network namespace that // doesn't route back into Tailscale. -func Listener(logf logger.Logf) *net.ListenConfig { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func Listener(logf logger.Logf, netMon *netmon.Monitor) *net.ListenConfig { if disabled.Load() { return new(net.ListenConfig) } - return &net.ListenConfig{Control: control(logf)} + return &net.ListenConfig{Control: control(logf, netMon)} } // NewDialer returns a new Dialer using a net.Dialer with its Control // hook func initialized as necessary to run in a logical network // namespace that doesn't route back into Tailscale. It also handles // using a SOCKS if configured in the environment with ALL_PROXY. -func NewDialer(logf logger.Logf) Dialer { - return FromDialer(logf, &net.Dialer{ +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func NewDialer(logf logger.Logf, netMon *netmon.Monitor) Dialer { + return FromDialer(logf, netMon, &net.Dialer{ KeepAlive: netknob.PlatformTCPKeepAlive(), }) } @@ -76,11 +79,12 @@ func NewDialer(logf logger.Logf) Dialer { // network namespace that doesn't route back into Tailscale. It also // handles using a SOCKS if configured in the environment with // ALL_PROXY. -func FromDialer(logf logger.Logf, d *net.Dialer) Dialer { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func FromDialer(logf logger.Logf, netMon *netmon.Monitor, d *net.Dialer) Dialer { if disabled.Load() { return d } - d.Control = control(logf) + d.Control = control(logf, netMon) if wrapDialer != nil { return wrapDialer(d) } diff --git a/net/netns/netns_android.go b/net/netns/netns_android.go index 218b132e3..162e5c79a 100644 --- a/net/netns/netns_android.go +++ b/net/netns/netns_android.go @@ -10,6 +10,7 @@ import ( "sync" "syscall" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -49,7 +50,7 @@ func SetAndroidProtectFunc(f func(fd int) error) { androidProtectFunc = f } -func control(logger.Logf) func(network, address string, c syscall.RawConn) error { +func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error { return controlC } diff --git a/net/netns/netns_darwin.go b/net/netns/netns_darwin.go index c24c51102..7315dff40 100644 --- a/net/netns/netns_darwin.go +++ b/net/netns/netns_darwin.go @@ -19,24 +19,25 @@ import ( "golang.org/x/sys/unix" "tailscale.com/envknob" "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) -func control(logf logger.Logf) func(network, address string, c syscall.RawConn) error { +func control(logf logger.Logf, netMon *netmon.Monitor) func(network, address string, c syscall.RawConn) error { return func(network, address string, c syscall.RawConn) error { - return controlLogf(logf, network, address, c) + return controlLogf(logf, netMon, network, address, c) } } var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_ROUTE") -var errInterfaceIndexInvalid = errors.New("interface index invalid") +var errInterfaceStateInvalid = errors.New("interface state invalid") // controlLogf marks c as necessary to dial in a separate network namespace. // // It's intentionally the same signature as net.Dialer.Control // and net.ListenConfig.Control. -func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) error { +func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address string, c syscall.RawConn) error { if isLocalhost(address) { // Don't bind to an interface for localhost connections. return nil @@ -47,7 +48,7 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e return nil } - idx, err := getInterfaceIndex(logf, address) + idx, err := getInterfaceIndex(logf, netMon, address) if err != nil { // callee logged return nil @@ -56,20 +57,31 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e return bindConnToInterface(c, network, address, idx, logf) } -func getInterfaceIndex(logf logger.Logf, address string) (int, error) { +func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string) (int, error) { // Helper so we can log errors. defaultIdx := func() (int, error) { - idx, err := interfaces.DefaultRouteInterfaceIndex() - if err != nil { - // It's somewhat common for there to be no default gateway route - // (e.g. on a phone with no connectivity), don't log those errors - // since they are expected. - if !errors.Is(err, interfaces.ErrNoGatewayIndexFound) { - logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) + if netMon == nil { + idx, err := interfaces.DefaultRouteInterfaceIndex() + if err != nil { + // It's somewhat common for there to be no default gateway route + // (e.g. on a phone with no connectivity), don't log those errors + // since they are expected. + if !errors.Is(err, interfaces.ErrNoGatewayIndexFound) { + logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) + } + return -1, err } - return -1, err + return idx, nil } - return idx, nil + state := netMon.InterfaceState() + if state == nil { + return -1, errInterfaceStateInvalid + } + + if iface, ok := state.Interface[state.DefaultRouteInterface]; ok { + return iface.Index, nil + } + return -1, errInterfaceStateInvalid } useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv() diff --git a/net/netns/netns_darwin_test.go b/net/netns/netns_darwin_test.go index e1dc00b42..0fc92f6f3 100644 --- a/net/netns/netns_darwin_test.go +++ b/net/netns/netns_darwin_test.go @@ -34,7 +34,7 @@ func TestGetInterfaceIndex(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - idx, err := getInterfaceIndex(t.Logf, tc.addr) + idx, err := getInterfaceIndex(t.Logf, nil, tc.addr) if err != nil { if tc.err == "" { t.Fatalf("got unexpected error: %v", err) @@ -68,7 +68,7 @@ func TestGetInterfaceIndex(t *testing.T) { t.Fatal(err) } - idx, err := getInterfaceIndex(t.Logf, "100.100.100.100:53") + idx, err := getInterfaceIndex(t.Logf, nil, "100.100.100.100:53") if err != nil { t.Fatal(err) } diff --git a/net/netns/netns_default.go b/net/netns/netns_default.go index 43a573959..94f24d8fa 100644 --- a/net/netns/netns_default.go +++ b/net/netns/netns_default.go @@ -8,10 +8,11 @@ package netns import ( "syscall" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) -func control(logger.Logf) func(network, address string, c syscall.RawConn) error { +func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error { return controlC } diff --git a/net/netns/netns_linux.go b/net/netns/netns_linux.go index dea6a9e8d..5d09d7d19 100644 --- a/net/netns/netns_linux.go +++ b/net/netns/netns_linux.go @@ -15,6 +15,7 @@ import ( "golang.org/x/sys/unix" "tailscale.com/envknob" "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -85,7 +86,7 @@ func ignoreErrors() bool { return false } -func control(logger.Logf) func(network, address string, c syscall.RawConn) error { +func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error { return controlC } diff --git a/net/netns/netns_test.go b/net/netns/netns_test.go index d95c04981..82f919b94 100644 --- a/net/netns/netns_test.go +++ b/net/netns/netns_test.go @@ -24,7 +24,7 @@ func TestDial(t *testing.T) { if !*extNetwork { t.Skip("skipping test without --use-external-network") } - d := NewDialer(t.Logf) + d := NewDialer(t.Logf, nil) c, err := d.Dial("tcp", "google.com:80") if err != nil { t.Fatal(err) diff --git a/net/netns/netns_windows.go b/net/netns/netns_windows.go index 147a762ec..8aa1da18d 100644 --- a/net/netns/netns_windows.go +++ b/net/netns/netns_windows.go @@ -12,6 +12,7 @@ import ( "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "tailscale.com/types/logger" ) @@ -26,7 +27,7 @@ func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 { return iface.IfIndex } -func control(logger.Logf) func(network, address string, c syscall.RawConn) error { +func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error { return controlC } diff --git a/net/ping/ping.go b/net/ping/ping.go index f4bd7f0e0..9b9618e0f 100644 --- a/net/ping/ping.go +++ b/net/ping/ping.go @@ -18,6 +18,7 @@ import ( "golang.org/x/net/icmp" "golang.org/x/net/ipv4" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/types/logger" ) @@ -52,8 +53,9 @@ type Pinger struct { // New creates a new Pinger. The Context provided will be used to create // network listeners, and to set an absolute deadline (if any) on the net.Conn -func New(ctx context.Context, logf logger.Logf) (*Pinger, error) { - p, err := newUnstarted(ctx, logf) +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func New(ctx context.Context, logf logger.Logf, netMon *netmon.Monitor) (*Pinger, error) { + p, err := newUnstarted(ctx, logf, netMon) if err != nil { return nil, err } @@ -72,14 +74,14 @@ func New(ctx context.Context, logf logger.Logf) (*Pinger, error) { return p, nil } -func newUnstarted(ctx context.Context, logf logger.Logf) (*Pinger, error) { +func newUnstarted(ctx context.Context, logf logger.Logf, netMon *netmon.Monitor) (*Pinger, error) { var id [2]byte _, err := rand.Read(id[:]) if err != nil { return nil, err } - conn, err := netns.Listener(logf).ListenPacket(ctx, "ip4:icmp", "0.0.0.0") + conn, err := netns.Listener(logf, netMon).ListenPacket(ctx, "ip4:icmp", "0.0.0.0") if err != nil { return nil, err } diff --git a/net/portmapper/igd_test.go b/net/portmapper/igd_test.go index f48959631..57c0d7aa6 100644 --- a/net/portmapper/igd_test.go +++ b/net/portmapper/igd_test.go @@ -249,7 +249,7 @@ func (d *TestIGD) handlePCPQuery(pkt []byte, src netip.AddrPort) { func newTestClient(t *testing.T, igd *TestIGD) *Client { var c *Client - c = NewClient(t.Logf, nil, func() { + c = NewClient(t.Logf, nil, nil, func() { t.Logf("port map changed") t.Logf("have mapping: %v", c.HaveMapping()) }) diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index 2d8c06591..2e500b555 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -21,6 +21,7 @@ import ( "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" "tailscale.com/net/neterror" + "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/sockstats" "tailscale.com/types/logger" @@ -59,6 +60,7 @@ const trustServiceStillAvailableDuration = 10 * time.Minute // Client is a port mapping client. type Client struct { logf logger.Logf + netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand ipAndGateway func() (gw, ip netip.Addr, ok bool) onChange func() // or nil debug DebugKnobs @@ -153,15 +155,19 @@ func (m *pmpMapping) Release(ctx context.Context) { // NewClient returns a new portmapping client. // +// The netMon parameter is optional; if non-nil it's used to do faster interface +// lookups. +// // The debug argument allows configuring the behaviour of the portmapper for // debugging; if nil, a sensible set of defaults will be used. // // The optional onChange argument specifies a func to run in a new // goroutine whenever the port mapping status has changed. If nil, // it doesn't make a callback. -func NewClient(logf logger.Logf, debug *DebugKnobs, onChange func()) *Client { +func NewClient(logf logger.Logf, netMon *netmon.Monitor, debug *DebugKnobs, onChange func()) *Client { ret := &Client{ logf: logf, + netMon: netMon, ipAndGateway: interfaces.LikelyHomeRouterIP, onChange: onChange, } @@ -271,7 +277,7 @@ func (c *Client) listenPacket(ctx context.Context, network, addr string) (nettyp } return pc.(*net.UDPConn), nil } - pc, err := netns.Listener(c.logf).ListenPacket(ctx, network, addr) + pc, err := netns.Listener(c.logf, c.netMon).ListenPacket(ctx, network, addr) if err != nil { return nil, err } diff --git a/net/portmapper/portmapper_test.go b/net/portmapper/portmapper_test.go index 67c37f8bd..15f2849fc 100644 --- a/net/portmapper/portmapper_test.go +++ b/net/portmapper/portmapper_test.go @@ -16,7 +16,7 @@ func TestCreateOrGetMapping(t *testing.T) { if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v { t.Skip("skipping test without HIT_NETWORK=1") } - c := NewClient(t.Logf, nil, nil) + c := NewClient(t.Logf, nil, nil, nil) defer c.Close() c.SetLocalPort(1234) for i := 0; i < 2; i++ { @@ -32,7 +32,7 @@ func TestClientProbe(t *testing.T) { if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v { t.Skip("skipping test without HIT_NETWORK=1") } - c := NewClient(t.Logf, nil, nil) + c := NewClient(t.Logf, nil, nil, nil) defer c.Close() for i := 0; i < 3; i++ { if i > 0 { @@ -47,7 +47,7 @@ func TestClientProbeThenMap(t *testing.T) { if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v { t.Skip("skipping test without HIT_NETWORK=1") } - c := NewClient(t.Logf, nil, nil) + c := NewClient(t.Logf, nil, nil, nil) defer c.Close() c.SetLocalPort(1234) res, err := c.Probe(context.Background()) diff --git a/net/portmapper/upnp.go b/net/portmapper/upnp.go index 5844d7428..6a525c54f 100644 --- a/net/portmapper/upnp.go +++ b/net/portmapper/upnp.go @@ -237,7 +237,7 @@ func (c *Client) upnpHTTPClientLocked() *http.Client { if c.uPnPHTTPClient == nil { c.uPnPHTTPClient = &http.Client{ Transport: &http.Transport{ - DialContext: netns.NewDialer(c.logf).DialContext, + DialContext: netns.NewDialer(c.logf, c.netMon).DialContext, IdleConnTimeout: 2 * time.Second, // LAN is cheap }, } diff --git a/net/sockstats/sockstats.go b/net/sockstats/sockstats.go index e968ac7bf..b39d60afb 100644 --- a/net/sockstats/sockstats.go +++ b/net/sockstats/sockstats.go @@ -109,8 +109,8 @@ func GetValidation() *ValidationSockStats { // SetNetMon configures the sockstats package to monitor the active // interface, so that per-interface stats can be collected. -func SetNetMon(lm *netmon.Monitor) { - setNetMon(lm) +func SetNetMon(netMon *netmon.Monitor) { + setNetMon(netMon) } // DebugInfo returns a string containing debug information about the tracked diff --git a/net/sockstats/sockstats_noop.go b/net/sockstats/sockstats_noop.go index 46b5ceaef..96723111a 100644 --- a/net/sockstats/sockstats_noop.go +++ b/net/sockstats/sockstats_noop.go @@ -30,7 +30,7 @@ func getValidation() *ValidationSockStats { return nil } -func setNetMon(lm *netmon.Monitor) { +func setNetMon(netMon *netmon.Monitor) { } func debugInfo() string { diff --git a/net/sockstats/sockstats_tsgo.go b/net/sockstats/sockstats_tsgo.go index e2dd05f22..26211958f 100644 --- a/net/sockstats/sockstats_tsgo.go +++ b/net/sockstats/sockstats_tsgo.go @@ -249,13 +249,13 @@ func getValidation() *ValidationSockStats { return r } -func setNetMon(lm *netmon.Monitor) { +func setNetMon(netMon *netmon.Monitor) { sockStats.mu.Lock() defer sockStats.mu.Unlock() // We intentionally populate all known interfaces now, so that we can // increment stats for them without holding mu. - state := lm.InterfaceState() + state := netMon.InterfaceState() for ifName, iface := range state.Interface { sockStats.knownInterfaces[iface.Index] = ifName } @@ -266,7 +266,7 @@ func setNetMon(lm *netmon.Monitor) { sockStats.usedInterfaces[ifIndex] = 1 } - lm.RegisterChangeCallback(func(changed bool, state *interfaces.State) { + netMon.RegisterChangeCallback(func(changed bool, state *interfaces.State) { if changed { if ifName := state.DefaultRouteInterface; ifName != "" { ifIndex := state.Interface[ifName].Index diff --git a/net/tsdial/tsdial.go b/net/tsdial/tsdial.go index bb1dde397..54e91ca13 100644 --- a/net/tsdial/tsdial.go +++ b/net/tsdial/tsdial.go @@ -128,14 +128,14 @@ func (d *Dialer) Close() error { return nil } -func (d *Dialer) SetNetMon(mon *netmon.Monitor) { +func (d *Dialer) SetNetMon(netMon *netmon.Monitor) { d.mu.Lock() defer d.mu.Unlock() if d.netMonUnregister != nil { go d.netMonUnregister() d.netMonUnregister = nil } - d.netMon = mon + d.netMon = netMon d.netMonUnregister = d.netMon.RegisterChangeCallback(d.linkChanged) } @@ -279,7 +279,7 @@ func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn if logf == nil { logf = logger.Discard } - d.netnsDialer = netns.NewDialer(logf) + d.netnsDialer = netns.NewDialer(logf, d.netMon) }) c, err := d.netnsDialer.DialContext(ctx, network, addr) if err != nil { diff --git a/prober/derp.go b/prober/derp.go index 21583a9bf..cd267e27e 100644 --- a/prober/derp.go +++ b/prober/derp.go @@ -346,7 +346,7 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de return !strings.Contains(s, "derphttp.Client.Connect: connecting to") }) priv := key.NewNode() - dc := derphttp.NewRegionClient(priv, l, func() *tailcfg.DERPRegion { + dc := derphttp.NewRegionClient(priv, l, nil /* no netMon */, func() *tailcfg.DERPRegion { rid := n.RegionID return &tailcfg.DERPRegion{ RegionID: rid, diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 6e7db9440..2bea9026b 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -203,7 +203,7 @@ func (s *Server) Loopback() (addr string, proxyCred, localAPICred string, err er // out the CONNECT code from tailscaled/proxy.go that uses // httputil.ReverseProxy and adding auth support. go func() { - lah := localapi.NewHandler(s.lb, s.logf, s.logid) + lah := localapi.NewHandler(s.lb, s.logf, s.netMon, s.logid) lah.PermitWrite = true lah.PermitRead = true lah.RequiredPassword = s.localAPICred @@ -564,7 +564,7 @@ func (s *Server) start() (reterr error) { go s.printAuthURLLoop() // Run the localapi handler, to allow fetching LetsEncrypt certs. - lah := localapi.NewHandler(lb, logf, s.logid) + lah := localapi.NewHandler(lb, logf, s.netMon, s.logid) lah.PermitWrite = true lah.PermitRead = true @@ -620,7 +620,7 @@ func (s *Server) startLogger(closePool *closeOnErrorPool) error { } return w }, - HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost)}, + HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, s.netMon)}, } s.logtail = logtail.NewLogger(c, s.logf) closePool.addFunc(func() { s.logtail.Shutdown(context.Background()) }) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 5a0a2bf0d..123b2e70b 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -642,7 +642,7 @@ func NewConn(opts Options) (*Conn, error) { c.idleFunc = opts.IdleFunc c.testOnlyPacketListener = opts.TestOnlyPacketListener c.noteRecvActivity = opts.NoteRecvActivity - c.portMapper = portmapper.NewClient(logger.WithPrefix(c.logf, "portmapper: "), nil, c.onPortMapChanged) + c.portMapper = portmapper.NewClient(logger.WithPrefix(c.logf, "portmapper: "), opts.NetMon, nil, c.onPortMapChanged) if opts.NetMon != nil { c.portMapper.SetGatewayLookupFunc(opts.NetMon.GatewayAndSelfIP) } @@ -656,6 +656,7 @@ func NewConn(opts Options) (*Conn, error) { c.donec = c.connCtx.Done() c.netChecker = &netcheck.Client{ Logf: logger.WithPrefix(c.logf, "netcheck: "), + NetMon: c.netMon, GetSTUNConn4: func() netcheck.STUNConn { return &c.pconn4 }, GetSTUNConn6: func() netcheck.STUNConn { return &c.pconn6 }, SkipExternalNetwork: inTest(), @@ -1554,7 +1555,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha // Note that derphttp.NewRegionClient does not dial the server // (it doesn't block) so it is safe to do under the c.mu lock. - dc := derphttp.NewRegionClient(c.privateKey, c.logf, func() *tailcfg.DERPRegion { + dc := derphttp.NewRegionClient(c.privateKey, c.logf, c.netMon, func() *tailcfg.DERPRegion { // Warning: it is not legal to acquire // magicsock.Conn.mu from this callback. // It's run from derphttp.Client.connect (via Send, etc) @@ -3251,7 +3252,7 @@ func (c *Conn) listenPacket(network string, port uint16) (nettype.PacketConn, er if c.testOnlyPacketListener != nil { return nettype.MakePacketListenerWithNetIP(c.testOnlyPacketListener).ListenPacket(ctx, network, addr) } - return nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf)).ListenPacket(ctx, network, addr) + return nettype.MakePacketListenerWithNetIP(netns.Listener(c.logf, c.netMon)).ListenPacket(ctx, network, addr) } var debugBindSocket = envknob.RegisterBool("TS_DEBUG_MAGICSOCK_BIND_SOCKET") diff --git a/wgengine/netlog/logger.go b/wgengine/netlog/logger.go index bb3f667a3..a694308e6 100644 --- a/wgengine/netlog/logger.go +++ b/wgengine/netlog/logger.go @@ -19,6 +19,7 @@ import ( "tailscale.com/logpolicy" "tailscale.com/logtail" "tailscale.com/net/connstats" + "tailscale.com/net/netmon" "tailscale.com/net/sockstats" "tailscale.com/net/tsaddr" "tailscale.com/smallzstd" @@ -91,7 +92,8 @@ var testClient *http.Client // is a non-tailscale IP address to contact for that particular tailscale node. // The IP protocol and source port are always zero. // The sock is used to populated the PhysicalTraffic field in Message. -func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device) error { +// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. +func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor) error { nl.mu.Lock() defer nl.mu.Unlock() if nl.logger != nil { @@ -99,7 +101,7 @@ func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID lo } // Startup a log stream to Tailscale's logging service. - httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost)} + httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon)} if testClient != nil { httpc = testClient } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index c7caf1492..1356b36df 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -917,7 +917,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, nid := cfg.NetworkLogging.NodeID tid := cfg.NetworkLogging.DomainID e.logf("wgengine: Reconfig: starting up network logger (node:%s tailnet:%s)", nid.Public(), tid.Public()) - if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn); err != nil { + if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn, e.netMon); err != nil { e.logf("wgengine: Reconfig: error starting up network logger: %v", err) } e.networkLogger.ReconfigRoutes(routerCfg)