Protocol specification · working draft

Working draft · v0.2 implemented, v0.3 partial

DNX Protocol Specification

This document describes running code. Every implemented claim corresponds to something in the reference implementation; everything not yet built is marked as specification or future work. Where this document and the code disagree, the code is the bug.

1 · Overview

DNX is a name-native overlay protocol. A machine is identified by a fully-qualified domain name bound to an ed25519 keypair; packets are routed by walking that name's hierarchy; and every session is encrypted with keys that are provably owned by whoever owns the name.

Goals. Names are the only addresses humans or applications touch. Identity is native to the addressing layer, not bolted above it. A machine behind an unconfigured NAT is as reachable as a public server. Records are fresh in seconds.

Non-goals. DNX does not replace IP forwarding. Packets are UDP/IP datagrams moved by existing infrastructure; DNX makes the numeric layer invisible rather than absent. Name-native forwarding in hardware is a target architecture, not a current claim.

Design rule

We build the protocol; we never build the cryptographic primitives. All primitives come from audited implementations. The novel work is the state machine and how it inherits name-to-key trust.

2 · Identity

On first boot a node generates an ed25519 keypair. The keypair is the machine. The domain name is a human-readable handle that the registry binds to that key, first-come-first-served. Steal the name without the key and you are nobody: every control message and every reply is signed.

The private key is written to ~/.dnx/identity.json with mode 0600 and never leaves the host. Only public keys and signatures cross the wire.

registry binds name → ed25519 public key the name's owner is now defined that key signs an ephemeral X25519 key X25519 → HKDF directional session keys sealed traffic ChaCha20-Poly1305 Result: only the machine that owns the NAME can derive the session key. The registry never sees it. It is a phone book, not a key escrow.
Encryption inherits name-identity for free. Nothing new has to be trusted.

3 · The registry

The registry is a living name table: it maps a name to its public key and its currently observed endpoint. Think of it as DNS that updates in seconds and answers with identity attached.

Every node sends a signed REGISTER every 15 seconds — chosen to sit under typical NAT UDP mapping timeouts. The registry verifies the signature, enforces the binding rule, and records the source address it observed on the datagram.

A node never states where it is

The endpoint is not asserted by the node — it is observed by the registry from the packet's own source address, and is deliberately excluded from the signature. A node therefore cannot lie about its location, because it never claims one.

A REGISTER for an already-bound name under a different key is rejected regardless of signature validity — possession of a name requires possession of its key.

Changing an owner. A name may be transferred with a REBIND signed by the key that currently holds it. The signature covers the destination key, not merely the name — otherwise it would authorise moving that name to any key, and anyone who observed one legitimate transfer could replay it to install a key of their own. A transfer also clears the endpoint: the machine holding the old key is no longer the owner, and the new one must register before anybody is directed to it.

A lost key is not a protocol problem. If an owner cannot sign, nothing distinguishes them from someone impersonating them, so no cryptographic mechanism can return the name to them. Releasing it is an authority decision, performed by whoever operates the registry with shell access to it, and is deliberately unreachable over the network.

Ownership is not liveness. Two lifetimes run independently: an endpoint is forgotten after 60 seconds without a heartbeat, so nobody is directed to a machine that has moved or gone away; but the name-to-key binding is retained for a year. An earlier implementation conflated the two and deleted the whole record after ten minutes of silence, which meant a machine switched off for long enough lost its name to whoever registered next. A name is owned, not leased to whoever happens to be awake.

How registries delegate, federate, and deploy in practice — and how much of that is still unbuilt — is covered in the registry design note.

4 · NAT traversal

NATs were never really blocking inbound traffic — they were blocking unexpected inbound traffic. DNX makes every connection expected at both ends simultaneously.

registry matchmaker only — then it steps out node A behind NAT A node B behind NAT B 1 · introduce me to B 2 · A is knocking, from here 3 · both fire outward at once — each NAT opens from the inside
Every DNX path is initiated outbound from both ends, which is why a fully firewalled node with no inbound rules is still reachable by name.

