BINDA is an independent FOSS DNS resolver project. The name is derived from hexadecimal notation for "bind10" and is unrelated to any commercial brands. It is not affiliated with, endorsed by, or derived from the BIND/BIND9 project or ISC (Internet Systems Consortium) — BINDA's codebase is entirely independent and unique from that project.
BINDA is a Rust-native DNS resolver and naming system designed to make traditional domain registrars unnecessary: anyone can register a free domain name and keep it for as long as they answer a liveliness probe within a 1-minute window.
- Free-for-life registration — a domain stays yours as long as you respond to a liveness probe within a minute; miss the window and it falls back into the pool. (Originally a stricter 3 seconds; relaxed to give room for ordinary reconfiguration downtime.)
- Squatting resistance — a maximum of 5 live registrations per
forward-confirmed host allocation, regardless of how many Ed25519 owner
keys it rotates through. Owner keys authenticate updates; they do not
create capacity. That hostname isn't just asserted: every
registration is checked with a real, forward-confirmed reverse DNS
(FCrDNS) lookup against the request's actual source IP — the claimed
hostname's PTR record must name it, and its A/AAAA record must resolve
back to that same IP (see
fcrdns) — so the cap applies to a real, distinctly-controlled host rather than to a free-to-mint keypair. When separate instances concurrently learn more than five valid claims for one host, every replica deterministically retains the same first five and rejects the rest. A later vacancy is free for a new claim; it never automatically promotes an earlier rejected registration. - Full Unicode names, no length limit — domain labels may use any printable Unicode scalar value: emoji, combining-mark ("zalgo") sequences, and mixed right-to-left/left-to-right scripts (intermixed within a single label, not just adjacent) are all valid. Unlike classic DNS's 255-octet/63-byte-label ceiling, there is no length cap at all — a name is only as long as available memory and the transport's own datagram size allow.
- Mutual collision arbitration — when two nodes learn of simultaneous claims on the same name, neither unilaterally decides the tie-break rule; they each randomly propose "higher wins" or "lower wins" every round until they agree, and that agreed rule decides the winner.
- Gossip propagation — nodes exchange registration state via
epidemic/anti-entropy gossip. No node is marked trusted or distrusted;
every information pull bundles a small behavioural test (a
ConformanceChallenge— two synthetic tokens and a win condition), answerable only by actually running BINDA's own deterministic collision-resolution logic, and a peer's rumors are adopted only if it answers that correctly and obeys the request/response contract (no duplicates and no unrequested domains). Getting any of that wrong, or being malformed at all, means the same thing either way: for that exchange, we assume we're not talking to a real BINDA node and ignore everything it sent — with no memory of the failure carried into the next exchange. - TOML zone files — the same information a BIND9 zone file carries (SOA, records), expressed as TOML.
crates/binda-core— the library crate: domain names, client identity, liveness/registration rules, the gossip message types, collision resolution, the zone file format, the resolver query/answer types, and the JSON wire encoding shared by all of them.crates/binda— thebindabinary crate: a UDP gossip daemon plus a UDP resolver query daemon built on top ofbinda-core.
Requires Rust nightly (see rust-toolchain.toml).
cargo build --workspace
cargo test --workspace
cargo doc --workspace --no-deps --openCoverage is measured with cargo-llvm-cov
and reported to Codecov on every push
via .github/workflows/coverage.yml (the
badge above needs a CODECOV_TOKEN repository secret to actually upload —
add one from codecov.io's project settings for it to go live). To check
locally:
cargo install cargo-llvm-cov # once
cargo llvm-cov --workspace --summary-only # terminal summary
cargo llvm-cov --workspace --html --open # browsable per-line reportbinda --gossip 0.0.0.0:9530 --resolver 0.0.0.0:9531 --api 0.0.0.0:9532 \
[--dns 0.0.0.0:9533] [--peer <host:port> ...]--gossip— UDP address to listen on for anti-entropy gossip with peers.--resolver— UDP address to listen on for BINDA's ownResolveQuery/ResolveAnswerlookups.--api— UDP address to listen on for the client-facing registration API: signedProbe/Register/SetRecordsrequests (seeclient_api).--dns— optional UDP address to also speak BINDA's own native, DNS-shaped name-lookup protocol on (seedns): same message framing as classic DNS, but every label is raw UTF-8 on the wire. It is not RFC 1035 wire-compatible and never produces or accepts Punycode/ASCII-compatible encoding — that translation step is exactly what BINDA exists to make unnecessary. Omit the flag to skip this listener entirely.--peer— a known peer's gossip address; repeatable. Peers also learn about each other dynamically from inbound gossip, so only one bootstrap peer per new node is typically needed.--insecure-skip-rdns-verification— accept every claimedrdnshostname without checking it (seefcrdns::AllowAllVerifier). This is for running a node locally, for demos and manual testing, where no claimed hostname could ever genuinely forward-confirm. Never pass this on a node anyone else can reach — it turns the "5 domains per live socket" cap back into "5 domains per free keypair."
A node's clock is disciplined against public NTP servers
(ntp::NtpTimeSource) rather than trusting
the raw local clock, since the whole liveness model depends on every node
agreeing on roughly the same time.
See examples/client_demo.rs for
a full walkthrough of a client: probe liveness, register a Unicode
domain, publish an A record, then resolve it both via BINDA's own
resolver protocol and via BINDA's native, DNS-shaped protocol — proving
the label crosses the wire as raw UTF-8, not Punycode. The node below is
started with --insecure-skip-rdns-verification so the demo's made-up
rdns claim is accepted and register actually succeeds; drop that flag
to see the real FCrDNS check correctly refuse it instead.
cargo run -p binda --bin binda -- --gossip 127.0.0.1:9530 --resolver 127.0.0.1:9531 --api 127.0.0.1:9532 --dns 127.0.0.1:9533 --insecure-skip-rdns-verification &
cargo run -p binda --example client_demoCore registration, liveness, collision, gossip, zone-file, client API,
NTP time, and native DNS-shaped wire-protocol types are implemented and
unit-tested, and networking is wired up end-to-end: nodes gossip via UDP
anti-entropy digests, and clients can probe/register/publish records over
the client API and resolve via either BINDA's own JSON resolver protocol
or its native DNS-shaped protocol — both fully Unicode-native, with no
Punycode/ASCII-compatibility step anywhere in BINDA. Overall test coverage
sits at ~98% (line/region across the whole workspace, 142 + 25 = 167
tests), including real end-to-end integration tests for the gossip
conformance-challenge protocol (a rogue peer answering incorrectly, a
peer answering correctly, and full convergence between two real nodes
over UDP), local fake-server tests exercising the actual network code
paths of NTP sync and FCrDNS verification without needing real internet
access, and coverage of every listener's bind-failure path. main()
itself is excluded from the coverage count (#[coverage(off)]) since
it's pure top-level wiring around already-covered pieces and, by design,
never returns in a real run.
Still missing/simplified, in rough priority order:
- The native DNS-shaped codec supports A/AAAA/CNAME/MX/TXT/NS over a single question, no compression on the way in, no EDNS0, no zone transfers, and — being intentionally not RFC 1035 compatible — no interoperability with legacy DNS resolvers (a translating gateway, if ever wanted, would be a separate, optional component).
- Gossip's "is this rumor newer" check compares timestamps only; it doesn't yet re-run collision resolution for rumors with an equal or older timestamp than a differing local claim, so some collisions only resolve one node at a time as digests keep exchanging.
- No persistence: every node's registry is purely in-memory and starts empty on restart, by design ("temporary... in-memory datastore"), but there's no snapshot/replay to speed up rejoining a network either.
- Every UDP listener (gossip, resolver, client API, native name-lookup)
is now rate-limited per source address via a token bucket (see
rate_limit) rather than a hard message-size cap — a deliberate choice so a single large-but-legitimate message (an extravagant zalgo name, say) is never penalized, while a sender flooding a listener gets throttled.MAX_DATAGRAM_BYTESstill exists, but only as the actual physical UDP payload ceiling (65,507 bytes), not a policy limit. Source addresses are still spoofable, so this doesn't stop a distributed flood; it's a first line of defense, not the whole story. - The remaining ~2% is essentially irreducible without contrived tests:
a handful of
node.rslines are the "kick off an infinite listener loop" statements inside tests that intentionally never let that loop finish (so the async state machine's completion path is never counted, even though the loop's real behavior is thoroughly exercised via real socket round-trips elsewhere in the same test); a couple offcrdns/ntplines are a test-helper's own defensive error arm that only fires if a background thread's socket read fails after the test has already gotten what it needed; and a few are error variants (like a JSON encode failure) that aren't reachable through this project's own types without constructing a deliberately-broken value.