---
title: "Reason codes"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Reason codes}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
```
```{r setup}
library(raddr)
```
A reason code is raddr's answer to "why". A reading that produced no address
comes with the code that says what stopped it, and an address that classified
into a registry block comes with codes for the rules it sits awkwardly against.
Codes are the reason a refusal is a finding rather than a shrug.
The whole vocabulary is one data frame:
```{r}
registry <- addr_codes_registry()
nrow(registry)
table(registry$layer)
```
This vignette walks every code in it. Each entry says what the code *means*,
why it is *a signal* worth having, and how it is *detected* — including what it
does not prove, which is usually the part that matters.
## Two layers, and they are not the same kind of statement
```{r}
registry[registry$layer == "parse", "code"]
registry[registry$layer == "classify", "code"]
```
A **parse** code describes what a parser *did* with a piece of text. It is a
fact about one dialect's reading, and a different dialect may have no objection
at all. A **classify** code describes an *address* against a rule in a
specification: the address parsed fine, and something about it contradicts a
document. That difference is why only the classify layer is graded:
```{r}
table(registry$layer, registry$strength, useNA = "ifany")
```
`strength` is `NA` for all fifteen parse codes, and that is the honest value
rather than a placeholder. "This dialect wanted digits here" is not a rule with
normative force; it is a description of a grammar.
### A helper for the examples below
Parse codes are per-dialect, so the interesting question about any example is
always *which dialects said this*. One helper answers it for every example in
the next section:
```{r}
codes_by_dialect <- function(literal) {
p <- addr_parse(literal)
found <- vapply(
c("strict", "whatwg", "pton", "aton", "getaddrinfo", "curl"),
function(d) paste(unlist(addr_codes(p, d)), collapse = ", "),
character(1)
)
data.frame(codes = found)
}
```
## The parse layer
### `not_a_number`
*Meaning.* A dot-separated part is not a number in any radix this dialect
reads.
*Why it is a signal.* It is the ordinary "that is not an address" answer, and
its value is mostly that it is *not* one of the interesting codes below. A
literal that fails this way fails everywhere, so there is no divergence to
reason about.
*Detection and precision.* Raised on the first part that fails to parse.
A code fires once per part, first match wins, so a part that is not a number
does not also report the range its garbage value happened to land outside of.
```{r}
codes_by_dialect("1.2.a.4")
```
### `leading_zero`
*Meaning.* A part carries a leading zero and this dialect forbids one.
*Why it is a signal.* **This is the most important code in the package.** A
leading zero is where the dialects part company: the RFC dotted-quad grammar
refuses it, a browser reads it as octal, and `inet_pton` strips it and reads
decimal. One string, several hosts.
*Detection and precision.* Note carefully what the example shows — only
`strict` raises it. **A `leading_zero` code does not mean the literal was
rejected.** It means one dialect objected while the others read the literal
happily, and quite possibly as *different addresses*. Seeing this code is a
reason to go look at the readings, not a reason to stop.
```{r}
codes_by_dialect("01.2.3.4")
addr_reading(addr_parse("0177.0.0.1"), "whatwg")
addr_reading(addr_parse("0177.0.0.1"), "pton")
```
### `empty_part`
*Meaning.* Two consecutive dots, or a leading dot, leave a part empty.
*Why it is a signal.* An empty part is almost always a truncation or a
concatenation bug upstream rather than a deliberate spelling.
*Detection and precision.* Every dialect raises it; there is no divergence
here. A *trailing* dot is a different case with its own code, because the
dialects disagree about that one.
```{r}
codes_by_dialect(".1.2.3")
```
### `empty_hex`
*Meaning.* A part is a digitless `"0x"` where this dialect requires digits.
*Why it is a signal.* `inet_aton` tolerates a digitless `0x` in any part but
the last, treating it as zero, and WHATWG has no such carve-out — a bare `0x`
is simply zero there. It is a small, real difference between two widely
deployed number parsers.
*Detection and precision.* Only `aton` raises it, and only in the final part;
`0x.1` and `0x.0x.0` parse without complaint. The other dialects reach a
different objection to the same text, which is why the example below shows
three different sets of codes for one literal.
```{r}
codes_by_dialect("0x")
```
### `out_of_range`
*Meaning.* A part exceeds the largest value its position can hold.
*Why it is a signal.* It separates "this was never an address" from "this was
an address that overflowed", which is a real distinction when you are looking
at logs.
*Detection and precision.* The bound is positional, not a flat 255.
`inet_aton` range-checks every arity except the whole-host number: with two to
four parts the final part is bounded by `256^(5-k) - 1`, so `127.16777215` is
fine and `127.16777216` is not. With one part there is no check at all and the
value is truncated modulo 2^32 — which is why `4294967296` is `0.0.0.0` under
`aton` and rejected everywhere else.
```{r}
codes_by_dialect("256.1.1.1")
addr_aton(c("127.16777215", "127.16777216", "4294967296"))
```
### `wrong_part_count`
*Meaning.* The number of dot-separated parts is not one this dialect accepts.
*Why it is a signal.* The short forms are the reason `10.1` reaches `10.0.0.1`
in a browser and a shell but not in a dotted-quad validator.
*Detection and precision.* Only the dialects that require exactly four parts
raise it. `whatwg` and `aton` accept one, two, three or four parts, so a
three-part literal is an address to them and an error only to `strict` and
`pton`.
```{r}
codes_by_dialect("1.2.3")
addr_whatwg("1.2.3")
```
### `trailing_dot`
*Meaning.* The literal ends in a dot that this dialect does not drop.
*Why it is a signal.* A trailing dot is a fully qualified domain name's
spelling, so it turns up whenever host text has been through a DNS-shaped code
path.
*Detection and precision.* `whatwg` is the odd one out: it drops a single
trailing dot before parsing, so it alone has no objection. Every other dialect
raises the code. Do not read this as "harmless" — it is the WHATWG parser
accepting a literal the others reject.
```{r}
codes_by_dialect("1.2.3.4.")
addr_whatwg("1.2.3.4.")
```
### `zone_not_permitted`
*Meaning.* The literal carries a zone ID and this dialect has no zone ID at
all.
*Why it is a signal.* RFC 4007 zone IDs exist in the socket API and in what
people type, and not in the address grammars. The code marks that boundary
rather than pretending the text was malformed.
*Detection and precision.* Raised by the two paper dialects only. The reality
dialects read the zone and keep it beside the bits, where it takes no part in
equality.
```{r}
codes_by_dialect("fe80::1%lo0")
addr_zone(addr_pton("fe80::1%lo0"))
```
### `multiple_zones`
*Meaning.* More than one `%`, so the zone ID has no single delimiter.
*Why it is a signal.* There is no reading of two zone IDs, so this is a
genuine malformation rather than a difference of opinion.
*Detection and precision.* The paper dialects reach `zone_not_permitted`
first — they object to the *first* `%` and never get as far as the second — so
this code comes only from the dialects that have zones at all. Two dialects
refusing one literal for two different reasons is normal and is why codes are
per-dialect.
```{r}
codes_by_dialect("fe80::1%lo0%en0")
```
### `bad_hextet`
*Meaning.* A group is not one to four hexadecimal digits.
*Why it is a signal.* It catches both non-hex characters and over-long groups.
*Detection and precision.* Note that `aton` raises nothing at all here, and
that is not an oversight. `inet_aton` is an `AF_INET` parser: it has no
*objection* to a colon literal, it has no *reading* of one. That is an outcome,
not a code, and it is why there is no `no_ipv6_reading` in the vocabulary.
```{r}
codes_by_dialect("g::1")
addr_outcome(addr_parse("g::1"), "aton")
```
### `empty_group`
*Meaning.* A stray colon leaves a group empty.
*Why it is a signal.* It is the IPv6 counterpart of `empty_part`, and like it,
usually a truncation.
*Detection and precision.* A single leading or trailing colon is an empty
group; a *doubled* one is the elision `::` and is fine. The engine narrows row
by row through one gate at a time, so the first gate a row fails is its reason.
```{r}
codes_by_dialect(":1")
```
### `bad_elision`
*Meaning.* More than one `"::"`, or a `":::"` run.
*Why it is a signal.* The elision has to be unambiguous — two of them cannot be
expanded to a unique address.
*Detection and precision.* Raised by every dialect that reads IPv6. It is
about the *count* of elisions, not their position; a single `::` anywhere is
legal, including at either end.
```{r}
codes_by_dialect("::1::2")
```
### `wrong_group_count`
*Meaning.* The literal does not resolve to exactly eight groups.
*Why it is a signal.* Eight groups is the whole of the IPv6 address, and a
literal that resolves to seven or nine is not an address under any reading.
*Detection and precision.* "Resolve to" is the operative phrase: the check runs
*after* the elision has been expanded, so `::1` is one written group and eight
resolved ones. Watch out for text handling that drops a trailing empty field
before this check runs — a literal ending in `:` can be made to look like it has
one fewer group than it does.
```{r}
codes_by_dialect("1:2:3:4:5:6:7")
```
### `bad_embedded_ipv4`
*Meaning.* The dotted-quad tail is not an address under this dialect's own
IPv4 rules.
*Why it is a signal.* It is the one place the two families meet, and each
dialect judges the tail by its own IPv4 grammar rather than by a shared one.
*Detection and precision.* Because the tail is judged by the *dialect's own*
rules, a tail that one dialect reads as an address is a `bad_embedded_ipv4` to
another — `::ffff:1.2.3.04` is fine to `pton` and not to `strict`. The IPv4
codes are not repeated on the outer literal; you get this code instead.
```{r}
codes_by_dialect("::ffff:1.2.3.999")
addr_pton("::ffff:1.2.3.04")
addr_strict("::ffff:1.2.3.04")
```
### `whitespace`
*Meaning.* The literal contains whitespace, which this dialect rejects
outright.
*Why it is a signal.* `inet_aton` stops at the first whitespace character and
ignores everything after it, so `"1.2.3.4 junk"` is an address to it.
`getaddrinfo` refuses whitespace-bearing input before either primitive sees it.
The code marks a gate, not a grammar.
*Detection and precision.* Look hard at the example: **`getaddrinfo` raises it
and `curl` does not**, even though `curl` composes `getaddrinfo`. `curl` tries
`aton` first, and `aton` answers, so the gate is never reached. A gate in a
composition is not a gate in everything built on it. The gate also covers the
address and not the zone ID — it tests the text before the `%`.
```{r}
codes_by_dialect("1.2.3.4 junk")
addr_curl("1.2.3.4 junk")
```
## The classify layer
These are statements about an address that parsed successfully, and each one
reports a rule from a specification. Every one of them is graded:
```{r}
classify <- registry[registry$layer == "classify", ]
classify[c("code", "strength", "rfc")]
```
Reporting only the MUST rules would collapse a spectrum into a binary, which is
the move raddr exists to refuse. So a rule stated in weaker language is still
reported, and the grade is what tells you not to act on it as though it were a
MUST.
Classify codes live in the `codes` field of the classification, which the print
method leaves out. One more helper puts them next to the block that matched:
```{r}
classify_view <- function(literals) {
d <- as.data.frame(addr_classify(addr_pton(literals)))
data.frame(
input = literals,
category = as.character(d$category),
block = d$block,
codes = vapply(d$codes, paste, character(1), collapse = ", ")
)
}
```
**The grade follows the rule's substance, not a keyword search.** The sources
do not agree about RFC 2119: RFC 4291 and RFC 8215 invoke it nowhere and state
their rules in lowercase or as a format diagram, while RFC 3056, 3927, 4193,
4380 and 6052 all invoke it. Grading by keyword would mark a binding format
definition as unspecified purely because of how its author wrote it down.
### `nat64_wk_embedded_not_global` — `must`
*Meaning.* The NAT64 well-known prefix carries a non-global embedded IPv4
address.
*Why it is a signal.* RFC 6052 §3.1 says translators MUST NOT translate such
packets and MUST drop them. An address like `64:ff9b::a9fe:a9fe` embeds
`169.254.169.254` inside the well-known prefix — a link-local address wrapped
in an IPv6 costume.
*Detection and precision.* The rule binds `64:ff9b::/96` **alone**, never a
network-specific prefix, and RFC 8215 §5 says it does not reach
`64:ff9b:1::/48`. raddr applies it exactly that narrowly.
```{r}
classify_view("64:ff9b::a9fe:a9fe")
```
### `sixtofour_embedded_not_global` — `must`
*Meaning.* The 6to4 `V4ADDR` is not in the format of a global unicast address.
*Why it is a signal.* RFC 3056 §9 requires such traffic to be silently
discarded by both encapsulators and decapsulators, so an address carrying a
private V4ADDR is one no correct implementation should be relaying.
*Detection and precision.* Spelled `sixtofour` because a code may not begin
with a digit; CPython's `ipaddress` names the same property the same way. The
code is about the *embedded* address, so the outer address still classifies
into `2002::/16` normally.
```{r}
classify_view("2002:a00:1::")
```
### `teredo_client_not_global` — `must`
*Meaning.* A global Teredo address embeds a non-global IPv4 client address.
*Why it is a signal.* RFC 4380 §4 requires a global Teredo address to embed a
global-scope unicast IPv4 as its client address.
*Detection and precision.* **The rule is conditional on the outer address.**
Only a *link-local* Teredo identifier MAY embed a private client address, so
the code fires on global Teredo addresses and not on link-local ones. Note also
that Teredo stores the client address ones-complemented; raddr undoes that
before judging it.
```{r}
classify_view("2001:0:4136:e378:8000:63bf:f5ff:fffe")
addr_embeddings(addr_pton("2001:0:4136:e378:8000:63bf:f5ff:fffe"))
```
### `link_local_outside_fe80_64` — `must`
*Meaning.* The address is inside the `fe80::/10` reservation but outside
`fe80::/64`.
*Why it is a signal.* The link-local format fixes the 54 bits after the prefix
to zero. `febf::1` matches the registry row without being a link-local address —
so a check written against the registry prefix alone accepts something the
format does not define.
*Detection and precision.* RFC 4291 §2.5.6 states this as a **format
definition** and invokes no RFC 2119 keyword anywhere. It is graded `must`
regardless, on substance: a format definition that fixes bits is not weaker
than a sentence containing the word MUST.
```{r}
classify_view(c("febf::1", "fe80::1"))
```
### `link_local_reserved_range` — `must`
*Meaning.* The address is in `169.254.0.0/24` or `169.254.255.0/24`.
*Why it is a signal.* RFC 3927 §2.1 reserves those 512 addresses for future
use and says they MUST NOT be selected by a host configuring an IPv4
link-local address.
*Detection and precision.* **The rule binds the host that picks an address,
not a packet carrying one.** Seeing this code means the address should not have
been self-assigned; it does not by itself mean a packet bearing it is invalid.
The rest of `169.254.0.0/16` is ordinary link-local space and raises nothing.
```{r}
classify_view(c("169.254.255.5", "169.254.1.5"))
```
### `ipv4_compatible_low_tail` — `may`
*Meaning.* The deprecated IPv4-compatible tail is below `1.0.0.0`, so it lands
in `0.0.0.0/8` and is not a host address.
*Why it is a signal.* `::2` looks like an IPv4-compatible IPv6 address carrying
`0.0.0.2`, which is not a host address at all.
*Detection and precision.* Graded `may` because implementations are "not
required to support this address type", so a consumer may reasonably discount
the reading entirely. It is reported rather than suppressed: the exclusion
raddr applies is the registry fact that `::` and `::1` are separate rows, not a
judgment about the rest of the range.
```{r}
classify_view("::2")
```
### `nat64_local_layout_unspecified` — `unspecified`
*Meaning.* raddr read RFC 6052 `/48` geometry under `64:ff9b:1::/48`, whose
syntax RFC 8215 leaves deliberately undefined.
*Why it is a signal.* This code exists to mark raddr's *own* extraction as
contested. RFC 8215 says nodes "must not make any assumptions regarding the
syntax or properties of those addresses (e.g., the existence and location of
embedded IPv4 addresses)" — lowercase, in a document that invokes no RFC 2119.
*Detection and precision.* The extraction is kept because deployments do use
that geometry, and the code is the disclosure that it is a convention rather
than something the prefix implies. This is the one code that tells you to trust
the accompanying `embeddings` field *less*.
```{r}
classify_view("64:ff9b:1::c000:201")
```
### `ula_l_bit_unset` — `unspecified`
*Meaning.* The ULA L bit is 0, so the address is in `fc00::/8` rather than the
locally assigned `fd00::/8`.
*Why it is a signal.* RFC 4193 §3.1 defines only `L = 1` and says `L = 0` "may
be defined in the future". No allocation mechanism ever was defined, so such an
address is unspecified rather than merely unusual — nothing says what it means
or who may use it.
*Detection and precision.* The address still classifies as `private`, because
`fc00::/7` is the registry row and that is a fact about the registry. The code
is the finer statement layered on top.
```{r}
classify_view(c("fc00::1", "fd00::1"))
```
## What a code does not prove
Three habits are worth keeping.
**A parse code is not a rejection.** It belongs to one dialect. `leading_zero`
on `strict` sits beside two other dialects that read the literal as two
different addresses. Always ask which dialect raised it:
```{r}
codes_by_dialect("0177.0.0.1")
```
**An empty code set is not an endorsement.** A dialect with no reading of a
literal raises nothing, because it has no objection to state — `aton` is silent
on every IPv6 literal in this vignette. Read `addr_outcome()` alongside the
codes, never the codes alone.
**A classify code is evidence, not a verdict.** raddr states what a
specification says about an address. It does not decide what you should do
about it, and `strength` exists precisely so a `may` and a `must` do not arrive
looking alike. Turning these facts into an allow-or-deny decision is policy,
depends on what you are defending, and belongs in a different package.
The registry itself records when each code entered the vocabulary, so a
consumer can tell an unfamiliar code from a new one:
```{r}
unique(registry$since)
```
Adding a code is an addition to raddr's API; removing one is a breaking change.