dyns 0.3.0

DNS discovery and resolver support for DHTTP applications
Documentation

DDNS

ddns provides DNS discovery and resolver support for DHTTP applications. It is a single Rust package: the historical ddns-core, gmdns, ddns, and ddns-server crate boundaries now live as modules and feature-gated targets in one published Cargo package named dyns, with a library target kept as ddns for source compatibility.

Crate layout

Module / target Role
ddns::core DNS packet parser, resource-record types, endpoint E record encoding, and HTTP multi-record response wire format.
ddns::mdns RFC 6762 multicast DNS transport, LAN publisher, and LAN resolver support.
ddns::resolvers Resolver chain plus optional System, mDNS, DNS-over-H3, and DNS-over-HTTP resolvers.
ddns::publisher Feature-gated endpoint record signing and publishing loop helpers for DHTTP endpoints.
ddns-server DNS-over-H3 publish/lookup server binary, enabled by the server feature.

ddns is endpoint-facing support code for the DHTTP ecosystem. Applications normally reach it through the dhttp endpoint facade; lower-level consumers can depend on package dyns directly (typically renamed locally to ddns) when they need DNS wire types, resolver composition, mDNS, or the DNS-over-H3 server.

ddns = { package = "dyns", version = "0.3.0" }

Features

All optional integrations are feature-gated; the default feature set is empty.

Feature Enables
h3x-resolver DNS-over-H3 resolver and publisher using h3x/dquic.
mdns-resolver mDNS resolver integration backed by an existing h3x::dquic::Network.
http-resolver DNS-over-HTTP resolver/publisher using reqwest and native roots.
server ddns-server, Redis storage support, TOML config parsing, and tracing setup.

Bootstrap constants

build.rs generates the resolver defaults exposed from ddns::resolvers:

Environment variable Public constant Fallback when unset
DHTTP_H3_DNS_SERVER DHTTP_H3_DNS_SERVER https://dhttp.example.net
DHTTP_HTTP_DNS_SERVER DHTTP_HTTP_DNS_SERVER https://dhttp.example.net
DHTTP_MDNS_SERVICE DHTTP_MDNS_SERVICE dhttp.example.net

The fallbacks are docs/build placeholders, not operational defaults. Real endpoint, server, and E2E runs should set the DHTTP bootstrap environment before building.

Quick start

Resolver chain

Resolvers queries all configured resolvers and streams endpoint addresses from successful backends. System DNS is always available; mDNS, H3, and HTTP builders appear behind their features.

use ddns::resolvers::Resolvers;
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), ddns::resolvers::DnsErrors> {
    let resolvers = Resolvers::builder().system().build();
    let mut endpoints = resolvers.lookup("demo.example.dhttp.net").await?;

    while let Some((source, endpoint)) = endpoints.next().await {
        println!("{source:?}: {endpoint}");
    }

    Ok(())
}

mDNS discovery

use ddns::{mdns::service::Mdns, resolvers::DHTTP_MDNS_SERVICE};
use futures::StreamExt;

#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
    let mdns = Mdns::new(
        DHTTP_MDNS_SERVICE,
        std::net::Ipv4Addr::LOCALHOST.into(),
        "lo0",
    )?;
    let mut discoveries = mdns.discover();

    while let Some((source, packet)) = discoveries.next().await {
        println!("received packet from {source}: {packet}");
    }

    Ok(())
}

Runnable examples live in examples/:

cargo run --example mdns_discover -- --ip 127.0.0.1 --device lo0
cargo run --example mdns_query -- --ip 192.168.5.156 --device en0

DNS-over-H3 examples

cargo run --example query --features h3x-resolver -- \
  --server-ca /path/to/root.crt \
  --host nat.genmeta.net

cargo run --example publish --features h3x-resolver -- \
  --server-ca /path/to/root.crt \
  --client-name demo.example.dhttp.net \
  --client-cert /path/to/demo.example.dhttp.net.pem \
  --client-key /path/to/demo.example.dhttp.net.key \
  --host demo.example.dhttp.net \
  --addr 192.168.1.100:8080,192.168.1.101:8080

