feat(net/unstable): classify IP addresses by IANA special-purpose registry - #7324
Open
tomas-zijdemans wants to merge 1 commit into
Open
tomas-zijdemans wants to merge 1 commit into
tomas-zijdemans wants to merge 1 commit into
Conversation
…istry Adds `classifyIP()`, returning one of eleven kinds, with `isLoopback()`, `isPrivate()`, `isLinkLocal()`, `isMulticast()` and `isUnspecified()` as thin wrappers over it. Lookup is a longest-prefix match over a table transcribed from the two IANA special-purpose registries, so a carve-out beats the block it sits inside. `192.0.0.9` is globally reachable inside a reserved `192.0.0.0/24`, and `2001::/23` is reserved "unless allowed by a more specific allocation" with seven reachable allocations inside it. A chain of `if`s gets those wrong unless every exception is hand-written in the right order. Three blocks come from RFCs rather than the registries and are cited in place: `224.0.0.0/4`, `ff00::/8`, and `fec0::/10`, which IANA removed when RFC 3879 deprecated site-local. IPv4-mapped addresses are unmapped first, since `::ffff:127.0.0.1` is `127.0.0.1`. Tunnels are not: 6to4, NAT64, Teredo and the deprecated `::/96` are a route to an address, not that address, so they classify by their own block. Extracting their embedded IPv4 is a follow-up. I diffed every row against the registry CSVs. Every row IANA marks Globally Reachable True classifies as "global". I also compared the predicates against Python's `ipaddress` over 16,244 addresses drawn from block boundaries and random sampling, with no disagreement on loopback, link-local, multicast or unspecified. Refs denoland#7315
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7324 +/- ##
========================================
Coverage 95.04% 95.05%
========================================
Files 618 619 +1
Lines 51746 52110 +364
Branches 9402 9462 +60
========================================
+ Hits 49181 49531 +350
- Misses 2022 2031 +9
- Partials 543 548 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
classifyIP()plusisLoopback(),isPrivate(),isLinkLocal(),isMulticast()andisUnspecified(). Step 2 of #7315, on top of the parsers from#7316.
Lookup is a longest-prefix match over a table transcribed from the two IANA
special-purpose registries, and the nesting is why.
192.0.0.9and192.0.0.10are globally reachable inside a reserved192.0.0.0/24.2001::/23is reserved "unless allowed by a more specific allocation", withseven reachable allocations inside it. A chain of
ifs gets that right only ifevery exception is hand-written in the right place.
IPv4-mapped addresses are unmapped before lookup, because
::ffff:127.0.0.1is127.0.0.1. Tunnels are not: 6to4, NAT64, Teredo and the deprecated::/96are a route to an address rather than that address, so they classify by their
own block. Embedded-IPv4 extraction and
isGloballyReachable()wait for acaller who needs them. Next from me is
maskIP()andformatIP(), which iswhat
@std/rate-limitcalls to key a client by its IPv6 allocation rather thanits address.
Look hardest at the two tables. I diffed every row against the registry CSVs,
checked that every Globally-Reachable-True row comes back
global, and checkedthat every row is a canonical CIDR, because
parseSubnet()masks host bitssilently. Three rows come from RFCs instead of the registries and are cited in
place:
224.0.0.0/4,ff00::/8, andfec0::/10, which IANA dropped when RFC3879 deprecated site-local. I also ran the predicates against Python's
ipaddressover 16,244 addresses: loopback, link-local, multicast andunspecified agree everywhere.
Two caller notes.
url.hostnamekeeps IPv6 brackets, so you strip themyourself (documented, with a runnable example). Unallocated space like
4000::/3comes backglobal, the same simplification Go'sIsGlobalUnicastmakes.
I used Claude Code to help investigate and write this change.