A service blocks internal destinations with an IPv4-only deny list (127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16); which IPv6 address forms slip past it, and how should the check work?
answer
- the same host, another spelling
- loopback and unspecified
- fe80 and fd
- ::ffff: wraps an IPv4 address
- parse to 128 bits first
basics
~10 sIPv6 loopback ::1, the unspecified ::, link-local fe80::/10, unique local fc00::/7 and IPv4-mapped forms such as ::ffff:127.0.0.1 all bypass it. Parse the address to 128 bits, unwrap mapped IPv4, then test the IPv6 blocks.
solid answer
~40 sAn IPv4-only deny list sees none of the IPv6 ways to name the same internal places. `::1` is the node itself; `::` is the unspecified address, which some operating systems, Linux among them, treat as the local host when used as a destination; `fe80::/10` reaches neighbours on the link; `fc00::/7` (in practice `fd00::/8`) is internal site addressing; and an **IPv4-mapped** address in `::ffff:0:0/96` carries a blocked IPv4 address, which can also be written in hex (`::ffff:7f00:1` is 127.0.0.1). String matching fails too, because one address has many legal spellings. The fix: parse to a 128-bit value with a real parser, unwrap mapped IPv4 and re-run the IPv4 check, deny the special-purpose IPv6 blocks (or allow only global unicast), and check the resolved address the connection actually uses.
code
pseudocode · 11 linescheck(text):
a = parse_to_128_bits(text) # rejects malformed input
if a in ::ffff:0:0/96:
return ipv4_rules(low32(a)) # ::ffff:a00:5 -> 10.0.0.5
if a in ::/96: return DENY # ::, ::1, IPv4-compatible
if a in fe80::/10: return DENY # link-local
if a in fec0::/10: return DENY # deprecated site-local
if a in fc00::/7: return DENY # unique local
if a in ff00::/8: return DENY # multicast
if a in 2001:db8::/32: return DENY # documentation
return ALLOWgo deeper
Know that IPv6 has its own loopback (::1), link-local (fe80) and unique local (fd) addresses, so an IPv4-only block list does not cover them.
Explain the IPv4-mapped form ::ffff:0:0/96, write the same address in dotted and hex form, and say why string comparison fails for IPv6.
Design the check: parse to 128 bits, unwrap mapped IPv4, deny or allow by block, and run it on the resolved address at connect time and on every redirect.
Make dual-stack parity a rule for every access control the organisation writes, and derive address policy from the IANA special-purpose registry rather than hand lists.
## The scenario A service fetches URLs supplied by users, and to stop it reaching internal systems it rejects destinations in `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` and `169.254.0.0/16`. The list is correct for IPv4. On a dual-stack host it is incomplete, because IPv6 has its own names for "this machine", "this link" and "this site", and because IPv6 can carry IPv4 addresses inside it. ## The IPv6 forms that slip past | Form | Block | What it reaches | |---|---|---| | `::1` | `::1/128` loopback | The node itself, like 127.0.0.1 | | `::` | `::/128` unspecified | Must not be a destination per RFC 4291, yet some operating systems, Linux among them, connect it to the local host | | `fe80::1%2` | `fe80::/10` link-local | Any neighbour on an attached link; the `%2` zone index picks the interface | | `fd12:3456:789a::10` | `fc00::/7` unique local | Internal services numbered with ULA | | `::ffff:127.0.0.1` or `::ffff:7f00:1` | `::ffff:0:0/96` IPv4-mapped | The IPv4 address in the low 32 bits | | `::127.0.0.1` | IPv4-compatible (deprecated by RFC 4291) | Legal text; what it reaches depends on the host | | `fec0::1` | `fec0::/10` site-local, deprecated by RFC 3879 | Legacy internal numbering on old networks | The **IPv4-mapped** row deserves attention. RFC 4291 defines `::ffff:0:0/96` as a way to represent IPv4 nodes' addresses as IPv6 addresses: 80 zero bits, 16 one bits, then the IPv4 address. On a host whose socket layer accepts IPv4 traffic through IPv6 sockets, a connection to `::ffff:10.0.0.5` is a connection to `10.0.0.5`. The low 32 bits can be written in dotted decimal or in hex, so `::ffff:10.0.0.5` and `::ffff:a00:5` are the same address (10 = `0x0a`, 0 = `00`, then `0005`). ## Why string matching cannot work RFC 5952 exists because one IPv6 address has many legal spellings, and parsers MUST accept all of them. A check that compares text or uses a regular expression for `::1` misses: - `0:0:0:0:0:0:0:1`, `0000::0001` and `::0:1`, all loopback; - uppercase hex, which is legal input; - the bracketed URI form `[::1]` and a zone suffix such as `%2`; - mixed notation versus hex for the embedded IPv4 part. Only a parsed 128-bit value has a single identity. ## How to write the check 1. **Resolve first, then check the address you will use.** A host name may resolve to AAAA records as well as A records; check every address the connection could use, at connect time, and again on each redirect. 2. **Parse with a real address parser** into a 128-bit value. Reject what does not parse, and decide explicitly whether zone indexes are acceptable (for an outbound fetcher, they almost never are). 3. **Unwrap embedded IPv4.** If the value is in `::ffff:0:0/96`, take the low 32 bits and run the existing IPv4 deny list on them. Treat the deprecated IPv4-compatible space the same way rather than letting it count as global. 4. **Test the IPv6 special-purpose blocks**: `::/128`, `::1/128`, `fe80::/10`, `fc00::/7`, `ff00::/8` (multicast) and the documentation prefix `2001:db8::/32`. Add `fec0::/10` as policy: RFC 4291 has new stacks treat it as global unicast, but nothing legitimate on the internet uses it and old networks may still number internal hosts from it. 5. **Prefer an allow rule where you can.** "Global unicast only" is sturdier than enumerating everything that is not. The IANA special-purpose registry, described in RFC 6890, records for each block whether it is globally reachable, which is the right source to derive the list from. 6. **Remember translation.** Where the network runs IPv6-to-IPv4 translation, addresses under its prefix (the well-known `64:ff9b::/96` of RFC 6052, or a local one) also lead to IPv4 destinations; unwrap them like mapped addresses. Run the IPv4-mapped unwrap before the IPv6 block tests, because `::ffff:0:0/96` matches none of the IPv6 blocks in step 4 and would otherwise pass as an ordinary address. ## Test inputs worth keeping A regression suite for the check should include each of these, all of which must be refused: - `::1`, `0:0:0:0:0:0:0:1`, `0000::0001` and `[::1]`, the loopback in four spellings; - `::`, the unspecified address; - `fe80::1` and `fe80::1%2`, link-local with and without a zone index; - `fd12:3456:789a::10`, a unique local address; - `::ffff:127.0.0.1` and `::ffff:7f00:1`, the mapped loopback in dotted and hex form; - `::ffff:10.0.0.5` and `::ffff:a00:5`, a mapped private address in both forms. A global address such as `2001:db8::1` belongs in the suite only as documentation-prefix input; real allowed cases should come from addresses the service genuinely needs to reach. ## The general lesson An access rule written against one address family protects only that family. Whenever a host is dual-stack, every IPv4 rule needs its IPv6 counterpart, and every check must run on parsed values, after resolution, not on the text a user typed.
- Is ::ffff:a00:5 the same destination as ::ffff:10.0.0.5?Yes. Both are the IPv4-mapped address of `10.0.0.5`: the low 32 bits are `0a00:0005`, and RFC 4291 allows the last 32 bits to be written either in hex or in dotted decimal. A check that only looks for dotted decimal after `::ffff:` misses the hex spelling.
- Why check the address after DNS resolution rather than the host name in the request?A name can resolve to any address, including `::1` or a `fd00::/8` address in its AAAA record, and the answer can change between the check and the connection. Checking the parsed address the socket actually connects to, at connect time and on every redirect, is the only point where the decision matches the traffic.
- Why is an allow rule for global unicast safer than a longer deny list?A deny list has to name every special block, and it silently fails open on any it forgets. Allowing only global unicast, and deriving the exceptions from the IANA special-purpose registry described in RFC 6890, fails closed when a new kind of address appears.
saying these in an interview costs you the question
- Blocking 127.0.0.0/8 also blocks the IPv6 loopback.
- A regular expression for ::1 is enough to catch IPv6 loopback.
- IPv4-mapped addresses never reach IPv4 hosts, so they are harmless.
- Unique local fd00::/8 addresses are public because their scope is global.
- Checking the host name before resolution is enough to block internal targets.