Skip to content

Instantly share code, notes, and snippets.

@W-Floyd
Last active February 17, 2025 15:57
Show Gist options
  • Save W-Floyd/0699abed694a5166c4b235d7d5eb5351 to your computer and use it in GitHub Desktop.
Save W-Floyd/0699abed694a5166c4b235d7d5eb5351 to your computer and use it in GitHub Desktop.
Find the largest usable MTU and output OpenVPN lines required, useful if your ISP/router blocks ICMP messages
#!/bin/bash
PS4='\033[0;33m+(${BASH_SOURCE}:${LINENO}):\033[0m ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -e
# set -xE -o functrace
# Internal parameters
__mtu_largest_success=0
__mtu_smallest_fail=0
__ping_mtu_byte_offset=-24
__mode=""
# Defaults
__mtu=""
__mtu_growth_factor="2"
__timeout="1.0"
__default_mode="ping"
__vpn_curl_address=""
__network_interface=""
__ping_address="google.com"
__max_retries='2'
__help_string="${0} - A script to find an appropriate MTU size by experimentation
-h | --help - Program usage
-s | --mtu-start <n> - Starting MTU for testing (integer, default derived from interface)
-g | --mtu-growth-factor <n> - MTU growth factor (integer, default: '${__mtu_growth_factor}')
-t | --timeout <t> - Operation timeout (float seconds, default: '${__timeout}')
-i | --interface <interface> - Network interface to use (default: default derived from mode)
-r | --retries <n> - Maximum number of retries on inconsistency (default: '${__max_retries}')
-m | --mode vpn|ping - Mode to test with (default: '${__default_mode}')
If unset, will use the default, or infer it from the mode options used
vpn - Modifies live VPN MTU and tests with curl
ping - Tests maximum ping size to find MTU
VPN Options:
-c | --curl-address <address> - Address to test using curl (default: '${__vpn_curl_address}')
Ping Options:
-p | --ping-address <address> - Address to test using ping (default: '${__ping_address}')"
# Functions
function __set_mode() {
if (! [[ -z "${__mode}" ]]) || [[ -z "${1}" ]]; then
echo "Please specify only one operating mode"
return 1
else
__mode="${1}"
fi
}
function __require_mode() {
if [[ -z "${__mode}" ]]; then
__set_mode "${1}" || return 1
fi
if [[ "${__mode}" != "${1}" ]]; then
echo "Option '${2}' must be used with operating mode ${1}"
return 1
fi
}
function __has() {
shift
if [[ "${#}" -lt "${1}" ]]; then
echo "Not enough arguments provided"
return 1
fi
}
# Parse Options
while [[ "${#}" -gt 0 ]]; do
case "${1}" in
"-h" | "--help")
echo "${__help_string}"
exit
;;
"--mtu-start" | "-s")
__has 2 ${@} || exit 1
__mtu="${2}"
shift 2
;;
"--mtu-growth-factor" | "-g")
__has 2 ${@} || exit 1
__mtu_growth_factor="${2}"
shift 2
;;
"--timeout" | "-t")
__has 2 ${@} || exit 1
__timeout="${2}"
shift 2
;;
"-i" | "--interface")
__has 2 ${@} || exit 1
__network_interface="${2}"
shift 2
;;
"-r" | "--retries")
__has 2 ${@} || exit 1
__max_retries="${2}"
shift 2
;;
"--mode" | "-m")
__has 2 ${@} || exit 1
__set_mode "${2}" || exit 1
shift 2
;;
"-c" | "--curl-address")
__has 2 ${@} || exit 1
__require_mode 'vpn' "${1}" || exit 1
__vpn_curl_address="${2}"
shift 2
;;
"-p" | "--ping-address")
__has 2 ${@} || exit 1
__require_mode 'ping' "${1}" || exit 1
__ping_address="${2}"
shift 2
;;
*)
echo "Unknown option ${1}"
exit 1
;;
esac
done
if [[ -z "${__mode}" ]]; then
__mode="${__default_mode}"
fi
function __test_mtu_ping() {
ping -4 "${__ping_address}" -c 1 -W "${__timeout}" -s "$((${1} - ${__ping_mtu_byte_offset}))" -I "${__network_interface}" -q &>/dev/null || return 1
}
function __test_mtu_vpn() {
sudo ip link set dev "${__network_interface}" mtu "${1}"
sleep 0.2 # This can help prevent random failures
curl \
"${__vpn_curl_address}" \
-s \
-o /dev/null \
-m "${__timeout}" &&
return 0 ||
return 1
}
function __test() {
__test_mtu="${__mtu}"
if ! [ -z "${1}" ]; then
__test_mtu="${1}"
fi
__test_retry_count='0'
while true; do
case "${__mode}" in
"vpn")
__test_mtu_vpn "${__test_mtu}" && return 0 || __test_retry_count="$((${__test_retry_count} + 1))"
;;
"ping")
__test_mtu_ping "${__test_mtu}" && return 0 || __test_retry_count="$((${__test_retry_count} + 1))"
;;
esac
if [[ "${__test_retry_count}" -gt 2 ]]; then
return 1
fi
done
}
case "${__mode}" in
"vpn")
if [[ -z "${__network_interface}" ]]; then
__network_interface='tun0'
fi
ip addr show "${__network_interface}" &>/dev/null ||
(
echo "Interface '${__network_interface}' does not exist"
exit 1
)
if [[ -z "${__vpn_curl_address}" ]]; then
echo 'VPN curl address empty'
exit 1
fi
echo 'Need root access, testing...'
sudo echo 'Done'
;;
"ping")
if [[ -z "${__network_interface}" ]]; then
__ip=$(getent ahosts "${__ping_address}" | awk '{print $1; exit}')
__network_interface="$(ip route get "${__ip}" | grep -Po '(?<=(dev ))(\S+)')"
fi
echo '⚠️ WARNING: Ping may result in incorrect detected MTU depending on what host is specified'
echo "Pinging: ${__ping_address}"
;;
*)
echo "Unknown mode '${__mode}'"
exit 1
;;
esac
__mtu_detected='0'
echo -n "Detecting mtu from '${__network_interface}'... "
__mtu_detected="$(ip link show "${__network_interface}" | grep -Eo 'mtu [0-9]+' | sed -e 's/.* //')"
echo "${__mtu_detected}"
if [[ -z "${__mtu}" ]]; then
__mtu="${__mtu_detected}"
fi
__retry_count=0
while true; do
if [[ "${__retry_count}" -gt "${__max_retries}" ]]; then
echo "Exceeded maximum retries of '${__max_retries}', giving up"
exit 1
fi
if [[ "${__last_mtu}" == "${__mtu}" ]]; then
echo "Repeated MTU"
exit 1
fi
__last_mtu="${__mtu}"
echo -n "Testing MTU: ${__mtu}... "
if __test "${__mtu}"; then
echo "✔️"
__mtu_largest_success="${__mtu}"
if [[ "${__mtu_smallest_fail}" == 0 ]]; then
if [[ "${__mtu}" == "${__mtu_detected}" ]]; then
__mtu="$((${__mtu} + 1))"
else
__mtu="$((${__mtu_largest_success} * ${__mtu_growth_factor}))"
fi
continue
fi
else
echo "❌"
__mtu_smallest_fail="${__mtu}"
if [[ "${__mode}" == 'ping' ]] && ! [[ "${__mtu}" -gt 28 ]]; then
echo "Ping to '${__ping_address}' failed at <=28 bytes, please change interfaces or specify a host that is pingable"
exit 1
fi
fi
__mtu="$((${__mtu_largest_success} + (${__mtu_smallest_fail} - ${__mtu_largest_success}) / 2))"
if [[ "$((${__mtu_smallest_fail} - ${__mtu_largest_success}))" == 1 ]]; then
echo -n "Confirming results... "
if (! __test "${__mtu_smallest_fail}") && (__test "${__mtu_largest_success}"); then
echo "✔️"
echo "Largest working MTU: ${__mtu}"
echo
break
else
echo "❌"
echo "Inconsistent results, retesting!"
echo 'Resetting upper bound'
__mtu_smallest_fail=0
__last_mtu=0
__retry_count="$((${__retry_count} + 1))"
fi
fi
done
if [[ "${__mtu_detected}" == "${__mtu}" ]]; then
echo -e "Interface '${__network_interface}' is already using the maximum MTU: ${__mtu}"
else
echo -e "Add the following to your .ovpn file:\npull-filter ignore \"tun-mtu\"\ntun-mtu ${__mtu}"
fi
exit
@W-Floyd
Copy link
Author

