Skip to content

Commit 217c160

Browse files
committed
(macOS) Firewall: Intentional routing of all traffic through the VPN interface + REFACTORING!
#394
1 parent f4a2ac8 commit 217c160

File tree

7 files changed

+239
-66
lines changed

7 files changed

+239
-66
lines changed

daemon/References/macOS/etc/firewall.sh

+207-49
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,65 @@
2020

2121
PATH=/sbin:/usr/sbin:$PATH
2222

23+
24+
2325
ANCHOR_NAME="ivpn_firewall"
24-
EXCEPTIONS_TABLE="ivpn_servers"
25-
USER_EXCEPTIONS_TABLE="ivpn_exceptions"
26+
SA_DNS="dns"
27+
SA_TUNNEL="tunnel"
28+
SA_ROUTE="route_to"
29+
30+
TBL_EXCEPTIONS="ivpn_servers"
31+
TBL_USER_EXCEPTIONS="ivpn_exceptions"
32+
33+
# If IS_DO_ROUTING=1, all traffic will be intentionally routed through the VPN interface:
34+
# Any packets that do not follow the default routing configuration and still use a non-VPN interface
35+
# will be NAT-ed to the VPN interface's IP address and then routed through the VPN.
36+
#
37+
# This helps resolve issues like those in macOS 15.0, where certain apps (such as iMessage and FaceTime) stop working when the VPN is connected.
38+
# These services ignore the routing configuration and continue using the "en0" interface, bypassing the VPN.
39+
IS_DO_ROUTING=1
40+
41+
TBL_DNS_ADDR_TO_NAT="dns_addr_to_nat"
42+
TBL_DNS_ADDR_TO_NOT_ROUTE="dns_addr_to_not_route"
2643

2744
# Checks whether anchor is present in the system
2845
# 0 - if anchor is present
2946
# 1 - if not present
3047
function get_anchor_present {
3148
pfctl -sr 2> /dev/null | grep -q "anchor.*${ANCHOR_NAME}"
3249
}
50+
function get_anchor_present_nat {
51+
pfctl -sn 2> /dev/null | grep -q "nat-anchor.*${ANCHOR_NAME}/${SA_ROUTE}"
52+
}
3353

3454
# Add IVPN Firewall anchor after existing pf rules.
3555
function install_anchor {
3656
cat \
3757
<(pfctl -sr 2> /dev/null) \
3858
<(echo "anchor ${ANCHOR_NAME} all") \
39-
| pfctl -f -
59+
| pfctl -R -f -
60+
}
61+
function install_anchor_nat {
62+
cat \
63+
<(echo "nat-anchor '${ANCHOR_NAME}/${SA_ROUTE}' all ") \
64+
<(pfctl -sn 2> /dev/null) \
65+
| pfctl -N -f -
4066
}
4167

4268
# Checks whether IVPN Firewall anchor exists
4369
# and add it if require
4470
function add_anchor_if_required {
45-
4671
get_anchor_present
47-
4872
if (( $? != 0 )) ; then
4973
install_anchor
5074
fi
75+
76+
if (( ${IS_DO_ROUTING} == 1 )) ; then
77+
get_anchor_present_nat
78+
if (( $? != 0 )) ; then
79+
install_anchor_nat
80+
fi
81+
fi
5182
}
5283

