Skip to content
bwaliaPublic

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

2 watching

Forks

🛰️ NebulaDNS

A modern, observable, API-first authoritative DNS server written in safe Rust.

License MSRV Status Unsafe Platforms

NebulaDNS replaces 2001-era daemons (TinyDNS / djbdns) and 40-year-old C codebases (BIND) with a single small binary that ships with metrics, a control plane, and a Kubernetes operator — so the next failure is detected in seconds, not by customers.


Table of contents


Status

M1 (Wire + Zone) — the server answers real DNS queries over UDP and TCP from a TOML zone file and exposes labelled Prometheus metrics for every query.

Milestone Scope State
M0 Workspace, CI, /livez, /readyz, /metrics, systemd unit, Docker image, Helm chart skeleton, nebula-wire fuzz harness ✅ Done
M1 Full RFC 1035 codec, name compression, RR types, TOML zone loader, UDP + TCP listeners, data-plane metrics ✅ Done
M2 Wildcards, CNAME chasing, glue, RFC 2308 negative responses, EDNS negotiation, RRL Next
M3 AXFR / IXFR / NOTIFY / TSIG + daily interop matrix Planned
M4 Online DNSSEC signing (Ed25519 / ECDSA / RSA), HSM + KMS Planned
M5 Full REST API, gRPC, Swagger, CLI expansion, audit log Planned
M6 Propagation verifier + deploy gate Planned
M7 HA + multi-region replication (Raft + async log-ship) Planned
M8 Kubernetes operator + CRDs, ExternalDNS, CoreDNS drop-in Planned
M9 React dashboard Planned

Full milestone schedule in PROJECT_PROMPT.md §15.


Features

Today (M0 + M1)

Feature Detail
RFC 1035 wire codec Header, name (with compression + cycle detection), question, RR, message. Strict parsing — errors like QdCountMismatch are explicit variants, not silent corruption.
RR types A, AAAA, NS, CNAME, SOA, MX, TXT, PTR, SRV, CAA; unknown types pass through verbatim (good-secondary behaviour).
EDNS(0) OPT pseudo-RR decoded and re-emitted; DO bit parsed.
RFC 4035 flag bits AD and CD bits recognised (caught the first time dig queried us).
Zone loader Native TOML schema with deny_unknown_fields; hash-indexed RRsets with case-insensitive lookup.
Authoritative answer path NOERROR with answers / NODATA / NXDOMAIN + SOA / REFUSED for out-of-zone.
UDP + TCP listeners tokio-based; RFC 7766 length-prefix framing on TCP; UDP truncation with TC=1 when responses exceed 512 bytes.
Zone registry arc-swap-backed lock-free snapshot; readers never block; zones replaced atomically.
Control plane /livez, /readyz, /api/v1/version on a separate bind. Authenticated PUT /api/v1/zones/{zone}/records for fast CNAME/A/AAAA/TXT changes (fail-closed bearer token, default TTL 5s, hash-chained audit log).
Metrics Always-on Prometheus with compile-time-bounded labels (proto/qtype/rcode). Counter + histogram + drop reasons. Zero hot-path allocation.
Logging Structured JSON via tracing; configurable filter.
systemd integration Type=notify with sd_notify READY + watchdog; graceful SIGTERM shutdown.
Container Distroless non-root image, multi-arch, ~25 MB compressed. Built-in HEALTHCHECK.
CLI nebulactl version / nebulactl health.
Fuzz harness Four cargo-fuzz targets (header / question / name / message). 60 s smoke in CI on every PR.
Property tests proptest roundtrip for header, question, message.
Workspace hygiene #![forbid(unsafe_code)] in every crate. Clippy -D warnings with pedantic + nursery. cargo deny, cargo audit, rustfmt in CI.

Deployment and release engineering

Feature Detail
Helm chart StatefulSet, headless Service, LB Service (DNS), PodDisruptionBudget, NetworkPolicy, ServiceMonitor, PrometheusRule, helm test smoke pod. Zones mounted via ConfigMap from values.yaml.
Docker Compose NebulaDNS + Prometheus + Grafana (pre-provisioned dashboard) + optional BIND 9.18 peer. One-shot smoke profile runs dig + curl assertions.
Ansible deploy Idempotent role with staged binary install, pre-flip smoke test, atomic symlink swap, CAP_NET_BIND_SERVICE grant, config validation, health probe, old-binary pruning.
Rollback ansible-playbook rollback.yml flips the symlink back to the previously staged binary — no downloads, seconds per host.
Release pipeline deploy.yml: test gate → cross-compiled musl build (amd64 + arm64) → Ansible deploy with --check --diff preview in production.
Docker test workflow test-docker.yml: builds the image, spins up compose, runs real dig queries from the runner, scrapes /metrics.
k3s test workflow test-k3s.yml: spins up k3d, loads the image, helm install, helm test, port-forward + dig.
CI gates Fmt, clippy, nextest, cargo deny, cargo audit, fuzz-smoke, helm lint + kubeconform, hadolint, shellcheck, ansible-lint + syntax-check.

Quickstart

Docker Compose

The fastest way to see NebulaDNS serve real traffic. Includes Prometheus, Grafana with a pre-provisioned dashboard, and a dig-based smoke container.

git clone https://github.com/nebuladns/nebuladns.git
cd nebuladns

docker compose -f deploy/docker/compose/docker-compose.yml up --build -d

# Real DNS over UDP and TCP
dig @127.0.0.1 -p 5353 www.example.com A +short
# 192.0.2.10
# 192.0.2.11

dig @127.0.0.1 -p 5353 +tcp example.com SOA +short
dig @127.0.0.1 -p 5353 nope.example.com A | grep status:     # NXDOMAIN
dig @127.0.0.1 -p 5353 foo.not-our-zone.test A | grep status: # REFUSED

# Metrics + dashboards
curl -s http://127.0.0.1:9090/metrics | grep nebula_dns_queries
open http://127.0.0.1:3000   # Grafana  (admin / admin)
open http://127.0.0.1:9091   # Prometheus

# Automated smoke suite (dig + /metrics assertions). Exits non-zero on failure.
docker compose -f deploy/docker/compose/docker-compose.yml --profile smoke run --rm smoke

# Teardown
docker compose -f deploy/docker/compose/docker-compose.yml down -v

Fast record changes (CNAME failover)

Zone writes are fail-closed: they require Authorization: Bearer … matching NEBULA_API_TOKEN (or api.token_sha256). Default TTL is 5 seconds so resolver caches expire quickly. Apex CNAME and CNAME-alongside-other-types are rejected.

export NEBULA_API_TOKEN=please-use-a-long-random-secret
# Restart nebuladns so it picks up the token, then:

curl -sS -X PUT http://127.0.0.1:8080/api/v1/zones/example.com/records \
  -H "Authorization: Bearer $NEBULA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: failover-1" \
  -d '{"name":"app","type":"CNAME","value":"west.example.net.","ttl":5}'

dig @127.0.0.1 -p 15353 app.example.com CNAME +ttl

# Automated workflow (spawns a throwaway server, stdlib only):
cargo build -p nebula-server --bin nebuladns
python3 tests/python/test_fast_record.py --bin target/debug/nebuladns

See docs/decisions/0002-fast-record-api.md.

Helm / Kubernetes

# Basic install (from the repo — OCI chart publishing lands with v1.0).
helm install nebuladns deploy/helm/nebuladns \
  --namespace nebuladns --create-namespace

# Or with a real zone inlined in values:
cat <<'EOF' > /tmp/values.yaml
zones:
  - name: "example.com"
    content: |
      origin = "example.com."
      default_ttl = 300
      [soa]
      mname = "ns1.example.com."
      rname = "hostmaster.example.com."
      serial = 2026041701
      refresh = 10800
      retry = 3600
      expire = 604800
      minimum = 300
      [[records]]
      name = "www"
      type = "A"
      value = "192.0.2.10"
EOF
helm install nebuladns deploy/helm/nebuladns \
  -n nebuladns --create-namespace -f /tmp/values.yaml

# Smoke
helm test nebuladns -n nebuladns --logs

# Probe
kubectl -n nebuladns port-forward svc/nebuladns 15353:53 9090:9090 &
dig +tcp @127.0.0.1 -p 15353 www.example.com A +short
curl -s http://127.0.0.1:9090/metrics | head