See examples/README.md for the example CLI parameters and response decoding notes.

DNS-over-H3 server

Start the server with the server feature:

cargo run --bin ddns-server --features server -- --config server.toml

The server exposes two HTTP/3 routes:

Route Meaning
POST /publish?host=<name> Publish a DNS packet for host. Client mTLS is required.
GET /lookup?host=<name>[&limit=N] Look up active records for host; limit caps newest-first dynamic records.

Lookup responses use header x-record-format: multi and the binary body from ddns::core::wire::MultiResponse:

u32 count
repeated count times:
  u32 dns_len | dns packet bytes | u32 cert_len | DER publisher certificate bytes

Server configuration lives in server.toml:

  • storage is in-memory by default, or Redis when redis = "redis://..." is set;
  • ttl_secs controls dynamic record expiry;
  • require_signature controls signed endpoint-record enforcement for Standard domains;
  • domain_policies are matched in order, with unlisted domains using the Standard policy;
  • seed_records add static bootstrap endpoints to lookup results.

Domain policies:

Policy Behavior
standard Client certificate DNS SAN must match the published host; signed E records are required when require_signature = true; each certificate fingerprint owns one active record for the host.
open_multi Any authenticated client certificate may publish; signature checks are skipped; multiple certificate fingerprints can coexist and lookup returns newest-first records.

Public DHTTP identity hostnames should use the canonical DhttpName::SUFFIX (.dhttp.net). Infrastructure names such as nat.genmeta.net can remain under Genmeta infrastructure domains.

Endpoint E records

Custom DNS record type E (QTYPE = 266) carries DHTTP endpoint addresses. The current wire format is:

flags(u8)
[sequence(varint) if CLUSTERED]
primary address: port(u16) + IPv4/IPv6 bytes
[agent address if NAT]
[load(f32) if LOAD]
[signature: scheme(u16) + len(varint) + bytes if SIGNED]

Flag bits:

Bit mask Name Meaning
0x80 FAMILY 0 = IPv4, 1 = IPv6.
0x40 MAIN Primary endpoint for the name.
0x20 CLUSTERED Sequence number is present; multiple publishers share the name.
0x10 NAT Agent address is present for NAT traversal.
0x08 LOAD One-minute load value is present.
0x01 SIGNED Signature with explicit TLS signature scheme is present.

For DHTTP endpoint publishing, MAIN and sequence are derived from the publisher certificate's DHTTP subject key identifier. Operators do not choose these fields manually: primary certificates publish MAIN = true, secondary certificates publish MAIN = false, and the certificate-chain sequence becomes the normalized endpoint-record sequence. An omitted sequence field means sequence 0.

Signed records encode the signature scheme in the record; the no-scheme signed format is not accepted. Legacy unsigned fixed-length endpoint address records are still parsed by length for address-only compatibility.

Project structure

src/core.rs                  DNS core module root
src/core/parser/             DNS packet, name, question, record, varint, and signature parsers
src/core/parser/record/      A/AAAA/SRV/TXT/PTR/CNAME/E record parsing and encoding
src/core/wire.rs             HTTP multi-record response wire format
src/mdns.rs                  mDNS module root
src/mdns/protocol.rs         UDP multicast socket and packet routing
src/mdns/service.rs          High-level mDNS service API
src/mdns/resolvers/          mDNS resolver integration
src/resolvers.rs             Resolver chain and resolver defaults
src/resolvers/h3.rs          DNS-over-H3 resolver/publisher
src/resolvers/http.rs        DNS-over-HTTP resolver/publisher
src/resolvers/deferred.rs    Deferred resolver initialization helper
src/publisher.rs             Endpoint record signer and publication loop
src/publisher/               Address selection, publish dispatch, packet signing
src/bin/ddns-server/         DNS-over-H3 server implementation
examples/                    mDNS and DNS-over-H3 example programs
server.toml                  Example server configuration