Skip to content

[client] Add IPv6 reverse DNS and host configurator support#5686

Open
lixmal wants to merge 7 commits intoclient-ipv6-ifacefrom
client-ipv6-dns
Open

[client] Add IPv6 reverse DNS and host configurator support#5686
lixmal wants to merge 7 commits intoclient-ipv6-ifacefrom
client-ipv6-dns

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Mar 25, 2026

Describe your changes

  • Add IPv6 reverse DNS (PTR) support using RFC 3596 nibble format for ip6.arpa zones, alongside existing in-addr.arpa for v4
  • Fix DNS host configurators on Linux (systemd, NetworkManager) and iOS to handle IPv6 server addresses and socket binding
  • Add tests for PTR generation, reverse zone naming, and mixed v4/v6 record collection

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:

netbirdio/docs#667

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

This PR adds comprehensive IPv6 support to DNS PTR record generation and reverse zone management. Changes include IPv6-capable PTR creation using RFC 3596 nibble expansion (ip6.arpa), IPv6 address family detection across DNS backends (systemd, NetworkManager), and IPv6 upstream resolver support for iOS.

Changes

Cohort / File(s) Summary
Core DNS PTR & Reverse Zone Logic
client/internal/dns.go, client/internal/dns_test.go
Added IPv6 PTR record generation with nibble expansion and ip6.arpa support. Modified createPTRRecord, generateReverseZoneName, and collectPTRRecords to handle both A (IPv4) and AAAA (IPv6) records. Comprehensive test coverage added for IPv4/IPv6 scenarios and zone generation.
Platform DNS Configuration
client/internal/dns/host_darwin.go, client/internal/dns/network_manager_unix.go, client/internal/dns/systemd_linux.go
Updated platform-specific DNS backends to detect and apply IPv6 address family settings. NetworkManager now selects ipv4 vs ipv6 D-Bus keys; systemd now sets AF_INET or AF_INET6 based on ServerIP; macOS prefers IPv4 with added documentation.
iOS Upstream Resolver
client/internal/dns/upstream_ios.go
Expanded upstream resolver to track IPv6 local addresses and prefixes. Refactored private client selection logic to treat IPv6 upstreams as private when matching routed IPv6 prefix, with protocol/option selection based on IP version.
DNS Engine Integration
client/internal/engine.go
Modified DNS configuration generation to derive reverse zones from both IPv4 and IPv6 address networks. Updated toDNSConfig to accept full address object and conditionally add IPv6 reverse zones.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Engine as DNS Engine
    participant CoreDNS as PTR/Zone Logic
    participant Platform as Platform Backend
    participant System as System DNS

    App->>Engine: Generate DNS config
    Engine->>CoreDNS: toDNSConfig (IPv4 + IPv6 networks)
    
    alt IPv4 Address
        CoreDNS->>CoreDNS: Create PTR with in-addr.arpa
        Note over CoreDNS: Byte-based reverse
    else IPv6 Address
        CoreDNS->>CoreDNS: Create PTR with ip6.arpa<br/>(nibble-expanded)
        Note over CoreDNS: Nibble-based reverse (RFC 3596)
    end
    
    CoreDNS-->>Engine: Config with reverse zones
    Engine->>Platform: applyDNSConfig
    
    alt systemd Backend
        Platform->>Platform: Detect AF_INET vs AF_INET6
        Platform->>System: SetDNS (with address family)
    else NetworkManager Backend
        Platform->>Platform: Select ipv4 vs ipv6 key
        Platform->>System: Update D-Bus settings
    end
    
    System-->>Platform: DNS Updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • mlsmaycon

Poem

🐰 Hop-hop, the networks now leap in two!
IPv4's friend, IPv6 breaks through—
Nibbles expand where bytes once dwelled,
In arpa.reverse, our DNS spells!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature addition—IPv6 reverse DNS support and host configurator updates—matching the primary changes across multiple DNS-related files.
Description check ✅ Passed The pull request description includes all required sections: detailed change description, linked issue tickets in a stack format, completed checklist with feature enhancement and tests marked, confirmation of CLA agreement, and documentation status with PR link.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client-ipv6-dns

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.

@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented Mar 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
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.

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

72-88: Consider edge case with private IPv6 upstream but no local IPv6 address.

