DNS Mesh Protocol

Project pitch and executive summary

← / → arrow keys to navigate · F for fullscreen · O for grid view

What is DNS Mesh Protocol?

DMP is an open, free messaging protocol designed to do one job well:

Deliver a private message from one person to another, even when the network actively tries to stop you.

It is not a company, a product, or an app. It is a specification anyone can implement, plus a reference open-source implementation released under AGPL-3.0.

Think of it as the email of private messaging: nobody owns email; nobody can shut it off. DMP aims for the same durability, with end-to-end encryption baked in from the start.

Why does this exist?

Today's "private" messaging looks like this:

  • YouOne companyThe other person

That company is a single point of failure. They can be:

ThreatReal-world example
SubpoenaedGovernment compels a metadata handover
BlockedNational firewall drops the company's domain
AcquiredNew owner changes the privacy policy
HackedOne breach exposes everyone's contact graph
Shut downCompany decides the product isn't profitable

Encryption protects the content. None of it protects you from the company in the middle disappearing — or being made to.

Who is this for?

Journalists & sources

Need to talk to people in countries where the usual apps are blocked, monitored, or simply too dangerous to install on a phone.

Organizations

Want a backup channel that keeps working when their primary messaging vendor is down, regulated, or under a subpoena.

Privacy advocates

Don't want a phone number, an account, or any company in between two people who agreed to talk.

Builders

Need a small, auditable messaging primitive they can embed into their own products without licensing or vendor lock-in.

Refresher: how the internet finds things

Every time you visit example.com, your computer asks a question:

sequenceDiagram
    participant You as Your laptop
    participant DNS as A DNS server\n(e.g. 1.1.1.1)
    participant Auth as The owner of\nexample.com
    You->>DNS: "What is example.com?"
    DNS->>Auth: "What is example.com?"
    Auth-->>DNS: "It's at 93.184.216.34"
    DNS-->>You: "It's at 93.184.216.34"
  

This system is called DNS (Domain Name System). It's how every device on Earth turns names into addresses.

