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.