dns

package
v0.2.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: MPL-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package dns implements the chassis's authoritative-DNS head: a personality that answers DNS queries for zones explicitly delegated to this chassis, straight from an in-memory snapshot of the dns_zones/dns_records tables.

Phase 1 scope (internal docs/todo-dns-authority.md): materialized records only — no record synthesis, no DNS-01, no DNSSEC. The server is authoritative-only and NEVER recursive: a query whose name falls under no served zone is REFUSED.

Data-plane discipline: a query runs NO opstack and never touches the bus. It is answered from a prebuilt ZoneSnapshot that is rebuilt on config-apply (dbcache OnReload), so the hot path does zero DB reads — the same "no syscalls on the hot path" rule the static-asset index and redaction registry follow.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterChallengeStore

func RegisterChallengeStore(scheme string, f func(dsn string) (ChallengeStore, error))

RegisterChallengeStore registers a factory for a DSN scheme (e.g. "postgres"). Called from an overlay init(); core registers nothing.

Types

type ChallengeStore

type ChallengeStore interface {
	// Present publishes a challenge value at the given owner FQDN. Idempotent
	// per (fqdn, value); multiple distinct values may coexist (ACME can
	// place two during a single order). The value self-expires as a safety
	// net for an abandoned solve; the normal lifecycle is an explicit CleanUp.
	Present(fqdn, value string)

	// CleanUp removes a specific (fqdn, value). Idempotent — removing an
	// absent value, or one already expired, is a no-op.
	CleanUp(fqdn, value string)

	// ActiveTXT returns the live (unexpired) challenge values for an exact
	// owner FQDN, or nil. fqdn is matched case-insensitively as a
	// trailing-dot FQDN.
	ActiveTXT(fqdn string) []string
}

ChallengeStore holds the short-lived `_acme-challenge` TXT records the DNS head serves while a cert is being issued via ACME DNS-01. It is the shared substrate for Phase 3: the in-process ACME solver (bundled in chassis/tls) AND the RFC2136 UPDATE receiver (the Caddy deploy) both write through it, and the query path reads it.

These records are deliberately OUTSIDE the ZoneSnapshot / dbcache reload cycle: they live for seconds-to-minutes and churn during issuance, so routing them through config-apply would be both too slow and semantically wrong (they are not durable config). The store is a small, lock-guarded, self-expiring map consulted on the query hot path — but only for the `_acme-challenge.*` name, so normal lookups never touch it.

Seam shape mirrors chassis/auth/registry/dialect.go: core ships the interface + an in-memory default; a shared (Postgres) implementation is registered out of tree by the overlay so a challenge written on one chassis is served by another when Let's Encrypt's validator lands there (see ChallengeStoreForDSN).

func ChallengeStoreForDSN

func ChallengeStoreForDSN(dsn string) (ChallengeStore, error)

ChallengeStoreForDSN selects the challenge backend. An empty DSN (the default) or any scheme without a registered factory yields the in-memory store — the safe, single-node default. A recognised scheme builds the registered backend.

type DNSController

type DNSController struct {
	// contains filtered or unexported fields
}

DNSController owns the authoritative-DNS listeners and the prebuilt zone snapshot they answer from.

One controller hosts a UDP and a TCP listener per configured address. The snapshot is rebuilt on every dbcache reload (config-apply, fs-watch) and swapped atomically, so the query hot path does zero DB work and never blocks a reload.

DNS is OFF by default. Both gates must be flipped:

  • `dns` must appear in `--personalities`
  • `--dns-listen-addrs` must be non-empty

func NewController

func NewController(ctx context.Context, pu *processor.Unit) *DNSController

NewController constructs (but does not start) a DNS controller. Mirrors the other personalities' constructor shape so server.go can treat them uniformly.

func (*DNSController) ChallengeStore

func (c *DNSController) ChallengeStore() ChallengeStore

ChallengeStore exposes the controller's transient ACME-challenge store so the in-process DNS-01 solver (chassis/tls) writes to the same instance this head serves from. Nil only before NewController runs.

func (*DNSController) Origins

func (c *DNSController) Origins() []string

Origins returns the canonical origins currently served, from the live snapshot (lock-free atomic read; safe to call from an OnReload hook). The bundled cert manager uses this to recompute the wildcard cert set when delegated zones change.

func (*DNSController) Start

func (c *DNSController) Start()

Start binds UDP+TCP listeners on each configured address and serves authoritative DNS from the zone snapshot. The double-gate (personality string AND non-empty listen addrs) means an upgrade can't silently acquire a privileged listener.

