Skip to content

Instantly share code, notes, and snippets.

@klinquist
Created December 10, 2025 14:59
Show Gist options
  • Select an option

  • Save klinquist/446c207670620adc4d383bedfeed6dc8 to your computer and use it in GitHub Desktop.

Select an option

Save klinquist/446c207670620adc4d383bedfeed6dc8 to your computer and use it in GitHub Desktop.
ProtonVPN+Synology+qBittorrent. Run natpmpc every minute, update qbittorrent config if port changes.
#!/bin/sh
# Keep qBittorrent's incoming port in sync with ProtonVPN NAT-PMP.
# Runs natpmpc to fetch the current mapped port, updates qBittorrent.conf
# if it changed, and restarts the container so the new port is applied.
set -eu
# ---- configurable bits ------------------------------------------------------
# NAT-PMP gateway (ProtonVPN typically uses 10.2.0.1)
GATEWAY="${GATEWAY:-10.2.0.1}"
# Path to natpmpc binary
NATPMPC_BIN="${NATPMPC_BIN:-/opt/bin/natpmpc}"
# qBittorrent config paths (adjust if your config lives elsewhere)
CONFIG_DIR="${CONFIG_DIR:-/volume1/docker/qbittorrent/qBittorrent}"
QBT_CONF="${QBT_CONF:-${CONFIG_DIR}/qBittorrent.conf}"
# Container controls (override these if you use docker compose or another name)
CONTAINER="${CONTAINER:-qbittorrent}"
STOP_CMD=${STOP_CMD:-"docker stop ${CONTAINER}"}
START_CMD=${START_CMD:-"docker start ${CONTAINER}"}
# ---- functions --------------------------------------------------------------
fatal() {
echo "[$(date '+%Y-%m-%d %H:%M
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fatal "missing command: $1"
}
update_port_in_conf() {
conf_file="$1"
new_port="$2"
tmp_file="$(mktemp "${conf_file}.XXXX")"
trap 'rm -f "$tmp_file"' EXIT
awk -v p="$new_port" '
BEGIN {s=0; c=0}
/^Session\\Port=/ {print "Session\\Port=" p; s=1; next}
/^Connection\\PortRangeMin=/ {print "Connection\\PortRangeMin=" p; c=1; next}
{print}
END {
if (!s) print "Session\\Port=" p
if (!c) print "Connection\\PortRangeMin=" p
}
' "$conf_file" >"$tmp_file"
mv "$tmp_file" "$conf_file"
trap - EXIT
}
# ---- main -------------------------------------------------------------------
require_cmd awk
require_cmd grep
require_cmd sed
require_cmd mktemp
require_cmd sh
require_cmd date
require_cmd cp
require_cmd "$NATPMPC_BIN"
[ -f "$QBT_CONF" ] || fatal "qBittorrent config not found at $QBT_CONF"
# Ask NAT-PMP for a fresh mapping (UDP then TCP, TCP is the one we care about).
map_output="$("$NATPMPC_BIN" -a 1 0 udp 60 -g "$GATEWAY" 2>&1 && \
"$NATPMPC_BIN" -a 1 0 tcp 60 -g "$GATEWAY" 2>&1)"
new_port="$(echo "$map_output" | awk '/Mapped public port .* protocol TCP/ {print $4; exit}')"
case "$new_port" in
''|*[!0-9]*) fatal "could not parse mapped TCP port from natpmpc output";;
esac
current_port="$(
awk -F= '
/^Session\\Port=/ {sp=$2}
/^Connection\\PortRangeMin=/ {cp=$2}
END {
if (sp) print sp; else if (cp) print cp
}
' "$QBT_CONF" | tail -n 1
)"
if [ "$new_port" = "$current_port" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Port unchanged ($new_port); nothing to do."
exit 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Port change detected: $current_port -> $new_port"
# Stop container before editing the config.
if [ -n "$STOP_CMD" ]; then
echo "Stopping qBittorrent container..."
sh -c "$STOP_CMD" >/dev/null
fi
# Backup config before modifying.
backup="${QBT_CONF}.bak.$(date +%Y%m%d%H%M%S)"
cp "$QBT_CONF" "$backup"
echo "Config backed up to $backup"
update_port_in_conf "$QBT_CONF" "$new_port"
# Start container again.
if [ -n "$START_CMD" ]; then
echo "Starting qBittorrent container..."
sh -c "$START_CMD" >/dev/null
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Updated Session\\Port to $new_port"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment