Standards and implementations disagree about what an IP address literal means: the string "0177.0.0.1" is rejected by the dotted-quad grammar, read as 127.0.0.1 by browsers, and read as 177.0.0.1 by some 'inet_pton' implementations. Most libraries pick one reading and discard the rest. This package reports them all, alongside the reason codes that explain each one, and classifies parsed values against the IANA special-purpose address registries. It is pure R, performs no network access, and returns facts rather than allow or deny verdicts.
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-push
Rscript -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.