From nobody Fri Sep 25 04:08:48 2026 Received: from mail-pj2-f13.google.com (mail-pj2-f13.google.com [74.125.227.141]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6D959472096 for ; Wed, 16 Sep 2026 21:38:26 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.227.141 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789594723; cv=none; b=AKiw20JbvK+bmlbeSCxA2opotkTlZldme3M7AkcwUSgUkJagH0F6rgL+GCN5J8auBEw9d5juDqXNuKrDeapvRUKMCfKtjwk4Sjfj4oNKBAsmDteVVxZZRAJquoIJ2bH91np38a0HuBUweKzt1tUyupI7PGVHWDav6CHZ9zWKQAs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789594723; c=relaxed/simple; bh=N92N9Soqsgy3ljnBeZT7dPYB/IKeBHlbbZ9u5e24XtI=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=pEge11AB4aYZH5tm+riNt7OPwKtTI2i9xekxV7bQbgRej5602NfdKe5Gf5tP15IGlDfCtZ1d5FP1AvfBeZQ7YBD5Hy2Vye5LLt0QWZ+4RM6DeyAJ3mhJCxvQK2mQndm9sujUDk4zfVPXSiBxMsaso17ye7umqRsspgXQv1LwKFQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=rZPYkm5m; arc=none smtp.client-ip=74.125.227.141 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="rZPYkm5m" Received: by mail-pj2-f13.google.com with SMTP id 98e67ed59e1d1-39b350c69b4so123518a91.2 for ; Wed, 16 Sep 2026 14:38:26 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1789594702; x=1790199502; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=z6nLWrSoP2nchYc66LNz6+l7g8MGnIRoZZkPmGmB13k=; b=rZPYkm5m9x2ktHKty7NqVhrYq6fgvTcAQ3pELF15yfrJDHUmFJe2l8fOEkjcgBIZEF Y22/xmMHGQD1NK2vVgzeD7Olbayph8FFbZ/SIkX21Z3ewOf/XEEcLjQqo3VVLIqrrcUp rmfj8ZUR5eoPGamZOg5ZSpmXflIFXK4KnIEVU3/I2ifZzGId0pV7y+X8U8fs6sJAto/h YY3uNK59VDWmWsjokKCi3TFiaPqQf2YnrVg/aHrUu4Z6cWCK/karrsh7F+eaCq8SxLRI YYWx9ClmigWMDwWXJR17Haw7+JAAitSqMMlPBAfuwpuFKCdu2HEXkO52nq1KOG1ka1SX hZig== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789594702; x=1790199502; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=z6nLWrSoP2nchYc66LNz6+l7g8MGnIRoZZkPmGmB13k=; b=O73ZpA2hFMO0SNGXGcD659V4TNqPytoF4vPrn3/vOWwBnAPz/SUcgSn630FGZddhXr Bqacum79LJT2sJyllSVDjzTn1h1Mg0E5xpQ/36+nfXeSBfQSn9AcrpL7JPt2BpqWQAkP ys4+fdb4l6lyALVBof8IweUcUDgJ+Ziqj/joKycKojszvmDIGTBBL78vAvWeOy3UUm0C W1poFbS6HFbM2is8dMZBfypwVVMt+FAv7cmzDBpT1RYynCXfrT5wZHF+KqsOSccKlp8O 2CTRX/O5l0uTlG6Mz9KdGWmFb6RnCW1E8oo6mDEIbDsG0v/t0OGqVwOSUFSmM5miuTMm mFXg== X-Forwarded-Encrypted: i=1; AKwUvByIRn7mWevSjkzMM7WoEbg/owlOgu2Otb/uJBs2y0/NpukUKiyiT8lInIZICL7TvBMREeEJjfR32cHZEBw=@vger.kernel.org X-Gm-Message-State: AFuF++ksDJ5H6K+I/QEljtyzBqv7gWJfCe1xo+fxh6nAXuLWCogXINIa X9CqfzOxrbQAgoc/3ui0J5F2VGTvUNHS52YNd5UO5z+/KELyseOqhB0ZZJ81RRiu X-Gm-Gg: AYBFou3vITzt1gwh8q+aUdpIS+CKaowQMtd1P0Q9w/6E83GV9G3S5CCXf771Dx2lsdx xOE3/0TvzakxG2sTVMRT81bkDEY4dwQOATJCHtY3OBf5FdfPnafnzlh/22Upl3TyqJY2BjALPMi OK5C7kXSqBbKKb/ssLsuVyjbkR2LqvPNc0NsFl+Wgs/O6TwXr2DOND3bKbOxja0jQ7je7wUx6al PIZoQ6qv2Z6tNBJM21CfbfUYhoz9rVZuqCbaRsDYIaAHbDQniKMfvr+AkkyLDsQtMf5tUD8club SIWbCvhV5D7c4xoP/cqwON1jDGonRySE6rz3W0kx20huOhAeTApREjAdxHE1U7kvdh/zwwfpvat /ctVvnj6j2filfr0Vn3HW5iY/kcEdu55fzSo8UCIpcDDyJX8Rj8qz5AbQZB1zIzkm3FiOlgnK7K 4oaJbkMa1bCd7KwNr892qW9HCs8Ck+PmY+6pyjaUHjV5ezXPEyu7wrjDTctSNKGSab19pmZKb1J bexA3hXT70= X-Received: by 2002:a17:90a:e18e:b0:39e:ac:b776 with SMTP id 98e67ed59e1d1-39e1e5187b4mr8796340a91.25.1789594702392; Wed, 16 Sep 2026 14:38:22 -0700 (PDT) Received: from [163.43.103.131] ([163.43.103.131]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-39e35d9f81asm1379913a91.3.2026.09.16.14.38.17 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 16 Sep 2026 14:38:20 -0700 (PDT) From: Yuya Kusakabe Date: Thu, 17 Sep 2026 06:38:12 +0900 Subject: [PATCH RFC net-next v3 1/2] seg6: add support for the SRv6 End.MAP behavior Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260917-seg6-mobile-end-map-v3-1-9890a60ac6a7@gmail.com> References: <20260917-seg6-mobile-end-map-v3-0-9890a60ac6a7@gmail.com> In-Reply-To: <20260917-seg6-mobile-end-map-v3-0-9890a60ac6a7@gmail.com> To: Andrea Mayer , Andrea Mayer , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , David Ahern , Ido Schimmel , Shuah Khan Cc: Justin Iurman , Florian Westphal , Fernando Fernandez Mancera , linux-kernel@vger.kernel.org, netdev@vger.kernel.org, linux-kselftest@vger.kernel.org, Yuya Kusakabe X-Mailer: b4 0.15.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=26518; i=yuya.kusakabe@gmail.com; h=from:subject:message-id; bh=N92N9Soqsgy3ljnBeZT7dPYB/IKeBHlbbZ9u5e24XtI=; b=owEBbQKS/ZANAwAIASrX0XUqXRtNAcsmYgBqqwxFgUclwbWvzdbdlOF7JJLUZ8Wjq7FaQ6R46 7PvE/Mzv3CJAjMEAAEIAB0WIQTaB7usAfxNKMeqa6Yq19F1Kl0bTQUCaqsMRQAKCRAq19F1Kl0b TSF7EAC/WfOeJLeYetJQSGOAoM7fi5YwTlEwm7mKKkjU5drHsMQVK3nbMq/hKQJ7BRUcfk1+LW6 RVt78gqNMJyCWAaGJqbJLqR1xX6b5+s8JRqoJvLEfwl/U1SGRWiKD9AL7RlXSS4wBE/wRzvcEGk qe9vNAZjBfRrKdydCwfWDSWYM/DIfIJANtqBdpbQ+P2G7HCfO8/3rPL0G+nfaAvHbqyvQcBvO6U VwHo/mped/7edv+rua0twAzJHFU9xwWgeUmLMf/uGGPBug7tz/BTUu4/mIuBtulWNX3lPqY4IvU 5MYa8aUh+FqZ5wbYzdaaBXwtKZL82ie8NGYNFw+OQHFZj88Hflz4XVXba/VNb8W4a3OJpOp/Skv WvON2z7a9iKZcl2pzDkWMbZjixTXGGpBfLQYLunNTCx51IhhVloYgrjiHp5uAppg/2TzdqNXpVh tlz/gS21IAm9BT7iCSmlPLjXdgaAq+VGZJwjVGOM0xy1bCnbEmYOa16l5UMbjGWVeGfESBAZ/pF bAAnVl9bXN4XfSWPEXJT4j1CZzZp82PC5seTaHpXnPd11209OzNqk0LnjtEDOESs3WkENwQDp/q mM1kCPTX2Bb4UjGNXonVfCG1UZjYnBNJqP0zfCYlWuPiEqvtRQpnfFsF1iRUgjSITfG5v6MgdWn 00p2aPofb5DeJZw== X-Developer-Key: i=yuya.kusakabe@gmail.com; a=openpgp; fpr=DA07BBAC01FC4D28C7AA6BA62AD7D1752A5D1B4D SRv6 End.MAP is defined in RFC 9433 [1]. The SRv6 End.MAP is an SRv6 endpoint that replaces the IPv6 destination address with the configured mapped SID and forwards the packet via the IPv6 FIB without consuming the SRH. The SRv6 End.MAP Linux implementation is the first behavior of the SRv6 Mobile User Plane and introduces a dedicated LWTUNNEL_ENCAP_SEG6_MOBILE encap type, a CONFIG_IPV6_SEG6_MOBILE build option and a net/ipv6/seg6_mobile.c file that hosts the action dispatch table. The user-space ABI lives in include/uapi/linux/seg6_mobile.h under a SEG6_MOBILE_* namespace, kept separate from SEG6_LOCAL_* so that attributes whose semantics differ between behaviors do not overload the same UAPI table. The SRv6 End.MAP behavior can be instantiated using a command similar to the following: $ ip -6 route add 2001:db8:f::/64 encap seg6mobile action End.MAP \ mapped_sid 2001:db8:2::e dev eth0 We introduce the "seg6mobile" extension in iproute2 in a following patch. [1] https://www.rfc-editor.org/rfc/rfc9433.html Assisted-by: Claude:claude-fable-5 Signed-off-by: Yuya Kusakabe --- include/net/seg6.h | 8 + include/uapi/linux/lwtunnel.h | 1 + include/uapi/linux/seg6_mobile.h | 61 ++++ net/core/lwtunnel.c | 2 + net/ipv6/Kconfig | 10 + net/ipv6/Makefile | 1 + net/ipv6/seg6.c | 7 + net/ipv6/seg6_mobile.c | 701 +++++++++++++++++++++++++++++++++++= ++++ 8 files changed, 791 insertions(+) diff --git a/include/net/seg6.h b/include/net/seg6.h index 82b3fbbcbb93..789e9bcc4773 100644 --- a/include/net/seg6.h +++ b/include/net/seg6.h @@ -64,6 +64,14 @@ static inline int seg6_local_init(void) { return 0; } static inline void seg6_local_exit(void) {} #endif =20 +#ifdef CONFIG_IPV6_SEG6_MOBILE +extern int seg6_mobile_init(void); +extern void seg6_mobile_exit(void); +#else +static inline int seg6_mobile_init(void) { return 0; } +static inline void seg6_mobile_exit(void) {} +#endif + extern bool seg6_validate_srh(struct ipv6_sr_hdr *srh, int len, bool reduc= ed); extern struct ipv6_sr_hdr *seg6_get_srh(struct sk_buff *skb, int flags); extern void seg6_icmp_srh(struct sk_buff *skb, struct inet6_skb_parm *opt); diff --git a/include/uapi/linux/lwtunnel.h b/include/uapi/linux/lwtunnel.h index 229655ef792f..6e48f79c548e 100644 --- a/include/uapi/linux/lwtunnel.h +++ b/include/uapi/linux/lwtunnel.h @@ -16,6 +16,7 @@ enum lwtunnel_encap_types { LWTUNNEL_ENCAP_RPL, LWTUNNEL_ENCAP_IOAM6, LWTUNNEL_ENCAP_XFRM, + LWTUNNEL_ENCAP_SEG6_MOBILE, __LWTUNNEL_ENCAP_MAX, }; =20 diff --git a/include/uapi/linux/seg6_mobile.h b/include/uapi/linux/seg6_mob= ile.h new file mode 100644 index 000000000000..068ab91b2873 --- /dev/null +++ b/include/uapi/linux/seg6_mobile.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * SRv6 Mobile User Plane implementation + * + * Author: + * Yuya Kusakabe + */ +#ifndef _UAPI_LINUX_SEG6_MOBILE_H +#define _UAPI_LINUX_SEG6_MOBILE_H + +enum { + SEG6_MOBILE_UNSPEC, + SEG6_MOBILE_ACTION, + SEG6_MOBILE_MAPPED_SID, + SEG6_MOBILE_COUNTERS, + __SEG6_MOBILE_MAX, +}; + +#define SEG6_MOBILE_MAX (__SEG6_MOBILE_MAX - 1) + +enum { + SEG6_MOBILE_ACTION_UNSPEC =3D 0, + /* swap IPv6 DA with the mapped SID, leave SRH untouched */ + SEG6_MOBILE_ACTION_END_MAP =3D 1, + + __SEG6_MOBILE_ACTION_MAX, +}; + +#define SEG6_MOBILE_ACTION_MAX (__SEG6_MOBILE_ACTION_MAX - 1) + +/* SRv6 Mobile Behavior counters are encoded as netlink attributes + * guaranteeing the correct alignment. + * Each counter is identified by a different attribute type (i.e. + * SEG6_MOBILE_CNT_PACKETS). + * + * - SEG6_MOBILE_CNT_PACKETS: identifies a counter that counts the number + * of packets that have been CORRECTLY processed by an SRv6 Behavior + * instance (i.e., packets that generate errors or are dropped are NOT + * counted). + * + * - SEG6_MOBILE_CNT_BYTES: identifies a counter that counts the total + * amount of traffic in bytes of all packets that have been CORRECTLY + * processed by an SRv6 Behavior instance (i.e., packets that generate + * errors or are dropped are NOT counted). + * + * - SEG6_MOBILE_CNT_ERRORS: identifies a counter that counts the number + * of packets that have NOT been properly processed by an SRv6 Behavior + * instance (i.e., packets that generate errors or are dropped). + */ +enum { + SEG6_MOBILE_CNT_UNSPEC, + SEG6_MOBILE_CNT_PACKETS, + SEG6_MOBILE_CNT_BYTES, + SEG6_MOBILE_CNT_ERRORS, + SEG6_MOBILE_CNT_PAD, /* pad for 64 bits values */ + __SEG6_MOBILE_CNT_MAX, +}; + +#define SEG6_MOBILE_CNT_MAX (__SEG6_MOBILE_CNT_MAX - 1) + +#endif /* _UAPI_LINUX_SEG6_MOBILE_H */ diff --git a/net/core/lwtunnel.c b/net/core/lwtunnel.c index b01a395d9a96..4476293ccb37 100644 --- a/net/core/lwtunnel.c +++ b/net/core/lwtunnel.c @@ -53,6 +53,8 @@ static const char *lwtunnel_encap_str(enum lwtunnel_encap= _types encap_type) case LWTUNNEL_ENCAP_XFRM: /* module autoload not supported for encap type */ return NULL; + case LWTUNNEL_ENCAP_SEG6_MOBILE: + return "SEG6MOBILE"; case LWTUNNEL_ENCAP_IP6: case LWTUNNEL_ENCAP_IP: case LWTUNNEL_ENCAP_NONE: diff --git a/net/ipv6/Kconfig b/net/ipv6/Kconfig index c3806c6ac96f..e094b835a118 100644 --- a/net/ipv6/Kconfig +++ b/net/ipv6/Kconfig @@ -314,6 +314,16 @@ config IPV6_SEG6_BPF depends on IPV6_SEG6_LWTUNNEL depends on IPV6 =3D y =20 +config IPV6_SEG6_MOBILE + bool "IPv6: SRv6 Mobile User Plane (RFC 9433) behaviors" + depends on IPV6_SEG6_LWTUNNEL + help + Support for the SRv6 Mobile User Plane behaviors defined by + RFC 9433, exposed via the LWTUNNEL_ENCAP_SEG6_MOBILE lightweight + tunnel encapsulation. + + If unsure, say N. + config IPV6_RPL_LWTUNNEL bool "IPv6: RPL Source Routing Header support" depends on IPV6 diff --git a/net/ipv6/Makefile b/net/ipv6/Makefile index cf5e01f83ce3..95961f180439 100644 --- a/net/ipv6/Makefile +++ b/net/ipv6/Makefile @@ -24,6 +24,7 @@ ipv6-$(CONFIG_SYN_COOKIES) +=3D syncookies.o ipv6-$(CONFIG_NETLABEL) +=3D calipso.o ipv6-$(CONFIG_IPV6_SEG6_LWTUNNEL) +=3D seg6_iptunnel.o seg6_local.o ipv6-$(CONFIG_IPV6_SEG6_HMAC) +=3D seg6_hmac.o +ipv6-$(CONFIG_IPV6_SEG6_MOBILE) +=3D seg6_mobile.o ipv6-$(CONFIG_IPV6_RPL_LWTUNNEL) +=3D rpl_iptunnel.o ipv6-$(CONFIG_IPV6_IOAM6_LWTUNNEL) +=3D ioam6_iptunnel.o =20 diff --git a/net/ipv6/seg6.c b/net/ipv6/seg6.c index 62a7eb779202..14626b15abd5 100644 --- a/net/ipv6/seg6.c +++ b/net/ipv6/seg6.c @@ -525,10 +525,16 @@ int __init seg6_init(void) if (err) goto out_unregister_iptun; =20 + err =3D seg6_mobile_init(); + if (err) + goto out_unregister_local; + pr_info("Segment Routing with IPv6\n"); =20 out: return err; +out_unregister_local: + seg6_local_exit(); out_unregister_iptun: seg6_iptunnel_exit(); out_unregister_genl: @@ -540,6 +546,7 @@ int __init seg6_init(void) =20 void seg6_exit(void) { + seg6_mobile_exit(); seg6_local_exit(); seg6_iptunnel_exit(); genl_unregister_family(&seg6_genl_family); diff --git a/net/ipv6/seg6_mobile.c b/net/ipv6/seg6_mobile.c new file mode 100644 index 000000000000..f3757ab45848 --- /dev/null +++ b/net/ipv6/seg6_mobile.c @@ -0,0 +1,701 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SRv6 Mobile User Plane implementation + * + * Author: + * Yuya Kusakabe + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_IPV6_SEG6_HMAC +#include +#endif +#include + +#define SEG6_MOBILE_F_ATTR(i) BIT(i) +#define SEG6_F_MOBILE_COUNTERS SEG6_MOBILE_F_ATTR(SEG6_MOBILE_COUNTERS) + +struct seg6_mobile_lwt; + +struct seg6_mobile_action_desc { + int action; + unsigned long attrs; + unsigned long optattrs; + int (*input)(struct sk_buff *skb, struct seg6_mobile_lwt *slwt); +}; + +struct seg6_mobile_action_param { + int (*parse)(struct nlattr **attrs, struct seg6_mobile_lwt *slwt, + struct netlink_ext_ack *extack); + int (*put)(struct sk_buff *skb, struct seg6_mobile_lwt *slwt); + int (*cmp)(struct seg6_mobile_lwt *a, struct seg6_mobile_lwt *b); + + /* optional destroy() callback to release resources acquired in + * the corresponding parse() function. + */ + void (*destroy)(struct seg6_mobile_lwt *slwt); +}; + +struct pcpu_seg6_mobile_counters { + u64_stats_t packets; + u64_stats_t bytes; + u64_stats_t errors; + + struct u64_stats_sync syncp; +}; + +/* User-space aggregate format for the per-CPU counters. Kept private + * to the kernel; userspace receives the values through SEG6_MOBILE_CNT_* + * nested netlink attributes. + */ +struct seg6_mobile_counters { + __u64 packets; + __u64 bytes; + __u64 errors; +}; + +#define seg6_mobile_alloc_pcpu_counters(__gfp) \ + __netdev_alloc_pcpu_stats(struct pcpu_seg6_mobile_counters, \ + ((__gfp) | __GFP_ZERO)) + +struct seg6_mobile_lwt { + int action; + struct in6_addr mapped_sid; + const struct seg6_mobile_action_desc *desc; + struct pcpu_seg6_mobile_counters __percpu *pcpu_counters; + + /* required attrs are tracked by desc->attrs; optional attrs that + * the user actually configured are tracked here so that fill_encap + * / cmp / destroy can iterate only over what was parsed. + */ + unsigned long parsed_optattrs; +}; + +static struct seg6_mobile_lwt *seg6_mobile_lwtunnel(struct lwtunnel_state = *lwt) +{ + return (struct seg6_mobile_lwt *)lwt->data; +} + +/* ABSENT is told apart from MALFORMED so that a behavior which accepts + * SRH-less packets still drops a malformed SRH + */ +enum seg6_mobile_srh_state { + SEG6_MOBILE_SRH_ABSENT, + SEG6_MOBILE_SRH_PRESENT, + SEG6_MOBILE_SRH_MALFORMED, +}; + +static struct ipv6_sr_hdr * +seg6_mobile_get_and_validate_srh(struct sk_buff *skb, + enum seg6_mobile_srh_state *state) +{ + struct ipv6_sr_hdr *srh; + unsigned int srhoff =3D 0; + int hdr_proto; + int flags =3D 0; + + srh =3D seg6_get_srh(skb, 0); + if (srh) { +#ifdef CONFIG_IPV6_SEG6_HMAC + if (!seg6_hmac_validate_skb(skb)) { + *state =3D SEG6_MOBILE_SRH_MALFORMED; + return NULL; + } +#endif + *state =3D SEG6_MOBILE_SRH_PRESENT; + return srh; + } + + hdr_proto =3D ipv6_find_hdr(skb, &srhoff, IPPROTO_ROUTING, NULL, &flags); + *state =3D hdr_proto =3D=3D -ENOENT ? SEG6_MOBILE_SRH_ABSENT + : SEG6_MOBILE_SRH_MALFORMED; + return NULL; +} + +static int seg6_mobile_l4_csum_hlen(u8 nexthdr) +{ + switch (nexthdr) { + case IPPROTO_TCP: + return sizeof(struct tcphdr); + case IPPROTO_UDP: + return sizeof(struct udphdr); + case IPPROTO_ICMPV6: + return sizeof(struct icmp6hdr); + } + return 0; +} + +static __sum16 *seg6_mobile_l4_csum(struct sk_buff *skb, int l4_off, + u8 nexthdr) +{ + switch (nexthdr) { + case IPPROTO_TCP: + return &((struct tcphdr *)(skb->data + l4_off))->check; + case IPPROTO_UDP: { + struct udphdr *uh =3D (struct udphdr *)(skb->data + l4_off); + + /* zero UDPv6 checksum on a non-offloaded skb means "no checksum" */ + if (!uh->check && skb->ip_summed !=3D CHECKSUM_PARTIAL) + return NULL; + return &uh->check; + } + case IPPROTO_ICMPV6: + return &((struct icmp6hdr *)(skb->data + l4_off))->icmp6_cksum; + } + return NULL; +} + +/* rewrite DA; the L4 checksum follows only when the receiver will verify + * it against the new DA, i.e. no SRH is left for it to process + */ +static int seg6_mobile_advance_da(struct sk_buff *skb, + const struct in6_addr *nh, bool srh_present) +{ + int l4_off =3D 0, l4_hlen =3D 0; + struct in6_addr old_da; + struct ipv6hdr *ip6h; + __be16 frag_off; + u8 nexthdr =3D 0; + __sum16 *csum; + int write_len; + + if (!pskb_may_pull(skb, sizeof(*ip6h))) + return -EINVAL; + + ip6h =3D ipv6_hdr(skb); + write_len =3D sizeof(*ip6h); + + if (!srh_present) { + nexthdr =3D ip6h->nexthdr; + l4_off =3D ipv6_skip_exthdr(skb, sizeof(*ip6h), &nexthdr, + &frag_off); + if (l4_off < 0) + return -EINVAL; + + /* only the first fragment carries the L4 header; frag_off is the + * raw field, M flag included + */ + if (!(frag_off & htons(IP6_OFFSET))) + l4_hlen =3D seg6_mobile_l4_csum_hlen(nexthdr); + if (l4_hlen) + write_len =3D l4_off + l4_hlen; + } + + if (skb_ensure_writable(skb, write_len)) + return -ENOMEM; + + /* skb_ensure_writable() may change skb pointers; evaluate ip6h again */ + ip6h =3D ipv6_hdr(skb); + old_da =3D ip6h->daddr; + + csum =3D l4_hlen ? seg6_mobile_l4_csum(skb, l4_off, nexthdr) : NULL; + if (csum) { + inet_proto_csum_replace16(csum, skb, old_da.s6_addr32, + nh->s6_addr32, true); + if (nexthdr =3D=3D IPPROTO_UDP && !*csum) + *csum =3D CSUM_MANGLED_0; + } else if (skb->ip_summed =3D=3D CHECKSUM_COMPLETE) { + /* no L4 patch, so skb->csum must track the DA change itself */ + update_csum_diff16(skb, old_da.s6_addr32, (__be32 *)nh); + } + + ip6h->daddr =3D *nh; + skb_clear_hash(skb); + + return 0; +} + +static int seg6_mobile_forward(struct sk_buff *skb) +{ + seg6_lookup_nexthop(skb, NULL, 0); + return dst_input(skb); +} + +/* replace DA with the mapped SID and forward, leaving the SRH untouched */ +static int input_action_end_map(struct sk_buff *skb, + struct seg6_mobile_lwt *slwt) +{ + enum seg6_mobile_srh_state srh_state; + struct ipv6_sr_hdr *srh; + + srh =3D seg6_mobile_get_and_validate_srh(skb, &srh_state); + if (srh_state =3D=3D SEG6_MOBILE_SRH_MALFORMED) + goto drop; + + /* SL =3D=3D 0: SRH spent, L4 is verified against the new DA */ + if (seg6_mobile_advance_da(skb, &slwt->mapped_sid, + srh && srh->segments_left)) + goto drop; + + return seg6_mobile_forward(skb); + +drop: + kfree_skb(skb); + return -EINVAL; +} + +static int parse_nla_mapped_sid(struct nlattr **attrs, + struct seg6_mobile_lwt *slwt, + struct netlink_ext_ack *extack) +{ + memcpy(&slwt->mapped_sid, nla_data(attrs[SEG6_MOBILE_MAPPED_SID]), + sizeof(struct in6_addr)); + + return 0; +} + +static int put_nla_mapped_sid(struct sk_buff *skb, struct seg6_mobile_lwt = *slwt) +{ + if (nla_put_in6_addr(skb, SEG6_MOBILE_MAPPED_SID, &slwt->mapped_sid)) + return -EMSGSIZE; + + return 0; +} + +static int cmp_nla_mapped_sid(struct seg6_mobile_lwt *a, struct seg6_mobil= e_lwt *b) +{ + return memcmp(&a->mapped_sid, &b->mapped_sid, sizeof(struct in6_addr)); +} + +static const struct +nla_policy seg6_mobile_counters_policy[SEG6_MOBILE_CNT_MAX + 1] =3D { + [SEG6_MOBILE_CNT_PACKETS] =3D { .type =3D NLA_U64 }, + [SEG6_MOBILE_CNT_BYTES] =3D { .type =3D NLA_U64 }, + [SEG6_MOBILE_CNT_ERRORS] =3D { .type =3D NLA_U64 }, +}; + +static int parse_nla_counters(struct nlattr **attrs, + struct seg6_mobile_lwt *slwt, + struct netlink_ext_ack *extack) +{ + struct pcpu_seg6_mobile_counters __percpu *pcounters; + struct nlattr *tb[SEG6_MOBILE_CNT_MAX + 1]; + int ret; + + ret =3D nla_parse_nested(tb, SEG6_MOBILE_CNT_MAX, + attrs[SEG6_MOBILE_COUNTERS], + seg6_mobile_counters_policy, extack); + if (ret < 0) + return ret; + + /* basic support for SRv6 Behavior counters requires at least: + * packets, bytes and errors. + */ + if (!tb[SEG6_MOBILE_CNT_PACKETS] || !tb[SEG6_MOBILE_CNT_BYTES] || + !tb[SEG6_MOBILE_CNT_ERRORS]) + return -EINVAL; + + /* counters are always zero initialized */ + pcounters =3D seg6_mobile_alloc_pcpu_counters(GFP_KERNEL); + if (!pcounters) + return -ENOMEM; + + slwt->pcpu_counters =3D pcounters; + + return 0; +} + +static int seg6_mobile_fill_nla_counters(struct sk_buff *skb, + struct seg6_mobile_counters *counters) +{ + if (nla_put_u64_64bit(skb, SEG6_MOBILE_CNT_PACKETS, counters->packets, + SEG6_MOBILE_CNT_PAD)) + return -EMSGSIZE; + + if (nla_put_u64_64bit(skb, SEG6_MOBILE_CNT_BYTES, counters->bytes, + SEG6_MOBILE_CNT_PAD)) + return -EMSGSIZE; + + if (nla_put_u64_64bit(skb, SEG6_MOBILE_CNT_ERRORS, counters->errors, + SEG6_MOBILE_CNT_PAD)) + return -EMSGSIZE; + + return 0; +} + +static int put_nla_counters(struct sk_buff *skb, struct seg6_mobile_lwt *s= lwt) +{ + struct seg6_mobile_counters counters =3D { 0, 0, 0 }; + struct nlattr *nest; + int rc, i; + + nest =3D nla_nest_start(skb, SEG6_MOBILE_COUNTERS); + if (!nest) + return -EMSGSIZE; + + for_each_possible_cpu(i) { + struct pcpu_seg6_mobile_counters *pcounters; + u64 packets, bytes, errors; + unsigned int start; + + pcounters =3D per_cpu_ptr(slwt->pcpu_counters, i); + do { + start =3D u64_stats_fetch_begin(&pcounters->syncp); + + packets =3D u64_stats_read(&pcounters->packets); + bytes =3D u64_stats_read(&pcounters->bytes); + errors =3D u64_stats_read(&pcounters->errors); + + } while (u64_stats_fetch_retry(&pcounters->syncp, start)); + + counters.packets +=3D packets; + counters.bytes +=3D bytes; + counters.errors +=3D errors; + } + + rc =3D seg6_mobile_fill_nla_counters(skb, &counters); + if (rc < 0) { + nla_nest_cancel(skb, nest); + return rc; + } + + return nla_nest_end(skb, nest); +} + +static int cmp_nla_counters(struct seg6_mobile_lwt *a, + struct seg6_mobile_lwt *b) +{ + /* tunnels with counters enabled and disabled are different. */ + return (!!((unsigned long)a->pcpu_counters)) ^ + (!!((unsigned long)b->pcpu_counters)); +} + +static void destroy_attr_counters(struct seg6_mobile_lwt *slwt) +{ + free_percpu(slwt->pcpu_counters); +} + +static const struct seg6_mobile_action_desc seg6_mobile_action_table[] =3D= { + { + .action =3D SEG6_MOBILE_ACTION_END_MAP, + .attrs =3D SEG6_MOBILE_F_ATTR(SEG6_MOBILE_MAPPED_SID), + .optattrs =3D SEG6_F_MOBILE_COUNTERS, + .input =3D input_action_end_map, + }, +}; + +static const struct seg6_mobile_action_param +seg6_mobile_action_params[SEG6_MOBILE_MAX + 1] =3D { + [SEG6_MOBILE_MAPPED_SID] =3D { + .parse =3D parse_nla_mapped_sid, + .put =3D put_nla_mapped_sid, + .cmp =3D cmp_nla_mapped_sid, + }, + [SEG6_MOBILE_COUNTERS] =3D { + .parse =3D parse_nla_counters, + .put =3D put_nla_counters, + .cmp =3D cmp_nla_counters, + .destroy =3D destroy_attr_counters, + }, +}; + +static const struct nla_policy +seg6_mobile_policy[SEG6_MOBILE_MAX + 1] =3D { + [SEG6_MOBILE_ACTION] =3D { .type =3D NLA_U32 }, + [SEG6_MOBILE_MAPPED_SID] =3D NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)= ), + [SEG6_MOBILE_COUNTERS] =3D { .type =3D NLA_NESTED }, +}; + +static const struct seg6_mobile_action_desc * +seg6_mobile_get_action_desc(int action) +{ + int i; + + for (i =3D 0; i < ARRAY_SIZE(seg6_mobile_action_table); i++) { + if (seg6_mobile_action_table[i].action =3D=3D action) + return &seg6_mobile_action_table[i]; + } + + return NULL; +} + +/* call the destroy() callback (if available) for each set attribute in + * @parsed_attrs, starting from the first attribute up to the @max_parsed + * (excluded) attribute. + */ +static void __destroy_attrs(unsigned long parsed_attrs, int max_parsed, + struct seg6_mobile_lwt *slwt) +{ + const struct seg6_mobile_action_param *param; + int i; + + for (i =3D SEG6_MOBILE_ACTION + 1; i < max_parsed; i++) { + if (!(parsed_attrs & SEG6_MOBILE_F_ATTR(i))) + continue; + + param =3D &seg6_mobile_action_params[i]; + if (param->destroy) + param->destroy(slwt); + } +} + +static void destroy_attrs(struct seg6_mobile_lwt *slwt) +{ + unsigned long attrs =3D slwt->desc->attrs | slwt->parsed_optattrs; + + __destroy_attrs(attrs, SEG6_MOBILE_MAX + 1, slwt); +} + +static int seg6_mobile_parse_attrs(struct nlattr **attrs, + struct seg6_mobile_lwt *slwt, + struct netlink_ext_ack *extack) +{ + const struct seg6_mobile_action_param *param; + const struct seg6_mobile_action_desc *desc; + unsigned long parsed_optattrs =3D 0; + int i, err; + + desc =3D slwt->desc; + + if (WARN_ON_ONCE(desc->attrs & desc->optattrs)) + return -EINVAL; + + for (i =3D SEG6_MOBILE_ACTION + 1; i <=3D SEG6_MOBILE_MAX; i++) { + bool required =3D desc->attrs & SEG6_MOBILE_F_ATTR(i); + bool optional =3D desc->optattrs & SEG6_MOBILE_F_ATTR(i); + + if (!required && !optional) + continue; + + if (required && !attrs[i]) { + NL_SET_ERR_MSG_MOD(extack, + "missing required attribute"); + err =3D -EINVAL; + goto err; + } + + if (!attrs[i]) + continue; + + param =3D &seg6_mobile_action_params[i]; + err =3D param->parse(attrs, slwt, extack); + if (err < 0) + goto err; + + if (optional) + parsed_optattrs |=3D SEG6_MOBILE_F_ATTR(i); + } + + slwt->parsed_optattrs =3D parsed_optattrs; + + return 0; + +err: + __destroy_attrs(desc->attrs | parsed_optattrs, i, slwt); + return err; +} + +static bool seg6_mobile_counters_enabled(struct seg6_mobile_lwt *slwt) +{ + return slwt->parsed_optattrs & SEG6_F_MOBILE_COUNTERS; +} + +static void seg6_mobile_update_counters(struct seg6_mobile_lwt *slwt, + unsigned int len, int err) +{ + struct pcpu_seg6_mobile_counters *pcounters; + + pcounters =3D this_cpu_ptr(slwt->pcpu_counters); + u64_stats_update_begin(&pcounters->syncp); + + if (likely(!err)) { + u64_stats_inc(&pcounters->packets); + u64_stats_add(&pcounters->bytes, len); + } else { + u64_stats_inc(&pcounters->errors); + } + + u64_stats_update_end(&pcounters->syncp); +} + +static int seg6_mobile_input(struct sk_buff *skb) +{ + struct dst_entry *orig_dst =3D skb_dst(skb); + struct seg6_mobile_lwt *slwt; + unsigned int len =3D skb->len; + int rc; + + if (skb->protocol !=3D htons(ETH_P_IPV6)) { + kfree_skb(skb); + return -EINVAL; + } + + slwt =3D seg6_mobile_lwtunnel(orig_dst->lwtstate); + + rc =3D slwt->desc->input(skb, slwt); + + if (seg6_mobile_counters_enabled(slwt)) + seg6_mobile_update_counters(slwt, len, rc); + + return rc; +} + +static int seg6_mobile_build_state(struct net *net, struct nlattr *nla, + unsigned int family, const void *cfg, + struct lwtunnel_state **ts, + struct netlink_ext_ack *extack) +{ + const struct seg6_mobile_action_desc *desc; + struct nlattr *tb[SEG6_MOBILE_MAX + 1]; + struct lwtunnel_state *newts; + struct seg6_mobile_lwt *slwt; + int err; + + if (family !=3D AF_INET6) + return -EINVAL; + + err =3D nla_parse_nested(tb, SEG6_MOBILE_MAX, nla, + seg6_mobile_policy, extack); + if (err < 0) + return err; + + if (!tb[SEG6_MOBILE_ACTION]) { + NL_SET_ERR_MSG_MOD(extack, "missing SEG6_MOBILE_ACTION"); + return -EINVAL; + } + + desc =3D seg6_mobile_get_action_desc(nla_get_u32(tb[SEG6_MOBILE_ACTION])); + if (!desc) { + NL_SET_ERR_MSG_MOD(extack, "unknown SRv6 Mobile action"); + return -EOPNOTSUPP; + } + + newts =3D lwtunnel_state_alloc(sizeof(*slwt)); + if (!newts) + return -ENOMEM; + + slwt =3D seg6_mobile_lwtunnel(newts); + slwt->action =3D desc->action; + slwt->desc =3D desc; + + err =3D seg6_mobile_parse_attrs(tb, slwt, extack); + if (err < 0) { + kfree(newts); + return err; + } + + newts->type =3D LWTUNNEL_ENCAP_SEG6_MOBILE; + newts->flags =3D LWTUNNEL_STATE_INPUT_REDIRECT; + + *ts =3D newts; + + return 0; +} + +static void seg6_mobile_destroy_state(struct lwtunnel_state *lwt) +{ + destroy_attrs(seg6_mobile_lwtunnel(lwt)); +} + +static int seg6_mobile_fill_encap(struct sk_buff *skb, + struct lwtunnel_state *lwt) +{ + struct seg6_mobile_lwt *slwt =3D seg6_mobile_lwtunnel(lwt); + const struct seg6_mobile_action_param *param; + unsigned long attrs; + int i, err; + + if (nla_put_u32(skb, SEG6_MOBILE_ACTION, slwt->action)) + return -EMSGSIZE; + + attrs =3D slwt->desc->attrs | slwt->parsed_optattrs; + for (i =3D SEG6_MOBILE_ACTION + 1; i <=3D SEG6_MOBILE_MAX; i++) { + if (!(attrs & SEG6_MOBILE_F_ATTR(i))) + continue; + + param =3D &seg6_mobile_action_params[i]; + err =3D param->put(skb, slwt); + if (err < 0) + return err; + } + + return 0; +} + +static int seg6_mobile_get_encap_size(struct lwtunnel_state *lwt) +{ + struct seg6_mobile_lwt *slwt =3D seg6_mobile_lwtunnel(lwt); + unsigned long attrs; + int nlsize; + + nlsize =3D nla_total_size(sizeof(u32)); /* SEG6_MOBILE_ACTION */ + + attrs =3D slwt->desc->attrs | slwt->parsed_optattrs; + if (attrs & SEG6_MOBILE_F_ATTR(SEG6_MOBILE_MAPPED_SID)) + nlsize +=3D nla_total_size(sizeof(struct in6_addr)); + + if (attrs & SEG6_F_MOBILE_COUNTERS) + nlsize +=3D nla_total_size(0) + /* nest SEG6_MOBILE_COUNTERS */ + /* SEG6_MOBILE_CNT_PACKETS */ + nla_total_size_64bit(sizeof(__u64)) + + /* SEG6_MOBILE_CNT_BYTES */ + nla_total_size_64bit(sizeof(__u64)) + + /* SEG6_MOBILE_CNT_ERRORS */ + nla_total_size_64bit(sizeof(__u64)); + + return nlsize; +} + +static int seg6_mobile_cmp_encap(struct lwtunnel_state *a, + struct lwtunnel_state *b) +{ + struct seg6_mobile_lwt *slwt_a =3D seg6_mobile_lwtunnel(a); + struct seg6_mobile_lwt *slwt_b =3D seg6_mobile_lwtunnel(b); + const struct seg6_mobile_action_param *param; + unsigned long attrs_a, attrs_b; + int i; + + if (slwt_a->action !=3D slwt_b->action) + return 1; + + attrs_a =3D slwt_a->desc->attrs | slwt_a->parsed_optattrs; + attrs_b =3D slwt_b->desc->attrs | slwt_b->parsed_optattrs; + + if (attrs_a !=3D attrs_b) + return 1; + + for (i =3D SEG6_MOBILE_ACTION + 1; i <=3D SEG6_MOBILE_MAX; i++) { + if (!(attrs_a & SEG6_MOBILE_F_ATTR(i))) + continue; + + param =3D &seg6_mobile_action_params[i]; + if (param->cmp(slwt_a, slwt_b)) + return 1; + } + + return 0; +} + +static const struct lwtunnel_encap_ops seg6_mobile_ops =3D { + .build_state =3D seg6_mobile_build_state, + .destroy_state =3D seg6_mobile_destroy_state, + .input =3D seg6_mobile_input, + .fill_encap =3D seg6_mobile_fill_encap, + .get_encap_size =3D seg6_mobile_get_encap_size, + .cmp_encap =3D seg6_mobile_cmp_encap, + .owner =3D THIS_MODULE, +}; + +int __init seg6_mobile_init(void) +{ + BUILD_BUG_ON(SEG6_MOBILE_MAX + 1 > BITS_PER_TYPE(unsigned long)); + + return lwtunnel_encap_add_ops(&seg6_mobile_ops, + LWTUNNEL_ENCAP_SEG6_MOBILE); +} + +void seg6_mobile_exit(void) +{ + lwtunnel_encap_del_ops(&seg6_mobile_ops, LWTUNNEL_ENCAP_SEG6_MOBILE); +} --=20 2.50.1 From nobody Fri Sep 25 04:08:48 2026 Received: from mail-pj2-f43.google.com (mail-pj2-f43.google.com [74.125.227.171]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1C97D4A204F for ; Wed, 16 Sep 2026 21:38:29 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.227.171 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789594715; cv=none; b=Fyb9lysw63rBVvYNL0r2Oyw9iV49DHjBm1HWiqJkCCbIvX11fkjtMOlEZ+TIpp9hFm2TWXtU5isLPyZ0bXeWmdC5uCb9wlRjL4yzDlbL/qqNnfvn94n6zCMI6szumAdxexj4EMFsljj36OAodOHGvDPFoBCZsCSj7rQ3152plQE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789594715; c=relaxed/simple; bh=+YIq54X0UuVYVuR9Ls3BnI6IJMfiuc+TQ2tyluDUtzA=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=UFRxTvP/+1enGr/VtWQJo6U6R70dKI1U7d2KKqpjqogBOKP9MuJuuP51YatqyV61elkuBkstF71ytGdMitklclxIKh+NMf0C8vbFYLO/ND8QjHBeGK3ISFFN5JWDhD2w2JBTvUDMfB+fCeaT65W4niaowVNi11b29+nHgLweD1I= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=Z6OGHS9X; arc=none smtp.client-ip=74.125.227.171 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Z6OGHS9X" Received: by mail-pj2-f43.google.com with SMTP id 98e67ed59e1d1-396ccdaea76so53983a91.0 for ; Wed, 16 Sep 2026 14:38:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1789594706; x=1790199506; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=E4oSSlXn4ycnPRRmpRIG08U5s07ZsIAZ2+Nia6ab8/Q=; b=Z6OGHS9X4c51Sm+d17BnUREukKVAXvCB74TfhEsGkYWb6W5yomVEZ7ahkK+2dhds2C OOUn1+62a3AKeATEnVUAyuyaApranPFh3RoIM6jx04qKVlyax3WoQjKP4vGvkxy68Oy2 0yRbApQQQ9LLXgUsctc3eqrgecgSbWHapbUg+3AqS+ci/lVeqdwwc3GAkUS7ajeLKjHn Yp338+s+Ev0hOUts8LPpFmcQMNW+j1719tTiNToHOR/YH3OM6MJvFsCfdduN/tsG0YCM y64+ssw/CKRyU6OGUpxpGKFngxDOPH9EcWK67f66vyhrWivJVnBq+nPohA4OESEv/hKo RzoQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1789594706; x=1790199506; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=E4oSSlXn4ycnPRRmpRIG08U5s07ZsIAZ2+Nia6ab8/Q=; b=0mkv6VX/DNHB2eSs/XoXchfhOzBzUp4gX6Zz4KwmuDP7uta33T05Rbv049EyoWJzEK Ex5T8t+9qQMQQhO8MneiC3C//u9+tlNDTETqH8UBdRLMr234viowvzSi/0Y3Xv4pibH7 34O9VJVHsCvEJ0KnN7sySN94UZBaEH3qu19vfHClGOY4M8VDUgF8RH/1xTf/vBRDK9VP tyHx96T3rxkvBbvlzL8xKYhaE/s0CRqeh3YDSI1LnYN4JgX9pg/VzVRgWEwV8ENbgIaT CZWETwrzUpVXCgzh1ijcxf8AniWueBAppATaaP0sWM4Jhxcvp3vhnSfUhQ5Vm9JIU8JX cuLg== X-Forwarded-Encrypted: i=1; AKwUvBzQ2kAcYZl06m7iYMnfFtuwwr6Xf3RGBTwXhODI4SuuNs6aYrlvUFrugxCDdg6BmvHdKtsIIp1Ofnw+mcI=@vger.kernel.org X-Gm-Message-State: AFuF++nSWoLS8hc+XPCKoz30xydvjX0kkNM90QCDkOPeEmtKuRKw43C8 l6zQcBAzGRZii58M3eZyonF9x5oizYGvvveLe/xAzqogTAtNDPDMZKwl X-Gm-Gg: AYBFou3/yowi8KZdwaM0TEtZt4b6qUmd4h4Q1SQwXwzGevUv8N52tYhnNgrObwiw0xO Jozrb/FCpYxv3rJitmzmSFaIL7r1Q8TH75/YpcpNjDcLFPfjSLx8pUMc4YesBEmeH9s1fQVsdRD KC+ISsUOIVjS44lPLksquo3vSvplC4sSu5x9w88IBTkkE9v9vX/Gy5RBWq4ZLnEswPeVfQ6IIYx Jk8u2jEF8fsGYM1SuIMQIq/2hnuIWAqmwoThi8i7vXBiWCw9oGIvGORnG6onh41/zMUSXrc76YK lErIeC/OxfYCmWSM7MsFNKGoRxz33NnHFzcZ312CjblQvpJU2roQlvS1GLVlhGi9L6EeiGpJFcy DSeGJZBgNzkQEpgCY4sP2V7jUZRTaIFKGRcypk6M3HDoft3lms9/rP6wY+WhSfcc9ojckImADzV A9svwKOn+sGcNv1Z2zVSzXZu3lN3WoKAnh8i3hnbxDtx/wJxqNMh24aLjO1bPPnFK4VzPH2ITtW VG5ih+eng== X-Received: by 2002:a17:90b:2b90:b0:39e:1bfa:c5f0 with SMTP id 98e67ed59e1d1-39e35e304f9mr1384454a91.5.1789594706012; Wed, 16 Sep 2026 14:38:26 -0700 (PDT) Received: from [163.43.103.131] ([163.43.103.131]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-39e35d9f81asm1379913a91.3.2026.09.16.14.38.22 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 16 Sep 2026 14:38:25 -0700 (PDT) From: Yuya Kusakabe Date: Thu, 17 Sep 2026 06:38:13 +0900 Subject: [PATCH RFC net-next v3 2/2] selftests: seg6: add selftest for End.MAP behavior Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260917-seg6-mobile-end-map-v3-2-9890a60ac6a7@gmail.com> References: <20260917-seg6-mobile-end-map-v3-0-9890a60ac6a7@gmail.com> In-Reply-To: <20260917-seg6-mobile-end-map-v3-0-9890a60ac6a7@gmail.com> To: Andrea Mayer , Andrea Mayer , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , David Ahern , Ido Schimmel , Shuah Khan Cc: Justin Iurman , Florian Westphal , Fernando Fernandez Mancera , linux-kernel@vger.kernel.org, netdev@vger.kernel.org, linux-kselftest@vger.kernel.org, Yuya Kusakabe X-Mailer: b4 0.15.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=22175; i=yuya.kusakabe@gmail.com; h=from:subject:message-id; bh=+YIq54X0UuVYVuR9Ls3BnI6IJMfiuc+TQ2tyluDUtzA=; b=owEBbQKS/ZANAwAIASrX0XUqXRtNAcsmYgBqqwxFHOctRgDIEwMrSQkhIxx1CYshB9Y7ZRC8+ n7G01zugjSJAjMEAAEIAB0WIQTaB7usAfxNKMeqa6Yq19F1Kl0bTQUCaqsMRQAKCRAq19F1Kl0b TaCvEACbzjo0WGwX7FN+l+e8b4JYVAjSVMwR/CgJQEHkACNUiWfzC2aNf1zPiqoPXo7SuHGVBE7 LVIHqstedSZxuAG1mFwL5meOs6rRQrE22XZ3MMZrzrTaSSFXwNWsPiypT7wUsiJdtDVdI7CRLfq IVQhW9dlxVhbubFCbZ4SRL4204uJbf3Izv8ZCOy5jE3iOm/DZt4bgUPUefaes88zHt0G7wkxJzh Zx5sdJttZEdq36uWmhN6f6KB6QFtNmvDbXo7M3t7XdorEWDVVAwkS952dds8dQXriZZUOL6nnfI /YNrLvjnEWQUQSrWEokMyCCK/HCi3Az0se+VUYhJDJXUcsvbnGZ3miIibXnnSyHzJyscXsITXPp nhBtDYcrcYqk/vxqgLOwFY1L6WJA2I+1e/60HofIcW8RpmLOoj2tOD7JmdcKYM5jDYmamunJpRS tf7YGBIh5lVPXOWokwI36LzgwtNzdDmd8w8c79gy5j8j8bzdbQokM+ReGKep5QeJhUpxyZFAcXg V09jc33PjoS3Myeku7UT7rJsKRNtSVOosQvYF37qbiyE1MMdRFg5oo4AQ6wUO1M1P1YWDsyET48 6+r3x5NdW6+w+psXvp+V9O+KDLKga+o57Y/dsnJgAJV+7xseGHj/Rs48P484WKEw1z9wmWtPnul icZAYkCsl/3oe2A== X-Developer-Key: i=yuya.kusakabe@gmail.com; a=openpgp; fpr=DA07BBAC01FC4D28C7AA6BA62AD7D1752A5D1B4D This selftest is designed for testing the SRv6 End.MAP behavior. It instantiates three network namespaces interconnected through veth pairs, with the middle one running End.MAP; the topology and the cases are described in the script header. The correct execution of the behavior is verified through ICMPv6 echo reachability with and without an SRH, asserting that the receiver counts no transport checksum errors, and through the route's own error counter for the packets End.MAP must drop. A small C helper, srv6_mobile_send, crafts the routing headers the standard tools cannot produce. Assisted-by: Claude:claude-fable-5 Signed-off-by: Yuya Kusakabe --- tools/testing/selftests/net/.gitignore | 1 + tools/testing/selftests/net/Makefile | 2 + tools/testing/selftests/net/config | 1 + tools/testing/selftests/net/srv6_end_map_test.sh | 400 +++++++++++++++++++= ++++ tools/testing/selftests/net/srv6_mobile_send.c | 262 +++++++++++++++ 5 files changed, 666 insertions(+) diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftes= ts/net/.gitignore index c9f46031ac73..9afff0d83dde 100644 --- a/tools/testing/selftests/net/.gitignore +++ b/tools/testing/selftests/net/.gitignore @@ -42,6 +42,7 @@ socket so_incoming_cpu so_netns_cookie so_rcv_listener +srv6_mobile_send stress_reuseport_listen tap tcp_fastopen_backup_key diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests= /net/Makefile index 3ee3378f8b26..105e0f507791 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -96,6 +96,7 @@ TEST_PROGS :=3D \ srv6_end_dx4_netfilter_test.sh \ srv6_end_dx6_netfilter_test.sh \ srv6_end_flavors_test.sh \ + srv6_end_map_test.sh \ srv6_end_next_csid_l3vpn_test.sh \ srv6_end_x_next_csid_l3vpn_test.sh \ srv6_hencap_red_l3vpn_test.sh \ @@ -165,6 +166,7 @@ TEST_GEN_FILES :=3D \ so_netns_cookie \ so_rcv_listener \ socket \ + srv6_mobile_send \ stress_reuseport_listen \ tcp_fastopen_backup_key \ tcp_inq \ diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/n= et/config index 30d5fcb09a83..afe9a5163167 100644 --- a/tools/testing/selftests/net/config +++ b/tools/testing/selftests/net/config @@ -49,6 +49,7 @@ CONFIG_IPV6_ROUTE_INFO=3Dy CONFIG_IPV6_ROUTER_PREF=3Dy CONFIG_IPV6_RPL_LWTUNNEL=3Dy CONFIG_IPV6_SEG6_LWTUNNEL=3Dy +CONFIG_IPV6_SEG6_MOBILE=3Dy CONFIG_IPV6_SIT=3Dy CONFIG_IPV6_VTI=3Dy CONFIG_IPVLAN=3Dm diff --git a/tools/testing/selftests/net/srv6_end_map_test.sh b/tools/testi= ng/selftests/net/srv6_end_map_test.sh new file mode 100755 index 000000000000..a4efcb58a1e4 --- /dev/null +++ b/tools/testing/selftests/net/srv6_end_map_test.sh @@ -0,0 +1,400 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# author: Yuya Kusakabe + +# Selftest for the SRv6 End.MAP behavior (RFC 9433). +# +# +------+ 2001:db8:1::/64 +------+ 2001:db8:2::/64 +------+ +# | rt-1 | --------------------- | rt-2 | --------------------- | rt-3 | +# +------+ veth1 +------+ veth2 +------+ +# (End.MAP) +# +# rt-2 holds the End.MAP route for 2001:db8:f::/64 that replaces the +# IPv6 destination with 2001:db8:3::3 (an address on rt-3's loopback, +# also used as the final SRv6 segment in the H.Encaps scenario). +# +# The original destination 2001:db8:f::1 and the replacement +# 2001:db8:3::3 have different 16-bit word sums, so any regression in +# the transport-checksum diff update would corrupt the ICMPv6 +# checksum and bump Icmp6InCsumErrors -- the forwarding cases assert +# that the counter does not move. +# +# Eight cases are exercised: +# +# 1. SRH absent -- plain ICMPv6 echo to the End.MAP SID. +# 2. SRH present -- the destination is reached through an +# H.Encaps wrapper that carries an SRH with +# two segments; End.MAP must leave the SRH +# structurally intact. +# 3. SRH inline -- the destination is reached through an +# H.Insert wrapper that inserts an SRH whose +# first hop is the End.MAP SID; End.MAP must +# NOT patch the L4 checksum, because the +# receiver's SRv6 processing restores the +# destination from segments[0] before the +# ICMPv6 handler verifies it. +# 4. RH not an SRH -- a C helper sends a packet whose Routing +# Header is type 0 rather than 4; End.MAP must +# drop it. The behavior's own errors counter +# binds the assertion to the drop. +# 5. SRH malformed -- the helper sends an SRH whose Last Entry +# exceeds its length; End.MAP must drop it. +# 6. SRH exhausted -- the helper sends a valid SRH with Segments +# Left 0, so the IPv6 DA already is the final +# destination; End.MAP must patch the L4 +# checksum as in the SRH-absent case and the +# receiver must accept the echo. +# 7. Fragmented -- an oversized echo without an SRH is fragmented +# by rt-1; only the first fragment carries the +# ICMPv6 header, and End.MAP must patch its +# checksum so the reassembled echo verifies at +# rt-3. +# 8. Hop Limit -- an echo whose Hop Limit is 1 on arrival at +# the End.MAP node must yield an ICMPv6 Time +# Exceeded from that node, confirming Hop Limit +# handling is delegated to the ip6_forward path. + +source lib.sh + +readonly PING_TIMEOUT_SEC=3D4 +readonly COUNTER_TIMEOUT_SEC=3D2 +readonly END_MAP_PREFIX=3D"2001:db8:f::/64" +readonly END_MAP_SID=3D"2001:db8:f::1" +readonly RT3_SID=3D"2001:db8:3::3" +HELPER_DIR=3D$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +readonly HELPER_DIR +readonly HELPER=3D"${HELPER_DIR}/srv6_mobile_send" + +ret=3D0 +nsuccess=3D0 +nfail=3D0 + +PAUSE_ON_FAIL=3D${PAUSE_ON_FAIL:=3Dno} + +log_test() +{ + local rc=3D$1 + local expected=3D$2 + local msg=3D"$3" + + if [ "${rc}" -eq "${expected}" ]; then + nsuccess=3D$((nsuccess + 1)) + printf "\n TEST: %-60s [ OK ]\n" "${msg}" + else + ret=3D1 + nfail=3D$((nfail + 1)) + printf "\n TEST: %-60s [FAIL]\n" "${msg}" + if [ "${PAUSE_ON_FAIL}" =3D "yes" ]; then + echo + echo "hit enter to continue, 'q' to quit" + read -r a + [ "$a" =3D "q" ] && exit 1 + fi + fi +} + +print_log_test_results() +{ + printf "\nTests passed: %3d\n" "${nsuccess}" + printf "Tests failed: %3d\n" "${nfail}" +} + +cleanup() +{ + cleanup_all_ns +} + +trap cleanup EXIT + +check_dependencies() +{ + if [ "$(id -u)" -ne 0 ]; then + echo "SKIP: need root privileges" + exit "${ksft_skip}" + fi + + for cmd in ip ping nstat sysctl ethtool; do + if ! command -v "$cmd" >/dev/null; then + echo "SKIP: ${cmd} is required" + exit "${ksft_skip}" + fi + done + + if [ ! -x "${HELPER}" ]; then + echo "SKIP: ${HELPER} not built" + exit "${ksft_skip}" + fi + + if ! ip route help 2>&1 | grep -qF "seg6mobile"; then + echo "SKIP: iproute2 lacks seg6mobile support" + exit "${ksft_skip}" + fi + + if ! ip route help 2>&1 | grep -qF "End.MAP"; then + echo "SKIP: iproute2 lacks End.MAP action" + exit "${ksft_skip}" + fi +} + +setup() +{ + setup_ns rt1 rt2 rt3 + + # shellcheck disable=3DSC2154 # variables assigned by setup_ns + for ns in "$rt1" "$rt2" "$rt3"; do + ip -n "$ns" link set lo up + done + + ip link add veth1 netns "$rt1" \ + type veth peer name veth1-rt2 netns "$rt2" + ip link add veth2 netns "$rt2" \ + type veth peer name veth2-rt3 netns "$rt3" + + ip -n "$rt1" addr add 2001:db8:1::1/64 dev veth1 nodad + ip -n "$rt2" addr add 2001:db8:1::2/64 dev veth1-rt2 nodad + ip -n "$rt2" addr add 2001:db8:2::1/64 dev veth2 nodad + ip -n "$rt3" addr add 2001:db8:2::2/64 dev veth2-rt3 nodad + # rt-3 also owns the End.MAP replacement SID / SRH endpoint. + ip -n "$rt3" addr add "$RT3_SID/128" dev lo nodad + + ip -n "$rt1" link set veth1 up + ip -n "$rt2" link set veth1-rt2 up + ip -n "$rt2" link set veth2 up + ip -n "$rt3" link set veth2-rt3 up + + ip netns exec "$rt2" sysctl -wq net.ipv6.conf.all.forwarding=3D1 + + # rt-3 must accept SRv6 packets so ipv6_srh_rcv lets the + # extension header chain through to local delivery. + ip netns exec "$rt3" sysctl -wq net.ipv6.conf.all.seg6_enabled=3D1 + ip netns exec "$rt3" \ + sysctl -wq net.ipv6.conf.veth2-rt3.seg6_enabled=3D1 + ip netns exec "$rt3" sysctl -wq net.ipv6.conf.lo.seg6_enabled=3D1 + + # Disable HW checksum offload so the kernel software checksum + # path runs unconditionally and any csum bug surfaces. + ip netns exec "$rt1" ethtool -K veth1 tx off rx off + ip netns exec "$rt2" ethtool -K veth1-rt2 tx off rx off + ip netns exec "$rt2" ethtool -K veth2 tx off rx off + ip netns exec "$rt3" ethtool -K veth2-rt3 tx off rx off + + # rt-1: route the End.MAP locator into rt-2. + ip -n "$rt1" -6 route add "$END_MAP_PREFIX" via 2001:db8:1::2 + + # rt-1: a separate H.Encaps route for the SRH-present scenario, + # wrapping the inner ICMPv6 echo in an outer IPv6+SRH carrying + # [End.MAP_SID, RT3_SID]. + ip -n "$rt1" -6 route add "$RT3_SID/128" via 2001:db8:1::2 \ + encap seg6 mode encap \ + segs "$END_MAP_SID","$RT3_SID" \ + dev veth1 + + # rt-2: End.MAP -- swap DA from the End.MAP SID to RT3_SID + # (an address on rt-3) and forward via the IPv6 FIB. "count" + # enables the per-behavior counters the drop tests read. + ip -n "$rt2" -6 route add "$END_MAP_PREFIX" \ + encap seg6mobile action End.MAP mapped_sid "$RT3_SID" count \ + dev veth2 + + # rt-2: reach RT3_SID (on rt-3's loopback) through the + # directly connected neighbour 2001:db8:2::2. + ip -n "$rt2" -6 route add "$RT3_SID/128" via 2001:db8:2::2 + + # rt-3: return route for the ICMPv6 echo reply. + ip -n "$rt3" -6 route add 2001:db8:1::/64 via 2001:db8:2::1 +} + +read_nstat_counter() +{ + local ns=3D$1 + local name=3D$2 + + # nstat -az reports a counter that has never incremented as 0, + # which is what we rely on for a clean before/after delta. + ip netns exec "$ns" nstat -az "$name" \ + | awk -v n=3D"$name" '$1 =3D=3D n {print $2}' +} + +read_route_errors() +{ + # The End.MAP route carries "count", so its errors counter + # increments once for every packet the behavior drops. Reading it + # binds the negative test to the drop itself rather than to any + # unrelated loss on the path to rt-3. + ip -n "$rt2" -j -s -6 route show "$END_MAP_PREFIX" \ + | grep -oE '"errors":[0-9]+' | grep -oE '[0-9]+' +} + +test_srh_absent() +{ + local before after rc=3D0 + + before=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + + if ! ip netns exec "$rt1" \ + ping -6 -c 1 -W "$PING_TIMEOUT_SEC" "$END_MAP_SID" \ + >/dev/null 2>&1; then + rc=3D1 + fi + + if [ "$rc" -eq 0 ]; then + after=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + [ "$before" !=3D "$after" ] && rc=3D1 + fi + + log_test "$rc" 0 "End.MAP forwards an ICMPv6 echo without an SRH" +} + +test_srh_present() +{ + local before after rc=3D0 + + before=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + + if ! ip netns exec "$rt1" \ + ping -6 -c 1 -W "$PING_TIMEOUT_SEC" "$RT3_SID" \ + >/dev/null 2>&1; then + rc=3D1 + fi + + if [ "$rc" -eq 0 ]; then + after=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + [ "$before" !=3D "$after" ] && rc=3D1 + fi + + log_test "$rc" 0 "End.MAP preserves an SRH carried by H.Encaps" +} + +test_srh_inline() +{ + local before after rc=3D0 + + ip -n "$rt1" -6 route add 2001:db8:2::2/128 via 2001:db8:1::2 \ + encap seg6 mode inline segs "$END_MAP_SID" \ + dev veth1 + + before=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + + if ! ip netns exec "$rt1" \ + ping -6 -c 1 -W "$PING_TIMEOUT_SEC" 2001:db8:2::2 \ + >/dev/null 2>&1; then + rc=3D1 + fi + + if [ "$rc" -eq 0 ]; then + after=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + [ "$before" !=3D "$after" ] && rc=3D1 + fi + + log_test "$rc" 0 "End.MAP preserves L4 csum across mode inline SRH" +} + +# The drop happens on rt-2's receive path, asynchronously to the +# sender, so wait for the errors counter instead of sampling it. +expect_end_map_drop() +{ + local before rc=3D0 + + before=3D$(read_route_errors) + + ip netns exec "$rt1" "$HELPER" -m end-map "$@" \ + -s 2001:db8:1::1 -d "$END_MAP_SID" >/dev/null 2>&1 + + slowwait "$COUNTER_TIMEOUT_SEC" until_counter_is ">=3D $((before + 1))" \ + read_route_errors >/dev/null || rc=3D1 + [ "$(read_route_errors)" -eq "$((before + 1))" ] || rc=3D1 + + echo "$rc" +} + +test_rh_not_srh() +{ + local rc + + rc=3D$(expect_end_map_drop --rh-type 0) + log_test "$rc" 0 "End.MAP drops a routing header that is not an SRH" +} + +test_srh_malformed() +{ + local rc + + rc=3D$(expect_end_map_drop --bad-srh) + log_test "$rc" 0 "End.MAP drops a malformed SRH" +} + +test_srh_exhausted() +{ + local before_echos before_csum rc=3D0 + + before_echos=3D$(read_nstat_counter "$rt3" Icmp6InEchos) + before_csum=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + + ip netns exec "$rt1" "$HELPER" -m end-map \ + -s 2001:db8:1::1 -d "$END_MAP_SID" >/dev/null 2>&1 + + slowwait "$COUNTER_TIMEOUT_SEC" \ + until_counter_is ">=3D $((before_echos + 1))" \ + read_nstat_counter "$rt3" Icmp6InEchos >/dev/null || rc=3D1 + [ "$(read_nstat_counter "$rt3" Icmp6InCsumErrors)" -eq \ + "$before_csum" ] || rc=3D1 + + log_test "$rc" 0 "End.MAP patches the L4 csum of an exhausted SRH" +} + +test_fragmented() +{ + local before after rc=3D0 + + before=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + + if ! ip netns exec "$rt1" \ + ping -6 -c 1 -s 2000 -M dont -W "$PING_TIMEOUT_SEC" \ + "$END_MAP_SID" >/dev/null 2>&1; then + rc=3D1 + fi + + if [ "$rc" -eq 0 ]; then + after=3D$(read_nstat_counter "$rt3" Icmp6InCsumErrors) + [ "$before" !=3D "$after" ] && rc=3D1 + fi + + log_test "$rc" 0 "End.MAP patches the L4 csum of a fragmented echo" +} + +test_hoplimit_expiry() +{ + local before after rc=3D0 + + before=3D$(read_nstat_counter "$rt2" Icmp6OutTimeExcds) + + ip netns exec "$rt1" \ + ping -6 -c 1 -t 1 -W "$PING_TIMEOUT_SEC" "$END_MAP_SID" \ + >/dev/null 2>&1 + + after=3D$(read_nstat_counter "$rt2" Icmp6OutTimeExcds) + [ "$((after - before))" -eq 1 ] || rc=3D1 + + log_test "$rc" 0 "End.MAP delegates Hop Limit expiry to ip6_forward" +} + +main() +{ + check_dependencies + setup + + test_srh_absent + test_srh_present + test_srh_inline + test_rh_not_srh + test_srh_malformed + test_srh_exhausted + test_fragmented + test_hoplimit_expiry + + print_log_test_results + exit "${ret}" +} + +main "$@" diff --git a/tools/testing/selftests/net/srv6_mobile_send.c b/tools/testing= /selftests/net/srv6_mobile_send.c new file mode 100644 index 000000000000..01329ec61676 --- /dev/null +++ b/tools/testing/selftests/net/srv6_mobile_send.c @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Author: Yuya Kusakabe (yuya.kusakabe@gmail.com) + * + * Helper for SRv6 Mobile (RFC 9433) selftests. + * + * Usage: + * srv6_mobile_send -m end-map -s -d [--rh-type N] [--bad-sr= h] + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* RFC 8200 Routing header common fields are 4 bytes; an additional + * 4 bytes of type-specific data follow (the Reserved field for the + * deprecated type 0, or first_segment/flags/tag for SRH type 4). The + * segment list then runs in 16-byte units, giving a total of 24 bytes + * for one segment -- which is what ip6r_len =3D 2 advertises. + */ +struct srh_one_seg { + struct ip6_rthdr rthdr; + uint32_t type_data; + struct in6_addr segment; +}; + +/* Built as a struct so the headers are written through typed, aligned + * members rather than casts of a byte array; static_assert keeps it + * exactly the on-wire layout. + */ +struct end_map_frame { + struct ip6_hdr ip6; + struct srh_one_seg srh; + struct icmp6_hdr icmp6; +}; + +static_assert(sizeof(struct end_map_frame) =3D=3D + sizeof(struct ip6_hdr) + sizeof(struct srh_one_seg) + + sizeof(struct icmp6_hdr), + "end_map_frame must not contain padding"); + +enum mode { + MODE_NONE, + MODE_END_MAP, +}; + +struct cfg { + enum mode mode; + struct in6_addr src6; + struct in6_addr dst6; + uint8_t rh_type; + bool bad_srh; +}; + +static void usage(const char *bin) +{ + fprintf(stderr, + "Usage: %s -m -s -d [opts]\n" + "\n" + "Modes:\n" + " end-map Send IPv6 + SRH + ICMPv6 echo for End.MAP testing\n" + "\n" + "Mode end-map options:\n" + " --rh-type Routing Header type (default 4, the SRH)\n" + " --bad-srh emit an SRH whose Last Entry exceeds its length (= drop test)\n" + "\n" + "Exit: 0 sent, 1 failure, 3 invalid arguments.\n", + bin); +} + +static int parse_u32(const char *s, uint32_t *out) +{ + unsigned long v; + char *end; + + errno =3D 0; + v =3D strtoul(s, &end, 0); + if (errno || !*s || *end || v > 0xffffffffUL) + return -1; + *out =3D (uint32_t)v; + return 0; +} + +static int parse_u8(const char *s, uint8_t *out) +{ + uint32_t v; + + if (parse_u32(s, &v) || v > 0xff) + return -1; + *out =3D (uint8_t)v; + return 0; +} + +static enum mode parse_mode(const char *s) +{ + if (!strcmp(s, "end-map")) + return MODE_END_MAP; + return MODE_NONE; +} + +static int parse_args(int argc, char **argv, struct cfg *cfg) +{ + enum { OPT_RH_TYPE =3D 256, OPT_BAD_SRH }; + static const struct option longopts[] =3D { + { "rh-type", required_argument, NULL, OPT_RH_TYPE }, + { "bad-srh", no_argument, NULL, OPT_BAD_SRH }, + { NULL, 0, NULL, 0 }, + }; + int c; + + cfg->rh_type =3D 4; /* RFC 8754: the SRH is Routing Header type 4 */ + while ((c =3D getopt_long(argc, argv, "m:s:d:", longopts, NULL)) + !=3D -1) { + switch (c) { + case 'm': + cfg->mode =3D parse_mode(optarg); + break; + case 's': + if (inet_pton(AF_INET6, optarg, &cfg->src6) !=3D 1) + return -1; + break; + case 'd': + if (inet_pton(AF_INET6, optarg, &cfg->dst6) !=3D 1) + return -1; + break; + case OPT_RH_TYPE: + if (parse_u8(optarg, &cfg->rh_type)) + return -1; + break; + case OPT_BAD_SRH: + cfg->bad_srh =3D true; + break; + default: + return -1; + } + } + if (cfg->mode =3D=3D MODE_NONE) + return -1; + return 0; +} + +static uint16_t csum_fold(uint32_t sum) +{ + while (sum >> 16) + sum =3D (sum & 0xffff) + (sum >> 16); + return ~sum; +} + +static uint32_t csum_partial(const void *buf, size_t len, uint32_t sum) +{ + const uint8_t *p =3D buf; + uint16_t word; + + while (len > 1) { + memcpy(&word, p, sizeof(word)); + sum +=3D word; + p +=3D sizeof(word); + len -=3D sizeof(word); + } + if (len) + sum +=3D *p; + return sum; +} + +static uint16_t pseudo_csum(const struct in6_addr *src, + const struct in6_addr *dst, + uint32_t plen, uint8_t nexthdr, + const void *payload, size_t len) +{ + uint32_t nh =3D htonl(nexthdr); + uint32_t pl =3D htonl(plen); + uint32_t sum; + + sum =3D csum_partial(src, sizeof(*src), 0); + sum =3D csum_partial(dst, sizeof(*dst), sum); + sum =3D csum_partial(&pl, sizeof(pl), sum); + sum =3D csum_partial(&nh, sizeof(nh), sum); + sum =3D csum_partial(payload, len, sum); + return csum_fold(sum); +} + +static int send_end_map(const struct cfg *cfg) +{ + struct sockaddr_in6 dst_addr =3D { .sin6_family =3D AF_INET6 }; + struct end_map_frame frame =3D {}; + ssize_t res; + int fd; + + frame.ip6.ip6_flow =3D htonl(6u << 28); + frame.ip6.ip6_plen =3D htons(sizeof(frame.srh) + sizeof(frame.icmp6)); + frame.ip6.ip6_nxt =3D IPPROTO_ROUTING; + frame.ip6.ip6_hops =3D 64; + frame.ip6.ip6_src =3D cfg->src6; + frame.ip6.ip6_dst =3D cfg->dst6; + + frame.srh.rthdr.ip6r_nxt =3D IPPROTO_ICMPV6; + frame.srh.rthdr.ip6r_len =3D 2; /* (1 + ip6r_len) * 8 =3D 24 */ + frame.srh.rthdr.ip6r_type =3D cfg->rh_type; + frame.srh.rthdr.ip6r_segleft =3D 0; + /* SRH Last Entry is the high byte of the type-specific word. A + * single segment makes it 0; --bad-srh claims a second segment the + * header has no room for, which seg6_validate_srh() rejects. + */ + frame.srh.type_data =3D htonl((uint32_t)(cfg->bad_srh ? 1 : 0) << 24); + frame.srh.segment =3D frame.ip6.ip6_dst; + + frame.icmp6.icmp6_type =3D ICMP6_ECHO_REQUEST; + frame.icmp6.icmp6_code =3D 0; + frame.icmp6.icmp6_dataun.icmp6_un_data16[0] =3D htons(0x1234); + frame.icmp6.icmp6_dataun.icmp6_un_data16[1] =3D htons(1); + frame.icmp6.icmp6_cksum =3D pseudo_csum(&frame.ip6.ip6_src, + &frame.ip6.ip6_dst, + sizeof(frame.icmp6), + IPPROTO_ICMPV6, &frame.icmp6, + sizeof(frame.icmp6)); + + fd =3D socket(AF_INET6, SOCK_RAW, IPPROTO_RAW); + if (fd < 0) { + perror("socket"); + return 1; + } + dst_addr.sin6_addr =3D frame.ip6.ip6_dst; + + res =3D sendto(fd, &frame, sizeof(frame), 0, + (struct sockaddr *)&dst_addr, sizeof(dst_addr)); + close(fd); + if (res !=3D (ssize_t)sizeof(frame)) { + perror("sendto"); + return 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + struct cfg cfg =3D {}; + + if (parse_args(argc, argv, &cfg)) { + usage(argv[0]); + return 3; + } + + switch (cfg.mode) { + case MODE_END_MAP: + return send_end_map(&cfg); + default: + usage(argv[0]); + return 3; + } +} --=20 2.50.1