PBR works, but DNS still leaks via Pi-hole - How to isolate VPN client DNS traffic?

Hello everyone,


TL;DR:
I'm trying to set up DNS split routing with OpenWrt 23.05.5, Policy-Based Routing (PBR), Pi-hole + Unbound (with DNSSEC), and Mullvad VPN (WireGuard).
LAN clients should use local DNS via Unbound (secured, no DoH/DoT), VPN clients should resolve via Mullvad DNS (10.64.0.1) inside the tunnel.
Despite working PBR, VPN clients still use Pi-hole, causing DNS leaks.
I want a clean solution to route DNS properly per interface/client โ€“ and avoid insecure or redundant DoH/DoT setups.


I am running an OpenWrt 23.05.5 setup with LuCI, Policy Based Routing (PBR) and a Raspberry Pi with Pi-hole and local unbound (on 192.168.1.250). My goal is to separate the data traffic: Clients in the normal LAN should resolve DNS queries via Unbound/Pi-hole, clients running via VPN should route DNS directly via the VPN - e.g. to Mullvad's internal DNS (10.64.0.1). The challenge is to separate this cleanly.

The initial situation:

  • DNS requests in the home network run via the Pi-hole, which locally forwards 127.0.0.1#5335 to Unbound (incl. DNSSEC, no DoH/DoT)

  • Policy-based routing basically works: e.g. 192.168.1.101 or the IoT network 192.168.10.0/24 are reliably routed via Mullvad using WireGuard tunnels

  • However, DNS queries from these VPN clients still end up at the local Pi-hole, which potentially generates DNS leaks - i.e. outside the tunnel

  • I have therefore created firewall rules for FireTV devices, for example, that send DNS queries to Google DNS (8.8.8.8, 8.8.4.4) to the blackhole in order to block hardcoded DNS

