Dns server for kubernetes with support for blocklists and CRD record definitions
  • Rust 99%
  • Dockerfile 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Renovate [bot] c4daa03223
All checks were successful
release.yaml / Generate next release tag (push) Successful in 2s
chore(deps): update https://git.zarantonello.dev/projects/forgejo-agent action to v1.9.12 (#40)
2026-09-09 03:15:37 +00:00
.devcontainer chore(deps): pin git.zarantonello.dev/infra/ci-rust docker tag to 58fbc89 (#11) 2026-08-26 09:56:51 +00:00
.forgejo chore(deps): update https://git.zarantonello.dev/projects/forgejo-agent action to v1.9.12 (#40) 2026-09-09 03:15:37 +00:00
examples fix: answer with nodata for non-rt matched crd domains (#9) 2026-08-26 09:48:15 +00:00
src refactor: use hickory-resolver directly (#27) 2026-08-31 22:40:27 +00:00
.dockerignore feat: initial commit 2026-08-21 19:33:08 +02:00
.gitignore feat: initial commit 2026-08-21 19:33:08 +02:00
.prettierrc.json docs: add some files (#38) 2026-09-08 15:51:35 +00:00
.releaserc.json fix: answer with nodata for non-rt matched crd domains (#9) 2026-08-26 09:48:15 +00:00
.renovaterc.jsonc chore: update configs (#2) 2026-08-21 17:51:31 +00:00
.yamllint.yml feat: initial commit 2026-08-21 19:33:08 +02:00
AGENTS.md docs: add some files (#38) 2026-09-08 15:51:35 +00:00
Cargo.lock chore(deps): update rust crate rustls to v0.23.44 (#37) 2026-09-08 13:45:11 +00:00
Cargo.toml refactor: use hickory-resolver directly (#27) 2026-08-31 22:40:27 +00:00
Dockerfile chore(deps): pin git.zarantonello.dev/infra/ci-rust docker tag to 58fbc89 (#11) 2026-08-26 09:56:51 +00:00
LICENSE docs: add some files (#38) 2026-09-08 15:51:35 +00:00
README.md docs: add some files (#38) 2026-09-08 15:51:35 +00:00

kube-dns-rs

A Kubernetes-aware DNS server written in Rust. It exposes DNS records derived from Kubernetes primitives (Services, EndpointSlices, and a custom DnsRecord CRD), blocks domains using remote block/allow lists, and recursively resolves everything else over DNS-over-TLS upstreams.

The server listens on TCP and UDP (port 1053 by default) and is designed to run as a container inside the cluster it serves.

Features

  • Kubernetes-native records — watches Services and EndpointSlices in every namespace and serves A/AAAA records under your cluster domain.
  • Custom DnsRecord CRD — declare arbitrary A/AAAA records for any fully qualified domain name (e.g. example.com) from inside Kubernetes.
  • Recursive resolver — anything not handled by Kubernetes records or block lists is resolved upstream over TLS (Quad9 and Cloudflare by default).
  • Blocking — wildcard block lists (with optional allow lists) are downloaded on startup and refreshed periodically; blocked names return NXDOMAIN.
  • Cluster domain SOA — serves a valid SOA record for the configured cluster domain.

How it works

The server registers three zone handlers on the root zone (plus one on the cluster domain):

Handler Zone Responsibility
KubernetesCrdZoneHandler . (root) Serves DnsRecord CRD entries (exact FQDN matches, A/AAAA, TTL 30s)
BlockerZoneHandler . (root) Consults block/allow lists and returns NXDOMAIN for blocked names
ResolverZoneHandler . (root) Forwards all remaining queries upstream over DNS-over-TLS
KubernetesSvcZoneHandler <cluster_domain> Serves Service/EndpointSlice records

Lookup order is determined by the catalog registered in src/bin/kube-dns-rs.rs: CRD records take precedence, then blocking, then recursive resolution. Queries for names under the cluster domain are answered by the SVC handler.

Service / EndpointSlice records

Services and EndpointSlices are watched cluster-wide and produce records in the cluster domain with the scheme:

<service-name>.<namespace>.svc.<cluster-domain>
<endpoint-name>.<service-name>.<namespace>.svc.<cluster-domain>
  • A service with clusterIP resolves to that address.
  • Headless services (no clusterIP) resolve to the addresses of their ready, non-terminating EndpointSlice endpoints.
  • Endpoint-slice targets that reference Kubernetes deployments are resolved by their <endpoint>.<service>.<namespace>.svc name; external targets provide addresses directly.
  • Port information from EndpointSlices is also tracked (SRV support is planned — the pattern is sketched in src/kubernetes/svc/handler.rs).

The DnsRecord CRD

apiVersion: zarantonello.dev/v1
kind: DnsRecord
metadata:
  name: example-com
  namespace: your-namespace
spec:
  fqdn: example.com
  data:
    - A:
        addresses:
          - 192.0.2.1
    - AAAA:
        addresses:
          - "2001:db8::1"

The CRD is type-derived from the Rust struct DnsRecordCrd and can be regenerated at any time:

cargo run --bin crdgen > dnsrecord.yaml

Domain blocking

Block and allow lists are plain text files, one domain per line, # for comments, *. prefixes for wildcard entries. Lists are downloaded synchronously at startup (before the listeners bind, so protection is in place immediately) and refreshed periodically with automatic retry on failure. Allow lists always win over block lists on ties (see BlockerContext::lookup for the exact priority rules).

Getting started

Build

cargo build --release

Binaries:

  • kube-dns-rs — the DNS server
  • crdgen — generates the DnsRecord CRD definition YAML

Configure

kube-dns-rs loads its configuration from /etc/kube-dns-rs/config.yaml by default (override with --config <path>):

blocker:
  allowlist_urls:
    - https://badblock.celenity.dev/wildcards-star/push_whitelist.txt
  blocklist_urls:
    - https://gitlab.com/hagezi/mirror/-/raw/main/dns-blocklists/wildcard/dyndns.txt
    # ...
kubernetes:
  cluster_domain: k8s.zarantonello.dev
listeners:
  tcp: "[::]:1053"
  udp: "[::]:1053"

Run locally

# Against a local/remote cluster (kube credentials via the default kube client lookup):
kube-dns-rs --config examples/config.yaml

# Point a resolver at it:
dig @127.0.0.1 -p 1053 my-service.my-namespace.svc.k8s.zarantonello.dev
dig @127.0.0.1 -p 1053 example.com

Deploy to Kubernetes

Reference manifests live in examples/kubernetes/:

  • namespace.yaml, serviceaccount.yaml, clusterrole.yaml, clusterrolebinding.yaml — RBAC: the server needs watch on services, discovery.k8s.io/endpointslices, and zarantonello.dev/dnsrecords.
  • config.yaml/configmap.yaml — the runtime configuration.
  • deployment.yaml — container image, TCP/UDP port 1053, read-only root FS, non-root user, and a startup probe.
  • service.yaml — optional cluster Service exposing the DNS port and giving the server a stable clusterIP (which you can point your k8s DNS at).

Apply the CRD first:

kubectl apply -f examples/crds/dnsrecord.yaml

Project layout

src/
├── bin/
│   ├── kube-dns-rs.rs       # main server binary: wiring, catalog, listeners, lifecycle
│   └── crdgen.rs            # generates the DnsRecord CRD YAML
├── args.rs                  # CLI argument parsing (--config)
├── settings.rs              # YAML config schema
├── init.rs                  # logger, settings load, socket binding, kube client
├── trie.rs                  # prefix trie used for domain matching
├── blocker/                 # block/allow list engine
│   ├── context.rs           # trie-backed store + priority lookup
│   ├── handler.rs           # zone handler returning NXDOMAIN for blocked names
│   └── refresher.rs         # initial download + periodic refresh tasks
├── kubernetes/
│   ├── crd/                 # DnsRecord CRD: context, zone handler, watcher
│   └── svc/                 # Service/EndpointSlice: context, zone handler, watcher
├── resolver/mod.rs          # recursive resolver zone handler (DoT upstreams)
└── utils/mod.rs             # shared record-set/aggregation helpers

Development

The repository is formatted with the project's Rust formatter and linted via the CI checks in .forgejo/workflows/. Commits must follow the Conventional Commits style (feat:, fix:, chore(deps):, ...) as enforced by the commitizen check; releases are cut with semantic-release and the forgejo-release plugin. When in doubt, check the CI workflows before pushing.

License

MIT