Subject: [PATCH RFC net 0/5] net: discard ARP/NDP bcast/null announce (poison)

From: Marc Suñé <[email-masked]>
Date: Sun, 21 Dec 2025 22:19:33 +0100

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