Setting up your very own macOS kernel-level IKEv2/IPsec VPN kill switch using PF

tesla-v100

Perhaps you are using a VPN (short for virtual private network) to increase your privacy and security when on the internet. Have you ever wondered what happens between the time you log in to your Mac and the time your VPN is connected? The answer is a lot is happening and this crucial moment can leak personal data and make you vulnerable.

Know your way around kill switches, skip to the guide.

Kill switch

A kill switch is a feature found in some VPN apps used to block internet traffic when the VPN fails to connect. Most VPN apps have built-in kill switches which is great-ish but doesn’t protect users during the crucial moment discussed earlier. These kill switches operate at the application-level meaning they are not enabled immediately when you boot.

Application-level vs kernel-level kill switch implementation

Implementing a kill switch at the application-level is better than not implementing it at all, but, in order to circumvent all leaks, secure by design kill switches should be implemented at the kernel(<https://en.wikipedia.org/wiki/Kernel_%28operating_system%29)-level. Thankfully, macOS ships with a neat firewall called PF (short for packet filter) that can be configured as a kernel-level kill switch.

PF

PF uses rules to define which packets are allowed in or out and can be configured to only only allow DHCP and VPN requests yielding the ultimate kill switch.

Guide

DISCLAIMER 1: This guide will only work if you know the IP of the VPN server ahead of time and it will not work when connecting to the internet using captive portals (at Starbucks for example). Connecting to these kinds of networks without disabling the kill switch requires wizardry which might be the subject of another guide.

Step 1: enable PF

Open System Preferences, click on Security & Privacy, then click on Turn On Firewall. Once the firewall is on, click on Firewall Options…, check Enable stealth mode and click OK.

firewall

firewall-options

Step 2: create anchor

Pro tip 1: when copy/pasting commands that start with cat << "EOF", select all lines (from cat << "EOF" to EOF) at once as they are part of the same (single) command

cat << "EOF" | sudo tee /etc/pf.anchors/local.killswitch > /dev/null
# Options
set block-policy drop
set ruleset-optimization basic
set skip on lo0# Interfaces
wifi = "en0"
vpn = "ipsec0"

# Subnet
subnet = "10.0.1.0/24"

# VPN server IP
ip = "159.203.26.109"

# Block everything
block all # Use "block log all" to log blocked packets

# Allow all DHCP requests (used to establish Wi-Fi connection)
pass on $wifi proto udp from port { 67, 68 } to port { 67, 68 } keep state

# Allow multicast DNS requests (used to find devices using Bonjour, disable these lines while traveling)
#pass on $wifi from $subnet to 255.255.255.255 keep state
#pass on $wifi from 255.255.255.255 to $subnet keep state
#pass on $wifi proto udp from $subnet port 5353 to 224.0.0.251 port 5353 keep state
#pass on $wifi proto udp from 224.0.0.251 port 5353 to $subnet port 5353 keep state

# Allow all local network requests (used to access private network, disable this line when you don’t trust the network)
#pass on $wifi proto { tcp, udp } from $subnet to $subnet

# Allow TCP and UDP requests to VPN server (used to establish VPN connection)
pass on $wifi proto { tcp, udp } from any to $ip

# Allow all VPN requests
pass on $vpn all
EOF

Ok, a lot is happening here. Let’s break it down into reviewable pieces.

# Block everything
block all # Use "block log all" to log blocked packets

Block everything by default

# Allow all DHCP requests (used to establish Wi-Fi connection)
pass on $wifi proto udp from port { 67, 68 } to port { 67, 68 } keep state

Allow all DHCP requests

# Allow TCP and UDP requests to VPN server (used to establish VPN connections)
pass on $wifi proto { tcp, udp } from any to $ip

Allow TCP and UDP requests to VPN server

# Allow all VPN requests
pass on $vpn all

Allow all VPN requests

# Allow multicast DNS requests (used to find devices using Bonjour, disable these lines when you don’t trust the network)
#pass on $wifi from $subnet to 255.255.255.255 keep state
#pass on $wifi from 255.255.255.255 to $subnet keep state
#pass on $wifi proto udp from $subnet port 5353 to 224.0.0.251 port 5353 keep state
#pass on $wifi proto udp from 224.0.0.251 port 5353 to $subnet port 5353 keep state

Allow multicast DNS requests (disabled by default)

# Allow all local network requests (used to access private network, disable this line when you don’t trust the network)
#pass on $wifi proto { tcp, udp } from $subnet to $subnet

Allow all local network requests (disabled by default)

Step 3: replace value of subnet and ip placeholder variables

Replace 10.0.1.0/24 with your subnet. Don’t know what your subnet is? For most home networks, take the IP of your computer (mine is currently 10.0.1.249), replace the last part (249) by 0 and add /24.

Replace 159.203.26.109 with the IP of your VPN server.

subnet="10.0.1.0/24"
ip="159.203.26.109"
escaped_subnet=$(echo -n "$subnet" | sed 's/[./]/\\&/g')
escaped_ip=$(echo -n "$ip" | sed 's/\./\\&/g')
sudo sed -i '' "s/subnet = \".*\"/subnet = \"$escaped_subnet\"/" /etc/pf.anchors/local.killswitch
sudo sed -i '' "s/ip = \".*\"/ip = \"$escaped_ip\"/" /etc/pf.anchors/local.killswitch

Step 4: backup and override PF config file

sudo cp /etc/pf.conf /etc/pf.conf.backup
cat << "EOF" | sudo tee /etc/pf.conf > /dev/null
#
# Default PF configuration file.
#
# This file contains the main ruleset, which gets automatically loaded
# at startup.  PF will not be automatically enabled, however.  Instead,
# each component which utilizes PF is responsible for enabling and disabling
# PF via -E and -X as documented in pfctl(8).  That will ensure that PF
# is disabled only when the last enable reference is released.
#
# Care must be taken to ensure that the main ruleset does not get flushed,
# as the nested anchors rely on the anchor point defined here. In addition,
# to the anchors loaded by this file, some system services would dynamically
# insert anchors into the main ruleset. These anchors will be added only when
# the system service is used and would removed on termination of the service.
#
# See pf.conf(5) for syntax.
#

#
# com.apple anchor point
#
#scrub-anchor "com.apple/*"
#nat-anchor "com.apple/*"
#rdr-anchor "com.apple/*"
#dummynet-anchor "com.apple/*"
#anchor "com.apple/*"
#load anchor "com.apple" from "/etc/pf.anchors/com.apple"
anchor "local.killswitch"
load anchor local.killswitch from "/etc/pf.anchors/local.killswitch"
EOF

Step 5: reload PF config file

sudo pfctl -F all -f /etc/pf.conf

Step 6: reboot

Step 7: confirm PF is running and local.killswitch anchor is loaded using commands sudo pfctl -s info and sudo pfctl -s rules

No ALTQ support in kernel
ALTQ related functions disabled
Status: Enabled for 0 days 00:06:01           Debug: Urgent
State Table                          Total             Rate
  current entries                       24
  searches                           10062           27.9/s
  inserts                              103            0.3/s
  removals                             116            0.3/s
Counters
  match                                460            1.3/s
  bad-offset                             0            0.0/s
  fragment                               0            0.0/s
  short                                  0            0.0/s
  normalize                              0            0.0/s
  memory                                 0            0.0/s
  bad-timestamp                          0            0.0/s
  congestion                             0            0.0/s
  ip-option                              0            0.0/s
  proto-cksum                            0            0.0/s
  state-mismatch                         0            0.0/s
  state-insert                           0            0.0/s
  state-limit                            0            0.0/s
  src-limit                              0            0.0/s
  synproxy                               0            0.0/s
  dummynet                               0            0.0/s

Status: Enabled 👍

No ALTQ support in kernel
ALTQ related functions disabled
anchor "local.killswitch" all

anchor "local.killswitch" all 👍

You are now protected by your very own macOS kernel-level IKEv2/IPsec VPN kill switch using PF.

Want to see how much is blocked by the kill switch?

DISCLAIMER 2: I recommend only experimenting with this when on a trusted network as it requires reloading the PF config file which temporarily disables the kill switch.

Step 1: create pflog0 virtual network interface

sudo ifconfig pflog0 create

Step 2: enable PF logging

sed -i '' 's/block all/block log all/' /etc/pf.anchors/local.killswitch

Step 3: reload PF config file

sudo pfctl -F all -f /etc/pf.conf

Step 4: output blocked traffic to stdout

tcpdump -n -e -ttt -i pflog0
00:00:00.456159 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.56131 > 1.1.1.1.53: 8671+ A? detectportal.firefox.com. (42)
00:00:00.000070 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.49444 > 1.1.1.1.53: 48217+ A? ap.spotify.com. (32)
00:00:00.000048 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.50557 > 1.1.1.1.53: 46267+ A? s3.amazonaws.com. (34)
00:00:00.000048 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.53145 > 1.1.1.1.53: 62651+ A? 1-courier.sandbox.push.apple.com. (50)
00:00:00.000083 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.59997 > 1.1.1.1.53: 22393+ A? www.apple.com. (31)
00:00:00.000102 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.62111 > 1.1.1.1.53: 61503+ A? sync-701-us-west-2.sync.services.mozilla.com. (62)
00:00:00.000046 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.60825 > 1.1.1.1.53: 23774+ A? textsecure-service.whispersystems.org. (55)
00:00:00.000050 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.59915 > 1.1.1.1.53: 23825+ A? 1-courier.push.apple.com. (42)
00:00:00.000090 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.51934 > 1.1.1.1.53: 48283+ A? push.services.mozilla.com. (43)
00:00:00.000067 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.54255 > 1.1.1.1.53: 8999+ A? 5-courier.sandbox.push.apple.com. (50)
00:00:00.000061 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.56644 > 1.1.1.1.53: 47969+ A? 27-courier.push.apple.com. (43)
00:00:00.000051 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.60771 > 1.1.1.1.53: 53946+ A? s.mzstatic.com. (32)
00:00:00.004005 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.64122 > 1.1.1.1.53: 404+ A? mail.gandi.net. (32)
00:00:00.000046 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.57342 > 1.1.1.1.53: 17759+ A? apple.com. (27)
00:00:00.058591 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.64463 > 1.0.0.1.53: 44209+ A? gspe1-ssl.ls.apple.com. (40)
00:00:00.103151 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.60323 > 1.1.1.1.53: 10013+ A? slack.com. (27)
00:00:00.128698 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.62618 > 1.1.1.1.53: 21428+ PTR? 249.1.0.10.in-addr.arpa. (41)
00:00:01.012178 rule 0.local.killswitch.0/0(match): block out on en0: 10.0.1.249.49629 > 1.1.1.1.53: 36484+ A? lickstats.slack.com. (37)

As you can see, as soon as your Mac is connected to the internet, it calls home. The above blocked requests are all DNS requests (port 53). If these requests would have been allowed, hundreds of HTTP requests would have followed (although if enabled, the kill switch would have blocked them too). All of these requests can leak personal data and make you vulnerable.

Step 5: disable PF logging

sed -i '' 's/block log all/block all/' /etc/pf.anchors/local.killswitch

Hope you enjoyed this guide.

Contributors:Sun KnudsenSun Knudsen

Wish to contribute? Please submit an issue or a pull request.
This website is not tracking you. PGP public key fingerprint: C4FB DDC1 6A26 2672 920D  0A0F C132 3A37 7DE1 4C8B