Guides

DNS forwarding

A recursive DNS forwarder offered to clients behind SiHA.

SiHA can act as a recursive DNS forwarder for the clients behind it — the shape VyOS exposes as set service dns forwarding. It listens on selected addresses, caches answers, routes queries per-domain to specific upstreams, gates by source prefix, and answers static records locally without touching an upstream.

This is a service SiHA offers to clients, not the appliance’s own name resolution (the node resolves via its baked /etc/resolv.conf).

Resource

One DNSForwarders.dns.siha resource per listen scope (typically per VRF).

type: DNSForwarders.dns.siha
metadata:
  namespace: dns
  id: edge
spec:
  # Empty vrf = default VRF. A non-default vrf scopes upstream egress to that
  # VPP FIB table and must reference a declared routing.VRF.
  vrf: ""
  # Each listen address must sit on an lcp-enabled Interface (see Reach below).
  listenAddresses: ["10.42.0.2"]
  # Source prefixes allowed to query — REQUIRED, fail-closed: a query from
  # outside every listed prefix is answered REFUSED. To deliberately serve
  # everyone, set 0.0.0.0/0 (and ::/0) explicitly.
  allowFrom: ["10.42.0.0/24"]
  # Global upstreams, tried in order; SERVFAIL/REFUSED soft-fails to the next.
  # port 0 means 53.
  upstreams:
    - { address: 1.1.1.1 }
    - { address: 9.9.9.9, port: 5353 }
  # Per-domain overrides — longest matching suffix wins over the global set.
  domains:
    - domain: corp.internal
      nameServers:
        - { address: 10.0.2.10 }
  # Locally-authoritative records, answered directly without an upstream.
  # `type` is any RR type mnemonic (A, AAAA, TXT, CNAME, MX, SRV, CAA, NS,
  # PTR, …). Each `values` entry is that RR's RDATA in zone-presentation form.
  # A CNAME must be the only record at its name (RFC 1034); an A/AAAA query for
  # a CNAME'd name is chased so the client gets the alias and the target address.
  staticRecords:
    - { name: gw.corp.internal,              type: A,     values: ["10.42.0.2"] }
    - { name: _acme-challenge.corp.internal, type: TXT,   ttl: 30,   values: ["<token>"] }
    - { name: www.corp.internal,             type: CNAME, ttl: 300,  values: ["gw.corp.internal"] }
    - { name: corp.internal,                 type: MX,    ttl: 3600, values: ["10 gw.corp.internal."] }
    - { name: corp.internal,                 type: CAA,              values: ['0 issue "letsencrypt.org"'] }
  # Response cache. Zeros use defaults (maxEntries 10 000, maxTtl 86 400 s,
  # negativeTtl 30 s). minTtl/maxTtl clamp the cached TTL.
  cache:
    maxEntries: 5000
    negativeTtl: 30
  # The forwarder actively probes each upstream (~15 s) so health is live even
  # with no organic traffic. Set to stop those periodic queries.
  disableUpstreamProbe: false

The resolve pipeline for each query is: allow-from → static override → cache → per-domain/global upstream (first healthy, else SERVFAIL) → cache-store.

DNSForwarderStatus (read-only) reports listening, the bound listenAddresses, cacheEntries, each upstream’s last-observed reachability, and lastError.

Reach (linux-cp)

VPP owns the data NICs; the node kernel has no route to the data subnets. A listen address must therefore be a linux-cp mirror address — declare lcpHostIf on the Interface that carries it (the same pattern BGP uses for its transport). VPP punts inbound :53 traffic to that mirror, where the forwarder’s socket receives it; upstream queries egress sourced from the listen address so they leave via the correct VRF.

config apply cross-checks this: a forwarder whose listen address is not on an lcp interface is rejected (checkDNSForwarderListen).

Multiple forwarders / per-VRF

You can run several forwarders — one per VRF, for example — as long as their listen addresses do not overlap. Two forwarders on the same address would collide at bind and are rejected at apply (checkDNSForwarderOverlap). Each forwarder’s vrf must reference a declared routing.VRF.

Overlapping listen addresses across VRFs (the same IP in two tenants) is a v2 capability gated on per-netns linux-cp support.

Operating

S="sihactl --sihaconfig ~/.siha/config --addr <node-ip>:6443"

# List forwarders with status columns (vrf, listen addresses, upstreams,
# listening, last error). Aliases: dnsforwarders, dnsforwarder, dns.
$S get dnsfwd

# Inspect or edit one.
$S get dnsfwd edge -o yaml
$S edit dnsfwd edge

# From a client behind the appliance:
dig @10.42.0.2 example.com
dig @10.42.0.2 gw.corp.internal                   # static A
dig @10.42.0.2 _acme-challenge.corp.internal TXT  # static TXT (ACME DNS-01)
dig @10.42.0.2 www.corp.internal                  # CNAME, chased to its A

Upstream health is reported under status.upstreams in the YAML output. Unless disableUpstreamProbe is set, the forwarder probes every upstream (global and per-domain) roughly every 15 s and flushes results on the ~30 s reconcile.

Metrics are deferred. There are no query/cache-hit/upstream-latency counters in v1; the status fields above are the only runtime visibility until a dedicated metrics source is added.