Skip to content

Conversation

@hurricanehrndz
Copy link
Contributor

@hurricanehrndz hurricanehrndz commented Nov 21, 2025

Describe your changes

This commit enhances Darwin (macOS) DNS configuration to support multiple DNS servers instead of just a single server:
Key Changes:

  • API Change: Renamed ServerIP to ServerIPs (slice) in SystemDNSSettings struct
  • Multi-server collection: Modified DNS settings parser to collect all DNS server addresses (IPv4 and IPv6) instead of stopping at the first IPv4
  • Multi-server configuration: Updated addDNSState() to accept and configure multiple DNS servers as space-separated string
  • Deduplication: Added domain deduplication logic to prevent duplicate entries in search domains
  • Validation: Improved IP validation using ip.IsValid() for all collected addresses

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • DNS settings now support multiple DNS server addresses for improved resilience.
  • Refactor

    • Configuration flows updated to handle multiple servers, validate/ignore invalid addresses, deduplicate domains, and surface parsing/logging for system DNS state; defaults (e.g., port) preserved.
  • Tests

    • Added unit tests covering parsing, multiple servers (including IPv6), deduplication, empty outputs, invalid IPs, and other edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings November 21, 2025 15:55
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

System DNS handling was changed to support multiple resolver addresses: SystemDNSSettings.ServerIP was replaced by ServerIPs []netip.Addr; Darwin-specific parsing, configuration-signatures, and tests were updated to parse, propagate, and validate multiple server IPs and domains.

Changes

Cohort / File(s) Summary
Data model
client/internal/dns/host.go
Replaced SystemDNSSettings.ServerIP netip.Addr with ServerIPs []netip.Addr.
Darwin DNS config & parsing
client/internal/dns/host_darwin.go
Updated signatures to accept []netip.Addr (addSearchDomains, addMatchDomains, addDNSState); applyDNSConfig and local-DNS paths pass slices; recordSystemDNSSettings checks slice length; added parseSystemConfigOutput(output []byte) (SystemDNSSettings, error) used by getSystemDNSSettings to extract domains, deduplicate them, parse multiple server IPs and port.
Tests
client/internal/dns/host_darwin_test.go
Added unit tests for parseSystemConfigOutput covering full output, multiple servers (including IPv6), domain deduplication, empty output defaults, invalid IP filtering, single-domain cases, and nested-array structures.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Focus areas:
    • parseSystemConfigOutput: IP parsing, invalid-IP filtering, domain deduplication, port extraction.
    • Updated function signatures in host_darwin.go: verify all call sites and correct handling of empty slices.
    • Tests in host_darwin_test.go: ensure edge cases and platform-specific assumptions are appropriate.

Poem

🐇 I nibbled lines of config, neat and keen,
One IP grew many — a resolvable scene,
Domains deduped, addresses arrayed,
I danced through tests the coder made,
Hooray for slices, tidy and green 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding multi-server DNS support for macOS (Darwin).
Description check ✅ Passed The description covers the key changes comprehensively, but the issue ticket and stack sections are empty, which are expected to be filled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances DNS configuration on macOS to support multiple DNS servers instead of a single server. The change allows collecting and configuring all available DNS servers (both IPv4 and IPv6) from the system.

  • Renamed ServerIP to ServerIPs to reflect the change from single to multiple server support
  • Modified DNS settings parser to collect all DNS server addresses instead of stopping at the first IPv4
  • Added domain deduplication logic to prevent duplicate entries in search domains

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
client/internal/dns/host.go Changed SystemDNSSettings struct field from ServerIP to ServerIPs slice
client/internal/dns/host_darwin.go Updated DNS configuration logic to handle multiple servers, added deduplication for domains, and modified all functions to accept/process server IP slices

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

searchDomains = append(searchDomains, strings.TrimSuffix(""+dConf.Domain, "."))
}

configServerIPs := []netip.Addr{config.ServerIP}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line attempts to access config.ServerIP which no longer exists after the API change. The HostDNSConfig struct should have a ServerIP or ServerIPs field that matches the new API. Verify that HostDNSConfig has been updated accordingly, or this should use the correct field name.

Suggested change
configServerIPs := []netip.Addr{config.ServerIP}
configServerIPs := config.ServerIPs

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is completely wrong on AI's part

