Skip to content

Sephyi/bind9-sdk

Repository files navigation

🌐 bind9-sdk   MSRV AGPL Commercial

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

✨ Highlights

  • 🔌 Full rndc wire protocol — 25+ commands over TCP, no shell subprocesses, no rndc binary 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_std core — core crate compiles without std, works in WASM and embedded contexts
  • 🔗 Connection poolingRndcPool for high-throughput rndc operations
  • 🧪 576 tests — unit, property (proptest), snapshot (insta), and integration tests
  • 🦀 Single workspace — one repo, one cargo build, all three artifacts

📋 Prerequisites

  • Rust 1.94+rust-toolchain.toml pins 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)

🔨 Building from source

git clone https://github.com/Sephyi/bind9-sdk.git
cd bind9-sdk

Using mise (recommended)

mise 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 artifacts

Manually

If 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

🐚 CLI tool

# 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

📦 Node.js native addon

# 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.mjs

This 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

📥 Using before publication

The SDK is not yet published to crates.io or npm. You can use it directly from git or a local checkout.

🦀 Rust — git dependency

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 }

📦 Node.js / Bun — local build

# 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';

🦀 Rust API

Once published, add to your Cargo.toml:

[dependencies]
bind9-sdk = "0.1"

📄 Parse a zone file

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);
}

🔀 Zone diff

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 records

📝 Dynamic update (RFC 2136)

use 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?;

🔌 rndc control

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?;

🔄 AXFR zone transfer

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),
        _ => {}
    }
}

🐚 CLI Usage

The bind9 CLI provides zone, record, DNSSEC, and stats management.

⚙️ Global options

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 json

📁 Configuration file

Instead 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` instead

Warning

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.

🔑 Secret resolution order

When the CLI needs the TSIG key secret, it checks these sources in order:

  1. --key-secret flag — highest priority, useful for scripting
  2. OS credential store — macOS Keychain / Linux Secret Service / Windows Credential Manager
  3. Config file [auth].key_secret — fallback with a warning

🔐 Auth commands

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:953

The --profile value should match the host:port of the server you connect to (e.g., 127.0.0.1:953).

🗂️ Zone commands

# 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.zone

📝 Record commands

Record 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 53

Supported 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.

🔐 DNSSEC commands

# Show DNSSEC status for a zone
bind9 dnssec status example.com.

# Check DS record publication
bind9 dnssec checkds example.com.

📊 Stats

# Fetch server statistics (requires stats_url in config or BIND9 statistics-channel)
bind9 stats
bind9 stats --output json

🐚 Shell completions

# Generate completions for your shell
bind9 completions bash >> ~/.bashrc
bind9 completions zsh >> ~/.zshrc
bind9 completions fish > ~/.config/fish/completions/bind9.fish

📦 Node.js / Bun API

import { 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.

🏗️ Architecture

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

🛡️ Compliance and Security

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 Chaincargo audit in 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).

💛 Sponsor

If you find bind9-sdk useful, consider sponsoring my work.

📄 License

This project is dual-licensed:

  • Open sourceGNU 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

About

Rust SDK for programmatic BIND9 DNS server management — rndc wire protocol, RFC 2136 dynamic updates, AXFR/IXFR zone transfers, zone file parsing, and statistics API. Ships as a Rust crate, Node.js native addon (napi-rs), and CLI tool.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Contributors

Languages