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.

Reply via email to