public inbox for netdev@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison)
@ 2026-01-26 23:53 Marc Suñé
  2026-01-26 23:53 ` [PATCH net v2 1/4] arp: discard invalid sha addr (b/mcast ARP poison) Marc Suñé
                   ` (4 more replies)
  0 siblings, 5 replies; 12+ messages in thread
From: Marc Suñé @ 2026-01-26 23:53 UTC (permalink / raw)
  To: kuba, willemdebruijn.kernel, pabeni
  Cc: netdev, dborkman, vadim.fedorenko, Marc Suñé

The current ARP and NDP implementations accept announcements with
multicast (broadcast incl.) and null MAC addresses as Sender HW Address
(SHA) in ARP or src/target lladdr in NDP, and updates the cache
for that neighbour.

Multicast (incl. broadcast) and null MAC addresses shall never be
associated with a unicast or a multicast IPv4/6 address (see RFC1812,
section 3.3.2).

ARP/NDP poisioning with a broadcast and certain multicast MAC addresses,
especially when poisoning a Gateway IP, have some undesired implications
compared to an ARP/NDP poisioning with a regular MAC (see commit message
in patch 1 for more information).

Worth mentioning that if an attacker is able to ARP/NDP poison in
a L2 segment, that in itself is probably a bigger security threat
(Man-in-middle etc., see Note2 in patch 1)

Since these MACs should never be announced, this patch series discards/drops
these packets, which prevents broadcast and multicast ARP/NDP poisoning
vectors.

This patchset only modifies the behaviour of the neighbouring subsystem
when processing network packets. Static entries can still be added with
mcast/bcast/null MACs.

v1: https://lore.kernel.org/netdev/cover.1766349632.git.marcdevel@gmail.com/

Changes since RFC v1
====================
  - Discard announcements with multicast MAC addresses
  - Check for dev->type == ARPHRD_ETHER instead of HW addrlen in ARP
  - Use !is_valid_ether_addr()
  - Added multicast test coverage and renamed tests accordingly
  - Dropped patch 5 (scapy utils)

Comments
========

As discussed in v1, certain Load Balancers make use of Multicast MAC addresses
for their VIPs:

https://support.huawei.com/enterprise/en/doc/EDOC1100213154/d8621162/dynamic-learning-of-arp-entries-with-multicast-mac-addresses
https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-network-to-support-nlb-operation-mode

But as stated in the Microsoft NLB documentation, it is expected that
static entries are created for it to work:

  To support this configuration, you must configure the network
  infrastructure to use static ARP entries and MAC address table
  entries. Network switches cannot learn the NLB multicast MAC address
  in the course of their usual operations. If you skip the manual
  configuration step, the network switches may flood NLB traffic to
  all ports or drop packets. The network may seem to function
  correctly at first, but problems increase over time.

Marc Suñé (4):
  arp: discard invalid sha addr (b/mcast ARP poison)
  selftests/net: add no ARP b/mcast,null poison test
  neigh: discard invalid lladdr (b/mcast poison)
  selftests/net: add no NDP b/mcast,null poison test

 net/ipv4/arp.c                                |   8 +
 net/ipv6/ndisc.c                              |  16 +
 tools/testing/selftests/net/.gitignore        |   2 +
 tools/testing/selftests/net/Makefile          |   3 +
 .../net/arp_ndisc_no_invalid_sha_poison.sh    | 368 ++++++++++++++++++
 tools/testing/selftests/net/arp_send.c        | 138 +++++++
 tools/testing/selftests/net/ndisc_send.c      | 198 ++++++++++
 7 files changed, 733 insertions(+)
 create mode 100755 tools/testing/selftests/net/arp_ndisc_no_invalid_sha_poison.sh
 create mode 100644 tools/testing/selftests/net/arp_send.c
 create mode 100644 tools/testing/selftests/net/ndisc_send.c

-- 
2.47.3


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH net v2 1/4] arp: discard invalid sha addr (b/mcast ARP poison)
  2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
@ 2026-01-26 23:53 ` Marc Suñé
  2026-01-26 23:53 ` [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test Marc Suñé
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 12+ messages in thread
From: Marc Suñé @ 2026-01-26 23:53 UTC (permalink / raw)
  To: kuba, willemdebruijn.kernel, pabeni
  Cc: netdev, dborkman, vadim.fedorenko, Marc Suñé

Prior to this commit, the ARP implementation accepted ARP req/replies
with multicast (including broadcast) and null MAC addresses as Sender
HW Address (SHA), and updated the ARP cache for that neighbour.
Broadcast, multicast and null MAC addresses shall never be associated
with a unicast or a multicast IPv4 address (see RFC1812, section 3.3.2).

ARP poisioning with a broadcast MAC address and certain multicast
addresses, especially when poisoning a Gateway IP, have some undesired
implications compared to an ARP poisioning with a regular MAC (see
Note1).

Worth mentioning that if an attacker is able to ARP poison in
a L2 segment, that in itself is probably a bigger security threat
(Man-in-middle etc., see Note2).

However, since these MACs should never be announced as SHA,
discard/drop ARPs with SHA={b/mcast, null}, which prevents the
broadcast/multicast ARP poisoning vector.

Note1:

After a successful broadcast/multicast ARP poisioning attack:

1. Unicast packets and refresh ("targeted") ARPs sent to or via
   the poisioned IP (e.g. the default GW) are flooded by
   bridges/switches. That is in absence of other security controls.

   Hardware swiches generally have rate-limits to prevent/mitigate
   broadcast storms, since ARPs are usually answered by the CPU.
   Legit unicast packets could be dropped (perf. degradation).

   Most modern NICs implement some form of L2 MAC filtering to early
   discard irrelevant packets. In contrast to an ARP poisoning
   attack with any other MAC, both unicast and ARP ("targeted")
   refresh packets are passed up to the Kernel networking stack
   (for all hosts in the L2 segment).

2. A single forged ARP packet (e.g. for the Gateway IP) can produce
   up to N "targeted" (to broadcast) ARPs, where N is the number of
   hosts in the L2 segment that have an ARP entry for that IP
   (e.g. GW), and some more traffic, since the real host will answer
   to targeted refresh ARPs with their (real) reply.

   This is a relatively low amount of traffic compared to 1).