This succeeds against full-cone and port-restricted NATs. Symmetric NATs, which allocate a fresh mapping per destination, defeat simple punching; relayed fallback is specified but not yet built.

5 · Session layer

Two messages, roughly one round trip:

A → B   HS_INIT  { ephA_pub, nonce_A, ts, sig_A }      signed by A's ed25519
A ← B   HS_RESP  { ephB_pub, nonce_A, nonce_B, sig_B } signed by B's ed25519

both:   shared = X25519(eph_priv, peer_eph_pub)
        keys   = HKDF-SHA256(shared,
                             salt = nonce_A || nonce_B,
                             info = direction label)

Echoing nonce_A in the response binds it to this exact handshake. Each side derives directional keys, so the two flows never share a nonce space and both may count from zero safely.

Forward secrecy. The X25519 keys are ephemeral and discarded on rekey every five minutes. Compromising a machine's long-term identity key tomorrow does not decrypt traffic captured today.

The signature covers a canonical string that includes a domain-separation tag, both names, the ephemeral key, both nonces and the timestamp — so an HS_INIT signature can never be replayed as an HS_RESP.

6 · Stream layer

DNX must run on UDP for NAT traversal, so something has to put reliability back. The stream layer is deliberately TCP-shaped, because TCP's shape is correct:

  • Byte-offset sequence numbers, so segment boundaries may change on retransmit.
  • Cumulative acknowledgement — "I have everything below N".
  • RTO estimation with smoothed RTT and variance, plus exponential backoff, and RTT samples taken only from segments that were never retransmitted.
  • Receiver-advertised window for flow control, with a persist timer that probes a closed window.
  • Slow start and congestion avoidance, halving on loss.

Deliberately omitted in v0.2: selective acknowledgement, fast retransmit on duplicate ACKs, path MTU discovery, and window scaling. Loss recovery is timeout-driven — correct, but slower than TCP on lossy paths.

Why the persist timer matters

If a receiver's window closes and the acknowledgement that later reopens it is lost, both sides wait forever: the receiver believes it announced space, the sender waits to be told. A zero-window probe breaks the stalemate. This deadlock was found by a test written specifically to provoke it — see the journal.

7 · Routing

IP forwards by longest-prefix match over numeric addresses. DNX forwards by walking the name hierarchy from the coarsest level inward. Each router holds a table mapping the level it owns to a next hop, and reads nothing else.

forward(packet):
    level = highest unresolved level of the destination
    hop   = table.lookup(level)
    if hop is LOCAL:   deliver
    elif hop exists:   mark level resolved; send to hop
    else:              no route — drop definitively

Why it scales. A router's table size is the number of distinct children it must distinguish — not the number of hosts, and not the number of allocated prefixes. The core is constant in the number of hosts: new machines appear as leaves under levels the core already knows.

Authoritative failure. A router responsible for a level is authoritative for its children. If it has no route, no router closer to the root knows better, so the packet is dropped there rather than being sent back up. Ascending would create a loop — a mistake made and corrected during development.

Under active revision

The current implementation encodes the destination as four fixed 64-bit fields. That caps hierarchy depth at four levels, which is too rigid — api.production.us-east.customer.company does not fit. A variable-length namespace path is being designed to replace it: see DNXP-0001.

8 · Wire formats

