201 lines
4.9 KiB
Go
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
|
|
}
|