5384
# Checks if the IVPN Firewall is enabled
@@ -87,22 +118,27 @@ function enable_firewall {
87118
set -e
88119

89120
pfctl -a ${ANCHOR_NAME} -f - <<_EOF
90-
block drop on ! lo0 all
91-
92-
table <${EXCEPTIONS_TABLE}> persist
93-
table <${USER_EXCEPTIONS_TABLE}> persist
121+
scrub all fragment reassemble
122+
123+
pass quick on lo0 all flags any keep state
94124
95-
pass out quick from any to <${EXCEPTIONS_TABLE}>
96-
pass in quick from <${EXCEPTIONS_TABLE}> to any
125+
table <${TBL_EXCEPTIONS}> persist
126+
table <${TBL_USER_EXCEPTIONS}> persist
97127
98-
pass out quick from any to <${USER_EXCEPTIONS_TABLE}> flags any keep state
99-
pass in quick from <${USER_EXCEPTIONS_TABLE}> to any
128+
pass out quick from any to <${TBL_EXCEPTIONS}> flags S/SA keep state
129+
pass in quick from <${TBL_EXCEPTIONS}> to any flags S/SA keep state
130+
pass out quick from any to <${TBL_USER_EXCEPTIONS}> flags any keep state
131+
pass in quick from <${TBL_USER_EXCEPTIONS}> to any flags any keep state
132+
133+
pass out quick inet proto udp from any port = 68 to 255.255.255.255 port = 67 no state
134+
pass in quick inet proto udp from any port = 67 to any port = 68 no state
100135
101-
pass out inet proto udp from 0.0.0.0 to 255.255.255.255 port = 67
102-
pass in proto udp from any to any port = 68
136+
anchor ${SA_DNS} all # IMPORTANT to block unwanted DNS requests before they are routed to the VPN
137+
anchor ${SA_TUNNEL} all # Allowing traffic to VPN interface and VPN server
138+
anchor ${SA_ROUTE} all # Intentionally ROUTE all the rest traffic through VPN interface
103139
104-
anchor tunnel all
105-
anchor dns all
140+
block return out quick all
141+
block drop quick all
106142
_EOF
107143

108144
local TOKEN=`pfctl -E 2>&1 | grep -i token | sed -e 's/.*oken.*://' | tr -d ' \n'`
@@ -125,17 +161,30 @@ _EOF
125161
function disable_firewall {
126162

127163
# remove all entries in exceptions table
128-
pfctl -a ${ANCHOR_NAME} -t ${EXCEPTIONS_TABLE} -T flush
129-
pfctl -a ${ANCHOR_NAME} -t ${USER_EXCEPTIONS_TABLE} -T flush
164+
pfctl -a ${ANCHOR_NAME} -t ${TBL_EXCEPTIONS} -T flush
165+
pfctl -a ${ANCHOR_NAME} -t ${TBL_USER_EXCEPTIONS} -T flush
130166

131-
# remove all rules in tun anchor
132-
pfctl -a ${ANCHOR_NAME}/tunnel -Fr
133-
# remove all rules in dns anchor
134-
pfctl -a ${ANCHOR_NAME}/dns -Fr
167+
if (( ${IS_DO_ROUTING} == 1 )) ; then
168+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_EXCEPTIONS} -T flush
169+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_USER_EXCEPTIONS} -T flush
135170

136-
# remove all the rules in anchor
137-
pfctl -a ${ANCHOR_NAME} -Fr
171+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
172+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush
173+
174+
# remove all rules from SA_ROUTE anchor
175+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fn # remove NAT rules
176+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fr # remove filter rules
177+
fi
178+
179+
# remove all rules from SA_TUNNEL anchor
180+
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -Fr
181+
182+
# remove all rules from SA_DNS anchor
183+
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -Fr
138184

185+
# remove all the rules from anchor
186+
pfctl -a ${ANCHOR_NAME} -Fr
187+
139188
local TOKEN=`echo 'show State:/Network/IVPN/PacketFilter' | scutil | grep Token | sed -e 's/.*: //' | tr -d ' \n'`
140189
pfctl -X "${TOKEN}"
141190

