//go:build windows package platform import ( "bufio" "fmt" "net" "os/exec" "regexp" "strconv" "strings" ) // WindowsNetworkDiscoverer discovers network configuration using netsh commands. // It does not require elevated privileges or PowerShell. type WindowsNetworkDiscoverer struct{} var ipv4Re = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`) var dnsLineRe = regexp.MustCompile(`^\s*(?:\S.*:\s+)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*$`) // Discover returns the gateway IP and DNS servers for the default IPv4 route. func (d *WindowsNetworkDiscoverer) Discover() (NetworkInfo, error) { info := NetworkInfo{} // Step 1: Find default route — extract interface index and gateway IP. ifIdx, gateway, err := windowsDefaultRoute() if err != nil { return info, fmt.Errorf("reading default IPv4 route: %w", err) } info.Gateway = gateway // Step 2: Map interface index to name. ifName, err := windowsInterfaceName(ifIdx) if err != nil { return info, fmt.Errorf("resolving interface name for index %d: %w", ifIdx, err) } info.Interface = ifName // Step 3: Enumerate DNS servers for that interface. servers, err := windowsDNSServers(ifName) if err != nil { return info, fmt.Errorf("reading DNS servers for interface %q: %w", ifName, err) } info.DNSServers = servers return info, nil } // windowsDefaultRoute runs "netsh interface ipv4 show route" and returns the // interface index and gateway IP for the best (lowest-metric) 0.0.0.0/0 route. func windowsDefaultRoute() (ifIdx int, gateway string, err error) { out, err := exec.Command("netsh", "interface", "ipv4", "show", "route").Output() if err != nil { return 0, "", fmt.Errorf("netsh interface ipv4 show route: %w", err) } bestMetric := -1 bestIdx := 0 bestGateway := "" scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { line := scanner.Text() if !strings.Contains(line, "0.0.0.0/0") { continue } // Format: fields := strings.Fields(line) if len(fields) < 6 { continue } metric, err := strconv.Atoi(fields[2]) if err != nil { continue } idx, err := strconv.Atoi(fields[4]) if err != nil { continue } // Gateway field is field 5+ — join and extract first IPv4. gatewayField := strings.Join(fields[5:], " ") m := ipv4Re.FindString(gatewayField) if m == "" { continue } if bestMetric < 0 || metric < bestMetric { bestMetric = metric bestIdx = idx bestGateway = m } } if bestIdx == 0 { return 0, "", fmt.Errorf("no default IPv4 route found") } return bestIdx, bestGateway, nil } // windowsInterfaceName runs "netsh interface ipv4 show interfaces" and maps // the given interface index to the interface name. func windowsInterfaceName(idx int) (string, error) { out, err := exec.Command("netsh", "interface", "ipv4", "show", "interfaces").Output() if err != nil { return "", fmt.Errorf("netsh interface ipv4 show interfaces: %w", err) } idxStr := strconv.Itoa(idx) scanner := bufio.NewScanner(strings.NewReader(string(out))) // Skip two header lines. for i := 0; i < 2; i++ { scanner.Scan() } for scanner.Scan() { line := scanner.Text() fields := strings.Fields(line) if len(fields) < 5 { continue } if fields[0] != idxStr { continue } // Name is fields[4:] joined — may contain spaces. return strings.Join(fields[4:], " "), nil } return "", fmt.Errorf("interface index %d not found", idx) } // windowsDNSServers runs "netsh interface ipv4 show dnsservers name=" // and returns all IPv4 DNS server addresses in order. func windowsDNSServers(ifName string) ([]string, error) { out, err := exec.Command("netsh", "interface", "ipv4", "show", "dnsservers", "name="+ifName).Output() if err != nil { return nil, fmt.Errorf("netsh interface ipv4 show dnsservers: %w", err) } var servers []string scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { m := dnsLineRe.FindStringSubmatch(scanner.Text()) if len(m) < 2 { continue } ip := m[1] if net.ParseIP(ip) != nil { servers = append(servers, ip) } } return servers, nil }