Skip to content

Commit bb46e43

Browse files
committed
Add gw change polling and time drift detection (informational only)
1 parent 11ba253 commit bb46e43

File tree

1 file changed

+134
-5
lines changed

1 file changed

+134
-5
lines changed

client/internal/networkmonitor/check_change_darwin.go

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"hash/fnv"
10+
"net/netip"
1011
"os/exec"
1112
"syscall"
1213
"time"
@@ -46,14 +47,22 @@ func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) er
4647
close(wakeUp)
4748
}()
4849

50+
gatewayChanged := make(chan string)
51+
go func() {
52+
gatewayPoll(ctx, nexthopv4, nexthopv6, gatewayChanged)
53+
}()
54+
4955
select {
5056
case <-ctx.Done():
5157
return ctx.Err()
5258
case <-routeChanged:
53-
log.Infof("route change detected")
59+
log.Infof("route change detected via routing socket")
5460
return nil
5561
case <-wakeUp:
56-
log.Infof("wakeup detected")
62+
log.Infof("wakeup detected via sleep hash change")
63+
return nil
64+
case reason := <-gatewayChanged:
65+
log.Infof("gateway change detected via polling: %s", reason)
5766
return nil
5867
}
5968
}
@@ -162,21 +171,52 @@ func wakeUpListen(ctx context.Context) {
162171
ticker := time.NewTicker(5 * time.Second)
163172
defer ticker.Stop()
164173

174+
lastCheck := time.Now()
175+
const maxTickerDrift = 1 * time.Minute
176+
165177
for {
166178
select {
167179
case <-ctx.Done():
168180
log.Info("context canceled, stopping wakeUpListen")
169181
return
170182

171183
case <-ticker.C:
184+
now := time.Now()
185+
elapsed := now.Sub(lastCheck)
186+
187+
// If more time passed than expected, system likely slept (informational only)
188+
if elapsed > maxTickerDrift {
189+
upOut, err := exec.Command("uptime").Output()
190+
if err != nil {
191+
log.Errorf("failed to run uptime command: %v", err)
192+
upOut = []byte("unknown")
193+
}
194+
log.Infof("Time drift detected (potential wakeup): expected ~5s, actual %s, uptime: %s", elapsed, upOut)
195+
196+
currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified())
197+
currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified())
198+
if errV4 == nil {
199+
log.Infof("Current IPv4 default gateway: %s via %s", currentV4.IP, currentV4.Intf.Name)
200+
} else {
201+
log.Debugf("No IPv4 default gateway: %v", errV4)
202+
}
203+
if errV6 == nil {
204+
log.Infof("Current IPv6 default gateway: %s via %s", currentV6.IP, currentV6.Intf.Name)
205+
} else {
206+
log.Debugf("No IPv6 default gateway: %v", errV6)
207+
}
208+
}
209+
172210
newHash, err := readSleepTimeHash()
173211
if err != nil {
174212
log.Errorf("failed to read sleep time hash: %v", err)
213+
lastCheck = now
175214
continue
176215
}
177216

178217
if newHash == initialHash {
179-
log.Infof("no wakeup detected")
218+
log.Debugf("no wakeup detected (hash unchanged: %d, time drift: %s)", initialHash, elapsed)
219+
lastCheck = now
180220
continue
181221
}
182222

@@ -185,7 +225,21 @@ func wakeUpListen(ctx context.Context) {
185225
log.Errorf("failed to run uptime command: %v", err)
186226
upOut = []byte("unknown")
187227
}
188-
log.Infof("Wakeup detected: %d -> %d, uptime: %s", initialHash, newHash, upOut)
228+
log.Infof("Wakeup detected via hash change: %d -> %d, uptime: %s", initialHash, newHash, upOut)
229+
230+
currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified())
231+
currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified())
232+
if errV4 == nil {
233+
log.Infof("Current IPv4 default gateway after wakeup: %s via %s", currentV4.IP, currentV4.Intf.Name)
234+
} else {
235+
log.Debugf("No IPv4 default gateway after wakeup: %v", errV4)
236+
}
237+
if errV6 == nil {
238+
log.Infof("Current IPv6 default gateway after wakeup: %s via %s", currentV6.IP, currentV6.Intf.Name)
239+
} else {
240+
log.Debugf("No IPv6 default gateway after wakeup: %v", errV6)
241+
}
242+
189243
return
190244
}
191245
}
@@ -207,9 +261,84 @@ func readSleepTimeHash() (uint32, error) {
207261
}
208262

209263
func hash(data []byte) (uint32, error) {
210-
hasher := fnv.New32a() // Create a new 32-bit FNV-1a hasher
264+
hasher := fnv.New32a()
211265
if _, err := hasher.Write(data); err != nil {
212266
return 0, err
213267
}
214268
return hasher.Sum32(), nil
215269
}
270+
271+
// gatewayPoll polls the default gateway every 5 seconds to detect changes that might be missed by routing socket or wake-up detection.
272+
func gatewayPoll(ctx context.Context, initialV4, initialV6 systemops.Nexthop, changed chan<- string) {
273+
ticker := time.NewTicker(5 * time.Second)
274+
defer ticker.Stop()
275+
276+
log.Infof("Gateway polling started - initial v4: %s via %v, v6: %s via %v",
277+
initialV4.IP, initialV4.Intf, initialV6.IP, initialV6.Intf)
278+
279+
for {
280+
select {
281+
case <-ctx.Done():
282+
log.Debug("context canceled, stopping gateway polling")
283+
return
284+
285+
case <-ticker.C:
286+
currentV4, errV4 := systemops.GetNextHop(netip.IPv4Unspecified())
287+
currentV6, errV6 := systemops.GetNextHop(netip.IPv6Unspecified())
288+
289+
var reason string
290+
291+
if errV4 == nil && initialV4.IP.IsValid() {
292+
if currentV4.IP.Compare(initialV4.IP) != 0 {
293+
reason = fmt.Sprintf("IPv4 gateway changed from %s to %s", initialV4.IP, currentV4.IP)
294+
log.Infof("Gateway poll detected change: %s", reason)
295+
changed <- reason
296+
return
297+
}
298+
if initialV4.Intf != nil && currentV4.Intf != nil && currentV4.Intf.Name != initialV4.Intf.Name {
299+
reason = fmt.Sprintf("IPv4 interface changed from %s to %s", initialV4.Intf.Name, currentV4.Intf.Name)
300+
log.Infof("Gateway poll detected change: %s", reason)
301+
changed <- reason
302+
return
303+
}
304+
} else if errV4 == nil && !initialV4.IP.IsValid() {
305+
reason = "IPv4 gateway appeared"
306+
log.Infof("Gateway poll detected change: %s (new: %s)", reason, currentV4.IP)
307+
changed <- reason
308+
return
309+
} else if errV4 != nil && initialV4.IP.IsValid() {
310+
reason = "IPv4 gateway disappeared"
311+
log.Infof("Gateway poll detected change: %s", reason)
312+
changed <- reason
313+
return
314+
}
315+
316+
if errV6 == nil && initialV6.IP.IsValid() {
317+
if currentV6.IP.Compare(initialV6.IP) != 0 {
318+
reason = fmt.Sprintf("IPv6 gateway changed from %s to %s", initialV6.IP, currentV6.IP)
319+
log.Infof("Gateway poll detected change: %s", reason)
320+
changed <- reason
321+
return
322+
}
323+
if initialV6.Intf != nil && currentV6.Intf != nil && currentV6.Intf.Name != initialV6.Intf.Name {
324+
reason = fmt.Sprintf("IPv6 interface changed from %s to %s", initialV6.Intf.Name, currentV6.Intf.Name)
325+
log.Infof("Gateway poll detected change: %s", reason)
326+
changed <- reason
327+
return
328+
}
329+
} else if errV6 == nil && !initialV6.IP.IsValid() {
330+
reason = "IPv6 gateway appeared"
331+
log.Infof("Gateway poll detected change: %s (new: %s)", reason, currentV6.IP)
332+
changed <- reason
333+
return
334+
} else if errV6 != nil && initialV6.IP.IsValid() {
335+
reason = "IPv6 gateway disappeared"
336+
log.Infof("Gateway poll detected change: %s", reason)
337+
changed <- reason
338+
return
339+
}
340+
341+
log.Debugf("Gateway poll: no change detected")
342+
}
343+
}
344+
}

0 commit comments

Comments
 (0)