The needsPrivate check at line 76-77 can be true for an IPv6 upstream via upstreamIP.IsPrivate() even when lIPv6 is invalid. In this case, bindIP would incorrectly remain as the IPv4 lIP (line 79), causing a protocol mismatch when binding the socket.

Consider adding a guard to skip private-client usage when the address families don't match:

 	needsPrivate := u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() ||
 		(u.lNetV6.IsValid() && u.lNetV6.Contains(upstreamIP))
 	if needsPrivate {
 		bindIP := u.lIP
 		if upstreamIP.Is6() && u.lIPv6.IsValid() {
 			bindIP = u.lIPv6
+		} else if upstreamIP.Is6() {
+			// No valid IPv6 local address, fall back to regular client
+			log.Debugf("skipping private client for IPv6 upstream %s: no local IPv6 address", upstream)
+			goto regularClient
 		}

Alternatively, this may be an acceptable limitation given the existing TODO comment acknowledges the heuristic's limitations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/dns/upstream_ios.go` around lines 72 - 88, The current
needsPrivate logic can select private-client handling for an IPv6 upstream
(upstreamIP.IsPrivate()) while lIPv6 is invalid, leaving bindIP set to the IPv4
lIP and causing an address-family mismatch when calling GetClientPrivate; update
the branch in which needsPrivate is handled (the block referencing bindIP, lIP,
lIPv6, upstreamIP.Is6(), interfaceName, and GetClientPrivate) to guard that you
only use the private-client path when the local IP family matches the upstream
family — i.e., if upstreamIP.Is6() require lIPv6.IsValid() (skip or fall back
otherwise), and similarly ensure IPv4 upstreams use lIP.IsValid() — so you never
bind an IPv4 local IP for an IPv6 upstream (or vice versa).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/internal/dns/upstream_ios.go`:
- Around line 72-88: The current needsPrivate logic can select private-client
handling for an IPv6 upstream (upstreamIP.IsPrivate()) while lIPv6 is invalid,
leaving bindIP set to the IPv4 lIP and causing an address-family mismatch when
calling GetClientPrivate; update the branch in which needsPrivate is handled
(the block referencing bindIP, lIP, lIPv6, upstreamIP.Is6(), interfaceName, and
GetClientPrivate) to guard that you only use the private-client path when the
local IP family matches the upstream family — i.e., if upstreamIP.Is6() require
lIPv6.IsValid() (skip or fall back otherwise), and similarly ensure IPv4
upstreams use lIP.IsValid() — so you never bind an IPv4 local IP for an IPv6
upstream (or vice versa).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c355c27-698d-4f8b-bf84-3e9980de8268

📥 Commits

Reviewing files that changed from the base of the PR and between 33628b0 and 3f50f7e.

📒 Files selected for processing (7)
  • client/internal/dns.go
  • client/internal/dns/host_darwin.go
  • client/internal/dns/network_manager_unix.go
  • client/internal/dns/systemd_linux.go
  • client/internal/dns/upstream_ios.go
  • client/internal/dns_test.go
  • client/internal/engine.go

@lixmal lixmal force-pushed the client-ipv6-iface branch from 33628b0 to 1a7e835 Compare March 25, 2026 08:56
@lixmal lixmal closed this Mar 25, 2026
@lixmal lixmal reopened this Mar 25, 2026
@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@lixmal lixmal force-pushed the client-ipv6-iface branch from 1a7e835 to 3be5a5f Compare March 25, 2026 09:06
# Conflicts:
#	client/iface/wgaddr/address.go
@lixmal lixmal force-pushed the client-ipv6-iface branch from 3be5a5f to baf2c03 Compare March 25, 2026 09:18
@sonarqubecloud
Copy link
Copy Markdown

bindIP = u.lIP
}

if bindIP.IsValid() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If have validation issue with lIPv6 or lIP then bindIP will be invalid.
The old code always used the private client for matching upstreams. This is a behavior change. If this i the expected then fine.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is a subtle intentional change. If neither lIP nor lIPv6 is valid for the upstream's address family, we fall through to the default (non-private) client instead of binding to an invalid address. In practice, if needsPrivate is true but the corresponding local IP is invalid, the configuration is broken and there's nothing useful to bind to.

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.

2 participants