3. An attacker could use this form of ARP poisoning to discover
   all hosts in a L2 segment in a very short period of time with
   one or few packets.

   By poisoning e.g. the default GW (likely multiple times, to
   avoid races with real gARPs from the GW), all hosts will eventually
   issue refresh "targeted" ARPs for the GW IP with the broadcast MAC
   address as destination. These packets will be flooded in the L2
   segment, revealing the presence of hosts to the attacker.

   For comparison:
     * Passive ARP monitoring: also stealthy, but can take a long
       time or not be possible at all in switches, as most refresh
       ARPs are targeted.
     * ARP req flooding: requires swiping the entire subnet. Noisy
       and easy to detect.
     * ICMP/L4 port scans: similar to the above.

4. In the unlikely case that hosts were to run with
   `/proc/sys/net/ipv4/conf/*/arp_accept=1` (unsafe, and disabled
   by default), poisoning with the broadcast MAC could be used to
   create significantly more broadcast traffic (low-volume
   amplification attack).

   An attacker could send M fake gARP with a number of IP addresses,
   where M is `/proc/sys/net/ipv4/neigh/*/gc_thresh3` (1024 by
   default). This would result in M x R ARPs, where R is the number
   of hosts in L2 segment with `arp_accept=1`, and likely other
   (real) ARP replies coming from the attacked host. This starts to
   get really relevant when R > 512, which is possible in large LANs
   but not very common.

Note2:

However, broadcast ARP poisoning might be subtle and difficult to
spot. These ARP packets appear on the surface as regular broadcast
ARP requests (unless ARP hdr is inspected), traffic continues to
flow uninterrupted (unless broadcast rate-limit in switches kick-in)
and, the next refresh ARP reply (from the GW) or any (valid) gARP
from the GW, will restore the original MAC in the ARP table, making
the traffic flow normally again.

Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
 net/ipv4/arp.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/net/ipv4/arp.c b/net/ipv4/arp.c
index c8c3e1713c0e..7dbb3fd5cc8a 100644
--- a/net/ipv4/arp.c
+++ b/net/ipv4/arp.c
@@ -800,6 +800,14 @@ static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb)
 		goto out_free_skb;
 
 /*
+ *	For Ethernet devices, Multicast/Broadcast and zero MAC addresses should
+ *	never be announced and accepted as sender HW address (RFC1812, 3.3.2).
+ *	Prevents Broadcast/Mcast ARP poisoning attack.
+ */
+	if (dev->type == ARPHRD_ETHER && !is_valid_ether_addr(sha))
+		goto out_free_skb;
+
+ /*
  *     Special case: We must set Frame Relay source Q.922 address
  */
 	if (dev_type == ARPHRD_DLCI)
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test
  2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
  2026-01-26 23:53 ` [PATCH net v2 1/4] arp: discard invalid sha addr (b/mcast ARP poison) Marc Suñé
@ 2026-01-26 23:53 ` Marc Suñé
  2026-01-29  4:27   ` Jakub Kicinski
  2026-01-26 23:53 ` [PATCH net v2 3/4] neigh: discard invalid lladdr (b/mcast poison) Marc Suñé
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 12+ messages in thread
From: Marc Suñé @ 2026-01-26 23:53 UTC (permalink / raw)
  To: kuba, willemdebruijn.kernel, pabeni
  Cc: netdev, dborkman, vadim.fedorenko, Marc Suñé

Add a selftest to test that ARP bcast/mcast/null poisioning checks
are never bypassed.

Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
 tools/testing/selftests/net/.gitignore        |   1 +
 tools/testing/selftests/net/Makefile          |   2 +
 .../net/arp_no_invalid_sha_poision.sh         | 176 ++++++++++++++++++
 tools/testing/selftests/net/arp_send.c        | 138 ++++++++++++++
 4 files changed, 317 insertions(+)
 create mode 100755 tools/testing/selftests/net/arp_no_invalid_sha_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 45c4ea381bc3..a765f1800752 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_invalid_sha_poision.sh \
 	arp_ndisc_evict_nocarrier.sh \
 	arp_ndisc_untracked_subnets.sh \
 	bareudp.sh \
