Skip to content

Migrate to net/netip - #1181

Open
fdomain wants to merge 2 commits into
vishvananda:mainfrom
fdomain:netip
Open

Migrate to net/netip#1181
fdomain wants to merge 2 commits into
vishvananda:mainfrom
fdomain:netip

Conversation

@fdomain

@fdomain fdomain commented Apr 13, 2026

Copy link
Copy Markdown

Simply picked up the original work from #1148 and rebased on top of master with some adjustments. @ShimmerGlass original commit authorship is preserved in git history.

Fixes #1114

Summary by CodeRabbit

  • New Features

    • Platform-wide migration to a modern IP/address representation for consistent prefix and address handling and clearer IPv4/IPv6 semantics.
  • Bug Fixes

    • Stricter validation and explicit errors for malformed addresses from system attributes; more reliable broadcast/gateway handling.
  • Refactor

    • Public APIs and types moved to value-based address/prefix semantics, simplifying presence checks, comparisons, and serialization.
  • Tests

    • Test suite updated to use the new address primitives and direct value comparisons; added regression coverage for mapped/next-hop scenarios.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Migrates package-wide IP and prefix representations from stdlib net types to net/netip (netip.Addr, netip.Prefix). Updates parsing, equality, netlink encode/decode, and public APIs across addr, route, link, tc, conntrack, xfrm, socket, ipset, filter, neigh, SEG6, FOU, GTP, rule, and numerous tests.

Changes

Cohort / File(s) Summary
Address core
addr.go, addr_linux.go, addr_test.go
Replaced *net.IPNet/net.IP with netip.Prefix/netip.Addr; Parse/Format/Equal/PeerEqual and netlink attribute handling updated to netip APIs (ParsePrefix, Bits, AsSlice, IsValid).
Routing & Nexthop
route.go, route_linux.go, route_test.go, nexthop.go, nexthop_linux.go, nexthop_test.go
Route Dst/Src/Gw and Nexthop.Gateway -> netip types; RouteGet signatures updated; encode/decode and equality use Prefix/Addr value semantics and AsSlice/AddrFromSlice.
Links & Tunnels
link.go, link_linux.go, link_test.go, bridge_linux_test.go
Tunnel/link structs (Vxlan, Geneve, GRE, VTI, etc.) switched to netip.Addr; family detection uses Is4/Is6; netlink attr parse/serialize uses AddrFromSlice/AsSlice.
Filters / TC / Flower
filter.go, filter_linux.go, filter_test.go, nl/tc_linux.go, nl/tc_linux_test.go
Flower match fields and action IPs migrated to netip.Prefix/netip.Addr; encode/decode, mask derivation, pedit parsing, and action serialization updated to netip APIs and Masked().Addr().
Conntrack & IPTuple
conntrack_linux.go, conntrack_test.go
Conntrack tuples/filters use netip.Addr/netip.Prefix; netlink (de)serialization changed to AddrFromSlice/AsSlice; filter APIs updated to accept Prefix/Addr and use IsValid().
Rules & XFRM / Policy / State
rule.go, rule_linux.go, rule_test.go, xfrm_policy_linux.go, xfrm_state_linux.go, nl/xfrm_linux.go, xfrm_*_test.go
Rule/selector/policy/state fields migrated to netip.Prefix/Addr; encoding/decoding reworked to use Bits(), Masked(), Prefix creation and XfrmAddress helpers return/accept netip types.
Socket, Neighbor, SEG6, FOU, GTP, IPSET
socket.go, socket_linux.go, socket_test.go, neigh.go, neigh_linux.go, neigh_test.go, nl/seg6_linux.go, fou.go, fou_linux.go, fou_test.go, gtp_linux.go, gtp_test.go, ipset_linux.go, ipset_linux_test.go
Multiple structs and netlink (de)serialization moved to netip types; parsing uses AddrFromSlice with explicit errors; serialization uses AsSlice; tests updated to MustParse*/Parse*.
Netlink helpers & APIs
netlink.go, netlink_unspecified.go, handle_unspecified.go, nl/nl_linux.go
Replaced ParseIPNet/NewIPNet with ParsePrefix/NewPrefix; GetIPFamily and RouteGet signatures updated to accept netip.Addr; added v4/v6 zero constants.
Tests & tooling / cmd
numerous *_test.go, cmd/ipset-test/main.go
Tests and cmd adjusted to construct/compare netip types and to handle parse errors (netip.ParseAddr/MustParse*); many equality checks switched from method calls to direct value comparisons.

Sequence Diagram(s)

