//go:build (linux && !android) || freebsd package dns import ( "bytes" "fmt" "net/netip" "os/exec" "strings" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/statemanager" ) const resolvconfCommand = "resolvconf" // resolvconfType represents the type of resolvconf implementation type resolvconfType int func (r resolvconfType) String() string { switch r { case typeOpenresolv: return "openresolv" case typeResolvconf: return "resolvconf" default: return "unknown" } } const ( typeOpenresolv resolvconfType = iota typeResolvconf ) type resolvconf struct { ifaceName string implType resolvconfType originalSearchDomains []string originalNameServers []string othersConfigs []string } func detectResolvconfType() (resolvconfType, error) { cmd := exec.Command(resolvconfCommand, "--version") out, err := cmd.Output() if err != nil { return typeOpenresolv, fmt.Errorf("failed to determine resolvconf type: %w", err) } if strings.Contains(string(out), "openresolv") { return typeOpenresolv, nil } return typeResolvconf, nil } func newResolvConfConfigurator(wgInterface string) (*resolvconf, error) { resolvConfEntries, err := parseDefaultResolvConf() if err != nil { log.Errorf("could not read original search domains from %s: %s", defaultResolvConfPath, err) } implType, err := detectResolvconfType() if err != nil { log.Warnf("failed to detect resolvconf type, defaulting to openresolv: %v", err) implType = typeOpenresolv } else { log.Infof("detected resolvconf type: %v", implType) } return &resolvconf{ ifaceName: wgInterface, implType: implType, originalSearchDomains: resolvConfEntries.searchDomains, originalNameServers: resolvConfEntries.nameServers, othersConfigs: resolvConfEntries.others, }, nil } func (r *resolvconf) supportCustomPort() bool { return false } func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { var err error if !config.RouteAll { err = r.restoreHostDNS() if err != nil { log.Errorf("restore host dns: %s", err) } return ErrRouteAllWithoutNameserverGroup } searchDomainList := searchDomains(config) searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains) options := prepareOptionsWithTimeout(r.othersConfigs, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts) buf := prepareResolvConfContent( searchDomainList, append([]string{config.ServerIP}, r.originalNameServers...), options) state := &ShutdownState{ ManagerType: resolvConfManager, WgIface: r.ifaceName, } if err := stateManager.UpdateState(state); err != nil { log.Errorf("failed to update shutdown state: %s", err) } err = r.applyConfig(buf) if err != nil { return fmt.Errorf("apply config: %w", err) } log.Infof("added %d search domains. Search list: %s", len(searchDomainList), searchDomainList) return nil } func (r *resolvconf) restoreHostDNS() error { var cmd *exec.Cmd switch r.implType { case typeOpenresolv: cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) case typeResolvconf: cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName) } _, err := cmd.Output() if err != nil { return fmt.Errorf("removing resolvconf configuration for %s interface: %w", r.ifaceName, err) } return nil } func (r *resolvconf) string() string { return fmt.Sprintf("resolvconf (%s)", r.implType) } func (r *resolvconf) applyConfig(content bytes.Buffer) error { var cmd *exec.Cmd switch r.implType { case typeOpenresolv: // OpenResolv supports exclusive mode with -x cmd = exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) case typeResolvconf: cmd = exec.Command(resolvconfCommand, "-a", r.ifaceName) default: return fmt.Errorf("unsupported resolvconf type: %v", r.implType) } cmd.Stdin = &content out, err := cmd.Output() log.Tracef("resolvconf output: %s", out) if err != nil { return fmt.Errorf("applying resolvconf configuration for %s interface: %w", r.ifaceName, err) } return nil } func (r *resolvconf) restoreUncleanShutdownDNS(*netip.Addr) error { if err := r.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns for interface %s: %w", r.ifaceName, err) } return nil }