Skip to content

[management, client] Add IPv6 overlay support#5631

Merged
lixmal merged 110 commits intomainfrom
proto-ipv6-overlay
May 7, 2026
Merged

[management, client] Add IPv6 overlay support#5631
lixmal merged 110 commits intomainfrom
proto-ipv6-overlay

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Mar 19, 2026

Describe your changes

This PR implements IPv6 overlay support in client and management.

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 IPv6 overlay network support for peer-to-peer connectivity
    • Added DisableIPv6 configuration option to disable IPv6 overlays
    • Extended peer status to display IPv6 addresses
    • Added IPv6 support for route selection and management
    • Extended DNS functionality with IPv6 PTR record support
    • Added IPv6 firewall rule support across all platforms
  • Improvements

    • Enhanced network configuration for dual-stack IPv4/IPv6 environments
    • Improved firewall rule handling with source prefix expansion
    • Better platform-specific IPv6 integration (Windows, Linux, macOS, iOS, Android)

@lixmal lixmal changed the title [management, shared] Add proto fields for IPv6 overlay and compact prefix encoding [management, client, shared] Add proto fields for IPv6 overlay and compact prefix encoding Mar 19, 2026
@lixmal lixmal changed the title [management, client, shared] Add proto fields for IPv6 overlay and compact prefix encoding [management, client] Add proto fields for IPv6 overlay and compact prefix encoding Mar 19, 2026
Comment thread shared/netiputil/compact.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.

🧹 Nitpick comments (1)
shared/netiputil/compact.go (1)

23-42: Consider validating prefix length bounds for robustness.

The decoded prefix length (last byte) is not validated against valid bounds. For IPv4, valid bits are 0-32; for IPv6, 0-128. If the input contains out-of-range values, netip.PrefixFrom returns an invalid prefix silently, requiring callers to check IsValid().

For a wire protocol decoder, explicit validation with a clear error message improves debuggability when malformed data is received.

