diff --git a/client/internal/networkmonitor/check_change_darwin.go b/client/internal/networkmonitor/check_change_darwin.go index 9bb3061d1..498aac3ac 100644 --- a/client/internal/networkmonitor/check_change_darwin.go +++ b/client/internal/networkmonitor/check_change_darwin.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "hash/fnv" + "net/netip" "os/exec" "syscall" "time" @@ -46,14 +47,22 @@ func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) er close(wakeUp) }() + gatewayChanged := make(chan string) + go func() { + gatewayPoll(ctx, nexthopv4, nexthopv6, gatewayChanged) + }() + select { case <-ctx.Done(): return ctx.Err() case <-routeChanged: - log.Infof("route change detected") + log.Infof("route change detected via routing socket") return nil case <-wakeUp: - log.Infof("wakeup detected") + log.Infof("wakeup detected via sleep hash change") + return nil + case reason := <-gatewayChanged: + log.Infof("gateway change detected via polling: %s", reason) return nil } } @@ -162,6 +171,9 @@ func wakeUpListen(ctx context.Context) { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() + lastCheck := time.Now() + const maxTickerDrift = 1 * time.Minute + for { select { case <-ctx.Done(): @@ -169,14 +181,42 @@ func wakeUpListen(ctx context.Context) { return case <-ticker.C: + now := time.Now() + elapsed := now.Sub(lastCheck) + + // If more time passed than expected, system likely slept (informational only) + if elapsed > maxTickerDrift { + upOut, err := exec.Command("uptime").Output() + if err != nil { + log.Errorf("failed to run uptime command: %v", err) + upOut = []byte("unknown") + } + log.Infof("Time drift detected (potential wakeup): expected ~5s, actual %s, uptime: %s", elapsed, upOut) + + currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified()) + currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified()) + if errV4 == nil { + log.Infof("Current IPv4 default gateway: %s via %s", currentV4.IP, currentV4.Intf.Name) + } else { + log.Debugf("No IPv4 default gateway: %v", errV4) + } + if errV6 == nil { + log.Infof("Current IPv6 default gateway: %s via %s", currentV6.IP, currentV6.Intf.Name) + } else { + log.Debugf("No IPv6 default gateway: %v", errV6) + } + } + newHash, err := readSleepTimeHash() if err != nil { log.Errorf("failed to read sleep time hash: %v", err) + lastCheck = now continue } if newHash == initialHash { - log.Infof("no wakeup detected") + log.Debugf("no wakeup detected (hash unchanged: %d, time drift: %s)", initialHash, elapsed) + lastCheck = now continue } @@ -185,7 +225,21 @@ func wakeUpListen(ctx context.Context) { log.Errorf("failed to run uptime command: %v", err) upOut = []byte("unknown") } - log.Infof("Wakeup detected: %d -> %d, uptime: %s", initialHash, newHash, upOut) + log.Infof("Wakeup detected via hash change: %d -> %d, uptime: %s", initialHash, newHash, upOut) + + currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified()) + currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified()) + if errV4 == nil { + log.Infof("Current IPv4 default gateway after wakeup: %s via %s", currentV4.IP, currentV4.Intf.Name) + } else { + log.Debugf("No IPv4 default gateway after wakeup: %v", errV4) + } + if errV6 == nil { + log.Infof("Current IPv6 default gateway after wakeup: %s via %s", currentV6.IP, currentV6.Intf.Name) + } else { + log.Debugf("No IPv6 default gateway after wakeup: %v", errV6) + } + return } } @@ -207,9 +261,84 @@ func readSleepTimeHash() (uint32, error) { } func hash(data []byte) (uint32, error) { - hasher := fnv.New32a() // Create a new 32-bit FNV-1a hasher + hasher := fnv.New32a() if _, err := hasher.Write(data); err != nil { return 0, err } return hasher.Sum32(), nil } + +// gatewayPoll polls the default gateway every 5 seconds to detect changes that might be missed by routing socket or wake-up detection. +func gatewayPoll(ctx context.Context, initialV4, initialV6 systemops.Nexthop, changed chan<- string) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + log.Infof("Gateway polling started - initial v4: %s via %v, v6: %s via %v", + initialV4.IP, initialV4.Intf, initialV6.IP, initialV6.Intf) + + for { + select { + case <-ctx.Done(): + log.Debug("context canceled, stopping gateway polling") + return + + case <-ticker.C: + currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified()) + currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified()) + + var reason string + + if errV4 == nil && initialV4.IP.IsValid() { + if currentV4.IP.Compare(initialV4.IP) != 0 { + reason = fmt.Sprintf("IPv4 gateway changed from %s to %s", initialV4.IP, currentV4.IP) + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + if initialV4.Intf != nil && currentV4.Intf != nil && currentV4.Intf.Name != initialV4.Intf.Name { + reason = fmt.Sprintf("IPv4 interface changed from %s to %s", initialV4.Intf.Name, currentV4.Intf.Name) + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + } else if errV4 == nil && !initialV4.IP.IsValid() { + reason = "IPv4 gateway appeared" + log.Infof("Gateway poll detected change: %s (new: %s)", reason, currentV4.IP) + changed <- reason + return + } else if errV4 != nil && initialV4.IP.IsValid() { + reason = "IPv4 gateway disappeared" + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + + if errV6 == nil && initialV6.IP.IsValid() { + if currentV6.IP.Compare(initialV6.IP) != 0 { + reason = fmt.Sprintf("IPv6 gateway changed from %s to %s", initialV6.IP, currentV6.IP) + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + if initialV6.Intf != nil && currentV6.Intf != nil && currentV6.Intf.Name != initialV6.Intf.Name { + reason = fmt.Sprintf("IPv6 interface changed from %s to %s", initialV6.Intf.Name, currentV6.Intf.Name) + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + } else if errV6 == nil && !initialV6.IP.IsValid() { + reason = "IPv6 gateway appeared" + log.Infof("Gateway poll detected change: %s (new: %s)", reason, currentV6.IP) + changed <- reason + return + } else if errV6 != nil && initialV6.IP.IsValid() { + reason = "IPv6 gateway disappeared" + log.Infof("Gateway poll detected change: %s", reason) + changed <- reason + return + } + + log.Debugf("Gateway poll: no change detected") + } + } +}