* [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison)
@ 2025-12-21 21:19 Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison) Marc Suñé
` (4 more replies)
0 siblings, 5 replies; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
The current ARP and NDP implementations accepts announcements with
broadcast (mcast, and null) MAC addresses as Sender HW Address
(SHA) in ARP or src/target lladdr in NDP, and updates the cache
for that neighbour.
Broadcast (and Multicast, see RFC1812, section 3.3.2) and null
MAC addresses are reserved addresses and shall never be associated
with a unicast or a multicast IPv4/6 address.
ARP/NDP poisioning with a broadcast MAC address, especially when
poisoning a Gateway IP, has some undesired implications compared to
an ARP/NDP poisioning with a regular MAC. See Note1.
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.
However, since these MACs should never be announced, this patch
series discards/drops these packets, which prevents broadcast
ARP/NDP poisoning vectors.
Comments:
a) This patchset only modifies the behaviour of the neighbouring
subsystem when processing network packets. Static entries can still
be added with bcast/null MACs.
b) According to RFC1812 multicast MAC addresses should also be
rejected:
> A router MUST not believe any ARP reply that claims that the Link
> Layer address of another host or router is a broadcast or multicast
> address.
Certain Load Balancers make use of Multicast MAC addresses:
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 be 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.
So I think it's safe to change `is_broadcast_ether_addr()` to
`is_multicast_ether_addr()` in patches 1 and 3. Alternatively, a
per-interface knob to control whether MCAST MAC addresses are learnt,
with the default to NOT accept them, could be an option.
c) Scapy: not clear whether acceptable in selftests (used somewhere but
not consistently). It is convenient for pkt generation. Patch 5
should be either dropped or squashed (replacing C progs) in patches
2 and 4.
d) In PATCH 1/5 (ARP), I _think_ it's safe to assume that all dev_types
with HW addrlen 6 are MAC addresses, but it would need to be double
checked by reviewers.
(Notes extracted from the ARP commit. NDP similar. MCAST MACs exhibit
a similar behaviour - depends on the exact MCAST MAC.)
Note1:
After a successful broadcast 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.
Marc Suñé (5):
arp: discard sha bcast/null (bcast ARP poison)
selftests/net: add no ARP bcast/null poison test
neigh: discard lladdr bcast/null (bcast poison)
selftests/net: add no NDP bcast/null poison test
selftests/net: use scapy for no_bcastnull_poison
net/ipv4/arp.c | 9 +
net/ipv6/ndisc.c | 22 ++
tools/testing/selftests/net/.gitignore | 2 +
tools/testing/selftests/net/Makefile | 1 +
.../net/arp_ndisc_no_bcastnull_poison.sh | 334 ++++++++++++++++++
tools/testing/selftests/net/arp_send.py | 24 ++
tools/testing/selftests/net/ndisc_send.py | 36 ++
7 files changed, 428 insertions(+)
create mode 100755 tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
create mode 100644 tools/testing/selftests/net/arp_send.py
create mode 100644 tools/testing/selftests/net/ndisc_send.py
--
2.47.3
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison)
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
@ 2025-12-21 21:19 ` Marc Suñé
2025-12-22 9:47 ` Vadim Fedorenko
2025-12-21 21:19 ` [PATCH RFC net 2/5] selftests/net: add no ARP bcast/null poison test Marc Suñé
` (3 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
The current ARP implementation accepts ARP req/replies with the
broadcast (mcast, and null) MAC addresses as Sender HW Address
(SHA), and updates the ARP cache for that neighbour.
Broadcast (and Multicast, see RFC1812, section 3.3.2) and null
MAC addresses are reserved addresses and shall never be associated
with a unicast or a multicast IPv4 address.
ARP poisioning with a broadcast MAC address, especially when
poisoning a Gateway IP, has 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={bcast, null}, which prevents the
broadcast ARP poisoning vector.
Note1:
After a successful broadcast 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 | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/net/ipv4/arp.c b/net/ipv4/arp.c
index 7f3863daaa40..83b34b89b1be 100644
--- a/net/ipv4/arp.c
+++ b/net/ipv4/arp.c
@@ -799,6 +799,15 @@ static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb)
goto out_free_skb;
/*
+ * For Ethernet devices, Broadcast/Multicast and zero MAC addresses should
+ * never be announced and accepted as sender HW address (prevent BCAST MAC
+ * and NULL ARP poisoning attack).
+ */
+ if (dev->addr_len == ETH_ALEN &&
+ (is_broadcast_ether_addr(sha) || is_zero_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] 11+ messages in thread
* [PATCH RFC net 2/5] selftests/net: add no ARP bcast/null poison test
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison) Marc Suñé
@ 2025-12-21 21:19 ` Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 3/5] neigh: discard lladdr bcast/null (bcast poison) Marc Suñé
` (2 subsequent siblings)
4 siblings, 0 replies; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
Add a selftest to test that ARP bcast/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 +
.../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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC net 3/5] neigh: discard lladdr bcast/null (bcast poison)
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison) Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 2/5] selftests/net: add no ARP bcast/null poison test Marc Suñé
@ 2025-12-21 21:19 ` Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 4/5] selftests/net: add no NDP bcast/null poison test Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison Marc Suñé
4 siblings, 0 replies; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
The current NDP implementation accepts NDP NS/NS with the
broadcast (mcast, and null) MAC addresses as src/dst lladdr, and
updates the neighbour cache for that host.
Broadcast (and Multicast, see RFC1812, section 3.3.2) and null
MAC addresses are reserved addresses and shall never be associated
with a unicast or a multicast IPv6 address.
NDP poisioning with a broadcast MAC address, especially when
poisoning a Gateway IP, has some undesired implications compared to
an NDP poisioning with a regular MAC. (see ARP bcast poison
commit for more details).
Worth mentioning that if an attacker is able to NDP poison in
a L2 segment, that in itself is probably a bigger security threat
(Man-in-middle etc.).
However, since these MACs should never be announced as SHA,
discard/drop NDP with lladdr={bcast, null}, which prevents the
broadcast NDP poisoning vector.
Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
net/ipv6/ndisc.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index 59d17b6f06bf..980768a79092 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -830,6 +830,17 @@ 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 BCAST MAC poisoning attack).
+ */
+ if (dev->addr_len == ETH_ALEN &&
+ (is_broadcast_ether_addr(lladdr) ||
+ is_zero_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 +1044,17 @@ 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 BCAST MAC poisoning attack).
+ */
+ if (dev->addr_len == ETH_ALEN &&
+ (is_broadcast_ether_addr(lladdr) ||
+ is_zero_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] 11+ messages in thread
* [PATCH RFC net 4/5] selftests/net: add no NDP bcast/null poison test
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
` (2 preceding siblings ...)
2025-12-21 21:19 ` [PATCH RFC net 3/5] neigh: discard lladdr bcast/null (bcast poison) Marc Suñé
@ 2025-12-21 21:19 ` Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison Marc Suñé
4 siblings, 0 replies; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
Add a selftest to test that NDP bcast/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 | 1 +
.../net/arp_ndisc_no_bcastnull_poison.sh | 334 ++++++++++++++++++
.../selftests/net/arp_no_bcastnull_poision.sh | 159 ---------
tools/testing/selftests/net/ndisc_send.c | 198 +++++++++++
5 files changed, 534 insertions(+), 159 deletions(-)
create mode 100755 tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
delete mode 100755 tools/testing/selftests/net/arp_no_bcastnull_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 8308f0067547..a5bd845e1c58 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -169,6 +169,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_bcastnull_poison.sh b/tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
new file mode 100755
index 000000000000..dd0bdd0e3f37
--- /dev/null
+++ b/tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
@@ -0,0 +1,334 @@
+#!/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 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"
+
+ ## 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"
+
+ ### 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"
+
+ ## NS
+ # Broadcast / SolNode node MAC, SolNode IP solic
+ msg="2.19 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.20 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.19 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.20 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.21 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.22 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.23 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.24 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.25 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.26 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"
+
+ 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_bcastnull_poision.sh b/tools/testing/selftests/net/arp_no_bcastnull_poision.sh
deleted file mode 100755
index d0b9241599f1..000000000000
--- a/tools/testing/selftests/net/arp_no_bcastnull_poision.sh
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/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/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] 11+ messages in thread
* [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
` (3 preceding siblings ...)
2025-12-21 21:19 ` [PATCH RFC net 4/5] selftests/net: add no NDP bcast/null poison test Marc Suñé
@ 2025-12-21 21:19 ` Marc Suñé
2025-12-22 9:34 ` Vadim Fedorenko
4 siblings, 1 reply; 11+ messages in thread
From: Marc Suñé @ 2025-12-21 21:19 UTC (permalink / raw)
To: kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman, Marc Suñé
Use Scapy to generate ARP/ND packets for ARP/ND no bcast/NULL MAC
poisoning.
Signed-off-by: Marc Suñé <marcdevel@gmail.com>
---
tools/testing/selftests/net/Makefile | 2 -
.../net/arp_ndisc_no_bcastnull_poison.sh | 12 +-
tools/testing/selftests/net/arp_send.c | 138 ------------
tools/testing/selftests/net/arp_send.py | 24 +++
tools/testing/selftests/net/ndisc_send.c | 198 ------------------
tools/testing/selftests/net/ndisc_send.py | 36 ++++
6 files changed, 66 insertions(+), 344 deletions(-)
delete mode 100644 tools/testing/selftests/net/arp_send.c
create mode 100644 tools/testing/selftests/net/arp_send.py
delete mode 100644 tools/testing/selftests/net/ndisc_send.c
create mode 100644 tools/testing/selftests/net/ndisc_send.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index a5bd845e1c58..6fe0b962cd05 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -164,12 +164,10 @@ TEST_GEN_FILES := \
# end of TEST_GEN_FILES
TEST_GEN_PROGS := \
- arp_send \
bind_timewait \
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_bcastnull_poison.sh b/tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
index dd0bdd0e3f37..8dc79da27a2e 100755
--- a/tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
+++ b/tools/testing/selftests/net/arp_ndisc_no_bcastnull_poison.sh
@@ -75,7 +75,7 @@ run_no_arp_poisoning() {
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} \
+ python3 ./arp_send.py "veth0" ${BCAST_MAC} ${VALID_MAC} ${op} \
${V4_ADDR0} ${VALID_MAC} ${V4_ADDR0} ${VALID_MAC}
neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0} | \
@@ -88,7 +88,7 @@ run_no_arp_poisoning() {
fi
# Poison with tmac
- ./arp_send ${veth0_ifindex} ${l2_dmac} ${VALID_MAC} ${op} \
+ python3 ./arp_send.py "veth0" ${BCAST_MAC} ${VALID_MAC} ${op} \
${V4_ADDR0} ${tmac} ${V4_ADDR0} ${tmac}
neigh=$(ip netns exec ${PEER_NS} ip neigh show ${V4_ADDR0} | \
@@ -119,8 +119,8 @@ run_no_ndp_poisoning() {
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}
+ python3 ./ndisc_send.py "veth0" ${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
@@ -131,8 +131,8 @@ run_no_ndp_poisoning() {
fi
# Poison with tmac
- ./ndisc_send ${veth0_ifindex} ${l2_dmac} ${VALID_MAC} ${dst_ip} \
- ${V6_ADDR0} ${tip} ${op} ${tmac}
+ python3 ./ndisc_send.py "veth0" ${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
diff --git a/tools/testing/selftests/net/arp_send.c b/tools/testing/selftests/net/arp_send.c
deleted file mode 100644
index 463ee435c9c1..000000000000
--- a/tools/testing/selftests/net/arp_send.c
+++ /dev/null
@@ -1,138 +0,0 @@
-// 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;
-}
diff --git a/tools/testing/selftests/net/arp_send.py b/tools/testing/selftests/net/arp_send.py
new file mode 100644
index 000000000000..1161dfd60b27
--- /dev/null
+++ b/tools/testing/selftests/net/arp_send.py
@@ -0,0 +1,24 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+import sys
+from scapy.all import *
+
+if len(sys.argv) != 9:
+ print(f"Usage: {sys.argv[0]} <iface> <mac_dst> <mac_src> <op_code> <target-ip> <target-hwaddr> <sender-ip> <sender-hwaddr>\n");
+
+iface = sys.argv[1]
+mac_dst = sys.argv[2]
+mac_src = sys.argv[3]
+op = int(sys.argv[4])
+tip = sys.argv[5]
+tha = sys.argv[6]
+sip = sys.argv[5]
+sha = sys.argv[6]
+
+pkt = (
+ Ether(dst=mac_dst, src=mac_src) /
+ ARP(op=op, psrc=sip, hwsrc=sha, pdst=tip, hwdst=tha)
+)
+
+sendp(pkt, iface=iface, verbose=False)
diff --git a/tools/testing/selftests/net/ndisc_send.c b/tools/testing/selftests/net/ndisc_send.c
deleted file mode 100644
index 4f226221d079..000000000000
--- a/tools/testing/selftests/net/ndisc_send.c
+++ /dev/null
@@ -1,198 +0,0 @@
-// 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;
-}
diff --git a/tools/testing/selftests/net/ndisc_send.py b/tools/testing/selftests/net/ndisc_send.py
new file mode 100644
index 000000000000..7b1a1c057862
--- /dev/null
+++ b/tools/testing/selftests/net/ndisc_send.py
@@ -0,0 +1,36 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+import sys
+from scapy.all import *
+
+if len(sys.argv) != 9:
+ print(f"Usage: {sys.argv[0]} <iface> <mac_dst> <mac_src> <ip_dst> <ip_src> <target_ip> <op> <lladr>")
+
+iface = sys.argv[1]
+mac_dst = sys.argv[2]
+mac_src = sys.argv[3]
+ip_dst = sys.argv[4]
+ip_src = sys.argv[5]
+tip = sys.argv[6]
+op = int(sys.argv[7])
+lladdr = sys.argv[8]
+
+NDP_NA=136
+
+if op == NDP_NA:
+ pkt = (
+ Ether(dst=mac_dst, src=mac_src) /
+ IPv6(src=ip_src, dst=ip_dst, hlim=255) /
+ ICMPv6ND_NA(R=0, S=0, O=1, tgt=tip) /
+ ICMPv6NDOptDstLLAddr(lladdr=lladdr)
+ )
+else:
+ pkt = (
+ Ether(dst=mac_dst, src=mac_src) /
+ IPv6(src=ip_src, dst=ip_dst, hlim=255) /
+ ICMPv6ND_NS(tgt=tip) /
+ ICMPv6NDOptSrcLLAddr(lladdr=lladdr)
+ )
+
+sendp(pkt, iface=iface, verbose=False)
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison
2025-12-21 21:19 ` [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison Marc Suñé
@ 2025-12-22 9:34 ` Vadim Fedorenko
2025-12-22 21:46 ` Marc Sune
0 siblings, 1 reply; 11+ messages in thread
From: Vadim Fedorenko @ 2025-12-22 9:34 UTC (permalink / raw)
To: Marc Suñé, kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman
On 21/12/2025 21:19, Marc Suñé wrote:
> Use Scapy to generate ARP/ND packets for ARP/ND no bcast/NULL MAC
> poisoning.
What's wrong with current implementation in arp_send.c and ndisc_send.c?
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison)
2025-12-21 21:19 ` [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison) Marc Suñé
@ 2025-12-22 9:47 ` Vadim Fedorenko
2025-12-22 21:37 ` Marc Sune
0 siblings, 1 reply; 11+ messages in thread
From: Vadim Fedorenko @ 2025-12-22 9:47 UTC (permalink / raw)
To: Marc Suñé, kuba, willemdebruijn.kernel, pabeni; +Cc: netdev, dborkman
On 21/12/2025 21:19, Marc Suñé wrote:
>
> /*
> + * For Ethernet devices, Broadcast/Multicast and zero MAC addresses should
> + * never be announced and accepted as sender HW address (prevent BCAST MAC
> + * and NULL ARP poisoning attack).
> + */
> + if (dev->addr_len == ETH_ALEN &&
dev_type == ARPHRD_ETHER ?
> + (is_broadcast_ether_addr(sha) || is_zero_ether_addr(sha)))
RFC says that neither broadcast, nor multicast must be believed. You
check for broadcast only. The better check would be:
!is_unicast_ether_addr(sha)
> + goto out_free_skb;
> +
> + /*
> * Special case: We must set Frame Relay source Q.922 address
> */
> if (dev_type == ARPHRD_DLCI)
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison)
2025-12-22 9:47 ` Vadim Fedorenko
@ 2025-12-22 21:37 ` Marc Sune
2025-12-22 22:08 ` Vadim Fedorenko
0 siblings, 1 reply; 11+ messages in thread
From: Marc Sune @ 2025-12-22 21:37 UTC (permalink / raw)
To: Vadim Fedorenko; +Cc: kuba, willemdebruijn.kernel, pabeni, netdev, dborkman
Missatge de Vadim Fedorenko <vadim.fedorenko@linux.dev> del dia dl.,
22 de des. 2025 a les 10:47:
>
> On 21/12/2025 21:19, Marc Suñé wrote:
> >
> > /*
> > + * For Ethernet devices, Broadcast/Multicast and zero MAC addresses should
> > + * never be announced and accepted as sender HW address (prevent BCAST MAC
> > + * and NULL ARP poisoning attack).
> > + */
> > + if (dev->addr_len == ETH_ALEN &&
>
> dev_type == ARPHRD_ETHER ?
This is discussed in the cover letter, comments section d). I would
think more dev_types than that need to check this, at least:
+ case ARPHRD_ETHER:
+ case ARPHRD_EETHER:
+ case ARPHRD_FDDI:
+ case ARPHRD_IEEE802:
+ case ARPHRD_IEEE80211:
but as said, I _think_ it's sufficient to check for HW addrlen == ETH_ALEN.
>
>
> > + (is_broadcast_ether_addr(sha) || is_zero_ether_addr(sha)))
>
> RFC says that neither broadcast, nor multicast must be believed. You
> check for broadcast only. The better check would be:
>
> !is_unicast_ether_addr(sha)
This is discussed in the cover letter, comments section b). In short,
some NLBs announce MCAST MAC addresses.
Mind the context there, but I think it's safe. This is applicable to
ARP and NDP, so I would suggest to follow up there.
Btw, the is_zero_ether_addr(addr) check is still needed.
is_unicast_ether_addr(addr) is implemented as
!is_multicast_ether_addr(addr), and the NULL mac (00:00:00:00:00:00)
doesn't have the "MCAST bit" set to 1.
>
> > + goto out_free_skb;
> > +
> > + /*
> > * Special case: We must set Frame Relay source Q.922 address
> > */
> > if (dev_type == ARPHRD_DLCI)
>
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison
2025-12-22 9:34 ` Vadim Fedorenko
@ 2025-12-22 21:46 ` Marc Sune
0 siblings, 0 replies; 11+ messages in thread
From: Marc Sune @ 2025-12-22 21:46 UTC (permalink / raw)
To: Vadim Fedorenko; +Cc: kuba, willemdebruijn.kernel, pabeni, netdev, dborkman
Missatge de Vadim Fedorenko <vadim.fedorenko@linux.dev> del dia dl.,
22 de des. 2025 a les 10:34:
>
> On 21/12/2025 21:19, Marc Suñé wrote:
> > Use Scapy to generate ARP/ND packets for ARP/ND no bcast/NULL MAC
> > poisoning.
>
> What's wrong with current implementation in arp_send.c and ndisc_send.c?
Nothing. The Scapy version of the tooling is shorter, that's all.
This is discussed in the cover letter, comments section c). Patch 5/5
can be dropped if Scapy is not acceptable, else squash them with
patches 2 (arp_send.c) and 4 (ndisc_send.c).
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison)
2025-12-22 21:37 ` Marc Sune
@ 2025-12-22 22:08 ` Vadim Fedorenko
0 siblings, 0 replies; 11+ messages in thread
From: Vadim Fedorenko @ 2025-12-22 22:08 UTC (permalink / raw)
To: Marc Sune; +Cc: kuba, willemdebruijn.kernel, pabeni, netdev, dborkman
On 22/12/2025 21:37, Marc Sune wrote:
> Missatge de Vadim Fedorenko <vadim.fedorenko@linux.dev> del dia dl.,
> 22 de des. 2025 a les 10:47:
>>
>> On 21/12/2025 21:19, Marc Suñé wrote:
>>>
>>> /*
>>> + * For Ethernet devices, Broadcast/Multicast and zero MAC addresses should
>>> + * never be announced and accepted as sender HW address (prevent BCAST MAC
>>> + * and NULL ARP poisoning attack).
>>> + */
>>> + if (dev->addr_len == ETH_ALEN &&
>>
>> dev_type == ARPHRD_ETHER ?
>
> This is discussed in the cover letter, comments section d). I would
> think more dev_types than that need to check this, at least:
>
> + case ARPHRD_ETHER:
> + case ARPHRD_EETHER:
> + case ARPHRD_FDDI:
> + case ARPHRD_IEEE802:
> + case ARPHRD_IEEE80211:
>
> but as said, I _think_ it's sufficient to check for HW addrlen == ETH_ALEN.
ARPHRD_EETHER and ARPHRD_IEEE80211 are not really used in the kernel.
For other 3 we already have such case a bit earlier in arp_process(),
it's fine to be aligned with the existing code.
>
>>
>>
>>> + (is_broadcast_ether_addr(sha) || is_zero_ether_addr(sha)))
>>
>> RFC says that neither broadcast, nor multicast must be believed. You
>> check for broadcast only. The better check would be:
>>
>> !is_unicast_ether_addr(sha)
>
> This is discussed in the cover letter, comments section b). In short,
> some NLBs announce MCAST MAC addresses.
>
> Mind the context there, but I think it's safe. This is applicable to
> ARP and NDP, so I would suggest to follow up there.
>
> Btw, the is_zero_ether_addr(addr) check is still needed.
> is_unicast_ether_addr(addr) is implemented as
> !is_multicast_ether_addr(addr), and the NULL mac (00:00:00:00:00:00)
> doesn't have the "MCAST bit" set to 1.
Yeah, "!is_valid_ether_addr(sha)" is better in this case.
>
>>
>>> + goto out_free_skb;
>>> +
>>> + /*
>>> * Special case: We must set Frame Relay source Q.922 address
>>> */
>>> if (dev_type == ARPHRD_DLCI)
>>
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2025-12-22 22:09 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-21 21:19 [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison) Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 1/5] arp: discard sha bcast/null (bcast ARP poison) Marc Suñé
2025-12-22 9:47 ` Vadim Fedorenko
2025-12-22 21:37 ` Marc Sune
2025-12-22 22:08 ` Vadim Fedorenko
2025-12-21 21:19 ` [PATCH RFC net 2/5] selftests/net: add no ARP bcast/null poison test Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 3/5] neigh: discard lladdr bcast/null (bcast poison) Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 4/5] selftests/net: add no NDP bcast/null poison test Marc Suñé
2025-12-21 21:19 ` [PATCH RFC net 5/5] selftests/net: use scapy for no_bcastnull_poison Marc Suñé
2025-12-22 9:34 ` Vadim Fedorenko
2025-12-22 21:46 ` Marc Sune
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).