← / → arrow keys to navigate · F for fullscreen · O for grid view
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.
Today's "private" messaging looks like this:
That company is a single point of failure. They can be:
| Threat | Real-world example |
|---|---|
| Subpoenaed | Government compels a metadata handover |
| Blocked | National firewall drops the company's domain |
| Acquired | New owner changes the privacy policy |
| Hacked | One breach exposes everyone's contact graph |
| Shut down | Company 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.
Need to talk to people in countries where the usual apps are blocked, monitored, or simply too dangerous to install on a phone.
Want a backup channel that keeps working when their primary messaging vendor is down, regulated, or under a subpoena.
Don't want a phone number, an account, or any company in between two people who agreed to talk.
Need a small, auditable messaging primitive they can embed into their own products without licensing or vendor lock-in.
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.
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.
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.
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:
DMP solves the same problem without any of those — no company, no account, no phone number. The next slides show how.
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.
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
dnsmesh tsig register) and gets a TSIG key.| Term | Plain-English meaning |
|---|---|
| TSIG key | A per-user DNS write credential. It proves "this client is allowed to publish records under this part of the zone." |
| Authoritative node | The DNS server that is the source of truth for a zone like alice.example. Public resolvers eventually ask it for the real answer. |
| Pinned contact | A contact whose signing key you have already verified and saved. After pinning, recv trusts records from that sender's zone. |
| Claim provider | A 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.
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.
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.
| 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.
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.io ↔
dmp.dnsmesh.pro: two real public nodes,
independent operators, full round-trip messaging end-to-end
encrypted.
| Setup | Works? |
|---|---|
| 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.
In the preferred M9 flow, almost never — only the one-time register step.
| You're doing... | Channel | Network requirements |
|---|---|---|
dnsmesh initsetting up a config |
none yet | nothing — local only |
dnsmesh tsig registeronboarding 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 publishdnsmesh senddnsmesh 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 fetchdnsmesh recvdnsmesh 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. |
| Network | Read 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 | ✗ | ✗ |
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.
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
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.
dnsmesh init uses 1.1.1.1 by defaultdns_resolvers:
- 1.1.1.1 # Cloudflare
- 8.8.8.8 # Google (failover)
Privacy-first deploys: dnsmesh init --no-default-resolvers, then point at your own resolver in ~/.dmp/config.yaml.
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.
dig @her-node.example \
dmp.alice.example TXT
Source of truth. No caching. Use this when something looks stale.
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.
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.
| State | Survives? |
|---|---|
| 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) |
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.
dig @node works but my CLI doesn't" — your CLI is using a different resolver. Check dns_resolvers in ~/.dmp/config.yaml.dns_resolvers: ['127.0.0.1'] after running unbound or similar locally.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