Skip to content

How Linux routes packets

Every IP packet a Linux system sends or forwards needs an answer to one question: where does this go next? The kernel answers it for each packet with a routing decision: a lookup that picks an outgoing interface and, if the destination isn't directly reachable, a gateway to hand the packet to. Whether the system is a laptop with one network connection or a router with dozens, the mechanism is the same.

This page explains how that decision is made, why the rules are what they are, and how the parts you configure (addresses, routes, rules) and the parts the kernel manages for you (connected routes, neighbour entries) fit together. For the commands themselves, see the reference page linked at the end.

A route is a rule for a range of addresses

A routing table is a list of routes. Each route describes a range of destination addresses, written as a prefix such as 203.0.113.0/24, and says what to do with packets for that range. The number after the slash is the prefix length: how many leading bits of an address must match. /24 fixes the first 24 bits, so the route covers 256 addresses; /32 covers exactly one; /0 covers every address.

What a route says to do comes in two main forms:

  • Send it out of an interface directly (a link route): the destination is on a network the system is attached to, so the packet goes straight to it.
  • Send it to a gateway (a route via a next hop): the destination is somewhere else, and a router on an attached network knows the way.
192.0.2.0/24 dev enp0s4 proto kernel scope link src 192.0.2.10   (directly reachable)
203.0.113.0/24 via 192.0.2.1 dev enp0s4                           (through a gateway)
default via 198.51.100.1 dev enp0s3                               (everything else)

A gateway must itself be directly reachable, so a gateway route always depends on a link route for the gateway's network. This is why the kernel refuses a route through a gateway that isn't on any attached network: it would have no way to deliver the packet to the gateway.

Connected routes appear by themselves

When you give an interface an address with a prefix, such as 192.0.2.10/24, you're stating two facts: this is my address, and the whole 192.0.2.0/24 network is on the other end of this link. The kernel turns the second fact into a route automatically, marked proto kernel and scope link. These connected routes come and go with the address and the interface's administrative state: take the address away or set the interface down, and the route disappears with it. If the interface only loses its carrier (a cable is unplugged), an IPv4 connected route stays in the table, flagged linkdown, and is used again as soon as the link returns.

The kernel also adds routes for the system's own addresses to a separate table, the local table, so that packets addressed to the system itself are recognised and delivered locally. You rarely look at that table, but it's consulted first, which is why a packet for one of your own addresses never leaves the machine, whichever interface the address belongs to.

Longest-prefix match

Routes overlap. In the example above, 203.0.113.25 matches both 203.0.113.0/24 and the default route, which covers every address. The kernel resolves this with longest-prefix match: of all routes whose prefix contains the destination, the one with the longest prefix wins, because it's the most specific statement about that address.

The rule has a useful consequence: you can add a specific exception without touching the general case. A /24 route through a second router takes precedence over the default route for those 256 addresses, while everything else still follows the default. A /32 host route takes precedence over both.

The order in which routes were added, or appear in the output of ip route, plays no part. The IPv4 table is stored in a structure (a level-compressed trie, fib_trie) designed to find the longest matching prefix quickly even with hundreds of thousands of routes, as on an internet router.

The default route

The default route is the route for 0.0.0.0/0 (and ::/0 for IPv6). Because its prefix length is zero, it matches every destination and loses to every other route, which makes it the route of last resort. On most hosts it points to the local router, often learned from DHCP or IPv6 router advertisements.

A host without a default route can reach only its attached networks and whatever specific routes you configure. That's sometimes intended: a management or storage interface often has an address and a few static routes but no default route, so that general traffic keeps using the main connection.

Metrics break ties

