Skip to content

[client] Add dual-stack nftables manager with IPv6 table support#5707

Open
lixmal wants to merge 12 commits intoclient-ipv6-routingfrom
client-ipv6-nftables
Open

[client] Add dual-stack nftables manager with IPv6 table support#5707
lixmal wants to merge 12 commits intoclient-ipv6-routingfrom
client-ipv6-nftables

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Mar 26, 2026

Describe your changes

  • Add addrFamily abstraction encapsulating IPv4/IPv6 header offsets, address lengths, set key types, and ICMP protocol numbers
  • Create parallel ip6 netbird table with its own router and ACL manager when the interface has IPv6
  • Route all firewall operations to the correct table by address family
  • Split UpdateSet prefixes by family for dynamic DNS route sets
  • Add IPv6 interval set tests (TestNftablesCreateIpSet_IPv6) and calculateLastIP tests covering both families
  • MSS clamping uses correct overhead per family (40 for v4, 60 for v6)

Stacked on #5706.

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

Summary by CodeRabbit

  • New Features

    • Added full IPv6 support across firewall filtering, routing, NAT (including DNAT) and MSS handling; firewall now manages IPv4 and IPv6 in parallel with mirrored NAT for IPv6 destinations and dual-stack rule management.
  • Tests

    • Expanded IPv6 test coverage and compatibility checks, including IP set creation, prefix/address calculations, protocol handling, and ip6tables interoperability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93a71fe5-b1d9-4727-b631-04c0c9cfec25

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds IPv6 support by introducing an address-family abstraction and parallel IPv6 router/ACL components; nftables constructs, payload offsets, protocol numbers, and set handling are parameterized by address family and rule operations dispatch to the appropriate family.

Changes

Cohort / File(s) Summary
Address Family Abstraction
client/firewall/nftables/addr_family_linux.go
New unexported addrFamily type with IPv4/IPv6 constants and protoNum/familyForAddr helpers to map protocol numbers and select family-specific offsets and types.
ACL Manager IPv6 Support
client/firewall/nftables/acl_linux.go
Added af addrFamily field; replaced IPv4-only IP/protocol conversions with family-aware helpers (ipToBytes, m.af.protoNum, offsets, m.af.setKeyType) and updated IP matching logic.
Firewall Manager IPv6 Routing
client/firewall/nftables/manager_linux.go
Added router6 and aclManager6 fields; Create/Init/Close/Flush/AllowNetbird/UpdateSet and rule operations now conditionally initialize and dispatch to v6 components and split v4/v6 updates.
Router Family-Aware Operations
client/firewall/nftables/router_linux.go
Router gains af field; NAT, payload offsets, set creation, prefix handling, MSS clamping, and rule construction use r.af; prefix-to-expression/set conversion moved onto router; removed IPv4-only assumptions.
Tests and Helpers
client/firewall/nftables/router_linux_test.go
Updated protocol expectations to use afIPv4.protoNum, added IPv6 work-table helpers and tests for IPv6 ipset creation, calculateLastIP, and convertPrefixesToSet for both families.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Manager as Firewall Manager
    participant IPv4 as IPv4 Router/ACL
    participant IPv6 as IPv6 Router/ACL
    participant nftables

    Client->>Manager: AddPeerFiltering(rule)
    alt rule is IPv4
        Manager->>IPv4: dispatch rule
        IPv4->>nftables: create/set IPv4 rules
    else rule is IPv6
        Manager->>IPv6: dispatch rule
        IPv6->>nftables: create/set IPv6 rules
    end

    Client->>Manager: UpdateSet(mixed prefixes)
    Manager->>Manager: split into v4/v6 batches
    Manager->>IPv4: update IPv4 routes/sets
    Manager->>IPv6: update IPv6 routes/sets
    IPv4->>nftables: apply IPv4 expressions
    IPv6->>nftables: apply IPv6 expressions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • mlsmaycon

"I hopped through bytes both small and wide,
Four and six now march side by side.
Sets and offsets snugly aligned,
Rules routed right, no rules left behind.
A rabbit cheers where packets glide! 🐇✨"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 change: adding IPv6 support to the dual-stack nftables manager.
Description check ✅ Passed The description includes a detailed summary of changes, issue tracking with full stack, appropriate checklist selections (feature enhancement and tests), CLA confirmation, and a valid docs PR URL.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client-ipv6-nftables

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 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 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.

@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/firewall/nftables/router_linux.go`:
- Around line 182-183: acceptFilterTableRules() and removeFilterTableRules()
currently create an iptables client with iptables.New() (defaulting to IPv4)
which breaks when r.af.tableFamily is nftables.TableFamilyIPv6; update both
functions to conditionally instantiate the iptables client using
iptables.NewWithProtocol(iptables.ProtocolIPv6) when r.af.tableFamily ==
nftables.TableFamilyIPv6 and fall back to iptables.New() (or
iptables.NewWithProtocol(iptables.ProtocolIPv4)) otherwise, then pass that
client into acceptFilterRulesIptables() so the correct IPv4/IPv6 protocol is
used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b6d8fa5-7ed3-4b3b-a8ed-21af1935e731

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7b3ee and e4857b4.

📒 Files selected for processing (5)
  • client/firewall/nftables/acl_linux.go
  • client/firewall/nftables/addr_family_linux.go
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/nftables/router_linux_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/firewall/nftables/acl_linux.go

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/firewall/nftables/manager_linux_test.go`:
- Around line 409-415: The test currently only checks for ip6tables-save but
later calls seedIp6tables() (which uses ip6tables) and also runs iptables-save
later, so expand the guard: before calling seedIp6tables() add exec.LookPath
checks for "ip6tables" and "iptables-save" (in addition to the existing
"ip6tables-save") and call t.Skipf with a clear message if any are missing;
reference the existing exec.LookPath check and the seedIp6tables function to
locate where to insert the extra checks.

In `@client/firewall/nftables/manager_linux.go`:
- Around line 385-389: The current early return on m.router6.Reset() prevents
cleanupNetbirdTables() and the final flush from running; instead, call
m.router6.Reset() and if it returns an error capture it (e.g., append to an
error variable or wrap it), but do not return immediately — continue to call
cleanupNetbirdTables() and the final flush/finalizer (the same cleanup path
executed when Reset succeeds), then after all teardown steps complete return the
collected error (or nil) so teardown completes even when router6.Reset()
partially fails; reference m.hasIPv6(), m.router6.Reset(),
cleanupNetbirdTables() and the final flush call to locate and update the logic.
- Around line 148-153: When m.initIPv6() fails during Init(), perform a
best-effort rollback of the already-programmed IPv4 state by calling the router
and ACL teardown/close methods before returning: invoke m.router.close() (or
m.router.teardown()/m.router.Destroy() if that’s the actual name) and
m.aclManager.close() (or m.aclManager.teardown()/m.aclManager.Destroy()), log
any errors from those calls but do not overwrite the original IPv6 init error,
then return the wrapped IPv6 error from Init(); this ensures the v4 table and
external accept rules set by m.router.init() and m.aclManager.init() are undone
on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4d50d73-c799-4762-845b-d31a38c35fd2

📥 Commits

Reviewing files that changed from the base of the PR and between e4857b4 and 44d16e8.

📒 Files selected for processing (3)
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/manager_linux_test.go
  • client/firewall/nftables/router_linux.go

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

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.

Please keep the Go convection. Vars -> struct -> at the end statuc functions

rules: make(map[string]*nftables.Rule),
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
wgIface: wgIface,
ipFwdState: ipfwdstate.NewIPForwardingState(),
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.

newRouter() always creates a fresh ipfwdstate.NewIPForwardingState().
As a result, router and router6 maintain separate counters.

When router6.AddDNATRule() is called, it increments router6.ipFwdState.
However, Manager.DisableRouting() only releases m.router.ipFwdState.

This means the router6 counter is never decremented, so IP forwarding is never fully disabled once a v6 DNAT rule has been added.


return nil
// rollbackInit performs best-effort cleanup of already-initialized state when Init fails partway through.
func (m *Manager) rollbackInit() {
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.

Missing route6.Reset()

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