Last active
February 17, 2025 15:57
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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)
VPN Curl Test (With MTU already set)
Good Ping Host
Bad Ping Host