Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/experimental-dns-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Experimental P2P DNS relay

This document describes a private proof-of-concept extension. It is disabled by
default and does not claim Handshake protocol assignments.

The temporary capability is service bit `0x40000000`; the temporary request and
response packet types are `0xf0` and `0xf1`. Version services occupy eight bytes
on the wire (the current JavaScript codec uses the low 32-bit word). Frames use
the existing four-byte magic, one-byte type, and four-byte payload length.
Legacy peers decode other one-byte types as unknown packets and continue the
connection.

The payloads and limits are defined in the sibling browser repository's
`docs/experimental-hns-p2p-dns-relay.md` and duplicated in the deterministic
fixtures under `fixtures/experimental-dns-relay/`. In short, requests contain a
nonzero little-endian u64 ID, little-endian u16 query length, and at most 4,096
raw DNS bytes. Responses contain that ID, a one-byte transport status,
little-endian u16 response length, and at most 65,535 raw DNS bytes. Packets
reject trailing data. Canonical responses with an unassigned nonzero status are
preserved as unusable exchange results with an empty body, rather than treated
as malformed peer frames.

The node option is `--experimental-dns-relay`. Advertisement requires the flag,
a synchronized chain, an initialized/ready recursive server, and available
global capacity. The service exists only on an established HNS peer connection;
it creates no DNS, HTTP, or management listener.

Requests must be standard recursive IN queries with one HNS-rooted question,
RD and EDNS DO, bounded EDNS, and no ECS. ANY, AXFR, IXFR, UPDATE, NOTIFY,
TKEY/TSIG abuse, malformed input, ICANN roots, private infrastructure names, and
names absent from current HNS state are refused. No destination address or port
is accepted. A dedicated, bounded relay resolver follows the node's local HNS
root stub, always sets Checking Disabled on its authority queries so locally
validatable DNSSEC material is not suppressed, keeps its own cache, and permits
only public port-53 authority endpoints throughout recursion. Its own AD result
is advisory and never trusted by the requester. The ordinary node resolver and
cache are not modified or exposed to relay requests.

The test-only `--experimental-dns-relay-allow-private-authorities` option may
be combined with `--experimental-dns-relay` on regtest only. It admits RFC1918
IPv4 authority endpoints on port 53 so an isolated regtest topology can serve a
delegated zone without pretending its Docker address is public. The dedicated
resolver remains IPv4-only. The option still rejects loopback, link-local,
carrier-grade NAT, metadata, benchmarking/documentation, multicast,
unspecified, IPv6, and non-port-53 endpoints. Supplying the option on another
network, or without enabling the experimental relay, aborts node construction.
It is not a canary or production option.

Allowed types are A, AAAA, CNAME, DNAME, NS, SOA, DS, DNSKEY, RRSIG, NSEC,
NSEC3, NSEC3PARAM, TLSA, SVCB, HTTPS, TXT, MX, SRV, and CAA.

Default bounds are 20 request attempts per second per peer connection with a
burst of 40, 200 attempts per second across all peer connections with a burst
of 400, 50 outbound requests per second per external authority address with a
burst of 100, 16 in-flight per peer, 64 globally, and a three-second backend
deadline. Authority buckets are capped at 1,024 entries.
Disconnect, timeout, success, and every error path release logical pending
state. Timed-out or disconnected physical resolver work remains charged to its
originating peer and to the global bound until it actually settles. Concurrency
backpressure returns `BUSY`. Rate-limit `BUSY` notices are capped at one per peer
per second; further over-limit attempts in that interval are dropped without a
response.

Controlled tests may override these through
`--experimental-dns-relay-timeout`, `--experimental-dns-relay-rate`,
`--experimental-dns-relay-burst`, `--experimental-dns-relay-global-rate`,
`--experimental-dns-relay-global-burst`,
`--experimental-dns-relay-peer-inflight`, and
`--experimental-dns-relay-global-inflight`.

