From ae350ec3b7bc5ee3ea746b1de5cfd72b6b8810c8 Mon Sep 17 00:00:00 2001 From: chbert Date: Wed, 23 Sep 2026 22:50:08 +0200 Subject: [PATCH] fix: don't panic when the default interface has no DNS server GetDNSServer sliced the resolvectl output with [:len-1], which panics (slice bounds out of range [:-1]) when the default interface reports no DNS server, as on Depot runners. Parse resolvectl output instead, fall back to the global resolvectl status and resolv.conf, and allow only the default DNS servers when no upstream server is found. --- pkg/agent/agent.go | 5 +- pkg/agent/network_info.go | 99 +++++++++++++++++++++++++++++----- pkg/agent/network_info_test.go | 33 ++++++++++++ 3 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 pkg/agent/network_info_test.go diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 4c19717..d57167d 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -353,7 +353,10 @@ func (a *Agent) loadAllowedDNSServers() error { if err != nil { return err } - mergedDNSServers := append(defaultDNSServers, dnsServer) + mergedDNSServers := append([]string{}, defaultDNSServers...) + if dnsServer != "" { + mergedDNSServers = append(mergedDNSServers, dnsServer) + } for _, dns := range mergedDNSServers { a.allowedDNSServers[dns] = true diff --git a/pkg/agent/network_info.go b/pkg/agent/network_info.go index efcbbae..1078ccf 100644 --- a/pkg/agent/network_info.go +++ b/pkg/agent/network_info.go @@ -2,7 +2,9 @@ package agent import ( "fmt" + "os" "os/exec" + "strings" ) type INetInfoProvider interface { @@ -13,27 +15,98 @@ type INetInfoProvider interface { type LinuxNetInfoProvider struct { } +// resolvConfPaths are read, in order, when resolvectl reports no DNS server. +// /run/systemd/resolve/resolv.conf lists systemd-resolved's upstream servers; +// /etc/resolv.conf covers hosts where resolv.conf points at a resolver directly. +var resolvConfPaths = []string{"/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"} + +// GetDNSServer returns the upstream DNS server the host resolves through, or "" +// when none can be determined. It never fails on empty command output: some +// runners (e.g. Depot) report no per-link DNS server for the default interface. func (l *LinuxNetInfoProvider) GetDNSServer() (string, error) { - networkInterface, err := exec.Command("sh", "-c", "ip route | grep default | awk '{print $5}'").Output() + networkInterface := "" + out, err := exec.Command("sh", "-c", "ip route | grep default | awk '{print $5}'").Output() if err != nil { fmt.Printf("Error getting default network interface: %s\n", err) - return "", err + } else { + networkInterface = firstLine(string(out)) } - // remove new line of networkInterface - networkInterface = networkInterface[:len(networkInterface)-1] fmt.Printf("Network interface: %s\n", networkInterface) - cmd := fmt.Sprintf("resolvectl status %s | grep 'DNS Servers' | awk '{print $3}'", networkInterface) + if networkInterface != "" { + if out, err := exec.Command("resolvectl", "status", networkInterface).CombinedOutput(); err == nil { + if dnsServer := parseResolvectlDNSServer(string(out)); dnsServer != "" { + fmt.Printf("DNS server: %s\n", dnsServer) + return dnsServer, nil + } + } + } - dnsServer, err := exec.Command("sh", "-c", cmd).CombinedOutput() - if err != nil { - fmt.Println("Error getting DNS server: ", err) - return "", err + if out, err := exec.Command("resolvectl", "status").CombinedOutput(); err == nil { + if dnsServer := parseResolvectlDNSServer(string(out)); dnsServer != "" { + fmt.Printf("DNS server (global): %s\n", dnsServer) + return dnsServer, nil + } + } + + for _, path := range resolvConfPaths { + content, err := os.ReadFile(path) + if err != nil { + continue + } + if dnsServer := parseResolvConfNameserver(string(content)); dnsServer != "" { + fmt.Printf("DNS server (%s): %s\n", path, dnsServer) + return dnsServer, nil + } + } + + fmt.Println("No upstream DNS server found; allowing only the default DNS servers") + return "", nil +} + +// parseResolvectlDNSServer extracts the first DNS server from `resolvectl status` +// output, preferring "Current DNS Server" over the "DNS Servers" list. +func parseResolvectlDNSServer(output string) string { + for _, key := range []string{"Current DNS Server:", "DNS Servers:"} { + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, key) { + continue + } + if fields := strings.Fields(strings.TrimPrefix(line, key)); len(fields) > 0 { + return fields[0] + } + } } - // remove new line of dnsServer - dnsServer = dnsServer[:len(dnsServer)-1] - fmt.Printf("DNS server: %s\n", dnsServer) - return string(dnsServer), nil + return "" +} + +// parseResolvConfNameserver returns the first nameserver in resolv.conf content +// that is not already one of the default DNS servers (the systemd-resolved stub). +func parseResolvConfNameserver(content string) string { + for _, line := range strings.Split(content, "\n") { + fields := strings.Fields(line) + if len(fields) < 2 || fields[0] != "nameserver" { + continue + } + if !isDefaultDNSServer(fields[1]) { + return fields[1] + } + } + return "" +} + +func isDefaultDNSServer(server string) bool { + for _, d := range defaultDNSServers { + if d == server { + return true + } + } + return false +} + +func firstLine(s string) string { + return strings.TrimSpace(strings.SplitN(s, "\n", 2)[0]) } func (l *LinuxNetInfoProvider) FlushDNSCache() error { diff --git a/pkg/agent/network_info_test.go b/pkg/agent/network_info_test.go new file mode 100644 index 0000000..792f989 --- /dev/null +++ b/pkg/agent/network_info_test.go @@ -0,0 +1,33 @@ +package agent + +import "testing" + +func TestParseResolvectlDNSServer(t *testing.T) { + cases := map[string]struct{ in, want string }{ + "per-link list": {"Link 2 (eth0)\n Current Scopes: DNS\n DNS Servers: 168.63.129.16 10.0.0.2\n", "168.63.129.16"}, + "current wins": {"Global\n Current DNS Server: 10.0.0.2\n DNS Servers: 8.8.8.8 10.0.0.2\n", "10.0.0.2"}, + // Depot: the default interface has no DNS server at all (previously a panic). + "no server": {"Link 2 (enp0s6)\n Current Scopes: none\n", ""}, + "empty output": {"", ""}, + "empty value": {" DNS Servers:\n", ""}, + } + for name, c := range cases { + if got := parseResolvectlDNSServer(c.in); got != c.want { + t.Errorf("%s: got %q, want %q", name, got, c.want) + } + } +} + +func TestParseResolvConfNameserver(t *testing.T) { + cases := map[string]struct{ in, want string }{ + "stub skipped": {"# managed\nnameserver 127.0.0.53\nnameserver 10.1.2.3\nsearch x\n", "10.1.2.3"}, + "stub only": {"nameserver 127.0.0.53\noptions edns0\n", ""}, + "direct": {"nameserver 169.254.169.253\n", "169.254.169.253"}, + "empty": {"", ""}, + } + for name, c := range cases { + if got := parseResolvConfNameserver(c.in); got != c.want { + t.Errorf("%s: got %q, want %q", name, got, c.want) + } + } +}