@@ -164,6 +165,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_invalid_sha_poision.sh b/tools/testing/selftests/net/arp_no_invalid_sha_poision.sh
new file mode 100755
index 000000000000..1505f1bde487
--- /dev/null
+++ b/tools/testing/selftests/net/arp_no_invalid_sha_poision.sh
@@ -0,0 +1,176 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Tests that ARP announcements with Broadcast, Multicast 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 MCAST_MAC="01:00:5e:00:00:00"
+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} ${l2_dmac} ${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"
+
+	msg="1.5  ARP no poisoning dmac=bcast req   sha=mcast"
+	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REQ}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="1.6  ARP no poisoning dmac=bcast reply sha=mcast"
+	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REPLY}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Targeted gARPs
+	msg="1.7  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.8  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.9  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.10 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"
+
+	msg="1.11 ARP no poisoning dmac=veth0 req   sha=mcast"
+	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REQ}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="1.12 ARP no poisoning dmac=veth0 reply sha=mcast"
+	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REPLY}
+	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


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH net v2 3/4] neigh: discard invalid lladdr (b/mcast poison)
  2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
  2026-01-26 23:53 ` [PATCH net v2 1/4] arp: discard invalid sha addr (b/mcast ARP poison) Marc Suñé
  2026-01-26 23:53 ` [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test Marc Suñé
@ 2026-01-26 23:53 ` Marc Suñé
  2026-01-29  4:24   ` [net,v2,3/4] " Jakub Kicinski
  2026-01-26 23:53 ` [PATCH net v2 4/4] selftests/net: add no NDP b/mcast,null poison test Marc Suñé
  2026-01-29  4:24 ` [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Jakub Kicinski
  4 siblings, 1 reply; 12+ messages in thread
From: Marc Suñé @ 2026-01-26 23:53 UTC (permalink / raw)
  To: kuba, willemdebruijn.kernel, pabeni
  Cc: netdev, dborkman, vadim.fedorenko, Marc Suñé

Prior to this commit, the NDP implementation accepted NDP NS/NA with
the broadcast, multicast and null addresses as src/dst lladdr, and
updated the neighbour cache for that host.

Broadcast, multicast and null MAC addresses shall never be associated
with a unicast or a multicast IPv6 address (see RFC1812, section 3.3.2).

NDP poisioning with a broadcast MAC and certain multicast MAC addresses,
especially when poisoning a Gateway IP, have some undesired implications
compared to an NDP poisioning with a regular MAC (see ARP bcast poison
commit for more details).

Since these MACs should never be announced, discard/drop NDP with
lladdr={bcast, null}, which prevents the broadcast/multicast NDP
poisoning vector.

Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
 net/ipv6/ndisc.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index f6a5d8c73af9..34202a816a4f 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -830,6 +830,14 @@ static enum skb_drop_reason ndisc_recv_ns(struct sk_buff *skb)
 			return reason;
 		}
 
+		/*
+		 * Broadcast/Multicast and zero MAC addresses should
+		 * never be announced and accepted as llsrc address (prevent
+		 * NDP B/MCAST MAC poisoning attack).
+		 */
+		if (dev->type == ARPHRD_ETHER && !is_valid_ether_addr(lladdr))
+			return reason;
+
 		/* RFC2461 7.1.1:
 		 *	If the IP source address is the unspecified address,
 		 *	there MUST NOT be source link-layer address option
@@ -1033,6 +1041,14 @@ static enum skb_drop_reason ndisc_recv_na(struct sk_buff *skb)
 			net_dbg_ratelimited("NA: invalid link-layer address length\n");
 			return reason;
 		}
+
+		/*
+		 * Broadcast/Multicast and zero MAC addresses should
+		 * never be announced and accepted as llsrc address (prevent
+		 * NDP B/MCAST MAC poisoning attack).
+		 */
+		if (dev->type == ARPHRD_ETHER && !is_valid_ether_addr(lladdr))
+			return reason;
 	}
 	ifp = ipv6_get_ifaddr(dev_net(dev), &msg->target, dev, 1);
 	if (ifp) {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH net v2 4/4] selftests/net: add no NDP b/mcast,null poison test
  2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
                   ` (2 preceding siblings ...)
  2026-01-26 23:53 ` [PATCH net v2 3/4] neigh: discard invalid lladdr (b/mcast poison) Marc Suñé