Normal logs do not contain qnames or raw DNS. They may contain aggregate status,
size/latency buckets, accepted/invalid/refused/rate-limited/busy/timeout/backend
failure/success/oversized counts. The ordinary P2P listener is plaintext;
Brontide is encrypted. Operators must not describe the one-hop design as ODoH.
Explicit spam-level DNS message logging can reveal queries and must remain off
for a privacy-preserving canary.

For regtest and public-canary cautions, topology, rollback, future `hnsd` work,
and future HIP subjects, see the complete sibling design document. Do not enable
this service as a general public resolver or deploy a canary automatically.
165 changes: 165 additions & 0 deletions docs/experimental-odoh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Experimental Handshake P2P ODoH relay

This branch is a regtest-only proof of concept for the draft **Handshake P2P
Transport for Oblivious DNS Relay** HIP. It is based on
`feat/p2p-dns-relay` at `ea31be1554f3235bfa96bdd394e6d33e7dda8080` and
reuses that branch's query admission, active-name check, resource limits, and
recursive backend.

The private assignments are:

| Item | Private value |
| --- | ---: |
| `EXPERIMENTAL_ODOH_SERVICE` | `0x20000000` |
| `EXPERIMENTAL_ODOH` packet | `0xf2` |

They are not standards assignments. The implementation refuses to enable a
proxy or target role outside regtest and must not be advertised on mainnet.

## Implemented profile

The PoC implements:

- the version-1 `ODNS` envelope and all ten opcodes;
- proxy and target capability negotiation;
- requester-selected direct Brontide target locators;
- signed, short-lived target configuration records;
- RFC 9230 configuration, message, padding, key-ID, and response encoding;
- RFC 9180 DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM through
the maintained `@hpke` packages;
- deterministic, published configuration, key-ID, padding, response-KDF, and
signed-record vectors in `test/data/odoh-v1-vectors.json`;
- independent random request IDs on the requester-proxy and proxy-target hops;
- bounded proxy mappings, deadlines, cancellation, and disconnect cleanup;
- target replay detection;
- automatic 22-hour HPKE key rotation with a two-hour old-key/record overlap,
monotonically increasing record sequence numbers, and retired-key wiping;
- generic target failure mapping without a decryption oracle;
- reuse of the base P2P DNS relay service after target decryption;
- public-address enforcement, with a narrow explicit regtest loopback/private
exception;
- preconnected authenticated target selection, with no arbitrary socket
forwarding.

HNSR locators, target connection establishment on demand, config caching,
persistent key storage, multi-target scheduling, outer bucket padding, and
production telemetry are optional extensions outside this direct-locator
reference profile. A proxy can forward only to an already connected outbound
Brontide peer whose address, port, authenticated peer key, service bits, and
target role match the signed locator.

## Configuration

All roles are disabled by default.

```text
Proxy:
--experimental-odoh-proxy

Target:
--experimental-dns-relay
--experimental-odoh-target
--experimental-odoh-target-host=<numeric host>
--experimental-odoh-target-port=<brontide port>

Regtest-only private target addresses:
--experimental-odoh-allow-private-targets

Limits:
--experimental-odoh-timeout=<milliseconds>
--experimental-odoh-max-live=<count>
--experimental-odoh-key-rotation=<seconds, default 79200>
--experimental-odoh-key-overlap=<seconds, default 7200>
```

`--experimental-odoh-instrumentation` is a regtest-only test hook. It emits
in-memory proxy-ciphertext and target-plaintext events used by the trial
harness. It is off by default and rejected outside regtest.

## Reproducible regtest trial

Install the locked JavaScript dependencies, then run:

```sh
npm ci
node scripts/run-odoh-regtest-trial.js ../artifacts/regtest-trial.json
```

The runner creates three independent persistent-but-disposable FullNodes with
different peer identities:

