Add tools/testing/selftests/net/srv6_end_map_test.sh covering the
End.MAP behavior over a three-node topology (the script header
documents the address plan).  Five cases are exercised: SRH-absent,
SRH-present (H.Encaps) and SRH-inline (H.Insert) forwarding verified
by ICMPv6 echo with Icmp6InCsumErrors asserted to stay zero, a
malformed routing header verified to be dropped via the route's own
errors counter, and a Hop Limit expiring at the End.MAP node answered
with an ICMPv6 Time Exceeded.  The original and replacement addresses
are chosen with different 16-bit word sums so a transport-checksum
regression cannot cancel out.  The malformed packet is crafted by a
small C helper (srv6_mobile_send) added to TEST_GEN_FILES; the test
SKIPs when iproute2 lacks the seg6mobile keyword.

Signed-off-by: Yuya Kusakabe <[email protected]>
---
 tools/testing/selftests/net/Makefile             |   2 +
 tools/testing/selftests/net/srv6_end_map_test.sh | 360 +++++++++++++++++++++++
 tools/testing/selftests/net/srv6_mobile_send.c   | 139 +++++++++
 3 files changed, 501 insertions(+)

diff --git a/tools/testing/selftests/net/Makefile 
b/tools/testing/selftests/net/Makefile
index 708d960ae07d..9acdd4187762 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -91,6 +91,7 @@ TEST_PROGS := \
        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 \
@@ -161,6 +162,7 @@ TEST_GEN_FILES := \
        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/srv6_end_map_test.sh 
b/tools/testing/selftests/net/srv6_end_map_test.sh
new file mode 100755
index 000000000000..2a45997b6728
--- /dev/null
+++ b/tools/testing/selftests/net/srv6_end_map_test.sh
@@ -0,0 +1,360 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# 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 SRH-present scenarios).
+#
+# 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 -- this test asserts that the
+# counter stays at zero across the run.
+#
+# Five 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. SRH malformed  -- a C helper sends a packet whose Routing
+#                        Header type is not 4; End.MAP must drop it.
+#                        The behavior's own errors counter binds the
+#                        assertion to the drop.
+#   5. 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=4
+readonly END_MAP_PREFIX="2001:db8:f::/64"
+readonly END_MAP_SID="2001:db8:f::1"
+readonly RT3_SID="2001:db8:3::3"
+HELPER_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
+readonly HELPER_DIR
+readonly HELPER="${HELPER_DIR}/srv6_mobile_send"
+
+ret=0
+nsuccess=0
+nfail=0
+
+PAUSE_ON_FAIL=${PAUSE_ON_FAIL:=no}
+
+log_test()
+{
+       local rc=$1
+       local expected=$2
+       local msg="$3"
+
+       if [ "${rc}" -eq "${expected}" ]; then
+               nsuccess=$((nsuccess + 1))
+               printf "\n    TEST: %-60s  [ OK ]\n" "${msg}"
+       else
+               ret=1
+               nfail=$((nfail + 1))
+               printf "\n    TEST: %-60s  [FAIL]\n" "${msg}"
+               if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+                       echo
+                       echo "hit enter to continue, 'q' to quit"
+                       read -r a
+                       [ "$a" = "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; 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
+
+       ip -n "$rt1" link set lo up
+       ip -n "$rt2" link set lo up
+       ip -n "$rt3" link set lo up
+
+       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=1
+
+       # 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=1
+       ip netns exec "$rt3" \
+               sysctl -wq net.ipv6.conf.veth2-rt3.seg6_enabled=1
+       ip netns exec "$rt3" sysctl -wq net.ipv6.conf.lo.seg6_enabled=1
+
+       # 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 2>/dev/null
+       ip netns exec "$rt2" ethtool -K veth1-rt2 tx off rx off \
+               2>/dev/null
+       ip netns exec "$rt2" ethtool -K veth2 tx off rx off 2>/dev/null
+       ip netns exec "$rt3" ethtool -K veth2-rt3 tx off rx off \
+               2>/dev/null
+
+       # 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 malformed-SRH test reads.
+       ip -n "$rt2" -6 route add "$END_MAP_PREFIX" \
+               encap seg6mobile action End.MAP nh6 "$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=$1
+       local name=$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="$name" '$1 == 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 1: SRH absent.
+test_srh_absent()
+{
+       local before after rc=0
+
+       before=$(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=1
+       fi
+
+       if [ "$rc" -eq 0 ]; then
+               after=$(read_nstat_counter "$rt3" Icmp6InCsumErrors)
+               [ "$before" != "$after" ] && rc=1
+       fi
+
+       log_test "$rc" 0 "End.MAP forwards an ICMPv6 echo without an SRH"
+}
+
+# Test 2: SRH present (H.Encaps from rt-1).
+#
+# The packet rt-1 emits carries an SRH whose only intermediate
+# segment is the End.MAP SID; the final segment is RT3_SID.  End.MAP
+# rewrites the outer destination from END_MAP_SID to RT3_SID
+# without touching the SRH, so on rt-3 the SRH processing advances
+# to segments[0] = RT3_SID (local) and the encapsulated inner
+# packet is decapsulated and replied to.  A regression that mangles
+# the SRH (e.g. flipping bits in segments_left) would break the chain
+# and the ping would not return.
+test_srh_present()
+{
+       local before after rc=0
+
+       before=$(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=1
+       fi
+
+       if [ "$rc" -eq 0 ]; then
+               after=$(read_nstat_counter "$rt3" Icmp6InCsumErrors)
+               [ "$before" != "$after" ] && rc=1
+       fi
+
+       log_test "$rc" 0 "End.MAP preserves an SRH carried by H.Encaps"
+}
+
+# Test 3: SRH inserted in-place by H.Insert (mode inline).
+#
+# rt-1 inserts an SRH between the IPv6 header and the L4 payload;
+# segments[0] is the original DA (2001:db8:2::2), segments[1] is the
+# End.MAP SID.  End.MAP rewrites the outer DA to RT3_SID but must
+# NOT patch the L4 checksum, because the kernel's standard SRv6
+# processing on rt-3 will decrement Segments Left to 0 and restore
+# the destination to segments[0] before the ICMPv6 handler verifies
+# the checksum against the original pseudo-header.  Over-patching the
+# checksum would corrupt it and bump Icmp6InCsumErrors.
+test_srh_inline()
+{
+       local before after rc=0
+
+       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=$(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=1
+       fi
+
+       if [ "$rc" -eq 0 ]; then
+               after=$(read_nstat_counter "$rt3" Icmp6InCsumErrors)
+               [ "$before" != "$after" ] && rc=1
+       fi
+
+       log_test "$rc" 0 "End.MAP preserves L4 csum across mode inline SRH"
+}
+
+# Test 4: SRH malformed (negative test).
+#
+# A C helper crafts an IPv6 packet whose Routing Header type is 0
+# rather than 4 (SRH), then sends it from rt-1 toward the End.MAP
+# SID.  End.MAP's seg6_mobile_get_and_validate_srh() must return
+# MALFORMED and the handler must drop the packet.  The assertion reads
+# the behavior's own errors counter, so it is satisfied only by the
+# End.MAP drop and not by any unrelated loss.
+test_srh_malformed()
+{
+       local before after rc=0
+
+       before=$(read_route_errors)
+
+       ip netns exec "$rt1" "$HELPER" 2001:db8:1::1 "$END_MAP_SID" \
+               >/dev/null 2>&1
+
+       after=$(read_route_errors)
+       [ "$((after - before))" -eq 1 ] || rc=1
+
+       log_test "$rc" 0 "End.MAP drops a packet carrying a malformed SRH"
+}
+
+# Test 5: Hop Limit expiry.
+#
+# End.MAP delegates the Hop Limit check and decrement to the ip6_forward
+# path.  An echo whose Hop Limit is 1 when it reaches rt-2 must elicit
+# an ICMPv6 Time Exceeded from rt-2 instead of being forwarded, so
+# rt-2's Icmp6OutTimeExcds increments by exactly one.
+test_hoplimit_expiry()
+{
+       local before after rc=0
+
+       before=$(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=$(read_nstat_counter "$rt2" Icmp6OutTimeExcds)
+       [ "$((after - before))" -eq 1 ] || rc=1
+
+       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_srh_malformed
+       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..37d75df3704b
--- /dev/null
+++ b/tools/testing/selftests/net/srv6_mobile_send.c
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Helper for SRv6 Mobile (RFC 9433) selftests.
+ *
+ * Usage: srv6_mobile_send <src-addr> <dst-addr>
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <netinet/ip6.h>
+#include <netinet/icmp6.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+/* 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 = 2 advertises.
+ */
+struct srh {
+       struct ip6_rthdr rthdr;
+       uint32_t type_data;
+       struct in6_addr segments[];
+};
+
+#define SRH_ONE_SEG_LEN (sizeof(struct srh) + sizeof(struct in6_addr))
+
+static uint16_t csum_fold(uint32_t sum)
+{
+       while (sum >> 16)
+               sum = (sum & 0xffff) + (sum >> 16);
+       return ~sum;
+}
+
+static uint32_t csum_partial(const void *buf, size_t len, uint32_t sum)
+{
+       const uint16_t *p = buf;
+
+       while (len > 1) {
+               sum += *p++;
+               len -= 2;
+       }
+       if (len)
+               sum += *(const uint8_t *)p;
+       return sum;
+}
+
+static uint16_t icmpv6_checksum(const struct in6_addr *src,
+                               const struct in6_addr *dst,
+                               const void *payload, size_t len)
+{
+       uint32_t nexthdr = htonl(IPPROTO_ICMPV6);
+       uint32_t plen = htonl(len);
+       uint32_t sum;
+
+       sum = csum_partial(src, sizeof(*src), 0);
+       sum = csum_partial(dst, sizeof(*dst), sum);
+       sum = csum_partial(&plen, sizeof(plen), sum);
+       sum = csum_partial(&nexthdr, sizeof(nexthdr), sum);
+       sum = csum_partial(payload, len, sum);
+       return csum_fold(sum);
+}
+
+int main(int argc, char **argv)
+{
+       uint8_t frame[sizeof(struct ip6_hdr) + SRH_ONE_SEG_LEN +
+                     sizeof(struct icmp6_hdr)];
+       struct sockaddr_in6 dst_addr = {};
+       struct icmp6_hdr *icmp6;
+       struct ip6_hdr *ip6;
+       struct srh *srh;
+       ssize_t res;
+       int fd;
+
+       if (argc != 3) {
+               fprintf(stderr, "usage: %s <src-addr> <dst-addr>\n", argv[0]);
+               return 1;
+       }
+
+       memset(frame, 0, sizeof(frame));
+       ip6 = (struct ip6_hdr *)frame;
+       srh = (struct srh *)(frame + sizeof(*ip6));
+       icmp6 = (struct icmp6_hdr *)(frame + sizeof(*ip6) + SRH_ONE_SEG_LEN);
+
+       ip6->ip6_flow = htonl(6u << 28);
+       ip6->ip6_plen = htons(SRH_ONE_SEG_LEN + sizeof(*icmp6));
+       ip6->ip6_nxt = IPPROTO_ROUTING;
+       ip6->ip6_hops = 64;
+       if (inet_pton(AF_INET6, argv[1], &ip6->ip6_src) != 1) {
+               fprintf(stderr, "invalid src %s\n", argv[1]);
+               return 1;
+       }
+       if (inet_pton(AF_INET6, argv[2], &ip6->ip6_dst) != 1) {
+               fprintf(stderr, "invalid dst %s\n", argv[2]);
+               return 1;
+       }
+
+       srh->rthdr.ip6r_nxt = IPPROTO_ICMPV6;
+       srh->rthdr.ip6r_len = 2;                /* (1 + ip6r_len) * 8 = 24 */
+       srh->rthdr.ip6r_type = 0;               /* RFC 8754: SRH is type 4 */
+       srh->rthdr.ip6r_segleft = 0;
+       srh->segments[0] = ip6->ip6_dst;
+
+       icmp6->icmp6_type = ICMP6_ECHO_REQUEST;
+       icmp6->icmp6_code = 0;
+       icmp6->icmp6_cksum = 0;
+       icmp6->icmp6_dataun.icmp6_un_data16[0] = htons(0x1234);
+       icmp6->icmp6_dataun.icmp6_un_data16[1] = htons(1);
+       icmp6->icmp6_cksum =
+               icmpv6_checksum(&ip6->ip6_src, &ip6->ip6_dst,
+                               icmp6, sizeof(*icmp6));
+
+       fd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);
+       if (fd < 0) {
+               perror("socket");
+               return 1;
+       }
+
+       dst_addr.sin6_family = AF_INET6;
+       dst_addr.sin6_addr = ip6->ip6_dst;
+
+       res = sendto(fd, frame, sizeof(frame), 0,
+                    (struct sockaddr *)&dst_addr, sizeof(dst_addr));
+       if (res != (ssize_t)sizeof(frame)) {
+               perror("sendto");
+               close(fd);
+               return 1;
+       }
+
+       close(fd);
+       return 0;
+}

-- 
2.50.1


Reply via email to