Skip to content

Reconcile UniFi gateway domain-firewall ipsets so DoH blocks remain enforceable #1065

Description

@jimstrang

Summary

NetworkOptimizer correctly credits a firewall policy that blocks TCP/443 to the known public DoH providers. On UniFi gateways, however, a domain-based firewall policy is only enforceable while the gateway's dnsmasq has populated its runtime destination ipsets with current A/AAAA answers.

The audit currently validates policy intent, but not whether the associated runtime ipsets are populated. A structurally correct DoH block can therefore be silently ineffective after a gateway reboot, firewall-policy rebuild, or dnsmasq restart until some client happens to resolve each provider hostname through the gateway's resolver.

This is an enforcement-liveness gap, not a request to change the definition of a healthy DNS posture.

Related: #1062 resolves domain-group policy destinations for the audit. This proposal uses those resolved domains to maintain and verify their runtime enforcement state.

Verified live shape

On UniFi OS 5.1.27, the persistent gateway state stores only hostname-to-ipset mappings:

{
  "hosts": [
    "cloudflare-dns.com",
    "dns.google",
    "dns.quad9.net",
    "doh.opendns.com"
  ],
  "ipsets": [
    "<policy destination set>"
  ]
}

The gateway generates dnsmasq configuration such as:

ipset=/dns.google/<v4 policy set>,<v6 policy set>

The resolved addresses are held only in runtime hash:net child ipsets. They are not persisted in the firewall policy or dnsForwarder.ipsets configuration.

Before resolution, the DoH block rule was compiled and installed but both IPv4 and IPv6 destination sets had zero entries. After querying gateway-local dnsmasq, the same runtime sets populated:

dig @127.0.0.1 cloudflare-dns.com A +short
dig @127.0.0.1 cloudflare-dns.com AAAA +short
dig @127.0.0.1 dns.google A +short
dig @127.0.0.1 dns.google AAAA +short
dig @127.0.0.1 dns.quad9.net A +short
dig @127.0.0.1 dns.quad9.net AAAA +short
dig @127.0.0.1 doh.opendns.com A +short
dig @127.0.0.1 doh.opendns.com AAAA +short

Observed IPv4 entries:

cloudflare-dns.com  104.16.248.249  104.16.249.249
dns.google          8.8.8.8  8.8.4.4
dns.quad9.net       9.9.9.9  149.112.112.112
doh.opendns.com     146.112.41.2

Observed IPv6 entries were also populated for all four providers.

The live child sets have no configured ipset timeout. I have not demonstrated per-entry expiry/removal. However, the DNS answers themselves have short TTLs, including observed values of 26 seconds for cloudflare-dns.com AAAA, 43 seconds for dns.google AAAA, and 46 seconds for doh.opendns.com A. A reconciliation loop is therefore useful both to restore empty sets after lifecycle events and to discover rotated provider addresses.

Why allow rules are different

For a domain-based allow rule, normal client DNS traffic can populate the gateway ipset when clients use its resolver. Sites with a separate LAN resolver can also use conditional forwarding to the gateway for the relevant domains.

For a domain-based block rule, the client is specifically expected not to contact the prohibited service. It may never resolve dns.google, dns.quad9.net, etc. through either path. The destination ipset can remain empty indefinitely, leaving the block rule effectively meaningless without an audit warning.

Proposed behavior

Add an opt-in Domain Firewall Set Reconciliation feature:

  • Discover domain destinations from enabled firewall rules, including native domain-group objects after Security Audit does not resolve UniFi domain-group firewall destinations #1062 support lands.
  • Default the feature to domain-backed block rules only. Do not automatically manage allow-rule destinations in the first version.
  • Resolve each selected hostname's A and AAAA records through gateway-local dnsmasq (127.0.0.1), not through the NetworkOptimizer host resolver or a public DNS service.
  • Run once after gateway connection/reconnection, on a configurable interval, and through a manual Warm now action.
  • Rate-limit lookups and preserve normal gateway ownership of dnsmasq and ipsets. Do not write to ipset directly or hand-edit runtime state.
  • Verify that the associated IPv4 and IPv6 child ipsets become non-empty after resolution.
  • Persist/display per-domain status: last attempted/successful warm-up, A/AAAA result, current IPv4/IPv6 entry counts, and error detail.
  • Emit an actionable warning when a policy is structurally valid but its runtime destination set is empty, for example DNS_DOH_BLOCK_RUNTIME_EMPTY.

Acceptance criteria

  • A configured TCP/443 DoH block covering Cloudflare, Google, Quad9, and OpenDNS is not considered fully healthy when its mapped runtime destination ipsets are empty.
  • After gateway dnsmasq or firewall-policy lifecycle events, configured block-only domain destinations are repopulated without waiting for client DNS traffic.
  • Both A and AAAA lookups are performed and the corresponding IPv4/IPv6 sets are verified.
  • Reconciliation failures are visible in NetworkOptimizer rather than silently leaving the audit green.
  • The implementation does not mutate UniFi firewall policy, directly modify ipsets, or introduce resolver loops.
  • Allow-rule reconciliation remains opt-in or out of scope until stale-address handling is deliberately designed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions