Ping worked. The neighbor entry looked normal. Two machines were still answering for the same IP.
That is the useful starting point for this lab: a successful ping and one row in ip neigh do not tell you how many devices answered an ARP request. We will deliberately assign one IPv4 address to two hosts, capture their replies, and remove the extra address. Then we will build a routed proxy-ARP case that produces a different diagnosis.
The proxy test adds a trap. On the Linux kernel tested here, arping -D returned success without a reply for an address that ordinary ARP and ping could reach. Its zero-source-address probe took a different path through the ARP code.
If namespaces and veth links are new to you, start with the Linux namespace lab. The existing Linux ARP and neighbor guide covers the cache states; this walkthrough tests what those states cannot establish on their own.
Build one broadcast domain and a separate backend
Use a Linux host with sudo, iproute2, tcpdump, and iputils arping. Other programs also use the name arping, so check arping -V before copying these flags. Tested with: Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, iputils 20250605, iproute2 6.19.0, tcpdump 4.99.6.
The client, owner A, and owner B connect to an unaddressed bridge in a fourth namespace. Their front interfaces have these fixed addresses:
client 192.0.2.10/24 02:00:00:91:00:10
owner A 192.0.2.20/24 02:00:00:91:00:20
owner B 192.0.2.30/24 02:00:00:91:00:30A separate veth connects A and B over 198.51.100.0/30. It is unused in the first test and carries routed traffic in the proxy test. There is no default route or external uplink. Run this setup once on a host where the four plarp- namespaces do not already exist:
for ns in plarp-client plarp-owner-a plarp-owner-b plarp-switch; do
sudo ip netns add "$ns"
sudo ip -n "$ns" link set lo up
done
sudo ip -n plarp-switch link add br0 type bridge
sudo ip -n plarp-switch link set br0 up
for item in 'c plarp-client 10' 'a plarp-owner-a 20' 'b plarp-owner-b 30'; do
read -r short ns suffix <<< "$item"
sudo ip link add "pla-$short" type veth peer name "pla-$short-sw"
sudo ip link set "pla-$short" netns "$ns"
sudo ip link set "pla-$short-sw" netns plarp-switch
sudo ip -n "$ns" link set "pla-$short" name eth0
sudo ip -n "$ns" link set eth0 address "02:00:00:91:00:$suffix"
sudo ip -n "$ns" addr add "192.0.2.$suffix/24" dev eth0
sudo ip -n "$ns" link set eth0 up
sudo ip -n plarp-switch link set "pla-$short-sw" master br0
sudo ip -n plarp-switch link set "pla-$short-sw" up
done
sudo ip link add pla-back-a type veth peer name pla-back-b
sudo ip link set pla-back-a netns plarp-owner-a
sudo ip link set pla-back-b netns plarp-owner-b
sudo ip -n plarp-owner-a link set pla-back-a name back0
sudo ip -n plarp-owner-b link set pla-back-b name back0
sudo ip -n plarp-owner-a addr add 198.51.100.2/30 dev back0
sudo ip -n plarp-owner-b addr add 198.51.100.1/30 dev back0
sudo ip -n plarp-owner-a link set back0 up
sudo ip -n plarp-owner-b link set back0 up
for ns in plarp-client plarp-owner-a plarp-owner-b; do
sudo ip -n "$ns" route show
doneEach endpoint should show a connected front-network route and, for A and B, a connected backend route. The bridge forwards Ethernet frames; it does not answer for the target address.
Capture a healthy answer before creating the duplicate
Keep a capture running in a second terminal while you run each test. Use a fresh filename for each phase, wait for tcpdump to report that it is listening, and stop it with Ctrl+C after the commands finish:
sudo ip netns exec plarp-client tcpdump --immediate-mode -U -nn -i eth0 -s 0 -w /tmp/plarp-direct.pcap 'arp or icmp'The immediate mode matters for these short captures. Check tcpdump's final captured-packet and kernel-drop counts before treating the file as the complete experiment. Our 19 saved case captures decoded to their reported captured counts and had zero kernel drops.
In the first terminal, clear only the lab target's mapping and send broadcast ARP requests:
sudo ip -n plarp-client neigh flush to 192.0.2.20 dev eth0
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.20
sudo ip -n plarp-client neigh show to 192.0.2.20 dev eth0
sudo ip netns exec plarp-client ping -c 3 -W 1 192.0.2.20The healthy capture contained three ARP requests and three replies, all from A's MAC, 02:00:00:91:00:20. Ping received three replies. The neighbor entry pointed to A.
-b keeps requests broadcast. Without it, iputils can switch to unicast after an answer, making this a less useful test for additional responders. With both a count and a deadline, the reply count can end the run before that many requests have been sent. Here -w 4 also bounds how long an unanswered command waits. See the iputils arping documentation for those option interactions.
Put the same IP on two hosts
Add the target as a secondary address on B. Keep its original .30 address:
sudo ip -n plarp-owner-b addr add 192.0.2.20/24 dev eth0Capture this phase separately, then repeat the broadcast arping command:
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.20
sudo ip -n plarp-client neigh show to 192.0.2.20 dev eth0
sudo ip netns exec plarp-client ping -c 3 -W 1 192.0.2.20The actual arping output included replies from both front-interface MACs:
ARPING 192.0.2.20 from 192.0.2.10 eth0
Unicast reply from 192.0.2.20 [02:00:00:91:00:30] 0.616ms
Unicast reply from 192.0.2.20 [02:00:00:91:00:20] 0.698ms
Unicast reply from 192.0.2.20 [02:00:00:91:00:30] 0.612ms
Sent 2 probes (2 broadcast(s))
Received 3 response(s)The packet capture contained two requests and four replies, two from each MAC. The tool exited after counting its third reply; tcpdump was still listening for the fourth. A tool's printed total and a capture taken around that tool need not stop at the same packet.
Read the saved file to see the Ethernet and ARP fields:
sudo tcpdump -nn -e -r /tmp/plarp-duplicate.pcap arpLook for two different sender hardware addresses claiming 192.0.2.20 in ARP replies. Also check the addresses actually configured on both hosts:
sudo ip -n plarp-owner-a -br addr show dev eth0
sudo ip -n plarp-owner-b -br addr show dev eth0In this controlled topology, that combination establishes the duplicate: both endpoints own .20 and both answer for it. In a real network, two responding MACs are a reason to investigate ownership and proxying before removing an address.
The client still had one REACHABLE entry pointing to A. Its three pings also succeeded. This run did not produce a flapping entry or a ping failure, and neither symptom is required to demonstrate the configuration error. The cache displayed its current mapping, not a history of every reply.
Read duplicate-detection exit status, then repair the address
Try the iputils duplicate-address-detection mode while the conflict is present:
sudo ip netns exec plarp-client arping -D -I eth0 -c 3 -w 4 192.0.2.20
printf 'exit=%s\n' "$?"The capture showed a probe whose sender protocol address was 0.0.0.0, followed by replies from both A and B. The tool printed only the first answer and returned 1. Against the healthy, single-owner baseline, the same mode also returned 1. A reply says that the probe found a responder; that exit status does not count how many owners exist.
Remove only the secondary address we added to B, then clear the client's mapping for this target:
sudo ip -n plarp-owner-b addr del 192.0.2.20/24 dev eth0
sudo ip -n plarp-client neigh flush to 192.0.2.20 dev eth0Repeat broadcast arping and ping. The repaired ARP capture contained three requests and three replies from A alone; ping again received three replies. B's .30 address remained configured. This is the repair evidence: the extra configured address is gone and its MAC has stopped answering for .20.
For comparison, query an unassigned address in this isolated /24:
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.99
printf 'ordinary exit=%s\n' "$?"
sudo ip netns exec plarp-client arping -D -I eth0 -c 3 -w 4 192.0.2.99
printf 'DAD exit=%s\n' "$?"Both captures contained three requests and no replies. Ordinary mode returned 1; -D returned 0. In this implementation, DAD success means no reply was received during the test.
Move the target behind a proxy
Start this phase from the repaired topology. Move .20 onto A's backend, take A's front interface down, and give B a route to it. B forwards IPv4 and answers proxy ARP on its client-facing interface:
sudo ip -n plarp-owner-a addr del 192.0.2.20/24 dev eth0
sudo ip -n plarp-owner-a link set eth0 down
sudo ip -n plarp-owner-a addr add 192.0.2.20/32 dev back0
sudo ip -n plarp-owner-a route add 192.0.2.10/32 via 198.51.100.1 dev back0
sudo ip -n plarp-owner-b route add 192.0.2.20/32 via 198.51.100.2 dev back0
sudo ip netns exec plarp-owner-b sysctl -w net.ipv4.ip_forward=1
sudo ip netns exec plarp-owner-b sysctl -w net.ipv4.conf.eth0.proxy_arp=1
sudo ip -n plarp-client neigh flush to 192.0.2.20 dev eth0A's front interface is down so it cannot answer on the shared Ethernet segment for an address on its backend. That distinction matters on Linux, where the default ARP policy can answer for a local address configured on another interface. The kernel IP sysctl documentation describes that policy and the proxy-ARP controls.
Check ownership and routing on B before querying:
sudo ip -n plarp-owner-b -br addr
sudo ip -n plarp-owner-b route get 192.0.2.20
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.20
sudo ip netns exec plarp-client ping -c 3 -W 1 192.0.2.20B has .30 and 198.51.100.1, but no local .20 address. Its route lookup uses 198.51.100.2 through back0. Ordinary ARP received three replies advertising .20 at B's front MAC, 02:00:00:91:00:30. The three ping replies had TTL 63; the direct baseline replies had TTL 64. In this unchanged endpoint setup, that decrement is consistent with the extra routed hop.
The reply tells the client which Ethernet destination to use for .20. It does not prove that B owns .20 locally. The target now lives on A's backend, and B forwards traffic to it.
Repeat the DAD command against .20. This time it sent three zero-source-address probes, received no answers, and returned 0—even though ordinary ARP and ping worked.
The upstream Linux 7.0 ARP implementation handles a request with sender IP zero in a special branch: it checks for a local target, then exits that path before the later forwarding/proxy logic. B's target is routed rather than local. That behavior matches this capture.
Separate proxy configuration from an already cached mapping
After the working proxy ping, disable proxy ARP on B and immediately ping again:
sudo ip netns exec plarp-owner-b sysctl -w net.ipv4.conf.eth0.proxy_arp=0
sudo ip netns exec plarp-client ping -c 3 -W 1 192.0.2.20
sudo ip -n plarp-client neigh show to 192.0.2.20 dev eth0The warm-cache ping still received all three replies at TTL 63. Its capture had no ARP exchange. The client retained B's MAC and B still forwarded IPv4, so changing the ARP response policy did not interrupt that existing mapping.
Now clear only .20 and test address resolution again:
sudo ip -n plarp-client neigh flush to 192.0.2.20 dev eth0
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.20
printf 'exit=%s\n' "$?"With proxy ARP off, this capture had three requests and no replies; arping returned 1. Restore the interface setting and repeat the ordinary ARP and ping commands:
sudo ip netns exec plarp-owner-b sysctl -w net.ipv4.conf.eth0.proxy_arp=1
sudo ip netns exec plarp-client arping -I eth0 -c 3 -w 4 -b 192.0.2.20
sudo ip netns exec plarp-client ping -c 3 -W 1 192.0.2.20The recovered run again received three ARP replies from B and three ping replies at TTL 63. These captures separate the response setting, the client's cached mapping, and the forwarding route instead of treating one successful ping as proof of all three.
When investigating an unexpected MAC for an IPv4 address, start with broadcast replies, then inspect configured ownership and the route behind the responder. Clear a scoped cache entry when testing a changed ARP policy. Keep the DAD probe's zero source address and exit semantics in view.
If the ARP request never reaches the expected segment, continue with the Linux–Cisco VLAN trunk mismatch lab. If resolution works but a routed TCP flow disappears, the rp_filter lab follows a different failure at the IP layer.