Skip to content

IPv4 Routing

opencode-agent[bot] edited this page Nov 1, 2026 · 1 revision

IPv4 Routing

IPv4 route identity, longest-prefix matching, connected-route maintenance, and the route/ifconfig prefix syntax that fill JNode's IPv4RoutingTable.

Overview

JNode's IPv4 routing is split across three layers:

  • IPv4RoutingTable stores IPv4Route entries in a Vector and selects the best route for a destination.
  • IPv4Route gives a route its identity (destination, subnetmask, gateway, device) and decides whether it is a host route or a network route.
  • Ifconfig and Route translate configuration into table operations, including the on-link routes that must follow an interface address.

The implementation was corrected by 67819dead (#768/#770) and 69df8bd10 (#773/#777). Before those changes a route lookup could prefer a less-specific route, the table could end up with duplicate or stale entries, a re-addressed interface kept its old connected route, and route could not parse the BSD order. The current code uses explicit longest-prefix matching, identity-based insertion/removal, connected-route add/remove around ifconfig, and a prefix-aware command argument.

The relevant regression tests live in net/src/test/org/jnode/net/ipv4/config/impl/RouteTest.java (13 tests) and net/src/test/org/jnode/net/syntax/IPv4NetworkArgumentTest.java (12 tests). The IPv4 suite reported 68 passing tests after the routing fixes.

Key Components

Class / File Role
net/src/net/org/jnode/net/ipv4/IPv4RoutingTable.java Entry storage, duplicate replacement, search() with host → LPM → default-gateway fallback
net/src/net/org/jnode/net/ipv4/IPv4Route.java Route value object, isUp/isHost/isNetwork/isGateway, equals/hashCode, use count
net/src/net/org/jnode/net/ipv4/IPv4Address.java Address arithmetic plus prefixLength() and fromPrefixLength(int)
net/src/net/org/jnode/net/ipv4/config/impl/Route.java Normalizes a target to target & netmask, resolves a device, adds to the table
net/src/net/org/jnode/net/ipv4/config/impl/Ifconfig.java Adds the connected route for a configured address and removes the old one on re-addressing
net/src/net/org/jnode/net/syntax/IPv4NetworkArgument.java Parses a.b.c.d, a.b.c.d/N, a.b.c.d/netmask, and default
cli/src/commands/org/jnode/command/net/RouteCommand.java User-facing route command
cli/src/test/org/jnode/test/cli/RouteSyntaxTest.java Loads the real descriptor and pins both argument orders plus rejections

How It Works

Route identity

IPv4Route.equals() compares destination, subnet mask, gateway and device. Flags and the use count are deliberately excluded:

destination.equals(other.destination)
&& subnetmask.equals(other.subnetmask)
&& (gateway == null) == (other.gateway == null)
&& (gateway == null || gateway.equals(other.gateway))
&& device == other.device

The device comparison is identity (==), because the Device object is the table's handle to the interface. hashCode() mirrors the same fields and uses System.identityHashCode(device).

This makes IPv4RoutingTable.remove(route) a value operation: an equal route can be removed even if it was not the exact object originally inserted.

Insertion and replacement

IPv4RoutingTable.add() removes an existing route only when destination, prefix, gateway presence/value and device all match, then appends the new entry:

for (IPv4Route r : entries.toArray(new IPv4Route[0])) {
    if (r.getDestination().equals(entry.getDestination())
        && r.getSubnetmask().equals(entry.getSubnetmask())
        && /* same gateway, null-safe */
        && r.getDevice() == entry.getDevice()) {
        entries.remove(r);
        break;
    }
}
entries.add(entry);

The subnet mask is part of the key. 10.0.0.0/8 and 10.0.0.0/16 via the same gateway and device are different routes and can coexist — which is exactly what longest-prefix matching needs. Earlier code replaced by destination alone, silently discarding the more-specific route.

Route.addRoute() normalizes the destination before insertion:

if (netmask == null) {
    netmask = target.getDefaultSubnetmask();
    subnet = target;                 // classful route keeps the typed address
} else {
    subnet = target.and(netmask);    // explicit mask is applied
}
rt.add(new IPv4Route(subnet, netmask, gateway, device));

Lookup — host, longest prefix, default

IPv4RoutingTable.search(destination) has three ordered tiers:

  1. Host routes — first isHost() && isUp() route whose destination equals the target exactly.
  2. Network routes — every isNetwork() && isUp() route whose destination.matches(target, subnetmask) succeeds. The route with the highest subnetmask.prefixLength() wins. On an equal prefix length, a gateway route beats a direct route.
  3. Default gateway — the first isGateway() && isUp() route.

NoRouteToHostException is thrown only after all three tiers fail. The whole search is wrapped in a retry loop because entries is a Vector and can be structurally modified by another thread; a ConcurrentModificationException restarts the scan instead of escaping to the caller.

Host vs. network classification

IPv4Route.isHost() handles three cases that a naive "destination has host bits set" test gets wrong:

return (subnetmask.prefixLength() == 32) ||
       (destination.isHost() && !destination.equals(destination.and(subnetmask)));
  • A /32 mask is always a host route.
  • A destination with host bits set is a host route only when masking it actually changes the address.
  • A classful network address with a zero fourth octet (for example 10.0.9.0 typed with the default /8) stays a network route even though the second octet has bits set for that mask.
  • A masked subnet (10.0.0.0/25) is already normalized, so it stays a network route.

Without the fourth-octet exception, classful class A/B routes fell into the host-route tier and disappeared from longest-prefix matching.

Connected routes and re-addressing

Ifconfig.setDefault(device, address, netmask) keeps the table consistent with the interface:

  1. If an address was already configured, compute its subnet and call removeConnectedRoute(device, previousAddress, previousNetmask).
  2. Add the new address to the device's IPv4ProtocolAddressInfo and make it the default.
  3. Call addConnectedRoute(device, address, netmask).

addConnectedRoute() computes subnet = address.and(netmask), refuses to install a route for the any-address (subnet.isAny()), and inserts new IPv4Route(subnet, netmask, null, device) — an on-link route with no gateway, always flagged up.

The matching removeConnectedRoute() skips null address/mask and removes the exact route. This closes the bug where re-addressing an interface left the old on-link subnet reachable after the address had changed.

Prefix parsing and validation

IPv4NetworkArgument accepts:

Form Result
a.b.c.d address with null mask — use the classful default
a.b.c.d/N address plus IPv4Address.fromPrefixLength(N)
a.b.c.d/dotted.mask address plus the literal mask, rejected if non-contiguous
default IPv4Address.ANY with null mask

Address octets are parsed locally with 0 <= v < 256. It does not delegate to new IPv4Address(String), whose unchecked (byte) cast silently turned 10.0.2.300 into 10.0.2.44. Bad octets and wrong component counts are syntax errors instead.

IPv4Address.prefixLength() simply popcounts set bits. That is only a meaningful prefix for a contiguous mask, so the dotted-mask path verifies round-tripping through fromPrefixLength():

if (!IPv4Address.fromPrefixLength(mask.prefixLength()).equals(mask)) {
    throw new CommandSyntaxException("non-contiguous IPv4 subnetmask '" + text + "'");
}

Without that check, 255.255.0.255 would report prefix length 24 and compete at the wrong specificity in the LPM search.

route command grammar

cli/descriptors/org.jnode.command.net.xml accepts both argument orders:

route <target> <device> [gateway]
route <target> <gateway> <device>

The second order is the BSD/route(8) form and is the one needed to install a static route through a gateway. Before the grammar used an <alternatives> of two <sequence> branches, <target> <gateway> <device> failed with Command syntax error(s): unknown device: <gateway>. The binding layer was not at fault; the descriptor simply did not describe the order.

RouteSyntaxTest loads the real plugin descriptor and pins both accepted orders and the rejected forms. cli/build-tests.xml's all-junit target was enabled in the same change.

Worked examples

Assume interface eth0 is configured as 192.168.1.10/24 with default gateway 192.168.1.1.

Action Table effect
ifconfig eth0 192.168.1.10/24 Adds connected 192.168.1.0/24 -> eth0 (no gateway)
ifconfig eth0 192.168.1.10/25 Removes 192.168.1.0/24, adds connected 192.168.1.0/25
route 10.0.0.0/8 192.168.1.1 eth0 Adds 10.0.0.0/8 -> 192.168.1.1 via eth0
route 10.1.0.0/16 192.168.1.1 eth0 Adds a second route; does not replace the /8
route 10.1.2.3/32 192.168.1.1 eth0 Adds a host route selected before any network route
route default 192.168.1.1 eth0 Adds the default gateway fallback

A packet to 10.1.2.3 then matches the /32 host route. A packet to 10.1.99.1 has no host route, matches both 10.0.0.0/8 and 10.1.0.0/16, and selects the /16 because 16 > 8. A packet to 8.8.8.8 matches no network route and falls through to the default gateway. A packet to 192.168.1.20 matches the connected /25 (or /24 before the re-addressing) directly.

The equivalent Java calls are:

rt.add(new IPv4Route(new IPv4Address("10.0.0.0"),
                     new IPv4Address("255.0.0.0"),
                     new IPv4Address("192.168.1.1"), eth0));

Route.addRoute() is preferred in command code because it normalizes 10.1.2.3/24 to 10.1.2.0/24 and resolves the device name; constructing IPv4Route directly assumes the caller has already masked the destination.

Testing and diagnostics

RouteTest covers insertion, duplicate replacement, removal by value, host/network classification, LPM ordering, gateway tie-break, default fallback and connected-route add/remove. IPv4NetworkArgumentTest covers the four target forms, octet bounds, component counts, and non-contiguous masks. RouteSyntaxTest is the black-box guard for the command descriptor; it is the test that fails first when an argument order is edited away.

When a route lookup misbehaves, check in this order:

  1. Was the destination masked? A route inserted as 10.1.2.3/24 without normalization never matches 10.1.2.0.
  2. Is the mask contiguous? A non-contiguous dotted mask is rejected by the syntax layer but can still reach the table through direct IPv4Route construction.
  3. Is the interface still the same Device instance? A replaced device object makes the old connected route unreachable by identity.
  4. Did re-addressing run? If the old subnet still matches, Ifconfig.setDefault() was bypassed.

Gotchas

  • A route is identified by the full tuple, not by destination alone. Same destination with a different mask is a different route. Do not "simplify" add() back to a destination-only lookup.
  • remove() is value-based; add() is tuple-based. An equal route can be removed even if a different instance was inserted, but replacement requires the same four fields. Keep both paths consistent when adding a new field.
  • Device identity is intentional. Two Device instances representing the same hardware do not compare equal for routing; routes are bound to the object the interface registered.
  • LPM is a linear scan. The table is small and the code prioritizes clarity over a trie. A very large routing table would scan every network route per packet.
  • prefixLength() does not validate contiguity. It is a popcount. Any caller accepting a dotted mask must round-trip it through fromPrefixLength() first.
  • Prefix 0 produces the all-zero mask. fromPrefixLength(0) is valid and yields 0.0.0.0; it is not rejected as an invalid prefix.
  • addConnectedRoute() skips the any-address subnet. Configuring 0.0.0.0 does not create an on-link route; the default route is a separate, explicit gateway route.
  • Re-addressing must remove first. Ifconfig.setDefault() tracks the previous default address and mask precisely so the old on-link route does not survive the change.
  • search() retries on ConcurrentModificationException. The table is mutated by configuration and read by the packet path, so a failed traversal is retried rather than propagated.
  • Gateway preference only breaks equal-prefix ties. It never beats a longer prefix; a more-specific direct route still wins over a shorter gateway route.
  • Classful addresses are special-cased. 10.0.9.0 with the default /8 is a network route, not a host route, even though its second octet has host bits set for that mask.

Related Pages

  • IPv4-Protocol — Parent hub: header parsing, fragment reassembly, outbound transmission
  • Network-Layers — How IPv4 sits above the link layer and below TCP/UDP
  • Network-Stack — Overall network architecture
  • Ethernet-Driver — Link-layer devices used by routes
  • ARP — Next-hop MAC resolution for a selected route
  • DNS-Resolver — Hostname-to-address lookups that feed route selection
  • Shell-Commands — The route and ifconfig commands in context

Clone this wiki locally