Skip to content

Commit

Permalink
(macOS) Firewall: Intentional routing of all traffic through the VPN …
Browse files Browse the repository at this point in the history
…interface + REFACTORING!

#394
  • Loading branch information
stenya committed Oct 22, 2024
1 parent f4a2ac8 commit 217c160
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 66 deletions.
256 changes: 207 additions & 49 deletions daemon/References/macOS/etc/firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,65 @@

PATH=/sbin:/usr/sbin:$PATH



ANCHOR_NAME="ivpn_firewall"
EXCEPTIONS_TABLE="ivpn_servers"
USER_EXCEPTIONS_TABLE="ivpn_exceptions"
SA_DNS="dns"
SA_TUNNEL="tunnel"
SA_ROUTE="route_to"

TBL_EXCEPTIONS="ivpn_servers"
TBL_USER_EXCEPTIONS="ivpn_exceptions"

# If IS_DO_ROUTING=1, all traffic will be intentionally routed through the VPN interface:
# Any packets that do not follow the default routing configuration and still use a non-VPN interface
# will be NAT-ed to the VPN interface's IP address and then routed through the VPN.
#
# 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.
# These services ignore the routing configuration and continue using the "en0" interface, bypassing the VPN.
IS_DO_ROUTING=1

TBL_DNS_ADDR_TO_NAT="dns_addr_to_nat"
TBL_DNS_ADDR_TO_NOT_ROUTE="dns_addr_to_not_route"

# Checks whether anchor is present in the system
# 0 - if anchor is present
# 1 - if not present
function get_anchor_present {
pfctl -sr 2> /dev/null | grep -q "anchor.*${ANCHOR_NAME}"
}
function get_anchor_present_nat {
pfctl -sn 2> /dev/null | grep -q "nat-anchor.*${ANCHOR_NAME}/${SA_ROUTE}"
}

# Add IVPN Firewall anchor after existing pf rules.
function install_anchor {
cat \
<(pfctl -sr 2> /dev/null) \
<(echo "anchor ${ANCHOR_NAME} all") \
| pfctl -f -
| pfctl -R -f -
}
function install_anchor_nat {
cat \
<(echo "nat-anchor '${ANCHOR_NAME}/${SA_ROUTE}' all ") \
<(pfctl -sn 2> /dev/null) \
| pfctl -N -f -
}

# Checks whether IVPN Firewall anchor exists
# and add it if require
function add_anchor_if_required {

get_anchor_present

if (( $? != 0 )) ; then
install_anchor
fi

if (( ${IS_DO_ROUTING} == 1 )) ; then
get_anchor_present_nat
if (( $? != 0 )) ; then
install_anchor_nat
fi
fi
}

