parse InterfaceIPs for network delta, not just ifs block

This commit is contained in:
Alex
2025-02-06 13:30:11 -05:00
committed by Cuong Manh Le
parent ae6945cedf
commit 38064d6ad5

View File

@@ -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.