Computing History · Networking History

Classful Addressing, Subnets, and the Move to CIDR

Why the fixed class A, B, and C boundaries wasted most of the IPv4 space, how subnetting borrowed host bits, and what prefix aggregation changed about routing.

The original IPv4 address was 32 bits with a self-describing structure: the first bits of the first octet told a router how long the network portion was. A leading zero meant class A, with 7 bits of network and 24 bits of host; a leading 10 meant class B, with 14 and 16; a leading 110 meant class C, with 21 and 8. The remaining patterns were reserved for multicast and experimental use. A router could therefore decide, from the first byte of a destination address alone, which part of the address identified the network and which part the host.

The scheme is elegant in the same way a fixed-format instruction is elegant. It makes the forwarding decision cheap and unambiguous, and it needs no configuration to communicate the boundary. It also allocates space in units that do not match demand. Class A contains 128 blocks of 16.7 million addresses each, of which two are reserved, leaving 126 assignable; class B contains 16,384 blocks of 65,534; class C contains about 2 million blocks of 254. An organisation with 300 hosts cannot use a class C, so it must be given a class B, and 65,000 of the addresses in that block are unreachable by anyone else forever.

The arithmetic of the waste

The commonly cited figure is that classful assignment wasted roughly half of the address space, but the mechanism is more specific than a single number. Two effects compound. First, the rounding to a power-of-two block boundary: an organisation needing 300 addresses consumes 65,534. Second, the permanence of the grant: a class A assigned in the 1980s to a single institution cannot be subdivided in the routing table without that institution’s cooperation, because the class A boundary is what every router knows.

By the early 1990s the class B space was the binding constraint. Growth in the number of organisations that needed more than 254 addresses but fewer than 65,000 was steady, and the projection at the time was that class B would be exhausted before the total address space was. The routing table was growing at the same time, because every assigned network needed an entry, and the entries could not be combined: two adjacent class C networks are separate entries even when one organisation holds both.

The three responses to that pressure arrived in sequence and are often conflated. Subnetting, standardised in RFC 950, let an organisation divide its own class B or class C internally without changing what the outside world saw. Supernetting, later folded into CIDR, let adjacent blocks be announced as one. And classless addressing removed the fixed boundaries entirely, so a block could be any power-of-two size with a stated prefix length.

Subnetting: borrowing host bits

Subnetting answers a question that has nothing to do with the global address space: how a site with one registered network number operates several physical networks internally, without asking for a new number for each one. The answer is to split the host portion of the address into a subnet field and a host field, with the boundary chosen locally and communicated by a mask.

#include <stdint.h>

/* A /24 divided into four subnets: two bits borrowed from the host part. */
#define NETMASK(a, b, c, d) (((uint32_t)(a) << 24) | ((uint32_t)(b) << 16) | \
                             ((uint32_t)(c) << 8)  |  (uint32_t)(d))

uint32_t site_mask     = NETMASK(255, 255, 255,   0);  /* /24 */
uint32_t subnet_mask   = NETMASK(255, 255, 255, 192);  /* /26, 2 bits borrowed */

int same_subnet(uint32_t a, uint32_t b, uint32_t mask)
{
    return (a & mask) == (b & mask);
}

Two subnets of a /24 with a /26 mask contain 64 addresses each, of which 62 are usable because the all-zeros and all-ones host values are reserved for the subnet’s own identity and its directed broadcast. That reservation is why the usable count of a prefix is $2^{32-\text{len}} - 2$ rather than $2^{32-\text{len}}$, with the later exception for point-to-point links where both values are assigned to the two ends.

Subnetting’s cost is that the boundary is local knowledge. A router outside the site sees only the class B or class C number and forwards on that. Inside, every router must be configured with the mask, and a host that guesses the mask wrong will compute the wrong broadcast address and the wrong set of peers it can reach directly. The mask had to be configured on every host, which is the reason the bootstrap protocols that followed carried it: a host that learns its address also has to learn its prefix length and its default gateway, because none of the three can be derived from the address under classless rules.

The directed broadcast address is the sharp edge. A packet sent to the all-ones host value in a subnet is intended for every host in that subnet, and a router that forwards it from outside the subnet turns one packet into a flood. The convention that such packets are dropped at the site boundary exists because the alternative is an amplification primitive.

