Networking · IP and Routing

How a Router Decides a Destination Is On-Link

The difference between a route that names a neighbour and one that names a gateway, how the link-layer address is resolved, and what happens when resolution fails.

A forwarding table entry does not necessarily name the machine that will receive the packet. It names either a gateway, meaning an IP address that must be reached before the destination is, or an interface plus a flag saying the destination is directly reachable on that interface. Turning either form into an Ethernet frame requires a second lookup, over a different table, keyed on a different kind of address. Getting the boundary between these two lookups wrong is the source of a large share of routing misconfigurations.

Two kinds of next hop

A route of the form “192.0.2.0/24 via 198.51.100.1” says that packets for that prefix should be sent to a router at 198.51.100.1. The packet’s IP destination remains the original destination; only the link-layer destination changes to the gateway. The gateway must itself be reachable, which means the forwarding table must contain a route for 198.51.100.1 that resolves to a directly attached network. If it does not, the route is unusable, and a router that installs it anyway will fail at the moment a packet arrives.

A route of the form “192.0.2.0/24 dev eth0” with no gateway says the destination is on the link attached to eth0. The sender resolves the destination address directly to a link-layer address and sends the frame to that machine. No intermediate IP hop is involved. The distinction matters because it changes whose link-layer address appears in the frame, and therefore which device processes it.

The flag that marks a route as directly attached is separate from the prefix length. A /24 reachable through a gateway and a /24 reachable directly have the same prefix but different next-hop types. Implementations represent this with a flag on the route, and the flag is set by the code that creates the route when an interface is configured with an address and mask, not by the routing protocol.

When an interface is assigned 192.0.2.5/24, the stack computes the prefix 192.0.2.0/24 and installs a route for it pointing at that interface with the on-link flag. The computation is a mask AND: the address ANDed with the mask gives the network, and the prefix length comes from the mask. RFC 1122 states the same test as a requirement for hosts, comparing the bits the mask extracts from the destination against the bits it extracts from the interface’s own address. Nothing else about the interface affects the result, so a second address on the same interface with a different mask creates a second on-link prefix, and both are installed.

Two addresses on the same link with different prefix lengths create an asymmetry that is legal and confusing. If host A has 192.0.2.1/24 and host B has 192.0.2.130/25, then A considers B on-link, because its mask covers the whole of 192.0.2.0, and resolves B’s link-layer address directly. B considers only 192.0.2.128 through 192.0.2.255 local, so it treats A as off-link and sends traffic for A to its configured gateway. Whether the packet arrives depends on whether that gateway routes back to the link. Nothing is broken in either stack; the two hosts simply disagree about the extent of the subnet, and the disagreement is silent until traffic fails.

The same arithmetic explains a common failure with point-to-point links. A /31 prefix has two addresses and no spare bits, so the older convention of reserving the all-zeros network address and the all-ones broadcast address would leave no usable address at all. RFC 3021 permits both addresses to be assigned as host addresses on point-to-point links, which makes the prefix useful for exactly the two endpoints a point-to-point link has. A stack that applies the broadcast convention to a /31 computes an on-link set with no usable members, and a stack that applies RFC 3021 computes two, so the same configuration can work on one end of a link and fail on the other.

IPv4 uses ARP as specified in RFC 826. The sender broadcasts a request containing the target IP address; the owner replies with its hardware address, usually unicast back to the requester. The reply is cached in a neighbour table with an expiry, because a machine can change its address or be replaced, and a stale entry would send frames to the wrong port.

The cache is the part that matters for forwarding. A router with a full table of resolved neighbours can forward without any resolution step. A router that must resolve for every packet would stall on the first packet of each flow, and the stall is not short: the request must propagate, the reply must come back, and during that time the packet is either queued or dropped. Implementations therefore keep a queue per unresolved neighbour and hold a bounded number of packets in it, dropping the oldest when it fills. A busy router under a neighbour-table exhaustion attack loses traffic in exactly this way.

The cache also creates a state problem. Entries expire on a timer, and the timer is not synchronised with the actual liveness of the neighbour. An entry that expires while traffic is flowing causes a brief resolution, which is harmless. An entry that does not expire while the neighbour has gone away causes frames to be sent to a MAC address that no longer exists, and the switch discards them. Positive feedback is absent: nothing tells the sender its cache is wrong until the entry expires or the transport layer gives up.

/* Sketch of the decision a router makes per packet. */
struct route *r = fib_lookup(dst);
if (!r)
    return DROP_NO_ROUTE;
if (r->blackhole)
    return DROP_POLICY;