```text
Requester == Brontide ==> Proxy == Brontide ==> Target
```

The requester retrieves a signed target configuration through the proxy and
sends an encrypted `www.relaytest. A` query. The target decrypts it and passes
the raw DNS query through the prerequisite `DNSRelayService` admission and
scheduling path. A deterministic trial backend returns the response; the
target encrypts it, the proxy forwards it unchanged, and the requester opens
and correlates it.

The runner fails unless all of these hold:

- proxy and target advertise the expected private capabilities;
- the target connection is authenticated by its Brontide peer key;
- the target configuration signature, network, lifetime, locator, and ID
validate at the requester;
- the serialized plaintext query is absent from the proxy-observed ODoH
ciphertext;
- the target sees the exact admitted raw query;
- client and target request IDs differ;
- a rotated record advertises the new and overlapping old key, increases its
sequence, and the previous record still completes an in-flight query;
- the base relay accepts exactly one request and returns exactly one response;
- the requester receives the expected response;
- the requester rejects malformed, non-recursive, or mismatched DNS replies;
- proxy and target live mappings return to zero.

The JSON artifact contains aggregate counters, public test identities,
per-run configuration/ciphertext digests, and the asserted privacy views. It
contains no private key material.

## Trial boundary

This is a transport/cryptography/regtest integration trial, not the base HIP's
full DNSSEC/DANE acceptance tier. It uses three real `hsd` FullNodes and real
Handshake framing/Brontide, but injects a deterministic recursive backend and
an active-name predicate after the normal base-HIP query parser and scheduler.

Therefore the result does not claim:

- a mined and registered regtest HNS name;
- live delegation recursion or authority isolation;
- requester-side Urkel, DNSSEC, TLSA, or DANE validation;
- an Android/iOS application-binary run;
- HNSR target reachability;
- multi-pair failover, load, or anonymity-set measurements;
- readiness for testnet or mainnet.

Those claims require the separate composed browser tier. The companion browser
reference implementation at `Denuo-Web/hns-dane-browser` commit `477c4e8`
provides an independent Rust requester and a real four-`hsd` regtest runner.
That runner combines a mined registered name, current Urkel proofs, a distinct
ODoH proxy and Brontide target, live recursion, local DNSSEC and TLSA/DANE,
HTTPS 200, and zero legacy-DoH contacts. The three-node artifact from this
repository remains the focused transport, cryptographic, and key-rotation
evidence; the two tiers are complementary rather than interchangeable.

## Verification

```sh
npx --yes eslint@9 \
lib/net/odoh.js lib/net/common.js lib/net/packets.js lib/net/parser.js \
lib/net/pool.js lib/node/fullnode.js lib/net/index.js \
test/odoh-test.js scripts/run-odoh-regtest-trial.js

npm run test-file -- \
test/odoh-test.js test/dns-relay-test.js test/net-test.js
```

