Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 86 additions & 13 deletions pkg/agent/network_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package agent

import (
"fmt"
"os"
"os/exec"
"strings"
)

type INetInfoProvider interface {
Expand All @@ -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 {
Expand Down
33 changes: 33 additions & 0 deletions pkg/agent/network_info_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}