diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt
index 29f2efc89..167950d9c 100644
--- a/cmd/derper/depaware.txt
+++ b/cmd/derper/depaware.txt
@@ -12,7 +12,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
- L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
@@ -47,6 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
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
+ tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -84,13 +85,14 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health
- tailscale.com/util/set from tailscale.com/health
+ tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/vizerror from tailscale.com/tsweb
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
tailscale.com/wgengine/filter from tailscale.com/types/netmap
+ tailscale.com/wgengine/monitor from tailscale.com/net/sockstats
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper
golang.org/x/crypto/argon2 from tailscale.com/tka
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index a926d9eae..f8147d646 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -13,7 +13,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
- L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress/flate from nhooyr.io/websocket
@@ -79,6 +79,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
+ tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
@@ -126,6 +127,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
tailscale.com/wgengine/filter from tailscale.com/types/netmap
+ tailscale.com/wgengine/monitor from tailscale.com/net/sockstats
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase+
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index b4235f86a..9e6e0c45d 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -246,6 +246,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
+ tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go
index cfdb6b78b..bab7c7be3 100644
--- a/control/controlclient/auto.go
+++ b/control/controlclient/auto.go
@@ -13,6 +13,7 @@
"tailscale.com/health"
"tailscale.com/logtail/backoff"
+ "tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -118,7 +119,11 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
statusFunc: opts.Status,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
+ c.authCtx = sockstats.WithSockStats(c.authCtx, "controlclient.Auto:auth")
+
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
+ c.mapCtx = sockstats.WithSockStats(c.mapCtx, "controlclient.Auto:map")
+
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
return c, nil
@@ -206,6 +211,7 @@ func (c *Auto) cancelAuth() {
}
if !c.closed {
c.authCtx, c.authCancel = context.WithCancel(context.Background())
+ c.authCtx = sockstats.WithSockStats(c.authCtx, "controlclient.Auto:auth")
}
c.mu.Unlock()
}
@@ -216,6 +222,8 @@ func (c *Auto) cancelMapLocked() {
}
if !c.closed {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
+ c.mapCtx = sockstats.WithSockStats(c.mapCtx, "controlclient.Auto:map")
+
}
}
diff --git a/control/controlhttp/client.go b/control/controlhttp/client.go
index 4eebbee9a..bdb02013b 100644
--- a/control/controlhttp/client.go
+++ b/control/controlhttp/client.go
@@ -41,6 +41,7 @@
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
@@ -272,6 +273,8 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
ctx, cancel := context.WithCancel(ctx)
defer cancel()
+ ctx = sockstats.WithSockStats(ctx, "controlclient.Dialer")
+
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
// we'll speak Noise.
diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go
index 4a1427c4c..160ae8bd3 100644
--- a/derp/derphttp/derphttp_client.go
+++ b/derp/derphttp/derphttp_client.go
@@ -32,6 +32,7 @@
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/netns"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
@@ -615,6 +616,8 @@ type res struct {
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
defer cancel()
+ ctx = sockstats.WithSockStats(ctx, "derphttp.Client")
+
nwait := 0
startDial := func(dstPrimary, proto string) {
nwait++
diff --git a/go.toolchain.rev b/go.toolchain.rev
index 235d5ae0a..4ff879a8e 100644
--- a/go.toolchain.rev
+++ b/go.toolchain.rev
@@ -1 +1 @@
-ec180cbca39fcb5dc420399b37583e53fcf382c9
+fb11c0df588717a3ee13b09dacae1e7093279d67
diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go
index 1a85775ff..16c2f496e 100644
--- a/ipn/ipnlocal/peerapi.go
+++ b/ipn/ipnlocal/peerapi.go
@@ -45,6 +45,7 @@
"tailscale.com/net/interfaces"
"tailscale.com/net/netaddr"
"tailscale.com/net/netutil"
+ "tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
@@ -709,6 +710,8 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
case "/v0/doctor":
h.handleServeDoctor(w, r)
+ case "/v0/sockstats":
+ h.handleServeSockStats(w, r)
return
case "/v0/ingress":
metricIngressCalls.Add(1)
@@ -850,6 +853,76 @@ func (h *peerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Reques
fmt.Fprintln(w, "")
}
+func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
+ if !h.canDebug() {
+ http.Error(w, "denied; no debug access", http.StatusForbidden)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintln(w, "
Socket Stats
")
+
+ stats := sockstats.Get()
+ if stats == nil {
+ fmt.Fprintln(w, "No socket stats available")
+ return
+ }
+
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Label | ")
+ fmt.Fprintln(w, "Tx | ")
+ fmt.Fprintln(w, "Rx | ")
+ for _, iface := range stats.Interfaces {
+ fmt.Fprintf(w, "Tx (%s) | ", html.EscapeString(iface))
+ fmt.Fprintf(w, "Rx (%s) | ", html.EscapeString(iface))
+ }
+ fmt.Fprintln(w, "")
+
+ fmt.Fprintln(w, "")
+ labels := make([]string, 0, len(stats.Stats))
+ for label := range stats.Stats {
+ labels = append(labels, label)
+ }
+ sort.Strings(labels)
+
+ txTotal := int64(0)
+ rxTotal := int64(0)
+ txTotalByInterface := map[string]int64{}
+ rxTotalByInterface := map[string]int64{}
+
+ for _, label := range labels {
+ stat := stats.Stats[label]
+ fmt.Fprintln(w, "")
+ fmt.Fprintf(w, "%s | ", html.EscapeString(label))
+ fmt.Fprintf(w, "%d | ", stat.TxBytes)
+ fmt.Fprintf(w, "%d | ", stat.RxBytes)
+
+ txTotal += stat.TxBytes
+ rxTotal += stat.RxBytes
+
+ for _, iface := range stats.Interfaces {
+ fmt.Fprintf(w, "%d | ", stat.TxBytesByInterface[iface])
+ fmt.Fprintf(w, "%d | ", stat.RxBytesByInterface[iface])
+ txTotalByInterface[iface] += stat.TxBytesByInterface[iface]
+ rxTotalByInterface[iface] += stat.RxBytesByInterface[iface]
+ }
+ fmt.Fprintln(w, "
")
+ }
+ fmt.Fprintln(w, "")
+
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Total | ")
+ fmt.Fprintf(w, "%d | ", txTotal)
+ fmt.Fprintf(w, "%d | ", rxTotal)
+ for _, iface := range stats.Interfaces {
+ fmt.Fprintf(w, "%d | ", txTotalByInterface[iface])
+ fmt.Fprintf(w, "%d | ", rxTotalByInterface[iface])
+ }
+ fmt.Fprintln(w, "")
+
+ fmt.Fprintln(w, "
")
+}
+
type incomingFile struct {
name string // "foo.jpg"
started time.Time
diff --git a/logtail/logtail.go b/logtail/logtail.go
index 8979c914f..7f19e3a84 100644
--- a/logtail/logtail.go
+++ b/logtail/logtail.go
@@ -24,6 +24,7 @@
"tailscale.com/envknob"
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces"
+ "tailscale.com/net/sockstats"
tslogger "tailscale.com/types/logger"
"tailscale.com/util/set"
"tailscale.com/wgengine/monitor"
@@ -426,6 +427,7 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
// origlen of -1 indicates that the body is not compressed.
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
const maxUploadTime = 45 * time.Second
+ ctx = sockstats.WithSockStats(ctx, "logtail.Logger")
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
defer cancel()
diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go
index 47be991c8..7c0ff42ec 100644
--- a/net/dns/resolver/forwarder.go
+++ b/net/dns/resolver/forwarder.go
@@ -26,6 +26,7 @@
"tailscale.com/net/dnscache"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
@@ -406,6 +407,7 @@ func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client
const dohType = "application/dns-message"
func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client, packet []byte) ([]byte, error) {
+ ctx = sockstats.WithSockStats(ctx, "dns.forwarder:doh")
metricDNSFwdDoH.Add(1)
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
if err != nil {
@@ -485,6 +487,7 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
}
metricDNSFwdUDP.Add(1)
+ ctx = sockstats.WithSockStats(ctx, "dns.forwarder:udp")
ln, err := f.packetListener(ipp.Addr())
if err != nil {
diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go
index 2705bcb92..2b8f76133 100644
--- a/net/netcheck/netcheck.go
+++ b/net/netcheck/netcheck.go
@@ -31,6 +31,7 @@
"tailscale.com/net/netns"
"tailscale.com/net/ping"
"tailscale.com/net/portmapper"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/stun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -783,6 +784,8 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
ctx, cancel := context.WithTimeout(ctx, overallProbeTimeout)
defer cancel()
+ ctx = sockstats.WithSockStats(ctx, "netcheck.Client")
+
if dm == nil {
return nil, errors.New("netcheck: GetReport: DERP map is nil")
}
diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go
index 8cb565b68..29bddc9d2 100644
--- a/net/portmapper/portmapper.go
+++ b/net/portmapper/portmapper.go
@@ -22,6 +22,7 @@
"tailscale.com/net/netaddr"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
+ "tailscale.com/net/sockstats"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
@@ -238,6 +239,8 @@ func (c *Client) upnpPort() uint16 {
}
func (c *Client) listenPacket(ctx context.Context, network, addr string) (nettype.PacketConn, error) {
+ ctx = sockstats.WithSockStats(ctx, "portmapper.Client")
+
// When running under testing conditions, we bind the IGD server
// to localhost, and may be running in an environment where our
// netns code would decide that binding the portmapper client
diff --git a/net/sockstats/sockstats.go b/net/sockstats/sockstats.go
new file mode 100644
index 000000000..28055d201
--- /dev/null
+++ b/net/sockstats/sockstats.go
@@ -0,0 +1,39 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package sockstats collects statistics about network sockets used by
+// the Tailscale client. The context where sockets are used must be
+// instrumented with the WithSockStats() function.
+//
+// Only available on POSIX platforms when built with Tailscale's fork of Go.
+package sockstats
+
+import (
+ "context"
+
+ "tailscale.com/wgengine/monitor"
+)
+
+type SockStats struct {
+ Stats map[string]SockStat
+ Interfaces []string
+}
+
+type SockStat struct {
+ TxBytes int64
+ RxBytes int64
+ TxBytesByInterface map[string]int64
+ RxBytesByInterface map[string]int64
+}
+
+func WithSockStats(ctx context.Context, label string) context.Context {
+ return withSockStats(ctx, label)
+}
+
+func Get() *SockStats {
+ return get()
+}
+
+func SetLinkMonitor(lm *monitor.Mon) {
+ setLinkMonitor(lm)
+}
diff --git a/net/sockstats/sockstats_noop.go b/net/sockstats/sockstats_noop.go
new file mode 100644
index 000000000..913f62d4d
--- /dev/null
+++ b/net/sockstats/sockstats_noop.go
@@ -0,0 +1,23 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !tailscale_go || !(darwin || ios || android)
+
+package sockstats
+
+import (
+ "context"
+
+ "tailscale.com/wgengine/monitor"
+)
+
+func withSockStats(ctx context.Context, label string) context.Context {
+ return ctx
+}
+
+func get() *SockStats {
+ return nil
+}
+
+func setLinkMonitor(lm *monitor.Mon) {
+}
diff --git a/net/sockstats/sockstats_tsgo.go b/net/sockstats/sockstats_tsgo.go
new file mode 100644
index 000000000..58a737d1c
--- /dev/null
+++ b/net/sockstats/sockstats_tsgo.go
@@ -0,0 +1,151 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build tailscale_go && (darwin || ios || android)
+
+package sockstats
+
+import (
+ "context"
+ "log"
+ "net"
+ "sync"
+ "sync/atomic"
+
+ "tailscale.com/net/interfaces"
+ "tailscale.com/wgengine/monitor"
+)
+
+type sockStatCounters struct {
+ txBytes, rxBytes atomic.Uint64
+ rxBytesByInterface, txBytesByInterface map[int]*atomic.Uint64
+}
+
+var sockStats = struct {
+ // mu protects fields in this group. It should not be held in the per-read/
+ // write callbacks.
+ mu sync.Mutex
+ countersByLabel map[string]*sockStatCounters
+ knownInterfaces map[int]string // interface index -> name
+ usedInterfaces map[int]int // set of interface indexes
+
+ // Separate atomic since the current interface is accessed in the per-read/
+ // write callbacks.
+ currentInterface atomic.Uint32
+}{
+ countersByLabel: make(map[string]*sockStatCounters),
+ knownInterfaces: make(map[int]string),
+ usedInterfaces: make(map[int]int),
+}
+
+func withSockStats(ctx context.Context, label string) context.Context {
+ sockStats.mu.Lock()
+ defer sockStats.mu.Unlock()
+ counters, ok := sockStats.countersByLabel[label]
+ if !ok {
+ counters = &sockStatCounters{
+ rxBytesByInterface: make(map[int]*atomic.Uint64),
+ txBytesByInterface: make(map[int]*atomic.Uint64),
+ }
+ for iface := range sockStats.knownInterfaces {
+ counters.rxBytesByInterface[iface] = &atomic.Uint64{}
+ counters.txBytesByInterface[iface] = &atomic.Uint64{}
+ }
+ sockStats.countersByLabel[label] = counters
+ }
+
+ didRead := func(n int) {
+ counters.rxBytes.Add(uint64(n))
+ if currentInterface := int(sockStats.currentInterface.Load()); currentInterface != 0 {
+ if a := counters.rxBytesByInterface[currentInterface]; a != nil {
+ a.Add(uint64(n))
+ }
+ }
+ }
+ didWrite := func(n int) {
+ counters.txBytes.Add(uint64(n))
+ if currentInterface := int(sockStats.currentInterface.Load()); currentInterface != 0 {
+ if a := counters.txBytesByInterface[currentInterface]; a != nil {
+ a.Add(uint64(n))
+ }
+ }
+ }
+ willOverwrite := func(trace *net.SockTrace) {
+ log.Printf("sockstats: trace %q was overwritten by another", label)
+ }
+
+ return net.WithSockTrace(ctx, &net.SockTrace{
+ DidRead: didRead,
+ DidWrite: didWrite,
+ WillOverwrite: willOverwrite,
+ })
+}
+
+func get() *SockStats {
+ sockStats.mu.Lock()
+ defer sockStats.mu.Unlock()
+
+ r := &SockStats{
+ Stats: make(map[string]SockStat),
+ Interfaces: make([]string, 0, len(sockStats.usedInterfaces)),
+ }
+ for iface := range sockStats.usedInterfaces {
+ r.Interfaces = append(r.Interfaces, sockStats.knownInterfaces[iface])
+ }
+
+ for label, counters := range sockStats.countersByLabel {
+ r.Stats[label] = SockStat{
+ TxBytes: int64(counters.txBytes.Load()),
+ RxBytes: int64(counters.rxBytes.Load()),
+ TxBytesByInterface: make(map[string]int64),
+ RxBytesByInterface: make(map[string]int64),
+ }
+ for iface, a := range counters.rxBytesByInterface {
+ ifName := sockStats.knownInterfaces[iface]
+ r.Stats[label].RxBytesByInterface[ifName] = int64(a.Load())
+ }
+ for iface, a := range counters.txBytesByInterface {
+ ifName := sockStats.knownInterfaces[iface]
+ r.Stats[label].TxBytesByInterface[ifName] = int64(a.Load())
+ }
+ }
+
+ return r
+}
+
+func setLinkMonitor(lm *monitor.Mon) {
+ 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()
+ for ifName, iface := range state.Interface {
+ sockStats.knownInterfaces[iface.Index] = ifName
+ }
+ if ifName := state.DefaultRouteInterface; ifName != "" {
+ ifIndex := state.Interface[ifName].Index
+ sockStats.currentInterface.Store(uint32(ifIndex))
+ sockStats.usedInterfaces[ifIndex] = 1
+ }
+
+ lm.RegisterChangeCallback(func(changed bool, state *interfaces.State) {
+ if changed {
+ if ifName := state.DefaultRouteInterface; ifName != "" {
+ ifIndex := state.Interface[ifName].Index
+ sockStats.mu.Lock()
+ defer sockStats.mu.Unlock()
+ // Ignore changes to unknown interfaces -- it would require
+ // updating the tx/rxBytesByInterface maps and thus
+ // additional locking for every read/write. Most of the time
+ // the set of interfaces is static.
+ if _, ok := sockStats.knownInterfaces[ifIndex]; ok {
+ sockStats.currentInterface.Store(uint32(ifIndex))
+ sockStats.usedInterfaces[ifIndex] = 1
+ } else {
+ sockStats.currentInterface.Store(0)
+ }
+ }
+ }
+ })
+}
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index 7daafc024..05f6b1abc 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -49,6 +49,7 @@
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/stun"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
@@ -3049,7 +3050,7 @@ func (c *Conn) ReSTUN(why string) {
// listenPacket opens a packet listener.
// The network must be "udp4" or "udp6".
func (c *Conn) listenPacket(network string, port uint16) (nettype.PacketConn, error) {
- ctx := context.Background() // unused without DNS name to resolve
+ ctx := sockstats.WithSockStats(context.Background(), fmt.Sprintf("magicsock.Conn:%s", network)) // unused without DNS name to resolve
addr := net.JoinHostPort("", fmt.Sprint(port))
if c.testOnlyPacketListener != nil {
return nettype.MakePacketListenerWithNetIP(c.testOnlyPacketListener).ListenPacket(ctx, network, addr)
diff --git a/wgengine/userspace.go b/wgengine/userspace.go
index c31288229..7def6c971 100644
--- a/wgengine/userspace.go
+++ b/wgengine/userspace.go
@@ -29,6 +29,7 @@
"tailscale.com/net/flowtrack"
"tailscale.com/net/interfaces"
"tailscale.com/net/packet"
+ "tailscale.com/net/sockstats"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
@@ -332,6 +333,9 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
conf.Dialer.SetLinkMonitor(e.linkMon)
e.dns = dns.NewManager(logf, conf.DNS, e.linkMon, conf.Dialer, fwdDNSLinkSelector{e, tunName})
+ // TODO: there's probably a better place for this
+ sockstats.SetLinkMonitor(e.linkMon)
+
logf("link state: %+v", e.linkMon.InterfaceState())
unregisterMonWatch := e.linkMon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {