The BIND9 management SDK that should have existed a decade ago.
Caution
This project is in its very early stages. I decided to share it from the beginning anyway. If you're interested in contributing, feel free to reach out — contact info is on my profile.
A Rust-native library for programmatic BIND9 DNS server management. Implements the full rndc wire protocol, RFC 1035 zone file parsing, RFC 2136 dynamic updates (nsupdate), IXFR/AXFR zone transfers, and the BIND9 statistics-channel JSON API — all with zero shell subprocess dependencies.
Ships as three coordinated artifacts from one codebase:
- 🦀 Rust crate (
bind9-sdk) — the primary library, published to crates.io - 📦 Node.js/Bun native addon (
bind9-sdk-bindings) — napi-rs v3, native.node+ WASM fallback - 🐚 CLI tool (
bind9) — zone, record, DNSSEC, and stats management from the terminal
- 🔌 Full rndc wire protocol — 25+ commands over TCP, no shell subprocesses, no
rndcbinary needed - 📝 RFC 2136 dynamic updates — add, delete, and replace DNS records with TSIG signing
- 🔄 AXFR/IXFR zone transfers — async streaming client with compression pointer rejection and per-message timeouts
- 📄 Zone file parser — RFC 1035 compliant parser/serializer with zone diffing
- 📊 Statistics API — BIND9 statistics-channel JSON API client
- 🔐 TSIG authentication — HMAC-SHA256/SHA512, secrets zeroized on drop, never in logs
- 🧱
no_stdcore — core crate compiles without std, works in WASM and embedded contexts - 🔗 Connection pooling —
RndcPoolfor high-throughput rndc operations - 🧪 576 tests — unit, property (proptest), snapshot (insta), and integration tests
- 🦀 Single workspace — one repo, one
cargo build, all three artifacts
- Rust 1.94+ —
rust-toolchain.tomlpins the channel - Node.js 18+ — for the napi-rs native addon (optional)
- BIND9 9.20 — for integration tests (Podman rootless or native
named) - mise — recommended for toolchain and task management (optional)
git clone https://github.com/Sephyi/bind9-sdk.git
cd bind9-sdkmise manages the Rust toolchain, Node.js, and provides task shortcuts. A mise.toml is included in the repo.
# Install tools (Rust 1.94, Node LTS) and configure git hooks
mise install
mise run setup
# Common tasks
mise run build # Build all crates (release)
mise run build:cli # Build CLI binary (release)
mise run build:bindings # Build napi-rs .node file + JS/TS bindings (release)
mise run check # Type-check all crates (fast, no codegen)
mise run check:wasm # Check no_std WASM compliance
mise run check:bindings # Check napi-rs bindings compile
mise run test # Run unit tests
mise run test:integration # Run integration tests (starts BIND9 container)
mise run clippy # Lint
mise run fmt # Format all files
mise run fmt:check # Check formatting only
mise run ci # Full CI gate (fmt, clippy, wasm, test, audit)
mise run cli # Run CLI in debug mode (pass args after --)
mise run doc # Build and open API docs
mise run clean # Remove build artifactsIf you prefer not to use mise, ensure Rust 1.94+ and Node.js 18+ are installed.
# Set up git hooks
git config core.hooksPath .githooks
# Build all crates
cargo build --workspace
# Run tests (unit only — integration tests require a live BIND9)
cargo test --workspace
# Lint
cargo clippy --workspace --all-targets -- -D warnings
# Format check
cargo fmt --all --check# With mise
mise run build:cli
# Or manually
cargo build -p bind9-sdk-cli --release
# The binary is at target/release/bind9
./target/release/bind9 --help
# Or install to ~/.cargo/bin/
cargo install --path crates/bind9-sdk-cli# With mise (handles npm install + napi build)
mise run build:bindings
# Or manually
cd crates/bind9-sdk-bindings
npm install
npm run build
# Verify it works
node tests/smoke.mjsThis produces three files:
| File | Description |
|---|---|
bind9-sdk.node |
Native binary (~6 MB on arm64) |
index.js |
Platform loader (auto-detects OS/arch) |
index.d.ts |
TypeScript type definitions |
The SDK is not yet published to crates.io or npm. You can use it directly from git or a local checkout.
Point your Cargo.toml at the repository:
[dependencies]
# From GitHub (latest development branch)
bind9-sdk = { git = "https://github.com/Sephyi/bind9-sdk", branch = "development" }
# Or pin to a specific commit
bind9-sdk = { git = "https://github.com/Sephyi/bind9-sdk", rev = "303f445" }
# Or from a local checkout
bind9-sdk = { path = "../bind9-sdk/bind9-sdk" }Note
The path must point to the bind9-sdk/ subdirectory (the re-export crate), not the repository root.
To disable the network features (rndc, nsupdate, transfers) and use only the no_std core:
[dependencies]
bind9-sdk = { git = "https://github.com/Sephyi/bind9-sdk", default-features = false }# 1. Clone and build
git clone https://github.com/Sephyi/bind9-sdk.git
cd bind9-sdk/crates/bind9-sdk-bindings
npm install
npm run build
# 2. From your project, link to the built package
cd /path/to/your-project
npm link /path/to/bind9-sdk/crates/bind9-sdk-bindings
# Or add it as a file dependency in your package.json{
"dependencies": {
"bind9-sdk": "file:../bind9-sdk/crates/bind9-sdk-bindings"
}
}Then import as usual:
import { JsDomainName, JsZoneFile } from 'bind9-sdk';Once published, add to your Cargo.toml:
[dependencies]
bind9-sdk = "0.1"use bind9_sdk::{ZoneFile, DomainName};
let zone = ZoneFile::parse("$ORIGIN example.com.\nexample.com. 3600 IN A 192.0.2.1\n")?;
for record in &zone.zone.records {
println!("{} {} IN {:?}", record.name, record.ttl, record.data);
}use bind9_sdk::ZoneFile;
let old = ZoneFile::parse(&std::fs::read_to_string("old.zone")?)?;
let new = ZoneFile::parse(&std::fs::read_to_string("new.zone")?)?;
let diff = old.zone.diff(&new.zone);
println!("{diff}"); // Shows added/removed/changed recordsuse bind9_sdk::{
DomainName, RecordClass, RecordData, ResourceRecord, Ttl,
UpdateBuilder, TsigKey, TsigAlgorithm, NsUpdateSender,
};
let zone = DomainName::new("example.com.")?;
let key = TsigKey::new(
DomainName::new("update-key.")?,
TsigAlgorithm::HmacSha256,
&base64_decode("your-key-secret-here")?,
)?;
let record = ResourceRecord::new(
DomainName::new("www.example.com.")?,
RecordData::A("192.0.2.1".parse()?),
Ttl::new(3600),
);
let update = UpdateBuilder::new(zone, RecordClass::IN)
.add_record(record)
.sign_now(&key)
.build();
let sender = NsUpdateSender::new("127.0.0.1:53".parse()?);
let response = sender.send(&update, Some(&key)).await?;use bind9_sdk::{Bind9Client, ClientConfig, DomainName, NamedControl};
let key = /* TsigKey as above */;
let config = ClientConfig::new("127.0.0.1:953".parse()?, key);
let client = Bind9Client::new(config);
let status = client.status().await?;
println!("BIND {} — {} zones", status.version, status.zone_count);
client.reload_zone(&DomainName::new("example.com.")?).await?;use bind9_sdk::{TransferClient, DomainName, TransferRecord};
use tokio_stream::StreamExt;
let client = TransferClient::connect("127.0.0.1:53".parse()?, None).await?;
let stream = client.axfr(DomainName::new("example.com.")?, None).await?;
tokio::pin!(stream);
while let Some(result) = stream.next().await {
let record = result?;
match record {
TransferRecord::Record(rr) => println!("{} {}", rr.name, rr.ttl),
_ => {}
}
}The bind9 CLI provides zone, record, DNSSEC, and stats management.
All commands accept these flags (CLI flags override config file values):
--server <HOST> Server hostname or IP address
--port <PORT> rndc control port (default: 953)
--dns-port <PORT> DNS port for updates and transfers (default: 53)
--key-name <NAME> TSIG key name
--key-secret <SECRET> Base64-encoded TSIG key secret
--output <FORMAT> Output format: text (default) or jsonInstead of passing flags every time, create a config file:
- 🍎 macOS:
~/Library/Application Support/bind9-sdk/config.toml - 🐧 Linux:
~/.config/bind9-sdk/config.toml - 🪟 Windows:
%APPDATA%\bind9-sdk\config.toml
[server]
host = "127.0.0.1"
port = 953
dns_port = 53
stats_url = "http://127.0.0.1:8053"
[auth]
key_name = "rndc-key"
algorithm = "hmac-sha256" # or hmac-sha512
# key_secret = "..." # NOT recommended — use `bind9 auth set-key` insteadWarning
Do not store key_secret in the config file — it is plaintext on disk. Use bind9 auth set-key to store the secret in your OS credential store (macOS Keychain, Linux Secret Service, Windows Credential Manager). The config file key_secret field is supported as a last-resort fallback and emits a warning when used.
When the CLI needs the TSIG key secret, it checks these sources in order:
--key-secretflag — highest priority, useful for scripting- OS credential store — macOS Keychain / Linux Secret Service / Windows Credential Manager
- Config file
[auth].key_secret— fallback with a warning
Manage TSIG key secrets in your OS credential store instead of plaintext config files.
# Store a key (prompts for secret on stdin if --secret is omitted)
bind9 auth set-key --profile 127.0.0.1:953 --secret "base64-encoded-secret"
# Remove a stored key
bind9 auth delete-key --profile 127.0.0.1:953The --profile value should match the host:port of the server you connect to (e.g., 127.0.0.1:953).
# List zones (shows server status and zone count)
bind9 zone list
# Show zone status
bind9 zone status example.com.
# Reload a zone
bind9 zone reload example.com.
# Export a zone via AXFR (requires --dns-port for the DNS server port)
bind9 zone export example.com. --dns-port 53
# Diff two local zone files (no server needed)
bind9 zone diff old.zone new.zoneRecord commands use RFC 2136 dynamic updates and require --dns-port.
# Add an A record
bind9 record add example.com. www.example.com. A 192.0.2.1 --ttl 300 --dns-port 53
# Add an AAAA record
bind9 record add example.com. www.example.com. AAAA 2001:db8::1 --dns-port 53
# Add an MX record
bind9 record add example.com. example.com. MX "10 mail.example.com." --dns-port 53
# Delete a specific record
bind9 record delete example.com. www.example.com. A 192.0.2.1 --dns-port 53
# Delete all records of a type (omit data)
bind9 record delete example.com. www.example.com. A --dns-port 53Supported record types: A, AAAA, CNAME, NS, PTR, SOA, MX, TXT, SRV, CAA, DNSKEY, RRSIG, NSEC, NSEC3, NSEC3PARAM, DS, CDS, CDNSKEY, TLSA, SSHFP, CSYNC, RP, DLV, plus RFC 3597 TYPEn syntax for any type by number.
# Show DNSSEC status for a zone
bind9 dnssec status example.com.
# Check DS record publication
bind9 dnssec checkds example.com.# Fetch server statistics (requires stats_url in config or BIND9 statistics-channel)
bind9 stats
bind9 stats --output json# Generate completions for your shell
bind9 completions bash >> ~/.bashrc
bind9 completions zsh >> ~/.zshrc
bind9 completions fish > ~/.config/fish/completions/bind9.fishimport { JsDomainName, JsZoneFile, JsRndcClient } from 'bind9-sdk';
// Parse a domain name
const name = new JsDomainName('example.com.');
console.log(name.toString()); // "example.com."
console.log(name.labelCount()); // 2
// Parse a zone file
const zone = JsZoneFile.parse('$ORIGIN example.com.\nexample.com. 3600 IN A 192.0.2.1\n');
console.log(zone.recordCount()); // 1
// rndc client (async)
const client = new JsRndcClient('127.0.0.1', 953, 'rndc-key', 'base64-secret');
const status = await client.status();
console.log(status);Available binding modules: domain, zone, record, tsig, update, rndc, nsupdate, stats, transfer, pool.
bind9-sdk/
├── bind9-sdk/ ← re-export crate (crates.io entry point)
├── crates/
│ ├── bind9-sdk-core/ ← no_std + alloc; DNS types, zone parsing, TSIG, RFC 2136
│ ├── bind9-sdk-net/ ← tokio; rndc, nsupdate, AXFR/IXFR, stats HTTP, connection pool
│ ├── bind9-sdk-bindings/ ← napi-rs v3; native .node + WASM fallback
│ └── bind9-sdk-cli/ ← clap CLI binary
└── Cargo.toml ← workspace root| Crate | no_std |
What it provides |
|---|---|---|
bind9-sdk-core |
✅ | DNS types, zone parser/serializer, zone diff, RFC 2136 UpdateBuilder, TSIG (HMAC-SHA256/SHA512) |
bind9-sdk-net |
❌ | rndc wire protocol (25+ commands), NsUpdateSender, AXFR/IXFR client, stats HTTP, connection pool |
bind9-sdk-bindings |
— | Node.js/Bun native addon via napi-rs v3 (11 binding modules) |
bind9-sdk-cli |
— | bind9 binary with zone/record/dnssec/stats subcommands |
bind9-sdk |
— | Re-export crate — the single dependency users add |
Targeting GDPR, NIS2, NIST SP 800-53/800-81/800-57, ISO 27001:2022, and SOC 2 Type II compliance for DNS infrastructure.
- 🔒 Authentication — No anonymous rndc. TSIG secrets zeroized on drop (
Zeroizing<Vec<u8>>), never in logs or errors. Compile-time typestate prevents unauthenticated commands. - 🔐 Transport — Non-localhost zone transfers rejected without TLS config. TLS 1.3 pinned (rustls). rndc TLS and XoT transport planned for v0.2.
- 🛡️ DNSSEC — DNSKEY, DS, CDS, CDNSKEY, RRSIG, NSEC/NSEC3 record types. CDS generation with SHA-256/SHA-384. DNSSEC rndc commands (sign, validation, checkds). Key rollover helpers planned for v1.0.
- 📦 Supply Chain —
cargo auditin CI.#![forbid(unsafe_code)]in core and net. SBOM generation planned. - ⚙️ Defaults — HMAC-MD5 rejected. HMAC-SHA1 warns at compile time and runtime. HMAC-SHA256/SHA512 default. Key material stored in OS credential store (macOS Keychain, Linux Secret Service).
If you find bind9-sdk useful, consider sponsoring my work.
This project is dual-licensed:
- Open source — GNU Affero General Public License v3.0 (AGPL-3.0-only). You may use, modify, and distribute this software under the terms of the AGPL. If you modify and deploy it as a network service, you must release your source code.
- Commercial — A commercial license is available for organizations that cannot comply with the AGPL (e.g., proprietary SaaS, closed-source integrations). Contact me@sephy.io for licensing.
Copyright 2026 Sephyi