Files
dnshelper/platform/network_linux.go
T

201 lines
4.9 KiB
Go

//go:build linux
package platform
import (
"bufio"
"fmt"
"net"
"os"
"os/exec"
"strings"
)
// LinuxNetworkDiscoverer discovers network configuration using ip(8) and
// /etc/resolv.conf. On systemd-resolved systems it falls back to resolvectl(1)
// when all nameservers are stub addresses.
type LinuxNetworkDiscoverer struct{}
// stubAddrs are local stub resolver addresses used by systemd-resolved.
var stubAddrs = map[string]bool{
"127.0.0.53": true,
"127.0.0.1": true,
"127.0.1.1": true,
}
// Discover returns the default gateway and DNS server list for this host.
func (d *LinuxNetworkDiscoverer) Discover() (NetworkInfo, error) {
info := NetworkInfo{}
// Step 1: Determine default gateway and interface via "ip route show default".
gateway, iface, err := linuxDefaultRoute()
if err != nil {
return info, fmt.Errorf("reading default route: %w", err)
}
info.Gateway = gateway
info.Interface = iface
// Step 2: Parse /etc/resolv.conf for nameserver lines.
servers, err := linuxResolvConf()
if err != nil {
// A missing resolv.conf is not fatal — return empty list.
return info, nil
}
// Step 3: If all servers are stub addresses, try resolvectl for upstream IPs.
if len(servers) > 0 && allStubs(servers) && iface != "" {
if upstream, err := linuxResolvectl(iface); err == nil && len(upstream) > 0 {
servers = upstream
}
}
info.DNSServers = servers
return info, nil
}
// linuxDefaultRoute parses "ip route show default" and returns the gateway IP
// and interface name for the best (lowest metric) default route.
func linuxDefaultRoute() (gateway, iface string, err error) {
out, err := exec.Command("ip", "route", "show", "default").Output()
if err != nil {
return "", "", fmt.Errorf("ip route show default: %w", err)
}
bestMetric := -1
bestGW := ""
bestIface := ""
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) == 0 || fields[0] != "default" {
continue
}
// Parse by keyword: "via <ip>" and "dev <iface>" and optional "metric <N>".
var gw, dev string
metric := 0
for i := 1; i < len(fields)-1; i++ {
switch fields[i] {
case "via":
gw = fields[i+1]
case "dev":
dev = fields[i+1]
case "metric":
if n, err := parseInt(fields[i+1]); err == nil {
metric = n
}
}
}
if gw == "" || net.ParseIP(gw) == nil {
continue
}
if bestMetric < 0 || metric < bestMetric {
bestMetric = metric
bestGW = gw
bestIface = dev
}
}
if bestGW == "" {
return "", "", fmt.Errorf("no default route found")
}
return bestGW, bestIface, nil
}
// linuxResolvConf reads /etc/resolv.conf and returns the nameserver IPs.
func linuxResolvConf() ([]string, error) {
f, err := os.Open("/etc/resolv.conf")
if err != nil {
return nil, err
}
defer f.Close()
var servers []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if !strings.HasPrefix(line, "nameserver") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
ip := fields[1]
if net.ParseIP(ip) != nil {
servers = append(servers, ip)
}
}
return servers, nil
}
// linuxResolvectl runs "resolvectl status <iface>" and returns the upstream DNS
// servers. Returns an error if resolvectl is not available.
func linuxResolvectl(iface string) ([]string, error) {
path, err := exec.LookPath("resolvectl")
if err != nil {
return nil, fmt.Errorf("resolvectl not found: %w", err)
}
out, err := exec.Command(path, "status", iface).Output()
if err != nil {
return nil, fmt.Errorf("resolvectl status %s: %w", iface, err)
}
// Parse "DNS Servers: <ip> <ip>..." and continuation lines.
var servers []string
inDNS := false
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := scanner.Text()
stripped := strings.TrimSpace(line)
if strings.HasPrefix(stripped, "DNS Servers:") {
inDNS = true
// Extract IPs from the same line after the label.
rest := strings.TrimPrefix(stripped, "DNS Servers:")
for _, f := range strings.Fields(rest) {
if net.ParseIP(f) != nil {
servers = append(servers, f)
}
}
continue
}
if inDNS {
// Continuation lines are indented; stop at a non-indented line.
if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
break
}
for _, f := range strings.Fields(stripped) {
if net.ParseIP(f) != nil {
servers = append(servers, f)
}
}
}
}
return servers, nil
}
// allStubs reports whether every address in servers is a known stub resolver.
func allStubs(servers []string) bool {
for _, s := range servers {
if !stubAddrs[s] {
return false
}
}
return true
}
func parseInt(s string) (int, error) {
n := 0
for _, c := range s {
if c < '0' || c > '9' {
return 0, fmt.Errorf("not an int: %q", s)
}
n = n*10 + int(c-'0')
}
return n, nil
}