@ 2026-01-26 23:53 ` Marc Suñé
  2026-01-29  4:24 ` [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Jakub Kicinski
  4 siblings, 0 replies; 12+ messages in thread
From: Marc Suñé @ 2026-01-26 23:53 UTC (permalink / raw)
  To: kuba, willemdebruijn.kernel, pabeni
  Cc: netdev, dborkman, vadim.fedorenko, Marc Suñé

Add a selftest to test that NDP bcast/mcast/null poisioning checks are
never bypassed.

Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
 tools/testing/selftests/net/.gitignore        |   1 +
 tools/testing/selftests/net/Makefile          |   3 +-
 .../net/arp_ndisc_no_invalid_sha_poison.sh    | 368 ++++++++++++++++++
 .../net/arp_no_invalid_sha_poision.sh         | 176 ---------
 tools/testing/selftests/net/ndisc_send.c      | 198 ++++++++++
 5 files changed, 569 insertions(+), 177 deletions(-)
 create mode 100755 tools/testing/selftests/net/arp_ndisc_no_invalid_sha_poison.sh
 delete mode 100755 tools/testing/selftests/net/arp_no_invalid_sha_poision.sh
 create mode 100644 tools/testing/selftests/net/ndisc_send.c

diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index fd08ceeab07c..5a82300a22a9 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -18,6 +18,7 @@ ipv6_flowlabel_mgr
 ipv6_fragmentation
 log.txt
 msg_zerocopy
+ndisc_send
 netlink-dumps
 nettest
 proc_net_pktgen
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index a765f1800752..afc24d419135 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -9,7 +9,7 @@ CFLAGS += -I../
 TEST_PROGS := \
 	altnames.sh \
 	amt.sh \
-	arp_no_invalid_sha_poision.sh \
+	arp_ndisc_no_invalid_sha_poison.sh \
 	arp_ndisc_evict_nocarrier.sh \
 	arp_ndisc_untracked_subnets.sh \
 	bareudp.sh \
@@ -170,6 +170,7 @@ TEST_GEN_PROGS := \
 	bind_wildcard \
 	epoll_busy_poll \
 	ipv6_fragmentation \
+	ndisc_send \
 	proc_net_pktgen \
 	reuseaddr_conflict \
 	reuseport_bpf \
diff --git a/tools/testing/selftests/net/arp_ndisc_no_invalid_sha_poison.sh b/tools/testing/selftests/net/arp_ndisc_no_invalid_sha_poison.sh
new file mode 100755
index 000000000000..65cde354b9f8
--- /dev/null
+++ b/tools/testing/selftests/net/arp_ndisc_no_invalid_sha_poison.sh
@@ -0,0 +1,368 @@
+#!/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 V6_ADDR0="fd00:1::1"
+readonly V4_ADDR1="10.0.10.2"
+readonly V6_ADDR1="fd00:1::2"
+readonly V6_ALL_NODES="ff02::1"
+readonly V6_SOL_NODE1="ff02::1:ff00:0002"
+readonly BCAST_MAC="ff:ff:ff:ff:ff:ff"
+readonly MCAST_MAC="01:00:5e:00:00:00"
+readonly NULL_MAC="00:00:00:00:00:00"
+readonly VALID_MAC="02:01:02:03:04:05"
+readonly V6_ALL_NODE_MAC="33:33:FF:00:00:01"
+readonly V6_SOL_NODE_MAC1="33:33:FF:00:00:02"
+readonly NS=135
+readonly NA=136
+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 addr add ${V6_ADDR0}/64 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
+
+	ip netns exec ${PEER_NS} ip addr add ${V6_ADDR1}/64 dev veth1
+	ip netns exec ${PEER_NS} ip route add default via ${V6_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
+}
+
+# Make sure NDP announcement with invalid MAC is never learnt
+run_no_ndp_poisoning() {
+	local l2_dmac=${1}
+	local dst_ip=${2}
+	local op=${3}
+	local tip=${V6_ADDR0}
+	local tmac=${4}
+
+	if [ "${op}" == "${NS}" ]; then
+		tip=${V6_ADDR1}
+	fi
+
+	ret=0
+
+	ip netns exec ${PEER_NS} ip -6 neigh flush dev veth1 >/dev/null 2>&1
+	ip netns exec ${PEER_NS} ping -c 1 ${V6_ADDR0} >/dev/null 2>&1
+
+	# Poison with a valid MAC to ensure injection is working
+	./ndisc_send ${veth0_ifindex} ${l2_dmac} ${VALID_MAC} ${dst_ip} \
+		${V6_ADDR0} ${tip} ${op} ${VALID_MAC}
+	neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V6_ADDR0} | \
+		grep ${VALID_MAC})
+	if [ "${neigh}" == "" ]; then
+		echo "ERROR: unable to NDP poision with a valid MAC ${VALID_MAC}"
+		ip netns exec ${PEER_NS} ip neigh show ${V6_ADDR0}
+		ret=1
+		return
+	fi
+
+	# Poison with tmac
+	./ndisc_send ${veth0_ifindex} ${l2_dmac} ${VALID_MAC} ${dst_ip} \
+		${V6_ADDR0} ${tip} ${op} ${tmac}
+	neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V6_ADDR0} | \
+		grep ${tmac})
+	if [ "${neigh}" != "" ]; then
+		echo "ERROR: NDP entry learnt for ${tmac} announcement."
+		ip netns exec ${PEER_NS} ip neigh show ${V6_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"
+
+	msg="1.5  ARP no poisoning dmac=bcast req   sha=mcast"
+	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REQ}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="1.6  ARP no poisoning dmac=bcast reply sha=mcast"
+	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REPLY}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Targeted gARPs
+	msg="1.7  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.8  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.9  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.10 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"
+
+	msg="1.11 ARP no poisoning dmac=veth0 req   sha=mcast"
+	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REQ}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="1.12 ARP no poisoning dmac=veth0 reply sha=mcast"
+	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REPLY}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	### NDP
+	## NA
+	# Broadcast / All node MAC, all-node IP announcements
+	msg="2.1  NDP no poisoning dmac=bcast   all_nodes na lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ALL_NODES} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.2  NDP no poisoning dmac=bcast   all_nodes na lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ALL_NODES} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.3  NDP no poisoning dmac=allnode all_nodes na lladdr=bcast"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ALL_NODES} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.4  NDP no poisoning dmac=allnode all_nodes na lladdr=null"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ALL_NODES} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.5  NDP no poisoning dmac=bcast   all_nodes na lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ALL_NODES} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.6  NDP no poisoning dmac=bcast   all_nodes na lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ALL_NODES} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.7  NDP no poisoning dmac=allnode all_nodes na lladdr=bcast"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ALL_NODES} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.8  NDP no poisoning dmac=allnode all_nodes na lladdr=null"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ALL_NODES} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Broadcast / All node MAC, Targeted IP announce
+	msg="2.9  NDP no poisoning dmac=bcast   targeted  na lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.10 NDP no poisoning dmac=bcast   targeted  na lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.11 NDP no poisoning dmac=allnode targeted  na lladdr=bcast"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ADDR1} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.12 NDP no poisoning dmac=allnode targeted  na lladdr=null"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ADDR1} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.13 NDP no poisoning dmac=bcast   targeted  na lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.14 NDP no poisoning dmac=bcast   targeted  na lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.15 NDP no poisoning dmac=allnode targeted  na lladdr=bcast"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ADDR1} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.16 NDP no poisoning dmac=allnode targeted  na lladdr=null"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ADDR1} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Targeted MAC, Targeted IP announce
+	msg="2.17 NDP no poisoning dmac=veth1   targeted  na lladdr=bcast"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NA} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.18 NDP no poisoning dmac=veth1   targeted  na lladdr=null"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NA} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Poison with MCAST mac
+	msg="2.19 NDP no poisoning dmac=allnode all_nodes na lladdr=mcast"
+	run_no_ndp_poisoning ${V6_ALL_NODE_MAC} ${V6_ALL_NODES} ${NA} ${MCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+	msg="2.20 NDP no poisoning dmac=veth1   targeted  na lladdr=mcast"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NA} ${MCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	## NS
+	# Broadcast / SolNode node MAC, SolNode IP solic
+	msg="2.21 NDP no poisoning dmac=bcast   solnode   ns lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_SOL_NODE1} ${NS} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.22 NDP no poisoning dmac=bcast   solnode   ns lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_SOL_NODE1} ${NS} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.23 NDP no poisoning dmac=solnode solnode   ns lladdr=bcast"
+	run_no_ndp_poisoning ${V6_SOL_NODE_MAC1} ${V6_SOL_NODE1} ${NS} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.24 NDP no poisoning dmac=solnode solnode   ns lladdr=null"
+	run_no_ndp_poisoning ${V6_SOL_NODE_MAC1} ${V6_SOL_NODE1} ${NS} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Broadcast / SolNode node MAC, target IP solic
+	msg="2.25 NDP no poisoning dmac=bcast   target    ns lladdr=bcast"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NS} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.26 NDP no poisoning dmac=bcast   target    ns lladdr=null"
+	run_no_ndp_poisoning ${BCAST_MAC} ${V6_ADDR1} ${NS} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.27 NDP no poisoning dmac=solnode target    ns lladdr=bcast"
+	run_no_ndp_poisoning ${V6_SOL_NODE_MAC1} ${V6_ADDR1} ${NS} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.28 NDP no poisoning dmac=solnode target    ns lladdr=null"
+	run_no_ndp_poisoning ${V6_SOL_NODE_MAC1} ${V6_ADDR1} ${NS} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Targeted MAC, Targeted IP solic
+	msg="2.29 NDP no poisoning dmac=veth1   target    ns lladdr=bcast"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NS} ${BCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.30 NDP no poisoning dmac=veth1   target    ns lladdr=null"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NS} ${NULL_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	# Poison with MCAST mac
+	msg="2.31 NDP no poisoning dmac=solnode solnode   ns lladdr=mcast"
+	run_no_ndp_poisoning ${V6_SOL_NODE_MAC1} ${V6_SOL_NODE1} ${NS} ${MCAST_MAC}
+	results+="$(print_test_result "${msg}" ${ret})\n"
+
+	msg="2.32 NDP no poisoning dmac=veth1   target    ns lladdr=mcast"
+	run_no_ndp_poisoning ${veth1_mac} ${V6_ADDR1} ${NS} ${MCAST_MAC}
+	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_no_invalid_sha_poision.sh b/tools/testing/selftests/net/arp_no_invalid_sha_poision.sh
deleted file mode 100755
index 1505f1bde487..000000000000
--- a/tools/testing/selftests/net/arp_no_invalid_sha_poision.sh
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: GPL-2.0
-#
-# Tests that ARP announcements with Broadcast, Multicast 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 MCAST_MAC="01:00:5e:00:00:00"
-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} ${l2_dmac} ${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"
-
-	msg="1.5  ARP no poisoning dmac=bcast req   sha=mcast"
-	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REQ}
-	results+="$(print_test_result "${msg}" ${ret})\n"
-
-	msg="1.6  ARP no poisoning dmac=bcast reply sha=mcast"
-	run_no_arp_poisoning ${BCAST_MAC} ${MCAST_MAC} ${ARP_REPLY}
-	results+="$(print_test_result "${msg}" ${ret})\n"
-
-	# Targeted gARPs
-	msg="1.7  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.8  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.9  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.10 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"
-
-	msg="1.11 ARP no poisoning dmac=veth0 req   sha=mcast"
-	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REQ}
-	results+="$(print_test_result "${msg}" ${ret})\n"
-
-	msg="1.12 ARP no poisoning dmac=veth0 reply sha=mcast"
-	run_no_arp_poisoning ${veth1_mac} ${MCAST_MAC} ${ARP_REPLY}
-	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/ndisc_send.c b/tools/testing/selftests/net/ndisc_send.c
new file mode 100644
index 000000000000..4f226221d079
--- /dev/null
+++ b/tools/testing/selftests/net/ndisc_send.c
@@ -0,0 +1,198 @@
+// 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>
+#include <linux/ipv6.h>
+#include <linux/icmpv6.h>
+
+#define ICMPV6_ND_NS 135
+#define ICMPV6_ND_NA 136
+#define ICMPV6_ND_SLLADR 1
+#define ICMPV6_ND_TLLADR 2
+
+#ifndef __noinline
+#define __noinline __attribute__((noinline))
+#endif
+#ifndef __packed
+#define __packed __attribute__((packed))
+#endif
+
+struct icmp6_pseudohdr {
+	struct in6_addr saddr;
+	struct in6_addr daddr;
+	uint32_t plen;
+	uint8_t zero[3];
+	uint8_t next;
+};
+
+struct ndisc_pkt {
+	struct ethhdr eth;
+	struct ipv6hdr ip6;
+	struct ndp_hdrs {
+		struct icmp6hdr hdr;
+		struct in6_addr target;
+
+		uint8_t opt_type;
+		uint8_t opt_len;
+		uint8_t opt_mac[ETH_ALEN];
+	} __packed ndp;
+} __packed;
+
+__noinline uint32_t csum_add(void *buf, int len, uint32_t sum)
+{
+	uint16_t *p = (uint16_t *)buf;
+
+	while (len > 1) {
+		sum += *p++;
+		len -= 2;
+	}
+
+	if (len)
+		sum += *(uint8_t *)p;
+
+	return sum;
+}
+
+static uint16_t csum_fold(uint32_t sum)
+{
+	return ~((sum & 0xffff) + (sum >> 16)) ? : 0xffff;
+}
+
+int parse_opts(int argc, char **argv, int *ifindex, struct ndisc_pkt *pkt)
+{
+	struct ether_addr *mac;
+	uint16_t op;
+	struct icmp6_pseudohdr ph = {0};
+	uint32_t sum = 0;
+
+	if (argc != 9) {
+		fprintf(stderr, "Usage: %s <iface> <mac_dst> <mac_src> <dst_ip> <src_ip> <target_ip> <op> <lladr>\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[1]);
+		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[2]);
+		return -1;
+	}
+	memcpy(pkt->eth.h_source, mac, ETH_ALEN);
+	pkt->eth.h_proto = htons(ETH_P_IPV6);
+
+	/* IPv6 */
+	pkt->ip6.version = 6;
+	pkt->ip6.nexthdr = IPPROTO_ICMPV6;
+	pkt->ip6.hop_limit = 255;
+
+	if (inet_pton(AF_INET6, argv[4], &pkt->ip6.daddr) != 1) {
+		fprintf(stderr, "Unable to parse src_ip from '%s'\n", argv[4]);
+		return -1;
+	}
+	if (inet_pton(AF_INET6, argv[5], &pkt->ip6.saddr) != 1) {
+		fprintf(stderr, "Unable to parse src_ip from '%s'\n", argv[5]);
+		return -1;
+	}
+
+	/* ICMPv6 */
+	op = atoi(argv[7]);
+	if (op != ICMPV6_ND_NS && op != ICMPV6_ND_NA) {
+		fprintf(stderr, "Invalid ICMPv6 op %d\n", op);
+		return -1;
+	}
+
+	pkt->ndp.hdr.icmp6_type = op;
+	pkt->ndp.hdr.icmp6_code = 0;
+
+	if (inet_pton(AF_INET6, argv[6], &pkt->ndp.target) != 1) {
+		fprintf(stderr, "Unable to parse target_ip from '%s'\n",
+			argv[6]);
+		return -1;
+	}
+
+	/* Target/Source Link-Layer Address */
+	if (op == ICMPV6_ND_NS) {
+		pkt->ndp.opt_type = ICMPV6_ND_SLLADR;
+	} else {
+		pkt->ndp.opt_type = ICMPV6_ND_TLLADR;
+		pkt->ndp.hdr.icmp6_override = 1;
+	}
+	pkt->ndp.opt_len = 1;
+
+	mac = ether_aton(argv[8]);
+	if (!mac) {
+		fprintf(stderr, "Invalid lladdr %s\n", argv[8]);
+		return -1;
+	}
+
+	memcpy(pkt->ndp.opt_mac, mac, ETH_ALEN);
+
+	pkt->ip6.payload_len = htons(sizeof(pkt->ndp));
+
+	/* Pseudoheader */
+	ph.saddr = pkt->ip6.saddr;
+	ph.daddr = pkt->ip6.daddr;
+	ph.plen = htonl(sizeof(pkt->ndp));
+	ph.next = IPPROTO_ICMPV6;
+
+	sum = csum_add(&ph, sizeof(ph), 0);
+	sum = csum_add(&pkt->ndp, sizeof(pkt->ndp), sum);
+
+	pkt->ndp.hdr.icmp6_cksum = csum_fold(sum);
+
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	int rc, fd;
+	struct sockaddr_ll bind_addr = {0};
+	int ifindex;
+	struct ndisc_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


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison)
  2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
                   ` (3 preceding siblings ...)
  2026-01-26 23:53 ` [PATCH net v2 4/4] selftests/net: add no NDP b/mcast,null poison test Marc Suñé
@ 2026-01-29  4:24 ` Jakub Kicinski
  2026-01-29 18:39   ` Marc Sune
  4 siblings, 1 reply; 12+ messages in thread
From: Jakub Kicinski @ 2026-01-29  4:24 UTC (permalink / raw)
  To: Marc Suñé
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

On Tue, 27 Jan 2026 00:53:01 +0100 Marc Suñé wrote:
> The current ARP and NDP implementations accept announcements with
> multicast (broadcast incl.) and null MAC addresses as Sender HW Address
> (SHA) in ARP or src/target lladdr in NDP, and updates the cache
> for that neighbour.
> 
> Multicast (incl. broadcast) and null MAC addresses shall never be
> associated with a unicast or a multicast IPv4/6 address (see RFC1812,
> section 3.3.2).
> 
> ARP/NDP poisioning with a broadcast and certain multicast MAC addresses,
> especially when poisoning a Gateway IP, have some undesired implications
> compared to an ARP/NDP poisioning with a regular MAC (see commit message
> in patch 1 for more information).
> 
> Worth mentioning that if an attacker is able to ARP/NDP poison in
> a L2 segment, that in itself is probably a bigger security threat
> (Man-in-middle etc., see Note2 in patch 1)
> 
> Since these MACs should never be announced, this patch series discards/drops
> these packets, which prevents broadcast and multicast ARP/NDP poisoning
> vectors.
> 
> This patchset only modifies the behaviour of the neighbouring subsystem
> when processing network packets. Static entries can still be added with
> mcast/bcast/null MACs.

