Report what an IP address literal means under each of the standards and implementations that disagree about it, and classify parsed values against the IANA special-purpose address registries.
Status: 0.1.2, not on CRAN. The v0.1 surface is complete — the address type, the six dialects,
addr_parse(), RFC 5952 formatting, registry-backed classification, containment, encoding round-trips, reverse pointers, three vignettes and the reference docs — and the design is written down indocs/architecture.md. The API is not yet stable.
One literal, one machine, three different hosts:
"0177.0.0.1"
strict (RFC dotted-quad; Python ipaddress, Go, Rust) -> reject
whatwg (= what browsers do) -> 127.0.0.1
pton (POSIX inet_pton, Apple libc) -> 177.0.0.1
aton (BSD inet_aton) -> 127.0.0.1
Every existing library picks one of those readings and discards the
rest. raddr does not pick. addr_parse() takes no mode
argument — it returns every reading, always, with the reason codes that
explain each one.
Not one list, but what a standard requires (“on paper”) versus what an implementation actually does (“in reality”):
| Axis | Dialect | Models | Stability |
|---|---|---|---|
| paper | strict |
RFC dotted-quad grammar; Python ipaddress, Go,
Rust |
fixed |
| paper | whatwg |
WHATWG URL host parser; what browsers do | fixed, versioned spec |
| reality | pton |
POSIX inet_pton |
platform-varying |
| reality | aton |
BSD inet_aton |
stable in practice |
Two more dialects are precedence orderings over those reality primitives, not separate parsers:
addr_getaddrinfo = pton, falling back to aton
addr_curl = aton, falling back to addr_getaddrinfo
curl falls back to the resolver entry point rather than to the bare parser under it, because that is what real curl reaches — measured against curl 8.20.0, after the derived version of this line turned out to be wrong for IPv6.
Measured on macOS Darwin 25.4.0 arm64, Apple libc, 2026-07-26; the
curl column against real curl 8.20.0, 2026-07-28:
| input | strict |
whatwg |
pton |
aton |
getaddrinfo |
curl |
|---|---|---|---|---|---|---|
127.0.0.1 |
127.0.0.1 | 127.0.0.1 | 127.0.0.1 | 127.0.0.1 | 127.0.0.1 | 127.0.0.1 |
0177.0.0.1 |
reject | 127.0.0.1 | 177.0.0.1 | 127.0.0.1 | 177.0.0.1 | 127.0.0.1 |
192.0.010.1 |
reject | 192.0.8.1 | 192.0.10.1 | 192.0.8.1 | 192.0.10.1 | 192.0.8.1 |
192.0.048.1 |
reject | reject | 192.0.48.1 | reject | 192.0.48.1 | 192.0.48.1 |
4294967296 |
reject | reject | reject | 0.0.0.0 | 0.0.0.0 | 0.0.0.0 |
1.2.3. |
reject | 1.2.0.3 | reject | reject | reject | reject |
2130706433 |
reject | 127.0.0.1 | reject | 127.0.0.1 | 127.0.0.1 | 127.0.0.1 |
10.048.1.1 |
reject | reject | 10.48.1.1 | reject | 10.48.1.1 | 10.48.1.1 |
Two rows carry most of the package’s value:
192.0.048.1 — curl reaches a host a
browser refuses to dial.4294967296 — aton wraps
modulo 2^32 to 0.0.0.0; the standards reject.The two paper dialects disagree about IPv4 on six of the eight rows above. About IPv6 they agree on every input measured, and all of the divergence moves to the reality side:
| input | strict |
whatwg |
pton |
aton |
getaddrinfo |
curl |
|---|---|---|---|---|---|---|
::1 |
::1 | ::1 | ::1 | reject | ::1 | ::1 |
00001:: |
reject | reject | 1:: | reject | 1:: | 1:: |
::1.2.3.04 |
reject | reject | ::102:304 | reject | ::102:304 | ::102:304 |
fe80:abcd::1 |
fe80:abcd::1 | fe80:abcd::1 | fe80:abcd::1 | reject | fe80::1 %43981 | fe80::1 %43981 |
fe80::1%lo0 |
reject | reject | ::1 %lo0 | reject | ::1 %lo0 | ::1 %lo0 |
fe80:abcd::1 — two libc entry points
on one machine return different bits for one string. Apple’s
getaddrinfo reads the second hextet of a link-local address
as a scope ID and clears it; inet_pton does not. This is
the IPv6 counterpart of 0177.0.0.1. curl reaches the entry
point that lifts, so pton is the odd one out on this
row.curl column is a copy of the
getaddrinfo one here, because aton
rejects every IPv6 literal and the composition is nothing but its
fallback. For IPv4 the two differ on 0177.0.0.1 and
192.0.010.1 above.00001:: — inet_pton
counts only the four significant hex digits and lets the
leading zeros run as wide as they like. The standards cap the digits
outright.fe80::1%lo0 == fe80::1%en0 is TRUE, and
addr_zone() is how you tell them apart.It reports facts. It never returns a verdict, a risk score, or an allow/deny decision. Security policy is a separate concern and belongs to a separate package.
| Excluded | Owner |
|---|---|
| DNS resolution, hostname lookup | ssrfr / curl |
| HTTP, redirects, connection pinning | ssrfr |
| Allow/deny policy, risk scores, verdicts | ssrfr |
| Cloud-metadata endpoint tables | ssrfr |
| “Most restrictive reading wins” convenience | ssrfr |
| Geolocation, ASN, country data | nowhere |
| IDNA, punycode | punycoder |
| Public-suffix logic | pslr |
| URL parsing, scheme/port policy, reg-name-vs-IP host form | rurl |
| General CIDR set algebra (collapse, exclude, subnets) | ipaddress |
X-Forwarded-For extraction (HTTP header parsing, not
address parsing) |
ssrfr |
| Visualization | ggip |
ipaddress?ipaddress is a good package and raddr does not replace
it — the table above assigns CIDR set algebra to it permanently. But it
answers a different question. It gives you one reading of a
literal and is silent about the rest: it transforms
0177.0.0.1 into 177.0.0.1 without a word,
which is exactly the transformation that changes which host you reach.
raddr’s premise is that 0177.0.0.1 has several defensible
answers and that you should see all of them.
raddr is pure R, performs no network access, and depends on
vctrs and rlang.
Install package dependencies (from DESCRIPTION) plus the
dev tooling used by the checks:
Rscript -e 'pak::local_install_deps(dependencies = TRUE)'Enable the git hooks once per clone:
pre-commit install && pre-commit install --hook-type pre-pushRscript -e 'lints <- lintr::lint_package(); if (length(lints)) { print(lints); quit(status = 1) }' && Rscript -e 'rcmdcheck::rcmdcheck(args = "--as-cran", error_on = "warning")'R CMD check runs the testthat suite, so the tests are
verified as part of the check.
R/ contains the package source.man/ contains generated help pages (regenerate with
devtools::document()).NAMESPACE and man/ are roxygen2-generated
— edit the roxygen comments in R/, not these.tests/testthat/ contains the testthat tests.vignettes/ contains long-form documentation.DESCRIPTION declares package metadata and
dependencies.docs/architecture.md is the settled design record —
read it before changing the API._scratch/ is local-only planning space and is ignored
by git.