170 lines
4.3 KiB
Go
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")
|
|
}
|