Not a very strong opinion but my intuition would be to target 
this to net-next. I read it as an improvement to RFC compliance
more than a solution.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [net,v2,3/4] neigh: discard invalid lladdr (b/mcast poison)
  2026-01-26 23:53 ` [PATCH net v2 3/4] neigh: discard invalid lladdr (b/mcast poison) Marc Suñé
@ 2026-01-29  4:24   ` Jakub Kicinski
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Kicinski @ 2026-01-29  4:24 UTC (permalink / raw)
  To: marcdevel
  Cc: Jakub Kicinski, dborkman, willemdebruijn.kernel, netdev,
	vadim.fedorenko, pabeni

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.

For local reproduction steps see https://netdev-ai.bots.linux.dev/ai-local.html
---
neigh: discard invalid lladdr (b/mcast poison)

This commit adds validation to reject NDP Neighbor Solicitation and
Neighbor Advertisement messages that contain broadcast, multicast, or
null MAC addresses in the link-layer address options.

> diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
> index f6a5d8c73af9..34202a816a4f 100644
> --- a/net/ipv6/ndisc.c
> +++ b/net/ipv6/ndisc.c

[ ... ]

> @@ -1033,6 +1041,14 @@ static enum skb_drop_reason ndisc_recv_na(struct sk_buff *skb)
> 			net_dbg_ratelimited("NA: invalid link-layer address length\n");
> 			return reason;
> 		}
> +
> +		/*
> +		 * Broadcast/Multicast and zero MAC addresses should
> +		 * never be announced and accepted as llsrc address (prevent
                                                  ^^^^^

In ndisc_recv_na(), should this comment say "lltgt" (target link-layer
address) instead of "llsrc"?

Looking at the code above this block, the lladdr variable is assigned from
ndopts.nd_opts_tgt_lladdr (Target Link-Layer Address option per RFC 4861),
not nd_opts_src_lladdr (Source Link-Layer Address option). The comment
appears to have been copy-pasted from the NS handler without updating the
terminology.

> +		 * NDP B/MCAST MAC poisoning attack).
> +		 */
> +		if (dev->type == ARPHRD_ETHER && !is_valid_ether_addr(lladdr))
> +			return reason;
> 	}
-- 
pw-bot: cr

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test
  2026-01-26 23:53 ` [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test Marc Suñé
@ 2026-01-29  4:27   ` Jakub Kicinski
  2026-01-29 18:28     ` Marc Sune
  0 siblings, 1 reply; 12+ messages in thread
From: Jakub Kicinski @ 2026-01-29  4:27 UTC (permalink / raw)
  To: Marc Suñé
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

On Tue, 27 Jan 2026 00:53:03 +0100 Marc Suñé wrote:
> Add a selftest to test that ARP bcast/mcast/null poisioning checks
> are never bypassed.

> diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
> index 45c4ea381bc3..a765f1800752 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_invalid_sha_poision.sh \

alphabetical sort pls

>  	arp_ndisc_evict_nocarrier.sh \
>  	arp_ndisc_untracked_subnets.sh \
>  	bareudp.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 MCAST_MAC="01:00:5e:00:00:00"
> +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

unused? (please run shellcheck)

> +ret=0
> +veth0_ifindex=0
> +veth1_mac=

> diff --git a/tools/testing/selftests/net/arp_send.c b/tools/testing/selftests/net/arp_send.c
> new file mode 100644

Could you check if mausezahn from netsniff-ng can send these
packets already? We already depend on mausezahn for other tests.

Similar comments on patch 4, I'm not gonna repeat.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test
  2026-01-29  4:27   ` Jakub Kicinski