Three things you should know about DNS: nobody owns it (it's federated), every network on Earth allows it (or nothing else works), and you can put more than just IP addresses into it — small text records too.

Refresher: what does "end-to-end encrypted" mean?

End-to-end means: the message is encrypted on the sender's device, decrypted on the recipient's device, and unreadable to everyone in between.

Even the company hosting the message can't read it.

flowchart LR
    Alice["Alice\nwrites 'hello'"]
    Server["Storage\n(sees only\nencrypted bytes)"]
    Bob["Bob\nreads 'hello'"]
    Alice -- "encrypts" --> Server
    Server -- "decrypts" --> Bob
  

Signal, WhatsApp, iMessage all do end-to-end encryption. What they don't hide: who is talking to whom, when, and through whose company. That's the gap DMP closes.

DMP in one sentence

Take the universal read-API of the internet (DNS), repurpose it as a private mailbox for end-to-end encrypted messages, and let anyone host their own.

Result: a messaging protocol with no central company, no app-store gatekeeper, that works on any network where DNS still resolves — which is almost all of them.

The gap DMP fills

You want to send an end-to-end encrypted message to someone. Today the typical answer is "use a messaging app" — Signal, WhatsApp, iMessage.

Each of these:

  • Has a company in the middle that can be subpoenaed, hacked, or shut down.
  • Requires a phone number or account on their service.
  • Goes dark if the company decides to.

DMP solves the same problem without any of those — no company, no account, no phone number. The next slides show how.

The big idea: use DNS as a mailbox

DNS is everywhere. Every laptop, every phone, every server already speaks it. It's how example.com turns into an IP address.

DMP repurposes DNS TXT records as a place to store signed, encrypted messages.

  • The server holding the records can't read them (encrypted).
  • The server can't forge them (signed).
  • Any DNS resolver in the world can read them (open by design).
Result: the "server" is just a DNS host. You don't need to trust it the way you trust a messaging company.

Two channels: DNS for everything, HTTPS once

flowchart LR
    A["Alice's CLI"] -- "HTTPS once\n(register, get TSIG key)" --> ANode["Alice's home node\n(serves alice.example)"]
    A2["Alice's CLI"] -- "DNS UPDATE\n(TSIG-signed)" --> ANode
    B["Bob's CLI"] -- "DNS query\n(no auth)" --> Resolvers["public DNS\nresolvers"]
    Resolvers -- "recursive chain" --> ANode
  
  • HTTPS once: Alice registers (dnsmesh tsig register) and gets a TSIG key.
  • Writes: RFC 2136 DNS UPDATE under that key. Every record — identity, prekeys, mailbox, chunks.
  • Reads: plain DNS queries. Bob walks the standard chain to Alice's node and pulls.
  • Between nodes: also DNS. No HTTPS between independent operators.

Four terms you need before the rest makes sense

TermPlain-English meaning
TSIG keyA per-user DNS write credential. It proves "this client is allowed to publish records under this part of the zone."
Authoritative nodeThe DNS server that is the source of truth for a zone like alice.example. Public resolvers eventually ask it for the real answer.
Pinned contactA contact whose signing key you have already verified and saved. After pinning, recv trusts records from that sender's zone.
Claim providerA DNS-visible directory node that stores tiny signed pointers so an unpinned stranger can be discovered for the first message.

If you remember one thing: pinned contacts are the normal path; claim providers are only for first-contact discovery.

Where messages actually go (the deep dive)

sequenceDiagram
    participant A as Alice CLI
    participant ANode as Alice's home node\n(serves alice.example)
    participant DNS as DNS (any resolver)
    participant B as Bob CLI
    A->>ANode: DNS UPDATE + TSIG\nslot-N.mb-{hash(bob)}.alice.example
    Note over ANode: stored in sqlite\nserved on UDP 53
    B->>DNS: TXT? slot-N.mb-{hash(bob)}.alice.example
    DNS->>ANode: walks the chain\nto Alice's authoritative
    ANode-->>DNS: ciphertext
    DNS-->>B: ciphertext
    B->>B: decrypt + verify Alice's signature
  

Records live under the sender's zone — Alice publishes to ...alice.example, addressed to Bob's recipient hash. Alice's TSIG key authorizes her writes (RFC 2136 UPDATE, HMAC-SHA256 per RFC 8945). The on-zone authority is the TSIG keystore at her home node; per-key scope (slot-*.mb-*.alice.example, chunk-*-*.alice.example, id-<hash>.alice.example) keeps Alice from overwriting another user's records on the same zone. Bob's CLI walks Alice's zone via the recursive DNS chain — no HTTPS to Alice's node ever.

Read path vs write path

flowchart LR
    Alice["Alice's CLI\n(write)"]
    Bob["Bob's CLI\n(read)"]
    Resolver["public resolver\n(1.1.1.1, 8.8.8.8)"]
    ANode["Alice's node\nauthoritative for\ndmp.dnsmesh.io"]

    Alice -- "DNS UPDATE + TSIG\n(direct, signed)" --> ANode
    Bob -- "TXT query\n(no auth)" --> Resolver
    Resolver -- "recursive chain" --> ANode
  

Writes go direct. An RFC 2136 UPDATE is a signed packet that has to land at the authoritative node. Recursors don't relay UPDATEs.
Reads go through recursors. A plain TXT query rides the same public DNS chain every other lookup uses. The recursor walks NS records, lands on Alice's node, returns the answer. Default in dnsmesh init: 1.1.1.1, 8.8.8.8.

Why writes are direct and reads go through recursors

Writes (Alice publishes)Reads (Bob fetches)
Hops Alice → Alice's node Bob → recursor → Alice's node
Authentication RFC 2136 UPDATE + RFC 8945 TSIG. Per-user key, per-user scope. None. The record is signed; that's the auth.
Cacheable? No. Yes — recursor cache absorbs repeated reads.
Privacy from the auth-node operator Operator sees Alice's IP (writer is identified). Operator sees the recursor's IP, not Bob's. Many readers blend.
What this enables Only the right user can write under a given owner name. Receivers blend in; auth nodes scale via cache; reads work on any network where DNS works.

Different security properties, different network paths. The asymmetry is the point: writes are authenticated and node-direct, reads are open and recursor-cached.

Cross-domain delivery: two operators, two zones, one message

Alice's node and Bob's node are run by different people. Different domains, different operators, no shared infrastructure between them. They never coordinate. The message still gets through — because the protocol speaks DNS, and DNS is federated by design.

sequenceDiagram
    participant A as Alice's CLI
    participant ANode as Alice's node\n(dmp.dnsmesh.io)
    participant DNS as public DNS\n(any recursor)
    participant B as Bob's CLI

    rect rgba(80,180,80,0.10)
      Note right of A: alice@dmp.dnsmesh.io
      A->>ANode: DNS UPDATE + TSIG\n(write encrypted message\nfor bob@dmp.dnsmesh.pro)
      Note over ANode: stored as TXT records\nunder dmp.dnsmesh.io
    end

    rect rgba(80,140,200,0.10)
      Note left of B: bob@dmp.dnsmesh.pro
      B->>DNS: TXT? slot-N.mb-{hash(bob)}.dmp.dnsmesh.io
      Note right of DNS: walks the recursive chain\nto whichever node is\nauthoritative for dmp.dnsmesh.io
      DNS->>ANode: forwards query
      ANode-->>DNS: ciphertext + Alice's signature
      DNS-->>B: ciphertext + Alice's signature
      B->>B: verify Alice's signature\ndecrypt with Bob's prekey
    end
  

The records live on Alice's node, addressed to Bob's recipient hash. Bob's node never sees them — Bob's CLI pulls them directly through the public recursive chain that every internet device already uses for every other DNS lookup. The two operators don't peer, don't gossip, don't even know about each other. They just both serve their own zones. This is the live demo from dmp.dnsmesh.iodmp.dnsmesh.pro: two real public nodes, independent operators, full round-trip messaging end-to-end encrypted.

What works today

SetupWorks?
Both users on the same node + same zone
Both users in a 3-node federated cluster (anti-entropy sync between cluster members)
Different operators, different domains, both sides have pinned the other as a contact
Different operators, different domains, unpinned stranger reaches you for the first time✓ via the claim layer
(requires a claim provider both sides discover)

Cross-domain delivery works because dnsmesh recv walks each pinned contact's zone, so where the sender lives doesn't matter — only that the receiver has them pinned. First-contact reach for unpinned strangers (the last row) routes through a tiny signed pointer at a claim-provider node — see the protocol overview.

"Does the CLI need HTTP?"

In the preferred M9 flow, almost never — only the one-time register step.

You're doing...ChannelNetwork requirements
dnsmesh init
setting up a config
none yet nothing — local only
dnsmesh tsig register
onboarding at a node, getting a TSIG key
HTTPS once outbound 443 to your home node. For normal user onboarding, this is the only HTTPS exchange.
dnsmesh identity publish
dnsmesh send
dnsmesh identity refresh-prekeys
DNS UPDATE outbound UDP 53 (or TCP 53) to your home node, signed with your TSIG key. RFC 2136 + RFC 8945. Legacy configs can still fall back to HTTPS writes until the user registers for TSIG.
dnsmesh identity fetch
dnsmesh recv
dnsmesh peers <zone>
DNS UDP 53 to any recursive resolver. Reads walk the chain.
First-contact claim publish
(stranger reaching you)
DNS UPDATE un-TSIG'd UPDATE to the provider's DNS server. The wire is a signed ClaimRecord — Ed25519 sig is the on-zone auth.

What if I'm on a restricted network?

NetworkRead messages?Send messages?
Home / coffee shop wifi
Hotel captive portal (after login)
Prepaid mobile (no data plan)
Corporate firewall, HTTPS allowed, DNS blocked outbound
Network blocking port 53 entirely
The protocol speaks DNS — both directions. Reads use plain DNS queries; writes use RFC 2136 UPDATE. Both need outbound port 53 (UDP and TCP). Most networks pass it (home wifi, hotel captive portals after login, mobile data). Networks that block port 53 entirely are the one shape DMP can't traverse today; DNS-over-HTTPS support for both reads and UPDATE writes is on the roadmap as a future-work item.

The pieces

flowchart LR
    CLI["dnsmesh CLI\nyour laptop / phone\n(holds private keys + TSIG)"]
    DNS["DNS server\nUDP/TCP 53\n(query + RFC 2136 UPDATE)"]
    HTTP["HTTPS\nregister only\n(one-time)"]
    Store[("sqlite\nstore")]
    World["any DNS resolver\nanywhere"]
    CLI -- "DNS UPDATE + TSIG" --> DNS
    CLI -. "register (once)" .-> HTTP
    HTTP --> Store
    DNS --> Store
    World -- "TXT query" --> DNS
  

A DMP node is one process. It serves DNS on UDP/TCP 53 (queries AND signed UPDATE writes) and exposes one HTTPS route for the one-time registration ceremony. After register, the CLI never opens HTTPS again — every record publish is RFC 2136 DNS UPDATE signed with the user's TSIG key.

End-to-end: Alice publishes, Bob fetches

sequenceDiagram
    participant A as Alice
    participant ANode as Alice's home node
    participant DNS as DNS (anywhere)
    participant B as Bob
    A->>ANode: DNS UPDATE + TSIG\nsigned identity record
    Note right of ANode: stored in sqlite\nserved on UDP 53
    B->>DNS: TXT? dmp.alice.example\n(zone-anchored) or\nid-{hash16}.shared.mesh\n(shared-domain fallback)
    DNS->>ANode: forwards query
    ANode->>DNS: signed record
    DNS->>B: signed record
    B->>B: verify Ed25519 signature\npin Alice's pubkey
  

What "DNS query" actually does

sequenceDiagram
    participant Bob as Bob
    participant R as 1.1.1.1\n(public resolver)
    participant DO as DigitalOcean DNS\n(holds NS for dnsmesh.io)
    participant Node as dnsmesh-node\n(authoritative for dmp.dnsmesh.io)
    Bob->>R: TXT? id-xxx.dmp.dnsmesh.io
    alt cached at resolver
      R-->>Bob: cached value
    else cache miss
      R->>DO: TXT? id-xxx.dmp.dnsmesh.io
      DO-->>R: dmp.dnsmesh.io NS = ns1.dnsmesh.io
      R->>Node: TXT? id-xxx.dmp.dnsmesh.io
      Node-->>R: signed record
      R-->>Bob: signed record
    end
  

Most queries hit a cache. Cold queries walk the chain. Either way, Bob's CLI verifies the signature locally — no trust in resolvers or paths required. "Authoritative" just means "the server that owns the real answer for that zone." The DigitalOcean step is the apex domain registrar, which delegates the dmp. subzone to the dnsmesh-node — see deployment/dns-delegation.

Why dnsmesh init uses 1.1.1.1 by default

dns_resolvers:
  - 1.1.1.1     # Cloudflare
  - 8.8.8.8     # Google (failover)
  • Skip stale negative cache. Your ISP might still cache "name doesn't exist" from before a delegation was set up.
  • Failover. If one resolver stops responding, the other catches it.
  • Public resolvers speak DoH on port 443 — but the CLI's resolver pool today only does plain DNS. DoH support inside the CLI is future work for networks that block UDP 53.

Privacy-first deploys: dnsmesh init --no-default-resolvers, then point at your own resolver in ~/.dmp/config.yaml.

Two ways to query

Through a public resolver

dig @1.1.1.1 \
  dmp.alice.example TXT

What everyone else sees. Goes through caching. Real owner names are dmp.<zone> for zone-anchored identities or id-<hash16(user)>.<shared-zone> for shared-domain TOFU.

Direct to the node

dig @her-node.example \
  dmp.alice.example TXT

Source of truth. No caching. Use this when something looks stale.

Triage tip: if @node returns the record but @resolver doesn't, you're seeing a stale cache somewhere upstream. Wait, or query a different resolver.

What if the node goes offline?

Reads (DNS): graceful degradation

sequenceDiagram
    participant Bob as Bob
    participant R as Resolver
    participant Node as Node (down)
    Bob->>R: TXT? alice's identity
    alt within cache TTL
      R-->>Bob: still resolves
    else cache expired
      R->>Node: TXT? (timeout)
      R-->>Bob: no answer
    end
  

Cached reads survive for one TTL window. Cold reads start failing as caches expire. Visible outage = one TTL plus a few minutes of negative-cache lag. TTL defaults vary by record type: DMP DNS server fallback is 60s; client message publishes default to 300s; identity / heartbeat publishes default to 86400s. Operators tune these via --ttl or env vars.

Node offline — writes fail immediately

sequenceDiagram
    participant Alice
    participant Node as Node (down)
    Alice->>Node: DNS UPDATE + TSIG
    Node--xAlice: timeout
    Note right of Alice: send fails\nno built-in retry\noperator handles it
  

No retry queue. Pending sends do not survive. Records already published are persisted on disk and come back when the node restarts. Legacy HTTP-write configs fail the same way; the transport changed, the failure mode did not.

Node-down survival cheat sheet

StateSurvives?
Records already published (sqlite on disk)
Resolver-cached records (within TTL)
Pending sends from clients
Live HTTP requests in flight
Heartbeat / discovery liveness✗ (until restart)

Federated cluster: surviving a node failure

flowchart LR
    Client["sender's CLI"]
    Client -- "DNS UPDATE + TSIG\n(to home node)" --> A
    subgraph Cluster
      A["node-a"]
      B["node-b\n(down)"]
      C["node-c"]
      A <-. "HTTP anti-entropy\n(operator-scoped HA)" .-> C
    end
  

The user's CLI sends a DNS UPDATE to its home node — DNS-only over the network. Inside the cluster, the three nodes anti-entropy-sync every ~30s over HTTP (/v1/sync/digest + /v1/sync/pull). Cluster peers share one operator-signed manifest, so they live in one administrative trust domain — that's why the boundary stays HTTP. Federation between independent operators is DNS-only; cluster replication is the only inter-node HTTP path. Reads from the world union across all live nodes. When a dead node returns, it pulls everything it missed since its last sync watermark. See cluster anti-entropy boundary.

Things people actually ask

  • "dig @node works but my CLI doesn't" — your CLI is using a different resolver. Check dns_resolvers in ~/.dmp/config.yaml.
  • "How long until everyone sees my publish?" — typically 1–3 minutes. Worst case 15 min for slow caches.
  • "Can I read or send on a network without DNS?" — no. After M9 the protocol is DNS both directions: reads are TXT queries, writes are RFC 2136 UPDATE. Networks that block port 53 entirely block both.
  • "Should I run my own resolver?" — privacy-conscious users yes. dns_resolvers: ['127.0.0.1'] after running unbound or similar locally.

Ready to try?

pipx install dnsmesh
dnsmesh init alice --domain dmp.dnsmesh.io --endpoint https://dnsmesh.io
dnsmesh tsig register --node dnsmesh.io
dnsmesh identity publish

Address: alice@dmp.dnsmesh.io. Replace dmp.dnsmesh.io with the served zone of any DMP node you trust (or your own).

Getting started · How DMP works (text) · Protocol spec · GitHub

← back to docs