What is unclear to me:

  • How can I achieve that only VPN clients use DNS via the tunnel interface (e.g. 10.64.0.1), while all others continue to use the local pi-hole?

  • For example, should I use the Mullvad HTTPS DNS proxy (127.0.0.1#5055) to force DNS in the tunnel, or would that be counterproductive? Does anyone here have any experience as to whether this is more useful than connecting directly to 10.64.0.1?

  • Can dnsmasq selectively define DNS servers depending on the client - or is that messy?

  • And is the order of the traffic rules in LuCI even decisive if I want to define DNS PBR for VPN clients, for example?

Here are a few of my configurations - if you need more, just let me know

/etc/config/network

config interface 'loopback'
        option device 'lo'
        option proto 'static'
        option ipaddr '127.0.0.1'
        option netmask '255.0.0.0'

config route
        option interface 'loopback'
        option type 'blackhole'
        option target '8.8.8.8/32'
        option metric '1234'

config globals 'globals'
        option ula_prefix 'fdb9:c844:d95a::/48'

config device
        option name 'br-lan'
        option type 'bridge'
        list ports 'lan1'
        list ports 'lan2'
        list ports 'lan3'

config device
        option name 'lan1'
        option macaddr 'e8:9c:25:af:a9:fe'

config device
        option name 'lan2'
        option macaddr 'XX:XX:XX:XX:XX:XX'

config device
        option name 'lan3'
        option macaddr 'XX:XX:XX:XX:XX:XX'

config interface 'lan'
        option device 'br-lan'
        option proto 'static'
        option ipaddr '192.168.1.1'
        option netmask '255.255.255.0'
        option ip6assign '60'

config device
        option name 'wan'
        option macaddr 'XX:XX:XX:XX:XX:XX'

config interface 'wan'
        option device 'wan'
        option proto 'dhcp'

config interface 'wan6'
        option device 'wan'
        option proto 'dhcpv6'

config interface 'WGINTERFACE'
        option proto 'wireguard'
        option force_link '1'
        option private_key 'XX:XX:XX:XX:XX:XX'
        option mtu '1320'
        list addresses 'XX:XX:XX:XX:XX:XX'
        list addresses 'XX:XX:XX:XX:XX:XX'

config wireguard_WGINTERFACE
        option description 'DESC'
        option public_key 'XXXXXXXXXXXX'
        option preshared_key 'XXXXXXXXXXXX'
        list allowed_ips '0.0.0.0/0'
        list allowed_ips '::/0'
        option endpoint_host 'XXXXXXXXXXXX'
        option endpoint_port '1637'
        option persistent_keepalive '15'
        option disabled '1'

config device
        option type 'bridge'
        option name 'br-guest'
        option bridge_empty '1'

config interface 'guest'
        option proto 'static'
        option device 'br-guest'
        option ipaddr '192.168.10.1'
        option netmask '255.255.255.0'

config interface 'iot'
        option proto 'static'
        option device 'br-iot'
        option ipaddr '192.168.20.1'
        option netmask '255.255.255.0'
        option type 'bridge'

config wireguard_WGINTERFACE
        option description 'DESC'
        option public_key 'XXXXXXXXXXXX'
        option preshared_key 'XXXXXXXXXXXX'
        list allowed_ips '0.0.0.0/0'
        list allowed_ips '::/0'
        option endpoint_host 'XXXXXXXXXXXX'
        option endpoint_port '1637'
        option persistent_keepalive '15'
        option disabled '1'

config wireguard_WGINTERFACE
        option description 'DESC'
        option public_key 'XXXXXXXXXXXX'
        list allowed_ips '0.0.0.0/0'
        list allowed_ips '::0/0'
        option endpoint_host 'XXXXXXXXXXXX'
        option endpoint_port '51820'
/etc/config/firewall
config defaults
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option synflood_protect '1'

config zone
        option name 'lan'
        list network 'lan'
        option input 'ACCEPT'
        option output 'ACCEPT'
        option forward 'ACCEPT'
        list device 'tunrw'

config zone
        option name 'wan'
        list network 'wan'
        list network 'wan6'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option masq '1'
        option mtu_fix '1'

config forwarding
        option src 'lan'
        option dest 'wan'

config rule
        option name 'Allow-DHCP-Renew'
        option src 'wan'
        option proto 'udp'
        option dest_port '68'
        option target 'ACCEPT'
        option family 'ipv4'

config rule
        option name 'Allow-Ping'
        option src 'wan'
        option proto 'icmp'
        option icmp_type 'echo-request'
        option family 'ipv4'
        option target 'ACCEPT'

config rule
        option name 'Allow-IGMP'
        option src 'wan'
        option proto 'igmp'
        option family 'ipv4'
        option target 'ACCEPT'

config rule
        option name 'Allow-DHCPv6'
        option src 'wan'
        option proto 'udp'
        option dest_port '546'
        option family 'ipv6'
        option target 'ACCEPT'

config rule
        option name 'Allow-MLD'
        option src 'wan'
        option proto 'icmp'
        option src_ip 'fe80::/10'
        list icmp_type '130/0'
        list icmp_type '131/0'
        list icmp_type '132/0'
        list icmp_type '143/0'
        option family 'ipv6'
        option target 'ACCEPT'

config rule
        option name 'Allow-ICMPv6-Input'
        option src 'wan'
        option proto 'icmp'
        list icmp_type 'echo-request'
        list icmp_type 'echo-reply'
        list icmp_type 'destination-unreachable'
        list icmp_type 'packet-too-big'
        list icmp_type 'time-exceeded'
        list icmp_type 'bad-header'
        list icmp_type 'unknown-header-type'
        list icmp_type 'router-solicitation'
        list icmp_type 'neighbour-solicitation'
        list icmp_type 'router-advertisement'
        list icmp_type 'neighbour-advertisement'
        option limit '1000/sec'
        option family 'ipv6'
        option target 'ACCEPT'

config rule
        option name 'Allow-ICMPv6-Forward'
        option src 'wan'
        option dest '*'
        option proto 'icmp'
        list icmp_type 'echo-request'
        list icmp_type 'echo-reply'
        list icmp_type 'destination-unreachable'
        list icmp_type 'packet-too-big'
        list icmp_type 'time-exceeded'
        list icmp_type 'bad-header'
        list icmp_type 'unknown-header-type'
        option limit '1000/sec'
        option family 'ipv6'
        option target 'ACCEPT'

config rule
        option name 'Allow-IPSec-ESP'
        option src 'wan'
        option dest 'lan'
        option proto 'esp'
        option target 'ACCEPT'

config rule
        option name 'Allow-ISAKMP'
        option src 'wan'
        option dest 'lan'
        option dest_port '500'
        option proto 'udp'
        option target 'ACCEPT'

config include 'pbr'
        option fw4_compatible '1'
        option type 'script'
        option path '/usr/share/pbr/firewall.include'

config zone
        option name 'WGVPN'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option masq '1'
        option mtu_fix '1'
        list network 'WGINTERFACE'

config forwarding
        option src 'WGVPN'
        option dest 'wan'

config forwarding
        option src 'lan'
        option dest 'WGVPN'

config zone
        option name 'GUEST'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        list network 'guest'

config rule
        option name 'Allow-DHCP-Guest'
        list proto 'udp'
        option src 'GUEST'
        option dest_port '67'
        option target 'ACCEPT'

config forwarding
        option src 'GUEST'
        option dest 'wan'

config forwarding
        option src 'GUEST'
        option dest 'WGVPN'

config zone
        option name 'IoT'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        list network 'iot'

config forwarding
        option src 'IoT'
        option dest 'wan'

config forwarding
        option src 'IoT'
        option dest 'WGVPN'

config rule
        option name 'Allow-DHCP-IoT'
        list proto 'udp'
        option src 'IoT'
        option dest_port '67'
        option target 'ACCEPT'

config rule
        option name 'OpenVPN'
        option src 'wan'
        option dest_port '7500-7505'
        option target 'ACCEPT'

config rule
        option name 'Block-Google-DNS-IoT'
        option src 'IoT'
        option dest 'wan'
        list dest_ip '8.8.8.8'
        option target 'REJECT'

config rule
        option name 'Block-Google-DNS-2-IoT'
        option src 'IoT'
        option dest 'wan'
        option target 'REJECT'
        list dest_ip '8.8.4.4'

config rule
        option name 'Block-Google-DNS-LAN'
        option src 'lan'
        option dest 'wan'
        list dest_ip '8.8.8.8'
        option target 'REJECT'

config rule
        option name 'Block-Google-DNS-2-LAN'
        option src 'lan'
        option dest 'wan'
        list dest_ip '8.8.4.4'
        option target 'REJECT'

config rule
        option name 'Force-DNS-to-Pi'
        option src 'lan'
        option dest 'lan'
        list dest_ip '192.168.1.250'
        option dest_port '53'
        option target 'ACCEPT'
        list src_ip '!192.168.10.0/24'
        list src_ip '!192.168.20.0/24'

config rule
        option name 'Allow-DNS-IoT'
        option src 'IoT'
        option target 'ACCEPT'
        option dest_port '53'

config rule
        option name 'Allow-DNS-Guest'
        option src 'GUEST'
        option dest_port '53'
        option target 'ACCEPT'
/etc/config/pbr
config pbr 'config'
        option enabled '1'
        option verbosity '2'
        option strict_enforcement '1'
        option resolver_set 'none'
        list resolver_instance '*'
        option ipv6_enabled '0'
        list ignored_interface 'vpnserver'
        option boot_timeout '30'
        option rule_create_option 'add'
        option procd_boot_delay '0'
        option procd_reload_delay '1'
        option webui_show_ignore_target '0'
        option nft_rule_counter '0'
        option nft_set_auto_merge '1'
        option nft_set_counter '0'
        option nft_set_flags_interval '1'
        option nft_set_flags_timeout '0'
        option nft_set_policy 'performance'
        list webui_supported_protocol 'all'
        list webui_supported_protocol 'tcp'
        list webui_supported_protocol 'udp'
        list webui_supported_protocol 'tcp udp'
        list webui_supported_protocol 'icmp'

config include
        option path '/usr/share/pbr/pbr.user.aws'
        option enabled '0'

config include
        option path '/usr/share/pbr/pbr.user.netflix'
        option enabled '0'

config include
        option path '/usr/share/pbr/pbr.user.wg_server_and_client'
        option enabled '0'

config dns_policy
        option name 'Redirect Local IP DNS'
        option src_addr '192.168.1.5'
        option dest_dns '1.1.1.1'
        option enabled '0'

config policy
        option name 'Ignore Local Requests'
        option interface 'ignore'
        option dest_addr '10.0.0.0/24 10.0.1.0/24 192.168.100.0/24 192.168.1.0/24'
        option enabled '0'

config policy
        option name 'Plex/Emby Local Server'
        option interface 'wan'
        option src_port '8096 8920 32400'
        option enabled '0'

config policy
        option name 'Plex/Emby Remote Servers'
        option interface 'wan'
        option dest_addr 'plex.tv my.plexapp.com emby.media app.emby.media tv.emby.media'
        option enabled '0'

config policy
        option name 'IoT-to-VPN'
        option src_addr '192.168.20.1/24'
        option interface 'WGINTERFACE'

config policy
        option name 'Guest-to-VPN'
        option src_addr '192.168.10.1/24'
        option interface 'WGINTERFACE'

config policy
        option name 'DESC'
        option src_addr 'XX:XX:XX:XX:XX:XX'
        option interface 'WGINTERFACE'
        option enabled '0'

config policy
        option name 'DESC'
        option src_addr 'XX:XX:XX:XX:XX:XX'
        option interface 'WGINTERFACE'
        option dest_addr '0.0.0.0/0'
        option enabled '0'

config policy
        option dest_addr 'dnsleaktest.com'
        option interface 'WGINTERFACE'
        option enabled '0'

config policy
        option name 'DESC'
        option src_addr '192.168.1.101'
        option interface 'WGINTERFACE'

config policy
        option name 'DESC'
        option src_addr ' 'XX:XX:XX:XX:XX:XX''
        option interface 'WGINTERFACE'

config policy
        option name 'Google'
        option dest_addr 'google.com'
        option interface 'WGINTERFACE'

config policy
        option name 'Mullvad Test'
        option dest_addr 'mullvad.net'
        option interface 'WGINTERFACE'
        option enabled '0'
/etc/config/dhcp
config dnsmasq
        option domainneeded '1'
        option localise_queries '1'
        option rebind_protection '1'
        option rebind_localhost '1'
        option local '/lan/'
        option domain 'lan'
        option expandhosts '1'
        option cachesize '1000'
        option authoritative '1'
        option readethers '1'
        option leasefile '/tmp/dhcp.leases'
        option localservice '1'
        option ednspacket_max '1232'
        list server '127.0.0.1#5055'
        list server '/mask.icloud.com/'
        list server '/mask-h2.icloud.com/'
        list server '/use-application-dns.net/'
        list server '127.0.0.1#5053'
        list server '127.0.0.1#5054'
        option doh_backup_noresolv '-1'
        option noresolv '1'
        list doh_backup_server '192.168.1.250'
        list doh_backup_server '127.0.0.1#5055'
        list doh_backup_server '10.64.0.1'
        list doh_backup_server '/mask.icloud.com/'
        list doh_backup_server '/mask-h2.icloud.com/'
        list doh_backup_server '/use-application-dns.net/'
        list doh_backup_server '127.0.0.1#5053'
        list doh_backup_server '127.0.0.1#5054'
        list doh_server '127.0.0.1#5053'
        list doh_server '127.0.0.1#5054'
        list doh_server '127.0.0.1#5055'

config dhcp 'lan'
        option interface 'lan'
        option start '100'
        option limit '50'
        option leasetime '12h'
        option dhcpv4 'server'
        option dhcpv6 'server'
        option ra 'server'
        list ra_flags 'managed-config'
        list ra_flags 'other-config'
        list dhcp_option '6,192.168.1.250'

config dhcp 'wan'
        option interface 'wan'
        option ignore '1'

config odhcpd 'odhcpd'
        option maindhcp '0'
        option leasefile '/tmp/hosts/odhcpd'
        option leasetrigger '/usr/sbin/odhcpd-update'
        option loglevel '4'

config dhcp 'guest'
        option interface 'guest'
        option start '100'
        option limit '30'
        option leasetime '12h'

config dhcp 'iot'
        option interface 'iot'
        option start '100'
        option limit '20'
        option leasetime '12h'

config host
        list mac 'XX:XX:XX:XX:XX:XX'
        option ip '192.168.20.110'
        option leasetime '12h'

config host
        option name 'DESC'
        list mac 'XX:XX:XX:XX:XX:XX'
        option ip '192.168.1.250'
/etc/config/https-dns-proxy
config main 'config'
        option dnsmasq_config_update '*'
        option force_dns '0'
        list force_dns_port '53'
        list force_dns_port '853'
        option procd_trigger_wan6 '0'

config https-dns-proxy
        option resolver_url 'https://dns.adguard-dns.com/dns-query'
        option bootstrap_dns '94.140.14.14,94.140.14.15'
        option listen_addr '127.0.0.1'
        option listen_port '5053'
        option user 'nobody'
        option group 'nogroup'

config https-dns-proxy
        option resolver_url 'https://cloudflare-dns.com/dns-query'
        option bootstrap_dns '1.1.1.1,1.0.0.1'
        option listen_addr '127.0.0.1'
        option listen_port '5054'
        option user 'nobody'
        option group 'nogroup'

config https-dns-proxy
        option resolver_url 'https://adblock.dns.mullvad.net/dns-query'
        option listen_addr '127.0.0.1'
        option listen_port '5055'
        option bootstrap_dns '194.242.2.2'
        option user 'nobody'
        option group 'nogroup'

Maybe someone of you has already implemented a similar setup - the goal is a clean split DNS behavior with VPN protection without DNS leak, but also without DoH/DoT overkill.

Thank you - any ideas are welcome!

Create blackhole routes.

What exactly leaks and to where?

I created blackhole routes for the hardware where Google's DNS is hardcoded. The problem is that my subnets (192.168.20.0/24 and 192.168.10.24/24) and all my policies from PBR use my PiHole or the DNS from my HTTPS DNS proxy.

For all networks, devices, etc., which use the WireGuard tunnel, I want to use the DNS from Mullvad (10.64.0.1), for example. However, I am unsure how to achieve this. When I set the DHCP server to 6,10.64.0.1 for my Wireguard interface (as described here: https://mullvad.net/en/help/running-wireguard-router), I cannot connect to the internet with my VPN tunnel anymore.

I hope I made my problem clear. Any help is appreciated. Thanks!

Have a look at my notes about Split DNS maybe those can be helpful

My notes about setting up WireGuard:

1 Like

I'll take a look โ€“ thanks! What's your opinion on DoH with Dnsmasq and HTTPS-DNS-Proxy and Unbound/Pi-Hole? Is it useful as a fallback, or is it inconsistent? As you can see in my settings, I have set my PiHole as the primary resolver for my LAN interface.

I am not an expert on these matters, I personally use DNSMasq and have HTTPS-DNS Proxy as the upstream resolver to get encrypted DNS (and use a DNS based Adblock package for adblocking).

Easy to setup and all I need but that is just my personal preference

1 Like

I rig the stubby start script to add host routes to upstreams so that they dont nibble to tunnels accidentally.

Interesting! I'll try to resolve the issue with @egc 's Split DNS notes. We'll keep you updated. Thanks guys

1 Like