@ 2026-01-29 18:28     ` Marc Sune
  2026-01-29 23:37       ` Jakub Kicinski
  0 siblings, 1 reply; 12+ messages in thread
From: Marc Sune @ 2026-01-29 18:28 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

Missatge de Jakub Kicinski <kuba@kernel.org> del dia dj., 29 de gen.
2026 a les 5:27:
>
> On Tue, 27 Jan 2026 00:53:03 +0100 Marc Suñé wrote:
> > Add a selftest to test that ARP bcast/mcast/null poisioning checks
> > are never bypassed.
>
> > diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
> > index 45c4ea381bc3..a765f1800752 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_invalid_sha_poision.sh \
>
> alphabetical sort pls
>
> >       arp_ndisc_evict_nocarrier.sh \
> >       arp_ndisc_untracked_subnets.sh \
> >       bareudp.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 MCAST_MAC="01:00:5e:00:00:00"
> > +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
>
> unused? (please run shellcheck)

Errors fixed in v3.

>
> > +ret=0
> > +veth0_ifindex=0
> > +veth1_mac=
>
> > diff --git a/tools/testing/selftests/net/arp_send.c b/tools/testing/selftests/net/arp_send.c
> > new file mode 100644
>
> Could you check if mausezahn from netsniff-ng can send these
> packets already? We already depend on mausezahn for other tests.
>
> Similar comments on patch 4, I'm not gonna repeat.

I can do that for ARP, but not for NDP apparently. Only basic ICMPv6
echo req/reply is supported. For NDP (ICMPv6), it needs to be
specified using -P (hex). Kind of ugly...

In RFC v1 I proposed to use a scapy
(https://lore.kernel.org/netdev/3cfd28edb2c2b055e74b975623a3d38ade0237f1.1766349632.git.marcdevel@gmail.com/),
which is substantially smaller than arp_send/ndisc_send.c. I dropped
it based on the v1 review I got. I see scapy is used in bpf/ and
tc-testing/ self-tests.

What's your preference?

a) use mausezahn only for ARP, use ndisc_send.c for NDP
b) use only .c progs for consistency
c) recover the scapy patch and squash it in patches 2/4

