DPI defeat for IPsec
Posted: Wed Jun 12, 2024 7:18 pm
Last week I've found that ISP I use on one of my sites blocks IPsec connections from the said site to one of my other sites - IPsec connections starts, then stalls. Monitoring the connection from both sides suggests that ISP starts to drop the packets as soon as the connections is identified as IPsec, other connections are not blocked, and port numbers have nothing to do with blocking. Wireguard is also blocked, and I haven't bothered to try OVPN because I'm sure it's blocked too. SSTP surely works, but it lacks HW offload on the hardware used, so I've decided to dig deeper.
After some research and proof-of-concept manual DPI defeat with traffic generator I've developed a script that automatically defeats the kind of DPI used by this ISP (and I'm quite sure some others):
Feel free to use, modify and distribute.
Comment and suggestions are welcome.
After some research and proof-of-concept manual DPI defeat with traffic generator I've developed a script that automatically defeats the kind of DPI used by this ISP (and I'm quite sure some others):
Code: Select all
# DPI defeat for IPsec ver. 0.1 by 611
#
# It's been found that some active DPI systems are behaving much like ROS firewall's L7 protocol matcher - they track the
# connections and analyze initial data (data size and/or number of packets may vary) to determine if the connection matches the
# criteria (if the connection is some form of VPN, etc.), connections matched could be throttled or blocked depending on ISP's
# intent. This peculiarity allows to defeat these DPIs by sending some dummy traffic (that won't match with the patterns DPI
# looks for) at the start of connection. The dummy traffic could have a lower TTL so it won't reach the server.
#
# This method ONLY works in the said circumstances, it WON'T WORK if ISP matches IP address, destination port, etc.
#
# The script implements such DPI defeat for IPsec connections. Mark the IPsec peers to be processed with "DPI-D" in their
# comment. The script will work with several IPsec peers requiring DPI defeat, but isn't designed to be reenterable.
# You'll normally want to start the script with scheduler on regular intervals (the interval should be much longer than the time
# required to establish IPsec connection):
# /system scheduler add name=sched-IPsec-DPI-defeat on-event="/system script run scr-IPsec-DPI-defeat" policy=read,write,test,sniff interval=30s
#
# The script will check if IPsec connection was successful, and if it's not it will create or modify traffic generator packet
# template named "tgpt-IPsec-DPI-defeat", use it to send specified number of packets (of specified size and TTL, with specified
# tempo) with dummy data (incrementing bytes, modify this if it doesn't work for you) towards the peer from a random UDP port
# within the specified range, then create or modify IP firewall NAT rule (with comment "DPI-D for <peer name>") so IPsec packets
# towards the peer will come from the port that was used for sending dummy traffic. Existing connection will be purged from
# connection tracker to apply the new NAT rule. The NAT rule placement is important, so you should create an anchor rule:
# /ip firewall nat add chain=srcnat action=passthrough comment="--- DPI-D rules go before this line"
#
# In order to find required dummy traffic parameters: start the script (or let the scheduler start it for you) and check if
# IPsec connection is established and works (ping the other side), if it doesn't work - adjust the parameters (increase packet
# size or count) and restart the script. If the connection works, you want to adjust the dummy traffic parameters down: remove
# the NAT rule, remove existing connection from the tracker, remove active peer (so IPsec will try to establish connection
# without DPI defeat) then restart the script.
#
# The script requires read, write, test and sniff permissions.
#
# Known limitations:
# * Not tested with non-IKE2 IPsec peers
# * No IPv6 support
# * No support for multiple A/AAAA records for peer's FQDN
# * No support for multiple peers on the same IP address (not sure if it's a realistic scenario)
# * Local address used for peer connections is not honored
# * Common dummy packet parameters for all peers (not sure if individual parameters is needed)
#
# Known side effects:
# * Traffic generator is started then stopped
# * Traffic generator packet template named "tgpt-IPsec-DPI-defeat" remains in the configuration
# * IP firewall NAT rules remain in the configuration (and could become orphan if IPsec peer name or address were to change)
#
# Size and number of packets to send, sending tempo
:local packetsize 512
:local packetcount 128
:local packetspersecond 32
:local packetttl 64
# IP fireall NAT anchor rule comment
:local anchornatrulecomment "--- DPI-D rules go before this line"
# Local port range used for connections
:local rndportstart 60000
:local rndportend 60099
# Get an active default gateway for main routing table (may need adjustment in complex routing scenarios)
:local defgateway [ip route get [find dst-address=0.0.0.0/0 routing-table=main active] gateway]
# Loop thru enabled IPsec peers with "DPI-D" in comment
:foreach curpeer in=[/ip ipsec peer find comment~"DPI-D" !disabled] do={
# Get peer name, address and port
:local peername [/ip ipsec peer get $curpeer name]
:local peeraddress [/ip ipsec peer get $curpeer address]
:local peerport [/ip ipsec peer get $curpeer port]
# Set local port depending on IPsec type (TBD: test it with non-IKE2)
:local localport
:if ([/ip ipsec peer get $curpeer exchange-mode] = "ike2") do={ :set $localport 4500 } else={ :set $localport 500 }
# Resolve address if it's a FQDN (TBD: multiple A/AAAA records)
:do { :if ([:typeof $peeraddress] = "str") do={ :set $peeraddress [:resolve $peeraddress] }} on-error={}
# Default port if the port is not specified
:if ([:typeof $peerport] != "num") do={ :set $peerport $localport }
# Proceed if there's no active coonnection with the peer
# Note that it's currently (as of ROS 7.15) impossible to match active peers on local or remote port as both values are named "port"
:if ([:len [/ip ipsec active-peers find state="established" remote-address=$peeraddress]] = 0) do={
# Generate a random port number within range
# Note there's no check if the new port is the same as the old one as the probability is low enough
:local rndport [:rndnum from=$rndportstart to=$rndportend]
:put ($peername . " is not OK, will try from port " . $rndport)
# Set up traffic generator packet template (your header stack may vary depending on interface used,
# you may try to use payload other than incrementing bytes)
# Add or modify the template depending on if it already exists or not
:if ([:len [/tool traffic-generator packet-template find name="tgpt-IPsec-DPI-defeat"]] = 0) do={
# TBD: IPv6, local address from IPsec peer
/tool traffic-generator packet-template add name="tgpt-IPsec-DPI-defeat" header-stack=mac,ip,udp data=incrementing ip-gateway=$defgateway ip-dst=$peeraddress ip-ttl=$packetttl udp-src-port=$rndport udp-dst-port=$peerport
} else={
# Duplicate names are not allowed, so there's no need to check if there's more than one template exists
# IP TTL is not modified as it's not expected to change
/tool traffic-generator packet-template set [find name="tgpt-IPsec-DPI-defeat"] ip-gateway=$defgateway ip-dst=$peeraddress udp-src-port=$rndport udp-dst-port=$peerport
}
# Send it! (yep, there's a "packet-count" parameter available in command line)
/tool traffic-generator start tx-template="tgpt-IPsec-DPI-defeat" packet-size=$packetsize pps=$packetspersecond packet-count=$packetcount
# Wait for completion + 1 second, stop traffic generator
:delay ($packetcount / $packetspersecond + 1s)
/tool traffic-generator stop
# Set up firewall NAT rule so IPsec will use the same port as traffic generator (TBD: IPv6, local address from IPsec peer)
# Add or modify the NAT entry depending on if it already exists or not (we assume neither local nor remote port changed from the previous try)
# Note that protocol, ports and addresses must be enclosed in quotation marks for /ip firewall nat find to work properly
:if ([:len [/ip firewall nat find chain=srcnat action=src-nat protocol="udp" dst-address="$peeraddress" comment="DPI-D for $peername"]] = 0) do={
/ip firewall nat add place-before=[find comment="$anchornatrulecomment"] chain=srcnat action=src-nat protocol=udp src-port=$localport dst-address=$peeraddress dst-port=$peerport to-ports=$rndport comment="DPI-D for $peername"
} else={
# Set command will modify all matching rules, so there's no need to check if there's more than one rule exists
/ip firewall nat set [find chain=srcnat action=src-nat protocol="udp" dst-address="$peeraddress" comment="DPI-D for $peername"] to-ports=$rndport
}
# Remove old connection from connection tracker to apply new rule (TBD: IPv6, local address from IPsec peer)
/ip firewall connection remove [find dst-address="$peeraddress:$peerport"]
# No cleanup:
# * traffic generator packet template is not removed to avoid unnecessary log entries - we'll need it for next one anyways
# * IP firewall NAT rule must stay for IPsec connection to function (in case connection is removed from connection tracker)
} else={ :put ($peername . " is OK, skipping") }
}
# Checkmark
:put ("Done.");
Comment and suggestions are welcome.