For Docker + k3s end-to-end tests, the GitHub Actions workflows are already wired:

systemd / Debian / Ubuntu

Hardened systemd unit with watchdog + sd_notify. Automated via Ansible.

# One-time host prep (on the target, as root):
adduser --disabled-password --gecos "NebulaDNS deploy" deploy
echo "<your SSH pub key>" >> /home/deploy/.ssh/authorized_keys
echo 'deploy ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/deploy-nebuladns

# On your machine (or GitHub runner):
cd deploy/ansible
ansible-galaxy collection install -r requirements.yml

# Copy inventory.example.ini → inventory.ini, edit hosts, then:
ansible-playbook -i inventory.ini playbook.yml \
  -e nebuladns_version=v0.1.0 \
  -e @environments/staging.yml

# Rollback in seconds (no downloads, symlink flip only):
ansible-playbook -i inventory.ini rollback.yml

See deploy/ansible/README.md for secrets, environments, and the full release pipeline.

Build from source

git clone https://github.com/nebuladns/nebuladns.git
cd nebuladns

cargo build --release --bin nebuladns --bin nebulactl

./target/release/nebuladns --config config/nebuladns.demo.toml &
dig @127.0.0.1 -p 15353 www.example.com A +short
./target/release/nebulactl health

Requires Rust stable 1.80+ (pinned via rust-toolchain.toml).

Model Context Protocol (MCP)

nebula-mcp exposes NebulaDNS admin operations to Claude (Desktop, Code, or any other MCP-aware client) as a stdio server. Read-only tools are active today against M0/M1; the full zone / deploy / secondaries tool set lights up once the M5 REST surface ships.

cargo build --release --bin nebula-mcp

Register it with Claude Desktop in ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "nebuladns": {
      "command": "/absolute/path/to/target/release/nebula-mcp",
      "env": {
        "NEBULA_API": "127.0.0.1:8080",
        "NEBULA_FAILOVER_API": "127.0.0.1:9119",
        "NEBULA_MCP_ALLOW_WRITES": "0"
      }
    }
  }
}

Or add it to Claude Code: claude mcp add nebuladns /absolute/path/to/nebula-mcp.

Mutating tools (create_zone, replace_zone, add_records, set_record, delete_record, rollback_zone, force_notify, trigger_dnssec_rollover, deploy, and the failover_* write tools) refuse to run unless NEBULA_MCP_ALLOW_WRITES=1 is set — a per-process kill switch that prevents the model from changing DNS state in read-only sessions.

Failover tools talk to nebula-dns-failover (NEBULA_FAILOVER_API, default 127.0.0.1:9119):

Tool Gate Effect
failover_status read POP health + per-hostname serving state / override
failover_force_secondary write Pin hostname to secondary (sticky)
failover_force_primary write Pin hostname to primary (sticky)
failover_clear_override write Back to health-driven policy
failover_reconcile write Probe + reconcile now

Write tools also need NEBULA_FAILOVER_TOKEN (the controller's FAILOVER_API_TOKEN). Overrides persist until cleared, so the tool descriptions tell the model to confirm with an operator before pinning.


Why NebulaDNS

We ran TinyDNS (djbdns 1.05, released 2001) behind a CDN. In 22 days we had two production outages on real customer traffic. Both traced to the same root cause: djbdns 1.05 sends AXFR responses with QDCOUNT=0, which BIND 9.18+ rejects as FORMERR. Our downstream secondaries silently upgraded to BIND 9.18, and our zone-transfer redundancy eroded invisibly for 13 months until only one working agent remained.

There was no /metrics, no API, no post-deploy verification, no way to see that half the fleet had stopped transferring. Engineers had to SSH into six servers to reconstruct state during the incident.

We evaluated every serious authoritative DNS server in production use today. Every single one has a structural limitation NebulaDNS is designed to overcome:

Server Critical limitation NebulaDNS answer
TinyDNS / djbdns 1.05 No DNSSEC, no IXFR, no TSIG, QDCOUNT=0 wire bug, no metrics, no API, unmaintained Full RFC-conformant wire, modern Rust safety, API-first, /metrics native, TSIG/DNSSEC built in
BIND 9 Monolithic C with long CVE history, named.conf is file-only and fragile, rndc limited, statistics-channels is XML/JSON without Prometheus, no built-in UI Single small binary, memory-safe Rust, /metrics native, REST + gRPC API, integrated UI, unified DNSSEC management
Knot DNS No native REST API (control is knotc CLI), no built-in UI, no propagation verification REST + gRPC + CLI + UI from day one, propagation verifier native
NSD Intentionally minimal: no dynamic updates, no online DNSSEC signing, no API, no UI, no geo-routing Full-featured while retaining NSD-class performance via Rust
PowerDNS Auth API is incomplete (many operations still require SQL backend edits), multiple moving parts, no native propagation gate API covers 100% of operations, propagation verifier built-in, single binary
CoreDNS Not a full-fledged auth server for internet-facing zones, DNSSEC primitive, Go GC tail latency at high QPS First-class authoritative + transfer + DNSSEC; drop-in replacement as cluster DNS; consistent low-tail latency (no GC)
Route 53 / Cloudflare DNS Vendor lock-in, cost at scale, no self-hosted option Self-hosted with the same UX quality; optionally integrates as a secondary

The common pattern across every open-source alternative: the control plane is an afterthought bolted onto a 1990s-era data plane. NebulaDNS inverts this — the API is the server, and the data plane is a first-class implementation of the API's contract.

Most importantly: no alternative ships a first-class propagation gate. That single primitive — "this change has reached all required downstreams; OK to proceed" — is the feature that would have prevented both of our incidents. It's core to NebulaDNS from M6.


Using this DNS (ns1 / ns2) for incidents and failover

Live fleet (co-located on diy-tax-return-infra cloud edge workers):

Name Host IP
ns1 cloud001 72.62.211.28
ns2 vps002 85.190.106.88

Apex nebuladns.net stays on Cloudflare (anycast). NebulaDNS is authoritative for zones you NS-delegate to ns1/ns2 (starter zone: demo.nebuladns.net). Operational Ansible + SSH live in the sibling checkout diy-tax-return-infra (docs/runbooks/nebuladns.md, ansible/nebuladns.yml). Product-side companion: docs/runbooks/NsSetupAndCloudflare.md.

Why this layout (incidents + failovers)

DNS outages are usually not “the nameserver binary died” — they are wrong answers, stale caches, or a single POP still advertised after it is dead. This stack separates three jobs so each has a clear fix path:

Job Who owns it Why it matters in an incident
Apex / public NS anycast Cloudflare Resolvers keep finding the zone even if one of our VPs is down
Authoritative answers for delegated zones NebulaDNS on ns1 + ns2 Two independent authoritative hosts; dig either IP to see ground truth without Cloudflare cache
App hostname traffic shift (lon1 ↔ lon2) nebula-dns-failover rewriting Cloudflare A/CNAME Health probes move clients off a dead edge in ~TTL (often 60–90s), fail-open keeps last-good RRset

Do not confuse ns1/ns2 with lon1/lon2. ns* are DNS servers; lon* are HTTP edge POPs. Failover rewrites records that point at POPs — it does not replace Cloudflare as the zone’s public nameservers (unless you deliberately cut over to Option A).

Best default for app failover today: provider.type = "cloudflare" on the failover controller (see docs/runbooks/DnsFailover.md). Use provider.type = "nebuladns" when the zone’s public NS are ns1/ns2.

Live CNAME failover test (ns1/ns2): https://test.failover.nebuladns.net/ — demo page shows which POP you landed on, live DoH CNAME + TTL, lon1/lon2 /healthz, and the failover story. DNS: TTL 30, CNAME lon1.pop0.uk ↔ lon2.pop0.uk via nebula-dns-failover on both NS hosts. Agent skill: .cursor/skills/dns-level-failover/SKILL.md. Reference pilot: abtesting.fictionally.org.

How to use it day to day

1. Point a name at NebulaDNS (delegated child)

  1. Upsert Cloudflare glue via infra script (reads the wslproxy Cloudflare token from k3s1 wslproxy-system/wslproxy-settings): python3 diy-tax-return-infra/scripts/nebuladns-cloudflare-dns.py — creates A ns1/ns2 (DNS only) and NS demo → ns1 + ns2.
  2. Put the zone TOML on both hosts (Ansible sync) and reload.
# Ground truth — bypass Cloudflare and public recursion
dig @72.62.211.28 www.demo.nebuladns.net A +norecurse
dig @85.190.106.88 www.demo.nebuladns.net A +norecurse

# What the world sees after delegation + TTL
dig www.demo.nebuladns.net A +trace

2. During a DNS / edge incident

# Is NebulaDNS up on both NS?
curl -sf http://127.0.0.1:18080/livez   # on each host (admin is localhost-only)
dig @72.62.211.28 demo.nebuladns.net SOA +short
dig @85.190.106.88 demo.nebuladns.net SOA +short

# App hostname: what does Cloudflare currently return?
dig +short app.example.com A

# Failover controller (control host, not the POP under test)
curl -sf http://127.0.0.1:9119/v1/status
# Break-glass: pin to secondary / clear override — see DnsFailover.md

3. Change a record fast (failover or hotfix)

  • Preferred for public app names on Cloudflare: let nebula-dns-failover rewrite managed A/CNAME via the Cloudflare API (marker-owned records only).
  • On a Nebula-authoritative zone: PUT /api/v1/zones/{zone}/records with a short TTL (default 5s) — see Fast record changes. Reload (systemctl reload nebuladns) reverts in-memory API writes to the zone file; the failover controller re-applies on its next tick.

4. Redeploy / roll back NS hosts

# From diy-tax-return-infra/ansible (existing cloud_workers SSH keys)
ansible-playbook nebuladns.yml -e nebuladns_version=dev-<sha>
# Or stop during a bad deploy:
ansible nebuladns_hosts -b -m systemd -a 'name=nebuladns state=stopped'

When to escalate to full Nebula apex (Option A)

Only after both NS answer SOA for nebuladns.net and you want Cloudflare as registrar only. Switch the zone to custom nameservers ns1/ns2, then run failover with provider.type = "nebuladns". Until then, keep the split model: Cloudflare apex + Nebula delegated zones + Cloudflare-backed failover.


Architecture

                       ┌──────────────────────────────────────────┐
                       │            React Web UI (v1.0)           │
                       │       Vite + TS + Tanstack Query         │
                       └───────────────────┬──────────────────────┘
                                           │ HTTPS (OpenAPI)
                       ┌───────────────────▼──────────────────────┐
                       │          Control Plane API               │
                       │   axum (REST) + tonic (gRPC)             │
                       │   AuthN: mTLS + OIDC   AuthZ: RBAC       │
                       └───────┬──────────────────┬───────────────┘
                               │                  │
                ┌──────────────▼──┐       ┌───────▼────────────┐
                │  Zone Manager   │       │ Propagation        │
                │  validate       │       │ Verifier           │
                │  sign (DNSSEC)  │       │  polls secondaries │
                │  atomic commit  │       │  confirms SOA      │
                └────────┬────────┘       └────────────────────┘
                         │
        ┌────────────────▼─────────────────────────┐
        │       Zone Store (content-addressed)     │
        │       sled / redb / pluggable            │
        └────────────────┬─────────────────────────┘
                         │
        ┌────────────────▼─────────────────────────┐
        │          DNS Data Plane                  │
        │   tokio + io_uring (Linux ≥ 5.19)        │
        │   UDP · TCP · DoT · DoH · DoQ            │
        │   AXFR / IXFR / NOTIFY / TSIG            │
        │   DNSSEC online signer                   │
        └──────────────────────────────────────────┘

    Observability spine: tracing → OpenTelemetry → Prometheus + Loki + Tempo

Crate layout

Crate Purpose
nebula-wire RFC 1035 wire codec. #![forbid(unsafe_code)], fuzz-tested.
nebula-proto High-level DNS message types (no I/O).
nebula-zone TOML zone loader + indexed in-memory zone.
nebula-signer DNSSEC online signing + key management (M4).
nebula-store Storage abstraction + embedded backends (M5).
nebula-transfer AXFR / IXFR / NOTIFY / TSIG (M3).
nebula-server DNS data plane (tokio) + nebuladns binary.
nebula-api REST (axum) + gRPC (tonic) + OpenAPI.
nebula-verify Propagation verifier (M6).
nebula-metrics Always-on Prometheus registry.
nebula-cli nebulactl admin CLI.
nebula-mcp Model Context Protocol server. Exposes admin ops to Claude / any MCP client.
nebula-dns-failover Health-aware DNS failover controller (probe POPs → rewrite managed A/CNAME). See crates/nebula-dns-failover/README.md.
nebula-testutil Test harness + reference-peer drivers.

Observability

Metrics are always on in production. No "enable metrics" flag exists — there is no scenario where observability competes with performance. Cardinality is bounded at compile time via enum-typed labels.

# Wire / query pipeline — live today
nebula_dns_queries_total{proto,qtype,rcode}
nebula_dns_query_duration_seconds_bucket{proto,qtype,rcode,le}
nebula_dns_dropped_total{reason}
nebula_build_info{version,commit,rustc,target} 1

# Failover controller (nebula-dns-failover, :9119) — live today
nebula_dns_failover_state{hostname}              # 1 primary … 4 degraded (see HELP)
nebula_dns_failover_pop_up{pop}                  # 0 | 1
nebula_dns_failover_transitions_total{hostname,from,to,reason}
nebula_dns_failover_dns_apply_total{hostname,result}
nebula_dns_failover_probe_latency_milliseconds{pop}
nebula_dns_failover_last_success_unixtime

# Transfers (M3)
nebula_axfr_attempts_total{peer,zone,direction,result}
nebula_axfr_last_success_timestamp_seconds{peer,zone}
nebula_peer_version_info{peer,software,version}   # the 1326 signal

# Propagation verifier (M6)
nebula_zone_propagation_converged{zone}           # 0 | 1

# Runtime (planned)
nebula_process_resident_memory_bytes
nebula_memory_hot_path_allocations_total          # MUST stay at 0

Grafana dashboard

NebulaDNS Overview (uid nebuladns-overview) covers every live metric above:

  • Overview: instances up/down, query rate, p99, SERVFAIL / NXDOMAIN / REFUSED ratios, drops.
  • Traffic: by protocol, query type, rcode and instance.
  • Latency: percentiles, heatmap, p99 by query type and by instance.
  • Errors and drops: non-NOERROR responses, drops by reason.
  • Fleet: per-instance table (version, up, load, p99, error ratio) and scrape health.
  • Failover controller: serving state, POP health, probe latency, state changes, apply results, transition history.

Variables (data source, job, instance, protocol, query type, rcode, failover job, hostname, POP) drive every panel. Annotations mark restarts and failover state changes.

  • Docker Compose: provisioned automatically at http://localhost:3000.
  • Kubernetes: --set grafanaDashboard.enabled=true renders a ConfigMap for the Grafana sidecar (kube-prometheus-stack).
  • Source of truth: deploy/docker/compose/grafana/dashboards/overview.json. The chart ships a copy in deploy/helm/nebuladns/dashboards/; CI fails if they drift.

Alerts

deploy/prometheus/nebuladns.rules.yml holds 14 alerts. Each one links a runbook in docs/runbooks/.

Area Alerts
Availability NebulaDNSDown (page), NebulaDNSAllInstancesDown (page), NebulaDNSTargetsMissing, NebulaDNSVersionDrift
Answers NebulaDNSServfailRatioHigh (page), NebulaDNSRefusedRatioHigh, NebulaDNSLatencyHigh, NebulaDNSDropsHigh
Failover NebulaDNSFailoverControllerDown (page), NebulaDNSFailoverApplyFailing (page), NebulaDNSFailoverBothPopsDown (page), NebulaDNSFailoverPopDown, NebulaDNSFailoverFlapping, NebulaDNSFailoverManualOverrideStale

Alerts not marked page are tickets.

  • Plain Prometheus (VM hosts): add the file under rule_files:. Scrape jobs need nebuladns and dns-failover in their names.
  • Docker Compose: loaded automatically; see http://localhost:9091/alerts.
  • Kubernetes: --set prometheusRule.enabled=true. The chart embeds a copy, and CI fails if it drifts.
  • Tests: promtool unit tests in deploy/prometheus/tests/ run in CI.
  • Slack routing: deploy/alertmanager/ sends pages (with @here) and tickets to separate Slack channels. It's included in the Compose rig.

Operations

  • systemd unit: deploy/systemd/nebuladns.service — Type=notify, watchdog 30 s, ProtectSystem=strict, AmbientCapabilities=CAP_NET_BIND_SERVICE, seccomp filter, MemoryDenyWriteExecute=true.
  • Graceful shutdown: SIGTERM/SIGINT drains connections, signals STOPPING=1, lets in-flight requests finish, then exits cleanly.
  • Zone reload: systemctl reload nebuladns (SIGHUP) re-reads the [[zones]] list and zone files and swaps them in atomically, with no dropped queries. A bad zone file is logged and the previous zones keep serving. Binds, logging and API settings still need a restart. Record-API writes are in-memory only, so a reload reverts them; nebula-dns-failover re-applies its overrides on its next tick.
  • Rollback: atomic binary symlink swap — MTTR ≈ seconds per host.
  • Zero-downtime restart: supported via nebuladns.socket (socket activation).

Security

  • Memory-safe by default: #![forbid(unsafe_code)] in every crate. Exceptions require a written justification and a fuzz harness.
  • Continuous fuzzing: 4 cargo-fuzz targets (header / question / name / message) run for 60 s on every PR; ClusterFuzzLite / OSS-Fuzz integration tracked for post-GA.
  • Supply chain: cargo deny in CI with a whitelist of licenses (Apache-2.0, MIT, BSD-, ISC, Unicode-, CC0, Zlib, MPL-2.0). cargo audit for advisory database checks.
  • Signed artifacts (planned): SBOMs (CycloneDX + SPDX) + cosign keyless via GitHub OIDC + SLSA-3 provenance on every release.
  • Reporting: see SECURITY.md — GPG key + 90-day coordinated disclosure.

Repository layout

nebuladns/
├── crates/                    # 11 Cargo crates (see "Crate layout" above)
├── config/                    # example + compose + demo configs
├── zones/                     # example zone files
├── deploy/
│   ├── docker/                # Dockerfile + compose rig + Grafana dashboards
│   ├── helm/nebuladns/        # Helm chart (StatefulSet, Service, PDB, tests/)
│   ├── systemd/               # hardened .service + .socket + sysusers/tmpfiles
│   ├── ansible/               # role + playbook + environments + rollback
│   ├── prometheus/            # alerting rules (template in M6)
│   ├── grafana/               # upstream dashboards (template in M6)
│   └── terraform/             # Terraform module (placeholder)
├── docs/
│   ├── architecture.md        # one-page system view
│   ├── decisions/             # ADRs — toolchain, storage, consensus
│   └── runbooks/              # one per alert
├── ui/                        # React dashboard (M9)
├── xtask/                     # dev-loop tasks (`cargo xtask ci|fmt|clippy|fuzz|bench`)
├── .github/workflows/         # ci, deploy, test-docker, test-k3s
├── index.html                 # landing page (open directly in a browser)
├── PROJECT_PROMPT.md          # full planning prompt — the source of truth for intent
└── README.md                  # you are here

Contributing

  1. Every feature ships with a test, a metric, and a doc section. No exceptions.
  2. API-first: every operator-visible behaviour must be an API call before any CLI or UI work merges.
  3. Every Prometheus alert has a runbook in docs/runbooks/. CI enforces the link.
  4. Every non-obvious design choice needs an ADR in docs/decisions/.

Dev loop:

cargo xtask ci        # fmt + clippy + tests + deny
cargo xtask fuzz wire # smoke-fuzz the wire codec
cargo xtask bench     # criterion benchmarks

Conventional Commits required (feat(scope): …, fix(scope): …). See CONTRIBUTING.md for the full guide.


License

Apache-2.0. See also SECURITY.md, CODE_OF_CONDUCT.md, and PROJECT_PROMPT.md (the planning document that serves as the canonical source of design intent).


Incidents 1273 and 1326 cannot recur in the same form. That's the bar.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages