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.