# Checks if the IVPN Firewall is enabled
Expand Down Expand Up @@ -87,22 +118,27 @@ function enable_firewall {
set -e

pfctl -a ${ANCHOR_NAME} -f - <<_EOF
block drop on ! lo0 all
table <${EXCEPTIONS_TABLE}> persist
table <${USER_EXCEPTIONS_TABLE}> persist
scrub all fragment reassemble
pass quick on lo0 all flags any keep state
pass out quick from any to <${EXCEPTIONS_TABLE}>
pass in quick from <${EXCEPTIONS_TABLE}> to any
table <${TBL_EXCEPTIONS}> persist
table <${TBL_USER_EXCEPTIONS}> persist
pass out quick from any to <${USER_EXCEPTIONS_TABLE}> flags any keep state
pass in quick from <${USER_EXCEPTIONS_TABLE}> to any
pass out quick from any to <${TBL_EXCEPTIONS}> flags S/SA keep state
pass in quick from <${TBL_EXCEPTIONS}> to any flags S/SA keep state
pass out quick from any to <${TBL_USER_EXCEPTIONS}> flags any keep state
pass in quick from <${TBL_USER_EXCEPTIONS}> to any flags any keep state
pass out quick inet proto udp from any port = 68 to 255.255.255.255 port = 67 no state
pass in quick inet proto udp from any port = 67 to any port = 68 no state
pass out inet proto udp from 0.0.0.0 to 255.255.255.255 port = 67
pass in proto udp from any to any port = 68
anchor ${SA_DNS} all # IMPORTANT to block unwanted DNS requests before they are routed to the VPN
anchor ${SA_TUNNEL} all # Allowing traffic to VPN interface and VPN server
anchor ${SA_ROUTE} all # Intentionally ROUTE all the rest traffic through VPN interface
anchor tunnel all
anchor dns all
block return out quick all
block drop quick all
_EOF

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

# remove all entries in exceptions table
pfctl -a ${ANCHOR_NAME} -t ${EXCEPTIONS_TABLE} -T flush
pfctl -a ${ANCHOR_NAME} -t ${USER_EXCEPTIONS_TABLE} -T flush
pfctl -a ${ANCHOR_NAME} -t ${TBL_EXCEPTIONS} -T flush
pfctl -a ${ANCHOR_NAME} -t ${TBL_USER_EXCEPTIONS} -T flush

# remove all rules in tun anchor
pfctl -a ${ANCHOR_NAME}/tunnel -Fr
# remove all rules in dns anchor
pfctl -a ${ANCHOR_NAME}/dns -Fr
if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_EXCEPTIONS} -T flush
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_USER_EXCEPTIONS} -T flush

# remove all the rules in anchor
pfctl -a ${ANCHOR_NAME} -Fr
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush

# remove all rules from SA_ROUTE anchor
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fn # remove NAT rules
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fr # remove filter rules
fi

# remove all rules from SA_TUNNEL anchor
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -Fr

# remove all rules from SA_DNS anchor
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -Fr

# remove all the rules from anchor
pfctl -a ${ANCHOR_NAME} -Fr

local TOKEN=`echo 'show State:/Network/IVPN/PacketFilter' | scutil | grep Token | sed -e 's/.*: //' | tr -d ' \n'`
pfctl -X "${TOKEN}"

