[PATCH net v2 2/3] ipv6: ioam6: use transformed packet nexthdr for ECMP re-lookup

From: Hangbin Liu

Date: Tue Nov 03 2026 - 04:28:40 EST


From: Hangbin Liu <liuhangbin@xxxxxxxxxx>

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.

On a destination-cache miss the old code always re-looked up the
transformed packet under hash policy 0 with NEXTHDR_HOP (and, for
local flows, keys that do not match the original socket lookup), so
it could pick a different ECMP sibling.

Inline insertion leaves the destination unchanged: reuse orig_dst and
cache the fake null_rt, keeping the sibling from the original lookup.

Encapsulation may switch the destination to tundst, so it still
re-looks up. Take flowi6_proto from the transformed packet HBH (41
after encap, so ipproto rules match as for plain ip6ip6) and
precompute fl6.mp_hash from the packet, since ip6_route_output()
carries no skb and would otherwise drop ICMPv6 quoted-flow and L4
keys.

Depends on patch "ipv6: route: consider IOAM options in ECMP hash
policy 0".

Fixes: 8cb3bf8bff3c ("ipv6: ioam: Add support for the ip6ip6 encapsulation")
Signed-off-by: Hangbin Liu <liuhangbin@xxxxxxxxxx>
---
For AI review:

The mp_hash precompute makes policy-2 hash inner keys while plain ip6ip6
baseline hashes outer — pre-patch also diverged (proto 0 vs 41);
C2 for policy 2 was broken before and after, no new violation class, and
the patches claim/test policy 0 only.
---
net/ipv6/ioam6_iptunnel.c | 79 +++++++++++++++++++++++++++++++----------------
1 file changed, 53 insertions(+), 26 deletions(-)

diff --git a/net/ipv6/ioam6_iptunnel.c b/net/ipv6/ioam6_iptunnel.c
index cfb2c41634a0..246533276be8 100644
--- a/net/ipv6/ioam6_iptunnel.c
+++ b/net/ipv6/ioam6_iptunnel.c
@@ -405,34 +405,61 @@ static int ioam6_output(struct net *net, struct sock *sk, struct sk_buff *skb)

if (unlikely(!dst)) {
struct ipv6hdr *hdr = ipv6_hdr(skb);
- struct flowi6 fl6;
-
- memset(&fl6, 0, sizeof(fl6));
- fl6.daddr = hdr->daddr;
- fl6.saddr = hdr->saddr;
- fl6.flowlabel = ip6_flowinfo(hdr);
- fl6.flowi6_mark = skb->mark;
- fl6.flowi6_proto = hdr->nexthdr;
-
- dst = ip6_route_output(net, NULL, &fl6);
- if (dst->error) {
- err = dst->error;
- goto drop;
- }

- /* If the destination is the same after transformation (which is
- * a valid use case for IOAM), then we don't want to add it to
- * the cache in order to avoid a reference loop. Instead, we add
- * our fake dst_entry to the cache as a way to detect this case.
- * Otherwise, we add the resolved destination to the cache.
+ /* Inline insertion does not change the destination. Keep
+ * orig_dst so the ECMP sibling from the original lookup is
+ * preserved (RFC 9486 C2 for locally generated traffic).
+ * Cache the fake null_rt so later packets take the same path.
*/
- local_bh_disable();
- if (orig_dst->lwtstate == dst->lwtstate)
- dst_cache_set_ip6(&ilwt->cache,
- &ilwt->null_rt.dst, &fl6.saddr);
- else
- dst_cache_set_ip6(&ilwt->cache, dst, &fl6.saddr);
- local_bh_enable();
+ if (ilwt->mode == IOAM6_IPTUNNEL_MODE_INLINE ||
+ (ilwt->mode == IOAM6_IPTUNNEL_MODE_AUTO && !skb->dev)) {
+ dst = orig_dst;
+ dst_hold(dst);
+
+ local_bh_disable();
+ dst_cache_set_ip6(&ilwt->cache, &ilwt->null_rt.dst,
+ &hdr->saddr);
+ local_bh_enable();
+ } else {
+ /* Encapsulation may change the destination (tundst);
+ * re-lookup with the transformed packet keys so the
+ * outer hash matches the identical packet without IOAM.
+ */
+ const struct ipv6_opt_hdr *hhdr;
+ struct flowi6 fl6;
+
+ hhdr = (const struct ipv6_opt_hdr *)skb_transport_header(skb);
+
+ memset(&fl6, 0, sizeof(fl6));
+ fl6.daddr = hdr->daddr;
+ fl6.saddr = hdr->saddr;
+ fl6.flowlabel = ip6_flowinfo(hdr);
+ fl6.flowi6_mark = skb->mark;
+ fl6.flowi6_proto = hhdr->nexthdr;
+ fl6.mp_hash = rt6_multipath_hash(net, &fl6, skb, NULL);
+
+ dst = ip6_route_output(net, NULL, &fl6);
+ if (dst->error) {
+ err = dst->error;
+ goto drop;
+ }
+
+ /* If the destination is the same after transformation
+ * (which is a valid use case for IOAM), then we don't
+ * want to add it to the cache in order to avoid a
+ * reference loop. Instead, we add our fake dst_entry
+ * to the cache as a way to detect this case.
+ * Otherwise, we add the resolved destination to the
+ * cache.
+ */
+ local_bh_disable();
+ if (orig_dst->lwtstate == dst->lwtstate)
+ dst_cache_set_ip6(&ilwt->cache,
+ &ilwt->null_rt.dst, &fl6.saddr);
+ else
+ dst_cache_set_ip6(&ilwt->cache, dst, &fl6.saddr);
+ local_bh_enable();
+ }

err = skb_cow_head(skb, LL_RESERVED_SPACE(dst_dev(dst)));
if (unlikely(err))

--
2.56.0