Comment on lines +256 to +245
if ip.IsValid() {
dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ip.IsValid() check is redundant here. If netip.ParseAddr(address) succeeds (err == nil), the resulting IP is guaranteed to be valid. The check can be removed to simplify the code.

Suggested change
if ip.IsValid() {
dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
}
dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking twice falls inline with what was previously done

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
client/internal/dns/host_darwin.go (2)

255-259: Consider removing redundant validation check.

The parsing logic correctly collects all DNS server addresses. However, the ip.IsValid() check on line 256 is redundant since netip.ParseAddr only returns a valid address when err == nil.

The use of ip.Unmap() to normalize IPv4-mapped IPv6 addresses is good practice.

Apply this diff to simplify the validation:

 } else if inServerAddressesArray {
     address := strings.Split(line, " : ")[1]
     if ip, err := netip.ParseAddr(address); err == nil {
-        if ip.IsValid() {
-            dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
-        }
+        dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
     }
 }

299-323: Consider validating non-empty dnsServers slice.

The implementation correctly converts the dnsServers slice to a space-separated string format for scutil. However, if an empty slice is passed, this would result in an empty serversStr, which might not be the intended behavior.

Consider adding validation at the start of the function:

func (s *systemConfigurator) addDNSState(state, domains string, dnsServers []netip.Addr, port int, enableSearch bool) error {
    if len(dnsServers) == 0 {
        return fmt.Errorf("dnsServers cannot be empty")
    }
    
    noSearch := "1"
    if enableSearch {
        noSearch = "0"
    }
    
    // ... rest of implementation
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32146e5 and 7a78e27.

📒 Files selected for processing (2)
  • client/internal/dns/host.go (1 hunks)
  • client/internal/dns/host_darwin.go (10 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: Linux
  • GitHub Check: Darwin
  • GitHub Check: Windows
🔇 Additional comments (5)
client/internal/dns/host_darwin.go (4)

82-82: LGTM: Correct bridge from single server to slice-based functions.

Creating a slice with the single NetBird DNS server IP correctly adapts the single-server HostDNSConfig to the multi-server function signatures.


172-190: LGTM: Correct usage of ServerIPs slice for local DNS configuration.

The empty slice checks and usage of s.systemDNSSettings.ServerIPs are correct. This properly leverages the system's existing DNS servers for local domain resolution.


222-252: Good improvement: Domain deduplication prevents duplicates in search domains.

The use of localDomainsMap to deduplicate both DomainName and SearchDomains entries is a solid enhancement that prevents duplicate entries in the final configuration.


273-297: LGTM: Function signatures correctly updated for multi-server support.

Both addSearchDomains and addMatchDomains now accept []netip.Addr parameters and properly forward them to addDNSState. The signature updates are consistent and correct.

client/internal/dns/host.go (1)

28-28: No issues found. All references to SystemDNSSettings.ServerIP have been properly updated to ServerIPs.

The verification confirms the field rename is complete and correct. All remaining ServerIP references in the codebase are to HostDNSConfig (a different struct with type netip.Addr), not to SystemDNSSettings. This is intentional—HostDNSConfig.ServerIP is singular and represents the single DNS server configuration at that layer, while SystemDNSSettings.ServerIPs is a slice supporting multi-server capability.

@hurricanehrndz hurricanehrndz force-pushed the fix/darwin-dns-multi-server-support branch from 7a78e27 to 2d7b9a0 Compare November 21, 2025 16:08
@hurricanehrndz hurricanehrndz force-pushed the fix/darwin-dns-multi-server-support branch from 2d7b9a0 to b1c6513 Compare November 30, 2025 15:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
client/internal/dns/host_darwin.go (1)

234-247: Consider defensive bounds checking on string splits.

The parsing logic splits on " : " and directly accesses index [1] at lines 235 and 241. While system output format is stable and tests validate the expected structure, adding bounds checking would make the code more resilient to unexpected formats.

Example defensive approach:

 if inSearchDomainsArray {
-    searchDomain := strings.Split(line, " : ")[1]
+    parts := strings.Split(line, " : ")
+    if len(parts) < 2 {
+        continue
+    }
+    searchDomain := parts[1]
     if _, exists := localDomainsMap[searchDomain]; !exists {
         localDomainsMap[searchDomain] = struct{}{}
         dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
     }
 } else if inServerAddressesArray {
-    address := strings.Split(line, " : ")[1]
+    parts := strings.Split(line, " : ")
+    if len(parts) < 2 {
+        continue
+    }
+    address := parts[1]
     if ip, err := netip.ParseAddr(address); err == nil {
         if ip.IsValid() {
             dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
         }
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a78e27 and b1c6513.

📒 Files selected for processing (3)
  • client/internal/dns/host.go (1 hunks)
  • client/internal/dns/host_darwin.go (9 hunks)
  • client/internal/dns/host_darwin_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
client/internal/dns/host.go (1)
shared/relay/client/dialer/quic/conn.go (1)
  • Addr (20-22)
client/internal/dns/host_darwin.go (1)
client/internal/dns/host.go (1)
  • SystemDNSSettings (21-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: release_ui
  • GitHub Check: Windows
  • GitHub Check: release
  • GitHub Check: Darwin
  • GitHub Check: Client / Unit
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Build Cache
  • GitHub Check: JS / Lint
🔇 Additional comments (6)
client/internal/dns/host.go (1)

21-25: LGTM! Clean API evolution for multi-server support.

The change from ServerIP (singular) to ServerIPs (slice) correctly enables SystemDNSSettings to represent multiple DNS servers. The implementation in host_darwin.go properly leverages this updated data model.

client/internal/dns/host_darwin_test.go (1)

113-255: Excellent test coverage for the new parsing logic.

The test suite comprehensively validates parseSystemConfigOutput across multiple scenarios:

  • Complete and partial configurations
  • IPv4 and IPv6 server addresses
  • Domain deduplication
  • Invalid input handling (empty output, malformed IPs)
  • Nested arrays and additional fields

The tests provide strong confidence in the parsing implementation.

client/internal/dns/host_darwin.go (4)

82-82: LGTM! Correct handling of NetBird DNS server.

Wrapping config.ServerIP into a slice is appropriate here, as the updated functions accept multiple DNS servers, but HostDNSConfig represents NetBird's single DNS server configuration.


172-195: LGTM! Consistent migration to slice-based checks.

The updated checks using len(s.systemDNSSettings.ServerIPs) correctly replace the previous ServerIP.IsValid() pattern, and the slice is properly propagated to addSearchDomains.


276-300: LGTM! Function signatures correctly updated.

Both addSearchDomains and addMatchDomains are properly updated to accept []netip.Addr instead of single addresses, and correctly propagate the slices to addDNSState.


302-326: LGTM! Clean implementation of multi-server state configuration.

The conversion of the DNS server slice to a space-separated string (lines 308-312) correctly formats the data for scutil commands, enabling multiple servers to be configured in the DNS state.

Multi-Server Support (lines 304-308):
- Converts slice of IPs to space-separated string

DNS Server Address Collection (lines 253-257):
- Collects all DNS server addresses (not just first)
- Proper validation with ip.IsValid()
Extracted scutil output parsing logic from getSystemDNSSettings() into a
standalone parseSystemConfigOutput() helper function. This improves code
maintainability by separating parsing logic from command execution and
enables comprehensive unit testing.

Added 7 unit tests covering:
- Complete output parsing with domains and servers
- Multiple IPv4/IPv6 server handling
- Domain deduplication
- Empty output edge case
- Invalid IP filtering
- Minimal output scenarios
- Array boundary handling
@hurricanehrndz hurricanehrndz force-pushed the fix/darwin-dns-multi-server-support branch from b1c6513 to 92c85dd Compare December 3, 2025 21:26
@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 3, 2025

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1c6513 and 92c85dd.

📒 Files selected for processing (3)
  • client/internal/dns/host.go (1 hunks)
  • client/internal/dns/host_darwin.go (9 hunks)
  • client/internal/dns/host_darwin_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
client/internal/dns/host_darwin.go (1)
client/internal/dns/host.go (1)
  • SystemDNSSettings (21-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build Cache
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: Android / Build
🔇 Additional comments (9)
client/internal/dns/host.go (1)

23-23: LGTM! Clean API change to support multiple DNS servers.

The change from a single ServerIP to a slice ServerIPs is well-aligned with the PR objectives and consistently implemented across the Darwin-specific code.

client/internal/dns/host_darwin_test.go (1)

113-255: Excellent test coverage for the new parsing logic.

The test suite comprehensively covers various scenarios including:

  • Multiple IPv4 and IPv6 servers
  • Domain deduplication
  • Invalid IP filtering
  • Edge cases (empty output, domain-only configs)

This provides strong confidence in the robustness of the parseSystemConfigOutput function.

client/internal/dns/host_darwin.go (7)

82-82: LGTM! Correct conversion of NetBird's DNS server to slice format.

This wraps the single NetBird DNS resolver IP from HostDNSConfig into a slice for compatibility with the updated function signatures that accept []netip.Addr.


86-86: LGTM! Correct usage of updated function signatures.

The calls to addMatchDomains and addSearchDomains correctly pass the []netip.Addr slice as required by their updated signatures.

Also applies to: 98-98


172-185: LGTM! Proper handling of system DNS settings.

The code correctly checks for empty ServerIPs slice and passes the system DNS servers when enabling local DNS. The logic properly handles the multi-server scenario.


194-194: LGTM! Correct update for multi-server API.

The length check properly replaces the previous IsValid() check for the slice-based ServerIPs field.


259-274: LGTM! Clean refactoring improves testability.

Extracting the parsing logic into parseSystemConfigOutput separates concerns and makes the code more testable, as evidenced by the comprehensive test suite.


276-300: LGTM! Function signatures correctly updated for multi-server support.

Both addSearchDomains and addMatchDomains now accept []netip.Addr slices and properly delegate to addDNSState.


302-326: LGTM! Correct implementation of multi-server DNS state configuration.

The function properly converts the []netip.Addr slice to a space-separated string for the scutil command, enabling multiple DNS servers to be configured.

Comment on lines 217 to +222
case strings.HasPrefix(line, "DomainName :"):
domainName := strings.TrimSpace(strings.Split(line, ":")[1])
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
if _, exists := localDomainsMap[domainName]; !exists {
localDomainsMap[domainName] = struct{}{}
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add case-insensitive domain deduplication.

The current deduplication logic is case-sensitive, but DNS domain names are case-insensitive. For example, "Example.com" and "example.com" should be treated as the same domain.

Apply this diff to normalize domains to lowercase before deduplication:

 		case strings.HasPrefix(line, "DomainName :"):
-			domainName := strings.TrimSpace(strings.Split(line, ":")[1])
+			domainName := strings.ToLower(strings.TrimSpace(strings.Split(line, ":")[1]))
 			if _, exists := localDomainsMap[domainName]; !exists {
 				localDomainsMap[domainName] = struct{}{}
 				dnsSettings.Domains = append(dnsSettings.Domains, domainName)
 			}
 		if inSearchDomainsArray {
-			searchDomain := strings.Split(line, " : ")[1]
+			searchDomain := strings.ToLower(strings.Split(line, " : ")[1])
 			if _, exists := localDomainsMap[searchDomain]; !exists {
 				localDomainsMap[searchDomain] = struct{}{}
 				dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
 			}

Also applies to: 234-239

🤖 Prompt for AI Agents
In client/internal/dns/host_darwin.go around lines 217-222 (and also apply same
change to lines 234-239), the deduplication uses the raw DomainName string so
"Example.com" and "example.com" are treated as distinct; normalize the domain to
lowercase before checking and inserting into localDomainsMap and before
appending to dnsSettings.Domains (use the lowered value as the deduplication key
and append the normalized lowercase domain).

}

if inSearchDomainsArray {
searchDomain := strings.Split(line, " : ")[1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add bounds checking to prevent potential panics.

Lines 235 and 241 use strings.Split(line, " : ")[1] without verifying that the split produced at least 2 elements. If the system config output format is unexpected, this will cause a panic.

Apply this diff to add defensive bounds checking:

 		if inSearchDomainsArray {
-			searchDomain := strings.Split(line, " : ")[1]
+			parts := strings.Split(line, " : ")
+			if len(parts) < 2 {
+				continue
+			}
+			searchDomain := parts[1]
 			if _, exists := localDomainsMap[searchDomain]; !exists {
 				localDomainsMap[searchDomain] = struct{}{}
 				dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
 			}
 		} else if inServerAddressesArray {
-			address := strings.Split(line, " : ")[1]
+			parts := strings.Split(line, " : ")
+			if len(parts) < 2 {
+				continue
+			}
+			address := parts[1]
 			if ip, err := netip.ParseAddr(address); err == nil {
 				if ip.IsValid() {
 					dnsSettings.ServerIPs = append(dnsSettings.ServerIPs, ip.Unmap())
 				}
 			}

Also applies to: 241-241

🤖 Prompt for AI Agents
In client/internal/dns/host_darwin.go around lines 235 and 241, the code calls
strings.Split(line, " : ")[1] without checking the split length which can panic;
update both locations to perform bounds checking by splitting into a slice,
verifying len(parts) >= 2 before indexing, and only assign parts[1] to
searchDomain (or other variable) when present, otherwise handle the missing
value gracefully (skip the line, continue, or set a default/empty value) and
optionally log a debug warning to aid troubleshooting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant