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
209263func 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