Addresses you're allowed to write down: documentation ranges and the rest of the reserved map

Every example IP in the previous article was 203.0.113.10. That is not a number I invented, and it is not a real server. It comes from TEST-NET-3, one of three IPv4 ranges the IETF reserved specifically so that technical writers have addresses they can safely put in front of readers.

The reason these exist is not tidiness. It is that documentation leaks into production, reliably and at scale, and an address in an example is an address someone will eventually paste into a config file.

What happens when you use a real address

The clearest illustration is 1.1.1.1. Before it became Cloudflare's DNS resolver, APNIC held 1.1.1.0/24 and could not meaningfully use it. The address had been typed into so many test configurations, tutorials, lab exercises and default fields over the years that it received a constant flood of unsolicited traffic from across the internet. It was not assigned to anything. People just used it as a placeholder, and the packets kept arriving.

Cloudflare's 2018 arrangement with APNIC was partly a research project for exactly this reason: put a real service there and study what the noise actually consists of.

That is one famous case, but the mechanism is entirely general. Write a plausible-looking IP in a tutorial, get a few thousand readers, and some fraction will paste it into a firewall rule, a monitoring check, or an /etc/hosts file. Whoever owns that address now receives traffic they never asked for and cannot easily stop — which is the same courtesy problem as crawler politeness, arriving from a completely different direction.

Documentation ranges solve it structurally. The addresses are guaranteed unroutable, so a leaked example fails immediately and locally rather than bothering a stranger.

IPv4: the three TEST-NETs

RFC 5737 reserves three /24 blocks:

RangeNameConventional use
192.0.2.0/24TEST-NET-1The default choice
198.51.100.0/24TEST-NET-2The second network in an example
203.0.113.0/24TEST-NET-3The third, or "the other end"

Three blocks rather than one because examples frequently need to show two or three distinct networks talking to each other — a client, a server, and a gateway; or two sites and a link between them. With a single range you end up implying everything is on the same subnet.

They are 256 addresses each, which is the practical constraint. If you are documenting anything with more than a handful of hosts you will be reusing addresses across examples, and that is fine.

IPv6: 2001:db8::/32, and the newer one

2001:db8::/32 from RFC 3849 is the one everybody knows, and it is still the right default. It is instantly recognisable — a reader who sees 2001:db8::1 knows immediately it is an example.

In August 2024, RFC 9637 added a second, much larger block: 3fff::/20. The reasoning is worth knowing, because it says something about how IPv6 is actually deployed. A /32 gives you 65,536 /48 subnets, which sounds enormous and is far too small to illustrate a real ISP or large enterprise address plan — the RFC notes that /29 was the most common allocation size recorded in August 2023. Documentation could not show realistic hierarchies inside a /32 without cheating.

The trap in 3fff::/20

A /20 boundary falls in the middle of the second hextet, which produces a genuinely counterintuitive result:

>>> import ipaddress
>>> doc = ipaddress.IPv6Network('3fff::/20')
>>> doc[0], doc[-1]
(3fff::, 3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff)

>>> ipaddress.ip_address('3fff:0fff::1') in doc
True
>>> ipaddress.ip_address('3fff:abcd::1') in doc
False

3fff:abcd::1 looks like documentation space and is not. Only the first nibble of the second hextet is fixed, so the block runs from 3fff:0000:: to 3fff:0fff:... and stops. Anything from 3fff:1000:: upward is ordinary allocatable space.

Since the whole point is picking addresses that cannot belong to anyone, this is the one worth checking rather than eyeballing. Stick to 3fff:0... or use the older 2001:db8::/32, where the boundary is on a clean hextet and mistakes are much harder to make.

The rest of the reserved map

Documentation ranges are one entry in a much longer list of addresses that are special for one reason or another:

RangePurposeRFC
0.0.0.0/8"This network" — source only1122
10.0.0.0/8Private1918
100.64.0.0/10Carrier-grade NAT6598
127.0.0.0/8Loopback — the whole /81122
169.254.0.0/16Link-local (APIPA)3927
172.16.0.0/12Private1918
192.0.0.0/24IETF protocol assignments6890
192.0.2.0/24Documentation5737
192.88.99.0/246to4 relay anycast — deprecated7526
192.168.0.0/16Private1918
198.18.0.0/15Benchmarking2544
198.51.100.0/24Documentation5737
203.0.113.0/24Documentation5737
224.0.0.0/4Multicast5771
240.0.0.0/4Reserved (former Class E)1112
255.255.255.255/32Limited broadcast919

