mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-02 13:03:56 +01:00
c815ad86fd
previously, we called the restore method from the startup when there was an unclean shutdown. But it never had the state keys to clean since they are stored in memory this change addresses the issue by falling back to default values when restoring the host's DNS
367 lines
10 KiB
Go
367 lines
10 KiB
Go
//go:build !ios
|
|
|
|
package dns
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
|
|
globalIPv4State = "State:/Network/Global/IPv4"
|
|
primaryServiceStateKeyFormat = "State:/Network/Service/%s/DNS"
|
|
keySupplementalMatchDomains = "SupplementalMatchDomains"
|
|
keySupplementalMatchDomainsNoSearch = "SupplementalMatchDomainsNoSearch"
|
|
keyServerAddresses = "ServerAddresses"
|
|
keyServerPort = "ServerPort"
|
|
arraySymbol = "* "
|
|
digitSymbol = "# "
|
|
scutilPath = "/usr/sbin/scutil"
|
|
searchSuffix = "Search"
|
|
matchSuffix = "Match"
|
|
localSuffix = "Local"
|
|
)
|
|
|
|
type systemConfigurator struct {
|
|
createdKeys map[string]struct{}
|
|
systemDNSSettings SystemDNSSettings
|
|
}
|
|
|
|
func newHostManager() (hostManager, error) {
|
|
return &systemConfigurator{
|
|
createdKeys: make(map[string]struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func (s *systemConfigurator) supportCustomPort() bool {
|
|
return true
|
|
}
|
|
|
|
func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
|
var err error
|
|
|
|
// create a file for unclean shutdown detection
|
|
if err := createUncleanShutdownIndicator(); err != nil {
|
|
log.Errorf("failed to create unclean shutdown file: %s", err)
|
|
}
|
|
|
|
var (
|
|
searchDomains []string
|
|
matchDomains []string
|
|
)
|
|
|
|
err = s.recordSystemDNSSettings(true)
|
|
if err != nil {
|
|
log.Errorf("unable to update record of System's DNS config: %s", err.Error())
|
|
}
|
|
|
|
if config.RouteAll {
|
|
searchDomains = append(searchDomains, "\"\"")
|
|
err = s.addLocalDNS()
|
|
if err != nil {
|
|
log.Infof("failed to enable split DNS")
|
|
}
|
|
}
|
|
|
|
for _, dConf := range config.Domains {
|
|
if dConf.Disabled {
|
|
continue
|
|
}
|
|
if dConf.MatchOnly {
|
|
matchDomains = append(matchDomains, dConf.Domain)
|
|
continue
|
|
}
|
|
searchDomains = append(searchDomains, dConf.Domain)
|
|
}
|
|
|
|
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
|
if len(matchDomains) != 0 {
|
|
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.ServerIP, config.ServerPort)
|
|
} else {
|
|
log.Infof("removing match domains from the system")
|
|
err = s.removeKeyFromSystemConfig(matchKey)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("add match domains: %w", err)
|
|
}
|
|
|
|
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
|
if len(searchDomains) != 0 {
|
|
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.ServerIP, config.ServerPort)
|
|
} else {
|
|
log.Infof("removing search domains from the system")
|
|
err = s.removeKeyFromSystemConfig(searchKey)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("add search domains: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) restoreHostDNS() error {
|
|
keys := s.getRemovableKeysWithDefaults()
|
|
for _, key := range keys {
|
|
keyType := "search"
|
|
if strings.Contains(key, matchSuffix) {
|
|
keyType = "match"
|
|
}
|
|
log.Infof("removing %s domains from system", keyType)
|
|
err := s.removeKeyFromSystemConfig(key)
|
|
if err != nil {
|
|
log.Errorf("failed to remove %s domains from system: %s", keyType, err)
|
|
}
|
|
}
|
|
|
|
if err := removeUncleanShutdownIndicator(); err != nil {
|
|
log.Errorf("failed to remove unclean shutdown file: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) getRemovableKeysWithDefaults() []string {
|
|
if len(s.createdKeys) == 0 {
|
|
// return defaults for startup calls
|
|
return []string{getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix), getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)}
|
|
}
|
|
|
|
keys := make([]string, 0, len(s.createdKeys))
|
|
for key := range s.createdKeys {
|
|
keys = append(keys, key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
|
|
line := buildRemoveKeyOperation(key)
|
|
_, err := runSystemConfigCommand(wrapCommand(line))
|
|
if err != nil {
|
|
return fmt.Errorf("remove key: %w", err)
|
|
}
|
|
|
|
delete(s.createdKeys, key)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) addLocalDNS() error {
|
|
if s.systemDNSSettings.ServerIP == "" || len(s.systemDNSSettings.Domains) == 0 {
|
|
err := s.recordSystemDNSSettings(true)
|
|
log.Errorf("Unable to get system DNS configuration")
|
|
return err
|
|
}
|
|
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
|
|
if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 {
|
|
err := s.addSearchDomains(localKey, strings.Join(s.systemDNSSettings.Domains, " "), s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't add local network DNS conf: %w", err)
|
|
}
|
|
} else {
|
|
log.Info("Not enabling local DNS server")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) recordSystemDNSSettings(force bool) error {
|
|
if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 && !force {
|
|
return nil
|
|
}
|
|
|
|
systemDNSSettings, err := s.getSystemDNSSettings()
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get current DNS config: %w", err)
|
|
}
|
|
s.systemDNSSettings = systemDNSSettings
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|
primaryServiceKey, _, err := s.getPrimaryService()
|
|
if err != nil || primaryServiceKey == "" {
|
|
return SystemDNSSettings{}, fmt.Errorf("couldn't find the primary service key: %w", err)
|
|
}
|
|
dnsServiceKey := getKeyWithInput(primaryServiceStateKeyFormat, primaryServiceKey)
|
|
line := buildCommandLine("show", dnsServiceKey, "")
|
|
stdinCommands := wrapCommand(line)
|
|
|
|
b, err := runSystemConfigCommand(stdinCommands)
|
|
if err != nil {
|
|
return SystemDNSSettings{}, fmt.Errorf("sending the command: %w", err)
|
|
}
|
|
|
|
var dnsSettings SystemDNSSettings
|
|
inSearchDomainsArray := false
|
|
inServerAddressesArray := false
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
switch {
|
|
case strings.HasPrefix(line, "DomainName :"):
|
|
domainName := strings.TrimSpace(strings.Split(line, ":")[1])
|
|
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
|
|
case line == "SearchDomains : <array> {":
|
|
inSearchDomainsArray = true
|
|
continue
|
|
case line == "ServerAddresses : <array> {":
|
|
inServerAddressesArray = true
|
|
continue
|
|
case line == "}":
|
|
inSearchDomainsArray = false
|
|
inServerAddressesArray = false
|
|
}
|
|
|
|
if inSearchDomainsArray {
|
|
searchDomains := strings.Split(line, " : ")
|
|
dnsSettings.Domains = append(dnsSettings.Domains, searchDomains...)
|
|
} else if inServerAddressesArray {
|
|
address := strings.Split(line, " : ")[1]
|
|
if ip := net.ParseIP(address); ip != nil && ip.To4() != nil {
|
|
dnsSettings.ServerIP = address
|
|
inServerAddressesArray = false // Stop reading after finding the first IPv4 address
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return dnsSettings, err
|
|
}
|
|
|
|
// default to 53 port
|
|
dnsSettings.ServerPort = 53
|
|
|
|
return dnsSettings, nil
|
|
}
|
|
|
|
func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error {
|
|
err := s.addDNSState(key, domains, ip, port, true)
|
|
if err != nil {
|
|
return fmt.Errorf("add dns state: %w", err)
|
|
}
|
|
|
|
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
|
|
|
s.createdKeys[key] = struct{}{}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
|
|
err := s.addDNSState(key, domains, dnsServer, port, false)
|
|
if err != nil {
|
|
return fmt.Errorf("add dns state: %w", err)
|
|
}
|
|
|
|
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
|
|
|
|
s.createdKeys[key] = struct{}{}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error {
|
|
noSearch := "1"
|
|
if enableSearch {
|
|
noSearch = "0"
|
|
}
|
|
lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains)
|
|
lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch)
|
|
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
|
|
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
|
|
|
addDomainCommand := buildCreateStateWithOperation(state, lines)
|
|
stdinCommands := wrapCommand(addDomainCommand)
|
|
|
|
_, err := runSystemConfigCommand(stdinCommands)
|
|
if err != nil {
|
|
return fmt.Errorf("applying state for domains %s, error: %w", domains, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *systemConfigurator) getPrimaryService() (string, string, error) {
|
|
line := buildCommandLine("show", globalIPv4State, "")
|
|
stdinCommands := wrapCommand(line)
|
|
|
|
b, err := runSystemConfigCommand(stdinCommands)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("sending the command: %w", err)
|
|
}
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
|
primaryService := ""
|
|
router := ""
|
|
for scanner.Scan() {
|
|
text := scanner.Text()
|
|
if strings.Contains(text, "PrimaryService") {
|
|
primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
|
|
}
|
|
if strings.Contains(text, "Router") {
|
|
router = strings.TrimSpace(strings.Split(text, ":")[1])
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil && err != io.EOF {
|
|
return primaryService, router, fmt.Errorf("scan: %w", err)
|
|
}
|
|
|
|
return primaryService, router, nil
|
|
}
|
|
|
|
func (s *systemConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
|
|
if err := s.restoreHostDNS(); err != nil {
|
|
return fmt.Errorf("restoring dns via scutil: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getKeyWithInput(format, key string) string {
|
|
return fmt.Sprintf(format, key)
|
|
}
|
|
|
|
func buildAddCommandLine(key, value string) string {
|
|
return buildCommandLine("d.add", key, value)
|
|
}
|
|
|
|
func buildCommandLine(action, key, value string) string {
|
|
return fmt.Sprintf("%s %s %s\n", action, key, value)
|
|
}
|
|
|
|
func wrapCommand(commands string) string {
|
|
return fmt.Sprintf("open\n%s\nquit\n", commands)
|
|
}
|
|
|
|
func buildRemoveKeyOperation(key string) string {
|
|
return fmt.Sprintf("remove %s\n", key)
|
|
}
|
|
|
|
func buildCreateStateWithOperation(state, commands string) string {
|
|
return buildWriteStateOperation("set", state, commands)
|
|
}
|
|
|
|
func buildWriteStateOperation(operation, state, commands string) string {
|
|
return fmt.Sprintf("d.init\n%s %s\n%s\nset %s\n", operation, state, commands, state)
|
|
}
|
|
|
|
func runSystemConfigCommand(command string) ([]byte, error) {
|
|
cmd := exec.Command(scutilPath)
|
|
cmd.Stdin = strings.NewReader(command)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("running system configuration command: \"%s\", error: %w", command, err)
|
|
}
|
|
return out, nil
|
|
}
|