7.2 Interacting with the Neighbor Subsystem from Userspace
In the previous section, we discussed the internal "housekeeping" of the neighbor subsystem: how memory is allocated, how reference counts are managed, and when entries are destroyed. If you are a kernel developer, these are your bricks and mortar.
But if you are a system administrator, or a developer debugging network issues, you don't directly interact with these structures. You hold the iproute2 toolbox, and you type ip neigh. Between you and the kernel lies an API layer. This section is about that API layer—how we peek into, modify, or even forcefully "instruct" the kernel's neighbor table from userspace.
Command-Line Tools: From net-tools to iproute2
There are two main ways to manage the ARP table (or rather, the neighbor table), representing two different eras:
- The old-school approach: using the
arpcommand from thenet-toolspackage. - The modern approach: using the
ip neigh(orip neighbour) command from theiproute2package.
Although they do the same thing, the underlying paths they take are completely different.
If you run the classic arp command:
$ arp
This request is ultimately handled by the arp_seq_show() method in the kernel, with the code located in net/ipv4/arp.c. Note that this is IPv4-specific—it can only see the ARP table.
If you run the modern ip neigh show:
$ ip neigh show
This time, it calls the generic neigh_dump_info() method, located in net/core/neighbour.c.
The biggest difference in output between the two is the amount of information. The arp command gives a brief list, whereas ip neigh show dumps the NUD state (Neighbour Unreachability Detection state) of the neighbor entries all at once—such as NUD_REACHABLE (reachable), NUD_STALE (stale), or NUD_DELAY (delay). These states are essentially the "thermometers" of the neighbor subsystem, and we will focus on them in the coming chapters.
Additionally, the ip command is dual-mode. With the -6 parameter:
$ ip -6 neigh show
It displays the IPv6 neighbor table (NDISC table). This is something the arp command cannot do.
Besides command-line tools, the kernel also exposes this data through procfs. If you are an old-school administrator accustomed to cat files, you can still operate this way:
- View the ARP table:
cat /proc/net/arp. Behind the scenes, this actually callsarp_seq_show(), which is essentially no different from typing thearpcommand. - View statistics:
cat /proc/net/stat/arp_cache(ARP statistics) orcat /proc/net/stat/ndisc_cache(NDISC statistics). Both files are handled uniformly by theneigh_stat_seq_show()method.
Manual Additions and Deletions: Laying Down the Law for the Kernel
Normally, the kernel dynamically learns the MAC addresses of its neighbors (passive learning). But sometimes, we want to forcefully tell the kernel something—for example, "192.168.0.121 is always at 00:30:48:5b:cc:45," regardless of whether it actually is. This is a static neighbor entry.
You can use ip neigh add to do this:
$ ip neigh add 192.168.0.121 dev eth0 lladdr 00:30:48:5b:cc:45 nud permanent
This command triggers the neigh_add() method. Note the final nud permanent; the NUD_PERMANENT state we mentioned in the previous section comes into play right here. This state tells the kernel: "Don't delete, don't modify, don't expire this entry—it is absolute."
To delete it, use ip neigh del, which calls the neigh_delete() method behind the scenes:
$ ip neigh del 192.168.0.121 dev eth0
Another very useful scenario is Proxy ARP.
Let's briefly recall this concept: Host A asks "Who has IP B?", and Host C (a router), even though it isn't IP B, can answer on B's behalf saying "I do, the MAC is xxx." It then receives the traffic and forwards it to B. This is useful in certain special network topologies.
To configure a Proxy ARP entry in the kernel, use the proxy keyword:
$ ip neigh add proxy 192.168.2.11 dev eth0
Although the command looks similar, the internal path taken by the kernel is completely different. The neigh_add() method will notice that the user-provided data carries a NTF_PROXY flag (stored in the ndm_flags field of the ndm object). Once it sees this flag, the kernel won't check the regular neighbor table; instead, it calls the pneigh_lookup() method to look up the proxy neighbor hash table (phash_buckets). If it finds nothing, it creates a new entry in this table.
Deleting a proxy entry follows the same logic:
$ ip neigh del proxy 192.168.2.11 dev eth0
After detecting the NTF_PROXY flag, the neigh_delete() method calls pneigh_delete() to clean up the proxy table.
Besides manipulating entries, you can also manipulate the parameters of the entire table. The ip ntable command is designed for this:
-
ip ntable show: Displays the parameter configuration of all neighbor tables. -
ip ntable change: Dynamically modifies parameters. For example, changing the ARP cache queue length on theeth0interface to 20:$ ip ntable change name arp_cache queue 20 dev eth0This calls the
neightbl_set()method. This is highly useful for tuning scenarios with massive numbers of neighbors (such as data center gateways).
Finally, a word on the old relic arp command. You can also use it to add static entries:
$ arp -s <IPAddress> <MacAddress>
The effect is similar to ip neigh add ... nud permanent, but don't expect static entries to survive a reboot—they only live in kernel memory and are lost once the power is off. If you want persistence, you need to add them to your startup scripts.
Network Event Callbacks: The Kernel's Ears
Having covered how userspace knocks on the door, let's look at how the kernel listens to "broadcasts" internally.
Here, "broadcasts" refer to network event notifications.
Interestingly, the core neighbor subsystem itself does not register for network event notifications. It is a quiet core that doesn't directly care about network interfaces being plugged in or MAC addresses changing. The ones actually worrying about these things are the specific protocol modules—ARP and NDISC.
In the IPv4 ARP module, arp_netdev_event() is registered as the network event callback function. It mainly watches for two types of events:
- MAC address changes: For example, if you run
ifconfig eth0 hw ether ...on a network interface. At this point, the kernel triggers an event, andarp_netdev_event()calls the genericneigh_changeaddr()method to flush the relevant neighbor entries, while also callingrt_cache_flush()to clear the routing cache. Because the L2 address changed, the previous caches might all be invalid. - Flag changes (starting from kernel 3.11): If the
IFF_NOARPflag of a network interface changes, it also triggers aNETDEV_CHANGEevent. Similarly,neigh_changeaddr()steps in to handle it.
In the IPv6 NDISC module, ndisc_netdev_event() takes on a similar responsibility, but it watches for a more diverse set of signals: NETDEV_CHANGEADDR (address change), NETDEV_DOWN (interface shutdown), and NETDEV_NOTIFY_PEERS (notify neighbors, typically used in NIC failover scenarios).
Chapter Echoes
In this section, we stood on the boundary between userspace and kernel space.
We saw how the same data—the neighbor table—is viewed from different perspectives. For iproute2, it is a set of objects that can be shown, added, or deleted; for the kernel, it is a set of hash tables, reference counts, and state machines.
More importantly, we introduced the concepts of static configuration and dynamic events. Although manually adding entries seems convenient, the real network world is fluid—MAC addresses change, network interfaces go down, and traffic migrates.
(Leading into the next section) Now, we have the containers and the means to manage them. But the containers are still empty. In the next section, we will shift our focus to the IPv4 world and see how the ARP protocol moves in as the first resident into this ready-made house—especially that most core question: when the kernel realizes "I don't know who the neighbor is," what exact signal does it send out?