IT

Record DNS locali

Work in progress — content may be incomplete.

Un record DNS locale risponde a un nome dalla tua configurazione — prima del filtro, prima dell’upstream. Split-horizon per l’homelab, in un paio di righe di TOML.

Cos’è

Un record DNS locale è una voce statica che dice a Warden di rispondere lui stesso a un nome specifico, con un indirizzo scelto da te, invece di mandare la query a monte. nas.home risolve a 192.168.1.50. sicuro.lab risolve a 10.10.1.15. La query non lascia mai la tua rete, non tocca mai un resolver pubblico, e non attraversa mai le blocklist — un record locale è fidato perché l’hai scritto tu.

I record vivono a due livelli di scope:

  • Globale[[local_dns.records]] in cima alla configurazione. Si applica a ogni client.
  • Per profilo[[profiles.<id>.local_records]] dentro un profilo. Si applica solo ai client che risolvono a quel profilo.

Questa pagina copre quando aggiungere un record, ogni campo che puoi impostarci, la capacità wildcard (match_subdomains), le superfici CLI / TUI che leggono e scrivono i record, come un record batte il filtro e l’upstream, e come il validator rifiuta i footgun — target IP riservati, wildcard su public suffix, cicli CNAME.

Quando ti serve

Aggiungi un record DNS locale quando vuoi che un nome sia risolto dalla LAN, non da internet:

  • Hostname dell’homelab. nas.home, printer.home, pbs.lab — nomi amichevoli per macchine che esistono solo dentro la tua rete.
  • Split-horizon. Un servizio che ha un nome pubblico ma dovrebbe risolvere a un IP interno per i client dietro Warden: git.example.com192.168.1.20 a casa, il suo vero IP pubblico ovunque.
  • Override per profilo. Punta intranet.lan al proxy d’ufficio 192.168.5.50 solo per il profilo employees, mentre guests continua a risolverlo (o a fallire) a monte.
  • Un host, molti nomi. Punta un apex e ogni sottodominio sotto di esso a una singola macchina interna con un solo record wildcard — un reverse proxy che fa da fronte a grafana.apps.home, jellyfin.apps.home, e il resto.

Non ti serve un record locale quando vuoi:

  • Bloccare o consentire un dominio per policy — è un compito da blocklist o da admin rule richiamata da un profilo. Un record locale risponde a un nome; non esprime un deny.
  • Riscrivere un nome interrogato in un nome diverso prima della risoluzione (migrazione di dominio, CNAME finto senza fissare un indirizzo) — è una regola di rewrite per profilo, un meccanismo a parte.
  • Servire MX, TXT, SRV o NS. Solo A, AAAA e CNAME sono sintetizzati localmente; ogni altro tipo prosegue oltre (vedi precedenza).

Schema

Il DNS locale ha una piccola sezione globale più righe di record ripetibili. La sezione porta il TTL di fallback e un interruttore di privacy; ogni riga di record è una voce di un array-of-tables.

La sezione [local_dns]

/etc/purge-warden/config.toml
toml
[local_dns]
ttl_secs = 3600                   # TTL di fallback per i record senza il proprio
nodata_for_missing_types = true   # rispondi NODATA per i tipi mancanti di un nome locale
CampoTipoDefaultScopo
ttl_secsu32 (secondi), 1..=864003600TTL servito per ogni record che non ha un ttl_secs proprio. Copre anche i record PTR auto-generati e la risposta negativa NODATA. Lo zero è rifiutato.
nodata_for_missing_typesbooltrueQuando un nome locale globale è interrogato per un tipo che non possiede, risponde NODATA invece di inoltrare a monte. Vedi sotto.
recordsarray di record[]I record globali, scritti come righe [[local_dns.records]].

Una riga di record

/etc/purge-warden/config.toml
toml
[[local_dns.records]]
domain = "nas.home"
type = "A"
value = "192.168.1.50"
CampoTipoObbligatorioDefaultScopo
domainstringa (FQDN)Il nome a cui rispondere. Messo in minuscolo automaticamente.
typeA | AAAA | CNAMETipo di record. A → IPv4, AAAA → IPv6, CNAME → nome target.
valuestringaIPv4 per A, IPv6 per AAAA, un FQDN target per CNAME.
match_subdomainsboolnofalseQuando true, il record risponde anche a ogni sottodominio di domain (una wildcard). Vedi sotto.
ttl_secsu32 (secondi), 1..=86400noricade su [local_dns].ttl_secsOverride di TTL per record. Lo zero è rifiutato.

La chiave TOML per il tipo di record è type, non record_type. I record per profilo usano esattamente la stessa forma di riga sotto [[profiles.<id>.local_records]].

match_subdomains — wildcard sotto un apex

Di default un record corrisponde solo al suo nome esatto. Imposta match_subdomains = true e risponde anche a ogni sottodominio, a qualsiasi profondità, con lo stesso valore dell’apex:

/etc/purge-warden/config.toml
toml
[[local_dns.records]]
domain = "sicuro.lab"
type = "A"
value = "10.10.1.15"
match_subdomains = true

Ora sicuro.lab, app.sicuro.lab e api.v2.app.sicuro.lab risolvono tutti a 10.10.1.15.

Vince il suffisso più lungo. Un record più specifico batte comunque la wildcard. Dichiara sia una wildcard su sicuro.lab (→ 10.10.1.15) sia un record esatto su app.sicuro.lab (→ 10.10.1.16), e una query per app.sicuro.lab ottiene 10.10.1.16 — la corrispondenza esatta è il suffisso più lungo. Qualsiasi altra cosa sotto sicuro.lab cade comunque sulla wildcard. È la stessa regola di corrispondenza più lunga che le subnet usano per i CIDR.

Le wildcard sono rifiutate sui public suffix
match_subdomains = true su un public suffix — un TLD o eTLD come com, org, co.uk — è rifiutato dal validator: riscriverebbe un’intera fetta del namespace. Un domain vuoto con match_subdomains = true è rifiutato per lo stesso motivo (corrisponderebbe a ogni query). Usa le wildcard su nomi che possiedi tu, come home, lab o sicuro.lab.

ttl_secs — TTL per record

Ogni record eredita [local_dns].ttl_secs (default 3600) a meno che non imposti il suo. L’intervallo valido è 1..=86400 secondi a entrambi i livelli — il fallback di sezione e l’override per record sono soggetti allo stesso limite, e lo zero è rifiutato in entrambi i casi.

/etc/purge-warden/config.toml
toml
[[profiles.employees.local_records]]
domain = "intranet.lan"
type = "A"
value = "192.168.5.50"
ttl_secs = 300            # i client cachano questa risposta per 5 minuti

Se un IP interno sta per cambiare, abbassa il TTL qualche ora prima così i client recepiscono in fretta il nuovo valore quando lo cambi.

nodata_for_missing_types — nessuna fuga verso l’upstream

Default true. Quando un nome è definito nei record globali ma la query chiede un tipo che quel nome non possiede — il caso classico è un nas.lan solo-A e un client IPv6 che chiede AAAA — Warden risponde NODATA (NOERROR con un SOA di authority, nessun indirizzo) invece di inoltrare la query. Due motivi:

  1. Privacy. Gli hostname interni (nas.lan, printer.lan) non raggiungono mai il resolver pubblico.
  2. Correttezza. Per un TLD privato l’upstream risponderebbe NXDOMAIN, e la cache negativa del DNS è per nome, non per tipo — un client che chiede prima AAAA e ottiene NXDOMAIN può sopprimere anche la sua query A successiva, rendendo il tuo record locale intermittentemente irraggiungibile.
/etc/purge-warden/config.toml
toml
[local_dns]
nodata_for_missing_types = false   # opt-out: ripristina il fall-through verso l'upstream

Imposta false solo se fai deliberatamente split-horizon sullo stesso nome (record A locale, AAAA o TXT pubblici). L’interruttore governa solo la tabella globale; i record per profilo mantengono comunque il proprio comportamento di fall-through.

Esempi

Una manciata di nomi dell’homelab, serviti globalmente a ogni client:

/etc/purge-warden/config.toml
toml
[[local_dns.records]]
domain = "nas.home"
type = "A"
value = "192.168.1.50"

[[local_dns.records]]
domain = "printer.home"
type = "A"
value = "192.168.1.51"

[[local_dns.records]]
domain = "media.home"
type = "CNAME"
value = "nas.home"

nas.home per tutti, ma intranet.lan solo per il profilo employeesguests e default non lo vedono mai:

/etc/purge-warden/config.toml
toml
# Globale — ogni client.
[[local_dns.records]]
domain = "nas.home"
type = "A"
value = "192.168.1.50"

# Per profilo — solo i client sul profilo "employees".
[[profiles.employees.local_records]]
domain = "intranet.lan"
type = "A"
value = "192.168.5.50"

