Files

152 lines
4.1 KiB
Go
Raw Permalink Normal View History

//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: <Publish> <Type> <Metric> <Prefix> <Idx> <Gateway or Iface Name...>
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=<ifName>"
// 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
}