A macOS VPN kill switch is a Packet Filter ruleset, loaded into an anchor, that blocks everything not leaving on the tunnel interface. macOS ships pf and ships pfctl to drive it, so nothing needs installing. What macOS does not ship is a switch in System Settings, which is why this ends up on the command line. This page covers the anchor file, IPv6, a LAN exception, and the utun interface problem that breaks most copied rulesets. It also covers how to get the rules back off a laptop that will not go online.
What Packet Filter has to block
The model is four rules, and pf expresses each of them directly.
- Block all outbound traffic by default, so anything not explicitly passed is dropped
- Pass everything on the tunnel interface, which is a
utundevice on macOS - Pass the VPN server address, port and protocol, so the tunnel can establish through the block
- Pass loopback, and one named LAN subnet if you want printing and file shares to keep working
pf evaluates every rule and the last match wins, unless a rule carries quick, which stops evaluation there. That single behaviour explains most broken rulesets found online. Put the blanket block first, mark every exception quick, and the ruleset reads top to bottom the way you expect.
Read this before running anything. These rules cut outbound network access on the machine. Load them from a Mac you are sitting at, not over SSH or Screen Sharing. The session you would use to fix a mistake is the session the rules drop.
Find your utun interface
macOS does not give your VPN a stable interface name. It hands out utun0, utun1, utun2 and upward in the order interfaces are created, and several Apple services create their own. macOS creates utun devices for purposes of its own as well as for your VPN, so the lowest number is usually not the one you want. Do not choose an interface by its number. Identify yours by the address it carries.
Connect the tunnel first, then identify it. Two commands narrow it down quickly:
ifconfig -a | grep -i POINTOPOINT
ifconfig | grep -A3 utunMatch the interface whose inet address is the address on the Address line of your WireGuard config, or the address your .ovpn profile was assigned. That is your tunnel. Under the WireGuard app, sudo wg show also names the interface directly.
Hardcoding utun0 is the mistake that makes a working ruleset stop working after a reboot. You will find rulesets that write on utun with no number, on the theory that pf reads the bare name as a group covering every utun device. Do not adopt that without testing it on your own machine. Note what it costs even when it works, because it passes traffic on Apple's tunnel interfaces alongside yours. Prefer the exact name and re-check it after every reboot.
Building the macOS VPN kill switch with pfctl
Writing the anchor file
Put your rules in your own anchor file rather than editing Apple's. Create /etc/pf.anchors/killswitch with sudo nano /etc/pf.anchors/killswitch and write:
vpn_if = "utun4"
vpn_server = "203.0.113.10"
vpn_port = "51820"
lan_net = "192.168.1.0/24"
set skip on lo0
block drop out all
block drop in all
pass out quick on $vpn_if all
pass in quick on $vpn_if all
pass out quick proto udp to $vpn_server port $vpn_port
pass in quick proto udp from $vpn_server port $vpn_port
pass out quick to $lan_net
pass in quick from $lan_netReplace all four macro values. For an OpenVPN profile, read the address, port and protocol from the remote and proto lines, and change proto udp to proto tcp if the profile connects over TCP. The rest of the file does not change between protocols.
set skip on lo0 exempts loopback entirely, which is cheaper and safer than writing pass rules for it. Leave it in.
Loading the anchor
macOS reads /etc/pf.conf at load time, and that file has a required order: options, then normalization, then queueing, then translation, then filtering. Add two lines to it, each in the right group. The anchor declaration goes with the other filtering anchors, and the load line goes with the other load line.
anchor "killswitch"
load anchor "killswitch" from "/etc/pf.anchors/killswitch"Place anchor "killswitch" immediately after the existing anchor "com.apple/*" line, and the load anchor line immediately after the existing load anchor "com.apple" line. Putting either one earlier in the file produces a syntax error that names the wrong line, which wastes a lot of time.
Check the syntax before loading anything:
sudo pfctl -vnf /etc/pf.conf-n parses without loading, so a mistake costs you an error message rather than your connection. When it parses cleanly, load and enable:
sudo pfctl -f /etc/pf.conf
sudo pfctl -epfctl -e enables pf for this boot only. The pfctl manual page macOS ships documents -E as enabling the packet filter and incrementing the pf enable reference count. It documents -X as releasing the reference represented by the token you pass back to it. That pair exists for tools that have to co-operate with other users of pf. Use -e for a kill switch you drive by hand.
When the parse fails
pfctl -vnf reports two errors far more often than any other, and both name a line that is not the line at fault.
The first is placement. /etc/pf.conf requires its statements in one order: options, then normalization, then queueing, then translation, then filtering. Put anchor "killswitch" above the scrub or translation anchors and pfctl rejects the file, usually pointing at the next line rather than at yours. Move the declaration down to sit with the other filtering anchors and re-check.
The second is the set skip on lo0 line inside the anchor file. set is an option, and options belong to the main ruleset rather than to an anchor. Published macOS rulesets carry it inside the anchor and report it working, so leave it there and test. If pfctl -vnf names that line, delete it from the anchor and put it in /etc/pf.conf instead, above the anchor declaration and with the other options. Loopback is then skipped globally, which is the same outcome.
Check after every edit rather than at the end. A file that parses is a file you can load without losing the network, and -n is the difference between an error message and a walk to another machine.
Checking what is loaded
Never assume the rules took. Read them back:
sudo pfctl -s info
sudo pfctl -s rules
sudo pfctl -a killswitch -s rules-s info prints Status: Enabled when pf is running. -s rules prints the main ruleset, where you should see the anchor referenced rather than your rules. -a killswitch -s rules prints the rules inside your anchor, which is where your block and pass lines actually live. If the third command prints nothing, the anchor loaded empty and the kill switch is not doing anything.
The full command set is short enough to keep on one card.
| Command | What it does | When you need it |
|---|---|---|
sudo pfctl -vnf /etc/pf.conf | Parses the config without loading it | Every single time, before -f |
sudo pfctl -f /etc/pf.conf | Loads the ruleset, replacing what is loaded | After any edit to the anchor file |
sudo pfctl -e / sudo pfctl -d | Enables and disables pf for this boot | -d is the command that gets you back online |
sudo pfctl -s info | Prints Status: Enabled or Disabled | When rules are loaded but nothing is blocked |
sudo pfctl -a killswitch -s rules | Prints the rules inside your anchor | To confirm the anchor is not empty |
sudo pfctl -a killswitch -F rules | Flushes the rules inside your anchor only | To remove the kill switch without touching Apple's rules |
Every flag in that table is documented in the pfctl manual page macOS ships, which man pfctl opens. The anchor rules are not, because they are yours. Parse them with -vnf after every edit, and read them back with -a killswitch -s rules after every load.
IPv6
The block drop out all line blocks IPv6 along with IPv4, because pf without an address family qualifier matches both. That is the correct default and it is why the ruleset above needs no separate IPv6 block.
The exceptions are where it goes wrong. pass out quick to $lan_net matches an IPv4 subnet only, and pass out quick proto udp to $vpn_server matches whichever family the server address belongs to. If your VPN server has an IPv6 endpoint, or your LAN uses IPv6 for local discovery, those need their own pass lines with the correct addresses.
Several published macOS rulesets solve this with a blanket block out inet6 all instead. That works, and it also kills IPv6 inside the tunnel. If your config carries ::/0 in AllowedIPs, or your provider assigns an IPv6 address, a blanket block silently removes something you are paying for. Filter it rather than disabling it.
LAN exceptions
The two $lan_net lines are what keep AirPrint, a NAS and your router admin page reachable while the tunnel is down. Nothing leaks without them, so this is a usability decision made deliberately.
Scope it to the subnet your router actually hands out. Read it from System Settings, or run ipconfig getifaddr en0 and take the first three octets. A /24 is almost always right. Copying 192.168.0.0/16 from another guide opens every private address on every network the laptop ever joins, and a laptop joins a lot of networks.
AirPrint and Bonjour discovery also use multicast, which the blanket block drops. Confirm that is what you are hitting before writing a rule for it. Run sudo pfctl -d, retry the printer, then run sudo pfctl -e again. If discovery works with pf disabled and fails with it enabled, what you need is a multicast pass rule rather than a wider subnet.
Making it survive a reboot
This is the section no incumbent has, and there are two separate problems inside it.
The first is that pf is not enabled at boot. macOS loads /etc/pf.conf during startup but does not turn pf on, so your rules are present and inert until you run pfctl -e. Fixing that means adding a LaunchDaemon of your own in /Library/LaunchDaemons that runs pfctl -e -f /etc/pf.conf at load. The launchctl subcommands for registering a daemon have changed across macOS releases, so read man launchctl on the machine you are configuring rather than copying an invocation from a guide. Then reboot and confirm with sudo pfctl -s info that pf came up enabled.
The second is that /etc/pf.conf is an operating system file. A macOS update can replace it, which silently removes your two lines while leaving /etc/pf.anchors/killswitch untouched. The kill switch then looks installed and does nothing. That is the reason for keeping your rules in a separate anchor file rather than pasting them into pf.conf: after an update you re-add two lines instead of rewriting a ruleset.
There is a third quirk worth knowing before you build tooling around this. A bug report on the Apple Developer Forums states that from Ventura onward, pfctl retains anchors after flushing rules, states and tables. The report adds that the only way to remove the leftover anchors is to reboot. Check your own machine with sudo pfctl -s Anchors after a flush rather than assuming it either way. If the anchor is still listed, reboot before you conclude that something is broken.
Turning it off
Include this in your notes, because a stuck packet filter on a laptop is worse than no packet filter at all. One command restores connectivity:
sudo pfctl -dThat disables pf entirely and takes effect immediately. Do that first, diagnose afterwards. To remove the rules rather than disable the filter, flush the anchor and reload:
sudo pfctl -a killswitch -F rules
sudo pfctl -f /etc/pf.confTo remove the kill switch permanently, delete the two lines you added to /etc/pf.conf, delete /etc/pf.anchors/killswitch, unload your LaunchDaemon, and reload with sudo pfctl -f /etc/pf.conf. Reboot afterwards, given the anchor behaviour noted above.
Confirming it blocks
Connect, confirm browsing works, drop the tunnel, confirm traffic stops. That is a sanity check and it is where every competing page ends.
It tests one application, in the foreground, after the risky interval has closed. It also cannot tell you whether the thing that leaked was an address or a hostname, and those are different failures with different fixes. The full method uses a packet capture on the physical interface, exercises name resolution separately from raw addresses, and drops the tunnel several different ways. It is written up in how to test if a VPN kill switch works.
Quick answers
- Does macOS have a built-in VPN kill switch
- Not as a system setting. The firewall in System Settings filters inbound connections only and has no concept of a tunnel. Packet Filter is the built-in mechanism, but it ships with no rules and no interface beyond pfctl. The WireGuard app is not where to look either. Open the tunnel's settings and search for an option that blocks untunneled traffic, because the wording varies by build. If there is nothing there, the pf ruleset on this page is your mechanism.
- Tunnelblick kill switch
- Tunnelblick has one built in, and most pages ranking for this term do not say so. Tunnelblick's own documentation places it on the Settings tab of the Configurations panel in VPN Details, where the On unexpected disconnect option can be set to Disable network access. Read the rest of that documentation before you rely on it. Tunnelblick states that all network access is disabled, including access to your local network. It also states that access stays disabled until you re-enable it, even after you restart the computer, and that a Re-enable Network Access command appears in the menu for exactly that. Tunnelblick warns against using the feature while more than one VPN is connected. The pf ruleset on this page is for everything Tunnelblick does not manage.
- Will these rules survive a macOS update
- The anchor file will. The two lines in /etc/pf.conf may not, because that file belongs to the operating system and an update can replace it. Check after every major update by running sudo pfctl -a killswitch -s rules. Empty output means the anchor is no longer being loaded, and the fix is to re-add the two lines rather than to rebuild anything.
- How to disable vpn kill switch
- Run sudo pfctl -d. That disables Packet Filter immediately and restores network access, without deleting any of your rules. To remove the rules as well, flush the anchor with sudo pfctl -a killswitch -F rules, then reload with sudo pfctl -f /etc/pf.conf. To remove it permanently, delete the two lines from /etc/pf.conf and unload your LaunchDaemon.
- Does vpn affect local network
- Yes, once this ruleset is loaded. block drop out all blocks your LAN alongside the internet, so AirPrint, network shares and the router admin page stop responding until you add the subnet exception. Multicast discovery is blocked separately from unicast traffic, so a printer can be reachable by address while remaining invisible in a browse list. For where a kill switch fits across operating systems, start at the kill switch setup guide. The client-side steps live in installing WireGuard and using an OpenVPN profile. Keep what a kill switch does and does not do alongside this page, because a kill switch does not fix bad DNS settings by itself. If the tunnel will not come up at all, that is connection troubleshooting rather than a filter problem.