package networkmonitor import ( "context" "fmt" "net" "net/netip" "strings" "time" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) const ( unreachable = 0 incomplete = 1 probe = 2 delay = 3 stale = 4 reachable = 5 permanent = 6 tbd = 7 ) const interval = 10 * time.Second func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error { var neighborv4, neighborv6 *systemops.Neighbor { initialNeighbors, err := getNeighbors() if err != nil { return fmt.Errorf("get neighbors: %w", err) } neighborv4 = assignNeighbor(nexthopv4, initialNeighbors) neighborv6 = assignNeighbor(nexthopv6, initialNeighbors) } log.Debugf("Network monitor: initial IPv4 neighbor: %v, IPv6 neighbor: %v", neighborv4, neighborv6) ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return ErrStopped case <-ticker.C: if changed(nexthopv4, neighborv4, nexthopv6, neighborv6) { go callback() return nil } } } } func assignNeighbor(nexthop systemops.Nexthop, initialNeighbors map[netip.Addr]systemops.Neighbor) *systemops.Neighbor { if n, ok := initialNeighbors[nexthop.IP]; ok && n.State != unreachable && n.State != incomplete && n.State != tbd { return &n } return nil } func changed( nexthopv4 systemops.Nexthop, neighborv4 *systemops.Neighbor, nexthopv6 systemops.Nexthop, neighborv6 *systemops.Neighbor, ) bool { neighbors, err := getNeighbors() if err != nil { log.Errorf("network monitor: error fetching current neighbors: %v", err) return false } if neighborChanged(nexthopv4, neighborv4, neighbors) || neighborChanged(nexthopv6, neighborv6, neighbors) { return true } routes, err := getRoutes() if err != nil { log.Errorf("network monitor: error fetching current routes: %v", err) return false } if routeChanged(nexthopv4, nexthopv4.Intf, routes) || routeChanged(nexthopv6, nexthopv6.Intf, routes) { return true } return false } // routeChanged checks if the default routes still point to our nexthop/interface func routeChanged(nexthop systemops.Nexthop, intf *net.Interface, routes []systemops.Route) bool { if !nexthop.IP.IsValid() { return false } if isSoftInterface(nexthop.Intf.Name) { log.Tracef("network monitor: ignoring default route change for soft interface %s", nexthop.Intf.Name) return false } unspec := getUnspecifiedPrefix(nexthop.IP) defaultRoutes, foundMatchingRoute := processRoutes(nexthop, intf, routes, unspec) log.Tracef("network monitor: all default routes:\n%s", strings.Join(defaultRoutes, "\n")) if !foundMatchingRoute { logRouteChange(nexthop.IP, intf) return true } return false } func getUnspecifiedPrefix(ip netip.Addr) netip.Prefix { if ip.Is6() { return netip.PrefixFrom(netip.IPv6Unspecified(), 0) } return netip.PrefixFrom(netip.IPv4Unspecified(), 0) } func processRoutes(nexthop systemops.Nexthop, nexthopIntf *net.Interface, routes []systemops.Route, unspec netip.Prefix) ([]string, bool) { var defaultRoutes []string foundMatchingRoute := false for _, r := range routes { if r.Destination == unspec { routeInfo := formatRouteInfo(r) defaultRoutes = append(defaultRoutes, routeInfo) if r.Nexthop == nexthop.IP && compareIntf(r.Interface, nexthopIntf) == 0 { foundMatchingRoute = true log.Debugf("network monitor: found matching default route: %s", routeInfo) } } } return defaultRoutes, foundMatchingRoute } func formatRouteInfo(r systemops.Route) string { newIntf := "" if r.Interface != nil { newIntf = r.Interface.Name } return fmt.Sprintf("Nexthop: %s, Interface: %s", r.Nexthop, newIntf) } func logRouteChange(ip netip.Addr, intf *net.Interface) { oldIntf := "" if intf != nil { oldIntf = intf.Name } log.Infof("network monitor: default route for %s (%s) is gone or changed", ip, oldIntf) } func neighborChanged(nexthop systemops.Nexthop, neighbor *systemops.Neighbor, neighbors map[netip.Addr]systemops.Neighbor) bool { if neighbor == nil { return false } // TODO: consider non-local nexthops, e.g. on point-to-point interfaces if n, ok := neighbors[nexthop.IP]; ok { if n.State == unreachable || n.State == incomplete { log.Infof("network monitor: neighbor %s (%s) is not reachable: %s", neighbor.IPAddress, neighbor.LinkLayerAddress, stateFromInt(n.State)) return true } else if n.InterfaceIndex != neighbor.InterfaceIndex { log.Infof( "network monitor: neighbor %s (%s) changed interface from '%s' (%d) to '%s' (%d): %s", neighbor.IPAddress, neighbor.LinkLayerAddress, neighbor.InterfaceAlias, neighbor.InterfaceIndex, n.InterfaceAlias, n.InterfaceIndex, stateFromInt(n.State), ) return true } } else { log.Infof("network monitor: neighbor %s (%s) is gone", neighbor.IPAddress, neighbor.LinkLayerAddress) return true } return false } func getNeighbors() (map[netip.Addr]systemops.Neighbor, error) { entries, err := systemops.GetNeighbors() if err != nil { return nil, fmt.Errorf("get neighbors: %w", err) } neighbours := make(map[netip.Addr]systemops.Neighbor, len(entries)) for _, entry := range entries { neighbours[entry.IPAddress] = entry } return neighbours, nil } func getRoutes() ([]systemops.Route, error) { entries, err := systemops.GetRoutes() if err != nil { return nil, fmt.Errorf("get routes: %w", err) } return entries, nil } func stateFromInt(state uint8) string { switch state { case unreachable: return "unreachable" case incomplete: return "incomplete" case probe: return "probe" case delay: return "delay" case stale: return "stale" case reachable: return "reachable" case permanent: return "permanent" case tbd: return "tbd" default: return "unknown" } } func compareIntf(a, b *net.Interface) int { switch { case a == nil && b == nil: return 0 case a == nil: return -1 case b == nil: return 1 default: return a.Index - b.Index } } func isSoftInterface(name string) bool { return strings.Contains(strings.ToLower(name), "isatap") || strings.Contains(strings.ToLower(name), "teredo") }