Two routes can have exactly the same prefix, for example two default routes when a host is connected by both cable and Wi-Fi. Longest-prefix match can't choose between them, so the kernel uses the route's metric (also called priority): the lower metric wins. Network managers set metrics deliberately for this reason. NetworkManager gives each kind of connection a default metric (100 for Ethernet and 600 for Wi-Fi, for example), so that a wired default route is preferred over a wireless one, and the other remains as a fallback that takes over if the preferred interface goes down. systemd-networkd doesn't distinguish link types: its DHCP and static routes get the same metric (1024) unless the configuration sets one, which netplan does for the DHCP routes it configures (100 on the Debian 13 cloud image). When two links are equally preferred by default, it's up to the administrator to set metrics.

Metrics are only a tie-breaker between routes with the same prefix. A more specific route always wins over a less specific one, whatever their metrics.

The source address

Choosing a route also helps decide which source address a new connection uses. A system with several addresses normally uses an address on the outgoing interface, and a route can name a preferred source explicitly (src), as connected routes do. This matters for replies: the remote system answers the source address it saw, and that reply needs a route back.

Looking up the next hop: ARP and NDP

The routing decision ends with an interface and a next-hop IP address: either the gateway, or the destination itself for a directly reachable host. But an Ethernet frame is addressed by hardware (MAC) address, not IP address. The last step is to translate the next hop's IP address into a link-layer address:

  • IPv4 uses the Address Resolution Protocol (ARP): a broadcast asking "who has 192.0.2.1?", answered by the owner of that address.
  • IPv6 uses Neighbor Discovery (NDP), which does the same job with ICMPv6 messages sent to multicast addresses rather than a broadcast.

The answers are kept in the neighbour table (ip neigh), with states such as REACHABLE, STALE and FAILED, so the question isn't asked for every packet. Note what is resolved: for a gateway route, the system looks up the gateway's hardware address, never the final destination's. The IP header still carries the final destination, but the frame is addressed to the router.

This separation explains a common failure pattern. A route can be perfectly correct while traffic still fails, because the next hop doesn't answer ARP or NDP: the neighbour entry becomes FAILED, and the sender reports that the host is unreachable. Routing chose where to send the packet; neighbour discovery couldn't find anyone there.

Policy routing and multiple tables

Everything so far describes one routing table, the main table, which is what ip route shows by default. Linux actually has many tables, identified by numbers (up to 2³² − 1) and optional names in /etc/iproute2/rt_tables or a related file. A set of routing policy rules (ip rule) decides which tables to consult, and in which order.

Out of the box there are three rules:

Priority Rule Effect
0 lookup local Your own addresses and broadcast addresses
32766 lookup main The ordinary routing table
32767 lookup default A normally empty table, a last resort

The kernel walks the rules in priority order. For each rule that matches the packet, it does a longest-prefix lookup in that rule's table; the first table that yields a route ends the search. Rules can match on more than the destination: the source address, the incoming interface, a firewall mark set by netfilter, the type of service, a UID range for locally generated traffic, and more.

That's what makes policy routing possible: routing based on something other than the destination alone. Typical uses are:

  • Multi-homed hosts: a server with addresses from two providers sends replies from each address out through that provider's gateway, using one table per provider and a rule per source address.
  • VPNs and tunnels: WireGuard tools and similar software use a separate table and rules to send most traffic through the tunnel, while traffic to the tunnel endpoint itself keeps the normal route.
  • Traffic classes: packets marked by a firewall rule (for example by port) leave through a different provider.

Policy routing adds power and complexity in equal measure. With several tables, ip route no longer shows the whole picture; ip route get with the relevant source and marks is the reliable way to see which route a packet will take.

Route types beyond "forward this"

Not every route sends packets somewhere. A route can also have a type that makes the kernel discard the packet: blackhole drops it silently, unreachable and prohibit drop it and report an ICMP error to the sender. These are used to make sure traffic for a range never leaks out through the default route, for example for address space a site owns but hasn't assigned yet. The local and broadcast types in the local table are the kernel's own bookkeeping for delivery to the system itself.

Hosts and routers: forwarding

