On Tue, 05 May 2026 01:30:13 +0900 Yuya Kusakabe <[email protected]> wrote:
Hi Yuya, I do not repeat below the points from my cover letter and patch 1-2 replies (drop reasons, OIF/VRF removal, C helper, coding style, etc.). > Add the End.M.GTP6.E behavior (RFC 9433 Section 6.5), the IPv6 dual > of End.M.GTP4.E. An End.M.GTP6.E SID always sits in the penultimate > position of an SR Policy (RFC 9433 Section 6.5 Notes); when it > becomes the active SID (segments_left == 1) the kernel pops the > IPv6/SRH outer, recovers TEID and QFI from the 40-bit > Args.Mob.Session field encoded in the locator-relative slice of the > SID, and re-encapsulates the inner T-PDU in IPv6/UDP/GTP-U toward > the next segment held in SRH[0]. > > The flow info, traffic class and hop limit are propagated from the > inbound IPv6 outer to the new outer (RFC 6040). > > When net.netfilter.nf_hooks_lwtunnel=1, the inner T-PDU traverses > NF_INET_PRE_ROUTING between the SRv6 strip and the GTP-U push, > mirroring End.DX4 / End.DX6. > > Configuration: > > ip -6 route add 2001:db8:e::/64 \ > encap seg6local action End.M.GTP6.E src 2001:db8:2::1 \ > dev <dev> SEG6_LOCAL_MOBILE_SRC_ADDR (the "src" attribute) is copied verbatim into the outer IPv6 source address. In patch 2 (End.M.GTP4.E) the same attribute is used as a template from which bits are extracted to form the IPv4 source address, and may be entirely unused depending on v4_mask_len. This UAPI overload needs revision. > > Link: https://www.rfc-editor.org/rfc/rfc9433.html#section-6.5 > Link: https://www.rfc-editor.org/rfc/rfc6040 > Signed-off-by: Yuya Kusakabe <[email protected]> > --- > include/uapi/linux/seg6_local.h | 2 + > net/ipv6/seg6_local.c | 312 ++++++++++++++++ > tools/testing/selftests/net/Makefile | 1 + > .../selftests/net/srv6_end_m_gtp6_e_test.sh | 402 > +++++++++++++++++++++ > 4 files changed, 717 insertions(+) > > diff --git a/include/uapi/linux/seg6_local.h b/include/uapi/linux/seg6_local.h > index b42cb526bb81..8e46ede2980d 100644 > --- a/include/uapi/linux/seg6_local.h > +++ b/include/uapi/linux/seg6_local.h > @@ -75,6 +75,8 @@ enum { > SEG6_LOCAL_ACTION_END_MAP = 17, > /* SRv6 to IPv4/GTP-U encap (RFC 9433 Section 6.6) */ > SEG6_LOCAL_ACTION_END_M_GTP4_E = 18, > + /* SRv6 to IPv6/GTP-U encap (RFC 9433 Section 6.5) */ > + SEG6_LOCAL_ACTION_END_M_GTP6_E = 19, > > __SEG6_LOCAL_ACTION_MAX, > }; > diff --git a/net/ipv6/seg6_local.c b/net/ipv6/seg6_local.c > index 4051fe89e6d1..4e5d138c3657 100644 > --- a/net/ipv6/seg6_local.c > +++ b/net/ipv6/seg6_local.c > + [snip] > +static int input_action_end_m_gtp6_e_finish(struct net *net, > + struct sock *sk, > + struct sk_buff *skb) > +{ > + enum skb_drop_reason reason = SKB_DROP_REASON_SEG6_MOBILE_NOMEM; > + struct seg6_mobile_gtp6_e_cb cb = *SEG6_MOBILE_GTP6_E_CB(skb); > + struct dst_entry *orig_dst = skb_dst(skb); > + const struct seg6_mobile_info *minfo; > + struct seg6_local_lwt *slwt; > + struct ipv6hdr *new_ip6h; > + struct udphdr *uh; > + > + slwt = seg6_local_lwtunnel(orig_dst->lwtstate); > + minfo = &slwt->mobile_info; > + Same dst/lwtstate issue as patch 2. > + /* Reject GSO packets that would not fit the egress IPv6/UDP/GTP-U > + * path after our outer headers are added; the GSO segmenter cannot > + * adjust mss across SRv6 -> GTP-U conversion. Skip the check > + * entirely when no MTU is known on the current dst. > + */ > + if (skb_is_gso(skb)) { > + unsigned int ovhd = sizeof(*new_ip6h) + sizeof(*uh) + > + sizeof(struct gtp1_header_long) + > + sizeof(struct seg6_mobile_pdu_session_ext); > + unsigned int mtu = dst_mtu(skb_dst(skb)); > + > + if (mtu && (mtu <= ovhd || > + !skb_gso_validate_network_len(skb, mtu - ovhd))) { > + reason = SKB_DROP_REASON_SEG6_MOBILE_MTU_EXCEEDED; > + goto drop; > + } > + } > + > + /* Reserve worst-case headroom for the entire outer chain we are about > + * to push: IPv6 + UDP + GTP-U long header + PDU Session extension. > + * Subsequent skb_cow_head() calls inside seg6_mobile_push_gtpu() then > + * become no-ops. > + */ > + if (skb_cow_head(skb, > + sizeof(*new_ip6h) + sizeof(*uh) + > + sizeof(struct gtp1_header_long) + > + sizeof(struct seg6_mobile_pdu_session_ext))) Same ovhd scoping point as patch 2. > + goto drop; > + Same missing iptunnel_handle_offloads() as patch 2. > + if (seg6_mobile_push_gtpu(skb, cb.teid, cb.qfi, cb.pdu_type, > + cb.pdu_type_set)) > + goto drop; > + > + uh = skb_push(skb, sizeof(*uh)); > + skb_reset_transport_header(skb); > + uh->source = htons(GTP1U_PORT); > + uh->dest = htons(GTP1U_PORT); > + uh->len = htons(skb->len); > + Same fixed source port question as patch 2. > + new_ip6h = skb_push(skb, sizeof(*new_ip6h)); > + skb_reset_network_header(skb); > + memset(new_ip6h, 0, sizeof(*new_ip6h)); > + ip6_flow_hdr(new_ip6h, cb.tclass, cb.flowlabel); > + new_ip6h->payload_len = htons(skb->len - sizeof(*new_ip6h)); > + new_ip6h->nexthdr = IPPROTO_UDP; > + new_ip6h->hop_limit = cb.hop_limit; > + new_ip6h->saddr = minfo->src_addr; > + new_ip6h->daddr = cb.next_sid; > + > + /* RFC 8200 requires UDP/IPv6 checksums. Initialise the > + * pseudo-header sum and let the stack/NIC complete it via > + * CHECKSUM_PARTIAL so we do not pay a per-packet linear sum and > + * we cooperate with offload. > + */ > + skb->ip_summed = CHECKSUM_PARTIAL; > + skb->csum_start = (unsigned char *)uh - skb->head; > + skb->csum_offset = offsetof(struct udphdr, check); > + uh->check = ~csum_ipv6_magic(&new_ip6h->saddr, &new_ip6h->daddr, > + skb->len - sizeof(*new_ip6h), > + IPPROTO_UDP, 0); > + udp6_set_csum() already handles the CHECKSUM_PARTIAL + pseudo-header seed setup and also covers the GSO case. Using it would avoid open-coding this sequence. > + skb->protocol = htons(ETH_P_IPV6); > + nf_reset_ct(skb); > + skb_dst_drop(skb); > + > + seg6_lookup_any_nexthop(skb, &cb.next_sid, 0, false, slwt->oif); > + return dst_input(skb); > + > +drop: > + kfree_skb_reason(skb, reason); > + return -EINVAL; > +} seg6_lookup_any_nexthop() already calls skb_dst_drop() internally. The explicit call above is redundant. > + [snip] > +static int input_action_end_m_gtp6_e(struct sk_buff *skb, > + struct seg6_local_lwt *slwt) > +{ > + enum skb_drop_reason reason = SKB_DROP_REASON_SEG6_MOBILE_BAD_SID; > + const struct seg6_mobile_info *minfo = &slwt->mobile_info; > + struct seg6_mobile_gtp6_e_cb *cb; > + struct in6_addr next_sid; > + struct ipv6_sr_hdr *srh; > + u8 hop_limit, tclass, qfi; > + unsigned int outer_len; > + struct ipv6hdr *ip6h; > + int inner_nfproto; > + __be32 flowlabel; > + __be16 frag_off; > + u64 args_mob; > + u32 teid; > + int off; > + u8 nh; > + Same reverse Christmas tree issue as patch 2. > + [snip] > + /* RFC 6040 outer-to-outer propagation: copy DSCP+ECN (tclass) and > + * the flow label from the SRv6 outer to the new IPv6 outer. Use > + * ip6_flowlabel() (not ip6_flowinfo()) so the tclass byte is > + * supplied exactly once via the @tclass argument of ip6_flow_hdr(). > + */ > + flowlabel = ip6_flowlabel(ip6h); > + tclass = ipv6_get_dsfield(ip6h); > + hop_limit = ip6h->hop_limit; > + Same RFC 6040 question as patch 2 (here also flow label). > + /* RFC 9433 Section 6.5 upper-layer S02 mandates "Pop the IPv6 > + * header and all its extension headers". ipv6_skip_exthdr() > + * walks every extension header (HBH/Routing/Dest-Opts/Fragment) > + * so HBH-before-SRH and DOpts-after-SRH are handled too. The > + * terminal next-header value also selects NFPROTO_IPV4 / > + * NFPROTO_IPV6 for the NF_INET_PRE_ROUTING hook below. > + */ > + nh = ip6h->nexthdr; > + off = ipv6_skip_exthdr(skb, sizeof(*ip6h), &nh, &frag_off); > + if (off < 0) { > + reason = SKB_DROP_REASON_SEG6_MOBILE_BAD_INNER; > + goto drop; > + } > + outer_len = off; > + Same BAD_INNER misuse as patch 2. Same frag_off check missing after ipv6_skip_exthdr() as patch 2. > + [snip] > + /* For inner IP traffic that may traverse NF_INET_PRE_ROUTING below, > + * pull the full inner IP header into the linear area so a netfilter > + * hook reading skb_transport_header() does not access stale data. > + * Non-IP inner is forwarded as-is via the GTP-U T-PDU payload. > + */ > + if (!pskb_may_pull(skb, outer_len + ((inner_nfproto == NFPROTO_IPV4) ? > + sizeof(struct iphdr) : > + (inner_nfproto == NFPROTO_IPV6) ? > + sizeof(struct ipv6hdr) : 0))) { > + reason = SKB_DROP_REASON_SEG6_MOBILE_BAD_INNER; > + goto drop; > + } > + Same repeated ternary as patch 2. > + [snip] > static struct seg6_action_desc seg6_action_table[] = { > { > @@ -2153,6 +2431,17 @@ static struct seg6_action_desc seg6_action_table[] = { > .build_state = seg6_mobile_v4_validate, > }, > }, > + { > + .action = SEG6_LOCAL_ACTION_END_M_GTP6_E, > + .attrs = SEG6_F_ATTR(SEG6_LOCAL_MOBILE_SRC_ADDR), > + .optattrs = SEG6_F_LOCAL_COUNTERS | > + SEG6_F_ATTR(SEG6_LOCAL_MOBILE_PDU_TYPE) | > + SEG6_F_ATTR(SEG6_LOCAL_OIF), > + .input = input_action_end_m_gtp6_e, > + .slwt_ops = { > + .build_state = seg6_mobile_gtp6_e_validate, > + }, > + }, > + [snip] > +/* End.M.GTP6.E SID layout (RFC 9433 Section 6.5): > + * > + * | locator (route prefix) | Args.Mob.Session (40) | pad | > + * > + * The locator length is the route's IPv6 destination prefix length. > + * Reject route additions whose prefix leaves no room for the 40-bit > + * Args.Mob.Session field at setup time so the operator gets a clear > + * error from `ip route add` instead of silent per-packet drops. > + */ > +static int seg6_mobile_gtp6_e_validate(struct seg6_local_lwt *slwt, > + const void *cfg, > + struct netlink_ext_ack *extack) > +{ > + const struct fib6_config *fib6_cfg = cfg; > + > + if ((unsigned int)fib6_cfg->fc_dst_len + SEG6_MOBILE_ARGS_MOB_LEN > > 128) { Nit: fc_dst_len is int in struct fib6_config (IPv6 prefix length, range 0..128); the (unsigned int) cast is not needed. > + [snip] Thanks, Ciao, Andrea P.S. I am temporarily writing from another address due to a mail delivery issue at my @uniroma2.it address. Please always Cc my default [email protected] address on replies.

