Getting Started

  1. Prerequisites
  2. Install the CLI
    1. From PyPI (recommended)
    2. Standalone binary (no Python required)
    3. From source (contributors)
  3. Run a node (local)
  4. Set your passphrase
    1. Option A — environment variable (quick, ephemeral)
    2. Option B — passphrase file (durable, recommended)
    3. Option C — interactive prompt
    4. Verify
  5. Send your first message
  6. What just happened
  7. Running against a cluster
  8. Next

Prerequisites

  • Python 3.10 or newer (for the PyPI install or source install)
  • Docker (for running a node locally; not needed if you only use the CLI against someone else’s node)

Install the CLI

Pick one. The PyPI wheel is the fastest path; source install is for contributors and people pinning to an unreleased commit.

pip install dnsmesh

Standalone binary (no Python required)

Single-file executables are attached to every release on GitHub. Pick the asset for your platform from the latest cli-vX.Y.Z release. Available for Linux x86_64, macOS arm64, and Windows x86_64.

# example: macOS arm64
curl -fsSL -o ~/.local/bin/dnsmesh \
    https://github.com/oscarvalenzuelab/DNSMeshProtocol/releases/latest/download/dnsmesh-macos-arm64
chmod +x ~/.local/bin/dnsmesh

From source (contributors)

git clone https://github.com/oscarvalenzuelab/DNSMeshProtocol.git
cd DNSMeshProtocol
pip install -e ".[dev]"

Verify any of the above:

dnsmesh --help

Run a node (local)

The pre-built image on Docker Hub is the easiest way:

docker run -d --name dnsmesh-node \
  -p 5353:5353/udp -p 8053:8053/tcp \
  -v dnsmesh-data:/var/lib/dmp \
  ovalenzuela/dnsmesh-node:latest

# Health check
curl http://127.0.0.1:8053/health

To put a node on the public internet (with auto TLS, hardening, etc.), follow Deployment → DigitalOcean or any of the other deployment guides — the same Docker recipe runs on any UDP-capable VPS.

If you’d rather build from source instead of pulling the published image:

docker build -t dnsmesh-node:latest .
docker run -d --name dnsmesh-node \
  -p 5353:5353/udp -p 8053:8053/tcp \
  -v dnsmesh-data:/var/lib/dmp \
  dnsmesh-node:latest

Ports:

  • 5353/udp — DNS server (map to :53 in production; see Deployment)
  • 8053/tcp — HTTP publish / metrics API

Set your passphrase

Identity keys are derived from a passphrase + a per-identity random salt (Argon2id). The CLI looks for the passphrase in three places, in order:

  1. The DMP_PASSPHRASE environment variable.
  2. A file path named in your config’s passphrase_file field.
  3. An interactive getpass prompt as a last resort.

Pick the one that fits how you’ll use the CLI.

The passphrase is the only thing protecting your keys. Lose it → identity unrecoverable (the salt is useless without it). Leak it → full account compromise. Treat it like a password-manager entry: long, random, and backed up.

Option A — environment variable (quick, ephemeral)

Good for a quick test on a dev box. The shell prompts you silently (no echo, no shell history):

read -rs DMP_PASSPHRASE
export DMP_PASSPHRASE
dnsmesh identity show

The passphrase lives in the shell’s environment until you close the shell, then it’s gone. You’ll re-enter it next session.

Avoid export DMP_PASSPHRASE='hunter2' directly — that lands in ~/.zsh_history (or ~/.bash_history).

What you want for a long-running setup or a server. The file is read on every dnsmesh invocation; trailing whitespace is stripped.

umask 077                                   # new files default 0600
mkdir -p ~/.dmp

# Generate a strong random passphrase (or paste from a password manager):
openssl rand -base64 32 > ~/.dmp/passphrase
chmod 400 ~/.dmp/passphrase

# Tell the CLI where to find it:
echo 'passphrase_file: ~/.dmp/passphrase' >> ~/.dmp/config.yaml

dnsmesh identity show

After that, every dnsmesh command uses the file automatically. Back up ~/.dmp/passphrase to your password manager.

Option C — interactive prompt

If neither the env var nor a file is configured, the CLI falls back to a getpass prompt. Safest for one-off invocations on a machine you don’t fully trust, since nothing is stored. Annoying for day-to-day use because every command prompts again.

Verify

dnsmesh identity show

prints your Ed25519 + X25519 public keys + user_id. The first successful derive on a fresh config writes the derived signing pubkey into the config as a typo-tripwire (verify_pubkey:). Every later command compares against it: a mismatch aborts with a clear message rather than silently producing a different identity.

If you ever need to bypass the check (e.g. you actually do want to swap to a new identity on the same config without init --force), set DMP_PASSPHRASE_OVERRIDE_VERIFY=1 for that one invocation. Use sparingly — it’s the wrong tool for almost every scenario.

Send your first message

Two terminal windows simulate two users. In practice you’d run two machines, but separate DMP_CONFIG_HOME directories work fine on one box.

Terminal 1 — Alice

export DMP_CONFIG_HOME=/tmp/alice-home
export DMP_PASSPHRASE=alice-pass
dnsmesh init alice --domain mesh.local \
               --endpoint http://127.0.0.1:8053 \
               --dns-host 127.0.0.1 --dns-port 5353
dnsmesh identity publish

Terminal 2 — Bob

export DMP_CONFIG_HOME=/tmp/bob-home
export DMP_PASSPHRASE=bob-pass
dnsmesh init bob --domain mesh.local \
             --endpoint http://127.0.0.1:8053 \
             --dns-host 127.0.0.1 --dns-port 5353

# Publish bob's identity + a pool of one-time prekeys for forward secrecy.
dnsmesh identity publish
dnsmesh identity refresh-prekeys

# Resolve alice's identity from DNS and pin her.
dnsmesh identity fetch alice --add

# Send.
dnsmesh send alice "hello alice"

Terminal 1 — Alice reads

# Resolve bob too so his signing key is pinned — receive then accepts
# only manifests from pinned signers, not TOFU.
dnsmesh identity fetch bob --add

dnsmesh recv

You should see:

from ef44bf…
  ts=1776721594
  hello alice

Running dnsmesh recv a second time in the same config home doesn’t re-deliver the same message — the replay cache persists to $DMP_CONFIG_HOME/replay_cache.json.

What just happened

  1. Alice encrypted “hello alice” with a recipient prekey → ECDH shared secret → ChaCha20-Poly1305 ciphertext.
  2. The ciphertext got cross-chunk erasure-coded and published as TXT records under the mesh domain.
  3. A signed manifest naming alice’s Ed25519 key + the prekey_id + total chunks went into one of bob’s 10 mailbox slots.
  4. Bob’s dnsmesh recv polled the slots, verified alice’s signature (pinned contact), checked the replay cache, fetched chunks, ran erasure decode, and decrypted with the prekey’s secret half.
  5. The prekey’s secret half was then deleted locally and from DNS — that message is now forward-secret even if alice’s or bob’s long-term key leaks.

Running against a cluster

For resilience against single-node failure, point the CLI at a multi-node cluster instead of one endpoint. The operator publishes a signed ClusterManifest at cluster.<base> TXT listing the node set; the client pins the operator’s Ed25519 pubkey + the base domain and fans every write to a majority of nodes while unioning every read.

# Pin the operator key + base domain once.
dnsmesh cluster pin 3c6a...the32byteoperatorpubkeyinhex mesh.example.com

# Sanity-check that the signed manifest is published and verifiable.
dnsmesh cluster fetch
# cluster: mesh.example.com
#   seq:   7
#   exp:   1816000000
#   nodes: 3
#     n01  http=https://n1.mesh.example.com:8053  dns=203.0.113.10:53
#     ...

# From here on every `dnsmesh send` / `dnsmesh recv` / `dnsmesh identity publish`
# fans writes across ceil(N/2) nodes and unions reads across all N.
# Manifests refresh in the background on `cluster_refresh_interval`
# (default 3600 seconds).

When either cluster_operator_spk or cluster_base_domain is unset the CLI falls back to the legacy single-endpoint mode, so existing configs keep working unchanged. See User Guide → CLI reference → dnsmesh cluster for the full subcommand list.

Next