mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-22 21:39:08 +02:00
[client] Fix macOS DNS panic on malformed scutil output (#7180)
This commit is contained in:
@@ -267,18 +267,38 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|||||||
return SystemDNSSettings{}, fmt.Errorf("sending the command: %w", err)
|
return SystemDNSSettings{}, fmt.Errorf("sending the command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dnsSettings SystemDNSSettings
|
dnsSettings, serverAddresses, err := parseSystemDNSSettings(b)
|
||||||
|
if err != nil {
|
||||||
|
return dnsSettings, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.origNameservers = serverAddresses
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
return dnsSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSystemDNSSettings parses the output of `scutil show State:/Network/Service/<id>/DNS`.
|
||||||
|
// Lines that don't match the expected "index : value" shape are skipped: hosts with unusual
|
||||||
|
// network services (e.g. orphaned hardware ports) can produce entries without a value.
|
||||||
|
func parseSystemDNSSettings(out []byte) (SystemDNSSettings, []netip.Addr, error) {
|
||||||
|
// port is not exposed by scutil, default to 53
|
||||||
|
dnsSettings := SystemDNSSettings{ServerPort: DefaultPort}
|
||||||
var serverAddresses []netip.Addr
|
var serverAddresses []netip.Addr
|
||||||
inSearchDomainsArray := false
|
inSearchDomainsArray := false
|
||||||
inServerAddressesArray := false
|
inServerAddressesArray := false
|
||||||
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, "DomainName :"):
|
case strings.HasPrefix(line, "DomainName :"):
|
||||||
domainName := strings.TrimSpace(strings.Split(line, ":")[1])
|
domainName := strings.TrimSpace(strings.TrimPrefix(line, "DomainName :"))
|
||||||
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
|
if domainName != "" {
|
||||||
|
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
|
||||||
|
}
|
||||||
|
continue
|
||||||
case line == "SearchDomains : <array> {":
|
case line == "SearchDomains : <array> {":
|
||||||
inSearchDomainsArray = true
|
inSearchDomainsArray = true
|
||||||
continue
|
continue
|
||||||
@@ -288,36 +308,45 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|||||||
case line == "}":
|
case line == "}":
|
||||||
inSearchDomainsArray = false
|
inSearchDomainsArray = false
|
||||||
inServerAddressesArray = false
|
inServerAddressesArray = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inSearchDomainsArray && !inServerAddressesArray {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, " : ", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
log.Debugf("skipping unexpected scutil DNS line %q", line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if inSearchDomainsArray {
|
if inSearchDomainsArray {
|
||||||
searchDomain := strings.Split(line, " : ")[1]
|
dnsSettings.Domains = append(dnsSettings.Domains, value)
|
||||||
dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
|
continue
|
||||||
} else if inServerAddressesArray {
|
}
|
||||||
address := strings.Split(line, " : ")[1]
|
|
||||||
if ip, err := netip.ParseAddr(address); err == nil && !ip.IsUnspecified() {
|
ip, err := netip.ParseAddr(value)
|
||||||
ip = ip.Unmap()
|
if err != nil || ip.IsUnspecified() {
|
||||||
serverAddresses = append(serverAddresses, ip)
|
continue
|
||||||
// Prefer the first IPv4 server as ServerIP since our DNS listener is IPv4.
|
}
|
||||||
if !dnsSettings.ServerIP.IsValid() && ip.Is4() {
|
ip = ip.Unmap()
|
||||||
dnsSettings.ServerIP = ip
|
serverAddresses = append(serverAddresses, ip)
|
||||||
}
|
// Prefer the first IPv4 server as ServerIP since our DNS listener is IPv4.
|
||||||
}
|
if !dnsSettings.ServerIP.IsValid() && ip.Is4() {
|
||||||
|
dnsSettings.ServerIP = ip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return dnsSettings, err
|
return dnsSettings, serverAddresses, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to 53 port
|
return dnsSettings, serverAddresses, nil
|
||||||
dnsSettings.ServerPort = DefaultPort
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.origNameservers = serverAddresses
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
return dnsSettings, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) getOriginalNameservers() []netip.Addr {
|
func (s *systemConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
@@ -435,11 +464,15 @@ func (s *systemConfigurator) getPrimaryService() (string, string, error) {
|
|||||||
router := ""
|
router := ""
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
|
parts := strings.SplitN(text, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.Contains(text, "PrimaryService") {
|
if strings.Contains(text, "PrimaryService") {
|
||||||
primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
|
primaryService = strings.TrimSpace(parts[1])
|
||||||
}
|
}
|
||||||
if strings.Contains(text, "Router") {
|
if strings.Contains(text, "Router") {
|
||||||
router = strings.TrimSpace(strings.Split(text, ":")[1])
|
router = strings.TrimSpace(parts[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil && err != io.EOF {
|
if err := scanner.Err(); err != nil && err != io.EOF {
|
||||||
|
|||||||
@@ -328,6 +328,120 @@ func removeTestDNSKey(key string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSystemDNSSettings(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
output string
|
||||||
|
expectedDomains []string
|
||||||
|
expectedServers []netip.Addr
|
||||||
|
expectedIP netip.Addr
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "well_formed",
|
||||||
|
output: `<dictionary> {
|
||||||
|
DomainName : example.com
|
||||||
|
SearchDomains : <array> {
|
||||||
|
0 : example.com
|
||||||
|
1 : corp.example.com
|
||||||
|
}
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 : 192.168.1.1
|
||||||
|
1 : fd00::53
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedDomains: []string{"example.com", "example.com", "corp.example.com"},
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1"), netip.MustParseAddr("fd00::53")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// entries without a value after the separator used to panic with
|
||||||
|
// "index out of range [1] with length 1"
|
||||||
|
name: "malformed_array_entries_skipped",
|
||||||
|
output: `<dictionary> {
|
||||||
|
SearchDomains : <array> {
|
||||||
|
0 :
|
||||||
|
(null)
|
||||||
|
|
||||||
|
1 : corp.example.com
|
||||||
|
}
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 :
|
||||||
|
1 : 192.168.1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedDomains: []string{"corp.example.com"},
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "domain_name_without_value_skipped",
|
||||||
|
output: `<dictionary> {
|
||||||
|
DomainName :
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 : 192.168.1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6_first_prefers_ipv4_server_ip",
|
||||||
|
output: `<dictionary> {
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 : fd00::53
|
||||||
|
1 : 192.168.1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("fd00::53"), netip.MustParseAddr("192.168.1.1")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_and_unspecified_addresses_skipped",
|
||||||
|
output: `<dictionary> {
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 : (null)
|
||||||
|
1 : 0.0.0.0
|
||||||
|
2 : 192.168.1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "v4_mapped_address_unmapped",
|
||||||
|
output: `<dictionary> {
|
||||||
|
ServerAddresses : <array> {
|
||||||
|
0 : ::ffff:192.168.1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||||
|
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_output",
|
||||||
|
output: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
settings, servers, err := parseSystemDNSSettings([]byte(tc.output))
|
||||||
|
require.NoError(t, err, "parsing should not fail")
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedDomains, settings.Domains, "domains should match")
|
||||||
|
assert.Equal(t, tc.expectedServers, servers, "server addresses should match")
|
||||||
|
assert.Equal(t, tc.expectedIP, settings.ServerIP, "server IP should match")
|
||||||
|
assert.Equal(t, DefaultPort, settings.ServerPort, "server port should default to 53")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetOriginalNameservers(t *testing.T) {
|
func TestGetOriginalNameservers(t *testing.T) {
|
||||||
configurator := &systemConfigurator{
|
configurator := &systemConfigurator{
|
||||||
createdKeys: make(map[string]struct{}),
|
createdKeys: make(map[string]struct{}),
|
||||||
|
|||||||
Reference in New Issue
Block a user