Functional Weave
Code in TypeScript

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 loopback
  • classifyIp(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 private
  • classifyIp(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
addressstringan IPv4 or IPv6 address
returnsIpClassification

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";
impl/typescript.ts · 49 lines · open · raw

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
Download for TypeScript net.ip-classify-1.0.0-typescript.fune · 24,276 bytes sha256 783e8329d5edac1524cdac0464ce3b80215f69eeb973ea6f48cae43accb9eebd

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md2,369
data/special-purpose.json7,019
impl/python.py1,657
impl/rust.rs2,794
impl/typescript.ts1,780
vectors.json7,609