And for IPv6:

RangePurposeRFC
::/128Unspecified4291
::1/128Loopback — a single address4291
fc00::/7Unique local (in practice fd00::/8)4193
fe80::/10Link-local4291
2001:db8::/32Documentation3849
3fff::/20Documentation (larger)9637
ff00::/8Multicast4291

Three that catch people out

100.64.0.0/10 is not private space. It is carrier-grade NAT space, reserved so ISPs can NAT customers behind a shared pool. It looks private, it is not routable on the public internet, and using it on your own network is asking for a collision with your upstream provider's infrastructure. Some equipment vendors also use it internally. Use RFC 1918 space.

198.18.0.0/15 is for benchmarking, not documentation. It exists so that performance tests between two devices do not need real addresses. It is a /15 — 131,072 addresses — which makes it tempting as extra private space, and it is a bad idea for the same reason: network test equipment may treat it specially.

Loopback is a whole /8 in IPv4 and a single address in IPv6. 127.0.0.1 is one of 16.7 million loopback addresses; 127.0.0.2 works just as well and is genuinely useful for binding several local services to distinct addresses on the same port. IPv6 gives you exactly ::1, which surprises people porting scripts across.

It's not just IP addresses

The same principle applies across most identifier spaces, and knowing these makes examples safer and more obviously fictional:

Domain names (RFC 2606, RFC 6761). example.com, example.net and example.org are reserved and operated by IANA. Four top-level domains are permanently reserved: .test for testing, .example for documentation, .invalid for deliberately broken names, and .localhost for loopback. Note that example.com resolves and serves a page, so it is safe in prose but will produce real DNS queries if pasted into a config.

AS numbers (RFC 5398). 64496–64511 for 16-bit examples, 65536–65551 for 32-bit. Distinct from the private ASN ranges 64512–65534 and 4200000000–4294967294, which are for real use in networks that never announce them publicly.

MAC addresses (RFC 7042). IANA reserves 00-00-5E-00-53-00 through 00-00-5E-00-53-FF for documentation. Worth using instead of the DE:AD:BE:EF style, which is a real OUI belonging to someone.

Phone numbers. In North America, 555-0100 to 555-0199 are reserved for fictional use — note that this is a narrow slice, not the whole 555 exchange, which is a common misconception. In the UK, Ofcom reserves drama ranges including 07700 900000–900999. Most countries have an equivalent; worth looking up for your own locale if you write examples with phone numbers in them.

Checking your own documentation

If you write technical articles with any volume of examples in them, this is worth automating. Python's ipaddress module knows about most of these already:

#!/usr/bin/env python3
"""Find IP addresses in text that aren't safe for documentation."""

import ipaddress
import re
import sys

V4 = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')

DOC_V4 = [ipaddress.ip_network(n) for n in
          ("192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24")]
DOC_V6 = [ipaddress.ip_network(n) for n in
          ("2001:db8::/32", "3fff::/20")]


def classify(addr):
    try:
        ip = ipaddress.ip_address(addr)
    except ValueError:
        return None                       # not an address, e.g. a version

    nets = DOC_V4 if ip.version == 4 else DOC_V6
    if any(ip in n for n in nets):
        return None                       # documentation range: fine
    if ip.is_private or ip.is_loopback or ip.is_link_local:
        return "private/loopback (fine in context, but not documentation)"
    if ip.is_multicast or ip.is_reserved or ip.is_unspecified:
        return "special-purpose"
    return "PUBLIC - belongs to someone"


def main(paths):
    problems = 0
    for path in paths:
        for lineno, line in enumerate(open(path), 1):
            for match in V4.findall(line):
                verdict = classify(match)
                if verdict and "PUBLIC" in verdict:
                    print(f"{path}:{lineno}: {match} - {verdict}")
                    problems += 1
    return 1 if problems else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Two caveats on that. It only catches IPv4 by regex, because matching IPv6 in prose reliably is genuinely hard — the abbreviated forms collide with too much other text. And version numbers like 8.3.1.2 parse as valid addresses, so expect some false positives; the classify function is where you would add your own suppressions.

The broader habit is the useful part. Once you know these ranges exist, using them costs nothing, and it means that on the day one of your examples ends up in a stranger's production config — which it will — the worst outcome is a connection that times out.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.