diff --git a/CHANGELOG.md b/CHANGELOG.md index 73fbf00..0482016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Cambios notables de la librería TypeScript y de la página. El proyecto usa ver ## Especificación 0.10, en la rama `v0.10` — sin versión +### NAT64, del borrador v0.13 (06-10-2026) + +- `testdata` se sincroniza con `datekeys-go` en `a83b44d`, el borrador v0.13, sin aprobar: añade `vectors/resolved_ip.json`, y ningún otro fichero cambia. `SPEC_VERSION` sigue en `0.12`. +- `checkResolvedIp` cuenta una dirección de NAT64 a la que resuelve un nombre, de `64:ff9b::/96` o del prefijo de la red (el parámetro `nat64`), por la IPv4 que lleva dentro, con los textos de `locator.CheckResolvedIP` de Go. `ipaddr.ts` exporta `isIpv4In6`. Un bloque nuevo de `vectors.test.ts` corre los 42 casos. + ### El localizador de `datekeys.capsule` (06-10-2026) El paquete `locator` de `datekeys-go` en `spec-v0.12` (§43 a §44.1), con los mismos checks en el mismo orden, los mismos códigos y los mismos textos de error, byte a byte, como lo portó `datekeys-dart` en sus partes 7a y 7b. diff --git a/README.md b/README.md index c676464..42a1584 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ El localizador sigue al paquete `locator` de Go en `spec-v0.12` también donde e - `parseInfo` no mira la cadena del stanza sellado, acepta una cabecera `age` sin cuerpo, e `infoExtension` no comprueba que el perfil esté pinneado; - `openSealed` lee como mucho 1 MiB del texto del localizador, y un defecto posterior queda oculto tras el error de `unmarshalLocator`. -`checkResolvedIp` rechaza hoy `64:ff9b::/96`, la de NAT64, como el §44.1 de la v0.12; es una función aparte, para cambiarla sola si una versión siguiente del spec lo cambia. +`checkResolvedIp` sigue el borrador v0.13 (§44.1, cambio 1 del §76), como `locator.CheckResolvedIP` de Go: una dirección de NAT64 a la que resuelve un nombre, de `64:ff9b::/96` o del prefijo de la red que la aplicación le pasa en `nat64`, cuenta por la IPv4 que lleva dentro. `vectors.test.ts` corre los 42 casos de `resolved_ip.json`. Diferencias de forma con Go, sin efecto en lo que se lee o se escribe: diff --git a/src/lib/dkc/ipaddr.ts b/src/lib/dkc/ipaddr.ts index 14e4f83..fe4b27c 100644 --- a/src/lib/dkc/ipaddr.ts +++ b/src/lib/dkc/ipaddr.ts @@ -41,8 +41,8 @@ export const isIpv4 = (a: IpAddress): boolean => a.bytes.length === 4; /** Whether `a` is an IPv6 address, IPv4-mapped ones included. */ export const isIpv6 = (a: IpAddress): boolean => a.bytes.length === 16; -// Whether the IPv6 address a is an IPv4-mapped one, of ::ffff:0:0/96. -function is4In6(a: IpAddress): boolean { +/** Whether `a` is an IPv4-mapped IPv6 address, of ::ffff:0:0/96, as netip's Is4In6. */ +export function isIpv4In6(a: IpAddress): boolean { for (let i = 0; i < 10; i++) if (a.bytes[i] !== 0) return false; return a.bytes[10] === 0xff && a.bytes[11] === 0xff; } @@ -59,7 +59,7 @@ export function ipString(a: IpAddress): string { const b = a.bytes; if (isIpv4(a)) return dotted(b, 0); const zoned = a.zone === '' ? '' : `%${a.zone}`; - if (is4In6(a)) return `::ffff:${dotted(b, 12)}${zoned}`; + if (isIpv4In6(a)) return `::ffff:${dotted(b, 12)}${zoned}`; const groups: number[] = []; for (let i = 0; i < 8; i++) groups.push((b[2 * i]! << 8) | b[2 * i + 1]!); // The first longest run of two or more groups of zeros, as appendTo6. diff --git a/src/lib/dkc/locator.test.ts b/src/lib/dkc/locator.test.ts index 774cbeb..0da835f 100644 --- a/src/lib/dkc/locator.test.ts +++ b/src/lib/dkc/locator.test.ts @@ -123,7 +123,7 @@ describe('IP addresses (locator-uris.json)', () => { } }); - it('checkResolvedIp checks the bytes of an address as publicIP of Go, IPv4-mapped and NAT64 addresses not public', () => { + it('checkResolvedIp checks the bytes of an address as publicIP of Go, IPv4-mapped addresses not public', () => { const cases = rows(uris, 'public'); expect(cases.length).toBe(868); for (const c of cases) { @@ -135,12 +135,20 @@ describe('IP addresses (locator-uris.json)', () => { const a = ipFromBytes(b); expect(ipString(a)).toBe(c[2]); expect(isPublicIp(a), ipString(a)).toBe(c[1]); + // An address of NAT64 counts by the IPv4 address it holds (spec + // v0.13): vectors.test.ts runs resolved_ip.json. + if (toHex(b).startsWith('0064ff9b0000000000000000')) continue; expect(errorText(() => checkResolvedIp(b))).toBe( c[1] === true ? '' : `locator: an https address whose name resolves to ${c[2] as string}, an IP address that is not public`, ); } expect(() => checkResolvedIp(Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 8, 8, 8, 8))).toThrow(LocatorError); - expect(() => checkResolvedIp(fromHex('0064ff9b000000000000000008080808'))).toThrow(LocatorError); + checkResolvedIp(fromHex('0064ff9b000000000000000008080808')); + expect(() => checkResolvedIp(fromHex('0064ff9b00000000000000000a000001'))).toThrow(LocatorError); + expect(errorText(() => checkResolvedIp(Uint8Array.of(8, 8, 8, 8), '::ffff:0.0.0.0/96'))).toBe('locator: the NAT64 prefix ::ffff:0.0.0.0/96 is not an IPv6 prefix'); + for (const bad of ['64:ff9b::', '64:ff9b::/129', '64:ff9b::/-1', '64:ff9b::/096', 'x/96', '64:ff9b::%eth0/96']) { + expect(() => checkResolvedIp(Uint8Array.of(8, 8, 8, 8), bad), bad).toThrow(RangeError); + } checkResolvedIp(Uint8Array.of(8, 8, 8, 8)); // An embedded IPv4 address with no room left, which netip refuses too. expect(parseIpAddress('1::2:3:4:5:6:7:1.2.3.4')).toBeUndefined(); diff --git a/src/lib/dkc/locator.ts b/src/lib/dkc/locator.ts index 5c92edb..1dcfda1 100644 --- a/src/lib/dkc/locator.ts +++ b/src/lib/dkc/locator.ts @@ -40,7 +40,7 @@ import { type Decoder, Encoder, MAX_SAFE_UINT, unmarshal } from './cbor.ts'; import { compactDateKey, type DateKey, parseDateKey } from './datekey.ts'; import { DateKeysError, withContext } from './errors.ts'; import { CAPSULE_ID, type Extension, type ExtensionRegistry, newExtension, NOTE_ID } from './extension.ts'; -import { ipFromBytes, ipString, isIpv4, isIpv6, isPublicIp, parseIpAddress } from './ipaddr.ts'; +import { type IpAddress, ipFromBytes, ipString, isIpv4, isIpv4In6, isIpv6, isPublicIp, parseIpAddress } from './ipaddr.ts'; import { checkNote, checkNoteData, MAX_NOTE_LEN } from './note.ts'; /** The most addresses of a locator (spec §44.1). */ @@ -573,19 +573,112 @@ function uvarint(b: readonly number[], at: number): [bigint, number] | undefined * redirections included (spec §44.1). Public is what an IP literal of an * address must be, as publicIP of Go: an IPv4 address outside the blocks of * §44.1, or an IPv6 address of 2000::/3 outside 2001::/23, 2001:db8::/32, - * 2002::/16 and 3fff::/20. An IPv6 address that holds an IPv4 one is not, - * IPv4-mapped and NAT64 (64:ff9b::/96) included: an IPv4 address is checked - * as its 4 bytes. + * 2002::/16 and 3fff::/20. An IPv4-mapped address is not public: an IPv4 + * address is checked as its 4 bytes. * - * Throws a LocatorError for an address that is not public, with the text of - * datekeys-dart's checkResolvedIp, and a RangeError for any other length. Go - * has no such function: a reader of the reference does not download. + * On an IPv6-only network with DNS64 and NAT64, a name that has only IPv4 + * addresses resolves to an IPv6 address that holds one (RFC 6052): one of the + * well-known prefix 64:ff9b::/96 is public when the IPv4 address in its last + * 32 bits is (spec v0.13, §44.1). `nat64` is the NAT64 prefix of the network, + * in the notation of netip.ParsePrefix (`64:ff9b:1::/48`), which the + * application discovers with RFC 7050, or undefined for none: an address in + * it is public only when the IPv4 address it holds, at the positions of RFC + * 6052, is, even when the prefix is a public one. The prefix must have one of + * the lengths of RFC 6052 and lie in 64:ff9b::/16 or be a public IPv6 prefix. + * + * Throws a LocatorError for an address that is not public or a prefix that + * cannot be one of NAT64, with the texts of locator.CheckResolvedIP of Go, + * and a RangeError for an address of another length or a prefix that does + * not parse. */ -export function checkResolvedIp(ip: Uint8Array): void { +export function checkResolvedIp(ip: Uint8Array, nat64?: string): void { const a = ipFromBytes(ip); + let network: Nat64Prefix | undefined; + if (nat64 !== undefined) { + network = parseNat64Prefix(nat64); + checkNat64Prefix(network); + } + // The prefixes of NAT64 decide first: the prefix of a network may be a + // public one, and an address in it reaches the IPv4 address it holds, + // which may be private. + for (const p of network === undefined ? [NAT64_WELL_KNOWN] : [NAT64_WELL_KNOWN, network]) { + if (!prefixContains(p, a)) continue; + const v4 = nat64Ipv4(p, a); + if (v4 === undefined) { + throw fail(`an https address whose name resolves to ${ipString(a)}, an address of the NAT64 prefix ${prefixString(p)} whose bits 64 to 71 are not zero`); + } + if (!isPublicIp(v4)) { + throw fail(`an https address whose name resolves to ${ipString(a)}, an address of NAT64 that holds ${ipString(v4)}, an IP address that is not public`); + } + return; + } if (!isPublicIp(a)) throw fail(`an https address whose name resolves to ${ipString(a)}, an IP address that is not public`); } +// A NAT64 prefix (RFC 6052), as the netip.Prefix that CheckResolvedIP of Go +// receives: the address as written and its length, the bits after the length +// kept, so that checkNat64Prefix can refuse them. +interface Nat64Prefix { + readonly addr: IpAddress; + readonly bits: number; +} + +// netip.ParsePrefix: an address without a zone, "/", and a length in +// decimal without a sign or leading zeros, up to the bits of the address. +function parseNat64Prefix(s: string): Nat64Prefix { + const slash = s.lastIndexOf('/'); + const addr = slash < 0 ? undefined : parseIpAddress(s.slice(0, slash)); + const len = slash < 0 ? '' : s.slice(slash + 1); + const bits = /^(0|[1-9][0-9]{0,2})$/.test(len) ? Number(len) : -1; + if (addr === undefined || addr.zone !== '' || bits < 0 || bits > addr.bytes.length * 8) { + throw new RangeError(`locator: ${JSON.stringify(s)} is not an IP prefix`); + } + return { addr, bits }; +} + +const NAT64_WELL_KNOWN = parseNat64Prefix('64:ff9b::/96'); +const NAT64_BLOCK = parseNat64Prefix('64:ff9b::/16'); + +const prefixString = (p: Nat64Prefix): string => `${ipString(p.addr)}/${p.bits}`; + +// netip.Prefix.Contains: an address of the same family, without a zone, +// whose first bits are those of the prefix. +function prefixContains(p: Nat64Prefix, a: IpAddress): boolean { + const pb = p.addr.bytes; + const b = a.bytes; + if (a.zone !== '' || b.length !== pb.length) return false; + for (let i = 0; i < p.bits; i++) { + const m = 0x80 >> (i & 7); + if ((b[i >> 3]! & m) !== (pb[i >> 3]! & m)) return false; + } + return true; +} + +// checkNAT64Prefix of Go. +function checkNat64Prefix(p: Nat64Prefix): void { + const b = p.addr.bytes; + if (!isIpv6(p.addr) || isIpv4In6(p.addr)) throw fail(`the NAT64 prefix ${prefixString(p)} is not an IPv6 prefix`); + for (let i = p.bits; i < 128; i++) { + if ((b[i >> 3]! & (0x80 >> (i & 7))) !== 0) throw fail(`the NAT64 prefix ${prefixString(p)} has bits set after its length`); + } + if (![32, 40, 48, 56, 64, 96].includes(p.bits)) throw fail(`the NAT64 prefix ${prefixString(p)} is not of 32, 40, 48, 56, 64 or 96 bits (RFC 6052)`); + if (!prefixContains(NAT64_BLOCK, p.addr) && !isPublicIp(p.addr)) { + throw fail(`the NAT64 prefix ${prefixString(p)} is neither in 64:ff9b::/16 nor a public IPv6 prefix`); + } +} + +// nat64IPv4 of Go: the IPv4 address that `a`, an address of the prefix `p`, +// holds at the positions of RFC 6052, section 2.2, the 32 bits after the +// prefix without bits 64 to 71, which must be zero; undefined if they are +// not. +function nat64Ipv4(p: Nat64Prefix, a: IpAddress): IpAddress | undefined { + const b = a.bytes; + if (p.bits < 96 && b[8] !== 0) return undefined; + const v4: number[] = []; + for (let i = p.bits >> 3; v4.length < 4; i++) if (i !== 8) v4.push(b[i]!); + return ipFromBytes(Uint8Array.from(v4)); +} + // --------------------------------------------------------------------------- // The locator diff --git a/src/lib/dkc/vectors.test.ts b/src/lib/dkc/vectors.test.ts index 31583dd..cc78cfc 100644 --- a/src/lib/dkc/vectors.test.ts +++ b/src/lib/dkc/vectors.test.ts @@ -50,6 +50,7 @@ import { inspect, inspectJSON, inspectView } from './inspect.ts'; import { addressHost, BLOCK, + checkResolvedIp, checkURI, LocatorError, marshalLocator, @@ -59,6 +60,7 @@ import { unmarshalLocator, usableAddresses, } from './locator.ts'; +import { parseIpAddress } from './ipaddr.ts'; import { checkNoteData, newNote, NOTE_ID, publicNote, unusableNote } from './note.ts'; import { open } from './open.ts'; import { checkPath, nfd, pathKey } from './pathrule.ts'; @@ -1550,6 +1552,54 @@ describe('vectors/locator.json', () => { }); }); +// --------------------------------------------------------------------------- +// vectors/resolved_ip.json +// +// The IP address that the name of an https address of a locator resolves to, +// the NAT64 prefix of the network or none, and whether a reader may connect +// (spec §44.1 of the draft v0.13): ok, or the text of Go's +// locator.CheckResolvedIP. + +interface ResolvedIpVector { + name: string; + ip: string; + nat64: string; + error?: string; +} + +describe('vectors/resolved_ip.json', () => { + const f = load('vectors/resolved_ip.json', (json) => { + const o = object(json, 'resolved_ip.json'); + keys(o, 'resolved_ip.json', ['spec', 'description', 'cases']); + checkSpec(o, 'resolved_ip.json'); + return array(o.cases, 'cases').map((v, i): ResolvedIpVector => { + const at = `cases[${i}]`; + const c = object(v, at); + const res = str(c.result, `${at}.result`); + if (res !== 'ok' && res !== 'error') throw new FormatError(`${at}.result`, 'not ok or error'); + keys(c, at, ['name', 'ip', 'nat64', 'result', ...(res === 'ok' ? [] : ['error'])]); + const r: ResolvedIpVector = { name: str(c.name, `${at}.name`), ip: str(c.ip, `${at}.ip`), nat64: str(c.nat64, `${at}.nat64`) }; + if (res === 'error') r.error = str(c.error, `${at}.error`); + return r; + }); + }); + if (f === undefined) return; + + it('has the cases of README, that pass and that fail', () => { + expect(f.length).toBeGreaterThanOrEqual(40); + expect(f.some((c) => c.error === undefined)).toBe(true); + expect(f.some((c) => c.error !== undefined)).toBe(true); + }); + + it.each(f.map((c) => [c.name, c] as const))('%s', (_name, c) => { + const ip = parseIpAddress(c.ip); + expect(ip, c.ip).toBeDefined(); + const check = (): void => checkResolvedIp(ip!.bytes, c.nat64 === '' ? undefined : c.nat64); + if (c.error === undefined) expect(check).not.toThrow(); + else expect(check).toThrow(new LocatorError(c.error)); + }); +}); + // The vector files the blocks above run; a new export needs its own block. const VECTOR_FILES = [ 'vectors/cbor.json', @@ -1565,6 +1615,7 @@ const VECTOR_FILES = [ 'vectors/paths.json', 'vectors/profile_quicknet.json', 'vectors/quicknet_rounds.json', + 'vectors/resolved_ip.json', 'vectors/security.json', 'vectors/security_cms.json', 'vectors/tlock_ibe.json', diff --git a/testdata/README.md b/testdata/README.md index 314ca5b..9e04cd8 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -56,6 +56,7 @@ Conventions for every file: | `vectors/security_cms.json` | security areas with a signature of `alg` 2 or a seal of `seal_type` 2, each with its context, verdicts, results and lines | §29.7, §29.10, §29.11 | | `vectors/ed25519_strict.json` | Ed25519 signatures and the result of the strict profile of the author signature | §29.9 | | `vectors/note.json` | the data of the public note and the result of its rules | §24.1, §29.6 | +| `vectors/resolved_ip.json` | the IP address a name of a locator resolves to, NAT64 included, and whether a reader may connect | §44.1 (draft v0.13) | | `vectors/wordkey.json` | the key of words: the words of a text, what a writer refuses, and the identity the words derive | §38.1, §64 | | `vectors/locator.json` | the extension `datekeys.capsule` of a `.dkk`, its envelope and its locator, and what a reader rejects and uses of them | §44.1, §64 | | `vectors/mutations.json` | the mutation corpus: the 178 mutations of §64 and further cases | §63, §64 | @@ -585,6 +586,28 @@ feed, a space at either end, U+202E, U+200B, a byte order mark, a noncharacter, a byte that is not UTF-8 and the UTF-8 of a lone surrogate, refused. +## `vectors/resolved_ip.json` + +The IP address that the name of an https address of a locator resolves to, +which a reader checks on every connection (spec §44.1 of the draft v0.13): +`ip`, `nat64`, the NAT64 prefix of the network that the reader knows, or "" +for none, and `result`, `ok`, or `error` with the text of the reference in +`error`. + +```json +{ "name": "the well-known prefix with 192.168.1.10", "ip": "64:ff9b::c0a8:10a", "nat64": "", "result": "error", "error": "locator: an https address whose name resolves to 64:ff9b::c0a8:10a, an address of NAT64 that holds 192.168.1.10, an IP address that is not public" } +``` + +A public address is accepted. An address of NAT64 (RFC 6052) of +`64:ff9b::/96`, or of the prefix of the network, counts by the IPv4 address +it holds, at the positions of RFC 6052: the cases put a public one and one of +several blocks that are not public in each, and the prefix of the network in +each length of RFC 6052. A prefix of another length, with bits after its +length, outside `64:ff9b::/16` and the public IPv6 addresses, or of IPv4, is +refused. Among the addresses that are not public: IPv4-mapped, 6to4, Teredo, +link-local, unique local, loopback, and the local-use prefix of RFC 8215 +without the prefix of the network. + ## `vectors/wordkey.json` The key of words of spec §38.1, the cases that §64 of v0.11 asks for, in diff --git a/testdata/SOURCE.json b/testdata/SOURCE.json index f019680..d61f364 100644 --- a/testdata/SOURCE.json +++ b/testdata/SOURCE.json @@ -1,8 +1,8 @@ { "module": "g.activething.com/go/DateKeys", - "commit": "fe405e2348744f50e54c72e35ae10470a01dc552", + "commit": "a83b44d1c88ca253f7fc5340c58ef38a170a6f2c", "files": { - "README.md": "0c4244bf5ec7fe3865bef4dd2f69e4370184bc95060339d82422a4ff29d44b75", + "README.md": "9832dcbef136c29ae5147e99e041e5419b7b3e3f3a24c12fe0284fe3f62a4367", "fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4", "fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59", "fixtures/empty_payload.json": "a8586332e32de5e052e48a420a8a61ec81b1274c1867527f43c37b5d6d9ba4ea", @@ -132,6 +132,7 @@ "vectors/paths.json": "bfdbcc9ceb8f6f1220bc998e00b6d32061e49bb8e6fcd3d903a43521b86e3388", "vectors/profile_quicknet.json": "c5b9a57db1e9c64a226c08ab4a51ffcb2e29599a74b5b145308a13ba8f195445", "vectors/quicknet_rounds.json": "b053c423d75a602d23777aa9fe0ec7860dbfb6ee42275a88a880731338c4ffe4", + "vectors/resolved_ip.json": "cc1112aae424bc6f358a39211d7e5cc5a9a3b04c27c40e9e4070a74524055b72", "vectors/security.json": "41da683fb4f0275c903d7e79029fed885c7b94e03c0160ab94bad6acf325f0b5", "vectors/security_cms.json": "13e0deece70f640e4507adc33c9df25545fc30a4b0a543cff07dcdddc8923ea4", "vectors/tlock_ibe.json": "fdc846000dd4da5fcb0d976994e07e4acf335819cf434a2badc973e90aa5dbfe", diff --git a/testdata/vectors/resolved_ip.json b/testdata/vectors/resolved_ip.json new file mode 100644 index 0000000..be49fd5 --- /dev/null +++ b/testdata/vectors/resolved_ip.json @@ -0,0 +1,287 @@ +{ + "spec": "0.12", + "description": "The IP address that the name of an https address of a locator resolves to, and whether a reader may connect (spec v0.13, 44.1): a public address, or an address of NAT64 (RFC 6052) of 64:ff9b::/96 or of the NAT64 prefix of the network, whose IPv4 address inside is public. See testdata/README.md.", + "cases": [ + { + "name": "a public IPv4 address", + "ip": "203.0.114.5", + "nat64": "", + "result": "ok" + }, + { + "name": "a public IPv6 address", + "ip": "2a01:4f8::1", + "nat64": "", + "result": "ok" + }, + { + "name": "a private IPv4 address", + "ip": "192.168.1.10", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 192.168.1.10, an IP address that is not public" + }, + { + "name": "loopback", + "ip": "127.0.0.1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 127.0.0.1, an IP address that is not public" + }, + { + "name": "IPv6 loopback", + "ip": "::1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to ::1, an IP address that is not public" + }, + { + "name": "an IPv6 link-local address", + "ip": "fe80::1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to fe80::1, an IP address that is not public" + }, + { + "name": "an IPv6 unique local address", + "ip": "fd00::1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to fd00::1, an IP address that is not public" + }, + { + "name": "an IPv4-mapped address of a public IPv4 address", + "ip": "::ffff:203.0.114.5", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to ::ffff:203.0.114.5, an IP address that is not public" + }, + { + "name": "6to4", + "ip": "2002:cb00:7205::1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 2002:cb00:7205::1, an IP address that is not public" + }, + { + "name": "Teredo", + "ip": "2001:0:cb00:7205::1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 2001:0:cb00:7205::1, an IP address that is not public" + }, + { + "name": "the well-known prefix with a public IPv4 address", + "ip": "64:ff9b::cb00:7205", + "nat64": "", + "result": "ok" + }, + { + "name": "the well-known prefix with 8.8.8.8", + "ip": "64:ff9b::808:808", + "nat64": "", + "result": "ok" + }, + { + "name": "the well-known prefix with an IPv4 address of 10.0.0.0/8", + "ip": "64:ff9b::a00:1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::a00:1, an address of NAT64 that holds 10.0.0.1, an IP address that is not public" + }, + { + "name": "the well-known prefix with 192.168.1.10", + "ip": "64:ff9b::c0a8:10a", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::c0a8:10a, an address of NAT64 that holds 192.168.1.10, an IP address that is not public" + }, + { + "name": "the well-known prefix with loopback", + "ip": "64:ff9b::7f00:1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::7f00:1, an address of NAT64 that holds 127.0.0.1, an IP address that is not public" + }, + { + "name": "the well-known prefix with 169.254.169.254", + "ip": "64:ff9b::a9fe:a9fe", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::a9fe:a9fe, an address of NAT64 that holds 169.254.169.254, an IP address that is not public" + }, + { + "name": "the well-known prefix with 100.64.0.1", + "ip": "64:ff9b::6440:1", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::6440:1, an address of NAT64 that holds 100.64.0.1, an IP address that is not public" + }, + { + "name": "the well-known prefix with 0.0.0.0", + "ip": "64:ff9b::", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::, an address of NAT64 that holds 0.0.0.0, an IP address that is not public" + }, + { + "name": "the well-known prefix with 255.255.255.255", + "ip": "64:ff9b::ffff:ffff", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::ffff:ffff, an address of NAT64 that holds 255.255.255.255, an IP address that is not public" + }, + { + "name": "the well-known prefix with a documentation address", + "ip": "64:ff9b::cb00:7105", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::cb00:7105, an address of NAT64 that holds 203.0.113.5, an IP address that is not public" + }, + { + "name": "an address of 64:ff9b::/16 outside 64:ff9b::/96", + "ip": "64:ff9b::1:cb00:7205", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b::1:cb00:7205, an IP address that is not public" + }, + { + "name": "the local-use prefix of RFC 8215 without the prefix of the network", + "ip": "64:ff9b:1::cb00:7205", + "nat64": "", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b:1::cb00:7205, an IP address that is not public" + }, + { + "name": "the well-known prefix, given as the prefix of the network", + "ip": "64:ff9b::cb00:7205", + "nat64": "64:ff9b::/96", + "result": "ok" + }, + { + "name": "the well-known prefix with another prefix of the network", + "ip": "64:ff9b::cb00:7205", + "nat64": "64:ff9b:1::/48", + "result": "ok" + }, + { + "name": "the local-use prefix of RFC 8215, /48", + "ip": "64:ff9b:1:cb00:72:500::", + "nat64": "64:ff9b:1::/48", + "result": "ok" + }, + { + "name": "the local-use prefix, /48, with a private IPv4 address", + "ip": "64:ff9b:1:c0a8:1:a00::", + "nat64": "64:ff9b:1::/48", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b:1:c0a8:1:a00::, an address of NAT64 that holds 192.168.1.10, an IP address that is not public" + }, + { + "name": "the local-use prefix, /48, with bits 64 to 71 set", + "ip": "64:ff9b:1:cb00:172:500::", + "nat64": "64:ff9b:1::/48", + "result": "error", + "error": "locator: an https address whose name resolves to 64:ff9b:1:cb00:172:500::, an address of the NAT64 prefix 64:ff9b:1::/48 whose bits 64 to 71 are not zero" + }, + { + "name": "a public prefix of the network, /96", + "ip": "2a01:4f8:c0:64::cb00:7205", + "nat64": "2a01:4f8:c0:64::/96", + "result": "ok" + }, + { + "name": "a public prefix of the network, /96, with a private IPv4 address", + "ip": "2a01:4f8:c0:64::c0a8:10a", + "nat64": "2a01:4f8:c0:64::/96", + "result": "error", + "error": "locator: an https address whose name resolves to 2a01:4f8:c0:64::c0a8:10a, an address of NAT64 that holds 192.168.1.10, an IP address that is not public" + }, + { + "name": "a public prefix of the network, /96, with loopback", + "ip": "2a01:4f8:c0:64::7f00:1", + "nat64": "2a01:4f8:c0:64::/96", + "result": "error", + "error": "locator: an https address whose name resolves to 2a01:4f8:c0:64::7f00:1, an address of NAT64 that holds 127.0.0.1, an IP address that is not public" + }, + { + "name": "a public prefix of the network, /32", + "ip": "2a01:4f8:cb00:7205::", + "nat64": "2a01:4f8::/32", + "result": "ok" + }, + { + "name": "a public prefix of the network, /40", + "ip": "2a01:4f8:c0cb:72:5::", + "nat64": "2a01:4f8:c000::/40", + "result": "ok" + }, + { + "name": "a public prefix of the network, /56", + "ip": "2a01:4f8:c0:64cb:0:7205::", + "nat64": "2a01:4f8:c0:6400::/56", + "result": "ok" + }, + { + "name": "a public prefix of the network, /64", + "ip": "2a01:4f8:c0:64:cb:72:500:0", + "nat64": "2a01:4f8:c0:64::/64", + "result": "ok" + }, + { + "name": "a public prefix of the network, /64, with a private IPv4 address", + "ip": "2a01:4f8:c0:64:c0:a801:a00:0", + "nat64": "2a01:4f8:c0:64::/64", + "result": "error", + "error": "locator: an https address whose name resolves to 2a01:4f8:c0:64:c0:a801:a00:0, an address of NAT64 that holds 192.168.1.10, an IP address that is not public" + }, + { + "name": "a public address outside the prefix of the network", + "ip": "2a01:4f8::1", + "nat64": "64:ff9b:1::/48", + "result": "ok" + }, + { + "name": "a prefix of the network of 80 bits", + "ip": "64:ff9b:1::cb00:7205", + "nat64": "64:ff9b:1::/80", + "result": "error", + "error": "locator: the NAT64 prefix 64:ff9b:1::/80 is not of 32, 40, 48, 56, 64 or 96 bits (RFC 6052)" + }, + { + "name": "a link-local prefix of the network", + "ip": "fe80::cb00:7205", + "nat64": "fe80::/96", + "result": "error", + "error": "locator: the NAT64 prefix fe80::/96 is neither in 64:ff9b::/16 nor a public IPv6 prefix" + }, + { + "name": "a unique local prefix of the network", + "ip": "fd00::cb00:7205", + "nat64": "fd00::/96", + "result": "error", + "error": "locator: the NAT64 prefix fd00::/96 is neither in 64:ff9b::/16 nor a public IPv6 prefix" + }, + { + "name": "a prefix of the network of 2001:db8::/32", + "ip": "2001:db8::cb00:7205", + "nat64": "2001:db8::/96", + "result": "error", + "error": "locator: the NAT64 prefix 2001:db8::/96 is neither in 64:ff9b::/16 nor a public IPv6 prefix" + }, + { + "name": "a prefix of the network with bits after its length", + "ip": "64:ff9b::cb00:7205", + "nat64": "64:ff9b::1/96", + "result": "error", + "error": "locator: the NAT64 prefix 64:ff9b::1/96 has bits set after its length" + }, + { + "name": "an IPv4 prefix of the network", + "ip": "203.0.114.5", + "nat64": "203.0.114.0/24", + "result": "error", + "error": "locator: the NAT64 prefix 203.0.114.0/24 is not an IPv6 prefix" + } + ] +}