[client] Skip engine restart when default route is unchanged

After the network monitor's debounce window, re-check the default next
hop before triggering a client restart. A flapping NIC that returns to
the same default route no longer forces a restart, avoiding redundant
sync stream reconnects and peer meta churn.
This commit is contained in:
Zoltán Papp
2026-06-22 23:53:06 +02:00
parent 1230d5891b
commit a071b55f35
2 changed files with 61 additions and 1 deletions

View File

@@ -97,7 +97,12 @@ func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
case <-event:
timer.Reset(debounceTime)
case <-timer.C:
return nil
// A flapping NIC may return to the same default route after the
// debounce window; only restart if the next hop actually changed.
if nexthopChanged(nexthop4, nexthop6) {
return nil
}
log.Debug("Network monitor: default route unchanged after debounce, ignoring")
case <-ctx.Done():
timer.Stop()
return ctx.Err()
@@ -134,3 +139,28 @@ func (nw *NetworkMonitor) checkChanges(ctx context.Context, event chan struct{},
}
}
}
// nexthopChanged reports whether the current default next hop differs from the
// given baseline. A lookup error is treated as a change so we fail safe and
// restart rather than miss a real network change.
func nexthopChanged(oldv4, oldv6 systemops.Nexthop) bool {
newv4, errv4 := systemops.GetNextHop(netip.IPv4Unspecified())
newv6, errv6 := systemops.GetNextHop(netip.IPv6Unspecified())
if errv4 != nil || errv6 != nil {
return true
}
return !sameNexthop(oldv4, newv4) || !sameNexthop(oldv6, newv6)
}
func sameNexthop(a, b systemops.Nexthop) bool {
if a.IP != b.IP {
return false
}
if (a.Intf == nil) != (b.Intf == nil) {
return false
}
if a.Intf != nil && a.Intf.Index != b.Intf.Index {
return false
}
return true
}

View File

@@ -3,6 +3,8 @@ package networkmonitor
import (
"context"
"errors"
"net"
"net/netip"
"testing"
"time"
@@ -97,3 +99,31 @@ func TestNetworkMonitor_MultiEvent(t *testing.T) {
t.Errorf("unexpected duration: %v", time.Since(started))
}
}
func TestSameNexthop(t *testing.T) {
eth0 := &net.Interface{Index: 1, Name: "eth0"}
eth1 := &net.Interface{Index: 2, Name: "eth1"}
ip1 := netip.MustParseAddr("192.168.1.1")
ip2 := netip.MustParseAddr("192.168.2.1")
tests := []struct {
name string
a, b systemops.Nexthop
want bool
}{
{"identical", systemops.Nexthop{IP: ip1, Intf: eth0}, systemops.Nexthop{IP: ip1, Intf: eth0}, true},
{"same index different pointer", systemops.Nexthop{IP: ip1, Intf: eth0}, systemops.Nexthop{IP: ip1, Intf: &net.Interface{Index: 1, Name: "eth0"}}, true},
{"different ip", systemops.Nexthop{IP: ip1, Intf: eth0}, systemops.Nexthop{IP: ip2, Intf: eth0}, false},
{"different interface", systemops.Nexthop{IP: ip1, Intf: eth0}, systemops.Nexthop{IP: ip1, Intf: eth1}, false},
{"both nil interface", systemops.Nexthop{IP: ip1}, systemops.Nexthop{IP: ip1}, true},
{"one nil interface", systemops.Nexthop{IP: ip1, Intf: eth0}, systemops.Nexthop{IP: ip1}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := sameNexthop(tt.a, tt.b); got != tt.want {
t.Errorf("sameNexthop() = %v, want %v", got, tt.want)
}
})
}
}