Skip to content

Commit 8c108cc

Browse files
authored
[client] Extend Darwin network monitoring with wakeup detection
1 parent 86eff0d commit 8c108cc

File tree

4 files changed

+246
-73
lines changed

4 files changed

+246
-73
lines changed
Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,30 @@
1-
//go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd
1+
//go:build dragonfly || freebsd || netbsd || openbsd
22

33
package networkmonitor
44

55
import (
66
"context"
77
"errors"
88
"fmt"
9-
"syscall"
10-
"unsafe"
119

1210
log "github.com/sirupsen/logrus"
13-
"golang.org/x/net/route"
1411
"golang.org/x/sys/unix"
1512

1613
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
1714
)
1815

1916
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
20-
fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
17+
fd, err := prepareFd()
2118
if err != nil {
2219
return fmt.Errorf("open routing socket: %v", err)
2320
}
21+
2422
defer func() {
2523
err := unix.Close(fd)
2624
if err != nil && !errors.Is(err, unix.EBADF) {
2725
log.Warnf("Network monitor: failed to close routing socket: %v", err)
2826
}
2927
}()
3028

31-
for {
32-
select {
33-
case <-ctx.Done():
34-
return ctx.Err()
35-
default:
36-
buf := make([]byte, 2048)
37-
n, err := unix.Read(fd, buf)
38-
if err != nil {
39-
if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) {
40-
log.Warnf("Network monitor: failed to read from routing socket: %v", err)
41-
}
42-
continue
43-
}
44-
if n < unix.SizeofRtMsghdr {
45-
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
46-
continue
47-
}
48-
49-
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
50-
51-
switch msg.Type {
52-
// handle route changes
53-
case unix.RTM_ADD, syscall.RTM_DELETE:
54-
route, err := parseRouteMessage(buf[:n])
55-
if err != nil {
56-
log.Debugf("Network monitor: error parsing routing message: %v", err)
57-
continue
58-
}
59-
60-
if route.Dst.Bits() != 0 {
61-
continue
62-
}
63-
64-
intf := "<nil>"
65-
if route.Interface != nil {
66-
intf = route.Interface.Name
67-
}
68-
switch msg.Type {
69-
case unix.RTM_ADD:
70-
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
71-
return nil
72-
case unix.RTM_DELETE:
73-
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
74-
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
75-
return nil
76-
}
77-
}
78-
}
79-
}
80-
}
81-
}
82-
83-
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
84-
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
85-
if err != nil {
86-
return nil, fmt.Errorf("parse RIB: %v", err)
87-
}
88-
89-
if len(msgs) != 1 {
90-
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
91-
}
92-
93-
msg, ok := msgs[0].(*route.RouteMessage)
94-
if !ok {
95-
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
96-
}
97-
98-
return systemops.MsgToRoute(msg)
29+
return routeCheck(ctx, fd, nexthopv4, nexthopv6)
9930
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//go:build dragonfly || freebsd || netbsd || openbsd || darwin
2+
3+
package networkmonitor
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"syscall"
10+
"unsafe"
11+
12+
log "github.com/sirupsen/logrus"
13+
"golang.org/x/net/route"
14+
"golang.org/x/sys/unix"
15+
16+
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
17+
)
18+
19+
func prepareFd() (int, error) {
20+
return unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
21+
}
22+
23+
func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error {
24+
for {
25+
select {
26+
case <-ctx.Done():
27+
return ctx.Err()
28+
default:
29+
buf := make([]byte, 2048)
30+
n, err := unix.Read(fd, buf)
31+
if err != nil {
32+
if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) {
33+
log.Warnf("Network monitor: failed to read from routing socket: %v", err)
34+
}
35+
continue
36+
}
37+
if n < unix.SizeofRtMsghdr {
38+
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
39+
continue
40+
}
41+
42+
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
43+
44+
switch msg.Type {
45+
// handle route changes
46+
case unix.RTM_ADD, syscall.RTM_DELETE:
47+
route, err := parseRouteMessage(buf[:n])
48+
if err != nil {
49+
log.Debugf("Network monitor: error parsing routing message: %v", err)
50+
continue
51+
}
52+
53+
if route.Dst.Bits() != 0 {
54+
continue
55+
}
56+
57+
intf := "<nil>"
58+
if route.Interface != nil {
59+
intf = route.Interface.Name
60+
}
61+
switch msg.Type {
62+
case unix.RTM_ADD:
63+
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
64+
return nil
65+
case unix.RTM_DELETE:
66+
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
67+
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
68+
return nil
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
77+
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
78+
if err != nil {
79+
return nil, fmt.Errorf("parse RIB: %v", err)
80+
}
81+
82+
if len(msgs) != 1 {
83+
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
84+
}
85+
86+
msg, ok := msgs[0].(*route.RouteMessage)
87+
if !ok {
88+
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
89+
}
90+
91+
return systemops.MsgToRoute(msg)
92+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//go:build darwin && !ios
2+
3+
package networkmonitor
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"hash/fnv"
10+
"os/exec"
11+
"time"
12+
13+
log "github.com/sirupsen/logrus"
14+
"golang.org/x/sys/unix"
15+
16+
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
17+
)
18+
19+
// todo: refactor to not use static functions
20+
21+
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
22+
fd, err := prepareFd()
23+
if err != nil {
24+
return fmt.Errorf("open routing socket: %v", err)
25+
}
26+
27+
defer func() {
28+
if err := unix.Close(fd); err != nil {
29+
if !errors.Is(err, unix.EBADF) {
30+
log.Warnf("Network monitor: failed to close routing socket: %v", err)
31+
}
32+
}
33+
}()
34+
35+
routeChanged := make(chan struct{})
36+
go func() {
37+
_ = routeCheck(ctx, fd, nexthopv4, nexthopv6)
38+
close(routeChanged)
39+
}()
40+
41+
wakeUp := make(chan struct{})
42+
go func() {
43+
wakeUpListen(ctx)
44+
close(wakeUp)
45+
}()
46+
47+
select {
48+
case <-ctx.Done():
49+
return ctx.Err()
50+
case <-routeChanged:
51+
if ctx.Err() != nil {
52+
return ctx.Err()
53+
}
54+
log.Infof("route change detected")
55+
return nil
56+
case <-wakeUp:
57+
if ctx.Err() != nil {
58+
return ctx.Err()
59+
}
60+
log.Infof("wakeup detected")
61+
return nil
62+
}
63+
}
64+
65+
func wakeUpListen(ctx context.Context) {
66+
log.Infof("start to watch for system wakeups")
67+
var (
68+
initialHash uint32
69+
err error
70+
)
71+
72+
// Keep retrying until initial sysctl succeeds or context is canceled
73+
for {
74+
select {
75+
case <-ctx.Done():
76+
log.Info("exit from wakeUpListen initial hash detection due to context cancellation")
77+
return
78+
default:
79+
initialHash, err = readSleepTimeHash()
80+
if err != nil {
81+
log.Errorf("failed to detect initial sleep time: %v", err)
82+
select {
83+
case <-ctx.Done():
84+
log.Info("exit from wakeUpListen initial hash detection due to context cancellation")
85+
return
86+
case <-time.After(3 * time.Second):
87+
continue
88+
}
89+
}
90+
log.Debugf("initial wakeup hash: %d", initialHash)
91+
break
92+
}
93+
break
94+
}
95+
96+
ticker := time.NewTicker(5 * time.Second)
97+
defer ticker.Stop()
98+
99+
for {
100+
select {
101+
case <-ctx.Done():
102+
log.Info("context canceled, stopping wakeUpListen")
103+
return
104+
105+
case <-ticker.C:
106+
newHash, err := readSleepTimeHash()
107+
if err != nil {
108+
log.Errorf("failed to read sleep time hash: %v", err)
109+
continue
110+
}
111+
112+
if newHash == initialHash {
113+
log.Tracef("no wakeup detected")
114+
continue
115+
}
116+
117+
upOut, err := exec.Command("uptime").Output()
118+
if err != nil {
119+
log.Errorf("failed to run uptime command: %v", err)
120+
upOut = []byte("unknown")
121+
}
122+
log.Infof("Wakeup detected: %d -> %d, uptime: %s", initialHash, newHash, upOut)
123+
return
124+
}
125+
}
126+
}
127+
128+
func readSleepTimeHash() (uint32, error) {
129+
cmd := exec.Command("sysctl", "kern.sleeptime")
130+
out, err := cmd.Output()
131+
if err != nil {
132+
return 0, fmt.Errorf("failed to run sysctl: %w", err)
133+
}
134+
135+
h, err := hash(out)
136+
if err != nil {
137+
return 0, fmt.Errorf("failed to compute hash: %w", err)
138+
}
139+
140+
return h, nil
141+
}
142+
143+
func hash(data []byte) (uint32, error) {
144+
hasher := fnv.New32a() // Create a new 32-bit FNV-1a hasher
145+
if _, err := hasher.Write(data); err != nil {
146+
return 0, err
147+
}
148+
return hasher.Sum32(), nil
149+
}

client/internal/networkmonitor/monitor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
8888
event := make(chan struct{}, 1)
8989
go nw.checkChanges(ctx, event, nexthop4, nexthop6)
9090

91+
log.Infof("start watching for network changes")
9192
// debounce changes
9293
timer := time.NewTimer(0)
9394
timer.Stop()

0 commit comments

Comments
 (0)