(Skipped — changes are broad type migrations and do not introduce a new multi-component sequential flow.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

  • #1139 — Related migration of nexthop/route Gateway fields to netip types; overlaps in Gateway/route handling.
  • #1178 — Related rule Src/Dst migration to netip.Prefix and default-route equivalence logic.
  • #1121 — Related SEG6/route test and SEG6 segment handling changes using netip types.

Suggested reviewers

  • aboch
  • borkmann
  • tklauser

Poem

🐇
I swapped old bytes for prefixes, nimble and bright,
hopped through Masks and Bits into netip light.
Slices now stride, no To4 to mislead,
prefixes snug — every mask and need.
A carrot-coded commit — hop, code, delight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Migrate to net/netip' is concise and clearly summarizes the primary change throughout the changeset.
Linked Issues check ✅ Passed The PR successfully addresses issue #1114 by replacing net.IP with netip.Addr throughout the codebase, fixing IPv4-mapped IPv6 address handling via netip's Is4()/Is6() predicates instead of To4() checks.
Out of Scope Changes check ✅ Passed All changes are directly related to the netip migration objective; the PR systematically replaces net.IP/net.IPNet with netip.Addr/netip.Prefix across all affected files without introducing unrelated modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (7)
link_test.go (1)

900-918: ⚠️ Potential issue | 🟠 Major

Skip PMtuDisc comparison for flow-based tunnels in compareGretap and compareGretun.

The failing tests expose a logic mismatch: addGretunAttrs() and addGretapAttrs() skip sending PMtuDisc to the kernel when FlowBased=true (only send IFLA_GRE_COLLECT_METADATA), but compareGretap() and compareGretun() unconditionally compare PMtuDisc regardless of flow-based mode. The kernel returns a non-zero PMtuDisc default for flow-based tunnels, causing the comparison to fail.

Add conditional checks in compareGretap() (line 503) and compareGretun() (line 569) to skip PMtuDisc comparison when FlowBased=true, mirroring the attribute handling in the add functions.

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

In `@link_test.go` around lines 900 - 918, compareGretap() and compareGretun()
currently always compare PMtuDisc but addGretapAttrs()/addGretunAttrs() do not
send PMtuDisc when FlowBased is true, causing false failures; update
compareGretap and compareGretun to skip the PMtuDisc field comparison if the
source link has FlowBased == true (i.e., only perform the PMtuDisc equality
check when FlowBased is false), mirroring the attribute-send behavior in
addGretapAttrs/addGretunAttrs and preventing mismatches against kernel defaults.
xfrm_state_linux_test.go (1)

362-374: ⚠️ Potential issue | 🔴 Critical

Fix XFRMA_SRCADDR encoding to match kernel expectations.

The XFRM state Get/Delete operations fail because XFRMA_SRCADDR is encoded incorrectly. The kernel's netlink validation expects xfrm_address_t (16 bytes), but line 469 in xfrm_state_linux.go sends only 4 bytes for IPv4 addresses via state.Src.AsSlice().

In contrast, the Add path uses FromIP(), which properly pads IPv4 addresses to 16 bytes. Use the same approach in xfrmStateGetOrDelete by creating a properly sized address buffer before passing it to NewRtAttr.

Code location
if state.Src.IsValid() {
	out := nl.NewRtAttr(nl.XFRMA_SRCADDR, state.Src.AsSlice())
	req.AddData(out)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xfrm_state_linux_test.go` around lines 362 - 374, The XFRMA_SRCADDR attribute
is being encoded with state.Src.AsSlice() (which yields 4 bytes for IPv4) inside
xfrmStateGetOrDelete, but the kernel expects a 16-byte xfrm_address_t; change
the encoding to produce a 16-byte address like the Add path does (use the same
FromIP()/16-byte padding approach) before calling nl.NewRtAttr(nl.XFRMA_SRCADDR,
...), i.e. construct a 16-byte buffer from state.Src (pad IPv4 to 16 bytes or
use the existing FromIP helper) and pass that buffer to NewRtAttr instead of
state.Src.AsSlice().
nexthop_test.go (1)

55-60: 🛠️ Refactor suggestion | 🟠 Major

Add a regression case for the actual ::ffff: gateway bug.

These fixtures still only cover native IPv6 and native IPv4 gateways. The issue this PR is supposed to fix is an IPv4-mapped IPv6 gateway being misclassified, so this test will still pass even if deriveFamilyFromNexthop regresses on ::ffff:100.95.128.2.

Also applies to: 123-129

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

In `@nexthop_test.go` around lines 55 - 60, Add a regression test for IPv4-mapped
IPv6 addresses: create a Nexthop fixture whose Gateway is the IPv4-mapped IPv6
address "::ffff:100.95.128.2" (use netip.MustParseAddr) and assert that
deriveFamilyFromNexthop (or the test's family-derivation expectation) classifies
it as the IPv4 family, similar to the existing native IPv4/IPv6 cases around the
Nexthop fixture at lines showing ID/OIF/Gateway/Protocol; this ensures the
::ffff:... mapping bug is covered.
filter_test.go (2)

2141-2158: ⚠️ Potential issue | 🟡 Minor

Apply .Masked() to canonicalize the IPv6 Flower prefix.

netip.MustParsePrefix("ffff::fff1/64") preserves host bits in the address, so it won't compare equal to the canonicalized /64 prefix returned by the kernel. This causes the test assertion at line 2189-2190 to fail.

Suggested fix
-		Dest:     netip.MustParsePrefix("ffff::fff1/64"),
+		Dest:     netip.MustParsePrefix("ffff::fff1/64").Masked(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@filter_test.go` around lines 2141 - 2158, The Flower test builds a
non-canonical IPv6 prefix causing mismatch with kernel-returned canonical
prefixes; update the Flower construction to canonicalize the Dest prefix by
calling .Masked() on netip.MustParsePrefix("ffff::fff1/64") (i.e., set Dest to
netip.MustParsePrefix(...).Masked()) so the prefix compares equal to the
kernel's /64 canonical form; locate this in the test where Flower is
instantiated (FilterAttrs/ Dest field) and apply the same masking if any other
IPv6 prefixes (e.g., peditAction.SrcIP if used as a prefix) are compared to
kernel results.

1769-1787: ⚠️ Potential issue | 🟡 Minor

Normalize the Flower prefixes before asserting on them.

These fixtures keep host bits (1.0.0.1/24, 2.0.0.1/24, etc.), but the kernel round-trip returns masked prefixes. That makes the equality checks at lines 1832-1843 fail even when the kernel state is correct, since netip.Prefix equality is strict and compares host bits.

Suggested fix
-		Dest:        netip.MustParsePrefix("1.0.0.1/24"),
-		Src:         netip.MustParsePrefix("2.0.0.1/24"),
+		Dest:        netip.MustParsePrefix("1.0.0.1/24").Masked(),
+		Src:         netip.MustParsePrefix("2.0.0.1/24").Masked(),
 		EthType:     unix.ETH_P_IP,
-		EncDest:     netip.MustParsePrefix("3.0.0.1/24"),
-		EncSrc:      netip.MustParsePrefix("4.0.0.1/24"),
+		EncDest:     netip.MustParsePrefix("3.0.0.1/24").Masked(),
+		EncSrc:      netip.MustParsePrefix("4.0.0.1/24").Masked(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@filter_test.go` around lines 1769 - 1787, The test constructs a Flower filter
with prefixes containing host bits (Dest, Src, EncDest, EncSrc) but compares
them to kernel-returned prefixes that have host bits masked; normalize these
prefixes before asserting by applying the netip prefix mask operation (i.e.,
mask the host bits of the expected prefixes) so comparisons use canonical/
masked netip.Prefix values; update the assertions around Flower (fields Dest,
Src, EncDest, EncSrc) to compare against the masked versions (or mask the
kernel-returned prefixes) so equality is not affected by host-bit differences.
route_linux.go (2)

698-718: ⚠️ Potential issue | 🟠 Major

Reject non-IPv6 addresses in IP6tnlEncap.Encode.

Lines 708-717 hardcode a 16-byte payload, but netip.Addr.AsSlice() returns 4 bytes for plain IPv4 addresses. If an IPv4 address is passed, the encap attribute will have a declared length of 20 bytes with only 8 bytes of actual payload, creating a malformed packet.

Proposed fix
 func (e *IP6tnlEncap) Encode() ([]byte, error) {
+	if e.Dst.BitLen() != net.IPv6len*8 || e.Src.BitLen() != net.IPv6len*8 {
+		return nil, fmt.Errorf("ip6tnl encap requires IPv6 src and dst")
+	}
 
 	final := []byte{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@route_linux.go` around lines 698 - 718, IP6tnlEncap.Encode currently assumes
e.Dst and e.Src are IPv6 and writes a 16-byte payload header, but
netip.Addr.AsSlice() can return 4 bytes for IPv4 causing malformed attributes;
update IP6tnlEncap.Encode to first validate that both e.Dst.Is6() and
e.Src.Is6() and return an error if not, and then derive the attribute lengths
from the actual address slice lengths when writing resDst and resSrc (i.e.
compute size as 2+2+len(addrSlice) before native.PutUint16) so the
nl.LWTUNNEL_IP6_SRC and nl.LWTUNNEL_IP6_DST attributes are correct.

781-791: ⚠️ Potential issue | 🔴 Critical

Fix Via.Encode() to write address bytes, not netip.Addr struct.

Line 787 uses binary.Write(buf, native, v.Addr) on a netip.Addr, which will fail at runtime because netip.Addr has unexported fields that binary.Write cannot serialize via reflection. However, Via.Decode (lines 794–809) expects only 4 bytes for IPv4 or 16 bytes for IPv6 after the family field. The current implementation breaks all routes using Via.

Use v.Addr.AsSlice() to write the wire-format address bytes instead:

Proposed fix
 func (v *Via) Encode() ([]byte, error) {
 	buf := &bytes.Buffer{}
 	err := binary.Write(buf, native, uint16(v.AddrFamily))
 	if err != nil {
 		return nil, err
 	}
-	err = binary.Write(buf, native, v.Addr)
-	if err != nil {
-		return nil, err
+	switch v.AddrFamily {
+	case nl.FAMILY_V4:
+		if v.Addr.BitLen() != net.IPv4len*8 {
+			return nil, fmt.Errorf("encoding failed: address family %d requires an IPv4 address", v.AddrFamily)
+		}
+	case nl.FAMILY_V6:
+		if v.Addr.BitLen() != net.IPv6len*8 {
+			return nil, fmt.Errorf("encoding failed: address family %d requires an IPv6 address", v.AddrFamily)
+		}
+	default:
+		return nil, fmt.Errorf("encoding failed: address family %d unknown", v.AddrFamily)
+	}
+	if _, err := buf.Write(v.Addr.AsSlice()); err != nil {
+		return nil, err
 	}
 	return buf.Bytes(), nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@route_linux.go` around lines 781 - 791, Via.Encode() currently calls
binary.Write(buf, native, v.Addr) which attempts to serialize a netip.Addr
struct (with unexported fields) and will fail; instead write the raw wire-format
address bytes so Decode (Via.Decode) which expects 4 bytes for IPv4 or 16 bytes
for IPv6 can read them. Replace the binary.Write of v.Addr with writing
v.Addr.AsSlice() (or the slice returned by v.Addr.AsSlice() for the appropriate
family) into buf, preserving the initial uint16(v.AddrFamily) write and
returning buf.Bytes().
🧹 Nitpick comments (6)
socket_linux.go (1)

187-199: Consider explicit nil-IP handling for clarity.

The ok return from netip.AddrFromSlice() is ignored. If l.IP or r.IP is nil, AddrFromSlice returns an invalid zero-value netip.Addr, which will fail both Is4() and Is6() checks, ultimately returning ErrNotImplemented.

While the current behavior is safe, explicit validation would make the failure mode clearer:

♻️ Optional: explicit nil handling
-	localIP, _ = netip.AddrFromSlice(l.IP)
+	localIP, ok := netip.AddrFromSlice(l.IP)
+	if !ok {
+		return nil, ErrNotImplemented
+	}
 	localPort = uint16(l.Port)
-	remoteIP, _ = netip.AddrFromSlice(r.IP)
+	remoteIP, ok = netip.AddrFromSlice(r.IP)
+	if !ok {
+		return nil, ErrNotImplemented
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@socket_linux.go` around lines 187 - 199, The code ignores the boolean result
from netip.AddrFromSlice when converting l.IP and r.IP (used to set localIP and
remoteIP), which silently produces invalid addrs if the underlying byte slices
are nil; update the TCP and UDP address handling to explicitly check the ok
return from netip.AddrFromSlice for both l.IP and r.IP and return
ErrNotImplemented (or a descriptive error) when ok is false so failures are
explicit; locate the conversion sites using netip.AddrFromSlice and variables
localIP, remoteIP, localPort, remotePort and ensure both conversions validate ok
before proceeding (same change in the TCP and UDP switch cases).
gtp_test.go (1)

21-37: These tests should assert the migrated address semantics, not just non-nil lookups.

Right now they only swap constructors and confirm lookup succeeds. That will not catch a broken AsSlice()/AddrFromSlice() round-trip. Please assert that the returned PDP carries the expected PeerAddress/MSAddress, and add at least one ::ffff:x.y.z.w case if this path is supposed to preserve the mapped-address behavior the PR is targeting.

Also applies to: 73-90

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

In `@gtp_test.go` around lines 21 - 37, Update the tests that call GTPPDPAdd,
GTPPDPList and GTPPDPByMSAddress to assert round-tripped address semantics
instead of just non-nil lookups: after adding a PDP (constructed with
PeerAddress and MSAddress) verify the returned PDP's PeerAddress and MSAddress
exactly equal the originals (use the PDP struct fields), and repeat the
assertions for the other test block as well; additionally add a test case that
uses an IPv4-mapped IPv6 address (::ffff:x.y.z.w) to ensure
AsSlice()/AddrFromSlice() preserve mapped-address behavior when looked up via
GTPPDPByMSAddress.
fou_linux.go (1)

191-194: Silently discarding parse errors is acceptable but consumers must check IsValid().

netip.AddrFromSlice returns a boolean indicating success, which is discarded here. On failure (e.g., unexpected byte length), fou.Local/fou.Peer will be an invalid zero Addr. This is safe as long as consumers call IsValid() before using these fields.

Per the context snippet from link_linux.go, other address fields like Gretap.Local follow a similar pattern where IsValid() checks are performed before use. Consider documenting that Fou.Local/Fou.Peer may be invalid if the kernel sends unexpected data.

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

In `@fou_linux.go` around lines 191 - 194, The code currently discards the boolean
result from netip.AddrFromSlice when setting fou.Local and fou.Peer for
FOU_ATTR_LOCAL_V4/6 and FOU_ATTR_PEER_V4/6, which can leave those fields as an
invalid zero Addr; update the codebase by adding a concise comment or
documentation next to the fou struct fields (or adjacent to the switch handling
those attributes) stating that fou.Local and fou.Peer may be invalid if the
kernel provides unexpected data and callers must call IsValid() on those fields
before use; reference the symbols fou.Local, fou.Peer, netip.AddrFromSlice and
IsValid() so reviewers can locate and rely on the documented behavior.
nl/xfrm_linux.go (1)

130-142: Consider checking AddrFromSlice success status.

The second return value from netip.AddrFromSlice is discarded. If the slice length is unexpected, this silently produces an invalid address rather than signaling an error. While this may be acceptable for internal usage where slice lengths are controlled, explicit handling would be more defensive.

addr, ok := netip.AddrFromSlice(x[0:4])
if !ok {
    return netip.Addr{} // or handle error
}
return addr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/xfrm_linux.go` around lines 130 - 142, In XfrmAddress.ToIP the call to
netip.AddrFromSlice discards the ok boolean; change the ipv4 branch in ToIP to
check the second return (ok) from netip.AddrFromSlice and handle failure
explicitly (e.g., return netip.Addr{} or another sentinel) instead of silently
using an invalid addr, so that AddrFromSlice(x[0:4]) is validated before
returning.
link_linux.go (1)

3052-3058: Consider handling AddrFromSlice failure for robustness.

The ok return value from netip.AddrFromSlice is discarded. While netlink typically provides valid address data, explicitly checking could help catch unexpected data:

if addr, ok := netip.AddrFromSlice(datum.Value[0:4]); ok {
    vxlan.SrcAddr = addr
}

This pattern is used correctly in parseBondArpIpTargets (line 3173) but inconsistently applied elsewhere.

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

In `@link_linux.go` around lines 3052 - 3058, The code currently ignores the
boolean return from netip.AddrFromSlice when parsing VXLAN addresses (in the
switch cases handling nl.IFLA_VXLAN_LOCAL, nl.IFLA_VXLAN_LOCAL6,
nl.IFLA_VXLAN_GROUP, nl.IFLA_VXLAN_GROUP6), so update those cases to check the
ok value and only assign to vxlan.SrcAddr or vxlan.Group when AddrFromSlice
returns ok (same pattern used in parseBondArpIpTargets). Ensure you slice the
correct bytes (0:4 for IPv4, 0:16 for IPv6) and avoid discarding errors by
guarding the assignment with the ok check.
filter_linux.go (1)

1043-1045: Consider checking AddrFromSlice success for consistency.

The ok return value is discarded here, unlike the thorough error handling in Flower.decode. For consistency:

 case nl.TCA_TUNNEL_KEY_ENC_IPV6_SRC, nl.TCA_TUNNEL_KEY_ENC_IPV4_SRC:
-	action.(*TunnelKeyAction).SrcAddr, _ = netip.AddrFromSlice(adatum.Value)
+	if addr, ok := netip.AddrFromSlice(adatum.Value); ok {
+		action.(*TunnelKeyAction).SrcAddr = addr
+	}
 case nl.TCA_TUNNEL_KEY_ENC_IPV6_DST, nl.TCA_TUNNEL_KEY_ENC_IPV4_DST:
-	action.(*TunnelKeyAction).DstAddr, _ = netip.AddrFromSlice(adatum.Value)
+	if addr, ok := netip.AddrFromSlice(adatum.Value); ok {
+		action.(*TunnelKeyAction).DstAddr = addr
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@filter_linux.go` around lines 1043 - 1045, The code assigns
netip.AddrFromSlice results to TunnelKeyAction.SrcAddr/DstAddr while discarding
the ok flag; update the assignments in the block that touches
action.(*TunnelKeyAction).SrcAddr and .DstAddr to capture the second return
(ok), verify it, and handle failures consistently with Flower.decode (e.g.,
return or propagate an error or log and skip) so malformed adatum.Value is not
silently accepted; reference the AddrFromSlice call sites and the
TunnelKeyAction type when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gtp_linux.go`:
- Line 186: The netlink attributes GENL_GTP_ATTR_MS_ADDRESS and
GENL_GTP_ATTR_PEER_ADDRESS must carry 4-byte IPv4 addresses only; update the
serialization in gtp_linux.go where you call
req.AddData(nl.NewRtAttr(nl.GENL_GTP_ATTR_MS_ADDRESS, addr.AsSlice())) (and the
similar occurrences around lines 207–208) to either normalize IPv4-mapped
addresses by using addr.Unmap().AsSlice() before passing to NewRtAttr, or
explicitly validate addr is IPv4 and return an error if not; ensure you apply
the same change for both MS_ADDRESS and PEER_ADDRESS usages so only 4-byte
payloads are sent.

In `@ipset_linux_test.go`:
- Around line 651-653: The bitmap:ip test condition is inverted causing exact
matches to fail; in ipset_linux_test.go locate the branch checking tC.typename
== "bitmap:ip" and change the equality checks on result.IPFrom/result.IPTo
against tC.options.IPFrom/tC.options.IPTo to use != (not equal) so the condition
becomes true only when the result addresses are invalid or do not match the
expected range; update the comparisons for both IPFrom and IPTo in that if
expression.

In `@neigh_linux.go`:
- Around line 339-346: The NDA_LLADDR branch currently parses the address and
assigns it to neigh.IP, which overwrites the real NDA_DST; instead, when
LinkByIndex(neigh.LinkIndex) returns a link with link.Attrs().EncapType ==
"tunnel6", set the parsed netip.Addr into neigh.LLIPAddr (not neigh.IP) so the
tunnel6 link-layer IP is preserved separately; update the NDA_LLADDR handling to
assign addr to neigh.LLIPAddr and leave neigh.IP untouched to allow correct
round-tripping.

In `@neigh_test.go`:
- Around line 127-133: The test's arpTable (arpEntry slice constructed using
netip.MustParseAddr and parseMAC) only contains native IPv4 addresses and
doesn't exercise the IPv4-mapped IPv6 neighbor path; add an entry using an
IPv4-mapped IPv6 address (e.g. netip.MustParseAddr("::ffff:10.99.0.6")) with a
corresponding MAC via parseMAC to the arpTable so the neigh handling regression
for To4()-style misclassification is covered (update any expectations/assertions
that count or lookup these entries accordingly).

In `@nl/xfrm_linux.go`:
- Around line 145-158: In XfrmAddress.ToIPNet, the code currently ignores the
supplied prefixlen for non-empty addresses and always returns host masks; update
ToIPNet (function: ToIPNet on type XfrmAddress) to pass the provided prefixlen
into netip.PrefixFrom by converting prefixlen to int (e.g., netip.PrefixFrom(ip,
int(prefixlen))) instead of hardcoding 32/128, preserving the original prefix
length returned by ToIP().

In `@route_linux.go`:
- Around line 1295-1301: The branch handling RT_FILTER_DST is clobbering an
explicitly provided filter.Dst because the validity check is inverted; change
the logic so you only call genZeroIPNet(family) when filter.Dst is not set
(i.e., when filter.Dst.IsValid() is false) instead of when it is true. Locate
the RT_FILTER_DST case that references filter.MPLSDst, route.MPLSDst,
filter.Dst, genZeroIPNet and family and flip the condition from "if
filter.Dst.IsValid()" to "if !filter.Dst.IsValid()" (or equivalent) so callers'
explicit filter.Dst values are preserved and defaulting only occurs when
filter.Dst is the zero value.

In `@route_test.go`:
- Around line 503-504: The assertion in the test after calling RouteAppend is
inverted: it checks routes[1].Src == ip which fails when append succeeded;
update the test so it asserts routes[1].Src == ip (or flip the boolean) and keep
the length check (len(routes) == 2). Locate the test that calls RouteAppend and
change the condition around routes, routes[1].Src and ip to expect equality
(routes[1].Src == ip) for a successful append.
- Around line 446-447: The test assertion is inverted: after calling
RouteReplace, routes should contain exactly one entry and that entry's Src must
equal ip; change the condition in the failing check to detect a mismatch (e.g.
use routes[0].Src != ip) so the test fails when replacement did not happen.
Update the check that currently reads with routes and routes[0].Src (after
RouteReplace) to assert routes length == 1 and routes[0].Src == ip, failing when
either len(routes) != 1 or routes[0].Src != ip.
- Around line 543-551: The helper expectRouteUpdate currently only compares
update.Route.Dst.Addr() to the provided dst, losing prefix length information;
update the comparison to include the full prefix (e.g., compare update.Route.Dst
to dst as a netip.Prefix or compare both Addr() and Bits()/MaskLength) so /16 vs
/24 are distinguished, and adjust callers so they pass the prefix (dst) itself
rather than dst.Addr(); locate the check in expectRouteUpdate and replace the
Addr-only comparison with a full-prefix equality check.

In `@rule_linux.go`:
- Around line 237-248: The code silently ignores errors from addr.Prefix when
decoding nl.FRA_SRC and nl.FRA_DST; modify the FRA_SRC and FRA_DST cases so you
call addr.Prefix(int(msg.Src_len)) and addr.Prefix(int(msg.Dst_len)) and check
the returned error, returning a parse error (e.g., fmt.Errorf with context) if
Prefix returns an error instead of discarding it; update the assignments to
rule.Src and rule.Dst only after Prefix succeeds to ensure invalid/out-of-range
prefix lengths are propagated as parse failures.
- Around line 66-83: The code encodes rule.Dst.Addr().AsSlice() and
rule.Src.Addr().AsSlice() without applying netip.Prefix.Masked(), causing host
bits to be preserved and mismatching kernel-canonicalized prefixes; update the
encoding in the rule handling (the block that sets msg.Dst_len/msg.Src_len,
msg.Family, dstFamily and appends to rtAttrs via nl.NewRtAttr with unix.RTA_DST
and unix.RTA_SRC) to call Masked() on rule.Dst and rule.Src first and use the
masked prefix for Bits() and Addr().AsSlice() so the netlink payload uses the
canonical network address.
- Around line 377-386: The comparison in ruleIPNetEqual should compare
canonicalized (masked) prefixes, not the raw netip.Prefix values; update
ruleIPNetEqual to canonicalize both inputs (e.g., call the
Prefix.Masked()/Masked form or otherwise zero host bits using the prefix's Addr
and Bits) and then compare those masked prefixes for equality (keep the existing
default/invalid handling and replace the final raw equality check with a
comparison of the masked/canonicalized prefixes).

In `@rule_test.go`:
- Around line 676-679: The comparisons for a.Src and a.Dst should use the
existing ruleIPNetEqual helper instead of comparing Prefix.String() to handle
canonicalization (e.g., 172.16.0.1/16 vs 172.16.0.0/16); replace the Src and Dst
boolean expressions that currently check IsValid()/String() equality with calls
to ruleIPNetEqual(a.Src, b.Src) and ruleIPNetEqual(a.Dst, b.Dst) so prefix
equivalence matches kernel-normalized rules (this will fix TestRuleAddDel).

In `@xfrm_policy_linux_test.go`:
- Around line 221-225: The equality check in the test compares netip.Prefix
directly (the return expression that checks a.Src == b.Src && a.Dst == b.Dst),
which fails when one side is an unset prefix and the other is a concrete /0; add
a helper like samePrefix(a,b netip.Prefix) that treats an invalid (unset) prefix
and a valid unspecified /0 as equal, then replace the direct checks in the
return statement with samePrefix(a.Src, b.Src) and samePrefix(a.Dst, b.Dst) so
policy round-trips are considered equivalent.

In `@xfrm_policy_linux.go`:
- Around line 104-112: The selector family (sel.Family) is only derived from
policy.Dst causing src-only IPv6 policies to be serialized with Family=IPv4;
update the logic in xfrm_policy_linux.go so that after defaulting sel.Family =
uint16(nl.FAMILY_V4) you set sel.Family =
uint16(nl.GetIPFamily(policy.Dst.Addr())) when policy.Dst.IsValid(), otherwise
if policy.Src.IsValid() set sel.Family =
uint16(nl.GetIPFamily(policy.Src.Addr())); keep existing assignments to
sel.Saddr/PrefixlenS and sel.Daddr/PrefixlenD unchanged.

In `@xfrm_state_linux.go`:
- Around line 468-471: The XFRMA_SRCADDR attribute is currently sent using
state.Src.AsSlice(), which yields variable-length (4 or 16 bytes); replace this
with a 16-byte xfrm_address_t by creating an nl.XfrmAddress, calling its
FromIP(state.Src) to serialize/zero-pad IPv4 to 16 bytes, and pass srcAddr[:]
into nl.NewRtAttr for nl.XFRMA_SRCADDR before calling req.AddData; update the
block that references state.Src, nl.XFRMA_SRCADDR, nl.XfrmAddress, FromIP,
nl.NewRtAttr and req.AddData accordingly.

---

Outside diff comments:
In `@filter_test.go`:
- Around line 2141-2158: The Flower test builds a non-canonical IPv6 prefix
causing mismatch with kernel-returned canonical prefixes; update the Flower
construction to canonicalize the Dest prefix by calling .Masked() on
netip.MustParsePrefix("ffff::fff1/64") (i.e., set Dest to
netip.MustParsePrefix(...).Masked()) so the prefix compares equal to the
kernel's /64 canonical form; locate this in the test where Flower is
instantiated (FilterAttrs/ Dest field) and apply the same masking if any other
IPv6 prefixes (e.g., peditAction.SrcIP if used as a prefix) are compared to
kernel results.
- Around line 1769-1787: The test constructs a Flower filter with prefixes
containing host bits (Dest, Src, EncDest, EncSrc) but compares them to
kernel-returned prefixes that have host bits masked; normalize these prefixes
before asserting by applying the netip prefix mask operation (i.e., mask the
host bits of the expected prefixes) so comparisons use canonical/ masked
netip.Prefix values; update the assertions around Flower (fields Dest, Src,
EncDest, EncSrc) to compare against the masked versions (or mask the
kernel-returned prefixes) so equality is not affected by host-bit differences.

In `@link_test.go`:
- Around line 900-918: compareGretap() and compareGretun() currently always
compare PMtuDisc but addGretapAttrs()/addGretunAttrs() do not send PMtuDisc when
FlowBased is true, causing false failures; update compareGretap and
compareGretun to skip the PMtuDisc field comparison if the source link has
FlowBased == true (i.e., only perform the PMtuDisc equality check when FlowBased
is false), mirroring the attribute-send behavior in
addGretapAttrs/addGretunAttrs and preventing mismatches against kernel defaults.

In `@nexthop_test.go`:
- Around line 55-60: Add a regression test for IPv4-mapped IPv6 addresses:
create a Nexthop fixture whose Gateway is the IPv4-mapped IPv6 address
"::ffff:100.95.128.2" (use netip.MustParseAddr) and assert that
deriveFamilyFromNexthop (or the test's family-derivation expectation) classifies
it as the IPv4 family, similar to the existing native IPv4/IPv6 cases around the
Nexthop fixture at lines showing ID/OIF/Gateway/Protocol; this ensures the
::ffff:... mapping bug is covered.

In `@route_linux.go`:
- Around line 698-718: IP6tnlEncap.Encode currently assumes e.Dst and e.Src are
IPv6 and writes a 16-byte payload header, but netip.Addr.AsSlice() can return 4
bytes for IPv4 causing malformed attributes; update IP6tnlEncap.Encode to first
validate that both e.Dst.Is6() and e.Src.Is6() and return an error if not, and
then derive the attribute lengths from the actual address slice lengths when
writing resDst and resSrc (i.e. compute size as 2+2+len(addrSlice) before
native.PutUint16) so the nl.LWTUNNEL_IP6_SRC and nl.LWTUNNEL_IP6_DST attributes
are correct.
- Around line 781-791: Via.Encode() currently calls binary.Write(buf, native,
v.Addr) which attempts to serialize a netip.Addr struct (with unexported fields)
and will fail; instead write the raw wire-format address bytes so Decode
(Via.Decode) which expects 4 bytes for IPv4 or 16 bytes for IPv6 can read them.
Replace the binary.Write of v.Addr with writing v.Addr.AsSlice() (or the slice
returned by v.Addr.AsSlice() for the appropriate family) into buf, preserving
the initial uint16(v.AddrFamily) write and returning buf.Bytes().

In `@xfrm_state_linux_test.go`:
- Around line 362-374: The XFRMA_SRCADDR attribute is being encoded with
state.Src.AsSlice() (which yields 4 bytes for IPv4) inside xfrmStateGetOrDelete,
but the kernel expects a 16-byte xfrm_address_t; change the encoding to produce
a 16-byte address like the Add path does (use the same FromIP()/16-byte padding
approach) before calling nl.NewRtAttr(nl.XFRMA_SRCADDR, ...), i.e. construct a
16-byte buffer from state.Src (pad IPv4 to 16 bytes or use the existing FromIP
helper) and pass that buffer to NewRtAttr instead of state.Src.AsSlice().

---

Nitpick comments:
In `@filter_linux.go`:
- Around line 1043-1045: The code assigns netip.AddrFromSlice results to
TunnelKeyAction.SrcAddr/DstAddr while discarding the ok flag; update the
assignments in the block that touches action.(*TunnelKeyAction).SrcAddr and
.DstAddr to capture the second return (ok), verify it, and handle failures
consistently with Flower.decode (e.g., return or propagate an error or log and
skip) so malformed adatum.Value is not silently accepted; reference the
AddrFromSlice call sites and the TunnelKeyAction type when making the change.

In `@fou_linux.go`:
- Around line 191-194: The code currently discards the boolean result from
netip.AddrFromSlice when setting fou.Local and fou.Peer for FOU_ATTR_LOCAL_V4/6
and FOU_ATTR_PEER_V4/6, which can leave those fields as an invalid zero Addr;
update the codebase by adding a concise comment or documentation next to the fou
struct fields (or adjacent to the switch handling those attributes) stating that
fou.Local and fou.Peer may be invalid if the kernel provides unexpected data and
callers must call IsValid() on those fields before use; reference the symbols
fou.Local, fou.Peer, netip.AddrFromSlice and IsValid() so reviewers can locate
and rely on the documented behavior.

In `@gtp_test.go`:
- Around line 21-37: Update the tests that call GTPPDPAdd, GTPPDPList and
GTPPDPByMSAddress to assert round-tripped address semantics instead of just
non-nil lookups: after adding a PDP (constructed with PeerAddress and MSAddress)
verify the returned PDP's PeerAddress and MSAddress exactly equal the originals
(use the PDP struct fields), and repeat the assertions for the other test block
as well; additionally add a test case that uses an IPv4-mapped IPv6 address
(::ffff:x.y.z.w) to ensure AsSlice()/AddrFromSlice() preserve mapped-address
behavior when looked up via GTPPDPByMSAddress.

In `@link_linux.go`:
- Around line 3052-3058: The code currently ignores the boolean return from
netip.AddrFromSlice when parsing VXLAN addresses (in the switch cases handling
nl.IFLA_VXLAN_LOCAL, nl.IFLA_VXLAN_LOCAL6, nl.IFLA_VXLAN_GROUP,
nl.IFLA_VXLAN_GROUP6), so update those cases to check the ok value and only
assign to vxlan.SrcAddr or vxlan.Group when AddrFromSlice returns ok (same
pattern used in parseBondArpIpTargets). Ensure you slice the correct bytes (0:4
for IPv4, 0:16 for IPv6) and avoid discarding errors by guarding the assignment
with the ok check.

In `@nl/xfrm_linux.go`:
- Around line 130-142: In XfrmAddress.ToIP the call to netip.AddrFromSlice
discards the ok boolean; change the ipv4 branch in ToIP to check the second
return (ok) from netip.AddrFromSlice and handle failure explicitly (e.g., return
netip.Addr{} or another sentinel) instead of silently using an invalid addr, so
that AddrFromSlice(x[0:4]) is validated before returning.

In `@socket_linux.go`:
- Around line 187-199: The code ignores the boolean result from
netip.AddrFromSlice when converting l.IP and r.IP (used to set localIP and
remoteIP), which silently produces invalid addrs if the underlying byte slices
are nil; update the TCP and UDP address handling to explicitly check the ok
return from netip.AddrFromSlice for both l.IP and r.IP and return
ErrNotImplemented (or a descriptive error) when ok is false so failures are
explicit; locate the conversion sites using netip.AddrFromSlice and variables
localIP, remoteIP, localPort, remotePort and ensure both conversions validate ok
before proceeding (same change in the TCP and UDP switch cases).
🪄 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: 13a5cd89-78c4-4925-ad0b-39e0f4d171bc

📥 Commits

Reviewing files that changed from the base of the PR and between c822ed7 and 949f050.

📒 Files selected for processing (48)
  • addr.go
  • addr_linux.go
  • addr_test.go
  • bridge_linux_test.go
  • cmd/ipset-test/main.go
  • conntrack_linux.go
  • conntrack_test.go
  • filter.go
  • filter_linux.go
  • filter_test.go
  • fou.go
  • fou_linux.go
  • fou_test.go
  • gtp_linux.go
  • gtp_test.go
  • handle_test.go
  • handle_unspecified.go
  • ipset_linux.go
  • ipset_linux_test.go
  • link.go
  • link_linux.go
  • link_test.go
  • neigh.go
  • neigh_linux.go
  • neigh_test.go
  • netlink.go
  • netlink_unspecified.go
  • nexthop.go
  • nexthop_linux.go
  • nexthop_test.go
  • nl/nl_linux.go
  • nl/seg6_linux.go
  • nl/tc_linux.go
  • nl/tc_linux_test.go
  • nl/xfrm_linux.go
  • route.go
  • route_linux.go
  • route_test.go
  • rule.go
  • rule_linux.go
  • rule_test.go
  • socket.go
  • socket_linux.go
  • socket_test.go
  • xfrm_policy_linux.go
  • xfrm_policy_linux_test.go
  • xfrm_state_linux.go
  • xfrm_state_linux_test.go

Comment thread gtp_linux.go Outdated
Comment thread ipset_linux_test.go
Comment on lines 651 to 653
} else if tC.typename == "bitmap:ip" {
if result.IPFrom == nil || result.IPTo == nil || result.IPFrom.Equal(tC.options.IPFrom) || result.IPTo.Equal(tC.options.IPTo) {
if !result.IPFrom.IsValid() || !result.IPTo.IsValid() || result.IPFrom == tC.options.IPFrom || result.IPTo == tC.options.IPTo {
t.Fatalf("expected ip range %v-%v, got %v-%v", tC.options.IPFrom, tC.options.IPTo, result.IPFrom, result.IPTo)

@coderabbitai coderabbitai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Flip the bitmap:ip equality check.

This condition currently fails when result.IPFrom/result.IPTo exactly match the expected range, so the first bitmap:ip case added here will report a false failure.

Suggested fix
-				if !result.IPFrom.IsValid() || !result.IPTo.IsValid() || result.IPFrom == tC.options.IPFrom || result.IPTo == tC.options.IPTo {
+				if !result.IPFrom.IsValid() || !result.IPTo.IsValid() || result.IPFrom != tC.options.IPFrom || result.IPTo != tC.options.IPTo {
 					t.Fatalf("expected ip range %v-%v, got %v-%v", tC.options.IPFrom, tC.options.IPTo, result.IPFrom, result.IPTo)
 				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if tC.typename == "bitmap:ip" {
if result.IPFrom == nil || result.IPTo == nil || result.IPFrom.Equal(tC.options.IPFrom) || result.IPTo.Equal(tC.options.IPTo) {
if !result.IPFrom.IsValid() || !result.IPTo.IsValid() || result.IPFrom == tC.options.IPFrom || result.IPTo == tC.options.IPTo {
t.Fatalf("expected ip range %v-%v, got %v-%v", tC.options.IPFrom, tC.options.IPTo, result.IPFrom, result.IPTo)
} else if tC.typename == "bitmap:ip" {
if !result.IPFrom.IsValid() || !result.IPTo.IsValid() || result.IPFrom != tC.options.IPFrom || result.IPTo != tC.options.IPTo {
t.Fatalf("expected ip range %v-%v, got %v-%v", tC.options.IPFrom, tC.options.IPTo, result.IPFrom, result.IPTo)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ipset_linux_test.go` around lines 651 - 653, The bitmap:ip test condition is
inverted causing exact matches to fail; in ipset_linux_test.go locate the branch
checking tC.typename == "bitmap:ip" and change the equality checks on
result.IPFrom/result.IPTo against tC.options.IPFrom/tC.options.IPTo to use !=
(not equal) so the condition becomes true only when the result addresses are
invalid or do not match the expected range; update the comparisons for both
IPFrom and IPTo in that if expression.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I believe this "issue" was already present prior this change (or there is a difference of how were done comparisons between net.IP Equal() and now with netip.Addr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fdomain, you're right — the original code used .Equal() (not !.Equal()), so the inverted logic was already there before this PR, and the migration faithfully preserved the same semantics (nil!IsValid(), .Equal()==). The translation is correct.

Worth noting as well: this else if tC.typename == "bitmap:ip" branch is effectively dead code in TestIpsetBitmapCreateListWithTestCases — none of the test cases in that table use "bitmap:ip" as the typename (they all use "bitmap:port"), so this condition never actually executes. That's likely why the pre-existing inversion went unnoticed.

Would you like me to open a follow-up issue to either fix the inverted condition or add a bitmap:ip test case so the branch is actually exercised?

Comment thread neigh_linux.go
Comment on lines 339 to +346
link, err := LinkByIndex(neigh.LinkIndex)
if err == nil && link.Attrs().EncapType == "tunnel6" {
neigh.IP = net.IP(attr.Value)
addr, ok := netip.AddrFromSlice(attr.Value)
if !ok {
return nil, fmt.Errorf("NDA_LLADDR: invalid address")
}

neigh.IP = addr

@coderabbitai coderabbitai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Populate LLIPAddr from tunnel6 NDA_LLADDR.

This branch is decoding NDA_LLADDR, but it stores the parsed address into neigh.IP. That loses the real NDA_DST value and breaks round-tripping for neighbors that use IP-valued link-layer addresses.

Suggested fix
-					neigh.IP = addr
+					neigh.LLIPAddr = addr
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
link, err := LinkByIndex(neigh.LinkIndex)
if err == nil && link.Attrs().EncapType == "tunnel6" {
neigh.IP = net.IP(attr.Value)
addr, ok := netip.AddrFromSlice(attr.Value)
if !ok {
return nil, fmt.Errorf("NDA_LLADDR: invalid address")
}
neigh.IP = addr
link, err := LinkByIndex(neigh.LinkIndex)
if err == nil && link.Attrs().EncapType == "tunnel6" {
addr, ok := netip.AddrFromSlice(attr.Value)
if !ok {
return nil, fmt.Errorf("NDA_LLADDR: invalid address")
}
neigh.LLIPAddr = addr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@neigh_linux.go` around lines 339 - 346, The NDA_LLADDR branch currently
parses the address and assigns it to neigh.IP, which overwrites the real
NDA_DST; instead, when LinkByIndex(neigh.LinkIndex) returns a link with
link.Attrs().EncapType == "tunnel6", set the parsed netip.Addr into
neigh.LLIPAddr (not neigh.IP) so the tunnel6 link-layer IP is preserved
separately; update the NDA_LLADDR handling to assign addr to neigh.LLIPAddr and
leave neigh.IP untouched to allow correct round-tripping.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This logic was existing before this change, not a regression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fdomain, you're right — looking at the diff, the original code was neigh.IP = net.IP(attr.Value) and the new code preserves exactly that behavior with neigh.IP = addr. This PR is only migrating the type, not changing the assignment target. I apologize for the noise.

If the pre-existing assignment target (neigh.IP vs neigh.LLIPAddr) is worth revisiting, that would be a separate follow-up. Would you like me to open an issue to track it?

( ••)
( •
•)>⌐■-■
(⌐■-■)


🧠 Learnings used
Learnt from: kyrtapz
Repo: vishvananda/netlink PR: 1159
File: bridge_linux.go:349-366
Timestamp: 2026-02-06T09:13:09.289Z
Learning: VNI filter operations (RTM_NEWTUNNEL, RTM_DELTUNNEL, RTM_GETTUNNEL) in bridge_linux.go do not require NLM_F_CREATE or NLM_F_APPEND flags. They only need NLM_F_ACK for acknowledgment, as confirmed by the iproute2 reference implementation in bridge/vni.c.

Learnt from: mattedallo
Repo: vishvananda/netlink PR: 1164
File: bridge_linux.go:44-48
Timestamp: 2026-02-09T08:32:40.092Z
Learning: In bridge_linux.go, BridgeVlanTunnelShow and BridgeVlanTunnelShowDev correctly use RTEXT_FILTER_BRVLAN (not RTEXT_FILTER_BRVLAN_COMPRESSED) for tunnel info dumps, matching iproute2's bridge vlan tunnelshow default behavior. RTEXT_FILTER_BRVLAN_COMPRESSED is only used with the -compressvlans flag to control VLAN range compression in display output, not tunnel info availability.

Comment thread neigh_test.go
Comment on lines 127 to 133
arpTable := []arpEntry{
{net.ParseIP("10.99.0.1"), parseMAC("aa:bb:cc:dd:00:01")},
{net.ParseIP("10.99.0.2"), parseMAC("aa:bb:cc:dd:00:02")},
{net.ParseIP("10.99.0.3"), parseMAC("aa:bb:cc:dd:00:03")},
{net.ParseIP("10.99.0.4"), parseMAC("aa:bb:cc:dd:00:04")},
{net.ParseIP("10.99.0.5"), parseMAC("aa:bb:cc:dd:00:05")},
{netip.MustParseAddr("10.99.0.1"), parseMAC("aa:bb:cc:dd:00:01")},
{netip.MustParseAddr("10.99.0.2"), parseMAC("aa:bb:cc:dd:00:02")},
{netip.MustParseAddr("10.99.0.3"), parseMAC("aa:bb:cc:dd:00:03")},
{netip.MustParseAddr("10.99.0.4"), parseMAC("aa:bb:cc:dd:00:04")},
{netip.MustParseAddr("10.99.0.5"), parseMAC("aa:bb:cc:dd:00:05")},
}

@coderabbitai coderabbitai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Cover the IPv4-mapped IPv6 neighbor path too.

One of the stated objectives is fixing To4()-style misclassification in neigh handling, but this table still only exercises native IPv4 entries. Please add a ::ffff: neighbor fixture here so this file actually guards the regression being fixed.

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

In `@neigh_test.go` around lines 127 - 133, The test's arpTable (arpEntry slice
constructed using netip.MustParseAddr and parseMAC) only contains native IPv4
addresses and doesn't exercise the IPv4-mapped IPv6 neighbor path; add an entry
using an IPv4-mapped IPv6 address (e.g. netip.MustParseAddr("::ffff:10.99.0.6"))
with a corresponding MAC via parseMAC to the arpTable so the neigh handling
regression for To4()-style misclassification is covered (update any
expectations/assertions that count or lookup these entries accordingly).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not sure this is relevant as well for this change, it's a suggestion to improve the coverage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fdomain, that's a fair point. The migration itself (switching from To4()/AsSlice() in neigh_linux.go) is the actual fix, so the test is purely additive coverage. However, since the PR's stated objectives explicitly call out neighbor handling as one of the To4()-misclassification paths being addressed (per issue #1114), a regression test here would directly validate that specific fix and guard against future regressions.

That said, it's entirely your call — if you'd prefer to keep it out of scope for this PR, that's reasonable. Would you like me to open a follow-up issue to track adding IPv4-mapped IPv6 coverage for the neighbor path instead?

Comment thread nl/xfrm_linux.go Outdated
Comment thread rule_test.go Outdated
Comment thread socket_linux.go Outdated
Comment thread xfrm_policy_linux_test.go
Comment thread xfrm_policy_linux.go
Comment thread xfrm_state_linux.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
nl/tc_linux.go (1)

1626-1656: ⚠️ Potential issue | 🟠 Major

Guard the explicit IPv4 setters against IPv6-family input.

Addr.AsSlice() returns 16 bytes for IPv6 and IPv4-mapped IPv6 addresses. Calling Uint32(ip.AsSlice()) on such addresses reads the first 4 bytes and silently encodes the wrong value. The old To4() path unwrapped mapped addresses and rejected plain IPv6 instead.

Suggested fix
 func (p *TcPedit) SetIPv4Src(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	ip = ip.Unmap()
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])

 	tKey := TcPeditKey{}
 	tKeyEx := TcPeditKeyEx{}
@@
 func (p *TcPedit) SetIPv4Dst(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	ip = ip.Unmap()
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])

 	tKey := TcPeditKey{}
 	tKeyEx := TcPeditKeyEx{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/tc_linux.go` around lines 1626 - 1656, The IPv4 setter functions
SetIPv4Src and SetIPv4Dst must reject or unwrap IPv6 inputs before reading 4
bytes; call ip.Unmap() (or use ip.As4()/ip.Is4()) and verify you have a valid
IPv4 address before calling NativeEndian().Uint32 on the byte slice. Update both
TcPedit.SetIPv4Src and TcPedit.SetIPv4Dst to obtain the 4-byte IPv4 form (e.g.,
via v4, ok := ip.As4() or ip = ip.Unmap(); v4, ok := ip.As4()), return or error
when ok is false, then use that 4-byte value to populate tKey.Val and proceed as
before.
route_linux.go (1)

781-788: ⚠️ Potential issue | 🔴 Critical

Fix Via.Encode() and Via.Decode() – undefined native variable and binary.Write incompatibility.

Via.Encode() fails at runtime because binary.Write cannot serialize netip.Addr (not fixed-size). Additionally, both Via.Encode() and Via.Decode() reference an undefined native variable, which will fail to compile. Define native := nl.NativeEndian() at the start of both methods, then serialize the address family and raw bytes manually using v.Addr.AsSlice(), validating byte length against the family.

Also fix RouteGetWithOptions (line 1628): msg.Src_len should use options.SrcAddr.BitLen(), not destination.BitLen().

Proposed fix for Via.Encode and Via.Decode
 func (v *Via) Encode() ([]byte, error) {
+	native := nl.NativeEndian()
-	buf := &bytes.Buffer{}
-	err := binary.Write(buf, native, uint16(v.AddrFamily))
-	if err != nil {
-		return nil, err
-	}
-	err = binary.Write(buf, native, v.Addr)
-	if err != nil {
-		return nil, err
-	}
-	return buf.Bytes(), nil
+	addr := v.Addr.AsSlice()
+	switch v.AddrFamily {
+	case nl.FAMILY_V4:
+		if len(addr) != net.IPv4len {
+			return nil, fmt.Errorf("RTA_VIA expects an IPv4 address")
+		}
+	case nl.FAMILY_V6:
+		if len(addr) != net.IPv6len {
+			return nil, fmt.Errorf("RTA_VIA expects an IPv6 address")
+		}
+	default:
+		return nil, fmt.Errorf("RTA_VIA unsupported family %d", v.AddrFamily)
+	}
+
+	buf := make([]byte, 2+len(addr))
+	native.PutUint16(buf[:2], uint16(v.AddrFamily))
+	copy(buf[2:], addr)
+	return buf, nil
 }

 func (v *Via) Decode(b []byte) error {
+	native := nl.NativeEndian()
 	if len(b) < 6 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@route_linux.go` around lines 781 - 788, Define native := nl.NativeEndian() at
the start of Via.Encode and Via.Decode, replace binary.Write of netip.Addr with
manual serialization using v.Addr.AsSlice() (or addr.AsSlice()) and write the
address family as uint16 then the raw bytes to the buffer/reader, validating the
slice length matches the expected length for the AddrFamily (e.g., 4 for IPv4,
16 for IPv6) and return errors on mismatch; in Via.Decode read the family using
native, read the expected number of bytes and construct the netip.Addr from the
slice. Also update RouteGetWithOptions to set msg.Src_len =
uint8(options.SrcAddr.BitLen()) (not destination.BitLen()) so the source prefix
length is taken from options.SrcAddr.
🧹 Nitpick comments (3)
filter_linux.go (1)

1042-1045: Consider adding error handling for invalid address data.

The netip.AddrFromSlice return value is discarded with _. If the slice contains invalid address data, SrcAddr/DstAddr will be set to the zero value, which will later cause an error during re-encoding at lines 750-763.

For consistency with Flower.decode (which returns an error for invalid addresses at lines 216-218, 224-226, etc.), consider propagating the error here as well.

♻️ Suggested improvement
 case nl.TCA_TUNNEL_KEY_ENC_IPV6_SRC, nl.TCA_TUNNEL_KEY_ENC_IPV4_SRC:
-	action.(*TunnelKeyAction).SrcAddr, _ = netip.AddrFromSlice(adatum.Value)
+	addr, ok := netip.AddrFromSlice(adatum.Value)
+	if !ok {
+		return nil, fmt.Errorf("invalid tunnel key src address")
+	}
+	action.(*TunnelKeyAction).SrcAddr = addr
 case nl.TCA_TUNNEL_KEY_ENC_IPV6_DST, nl.TCA_TUNNEL_KEY_ENC_IPV4_DST:
-	action.(*TunnelKeyAction).DstAddr, _ = netip.AddrFromSlice(adatum.Value)
+	addr, ok := netip.AddrFromSlice(adatum.Value)
+	if !ok {
+		return nil, fmt.Errorf("invalid tunnel key dst address")
+	}
+	action.(*TunnelKeyAction).DstAddr = addr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@filter_linux.go` around lines 1042 - 1045, The switch currently discards the
boolean from netip.AddrFromSlice when populating
TunnelKeyAction.SrcAddr/DstAddr; change this to check the ok return value and
return an error (propagate) when the slice is not a valid address instead of
silently assigning the zero addr. Specifically, call
netip.AddrFromSlice(adatum.Value), verify ok, only assign to
action.(*TunnelKeyAction).SrcAddr or DstAddr when ok, and return a descriptive
error (e.g., "invalid tunnel key address for TCA_TUNNEL_KEY_ENC_IPV6_SRC" or
similar) so callers receive the failure (consistent with Flower.decode’s error
propagation).
conntrack_linux.go (1)

463-467: Consider handling parse failures for robustness.

The parse error from netip.AddrFromSlice is discarded. While kernel netlink data should be well-formed, silently ignoring parse failures could mask issues. The zero-value Addr (invalid) would propagate silently.

This is low risk since the data comes from the kernel, but for defensive coding, you might consider logging parse failures in debug scenarios.

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

In `@conntrack_linux.go` around lines 463 - 467, The code currently discards
errors from netip.AddrFromSlice when populating tpl.SrcIP and tpl.DstIP inside
the CTA_IP_* case, which can hide malformed data; update the parsing to check
the returned boolean/error and, on failure, leave the Addr as invalid but emit a
debug log (or increment a metric) identifying the field (SrcIP or DstIP) and the
offending byte slice, e.g. inside the switch case handling
nl.CTA_IP_V4_SRC/nl.CTA_IP_V6_SRC and nl.CTA_IP_V4_DST/nl.CTA_IP_V6_DST, call
netip.AddrFromSlice, test the ok flag, assign only on success and log a debug
message including the slice and which tpl field failed to parse so failures are
visible in debug builds.
route_test.go (1)

2253-2261: Add a mapped-IPv6 next-hop regression here.

This test only covers a normal IPv6 Via (2001::1), so it doesn’t lock down the bug from #1114. Please add a case that uses an IPv4-mapped IPv6 next hop like ::ffff:100.95.128.2 here (or in an equivalent Gw test) to prove the family detection no longer collapses it to IPv4.

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

In `@route_test.go` around lines 2253 - 2261, Add a regression test entry that
uses an IPv4-mapped IPv6 next-hop to ensure family detection stays IPv6: modify
the Route test case that constructs route := &Route{... MultiPath:
[]*NexthopInfo{{ LinkIndex: link.Attrs().Index, Via: &Via{ AddrFamily:
FAMILY_V6, Addr: netip.MustParseAddr("::ffff:100.95.128.2"), ... } } }} (or add
an equivalent Gw test) and assert that the Via.AddrFamily remains FAMILY_V6 and
the Addr parses as the mapped IPv6 address, so the code no longer collapses
mapped-IPv6 to IPv4.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@route_linux.go`:
- Around line 1627-1629: The code sets msg.Src_len using destination.BitLen()
but should derive it from the source address; change the assignment so that when
options != nil and options.SrcAddr.IsValid() you set msg.Src_len =
uint8(options.SrcAddr.BitLen()) (i.e., use options.SrcAddr.BitLen() instead of
destination.BitLen()) to serialize the correct source prefix length.

---

Outside diff comments:
In `@nl/tc_linux.go`:
- Around line 1626-1656: The IPv4 setter functions SetIPv4Src and SetIPv4Dst
must reject or unwrap IPv6 inputs before reading 4 bytes; call ip.Unmap() (or
use ip.As4()/ip.Is4()) and verify you have a valid IPv4 address before calling
NativeEndian().Uint32 on the byte slice. Update both TcPedit.SetIPv4Src and
TcPedit.SetIPv4Dst to obtain the 4-byte IPv4 form (e.g., via v4, ok := ip.As4()
or ip = ip.Unmap(); v4, ok := ip.As4()), return or error when ok is false, then
use that 4-byte value to populate tKey.Val and proceed as before.

In `@route_linux.go`:
- Around line 781-788: Define native := nl.NativeEndian() at the start of
Via.Encode and Via.Decode, replace binary.Write of netip.Addr with manual
serialization using v.Addr.AsSlice() (or addr.AsSlice()) and write the address
family as uint16 then the raw bytes to the buffer/reader, validating the slice
length matches the expected length for the AddrFamily (e.g., 4 for IPv4, 16 for
IPv6) and return errors on mismatch; in Via.Decode read the family using native,
read the expected number of bytes and construct the netip.Addr from the slice.
Also update RouteGetWithOptions to set msg.Src_len =
uint8(options.SrcAddr.BitLen()) (not destination.BitLen()) so the source prefix
length is taken from options.SrcAddr.

---

Nitpick comments:
In `@conntrack_linux.go`:
- Around line 463-467: The code currently discards errors from
netip.AddrFromSlice when populating tpl.SrcIP and tpl.DstIP inside the CTA_IP_*
case, which can hide malformed data; update the parsing to check the returned
boolean/error and, on failure, leave the Addr as invalid but emit a debug log
(or increment a metric) identifying the field (SrcIP or DstIP) and the offending
byte slice, e.g. inside the switch case handling
nl.CTA_IP_V4_SRC/nl.CTA_IP_V6_SRC and nl.CTA_IP_V4_DST/nl.CTA_IP_V6_DST, call
netip.AddrFromSlice, test the ok flag, assign only on success and log a debug
message including the slice and which tpl field failed to parse so failures are
visible in debug builds.

In `@filter_linux.go`:
- Around line 1042-1045: The switch currently discards the boolean from
netip.AddrFromSlice when populating TunnelKeyAction.SrcAddr/DstAddr; change this
to check the ok return value and return an error (propagate) when the slice is
not a valid address instead of silently assigning the zero addr. Specifically,
call netip.AddrFromSlice(adatum.Value), verify ok, only assign to
action.(*TunnelKeyAction).SrcAddr or DstAddr when ok, and return a descriptive
error (e.g., "invalid tunnel key address for TCA_TUNNEL_KEY_ENC_IPV6_SRC" or
similar) so callers receive the failure (consistent with Flower.decode’s error
propagation).

In `@route_test.go`:
- Around line 2253-2261: Add a regression test entry that uses an IPv4-mapped
IPv6 next-hop to ensure family detection stays IPv6: modify the Route test case
that constructs route := &Route{... MultiPath: []*NexthopInfo{{ LinkIndex:
link.Attrs().Index, Via: &Via{ AddrFamily: FAMILY_V6, Addr:
netip.MustParseAddr("::ffff:100.95.128.2"), ... } } }} (or add an equivalent Gw
test) and assert that the Via.AddrFamily remains FAMILY_V6 and the Addr parses
as the mapped IPv6 address, so the code no longer collapses mapped-IPv6 to IPv4.
🪄 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: e0d16dbf-24c6-4b6b-85f1-d93bed1e92ad

📥 Commits

Reviewing files that changed from the base of the PR and between 949f050 and ddc498c.

📒 Files selected for processing (20)
  • addr.go
  • addr_test.go
  • bridge_linux_test.go
  • conntrack_linux.go
  • conntrack_test.go
  • filter_linux.go
  • link_linux.go
  • netlink.go
  • nexthop.go
  • nexthop_linux.go
  • nexthop_test.go
  • nl/tc_linux.go
  • nl/tc_linux_test.go
  • nl/xfrm_linux.go
  • route_linux.go
  • route_test.go
  • rule_linux.go
  • xfrm_policy_linux.go
  • xfrm_policy_linux_test.go
  • xfrm_state_linux.go
✅ Files skipped from review due to trivial changes (2)
  • bridge_linux_test.go
  • nexthop_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • nexthop.go
  • nexthop_linux.go
  • nl/tc_linux_test.go
  • addr_test.go
  • netlink.go
  • xfrm_policy_linux_test.go
  • rule_linux.go
  • link_linux.go
  • nl/xfrm_linux.go

Comment thread route_linux.go
Comment on lines +1627 to 1629
if options != nil && options.SrcAddr.IsValid() {
msg.Src_len = uint8(destination.BitLen())
}

@coderabbitai coderabbitai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "route_linux.go" -type f

Repository: vishvananda/netlink

Length of output: 99


🏁 Script executed:

sed -n '1620,1640p' route_linux.go

Repository: vishvananda/netlink

Length of output: 662


🏁 Script executed:

sed -n '1600,1650p' route_linux.go

Repository: vishvananda/netlink

Length of output: 1672


🏁 Script executed:

sed -n '1590,1610p' route_linux.go

Repository: vishvananda/netlink

Length of output: 618


🏁 Script executed:

sed -n '1630,1670p' route_linux.go

Repository: vishvananda/netlink

Length of output: 938


🏁 Script executed:

sed -n '1670,1710p' route_linux.go

Repository: vishvananda/netlink

Length of output: 951


🏁 Script executed:

grep -n "msg.Src_len" route_linux.go

Repository: vishvananda/netlink

Length of output: 111


🏁 Script executed:

grep -n "Src_len\|Dst_len" route_linux.go | head -20

Repository: vishvananda/netlink

Length of output: 332


Set msg.Src_len from the source address, not the destination.

Line 1628 incorrectly uses destination.BitLen() for msg.Src_len. When options.SrcAddr is set, its prefix length should be used, not the destination's. This causes the serialized source prefix length to be wrong when the source and destination have different widths.

Proposed fix
 	if options != nil && options.SrcAddr.IsValid() {
-		msg.Src_len = uint8(destination.BitLen())
+		msg.Src_len = uint8(options.SrcAddr.BitLen())
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if options != nil && options.SrcAddr.IsValid() {
msg.Src_len = uint8(destination.BitLen())
}
if options != nil && options.SrcAddr.IsValid() {
msg.Src_len = uint8(options.SrcAddr.BitLen())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@route_linux.go` around lines 1627 - 1629, The code sets msg.Src_len using
destination.BitLen() but should derive it from the source address; change the
assignment so that when options != nil and options.SrcAddr.IsValid() you set
msg.Src_len = uint8(options.SrcAddr.BitLen()) (i.e., use
options.SrcAddr.BitLen() instead of destination.BitLen()) to serialize the
correct source prefix length.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The logic has been kept. Using either destination or source doesn't change anything, since they're both supposed to have the same IP family.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fdomain, that's a fair point. Since SrcAddr and destination must share the same IP family, destination.BitLen() and options.SrcAddr.BitLen() will always yield the same value (32 for IPv4, 128 for IPv6). The original net.IP code also derived a single bitlen from the destination and reused it for both Dst_len and Src_len, so the intent is consistent. I'll withdraw the suggestion.


✏️ Learnings added
Learnt from: fdomain
Repo: vishvananda/netlink PR: 1181
File: route_linux.go:1626-1628
Timestamp: 2026-04-13T19:44:56.939Z
Learning: In route_linux.go (vishvananda/netlink), `msg.Src_len` in `RouteGetWithOptions` is intentionally set using `destination.BitLen()` rather than `options.SrcAddr.BitLen()`. This is correct because SrcAddr and destination are always expected to be the same IP family (both IPv4 = 32 bits, or both IPv6 = 128 bits), so both expressions yield the same value. The original net.IP implementation also used a single bitlen variable for both Dst_len and Src_len.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: kyrtapz
Repo: vishvananda/netlink PR: 1159
File: bridge_linux.go:349-366
Timestamp: 2026-02-06T09:13:09.289Z
Learning: VNI filter operations (RTM_NEWTUNNEL, RTM_DELTUNNEL, RTM_GETTUNNEL) in bridge_linux.go do not require NLM_F_CREATE or NLM_F_APPEND flags. They only need NLM_F_ACK for acknowledgment, as confirmed by the iproute2 reference implementation in bridge/vni.c.

Learnt from: mattedallo
Repo: vishvananda/netlink PR: 1164
File: bridge_linux.go:44-48
Timestamp: 2026-02-09T08:32:40.092Z
Learning: In bridge_linux.go, BridgeVlanTunnelShow and BridgeVlanTunnelShowDev correctly use RTEXT_FILTER_BRVLAN (not RTEXT_FILTER_BRVLAN_COMPRESSED) for tunnel info dumps, matching iproute2's bridge vlan tunnelshow default behavior. RTEXT_FILTER_BRVLAN_COMPRESSED is only used with the -compressvlans flag to control VLAN range compression in display output, not tunnel info availability.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
nl/tc_linux.go (1)

1626-1644: ⚠️ Potential issue | 🟠 Major

Use Unmap() before encoding IPv4 fields in SetIPv4Src and SetIPv4Dst.

Lines 1627 and 1643 use ip.AsSlice() directly on a netip.Addr argument. For IPv4-mapped IPv6 addresses (e.g., ::ffff:192.0.2.1), AsSlice() returns the 16-byte IPv6 representation, so reading the first 4 bytes encodes the wrong data. Plain IPv6 addresses also get silently mis-encoded instead of being rejected. The previous net.IP.To4() behavior collapsed mapped IPv6 to their embedded IPv4, which this implementation loses.

Suggested fix
 func (p *TcPedit) SetIPv4Src(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	ip = ip.Unmap()
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])
@@
 func (p *TcPedit) SetIPv4Dst(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	ip = ip.Unmap()
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/tc_linux.go` around lines 1626 - 1644, The SetIPv4Src and SetIPv4Dst
methods must call ip = ip.Unmap() and validate that the result is an IPv4
address before extracting bytes: unmap the incoming netip.Addr to collapse
IPv4-mapped IPv6, check ip.Is4() (and return/handle error or skip if not IPv4),
then extract the 4-byte IPv4 value (use the 4-byte view/As4()/AsSlice() after
Unmap) and use that uint32 when populating TcPeditKey.Val; update both
SetIPv4Src and SetIPv4Dst accordingly to avoid encoding 16-byte IPv6
representations.
🧹 Nitpick comments (1)
nexthop_test.go (1)

55-60: Add a mapped-IPv6 gateway case here.

This only exercises a pure IPv6 gateway and a pure IPv4 gateway. The regression in issue #1114 is ::ffff:x.y.z.w being treated as IPv4, so this test can still pass while the original bug remains untested. Please add one case with something like netip.MustParseAddr("::ffff:100.95.128.2").

Also applies to: 123-128

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

In `@nexthop_test.go` around lines 55 - 60, Add a mapped-IPv6 gateway test case to
cover the ::ffff:IPv4-mapped address scenario: create an additional Nexthop
instance (similar to nh1/nh2) with Gateway set to
netip.MustParseAddr("::ffff:100.95.128.2") and the same fields (ID, OIF,
Protocol) used in the other cases, then assert expected behavior in the same way
the test asserts for pure IPv4/IPv6 gateways; update any table-driven test
entries (lines around the nh1/nh2 cases) to include this new case so the code
handling Nexthop/Gateway correctly treats mapped IPv6 addresses as IPv6-mapped
addresses rather than plain IPv4.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@filter_linux.go`:
- Around line 57-64: The mask decoding currently uses maskLen to count set bits
but doesn't verify contiguity, allowing non-CIDR masks to be accepted; update
maskLen (or the netlink decode path that converts masks to netip.Prefix) to
validate that all 1 bits are contiguous (i.e., once a 0 bit is seen no
subsequent 1 bits are allowed) and return an error if the mask is
non-contiguous, then propagate that error from the decoder so callers
constructing netip.Prefix (e.g., in the functions handling
EncSrc/EncDest/Src/Dest decoding) reject invalid masks instead of silently
converting them to a shorter prefix length.

In `@nl/xfrm_linux.go`:
- Around line 159-168: The invalid-input branch in XfrmAddress.FromIP currently
only clears x[4:16], leaving x[0:4] stale and leaking previous IPv4 bytes;
update the invalid case in the FromIP method to zero the entire XfrmAddress (all
16 bytes, e.g., copy(x[0:16], empty[:]) or equivalent) so a reused XfrmAddress
does not retain previous data.

In `@route_test.go`:
- Around line 2253-2262: Add a regression test case in route_test.go alongside
the existing Route/NexthopInfo/Via test that uses an IPv4-mapped IPv6 gateway
address to exercise the previously failing path; specifically, create a Route
with MultiPath containing a NexthopInfo whose Via has AddrFamily FAMILY_V6 but
Addr set to an IPv4-mapped IPv6 address (e.g. ::ffff:100.95.128.2) and assert
the same expected behavior/translation as the native-IPv6 case so the bug in
`#1114` cannot regress (look for the Route, NexthopInfo, Via, and FAMILY_V6
identifiers to place the new case).

---

Outside diff comments:
In `@nl/tc_linux.go`:
- Around line 1626-1644: The SetIPv4Src and SetIPv4Dst methods must call ip =
ip.Unmap() and validate that the result is an IPv4 address before extracting
bytes: unmap the incoming netip.Addr to collapse IPv4-mapped IPv6, check
ip.Is4() (and return/handle error or skip if not IPv4), then extract the 4-byte
IPv4 value (use the 4-byte view/As4()/AsSlice() after Unmap) and use that uint32
when populating TcPeditKey.Val; update both SetIPv4Src and SetIPv4Dst
accordingly to avoid encoding 16-byte IPv6 representations.

---

Nitpick comments:
In `@nexthop_test.go`:
- Around line 55-60: Add a mapped-IPv6 gateway test case to cover the
::ffff:IPv4-mapped address scenario: create an additional Nexthop instance
(similar to nh1/nh2) with Gateway set to
netip.MustParseAddr("::ffff:100.95.128.2") and the same fields (ID, OIF,
Protocol) used in the other cases, then assert expected behavior in the same way
the test asserts for pure IPv4/IPv6 gateways; update any table-driven test
entries (lines around the nh1/nh2 cases) to include this new case so the code
handling Nexthop/Gateway correctly treats mapped IPv6 addresses as IPv6-mapped
addresses rather than plain IPv4.
🪄 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: 13e484df-ceda-44e0-9658-34f05edcb99a

📥 Commits

Reviewing files that changed from the base of the PR and between ddc498c and 2fc33b8.

📒 Files selected for processing (24)
  • addr.go
  • addr_test.go
  • bridge_linux_test.go
  • conntrack_linux.go
  • conntrack_test.go
  • filter_linux.go
  • filter_test.go
  • link_linux.go
  • netlink.go
  • nexthop.go
  • nexthop_linux.go
  • nexthop_test.go
  • nl/tc_linux.go
  • nl/tc_linux_test.go
  • nl/xfrm_linux.go
  • route_linux.go
  • route_test.go
  • rule_linux.go
  • rule_test.go
  • socket_linux.go
  • socket_test.go
  • xfrm_policy_linux.go
  • xfrm_policy_linux_test.go
  • xfrm_state_linux.go
✅ Files skipped from review due to trivial changes (4)
  • bridge_linux_test.go
  • socket_test.go
  • addr_test.go
  • rule_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • nexthop.go
  • nl/tc_linux_test.go
  • rule_linux.go
  • socket_linux.go
  • xfrm_policy_linux_test.go
  • link_linux.go
  • route_linux.go

Comment thread filter_linux.go
Comment on lines 57 to +64
type Flower struct {
FilterAttrs
ClassId uint32
DestIP net.IP
DestIPMask net.IPMask
SrcIP net.IP
SrcIPMask net.IPMask
Dest netip.Prefix
Src netip.Prefix
EthType uint16
EncDestIP net.IP
EncDestIPMask net.IPMask
EncSrcIP net.IP
EncSrcIPMask net.IPMask
EncDest netip.Prefix
EncSrc netip.Prefix

@coderabbitai coderabbitai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Can Go's net/netip.Prefix represent non-contiguous IP masks, or only contiguous CIDR prefixes?

💡 Result:

No, Go's net/netip.Prefix can only represent contiguous CIDR prefixes, not non-contiguous IP masks.

Citations:


🏁 Script executed:

fd -n "filter_linux.go" --type f

Repository: vishvananda/netlink

Length of output: 293


🏁 Script executed:

fd "filter_linux.go" --type f

Repository: vishvananda/netlink

Length of output: 78


🏁 Script executed:

wc -l filter_linux.go

Repository: vishvananda/netlink

Length of output: 83


🏁 Script executed:

sed -n '57,64p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 264


🏁 Script executed:

sed -n '203,248p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 1613


🏁 Script executed:

sed -n '300,327p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 514


🏁 Script executed:

sed -n '180,210p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 798


🏁 Script executed:

sed -n '100,180p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 3041


🏁 Script executed:

sed -n '70,105p' filter_linux.go

Repository: vishvananda/netlink

Length of output: 822


Validate CIDR contiguity when decoding masks, or reject non-CIDR masks explicitly.

The maskLen function naively counts set bits without validating contiguity. When decoding netlink attributes, a non-CIDR mask like ff00ff00 would be decoded as /16 instead of being rejected, causing the filter to silently interpret a mismatched mask. Since netip.Prefix only supports contiguous CIDR prefixes, the decode path should validate mask contiguity bit-by-bit or return an error for invalid masks.

Suggested fix
-	maskLen := func(d []byte) (r int) {
-		for _, v := range d {
-			r += bits.OnesCount8(v)
-		}
-		return
-	}
+	maskLen := func(d []byte) (int, error) {
+		n := 0
+		seenZero := false
+		for _, v := range d {
+			for bit := 7; bit >= 0; bit-- {
+				one := v&(1<<bit) != 0
+				if seenZero && one {
+					return 0, fmt.Errorf("non-CIDR mask")
+				}
+				if one {
+					n++
+				} else {
+					seenZero = true
+				}
+			}
+		}
+		return n, nil
+	}

Applies to lines 203–248, 300–327.

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

In `@filter_linux.go` around lines 57 - 64, The mask decoding currently uses
maskLen to count set bits but doesn't verify contiguity, allowing non-CIDR masks
to be accepted; update maskLen (or the netlink decode path that converts masks
to netip.Prefix) to validate that all 1 bits are contiguous (i.e., once a 0 bit
is seen no subsequent 1 bits are allowed) and return an error if the mask is
non-contiguous, then propagate that error from the decoder so callers
constructing netip.Prefix (e.g., in the functions handling
EncSrc/EncDest/Src/Dest decoding) reject invalid masks instead of silently
converting them to a shorter prefix length.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We're not supposed to receive invalid CIDR mask from netlink. And there was no "contiguity" check before.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fdomain, that's a fair point — the Linux kernel always emits well-formed CIDR masks over netlink, so there's no real risk of receiving a non-contiguous mask in practice, and the pre-migration code had no such check either. I'll drop this suggestion.


✏️ Learnings added
Learnt from: fdomain
Repo: vishvananda/netlink PR: 1181
File: filter_linux.go:57-64
Timestamp: 2026-04-13T19:48:23.020Z
Learning: In vishvananda/netlink (filter_linux.go), the `maskLen` helper used in `Flower.decode` does not need to validate CIDR contiguity. The Linux kernel always sends valid contiguous CIDR masks over netlink, and the original `net.IP`/`net.IPMask`-based implementation had no such check either. Do not flag the absence of contiguity validation in netlink mask decoding as an issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: fdomain
Repo: vishvananda/netlink PR: 1181
File: route_linux.go:1626-1628
Timestamp: 2026-04-13T19:44:56.939Z
Learning: In route_linux.go (vishvananda/netlink), `msg.Src_len` in `RouteGetWithOptions` is intentionally set using `destination.BitLen()` rather than `options.SrcAddr.BitLen()`. This is correct because SrcAddr and destination are always expected to be the same IP family (both IPv4 = 32 bits, or both IPv6 = 128 bits), so both expressions yield the same value. The original net.IP implementation also used a single bitlen variable for both Dst_len and Src_len.

Learnt from: kyrtapz
Repo: vishvananda/netlink PR: 1159
File: bridge_linux.go:349-366
Timestamp: 2026-02-06T09:13:09.289Z
Learning: VNI filter operations (RTM_NEWTUNNEL, RTM_DELTUNNEL, RTM_GETTUNNEL) in bridge_linux.go do not require NLM_F_CREATE or NLM_F_APPEND flags. They only need NLM_F_ACK for acknowledgment, as confirmed by the iproute2 reference implementation in bridge/vni.c.

Learnt from: mattedallo
Repo: vishvananda/netlink PR: 1164
File: bridge_linux.go:44-48
Timestamp: 2026-02-09T08:32:40.092Z
Learning: In bridge_linux.go, BridgeVlanTunnelShow and BridgeVlanTunnelShowDev correctly use RTEXT_FILTER_BRVLAN (not RTEXT_FILTER_BRVLAN_COMPRESSED) for tunnel info dumps, matching iproute2's bridge vlan tunnelshow default behavior. RTEXT_FILTER_BRVLAN_COMPRESSED is only used with the -compressvlans flag to control VLAN range compression in display output, not tunnel info availability.

Comment thread nl/xfrm_linux.go
Comment thread route_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
socket_linux.go (1)

276-290: ⚠️ Potential issue | 🟠 Major

SocketDestroy still uses the old To4() classification path.

Lines 276-290 force IPv6 through To4() and hard-code Family: unix.AF_INET, leaving a family-mismatch issue. The parallel SocketGet function correctly handles this using .Unmap() and dynamic family detection (Is4()/Is6()). Apply the same pattern here: either derive the family dynamically or reject non-IPv4 addresses and document as IPv4-only.

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

In `@socket_linux.go` around lines 276 - 290, SocketDestroy currently forces IPv4
by calling To4() on localTCP.IP/remoteTCP.IP and hard-coding Family:
unix.AF_INET, causing family mismatches; change it to follow the SocketGet
pattern: use .Unmap() on the IPs and detect family with addr.Is4()/Is6(), set
socketRequest.Family dynamically (unix.AF_INET or unix.AF_INET6) and construct
netip.AddrFromSlice from the appropriate 4- or 16-byte slice, or explicitly
return an error if you intend to only support IPv4; update handling of
localIP/remoteIP and Family in the socketRequest creation accordingly.
♻️ Duplicate comments (2)
nl/xfrm_linux.go (1)

159-168: ⚠️ Potential issue | 🟠 Major

Zero the entire XfrmAddress on invalid input.

The invalid branch only clears x[4:16]. If this struct is reused after an IPv4 write, x[0:4] keeps stale bytes and the reset leaks the previous address.

Proposed fix
 func (x *XfrmAddress) FromIP(ip netip.Addr) {
-	var empty = [16]byte{}
+	*x = XfrmAddress{}
 	if !ip.IsValid() {
-		copy(x[4:16], empty[:])
+		return
 	} else if ip.Is4() {
 		copy(x[0:4], ip.AsSlice())
-		copy(x[4:16], empty[:12])
 	} else {
 		copy(x[0:16], ip.AsSlice())
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/xfrm_linux.go` around lines 159 - 168, The FromIP method on XfrmAddress
leaves bytes 0:4 intact when ip is invalid, so stale IPv4 data can leak; update
XfrmAddress.FromIP to zero the entire receiver when ip.IsValid() is false (e.g.,
copy a zeroed 16-byte array into x[:]) and keep the current behavior for
ip.Is4() (write first 4 bytes then zero the remaining 12) and non-IPv4 (copy all
16 bytes) to ensure no stale bytes remain.
filter_linux.go (1)

203-208: ⚠️ Potential issue | 🟠 Major

Reject non-CIDR masks before converting them to netip.Prefix.

maskLen just counts set bits. A mask like ff00ff00 becomes /16, but netip.Prefix can only represent contiguous CIDR masks, so the decode path silently changes the filter the kernel returned.

Proposed fix
-	maskLen := func(d []byte) (r int) {
-		for _, v := range d {
-			r += bits.OnesCount8(v)
-		}
-		return
-	}
+	maskLen := func(d []byte) (int, error) {
+		n := 0
+		seenZero := false
+		for _, v := range d {
+			for bit := 7; bit >= 0; bit-- {
+				one := v&(1<<bit) != 0
+				if seenZero && one {
+					return 0, fmt.Errorf("non-CIDR flower mask")
+				}
+				if one {
+					n++
+				} else {
+					seenZero = true
+				}
+			}
+		}
+		return n, nil
+	}

Then propagate the returned error at Lines 221, 230, 239, and 248 instead of accepting the decoded mask unconditionally.

Can Go's net/netip.Prefix represent non-contiguous IP masks, or only contiguous CIDR prefixes?

Also applies to: 220-248, 300-327

🧹 Nitpick comments (2)
rule_linux.go (1)

380-384: Update stale documentation comment.

The comment still references *net.IPNet and "nil IPNet" but the function now uses netip.Prefix. Consider updating for accuracy.

📝 Suggested documentation update
-// ruleIPNetEqual compares two *net.IPNet for rule filtering purposes.
-// Unlike ipNetEqual in route.go, this treats a nil IPNet as equivalent
+// ruleIPNetEqual compares two netip.Prefix for rule filtering purposes.
+// Unlike ipNetEqual in route.go, this treats an invalid Prefix as equivalent
 // to 0.0.0.0/0 or ::/0, which is how the kernel represents "from all" /
 // "to all" rules (it omits FRA_SRC/FRA_DST when prefix length is 0).
 // Two /0 prefixes are always equal regardless of IP (e.g. 0.0.0.0/0 == 1.2.3.4/0).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rule_linux.go` around lines 380 - 384, The doc comment for ruleIPNetEqual is
stale: it refers to *net.IPNet and "nil IPNet" but the function now uses
netip.Prefix; update the comment to accurately describe that ruleIPNetEqual
compares netip.Prefix values, treats an invalid/zero-value netip.Prefix (or a
prefix with bits == 0) as the kernel's "from all"/"to all" (/0) representation,
and that any two /0 prefixes are considered equal regardless of underlying
address; keep the examples and intent the same but replace mentions of
nil/*net.IPNet with netip.Prefix and invalid/zero-value prefix semantics and
reference the function name ruleIPNetEqual.
addr_test.go (1)

237-246: Avoid per-iteration timeout allocation in expectAddrUpdate.

time.After(time.Minute) is recreated on every loop pass. Hoisting it outside the loop avoids repeated timer allocation while preserving behavior.

♻️ Proposed refactor
 func expectAddrUpdate(ch <-chan AddrUpdate, add bool, dst netip.Addr) bool {
+	timeout := time.After(time.Minute)
 	for {
-		timeout := time.After(time.Minute)
 		select {
 		case update := <-ch:
 			if update.NewAddr == add && update.LinkAddress.Addr() == dst {
 				return true
 			}
 		case <-timeout:
 			return false
 		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@addr_test.go` around lines 237 - 246, The loop in expectAddrUpdate recreates
time.After(time.Minute) on every iteration; move the timeout channel creation
out of the loop to avoid per-iteration timer allocation: before the for { ... }
create a single timeoutCh := time.After(time.Minute) (or a time.NewTimer and
defer timer.Stop() if you need to stop/reset) and then use case <-timeoutCh:
inside the select, keeping the existing checks against update.NewAddr and
update.LinkAddress.Addr() == dst in the expectAddrUpdate function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nl/xfrm_linux.go`:
- Around line 144-156: The current ToPrefix falls back to x.ToIP() for an
all-zero buffer which decodes as 0.0.0.0 and breaks IPv6 selectors (e.g. ::/64);
change the empty-buffer branch to switch on family and return
netip.PrefixFrom(v6zero, int(prefixlen)) for FAMILY_V6 (and
netip.PrefixFrom(v4zero, int(prefixlen)) for FAMILY_V4) instead of falling
through to x.ToIP(); update the XfrmAddress.ToPrefix function to handle the
all-zero case by returning the appropriate zero address prefix based on family.

In `@socket_linux.go`:
- Around line 187-216: SocketGet is collapsing IPv6-mapped IPv4 by calling
Unmap() on addresses; remove the Unmap() calls in the TCP and UDP branches (the
spots that set localIP and remoteIP) so you preserve the original netip.Addr
representation, keep the rest of the assignments (localPort/remotePort/protocol)
the same, and let the existing family detection (the checks that set family to
unix.AF_INET or unix.AF_INET6) operate on the unmodified netip.Addr values;
alternatively consider changing the API to accept netip.Addr directly instead of
net.TCPAddr/net.UDPAddr to avoid losing family semantics.

---

Outside diff comments:
In `@socket_linux.go`:
- Around line 276-290: SocketDestroy currently forces IPv4 by calling To4() on
localTCP.IP/remoteTCP.IP and hard-coding Family: unix.AF_INET, causing family
mismatches; change it to follow the SocketGet pattern: use .Unmap() on the IPs
and detect family with addr.Is4()/Is6(), set socketRequest.Family dynamically
(unix.AF_INET or unix.AF_INET6) and construct netip.AddrFromSlice from the
appropriate 4- or 16-byte slice, or explicitly return an error if you intend to
only support IPv4; update handling of localIP/remoteIP and Family in the
socketRequest creation accordingly.

---

Duplicate comments:
In `@nl/xfrm_linux.go`:
- Around line 159-168: The FromIP method on XfrmAddress leaves bytes 0:4 intact
when ip is invalid, so stale IPv4 data can leak; update XfrmAddress.FromIP to
zero the entire receiver when ip.IsValid() is false (e.g., copy a zeroed 16-byte
array into x[:]) and keep the current behavior for ip.Is4() (write first 4 bytes
then zero the remaining 12) and non-IPv4 (copy all 16 bytes) to ensure no stale
bytes remain.

---

Nitpick comments:
In `@addr_test.go`:
- Around line 237-246: The loop in expectAddrUpdate recreates
time.After(time.Minute) on every iteration; move the timeout channel creation
out of the loop to avoid per-iteration timer allocation: before the for { ... }
create a single timeoutCh := time.After(time.Minute) (or a time.NewTimer and
defer timer.Stop() if you need to stop/reset) and then use case <-timeoutCh:
inside the select, keeping the existing checks against update.NewAddr and
update.LinkAddress.Addr() == dst in the expectAddrUpdate function.

In `@rule_linux.go`:
- Around line 380-384: The doc comment for ruleIPNetEqual is stale: it refers to
*net.IPNet and "nil IPNet" but the function now uses netip.Prefix; update the
comment to accurately describe that ruleIPNetEqual compares netip.Prefix values,
treats an invalid/zero-value netip.Prefix (or a prefix with bits == 0) as the
kernel's "from all"/"to all" (/0) representation, and that any two /0 prefixes
are considered equal regardless of underlying address; keep the examples and
intent the same but replace mentions of nil/*net.IPNet with netip.Prefix and
invalid/zero-value prefix semantics and reference the function name
ruleIPNetEqual.
🪄 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: c8f6731a-3a2b-40e8-bd41-e6073b077ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc33b8 and edaebe6.

📒 Files selected for processing (25)
  • addr.go
  • addr_test.go
  • bridge_linux_test.go
  • conntrack_linux.go
  • conntrack_test.go
  • filter_linux.go
  • filter_test.go
  • link.go
  • link_linux.go
  • netlink.go
  • nexthop.go
  • nexthop_linux.go
  • nexthop_test.go
  • nl/tc_linux.go
  • nl/tc_linux_test.go
  • nl/xfrm_linux.go
  • route_linux.go
  • route_test.go
  • rule_linux.go
  • rule_test.go
  • socket_linux.go
  • socket_test.go
  • xfrm_policy_linux.go
  • xfrm_policy_linux_test.go
  • xfrm_state_linux.go
✅ Files skipped from review due to trivial changes (5)
  • bridge_linux_test.go
  • nexthop.go
  • nexthop_test.go
  • socket_test.go
  • route_linux.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • nl/tc_linux_test.go
  • nexthop_linux.go
  • xfrm_policy_linux_test.go
  • rule_test.go
  • conntrack_test.go
  • link.go
  • route_test.go

Comment thread nl/xfrm_linux.go
Comment thread socket_linux.go
Comment on lines +187 to 216
localIP, _ = netip.AddrFromSlice(l.IP)
localIP = localIP.Unmap()
localPort = uint16(l.Port)
remoteIP = r.IP
remoteIP, _ = netip.AddrFromSlice(r.IP)
remoteIP = remoteIP.Unmap()
remotePort = uint16(r.Port)
protocol = unix.IPPROTO_TCP
case *net.UDPAddr:
r, ok := remote.(*net.UDPAddr)
if !ok {
return nil, ErrNotImplemented
}
localIP = l.IP
localIP, _ = netip.AddrFromSlice(l.IP)
localIP = localIP.Unmap()
localPort = uint16(l.Port)
remoteIP = r.IP
remoteIP, _ = netip.AddrFromSlice(r.IP)
remoteIP = remoteIP.Unmap()
remotePort = uint16(r.Port)
protocol = unix.IPPROTO_UDP
default:
return nil, ErrNotImplemented
}

var family uint8
if localIP.To4() != nil && remoteIP.To4() != nil {
if localIP.Is4() && remoteIP.Is4() {
family = unix.AF_INET
}

if family == 0 && localIP.To16() != nil && remoteIP.To16() != nil {
if family == 0 && localIP.Is6() && remoteIP.Is6() {
family = unix.AF_INET6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read socket_linux.go
find . -name "socket_linux.go" -type f

Repository: vishvananda/netlink

Length of output: 80


🏁 Script executed:

# Read the socket_linux.go file to see the code at the specified lines
cat -n socket_linux.go | head -220 | tail -50

Repository: vishvananda/netlink

Length of output: 1730


🏁 Script executed:

# Let me understand the netip.Addr.Unmap() behavior and look at how Socket is used
# First, check the Socket struct and its deserialize method
cat -n socket_linux.go | grep -A 30 "type Socket struct"

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

# Search for the Socket struct definition
rg "type Socket" socket_linux.go -A 10

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

# Let's see the full socket_linux.go file structure
wc -l socket_linux.go

Repository: vishvananda/netlink

Length of output: 82


🏁 Script executed:

# Look at the imports and early structure
cat -n socket_linux.go | head -50

Repository: vishvananda/netlink

Length of output: 1277


🏁 Script executed:

# Let's check what comes after line 220 in SocketGet to understand the complete picture
cat -n socket_linux.go | sed -n '220,240p'

Repository: vishvananda/netlink

Length of output: 809


🏁 Script executed:

# Let's look at the Socket type and see how it's deserialized
rg "func.*deserialize|type Socket|SocketID" socket_linux.go | head -20

Repository: vishvananda/netlink

Length of output: 340


🏁 Script executed:

# Let's find the Socket type definition
rg "^type Socket " socket_linux.go -A 20

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

# Search for Socket struct
cat -n socket_linux.go | grep -B 5 "func.*Socket.*deserialize"

Repository: vishvananda/netlink

Length of output: 390


🏁 Script executed:

# Let's search more broadly for Socket type definition
rg "^type.*Socket" socket_linux.go -B 2 -A 15

Repository: vishvananda/netlink

Length of output: 499


🏁 Script executed:

# Let's search in a broader way for all type definitions
rg "^type" socket_linux.go | head -30

Repository: vishvananda/netlink

Length of output: 173


🏁 Script executed:

# Socket might be defined in a different file. Let's search the repo
find . -name "*.go" -type f | xargs grep -l "^type Socket struct" 2>/dev/null

Repository: vishvananda/netlink

Length of output: 74


🏁 Script executed:

# Look at the Socket type and its deserialize method
cat -n socket.go | head -100

Repository: vishvananda/netlink

Length of output: 2936


🏁 Script executed:

# Now let's look at the deserialize method in socket_linux.go
cat -n socket_linux.go | sed -n '109,156p'

Repository: vishvananda/netlink

Length of output: 1728


🌐 Web query:

netip.Addr.Unmap() Go documentation behavior IPv4-mapped IPv6

💡 Result:

The netip.Addr.Unmap method in Go's net/netip package removes the IPv4-mapped IPv6 address prefix (::ffff:0:0/96) from an IPv6 address if present, returning the underlying IPv4 address as a netip.Addr. For non IPv4-mapped IPv6 addresses (pure IPv4 or IPv6), it returns the address unmodified. Signature: func (ip Addr) Unmap Addr Behavior: - If ip.Is4In6 is true (IPv4-mapped IPv6 per RFC 4291), it sets the internal zone to IPv4 mode (z4), effectively converting to IPv4 Addr. - Otherwise, returns ip unchanged. Example from source and docs: - "::ffff:192.0.2.1".Unmap -> "192.0.2.1" (IPv4 Addr) - "2001:db8::1".Unmap -> "2001:db8::1" (unchanged IPv6 Addr) - "192.0.2.1".Unmap -> "192.0.2.1" (unchanged IPv4 Addr) Source code (netip.go): func (ip Addr) Unmap Addr { if ip.Is4In6 { ip.z = z4 } return ip } Related methods: - Is4 returns false for IPv4-mapped IPv6; use Unmap.Is4 to check underlying IPv4. - Is4In6 detects IPv4-mapped IPv6 addresses. - AddrFrom16 leaves IPv4-mapped as IPv6; use Unmap to convert. This behavior is consistent across Go 1.18+ (pkg.go.dev/net/netip, tip.golang.org/src/net/netip/netip.go).

Citations:


SocketGet still collapses mapped IPv6 endpoints to IPv4.

Lines 188, 191, 200, and 203 call Unmap(), which converts IPv4-mapped IPv6 addresses (e.g., ::ffff:192.0.2.1) to plain IPv4. This causes the family check at lines 211-216 to select AF_INET instead of AF_INET6, so the lookup request goes out with the wrong address family. This defeats proper support for mapped IPv6 and may cause lookups to fail if the socket was stored with an AF_INET6 family. Consider using a netip-native entry point instead of net.TCPAddr / net.UDPAddr to preserve address family semantics.

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

In `@socket_linux.go` around lines 187 - 216, SocketGet is collapsing IPv6-mapped
IPv4 by calling Unmap() on addresses; remove the Unmap() calls in the TCP and
UDP branches (the spots that set localIP and remoteIP) so you preserve the
original netip.Addr representation, keep the rest of the assignments
(localPort/remotePort/protocol) the same, and let the existing family detection
(the checks that set family to unix.AF_INET or unix.AF_INET6) operate on the
unmodified netip.Addr values; alternatively consider changing the API to accept
netip.Addr directly instead of net.TCPAddr/net.UDPAddr to avoid losing family
semantics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
nl/tc_linux.go (1)

1626-1656: ⚠️ Potential issue | 🟠 Major

SetIPv4Src/SetIPv4Dst do not validate IPv4 input and read incorrect bytes for IPv6 addresses.

Lines 1627 and 1643 call ip.AsSlice() directly on a netip.Addr that could be IPv6. For IPv6 (or IPv4-mapped IPv6), AsSlice() returns 16 bytes; Uint32() then consumes only the first four bytes, silently encoding the wrong address. The IPv6 setter methods (SetIPv6Src/SetIPv6Dst at lines 1491 and 1567) correctly use As16() first. Add an Is4() check and use As4() to extract the IPv4 bytes explicitly, matching the pattern used in SetSrcIP/SetDstIP wrapper methods.

🛠️ Suggested fix
 func (p *TcPedit) SetIPv4Src(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])
 
 	tKey := TcPeditKey{}
 	tKeyEx := TcPeditKeyEx{}
@@
 func (p *TcPedit) SetIPv4Dst(ip netip.Addr) {
-	u32 := NativeEndian().Uint32(ip.AsSlice())
+	if !ip.Is4() {
+		return
+	}
+	ip4 := ip.As4()
+	u32 := NativeEndian().Uint32(ip4[:])
 
 	tKey := TcPeditKey{}
 	tKeyEx := TcPeditKeyEx{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/tc_linux.go` around lines 1626 - 1656, The IPv4 setters must validate and
extract IPv4 bytes explicitly: in TcPedit.SetIPv4Src and TcPedit.SetIPv4Dst
check ip.Is4() and return early if false, then call ip.As4() and pass that
4-byte slice to NativeEndian().Uint32(...) (instead of ip.AsSlice()), keeping
the rest of the logic (tKey.Val, tKey.Off, tKeyEx.HeaderType/Cmd, appending to
p.Keys/p.KeysEx and incrementing p.Sel.NKeys) unchanged so they mirror the
pattern used by SetSrcIP/SetDstIP and the IPv6 setters.
♻️ Duplicate comments (1)
rule_test.go (1)

676-679: ⚠️ Potential issue | 🟡 Minor

Reuse ruleIPNetEqual in this helper.

Masked().String() fixes host-bit canonicalization, but it still treats an explicit /0 prefix as different from the invalid prefix the kernel returns for “from/to all”. Reusing the production helper keeps the test logic aligned with RuleListFiltered and avoids another edge-case mismatch.

🔧 Suggested change
 func ruleEquals(a, b Rule) bool {
 	return a.Table == b.Table &&
-		((!a.Src.IsValid() && !b.Src.IsValid()) ||
-			(a.Src.IsValid() && b.Src.IsValid() && a.Src.Masked().String() == b.Src.Masked().String())) &&
-		((!a.Dst.IsValid() && !b.Dst.IsValid()) ||
-			(a.Dst.IsValid() && b.Dst.IsValid() && a.Dst.Masked().String() == b.Dst.Masked().String())) &&
+		ruleIPNetEqual(a.Src, b.Src) &&
+		ruleIPNetEqual(a.Dst, b.Dst) &&
 		a.OifName == b.OifName &&
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rule_test.go` around lines 676 - 679, The test currently compares prefixes
using Masked().String(), which misses the kernel's "from/to all" invalid vs
explicit /0 edge case; replace those comparisons with calls to the production
helper ruleIPNetEqual to ensure canonicalization matches RuleListFiltered
behavior—i.e., in the helper that checks a.Src vs b.Src and a.Dst vs b.Dst, call
ruleIPNetEqual(a.Src, b.Src) and ruleIPNetEqual(a.Dst, b.Dst) instead of
comparing Masked().String().
🧹 Nitpick comments (1)
filter_test.go (1)

1269-1271: Add a mapped-IPv6 tunnel-key case here.

This fixture only covers plain IPv4. Since the migrated encode path now branches on netip.Addr.Is4()/Is6(), a ::ffff:10.10.10.{1,2} subtest is the case that actually locks in the family-detection regression this PR is fixing.

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

In `@filter_test.go` around lines 1269 - 1271, Add a new subtest/fixture that uses
an IPv4-mapped IPv6 address for the tunnel key to exercise the Is4/Is6
branching: create a TunnelKeyAction via NewTunnelKeyAction() and set
tunnelAct.SrcAddr and tunnelAct.DstAddr to
netip.MustParseAddr("::ffff:10.10.10.1") and
netip.MustParseAddr("::ffff:10.10.10.2") (mirroring the existing IPv4 case),
then include assertions identical to the IPv4 test so the encode path that
checks netip.Addr.Is4()/Is6() is validated for mapped-IPv6 addresses as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@filter_test.go`:
- Around line 2158-2159: The test currently sets PeditAction.SrcIP but only
asserts the decoded type; update the test to assert the SrcIP value round-trips
correctly by comparing the original peditAction.SrcIP to the parsed/decoded
action's SrcIP (after extracting the decoded action from filter.Actions), so any
regression in SetSrcIP or parsePeditExtendedKeys fails the test; locate the
PeditAction instance and the decoded action assertion and add a strict equality
check between their SrcIP values.

In `@link.go`:
- Around line 1285-1289: Vti.Type() regresses when Local is the zero-value
because Is6() is false; change the check to mirror Gretap/Gretun by using
!vti.Local.Is4() (or equivalently the previous To4()==nil logic) so an
unset/IPv6 Local still returns "vti6"; update the conditional in Vti.Type to use
Local.Is4/Is6 consistently with Gretap and Gretun (reference: Vti.Type,
vti.Local, Local.Is4, Local.Is6, Gretap, Gretun).

In `@nl/tc_linux.go`:
- Around line 1551-1565: The SetDstIP and SetSrcIP methods on TcPedit must first
validate the netip.Addr to avoid routing invalid zero-value addresses into the
IPv6 path; add an ip.IsValid() guard at the start of both TcPedit.SetDstIP and
TcPedit.SetSrcIP and return early if false so you only call
SetIPv4Dst/SetIPv6Dst and SetIPv4Src/SetIPv6Src for valid addresses (this
prevents downstream calls like As16() on an invalid addr).

---

Outside diff comments:
In `@nl/tc_linux.go`:
- Around line 1626-1656: The IPv4 setters must validate and extract IPv4 bytes
explicitly: in TcPedit.SetIPv4Src and TcPedit.SetIPv4Dst check ip.Is4() and
return early if false, then call ip.As4() and pass that 4-byte slice to
NativeEndian().Uint32(...) (instead of ip.AsSlice()), keeping the rest of the
logic (tKey.Val, tKey.Off, tKeyEx.HeaderType/Cmd, appending to p.Keys/p.KeysEx
and incrementing p.Sel.NKeys) unchanged so they mirror the pattern used by
SetSrcIP/SetDstIP and the IPv6 setters.

---

Duplicate comments:
In `@rule_test.go`:
- Around line 676-679: The test currently compares prefixes using
Masked().String(), which misses the kernel's "from/to all" invalid vs explicit
/0 edge case; replace those comparisons with calls to the production helper
ruleIPNetEqual to ensure canonicalization matches RuleListFiltered
behavior—i.e., in the helper that checks a.Src vs b.Src and a.Dst vs b.Dst, call
ruleIPNetEqual(a.Src, b.Src) and ruleIPNetEqual(a.Dst, b.Dst) instead of
comparing Masked().String().

---

Nitpick comments:
In `@filter_test.go`:
- Around line 1269-1271: Add a new subtest/fixture that uses an IPv4-mapped IPv6
address for the tunnel key to exercise the Is4/Is6 branching: create a
TunnelKeyAction via NewTunnelKeyAction() and set tunnelAct.SrcAddr and
tunnelAct.DstAddr to netip.MustParseAddr("::ffff:10.10.10.1") and
netip.MustParseAddr("::ffff:10.10.10.2") (mirroring the existing IPv4 case),
then include assertions identical to the IPv4 test so the encode path that
checks netip.Addr.Is4()/Is6() is validated for mapped-IPv6 addresses as well.
🪄 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: 602bea86-b66c-467c-8e6b-07e79a3fb518

📥 Commits

Reviewing files that changed from the base of the PR and between edaebe6 and 3ee2da8.

📒 Files selected for processing (26)
  • addr.go
  • addr_test.go
  • bridge_linux_test.go
  • conntrack_linux.go
  • conntrack_test.go
  • filter_linux.go
  • filter_test.go
  • gtp_linux.go
  • link.go
  • link_linux.go
  • netlink.go
  • nexthop.go
  • nexthop_linux.go
  • nexthop_test.go
  • nl/tc_linux.go
  • nl/tc_linux_test.go
  • nl/xfrm_linux.go
  • route_linux.go
  • route_test.go
  • rule_linux.go
  • rule_test.go
  • socket_linux.go
  • socket_test.go
  • xfrm_policy_linux.go
  • xfrm_policy_linux_test.go
  • xfrm_state_linux.go
✅ Files skipped from review due to trivial changes (4)
  • bridge_linux_test.go
  • nexthop_test.go
  • xfrm_policy_linux.go
  • route_linux.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • nexthop.go
  • socket_test.go
  • nl/tc_linux_test.go
  • conntrack_linux.go
  • gtp_linux.go
  • link_linux.go
  • route_test.go
  • socket_linux.go

Comment thread filter_test.go
Comment on lines +2158 to 2159
peditAction.SrcIP = netip.MustParseAddr("ffff::fff2")
filter.Actions = append(filter.Actions, peditAction)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the migrated PeditAction.SrcIP round-trip.

The test now sets SrcIP, but it still only checks that the decoded action is a *PeditAction. A regression in SetSrcIP or parsePeditExtendedKeys would still pass.

🔧 Suggested assertion
-	_, ok = flower.Actions[0].(*PeditAction)
+	pedit, ok := flower.Actions[0].(*PeditAction)
 	if !ok {
 		t.Fatal("Unable to find pedit action")
 	}
+	if pedit.SrcIP != peditAction.SrcIP {
+		t.Fatal("Pedit SrcIP doesn't match")
+	}

Also applies to: 2200-2203

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

In `@filter_test.go` around lines 2158 - 2159, The test currently sets
PeditAction.SrcIP but only asserts the decoded type; update the test to assert
the SrcIP value round-trips correctly by comparing the original
peditAction.SrcIP to the parsed/decoded action's SrcIP (after extracting the
decoded action from filter.Actions), so any regression in SetSrcIP or
parsePeditExtendedKeys fails the test; locate the PeditAction instance and the
decoded action assertion and add a strict equality check between their SrcIP
values.

Comment thread link.go
Comment on lines 1285 to 1289
func (vti *Vti) Type() string {
if vti.Local.To4() == nil {
if vti.Local.Is6() {
return "vti6"
}
return "vti"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "link\.go$"

Repository: vishvananda/netlink

Length of output: 92


🏁 Script executed:

find . -name "link.go" -type f

Repository: vishvananda/netlink

Length of output: 72


🏁 Script executed:

ls -la | head -20

Repository: vishvananda/netlink

Length of output: 1243


🏁 Script executed:

find . -type f -name "*.go" | head -20

Repository: vishvananda/netlink

Length of output: 415


🏁 Script executed:

cat -n link.go | sed -n '1280,1300p'

Repository: vishvananda/netlink

Length of output: 536


🏁 Script executed:

wc -l link.go

Repository: vishvananda/netlink

Length of output: 75


🏁 Script executed:

git log --oneline link.go | head -20

Repository: vishvananda/netlink

Length of output: 105


🏁 Script executed:

git show HEAD:link.go | grep -A 20 "func (vti \*Vti) Type()"

Repository: vishvananda/netlink

Length of output: 417


🏁 Script executed:

grep -n "func (gretap \*Gretap) Type()" link.go

Repository: vishvananda/netlink

Length of output: 105


🏁 Script executed:

grep -n "func (gretun \*Gretun) Type()" link.go

Repository: vishvananda/netlink

Length of output: 105


🏁 Script executed:

cat -n link.go | sed -n '1200,1220p'

Repository: vishvananda/netlink

Length of output: 600


🏁 Script executed:

cat -n link.go | sed -n '1230,1250p'

Repository: vishvananda/netlink

Length of output: 1146


🏁 Script executed:

grep -n "func.*Type() string" link.go

Repository: vishvananda/netlink

Length of output: 1433


🏁 Script executed:

cat -n link.go | sed -n '1160,1170p'

Repository: vishvananda/netlink

Length of output: 314


🏁 Script executed:

cat -n link.go | sed -n '1315,1325p'

Repository: vishvananda/netlink

Length of output: 286


🏁 Script executed:

git log --all --oneline | grep -i "netip\|migration" | head -10

Repository: vishvananda/netlink

Length of output: 105


🏁 Script executed:

git show 3ee2da8:link.go | grep -A 10 "func (vti \*Vti) Type()"

Repository: vishvananda/netlink

Length of output: 222


🏁 Script executed:

git show 3ee2da8:link.go | grep -A 10 "func (gretap \*Gretap) Type()"

Repository: vishvananda/netlink

Length of output: 237


🏁 Script executed:

git show 3ee2da8:link.go | grep -A 10 "func (gretun \*Gretun) Type()"

Repository: vishvananda/netlink

Length of output: 209


🏁 Script executed:

git log --oneline link.go | head -5

Repository: vishvananda/netlink

Length of output: 105


🏁 Script executed:

git show 3ee2da8^:link.go | grep -A 10 "func (vti \*Vti) Type()"

Repository: vishvananda/netlink

Length of output: 101


🏁 Script executed:

git show 3ee2da8^:link.go | grep -A 10 "func (gretap \*Gretap) Type()"

Repository: vishvananda/netlink

Length of output: 101


🏁 Script executed:

git show 3ee2da8^:link.go | grep -A 10 "func (gretun \*Gretun) Type()"

Repository: vishvananda/netlink

Length of output: 101


🏁 Script executed:

git log --all --oneline | head -20

Repository: vishvananda/netlink

Length of output: 1247


🏁 Script executed:

git show HEAD~1:link.go 2>/dev/null | grep -A 10 "func (vti \*Vti) Type()" || echo "No previous commits"

Repository: vishvananda/netlink

Length of output: 82


🏁 Script executed:

git rev-list --all | head -10

Repository: vishvananda/netlink

Length of output: 472


🏁 Script executed:

git log --all --graph --oneline | head -30

Repository: vishvananda/netlink

Length of output: 1867


🏁 Script executed:

git branch -a

Repository: vishvananda/netlink

Length of output: 160


🏁 Script executed:

git log --all --oneline -20

Repository: vishvananda/netlink

Length of output: 1247


🏁 Script executed:

git log -p --all -- link.go | grep -B 5 -A 10 "func (vti \*Vti) Type()" | head -50

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

grep -B 20 "func (vti \*Vti) Type()" link.go | grep -A 20 "type Vti struct"

Repository: vishvananda/netlink

Length of output: 272


🏁 Script executed:

cat -n link.go | sed -n '1265,1290p'

Repository: vishvananda/netlink

Length of output: 595


🏁 Script executed:

cat > /tmp/test_netip.go << 'EOF'
package main

import (
	"fmt"
	"net/netip"
)

func main() {
	var zeroAddr netip.Addr
	fmt.Printf("Zero netip.Addr.Is4(): %v\n", zeroAddr.Is4())
	fmt.Printf("Zero netip.Addr.Is6(): %v\n", zeroAddr.Is6())
	fmt.Printf("Zero netip.Addr: %v\n", zeroAddr)
	
	// Check behavior similar to old net.IP
	// Old: net.IP(nil).To4() == nil was true for both nil and IPv6
	// Let's see netip.Addr equivalence
	var nilIP net.IP
	fmt.Printf("nil net.IP.To4() == nil: %v\n", nilIP.To4() == nil)
	fmt.Printf("nil net.IP.To6() == nil: %v\n", nilIP.To6() == nil)
}
EOF
go run /tmp/test_netip.go 2>/dev/null || echo "Go not available or compilation failed"

Repository: vishvananda/netlink

Length of output: 101


🏁 Script executed:

grep -r "Vti.*Type()" . --include="*.go"

Repository: vishvananda/netlink

Length of output: 104


🏁 Script executed:

grep -r "Type().*vti\|vti.*Type()" . --include="*.go" -i

Repository: vishvananda/netlink

Length of output: 104


🏁 Script executed:

find . -name "*test*.go" -type f | grep -i vti

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

find . -name "*test*.go" -type f | head -10

Repository: vishvananda/netlink

Length of output: 238


🏁 Script executed:

grep -l "Vti" *.go

Repository: vishvananda/netlink

Length of output: 97


🏁 Script executed:

grep -n "Vti\|vti" link_test.go

Repository: vishvananda/netlink

Length of output: 368


🏁 Script executed:

git show 3ee2da8 -- link.go | head -200

Repository: vishvananda/netlink

Length of output: 5368


🏁 Script executed:

git log -p --all -- link.go | grep -B 15 -A 5 "To4()" | head -100

Repository: vishvananda/netlink

Length of output: 45


🏁 Script executed:

git show c822ed7:link.go 2>/dev/null | grep -A 10 "func (vti \*Vti) Type()" || echo "File not found in c822ed7"

Repository: vishvananda/netlink

Length of output: 229


🏁 Script executed:

git log --all -- link.go | tail -5

Repository: vishvananda/netlink

Length of output: 239


🏁 Script executed:

git show ddba687:link.go 2>/dev/null | grep -A 10 "func (vti \*Vti) Type()" || echo "Not found"

Repository: vishvananda/netlink

Length of output: 229


🏁 Script executed:

git show 3ee2da8 | head -300

Repository: vishvananda/netlink

Length of output: 13356


🏁 Script executed:

git show 3ee2da8 --stat

Repository: vishvananda/netlink

Length of output: 6685


🏁 Script executed:

git show 3ee2da8 -- link.go | grep -A 30 "type Vti struct"

Repository: vishvananda/netlink

Length of output: 570


🏁 Script executed:

git show 3ee2da8 -- link.go | grep -B 5 -A 15 "vti.Local"

Repository: vishvananda/netlink

Length of output: 427


🏁 Script executed:

git diff 3ee2da8~1..3ee2da8 -- link.go 2>/dev/null | grep -A 10 -B 10 "Vti\|vti" || git log -1 --pretty=format:"%B" 3ee2da8

Repository: vishvananda/netlink

Length of output: 97


Vti.Type() regresses when Local is unset.

Line 1286 switched from the old To4()==nil behavior to Local.Is6(). With netip.Addr, Is6() is false for the zero value, so a Vti with an unset Local now reports "vti" instead of "vti6". That changes link-kind selection for callers that only populate Remote or relied on the previous zero-value fallback. This inconsistency also differs from the pattern used in Gretap and Gretun, which both use !Local.Is4().

Suggested fix
 func (vti *Vti) Type() string {
-	if vti.Local.Is6() {
+	if !vti.Local.Is4() {
 		return "vti6"
 	}
 	return "vti"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (vti *Vti) Type() string {
if vti.Local.To4() == nil {
if vti.Local.Is6() {
return "vti6"
}
return "vti"
func (vti *Vti) Type() string {
if !vti.Local.Is4() {
return "vti6"
}
return "vti"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@link.go` around lines 1285 - 1289, Vti.Type() regresses when Local is the
zero-value because Is6() is false; change the check to mirror Gretap/Gretun by
using !vti.Local.Is4() (or equivalently the previous To4()==nil logic) so an
unset/IPv6 Local still returns "vti6"; update the conditional in Vti.Type to use
Local.Is4/Is6 consistently with Gretap and Gretun (reference: Vti.Type,
vti.Local, Local.Is4, Local.Is6, Gretap, Gretun).

Comment thread nl/tc_linux.go
Comment on lines +1551 to 1565
func (p *TcPedit) SetDstIP(ip netip.Addr) {
if ip.Is4() {
p.SetIPv4Dst(ip)
} else {
p.SetIPv6Dst(ip)
}
}

func (p *TcPedit) SetSrcIP(ip net.IP) {
if ip.To4() != nil {
func (p *TcPedit) SetSrcIP(ip netip.Addr) {
if ip.Is4() {
p.SetIPv4Src(ip)
} else {
p.SetIPv6Src(ip)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Go's net/netip package, does the zero-value Addr{}reportIs4() == false, and is Addr.IsValid() the intended way to reject that invalid value before dispatching on IP family?

💡 Result:

Yes, the zero-value net/netip.Addr{} reports Is4 == false. Yes, Addr.IsValid is the intended way to reject the invalid zero value before dispatching on IP family. The official documentation states: "The zero Addr is not a valid IP address." and "IsValid reports whether the Addr is an initialized address (not the zero Addr). Note that '0.0.0.0' and '::' are both valid values." The source code confirms the zero value has z == z0 (nil/invalid), while Is4 returns ip.z == z4, so false for zero. Methods like Is4In6 and IsPrivate explicitly return false for the zero Addr. As4 panics on zero Addr, emphasizing it must be checked first with IsValid before family-specific operations.

Citations:


🏁 Script executed:

# First, locate the file and check its size
fd -n tc_linux.go | head -5

Repository: vishvananda/netlink

Length of output: 293


🏁 Script executed:

# Get the actual implementation around lines 1551-1565 and surrounding setters
fd -n tc_linux.go -x wc -l {} \;

Repository: vishvananda/netlink

Length of output: 293


🏁 Script executed:

# Read the relevant sections of nl/tc_linux.go
fd -n tc_linux.go -x cat -n {} \; | sed -n '1540,1700p'

Repository: vishvananda/netlink

Length of output: 293


🏁 Script executed:

# Find tc_linux.go file
find . -name "tc_linux.go" -type f

Repository: vishvananda/netlink

Length of output: 79


🏁 Script executed:

# Read the relevant sections once we locate the file
cat -n nl/tc_linux.go | sed -n '1540,1700p'

Repository: vishvananda/netlink

Length of output: 4656


Add IsValid() guard before family dispatch in generic IP setters.

Lines 1551-1565: SetDstIP and SetSrcIP only test Is4() before dispatching, so an invalid zero-value netip.Addr (where Is4() == false) will route to the IPv6 path and call SetIPv6Dst, which immediately invokes As16() on an invalid address. After migration to the netip.Addr value type, invalid addresses are easier to pass accidentally and must be rejected explicitly.

Suggested guard
 func (p *TcPedit) SetDstIP(ip netip.Addr) {
+	if !ip.IsValid() {
+		return
+	}
 	if ip.Is4() {
 		p.SetIPv4Dst(ip)
 	} else {
 		p.SetIPv6Dst(ip)
 	}
 }
 
 func (p *TcPedit) SetSrcIP(ip netip.Addr) {
+	if !ip.IsValid() {
+		return
+	}
 	if ip.Is4() {
 		p.SetIPv4Src(ip)
 	} else {
 		p.SetIPv6Src(ip)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nl/tc_linux.go` around lines 1551 - 1565, The SetDstIP and SetSrcIP methods
on TcPedit must first validate the netip.Addr to avoid routing invalid
zero-value addresses into the IPv6 path; add an ip.IsValid() guard at the start
of both TcPedit.SetDstIP and TcPedit.SetSrcIP and return early if false so you
only call SetIPv4Dst/SetIPv6Dst and SetIPv4Src/SetIPv6Src for valid addresses
(this prevents downstream calls like As16() on an invalid addr).

@fdomain

fdomain commented Jul 3, 2026

Copy link
Copy Markdown
Author

any opinion on this @aojea, @aboch or @vishvananda ?

@aojea

aojea commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

IMHO the amount of code changes required should be justified with a clear benchmark that shows the improvement and why is worth taking the risk

@fdomain

fdomain commented Jul 3, 2026

Copy link
Copy Markdown
Author

@aojea

My initial motivation was more to fix #1114 .
The net package doesn't handle correctly the IPv4-mapped IPv6 address (::ffff:100.95.128.2) preventing to add IPv6 routes towards an IPv4-mapped IPv6 address nexthop.

There's some background here on the motivations behind this package: netip's predecessor inet.af/netaddr was written by Tailscale to fix similar net.IP limitations (see their writeup), and it was later adopted into the standard library, see the Go 1.18 release notes.

Regarding the performance improvement itself, I didn't do any benchmark but it is supposed to be more memory efficient as stated in the go1.18 release notes Compared to the existing [net.IP](https://go.dev/pkg/net/#IP) type, the netip.Addr type takes less memory, is immutable, and is comparable so it supports == and can be used as a map key.

@aojea

aojea commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

not a maintainer here, I'm well aware of this work and I myself started this in Kubernetes some time ago, although we found some edges on things that were not working the same , that is like with my maintainer hat I will prefer to be conservative and just change only the parts that I know that I will get a benefit rather than explore a full entire migration

@fdomain

fdomain commented Jul 3, 2026

Copy link
Copy Markdown
Author

my bad, I thought you were a maintainer! your feedback is valuable though, I agree with you that it would be easier to only fix the part where there is a real benefit. In my case that's only adding/removing routes with an IPv4-mapped IPv6 nexthop.
I'll try to propose an alternative PR fixing that particular issue without migrating to netip.

@fdomain

fdomain commented Jul 6, 2026

Copy link
Copy Markdown
Author

pushed #1198 as an alternative, hope it will be more acceptable for the maintainers!

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.

IPv4-mapped IPv6 address cannot be used as route gateway

2 participants