mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-07 15:31:30 +02:00
Merge branch 'main' into fix/ice-handshake
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -70,7 +71,7 @@ func (c *ConnectClient) RunOnAndroid(
|
||||
tunAdapter device.TunAdapter,
|
||||
iFaceDiscover stdnet.ExternalIFaceDiscover,
|
||||
networkChangeListener listener.NetworkChangeListener,
|
||||
dnsAddresses []string,
|
||||
dnsAddresses []netip.AddrPort,
|
||||
dnsReadyListener dns.ReadyListener,
|
||||
) error {
|
||||
// in case of non Android os these variables will be nil
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
)
|
||||
|
||||
type resolvConf struct {
|
||||
nameServers []string
|
||||
nameServers []netip.Addr
|
||||
searchDomains []string
|
||||
others []string
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func parseBackupResolvConf() (*resolvConf, error) {
|
||||
func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
|
||||
rconf := &resolvConf{
|
||||
searchDomains: make([]string, 0),
|
||||
nameServers: make([]string, 0),
|
||||
nameServers: make([]netip.Addr, 0),
|
||||
others: make([]string, 0),
|
||||
}
|
||||
|
||||
@@ -94,7 +94,11 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
|
||||
if len(splitLines) != 2 {
|
||||
continue
|
||||
}
|
||||
rconf.nameServers = append(rconf.nameServers, splitLines[1])
|
||||
if addr, err := netip.ParseAddr(splitLines[1]); err == nil {
|
||||
rconf.nameServers = append(rconf.nameServers, addr.Unmap())
|
||||
} else {
|
||||
log.Warnf("invalid nameserver address in resolv.conf: %s, skipping", splitLines[1])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -104,31 +108,3 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
|
||||
}
|
||||
return rconf, nil
|
||||
}
|
||||
|
||||
// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position
|
||||
// and writes the file back to the original location
|
||||
func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error {
|
||||
resolvConf, err := parseResolvConfFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse backup resolv.conf: %w", err)
|
||||
}
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", filename, err)
|
||||
}
|
||||
|
||||
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP.String() {
|
||||
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
|
||||
|
||||
stat, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat %s: %w", filename, err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(newContent), stat.Mode()); err != nil {
|
||||
return fmt.Errorf("write %s: %w", filename, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseResolvConf(t *testing.T) {
|
||||
@@ -99,9 +95,13 @@ options debug
|
||||
t.Errorf("invalid parse result for search domains, expected: %v, got: %v", testCase.expectedSearch, cfg.searchDomains)
|
||||
}
|
||||
|
||||
ok = compareLists(cfg.nameServers, testCase.expectedNS)
|
||||
nsStrings := make([]string, len(cfg.nameServers))
|
||||
for i, ns := range cfg.nameServers {
|
||||
nsStrings[i] = ns.String()
|
||||
}
|
||||
ok = compareLists(nsStrings, testCase.expectedNS)
|
||||
if !ok {
|
||||
t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, cfg.nameServers)
|
||||
t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, nsStrings)
|
||||
}
|
||||
|
||||
ok = compareLists(cfg.others, testCase.expectedOther)
|
||||
@@ -176,87 +176,3 @@ nameserver 192.168.0.1
|
||||
t.Errorf("unexpected resolv.conf content: %v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFirstNbNameserver(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
content string
|
||||
ipToRemove string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Unrelated nameservers with comments and options",
|
||||
content: `# This is a comment
|
||||
options rotate
|
||||
nameserver 1.1.1.1
|
||||
# Another comment
|
||||
nameserver 8.8.4.4
|
||||
search example.com`,
|
||||
ipToRemove: "9.9.9.9",
|
||||
expected: `# This is a comment
|
||||
options rotate
|
||||
nameserver 1.1.1.1
|
||||
# Another comment
|
||||
nameserver 8.8.4.4
|
||||
search example.com`,
|
||||
},
|
||||
{
|
||||
name: "First nameserver matches",
|
||||
content: `search example.com
|
||||
nameserver 9.9.9.9
|
||||
# oof, a comment
|
||||
nameserver 8.8.4.4
|
||||
options attempts:5`,
|
||||
ipToRemove: "9.9.9.9",
|
||||
expected: `search example.com
|
||||
# oof, a comment
|
||||
nameserver 8.8.4.4
|
||||
options attempts:5`,
|
||||
},
|
||||
{
|
||||
name: "Target IP not the first nameserver",
|
||||
// nolint:dupword
|
||||
content: `# Comment about the first nameserver
|
||||
nameserver 8.8.4.4
|
||||
# Comment before our target
|
||||
nameserver 9.9.9.9
|
||||
options timeout:2`,
|
||||
ipToRemove: "9.9.9.9",
|
||||
// nolint:dupword
|
||||
expected: `# Comment about the first nameserver
|
||||
nameserver 8.8.4.4
|
||||
# Comment before our target
|
||||
nameserver 9.9.9.9
|
||||
options timeout:2`,
|
||||
},
|
||||
{
|
||||
name: "Only nameserver matches",
|
||||
content: `options debug
|
||||
nameserver 9.9.9.9
|
||||
search localdomain`,
|
||||
ipToRemove: "9.9.9.9",
|
||||
expected: `options debug
|
||||
nameserver 9.9.9.9
|
||||
search localdomain`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
tempFile := filepath.Join(tempDir, "resolv.conf")
|
||||
err := os.WriteFile(tempFile, []byte(tc.content), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ip, err := netip.ParseAddr(tc.ipToRemove)
|
||||
require.NoError(t, err, "Failed to parse IP address")
|
||||
err = removeFirstNbNameserver(tempFile, ip)
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rCon
|
||||
return true
|
||||
}
|
||||
|
||||
if rConf.nameServers[0] != nbNameserverIP.String() {
|
||||
if rConf.nameServers[0] != nbNameserverIP {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ type fileConfigurator struct {
|
||||
repair *repair
|
||||
originalPerms os.FileMode
|
||||
nbNameserverIP netip.Addr
|
||||
originalNameservers []string
|
||||
originalNameservers []netip.Addr
|
||||
}
|
||||
|
||||
func newFileConfigurator() (*fileConfigurator, error) {
|
||||
@@ -70,7 +70,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st
|
||||
}
|
||||
|
||||
// getOriginalNameservers returns the nameservers that were found in the original resolv.conf
|
||||
func (f *fileConfigurator) getOriginalNameservers() []string {
|
||||
func (f *fileConfigurator) getOriginalNameservers() []netip.Addr {
|
||||
return f.originalNameservers
|
||||
}
|
||||
|
||||
@@ -128,20 +128,14 @@ func (f *fileConfigurator) backup() error {
|
||||
}
|
||||
|
||||
func (f *fileConfigurator) restore() error {
|
||||
err := removeFirstNbNameserver(fileDefaultResolvConfBackupLocation, f.nbNameserverIP)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to remove netbird nameserver from %s on backup restore: %s", fileDefaultResolvConfBackupLocation, err)
|
||||
}
|
||||
|
||||
err = copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
|
||||
if err != nil {
|
||||
if err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath); err != nil {
|
||||
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
|
||||
}
|
||||
|
||||
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
|
||||
}
|
||||
|
||||
func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error {
|
||||
func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress netip.Addr) error {
|
||||
resolvConf, err := parseDefaultResolvConf()
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse current resolv.conf: %w", err)
|
||||
@@ -152,16 +146,9 @@ func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Add
|
||||
return restoreResolvConfFile()
|
||||
}
|
||||
|
||||
currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0])
|
||||
// not a valid first nameserver -> restore
|
||||
if err != nil {
|
||||
log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[0], err)
|
||||
return restoreResolvConfFile()
|
||||
}
|
||||
|
||||
// current address is still netbird's non-available dns address -> restore
|
||||
// comparing parsed addresses only, to remove ambiguity
|
||||
if currentDNSAddress.String() == storedDNSAddress.String() {
|
||||
currentDNSAddress := resolvConf.nameServers[0]
|
||||
if currentDNSAddress == storedDNSAddress {
|
||||
return restoreResolvConfFile()
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
||||
} else if inServerAddressesArray {
|
||||
address := strings.Split(line, " : ")[1]
|
||||
if ip, err := netip.ParseAddr(address); err == nil && ip.Is4() {
|
||||
dnsSettings.ServerIP = ip
|
||||
dnsSettings.ServerIP = ip.Unmap()
|
||||
inServerAddressesArray = false // Stop reading after finding the first IPv4 address
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
||||
}
|
||||
|
||||
// default to 53 port
|
||||
dnsSettings.ServerPort = defaultPort
|
||||
dnsSettings.ServerPort = DefaultPort
|
||||
|
||||
return dnsSettings, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (t osManagerType) String() string {
|
||||
|
||||
type restoreHostManager interface {
|
||||
hostManager
|
||||
restoreUncleanShutdownDNS(*netip.Addr) error
|
||||
restoreUncleanShutdownDNS(netip.Addr) error
|
||||
}
|
||||
|
||||
func newHostManager(wgInterface string) (hostManager, error) {
|
||||
@@ -130,8 +130,9 @@ func checkStub() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
systemdResolvedAddr := netip.AddrFrom4([4]byte{127, 0, 0, 53}) // 127.0.0.53
|
||||
for _, ns := range rConf.nameServers {
|
||||
if ns == "127.0.0.53" {
|
||||
if ns == systemdResolvedAddr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,10 @@ const (
|
||||
)
|
||||
|
||||
type registryConfigurator struct {
|
||||
guid string
|
||||
routingAll bool
|
||||
gpo bool
|
||||
guid string
|
||||
routingAll bool
|
||||
gpo bool
|
||||
nrptEntryCount int
|
||||
}
|
||||
|
||||
func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
||||
@@ -177,7 +178,11 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
||||
log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP)
|
||||
}
|
||||
|
||||
if err := stateManager.UpdateState(&ShutdownState{Guid: r.guid, GPO: r.gpo}); err != nil {
|
||||
if err := stateManager.UpdateState(&ShutdownState{
|
||||
Guid: r.guid,
|
||||
GPO: r.gpo,
|
||||
NRPTEntryCount: r.nrptEntryCount,
|
||||
}); err != nil {
|
||||
log.Errorf("failed to update shutdown state: %s", err)
|
||||
}
|
||||
|
||||
@@ -193,13 +198,24 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
||||
}
|
||||
|
||||
if len(matchDomains) != 0 {
|
||||
if err := r.addDNSMatchPolicy(matchDomains, config.ServerIP); err != nil {
|
||||
count, err := r.addDNSMatchPolicy(matchDomains, config.ServerIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add dns match policy: %w", err)
|
||||
}
|
||||
r.nrptEntryCount = count
|
||||
} else {
|
||||
if err := r.removeDNSMatchPolicies(); err != nil {
|
||||
return fmt.Errorf("remove dns match policies: %w", err)
|
||||
}
|
||||
r.nrptEntryCount = 0
|
||||
}
|
||||
|
||||
if err := stateManager.UpdateState(&ShutdownState{
|
||||
Guid: r.guid,
|
||||
GPO: r.gpo,
|
||||
NRPTEntryCount: r.nrptEntryCount,
|
||||
}); err != nil {
|
||||
log.Errorf("failed to update shutdown state: %s", err)
|
||||
}
|
||||
|
||||
if err := r.updateSearchDomains(searchDomains); err != nil {
|
||||
@@ -216,32 +232,38 @@ func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error {
|
||||
return fmt.Errorf("adding dns setup for all failed: %w", err)
|
||||
}
|
||||
r.routingAll = true
|
||||
log.Infof("configured %s:53 as main DNS forwarder for this peer", ip)
|
||||
log.Infof("configured %s:%d as main DNS forwarder for this peer", ip, DefaultPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) error {
|
||||
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) (int, error) {
|
||||
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
|
||||
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
|
||||
if r.gpo {
|
||||
if err := r.configureDNSPolicy(gpoDnsPolicyConfigMatchPath, domains, ip); err != nil {
|
||||
return fmt.Errorf("configure GPO DNS policy: %w", err)
|
||||
for i, domain := range domains {
|
||||
policyPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i)
|
||||
if r.gpo {
|
||||
policyPath = fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, i)
|
||||
}
|
||||
|
||||
singleDomain := []string{domain}
|
||||
|
||||
if err := r.configureDNSPolicy(policyPath, singleDomain, ip); err != nil {
|
||||
return i, fmt.Errorf("configure DNS policy for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
log.Debugf("added NRPT entry for domain: %s", domain)
|
||||
}
|
||||
|
||||
if r.gpo {
|
||||
if err := refreshGroupPolicy(); err != nil {
|
||||
log.Warnf("failed to refresh group policy: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil {
|
||||
return fmt.Errorf("configure local DNS policy: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("added %d match domains. Domain list: %s", len(domains), domains)
|
||||
return nil
|
||||
log.Infof("added %d separate NRPT entries. Domain list: %s", len(domains), domains)
|
||||
return len(domains), nil
|
||||
}
|
||||
|
||||
// configureDNSPolicy handles the actual configuration of a DNS policy at the specified path
|
||||
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil {
|
||||
return fmt.Errorf("remove existing dns policy: %w", err)
|
||||
@@ -374,12 +396,25 @@ func (r *registryConfigurator) restoreHostDNS() error {
|
||||
|
||||
func (r *registryConfigurator) removeDNSMatchPolicies() error {
|
||||
var merr *multierror.Error
|
||||
|
||||
// Try to remove the base entries (for backward compatibility)
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove local registry key: %w", err))
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove local base entry: %w", err))
|
||||
}
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(gpoDnsPolicyConfigMatchPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove GPO base entry: %w", err))
|
||||
}
|
||||
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(gpoDnsPolicyConfigMatchPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove GPO registry key: %w", err))
|
||||
for i := 0; i < r.nrptEntryCount; i++ {
|
||||
localPath := fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i)
|
||||
gpoPath := fmt.Sprintf("%s-%d", gpoDnsPolicyConfigMatchPath, i)
|
||||
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(localPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove local entry %d: %w", i, err))
|
||||
}
|
||||
if err := removeRegistryKeyFromDNSPolicyConfig(gpoPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove GPO entry %d: %w", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := refreshGroupPolicy(); err != nil {
|
||||
|
||||
@@ -1,38 +1,31 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type hostsDNSHolder struct {
|
||||
unprotectedDNSList map[string]struct{}
|
||||
unprotectedDNSList map[netip.AddrPort]struct{}
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func newHostsDNSHolder() *hostsDNSHolder {
|
||||
return &hostsDNSHolder{
|
||||
unprotectedDNSList: make(map[string]struct{}),
|
||||
unprotectedDNSList: make(map[netip.AddrPort]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hostsDNSHolder) set(list []string) {
|
||||
func (h *hostsDNSHolder) set(list []netip.AddrPort) {
|
||||
h.mutex.Lock()
|
||||
h.unprotectedDNSList = make(map[string]struct{})
|
||||
for _, dns := range list {
|
||||
dnsAddr, err := h.normalizeAddress(dns)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h.unprotectedDNSList[dnsAddr] = struct{}{}
|
||||
h.unprotectedDNSList = make(map[netip.AddrPort]struct{})
|
||||
for _, addrPort := range list {
|
||||
h.unprotectedDNSList[addrPort] = struct{}{}
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (h *hostsDNSHolder) get() map[string]struct{} {
|
||||
func (h *hostsDNSHolder) get() map[netip.AddrPort]struct{} {
|
||||
h.mutex.RLock()
|
||||
l := h.unprotectedDNSList
|
||||
h.mutex.RUnlock()
|
||||
@@ -40,24 +33,10 @@ func (h *hostsDNSHolder) get() map[string]struct{} {
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (h *hostsDNSHolder) isContain(upstream string) bool {
|
||||
func (h *hostsDNSHolder) contains(upstream netip.AddrPort) bool {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
_, ok := h.unprotectedDNSList[upstream]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (h *hostsDNSHolder) normalizeAddress(addr string) (string, error) {
|
||||
a, err := netip.ParseAddr(addr)
|
||||
if err != nil {
|
||||
log.Errorf("invalid upstream IP address: %s, error: %s", addr, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if a.Is4() {
|
||||
return fmt.Sprintf("%s:53", addr), nil
|
||||
} else {
|
||||
return fmt.Sprintf("[%s]:53", addr), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m *MockServer) DnsIP() netip.Addr {
|
||||
return netip.MustParseAddr("100.10.254.255")
|
||||
}
|
||||
|
||||
func (m *MockServer) OnUpdatedHostDNSServer(strings []string) {
|
||||
func (m *MockServer) OnUpdatedHostDNSServer(addrs []netip.AddrPort) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
|
||||
func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(netip.Addr) error {
|
||||
if err := n.restoreHostDNS(); err != nil {
|
||||
return fmt.Errorf("restoring dns via network-manager: %w", err)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type resolvconf struct {
|
||||
implType resolvconfType
|
||||
|
||||
originalSearchDomains []string
|
||||
originalNameServers []string
|
||||
originalNameServers []netip.Addr
|
||||
othersConfigs []string
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolvconf) getOriginalNameservers() []string {
|
||||
func (r *resolvconf) getOriginalNameservers() []netip.Addr {
|
||||
return r.originalNameServers
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func (r *resolvconf) applyConfig(content bytes.Buffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolvconf) restoreUncleanShutdownDNS(*netip.Addr) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ type Server interface {
|
||||
Stop()
|
||||
DnsIP() netip.Addr
|
||||
UpdateDNSServer(serial uint64, update nbdns.Config) error
|
||||
OnUpdatedHostDNSServer(strings []string)
|
||||
OnUpdatedHostDNSServer(addrs []netip.AddrPort)
|
||||
SearchDomains() []string
|
||||
ProbeAvailability()
|
||||
}
|
||||
@@ -55,7 +55,7 @@ type nsGroupsByDomain struct {
|
||||
// hostManagerWithOriginalNS extends the basic hostManager interface
|
||||
type hostManagerWithOriginalNS interface {
|
||||
hostManager
|
||||
getOriginalNameservers() []string
|
||||
getOriginalNameservers() []netip.Addr
|
||||
}
|
||||
|
||||
// DefaultServer dns server object
|
||||
@@ -136,7 +136,7 @@ func NewDefaultServer(
|
||||
func NewDefaultServerPermanentUpstream(
|
||||
ctx context.Context,
|
||||
wgInterface WGIface,
|
||||
hostsDnsList []string,
|
||||
hostsDnsList []netip.AddrPort,
|
||||
config nbdns.Config,
|
||||
listener listener.NetworkChangeListener,
|
||||
statusRecorder *peer.Status,
|
||||
@@ -144,6 +144,7 @@ func NewDefaultServerPermanentUpstream(
|
||||
) *DefaultServer {
|
||||
log.Debugf("host dns address list is: %v", hostsDnsList)
|
||||
ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder, nil, disableSys)
|
||||
|
||||
ds.hostsDNSHolder.set(hostsDnsList)
|
||||
ds.permanent = true
|
||||
ds.addHostRootZone()
|
||||
@@ -340,7 +341,7 @@ func (s *DefaultServer) disableDNS() error {
|
||||
|
||||
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
|
||||
// It will be applied if the mgm server do not enforce DNS settings for root zone
|
||||
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
|
||||
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []netip.AddrPort) {
|
||||
s.hostsDNSHolder.set(hostsDnsList)
|
||||
|
||||
// Check if there's any root handler
|
||||
@@ -461,7 +462,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
||||
|
||||
s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
|
||||
|
||||
if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() {
|
||||
if s.service.RuntimePort() != DefaultPort && !s.hostManager.supportCustomPort() {
|
||||
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
|
||||
"Learn more at: https://docs.netbird.io/how-to/manage-dns-in-your-network#local-resolver")
|
||||
s.currentConfig.RouteAll = false
|
||||
@@ -581,14 +582,13 @@ func (s *DefaultServer) registerFallback(config HostDNSConfig) {
|
||||
}
|
||||
|
||||
for _, ns := range originalNameservers {
|
||||
if ns == config.ServerIP.String() {
|
||||
if ns == config.ServerIP {
|
||||
log.Debugf("skipping original nameserver %s as it is the same as the server IP %s", ns, config.ServerIP)
|
||||
continue
|
||||
}
|
||||
|
||||
ns = formatAddr(ns, defaultPort)
|
||||
|
||||
handler.upstreamServers = append(handler.upstreamServers, ns)
|
||||
addrPort := netip.AddrPortFrom(ns, DefaultPort)
|
||||
handler.upstreamServers = append(handler.upstreamServers, addrPort)
|
||||
}
|
||||
handler.deactivate = func(error) { /* always active */ }
|
||||
handler.reactivate = func() { /* always active */ }
|
||||
@@ -695,7 +695,13 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
|
||||
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
|
||||
continue
|
||||
}
|
||||
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
|
||||
|
||||
if ns.IP == s.service.RuntimeIP() {
|
||||
log.Warnf("skipping nameserver %s as it matches our DNS server IP, preventing potential loop", ns.IP)
|
||||
continue
|
||||
}
|
||||
|
||||
handler.upstreamServers = append(handler.upstreamServers, ns.AddrPort())
|
||||
}
|
||||
|
||||
if len(handler.upstreamServers) == 0 {
|
||||
@@ -770,18 +776,6 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
||||
s.dnsMuxMap = muxUpdateMap
|
||||
}
|
||||
|
||||
func getNSHostPort(ns nbdns.NameServer) string {
|
||||
return formatAddr(ns.IP.String(), ns.Port)
|
||||
}
|
||||
|
||||
// formatAddr formats a nameserver address with port, handling IPv6 addresses properly
|
||||
func formatAddr(address string, port int) string {
|
||||
if ip, err := netip.ParseAddr(address); err == nil && ip.Is6() {
|
||||
return fmt.Sprintf("[%s]:%d", address, port)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", address, port)
|
||||
}
|
||||
|
||||
// upstreamCallbacks returns two functions, the first one is used to deactivate
|
||||
// the upstream resolver from the configuration, the second one is used to
|
||||
// reactivate it. Not allowed to call reactivate before deactivate.
|
||||
@@ -879,10 +873,7 @@ func (s *DefaultServer) addHostRootZone() {
|
||||
return
|
||||
}
|
||||
|
||||
handler.upstreamServers = make([]string, 0)
|
||||
for k := range hostDNSServers {
|
||||
handler.upstreamServers = append(handler.upstreamServers, k)
|
||||
}
|
||||
handler.upstreamServers = maps.Keys(hostDNSServers)
|
||||
handler.deactivate = func(error) {}
|
||||
handler.reactivate = func() {}
|
||||
|
||||
@@ -893,9 +884,9 @@ func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
||||
var states []peer.NSGroupState
|
||||
|
||||
for _, group := range groups {
|
||||
var servers []string
|
||||
var servers []netip.AddrPort
|
||||
for _, ns := range group.NameServers {
|
||||
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
|
||||
servers = append(servers, ns.AddrPort())
|
||||
}
|
||||
|
||||
state := peer.NSGroupState{
|
||||
@@ -927,7 +918,7 @@ func (s *DefaultServer) updateNSState(nsGroup *nbdns.NameServerGroup, err error,
|
||||
func generateGroupKey(nsGroup *nbdns.NameServerGroup) string {
|
||||
var servers []string
|
||||
for _, ns := range nsGroup.NameServers {
|
||||
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
|
||||
servers = append(servers, ns.AddrPort().String())
|
||||
}
|
||||
return fmt.Sprintf("%v_%v", servers, nsGroup.Domains)
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ func init() {
|
||||
}
|
||||
|
||||
func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase {
|
||||
var srvs []string
|
||||
var srvs []netip.AddrPort
|
||||
for _, srv := range servers {
|
||||
srvs = append(srvs, getNSHostPort(srv))
|
||||
srvs = append(srvs, srv.AddrPort())
|
||||
}
|
||||
return &upstreamResolverBase{
|
||||
domain: domain,
|
||||
@@ -705,7 +705,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
|
||||
}
|
||||
defer wgIFace.Close()
|
||||
|
||||
var dnsList []string
|
||||
var dnsList []netip.AddrPort
|
||||
dnsConfig := nbdns.Config{}
|
||||
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, peer.NewRecorder("mgm"), false)
|
||||
err = dnsServer.Initialize()
|
||||
@@ -715,7 +715,8 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
|
||||
}
|
||||
defer dnsServer.Stop()
|
||||
|
||||
dnsServer.OnUpdatedHostDNSServer([]string{"8.8.8.8"})
|
||||
addrPort := netip.MustParseAddrPort("8.8.8.8:53")
|
||||
dnsServer.OnUpdatedHostDNSServer([]netip.AddrPort{addrPort})
|
||||
|
||||
resolver := newDnsResolver(dnsServer.service.RuntimeIP(), dnsServer.service.RuntimePort())
|
||||
_, err = resolver.LookupHost(context.Background(), "netbird.io")
|
||||
@@ -731,7 +732,8 @@ func TestDNSPermanent_updateUpstream(t *testing.T) {
|
||||
}
|
||||
defer wgIFace.Close()
|
||||
dnsConfig := nbdns.Config{}
|
||||
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false)
|
||||
addrPort := netip.MustParseAddrPort("8.8.8.8:53")
|
||||
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []netip.AddrPort{addrPort}, dnsConfig, nil, peer.NewRecorder("mgm"), false)
|
||||
err = dnsServer.Initialize()
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialize DNS server: %v", err)
|
||||
@@ -823,7 +825,8 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
|
||||
}
|
||||
defer wgIFace.Close()
|
||||
dnsConfig := nbdns.Config{}
|
||||
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false)
|
||||
addrPort := netip.MustParseAddrPort("8.8.8.8:53")
|
||||
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []netip.AddrPort{addrPort}, dnsConfig, nil, peer.NewRecorder("mgm"), false)
|
||||
err = dnsServer.Initialize()
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialize DNS server: %v", err)
|
||||
@@ -2054,55 +2057,123 @@ func TestLocalResolverPriorityConstants(t *testing.T) {
|
||||
assert.Equal(t, "local.example.com", localMuxUpdates[0].domain)
|
||||
}
|
||||
|
||||
func TestFormatAddr(t *testing.T) {
|
||||
func TestDNSLoopPrevention(t *testing.T) {
|
||||
wgInterface := &mocWGIface{}
|
||||
service := NewServiceViaMemory(wgInterface)
|
||||
dnsServerIP := service.RuntimeIP()
|
||||
|
||||
server := &DefaultServer{
|
||||
ctx: context.Background(),
|
||||
wgInterface: wgInterface,
|
||||
service: service,
|
||||
localResolver: local.NewResolver(),
|
||||
handlerChain: NewHandlerChain(),
|
||||
hostManager: &noopHostConfigurator{},
|
||||
dnsMuxMap: make(registeredHandlerMap),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
port int
|
||||
expected string
|
||||
name string
|
||||
nsGroups []*nbdns.NameServerGroup
|
||||
expectedHandlers int
|
||||
expectedServers []netip.Addr
|
||||
shouldFilterOwnIP bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4 address",
|
||||
address: "8.8.8.8",
|
||||
port: 53,
|
||||
expected: "8.8.8.8:53",
|
||||
name: "FilterOwnDNSServerIP",
|
||||
nsGroups: []*nbdns.NameServerGroup{
|
||||
{
|
||||
Primary: true,
|
||||
NameServers: []nbdns.NameServer{
|
||||
{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: dnsServerIP, NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: netip.MustParseAddr("1.1.1.1"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
},
|
||||
Domains: []string{},
|
||||
},
|
||||
},
|
||||
expectedHandlers: 1,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("1.1.1.1")},
|
||||
shouldFilterOwnIP: true,
|
||||
},
|
||||
{
|
||||
name: "IPv4 address with custom port",
|
||||
address: "1.1.1.1",
|
||||
port: 5353,
|
||||
expected: "1.1.1.1:5353",
|
||||
name: "AllServersFiltered",
|
||||
nsGroups: []*nbdns.NameServerGroup{
|
||||
{
|
||||
Primary: false,
|
||||
NameServers: []nbdns.NameServer{
|
||||
{IP: dnsServerIP, NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
},
|
||||
Domains: []string{"example.com"},
|
||||
},
|
||||
},
|
||||
expectedHandlers: 0,
|
||||
expectedServers: []netip.Addr{},
|
||||
shouldFilterOwnIP: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
address: "fd78:94bf:7df8::1",
|
||||
port: 53,
|
||||
expected: "[fd78:94bf:7df8::1]:53",
|
||||
name: "MixedServersWithOwnIP",
|
||||
nsGroups: []*nbdns.NameServerGroup{
|
||||
{
|
||||
Primary: false,
|
||||
NameServers: []nbdns.NameServer{
|
||||
{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: dnsServerIP, NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: netip.MustParseAddr("1.1.1.1"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: dnsServerIP, NSType: nbdns.UDPNameServerType, Port: 53}, // duplicate
|
||||
},
|
||||
Domains: []string{"test.com"},
|
||||
},
|
||||
},
|
||||
expectedHandlers: 1,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("1.1.1.1")},
|
||||
shouldFilterOwnIP: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with custom port",
|
||||
address: "2001:db8::1",
|
||||
port: 5353,
|
||||
expected: "[2001:db8::1]:5353",
|
||||
},
|
||||
{
|
||||
name: "IPv6 localhost",
|
||||
address: "::1",
|
||||
port: 53,
|
||||
expected: "[::1]:53",
|
||||
},
|
||||
{
|
||||
name: "Invalid address treated as hostname",
|
||||
address: "dns.example.com",
|
||||
port: 53,
|
||||
expected: "dns.example.com:53",
|
||||
name: "NoOwnIPInList",
|
||||
nsGroups: []*nbdns.NameServerGroup{
|
||||
{
|
||||
Primary: true,
|
||||
NameServers: []nbdns.NameServer{
|
||||
{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
{IP: netip.MustParseAddr("1.1.1.1"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||
},
|
||||
Domains: []string{},
|
||||
},
|
||||
},
|
||||
expectedHandlers: 1,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("1.1.1.1")},
|
||||
shouldFilterOwnIP: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := formatAddr(tt.address, tt.port)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
muxUpdates, err := server.buildUpstreamHandlerUpdate(tt.nsGroups)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, muxUpdates, tt.expectedHandlers)
|
||||
|
||||
if tt.expectedHandlers > 0 {
|
||||
handler := muxUpdates[0].handler.(*upstreamResolver)
|
||||
assert.Len(t, handler.upstreamServers, len(tt.expectedServers))
|
||||
|
||||
if tt.shouldFilterOwnIP {
|
||||
for _, upstream := range handler.upstreamServers {
|
||||
assert.NotEqual(t, dnsServerIP, upstream.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
for _, expected := range tt.expectedServers {
|
||||
found := false
|
||||
for _, upstream := range handler.upstreamServers {
|
||||
if upstream.Addr() == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Expected server %s not found", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 53
|
||||
DefaultPort = 53
|
||||
)
|
||||
|
||||
type service interface {
|
||||
|
||||
@@ -122,7 +122,7 @@ func (s *serviceViaListener) RuntimePort() int {
|
||||
defer s.listenerFlagLock.Unlock()
|
||||
|
||||
if s.ebpfService != nil {
|
||||
return defaultPort
|
||||
return DefaultPort
|
||||
} else {
|
||||
return int(s.listenPort)
|
||||
}
|
||||
@@ -148,9 +148,9 @@ func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) {
|
||||
return s.customAddr.Addr(), s.customAddr.Port(), nil
|
||||
}
|
||||
|
||||
ip, ok := s.testFreePort(defaultPort)
|
||||
ip, ok := s.testFreePort(DefaultPort)
|
||||
if ok {
|
||||
return ip, defaultPort, nil
|
||||
return ip, DefaultPort, nil
|
||||
}
|
||||
|
||||
ebpfSrv, port, ok := s.tryToUseeBPF()
|
||||
|
||||
@@ -33,7 +33,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
|
||||
dnsMux: dns.NewServeMux(),
|
||||
|
||||
runtimeIP: lastIP,
|
||||
runtimePort: defaultPort,
|
||||
runtimePort: DefaultPort,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
|
||||
func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(netip.Addr) error {
|
||||
if err := s.restoreHostDNS(); err != nil {
|
||||
return fmt.Errorf("restoring dns via systemd: %w", err)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (s *ShutdownState) Cleanup() error {
|
||||
return fmt.Errorf("create previous host manager: %w", err)
|
||||
}
|
||||
|
||||
if err := manager.restoreUncleanShutdownDNS(&s.DNSAddress); err != nil {
|
||||
if err := manager.restoreUncleanShutdownDNS(s.DNSAddress); err != nil {
|
||||
return fmt.Errorf("restore unclean shutdown dns: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
)
|
||||
|
||||
type ShutdownState struct {
|
||||
Guid string
|
||||
GPO bool
|
||||
Guid string
|
||||
GPO bool
|
||||
NRPTEntryCount int
|
||||
}
|
||||
|
||||
func (s *ShutdownState) Name() string {
|
||||
@@ -15,8 +16,9 @@ func (s *ShutdownState) Name() string {
|
||||
|
||||
func (s *ShutdownState) Cleanup() error {
|
||||
manager := ®istryConfigurator{
|
||||
guid: s.Guid,
|
||||
gpo: s.GPO,
|
||||
guid: s.Guid,
|
||||
gpo: s.GPO,
|
||||
nrptEntryCount: s.NRPTEntryCount,
|
||||
}
|
||||
|
||||
if err := manager.restoreUncleanShutdownDNS(); err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -48,7 +49,7 @@ type upstreamResolverBase struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
upstreamClient upstreamClient
|
||||
upstreamServers []string
|
||||
upstreamServers []netip.AddrPort
|
||||
domain string
|
||||
disabled bool
|
||||
failsCount atomic.Int32
|
||||
@@ -79,17 +80,20 @@ func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, d
|
||||
|
||||
// String returns a string representation of the upstream resolver
|
||||
func (u *upstreamResolverBase) String() string {
|
||||
return fmt.Sprintf("upstream %v", u.upstreamServers)
|
||||
return fmt.Sprintf("upstream %s", u.upstreamServers)
|
||||
}
|
||||
|
||||
// ID returns the unique handler ID
|
||||
func (u *upstreamResolverBase) ID() types.HandlerID {
|
||||
servers := slices.Clone(u.upstreamServers)
|
||||
slices.Sort(servers)
|
||||
slices.SortFunc(servers, func(a, b netip.AddrPort) int { return a.Compare(b) })
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(u.domain + ":"))
|
||||
hash.Write([]byte(strings.Join(servers, ",")))
|
||||
for _, s := range servers {
|
||||
hash.Write([]byte(s.String()))
|
||||
hash.Write([]byte("|"))
|
||||
}
|
||||
return types.HandlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
|
||||
}
|
||||
|
||||
@@ -130,7 +134,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
func() {
|
||||
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
|
||||
defer cancel()
|
||||
rm, t, err = u.upstreamClient.exchange(ctx, upstream, r)
|
||||
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
@@ -197,7 +201,7 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) {
|
||||
proto.SystemEvent_DNS,
|
||||
"All upstream servers failed (fail count exceeded)",
|
||||
"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
|
||||
map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")},
|
||||
map[string]string{"upstreams": u.upstreamServersString()},
|
||||
// TODO add domain meta
|
||||
)
|
||||
}
|
||||
@@ -258,7 +262,7 @@ func (u *upstreamResolverBase) ProbeAvailability() {
|
||||
proto.SystemEvent_DNS,
|
||||
"All upstream servers failed (probe failed)",
|
||||
"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
|
||||
map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")},
|
||||
map[string]string{"upstreams": u.upstreamServersString()},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -278,7 +282,7 @@ func (u *upstreamResolverBase) waitUntilResponse() {
|
||||
operation := func() error {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServers))
|
||||
return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServersString()))
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -291,7 +295,7 @@ func (u *upstreamResolverBase) waitUntilResponse() {
|
||||
}
|
||||
}
|
||||
|
||||
log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServers, exponentialBackOff.NextBackOff())
|
||||
log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServersString(), exponentialBackOff.NextBackOff())
|
||||
return fmt.Errorf("upstream check call error")
|
||||
}
|
||||
|
||||
@@ -301,7 +305,7 @@ func (u *upstreamResolverBase) waitUntilResponse() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServers)
|
||||
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServersString())
|
||||
u.failsCount.Store(0)
|
||||
u.successCount.Add(1)
|
||||
u.reactivate()
|
||||
@@ -331,13 +335,21 @@ func (u *upstreamResolverBase) disable(err error) {
|
||||
go u.waitUntilResponse()
|
||||
}
|
||||
|
||||
func (u *upstreamResolverBase) testNameserver(server string, timeout time.Duration) error {
|
||||
func (u *upstreamResolverBase) upstreamServersString() string {
|
||||
var servers []string
|
||||
for _, server := range u.upstreamServers {
|
||||
servers = append(servers, server.String())
|
||||
}
|
||||
return strings.Join(servers, ", ")
|
||||
}
|
||||
|
||||
func (u *upstreamResolverBase) testNameserver(server netip.AddrPort, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(u.ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
r := new(dns.Msg).SetQuestion(testRecord, dns.TypeSOA)
|
||||
|
||||
_, _, err := u.upstreamClient.exchange(ctx, server, r)
|
||||
_, _, err := u.upstreamClient.exchange(ctx, server.String(), r)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ func (u *upstreamResolver) exchangeWithoutVPN(ctx context.Context, upstream stri
|
||||
}
|
||||
|
||||
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
||||
if u.hostsDNSHolder.isContain(upstream) {
|
||||
return true
|
||||
if addrPort, err := netip.ParseAddrPort(upstream); err == nil {
|
||||
return u.hostsDNSHolder.contains(addrPort)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
|
||||
upstreamIP, err := netip.ParseAddr(upstreamHost)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse upstream host %s: %s", upstreamHost, err)
|
||||
} else {
|
||||
upstreamIP = upstreamIP.Unmap()
|
||||
}
|
||||
if u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() {
|
||||
log.Debugf("using private client to query upstream: %s", upstream)
|
||||
|
||||
@@ -59,7 +59,14 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
resolver, _ := newUpstreamResolver(ctx, "", netip.Addr{}, netip.Prefix{}, nil, nil, ".")
|
||||
resolver.upstreamServers = testCase.InputServers
|
||||
// Convert test servers to netip.AddrPort
|
||||
var servers []netip.AddrPort
|
||||
for _, server := range testCase.InputServers {
|
||||
if addrPort, err := netip.ParseAddrPort(server); err == nil {
|
||||
servers = append(servers, netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()))
|
||||
}
|
||||
}
|
||||
resolver.upstreamServers = servers
|
||||
resolver.upstreamTimeout = testCase.timeout
|
||||
if testCase.cancelCTX {
|
||||
cancel()
|
||||
@@ -128,7 +135,8 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
||||
reactivatePeriod: reactivatePeriod,
|
||||
failsTillDeact: failsTillDeact,
|
||||
}
|
||||
resolver.upstreamServers = []string{"0.0.0.0:-1"}
|
||||
addrPort, _ := netip.ParseAddrPort("0.0.0.0:1") // Use valid port for parsing, test will still fail on connection
|
||||
resolver.upstreamServers = []netip.AddrPort{netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())}
|
||||
resolver.failsTillDeact = 0
|
||||
resolver.reactivatePeriod = time.Microsecond * 100
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) *dns
|
||||
defer cancel()
|
||||
ips, err := f.resolver.LookupNetIP(ctx, network, domain)
|
||||
if err != nil {
|
||||
f.handleDNSError(w, query, resp, domain, err)
|
||||
f.handleDNSError(ctx, w, question, resp, domain, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -244,20 +244,57 @@ func (f *DNSForwarder) updateFirewall(matchingEntries []*ForwarderEntry, prefixe
|
||||
}
|
||||
}
|
||||
|
||||
// setResponseCodeForNotFound determines and sets the appropriate response code when IsNotFound is true
|
||||
// It distinguishes between NXDOMAIN (domain doesn't exist) and NODATA (domain exists but no records of requested type)
|
||||
//
|
||||
// LIMITATION: This function only checks A and AAAA record types to determine domain existence.
|
||||
// If a domain has only other record types (MX, TXT, CNAME, etc.) but no A/AAAA records,
|
||||
// it may incorrectly return NXDOMAIN instead of NODATA. This is acceptable since the forwarder
|
||||
// only handles A/AAAA queries and returns NOTIMP for other types.
|
||||
func (f *DNSForwarder) setResponseCodeForNotFound(ctx context.Context, resp *dns.Msg, domain string, originalQtype uint16) {
|
||||
// Try querying for a different record type to see if the domain exists
|
||||
// If the original query was for AAAA, try A. If it was for A, try AAAA.
|
||||
// This helps distinguish between NXDOMAIN and NODATA.
|
||||
var alternativeNetwork string
|
||||
switch originalQtype {
|
||||
case dns.TypeAAAA:
|
||||
alternativeNetwork = "ip4"
|
||||
case dns.TypeA:
|
||||
alternativeNetwork = "ip6"
|
||||
default:
|
||||
resp.Rcode = dns.RcodeNameError
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := f.resolver.LookupNetIP(ctx, alternativeNetwork, domain); err != nil {
|
||||
var dnsErr *net.DNSError
|
||||
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
|
||||
// Alternative query also returned not found - domain truly doesn't exist
|
||||
resp.Rcode = dns.RcodeNameError
|
||||
return
|
||||
}
|
||||
// Some other error (timeout, server failure, etc.) - can't determine, assume domain exists
|
||||
resp.Rcode = dns.RcodeSuccess
|
||||
return
|
||||
}
|
||||
|
||||
// Alternative query succeeded - domain exists but has no records of this type
|
||||
resp.Rcode = dns.RcodeSuccess
|
||||
}
|
||||
|
||||
// handleDNSError processes DNS lookup errors and sends an appropriate error response
|
||||
func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, query, resp *dns.Msg, domain string, err error) {
|
||||
func (f *DNSForwarder) handleDNSError(ctx context.Context, w dns.ResponseWriter, question dns.Question, resp *dns.Msg, domain string, err error) {
|
||||
var dnsErr *net.DNSError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &dnsErr):
|
||||
resp.Rcode = dns.RcodeServerFailure
|
||||
if dnsErr.IsNotFound {
|
||||
// Pass through NXDOMAIN
|
||||
resp.Rcode = dns.RcodeNameError
|
||||
f.setResponseCodeForNotFound(ctx, resp, domain, question.Qtype)
|
||||
}
|
||||
|
||||
if dnsErr.Server != "" {
|
||||
log.Warnf("failed to resolve query for type=%s domain=%s server=%s: %v", dns.TypeToString[query.Question[0].Qtype], domain, dnsErr.Server, err)
|
||||
log.Warnf("failed to resolve query for type=%s domain=%s server=%s: %v", dns.TypeToString[question.Qtype], domain, dnsErr.Server, err)
|
||||
} else {
|
||||
log.Warnf(errResolveFailed, domain, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package dnsfwd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
)
|
||||
|
||||
func Test_getMatchingEntries(t *testing.T) {
|
||||
@@ -708,6 +709,131 @@ func TestDNSForwarder_MultipleOverlappingPatterns(t *testing.T) {
|
||||
assert.Len(t, matches, 3, "Should match 3 patterns")
|
||||
}
|
||||
|
||||
// TestDNSForwarder_NodataVsNxdomain tests that the forwarder correctly distinguishes
|
||||
// between NXDOMAIN (domain doesn't exist) and NODATA (domain exists but no records of that type)
|
||||
func TestDNSForwarder_NodataVsNxdomain(t *testing.T) {
|
||||
mockFirewall := &MockFirewall{}
|
||||
mockResolver := &MockResolver{}
|
||||
|
||||
forwarder := NewDNSForwarder("127.0.0.1:0", 300, mockFirewall, &peer.Status{})
|
||||
forwarder.resolver = mockResolver
|
||||
|
||||
d, err := domain.FromString("example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
set := firewall.NewDomainSet([]domain.Domain{d})
|
||||
entries := []*ForwarderEntry{{Domain: d, ResID: "test-res", Set: set}}
|
||||
forwarder.UpdateDomains(entries)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queryType uint16
|
||||
setupMocks func()
|
||||
expectedCode int
|
||||
expectNoAnswer bool // true if we expect NOERROR with empty answer (NODATA case)
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "domain exists but no AAAA records (NODATA)",
|
||||
queryType: dns.TypeAAAA,
|
||||
setupMocks: func() {
|
||||
// First query for AAAA returns not found
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip6", "example.com.").
|
||||
Return([]netip.Addr{}, &net.DNSError{IsNotFound: true, Name: "example.com"}).Once()
|
||||
// Check query for A records succeeds (domain exists)
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").
|
||||
Return([]netip.Addr{netip.MustParseAddr("1.2.3.4")}, nil).Once()
|
||||
},
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectNoAnswer: true,
|
||||
description: "Should return NOERROR when domain exists but has no records of requested type",
|
||||
},
|
||||
{
|
||||
name: "domain exists but no A records (NODATA)",
|
||||
queryType: dns.TypeA,
|
||||
setupMocks: func() {
|
||||
// First query for A returns not found
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").
|
||||
Return([]netip.Addr{}, &net.DNSError{IsNotFound: true, Name: "example.com"}).Once()
|
||||
// Check query for AAAA records succeeds (domain exists)
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip6", "example.com.").
|
||||
Return([]netip.Addr{netip.MustParseAddr("2001:db8::1")}, nil).Once()
|
||||
},
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectNoAnswer: true,
|
||||
description: "Should return NOERROR when domain exists but has no A records",
|
||||
},
|
||||
{
|
||||
name: "domain doesn't exist (NXDOMAIN)",
|
||||
queryType: dns.TypeA,
|
||||
setupMocks: func() {
|
||||
// First query for A returns not found
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").
|
||||
Return([]netip.Addr{}, &net.DNSError{IsNotFound: true, Name: "example.com"}).Once()
|
||||
// Check query for AAAA also returns not found (domain doesn't exist)
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip6", "example.com.").
|
||||
Return([]netip.Addr{}, &net.DNSError{IsNotFound: true, Name: "example.com"}).Once()
|
||||
},
|
||||
expectedCode: dns.RcodeNameError,
|
||||
expectNoAnswer: true,
|
||||
description: "Should return NXDOMAIN when domain doesn't exist at all",
|
||||
},
|
||||
{
|
||||
name: "domain exists with records (normal success)",
|
||||
queryType: dns.TypeA,
|
||||
setupMocks: func() {
|
||||
mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").
|
||||
Return([]netip.Addr{netip.MustParseAddr("1.2.3.4")}, nil).Once()
|
||||
// Expect firewall update for successful resolution
|
||||
expectedPrefix := netip.PrefixFrom(netip.MustParseAddr("1.2.3.4"), 32)
|
||||
mockFirewall.On("UpdateSet", set, []netip.Prefix{expectedPrefix}).Return(nil).Once()
|
||||
},
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectNoAnswer: false,
|
||||
description: "Should return NOERROR with answer when records exist",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset mock expectations
|
||||
mockResolver.ExpectedCalls = nil
|
||||
mockResolver.Calls = nil
|
||||
mockFirewall.ExpectedCalls = nil
|
||||
mockFirewall.Calls = nil
|
||||
|
||||
tt.setupMocks()
|
||||
|
||||
query := &dns.Msg{}
|
||||
query.SetQuestion(dns.Fqdn("example.com"), tt.queryType)
|
||||
|
||||
var writtenResp *dns.Msg
|
||||
mockWriter := &test.MockResponseWriter{
|
||||
WriteMsgFunc: func(m *dns.Msg) error {
|
||||
writtenResp = m
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
resp := forwarder.handleDNSQuery(mockWriter, query)
|
||||
|
||||
// If a response was returned, it means it should be written (happens in wrapper functions)
|
||||
if resp != nil && writtenResp == nil {
|
||||
writtenResp = resp
|
||||
}
|
||||
|
||||
require.NotNil(t, writtenResp, "Expected response to be written")
|
||||
assert.Equal(t, tt.expectedCode, writtenResp.Rcode, tt.description)
|
||||
|
||||
if tt.expectNoAnswer {
|
||||
assert.Empty(t, writtenResp.Answer, "Response should have no answer records")
|
||||
}
|
||||
|
||||
mockResolver.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSForwarder_EmptyQuery(t *testing.T) {
|
||||
// Test handling of malformed query with no questions
|
||||
forwarder := NewDNSForwarder("127.0.0.1:0", 300, nil, &peer.Status{})
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
|
||||
"github.com/netbirdio/management-integrations/integrations"
|
||||
"github.com/netbirdio/netbird/management/server/groups"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/iface/bind"
|
||||
@@ -1564,13 +1565,14 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
||||
AnyTimes()
|
||||
|
||||
permissionsManager := permissions.NewManager(store)
|
||||
groupsManager := groups.NewManagerMock()
|
||||
|
||||
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager)
|
||||
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
||||
mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &server.MockIntegratedValidator{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface/device"
|
||||
"github.com/netbirdio/netbird/client/internal/dns"
|
||||
"github.com/netbirdio/netbird/client/internal/listener"
|
||||
@@ -13,7 +15,7 @@ type MobileDependency struct {
|
||||
TunAdapter device.TunAdapter
|
||||
IFaceDiscover stdnet.ExternalIFaceDiscover
|
||||
NetworkChangeListener listener.NetworkChangeListener
|
||||
HostDNSAddresses []string
|
||||
HostDNSAddresses []netip.AddrPort
|
||||
DnsReadyListener dns.ReadyListener
|
||||
|
||||
// iOS only
|
||||
|
||||
@@ -140,7 +140,7 @@ type RosenpassState struct {
|
||||
// whether it's enabled, and the last error message encountered during probing.
|
||||
type NSGroupState struct {
|
||||
ID string
|
||||
Servers []string
|
||||
Servers []netip.AddrPort
|
||||
Domains []string
|
||||
Enabled bool
|
||||
Error error
|
||||
|
||||
@@ -593,17 +593,9 @@ func update(input ConfigInput) (*Config, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetConfig read config file and return with Config. Errors out if it does not exist
|
||||
func GetConfig(configPath string) (*Config, error) {
|
||||
if !fileExists(configPath) {
|
||||
return nil, fmt.Errorf("config file %s does not exist", configPath)
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if _, err := util.ReadJson(configPath, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return readConfig(configPath, false)
|
||||
}
|
||||
|
||||
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
|
||||
@@ -695,6 +687,11 @@ func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
|
||||
|
||||
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
||||
func ReadConfig(configPath string) (*Config, error) {
|
||||
return readConfig(configPath, true)
|
||||
}
|
||||
|
||||
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
||||
func readConfig(configPath string, createIfMissing bool) (*Config, error) {
|
||||
if fileExists(configPath) {
|
||||
err := util.EnforcePermission(configPath)
|
||||
if err != nil {
|
||||
@@ -715,6 +712,8 @@ func ReadConfig(configPath string) (*Config, error) {
|
||||
}
|
||||
|
||||
return config, nil
|
||||
} else if !createIfMissing {
|
||||
return nil, fmt.Errorf("config file %s does not exist", configPath)
|
||||
}
|
||||
|
||||
cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
|
||||
|
||||
Reference in New Issue
Block a user