The tests cover strict envelope/body parsing, locator restrictions, signed
config verification, all-zero padding, HPKE query/response round trips, wrong
keys, requester-side DNS response correlation, proxy/target routing,
independent hop IDs, key rotation and overlap, replay rejection, and the
prerequisite DNS-relay and network packet regression suites.
1 change: 1 addition & 0 deletions fixtures/experimental-dns-relay/malformed-length.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
08070605040302012b00123401100001000000000001037777770972656c617974657374000001000100002904d0000080000000
106 changes: 106 additions & 0 deletions fixtures/experimental-dns-relay/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"version": 1,
"byte_order": "little-endian packet integers; DNS network byte order",
"temporary_service_bit": "0x40000000",
"temporary_request_packet": "0xf0",
"temporary_response_packet": "0xf1",
"request_id": "0x0102030405060708",
"maximum_query_bytes": 4096,
"maximum_response_bytes": 65535,
"statuses": {
"OK": 0,
"REFUSED": 1,
"UNSUPPORTED": 2,
"BUSY": 3,
"INVALID_QUERY": 4,
"RESOLVER_UNAVAILABLE": 5,
"TIMEOUT": 6,
"INTERNAL_ERROR": 7
},
"fixtures": [
{
"file": "request-basic.hex",
"description": "basic request",
"valid": true,
"wire_bytes": 52,
"sha256": "d38597681c22abdb6c7ab72454658f5c0a07ff74fb53d5ce72e81b922f23e520"
},
{
"file": "response-ok.hex",
"description": "successful response with an untrusted AD bit",
"valid": true,
"wire_bytes": 69,
"sha256": "63d59820b3a078f214ff6870d9322443c6131a84be4b53ebc1b35b7f36746048"
},
{
"file": "response-error.hex",
"description": "BUSY transport response",
"valid": true,
"wire_bytes": 11,
"sha256": "37b13837928fca8d3a1427f7c2ef6bf7917fad052a88515a4f25c7e25ff2e27c"
},
{
"file": "request-max.hex",
"description": "maximum legal request",
"valid": true,
"wire_bytes": 4106,
"sha256": "2dc036004171fc77651a1732363e8260bba636d98d5a2678b744d4e579fa5bef"
},
{
"file": "response-max.hex",
"description": "maximum legal response",
"valid": true,
"wire_bytes": 65546,
"sha256": "0a6bbf5e85b49a03f3914d12a4eb54e2c51bf18e6e9c757334025b4090f30d12"
},
{
"file": "request-max-qname.hex",
"description": "maximum 255-byte DNS name",
"valid": true,
"wire_bytes": 292,
"sha256": "b971391700b4b12aaf9521bccf78c0ca2738be81d8a97b03eedbd8ce1d0530b2"
},
{
"file": "malformed-length.hex",
"description": "declared request body is longer than packet",
"valid": false,
"wire_bytes": 52,
"sha256": "203d58767d89c396b85ef71398c186043006dcd0d42e6f381638eee30e69cbac"
},
{
"file": "trailing-bytes.hex",
"description": "packet data after declared request body",
"valid": false,
"wire_bytes": 53,
"sha256": "cebc2f53063ff1e8ec35d3529fa038690793735086833ac895fa314e1baebec9"
},
{
"file": "unknown-status.hex",
"description": "unknown response transport status",
"valid": false,
"wire_bytes": 11,
"sha256": "6f763985eab74de92fe521f52ce882451a366d759b1e10f9a6e381d2c2d117fa"
},
{
"file": "oversized-request.hex",
"description": "declared request exceeds limit before allocation",
"valid": false,
"wire_bytes": 10,
"sha256": "0d8b7866de6682bcfa9e9ad6c215225965d959b241a20ab9b48e26808b61bcdc"
},
{
"file": "oversized-response.hex",
"description": "response exceeds the maximum packet body",
"valid": false,
"wire_bytes": 65547,
"sha256": "deb7812c42fa0d8617e19d7a7d6d36524bc9c2c67da7f9d4bf61b7b6482cc3b5"
},
{
"file": "zero-request-id.hex",
"description": "zero request identifier",
"valid": false,
"wire_bytes": 11,
"sha256": "5c66e18d8148d655710b2cee7313aa7110611b3ac663abb3771abd1baa9394f3"
}
]
}
1 change: 1 addition & 0 deletions fixtures/experimental-dns-relay/oversized-request.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
08070605040302010110
1 change: 1 addition & 0 deletions fixtures/experimental-dns-relay/oversized-response.hex

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions fixtures/experimental-dns-relay/request-basic.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
08070605040302012a00123401100001000000000001037777770972656c617974657374000001000100002904d0000080000000
1 change: 1 addition & 0 deletions fixtures/experimental-dns-relay/request-max-qname.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
08070605040302011a011234011000010000000000013f6161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161613f6262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262623f6363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363633d64646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464000001000100002904d0000080000000
Loading