The routing decision happens for packets the system sends and for packets it receives. For a received packet, the decision tells the kernel whether the packet is for this system (deliver it locally) or for somewhere else. In the second case, the system forwards it only if forwarding is enabled (the net.ipv4.ip_forward and net.ipv6.conf.all.forwarding kernel parameters). With forwarding off, the default on most distributions, a Linux system is a host; with it on, the same routing tables make it a router.

A related safeguard is reverse path filtering (rp_filter): when enabled, the kernel checks whether a packet's source address would be routed back out of the interface it arrived on, and drops it otherwise. It protects against spoofed addresses, but on multi-homed systems with asymmetric routes it can drop legitimate traffic, which is why some setups use its "loose" mode.

Where netfilter fits

Packet filtering and NAT (netfilter, configured with nftables or iptables) hook into the packet's path at fixed points, and those points are defined relative to the routing decision:

flowchart LR
    IN([packet arrives]) --> PRE[prerouting]
    PRE --> R1{routing decision}
    R1 -- for this host --> INPUT[input] --> LOCAL([local process])
    R1 -- for another host --> FWD[forward] --> POST[postrouting]
    LOCAL2([local process sends]) --> R2{routing decision} --> OUT[output] --> POST
    POST --> OUTIF([packet leaves])
  • prerouting runs before the routing decision for incoming packets. That's why destination NAT (for example port forwarding) belongs there: changing the destination address first lets routing choose the right path for the new address.
  • input and forward run after routing has decided whether the packet is for this host or passing through, so filter rules there already know which case they're handling.
  • output sees locally generated packets after their initial routing decision; if a rule there changes the destination or the packet's mark, the kernel routes the packet again.
  • postrouting runs after routing has picked the outgoing interface, so source NAT (masquerading) belongs there: the new source address can depend on the interface the packet will leave by.

Firewall marks set in prerouting or output are also one of the things policy rules can match, which is how a firewall rule can steer traffic to a different routing table.

Design trade-offs

Linux separates routing into small, independent mechanisms: tables of prefixes, rules that pick tables, a neighbour cache, and hooks around the decision. Each piece is simple, and the combination covers everything from a single-homed laptop to a multi-table router.

The cost is that the effective behaviour is spread across several places, and some of it is created implicitly (connected routes, local table entries, default rules). Older tools such as route and netstat from net-tools predate most of this and show only part of the picture, for example only the main table. The ip command from iproute2 was built to expose the full model, which is why current documentation and distribution tooling use it.

Network managers (NetworkManager, systemd-networkd, netplan, netifrc) don't route packets themselves. They are configuration layers: they read persistent configuration and program the same kernel tables you can change by hand with ip. That's also why a change made only with ip is temporary: the kernel keeps it until a reboot, or until a network manager reapplies its own configuration to that interface.

The kernel's IPv4 routing also changed internally over time. Until Linux 3.6 it kept a cache of recent routing decisions per destination; that cache was removed because it could be flooded by traffic to many different addresses, and lookups in the trie became fast enough on their own.

Alternatives

The routing model described here is the kernel's, so it's the same whichever tools configure it; the alternatives are in how you look at and change it.

  • net-tools versus iproute2. The older route, ifconfig, arp and netstat commands still exist on many systems (often as an optional package), but they use older kernel interfaces and show only part of the model: the main table, one IPv4 address per interface label, and no policy rules. The ip and ss commands from iproute2 use netlink and show everything, which is why distributions and this knowledge base use them.
  • Network managers. NetworkManager, systemd-networkd (alone or behind netplan), ifupdown on Debian and netifrc on Gentoo all store persistent configuration and apply it to the same kernel tables. They differ in scope (NetworkManager also handles Wi-Fi, VPNs and roaming between networks; systemd-networkd and ifupdown aim at static server setups) and in how they react to change, but none of them changes how a packet is routed once the tables are set.
  • Routing daemons. On routers, dynamic routing protocols such as OSPF and BGP (implemented by daemons such as FRRouting or BIRD) replace hand-written static routes: the daemon learns routes from neighbours and installs them in the kernel tables, where the lookup works exactly as described above.

Further reading