Un client su employees che chiede intranet.lan ottiene 192.168.5.50. Lo stesso client che chiede nas.home ottiene 192.168.1.50 (cade sul record globale). Un client su default che chiede intranet.lan va a monte — nessuna corrispondenza locale.

Una wildcard che manda un apex e ogni sottodominio sotto di esso a un unico reverse proxy interno:

/etc/purge-warden/config.toml
toml
[[local_dns.records]]
domain = "apps.home"
type = "A"
value = "192.168.1.30"
match_subdomains = true

Ora apps.home, grafana.apps.home e jellyfin.apps.home risolvono tutti al proxy su 192.168.1.30, che instrada per hostname. Per bloccare un dominio invece di risolverlo, usa una blocklist o una regola di deny del profilo — il compito di un record locale è rispondere con un indirizzo reale, e il validator rifiuta i target black-hole come 0.0.0.0 (vedi note di sicurezza).

Un servizio dual-stack con TTL breve, pronto per un cambio di IP:

/etc/purge-warden/config.toml
toml
[[local_dns.records]]
domain = "git.lab"
type = "A"
value = "192.168.1.20"
ttl_secs = 300

[[local_dns.records]]
domain = "git.lab"
type = "AAAA"
value = "fd00::20"
ttl_secs = 300
Tip
Tieni i record in un loro frammento di include — local-dns.d/home.toml, local-dns.d/lab.toml — richiamato nel config.toml master tramite includes. I verbi della CLI scrivono la tabella globale in config.toml di default; modifica a mano il frammento, o sposta il blocco, se preferisci tenere i record raggruppati per sito.

CLI

Ogni sottocomando di scrittura valida prima contro la configurazione fusa, scrive il TOML, poi attiva un hot reload (swap atomico, niente restart del daemon). I sottocomandi di sola lettura funzionano anche senza il daemon.

ComandoCosa fa
warden local-dns add <domain> <A|AAAA|CNAME> <value> [--profile <id>] [--match-subdomains] [--ttl-secs <n>]Aggiunge un record. Senza --profile atterra nella tabella globale [[local_dns.records]]; con --profile <id> atterra solo nel local_records di quel profilo.
warden local-dns remove <domain> [--profile <id>] [--record-type <A|AAAA|CNAME>]Rimuove i record per un dominio. Senza --record-type, ogni tipo che corrisponde al dominio nello scope scelto è rimosso; con esso, solo quel tipo. Senza --profile, rimuove dalla tabella globale.
warden local-dns list [--profile <id>] [--scope <global|profile|all>] [--record-type <A|AAAA|CNAME>]Elenca i record configurati. Lo scope di default è all (globale più ogni profilo). Se dai sia --profile sia --scope, vince --profile e --scope è ignorato (nessun errore).
warden local-dns show <domain> [--profile <id>]Mostra ogni record che corrisponde a domain — tipo, valore, flag di sottodominio, TTL, scope. Senza --profile, sono cercati sia la tabella globale sia ogni profilo.

Pattern comuni:

# Record globale — risponde per ogni client.
sudo warden local-dns add nas.home A 192.168.1.50

# Record per profilo.
sudo warden local-dns add intranet.lan A 192.168.5.50 --profile employees

# Wildcard sotto un apex, con TTL breve.
sudo warden local-dns add sicuro.lab A 10.10.1.15 \
  --match-subdomains \
  --ttl-secs 300

# Ispeziona cosa è configurato.
warden local-dns list
warden local-dns list --profile employees
warden local-dns show intranet.lan --profile employees

# Rimuovi — un tipo, o tutti i tipi per il nome.
sudo warden local-dns remove intranet.lan --profile employees --record-type A
sudo warden local-dns remove intranet.lan --profile employees
sudo warden local-dns remove nas.home        # tabella globale

Dopo ogni modifica con hot-reload, viene stampato esattamente uno di quattro messaggi — gli stessi usati da ogni percorso di scrittura della CLI:

  • daemon reloaded — change is live
  • daemon not running — change will take effect on next start
  • note: change landed on disk but no admin token is available to request a daemon reload. Run warden token generate or restart the daemon to activate.
  • warning: change landed on disk but the daemon rejected the reload (<msg>). Check journalctl -u purge-warden and consider systemctl restart purge-warden.

local_records.add e local_records.remove sono registrati nell’audit log. Non c’è un flag --json su questi verbi.

TUI

