From 38064d6ad54b75ac340568affd1889cd1eb39f56 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 6 Feb 2025 13:30:11 -0500 Subject: [PATCH] parse InterfaceIPs for network delta, not just ifs block --- cmd/cli/dns_proxy.go | 141 +++++++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 40 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 01e1673..f9d6478 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/hex" + "encoding/json" "errors" "fmt" "net" @@ -11,6 +12,7 @@ import ( "os/exec" "runtime" "slices" + "sort" "strconv" "strings" "sync" @@ -1309,44 +1311,68 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error { // Get map of valid interfaces validIfaces := validInterfacesMap() - // log the delta for debugging + // Log the delta for debugging mainLog.Load().Warn(). Interface("old_state", delta.Old). Interface("new_state", delta.New). Msg("Network change detected") - // Parse old and new interface states - oldIfs := parseInterfaceState(delta.Old) - newIfs := parseInterfaceState(delta.New) + // Parse old and new interface states, and extract IPs as well. + oldIfs, oldIPs := parseInterfaceState(delta.Old) + newIfs, newIPs := parseInterfaceState(delta.New) - // Check for changes in valid interfaces changed := false var changedIface, changedIfaceState string activeInterfaceExists := false + // Iterate over valid interfaces. for ifaceName := range validIfaces { - oldState, oldExists := oldIfs[strings.ToLower(ifaceName)] - newState, newExists := newIfs[strings.ToLower(ifaceName)] + lname := strings.ToLower(ifaceName) + oldState, oldExists := oldIfs[lname] + newState, newExists := newIfs[lname] + // Check if the interface appears active in the new state. if newState != "" && !strings.Contains(newState, "down") { activeInterfaceExists = true } - // Compare states directly - if oldExists != newExists || oldState != newState { + // Compare raw state strings... + stateChanged := (oldExists != newExists || oldState != newState) - // If the interface is up, we need to reinitialize the OS resolver + // ... and also compare the parsed IP slices. + ipChanged := false + oldIPSlice, okOld := oldIPs[lname] + newIPSlice, okNew := newIPs[lname] + if okOld && okNew { + // Create copies and sort them so that order does not matter. + sortedOld := append([]string(nil), oldIPSlice...) + sortedNew := append([]string(nil), newIPSlice...) + sort.Strings(sortedOld) + sort.Strings(sortedNew) + if !slices.Equal(sortedOld, sortedNew) { + ipChanged = true + } + } else if okOld != okNew { + ipChanged = true + } + + // If either the state string or the IPs have changed... + if stateChanged || ipChanged { if newState != "" && !strings.Contains(newState, "down") { changed = true changedIface = ifaceName - changedIfaceState = newState + // Prefer newState if present; if not, generate one from the IP slice. + if newState == "" && okNew { + changedIfaceState = "[" + strings.Join(newIPSlice, " ") + "]" + } else { + changedIfaceState = newState + } } - mainLog.Load().Warn(). Str("interface", ifaceName). Str("old_state", oldState). Str("new_state", newState). - Msg("Valid interface changed state") + Msg("Valid interface changed state (IP change detected: " + strconv.FormatBool(ipChanged) + ")") break } else { mainLog.Load().Warn(). @@ -1367,19 +1393,19 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error { return } - // Use the defaultRouteIP() result or fallback to the changed interface's IP from the delta. + // Use the defaultRouteIP() result, or fall back to the changed interface's IPv4 from the new state. selfIP := defaultRouteIP() if selfIP == "" && changedIface != "" { selfIP = extractIPv4FromState(changedIfaceState) - mainLog.Load().Info().Msgf("defaultRouteIP returned empty, using changed iface '%s' IP: %s", changedIface, selfIP) + mainLog.Load().Info().Msgf("defaultRouteIP returned empty, using changed iface '%s' IPv4: %s", changedIface, selfIP) } - // Extract IPv6 from the changed interface state. + // Extract IPv6 from the changed state. ipv6 := extractIPv6FromState(changedIfaceState) if ip := net.ParseIP(selfIP); ip != nil { ctrld.SetDefaultLocalIPv4(ip) - // if we have a new IP, set the client info to the new IP + // If we have a new IP, update the client info. if !isMobile() && p.ciTable != nil { p.ciTable.SetSelfIP(selfIP) } @@ -1396,52 +1422,87 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error { return nil } -// parseInterfaceState parses the interface state string into a map of interface name -> state -func parseInterfaceState(state *netmon.State) map[string]string { +// parseInterfaceState parses the netmon state into two maps: +// 1. stateMap: a mapping from interfaces (lowercase) to their original state string, +// formatted in square brackets (e.g. "[192.168.1.200/24 fe80::69f6:e16e:8bdb:0000/64]"). +// 2. ipMap: a mapping from interfaces (lowercase) to a slice of IP addresses extracted from that state. +// +// It first attempts JSON parsing to pull out both the "Interface" and "InterfaceIPs" fields. +// If JSON parsing fails, it falls back to the legacy parsing logic. +func parseInterfaceState(state *netmon.State) (map[string]string, map[string][]string) { + result := make(map[string]string) // Interface name -> state string. + ipMap := make(map[string][]string) // Interface name -> slice of IP addresses. + if state == nil { - return nil + return result, ipMap } - - result := make(map[string]string) - stateStr := state.String() - // Extract interface information - ifsStart := strings.Index(stateStr, "ifs={") - if ifsStart == -1 { - return result + // Attempt to parse the state string as JSON so we can extract both "Interface" and "InterfaceIPs". + var raw map[string]json.RawMessage + if err := json.Unmarshal([]byte(stateStr), &raw); err == nil { + var interfaces map[string]interface{} + var interfaceIPs map[string][]string + + if v, ok := raw["Interface"]; ok { + _ = json.Unmarshal(v, &interfaces) + } + if v, ok := raw["InterfaceIPs"]; ok { + _ = json.Unmarshal(v, &interfaceIPs) + } + // For every interface in the "Interface" section, check for its IPs. + for name := range interfaces { + lowerName := strings.ToLower(name) + if ips, ok := interfaceIPs[name]; ok && len(ips) > 0 { + result[lowerName] = "[" + strings.Join(ips, " ") + "]" + ipMap[lowerName] = ips + } else { + result[lowerName] = "[]" + ipMap[lowerName] = []string{} + } + } + return result, ipMap } + // Fallback: try parsing the legacy "ifs={...}" section from the state string. + ifsStart := strings.Index(stateStr, "ifs={") + if ifsStart == -1 { + return result, ipMap + } ifsStr := stateStr[ifsStart+5:] ifsEnd := strings.Index(ifsStr, "}") if ifsEnd == -1 { - return result + return result, ipMap } - - // Get the content between ifs={ } ifsContent := strings.TrimSpace(ifsStr[:ifsEnd]) - - // Split on "] " to get each interface entry entries := strings.Split(ifsContent, "] ") - for _, entry := range entries { if entry == "" { continue } - - // Split on ":[" parts := strings.Split(entry, ":[") if len(parts) != 2 { continue } - name := strings.TrimSpace(parts[0]) - state := "[" + strings.TrimSuffix(parts[1], "]") + "]" + stateEntry := "[" + strings.TrimSuffix(parts[1], "]") + "]" + lowerName := strings.ToLower(name) + result[lowerName] = stateEntry - result[strings.ToLower(name)] = state + // Attempt to extract IP addresses from stateEntry. + ipList := []string{} + trimmed := strings.Trim(stateEntry, "[]") + fields := strings.Fields(trimmed) + for _, f := range fields { + // We assume the IP is the part before the "/", if present. + candidate := strings.Split(f, "/")[0] + if ip := net.ParseIP(candidate); ip != nil { + ipList = append(ipList, candidate) + } + } + ipMap[lowerName] = ipList } - - return result + return result, ipMap } // extractIPv4FromState extracts an IPv4 address from an interface state string.