From: David Carlier <devnexen@gmail.com>
To: "David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>,
Herbert Xu <herbert@gondor.apana.org.au>
Cc: netdev@vger.kernel.org, stable@vger.kernel.org,
David Carlier <devnexen@gmail.com>
Subject: [PATCH v2 2/2] selftests: net: add act_nat ICMP inner checksum test
Date: Sat, 4 Apr 2026 13:03:10 +0100 [thread overview]
Message-ID: <20260404120310.88218-2-devnexen@gmail.com> (raw)
In-Reply-To: <20260404120310.88218-1-devnexen@gmail.com>
Verify that act_nat correctly updates the inner IP header
checksum when rewriting addresses inside ICMP error payloads.
The test sets up two namespaces with act_nat on a veth pair,
triggers an ICMP destination unreachable, and validates the
inner IP header checksum in the received ICMP error.
Signed-off-by: David Carlier <devnexen@gmail.com>
---
tools/testing/selftests/net/Makefile | 1 +
.../selftests/net/act_nat_icmp_csum.sh | 72 +++++++++
.../selftests/net/act_nat_icmp_csum_verify.py | 144 ++++++++++++++++++
3 files changed, 217 insertions(+)
create mode 100755 tools/testing/selftests/net/act_nat_icmp_csum.sh
create mode 100755 tools/testing/selftests/net/act_nat_icmp_csum_verify.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6bced3ed798b..1683db55e36f 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -7,6 +7,7 @@ CFLAGS += -I../../../../usr/include/ $(KHDR_INCLUDES)
CFLAGS += -I../
TEST_PROGS := \
+ act_nat_icmp_csum.sh \
altnames.sh \
amt.sh \
arp_ndisc_evict_nocarrier.sh \
diff --git a/tools/testing/selftests/net/act_nat_icmp_csum.sh b/tools/testing/selftests/net/act_nat_icmp_csum.sh
new file mode 100755
index 000000000000..2a0986bfe577
--- /dev/null
+++ b/tools/testing/selftests/net/act_nat_icmp_csum.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test that act_nat correctly updates the inner IP header checksum
+# when rewriting addresses inside ICMP error payloads.
+#
+# Setup:
+# +---------------------+ +---------------------+
+# | NS1 | | NS2 |
+# | | | |
+# | +-------+ | | +-------+ |
+# | | veth0 +---+----------------------+---+ veth0 | |
+# | +-------+ | | +-------+ |
+# | 10.0.1.1/24 | | 10.0.2.1/24 |
+# +---------------------+ +---------------------+
+#
+# On NS1's veth0:
+# egress act_nat: src 10.0.1.0/24 -> 10.0.2.0/24
+# ingress act_nat: dst 10.0.2.0/24 -> 10.0.1.0/24
+#
+# NS1 pings 10.0.2.99 (unreachable in NS2). NS2 sends back an ICMP
+# "destination host unreachable". The ICMP error contains a copy of the
+# original IP header whose source was NATted. On ingress, act_nat rewrites
+# the inner destination back. The inner IP header checksum must be updated.
+#
+# We use a raw ICMP socket in NS1 to receive the post-NAT ICMP error
+# and verify the inner IP header checksum is correct.
+
+source lib.sh
+
+cleanup()
+{
+ cleanup_ns $NS1 $NS2
+}
+
+trap cleanup EXIT
+
+# Check for required modules
+for mod in act_nat cls_u32 sch_ingress; do
+ modinfo $mod &>/dev/null || { echo "SKIP: Need $mod module"; exit $ksft_skip; }
+done
+
+setup_ns NS1 NS2
+
+ip -netns $NS1 link add veth0 type veth peer name veth0 netns $NS2
+ip -netns $NS1 link set dev veth0 up
+ip -netns $NS2 link set dev veth0 up
+
+ip -netns $NS1 addr add 10.0.1.1/24 dev veth0
+ip -netns $NS2 addr add 10.0.2.1/24 dev veth0
+
+ip netns exec $NS2 sysctl -qw net.ipv4.ip_forward=1
+ip netns exec $NS2 sysctl -qw net.ipv4.icmp_ratelimit=0
+
+ip -netns $NS1 route add 10.0.2.0/24 dev veth0
+
+# act_nat on NS1's veth0
+ip netns exec $NS1 tc qdisc add dev veth0 clsact
+
+# Egress: rewrite src 10.0.1.x -> 10.0.2.x
+ip netns exec $NS1 tc filter add dev veth0 egress protocol ip prio 1 \
+ u32 match ip src 10.0.1.0/24 \
+ action nat egress 10.0.1.0/24 10.0.2.0
+
+# Ingress: rewrite dst 10.0.2.x -> 10.0.1.x
+ip netns exec $NS1 tc filter add dev veth0 ingress protocol ip prio 1 \
+ u32 match ip dst 10.0.2.0/24 \
+ action nat ingress 10.0.2.0/24 10.0.1.0
+
+# Run the test: send ping and capture the ICMP error via raw socket
+ip netns exec $NS1 python3 "$(dirname "$0")/act_nat_icmp_csum_verify.py"
+exit $?
diff --git a/tools/testing/selftests/net/act_nat_icmp_csum_verify.py b/tools/testing/selftests/net/act_nat_icmp_csum_verify.py
new file mode 100755
index 000000000000..5d62831f6f49
--- /dev/null
+++ b/tools/testing/selftests/net/act_nat_icmp_csum_verify.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Verify that act_nat correctly updates the inner IP header checksum
+# in ICMP error packets. Sends a ping to an unreachable host and
+# captures the resulting ICMP error via a raw socket, then validates
+# the inner IP header checksum.
+#
+# This script is expected to run inside a network namespace that has
+# act_nat configured on its veth interface.
+
+import socket
+import struct
+import sys
+import os
+import signal
+
+
+def ip_checksum(header_bytes):
+ """Compute IP header checksum."""
+ if len(header_bytes) % 2:
+ header_bytes += b'\x00'
+ total = 0
+ for i in range(0, len(header_bytes), 2):
+ total += (header_bytes[i] << 8) + header_bytes[i + 1]
+ while total >> 16:
+ total = (total & 0xffff) + (total >> 16)
+ return (~total) & 0xffff
+
+
+def verify_inner_checksum(icmp_payload):
+ """Extract and verify the inner IP header checksum from ICMP error."""
+ # ICMP error payload starts with the original IP header
+ if len(icmp_payload) < 20:
+ return None, "inner IP header too short"
+
+ inner_ihl = (icmp_payload[0] & 0x0f) * 4
+ if len(icmp_payload) < inner_ihl:
+ return None, "inner IP header truncated"
+
+ inner_hdr = icmp_payload[:inner_ihl]
+
+ stored_csum = (inner_hdr[10] << 8) | inner_hdr[11]
+
+ # Zero out checksum field and recompute
+ hdr_for_csum = bytearray(inner_hdr)
+ hdr_for_csum[10] = 0
+ hdr_for_csum[11] = 0
+ computed_csum = ip_checksum(bytes(hdr_for_csum))
+
+ inner_src = socket.inet_ntoa(inner_hdr[12:16])
+ inner_dst = socket.inet_ntoa(inner_hdr[16:20])
+ info = f"inner src={inner_src} dst={inner_dst}"
+
+ if stored_csum == computed_csum:
+ return True, f"valid (0x{stored_csum:04x}) {info}"
+ else:
+ return False, (f"mismatch: stored=0x{stored_csum:04x} "
+ f"computed=0x{computed_csum:04x} {info}")
+
+
+def main():
+ # Open raw ICMP socket to receive ICMP errors
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
+ socket.IPPROTO_ICMP)
+ except PermissionError:
+ print("SKIP - need CAP_NET_RAW")
+ return 4
+
+ sock.settimeout(5)
+
+ # Send a ping to an unreachable address to trigger ICMP error
+ try:
+ ping_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
+ socket.IPPROTO_ICMP)
+ except (PermissionError, OSError):
+ # Fallback: use raw socket for ping
+ ping_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
+ socket.IPPROTO_ICMP)
+
+ # ICMP echo request: type=8, code=0, checksum, id, seq
+ icmp_id = os.getpid() & 0xffff
+ icmp_echo = struct.pack('!BBHHH', 8, 0, 0, icmp_id, 1)
+ # Compute ICMP checksum
+ csum = ip_checksum(icmp_echo)
+ icmp_echo = struct.pack('!BBHHH', 8, 0, csum, icmp_id, 1)
+
+ try:
+ ping_sock.sendto(icmp_echo, ('10.0.2.99', 0))
+ except OSError:
+ pass
+ ping_sock.close()
+
+ # Wait for ICMP error response
+ try:
+ data, addr = sock.recvfrom(4096)
+ except socket.timeout:
+ print("SKIP - no ICMP error received (timeout)")
+ sock.close()
+ return 4
+
+ sock.close()
+
+ # Parse outer IP header
+ if len(data) < 20:
+ print("SKIP - received packet too short")
+ return 4
+
+ outer_ihl = (data[0] & 0x0f) * 4
+
+ # ICMP header at offset outer_ihl
+ icmp_offset = outer_ihl
+ if len(data) < icmp_offset + 8:
+ print("SKIP - packet too short for ICMP header")
+ return 4
+
+ icmp_type = data[icmp_offset]
+ icmp_code = data[icmp_offset + 1]
+
+ # Expect ICMP dest unreachable (type 3) or similar error
+ if icmp_type not in (3, 4, 5, 11, 12):
+ print(f"SKIP - received ICMP type {icmp_type}, not an error")
+ return 4
+
+ # Inner IP header starts after ICMP header (8 bytes)
+ inner_offset = icmp_offset + 8
+ inner_payload = data[inner_offset:]
+
+ result, msg = verify_inner_checksum(inner_payload)
+
+ if result is None:
+ print(f"SKIP - {msg}")
+ return 4
+ elif result:
+ print(f"OK - inner IP header checksum {msg}")
+ return 0
+ else:
+ print(f"FAIL - inner IP header checksum {msg}")
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
--
2.53.0
next prev parent reply other threads:[~2026-04-04 12:03 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-04 12:03 [PATCH v2 1/2] net/sched: act_nat: fix inner IP header checksum in ICMP error packets David Carlier
2026-04-04 12:03 ` David Carlier [this message]
2026-04-04 18:01 ` [PATCH v2 2/2] selftests: net: add act_nat ICMP inner checksum test Jakub Kicinski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260404120310.88218-2-devnexen@gmail.com \
--to=devnexen@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=herbert@gondor.apana.org.au \
--cc=horms@kernel.org \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=stable@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.