La tab Local DNS è una foglia sotto la sezione Network. È una vista master/detail: la tabella globale [[local_dns.records]] in alto, i record per profilo sotto, con una side-card di storico audit per il record in evidenza.

ElementoCosa mostraTasti utili
Pannello globaleOgni record globale — domain, type, value, flag di sottodominio, TTL, hitsj / k / / scorre
Pannello profiloI record di un profilo alla volta, stesse colonneo cambia focus tra Globale e Profilo; n / N profilo successivo / precedente
Add / Edit / RemoveModal in-tab che scrivono attraverso lo stesso percorso di codice dei verbi CLIa aggiungi, e modifica, d rimuovi sulla riga in evidenza
RefreshRilegge la configurazione da disco (vista offline — le modifiche in staging appaiono prima del reload)r

La lista dei record è letta dalla configurazione su disco (il master più i suoi include), non dal daemon — così una modifica che hai messo in staging ma non ancora ricaricato appare comunque. La colonna hits legge un contatore di hit per record recuperato via IPC su un tick lento; mostra su una TUI appena avviata finché non arriva il primo poll, poi conteggi live.

API REST

Non c’è una CRUD REST per i record DNS locali — niente POST / PATCH / DELETE contro l’entità. I record sono scritti tramite la CLI, un frammento TOML modificato a mano, o la TUI, e il daemon li raccoglie al reload. Rispecchia le altre entità di configurazione v1: la superficie REST è lettura e telemetria, non authoring dichiarativo.

IPC

Il socket Unix espone un verbo di sola lettura senza token che restituisce i contatori di hit per record che la TUI mostra nella sua colonna hits. Le mutazioni non hanno un verbo IPC dedicato — atterrano su disco e il daemon fa refresh tramite il percorso di reload standard (IpcCommand::Reload { token }) che ogni scrittura CLI attiva.

Ordine di precedenza

Una query è risolta dal primo stadio che corrisponde, in quest’ordine:

  1. Record del profilo — i local_records del profilo risolto.
  2. Record globali[[local_dns.records]].
  3. Filtro — regole di allow / deny, poi le blocklist.
  4. Upstream — inoltrata al resolver configurato.

Un record di profilo oscura silenziosamente un record globale per lo stesso nome: se sia profiles.employees.local_records sia la tabella globale definiscono intranet.lan, un client employees ottiene la risposta del profilo.

Profilo risolto?Corrispondenza in profile.local_records?Corrispondenza in local_dns.records globale?Risultato
nonononessuna corrispondenza locale → filtro, poi upstream
nononessuna corrispondenza locale → filtro, poi upstream
qualsiasinorisposta locale globale, TTL globale (o del record)
qualsiasirisposta locale del profilo (oscura l’eventuale globale), TTL del profilo o fallback

Solo A / AAAA / CNAME sono sintetizzati. Una query MX, TXT, SRV o NS per un nome definito localmente non è mai risolta da un record locale — o restituisce NODATA (vedi sotto) o va a monte.

DNS inverso (auto-PTR)

Per ogni record A / AAAA globale, Warden risponde anche automaticamente alla lookup inversa (PTR) corrispondente — nessuna voce separata necessaria. Il PTR eredita il TTL effettivo del record. I record per profilo non generano voci PTR; il DNS inverso è servito solo dalla tabella globale.

NODATA per i tipi mancanti

Con nodata_for_missing_types = true (il default), una query per un nome locale globale che chiede un tipo che non possiede ottiene NODATA invece di essere inoltrata — tenendo il nome interno fuori dal resolver pubblico e schivando la trappola AAAA-first NXDOMAIN. Vedi la nota di schema. Il gate si applica solo alla tabella globale.

Note di sicurezza