func (*DNSController) Stop

func (c *DNSController) Stop()

Stop drains in-flight queries and closes the listeners with a 5s ceiling so a wedged TCP session can't stall chassis shutdown.

type SynthConfig

type SynthConfig struct {
	Nameservers []string // FQDNs advertised as the apex NS set
	EdgeIPs     []string // A/AAAA targets for apex + per-stack hosts
	MXHost      string   // MX target hostname (the LMTP head's public name)
	MXPriority  uint16
	TTL         uint32 // TTL for synthesized records (falls back to the zone default if 0)
	// Mail-auth TXT at the apex, emitted alongside the MX (i.e. only when
	// MXHost is set). SPFOverride replaces the auto-derived SPF; DMARC is the
	// full policy string. Both overridable per-zone by a materialized
	// dns_records TXT (first-match-clears).
	SPFOverride string // "" → auto-derive from EdgeIPs + mx
	DMARC       string // e.g. "v=DMARC1; p=none"
	// StructuredSuffix is the platform's default structured-host suffix
	// (TXCO_STRUCTURED_HOST_SUFFIX), bare (no leading dot), e.g.
	// "stacks.thanks.computer". When a served zone's origin equals it, the
	// zone is the default-suffix zone and gets a WILDCARD RRset (so every
	// <stack>-<rand>.<suffix> resolves) instead of per-stack records.
	StructuredSuffix string
}

SynthConfig parameterizes the synthesized record pattern for delegated (pattern-mode) zones. Empty fields disable the corresponding records — synthesis is purely additive and degrades to "SOA only" when nothing is configured.

func EffectiveSynthConfig

func EffectiveSynthConfig(db *sql.DB, flagDefaults SynthConfig) SynthConfig

EffectiveSynthConfig is the synthesis config the chassis actually uses: the operator-set dns_settings row when present, otherwise the boot `--dns-*` flag defaults (flagDefaults). Read at snapshot build and by the admin config/zone-create endpoints — never on the query hot path. A read failure or a missing row falls back to the flags, so existing flag-only deployments are unchanged.

func SynthConfigFrom

func SynthConfigFrom(conf config.Config) SynthConfig

SynthConfigFrom builds a SynthConfig from chassis config. Single source so the dns controller and the admin render endpoint synthesize identically.

type ZoneSnapshot

type ZoneSnapshot struct {
	// contains filtered or unexported fields
}

ZoneSnapshot is an immutable, prebuilt view of every served zone. Build it with BuildSnapshot; serve from it with Lookup; preview it with Render. Swap a whole *ZoneSnapshot atomically on reload — never mutate one in place.

func BuildSnapshot

func BuildSnapshot(db *sql.DB, cfg SynthConfig, logger *zap.Logger) (*ZoneSnapshot, error)

BuildSnapshot reads all active zones + records from the runtime mirror and assembles a ZoneSnapshot. A malformed individual record is logged and skipped (best-effort, like the LMTP MIME parse) rather than darkening the whole zone; only a DB-level failure returns an error. Pass dbc.Snapshot() — never a captured dbc.Db handle.

func (*ZoneSnapshot) Lookup

func (s *ZoneSnapshot) Lookup(q dns.Question) (answer, ns []dns.RR, rcode int)

Lookup resolves a single question against the snapshot.

rcode REFUSED               → no served zone covers the name (we are
                              authoritative-only, never recursive)
rcode NOERROR + answer      → matching RRset
rcode NOERROR + ns(SOA)     → NODATA: name exists, type absent
rcode NXDOMAIN + ns(SOA)    → name does not exist in the zone

ANY is refused (no ANY expansion — anti-amplification). CNAME chasing and wildcards are out of scope for Phase 1.

func (*ZoneSnapshot) Origins

func (s *ZoneSnapshot) Origins() []string

Origins returns every canonical origin currently served, sorted. Used by the bundled cert manager to decide which `*.<origin>` + apex wildcard certificates to obtain/renew.

func (*ZoneSnapshot) OriginsForTenant

func (s *ZoneSnapshot) OriginsForTenant(tenantID string) []string

OriginsForTenant returns the canonical origins served for a tenant, sorted. Used by the admin render endpoint.

func (*ZoneSnapshot) Render

func (s *ZoneSnapshot) Render(origin string) (string, bool)

Render emits the zone TxCo would serve for origin in standard zone-file (presentation) form, or ok=false if the origin isn't served. The header comment carries the UTC generation stamp (the serial formatted as an RFC3339 instant) so an operator reads one unambiguous value; the SOA serial is the same number on the wire.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL