mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-24 11:16:38 +00:00
214 lines
5.8 KiB
Go
214 lines
5.8 KiB
Go
//go:build (linux && !android) || freebsd
|
|
|
|
package dns
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
netbirdManager osManagerType = iota
|
|
fileManager
|
|
networkManager
|
|
systemdManager
|
|
resolvConfManager
|
|
)
|
|
|
|
type osManagerType int
|
|
|
|
func (t osManagerType) String() string {
|
|
switch t {
|
|
case netbirdManager:
|
|
return "netbird"
|
|
case fileManager:
|
|
return "file"
|
|
case networkManager:
|
|
return "networkManager"
|
|
case systemdManager:
|
|
return "systemd"
|
|
case resolvConfManager:
|
|
return "resolvconf"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
type restoreHostManager interface {
|
|
hostManager
|
|
restoreUncleanShutdownDNS(netip.Addr) error
|
|
}
|
|
|
|
func newHostManager(wgInterface string) (hostManager, error) {
|
|
osManager, reason, err := getOSDNSManagerType()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get os dns manager type: %w", err)
|
|
}
|
|
|
|
log.Infof("System DNS manager discovered: %s (%s)", osManager, reason)
|
|
mgr, err := newHostManagerFromType(wgInterface, osManager)
|
|
// need to explicitly return nil mgr on error to avoid returning a non-nil interface containing a nil value
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create host manager: %w", err)
|
|
}
|
|
|
|
return mgr, nil
|
|
}
|
|
|
|
func newHostManagerFromType(wgInterface string, osManager osManagerType) (restoreHostManager, error) {
|
|
switch osManager {
|
|
case networkManager:
|
|
return newNetworkManagerDbusConfigurator(wgInterface)
|
|
case systemdManager:
|
|
return newSystemdDbusConfigurator(wgInterface)
|
|
case resolvConfManager:
|
|
return newResolvConfConfigurator(wgInterface)
|
|
default:
|
|
return newFileConfigurator()
|
|
}
|
|
}
|
|
|
|
func getOSDNSManagerType() (osManagerType, string, error) {
|
|
resolved := isSystemdResolvedRunning()
|
|
nss := isLibnssResolveUsed()
|
|
stub := checkStub()
|
|
|
|
// Prefer systemd-resolved whenever it owns libc resolution, regardless of
|
|
// who wrote /etc/resolv.conf. File-mode rewrites do not affect lookups
|
|
// that go through nss-resolve, and in foreign mode they can loop back
|
|
// through resolved as an upstream.
|
|
if resolved && (nss || stub) {
|
|
return systemdManager, fmt.Sprintf("systemd-resolved active (nss-resolve=%t, stub=%t)", nss, stub), nil
|
|
}
|
|
|
|
mgr, reason, rejected, err := scanResolvConfHeader()
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
if reason != "" {
|
|
return mgr, reason, nil
|
|
}
|
|
|
|
fallback := fmt.Sprintf("no manager matched (resolved=%t, nss-resolve=%t, stub=%t)", resolved, nss, stub)
|
|
if len(rejected) > 0 {
|
|
fallback += "; rejected: " + strings.Join(rejected, ", ")
|
|
}
|
|
return fileManager, fallback, nil
|
|
}
|
|
|
|
// scanResolvConfHeader walks /etc/resolv.conf header comments and returns the
|
|
// matching manager. If reason is empty the caller should pick file mode and
|
|
// use rejected for diagnostics.
|
|
func scanResolvConfHeader() (osManagerType, string, []string, error) {
|
|
file, err := os.Open(defaultResolvConfPath)
|
|
if err != nil {
|
|
return 0, "", nil, fmt.Errorf("unable to open %s for checking owner, got error: %w", defaultResolvConfPath, err)
|
|
}
|
|
defer func() {
|
|
if cerr := file.Close(); cerr != nil {
|
|
log.Errorf("close file %s: %s", defaultResolvConfPath, cerr)
|
|
}
|
|
}()
|
|
|
|
var rejected []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
text := scanner.Text()
|
|
if len(text) == 0 {
|
|
continue
|
|
}
|
|
if text[0] != '#' {
|
|
break
|
|
}
|
|
if mgr, reason, rej := matchResolvConfHeader(text); reason != "" {
|
|
return mgr, reason, nil, nil
|
|
} else if rej != "" {
|
|
rejected = append(rejected, rej)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil && err != io.EOF {
|
|
return 0, "", nil, fmt.Errorf("scan: %w", err)
|
|
}
|
|
return 0, "", rejected, nil
|
|
}
|
|
|
|
// matchResolvConfHeader inspects a single comment line. Returns either a
|
|
// definitive (manager, reason) or a non-empty rejected diagnostic.
|
|
func matchResolvConfHeader(text string) (osManagerType, string, string) {
|
|
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
|
|
return netbirdManager, "netbird-managed resolv.conf header detected", ""
|
|
}
|
|
if strings.Contains(text, "NetworkManager") {
|
|
if isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
|
return networkManager, "NetworkManager header + supported version on dbus", ""
|
|
}
|
|
return 0, "", "NetworkManager header (no dbus or unsupported version)"
|
|
}
|
|
if strings.Contains(text, "resolvconf") {
|
|
if isSystemdResolveConfMode() {
|
|
return systemdManager, "resolvconf header in systemd-resolved compatibility mode", ""
|
|
}
|
|
return resolvConfManager, "resolvconf header detected", ""
|
|
}
|
|
return 0, "", ""
|
|
}
|
|
|
|
// checkStub reports whether systemd-resolved's stub (127.0.0.53) is listed
|
|
// in /etc/resolv.conf. On parse failure we assume it is, to avoid dropping
|
|
// into file mode while resolved is active.
|
|
func checkStub() bool {
|
|
rConf, err := parseDefaultResolvConf()
|
|
if err != nil {
|
|
log.Warnf("failed to parse resolv conf, assuming stub is active: %s", err)
|
|
return true
|
|
}
|
|
|
|
systemdResolvedAddr := netip.AddrFrom4([4]byte{127, 0, 0, 53}) // 127.0.0.53
|
|
for _, ns := range rConf.nameServers {
|
|
if ns == systemdResolvedAddr {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isLibnssResolveUsed reports whether nss-resolve is listed before dns on
|
|
// the hosts: line of /etc/nsswitch.conf. When it is, libc lookups are
|
|
// delegated to systemd-resolved regardless of /etc/resolv.conf.
|
|
func isLibnssResolveUsed() bool {
|
|
bs, err := os.ReadFile(nsswitchConfPath)
|
|
if err != nil {
|
|
log.Debugf("read %s: %v", nsswitchConfPath, err)
|
|
return false
|
|
}
|
|
return parseNsswitchResolveAhead(bs)
|
|
}
|
|
|
|
func parseNsswitchResolveAhead(data []byte) bool {
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if i := strings.IndexByte(line, '#'); i >= 0 {
|
|
line = line[:i]
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 || fields[0] != "hosts:" {
|
|
continue
|
|
}
|
|
for _, module := range fields[1:] {
|
|
switch module {
|
|
case "dns":
|
|
return false
|
|
case "resolve":
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|