@@ -145,44 +194,138 @@ function disable_firewall {
145194
function client_connected {
146195

147196
IFACE=$1
148-
149-
#SRC_ADDR=$2
197+
SRC_ADDR=$2
150198
SRC_PORT=$3
151199
DST_ADDR=$4
152200
DST_PORT=$5
153201
PROTOCOL=$6
154202

155-
# echo "CONNECTED IFACE=${IFACE} SRC_ADDR=${SRC_ADDR} SRC_PORT=${SRC_PORT} DST_ADDR=${DST_ADDR} DST_PORT=${DST_PORT} PROTOCOL=${PROTOCOL}"
156-
pfctl -a ${ANCHOR_NAME}/tunnel -f - <<_EOF
157-
pass out on ${IFACE} from any to any
158-
pass in on ${IFACE} from any to any
159-
pass out quick proto ${PROTOCOL} from any to ${DST_ADDR} port = ${DST_PORT}
203+
# FILTER RULES (TUNNEL)
204+
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -f - <<_EOF
205+
pass quick on ${IFACE} all flags S/SA keep state # Pass all traffic on VPN interface
206+
pass out quick proto ${PROTOCOL} from any to ${DST_ADDR} port = ${DST_PORT} keep state # Pass all traffic to VPN server
207+
_EOF
208+
209+
if (( ${IS_DO_ROUTING} == 1 )) ; then
210+
# NAT & ROUTING RULES
211+
# All traffic will be intentionally routed through the VPN interface:
212+
# Any packets that do not follow the default routing configuration and still use a non-VPN interface
213+
# will be NAT-ed to the VPN interface's IP address and then routed through the VPN.
214+
#
215+
# This helps resolve issues like those in macOS 15.0, where certain apps (such as iMessage and FaceTime) stop working when the VPN is connected.
216+
# These services ignore the routing configuration and continue using the "en0" interface, bypassing the VPN.
217+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -f - <<_EOF
218+
table <${TBL_DNS_ADDR_TO_NAT}> persist # Table to store DNS addresses that need to be NAT-ed
219+
table <${TBL_DNS_ADDR_TO_NOT_ROUTE}> persist # Table to store DNS addresses that should not be routed through VPN interface
220+
221+
table <${TBL_EXCEPTIONS}> persist # Table similar (copy) to table defined in ${ANCHOR_NAME} anchor
222+
table <${TBL_USER_EXCEPTIONS}> persist # Table similar (copy) to table defined in ${ANCHOR_NAME} anchor
223+
224+
#
225+
# === NAT rules ===
226+
# NAT rules are required to change SRC address for all traffic to VPN interface IP.
227+
# First NAT rule wins, so we need to put more specific rules first
228+
# NOTE: NAT rules are processed BEFORE ANY FILTER RULES!
229+
#
230+
231+
# Do not NAT loopback packets
232+
no nat on lo0 all
233+
234+
# NAT: for addresses from table TBL_DNS_ADDR_TO_NAT
235+
# E.g. default VPN DNS server IP is accessible only via VPN interface,
236+
# but ranges for local networks are skipped for NAT-ting (bellow)
237+
nat from any to <${TBL_DNS_ADDR_TO_NAT}> port 53 -> ${SRC_ADDR}
238+
239+
# Do not NAT addresses to/from TBL_USER_EXCEPTIONS tables
240+
no nat from any to <${TBL_EXCEPTIONS}>
241+
no nat from any to <${TBL_USER_EXCEPTIONS}>
242+
no nat from <${TBL_USER_EXCEPTIONS}> to any
243+
244+
# Do not NAT LAN addresses
245+
no nat from any to { 172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 169.254.0.0/16, 255.255.255.255, 224.0.0.0/24, 239.0.0.0/8 }
246+
no nat from any to { fe80::/10, fc00::/7, ff01::/16, ff02::/16, ff03::/16, ff04::/16, ff05::/16, ff08::/16 }
247+
248+
# Do not NAT packets to remote server
249+
no nat inet from any to ${DST_ADDR}
250+
251+
# Do not NAT packets on VPN innterface
252+
no nat on ${IFACE} all
253+
254+
# NAT: Change SRC address for all traffic to IP of VPN interface
255+
nat inet all -> ${SRC_ADDR}
256+
257+
#
258+
# === FILTER rules ===
259+
#
260+
261+
# Pass & do not route DNS traffic to IPs from TBL_DNS_ADDR_TO_NOT_ROUTE table
262+
# (it is necessary if the custom DNS server is on the local network and not accessible through the VPN interface)
263+
pass out quick proto udp from any to <${TBL_DNS_ADDR_TO_NOT_ROUTE}> port 53 keep state
264+
pass out quick proto tcp from any to <${TBL_DNS_ADDR_TO_NOT_ROUTE}> port 53 flags S/SA keep state
265+
266+
# Route all traffic through VPN interface
267+
pass out quick route-to ${IFACE} inet all flags S/SA keep state
268+
pass out quick route-to ${IFACE} inet6 all flags S/SA keep state
160269
_EOF
161-
# pass out proto ${PROTOCOL} from port = ${SRC_PORT} to ${DST_ADDR}
270+
fi
162271
}
163272

164273
function client_disconnected {
165-
pfctl -a ${ANCHOR_NAME}/tunnel -Fr
274+
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -Fr
275+
276+
if (( ${IS_DO_ROUTING} == 1 )) ; then
277+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
278+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush
279+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fn
280+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fr
281+
fi
166282
}
167283

168284
function set_dns {
169-
DNS=$1
170-
# remove all rules in dns anchor
171-
pfctl -a ${ANCHOR_NAME}/dns -Fr
285+
286+
# IS_LAN: "true" or "false":
287+
# - if "true" then DNS is custom local non-routable IP (not in VPN network)
288+
# This IP must be skipped from NAT-ing and routing through VPN interface
289+
# - if "false" then DNS must be routed through VPN interface
290+
IS_LAN=$1
291+
DNS=$2
292+
293+
# remove all rules in ${SA_DNS} anchor
294+
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -Fr
295+
296+
if (( ${IS_DO_ROUTING} == 1 )) ; then
297+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
298+
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush
299+
fi
172300

173301
if [[ -z "${DNS}" ]] ; then
174302
# DNS not defined. Block all connections to port 53
175-
pfctl -a ${ANCHOR_NAME}/dns -f - <<_EOF
176-
block drop out proto udp from any to port = 53
177-
block drop out proto tcp from any to port = 53
303+
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -f - <<_EOF
304+
block return out quick proto udp from any to port = 53
305+
block return out quick proto tcp from any to port = 53
178306
_EOF
179307
return 0
180308
fi
181309

182-
pfctl -a ${ANCHOR_NAME}/dns -f - <<_EOF
183-
block drop out proto udp from any to ! ${DNS} port = 53
184-
block drop out proto tcp from any to ! ${DNS} port = 53
310+
if (( ${IS_DO_ROUTING} == 1 )) ; then
311+
if [[ "${IS_LAN}" = "true" ]] ; then
312+
# Add DNS server to the table of addresses that should not be routed through VPN interface
313+
# It also will be skipped from NAT-ing (as it must belongs to LAN (not routable IP address))
314+
# (it is necessary if the custom DNS server is on the local network and not accessible through the VPN interface)
315+
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_DNS_ADDR_TO_NOT_ROUTE}" -T replace ${DNS}
316+
else
317+
# Add DNS server to the table of addresses that need to be NAT-ed (and routed through VPN interface)
318+
# It is necessary if DNS server is accessible only via VPN interface
319+
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_DNS_ADDR_TO_NAT}" -T replace ${DNS}
320+
fi
321+
fi
322+
323+
# Block all DNS requests except to the specified DNS server
324+
pfctl -a "${ANCHOR_NAME}/${SA_DNS}" -f - <<_EOF
325+
block return out quick proto udp from any to ! ${DNS} port = 53
326+
block return out quick proto tcp from any to ! ${DNS} port = 53
185327
_EOF
328+
186329
}
187330