I record locali bypassano il filtro e la cache — una risposta che scrivi è servita direttamente, davanti a qualsiasi blocklist. È questo il punto, ed è anche perché sono scritti solo dall’operatore: non c’è alcun percorso per cui una blocklist esterna o un feed remoto possa iniettare un record locale. Il validator rifiuta le forme pericolose prima che una configurazione che ne contiene una possa caricarsi:

  • I target IP riservati sono rifiutati. Un valore A / AAAA in un range unspecified, loopback, multicast, broadcast, o reserved-future-use (0.0.0.0, 127.0.0.0/8, ::1, 224.0.0.0/4, 255.255.255.255, 240.0.0.0/4, e simili) è rifiutato. Non sono mai target di redirect legittimi e sono typo comuni. Per bloccare un nome, usa una blocklist o una regola di deny — non un record locale puntato a un indirizzo black-hole.
  • I target IP pubblici generano un warning. Un target fuori dai range privati — RFC1918, ULA IPv6, e link-local (169.254.0.0/16, fe80::/10) — è ammesso (un servizio self-hosted su un IP pubblico statico è un caso reale) ma emette un warning nella traccia di audit, perché puntare un nome interno a un IP pubblico è ciò che sembrerebbe un redirect man-in-the-middle. Loopback e gli altri range riservati qui sopra sono rifiutati del tutto, non avvisati.
  • Le wildcard sui public suffix sono rifiutate. match_subdomains = true su un TLD o eTLD è rifiutato, così come il dominio vuoto — nessuno dei due dovrebbe mai estendersi in wildcard sull’intero namespace.
  • I cicli CNAME sono rifiutati. Una catena CNAME che cicla su sé stessa (a → b → a, o un self-loop) dentro uno scope è colta al caricamento e rifiutata.
  • I record duplicati sono rifiutati. Lo stesso dominio e tipo dichiarati due volte in uno scope fanno fallire il caricamento.
  • A e CNAME non possono condividere un nome. RFC 1034 vieta a un nome di avere sia un record di indirizzo (A / AAAA) sia un CNAME; dichiararli entrambi per lo stesso nome in uno scope fa fallire il caricamento.
I client DNSSEC-strict vedono una risposta locale come falsificata
Una risposta locale sintetizzata non è firmata. Un client che fa validazione DNSSEC stretta la tratterà come una risposta bogus. I record locali sono pensati per nomi interni dove il client si fida del resolver — non per banche, non per domini pubblici DNSSEC-hardened che non controlli.

Il numero di record non è limitato. Il match per-query è uno scan lineare dei suffissi, quindi come regola pratica tieni i record con subdomain-matching sotto qualche centinaio per profilo per i lookup più veloci — ma è una guida, non un limite: niente warning e niente viene rifiutato in base alla dimensione.

Risoluzione problemi

Un reload fallisce con target '<ip>' is in a reserved/loopback/multicast range. Il record punta a un indirizzo riservato — 0.0.0.0, un loopback 127.x, un indirizzo multicast o broadcast. Correggi il valore con un IP host reale. Se il tuo obiettivo era bloccare il nome invece di risolverlo, usa una blocklist o una regola di deny.

Un reload fallisce con cannot enable match_subdomains on '<domain>' — that is a public suffix. Hai impostato match_subdomains = true su un TLD o eTLD (com, co.uk, …). Le wildcard sono ammesse solo su nomi che possiedi — metti la wildcard una label più in profondità (internal.example.com, non com).

Un reload fallisce con un errore di ciclo CNAME. Due o più record CNAME puntano l’uno all’altro, o uno punta a sé stesso. Spezza il ciclo — un CNAME deve alla fine risolvere a un nome fuori dall’insieme locale.

Un client IPv6 non raggiunge un nome che ha solo un record A. È nodata_for_missing_types che fa il suo lavoro: la query AAAA ottiene NODATA così il client ricade sul record A invece di cachare un NXDOMAIN. Se servi davvero quel nome pubblicamente su IPv6 e vuoi l’AAAA a monte, aggiungi un record AAAA locale, oppure imposta nodata_for_missing_types = false.

Un record per profilo non risponde; risponde quello globale. Controlla a quale profilo il client risolve davvero — warden local-dns list mostra entrambi gli scope, e la query deve atterrare sul profilo che possiede il record. Un client su un profilo diverso vede sempre solo la tabella globale.

Una modifica non ha avuto effetto. Conferma il messaggio di reload dopo la modifica. Se diceva che il daemon non era in esecuzione o che nessun token era disponibile, il record è su disco ma non attivo — avvia o ricarica il daemon.

Per altro, vedi risoluzione problemi.

Vedi anche

  • Profili — i local_records di un profilo oscurano la tabella globale per i client di quel profilo.
  • Liste di blocco — il filtro di policy, davanti al quale stanno i record locali.
  • Subnet — la stessa regola di corrispondenza più lunga che usano le wildcard, applicata ai CIDR.
  • Globali del server — dove vive la sezione [local_dns], accanto agli altri blocchi globali.
  • Riferimento TOML — ogni campo, ogni vincolo.
  • Riferimento CLI — la superficie completa di warden local-dns ….