Prevent outgoing traffic unless OpenVPN connection is active using pf.conf on Mac OS X

Solution 1:

By monitoring network connections using Little Snitch, I've found that Apple uses the mDNSResponder app in the background to check if the Wi-Fi connection is available. mDNSResponder sends UDP packets to nameservers to check connectivity and resolve hostnames to IPs.

Changing the UDP rule I had previously to allow all UDP packets over Wi-Fi allows mDNSResponder to connect, which means Wi-Fi now reconnects first time after a disconnection. In case it helps others in future, my final pf.conf including Apple's default rules for Mountain Lion looks like this:

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

#
# Allow connection via Viscosity only
#
wifi=en1 #change this to en0 on MacBook Airs and other Macs without ethernet ports
vpn=tun0
vpn2=tap0

block all

set skip on lo          # allow local traffic

pass on p2p0            #allow AirDrop
pass on p2p1            #allow AirDrop
pass on p2p2            #allow AirDrop
pass quick proto tcp to any port 631    #allow AirPrint

pass on $wifi proto udp # allow only UDP packets over unprotected Wi-Fi
pass on $vpn            # allow everything else through the VPN (tun interface)
pass on $vpn2           # allow everything else through the VPN (tap interface)

This means that data can now be leaked over Wi-Fi by the small number of applications that use the UDP protocol, unfortunately, such as ntpd (for time synchronisation) and mDNSResponder. But this still seems better than allowing data to travel unprotected over TCP, which is what the majority of applications use. If anyone has any suggestions to improve on this setup, comments or further answers are welcome.

Solution 2:

You don't need to allow all UDP. The 'm' in mDNS means 'multicast', and it uses a specific multicast destination IP address called the "link local multicast address", and a UDP port number 5353.

This means in your solution above, you are unnecessarily allowing traffic to all 65535 UDP ports to all 3.7 Billion routable IP addresses in the world to bypass your VPN. You'd be surprised how many applications use UDP, so you are significantly defeating the purpose of your original idea to prevent outgoing traffic when the VPN is down.

Why not use this rule instead:

pass on $wifi proto udp to 224.0.0.251 port 5353

A very important rule of thumb with firewall configuration - when making exceptions through your firewall, always try to use the most specific rule possible. The specificity sometimes comes at the expense of convenience & ease of use, i.e. you might then find there's some other link-local protocol that needs to be let through, and add yet another specific rule.

If you swap in the above rule and find that the original wifi problem returns, then your PF may be blocking DHCP, the protocol used to autoconfigure your network devices' IP addresses. (in a home network, typically your broadband router would be your DHCP server). The rule you'd need to allow DHCP, would be:

pass on $wifi proto udp from 0.0.0.0 port 68 to 255.255.255.255 port 67

*Note: you may need to substitute 0.0.0.0 for any. The DHCPREQUEST packet your computer first sends, has a source address 0.0.0.0 because at that stage, your computer doesn't have an IP address yet.
To be honest, I would lean more towards using any. Another option is to rip out any source specification, i.e. pass on $wifi proto udp to 255.255.255.255 port 67, but that means we lose the source-port part of the rule, and being as specific as possible is always the most secure option.

Hope that helps. Here are some useful links:

mDNS: http://en.wikipedia.org/wiki/Multicast_DNS#Packet_structure

DHSP: http://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol#DHCP_discovery

Solution 3:

With the aim to create the PF rules in an "easy" way, identifying existing active interfaces including the current (vpn) interfaces this small killswitch program may be used,

Still in progress but could be a good start for identifying external IP, and active interfaces in order to properly create the firewall rules.

example or output using the -i (info) option:

$ killswitch -i
Interface  MAC address         IP
en1        bc:57:36:d1:82:ba   192.168.1.7
ppp0                           10.10.1.3

public IP address: 93.117.82.123

Passing the server ip -ip:

# --------------------------------------------------------------
# Sat, 19 Nov 2016 12:37:24 +0100
# sudo pfctl -Fa -f ~/.killswitch.pf.conf -e
# --------------------------------------------------------------
int_en1 = "en1"
vpn_ppp0 = "ppp0"
vpn_ip = "93.117.82.123"
set block-policy drop
set ruleset-optimization basic
set skip on lo0
block all
pass on $int_en1 proto udp to 224.0.0.251 port 5353
pass on $int_en1 proto udp from any port 67 to any port 68
pass on $int_en1 inet proto icmp all icmp-type 8 code 0
pass on $int_en1 proto {tcp, udp} from any to $vpn_ip
pass on $vpn_ppp0 all

Is far from perfect but work is in progress more info/code can be found here: https://github.com/vpn-kill-switch/killswitch