mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
[client] Extend Darwin network monitoring with wakeup detection
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
//go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd
|
//go:build dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
package networkmonitor
|
package networkmonitor
|
||||||
|
|
||||||
@@ -6,21 +6,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/route"
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
||||||
fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
|
fd, err := prepareFd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open routing socket: %v", err)
|
return fmt.Errorf("open routing socket: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := unix.Close(fd)
|
err := unix.Close(fd)
|
||||||
if err != nil && !errors.Is(err, unix.EBADF) {
|
if err != nil && !errors.Is(err, unix.EBADF) {
|
||||||
@@ -28,72 +26,5 @@ func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) er
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
return routeCheck(ctx, fd, nexthopv4, nexthopv6)
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
n, err := unix.Read(fd, buf)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) {
|
|
||||||
log.Warnf("Network monitor: failed to read from routing socket: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n < unix.SizeofRtMsghdr {
|
|
||||||
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
|
|
||||||
|
|
||||||
switch msg.Type {
|
|
||||||
// handle route changes
|
|
||||||
case unix.RTM_ADD, syscall.RTM_DELETE:
|
|
||||||
route, err := parseRouteMessage(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.Dst.Bits() != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
intf := "<nil>"
|
|
||||||
if route.Interface != nil {
|
|
||||||
intf = route.Interface.Name
|
|
||||||
}
|
|
||||||
switch msg.Type {
|
|
||||||
case unix.RTM_ADD:
|
|
||||||
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
|
||||||
return nil
|
|
||||||
case unix.RTM_DELETE:
|
|
||||||
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
|
|
||||||
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
|
||||||
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse RIB: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(msgs) != 1 {
|
|
||||||
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, ok := msgs[0].(*route.RouteMessage)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemops.MsgToRoute(msg)
|
|
||||||
}
|
}
|
||||||
|
|||||||
92
client/internal/networkmonitor/check_change_common.go
Normal file
92
client/internal/networkmonitor/check_change_common.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
//go:build dragonfly || freebsd || netbsd || openbsd || darwin
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/net/route"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareFd() (int, error) {
|
||||||
|
return unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, err := unix.Read(fd, buf)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) {
|
||||||
|
log.Warnf("Network monitor: failed to read from routing socket: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n < unix.SizeofRtMsghdr {
|
||||||
|
log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
// handle route changes
|
||||||
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
|
route, err := parseRouteMessage(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if route.Dst.Bits() != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
intf := "<nil>"
|
||||||
|
if route.Interface != nil {
|
||||||
|
intf = route.Interface.Name
|
||||||
|
}
|
||||||
|
switch msg.Type {
|
||||||
|
case unix.RTM_ADD:
|
||||||
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
|
return nil
|
||||||
|
case unix.RTM_DELETE:
|
||||||
|
if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 {
|
||||||
|
log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
||||||
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse RIB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgs) != 1 {
|
||||||
|
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ok := msgs[0].(*route.RouteMessage)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemops.MsgToRoute(msg)
|
||||||
|
}
|
||||||
149
client/internal/networkmonitor/check_change_darwin.go
Normal file
149
client/internal/networkmonitor/check_change_darwin.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
//go:build darwin && !ios
|
||||||
|
|
||||||
|
package networkmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
|
)
|
||||||
|
|
||||||
|
// todo: refactor to not use static functions
|
||||||
|
|
||||||
|
func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
|
||||||
|
fd, err := prepareFd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open routing socket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := unix.Close(fd); err != nil {
|
||||||
|
if !errors.Is(err, unix.EBADF) {
|
||||||
|
log.Warnf("Network monitor: failed to close routing socket: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
routeChanged := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_ = routeCheck(ctx, fd, nexthopv4, nexthopv6)
|
||||||
|
close(routeChanged)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wakeUp := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wakeUpListen(ctx)
|
||||||
|
close(wakeUp)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-routeChanged:
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
log.Infof("route change detected")
|
||||||
|
return nil
|
||||||
|
case <-wakeUp:
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
log.Infof("wakeup detected")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wakeUpListen(ctx context.Context) {
|
||||||
|
log.Infof("start to watch for system wakeups")
|
||||||
|
var (
|
||||||
|
initialHash uint32
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep retrying until initial sysctl succeeds or context is canceled
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Info("exit from wakeUpListen initial hash detection due to context cancellation")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
initialHash, err = readSleepTimeHash()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to detect initial sleep time: %v", err)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Info("exit from wakeUpListen initial hash detection due to context cancellation")
|
||||||
|
return
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("initial wakeup hash: %d", initialHash)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Info("context canceled, stopping wakeUpListen")
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-ticker.C:
|
||||||
|
newHash, err := readSleepTimeHash()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to read sleep time hash: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHash == initialHash {
|
||||||
|
log.Tracef("no wakeup detected")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
upOut, err := exec.Command("uptime").Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to run uptime command: %v", err)
|
||||||
|
upOut = []byte("unknown")
|
||||||
|
}
|
||||||
|
log.Infof("Wakeup detected: %d -> %d, uptime: %s", initialHash, newHash, upOut)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSleepTimeHash() (uint32, error) {
|
||||||
|
cmd := exec.Command("sysctl", "kern.sleeptime")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to run sysctl: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := hash(out)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to compute hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(data []byte) (uint32, error) {
|
||||||
|
hasher := fnv.New32a() // Create a new 32-bit FNV-1a hasher
|
||||||
|
if _, err := hasher.Write(data); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return hasher.Sum32(), nil
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
|
|||||||
event := make(chan struct{}, 1)
|
event := make(chan struct{}, 1)
|
||||||
go nw.checkChanges(ctx, event, nexthop4, nexthop6)
|
go nw.checkChanges(ctx, event, nexthop4, nexthop6)
|
||||||
|
|
||||||
|
log.Infof("start watching for network changes")
|
||||||
// debounce changes
|
// debounce changes
|
||||||
timer := time.NewTimer(0)
|
timer := time.NewTimer(0)
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user