CIDR: prefix length instead of class

Classless Inter-Domain Routing replaced the class boundary with an explicit prefix length written beside the address: 192.0.2.0/24 means the first 24 bits identify the network. RFC 1519 introduced the scheme in 1993 with two goals that are easy to separate and were pursued together.

The allocation goal was to hand out blocks sized to need. An organisation that needs 1,000 addresses receives four contiguous /24s, or a /22, rather than a /16. The routing goal was aggregation: if a provider holds a contiguous range and assigns pieces of it to customers, the provider can announce the whole range as one prefix and the customer prefixes disappear from the global table. A single announcement replaces many, and the table grows with the number of providers rather than the number of sites.

Aggregation only works if the addresses are contiguous and the assignment follows the topology. That requirement is what makes provider-independent addresses expensive: an organisation that insists on keeping its own prefix when it changes providers cannot be aggregated into the new provider’s range, so its prefix must be carried in the global table separately. The tension between aggregatable and portable addresses is structural, not a policy failure, and it is why the global table has always grown faster than the number of providers would suggest.

The forwarding consequence of CIDR is longest-prefix match. With variable prefix lengths, two entries can both match a destination, and the correct choice is the one with the longer prefix, because it is the more specific statement about where the address lives. The table that is searched is filled by an exterior protocol whose announcements are themselves prefixes, described in Path Vectors, Policy, and Why BGP Does Not Minimize Anything, and the size of that table is the quantity CIDR’s aggregation was meant to control.

def lookup(table, addr):
    """table: list of (prefix_int, prefix_len, next_hop). Longest match wins."""
    best = None
    for net, length, nh in table:
        mask = (0xFFFFFFFF << (32 - length)) & 0xFFFFFFFF if length else 0
        if (addr & mask) == net:
            if best is None or length > best[0]:
                best = (length, nh)
    return best[1] if best else None

# 10.1.0.0/16 is more specific than 10.0.0.0/8, so it wins for 10.1.2.3
table = [(0x0A000000, 8, "core"), (0x0A010000, 16, "site-b")]
assert lookup(0x0A010203, table) == "site-b"
assert lookup(0x0A020304, table) == "core"

The linear scan above is correct and unusable at scale. Real forwarding tables are stored in a structure that exploits the prefix lengths being few: a binary trie walked bit by bit, or a multibit trie that consumes several bits per level, giving a lookup bounded by the number of distinct prefix lengths rather than by the number of routes. The number of distinct prefix lengths in the global table is small, which is what makes the bounded-depth claim hold.

Why the transition was not clean

Classless addressing required every router and host stack to accept a mask alongside an address, and the routers of the early 1990s did not. The migration used a compromise: large parts of the old class C space were reserved for provider assignment, and a router that had not been updated could still forward them because they looked like ordinary class C networks, while an updated router could aggregate them. Those ranges are still in use and still recognisable by their size.

The other half of the transition was that the class boundaries were never removed from the interpretation of an address, only from its routing. A stack that receives a bare address with no mask must still guess, and the guess is the classful one. That is why a host configured with 192.0.2.10 and no mask assumes a /24 while a host configured with 10.1.2.3 assumes a /8, and why the guess is wrong often enough that the modern practice is to refuse to operate without an explicit prefix length.

The exhaustion that CIDR was meant to postpone arrived anyway, and the mechanism that absorbed it is treated in NAT, Port Overloading, and the Address Exhaustion Boundary. CIDR’s lasting contribution is not the extra years; it is the idea that an address carries a topological claim rather than an identity, which is what allows the routing system to describe reachability in terms of where an address is rather than who owns it. The address had already been asked to carry more than a location once before, when NCP’s socket named a host by its position on one network, as described in NCP and the Limits That Forced TCP/IP.

References

  1. RFC 791: Internet Protocol — IETF
  2. RFC 950: Internet Standard Subnetting Procedure — IETF
  3. RFC 1519: Classless Inter-Domain Routing (CIDR): an Address Assignment and Aggregation Strategy — IETF
  4. RFC 4632: Classless Inter-domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan — IETF