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
classifyIp(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 loopbackclassifyIp(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 privateclassifyIp(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.
export function classifyIp(address: string): IpClassification
| address | string | an IPv4 or IPv6 address |
| returns | IpClassification |
The types it declares, generated into your project
/** Which special-purpose block an address falls in, the most specific one. */
export interface IpClassification {
/** canonical text */
readonly address: string;
/** 4 or 6 */
readonly version: number;
readonly category: IpCategory;
/** the IANA registry's name for the block, or Global unicast */
readonly name: string;
/** the matching block, null for global */
readonly block: string | null;
/** the document that assigned it */
readonly rfc: string | null;
/** as the IANA registry says; null where it says N/A */
readonly globallyReachable: boolean | null;
}
export type IpCategory = "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
import { classifyIp } from "#fune/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.
import { cidrContains } from "./net_cidr.ts"; ← from net.cidr ^1.0.0 · built alongside by fune
import { isIpv4 } from "./net_ipv4.ts"; ← from net.ipv4 ^1.0.0 · built alongside by fune
import { formatIpv6, isIpv6, parseIpv6 } from "./net_ipv6.ts"; ← from net.ipv6 ^1.0.0 · built alongside by fune
import { SPECIAL_BLOCKS } from "./net_ip_classify_data.ts"; ← this capability’s own data, compiled from data/special-purpose.json into the same file by fune build
import { type IpCategory, type IpClassification } from "./net_ip_classify_types.ts";
/**
* 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".
*/
export function classifyIp(address: string): IpClassification {
let version: number;
let canonical: string;
if (isIpv4(address)) {
version = 4;
canonical = address;
} else if (isIpv6(address)) {
version = 6;
canonical = formatIpv6(parseIpv6(address));
} else {
throw new Error(`"${String(address)}" is not an IPv4 or IPv6 address`);
}
let best: (typeof SPECIAL_BLOCKS)[number] | null = null;
let bestPrefix = -1;
for (const row of SPECIAL_BLOCKS) {
if (row.block.includes(":") !== (version === 6)) continue;
if (!cidrContains(row.block, canonical)) continue;
const prefix = Number(row.block.slice(row.block.indexOf("/") + 1));
if (prefix > bestPrefix) {
best = row;
bestPrefix = prefix;
}
}
if (best === null) {
return { address: canonical, version, category: "global", name: "Global unicast", block: null, rfc: null, globallyReachable: true };
}
return {
address: canonical,
version,
category: best.category as IpCategory,
name: best.name,
block: best.block,
rfc: best.rfc,
globallyReachable: best.globallyReachable,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript 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 TypeScript implementation. Install it without the registry with fune add ./net.ip-classify-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.ip-classify@1.0.0:typescript.
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 |