net.ip-classify
Classify an IPv4 or IPv6 address as loopback, private, link-local, multicast, documentation, CGNAT or global.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 34 tests, run in TypeScript, Python and Rust.
What it does
Say what kind of address an IPv4 or IPv6 address is: loopback, private, shared (carrier-grade NAT), link-local, multicast, documentation, benchmarking, reserved, broadcast, protocol assignments, translation, IPv4-mapped, discard, unspecified, or global.
## How it decides
For example
classify_ip(127.0.0.1)→ address 127.0.0.1, version 4, category loopback, name Loopback, block 127.0.0.0/8, rfc RFC 1122, globally reachable false IPv4 loopbackclassify_ip(10.1.2.3)→ address 10.1.2.3, version 4, category private, name Private-Use, block 10.0.0.0/8, rfc RFC 1918, globally reachable false 10/8 privateclassify_ip(172.20.0.1)→ address 172.20.0.1, version 4, category private, name Private-Use, block 172.16.0.0/12, rfc RFC 1918, globally reachable false 172.16/12 private, not on an octet boundary
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def classify_ip(address: str) -> IpClassification
| address | string | an IPv4 or IPv6 address |
| returns | IpClassification |
The types it declares, generated into your project
@dataclass(frozen=True)
class IpClassification:
"""Which special-purpose block an address falls in, the most specific one."""
#: canonical text
address: str
#: 4 or 6
version: int
category: IpCategory
#: the IANA registry's name for the block, or Global unicast
name: str
#: the matching block, null for global
block: Optional[str]
#: the document that assigned it
rfc: Optional[str]
#: as the IANA registry says; null where it says N/A
globally_reachable: Optional[bool]
IpCategory = Literal["unspecified", "this-network", "loopback", "private", "shared", "link-local", "multicast", "documentation", "benchmarking", "reserved", "broadcast", "protocol", "translation", "ipv4-mapped", "discard", "global"]
Your code names it in one line, in the file that uses it
from fune.net.ip_classify import classify_ip # net.ip-classify@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .net_cidr import cidr_contains ← from net.cidr ^1.0.0 · built alongside by fune
from .net_ip_classify_data import SPECIAL_BLOCKS ← this capability’s own data, compiled from data/special-purpose.json into the same file by fune build
from .net_ip_classify_types import IpClassification
from .net_ipv4 import is_ipv4 ← from net.ipv4 ^1.0.0 · built alongside by fune
from .net_ipv6 import format_ipv6, is_ipv6, parse_ipv6 ← from net.ipv6 ^1.0.0 · built alongside by fune
def classify_ip(address: str) -> IpClassification:
"""Which special-purpose block an address is in.
From the IANA IPv4 and IPv6 Special-Purpose Address Registries (RFC 6890)
plus the multicast ranges. The most specific block wins: 192.0.0.9 is Port
Control Protocol Anycast (globally reachable), not the /24 of IETF
assignments around it (not). An address in no block is "global".
"""
if is_ipv4(address):
version = 4
canonical = address
elif is_ipv6(address):
version = 6
canonical = format_ipv6(parse_ipv6(address))
else:
raise ValueError('"%s" is not an IPv4 or IPv6 address' % (address,))
best = None
best_prefix = -1
for row in SPECIAL_BLOCKS:
if (":" in row.block) != (version == 6):
continue
if not cidr_contains(row.block, canonical):
continue
prefix = int(row.block[row.block.index("/") + 1:])
if prefix > best_prefix:
best, best_prefix = row, prefix
if best is None:
return IpClassification(
address=canonical, version=version, category="global", name="Global unicast",
block=None, rfc=None, globally_reachable=True,
)
return IpClassification(
address=canonical, version=version, category=best.category, name=best.name,
block=best.block, rfc=best.rfc, globally_reachable=best.globally_reachable,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add net.ip-classify
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./net.ip-classify-1.0.0-python.fune, or fetch it from a terminal with fune pull net.ip-classify@1.0.0:python.
The whole function, every language, is one file too: net.ip-classify-1.0.0.fune, 28,917 bytes, sha256 597f539abc40a7ef8f254dd3c14b52974a0a93ad2d1d5931dc716c99f023dd92. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before net.ip-classify
after — your function gets the result and the arguments, and returns the final result.
# fune: after net.ip-classify
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace net.cidr in net.ip-classify
# fune: replace net.ipv4 in net.ip-classify
# fune: replace net.ipv6 in net.ip-classify
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.ip-classify --steps.
# fune: step net.ip-classify after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| IPv4 loopback | 127.0.0.1 | → | address 127.0.0.1, version 4, category loopback, name Loopback, block 127.0.0.0/8, rfc RFC 1122, globally reachable false |
| 10/8 private | 10.1.2.3 | → | address 10.1.2.3, version 4, category private, name Private-Use, block 10.0.0.0/8, rfc RFC 1918, globally reachable false |
| 172.16/12 private, not on an octet boundary | 172.20.0.1 | → | address 172.20.0.1, version 4, category private, name Private-Use, block 172.16.0.0/12, rfc RFC 1918, globally reachable false |
| 192.168/16 private | 192.168.1.10 | → | address 192.168.1.10, version 4, category private, name Private-Use, block 192.168.0.0/16, rfc RFC 1918, globally reachable false |
| carrier-grade NAT shared space | 100.64.0.1 | → | address 100.64.0.1, version 4, category shared, name Shared Address Space, block 100.64.0.0/10, rfc RFC 6598, globally reachable false |
| 100.128.0.1 is just past the /10, so global | 100.128.0.1 | → | address 100.128.0.1, version 4, category global, name Global unicast, block —, rfc —, globally reachable true |
| IPv4 link-local | 169.254.10.20 | → | address 169.254.10.20, version 4, category link-local, name Link Local, block 169.254.0.0/16, rfc RFC 3927, globally reachable false |
| mDNS group is multicast | 224.0.0.251 | → | address 224.0.0.251, version 4, category multicast, name Multicast, block 224.0.0.0/4, rfc RFC 5771, globally reachable — |
| SSDP group is multicast | 239.255.255.250 | → | address 239.255.255.250, version 4, category multicast, name Multicast, block 224.0.0.0/4, rfc RFC 5771, globally reachable — |
| TEST-NET-1 documentation | 192.0.2.10 | → | address 192.0.2.10, version 4, category documentation, name Documentation (TEST-NET-1), block 192.0.2.0/24, rfc RFC 5737, globally reachable false |
Show the other 24 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 0.0.0.0 is the most specific /32, not this-network /8 | 0.0.0.0 | → | address 0.0.0.0, version 4, category unspecified, name This host on this network, block 0.0.0.0/32, rfc RFC 1122, globally reachable false |
| elsewhere in 0/8 is this network | 0.1.2.3 | → | address 0.1.2.3, version 4, category this-network, name This network, block 0.0.0.0/8, rfc RFC 791, globally reachable false |
| PCP anycast /32 overrides its /24 and is globally reachable | 192.0.0.9 | → | address 192.0.0.9, version 4, category protocol, name Port Control Protocol Anycast, block 192.0.0.9/32, rfc RFC 7723, globally reachable true |
| the rest of 192.0.0.0/24 is IETF protocol assignments | 192.0.0.100 | → | address 192.0.0.100, version 4, category protocol, name IETF Protocol Assignments, block 192.0.0.0/24, rfc RFC 6890, globally reachable false |
| limited broadcast | 255.255.255.255 | → | address 255.255.255.255, version 4, category broadcast, name Limited Broadcast, block 255.255.255.255/32, rfc RFC 8190, globally reachable false |
| class E reserved | 250.1.1.1 | → | address 250.1.1.1, version 4, category reserved, name Reserved, block 240.0.0.0/4, rfc RFC 1112, globally reachable false |
| benchmarking /15 reaches 198.19.255.255 | 198.19.255.255 | → | address 198.19.255.255, version 4, category benchmarking, name Benchmarking, block 198.18.0.0/15, rfc RFC 2544, globally reachable false |
| a public resolver is global | 8.8.8.8 | → | address 8.8.8.8, version 4, category global, name Global unicast, block —, rfc —, globally reachable true |
| IPv6 loopback | ::1 | → | address ::1, version 6, category loopback, name Loopback Address, block ::1/128, rfc RFC 4291, globally reachable false |
| IPv6 unspecified | :: | → | address ::, version 6, category unspecified, name Unspecified Address, block ::/128, rfc RFC 4291, globally reachable false |
| IPv6 link-local, returned canonical | FE80::0001 | → | address fe80::1, version 6, category link-local, name Link-Local Unicast, block fe80::/10, rfc RFC 4291, globally reachable false |
| unique local is private | fd12:3456::1 | → | address fd12:3456::1, version 6, category private, name Unique-Local, block fc00::/7, rfc RFC 4193, globally reachable false |
| IPv6 documentation | 2001:db8::1 | → | address 2001:db8::1, version 6, category documentation, name Documentation, block 2001:db8::/32, rfc RFC 3849, globally reachable false |
| Teredo /32 is more specific than 2001::/23; reachability N/A | 2001:0:1::1 | → | address 2001:0:1::1, version 6, category translation, name TEREDO, block 2001::/32, rfc RFC 4380, globally reachable — |
| PCP anycast /128 | 2001:1::1 | → | address 2001:1::1, version 6, category protocol, name Port Control Protocol Anycast, block 2001:1::1/128, rfc RFC 7723, globally reachable true |
| IPv4-mapped keeps its dotted tail | ::FFFF:192.168.1.1 | → | address ::ffff:192.168.1.1, version 6, category ipv4-mapped, name IPv4-mapped Address, block ::ffff:0:0/96, rfc RFC 4291, globally reachable false |
| all-nodes multicast | ff02::1 | → | address ff02::1, version 6, category multicast, name Multicast, block ff00::/8, rfc RFC 4291, globally reachable — |
| NAT64 well-known prefix, canonical in hex | 64:ff9b::8.8.8.8 | → | address 64:ff9b::808:808, version 6, category translation, name IPv4-IPv6 Translat., block 64:ff9b::/96, rfc RFC 6052, globally reachable true |
| a public IPv6 address is global | 2606:4700::1111 | → | address 2606:4700::1111, version 6, category global, name Global unicast, block —, rfc —, globally reachable true |
| a host name is not an address | localhost | → | error: is not an IPv4 or IPv6 address |
| a block is not an address | 10.0.0.1/8 | → | error: is not an IPv4 or IPv6 address |
| a zone id is refused | fe80::1%eth0 | → | error: is not an IPv4 or IPv6 address |
| a leading zero is refused | 010.0.0.1 | → | error: is not an IPv4 or IPv6 address |
| a trailing newline is refused | 127.0.0.1 | → | error: is not an IPv4 or IPv6 address |
More from the author
The rules are data (`data/special-purpose.json`), one row per block of the IANA special-purpose registries, with the registry's own name, RFC and "Globally Reachable" column. An address is matched against every block of its family and **the most specific (longest prefix) wins**, as the registry intends: `192.0.0.9` is Port Control Protocol Anycast (globally reachable), not the surrounding `192.0.0.0/24` IETF assignments (not reachable). An address in no block is `global`, name `Global unicast`, reachable.
`category` is this capability's grouping of the registry's names, so callers can branch on a closed set; `name`, `block` and `rfc` say exactly which row matched. `globallyReachable` is the registry's column, null where it says N/A (Teredo, 6to4, deprecated blocks) and for the multicast ranges, which the special-purpose registries do not list.
## Edge cases
- IPv6 is returned in RFC 5952 canonical form (`FE80::0001` gives `fe80::1`). - An IPv4-mapped address (`::ffff:192.168.1.1`) is `ipv4-mapped`; it is not classified as the IPv4 address inside it. Classify the IPv4 part yourself if that is what you mean. - `global` for IPv6 means "in no special-purpose block", which includes space IANA has not allocated yet. - Text follows `net.ipv4` / `net.ipv6` strictly: no leading zeros, zone ids or prefixes.
## Sources (checked 2026-09-26 against the registries' CSV exports)
- IANA IPv4 Special-Purpose Address Registry, https://www.iana.org/assignments/iana-ipv4-special-registry/ (RFC 6890, RFC 8190). All 25 rows, including 192.88.99.2/32 (6a44 relay) and the two NAT64/DNS64 discovery /32s listed as one row there. - IANA IPv6 Special-Purpose Address Registry, https://www.iana.org/assignments/iana-ipv6-special-registry/. All 26 rows, including 100:0:0:1::/64 (RFC 9780), 3fff::/20 (RFC 9637) and 5f00::/16 (RFC 9602). - IPv4 multicast 224.0.0.0/4: RFC 5771 (IANA Guidelines for IPv4 Multicast Address Assignments). IPv6 multicast ff00::/8: RFC 4291 section 2.7.
When the registry changes, publish a new version with the new rows.
Files
| Path | Bytes |
|---|---|
| README.md | 2,369 |
| data/special-purpose.json | 7,019 |
| impl/python.py | 1,657 |
| impl/rust.rs | 2,794 |
| impl/typescript.ts | 1,780 |
| vectors.json | 7,609 |