if (r->onlink)                             /* no gateway: resolve the destination */
    ll = neigh_resolve_output(r->ifp, dst);
else                                       /* resolve the gateway instead */
    ll = neigh_resolve_output(r->ifp, r->gateway);
if (!ll)
    return neigh_queue(r, pkt);             /* bounded queue, may drop */
return dev_queue_xmit(r->ifp, ll, pkt);

The two calls to the same resolution function with different keys is the entire distinction between on-link and gatewayed forwarding. Everything else in the path is identical. The output interface’s MTU, though, belongs to the link rather than to the route, so a packet that resolves correctly can still be too large for the link it is about to traverse, which is the subject of fragmentation and path MTU discovery.

IPv6 replaces ARP with a multicast protocol

IPv6 has no broadcast, so neighbour resolution cannot use a broadcast request. RFC 4861 defines Neighbor Discovery, which sends a solicitation to a solicited-node multicast address derived from the target’s address by taking the low twenty-four bits and appending a fixed prefix. Only hosts whose address matches that pattern join the group, so the solicitation reaches a small set of listeners rather than the whole link.

The derived group also narrows who must process a solicitation in a way ARP cannot. A host joins the solicited-node group for each address it holds, so a solicitation computed from that address reaches it without any registry of which hosts are on the link, and hosts holding unrelated addresses never see the frame. The advertisement is then sent unicast back to the solicitor’s link-local address, so it is not multicast at all; the one exception is a solicitation sent from the unspecified address, as duplicate address detection does, which draws a reply to the all-nodes group because the sender has no unicast address yet. The protocol carries additional state: routers advertise their presence with Router Advertisement messages, and hosts build a default route from those advertisements rather than from static configuration. That is a larger change than a resolution protocol, and it is covered in IPv6 forwarding and scoped addresses.

The neighbour cache in IPv6 is explicitly a state machine with reachable, stale, delay, and probe states, rather than a simple timed entry. The reachable state means recent confirmation; stale means the entry may still be correct but has not been confirmed; delay and probe are the transition back to confirmation. The design intent is to avoid resolving on every packet while still detecting a dead neighbour promptly, and it produces behaviour that looks like a periodic probe of active neighbours rather than a timeout.

Recursive next hops and the resolution chain

A route can name a gateway that is itself reached through another route. The forwarding table may hold a default route via 198.51.100.1 and a route for 198.51.100.0/24 on eth0, in which case resolving the default requires first resolving the /24. Implementations either resolve recursively at lookup time, walking the chain until they reach an on-link route, or precompute the resolved gateway when the route is installed.

Recursion is the more flexible choice and the more dangerous one. A cycle in the chain, where route A’s gateway is reached through route B and B’s through A, makes the resolution loop. Implementations cap the recursion depth and reject routes that exceed it, which is why a route whose gateway is unreachable is usually rejected at installation rather than at the first packet. The check is a graph reachability test, and doing it at installation is what keeps the packet path free of it.

Precomputation has the opposite trade. A route stores a resolved gateway and interface, so lookup is one step. But a change to the route that resolves the gateway invalidates every route that depended on it, and the invalidation must propagate. That is a dependency graph in the control plane, and it is why some stacks prefer recursion with a depth limit even though it costs work per lookup.

When resolution fails and what the sender learns

An ARP request that gets no reply is not an error in itself; the stack retries a few times and then gives up on that attempt. IPv4 has no ICMP message for “the neighbour did not answer”, so the sender’s only signal is the absence of progress, which the transport layer eventually reports as a timeout. The router that could not resolve may generate ICMP Host Unreachable, but that message is about the destination being unreachable from the router, not about the specific failure.

This is the practical difference between a routing failure and a resolution failure. A missing route produces an immediate ICMP Network Unreachable from the first router, with a code that says so. A failed resolution produces a delay followed by silence, and the two look identical to an application that only sees its connection time out. Distinguishing them requires looking at the router’s neighbour table and its error counters, which is why the tools for the two cases are different.

The asymmetry has a security dimension. Because the resolution protocol is unauthenticated in IPv4, any host on the link can answer for any address, and the sender has no way to check the claim. That is the basis of link-layer address spoofing, and the mitigations are switch features rather than protocol features, which places them outside the forwarding path described here.

References

  1. RFC 826: An Ethernet Address Resolution Protocol — IETF
  2. RFC 4861: Neighbor Discovery for IP version 6 (IPv6) — IETF
  3. RFC 1122: Requirements for Internet Hosts — Communication Layers — IETF
  4. RFC 3021: Using 31-Bit Prefixes on IPv4 Point-to-Point Links — IETF