Three formats share one UDP socket, distinguished by their first byte. Control messages are JSON and begin with {; sealed frames begin 0xD8; routed frames begin 0xDF. This lets old and new coexist during migration.

Control message (JSON)

{
  "kind":     "REGISTER",              // verb
  "name":     "host1.dnx.dnxroute.com",
  "pubkey":   "base64(ed25519 public)",// identity
  "endpoint": "203.0.113.7:44313",     // registry-observed, never self-asserted
  "ts":       1783312169042,           // unix ms, ±30s replay window
  "nonce":    "base64url(12 bytes)",
  "sig":      "base64(ed25519 signature)"
}

Sealed frame

+--------+----------------+------------------------------+
| 0xD8   | counter (u64)  | ciphertext + 16-byte AEAD tag|
+--------+----------------+------------------------------+

The counter doubles as the AEAD nonce, so nonces cannot repeat within a session. The header is authenticated as additional data — an attacker cannot alter it without the tag failing. A counter is accepted at most once, and only recorded after the tag verifies, so forged frames cannot poison the replay table.

Stream frame (inside a sealed frame)

+------+---------+---------+---------+---------+---------+
| type | seq u32 | ack u32 | wnd u16 | len u16 | payload |
+------+---------+---------+---------+---------+---------+

Routed frame

+--------+-------------------+------------------+---------+
| 0xDF   | destination path  | levels consumed  | payload |
+--------+-------------------+------------------+---------+

"Levels consumed" tells the receiving router which level to read next, so each hop picks up exactly where the previous one stopped. The payload is itself a sealed frame — routers forward what they cannot read.

9 · Security model

What a verified reply proves: the responder holds the private key that the registry associates with that name. Not merely that an address answered.

What it does not prove: anything about the peer's intentions, and — in the v0.1 plaintext path — anything about confidentiality.

Known weaknesses, stated plainly

WeaknessConsequenceFix
Registry replies unsigned FixedThis was the most serious weakness listed here. A resolution answer carries both the peer's endpoint and its public key, and a node has no independent knowledge of that key — so an on-path attacker who forges the answer supplies a key it controls, completes a valid handshake, and the caller reports identity_verified=true for a machine that is not the name's owner. Every identity guarantee in DNX currently rests on the integrity of this one unsigned message. An earlier version of this table claimed impersonation was not possible; that was wrong, and assumed an attacker would forge only the endpoint.Fixed: the registry signs answers and referrals over the name, key and endpoint; a node given the registry's key refuses what it cannot verify
Rendezvous cues unsignedA spoofed cue can induce a node to emit a small burst of packets to an arbitrary address — a minor reflection primitive.Folds into signed registry traffic
Single registry PartlyDelegation is implemented, so authority can be split across registries and each sees only its own branch. The public deployment still runs one registry, and there is no replication yet.Replication; deploying a delegated hierarchy
First-come-first-served namesNothing prevents squatting desirable names, and nothing ties a DNX name to actual control of the corresponding domain.Delegation anchored to domain-ownership proofs
Hash-derived address fieldsThe lower levels of the current address are a 64-bit truncated hash of the label, so two distinct labels can collide. A deliberate collision costs roughly 232 work — hours on commodity hardware — and causes packets to be forwarded to the wrong host. It does not permit impersonation: the session handshake still fails against the real name's key, so this is an availability attack rather than a takeover.Registry-assigned identifiers — see DNXP-0001
No homograph policyVisually identical names built from different scripts are distinct names, so one can impersonate another to a human reader.Registration rules at the naming authority
Symmetric NATSimple hole punching fails; those peers are unreachable.Relayed fallback

This list is published because a protocol that hides its weaknesses cannot be evaluated. If you find one that is not here, that is a bug in this document.

10 · Constants

ConstantValueRationale
heartbeat15 sBelow common ~30 s NAT UDP mapping timeout
offline after60 sTolerates missed heartbeats before a name reads offline
prune after600 sTen times the offline threshold
replay window±30 sClock-skew tolerance versus replay surface
session rekey5 minBounds the forward-secrecy window
max segment1200 BFits under a 1500 B MTU after all DNX and IP overhead
retransmit timeout200 ms – 10 sSmoothed RTT plus variance, with exponential backoff

11 · Roadmap

Next. Variable-length namespace paths replacing the fixed four-level address. Signed registry responses, which closes two of the weaknesses above at once. Deploying the stream and tunnel layers to the live network.

After that. Relayed fallback for symmetric NATs. Registry federation with a signed name table. Namespace delegation anchored to existing domain ownership.

Long term. Pushing name-based forwarding further down the stack, so that routers decide on names natively rather than over an IP substrate. That is a research direction, not a schedule.