Thanks

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison)
  2026-01-29  4:24 ` [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Jakub Kicinski
@ 2026-01-29 18:39   ` Marc Sune
  2026-01-29 23:32     ` Jakub Kicinski
  0 siblings, 1 reply; 12+ messages in thread
From: Marc Sune @ 2026-01-29 18:39 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

Missatge de Jakub Kicinski <kuba@kernel.org> del dia dj., 29 de gen.
2026 a les 5:24:
>
> On Tue, 27 Jan 2026 00:53:01 +0100 Marc Suñé wrote:
> > The current ARP and NDP implementations accept announcements with
> > multicast (broadcast incl.) and null MAC addresses as Sender HW Address
> > (SHA) in ARP or src/target lladdr in NDP, and updates the cache
> > for that neighbour.
> >
> > Multicast (incl. broadcast) and null MAC addresses shall never be
> > associated with a unicast or a multicast IPv4/6 address (see RFC1812,
> > section 3.3.2).
> >
> > ARP/NDP poisioning with a broadcast and certain multicast MAC addresses,
> > especially when poisoning a Gateway IP, have some undesired implications
> > compared to an ARP/NDP poisioning with a regular MAC (see commit message
> > in patch 1 for more information).
> >
> > Worth mentioning that if an attacker is able to ARP/NDP poison in
> > a L2 segment, that in itself is probably a bigger security threat
> > (Man-in-middle etc., see Note2 in patch 1)
> >
> > Since these MACs should never be announced, this patch series discards/drops
> > these packets, which prevents broadcast and multicast ARP/NDP poisoning
> > vectors.
> >
> > This patchset only modifies the behaviour of the neighbouring subsystem
> > when processing network packets. Static entries can still be added with
> > mcast/bcast/null MACs.
>
> Not a very strong opinion but my intuition would be to target
> this to net-next. I read it as an improvement to RFC compliance
> more than a solution.

The main driver for this patchset is to remove the attack vectors
described in Note 1 and Note 2 of Patch 1/4 (in the cover letter of
RFC v1), not so much being RFC compliant. They are arguably low risk,
but I would think there is value in having them on all stable
versions. I originally targeted net and didn't add Fixes as I think
these sanity checks have never been there.

Let me know if you prefer v3 to target net-next instead.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison)
  2026-01-29 18:39   ` Marc Sune
@ 2026-01-29 23:32     ` Jakub Kicinski
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Kicinski @ 2026-01-29 23:32 UTC (permalink / raw)
  To: Marc Sune
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

On Thu, 29 Jan 2026 19:39:59 +0100 Marc Sune wrote:
> > > This patchset only modifies the behaviour of the neighbouring subsystem
> > > when processing network packets. Static entries can still be added with
> > > mcast/bcast/null MACs.  
> >
> > Not a very strong opinion but my intuition would be to target
> > this to net-next. I read it as an improvement to RFC compliance
> > more than a solution.  
> 
> The main driver for this patchset is to remove the attack vectors
> described in Note 1 and Note 2 of Patch 1/4 (in the cover letter of
> RFC v1), not so much being RFC compliant. They are arguably low risk,
> but I would think there is value in having them on all stable
> versions. I originally targeted net and didn't add Fixes as I think
> these sanity checks have never been there.
> 
> Let me know if you prefer v3 to target net-next instead.

Nobody else chiming in to disagree with me so if it's your word against
mine I do prefer net-next :)

No matter what we do an unsecured L2 is not defensible by making tweaks
at the endpoint in the IP protocol stack.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test
  2026-01-29 18:28     ` Marc Sune
@ 2026-01-29 23:37       ` Jakub Kicinski
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Kicinski @ 2026-01-29 23:37 UTC (permalink / raw)
  To: Marc Sune
  Cc: willemdebruijn.kernel, pabeni, netdev, dborkman, vadim.fedorenko

On Thu, 29 Jan 2026 19:28:22 +0100 Marc Sune wrote:
> > > diff --git a/tools/testing/selftests/net/arp_send.c b/tools/testing/selftests/net/arp_send.c
> > > new file mode 100644  
> >
> > Could you check if mausezahn from netsniff-ng can send these
> > packets already? We already depend on mausezahn for other tests.
> >
> > Similar comments on patch 4, I'm not gonna repeat.  
> 
> I can do that for ARP, but not for NDP apparently. Only basic ICMPv6
> echo req/reply is supported. For NDP (ICMPv6), it needs to be
> specified using -P (hex). Kind of ugly...
> 
> In RFC v1 I proposed to use a scapy
> (https://lore.kernel.org/netdev/3cfd28edb2c2b055e74b975623a3d38ade0237f1.1766349632.git.marcdevel@gmail.com/),
> which is substantially smaller than arp_send/ndisc_send.c. I dropped
> it based on the v1 review I got. I see scapy is used in bpf/ and
> tc-testing/ self-tests.
> 
> What's your preference?
> 
> a) use mausezahn only for ARP, use ndisc_send.c for NDP
> b) use only .c progs for consistency
> c) recover the scapy patch and squash it in patches 2/4

Hm, I think there's another tool we use for ndisc - ndisc6
Could you try if that works? if not maybe grep around if any
other tool is already used by the tests, we have a bunch of 
ndisc and igmp tests, maybe one of those will do.

If not, my first choice would be to update ndisc6 or mausezahn
to teach them. Second choice a local C code just ndisc_send.c
scapy as the last choice.

^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-01-29 23:37 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-26 23:53 [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Marc Suñé
2026-01-26 23:53 ` [PATCH net v2 1/4] arp: discard invalid sha addr (b/mcast ARP poison) Marc Suñé
2026-01-26 23:53 ` [PATCH net v2 2/4] selftests/net: add no ARP b/mcast,null poison test Marc Suñé
2026-01-29  4:27   ` Jakub Kicinski
2026-01-29 18:28     ` Marc Sune
2026-01-29 23:37       ` Jakub Kicinski
2026-01-26 23:53 ` [PATCH net v2 3/4] neigh: discard invalid lladdr (b/mcast poison) Marc Suñé
2026-01-29  4:24   ` [net,v2,3/4] " Jakub Kicinski
2026-01-26 23:53 ` [PATCH net v2 4/4] selftests/net: add no NDP b/mcast,null poison test Marc Suñé
2026-01-29  4:24 ` [PATCH net v2 0/4] discard ARP/NDP b/mcast/null announce (poison) Jakub Kicinski
2026-01-29 18:39   ` Marc Sune
2026-01-29 23:32     ` Jakub Kicinski

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox