[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <1714feebff7b8d17c3b355430e046344a81399cb.1766349632.git.marcdevel@gmail.com>
Date: Sun, 21 Dec 2025 22:19:35 +0100
From: Marc Suñé <marcdevel@...il.com>
To: kuba@...nel.org,
willemdebruijn.kernel@...il.com,
pabeni@...hat.com
Cc: netdev@...r.kernel.org,
dborkman@...nel.org,
Marc Suñé <marcdevel@...il.com>
Subject: [PATCH RFC net 2/5] selftests/net: add no ARP bcast/null poison test
Add a selftest to test that ARP bcast/null poisioning checks are
never bypassed.
Signed-off-by: Marc Suñé <marcdevel@...il.com>
---
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 2 +
.../selftests/net/arp_no_bcastnull_poision.sh | 159 ++++++++++++++++++
tools/testing/selftests/net/arp_send.c | 138 +++++++++++++++
4 files changed, 300 insertions(+)
create mode 100755 tools/testing/selftests/net/arp_no_bcastnull_poision.sh
create mode 100644 tools/testing/selftests/net/arp_send.c
diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index 6930fe926c58..fd08ceeab07c 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
+arp_send
bind_bhash
bind_timewait
bind_wildcard
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index b66ba04f19d9..8308f0067547 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -9,6 +9,7 @@ CFLAGS += -I../
TEST_PROGS := \
altnames.sh \
amt.sh \
+ arp_no_bcastnull_poision.sh \
arp_ndisc_evict_nocarrier.sh \
arp_ndisc_untracked_subnets.sh \
bareudp.sh \
@@ -163,6 +164,7 @@ TEST_GEN_FILES := \
# end of TEST_GEN_FILES
TEST_GEN_PROGS := \
+ arp_send \
bind_timewait \
bind_wildcard \
epoll_busy_poll \
diff --git a/tools/testing/selftests/net/arp_no_bcastnull_poision.sh b/tools/testing/selftests/net/arp_no_bcastnull_poision.sh
new file mode 100755
index 000000000000..d0b9241599f1
--- /dev/null
+++ b/tools/testing/selftests/net/arp_no_bcastnull_poision.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Tests that ARP announcements with Broadcast or NULL mac are never
+# accepted
+#
+
+source lib.sh
+
+readonly V4_ADDR0="10.0.10.1"
+readonly V4_ADDR1="10.0.10.2"
+readonly BCAST_MAC="ff:ff:ff:ff:ff:ff"
+readonly NULL_MAC="00:00:00:00:00:00"
+readonly VALID_MAC="02:01:02:03:04:05"
+readonly ARP_REQ=1
+readonly ARP_REPLY=2
+nsid=100
+ret=0
+veth0_ifindex=0
+veth1_mac=
+
+setup() {
+ setup_ns PEER_NS
+
+ ip link add name veth0 type veth peer name veth1
+ ip link set dev veth0 up
+ ip link set dev veth1 netns ${PEER_NS}
+ ip netns exec ${PEER_NS} ip link set dev veth1 up
+ ip addr add ${V4_ADDR0}/24 dev veth0
+ ip netns exec ${PEER_NS} ip addr add ${V4_ADDR1}/24 dev veth1
+ ip netns exec ${PEER_NS} ip route add default via ${V4_ADDR0} dev veth1
+
+ # Raise ARP timers to avoid flakes due to refreshes
+ sysctl -w net.ipv4.neigh.veth0.base_reachable_time=3600 \
+ >/dev/null 2>&1
+ ip netns exec ${PEER_NS} \
+ sysctl -w net.ipv4.neigh.veth1.gc_stale_time=3600 \
+ >/dev/null 2>&1
+ ip netns exec ${PEER_NS} \
+ sysctl -w net.ipv4.neigh.veth1.base_reachable_time=3600 \
+ >/dev/null 2>&1
+
+ veth0_ifindex=$(ip -j link show veth0 | jq -r '.[0].ifindex')
+ veth1_mac="$(ip netns exec ${PEER_NS} ip -j link show veth1 | \
+ jq -r '.[0].address' )"
+}
+
+cleanup() {
+ ip neigh flush dev veth0
+ ip link del veth0
+ cleanup_ns ${PEER_NS}
+}
+
+# Make sure ARP announcement with invalid MAC is never learnt
+run_no_arp_poisoning() {
+ local l2_dmac=${1}
+ local tmac=${2}
+ local op=${3}
+
+ ret=0
+
+ ip netns exec ${PEER_NS} ip neigh flush dev veth1 >/dev/null 2>&1
+ ip netns exec ${PEER_NS} ping -c 1 ${V4_ADDR0} >/dev/null 2>&1
+
+ # Poison with a valid MAC to ensure injection is working
+ ./arp_send ${veth0_ifindex} ${BCAST_MAC} ${VALID_MAC} ${op} \
+ ${V4_ADDR0} ${VALID_MAC} ${V4_ADDR0} ${VALID_MAC}
+
+ neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0} | \
+ grep ${VALID_MAC})
+ if [ "${neigh}" == "" ]; then
+ echo "ERROR: unable to ARP poision with a valid MAC ${VALID_MAC}"
+ ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0}
+ ret=1
+ return
+ fi
+
+ # Poison with tmac
+ ./arp_send ${veth0_ifindex} ${l2_dmac} ${VALID_MAC} ${op} \
+ ${V4_ADDR0} ${tmac} ${V4_ADDR0} ${tmac}
+
+ neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0} | \
+ grep ${tmac})
+ if [ "${neigh}" != "" ]; then
+ echo "ERROR: ARP entry learnt for ${tmac} announcement."
+ ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0}
+ ret=1
+ return
+ fi
+}
+
+print_test_result() {
+ local msg=${1}
+ local rc=${2}
+
+ if [ ${rc} == 0 ]; then
+ printf "TEST: %-60s [ OK ]" "${msg}"
+ else
+ printf "TEST: %-60s [ FAIL ]" "${msg}"
+ fi
+}
+
+run_all_tests() {
+ local results
+
+ setup
+
+ ## ARP
+ # Broadcast gARPs
+ msg="1.1 ARP no poisoning dmac=bcast reply sha=bcast"
+ run_no_arp_poisoning ${BCAST_MAC} ${BCAST_MAC} ${ARP_REPLY}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.2 ARP no poisoning dmac=bcast reply sha=null"
+ run_no_arp_poisoning ${BCAST_MAC} ${NULL_MAC} ${ARP_REPLY}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.3 ARP no poisoning dmac=bcast req sha=bcast"
+ run_no_arp_poisoning ${BCAST_MAC} ${BCAST_MAC} ${ARP_REQ}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.4 ARP no poisoning dmac=bcast req sha=null"
+ run_no_arp_poisoning ${BCAST_MAC} ${NULL_MAC} ${ARP_REQ}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ # Targeted gARPs
+ msg="1.5 ARP no poisoning dmac=veth0 reply sha=bcast"
+ run_no_arp_poisoning ${veth1_mac} ${BCAST_MAC} ${ARP_REPLY}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.6 ARP no poisoning dmac=veth0 reply sha=null"
+ run_no_arp_poisoning ${veth1_mac} ${NULL_MAC} ${ARP_REPLY}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.7 ARP no poisoning dmac=veth0 req sha=bcast"
+ run_no_arp_poisoning ${veth1_mac} ${BCAST_MAC} ${ARP_REQ}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ msg="1.8 ARP no poisoning dmac=veth0 req sha=null"
+ run_no_arp_poisoning ${veth1_mac} ${NULL_MAC} ${ARP_REQ}
+ results+="$(print_test_result "${msg}" ${ret})\n"
+
+ cleanup
+
+ printf '%b' "${results}"
+}
+
+if [ "$(id -u)" -ne 0 ];then
+ echo "SKIP: Need root privileges"
+ exit $ksft_skip;
+fi
+
+if [ ! -x "$(command -v ip)" ]; then
+ echo "SKIP: Could not run test without ip tool"
+ exit $ksft_skip
+fi
+
+run_all_tests
+exit $ret
diff --git a/tools/testing/selftests/net/arp_send.c b/tools/testing/selftests/net/arp_send.c
new file mode 100644
index 000000000000..463ee435c9c1
--- /dev/null
+++ b/tools/testing/selftests/net/arp_send.c
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <inttypes.h>
+#include <netinet/ether.h>
+#include <arpa/inet.h>
+
+#include <linux/if_packet.h>
+#include <linux/if_ether.h>
+
+#ifndef __packed
+#define __packed __attribute__((packed))
+#endif
+
+struct arp_pkt {
+ struct ethhdr eth;
+ struct {
+ struct arphdr hdr;
+
+ /* Variable part for Ethernet IP ARP */
+ unsigned char ar_sha[ETH_ALEN]; /* sender hardware address */
+ __be32 ar_sip; /* sender IP address */
+ unsigned char ar_tha[ETH_ALEN]; /* target hardware address */
+ __be32 ar_tip; /* target IP address */
+ } __packed arp;
+} __packed;
+
+int parse_opts(int argc, char **argv, int *ifindex, struct arp_pkt *pkt)
+{
+ int rc;
+ struct ether_addr *mac;
+ uint16_t op_code;
+
+ if (argc != 9) {
+ fprintf(stderr, "Usage: %s <iface> <mac_dst> <mac_src> <op_code> <target-ip> <target-hwaddr> <sender-ip> <sender-hwaddr>\n",
+ argv[0]);
+ return -1;
+ }
+
+ *ifindex = atoi(argv[1]);
+ mac = ether_aton(argv[2]);
+ if (!mac) {
+ fprintf(stderr, "Unable to parse mac_dst from '%s'\n", argv[2]);
+ return -1;
+ }
+
+ /* Ethernet */
+ memcpy(pkt->eth.h_dest, mac, ETH_ALEN);
+ mac = ether_aton(argv[3]);
+ if (!mac) {
+ fprintf(stderr, "Unable to parse mac_src from '%s'\n", argv[3]);
+ return -1;
+ }
+ memcpy(pkt->eth.h_source, mac, ETH_ALEN);
+ pkt->eth.h_proto = htons(ETH_P_ARP);
+
+ /* ARP */
+ op_code = atol(argv[4]);
+ if (op_code != ARPOP_REQUEST && op_code != ARPOP_REPLY) {
+ fprintf(stderr, "Invalid ARP op %s\n", argv[4]);
+ return -1;
+ }
+ pkt->arp.hdr.ar_op = htons(op_code);
+
+ pkt->arp.hdr.ar_hrd = htons(0x1); /* Ethernet */
+ pkt->arp.hdr.ar_pro = htons(ETH_P_IP);
+ pkt->arp.hdr.ar_hln = ETH_ALEN;
+ pkt->arp.hdr.ar_pln = 4;
+
+ rc = inet_pton(AF_INET, argv[5], &pkt->arp.ar_tip);
+ if (rc != 1) {
+ fprintf(stderr, "Invalid IPv4 address %s\n", argv[5]);
+ return -1;
+ }
+ rc = inet_pton(AF_INET, argv[7], &pkt->arp.ar_sip);
+ if (rc != 1) {
+ fprintf(stderr, "Invalid IPv4 address %s\n", argv[7]);
+ return -1;
+ }
+
+ mac = ether_aton(argv[6]);
+ if (!mac) {
+ fprintf(stderr, "Unable to parse target-hwaddr from '%s'\n",
+ argv[6]);
+ return -1;
+ }
+ memcpy(pkt->arp.ar_tha, mac, ETH_ALEN);
+ mac = ether_aton(argv[8]);
+ if (!mac) {
+ fprintf(stderr, "Unable to parse sender-hwaddr from '%s'\n",
+ argv[8]);
+ return -1;
+ }
+ memcpy(pkt->arp.ar_sha, mac, ETH_ALEN);
+
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ int rc, fd;
+ struct sockaddr_ll bind_addr = {0};
+ int ifindex;
+ struct arp_pkt pkt = {0};
+
+ if (parse_opts(argc, argv, &ifindex, &pkt) < 0)
+ return -1;
+
+ fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
+ if (fd < 0) {
+ fprintf(stderr, "Unable to open raw socket(%d). Need root privileges?\n",
+ fd);
+ return 1;
+ }
+
+ bind_addr.sll_family = AF_PACKET;
+ bind_addr.sll_protocol = htons(ETH_P_ALL);
+ bind_addr.sll_ifindex = ifindex;
+
+ rc = bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr));
+ if (rc < 0) {
+ fprintf(stderr, "Unable to bind raw socket(%d). Invalid iface '%d'?\n",
+ rc, ifindex);
+ return 1;
+ }
+
+ rc = send(fd, &pkt, sizeof(pkt), 0);
+ if (rc < 0) {
+ fprintf(stderr, "Unable to send packet: %d\n", rc);
+ return 1;
+ }
+
+ return 0;
+}
--
2.47.3
Powered by blists - more mailing lists