* [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP
@ 2026-10-28 5:45 Hangbin Liu
2026-10-28 5:45 ` [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 Hangbin Liu
` (4 more replies)
0 siblings, 5 replies; 12+ messages in thread
From: Hangbin Liu @ 2026-10-28 5:45 UTC (permalink / raw)
To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman,
Shuah Khan
Cc: Hangbin Liu, linux-kernel, netdev, linux-kselftest, Hangbin Liu
This series makes IOAM insertion transparent to IPv6 ECMP, as required
by RFC 9486 (C2): a packet carrying IOAM options must be forwarded
along the same path as the identical packet without them.
Patch 1 fixes the transit side: hash policy 0 hashed the raw IPv6
nexthdr, which becomes NEXTHDR_HOP once an IOAM Hop-by-Hop header is
inserted. The policy-0 key (and the ICMPv6-error flow selection) now
uses the nexthdr the packet would have had if the Hop-by-Hop header
carried only IOAM options and padding.
Patch 2 fixes the injecting node: the ioam6 output re-lookup on a
destination-cache miss hashed the transformed packet's NEXTHDR_HOP
instead of the original nexthdr (inline) or IPPROTO_IPV6 (encap), and
rebuilt the flow keys from the outer header only.
Patch 3 adds selftests/net/ioam6_ecmp.sh, covering both places: a
transit forwarder with a multipath route, and an encapsulating node
compared against a plain ip6ip6 baseline.
The issue was found while reviewing RFC9486. It can be reproduced and
verified by the newly added ioam6_ecmp.sh test.
Tested with:
ecmp_rehash.sh
fib_nexthops.sh
ioam6_ecmp.sh
tcp_ecmp_failover.sh
forwarding/gre_inner_v4_multipath.sh
forwarding/gre_inner_v6_multipath.sh
forwarding/gre_multipath_nh_res.sh
forwarding/gre_multipath_nh.sh
forwarding/gre_multipath.sh
forwarding/ip6gre_inner_v4_multipath.sh
forwarding/ip6gre_inner_v6_multipath.sh
forwarding/router_mpath_nh_res.sh
forwarding/router_mpath_nh.sh
forwarding/router_multipath.sh
Signed-off-by: Hangbin Liu <liuhangbin@kylinos.cn>
---
Hangbin Liu (3):
ipv6: route: consider IOAM options in ECMP hash policy 0
ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup
selftests: net: add ioam6 ECMP test (RFC 9486 C2)
include/uapi/linux/in6.h | 1 +
net/ipv6/ioam6_iptunnel.c | 4 +-
net/ipv6/route.c | 67 +++++-
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/ioam6_ecmp.sh | 349 ++++++++++++++++++++++++++++++
5 files changed, 419 insertions(+), 3 deletions(-)
---
base-commit: 5d7f3375cd574fc83f0cd30839b21455c0e79ae2
change-id: 20261020-ipv6_ioam_ecmp-ae4ef0a4bdae
Best regards,
--
Hangbin Liu <liuhangbin@kylinos.cn>
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu @ 2026-10-28 5:45 ` Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 5:45 ` [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup Hangbin Liu ` (3 subsequent siblings) 4 siblings, 1 reply; 12+ messages in thread From: Hangbin Liu @ 2026-10-28 5:45 UTC (permalink / raw) To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan Cc: Hangbin Liu, linux-kernel, netdev, linux-kselftest, Hangbin Liu From: Hangbin Liu <liuhangbin@kylinos.cn> RFC 9486 (C2) requires that the ECMP hash of a packet carrying IOAM options is the same as the hash of the identical packet without them. The default multipath hash policy (0) hashes basic.ip_proto, which ip6_multipath_l3_keys() fills from the raw IPv6 nexthdr. An IOAM Hop-by-Hop option is inserted ahead of the original next header, so a transit router sees NEXTHDR_HOP (0) instead of e.g. IPPROTO_TCP and may pick a different ECMP sibling than for the same packet without IOAM. Recover the nexthdr the packet would have had if its Hop-by-Hop header carried only IOAM options (0x31, and 0x11 for DEX) plus padding, and use it for the policy-0 key and for the ICMPv6-error check, so errors that quote a flow keep hashing that flow. Any other Hop-by-Hop option (Router Alert, Jumbo, ...) keeps hashing exactly as before. The walk only runs for the rare nexthdr == NEXTHDR_HOP case and is safe: ipv6_parse_hopopts() has already pulled and validated the header before routing. ICMPv6-quoted inner headers are not walked: they are payload and may be truncated. Fixes: 3edede08ff37 ("ipv6: ioam: Support for IOAM injection with lwtunnels") Signed-off-by: Hangbin Liu <liuhangbin@kylinos.cn> --- The ECMP path with and without IOAM is verified by tools/testing/selftests/net/ioam6_ecmp.sh in the next patch. --- include/uapi/linux/in6.h | 1 + net/ipv6/route.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/include/uapi/linux/in6.h b/include/uapi/linux/in6.h index 5a47339ef7d7..2e66dc37e1e5 100644 --- a/include/uapi/linux/in6.h +++ b/include/uapi/linux/in6.h @@ -145,6 +145,7 @@ struct in6_flowlabel_req { #define IPV6_TLV_PADN 1 #define IPV6_TLV_ROUTERALERT 5 #define IPV6_TLV_CALIPSO 7 /* RFC 5570 */ +#define IPV6_TLV_IOAM_DEX 17 /* RFC 9486 */ #define IPV6_TLV_IOAM 49 /* RFC 9486 */ #define IPV6_TLV_JUMBO 194 #define IPV6_TLV_HAO 201 /* home address option */ diff --git a/net/ipv6/route.c b/net/ipv6/route.c index 94e5ef1c4f83..d0761f05d326 100644 --- a/net/ipv6/route.c +++ b/net/ipv6/route.c @@ -2342,6 +2342,60 @@ struct dst_entry *ip6_route_input_lookup(struct net *net, } EXPORT_SYMBOL_GPL(ip6_route_input_lookup); +/* RFC 9486 (C2) requires that the ECMP hash of a packet carrying IOAM + * options is the same as the hash of the identical packet without them. + * Recover the nexthdr if its Hop-by-Hop header carried only IOAM options + * and padding, every other packet is hashed exactly as before. + */ +static u8 ip6_nexthdr_without_ioam(const struct sk_buff *skb, + const struct ipv6hdr *iph) +{ + const struct ipv6_opt_hdr *hhdr; + struct ipv6_opt_hdr hbuf; + bool has_ioam = false; + u8 obuf[2], *opt; + int off, len, i; + + if (iph->nexthdr != NEXTHDR_HOP) + return iph->nexthdr; + + /* The Hop-by-Hop header was already pulled and validated by + * ipv6_parse_hopopts() before routing, so this walk is safe. + */ + off = sizeof(*iph); + hhdr = skb_header_pointer(skb, off, sizeof(hbuf), &hbuf); + if (!hhdr) + return iph->nexthdr; + + len = (hhdr->hdrlen + 1) << 3; + + for (i = 2; i < len; ) { + opt = skb_header_pointer(skb, off + i, sizeof(obuf), obuf); + if (!opt) + return iph->nexthdr; + if (opt[0] == IPV6_TLV_PAD1) { + i++; + continue; + } + if (opt[0] == IPV6_TLV_PADN) { + i += opt[1] + 2; + continue; + } + if (opt[0] != IPV6_TLV_IOAM && opt[0] != IPV6_TLV_IOAM_DEX) + return iph->nexthdr; + + has_ioam = true; + + /* Keep searching for the next TLV. As long as non-IOAM + * options (Router Alert, Jumbo, etc.) exist in the HBH + * header, we should preserve the original NEXTHDR_HOP. + */ + i += opt[1] + 2; + } + + return has_ioam ? hhdr->nexthdr : iph->nexthdr; +} + static void ip6_multipath_l3_keys(const struct sk_buff *skb, struct flow_keys *keys, struct flow_keys *flkeys) @@ -2353,8 +2407,10 @@ static void ip6_multipath_l3_keys(const struct sk_buff *skb, const struct icmp6hdr *icmph; struct ipv6hdr _inner_iph; struct icmp6hdr _icmph; + u8 nexthdr; - if (likely(outer_iph->nexthdr != IPPROTO_ICMPV6)) + nexthdr = ip6_nexthdr_without_ioam(skb, outer_iph); + if (likely(nexthdr != IPPROTO_ICMPV6)) goto out; icmph = skb_header_pointer(skb, skb_transport_offset(skb), @@ -2383,7 +2439,14 @@ static void ip6_multipath_l3_keys(const struct sk_buff *skb, keys->addrs.v6addrs.src = key_iph->saddr; keys->addrs.v6addrs.dst = key_iph->daddr; keys->tags.flow_label = ip6_flowlabel(key_iph); - keys->basic.ip_proto = key_iph->nexthdr; + /* Only for the outer header: an ICMPv6-quoted inner header + * is payload, and its (possibly truncated) extension + * headers must not be walked here. + */ + if (key_iph == outer_iph) + keys->basic.ip_proto = nexthdr; + else + keys->basic.ip_proto = key_iph->nexthdr; } } -- 2.56.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 2026-10-28 5:45 ` [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 Hangbin Liu @ 2026-10-30 11:47 ` netdev-bot+sashiko 0 siblings, 0 replies; 12+ messages in thread From: netdev-bot+sashiko @ 2026-10-30 11:47 UTC (permalink / raw) To: hangbin.liu Cc: dsahern, idosch, davem, edumazet, kuba, pabeni, horms, justin.iurman, shuah, linux-kernel, netdev, linux-kselftest, liuhangbin Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider. Critical: 0 · High: 0 · Medium: 3 · Low: 2 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] ip6_nexthdr_without_ioam() (net/ipv6/route.c) reads the Hop- by-Hop header at `off = sizeof(*iph)`. - [Medium] In ip6_multipath_l3_keys(), the outer header of an IOAM- carrying packet is now hashed with the recovered L4 protocol… - [Medium] On IOAM-enabled ingress with a known namespace, ipv6_hop_ioam() calls ip6_route_input() while ip6_parse_tlv() is still… - [Low] The new comment in ip6_nexthdr_without_ioam() says 'The Hop-by- Hop header was already pulled and validated by ipv6_parse_hopopts()… - [Low] In the ip6_nexthdr_without_ioam() TLV loop, `skb_header_pointer(skb, off + i, sizeof(obuf), obuf)` fetches 2 bytes… -- ipv6: route: consider IOAM options in ECMP hash policy 0 This patch adds ip6_nexthdr_without_ioam(). It walks a Hop-by-Hop header and, if the header holds only IOAM options and padding, returns the nexthdr that follows it. ip6_multipath_l3_keys() uses that value for the policy 0 ip_proto key and for its ICMPv6 error check, so that packets with and without IOAM hash the same way, as RFC 9486 (C2) requires. > diff --git a/net/ipv6/route.c b/net/ipv6/route.c > index 94e5ef1c4f83e..d0761f05d3260 100644 > --- a/net/ipv6/route.c > +++ b/net/ipv6/route.c > @@ -2342,6 +2342,60 @@ struct dst_entry *ip6_route_input_lookup(struct net *net, [ ... ] > +static u8 ip6_nexthdr_without_ioam(const struct sk_buff *skb, > + const struct ipv6hdr *iph) > +{ [ ... ] > + if (iph->nexthdr != NEXTHDR_HOP) > + return iph->nexthdr; > + > + /* The Hop-by-Hop header was already pulled and validated by > + * ipv6_parse_hopopts() before routing, so this walk is safe. > + */ [Severity: Low] Does this comment hold for every caller of rt6_multipath_hash()? icmp6_send() also hashes the offending skb: net/ipv6/icmp.c:icmp6_send() { ... fl6.mp_hash = rt6_multipath_hash(net, &fl6, skb, NULL); ... } That skb can be generated locally and never pass through ipv6_parse_hopopts(). One example is a packet built with IPV6_HOPOPTS or a raw socket that hits a reject or unreachable route: ip6_pkt_discard_out()->ip6_pkt_drop()->icmpv6_send()->icmp6_send()-> rt6_multipath_hash() On IOAM-enabled ingress, ipv6_hop_ioam() calls ip6_route_input() while ip6_parse_tlv() is still running. So any TLVs after the IOAM option have not been validated yet: net/ipv6/exthdrs.c:ipv6_hop_ioam() { ... if (!skb_valid_dst(skb)) ip6_route_input(skb); ... } Today the walk is memory safe only because every access goes through skb_header_pointer(). With unvalidated input it can also return hhdr->nexthdr for a malformed HBH header whose TLV lengths run past the header. Could the comment be updated to describe what actually makes the walk safe? > + off = sizeof(*iph); > + hhdr = skb_header_pointer(skb, off, sizeof(hbuf), &hbuf); [Severity: Medium] Does this offset assume that skb->data sits at the network header? skb_header_pointer() counts the offset from skb->data. iph is ipv6_hdr(skb), i.e. skb_network_header(). The two only agree when skb->data has not been pulled past the network header. They do not agree when icmp6_send() is called from the L4 receive paths. By then ip6_protocol_deliver_rcu() has already pulled to the transport header: net/ipv6/ip6_input.c:ip6_protocol_deliver_rcu() { ... if (!pskb_pull(skb, skb_transport_offset(skb))) ... } icmp6_send() then hashes the offending skb with policy 0: __udp6_lib_rcv()->icmpv6_send()->icmp6_send()->rt6_multipath_hash()-> ip6_multipath_l3_keys()->ip6_nexthdr_without_ioam() icmp6_send() already handles this when it quotes the packet: msg.offset = skb_network_offset(skb); The ICMPV6_UNK_NEXTHDR parameter problem in ip6_protocol_deliver_rcu() and the tunnel6/xfrm6 errors take the same path. If the offending packet has nexthdr == NEXTHDR_HOP (even PadN only), the helper treats the bytes 40 past the L4 header as the HBH header and its TLVs. Those bytes are payload controlled by the sender. If the packet has a real IOAM-only HBH header, the locally generated ICMP error is still hashed with proto 0. That is the mismatch this patch sets out to fix. A crafted payload can also make the helper return any nexthdr, including IPPROTO_ICMPV6. ip6_multipath_l3_keys() then reads the UDP header at transport offset 0 as an icmp6hdr. If the high byte of the source port is 1-4, icmpv6_is_err() passes and the UDP payload is used as the inner ipv6hdr. The payload then picks the addresses, flow label and proto used as ECMP hash keys. Before this patch, HBH packets never reached this parsing. All reads are bounded by skb_header_pointer(), so the result is wrong hash input, not an out-of-bounds access. Would using skb_network_offset(skb) + sizeof(*iph) as the base offset avoid this? The follow-up patch in the series only changes ioam6_iptunnel.c and the selftests, so route.c is unchanged after it. > + if (!hhdr) > + return iph->nexthdr; > + > + len = (hhdr->hdrlen + 1) << 3; > + > + for (i = 2; i < len; ) { > + opt = skb_header_pointer(skb, off + i, sizeof(obuf), obuf); > + if (!opt) > + return iph->nexthdr; > + if (opt[0] == IPV6_TLV_PAD1) { > + i++; > + continue; > + } [Severity: Low] Can a trailing Pad1 make this 2-byte fetch fail? ip6_parse_tlv() accepts Pad1 as the last byte of the HBH header, because the Pad1 branch continues before the len < 2 check: net/ipv6/exthdrs.c:ip6_parse_tlv() { ... if (nh[off] == IPV6_TLV_PAD1) { padlen++; if (padlen > 7) goto bad; off++; len--; continue; } if (len < 2) goto bad; ... } Suppose an IOAM-only HBH header ends in Pad1 and nothing follows it in the skb, e.g. nexthdr is NEXTHDR_NONE with no payload. The fetch at off + len - 1 needs one byte past skb->len, so it returns NULL. The function then returns iph->nexthdr (0) even though has_ioam is true. This packet then hashes differently from the same packet without IOAM. Would it work to read one byte first and fetch the length byte only for options other than Pad1? [ ... ] > @@ -2353,8 +2407,10 @@ static void ip6_multipath_l3_keys(const struct sk_buff *skb, > const struct icmp6hdr *icmph; > struct ipv6hdr _inner_iph; > struct icmp6hdr _icmph; > + u8 nexthdr; > > - if (likely(outer_iph->nexthdr != IPPROTO_ICMPV6)) > + nexthdr = ip6_nexthdr_without_ioam(skb, outer_iph); > + if (likely(nexthdr != IPPROTO_ICMPV6)) > goto out; > > icmph = skb_header_pointer(skb, skb_transport_offset(skb), [Severity: Medium] On IOAM-enabled ingress, where does skb_transport_offset(skb) point when this code runs? For a known namespace, ipv6_hop_ioam() calls ip6_route_input() from inside ip6_parse_tlv(). ip6_rcv_core() has set transport_header to the HBH header: skb->transport_header = skb->network_header + sizeof(*hdr); ipv6_parse_hopopts() moves it past the HBH header only after ip6_parse_tlv() returns: skb->transport_header += extlen; Take an ICMPv6 error with an IOAM-only HBH header. ip6_nexthdr_without_ioam() now returns IPPROTO_ICMPV6, but icmph is read from the HBH header itself. icmp6_type is then the HBH nexthdr (58), and icmpv6_is_err(58) is false. The error is therefore hashed on its outer addresses with proto 58. The same error without IOAM would be hashed on the quoted inner header. ip6_rcv_finish_core() only routes when !skb_valid_dst(skb), so this early dst stays in place. On IOAM transit nodes, does this match what the commit message claims? "and use it for the policy-0 key and for the ICMPv6-error check, so errors that quote a flow keep hashing that flow." No later patch in the series addresses this. [ ... ] > @@ -2383,7 +2439,14 @@ static void ip6_multipath_l3_keys(const struct sk_buff *skb, > keys->addrs.v6addrs.src = key_iph->saddr; > keys->addrs.v6addrs.dst = key_iph->daddr; > keys->tags.flow_label = ip6_flowlabel(key_iph); > - keys->basic.ip_proto = key_iph->nexthdr; > + /* Only for the outer header: an ICMPv6-quoted inner header > + * is payload, and its (possibly truncated) extension > + * headers must not be walked here. > + */ > + if (key_iph == outer_iph) > + keys->basic.ip_proto = nexthdr; > + else > + keys->basic.ip_proto = key_iph->nexthdr; [Severity: Medium] Does this give flow packets and the ICMPv6 errors that quote them different ip_proto keys? Consider a transit router using policy 0. A flow packet carrying IOAM reaches this code through fib6_select_path() and is hashed with the recovered proto, e.g. IPPROTO_TCP. An ICMPv6 error that quotes such a packet takes the inner branch: key_iph = inner_iph; _flkeys = NULL; It then hashes the raw nexthdr of the quoted header, which is NEXTHDR_HOP (0). Before this patch, when IOAM was present in both directions, both sides hashed proto 0 and matched. Now they differ, so an error such as Packet Too Big can go to a different ECMP sibling than the flow. Commit 23aebdacb05d ("ipv6: Compute multipath hash for ICMP errors from offending packet") hashes ICMP errors on the inner header so that the two would match. This seems to contradict the commit message: "so errors that quote a flow keep hashing that flow." The stated reason for not walking the quoted header is truncation. However, every access in ip6_nexthdr_without_ioam() goes through skb_header_pointer(), is bounded by skb->len, and falls back on failure. Could the helper take a base offset and also be used for the quoted header? No later patch in the series changes this. -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028-ipv6_ioam_ecmp-v1-0-9b6715b6f4c0%40kylinos.cn ^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu 2026-10-28 5:45 ` [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 Hangbin Liu @ 2026-10-28 5:45 ` Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 5:45 ` [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) Hangbin Liu ` (2 subsequent siblings) 4 siblings, 1 reply; 12+ messages in thread From: Hangbin Liu @ 2026-10-28 5:45 UTC (permalink / raw) To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan Cc: Hangbin Liu, linux-kernel, netdev, linux-kselftest, Hangbin Liu From: Hangbin Liu <liuhangbin@kylinos.cn> RFC 9486 (C2) requires that the ECMP hash of a packet carrying IOAM options is the same as the hash of the identical packet without them. When the ioam6 output path misses the destination cache, it re-looks up the route for the transformed packet. Under the default hash policy 0 that lookup hashed NEXTHDR_HOP rather than the nexthdr the identical packet without IOAM would carry, so it could pick a different ECMP sibling. Use the Hop-by-Hop nexthdr already stored by do_inline()/do_encap() (eh.nexthdr) for the re-lookup, and precompute the multipath hash from the packet: ip6_route_output() lookups carry no skb, so without this ICMPv6 errors would stop hashing their quoted flow and L4 hash policy packets would lose their ports. Fixes: 8cb3bf8bff3c ("ipv6: ioam: Add support for the ip6ip6 encapsulation") Signed-off-by: Hangbin Liu <liuhangbin@kylinos.cn> --- net/ipv6/ioam6_iptunnel.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/net/ipv6/ioam6_iptunnel.c b/net/ipv6/ioam6_iptunnel.c index cfb2c41634a0..713e625bfa3a 100644 --- a/net/ipv6/ioam6_iptunnel.c +++ b/net/ipv6/ioam6_iptunnel.c @@ -13,6 +13,7 @@ #include <linux/ioam6.h> #include <linux/ioam6_iptunnel.h> #include <net/dst.h> +#include <net/ip6_route.h> #include <net/sock.h> #include <net/lwtunnel.h> #include <net/ioam6.h> @@ -412,7 +413,8 @@ static int ioam6_output(struct net *net, struct sock *sk, struct sk_buff *skb) fl6.saddr = hdr->saddr; fl6.flowlabel = ip6_flowinfo(hdr); fl6.flowi6_mark = skb->mark; - fl6.flowi6_proto = hdr->nexthdr; + fl6.flowi6_proto = ilwt->tuninfo.eh.nexthdr; + fl6.mp_hash = rt6_multipath_hash(net, &fl6, skb, NULL); dst = ip6_route_output(net, NULL, &fl6); if (dst->error) { -- 2.56.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup 2026-10-28 5:45 ` [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup Hangbin Liu @ 2026-10-30 11:47 ` netdev-bot+sashiko 0 siblings, 0 replies; 12+ messages in thread From: netdev-bot+sashiko @ 2026-10-30 11:47 UTC (permalink / raw) To: hangbin.liu Cc: dsahern, idosch, davem, edumazet, kuba, pabeni, horms, justin.iurman, shuah, linux-kernel, netdev, linux-kselftest, liuhangbin Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider. Critical: 0 · High: 2 (1) · Medium: 3 · Low: 3 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [High] In ioam6_output() (net/ipv6/ioam6_iptunnel.c:416-417), the inline-mode re-lookup does not keep ICMPv6 errors hashed on their… - [High] For locally generated packets that ioam6_output() handles in INLINE mode (or AUTO mode with !skb->dev), orig_dst came from… - [Medium] ioam6_output() (ioam6_iptunnel.c:416) reads the per-route shared template field ilwt->tuninfo.eh.nexthdr into fl6.flowi6_proto… - [Medium] The commit message credits the policy-0 fix to using eh.nexthdr as fl6.flowi6_proto in the re-lookup. - [Medium] ipv6: route: wrong skb offset in ip6_nexthdr_without_ioam() via icmp6_send() - [Low] Because mp_hash is precomputed from the skb, the main user- visible effect of changing fl6.flowi6_proto from hdr->nexthdr… - [Low] fib6_select_path() (route.c:464-469) treats fl6->mp_hash == 0 as 'not computed'. - [Low] ipv6: ioam6: duplicate ip6_route.h include in ioam6_iptunnel.c Pre-existing issues: - [High] This problem wasn't introduced by this patch, but the patch relies on the same racy template. -- ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup On a dst_cache miss, the ioam6 output path re-looks up the route. This patch changes that lookup to use the Hop-by-Hop nexthdr stored in the tunnel template (eh.nexthdr) as flowi6_proto instead of NEXTHDR_HOP. It also precomputes fl6.mp_hash from the transformed skb with rt6_multipath_hash(), so that the ECMP hash matches RFC 9486 (C2). > diff --git a/net/ipv6/ioam6_iptunnel.c b/net/ipv6/ioam6_iptunnel.c > index cfb2c41634a0b..713e625bfa3aa 100644 > --- a/net/ipv6/ioam6_iptunnel.c > +++ b/net/ipv6/ioam6_iptunnel.c [ ... ] > @@ -412,7 +413,8 @@ static int ioam6_output(struct net *net, struct sock *sk, struct sk_buff *skb) > fl6.saddr = hdr->saddr; > fl6.flowlabel = ip6_flowinfo(hdr); > fl6.flowi6_mark = skb->mark; > - fl6.flowi6_proto = hdr->nexthdr; > + fl6.flowi6_proto = ilwt->tuninfo.eh.nexthdr; [Severity: Medium] Is ilwt->tuninfo.eh.nexthdr guaranteed to still belong to this packet when it is read here? The template is shared per route. Every CPU sending through it writes this field with plain stores: ioam6_do_inline(): tuninfo->eh.nexthdr = hdr->nexthdr; ioam6_do_encap(): tuninfo->eh.nexthdr = NEXTHDR_IPV6; Nothing serializes the lwtunnel output path. Between the return of ioam6_do_inline()/ioam6_do_encap() and this read, another CPU may have stored a different packet's nexthdr. In inline mode that could be TCP, UDP or ICMPv6. In auto mode the inline and encap values alternate. The protocol read here is used by fib6_rule_match() for ipproto selectors. fib6_select_path() also falls back to it when the precomputed mp_hash is 0. The resulting dst is then cached in ilwt->cache. Could the value come from a local instead, for example ipv6_hdr(skb)->nexthdr saved before ioam6_do_inline(), or NEXTHDR_IPV6 for encap? KCSAN would probably also flag this plain read/write pair. [Severity: High] This isn't a bug introduced by this patch, but the new read depends on the same shared template, and the write side of it already looks racy. ioam6_do_inline() stores to the template and then copies it into the packet: tuninfo->eh.nexthdr = hdr->nexthdr; skb_set_transport_header(skb, sizeof(*hdr)); skb_postpush_rcsum(skb, hdr, sizeof(*hdr) + hdrlen); memcpy(skb_transport_header(skb), (u8 *)tuninfo, hdrlen); ioam6_do_encap() does the same with NEXTHDR_IPV6: tuninfo->eh.nexthdr = NEXTHDR_IPV6; memcpy(skb_transport_header(skb), (u8 *)tuninfo, hdrlen); There is no locking here. Can another CPU's store land between this CPU's store and its memcpy()? If so, the packet goes out with an HBH nexthdr that does not describe its payload. With mixed-protocol inline traffic, a TCP segment could be labelled as UDP, and the receiver would drop it. In auto mode, a local packet could be labelled nexthdr=41. The receiver would then parse the UDP header and payload as an inner IPv6 header, so UDP payload bytes 0-31 become the inner saddr/daddr. If the destination runs an ip6ip6 decap tunnel, this might allow spoofed inner packets to be injected there. That part depends on how the receiver is configured. [Severity: Low] mp_hash is now precomputed from the skb. So the main visible effect of this flowi6_proto change seems to be on policy routing, not on the ECMP hash. fib6_rule_match() does: if (rule->ip_proto && (rule->ip_proto != fl6->flowi6_proto)) return 0; Before this patch, "ip -6 rule ... ipproto X" never matched the ioam6 re-lookup. Now it matches the inner L4 protocol in inline mode and IPPROTO_IPV6 in encap mode. IOAM traffic can therefore be routed through a different table than before. This is arguably more consistent with the original lookup. Could the commit message mention this routing behaviour change? > + fl6.mp_hash = rt6_multipath_hash(net, &fl6, skb, NULL); [Severity: Medium] This isn't introduced by this patch. It is in the helper added by the earlier patch in this series, which this re-lookup now depends on, so I am raising it here. ip6_nexthdr_without_ioam() looks for the Hop-by-Hop header at off = sizeof(*iph). It passes that offset to skb_header_pointer(), which indexes from skb->data. That only works when skb->data points at the IPv6 header. It does in ioam6_output(), but not for every caller of rt6_multipath_hash(). icmp6_send() also calls rt6_multipath_hash() with the offending skb. For locally delivered packets, ip6_protocol_deliver_rcu() has already done: pskb_pull(skb, skb_transport_offset(skb)) After that, skb->data sits at the transport header and skb_network_offset(skb) is negative. Two cases reach the helper with such an skb when the packet carries a Hop-by-Hop header: - udpv6_rcv() sends ICMPV6_PORT_UNREACH - ip6_protocol_deliver_rcu() reports an unknown nexthdr In both, ip6_multipath_l3_keys() calls ip6_nexthdr_without_ioam(), which then parses L4 payload bytes as a Hop-by-Hop header and walks them as options. skb_header_pointer() keeps the reads bounds-checked. The problem is the ip_proto fed into the policy 0 hash for the ICMPv6 error. It is no longer the real HBH nexthdr. It is either derived from remote-supplied payload bytes, or it falls back to NEXTHDR_HOP when the payload is short. So the precomputed mp_hash in icmp6_send() no longer reflects the offending flow, and the sender can influence which ECMP sibling the error takes. Should the helper start at skb_network_offset(skb) + sizeof(*iph), or derive the offset from iph? That would match the rest of ip6_multipath_l3_keys(), which works from the header offsets rather than assuming skb->data is the network header. [Severity: High] Does this keep ICMPv6 errors hashed on their quoted flow in inline mode, as the commit message says? ioam6_do_inline() leaves the transport header pointing at the inserted Hop-by-Hop header: skb_set_transport_header(skb, sizeof(*hdr)); ... memcpy(skb_transport_header(skb), (u8 *)tuninfo, hdrlen); Under policy 0 and the policy 2 fallback, rt6_multipath_hash() calls ip6_multipath_l3_keys(). That function gets IPPROTO_ICMPV6 back from ip6_nexthdr_without_ioam(), but then reads the ICMPv6 header at the transport offset: icmph = skb_header_pointer(skb, skb_transport_offset(skb), sizeof(_icmph), &_icmph); ... if (!icmpv6_is_err(icmph->icmp6_type)) goto out; Those bytes are the HBH header, so icmp6_type is eh.nexthdr (58) and icmpv6_is_err() returns false. The outer addresses and flowlabel are hashed instead of the quoted flow. orig_dst was chosen on the quoted flow. For forwarded errors that was done by ip6_route_input(). For local errors, icmp6_send() precomputes fl6.mp_hash from the offending skb. On a dst_cache miss, can this re-lookup pick a different ECMP sibling and cache it for this lwtstate? Also, this comment in ip6_nexthdr_without_ioam() does not hold for the new output-path caller: /* The Hop-by-Hop header was already pulled and validated by * ipv6_parse_hopopts() before routing, so this walk is safe. */ Would it work to compute the hash before the HBH is inserted, or to find the ICMPv6 header past the IOAM HBH? [Severity: High] Locally generated packets take inline mode, or auto mode with !skb->dev. For them, orig_dst came from ip6_route_output() with no skb, so fib6_select_path() used the fl6 branch of rt6_multipath_hash(). This re-lookup uses the skb branch instead. For the same flow, the two branches hash different keys: Policy 0 with default sysctls: the socket lookup hashes flowi6_get_flowlabel(fl6), which is 0 unless a label was set. The skb branch hashes ip6_flowlabel(hdr), which is the auto flowlabel written by ip6_make_flowlabel(). auto_flowlabels is on by default. Policy 1: skb_set_hash_from_sk() sets skb->l4_hash from sk_txhash, so the skb branch returns early instead of hashing the fl6 5-tuple: if (skb->l4_hash) return skb_get_hash_raw(skb) >> 1; Policy 3: the skb branch uses rt6_multipath_custom_hash_skb(), while the original lookup used rt6_multipath_custom_hash_fl6(). Each nexthop has its own lwtstate. A different sibling would fail the orig_dst->lwtstate check, and its dst would be cached in ilwt->cache for later packets. In the default configuration, is the policy 0 divergence described in the commit message still reachable for local traffic? Closing it seems to need the re-lookup to reproduce the original lookup's hash. Changing flowi6_proto alone would not do that. [Severity: Medium] The commit message credits the policy 0 fix to using eh.nexthdr as flowi6_proto. With a non-NULL skb, though, no rt6_multipath_hash() policy reads fl6->flowi6_proto: - policy 0 and the policy 2 fallback use ip6_multipath_l3_keys() - policy 1 uses the flow dissector - policy 3 uses rt6_multipath_custom_hash_skb() fib6_select_path() only recomputes when the precomputed value is zero: if (!fl6->mp_hash && (!match->nh || nexthop_is_multipath(match->nh))) fl6->mp_hash = rt6_multipath_hash(net, fl6, skb, NULL); So the policy 0 behaviour comes entirely from ip6_nexthdr_without_ioam(), which the earlier patch "ipv6: route: consider IOAM options in ECMP hash policy 0" added. This commit still carries its own tag: Fixes: 8cb3bf8bff3c ("ipv6: ioam: Add support for the ip6ip6 encapsulation") If this commit is backported alone, ip6_multipath_l3_keys() would hash outer_iph->nexthdr == NEXTHDR_HOP, and the stated bug would not be fixed. Could the commit message describe this dependency, and say which change actually affects the hash? [Severity: Low] fib6_select_path() treats mp_hash == 0 as "not computed". Its comment assumes that only ICMPv6 errors precompute, and that the value is always non-zero: /* We might have already computed the hash for ICMPv6 errors. In such * case it will always be non-zero. Otherwise now is the time to do it. */ rt6_multipath_hash() returns mhash >> 1, and __flow_hash_from_keys() maps 0 to 1, so the result can be 0. The policy 1 path, skb_get_hash_raw(skb) >> 1, can also return 0. When that happens, fib6_select_path() recomputes from fl6 with no skb, and the skb-based hash is lost. This is rare, and icmp6_send() already has the same sentinel collision. Since ioam6_output() now precomputes too, should the comment be updated, and is the zero case worth handling? > > dst = ip6_route_output(net, NULL, &fl6); > if (dst->error) { [ ... ] > @@ -13,6 +13,7 @@ > #include <linux/ioam6.h> > #include <linux/ioam6_iptunnel.h> > #include <net/dst.h> > +#include <net/ip6_route.h> > #include <net/sock.h> > #include <net/lwtunnel.h> > #include <net/ioam6.h> [Severity: Low] This has no runtime effect, but ioam6_iptunnel.c already includes <net/ip6_route.h> further down, after <net/dst_cache.h>. The new include at the top is a duplicate of it. rt6_multipath_hash() was already visible through the existing include. Could this hunk be dropped? -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028-ipv6_ioam_ecmp-v1-0-9b6715b6f4c0%40kylinos.cn ^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu 2026-10-28 5:45 ` [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 Hangbin Liu 2026-10-28 5:45 ` [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup Hangbin Liu @ 2026-10-28 5:45 ` Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 8:01 ` [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu 2026-10-30 8:12 ` Hangbin Liu 4 siblings, 1 reply; 12+ messages in thread From: Hangbin Liu @ 2026-10-28 5:45 UTC (permalink / raw) To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan Cc: Hangbin Liu, linux-kernel, netdev, linux-kselftest, Hangbin Liu From: Hangbin Liu <liuhangbin@kylinos.cn> Add new test ioam6_ecmp.sh, verifying that the ECMP path taken with IOAM options is identical to the path of the same packet without them under the default hash policy 0, as required by RFC 9486 (C2). - transit input hashing: the sender inserts an IOAM Hop-by-Hop option (inline mode), the forwarder has a multipath route; - encapsulating node re-lookup: the forwarder itself encapsulates (ioam6 encap mode, tunnel dst equal to the original dst) over a multipath route, with a plain ip6ip6 tunnel as the identical-packet-without-IOAM baseline. Signed-off-by: Hangbin Liu <liuhangbin@kylinos.cn> --- tools/testing/selftests/net/Makefile | 1 + tools/testing/selftests/net/ioam6_ecmp.sh | 349 ++++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+) diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile index df215e1a6262..397b19799cc9 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -52,6 +52,7 @@ TEST_PROGS := \ icmp_redirect.sh \ io_uring_zerocopy_tx.sh \ ioam6.sh \ + ioam6_ecmp.sh \ ip6_gre_headroom.sh \ ip6_tunnel.sh \ ip_defrag.sh \ diff --git a/tools/testing/selftests/net/ioam6_ecmp.sh b/tools/testing/selftests/net/ioam6_ecmp.sh new file mode 100755 index 000000000000..c5c82884c757 --- /dev/null +++ b/tools/testing/selftests/net/ioam6_ecmp.sh @@ -0,0 +1,349 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0+ +# +# Verify RFC 9486 C2 for ECMP: with hash policy 0 (the default), the ECMP +# nexthop chosen for a packet carrying IOAM options must be the same as for +# the identical packet without them. +# +# Two scenarios, matching the two places where IOAM changed the hashed +# IPv6 nexthdr: +# +# 1) transit node: alpha inserts an IOAM Hop-by-Hop option (inline mode); +# beta forwards with a multipath route. Policy 0 hashed the raw +# nexthdr (NEXTHDR_HOP vs IPPROTO_ICMPV6), so beta could pick a +# different ECMP sibling than for the same packet without IOAM. +# +# 2) encapsulating node: beta itself encapsulates (ioam6 encap mode, +# tunnel dst = original dst, tunnel src pinned) over a multipath +# route. The ioam6 re-lookup hashed NEXTHDR_HOP instead of the +# transformed packet's real nexthdr (IPPROTO_IPV6). The baseline is +# the same encapsulation without the IOAM option: a plain ip6ip6 +# tunnel over the same multipath route, with the same outer +# addresses. +# +# Hash policy 0 hashes addresses, the flow label and the nexthdr (not L4 +# ports), so whether both variants land on the same ECMP sibling depends +# on the per-netns hash seed. Each test therefore repeats with several +# seeds and fails on the first divergence: pre-fix kernels fail with +# high probability, fixed kernels always pass. +# +# Topology: +# +# alpha beta gamma +# veth0 2001:db8:1::2 <-> veth0 2001:db8:1::1 +# veth1 2001:db8:2:1::1 <-> veth0 2001:db8:2:1::2 +# veth2 2001:db8:2:2::1 <-> veth1 2001:db8:2:2::2 +# +# beta's ECMP route: 2001:db8:2:1::2/128 +# nexthop via 2001:db8:2:1::2 dev veth1 +# nexthop via 2001:db8:2:2::2 dev veth2 +# +# Scenario 1 sends to 2001:db8:2:1::2 (gamma). Scenario 2 sends to +# 2001:db8:3::2, which beta encapsulates towards 2001:db8:2:1::2. The +# link gamma received the packets on is detected from tc flower +# counters (read via jq) on the ingress of gamma's veth0/veth1. The +# filters match the exact measurement packets: ICMPv6 echo requests +# for scenario 1, and the outer addresses of the ip6ip6 packets for +# scenario 2 (flower classifies outer headers only). + +# shellcheck disable=SC2154,SC2329 +source lib.sh + +readonly PKTS=3 +readonly SEEDS="1 2 3 4 5 6 7 8 9 10 11 12" + +ip6tnl_loaded=0 + +cleanup() +{ + cleanup_all_ns + + if [ "$ip6tnl_loaded" != 0 ]; then + modprobe -r ip6_tunnel &>/dev/null + fi +} + +# Replace the tc measurement filters on gamma's ECMP links. $1 is the +# flower match list, e.g. "ip_proto icmpv6 type 128 src_ip <sender>" +# for plain IPv6 echo requests, or the outer addresses for ip6ip6 +# packets (flower classifies the outer header only, see +# FLOW_DISSECTOR_F_STOP_BEFORE_ENCAP in cls_flower). +set_measure_filters() +{ + local match=$1 + local dev + + for dev in veth0 veth1; do + tc -n "$ns_gamma" qdisc del dev "$dev" clsact &>/dev/null + tc -n "$ns_gamma" qdisc add dev "$dev" clsact || return 1 + # shellcheck disable=SC2086 + tc -n "$ns_gamma" filter add dev "$dev" ingress pref 1 \ + protocol ipv6 flower skip_hw $match action pass || return 1 + done +} + +# Number of packets counted by the measurement filter on gamma's $1. +icmp_packets() +{ + tc -n "$ns_gamma" -s -j filter show dev "$1" ingress | + jq '[.[] | .options.actions[]?.stats | + (.packets // .basic.packets // 0)] | add // 0' +} + +# Send PKTS pings from alpha to $1 and report which of gamma's links +# received them: "A" (veth0), "B" (veth1), "TIE" or "LOST:<a>:<b>". +measure_link() +{ + local dst=$1 + local a0 b0 da db + + a0=$(icmp_packets veth0) + b0=$(icmp_packets veth1) + + ip netns exec "$ns_alpha" \ + ping -6 -q -c "$PKTS" -i 0.2 -W 1 -w 2 "$dst" &>/dev/null || true + + da=$(($(icmp_packets veth0) - a0)) + db=$(($(icmp_packets veth1) - b0)) + + if [ $((da + db)) -lt "$PKTS" ]; then + echo "LOST:$da:$db" + return + fi + + if [ "$da" -gt "$db" ]; then + echo "A" + elif [ "$db" -gt "$da" ]; then + echo "B" + else + echo "TIE" + fi +} + +set_alpha_plain() +{ + ip -netns "$ns_alpha" -6 route replace 2001:db8:2:1::/64 \ + via 2001:db8:1::1 dev veth0 +} + +set_alpha_ioam() +{ + ip -netns "$ns_alpha" -6 route replace 2001:db8:2:1::/64 \ + encap ioam6 mode inline trace prealloc \ + type 0x800000 ns 0 size 12 \ + via 2001:db8:1::1 dev veth0 +} + +set_beta_tnl() +{ + # Recreate the tunnel: ip6_tnl caches the outer dst per CPU for a + # few seconds, and neither the route change below nor the + # fib_multipath_hash_seed write resets it. A stale cache would + # resolve the outer sibling under a previous seed (or on another + # CPU), making the baseline flip between attempts of one seed. + ip -netns "$ns_beta" link del ioamcmp &>/dev/null + ip -netns "$ns_beta" link add name ioamcmp type ip6tnl \ + mode ip6ip6 local 2001:db8:2:1::1 remote 2001:db8:2:1::2 + ip -netns "$ns_beta" link set ioamcmp up + ip -netns "$ns_beta" -6 route replace 2001:db8:3::2/128 dev ioamcmp +} + +set_beta_ioam_encap() +{ + ip -netns "$ns_beta" -6 route replace 2001:db8:3::2/128 \ + encap ioam6 mode encap tunsrc 2001:db8:2:1::1 tundst 2001:db8:2:1::2 \ + trace prealloc type 0x800000 ns 0 size 12 dev veth1 +} + +# $1: description; $2: function installing the without-IOAM variant; +# $3: function installing the with-IOAM variant; $4: destination. +run_scenario() +{ + local desc=$1 + local plain=$2 + local ioam=$3 + local dst=$4 + local seed pl il + + log_info "$desc" + + for seed in $SEEDS; do + RET=0 + ip netns exec "$ns_beta" \ + sysctl -wq net.ipv4.fib_multipath_hash_seed="$seed" + + if ! $plain; then + RET=$ksft_fail + log_test "$desc (plain route setup failed, seed $seed)" + continue + fi + pl=$(measure_link "$dst") + if ! $ioam; then + RET=$ksft_fail + log_test "$desc (ioam route setup failed, seed $seed)" + continue + fi + il=$(measure_link "$dst") + + case "$pl$il" in + *LOST*) + RET=$ksft_fail + log_test "$desc (lost packets, seed $seed: without=$pl with=$il)" + continue + ;; + *TIE*) + RET=$ksft_fail + log_test "$desc (no clear link, seed $seed)" + continue + ;; + esac + + if [ "$pl" = "$il" ]; then + log_test "$desc (seed $seed: same link $pl)" + else + RET=$ksft_fail + log_test "$desc (diverged on seed $seed: without IOAM via link $pl, with IOAM via link $il)" + fi + done +} + +setup_topology() +{ + local dev + + setup_ns ns_alpha ns_beta ns_gamma || return 1 + + ip link add veth0 netns "$ns_alpha" type veth \ + peer name veth0 netns "$ns_beta" || return 1 + ip link add veth1 netns "$ns_beta" type veth \ + peer name veth0 netns "$ns_gamma" || return 1 + ip link add veth2 netns "$ns_beta" type veth \ + peer name veth1 netns "$ns_gamma" || return 1 + + ip -n "$ns_alpha" addr add 2001:db8:1::2/64 dev veth0 nodad + ip -n "$ns_beta" addr add 2001:db8:1::1/64 dev veth0 nodad + ip -n "$ns_beta" addr add 2001:db8:2:1::1/64 dev veth1 nodad + ip -n "$ns_beta" addr add 2001:db8:2:2::1/64 dev veth2 nodad + ip -n "$ns_gamma" addr add 2001:db8:2:1::2/64 dev veth0 nodad + ip -n "$ns_gamma" addr add 2001:db8:2:2::2/64 dev veth1 nodad + + ip -n "$ns_alpha" link set veth0 up + ip -n "$ns_beta" link set veth0 up + ip -n "$ns_beta" link set veth1 up + ip -n "$ns_beta" link set veth2 up + ip -n "$ns_gamma" link set veth0 up + ip -n "$ns_gamma" link set veth1 up + + ip netns exec "$ns_alpha" sysctl -wq net.ipv6.auto_flowlabels=0 + ip netns exec "$ns_alpha" \ + sysctl -wq net.ipv6.conf.veth0.accept_ra=0 + ip netns exec "$ns_beta" sysctl -wq net.ipv6.conf.all.forwarding=1 + ip netns exec "$ns_beta" \ + sysctl -wq net.ipv6.fib_multipath_hash_policy=0 + ip netns exec "$ns_gamma" sysctl -wq net.ipv6.conf.veth0.accept_ra=0 + ip netns exec "$ns_gamma" sysctl -wq net.ipv6.conf.veth1.accept_ra=0 + + for dev in veth0 veth1 veth2 + do + ip netns exec "$ns_beta" \ + sysctl -wq net.ipv6.conf.$dev.accept_ra=0 + ip netns exec "$ns_beta" \ + sysctl -wq net.ipv6.conf.$dev.ioam6_enabled=1 + done + + # The ECMP route under test (scenario 1 hashes its selection at + # beta's input; scenario 2 at the encapsulating re-lookup). + ip -netns "$ns_beta" -6 route add 2001:db8:2:1::2/128 \ + nexthop via 2001:db8:2:1::2 dev veth1 \ + nexthop via 2001:db8:2:2::2 dev veth2 + + ip -netns "$ns_alpha" -6 route add 2001:db8:2:1::/64 \ + via 2001:db8:1::1 dev veth0 + ip -netns "$ns_alpha" -6 route add 2001:db8:3::/64 \ + via 2001:db8:1::1 dev veth0 + ip -netns "$ns_gamma" -6 route add 2001:db8:1::/64 \ + via 2001:db8:2:1::1 dev veth0 + + # drop unreachable message when ping 2001:db8:3::2 + tc -n "$ns_alpha" qdisc add dev veth0 clsact + tc -n "$ns_alpha" filter add dev veth0 ingress pref 1 \ + protocol ipv6 flower skip_hw ip_proto icmpv6 type 1 action drop + +} + +setup_tunnel() +{ + if ! lsmod 2>/dev/null | grep -q "ip6_tunnel"; then + modprobe ip6_tunnel &>/dev/null + lsmod 2>/dev/null | grep -q "ip6_tunnel" || \ + ip a 2>/dev/null | grep -q "ip6tnl0" || return 1 + ip6tnl_loaded=1 + fi + + ip -netns "$ns_beta" link add name ioamcmp type ip6tnl \ + mode ip6ip6 local 2001:db8:2:1::1 remote 2001:db8:2:1::2 \ + 2>/dev/null || return 1 + ip -netns "$ns_beta" link set ioamcmp up +} + +################################################################################ +# # +# MAIN # +# # +################################################################################ + +require_command ip +require_command jq +require_command ping +require_command tc + +if [ ! -e /proc/sys/net/ipv4/fib_multipath_hash_seed ]; then + log_test_skip "Kernel without CONFIG_IP_ROUTE_MULTIPATH." + exit "${EXIT_STATUS}" +fi + +setup_topology || { log_test_skip "Setup failed."; exit "${EXIT_STATUS}"; } +trap cleanup EXIT + +# Compat check: can this kernel insert an IOAM trace into a route? +if ! ip -netns "$ns_alpha" -6 route replace 2001:db8:2:1::/64 \ + encap ioam6 mode inline trace prealloc type 0x800000 ns 0 size 12 \ + via 2001:db8:1::1 dev veth0 &>/dev/null +then + log_test_skip "Cannot attach an IOAM trace to a route" + exit "${EXIT_STATUS}" +fi + +log_info "IOAM + ECMP (RFC 9486 C2, hash policy 0)" + +# Warm up both ECMP links (first-packet neighbor/RA activity) and check +# basic connectivity before measuring. +set_alpha_plain +measure_link 2001:db8:2:1::2 >/dev/null + +# Scenario 1 counts the ICMPv6 echo requests sent by alpha. +if ! set_measure_filters "ip_proto icmpv6 type 128 src_ip 2001:db8:1::2"; then + log_test_skip "Cannot install tc flower measurement filters" + exit "${EXIT_STATUS}" +fi +run_scenario \ + "Transit input ECMP with/without IOAM" \ + set_alpha_plain set_alpha_ioam 2001:db8:2:1::2 + +if setup_tunnel; then + # Scenario 2 counts the outer ip6ip6 packets: the echo request is + # not visible to flower (outer headers only), so match the pinned + # tunnel addresses instead. No need to check return value again as + # we have done it at scenario 1 + set_measure_filters "src_ip 2001:db8:2:1::1 dst_ip 2001:db8:2:1::2" + set_beta_tnl + measure_link 2001:db8:3::2 >/dev/null + + run_scenario \ + "Encapsulating node re-lookup ECMP with/without IOAM" \ + set_beta_tnl set_beta_ioam_encap 2001:db8:3::2 +else + log_test_skip "Encapsulating node re-lookup ECMP with/without IOAM (no ip6_tunnel)" +fi + +exit "${EXIT_STATUS}" -- 2.56.0 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) 2026-10-28 5:45 ` [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) Hangbin Liu @ 2026-10-30 11:47 ` netdev-bot+sashiko 0 siblings, 0 replies; 12+ messages in thread From: netdev-bot+sashiko @ 2026-10-30 11:47 UTC (permalink / raw) To: hangbin.liu Cc: dsahern, idosch, davem, edumazet, kuba, pabeni, horms, justin.iurman, shuah, linux-kernel, netdev, linux-kselftest, liuhangbin Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider. Critical: 0 · High: 0 · Medium: 2 · Low: 5 If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] The compatibility probe in MAIN (lines 308-315) only checks that an inline-mode IOAM trace can be attached (`encap ioam6 mode… - [Medium] In MAIN, `setup_topology || { log_test_skip "Setup failed."; exit ...; }` (line 305) runs before `trap cleanup EXIT` (line 306). - [Low] The commit message and the ioam6_ecmp.sh header comment describe scenario 2 differently from what the script does. - [Low] ioam6_ecmp.sh skips the whole test when /proc/sys/net/ipv4/fib_multipath_hash_seed is missing (lines 300-303). - [Low] The comment in set_beta_tnl() (lines 139-143) says "ip6_tnl caches the outer dst per CPU for a few seconds". - [Low] setup_topology() does not check the return value of the multipath route install, which is the core of the test (`ip -netns $ns_beta -6… - [Low] setup_tunnel() (lines 274-287) decides whether the test loaded ip6_tunnel only from `lsmod | grep -q ip6_tunnel`. -- selftests: net: add ioam6 ECMP test (RFC 9486 C2) This adds ioam6_ecmp.sh. The test checks that, with hash policy 0, the ECMP nexthop chosen for a packet carrying IOAM options is the same as for the packet without them. It covers a transit node forwarding inline-inserted IOAM Hop-by-Hop packets, and an encapsulating node doing the ioam6 encap re-lookup with a plain ip6ip6 tunnel as the baseline. > diff --git a/tools/testing/selftests/net/ioam6_ecmp.sh b/tools/testing/selftests/net/ioam6_ecmp.sh > new file mode 100755 > index 0000000000000..c5c82884c7579 > --- /dev/null > +++ b/tools/testing/selftests/net/ioam6_ecmp.sh > @@ -0,0 +1,349 @@ > +#!/bin/bash > +# SPDX-License-Identifier: GPL-2.0+ [ ... ] > +# 2) encapsulating node: beta itself encapsulates (ioam6 encap mode, > +# tunnel dst = original dst, tunnel src pinned) over a multipath > +# route. The ioam6 re-lookup hashed NEXTHDR_HOP instead of the > +# transformed packet's real nexthdr (IPPROTO_IPV6). The baseline is > +# the same encapsulation without the IOAM option: a plain ip6ip6 > +# tunnel over the same multipath route, with the same outer > +# addresses. [Severity: Low] Is "tunnel dst = original dst" accurate here? The commit message says the same thing: - encapsulating node re-lookup: the forwarder itself encapsulates (ioam6 encap mode, tunnel dst equal to the original dst) over a multipath route, with a plain ip6ip6 tunnel as the identical-packet-without-IOAM baseline. The script sends to 2001:db8:3::2, but set_beta_ioam_encap() installs this route: 2001:db8:3::2/128 encap ioam6 mode encap ... tundst 2001:db8:2:1::2 So the tunnel dst is not the original dst. The topology comment further down describes this correctly. If the tunnel dst really were the original dst, the re-lookup would resolve back to the same ioam6 route: net/ipv6/ioam6_iptunnel.c:ioam6_output() { ... if (orig_dst->lwtstate == dst->lwtstate) dst_cache_set_ip6(&ilwt->cache, &ilwt->null_rt.dst, &fl6.saddr); ... } In that case orig_output would be used and the multipath route would never be consulted. The ip6ip6 baseline is also not an identical packet on the wire. ip6_tnl_xmit() pushes an encap-limit Destination Options header when encap_limit >= 0, and it sets the flow label with ip6_make_flowlabel() after the route lookup. Only the hash inputs at beta's lookup match. Could the commit message and this header comment be updated to match what the test actually does? [ ... ] > +set_beta_tnl() > +{ > + # Recreate the tunnel: ip6_tnl caches the outer dst per CPU for a > + # few seconds, and neither the route change below nor the > + # fib_multipath_hash_seed write resets it. A stale cache would > + # resolve the outer sibling under a previous seed (or on another > + # CPU), making the baseline flip between attempts of one seed. [Severity: Low] This isn't a bug, but is "for a few seconds" accurate? ip6_tnl_xmit() reads the cache with dst_cache_get(&t->dst_cache), and dst_cache_per_cpu_get() only drops an entry in two cases: net/core/dst_cache.c:dst_cache_per_cpu_get() { ... if (unlikely(!time_after(idst->refresh_ts, READ_ONCE(dst_cache->reset_ts)) || (READ_ONCE(dst->obsolete) && !dst->ops->check(dst, idst->cookie)))) { ... } That means an explicit dst_cache_reset(), or an obsolete dst that fails ->check(). There is no time-based expiry, so a cached outer dst can stay indefinitely. Recreating the tunnel for every seed is still the right workaround. The comment, though, could lead someone to replace the recreation with a sleep. [ ... ] > +set_beta_ioam_encap() > +{ > + ip -netns "$ns_beta" -6 route replace 2001:db8:3::2/128 \ > + encap ioam6 mode encap tunsrc 2001:db8:2:1::1 tundst 2001:db8:2:1::2 \ > + trace prealloc type 0x800000 ns 0 size 12 dev veth1 > +} [Severity: Medium] What happens on a kernel or iproute2 that supports ioam6 inline and encap modes but not the tunsrc attribute? The compat check in MAIN only probes inline mode, so it passes, and setup_tunnel() succeeds. This route install would then fail on every seed, and run_scenario() takes this path: if ! $ioam; then RET=$ksft_fail log_test "$desc (ioam route setup failed, seed $seed)" The result is 12 hard FAILs instead of a SKIP. ioam6.sh already treats tunsrc as a feature under test. Is the tunsrc pin needed at all? Without it, ioam6_do_encap() calls ipv6_dev_get_saddr() on the original dst device (veth1), which should pick 2001:db8:2:1::1 anyway. [ ... ] > + # The ECMP route under test (scenario 1 hashes its selection at > + # beta's input; scenario 2 at the encapsulating re-lookup). > + ip -netns "$ns_beta" -6 route add 2001:db8:2:1::2/128 \ > + nexthop via 2001:db8:2:1::2 dev veth1 \ > + nexthop via 2001:db8:2:2::2 dev veth2 [Severity: Low] Should this route add check its return value? If it fails, beta still has the connected 2001:db8:2:1::/64 prefix on veth1, so all traffic to 2001:db8:2:1::2 leaves through veth1 and arrives on gamma's veth0. measure_link() would then return "A" for both variants on every seed. run_scenario() would report PASS even though no ECMP route exists. Adding "|| return 1" here would catch this. [ ... ] > +setup_tunnel() > +{ > + if ! lsmod 2>/dev/null | grep -q "ip6_tunnel"; then > + modprobe ip6_tunnel &>/dev/null > + lsmod 2>/dev/null | grep -q "ip6_tunnel" || \ > + ip a 2>/dev/null | grep -q "ip6tnl0" || return 1 > + ip6tnl_loaded=1 > + fi [Severity: Low] Can this unload an ip6_tunnel module the test did not load? Consider lsmod being unavailable or failing while modprobe works, with ip6_tunnel already loaded: - the first grep fails; - modprobe ip6_tunnel succeeds as a no-op; - grep ip6tnl0 matches the existing fallback device in the init netns; - ip6tnl_loaded=1 is set. cleanup() then runs: modprobe -r ip6_tunnel &>/dev/null This removes the module if nothing else is using it. When ip6_tunnel is built in, lsmod does not list it either, so ip6tnl_loaded=1 is set in that case too. The later modprobe -r then fails harmlessly. ioam6.sh uses the same heuristic. [ ... ] > +if [ ! -e /proc/sys/net/ipv4/fib_multipath_hash_seed ]; then > + log_test_skip "Kernel without CONFIG_IP_ROUTE_MULTIPATH." > + exit "${EXIT_STATUS}" > +fi [Severity: Low] Should tools/testing/selftests/net/config be updated for this test? fib_multipath_hash_seed only exists with CONFIG_IP_ROUTE_MULTIPATH. That option depends on CONFIG_IP_ADVANCED_ROUTER, neither has a default, and neither is in the net selftests config fragment. On a kernel built from a minimal base plus that fragment, this test would always SKIP. CONFIG_IPV6_TUNNEL, which scenario 2 needs, is not listed explicitly either. It only gets enabled because CONFIG_IPV6_GRE=m selects it. > + > +setup_topology || { log_test_skip "Setup failed."; exit "${EXIT_STATUS}"; } > +trap cleanup EXIT [Severity: Medium] Can this leak ns_alpha, ns_beta and ns_gamma? setup_ns has already created them by the time setup_topology() returns failure. The trap is installed only after this exit, so cleanup_all_ns never runs and the namespaces and any veths stay on the host. setup_topology() can fail after setup_ns through the veth "|| return 1" paths. Its exit status is also that of its last command: tc -n "$ns_alpha" filter add dev veth0 ingress pref 1 \ protocol ipv6 flower skip_hw ip_proto icmpv6 type 1 action drop That command fails on a kernel without cls_flower or act_gact. setup_ns only cleans up when ip netns add itself fails, and lib.sh does not install an EXIT trap. As a side effect, a missing flower is reported as "Setup failed.", and the later "Cannot install tc flower measurement filters" skip is never reached for that cause. Would installing the trap before calling setup_topology() fix this? [ ... ] -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028-ipv6_ioam_ecmp-v1-0-9b6715b6f4c0%40kylinos.cn ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu ` (2 preceding siblings ...) 2026-10-28 5:45 ` [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) Hangbin Liu @ 2026-10-28 8:01 ` Hangbin Liu 2026-10-28 23:58 ` Jakub Kicinski 2026-10-30 8:12 ` Hangbin Liu 4 siblings, 1 reply; 12+ messages in thread From: Hangbin Liu @ 2026-10-28 8:01 UTC (permalink / raw) To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan Cc: linux-kernel, netdev, linux-kselftest, Hangbin Liu On Wed, Oct 28, 2026 at 01:45:28PM +0800, Hangbin Liu wrote: > This series makes IOAM insertion transparent to IPv6 ECMP, as required > by RFC 9486 (C2): a packet carrying IOAM options must be forwarded > along the same path as the identical packet without them. Sorry, forgot to add target repo "net". I will add it in next version. Thanks Hangbin ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP 2026-10-28 8:01 ` [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu @ 2026-10-28 23:58 ` Jakub Kicinski 2026-10-29 0:38 ` Hangbin Liu 2026-10-29 9:02 ` Hangbin Liu 0 siblings, 2 replies; 12+ messages in thread From: Jakub Kicinski @ 2026-10-28 23:58 UTC (permalink / raw) To: Hangbin Liu Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan, linux-kernel, netdev, linux-kselftest, Hangbin Liu On Wed, 28 Oct 2026 16:01:13 +0800 Hangbin Liu wrote: > On Wed, Oct 28, 2026 at 01:45:28PM +0800, Hangbin Liu wrote: > > This series makes IOAM insertion transparent to IPv6 ECMP, as required > > by RFC 9486 (C2): a packet carrying IOAM options must be forwarded > > along the same path as the identical packet without them. > > Sorry, forgot to add target repo "net". I will add it in next version. CI bot says: The new selftest added alongside this series (tools/testing/selftests/net/ioam6_ecmp.sh) is failing in our CI on the "Transit input ECMP with/without IOAM" scenario: TEST: Transit input ECMP with/without IOAM (diverged on seed 1: without IOAM via link A, with IOAM via link B) [FAIL] TEST: Transit input ECMP with/without IOAM (seed 2..12) [ OK ] TEST: Encapsulating node re-lookup ECMP with/without IOAM (seed 1..12) [ OK ] 11 of the 12 "transit input" seeds pass, and the whole "encapsulating node re-lookup" scenario passes on all seeds, so this looks like an edge case in the Hop-by-Hop nexthdr-recovery logic used for the policy-0 multipath hash, rather than a test infra/flake issue. Could you take a look at whether the recovered "virtual" nexthdr (or the padding/option-length walk used to compute it) is correct for all IOAM option layouts/trace sizes, since one specific packet still ends up hashed to a different ECMP sibling than the equivalent packet without IOAM? ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP 2026-10-28 23:58 ` Jakub Kicinski @ 2026-10-29 0:38 ` Hangbin Liu 2026-10-29 9:02 ` Hangbin Liu 1 sibling, 0 replies; 12+ messages in thread From: Hangbin Liu @ 2026-10-29 0:38 UTC (permalink / raw) To: Jakub Kicinski Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan, linux-kernel, netdev, linux-kselftest, Hangbin Liu Hi Jakub, On Wed, Oct 28, 2026 at 04:58:12PM -0700, Jakub Kicinski wrote: > On Wed, 28 Oct 2026 16:01:13 +0800 Hangbin Liu wrote: > > On Wed, Oct 28, 2026 at 01:45:28PM +0800, Hangbin Liu wrote: > > > This series makes IOAM insertion transparent to IPv6 ECMP, as required > > > by RFC 9486 (C2): a packet carrying IOAM options must be forwarded > > > along the same path as the identical packet without them. > > > > Sorry, forgot to add target repo "net". I will add it in next version. > > CI bot says: > > The new selftest added alongside this series (tools/testing/selftests/net/ioam6_ecmp.sh) > is failing in our CI on the "Transit input ECMP with/without IOAM" scenario: > > TEST: Transit input ECMP with/without IOAM (diverged on seed 1: without IOAM via > link A, with IOAM via link B) [FAIL] > TEST: Transit input ECMP with/without IOAM (seed 2..12) [ OK ] > TEST: Encapsulating node re-lookup ECMP with/without IOAM (seed 1..12) [ OK ] > > 11 of the 12 "transit input" seeds pass, and the whole "encapsulating node re-lookup" > scenario passes on all seeds, so this looks like an edge case in the Hop-by-Hop > nexthdr-recovery logic used for the policy-0 multipath hash, rather than a test > infra/flake issue. Could you take a look at whether the recovered "virtual" nexthdr > (or the padding/option-length walk used to compute it) is correct for all IOAM option > layouts/trace sizes, since one specific packet still ends up hashed to a different ECMP > sibling than the equivalent packet without IOAM? Thanks for the report. I run it locally multi times and didn't got this issue. I will try reproduce it first and check the reason. Thanks Hangbin ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP 2026-10-28 23:58 ` Jakub Kicinski 2026-10-29 0:38 ` Hangbin Liu @ 2026-10-29 9:02 ` Hangbin Liu 1 sibling, 0 replies; 12+ messages in thread From: Hangbin Liu @ 2026-10-29 9:02 UTC (permalink / raw) To: Jakub Kicinski Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan, linux-kernel, netdev, linux-kselftest, Hangbin Liu Hi Jakub, On Wed, Oct 28, 2026 at 04:58:12PM -0700, Jakub Kicinski wrote: > On Wed, 28 Oct 2026 16:01:13 +0800 Hangbin Liu wrote: > > On Wed, Oct 28, 2026 at 01:45:28PM +0800, Hangbin Liu wrote: > > > This series makes IOAM insertion transparent to IPv6 ECMP, as required > > > by RFC 9486 (C2): a packet carrying IOAM options must be forwarded > > > along the same path as the identical packet without them. > > > > Sorry, forgot to add target repo "net". I will add it in next version. > > CI bot says: > > The new selftest added alongside this series (tools/testing/selftests/net/ioam6_ecmp.sh) > is failing in our CI on the "Transit input ECMP with/without IOAM" scenario: > > TEST: Transit input ECMP with/without IOAM (diverged on seed 1: without IOAM via > link A, with IOAM via link B) [FAIL] > TEST: Transit input ECMP with/without IOAM (seed 2..12) [ OK ] > TEST: Encapsulating node re-lookup ECMP with/without IOAM (seed 1..12) [ OK ] > > 11 of the 12 "transit input" seeds pass, and the whole "encapsulating node re-lookup" > scenario passes on all seeds, so this looks like an edge case in the Hop-by-Hop > nexthdr-recovery logic used for the policy-0 multipath hash, rather than a test > infra/flake issue. Could you take a look at whether the recovered "virtual" nexthdr > (or the padding/option-length walk used to compute it) is correct for all IOAM option > layouts/trace sizes, since one specific packet still ends up hashed to a different ECMP > sibling than the equivalent packet without IOAM? OK, after running the test 50 times on a debug kernel and over 70 times on a normal kernel, I've finally reproduced the issue and identified the root cause. The reason is the ping `-w` parameter. I originally thought it set the maximum wait time, but it actually keeps sending packets regardless of how many packets have been sent or received. Combined with `-i 0.2`, it sometimes sends 10 packets, which carry over into the seed 1 test and affect the result. That's why the debug kernel case worked and the normal kernel failed in Jakub's environment, while it always passed in my laptop test environment. After removing the "-W 1 -w 2" parameters, I ran the test 200 times and all passed. I will post the fix in the next version. TL;DR -------------------- Here are my testing steps. After reproducing the issue, I dumped the tc output and found 10 extra ICMPv6 echo requests. filter protocol ipv6 pref 1 flower chain 0 filter protocol ipv6 pref 1 flower chain 0 handle 0x1 eth_type ipv6 ip_proto icmpv6 src_ip 2001:db8:1::2 icmp_type 128 skip_hw not_in_hw action order 1: gact action pass random type none pass val 0 index 1 ref 1 bind 1 installed 0 sec Action statistics: Sent 1040 bytes 10 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0 on gamma veth0 at the start of seed 1 testing, but *a0* is 0. This means some echo requests are still in flight during this window. I then captured traffic with tcpdump and confirmed those echo requests are present on gamma veth0: 15:47:09.161066 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 1, length 64 15:47:09.161083 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 2, length 64 15:47:09.161086 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 3, length 64 15:47:09.161088 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 4, length 64 15:47:09.161091 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 5, length 64 15:47:09.161093 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 6, length 64 15:47:09.161095 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 7, length 64 15:47:09.161097 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 8, length 64 15:47:09.161100 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 9, length 64 15:47:09.161102 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3254, seq 10, length 64 The question is: where do these 10 packets come from, and why are they sent within this narrow window? We expect only 3 echo requests during the ECMP link warm-up, which does not match the tcpdump capture. The timestamps, however, show the traffic starts during the warm-up phase. The subsequent echo requests on veth1 are correctly sent as 3 packets with a 0.2s interval. 15:47:09.221513 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3284, seq 1, length 64 15:47:09.426247 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3284, seq 2, length 64 15:47:09.633125 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3284, seq 3, length 64 It looks like the skbs were held in the unresolved neighbour queue and flushed after receiving the NA, causing the packets to be delivered to gamma ingress later. Then I tried to send 1 packet on before the warm up. ip netns exec "$ns_alpha" ping -6 -q -c 1 -i 0.2 -W 1 -w 2 2001:db8:2:1::2 &> /dev/null Now the packet shows 16:05:20.744986 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 1, length 64 16:05:20.744987 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 2, length 64 16:05:20.744988 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 3, length 64 16:05:20.744988 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 4, length 64 16:05:20.744989 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 5, length 64 16:05:20.744990 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 6, length 64 16:05:20.744990 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 7, length 64 16:05:20.744991 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 8, length 64 16:05:20.744992 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 9, length 64 16:05:20.744992 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3765, seq 10, length 64 16:05:20.744992 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3774, seq 1, length 64 16:05:20.936373 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3774, seq 2, length 64 16:05:21.137153 IP6 2001:db8:1::2 > 2001:db8:2:1::2: ICMP6, echo request, id 3774, seq 3, length 64 The warm-up packets are correctly back to 3 with a 0.2s interval. However, the first `-c 1` ping still generates 10 packets. With set -x, it shows + ip netns exec ns_alpha-nBg602 ping -6 -q -c 1 -i 0.2 -W 1 -w 2 2001:db8:2:1::2 PING 2001:db8:2:1::2 (2001:db8:2:1::2) 56 data bytes --- 2001:db8:2:1::2 ping statistics --- 10 packets transmitted, 0 received, 100% packet loss, time 1865ms After checking, I finally got the answer: -w deadline: Specify a timeout, in seconds, before ping exits regardless of how many packets have been sent or received. When `-w` is used, the `-c` packet count no longer acts as a hard limit. Ping keeps sending packets until either the expected number of replies are received or the timeout expires. In our case, the timeout was hit. All 10 packets (sent at a 0.2s interval within the 2s window) got deferred and surfaced during the seed 1 test, corrupting the result. Thanks Hangbin ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu ` (3 preceding siblings ...) 2026-10-28 8:01 ` [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu @ 2026-10-30 8:12 ` Hangbin Liu 4 siblings, 0 replies; 12+ messages in thread From: Hangbin Liu @ 2026-10-30 8:12 UTC (permalink / raw) To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Justin Iurman, Shuah Khan Cc: linux-kernel, netdev, linux-kselftest, Hangbin Liu On Wed, Oct 28, 2026 at 01:45:28PM +0800, Hangbin Liu wrote: > This series makes IOAM insertion transparent to IPv6 ECMP, as required > by RFC 9486 (C2): a packet carrying IOAM options must be forwarded > along the same path as the identical packet without them. > > Patch 1 fixes the transit side: hash policy 0 hashed the raw IPv6 > nexthdr, which becomes NEXTHDR_HOP once an IOAM Hop-by-Hop header is > inserted. The policy-0 key (and the ICMPv6-error flow selection) now > uses the nexthdr the packet would have had if the Hop-by-Hop header > carried only IOAM options and padding. > > Patch 2 fixes the injecting node: the ioam6 output re-lookup on a > destination-cache miss hashed the transformed packet's NEXTHDR_HOP > instead of the original nexthdr (inline) or IPPROTO_IPV6 (encap), and > rebuilt the flow keys from the outer header only. > > Patch 3 adds selftests/net/ioam6_ecmp.sh, covering both places: a > transit forwarder with a multipath route, and an encapsulating node > compared against a plain ip6ip6 baseline. > > The issue was found while reviewing RFC9486. It can be reproduced and > verified by the newly added ioam6_ecmp.sh test. > > Tested with: > ecmp_rehash.sh > fib_nexthops.sh > ioam6_ecmp.sh > tcp_ecmp_failover.sh > forwarding/gre_inner_v4_multipath.sh > forwarding/gre_inner_v6_multipath.sh > forwarding/gre_multipath_nh_res.sh > forwarding/gre_multipath_nh.sh > forwarding/gre_multipath.sh > forwarding/ip6gre_inner_v4_multipath.sh > forwarding/ip6gre_inner_v6_multipath.sh > forwarding/router_mpath_nh_res.sh > forwarding/router_mpath_nh.sh > forwarding/router_multipath.sh > > Signed-off-by: Hangbin Liu <liuhangbin@kylinos.cn> > --- > Hangbin Liu (3): > ipv6: route: consider IOAM options in ECMP hash policy 0 > ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup > selftests: net: add ioam6 ECMP test (RFC 9486 C2) > > include/uapi/linux/in6.h | 1 + > net/ipv6/ioam6_iptunnel.c | 4 +- > net/ipv6/route.c | 67 +++++- > tools/testing/selftests/net/Makefile | 1 + > tools/testing/selftests/net/ioam6_ecmp.sh | 349 ++++++++++++++++++++++++++++++ > 5 files changed, 419 insertions(+), 3 deletions(-) > --- > base-commit: 5d7f3375cd574fc83f0cd30839b21455c0e79ae2 > change-id: 20261020-ipv6_ioam_ecmp-ae4ef0a4bdae > > Best regards, > -- > Hangbin Liu <liuhangbin@kylinos.cn> > Sashiko found some issues. I will fix them and post new version. pw-bot: cr ^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-10-30 11:47 UTC | newest] Thread overview: 12+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-10-28 5:45 [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu 2026-10-28 5:45 ` [PATCH 1/3] ipv6: route: consider IOAM options in ECMP hash policy 0 Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 5:45 ` [PATCH 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 5:45 ` [PATCH 3/3] selftests: net: add ioam6 ECMP test (RFC 9486 C2) Hangbin Liu 2026-10-30 11:47 ` netdev-bot+sashiko 2026-10-28 8:01 ` [PATCH 0/3] net: ioam6: makes IOAM insertion transparent to IPv6 ECMP Hangbin Liu 2026-10-28 23:58 ` Jakub Kicinski 2026-10-29 0:38 ` Hangbin Liu 2026-10-29 9:02 ` Hangbin Liu 2026-10-30 8:12 ` Hangbin Liu
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox