9.1 IPv4 Headers, CIDR and Longest-Prefix Forwarding
IP provides a best-effort datagram service. This lesson checks an IPv4 packet, then uses address sets to explain CIDR and longest-prefix matching. Each packet carries source and destination addresses, and routers use forwarding state to select the next hop. IP does not guarantee delivery, ordering, deduplication, or confidentiality, and it does not treat addresses as trustworthy identifiers for users or devices.
1. IPv4 addresses are routing locators, not necessarily globally unique
An IP address is configured on an interface or logical endpoint and can change with network attachment. Private addresses, link-local addresses, loopback addresses, multicast, anycast-style deployments, and NAT all undermine the assumption that "one machine, one globally unique IP" holds true.
Documentation and experiments should use RFC 5737-reserved IPv4 documentation prefixes: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24. These prefixes are for documentation and isolated experiments; do not expect public reachability.
2. Basic Fields of the IPv4 Header
The minimum header size is 20 bytes, and the IHL field can extend it up to 60 bytes:
| Field | Purpose |
|---|---|
| Version | Must be 4 for IPv4; if the first nibble is 6, the header should be parsed as an IPv6 header |
| IHL | Header length, measured in 32-bit words |
| DSCP/ECN | Quality of Service classification and explicit congestion notification |
| Total Length | Total byte count of the header plus payload |
| Identification/Flags/Fragment Offset | IPv4 fragmentation and reassembly; see section 8.3 |
| TTL | Decremented by at least 1 when forwarding; packets reaching 0 are discarded to stop indefinite loops |
| Protocol | Identifier for the payload protocol, such as ICMP=1, TCP=6, UDP=17 |
| Header Checksum | Covers only the IPv4 header; routers must update it when TTL changes |
| Source/Destination | 32-bit IPv4 addresses |
| Options + padding | Optional; used to pad the header to a 32-bit boundary |
For an already constructed IPv4 datagram exceeding the outgoing MTU, the DF flag determines whether fragmentation is allowed or the packet is dropped with ICMP feedback. TCP segmentation happens at the sender when constructing packets; routers do not resegment TCP. Fragmentation and path MTU discovery are covered in 8.3.
3. Check IHL and Total Length
This Python 3 teaching parser expects input starting at the IPv4 header, without an Ethernet header. Save it as ipv4_parse.py and run python3 ipv4_parse.py. The final example constructs IHL=6 and a ping payload. Its checksum is deliberately 0 for length-check testing; it is not a valid UDP packet ready for transmission.
from dataclasses import dataclass
import ipaddress
import struct
@dataclass(frozen=True)
class IPv4Packet:
source: ipaddress.IPv4Address
destination: ipaddress.IPv4Address
protocol: int
ttl: int
header: bytes
payload: bytes
def parse_ipv4(raw: bytes) -> IPv4Packet:
if len(raw) < 20:
raise ValueError("shorter than the minimum IPv4 header")
version_ihl = raw[0]
version = version_ihl >> 4
ihl_words = version_ihl & 0x0F
if version != 4:
raise ValueError(f"expected IPv4, got version {version}")
if ihl_words < 5:
raise ValueError("invalid IPv4 IHL")
header_length = ihl_words * 4
if len(raw) < header_length:
raise ValueError("truncated IPv4 options/header")
total_length = struct.unpack("!H", raw[2:4])[0]
if total_length < header_length:
raise ValueError("total length is smaller than header length")
if len(raw) < total_length:
raise ValueError("truncated IPv4 packet")
return IPv4Packet(
source=ipaddress.IPv4Address(raw[12:16]),
destination=ipaddress.IPv4Address(raw[16:20]),
protocol=raw[9],
ttl=raw[8],
header=raw[:header_length],
payload=raw[header_length:total_length],
)
raw = struct.pack(
"!BBHHHBBH4s4s", 0x46, 0, 28, 1, 0, 64, 17, 0,
ipaddress.IPv4Address("192.0.2.1").packed,
ipaddress.IPv4Address("192.0.2.2").packed,
) + b"\x01" * 4 + b"ping"
packet = parse_ipv4(raw)
print(len(packet.header), packet.payload)
try:
parse_ipv4(raw[:-1])
except ValueError as error:
print(error)Expected output:
24 b'ping'
truncated IPv4 packetThe header occupies 24 bytes; skipping 20 would mistake options for payload. Removing one byte triggers the truncation error. A production parser also needs checksum, fragmentation, option, and capture-truncation checks. This function returns the current fragment's payload without reassembly or UDP/TCP validation. It ignores trailing bytes beyond Total Length.
4. CIDR prefixes define address sets
192.0.2.0/24 indicates the first 24 bits are the network prefix, with the last 8 bits variable. The IPv4 prefix length ranges from 0 to 32, independent of class A/B/C boundaries. The following Python 3 example normalizes a host address to its subnet:
import ipaddress
network = ipaddress.ip_network("192.0.2.14/28", strict=False)
print(network.network_address) # 192.0.2.0
print(network.broadcast_address) # 192.0.2.15
print(network.num_addresses) # 16
print(list(network.hosts()))The first three lines print 192.0.2.0, 192.0.2.15, and 16. The last lists 14 host addresses, .1 through .14. strict=False accepts set host bits and normalizes the network prefix.
Subtracting network and broadcast addresses from every IPv4 subnet has exceptions:
- RFC 3021 permits point-to-point links to use
/31for both endpoints; /32denotes a single host route or endpoint, commonly used for loopback addresses or precise routing;- Cloud or virtual networks may reserve additional addresses according to platform rules.
IPv6 does not have broadcast addresses like IPv4, so the IPv4 subnet rule cannot be directly applied to IPv6.
5. Routers use longest-prefix match to select a forwarding entry
Assume the FIB contains:
0.0.0.0/0 -> gateway A
203.0.113.0/24 -> gateway B
203.0.113.128/25 -> gateway CThe destination 203.0.113.200 matches all three entries. Since the most specific route (/25) is the one with the longest prefix, gateway C is selected. The default route /0 serves only as a fallback when no more specific route exists. Run the following Python 3 example directly; successful assertions produce no output.
import ipaddress
routes = [
(ipaddress.ip_network("0.0.0.0/0"), "gateway-a"),
(ipaddress.ip_network("203.0.113.0/24"), "gateway-b"),
(ipaddress.ip_network("203.0.113.128/25"), "gateway-c"),
]
def longest_prefix_match(address: str) -> str:
target = ipaddress.ip_address(address)
matches = [(prefix.prefixlen, next_hop) for prefix, next_hop in routes
if target in prefix]
if not matches:
raise LookupError("no route")
return max(matches, key=lambda match: match[0])[1]
assert longest_prefix_match("203.0.113.200") == "gateway-c"
assert longest_prefix_match("198.51.100.8") == "gateway-a"This example has one next hop per prefix and scans in O(n) time. It does not implement equal-prefix route selection, policy routing, or ECMP. In a real router, control-plane protocols or static configurations first generate candidate routes that are inserted into the RIB. Only then are active forwarding entries programmed into the FIB or hardware forwarding table. Longest-prefix match is a packet lookup rule, not the complete logic used to select the best route in OSPF or BGP.
After selecting an outgoing interface or next hop, a router uses ARP or IPv6 NDP on links that need neighbor resolution. Packets do not cross the Internet using the remote destination's MAC address.
6. Route Aggregation is a Reachability Promise
192.0.2.0/25 and 192.0.2.128/25 can be aggregated into 192.0.2.0/24, reducing upstream route state. However, an aggregate announcement represents the announcer's promise of reachability for the entire prefix. If a more-specific component within that prefix is actually unreachable, the aggregate may still direct traffic to the announcer, so discard routes and component-health policies can prevent unresolved traffic from following a default route back upstream.
Aggregation only makes sense for aligned, contiguous prefixes and cannot combine any four arbitrary /24 into a single /22. ipaddress.collapse_addresses() can compute a set of prefixes that safely represent the same address space, but whether to announce the aggregate depends on actual reachability and policy. Saving a routing-table entry does not remove the forwarding responsibility.
7. Acceptance Questions
- Why does the IPv4 parser first check the IHL field instead of simply skipping the first 20 bytes?
- Why can't the
/31and/32cases be handled using the "subtract network/broadcast" rule? - How do RIB candidate selection and FIB longest-prefix lookup differ?
- When both
203.0.113.192/26and203.0.113.128/25match the destination, which route is selected? - Why is route aggregation considered a reachability promise?
The next lesson moves into 9.2 ICMP, ping, and traceroute evidence boundaries.