fix(relay-bench): judge 6to4 and NAT64-local by the embedded v4, refuse Teredo, scrub the pair line and sibling scripts

This commit is contained in:
Jinwoo-H
2026-09-07 15:44:34 -04:00
parent c063b44b3d
commit ad4101bb49
5 changed files with 43 additions and 19 deletions
@@ -2,8 +2,9 @@
// sample count, spread rule, and Node fetch, and prints why each region passed or failed.
import { pathToFileURL } from 'node:url'
import {
classifyPublicHttpsOrigin,
LIVE_ENV_VAR,
classifyPublicHttpsOrigin,
describeUntrustedText,
parseArgs,
requireBoundedInteger,
requireDirector,
@@ -112,7 +113,7 @@ async function main() {
console.error(
timedOut
? `director ${director}/v1/regions did not answer within ${CATALOG_TIMEOUT_MS} ms`
: `director ${director}/v1/regions failed: ${err.message}`
: `director ${director}/v1/regions failed: ${describeUntrustedText(err.code ?? err.message)}`
)
process.exitCode = 1
return
@@ -190,28 +190,39 @@ function ipv6ToBytes(host) {
return bytes
}
function v4At(bytes, offset) {
return (
((bytes[offset] << 24) >>> 0) +
(bytes[offset + 1] << 16) +
(bytes[offset + 2] << 8) +
bytes[offset + 3]
)
}
function isPublicIpv6(bytes) {
const leadingZeros = bytes.slice(0, 10).every((byte) => byte === 0)
if (leadingZeros && bytes[10] === 0xff && bytes[11] === 0xff) {
return isPublicIpv4(
((bytes[12] << 24) >>> 0) + (bytes[13] << 16) + (bytes[14] << 8) + bytes[15]
)
return isPublicIpv4(v4At(bytes, 12))
}
if (leadingZeros && bytes[10] === 0 && bytes[11] === 0) {
// Covers :: and ::1 as well as the deprecated v4-compatible form.
return false
}
// NAT64 (64:ff9b::/96) reaches the embedded v4 address, so judge that address.
// Transition prefixes that reach an embedded v4 address: judge that address.
// NAT64 64:ff9b::/96 and 64:ff9b:1::/48 (v4 in the last 32 bits), 6to4 2002::/16
// (v4 in bits 16-47). Teredo 2001:0::/32 embeds the client v4 inverted, so it is refused.
const nat64 = bytes[0] === 0x00 && bytes[1] === 0x64 && bytes[2] === 0xff && bytes[3] === 0x9b
if (
bytes[0] === 0x00 &&
bytes[1] === 0x64 &&
bytes[2] === 0xff &&
bytes[3] === 0x9b &&
bytes.slice(4, 12).every((byte) => byte === 0)
nat64 &&
(bytes.slice(4, 12).every((byte) => byte === 0) || (bytes[4] === 0 && bytes[5] === 1))
) {
return isPublicIpv4(
((bytes[12] << 24) >>> 0) + (bytes[13] << 16) + (bytes[14] << 8) + bytes[15]
)
return isPublicIpv4(v4At(bytes, 12))
}
if (bytes[0] === 0x20 && bytes[1] === 0x02) {
return isPublicIpv4(v4At(bytes, 2))
}
if (bytes[0] === 0x20 && bytes[1] === 0x01 && bytes[2] === 0 && bytes[3] === 0) {
return false
}
if ((bytes[0] & 0xfe) === 0xfc || bytes[0] === 0xff) {
return false
@@ -168,6 +168,10 @@ describe('classifyPublicHttpsOrigin', () => {
['not a url', 'not a URL'],
['https://[64:ff9b::7f00:1]/', 'loopback, link-local, or private'],
['https://[64:ff9b::a9fe:a9fe]/', 'loopback, link-local, or private'],
['https://[64:ff9b:1::7f00:1]/', 'loopback, link-local, or private'],
['https://[2002:7f00:1::]/', 'loopback, link-local, or private'],
['https://[2002:c0a8:101::1]/', 'loopback, link-local, or private'],
['https://[2001:0:4136:e378:8000:63bf:3fff:fdd2]/', 'loopback, link-local, or private'],
['', 'missing origin']
])('refuses %s', (value, reason) => {
const verdict = classifyPublicHttpsOrigin(value)
@@ -175,8 +179,11 @@ describe('classifyPublicHttpsOrigin', () => {
expect(verdict.reason).toContain(reason)
})
it('accepts a NAT64 address that embeds a public v4 address', () => {
it('accepts transition addresses that embed a public v4 address', () => {
expect(classifyPublicHttpsOrigin('https://[64:ff9b::808:808]/').ok).toBe(true)
expect(classifyPublicHttpsOrigin('https://[2002:808:808::]/').ok).toBe(true)
// A 2001: address outside the Teredo /32 is ordinary global unicast.
expect(classifyPublicHttpsOrigin('https://[2001:db8::1]/').ok).toBe(true)
})
it('never echoes control bytes from a refused value, since the desktop chose it', () => {
@@ -6,6 +6,7 @@ import { performance } from 'node:perf_hooks'
import { pathToFileURL } from 'node:url'
import {
LIVE_ENV_VAR,
describeUntrustedText,
parseArgs,
requireBoundedInteger,
requireDirector,
@@ -46,7 +47,9 @@ async function timeResolve(director, relayHostId) {
return {
ms: Math.round(performance.now() - started),
status: null,
error: timedOut ? `timeout after ${RESOLVE_TIMEOUT_MS} ms` : err.message
error: timedOut
? `timeout after ${RESOLVE_TIMEOUT_MS} ms`
: describeUntrustedText(err.code ?? err.message)
}
}
}
@@ -91,7 +94,7 @@ function timeCellHello(cell, relayHostId) {
})
ws.on('message', (message) => done({ hello: message.toString().slice(0, 80) }))
ws.on('close', (code, reason) => done({ close: code, reason: reason.toString() }))
ws.on('error', (err) => done({ error: err.message }))
ws.on('error', (err) => done({ error: describeUntrustedText(err.code ?? err.message) }))
})
}
@@ -170,7 +170,7 @@ export function dialRelay({
const msg = JSON.parse(text)
if (msg.type !== 'e2ee_authenticated') {
// Type only: the plaintext is the desktop's and would land in row.error.
throw new Error(`auth rejected: desktop sent ${JSON.stringify(msg.type ?? null)}`)
throw new Error(`auth rejected: desktop sent ${describeUntrustedText(msg.type)}`)
}
mark('e2eeAuthenticated')
stage = 'ready'
@@ -401,7 +401,9 @@ async function pair(pairingUrl, statePath) {
const resumeToken = b64url(nacl.randomBytes(32))
const resumeTokenHash = b64url(sha256(utf8(resumeToken)))
const installReqId = `install-${b64url(nacl.randomBytes(12))}`
console.log(`pair: dialing ${relay.cellUrl} host=${relay.relayHostId}`)
// The offer is operator-pasted text: the vetted origin, not the raw URL, and the host id
// through the same scrub as any other peer-chosen string.
console.log(`pair: dialing ${verdict.origin} host=${describeUntrustedText(relay.relayHostId)}`)
const dial = await dialRelay({
cellUrl: relay.cellUrl,
relayHostId: relay.relayHostId,