W-Floyd commented Feb 11, 2025

Notes

VPN

vpn is slow-ish, but most reliable.
It explicitly sets the MTU on the VPN link to determine the largest working MTU.
Just provide an address that curl can test against, and optionally set the VPN interface, and it will set and test values until success.

Ping

ping mode is fast-ish, but less reliable.
When pinging, try to use a nearby host (google.com is a good default).
Longer ping times may occasionally fail and lead to strange results, and some hosts either reject ping or limit the size.

Example outputs

VPN Curl Test (Without MTU already set)

❯ ./mtu-finder.sh -c 'https://<example>/'
Need root access, testing...
Done
Detecting mtu from 'tun0'... 1500
Testing MTU: 1500... ❌
Testing MTU: 750... ✔️
Testing MTU: 1125... ✔️
Testing MTU: 1312... ✔️
Testing MTU: 1406... ✔️
Testing MTU: 1453... ❌
Testing MTU: 1429... ❌
Testing MTU: 1417... ✔️
Testing MTU: 1423... ❌
Testing MTU: 1420... ✔️
Testing MTU: 1421... ❌
Confirming results... ✔️
Largest working MTU: 1420

Add the following to your .ovpn file:
pull-filter ignore "tun-mtu"
tun-mtu 1420

VPN Curl Test (With MTU already set)

❯ ./mtu-finder.sh -c 'https://<example>/'
Need root access, testing...
Done
Detecting mtu from 'tun0'... 1420
Testing MTU: 1420... ✔️
Testing MTU: 1421... ❌
Confirming results... ✔️
Largest working MTU: 1420

Interface 'tun0' is already using the maximum MTU: 1420

Good Ping Host

❯ ./mtu-finder.sh
⚠️ WARNING: Ping may result in incorrect detected MTU depending on what host is specified
Pinging: google.com
Detecting mtu from 'wlan0'... 1500
Testing MTU: 1500... ❌
Testing MTU: 750... ✔️
Testing MTU: 1125... ✔️
Testing MTU: 1312... ✔️
Testing MTU: 1406... ✔️
Testing MTU: 1453... ❌
Testing MTU: 1429... ❌
Testing MTU: 1417... ✔️
Testing MTU: 1423... ❌
Testing MTU: 1420... ✔️
Testing MTU: 1421... ❌
Confirming results... ✔️
Largest working MTU: 1420

Add the following to your .ovpn file:
pull-filter ignore "tun-mtu"
tun-mtu 1420

Bad Ping Host

❯ ./mtu-finder.sh -p github.com
⚠️ WARNING: Ping may result in incorrect detected MTU depending on what host is specified
Pinging: github.com
Detecting mtu from 'wlan0'... 1500
Testing MTU: 1500... ✔️
Testing MTU: 3000... ✔️
Testing MTU: 6000... ✔️
Testing MTU: 12000... ✔️
Testing MTU: 24000... ✔️
Testing MTU: 48000... ❌
Testing MTU: 36000... ❌
Testing MTU: 30000... ❌
Testing MTU: 27000... ❌
Testing MTU: 25500... ❌
Testing MTU: 24750... ❌
Testing MTU: 24375... ❌
Testing MTU: 24187... ❌
Testing MTU: 24093... ❌
Testing MTU: 24046... ❌
Testing MTU: 24023... ❌
Testing MTU: 24011... ❌
Testing MTU: 24005... ❌
Testing MTU: 24002... ❌
Testing MTU: 24001... ❌
Confirming results... ❌
Inconsistent results, retesting!
Resetting upper bound
Testing MTU: 24000... ❌
Repeated MTU

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment