Skip to content

DNS Architecture

DNS in this estate is split into five deliberately separate planes: an external authoritative service, an internal authoritative + resolver plane, a split-horizon view resolver, a public recursive resolver run as a community service, and a fixed upstream policy. Each plane fails independently of the others.

flowchart TD
    subgraph external["External / authoritative"]
        bind["BIND 9.20\nnameserver ns — LB .53\npublic 31.3.128.59"]
        dnsdist["dnsdist on BPI-R4\nnative IPv6 frontend\nPROXYv2 → BIND"]
        hap["haproxy on BPI-R4\npublic v4 TCP :53\nsend-proxy-v2 → BIND"]
    end
    subgraph internal["Internal"]
        tech["Technitium — LB .54\ninternal zones + DHCP"]
        unb["unbound split-horizon\nLB 192.168.1.1\ninternal views"]
    end
    opennic["OpenNIC tier-2\nrecursive — LB .44"]
    rtr["bpi-r4 dnsmasq\n.254"]
    nextdns["NextDNS\n(+ Quad9 last resort)"]

    v6c["IPv6 clients"] --> dnsdist --> bind
    v4t["IPv4 TCP clients"] --> hap --> bind
    v4u["IPv4 UDP clients"] -->|DNAT| bind
    lan["LAN clients"] --> rtr
    lan --> unb --> tech
    rtr -->|"own zones"| unb
    rtr --> tech
    tech -.->|"DoT upstream"| nextdns
    rtr -.->|"fallback"| nextdns
    world["Public resolver users"] --> opennic

External authoritative — BIND

ns.mdapi.ch (BIND 9.20 in the nameserver namespace, LoadBalancer 192.168.1.53, public 31.3.128.59) is authoritative for the public zones, with DNSSEC and external secondaries (zone transfers restricted to them). Dynamic updates arrive TSIG-signed (cert-manager DNS-01, router ACME).

What makes the front end unusual is client-address preservation through the PROXY protocol on both address families:

  • IPv6 terminates natively at dnsdist on the router — no NAT64 — and reaches BIND over IPv4 carrying a PROXYv2 header per UDP datagram. DNS is the one protocol ecosystem where PROXY-over-UDP exists on both ends (dnsdist sends it, BIND ≥ 9.20 accepts it on a dedicated proxy plain listener).
  • IPv4 TCP is fronted by haproxy on the router with send-proxy-v2 into the same listener; plain UDP is a straight DNAT.

The result: BIND sees every real client address on every transport, so rate-limiting and ACLs operate per-client rather than per-translator.

Internal — Technitium and split-horizon unbound

Technitium (DNS LB .54, DHCP LB .55) is the internal authoritative server and DHCP for the LAN — internal zones, per-client control, and an API that the automation plane drives (zone diffing, health probes, query-log shipping run as Windmill probes). It also serves as unbound's recursive upstream, resolving external names on the LAN's behalf (itself over DoT to NextDNS) as well as the internal zones.

unbound (LB 192.168.1.1 — deliberately the address every DHCP client already trusts) serves the split-horizon view: internal names resolve to internal addresses, everything else is forwarded. Public names of self-hosted services resolve to their LAN addresses here, so internal traffic never hairpins through the edge. Non-override queries forward to Technitium; unbound has no other upstream, and fallback happens one layer up — at the clients' second resolver and in CoreDNS.

LAN clients are handed two resolvers in their DHCP offer — the router at .254 first, then .1 — so neither the cluster nor the router is a single resolver point of failure. The router's dnsmasq sends the split-horizon zones to .1 and everything else to Technitium, then NextDNS, then Quad9, so it stays independent of the split-horizon resolver; if the cluster side stops answering entirely it can also arm a harvested home.tillo.ch hosts-file zone, keeping the estate's own names resolvable through a full cluster outage.

OpenNIC tier-2

A public recursive resolver (LB .44, IPv6 via the NAT64 prefix) run as an OpenNIC community tier-2 — one of the estate's public-service contributions, alongside the NTP pool membership.

Upstream policy

Recursion is internal-first: unbound forwards non-override queries to Technitium, which is the actual recursive resolver (DoT upstream to NextDNS). NextDNS is the external fallback, with Quad9 as a final last-resort rung, and every chain is strictly sequential — one place for filtering policy, no fan-out across public resolvers, and no outage cascades from a single upstream being unreachable. In-cluster CoreDNS is internal-first in the same way: .1, then .254, then the NextDNS pair, then Quad9.