🛡️ Proposed fix to validate prefix length
 func DecodePrefix(b []byte) (netip.Prefix, error) {
 	switch len(b) {
 	case 5:
 		var ip4 [4]byte
 		copy(ip4[:], b)
-		return netip.PrefixFrom(netip.AddrFrom4(ip4), int(b[len(b)-1])), nil
+		bits := int(b[len(b)-1])
+		if bits > 32 {
+			return netip.Prefix{}, fmt.Errorf("invalid IPv4 prefix length %d (max 32)", bits)
+		}
+		return netip.PrefixFrom(netip.AddrFrom4(ip4), bits), nil
 	case 17:
 		var ip6 [16]byte
 		copy(ip6[:], b)
 		addr := netip.AddrFrom16(ip6).Unmap()
 		bits := int(b[len(b)-1])
+		maxBits := 128
+		if addr.Is4() {
+			maxBits = 32
+		}
+		if bits > maxBits {
+			return netip.Prefix{}, fmt.Errorf("invalid prefix length %d (max %d)", bits, maxBits)
+		}
 		if addr.Is4() && bits > 32 {
 			bits = 32
 		}
 		return netip.PrefixFrom(addr, bits), nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/netiputil/compact.go` around lines 23 - 42, DecodePrefix does not
validate the prefix length byte and can produce an invalid netip.Prefix; update
DecodePrefix to read the trailing byte into bits and explicitly validate bounds:
for the 5-byte (IPv4) case ensure 0 <= bits <= 32 (return a clear fmt.Errorf if
not), and for the 17-byte (IPv6) case ensure 0 <= bits <= 128 (return error if
out of range); keep the existing addr.Unmap() and the special case that an
IPv6-mapped IPv4 address should not claim >32 bits (either validate before or
after Unmap and return an error if inconsistent), and only call netip.PrefixFrom
when bits are within the validated range so callers never receive silently
invalid prefixes from DecodePrefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@shared/netiputil/compact.go`:
- Around line 23-42: DecodePrefix does not validate the prefix length byte and
can produce an invalid netip.Prefix; update DecodePrefix to read the trailing
byte into bits and explicitly validate bounds: for the 5-byte (IPv4) case ensure
0 <= bits <= 32 (return a clear fmt.Errorf if not), and for the 17-byte (IPv6)
case ensure 0 <= bits <= 128 (return error if out of range); keep the existing
addr.Unmap() and the special case that an IPv6-mapped IPv4 address should not
claim >32 bits (either validate before or after Unmap and return an error if
inconsistent), and only call netip.PrefixFrom when bits are within the validated
range so callers never receive silently invalid prefixes from DecodePrefix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c0facdd-fccf-46f0-b534-18bb5d96c612

📥 Commits

Reviewing files that changed from the base of the PR and between 12cffdb and d7dbe40.

📒 Files selected for processing (1)
  • shared/netiputil/compact.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.

🧹 Nitpick comments (1)
shared/netiputil/compact.go (1)

16-21: Consider validating input prefix.

The implementation is correct for valid prefixes. However, if an invalid/zero netip.Prefix is passed, p.Bits() returns -1 (becomes 255 as byte) and a zero Addr produces a 1-byte result. Both would fail on decode, so it's fail-safe, but explicit validation could provide clearer error messages at the source.

💡 Optional: Add validation for invalid prefixes
 // EncodePrefix encodes a netip.Prefix into compact bytes.
 // The address is always unmapped before encoding.
-func EncodePrefix(p netip.Prefix) []byte {
+func EncodePrefix(p netip.Prefix) ([]byte, error) {
+	if !p.IsValid() {
+		return nil, fmt.Errorf("invalid prefix")
+	}
 	addr := p.Addr().Unmap()
-	return append(addr.AsSlice(), byte(p.Bits()))
+	return append(addr.AsSlice(), byte(p.Bits())), nil
 }

Note: This would require updating callers to handle the error. Given the current usage context where callers likely pass valid prefixes, keeping the current signature may be acceptable.

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

In `@shared/netiputil/compact.go` around lines 16 - 21, Validate the input prefix
in EncodePrefix by checking netip.Prefix validity and bit length before
encoding: change EncodePrefix(p netip.Prefix) to return ([]byte, error), verify
p.IsValid() and p.Bits() >= 0 (or other appropriate checks) and return a
descriptive error when invalid instead of producing a malformed byte slice;
update all callers to handle the error accordingly so decoding won't receive
impossible values like 255 bits or a one-byte address.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@shared/netiputil/compact.go`:
- Around line 16-21: Validate the input prefix in EncodePrefix by checking
netip.Prefix validity and bit length before encoding: change EncodePrefix(p
netip.Prefix) to return ([]byte, error), verify p.IsValid() and p.Bits() >= 0
(or other appropriate checks) and return a descriptive error when invalid
instead of producing a malformed byte slice; update all callers to handle the
error accordingly so decoding won't receive impossible values like 255 bits or a
one-byte address.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58f63604-5572-410f-90a1-859c6db3bd11

📥 Commits

Reviewing files that changed from the base of the PR and between d7dbe40 and 0eaa9ad.

📒 Files selected for processing (2)
  • shared/netiputil/compact.go
  • shared/netiputil/compact_test.go
✅ Files skipped from review due to trivial changes (1)
  • shared/netiputil/compact_test.go

pascal-fischer
pascal-fischer previously approved these changes Mar 19, 2026
@lixmal lixmal force-pushed the proto-ipv6-overlay branch from b7be56f to e2f7748 Compare March 22, 2026 06:16
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/firewall/uspfilter/filter.go (1)

1533-1574: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delete blockRules before the netstack/local-forwarding early return.

EnableRouting() now persists both temporary block rules, but DisableRouting() still returns early in netstack+local-forwarding mode before this cleanup runs. In that mode, routing is marked disabled while the overlay drop rules remain installed, so routed traffic keeps getting blocked until the manager is torn down.

Suggested shape
 func (m *Manager) DisableRouting() error {
 	m.mutex.Lock()
 	defer m.mutex.Unlock()

 	fwder := m.forwarder.Load()
 	if fwder == nil {
 		return nil
 	}

 	m.routingEnabled.Store(false)
 	m.nativeRouter.Store(false)
+
+	var merr *multierror.Error
+	for _, rule := range m.blockRules {
+		if err := m.deleteRouteRule(rule); err != nil {
+			merr = multierror.Append(merr, fmt.Errorf("delete block rule: %w", err))
+		}
+	}
+	m.blockRules = nil

 	// don't stop forwarder if in use by netstack
 	if m.netstack && m.localForwarding {
-		return nil
+		return nberrors.FormatErrorOrNil(merr)
 	}

 	fwder.Stop()
 	m.forwarder.Store(nil)

 	log.Debug("forwarder stopped")
-
-	var merr *multierror.Error
-	for _, rule := range m.blockRules {
-		if err := m.deleteRouteRule(rule); err != nil {
-			merr = multierror.Append(merr, fmt.Errorf("delete block rule: %w", err))
-		}
-	}
-	m.blockRules = nil

 	return nberrors.FormatErrorOrNil(merr)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/uspfilter/filter.go` around lines 1533 - 1574, DisableRouting
currently returns early when m.netstack && m.localForwarding without removing
persisted overlay drop rules, leaving m.blockRules in place and blocking routed
traffic; fix by moving the loop that iterates m.blockRules and calls
m.deleteRouteRule(rule) (and clears m.blockRules) to occur before the early
return so rules are always deleted and m.blockRules set to nil even when
netstack+localForwarding is true, while keeping the existing locking, multierror
handling (multierror.Append) and avoiding stopping the forwarder when the
early-return condition holds.
🤖 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/uspfilter/filter.go`:
- Around line 244-255: The IPv6 parser (parser6 created via
gopacket.NewDecodingLayerParser) ignores IPv6 extension headers so packets stop
decoding before TCP/UDP/ICMP and get dropped; update the parser6 initialization
in filter.go to include the IPv6 extension header decoding layers (e.g.,
HopByHopOptions, IPv6Routing, DestinationOptions, Fragment, Authentication/ESP
decoders as available in gopacket) so the decoding chain advances to &d.ip6,
&d.tcp, &d.udp, &d.icmp6; alternatively (or additionally) update isValidPacket()
to explicitly walk the IPv6 extension header chain from the IPv6 header and
locate the next-header/tcp/udp/icmp payload before deciding to drop, ensuring
forwarder.parseICMPv6 and the local/routed handlers receive packets with
extension headers.

---

Outside diff comments:
In `@client/firewall/uspfilter/filter.go`:
- Around line 1533-1574: DisableRouting currently returns early when m.netstack
&& m.localForwarding without removing persisted overlay drop rules, leaving
m.blockRules in place and blocking routed traffic; fix by moving the loop that
iterates m.blockRules and calls m.deleteRouteRule(rule) (and clears
m.blockRules) to occur before the early return so rules are always deleted and
m.blockRules set to nil even when netstack+localForwarding is true, while
keeping the existing locking, multierror handling (multierror.Append) and
avoiding stopping the forwarder when the early-return condition holds.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5d5bd4b-d012-4c5c-879d-f039f8ba1710

📥 Commits

Reviewing files that changed from the base of the PR and between adb5df2 and 5ba9882.

📒 Files selected for processing (4)
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/uspfilter/filter.go
  • client/firewall/uspfilter/forwarder/forwarder.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go

Comment thread client/firewall/uspfilter/filter.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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/internal/routemanager/server/server.go (1)

37-88: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Refresh retained routes when useNewDNSRoute changes.

UpdateRoutes only reprograms routes that are added or removed in this call. If the DNS-route mode flips while a route stays in r.routes, its NAT rule keeps the old shape, but later removals/CleanUp will use the new flag and miss that entry. Please track the mode per route or force a remove+re-add for existing routes when the flag changes.

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

In `@client/internal/routemanager/server/server.go` around lines 37 - 88,
UpdateRoutes currently leaves existing routes unchanged when r.useNewDNSRoute
flips, so NAT rules keep the old shape and later removals (or CleanUp) use the
new flag and miss them. Fix: detect change by comparing prevUseNewDNSRoute and
useNewDNSRoute, and for any route still present in routesMap but already in
r.routes, call removeFromServerNetwork(oldRoute, prevUseNewDNSRoute) to tear
down the old rule, then later (after setting r.useNewDNSRoute = useNewDNSRoute
and enabling routing) call addToServerNetwork(newRoute, useNewDNSRoute) to
reprogram it; ensure r.routes entries are replaced with the refreshed newRoute.
Use the existing symbols UpdateRoutes, r.useNewDNSRoute, r.routes,
removeFromServerNetwork, addToServerNetwork (or alternatively store per-route
mode if preferred).
♻️ Duplicate comments (1)
client/iface/wgproxy/bind/proxy.go (1)

223-233: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Apply the same port-range guard here before casting to uint16.

RedirectAs can still pass a malformed net.UDPAddr, and this helper will silently wrap negative or out-of-range ports into the wrong endpoint.

Suggested fix
 func addrToEndpoint(addr *net.UDPAddr) (*bind.Endpoint, error) {
 	if addr == nil {
 		return nil, fmt.Errorf("invalid address")
 	}
+	if addr.Port < 0 || addr.Port > 65535 {
+		return nil, fmt.Errorf("invalid UDP port: %d", addr.Port)
+	}
 	ip, ok := netip.AddrFromSlice(addr.IP)
 	if !ok {
 		return nil, fmt.Errorf("convert %s to netip.Addr", addr)
 	}
 
 	addrPort := netip.AddrPortFrom(ip.Unmap(), uint16(addr.Port))
 	return &bind.Endpoint{AddrPort: addrPort}, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/iface/wgproxy/bind/proxy.go` around lines 223 - 233, In addrToEndpoint
ensure the UDP port is validated before casting to uint16: check that addr.Port
is within 0..65535 and return an error if out of range or negative, then proceed
to create netip.AddrPortFrom using the validated port; update any call sites
(e.g., RedirectAs flows that pass net.UDPAddr) to rely on this guard so
malformed addresses no longer produce wrapped/incorrect endpoints.
🧹 Nitpick comments (2)
client/firewall/uspfilter/filter_routeacl_test.go (1)

167-176: ⚡ Quick win

Add a dual-stack idempotency variant for blockInvalidRouted.

This test only exercises IPv4 (routeRules == 1, first returned rule ID). With IPv6 overlay support, add a sibling case with IPv6Net set and assert both returned rule IDs are stable across repeated calls and total rule count is 2.

📌 Suggested test shape
+func TestBlockInvalidRoutedIdempotentDualStack(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	dev := mocks.NewMockDevice(ctrl)
+	dev.EXPECT().MTU().Return(1500, nil).AnyTimes()
+
+	wgNet4 := netip.MustParsePrefix("100.64.0.1/16")
+	wgNet6 := netip.MustParsePrefix("fd00::1/64")
+
+	ifaceMock := &IFaceMock{
+		SetFilterFunc: func(device.PacketFilter) error { return nil },
+		AddressFunc: func() wgaddr.Address {
+			return wgaddr.Address{
+				IP:      wgNet4.Addr(),
+				Network: wgNet4,
+				IPv6:    wgNet6.Addr(),
+				IPv6Net: wgNet6,
+			}
+		},
+		GetDeviceFunc:   func() *device.FilteredDevice { return &device.FilteredDevice{Device: dev} },
+		GetWGDeviceFunc: func() *wgdevice.Device { return &wgdevice.Device{} },
+	}
+
+	manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
+	require.NoError(t, err)
+	t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
+
+	rules1, err := manager.blockInvalidRouted(ifaceMock)
+	require.NoError(t, err)
+	require.Len(t, rules1, 2)
+
+	rules2, err := manager.blockInvalidRouted(ifaceMock)
+	require.NoError(t, err)
+	require.Len(t, rules2, 2)
+
+	assert.Equal(t, rules1[0].ID(), rules2[0].ID())
+	assert.Equal(t, rules1[1].ID(), rules2[1].ID())
+
+	manager.mutex.RLock()
+	ruleCount := len(manager.routeRules)
+	manager.mutex.RUnlock()
+	assert.Equal(t, 2, ruleCount)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/firewall/uspfilter/filter_routeacl_test.go` around lines 167 - 176,
Add a dual-stack idempotency variant for blockInvalidRouted: duplicate the
existing case that uses wgNet (netip.MustParsePrefix("100.64.0.1/16")) and
create a sibling case that also sets an IPv6 prefix (e.g. IPv6Net :=
netip.MustParsePrefix("fd00::1/64")) on the IFaceMock AddressFunc or IPv6Net
field so the code exercises IPv6 overlay; call blockInvalidRouted twice and
assert the returned rule IDs from the first and second call are identical and
that the total number of route rules created is 2 (i.e. both IPv4 and IPv6 rules
exist); reuse the same IFaceMock, SetFilterFunc, AddressFunc and wgaddr.Address
setup so the test mirrors the IPv4 path and checks idempotency and stable rule
IDs for both families.
client/firewall/nftables/router_linux.go (1)

1851-1907: ⚡ Quick win

Add explicit address-family validation in DNAT entrypoints.

AddInboundDNAT and AddOutputDNAT build expressions from both r.af and localAddr. If caller input ever mismatches router family, rules are built inconsistently and fail later at flush-time. Fail fast with a clear error.

🛠️ Suggested patch
+func (r *router) validateLocalAddrFamily(addr netip.Addr) error {
+	isIPv4Router := r.af.tableFamily == nftables.TableFamilyIPv4
+	if addr.Is4() != isIPv4Router {
+		return fmt.Errorf("address family mismatch: router=%s, addr=%s", familyName(r.af.tableFamily), addr)
+	}
+	return nil
+}
+
 func (r *router) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
+	if err := r.validateLocalAddrFamily(localAddr); err != nil {
+		return err
+	}
 	ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)
 ...
 func (r *router) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
+	if err := r.validateLocalAddrFamily(localAddr); err != nil {
+		return err
+	}
 	ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)

Also applies to: 1982-2036

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

In `@client/firewall/nftables/router_linux.go` around lines 1851 - 1907, Add
explicit validation that the provided localAddr matches the router's address
family before building nft expressions in AddInboundDNAT (and likewise in
AddOutputDNAT): check r.af (or r.af.tableFamily / address-family enum) against
localAddr.Is6() (or netip.Addr being v4) and return a clear error if they
mismatch so callers fail fast instead of creating invalid rules that later fail
at flush-time; update both AddInboundDNAT and AddOutputDNAT to perform this
check near the start (before using r.af or calling applyPrefix) and return a
descriptive error referencing the expected family and the provided localAddr.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@client/internal/routemanager/server/server.go`:
- Around line 37-88: UpdateRoutes currently leaves existing routes unchanged
when r.useNewDNSRoute flips, so NAT rules keep the old shape and later removals
(or CleanUp) use the new flag and miss them. Fix: detect change by comparing
prevUseNewDNSRoute and useNewDNSRoute, and for any route still present in
routesMap but already in r.routes, call removeFromServerNetwork(oldRoute,
prevUseNewDNSRoute) to tear down the old rule, then later (after setting
r.useNewDNSRoute = useNewDNSRoute and enabling routing) call
addToServerNetwork(newRoute, useNewDNSRoute) to reprogram it; ensure r.routes
entries are replaced with the refreshed newRoute. Use the existing symbols
UpdateRoutes, r.useNewDNSRoute, r.routes, removeFromServerNetwork,
addToServerNetwork (or alternatively store per-route mode if preferred).

---

Duplicate comments:
In `@client/iface/wgproxy/bind/proxy.go`:
- Around line 223-233: In addrToEndpoint ensure the UDP port is validated before
casting to uint16: check that addr.Port is within 0..65535 and return an error
if out of range or negative, then proceed to create netip.AddrPortFrom using the
validated port; update any call sites (e.g., RedirectAs flows that pass
net.UDPAddr) to rely on this guard so malformed addresses no longer produce
wrapped/incorrect endpoints.

---

Nitpick comments:
In `@client/firewall/nftables/router_linux.go`:
- Around line 1851-1907: Add explicit validation that the provided localAddr
matches the router's address family before building nft expressions in
AddInboundDNAT (and likewise in AddOutputDNAT): check r.af (or r.af.tableFamily
/ address-family enum) against localAddr.Is6() (or netip.Addr being v4) and
return a clear error if they mismatch so callers fail fast instead of creating
invalid rules that later fail at flush-time; update both AddInboundDNAT and
AddOutputDNAT to perform this check near the start (before using r.af or calling
applyPrefix) and return a descriptive error referencing the expected family and
the provided localAddr.

In `@client/firewall/uspfilter/filter_routeacl_test.go`:
- Around line 167-176: Add a dual-stack idempotency variant for
blockInvalidRouted: duplicate the existing case that uses wgNet
(netip.MustParsePrefix("100.64.0.1/16")) and create a sibling case that also
sets an IPv6 prefix (e.g. IPv6Net := netip.MustParsePrefix("fd00::1/64")) on the
IFaceMock AddressFunc or IPv6Net field so the code exercises IPv6 overlay; call
blockInvalidRouted twice and assert the returned rule IDs from the first and
second call are identical and that the total number of route rules created is 2
(i.e. both IPv4 and IPv6 rules exist); reuse the same IFaceMock, SetFilterFunc,
AddressFunc and wgaddr.Address setup so the test mirrors the IPv4 path and
checks idempotency and stable rule IDs for both families.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24c8e2d3-42fb-453d-832d-026ee7e686bd

📥 Commits

Reviewing files that changed from the base of the PR and between adb5df2 and 2cd11eb.

📒 Files selected for processing (10)
  • client/firewall/nftables/manager_linux.go
  • client/firewall/nftables/router_linux.go
  • client/firewall/uspfilter/filter.go
  • client/firewall/uspfilter/filter_routeacl_test.go
  • client/firewall/uspfilter/forwarder/forwarder.go
  • client/firewall/uspfilter/forwarder/forwarder_test.go
  • client/iface/wgproxy/bind/proxy.go
  • client/internal/dns/local/local.go
  • client/internal/dns/server.go
  • client/internal/routemanager/server/server.go
✅ Files skipped from review due to trivial changes (4)
  • client/internal/dns/local/local.go
  • client/internal/dns/server.go
  • client/firewall/uspfilter/forwarder/forwarder_test.go
  • client/firewall/nftables/manager_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.

♻️ Duplicate comments (1)
client/internal/debug/debug.go (1)

1421-1430: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SourcePrefixes anonymization is not fail-closed, unlike AddressV6.

The AddressV6 path above clears the field on decode/encode failure (lines 1299-1312), but here the loop uses continue on decode error and silently leaves the entry untouched on encode error. In both failure cases the original raw SourcePrefixes[i] bytes survive and are later emitted as base64 in network_map.json by protojson, so --anonymize can still leak real prefixes on malformed/unexpected input. Mirror the fail-closed handling used for AddressV6 for consistency.

🔒 Suggested fail-closed handling
 	for i, raw := range rule.GetSourcePrefixes() {
 		p, err := netiputil.DecodePrefix(raw)
 		if err != nil {
-			continue
+			rule.SourcePrefixes[i] = nil
+			continue
 		}
 		anonAddr := anonymizer.AnonymizeIP(p.Addr())
 		if b, err := netiputil.EncodePrefix(netip.PrefixFrom(anonAddr, p.Bits())); err == nil {
 			rule.SourcePrefixes[i] = b
+		} else {
+			rule.SourcePrefixes[i] = nil
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/internal/debug/debug.go` around lines 1421 - 1430, Loop over
rule.GetSourcePrefixes() must be fail-closed like AddressV6: on any
netiputil.DecodePrefix or netiputil.EncodePrefix error you should clear the
stored entry instead of leaving the original bytes. Modify the block that uses
netiputil.DecodePrefix, anonymizer.AnonymizeIP and netiputil.EncodePrefix so
that when DecodePrefix fails or when EncodePrefix returns an error you set
rule.SourcePrefixes[i] to nil/empty (same clearing behavior used for AddressV6),
and only assign rule.SourcePrefixes[i] = b when encode succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@client/internal/debug/debug.go`:
- Around line 1421-1430: Loop over rule.GetSourcePrefixes() must be fail-closed
like AddressV6: on any netiputil.DecodePrefix or netiputil.EncodePrefix error
you should clear the stored entry instead of leaving the original bytes. Modify
the block that uses netiputil.DecodePrefix, anonymizer.AnonymizeIP and
netiputil.EncodePrefix so that when DecodePrefix fails or when EncodePrefix
returns an error you set rule.SourcePrefixes[i] to nil/empty (same clearing
behavior used for AddressV6), and only assign rule.SourcePrefixes[i] = b when
encode succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bfe4907c-e11f-44d0-8fcc-239df22a9946

📥 Commits

Reviewing files that changed from the base of the PR and between 2d36ee6 and 3722dc6.

📒 Files selected for processing (2)
  • client/internal/debug/debug.go
  • client/internal/debug/debug_test.go

@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented May 6, 2026

@coderabbitai clean up old comments (NOT review threads) and only keep the latest one

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all issue comments (not review comments) on PR `#5631` by coderabbitai[bot]
gh api repos/netbirdio/netbird/issues/5631/comments --paginate \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, created_at: .created_at, body_preview: .body[0:100]}'

Length of output: 409


🏁 Script executed:

#!/bin/bash
# Delete the older comment (walkthrough summary), keep the latest one
gh api repos/netbirdio/netbird/issues/comments/4089898938 -X DELETE
echo "Deleted comment 4089898938 (exit code: $?)"

Length of output: 42


@lixmal Done! I've deleted the older auto-generated walkthrough/summary comment (from 2026-03-19) and kept only the latest one (the "Actions performed" reply from 2026-04-10).

@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented May 6, 2026

@coderabbitai also clean up old comments like these: Actionable comments posted: 7, keep the latest only

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all issue comments by coderabbitai[bot] on PR `#5631` that contain "Actionable comments posted"
gh api repos/netbirdio/netbird/issues/5631/comments --paginate \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | select(.body | contains("Actionable comments posted")) | {id: .id, created_at: .created_at, body_preview: .body[0:150]}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# List all PR review comments by coderabbitai[bot] - these are the summary comments posted as regular PR comments
# Try pulls comments endpoint
gh api repos/netbirdio/netbird/pulls/5631/comments --paginate \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | select(.body | contains("Actionable comments posted")) | {id: .id, created_at: .created_at, body_preview: .body[0:150]}' 2>/dev/null | head -50

echo "---"
# Also list all issue comments to see what exists
gh api repos/netbirdio/netbird/issues/5631/comments --paginate \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, created_at: .created_at, body_preview: .body[0:150]}'

Length of output: 545


🏁 Script executed:

#!/bin/bash
# List all PR reviews by coderabbitai[bot] that contain "Actionable comments posted"
gh api repos/netbirdio/netbird/pulls/5631/reviews --paginate \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | select(.body | contains("Actionable comments posted")) | {id: .id, submitted_at: .submitted_at, body_preview: .body[0:200]}'

Length of output: 3061


🏁 Script executed:

#!/bin/bash
# GitHub doesn't allow deleting PR reviews, but we can update/dismiss them.
# Try to update the body of older reviews to clear them out.
OLD_REVIEW_IDS=(3974901569 4035824682 4069836254 4069862438 4073058555 4195230770 4218981913 4219146528)

for id in "${OLD_REVIEW_IDS[@]}"; do
  echo "Updating review $id..."
  result=$(gh api repos/netbirdio/netbird/pulls/5631/reviews/$id \
    -X PUT \
    -f body="" \
    --jq '.id' 2>&1)
  echo "Result for $id: $result"
done

Length of output: 512


@lixmal Done! I've cleared the body of the 8 older "Actionable comments posted" review summaries, keeping only the latest one (from 2026-05-04T10:39:31Z). Note that GitHub's API doesn't allow deleting PR reviews outright, so the older reviews still exist but their content has been emptied.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5c5d885-eca0-4ebf-aa2b-ea5a8f3f6fb8

📥 Commits

Reviewing files that changed from the base of the PR and between 3722dc6 and 4cd2651.

📒 Files selected for processing (2)
  • client/ios/NetBirdSDK/client.go
  • util/capture/text.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/ios/NetBirdSDK/client.go

📝 Walkthrough

Walkthrough

This PR comprehensively adds IPv6 overlay support across NetBird by introducing IPv6 fields to peer/account/configuration models, extending firewall managers with dual-stack rule dispatch, updating engine/interface handling for dual-stack addresses, adding DNS IPv6 reverse-zone support, and enabling IPv6-aware routing and peer management throughout the codebase.

Changes

IPv6 Overlay Support & Core Infrastructure

Layer / File(s) Summary
Proto & Data Shape
shared/management/proto/management.proto, shared/netiputil/compact.go, client/iface/wgaddr/address.go, management/server/peer/peer.go
Added PeerConfig.address_v6 (compact bytes), FirewallRule.sourcePrefixes (compact v4/v6), PeerCapability enum with IPv6Overlay and SourcePrefixes, new netiputil package for compact IPv6 prefix encoding/decoding, and wgaddr.Address extended with IPv6/IPv6Net fields plus helpers (HasIPv6(), SetIPv6FromCompact(), IPv6Prefix()). Peer model gains IPv6 netip.Addr, Flags.DisableIPv6, PeerSystemMeta.Capabilities, and capability helpers (SupportsIPv6(), SupportsSourcePrefixes()).
Proto & Client Daemon
client/proto/daemon.proto
Added disable_ipv6 fields to LoginRequest/SetConfigRequest/GetConfigResponse, and ipv6 string fields to PeerState/LocalPeerState for status reporting.
Account & Settings
management/server/account.go, management/server/account/manager.go, management/server/peer.go
New UpdatePeerIPv6 method on manager, IPv6 subnet allocation in AddPeer via types.AllocateRandomPeerIPv6, UpdateAccountSettings now detects IPv6 changes and reconciles peer IPv6 via updatePeerIPv6Addresses; new Settings.IPv6EnabledGroups field controls which groups' peers get IPv6.
Group & User Management
management/server/group.go, management/server/user.go
UpdateGroup/DeleteGroups/GroupAddPeer/GroupDeletePeer now call reconcileIPv6ForGroupChanges to update peer IPv6 when group membership changes. User group propagation via propagateUserGroupMemberships now reconciles IPv6 for affected groups.
Store & Persistence
management/server/store/sql_store.go, management/server/store/store.go
GetTakenIPs now returns []netip.Addr; peers and accounts persist meta_capabilities, ipv6, network_net_v6, settings_network_range_v6, settings_ipv6_enabled_groups via new SQL columns/unmarshalling. Added UpdateAccountNetworkV6 method to persist IPv6 network updates.
HTTP API & OpenAPI
shared/management/http/api/openapi.yml, shared/management/http/api/types.gen.go, management/server/http/handlers/peers/peers_handler.go, management/server/http/handlers/accounts/accounts_handler.go
Peer schemas and requests now include ipv6 fields; AccountSettings gains network_range_v6 and ipv6_enabled_groups; peer update handler supports UpdatePeerIPv6 via new endpoint path; account settings validation enforces IPv6 range size and enabled-group existence.

Firewall & Network Filtering (Dual-Stack)

Layer / File(s) Summary
ACL & Rule Management
client/internal/acl/manager.go, client/firewall/iptables/acl_linux.go, client/firewall/nftables/acl_linux.go, client/firewall/nftables/addr_family_linux.go
ACL managers now accumulate errors across rules via multierror and continue applying rather than failing on first error; iptables ACL adds IPv6 mode flag and suffixes ipset names with -v6 for v4/v6 separation; nftables ACL generalizes to address-family-aware logic via new addrFamily struct capturing protocol offsets and ICMP variants.
Routing & Filtering
client/firewall/iptables/manager_linux.go, client/firewall/iptables/router_linux.go, client/firewall/nftables/manager_linux.go, client/firewall/nftables/router_linux.go
Both iptables and nftables managers conditionally initialize IPv6 router/ACL components when peer has IPv6; rule-dispatch methods select v4/v6 managers based on rule IP family; external-chain monitor added for nftables to reconcile passthrough accept rules; DNAT port parameters renamed (originalPort/translatedPort); MSS clamping uses family-specific TCP header sizes.
Packet Filtering & Forwarding
client/firewall/uspfilter/filter.go, client/firewall/uspfilter/forwarder/forwarder.go, client/firewall/uspfilter/forwarder/icmp.go, client/firewall/uspfilter/nat.go, client/firewall/uspfilter/conntrack/icmp.go
blockInvalidRouted returns rule slices instead of single rule; separate IPv4/IPv6 MSS clamp values; decodePacket dispatcher selects IPv4/IPv6 parsers; ICMPv6 echo/error handling added with extension-header walking; ICMP to ICMPv6 protocol mapping; NAT packet rewriting extended to IPv6 via rewriteIPv6 helper; conntrack/flow event protocol derived dynamically from address family.
Firewall Manager Interface
client/firewall/manager/firewall.go, client/firewall/manager/routerpair.go
New ErrIPv6NotInitialized sentinel; DNAT parameter names updated; RouterPair gains Dynamic flag and new ToV6NatPair helper for IPv6 NAT mirroring.

Engine & Interface Management

Layer / File(s) Summary
WireGuard Interface
client/iface/wgaddr/address.go, client/iface/iface.go, client/iface/device/device_darwin.go, client/iface/device/device_windows.go, client/iface/device/device_linux.go, client/iface/netstack/tun.go, client/iface/device/device_kernel_unix.go, client/iface/device/device_android.go
WGIFaceOpts.Address and UpdateAddr retyped from string to wgaddr.Address; device adapters conditionally configure IPv6 (ConfigureInterface(address, addressV6, ...)); Linux tun device attempts IPv6 assignment with graceful v6-clear fallback on error; Windows assigns dual-stack prefixes; netstack TUN accepts address slice instead of single IP; MTU setup includes IPv6 when present.
Engine Configuration
client/internal/engine.go
EngineConfig.WgAddr retyped to wgaddr.Address; new DisableIPv6 flag propagated to system info; peer IP comparisons and updates use netip.Addr equality; IPv6 change detection via hasIPv6Changed(); DNS config generation accepts wgaddr.Address for dual-stack reverse zones; allowed-IP filtering strips IPv6 when interface lacks IPv6; offline peers populate separate IPv6 state; Engine.GetWgV6Addr() method added.
Interface Initialization
client/iface/iface_new_*.go (multiple platform files)
Removed address parsing from NewWGIFace signatures; platform-specific constructors now pass wgaddr.Address directly to device creation without intermediate string parsing.

Client Configuration & CLI

Layer / File(s) Summary
Config Persistence
client/internal/profilemanager/config.go, client/embed/embed.go, client/cmd/system.go, client/cmd/up.go, client/cmd/status.go, client/android/preferences.go, client/ios/NetBirdSDK/preferences.go
DisableIPv6 field added to Config/ConfigInput with getter/setter methods on Android and iOS; CLI system flag --disable-ipv6 wired to global variable; --ipv6 status flag added to print local IPv6; up command conditionally propagates DisableIPv6 into SetConfigRequest/LoginRequest/ProfileManager.ConfigInput when flag changed.
Server Configuration
client/server/server.go
SetConfig copies DisableIpv6 into config; GetConfig returns DisableIPv6 in response.
Status & Notifications
client/internal/peer/status.go, client/internal/listener/network_change.go, client/android/peer_notifier.go, client/ios/NetBirdSDK/peer_notifier.go, client/status/status.go
Peer/local status states gain IPv6 string fields; NetworkChangeListener interface gains SetInterfaceIPv6(string) method; status outputs include IPv6 for filtering/display; AddPeer signature extended with ipv6 parameter.
SSH & Port Forwarding
client/ssh/config/manager.go, client/ssh/server/server.go, client/ssh/proxy/proxy.go, client/ssh/server/port_forwarding.go
PeerSSHInfo gains IPv6 netip.Addr and uses netip.Addr for both IPs; host-pattern generation includes both IPv4 and IPv6 when valid; SSH server adds AddListener(ctx, addr) for binding extra sockets; DNAT rules support IPv6; port-forwarding formatting uses net.JoinHostPort for IPv6-safe construction.

DNS & Routing

Layer / File(s) Summary
DNS Configuration
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_*.go, client/internal/dns/server_test.go, client/internal/dns_test.go
PTR record generation now supports IPv6 via ip6.arpa reverse zones; generateReverseZoneName handles IPv6; collectPTRRecords includes AAAA records; DNS server selection and forwarding use address-family awareness; upstream DNS client creation accepts interface object for dual-stack bind selection; new test coverage for IPv6 PTR/reverse-zone behavior.
Dynamic Routes
client/internal/routemanager/dynamic/route.go, client/internal/routemanager/dynamic/route_ios.go
Route resolver endpoint represented as netip.AddrPort instead of string; dual DNS queries (A then AAAA) attempted in sequence with error fallback.
Route Management & System
client/server/network.go, client/internal/routemanager/manager.go, client/internal/routemanager/notifier/notifier_*.go, client/internal/routemanager/fakeip/fakeip.go, client/internal/routemanager/systemops/systemops*.go
Route selection merges v6-exit pairs and augments range strings; fake-IP allocator now dual-pool supporting IPv4 and IPv6; IPv6 route reconciliation integrated into group/settings changes; system operations conditionally add/remove IPv6 split-default routes and validate via dual-stack address checking.
Netflow & Conntrack
client/internal/netflow/logger/logger.go, client/internal/netflow/manager.go, client/internal/netflow/types/types.go, client/internal/netflow/conntrack/conntrack.go
Logger tracks both IPv4 and IPv6 prefixes with isOverlayIP helper; manager derives both prefixes from interface; flow events include ICMPv6 protocol constant; conntrack unmarked-flow fallback uses dual-stack checks.

Client SDKs & UI

Layer / File(s) Summary
Android & iOS SDKs
client/android/client.go, client/ios/NetBirdSDK/client.go
Route selection logic now accounts for v6-exit merge set; buildNetwork/findBestRoutePeer helpers filter/select routes; peer info includes IPv6 field; status includes local peer IPv6.
UI & Event Handling
client/ui/client_ui.go, client/ui/network.go, client/ui/event/event.go
Settings UI adds IPv6 disable checkbox; route selection treats ::/0 as default route alongside 0.0.0.0/0; toast notifications suppress v6-default-route events.
WASM Client
client/wasm/cmd/main.go
CLI ping/ping-tcp accept optional IP-version argument; SSH method parses IP version; helper functions map protocol/network selection.

Testing & Utilities

Layer / File(s) Summary
Test Infrastructure
management/server/peer_test.go, management/server/account_test.go, management/server/group_ipv6_test.go, management/server/store/sql_store_test.go, etc.
Peer fixtures now populate both IP and IPv6 using netip constructors; IPv4 peer address allocation now derives netip.Prefix and uses types.AllocatePeerIP; new IPv6 eligibility and group-reconciliation tests; store tests exercise IPv6 persistence.
IP Address Utilities
shared/netiputil/compact_test.go, client/anonymize/anonymize*.go, client/internal/lazyconn/activity/listener_bind.go
Comprehensive round-trip tests for compact encoding/decoding; anonymization rules updated for IPv6 (using 2001:db8:ffff:: base); fake-IP derivation supports IPv6-only peer fallback.
Firewall & Packet Tests
client/firewall/uspfilter/forwarder/forwarder_test.go, client/firewall/uspfilter/filter_filter_test.go, client/firewall/nftables/router_linux_test.go, etc.
Tests for ICMPv6 parsing (extensions, truncation), IPv6 peer ACLs, IPv6 route ACLs, IPv6 nftables compatibility with ip6tables, v6 ipset key types, and address-family-driven rule generation.
Debug & Anonymization
client/internal/debug/debug.go, client/internal/debug/debug_test.go
Anonymization logic extended to handle AddressV6 compact bytes and SourcePrefixes; IPv6 fixtures and assertions added to test suite.
Networking & Address
client/anonymize/anonymize*.go, client/wasm/internal/rdp/rdcleanpath.go, combined/cmd/config.go, management/internals/modules/reverseproxy/service/manager/*.go, proxy/internal/debug/*.go
Widespread replacement of manual fmt.Sprintf-based host:port formatting with net.JoinHostPort for IPv6-safe construction; STUN URI, TURN, DNS nameserver, and debug ping handlers updated.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Management
    participant Firewall
    participant LocalIface

    Client->>Management: SyncPeer with PeerCapabilities
    Management->>Management: Extract DisableIPv6 from metadata
    Management->>Management: Allocate IPv6 from NetV6 if enabled
    Management->>Client: SyncResponse with address_v6 bytes
    
    Client->>Client: Decode address_v6 via netiputil
    Client->>Client: Update EngineConfig with IPv6 overlay
    Client->>LocalIface: UpdateAddr with wgaddr.Address
    LocalIface->>LocalIface: Configure IPv4 and IPv6 on TUN
    
    Client->>Firewall: AddPeerFiltering with peer IPv6
    Firewall->>Firewall: Dispatch to IPv4 or IPv6 manager
    Firewall->>LocalIface: Install dual-stack rules
    
    Client->>Client: filterAllowedIPs for IPv6
    Client->>Client: Detect IPv6 capability changes
    Client->>Management: OnPeersUpdated when flipped
    Management->>Client: Updated NetworkMap propagates
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

The PR introduces foundational IPv6 dual-stack support across NetBird's core architecture. The scope is massive (~200+ files), touching firewall rule dispatch, peer/account/group management, DNS/routing subsystems, interface configuration, client SDKs, and test infrastructure. Changes are heterogeneous—ranging from data-shape updates (protobuf, netiputil compact encoding) to complex algorithms (dual-pool fake-IP allocation, firewall family-aware dispatching, IPv6 conntrack/ICMP handling) to multi-platform device configuration. Logic density is high, especially in firewall managers (iptables/nftables v4/v6 branching), route management (merge-set handling, v6-exit pairing), and engine peer-state synchronization (IPv6-change detection, allowed-IP filtering). The deep interdependencies between layers and the need to verify both IPv4 backward-compatibility and new IPv6 functionality across diverse subsystems demand careful, multi-pass review.

Possibly related PRs

  • netbirdio/netbird#5708: Both PRs add dual-stack support to iptables/nftables firewall managers; touches same router/ACL initialization and rule-dispatch code.
  • netbirdio/netbird#5698: Direct overlap—both implement IPv6 overlay capability gating, management types, peer IPv6 fields, netiputil compact handling, store/UpdatePeerIPv6 APIs.
  • netbirdio/netbird#5706: Implements same IPv6 dual-stack changes (addrFamily abstraction, forwarder/endpoint IPv6, MSS clamping per-family, fakeIP dual-pool).

Suggested reviewers

  • pascal-fischer

🐰 Hops through a thicket of dual-stack routing rules,
IPv6 tunnels nested in the wireguard pools,
From proto-buf to firewall's gates so tall,
This overlay magic enchants them all!

✨ 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 proto-ipv6-overlay

pappz
pappz previously approved these changes May 6, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026

@lixmal lixmal merged commit 205ebcf into main May 7, 2026
46 of 48 checks passed
@lixmal lixmal deleted the proto-ipv6-overlay branch May 7, 2026 09:33
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.

3 participants