My goal was to have a Split-Tunnel WireGuard connection which grants access to some private services, including a DNS server, and to serve some DNS results over the WireGuard connection. I wanted the rest of the requests to be restored to the original DNS of the WIFI connection. Without this, internal hostnames either fail or leak to public DNS. To do this we need to setup a DNS resolver.
By creating a resolver that is only responsible for your internal domain, queries for names such as host.internal are sent through the DNS server reachable over the WireGuard tunnel. All other DNS queries continue to use your normal network’s DNS servers, so public traffic behaves exactly as it would without the VPN.
This approach is often referred to as split DNS. Instead of routing all DNS traffic through the VPN, only the domains that actually exist on the remote network use the VPN’s DNS server. This avoids unnecessary latency for public lookups while still allowing private hostnames to resolve correctly.
Point Dnsmasq to original DNS nameservers
I followed dferg’s guide on how to configure splitdns on macos. Instead of having no-resolve the goal is to point towards a custom updated resolve file.
First install Dnsmasq via homebrew (install homebrew via https://brew.sh/).
brew install dnsmasq
# https://lansenou.com/blog/2026/06/27/split-dns-on-macos/
# Edited from: https://gist.github.com/dferg/0472269333be4aca6aaa21cf3b165c02
# Ignore /etc/resolv.conf - Keep this disabled to make resolv-file work
# no-resolv
resolv-file=/opt/dnsmasq/upstream.conf
# For queries *.internal and *.box, forward to the specified DNS server
# Servers are queried in order (if the previous fails)
# -- Note: These are EXAMPLES. Replace with your desired config.
server=/internal/box/10.0.0.1
server=/internal/box/10.0.0.2
# Forward all other requests to Google's public DNS server - Keep this disabled since we use our own resolv-file
# server=8.8.8.8
# Only listen for DNS queries on localhost
listen-address=127.0.0.1
# Required due to macOS limitations
bind-interfaces
Restoring DNS settings
Now I needed a way to put the original DNS settings that DHCP is handing out into the upstream.conf file.
Grabbing original DNS values from scutil
This file grabs all upstream servers via the scutil command, and filters out the own resolver address (127.0.0.1). It outputs the result into the upstream.conf file.
#!/bin/bash
# https://lansenou.com/blog/2026/06/27/split-dns-on-macos/
OUT="/opt/dnsmasq/upstream.conf"
# Get macOS DNS entries
scutil --dns | awk '/nameserver\[[0-9]+\]/{print $3}' \
| grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$' \
| grep -vx '127\.0\.0\.1' \
| sort -u \
| sed 's/^/nameserver /' \
> "$OUT"
# Fallback (never leave empty)
if [ ! -s "$OUT" ]; then
echo "127.0.0.1" > "$OUT"
fi
# restart dnsmasq safely
killall -HUP dnsmasq 2>/dev/null
sudo chmod +x /opt/dnsmasq/update-dns.sh
Running a background script
I’m using launchctl to make sure the update-dns.sh is ran at load time and also when settings change.
The /Library/Preferences/SystemConfiguration directory contains macOS’s network configuration state. Changes to the system will cause the script to update again:
- VPN connect/disconnect
- Joining a different Wi-Fi network
- Ethernet connecting/disconnecting
- DNS changes
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.dnsmasq.dnsupdate</string>
<key>ProgramArguments</key>
<array>
<string>/opt/dnsmasq/update-dns.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WatchPaths</key>
<array>
<string>/Library/Preferences/SystemConfiguration</string>
</array>
</dict>
</plist>
Optional: Depends on your setup, but my script couldn’t access the update-dns script. To allow the current user to access the script run the following code:
sudo mkdir -p /opt/dnsmasq
sudo chown -R $(whoami) /opt/dnsmasq
Load the dns update script
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.dnsmasq.dnsupdate.plist
Setting up wireguard to point to the internal resolver
To make sure your local dnsmasq resolver is used, point towards it in the wireguard configuration. In my wireguard setup I want to have access to the whole subnet.
[Interface]
DNS = 127.0.0.1
#... rest of the interface
[Peer]
AllowedIPs = 10.0.0.0/24 # Exposes the entire network range 10.0.0.0-10.0.0.255
#... rest of the peer setup
Now you should activate the configuration.
Finishing it all
Restart the dnsmasq to use make use of the upstream config file.
sudo brew services restart dnsmasq
The DNS resolver should now correctly be setup for wireguard and support network switching. After reconnecting the VPN, internal domains resolves through the VPN DNS while public domains continue using the system resolver.
$ nslookup google.com
Server: 127.0.0.1
Address: 127.0.0.1#53
Non-authoritative answer:
Name: google.com
Address: 172.217.23.238
$ nslookup nas.internal
Server: 127.0.0.1
Address: 127.0.0.1#53
Non-authoritative answer:
Name: nas.internal
Address: 10.0.0.3
Thanks for reading! This my first blog post, please let me know if my experience helped you, or if I should have described some steps in more depth.
Troubleshooting
Check launched processes
launchctl list | grep dnsmasq
Unloading the script
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/local.dnsmasq.dnsupdate.plist
Manually check the logs of dnsmasq
sudo brew services stop dnsmasq
sudo dnsmasq --no-daemon --log-queries --log-facility=-
Don’t forget to start dnsmasq again after this.
