Unbound Upstream via Gateway
Sends Unbound's internet lookups to a resolver inside your VPN tunnels, through the best member of a gateway group, and switches with the group. When no tunnel is up, Unbound falls back to its own forwarders, or chosen clients fail closed.
Why this plugin
A VPN provider usually runs a resolver inside its tunnels, and policy routing sends your clients' traffic through them. Unbound's own queries do not follow: it forwards to one fixed list of servers, which leaves by the firewall's default route, the WAN. Pointing Unbound's forwarders at the provider's resolver by hand breaks whenever the tunnel it is reached through goes down, and nothing switches it to another tunnel or back.
The plugin makes the tunnels' gateway group decide. While the group has a usable member, Unbound's root zone forwards to that member's resolver, reached by a host route through that member's tunnel and kept off the WANs by a firewall rule. When the best member changes, so does the route. When none is usable, the root zone returns to Unbound's own forwarders (Query Forwarding or DNS over TLS), or to recursion, without flushing the cache.
How it works
- At run time, through Unbound's control channel. Switching is a forward change in the running Unbound, never a restart. A recovered member is used again only after the failback delay.
- A route and a rule per resolver. The resolver's host route goes through the active member; a block keeps its address off every other path, and a counting block shows on the page how many packets it stopped.
- A resolver per member, or one for all. Each member can have its own resolver, or use the default one. IPv4 or IPv6; a group holds one family.
- A contract. Tunnel mode is refused, with each reason on the page, when the setup cannot work as designed: an unmonitored member, a group of mixed families, Unbound set not to use that family, Outgoing Network Interfaces set, or Unbound's own root zone already forwarding to a member's resolver.
- No surprises. Dry run (plan and log only) is the default. Nothing writes the configuration at run time; the only writer is Save.
Failing closed
By default the fallback keeps everyone resolving. Since 1.5 you can choose, instead, that some clients get REFUSED for internet names while no tunnel is up, so their lookups never leave by the WAN:
- Fail closed on: interfaces, such as a VLAN whose traffic must only ever use the tunnels.
- Fail closed for networks: source networks routed to the firewall, such as a Tailscale tailnet (
100.64.0.0/10andfd7a:115c:a1e0::/48). - Fall back to Unbound's own forwarders off: every client fails closed.
The firewall's own lookups from 127.0.0.1 are never refused. Local names keep resolving for a fail-closed client: host overrides and their reverse records, the system domain, DHCP lease names, domain overrides and the private reverse zones.
The set lives in an Unbound view, in the plugin's own file in core's unbound.opnsense.d include directory. Unbound loads it at every start already refused, so the set is refused from the first query after a boot, a restart or an upgrade, and lifted once a member is in use. Falling back arms the refuse again before the root returns to the forwarders. Apply restarts Unbound once when the set's networks change.
The page's Fail closed row shows the set and its state: lifted, armed, not armed (with the reason and the remedy), or stuck.
Settings
On Services > Unbound DNS > Upstream via Gateway, with its log under Upstream via Gateway Log.
| Setting | What it does | Default |
|---|---|---|
| Enable | Forward the root zone through the group's best member, and fall back when none is usable. Off returns Unbound to its own forwarders. | Off |
| Dry run | Decide and log only: Unbound's forwarders and the routes are left alone; the block and pass rules are still loaded. | On |
| Gateway group | The saved group, all of one family, whose members are the tunnels to the resolver. Every member must be monitored. | |
| Default resolver | The resolver's address inside the tunnels, for every member without its own row, for example 198.51.100.53. | |
| Failback delay (seconds) | How long a better member must stay usable before Unbound returns to it, 60 to 3600; at least the longest monitor period of the members. | 120 |
| Probe name | The name used to check that the resolver answers; . asks for the root. | . |
| Fall back to Unbound's own forwarders | Off: every client gets REFUSED while no member is usable. The firewall's own lookups always fall back. | On |
| Fail closed on | Interfaces whose clients get REFUSED for internet names while no member is usable. | none |
| Fail closed for networks | Source networks routed to the firewall that fail closed the same way. | none |
| Notify on switch, Notify URL | POST each switch as JSON, for example to a Home Assistant webhook. | Off |
| Per-member resolvers | Rows below the settings: a member and its own resolver. | none |
Save refuses for the fail-closed fields: a gateway-group member's interface, the loopback interface, an interface with no address, a network with host bits, 0.0.0.0/0 and ::/0, and a network that overlaps loopback or a member's network.
Status, notifications, Home Assistant
The page shows the mode (a member, or the fallback), the resolver, how long the member has been healthy, the last outage, the resolver's routes, the blocked-packet count and the Fail closed row. The privilege Status: Unbound DNS: Upstream via Gateway (API) reads the status and nothing else, for a poller's API key. Notify on switch POSTs each switch, and each change of a fail-closed problem, as JSON.
configctl unboundupstream state # the status as JSON
configctl unboundupstream stop # undo tunnel mode now (the minute job re-enters it while enabled)
Limits
- WireGuard is the only tunnel type tested.
- A client querying from an IPv6 link-local address is not covered by a fail-closed interface. A point-to-point interface contributes only its own address: list its peers under Fail closed for networks.
- A device that falls back to DNS over HTTPS or iCloud Private Relay when refused resolves elsewhere; only firewall rules stop that.
- A tailnet fails closed only if the firewall is the tailnet's only global nameserver ("Override DNS servers" on); Tailscale treats
REFUSEDas a soft error and takes another nameserver's answer. - A name created after the last minute's evaluation (a new host override or lease) is refused for a fail-closed client until the next one.
- An upgrade of the package falls back to Unbound's own forwarders for about a minute while the old version is removed; a fail-closed set stays refused through it.
Recovery
After a failed upgrade, a downgrade to 1.4, or a removal that could not finish, remove the view file and restart Unbound:
rm /usr/local/etc/unbound.opnsense.d/unboundupstream.conf
configctl unbound restart
Source and install
Install it from the LegoTypes repository: see Install. Source: LegoTypes/plugins, dns/unbound-upstream.