Skip to content

feat(network): add IPv6 (dual-stack) support to firewall and policy/traffic-shaping - #952

Merged
brunodam merged 2 commits into
refactor-traffic-shaper-enginefrom
00945-ipv6-dual-stack-firewall-policy
Aug 1, 2026
Merged

feat(network): add IPv6 (dual-stack) support to firewall and policy/traffic-shaping#952
brunodam merged 2 commits into
refactor-traffic-shaper-enginefrom
00945-ipv6-dual-stack-firewall-policy

Conversation

@alex-au

@alex-au alex-au commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

Both nftables planes the network command family manages were IPv4-only. This makes them dual-stack: the v4 and v6 sets are always rendered, and the existing flags (--mgmt-cidrs, --blocked-cidrs, --pod-cidr, network policy --cidrs) accept mixed v4/v6 lists, routing each entry to the matching family's nft set. tc/HTB is address-family agnostic, so "IPv6 traffic shaping" is delivered by teaching the inet weaver classification rules to match IPv6 peers — no qdisc change.

Design decisions (from #945): both planes, always dual-stack, unified mixed-family flags, full parity.

Stacking note

Based on refactor-traffic-shaper-engine (PR #949, unmerged) — this PR targets that branch, not main. Rebase onto #949's merge target once it lands.

Host firewall (inet host)

  • mgmt_addrs6 / blocked_addrs6 (ipv6_addr) sets + parallel ip6 rules; new Table.PodCIDR6 and a v6 in-cluster rule.
  • ICMPv6 allowlist the default-drop input chain requires or IPv6 is dead: Neighbor Discovery (nd-neighbor-solicit/advert, nd-router-solicit/advert, hop-limit-255 guarded), MLD, and packet-too-big (the v6 PMTUD signal). ICMPv6 Redirect is not accepted (on-link MITM).
  • --pod-cidr is now a mixed v4/v6 list.

Workload policy / shaping (inet weaver)

  • Removed the explicit IPv6 rejection. Each policy gains a <name>6 companion set (policy.V6SetName), including the compound ipv6_addr . inet_service reply-stamp set. Membership routes by family across snapshot/restore and the add/remove/set/show verbs.
  • renderStampRule emits one rule per pod-CIDR family, so v6 classification activates once a v6 --pod-cidr is supplied (auto-detection resolves only the v4 pod CIDR today).
  • The traffic-shaper daemon reconciler diffs live membership merged from both family sets (otherwise v6 members would re-apply every tick), normalizes a bare v6 peer to /128, and brackets v6 compound endpoints.

Shared

Validation moves from sanity.ValidateIPv4CIDR to sanity.ValidateCIDR + a new sanity.CIDRIsIPv6 family classifier. Goldens gain a -update flag and a dual-stack fixture; docs/quickstart.md documents the mixed-family flags and the ICMPv6/NDP behavior.

Review guide

Code review checklist

  • internal/templates/files/network/network-host.nft.tmpl — NDP/MLD/packet-too-big accepts precede ct state established,related accept; NDP carries ip6 hoplimit 255; no nd-redirect accept rule.
  • internal/network/policy/render.gorenderStampRule returns one rule per present pod family; v6 rules reference @<name>6 and ip6 saddr/daddr; compound reply-stamp v6 uses ip6 daddr . tcp dport @<name>6.
  • internal/network/policy/manager.go — every membership path (snapshotMembership/restoreSet/Add/Remove/applySet/Show) touches both <name> and V6SetName(<name>); applySet full-replaces each family so a family with no members is cleared, not left stale.
  • internal/blocknode/shaper/policy_map.go computePolicyDeltas — live is merged from both b.policyName and V6SetName(b.policyName) before diffing.
  • internal/blocknode/shaper/reconciler.gohostCIDR bare v6 → /128; bucketize uses net.JoinHostPort (brackets v6).
  • internal/network/policy/render_weaver.gononEmptyStrings filter so a caller passing "" still triggers .nft pod-CIDR recovery.

Test commands

# Unit (native, no Linux-only deps in these packages):
go test ./internal/network/firewall/... ./internal/network/policy/... \
        ./internal/blocknode/shaper/... ./pkg/sanity/...
# Lint:
task lint
# Regenerate goldens after an intentional render change:
go test ./internal/network/firewall/... -run TestRender_GoldenStable -update
go test ./internal/network/policy/...   -run 'TestRender_GoldenMatchesBNInstallSet|TestRender_DualStackGolden' -update
# Full VM integration (real nft apply — REQUIRED before merge):
task vm:test:integration

Manual UAT (dual-stack host / VM)

  1. Host firewall — mixed allowlist + NDP:

    sudo solo-provisioner network firewall create \
      --mgmt-cidrs "203.0.113.0/24,2001:db8:a11::/48" \
      --blocked-cidrs "2001:db8:bad::/48" \
      --pod-cidr "10.244.0.0/16,2001:db8:c0de::/64" --force
    sudo nft list table inet host

    Expect: mgmt_addrs6/blocked_addrs6 sets populated; ip6 saddr @mgmt_addrs6 tcp dport 22 accept; the icmpv6 type { nd-* } ... hoplimit 255 accept, MLD, and packet-too-big rules present. SSH over IPv6 from an allowlisted source still connects; ping6 and a large-MTU v6 flow both work (PMTUD intact).

  2. Policy classification over IPv6:

    sudo solo-provisioner network policy create --name bn-publisher --stamp publisher \
      --ports 40840 --pod-cidr "10.244.0.0/16,2001:db8:c0de::/64" \
      --cidrs "10.1.0.1/32,2001:db8::1/128" --force
    sudo solo-provisioner network policy show --name bn-publisher
    sudo nft list table inet weaver

    Expect: live set @bn-publisher holds the v4 member, @bn-publisher6 the v6 member; the chain has both ip daddr 10.244.0.0/16 ip saddr @bn-publisher ... and ip6 daddr 2001:db8:c0de::/64 ip6 saddr @bn-publisher6 ... meta priority set 0x10010 accept. A v6 peer's traffic lands in the same HTB class as the v4 equivalent (tc -s class show dev $EGRESS).

  3. Reply-stamp compound over IPv6:

    sudo solo-provisioner network policy set --name bn-backfill --cidrs "[2001:db8::2]:443"
    sudo nft list set inet weaver bn-backfill6

    Expect: element 2001:db8::2 . 443 in the ipv6_addr . inet_service set.

Risks / rollback

  • Highest risk is an omitted or mis-keyworded ICMPv6 rule silently breaking IPv6 under the drop policy — the VM nft apply + UAT step 1 are the gate; unit goldens pin the rule text but not kernel acceptance.
  • v6 set-name scheme (<name>6) is single-sourced through policy.V6SetName; a divergence would misroute daemon reconciliation.
  • Rollback: revert this commit — the rendered tables are atomic-replace, so reverting the template + validators restores the prior IPv4-only ruleset on the next apply.

Related Issues

@alex-au
alex-au requested a review from a team as a code owner July 31, 2026 03:12
@alex-au
alex-au requested a review from tomzhenghedera July 31, 2026 03:12
@swirlds-automation

swirlds-automation commented Jul 31, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copilot AI left a comment

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.

Pull request overview

This PR extends the network command family’s nftables management to be dual-stack (IPv4 + IPv6) across both the host firewall (inet host) and workload policy/traffic-shaping classification (inet weaver), including v6-aware set membership routing and ICMPv6/NDP allowances required under a default-drop policy.

Changes:

  • Add family-aware CIDR classification and route mixed IPv4/IPv6 inputs into parallel ipv4_addr / ipv6_addr nft sets.
  • Update inet host rules to include v6 allow/block sets and ICMPv6 Neighbor Discovery/MLD/PMTUD allowances; extend parsing/rendering and goldens accordingly.
  • Update inet weaver policy rendering, parsing, and manager operations to maintain v4 + v6 companion sets (including compound ip:port reply-stamp sets), plus daemon reconciliation updates for v6 endpoints.

Reviewed changes

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

Show a summary per file
File Description
pkg/sanity/sanity.go Adds CIDRIsIPv6 helper for CIDR family classification.
pkg/sanity/sanity_cidr_test.go Unit tests for CIDRIsIPv6.
internal/workflows/steps/step_network_policy.go Adjusts install-time policy creation call to pass pod CIDR(s) as a slice.
internal/templates/files/network/network-host.nft.tmpl Adds ipv6 sets/rules and ICMPv6 NDP/MLD/PMTUD allowances under default drop.
internal/network/policy/validate_test.go Updates validation expectations to accept IPv6 and mixed-family inputs.
internal/network/policy/testdata/network-weaver.golden.nft Updates golden to include v6 companion sets/rules.
internal/network/policy/testdata/network-weaver-dualstack.golden.nft Adds dual-stack golden fixture.
internal/network/policy/render.go Renders v4+v6 rules (one per pod-CIDR family) and declares v6 companion sets.
internal/network/policy/render_weaver.go Updates RenderWeaverNft to accept variadic pod CIDRs and recover them from existing artifacts.
internal/network/policy/render_weaver_test.go Updates recovery test for plural pod CIDRs.
internal/network/policy/policy.go Removes IPv6 rejection; introduces V6SetName and family-partitioned element routing.
internal/network/policy/policy_test.go Adds -update golden regeneration and dual-stack golden test; updates manager call sites for pod CIDR slice.
internal/network/policy/parse.go Updates pod-CIDR extraction to recover both v4 and v6 CIDRs from rendered nft.
internal/network/policy/parse_test.go Tests dual-stack and v6-only pod-CIDR recovery.
internal/network/policy/manager.go Updates Create/Add/Remove/Set/Show/Delete and snapshot/restore to operate on both v4 and v6 sets.
internal/network/policy/manager_ops_test.go Adds tests ensuring membership routes by family and show surfaces both sets.
internal/network/policy/manager_apply_ports_test.go Updates manager create calls for pod CIDR slice.
internal/network/policy/manager_apply_membership_test.go Updates apply ordering expectations to include v6 set flush/replace per policy.
internal/network/policy/diff.go Extends compound element/canonicalization to support IPv6 and /128 host collapsing.
internal/network/policy/diff_test.go Tests bracketed IPv6 compound element acceptance and canonicalization.
internal/network/firewall/testdata/network-host.golden.nft Updates firewall golden for dual-stack set declarations and v6 rules.
internal/network/firewall/table.go Adds PodCIDR6 and switches CIDR validation to dual-stack ValidateCIDR.
internal/network/firewall/render.go Splits mixed CIDRs by family for v4/v6 set rendering and routes pod CIDRs by family.
internal/network/firewall/parse.go Parses v4/v6 sets and pod CIDRs back into the Table model.
internal/network/firewall/firewall_test.go Adds dual-stack fixture, -update goldens, and dual-stack render assertions.
internal/blocknode/shaper/reconciler.go Brackets IPv6 ip:port tokens via net.JoinHostPort; normalizes bare v6 host to /128.
internal/blocknode/shaper/reconciler_test.go Adds IPv6 bucketization/normalization tests.
internal/blocknode/shaper/policy_map.go Merges live membership from v4 and v6 sets before diffing to avoid perpetual v6 re-apply.
internal/blocknode/shaper/policy_map_test.go Tests merged live-set diff behavior for dual-stack desired membership.
docs/quickstart.md Documents mixed-family flags, dual-stack behavior, and required ICMPv6/NDP allowances.
cmd/cli/commands/network/policy/policy.go Changes --pod-cidr backing variable to []string.
cmd/cli/commands/network/policy/policy_test.go Resets updated flag state for tests.
cmd/cli/commands/network/policy/create.go Updates --pod-cidr to StringSlice, supports dual-stack input, and passes pod CIDR slice to Manager.
cmd/cli/commands/network/firewall/firewall.go Changes firewall --pod-cidr backing variable to []string.
cmd/cli/commands/network/firewall/create.go Routes mixed pod CIDRs into v4/v6 table fields and updates flag to StringSlice.

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

Comment thread internal/network/policy/manager.go
Comment thread pkg/sanity/sanity.go Outdated
@alex-au
alex-au force-pushed the 00945-ipv6-dual-stack-firewall-policy branch 2 times, most recently from d88bddb to 4d5ae31 Compare July 31, 2026 06:25
@alex-au
alex-au force-pushed the refactor-traffic-shaper-engine branch from 16a6b4a to 1b2de20 Compare July 31, 2026 06:35
@alex-au
alex-au force-pushed the 00945-ipv6-dual-stack-firewall-policy branch from 4d5ae31 to 79985dd Compare July 31, 2026 06:38
alex-au added 2 commits August 1, 2026 09:26
…ngle-source tc encoding (#955)

Signed-off-by: alex-au <alex.w.aus@gmail.com>
…raffic-shaping

Both nftables planes the `network` command family manages were IPv4-only.
Make them dual-stack, always rendering v4 and v6 rules, with the existing
flags accepting mixed v4/v6 lists routed to the matching family's set.

Host firewall (inet host):
- Add mgmt_addrs6/blocked_addrs6 (ipv6_addr) sets and parallel ip6 rules,
  plus a Table.PodCIDR6 companion and a v6 in-cluster rule.
- Add the ICMPv6 allowlist the default-drop input chain needs to keep IPv6
  working: Neighbor Discovery (NS/NA/RS/RA, hop-limit-255 guarded), MLD, and
  packet-too-big (the v6 PMTUD signal). ICMPv6 Redirect is deliberately not
  accepted. --pod-cidr now accepts a mixed v4/v6 list.

Workload policy / shaping (inet weaver):
- Drop the explicit IPv6 rejection; each policy gains a "<name>6" companion
  set (policy.V6SetName), including the compound ipv6_addr . inet_service
  reply-stamp set. Membership routes by family across snapshot/restore and the
  add/remove/set/show verbs. renderStampRule emits one rule per pod family, so
  v6 classification activates once a v6 --pod-cidr is supplied.
- The traffic-shaper daemon reconciler diffs live membership merged from both
  family sets (otherwise v6 members would re-apply every tick), normalizes a
  bare v6 peer to /128, and brackets v6 compound endpoints.

Validation moves from sanity.ValidateIPv4CIDR to sanity.ValidateCIDR plus a
new sanity.CIDRIsIPv6 family classifier. Goldens gain a -update flag and a
dual-stack fixture; quickstart documents the mixed-family flags and the
ICMPv6/NDP behavior.

Closes #945

Signed-off-by: alex-au <alex.w.aus@gmail.com>
@brunodam
brunodam force-pushed the 00945-ipv6-dual-stack-firewall-policy branch from 79985dd to 6540de8 Compare July 31, 2026 23:41
@alex-au
alex-au force-pushed the refactor-traffic-shaper-engine branch from 5c22714 to e0ce748 Compare July 31, 2026 23:41
@brunodam
brunodam merged commit 12bbafe into refactor-traffic-shaper-engine Aug 1, 2026
16 checks passed
@brunodam
brunodam deleted the 00945-ipv6-dual-stack-firewall-policy branch August 1, 2026 01:08
alex-au added a commit that referenced this pull request Aug 1, 2026
…raffic-shaping (#952)

Signed-off-by: alex-au <alex.w.aus@gmail.com>
brunodam pushed a commit that referenced this pull request Aug 1, 2026
…raffic-shaping (#952)

Signed-off-by: alex-au <alex.w.aus@gmail.com>
alex-au added a commit that referenced this pull request Aug 1, 2026
…raffic-shaping (#952)

Signed-off-by: alex-au <alex.w.aus@gmail.com>
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.

4 participants