Files

170 lines
4.3 KiB
Go

//go:build darwin
package platform
import (
"bufio"
"fmt"
"net"
"os/exec"
"strings"
)
// DarwinNetworkDiscoverer discovers network configuration using route(8) and
// scutil(8). Both commands are available on every macOS version since 10.5.
type DarwinNetworkDiscoverer struct{}
// Discover returns the default gateway IP and DNS server list for this host.
func (d *DarwinNetworkDiscoverer) Discover() (NetworkInfo, error) {
info := NetworkInfo{}
// Step 1: Determine default gateway and interface via "route -n get default".
gateway, iface, err := darwinDefaultRoute()
if err != nil {
return info, fmt.Errorf("reading default route: %w", err)
}
info.Gateway = gateway
info.Interface = iface
// Step 2: Extract DNS servers from scutil --dns, matching the interface.
servers, err := darwinDNSServers(iface)
if err != nil {
// Non-fatal — return what we have.
return info, nil
}
info.DNSServers = servers
return info, nil
}
// darwinDefaultRoute runs "route -n get default" and returns the gateway IP
// and interface name for the default route.
func darwinDefaultRoute() (gateway, iface string, err error) {
out, err := exec.Command("route", "-n", "get", "default").Output()
if err != nil {
return "", "", fmt.Errorf("route -n get default: %w", err)
}
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "gateway:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
ip := fields[1]
if net.ParseIP(ip) != nil {
gateway = ip
}
}
}
if strings.HasPrefix(line, "interface:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
iface = fields[1]
}
}
}
if gateway == "" {
return "", "", fmt.Errorf("no default gateway found")
}
return gateway, iface, nil
}
// darwinDNSServers runs "scutil --dns" and extracts DNS server IPs from the
// resolver block that matches iface. Falls back to resolver #1 if no match.
func darwinDNSServers(iface string) ([]string, error) {
out, err := exec.Command("scutil", "--dns").Output()
if err != nil {
return nil, fmt.Errorf("scutil --dns: %w", err)
}
type resolverBlock struct {
nameservers []string
ifaceMatch bool
isMDNS bool
number int
}
var blocks []resolverBlock
var current *resolverBlock
inScopedSection := false
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := scanner.Text()
stripped := strings.TrimSpace(line)
// Detect the "scoped queries" section — stop parsing there.
if strings.Contains(line, "DNS configuration (for scoped queries)") {
inScopedSection = true
}
if inScopedSection {
continue
}
// Start of a new resolver block.
if strings.HasPrefix(stripped, "resolver #") {
if current != nil {
blocks = append(blocks, *current)
}
num := 0
fmt.Sscanf(stripped, "resolver #%d", &num)
current = &resolverBlock{number: num}
continue
}
if current == nil {
continue
}
// Parse nameserver entries.
if strings.HasPrefix(stripped, "nameserver[") {
// Format: "nameserver[0] : 8.8.8.8"
idx := strings.Index(stripped, ": ")
if idx >= 0 {
ip := strings.TrimSpace(stripped[idx+2:])
if net.ParseIP(ip) != nil {
current.nameservers = append(current.nameservers, ip)
}
}
continue
}
// Check for interface match via if_index line: "if_index : 5 (en0)"
if strings.HasPrefix(stripped, "if_index") && iface != "" {
if strings.Contains(stripped, "("+iface+")") {
current.ifaceMatch = true
}
continue
}
// Detect mDNS blocks: domain + options lines.
if strings.HasPrefix(stripped, "domain") && strings.Contains(stripped, ": local") {
current.isMDNS = true
}
if strings.HasPrefix(stripped, "options") && strings.Contains(stripped, "mdns") {
current.isMDNS = true
}
}
if current != nil {
blocks = append(blocks, *current)
}
// Find the best block: interface-matching, non-mDNS.
for _, b := range blocks {
if b.ifaceMatch && !b.isMDNS && len(b.nameservers) > 0 {
return b.nameservers, nil
}
}
// Fallback: use resolver #1 if it's not mDNS.
for _, b := range blocks {
if b.number == 1 && !b.isMDNS && len(b.nameservers) > 0 {
return b.nameservers, nil
}
}
return nil, fmt.Errorf("no usable DNS resolver block found in scutil output")
}