How to ensure killswitch with WireGuard app on MacOS (with NextDNS optional)

Please take all of this advice carefully and double check recommendations for yourself. I have no particular networking knowledge and have cobbled this approach together from dozens of websites and online posts over the past few years (along with the recent help of AI). This is intended only for MacOS. If anybody has actual networking knowledge and wants to help refine this, please do! Please point out any errors or improvements.

First, install WireGuard. You can use either the GUI app (which is only available through the Mac App Store, and so requires an Apple Account), or you can use the CLI (wireguard-go and wireguard-tools, available through Homebrew). I used the CLI for the past 4-5 years but have recently switched to the GUI app because I’m trying to move away from using Homebrew (for reasons irrelevant to this guide). Install the version of the software you prefer. Go to your VPN provider and generate WireGuard configs. I use both ProtonVPN and Mullvad. Set up the configs in your WireGuard app following the guides offered by your provider: either import them into the GUI app, or place them in the relevant folder for the CLI and use “wg-quick up confg_name.conf” to enable.

If using the GUI app, ensure to tick the “On Demand” boxes for Ethernet and Wi-Fi when editing the configurations. Whichever configuration you want to use when the system starts, make sure “Connect on Demand” is enabled under MacOS’s settings (VPN → click the ‘i’ button).

You should also set WireGuard to load on system boot. This is easier with the GUI app, since you just need to add it to Login Items. With the CLI you’ll need to use a startup script.

  1. Create the kill switch:

When WireGuard is up and running and connected, find the interface that it’s using by running this command in terminal:

ifconfig

For me, it’s utun4. I’ve read that the interface MacOS assigns to the WireGuard tunnel can change, but I haven’t noticed this happening despite many restarts and updates over many months. I can determine it’s utun4 for me because, scanning the output of the ifconfig command, utun4 shows something like “inet XX.XX.XX.XX → XX.XX.XX.XX” where the XX.XX.XX.XX IP matches that listed in the WireGuard configuration file’s “Address” field under [Interface].

Next, create the kill switch file:

sudo nano /etc/killswitch.pf.conf

Paste the following. Replace “utunX” with the interface WireGuard is using, e.g. utun4. Replace the VPN server IPs and ports with the actual IPs and ports from the configs you’ve generated, which are included in the config files. If you’re using only one config, then just include that; if you’re using many, include them all. Including them here will allow WireGuard to bootstrap and connect.

# Block all traffic by default
block all

# Allow local network (en0)
pass on en0 from any to 192.168.0.0/24
pass on en0 from 192.168.0.0/24 to any

# Allow loopback traffic
pass on lo0 inet from 127.0.0.1 to 127.0.0.1
pass on lo0 inet6 from ::1 to ::1

# Allow DHCP for IP address negotiation
pass out proto udp from any port 68 to any port 67 keep state
pass in proto udp from any port 67 to any port 68 keep state

# Allow traffic ONLY through the WireGuard tunnel interface
pass on utunX all

# Allow traffic to your VPN server endpoints
pass out proto udp from any to XX.XX.XX.XX port 51820 keep state
pass out proto udp from any to XX.XX.XX.XX port 51820 keep state
pass out proto udp from any to XX.XX.XX.XX port 51820 keep state

# Allow mDNS if needed
pass out proto udp from any to 224.0.0.251 port 5353 keep state

# HTTPS and NTP through tunnel only
pass out on utun4 proto tcp to any port 443 keep state
pass out on utun4 proto udp to any port 123 keep state
pass in proto udp from any port 123 to any keep state

# Block uPNP unless needed
block out proto udp to 224.0.0.0/4 port 1900

# Block all IPv6 unless needed
block quick inet6 all

Regarding local network traffic (en0), find your subnet with

ifconfig en0 | grep "inet "

This gives me “inet 192.168.0.104 netmask 0xffffff00 broadcast 192.168.0.255”, for example, so my subnet is 192.168.0.0/24. Replace that given in the config with your own subnet.

Next, integrate killswitch.pf.conf into the system PF config:

sudo nano /etc/pf.conf

Add this line at the bottom:

include "/etc/killswitch.pf.conf"

Test the rules:

sudo pfctl -nf /etc/pf.conf     # Syntax check
sudo pfctl -Fa -f /etc/pf.conf  # Load rules
sudo pfctl -e                   # Enable PF

Verify the loaded rules:

sudo pfctl -sr 

You should see your rules listed.

Make the kill switch persistent across reboots: turn on MacOS Firewall in System Settings - Network - Firewall - Turn On.

You can confirm that the kill switch is working by disconnecting from WireGuard and trying to access anything on the internet. All connections should fail.

  1. (Optional) Integrate NextDNS

If you want to use NextDNS, then this is how I’ve managed to get it to work. Install NextDNS CLI following the instructions at my.nextdns.io/XXXXX/setup where XXXXX is the ID of the profile you want to use. Once NextDNS is installed, activated, and started on your system, you can edit your WireGuard configs to tell WireGuard to use NextDNS. In the GUI, simply hit Manage Tunnels, and Edit. Under [Interface], change DNS to:

DNS = 127.0.0.1, ::1

Do this for all of your WireGuard configs. If you’re using WireGuard CLI, then edit your files in terminal. You can then edit your killswitch config (sudo nano /etc/killswitch.pf.conf) to include the following. You can add it anywhere in the file:

# DNS through NextDNS only

block out proto udp from any to any port 53
block out proto tcp from any to any port 53
pass out proto udp from any to 127.0.0.1 port 53 keep state
pass out proto tcp from any to 127.0.0.1 port 53 keep state

This forces all DNS queries to 127.0.0.1:53, where NextDNS should be listening.

If at any point you’ve inadvertently or unexplainably lost internet connectivity during the setup and want to disable the killswitch, simply use

sudo pfctl -d

I would personally make sure you’re comfortable with NextDNS CLI’s commands (sudo nextdns start/stop/restart/activate/deactivate/install/uninstall/etc.) and with what each command does before proceeding with this step of the set up. If you’re using WireGuard via CLI as well, please make sure you’re comfortable with its most common commands (sudo wg-quick up XXX.conf, wg-quick down XXX.conf, wg-quick status, etc.). Also familiarise yourself with the commands to control the PF firewall:

sudo pfctl -e                             # Enable
sudo pfctl -d                             # Disable
sudo pfctl -nf /etc/killswitch.pf.conf    # Verify syntax
sudo pfctl -f /etc/pf.conf                # Load rules
sudo pfctl -sr                            # Confirm rules loaded

If people are interested in my set up using LittleSnitch, where I have two profiles (one called Connect, which only allows connections to my WireGuard VPN endpoints and NextDNS, and one called Established, which contains all other rules) I can briefly describe that too. This really isn’t necessary, though.

Last edited by @JG 2026-01-10T19:28:29Z

3 Likes

If you need to reverse anything, you can completely remove the kill switch. Just disable PF “sudo pfctl -d”, delete /etc/killswitch.pf.conf, edit /etc/pf.conf to remove the reference to the kill switch, and re-enable PF again with “sude pfctl -e” if you want it re-enabled. NextDNS can be removed with “sudo nextdns stop”, “sudo nextdns deactivate”, and “sudo nextdns uninstall”. If you check /etc/resolv.conf you should see your regular resolvers instead of localhost. Make sure to regenerate your WireGuard configs from your provider so that they contain the correct DNS IPs.