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@e956a95074. Fixes #19324 Signed-off-by: Doug Bryant <dougbryant@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user