Functional Weave
Code in Python

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

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
impl/python.py · 42 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.

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
Download for Python net.ip-classify-1.0.0-python.fune · 24,134 bytes sha256 829318dad4afe1e5482f35dbc80690dd7f65814cb11c011da938f888b7802e55

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.

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