188331
function main {
@@ -211,22 +354,33 @@ function main {
211354
elif [[ $1 = "-add_exceptions" ]]; then
212355

213356
shift
214-
pfctl -a "${ANCHOR_NAME}" -t "${EXCEPTIONS_TABLE}" -T add $@
357+
pfctl -a "${ANCHOR_NAME}" -t "${TBL_EXCEPTIONS}" -T add $@
358+
359+
if (( ${IS_DO_ROUTING} == 1 )) ; then
360+
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_EXCEPTIONS}" -T add $@
361+
fi
215362

216363
elif [[ $1 = "-remove_exceptions" ]]; then
217364

218365
shift
219-
pfctl -a "${ANCHOR_NAME}" -t "${EXCEPTIONS_TABLE}" -T delete $@
366+
pfctl -a "${ANCHOR_NAME}" -t "${TBL_EXCEPTIONS}" -T delete $@
367+
368+
if (( ${IS_DO_ROUTING} == 1 )) ; then
369+
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_EXCEPTIONS}" -T delete $@
370+
fi
220371

221372
elif [[ $1 = "-set_user_exceptions" ]]; then
222373

223374
shift
224-
pfctl -a "${ANCHOR_NAME}" -t "${USER_EXCEPTIONS_TABLE}" -T replace $@
375+
pfctl -a "${ANCHOR_NAME}" -t "${TBL_USER_EXCEPTIONS}" -T replace $@
376+
377+
if (( ${IS_DO_ROUTING} == 1 )) ; then
378+
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_USER_EXCEPTIONS}" -T replace $@
379+
fi
225380

226381
elif [[ $1 = "-connected" ]]; then
227382

228-
IFACE=$2
229-
383+
IFACE=$2
230384
SRC_ADDR=$3
231385
SRC_PORT=$4
232386
DST_ADDR=$5
@@ -241,8 +395,12 @@ function main {
241395
elif [[ $1 = "-set_dns" ]]; then
242396

243397
get_firewall_enabled || return 0
398+
399+
IS_LAN=$2 # "true" or "false"; if true, then DNS is custom local non-routable IP (not in VPN network)
400+
IP=$3
401+
402+
set_dns ${IS_LAN} ${IP}
244403

245-
set_dns $2
246404
else
247405
echo "Unknown command"
248406
return 2

daemon/service/dns/dns.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,28 @@ const (
9090
EncryptionDnsOverHttps DnsEncryption = 2
9191
)
9292

93+
type DnsMetadata struct {
94+
IsInternalDnsServer bool // FALSE if DNS settings are custom (defined by user)
95+
}
96+
9397
type DnsSettings struct {
9498
DnsHost string // DNS host IP address
9599
Encryption DnsEncryption
96100
DohTemplate string // DoH/DoT template URI (for Encryption = DnsOverHttps or Encryption = DnsOverTls)
101+
102+
metadata DnsMetadata
103+
}
104+
105+
func (d DnsSettings) Metadata() DnsMetadata {
106+
return d.metadata
97107
}
98108

99-
// create DnsSettings object with no encryption
109+
// Create DnsSettings object with no encryption
100110
func DnsSettingsCreate(ip net.IP) DnsSettings {
101111
if ip == nil {
102112
return DnsSettings{}
103113
}
104-
return DnsSettings{DnsHost: ip.String()}
114+
return DnsSettings{DnsHost: ip.String(), metadata: DnsMetadata{IsInternalDnsServer: true}}
105115
}
106116

107117
func (d DnsSettings) Equal(x DnsSettings) bool {

0 commit comments

Comments
 (0)