Expand All @@ -145,44 +194,138 @@ function disable_firewall {
function client_connected {

IFACE=$1

#SRC_ADDR=$2
SRC_ADDR=$2
SRC_PORT=$3
DST_ADDR=$4
DST_PORT=$5
PROTOCOL=$6

# echo "CONNECTED IFACE=${IFACE} SRC_ADDR=${SRC_ADDR} SRC_PORT=${SRC_PORT} DST_ADDR=${DST_ADDR} DST_PORT=${DST_PORT} PROTOCOL=${PROTOCOL}"
pfctl -a ${ANCHOR_NAME}/tunnel -f - <<_EOF
pass out on ${IFACE} from any to any
pass in on ${IFACE} from any to any
pass out quick proto ${PROTOCOL} from any to ${DST_ADDR} port = ${DST_PORT}
# FILTER RULES (TUNNEL)
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -f - <<_EOF
pass quick on ${IFACE} all flags S/SA keep state # Pass all traffic on VPN interface
pass out quick proto ${PROTOCOL} from any to ${DST_ADDR} port = ${DST_PORT} keep state # Pass all traffic to VPN server
_EOF

if (( ${IS_DO_ROUTING} == 1 )) ; then
# NAT & ROUTING RULES
# All traffic will be intentionally routed through the VPN interface:
# Any packets that do not follow the default routing configuration and still use a non-VPN interface
# will be NAT-ed to the VPN interface's IP address and then routed through the VPN.
#
# 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.
# These services ignore the routing configuration and continue using the "en0" interface, bypassing the VPN.
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -f - <<_EOF
table <${TBL_DNS_ADDR_TO_NAT}> persist # Table to store DNS addresses that need to be NAT-ed
table <${TBL_DNS_ADDR_TO_NOT_ROUTE}> persist # Table to store DNS addresses that should not be routed through VPN interface
table <${TBL_EXCEPTIONS}> persist # Table similar (copy) to table defined in ${ANCHOR_NAME} anchor
table <${TBL_USER_EXCEPTIONS}> persist # Table similar (copy) to table defined in ${ANCHOR_NAME} anchor
#
# === NAT rules ===
# NAT rules are required to change SRC address for all traffic to VPN interface IP.
# First NAT rule wins, so we need to put more specific rules first
# NOTE: NAT rules are processed BEFORE ANY FILTER RULES!
#
# Do not NAT loopback packets
no nat on lo0 all
# NAT: for addresses from table TBL_DNS_ADDR_TO_NAT
# E.g. default VPN DNS server IP is accessible only via VPN interface,
# but ranges for local networks are skipped for NAT-ting (bellow)
nat from any to <${TBL_DNS_ADDR_TO_NAT}> port 53 -> ${SRC_ADDR}
# Do not NAT addresses to/from TBL_USER_EXCEPTIONS tables
no nat from any to <${TBL_EXCEPTIONS}>
no nat from any to <${TBL_USER_EXCEPTIONS}>
no nat from <${TBL_USER_EXCEPTIONS}> to any
# Do not NAT LAN addresses
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 }
no nat from any to { fe80::/10, fc00::/7, ff01::/16, ff02::/16, ff03::/16, ff04::/16, ff05::/16, ff08::/16 }
# Do not NAT packets to remote server
no nat inet from any to ${DST_ADDR}
# Do not NAT packets on VPN innterface
no nat on ${IFACE} all
# NAT: Change SRC address for all traffic to IP of VPN interface
nat inet all -> ${SRC_ADDR}
#
# === FILTER rules ===
#
# Pass & do not route DNS traffic to IPs from TBL_DNS_ADDR_TO_NOT_ROUTE table
# (it is necessary if the custom DNS server is on the local network and not accessible through the VPN interface)
pass out quick proto udp from any to <${TBL_DNS_ADDR_TO_NOT_ROUTE}> port 53 keep state
pass out quick proto tcp from any to <${TBL_DNS_ADDR_TO_NOT_ROUTE}> port 53 flags S/SA keep state
# Route all traffic through VPN interface
pass out quick route-to ${IFACE} inet all flags S/SA keep state
pass out quick route-to ${IFACE} inet6 all flags S/SA keep state
_EOF
# pass out proto ${PROTOCOL} from port = ${SRC_PORT} to ${DST_ADDR}
fi
}

function client_disconnected {
pfctl -a ${ANCHOR_NAME}/tunnel -Fr
pfctl -a ${ANCHOR_NAME}/${SA_TUNNEL} -Fr

if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fn
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -Fr
fi
}

function set_dns {
DNS=$1
# remove all rules in dns anchor
pfctl -a ${ANCHOR_NAME}/dns -Fr

# IS_LAN: "true" or "false":
# - if "true" then DNS is custom local non-routable IP (not in VPN network)
# This IP must be skipped from NAT-ing and routing through VPN interface
# - if "false" then DNS must be routed through VPN interface
IS_LAN=$1
DNS=$2

# remove all rules in ${SA_DNS} anchor
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -Fr

if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NAT} -T flush
pfctl -a ${ANCHOR_NAME}/${SA_ROUTE} -t ${TBL_DNS_ADDR_TO_NOT_ROUTE} -T flush
fi

if [[ -z "${DNS}" ]] ; then
# DNS not defined. Block all connections to port 53
pfctl -a ${ANCHOR_NAME}/dns -f - <<_EOF
block drop out proto udp from any to port = 53
block drop out proto tcp from any to port = 53
pfctl -a ${ANCHOR_NAME}/${SA_DNS} -f - <<_EOF
block return out quick proto udp from any to port = 53
block return out quick proto tcp from any to port = 53
_EOF
return 0
fi

pfctl -a ${ANCHOR_NAME}/dns -f - <<_EOF
block drop out proto udp from any to ! ${DNS} port = 53
block drop out proto tcp from any to ! ${DNS} port = 53
if (( ${IS_DO_ROUTING} == 1 )) ; then
if [[ "${IS_LAN}" = "true" ]] ; then
# Add DNS server to the table of addresses that should not be routed through VPN interface
# It also will be skipped from NAT-ing (as it must belongs to LAN (not routable IP address))
# (it is necessary if the custom DNS server is on the local network and not accessible through the VPN interface)
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_DNS_ADDR_TO_NOT_ROUTE}" -T replace ${DNS}
else
# Add DNS server to the table of addresses that need to be NAT-ed (and routed through VPN interface)
# It is necessary if DNS server is accessible only via VPN interface
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_DNS_ADDR_TO_NAT}" -T replace ${DNS}
fi
fi

# Block all DNS requests except to the specified DNS server
pfctl -a "${ANCHOR_NAME}/${SA_DNS}" -f - <<_EOF
block return out quick proto udp from any to ! ${DNS} port = 53
block return out quick proto tcp from any to ! ${DNS} port = 53
_EOF

}

function main {
Expand Down Expand Up @@ -211,22 +354,33 @@ function main {
elif [[ $1 = "-add_exceptions" ]]; then

shift
pfctl -a "${ANCHOR_NAME}" -t "${EXCEPTIONS_TABLE}" -T add $@
pfctl -a "${ANCHOR_NAME}" -t "${TBL_EXCEPTIONS}" -T add $@

if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_EXCEPTIONS}" -T add $@
fi

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

shift
pfctl -a "${ANCHOR_NAME}" -t "${EXCEPTIONS_TABLE}" -T delete $@
pfctl -a "${ANCHOR_NAME}" -t "${TBL_EXCEPTIONS}" -T delete $@

if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_EXCEPTIONS}" -T delete $@
fi

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

shift
pfctl -a "${ANCHOR_NAME}" -t "${USER_EXCEPTIONS_TABLE}" -T replace $@
pfctl -a "${ANCHOR_NAME}" -t "${TBL_USER_EXCEPTIONS}" -T replace $@

if (( ${IS_DO_ROUTING} == 1 )) ; then
pfctl -a "${ANCHOR_NAME}/${SA_ROUTE}" -t "${TBL_USER_EXCEPTIONS}" -T replace $@
fi

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

IFACE=$2

IFACE=$2
SRC_ADDR=$3
SRC_PORT=$4
DST_ADDR=$5
Expand All @@ -241,8 +395,12 @@ function main {
elif [[ $1 = "-set_dns" ]]; then

get_firewall_enabled || return 0

IS_LAN=$2 # "true" or "false"; if true, then DNS is custom local non-routable IP (not in VPN network)
IP=$3

set_dns ${IS_LAN} ${IP}

set_dns $2
else
echo "Unknown command"
return 2
Expand Down
14 changes: 12 additions & 2 deletions daemon/service/dns/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,28 @@ const (
EncryptionDnsOverHttps DnsEncryption = 2
)

type DnsMetadata struct {
IsInternalDnsServer bool // FALSE if DNS settings are custom (defined by user)
}

type DnsSettings struct {
DnsHost string // DNS host IP address
Encryption DnsEncryption
DohTemplate string // DoH/DoT template URI (for Encryption = DnsOverHttps or Encryption = DnsOverTls)

metadata DnsMetadata
}

func (d DnsSettings) Metadata() DnsMetadata {
return d.metadata
}

// create DnsSettings object with no encryption
// Create DnsSettings object with no encryption
func DnsSettingsCreate(ip net.IP) DnsSettings {
if ip == nil {
return DnsSettings{}
}
return DnsSettings{DnsHost: ip.String()}
return DnsSettings{DnsHost: ip.String(), metadata: DnsMetadata{IsInternalDnsServer: true}}
}

func (d DnsSettings) Equal(x DnsSettings) bool {
Expand Down
Loading

0 comments on commit 217c160

Please sign in to comment.