Show What an IP Address Literal Means Under Every Standard

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.


raddr

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 in docs/architecture.md. The API is not yet stable.

The problem, in one string

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.

Dialects sit on two axes

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 divergence

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.

For IPv6 the disagreement inverts

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.
  • The 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.
  • The zone ID lives in its own field, never in the address bits, and does not participate in equality: fe80::1%lo0 == fe80::1%en0 is TRUE, and addr_zone() is how you tell them apart.

raddr is a shower, not a protector

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.

What raddr will never do

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

Why not just use 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.

Setup

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

Verification

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.

Project Layout

  • 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.

Reference manual

It appears you don't have a PDF plugin for this browser. You can click here to download the reference manual.

install.packages("raddr")

0.1.2 by Bart Turczynski, 22 days ago


https://gitlab.com/bart-turczynski/raddr


Report a bug at https://gitlab.com/bart-turczynski/raddr/-/issues


Browse source code at https://github.com/cran/raddr


Authors: Bart Turczynski [aut, cre] (ORCID: , web-platform-tests contributors [cph] (bundled URL test corpus , BSD-3-Clause; see inst/COPYRIGHTS)


Documentation:   PDF Manual  


MIT + file LICENSE license


Imports rlang, vctrs

Suggests bignum, bit64, digest, hedgehog, knitr, rmarkdown, spelling, testthat


See at CRAN