From 2767100bc216b91c9489bcc11ee319507c9c217d Mon Sep 17 00:00:00 2001 From: Doug Bryant Date: Mon, 8 Jun 2026 18:45:13 +0100 Subject: [PATCH] net/netmon: skip RTM_MISS route messages on darwin (#20050) macOS 26.4 emits RTM_MISS on the routing socket for every failed route lookup. skipRouteMessage never inspected the message type, so each miss woke the monitor as a link change and triggered a netcheck. On networks without an IPv6 default route the netcheck's IPv6 DERP probes fail and emit more RTM_MISS messages, sustaining the loop indefinitely: netchecks run at roughly 40x the intended rate, with sustained probe traffic and corresponding CPU and battery cost. RTM_MISS scales with traffic volume, not network state, and is never the leading signal for a topology change: route withdrawals emit RTM_DELETE synchronously before any subsequent lookup can miss, so ignoring it loses no signal. Other routing daemons (bird, dhcpcd, frr) ignore it as well. Same fix as coder/tailscale@e956a950741f. Fixes #19324 Signed-off-by: Doug Bryant --- net/netmon/netmon_darwin.go | 11 +++++++++++ net/netmon/netmon_darwin_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/net/netmon/netmon_darwin.go b/net/netmon/netmon_darwin.go index 588cbf616..d63ee6028 100644 --- a/net/netmon/netmon_darwin.go +++ b/net/netmon/netmon_darwin.go @@ -150,6 +150,17 @@ func (m *darwinRouteMon) skipInterfaceAddrMessage(msg *route.InterfaceAddrMessag } func (m *darwinRouteMon) skipRouteMessage(msg *route.RouteMessage) bool { + // RTM_MISS fires on every failed route lookup (no matching entry in the + // routing table). It scales with traffic volume, not network-state + // changes, and is never the leading signal for a topology change: route + // withdrawals emit RTM_DELETE synchronously before any subsequent lookup + // can miss. Letting these through causes netmon to report spurious + // link changes, which trigger a re-STUN/netcheck loop when a probe + // destination is unreachable (e.g. IPv6 DERP probes on a network with + // no IPv6 default route), as each failed probe emits another RTM_MISS. + if msg.Type == unix.RTM_MISS { + return true + } if ip := ipOfAddr(addrType(msg.Addrs, unix.RTAX_DST)); ip.IsLinkLocalUnicast() { // Skip those like: // dst = fe80::b476:66ff:fe30:c8f6%15 diff --git a/net/netmon/netmon_darwin_test.go b/net/netmon/netmon_darwin_test.go index e57b5ca84..690c1152c 100644 --- a/net/netmon/netmon_darwin_test.go +++ b/net/netmon/netmon_darwin_test.go @@ -9,6 +9,7 @@ import ( "testing" "golang.org/x/net/route" + "golang.org/x/sys/unix" ) func TestIssue1416RIB(t *testing.T) { @@ -25,3 +26,31 @@ func TestIssue1416RIB(t *testing.T) { } t.Logf("Got: %#v", msgs) } + +func TestSkipRouteMessage(t *testing.T) { + m := &darwinRouteMon{logf: t.Logf} + dst := &route.Inet6Addr{IP: [16]byte{0x26, 0x07}} // 2607:: (global unicast) + tests := []struct { + name string + msg *route.RouteMessage + want bool + }{ + { + name: "rtm_miss", + msg: &route.RouteMessage{Type: unix.RTM_MISS, Addrs: []route.Addr{unix.RTAX_DST: dst}}, + want: true, + }, + { + name: "rtm_add", + msg: &route.RouteMessage{Type: unix.RTM_ADD, Addrs: []route.Addr{unix.RTAX_DST: dst}}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := m.skipRouteMessage(tt.msg); got != tt.want { + t.Errorf("skipRouteMessage = %v; want %v", got, tt.want) + } + }) + } +}