Earlier I made a post saying that I figured out how to add local routes to another VLAN subnet in Wireguard without leaking DNS. However, what I figured out is that while blocking INCOMING connections is relatively simple (firewalld), blocking OUTBOUND connections can be extremely difficult. Mullvad VPN does this at a very low level, seen here: https://mullvad.net/en/help/split-tunneling-with-linux-advanced
So, prior to learning that I had done some quick testing and thought I had succeeded, when in reality I did not. Here are some attempts I made, why they did not work, and in the end a simplified approach that did end up working (no DNS leaks outside the wireguard tunnel).
Again, the scenario is: wanting to access a device on a different subnet of the internal network with the Wireguard tunnel activated. Unbound DNS is listening on all subnets, 192.168.1.1, 192.168.2.1, etc. Using wg-quick tool to bring up Wireguard.
Attempt 1: MODIFY LAN DNS WITH NMCLI
In a Wireguard config file you can add “PreUp” and “PostDown” system commands to run stuff before and after the tunnel connects. Unfortunately with some Linux networking (NetworkManager in this case) it will keep using the DHCP assigned DNS server, and if a local route to the DNS server is available it will use it for some things and therefore leak.
To prevent this, you can use the Pre/Post commands to force the LAN DNS server to match the Wireguard tunnel’s DNS server, and simply return it to normal after the tunnel is closed.
This only works with wg-quick, not the NetworkManager Wireguard plugin since that does not overwrite the resolv.conf or run the PreUp/PostDown commands as far as I can tell.
The configuration below seemed airtight at first test using https://dnsleaktest.com/:
PreUp = ip route add 192.168.3.0/24 via 192.168.1.1 dev enp4s0
PreUp = nmcli conn modify enp4s0 ipv4.ignore-auto-dns yes
PreUp = nmcli conn modify enp4s0 ipv4.dns "10.2.0.1"
PreUp = systemctl restart NetworkManager
PostDown = ip route del 192.168.3.0/24 via 192.168.1.1 dev enp4s0
PostDown = nmcli conn modify enp4s0 ipv4.ignore-auto-dns no
PostDown = nmcli conn modify enp4s0 ipv4.dns "192.168.1.1"
PostDown = systemctl restart NetworkManager
However, when monitoring the enp4s0 interface over an extended period of time with WireShark, and testing with https://ipleak.net/ and https://www.dnscheck.tools/, I began seeing a small trickle of DNS requests leaking out.
Attempt 2: USE FIREWALL RULES
I tried the following rules:
PreUp = ip route add 192.168.3.0/24 via 192.168.1.1 dev enp4s0
PreUp = firewall-cmd --zone=public --add-forward-port=port=53:proto=tcp:toport=53:toaddr=10.2.0.1
PreUp = firewall-cmd --zone=public --add-forward-port=port=53:proto=udp:toport=53:toaddr=10.2.0.1
PreUp = firewall-cmd --add-rich-rule='rule family="ipv4" destination not address="10.2.0.1" port port="53" protocol="udp" reject'
PreUp = firewall-cmd --add-rich-rule='rule family="ipv4" destination not address="10.2.0.1" port port="53" protocol="tcp" reject'
PreUp = firewall-cmd --add-masquerade
PostDown = ip route del 192.168.3.10/32 via 192.168.1.1 dev enp4s0
PostDown = firewall-cmd --zone=public --remove-forward-port=port=53:proto=udp:toport=53:toaddr=10.2.0.1
PostDown = firewall-cmd --zone=public --remove-forward-port=port=53:proto=tcp:toport=53:toaddr=10.2.0.1
PostDown = firewall-cmd --remove-rich-rule='rule family="ipv4" destination not address="10.2.0.1" port port="53" protocol="udp" reject'
PostDown = firewall-cmd --remove-rich-rule='rule family="ipv4" destination not address="10.2.0.1" port port="53" protocol="tcp" reject'
#PostDown = firewall-cmd --remove-masquerade
Same result, after a while a small trickle of DNS requests. At this point, I realized that I had a network bridge left over from some VM work, and deleted it just in case…
Attempt 3: FIREWALL RULES, DELETED NETWORK BRIDGE
This one almost had me fooled. I had to wait almost 5mins for just a couple of DNS queries, notably from the Obsidian app which somehow circumvented all this and sent a couple queries out from enp4s0. This is when I stumbled upon the Mullvad VPN article here https://mullvad.net/en/help/split-tunneling-with-linux-advanced which showed me I was straying into MUCH to deep waters for my liking. I was probably not going to succeed here without knowledge of nftables, and I have no desire to go that deep.
Attempt 4: ONLY ADD INDIVIDUAL IP ADDRESS ROUTES INSTEAD OF ROUTES TO A SUBNET
I used the following setup, which just points to the specific device I want to access, rather than the subnet:
PreUp = ip route add 192.168.3.10/32 via 192.168.1.1 dev enp4s0
PostDown = ip route del 192.168.3.10/32 via 192.168.1.1 dev enp4s0
I have monitored this for around 30 minutes while doing leak tests from https://ipleak.net/, https://dnsleaktest.com/, and https://www.dnscheck.tools/, and have seen no DNS leakage. I believe the issue is that if you allow access to the DNS server on the subnet you are trying to reach via the static route (in this case 192.168.3.1) it seems to require low level control to block ALL applications from accessing it.
I am definitely still a learner when it comes to networking, but it definitely surprised me about how big of a trap this was. My only comfort here is that I can verify exactly what is going on with WireShark. It also underscores the importance of only using VPN apps that have been made specifically for your distro/OS, and are actively supported on that distro/OS by the VPN provider. After this, I’d certainly be careful to test any Flatpak/Snap versions of VPN apps to make sure they perform as advertised with no DNS leaks, and would prefer native versions of these applications if available.
Hopefully this helps someone avoid the same amount of effort, and to just pick the simple (if annoyingly inelegant) way of doing this… you really don’t want to go down the rabbit hole.