20
20
21
21
PATH=/sbin:/usr/sbin:$PATH
22
22
23
+
24
+
23
25
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"
26
43
27
44
# Checks whether anchor is present in the system
28
45
# 0 - if anchor is present
29
46
# 1 - if not present
30
47
function get_anchor_present {
31
48
pfctl -sr 2> /dev/null | grep -q " anchor.*${ANCHOR_NAME} "
32
49
}
50
+ function get_anchor_present_nat {
51
+ pfctl -sn 2> /dev/null | grep -q " nat-anchor.*${ANCHOR_NAME} /${SA_ROUTE} "
52
+ }
33
53
34
54
# Add IVPN Firewall anchor after existing pf rules.
35
55
function install_anchor {
36
56
cat \
37
57
<( pfctl -sr 2> /dev/null) \
38
58
<( 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 -
40
66
}
41
67
42
68
# Checks whether IVPN Firewall anchor exists
43
69
# and add it if require
44
70
function add_anchor_if_required {
45
-
46
71
get_anchor_present
47
-
48
72
if (( $? != 0 )) ; then
49
73
install_anchor
50
74
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
51
82
}
52
83
53
84
# Checks if the IVPN Firewall is enabled
@@ -87,22 +118,27 @@ function enable_firewall {
87
118
set -e
88
119
89
120
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
94
124
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
97
127
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
100
135
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
103
139
104
- anchor tunnel all
105
- anchor dns all
140
+ block return out quick all
141
+ block drop quick all
106
142
_EOF
107
143
108
144
local TOKEN=` pfctl -E 2>&1 | grep -i token | sed -e ' s/.*oken.*://' | tr -d ' \n' `
@@ -125,17 +161,30 @@ _EOF
125
161
function disable_firewall {
126
162
127
163
# 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
130
166
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
135
170
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
138
184
185
+ # remove all the rules from anchor
186
+ pfctl -a ${ANCHOR_NAME} -Fr
187
+
139
188
local TOKEN=` echo ' show State:/Network/IVPN/PacketFilter' | scutil | grep Token | sed -e ' s/.*: //' | tr -d ' \n' `
140
189
pfctl -X " ${TOKEN} "
141
190
@@ -145,44 +194,138 @@ function disable_firewall {
145
194
function client_connected {
146
195
147
196
IFACE=$1
148
-
149
- # SRC_ADDR=$2
197
+ SRC_ADDR=$2
150
198
SRC_PORT=$3
151
199
DST_ADDR=$4
152
200
DST_PORT=$5
153
201
PROTOCOL=$6
154
202
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
160
269
_EOF
161
- # pass out proto ${PROTOCOL} from port = ${SRC_PORT} to ${DST_ADDR}
270
+ fi
162
271
}
163
272
164
273
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
166
282
}
167
283
168
284
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
172
300
173
301
if [[ -z " ${DNS} " ]] ; then
174
302
# 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
178
306
_EOF
179
307
return 0
180
308
fi
181
309
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
185
327
_EOF
328
+
186
329
}
187
330
188
331
function main {
@@ -211,22 +354,33 @@ function main {
211
354
elif [[ $1 = " -add_exceptions" ]]; then
212
355
213
356
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
215
362
216
363
elif [[ $1 = " -remove_exceptions" ]]; then
217
364
218
365
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
220
371
221
372
elif [[ $1 = " -set_user_exceptions" ]]; then
222
373
223
374
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
225
380
226
381
elif [[ $1 = " -connected" ]]; then
227
382
228
- IFACE=$2
229
-
383
+ IFACE=$2
230
384
SRC_ADDR=$3
231
385
SRC_PORT=$4
232
386
DST_ADDR=$5
@@ -241,8 +395,12 @@ function main {
241
395
elif [[ $1 = " -set_dns" ]]; then
242
396
243
397
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}
244
403
245
- set_dns $2
246
404
else
247
405
echo " Unknown command"
248
406
return 2
0 commit comments