[server] bind_addr = "0.0.0.0:53" # or a list: ["10.0.0.1:53", "127.0.0.1:53"] api_port = 5380 # api_bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN dashboard access # api_token = "…" # loopback is exempt, every other client needs this. # Minted on first start (stored in data_dir) # unless set here or via NUMA_API_TOKEN. # `numa token` prints the one in effect. # data_dir = "/var/lib/numa" # where numa stores TLS CA and cert material # Defaults: /var/lib/numa on linux (FHS), # /usr/local/var/numa on macos (homebrew prefix), # %PROGRAMDATA%\numa on windows. Override for # containerized deploys or tests that can't # write to the system path. # filter_aaaa = true # on IPv4-only networks, answer AAAA queries with # NODATA (NOERROR + empty answer) so Happy Eyeballs # clients don't wait on a v6 attempt that can't # succeed. Also strips `ipv6hint` from HTTPS/SVCB # records (RFC 9460) so modern browsers (Chrome # ≥103, Firefox, Safari) don't bypass the AAAA # filter via SVCB hints. Local zones, overrides, # and the .numa proxy are NOT filtered — you can # still configure v6 records for local services. # Default: false. # ── Client-IP allowlist ──────────────────────────────────────────── # Required on WAN-facing binds, or numa becomes an open recursive # resolver. Loopback always allowed. Denied peers: silent UDP drop, # TLS close pre-handshake, DoH 403. # # allow_from = ["10.0.0.0/8", "192.168.0.0/16", "fd00::/8"] # ── Resolution ceiling ───────────────────────────────────────────── # Cache misses running at once, across UDP, TCP, DoT and DoH together. # One charge per query; cached, local and coalesced answers are free, so # this bounds only what a flood of novel names can conjure. Over the # ceiling UDP is dropped silently and stream transports get SERVFAIL. # Watch `queries.refused` and `resolutions` in /stats before changing it. # # max_concurrent_resolutions = 512 # default # ── Upstream ─────────────────────────────────────────────────────── # Where numa sends the queries it can't answer itself. Uncomment one # [upstream] block: forward (the default), recursive, odoh or auto. # Forward: relay to upstream servers. Without `address`, numa follows # your system DNS as networks change, or uses Quad9 over DoH (then # Cloudflare if Quad9 fails) if it finds none. # [upstream] # mode = "forward" # address = "9.9.9.9" # single upstream (plain UDP) # address = ["192.168.1.1", "9.9.9.9:5353"] # multiple upstreams, asked in order; a failed one is asked last # address = "https://dns.quad9.net/dns-query" # DNS-over-HTTPS (encrypted) # address = "tls://9.9.9.9#dns.quad9.net" # DNS-over-TLS (encrypted, port 853) # fallback = ["8.8.8.8", "1.1.1.1"] # tried only when all primaries fail # port = 53 # default port for addresses without :port # timeout_ms = 5000 # default # hedge_ms = 0 # request hedging delay (ms). Default: 0 (off). # # Set to e.g. 10 to fire a parallel upstream # # request after 10ms of silence — rescues packet # # loss (UDP), dispatch spikes (DoH), TLS stalls # # (DoT). Doubles the upstream query count, so # # leave off for quota'd providers (NextDNS, # # Control D). # Recursive: resolve from the root servers, with no upstream provider. # `address`, `port`, `timeout_ms` and `hedge_ms` only affect forwarding, # and `fallback` is not tried when recursion fails. For DNSSEC # validation, also set [dnssec] enabled = true. # [upstream] # mode = "recursive" # root_hints = [ # default: the 13 root servers # "198.41.0.4", # a.root-servers.net (Verisign) # "199.9.14.201", # b.root-servers.net (USC-ISI) # "192.33.4.12", # c.root-servers.net (Cogent) # "199.7.91.13", # d.root-servers.net (UMD) # "192.203.230.10", # e.root-servers.net (NASA) # "192.5.5.241", # f.root-servers.net (ISC) # "192.112.36.4", # g.root-servers.net (US DoD) # "198.97.190.53", # h.root-servers.net (US Army) # "192.36.148.17", # i.root-servers.net (Netnod) # "192.58.128.30", # j.root-servers.net (Verisign) # "193.0.14.129", # k.root-servers.net (RIPE NCC) # "199.7.83.42", # l.root-servers.net (ICANN) # "202.12.27.33", # m.root-servers.net (WIDE) # ] # prime_tlds = [ # TLDs to pre-warm on startup (default list) # "com", "net", "org", "info", # gTLDs # "io", "dev", "app", "xyz", "me", # "eu", "uk", "de", "fr", "nl", # EU + European ccTLDs # "it", "es", "pl", "se", "no", # "dk", "fi", "at", "be", "ie", # "pt", "cz", "ro", "gr", "hu", # "bg", "hr", "sk", "si", "lt", # "lv", "ee", "ch", "is", # "co", "br", "au", "ca", "jp", # other major ccTLDs # ] # ODoH (Oblivious DNS-over-HTTPS, RFC 9230). The relay sees your IP but # not the question; the target sees the question but not your IP. Numa # refuses same-operator relay+target configs by default (eTLD+1 check). # [upstream] # mode = "odoh" # relay = "https://odoh-relay.numa.rs/relay" # target = "https://odoh.cloudflare-dns.com/dns-query" # strict = true # default: refuse to downgrade to `fallback` # # on relay failure. Set false to allow a # # non-oblivious fallback path. # relay_ip = "178.104.229.30" # optional: pin IPs so numa doesn't leak the # target_ip = "104.16.249.249" # relay/target hostnames via the bootstrap # # resolver on cold boot when numa is its # # own system DNS. See # # recipes/odoh-upstream.md. # Auto: probe the root servers at startup. Resolve recursively if they # answer, otherwise forward to Quad9 over DoH (then Cloudflare if Quad9 # fails) until the next restart. # [upstream] # mode = "auto" # numa also builds rules from the system DNS config: search domains on Linux, # split-DNS resolvers on macOS. It skips `local`, which is mDNS (RFC 6762). # A [[forwarding]] rule with suffix = "local" forwards it anyway. # # [[forwarding]] # per-suffix conditional forwarding rules # suffix = "168.192.in-addr.arpa" # single suffix → one upstream # upstream = "100.90.1.63:5361" # # [[forwarding]] # suffix = ["home.local", "home.arpa"] # multiple suffixes → same upstream # upstream = "10.0.0.1" # port 53 default # # [[forwarding]] # DoT upstream: tls://IP[:port]#hostname # suffix = ["google.com", "goog"] # hostname is the TLS SNI / cert name # upstream = "tls://9.9.9.9#dns.quad9.net" # port 853 default # # [[forwarding]] # DoH upstream: full https:// URL # suffix = "example.corp" # upstream = "https://dns.quad9.net/dns-query" # # [[forwarding]] # array of upstreams → asked in order, # suffix = ["google.com", "goog"] # a failed one is asked last # upstream = [ # "tls://9.9.9.9#dns.quad9.net", # "tls://149.112.112.112#dns.quad9.net", # ] # [blocking] # enabled = true # set to false to disable ad blocking # refresh_hours = 24 # a refresh that leaves nothing loaded retries in # # an hour instead of waiting the full cycle. # The last good copy of each remote list is kept under /blocklists so # a restart while the upstream is down still blocks. It never expires: age is # reported in the dashboard, but an old list always beats no list. # Every entry blocks the domain and all its subdomains, whatever the list # format — hosts lines, bare domains, adblock syntax, remote or local. # lists = [ # "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/pro-onlydomains.txt", # "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/nsfw-onlydomains.txt", # adult-content (opt-in) # "file:///etc/numa/local-blocklist.txt", # local file: hosts lines or bare domains # "/etc/numa/extra-blocks.txt", # bare absolute path also works # ] # Where raw.githubusercontent.com is unreachable, the jsDelivr mirror of the # same file works: https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro-onlydomains.txt # allowlist = [ # never blocked by the lists above, subdomains included # "example.com", # "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/whitelist-referral-onlydomains.txt", # "/etc/numa/allow.txt", # URLs and paths refresh like the lists above # ] # ── Per-client domain policies ───────────────────────────────────── # Block or allow specific domains for specific clients, on top of the # global blocklist above. `block`/`allow` use the same adblock syntax # as global lists. Loopback always bypasses. Rules layer in order: the # first rule matching the client that has an explicit block/allow for # the name wins; within a rule, allow beats block. # # [[client_policy]] # from = ["192.168.1.0/24"] # clients this rule applies to (CIDRs/IPs) # exclude = ["192.168.1.254"] # carve hosts out of the range (e.g. your own device) # block = ["youtube.com", "*.tiktok.com"] # allow = ["safe.example.com"] # exempt a name the global list would block # # [[client_policy]] # from = ["10.0.0.50"] # a single host: stricter rules just for it # block = ["reddit.com"] # filter_aaaa = true # override global [server].filter_aaaa for these clients (alone makes a rule valid) [cache] max_entries = 100000 min_ttl = 60 max_ttl = 86400 # warm = ["google.com", "github.com"] # resolve at startup, refresh before TTL expiry [proxy] enabled = true port = 80 tls_port = 443 tld = "numa" # authoritative = true # false when tld is a real domain: other names under it resolve as usual # bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN access to .numa services # ── Bring-your-own TLS cert ──────────────────────────────────────── # Skip per-device CA install. `tld` must be a domain you own. Renew # externally; restart to pick up. # # cert_path = "/etc/letsencrypt/live/example.com/fullchain.pem" # key_path = "/etc/letsencrypt/live/example.com/privkey.pem" # ── PROXY protocol v2 ────────────────────────────────────────────── # Preserve the real client IP behind an L4 front-end (dnsdist, # HAProxy, AWS NLB). Non-empty `from` is required-mode: connections # outside the allowlist, or missing the header, are dropped. # # [proxy.proxy_protocol] # from = ["10.0.0.0/8", "127.0.0.1"] # trusted L4 front-ends # header_timeout_ms = 5000 # kill stalled-header connections # Pre-configured services (numa.numa is always added automatically) # [[services]] # name = "frontend" # target_port = 5173 # # [[services]] # name = "api" # target_port = 8000 # target_host = "192.168.1.50" # optional, defaults to localhost # Many records: use an inline array, see recipes/local-dns-records.md. # Example zone records: # [[zones]] # domain = "dimescu.ro" # record_type = "A" # value = "3.120.139.105" # ttl = 30 # [[zones]] # domain = "test.local" # record_type = "A" # value = "127.0.0.1" # ttl = 60 # Reverse-DNS (PTR) for LAN hosts — makes tcpdump/access logs readable # [[zones]] # domain = "1.0.168.192.in-addr.arpa" # 192.168.0.1 reversed + .in-addr.arpa # record_type = "PTR" # value = "router.lan" # ttl = 300 # Wildcard zones — one rule covers an entire subtree. Leftmost label only # (RFC 4592). Exact entries always win; the wildcard does NOT match its parent # (e.g. `*.pool.ntp.org` matches `time2.pool.ntp.org` but not `pool.ntp.org`). # [[zones]] # domain = "*.pool.ntp.org" # record_type = "CNAME" # value = "time.onsite" # ttl = 60 # DNSSEC signature validation. Needs recursive resolution: forward and # odoh modes ignore it. # [dnssec] # enabled = false # opt-in: verify chain of trust from root KSK # strict = false # true = SERVFAIL on bogus signatures # DNS-over-TLS listener (RFC 7858) — encrypted DNS on port 853 # [dot] # enabled = true # on by default; set false to disable # port = 853 # standard DoT port # bind_addr = "0.0.0.0" # IPv4 or IPv6; unspecified binds all interfaces # cert_path = "/etc/numa/dot.crt" # PEM cert; omit to use self-signed (proxy CA if available) # key_path = "/etc/numa/dot.key" # PEM private key; must be set together with cert_path # ── PROXY v2 on DoT ──────────────────────────────────────────────── # Same semantics as [proxy.proxy_protocol] above. # # [dot.proxy_protocol] # from = ["10.0.0.0/8"] # header_timeout_ms = 5000 # LAN service discovery via mDNS (disabled by default — no network traffic unless enabled) # [lan] # enabled = true # discover other Numa instances via mDNS (_numa._tcp.local) # broadcast_interval_secs = 30 # peer_timeout_secs = 90 # ── Mobile API ───────────────────────────────────────────────────── # Read-only HTTP for the companion apps (discovery, /ca.pem, # /mobileconfig, QR onboarding via `numa setup-phone`). Binds to the # LAN by default. Don't enable on untrusted LANs (office Wi-Fi, # cafés) — an attacker on the same network can shadow you via mDNS # and serve their profile instead. [mobile] enabled = true # opt-in to the mobile API listener # port = 8765 # default; matches Discovery.swift defaultAPIPort # bind_addr = "0.0.0.0" # default; set to "127.0.0.1" for localhost-only