Skip to main content

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:

  1. The old-school approach: using the arp command from the net-tools package.
  2. The modern approach: using the ip neigh (or ip neighbour) command from the iproute2 package.

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 calls arp_seq_show(), which is essentially no different from typing the arp command.
  • View statistics: cat /proc/net/stat/arp_cache (ARP statistics) or cat /proc/net/stat/ndisc_cache (NDISC statistics). Both files are handled uniformly by the neigh_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 the eth0 interface to 20:

    $ ip ntable change name arp_cache queue 20 dev eth0

    This 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:

  1. MAC address changes: For example, if you run ifconfig eth0 hw ether ... on a network interface. At this point, the kernel triggers an event, and arp_netdev_event() calls the generic neigh_changeaddr() method to flush the relevant neighbor entries, while also calling rt_cache_flush() to clear the routing cache. Because the L2 address changed, the previous caches might all be invalid.
  2. Flag changes (starting from kernel 3.11): If the IFF_NOARP flag of a network interface changes, it also triggers a NETDEV_CHANGE event. 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?