Skip to content

Instantly share code, notes, and snippets.

@KaBankz
Last active March 4, 2024 05:22
Show Gist options
  • Save KaBankz/c5a08845e3c64fbae8053dc7d28f8191 to your computer and use it in GitHub Desktop.
Save KaBankz/c5a08845e3c64fbae8053dc7d28f8191 to your computer and use it in GitHub Desktop.
Enable ProtonVPN port forwarding with qBittorrent on docker
#!/command/with-contenv bash
# shellcheck shell=bash
#
# This is for hotio's qBittorrent docker image, but can be used for other qBittorrent docker images
# by mounting this script to wherever your image expects custom init scripts to be, and changing
# the shebang to the appropriate path for your image
#
# This script adds proton-port-forward-for-qbittorrent.sh to the crontab to run every 15 min on container start
# Make sure proton-port-forward-for-qbittorrent.sh is in the expected location /usr/local/bin
# if not, change the SCRIPT_DIR variable to the correct location
#
# For docker compose add a read-only bind mount from this script to
# /etc/cont-init.d/99-enable-proton-port-forward-cron.sh
# Example ./scripts/99-enable-proton-port-forward-cron.sh:/etc/cont-init.d/99-enable-proton-port-forward-cron.sh:ro
#
SCRIPT_DIR="/usr/local/bin"
# Start the cron daemon if it is not already running
if ! pgrep "crond" &>/dev/null; then
echo "99-enable-proton-port-forward-cron: Starting cron daemon..."
crond
fi
# Check if proton-port-forward-for-qbittorrent.sh is in the crontab
if ! crontab -l | grep -q "proton-port-forward-for-qbittorrent.sh"; then
echo "99-enable-proton-port-forward-cron: proton-port-forward-for-qbittorrent.sh is not in the crontab, adding it..."
# Add this script to the crontab to run every 15 min
(
crontab -l 2>/dev/null
echo "*/15 * * * * $SCRIPT_DIR/proton-port-forward-for-qbittorrent.sh"
) | crontab -
fi
# Run the script manually once on container start in a subshell to prevent blocking the container start
# Subsequent runs will be handled by the cron job
# shellcheck disable=SC1091
source "$SCRIPT_DIR/proton-port-forward-for-qbittorrent.sh" &
exit 0
#!/command/with-contenv bash
# shellcheck shell=bash
#
# This script will update the qBittorrent listening port to the current active ProtonVPN port
# This is to allow for port forwarding to work with qBittorrent when using ProtonVPN
#
# ProtonVPN port forwarding does not use static ports, so the port needs to be updated frequently
# This script should be added to crontab to run every 15 minutes to ensure the correct port is used
#
# Logs will be sotred at /var/log/proton-port-forward-for-qbittorrent.log or the path specified by the LOG_FILE variable
#
# For docker compose add a read-only bind mount from this script to
# /usr/local/bin/proton-port-forward-for-qbittorrent.sh
# or your preferred directory; make sure to update the INSTALL_DIR variable if you do
# Example ./scripts/proton-port-forward-for-qbittorrent.sh:/usr/local/bin/proton-port-forward-for-qbittorrent.sh:ro
#
# For hotio's qBittorrent docker image, you will also have to add a companion script
# that will add this script to the crontab on container start
# For other qBittorrent docker images you'll have to find a way to
# add this script to the crontab on container start and possibly change the shebang
#
# Refrences:
# ProtonVPN: https://protonvpn.com/support/port-forwarding-manual-setup
# u/TennesseeTater: https://old.reddit.com/r/ProtonVPN/comments/10owypt/successful_port_forward_on_debian_wdietpi_using
# yimingliu: https://github.com/yimingliu/py-natpmp
#
# Exit on error from wherever the error is thrown, even in a subshell (function)
# Source: https://stackoverflow.com/a/9894126
trap "exit 1" TERM
export TOP_PID=$$
set -e
# Config variables
INSTALL_DIR="/usr/local/bin"
LOG_FILE="/var/log/proton-port-forward-for-qbittorrent.log"
# Username and password can be left blank if localhost authentication is disabled
QBITTORRENT_USERNAME="admin"
QBITTORRENT_PASSWORD="adminadmin"
QBITTORRENT_BASE_URL="http://localhost:8080"
QBITTORRENT_LOGIN_API_ENDPOINT="$QBITTORRENT_BASE_URL/api/v2/auth/login"
QBITTORRENT_GET_PREFS_API_ENDPOINT="$QBITTORRENT_BASE_URL/api/v2/app/preferences"
QBITTORRENT_SET_PREFS_API_ENDPOINT="$QBITTORRENT_BASE_URL/api/v2/app/setPreferences"
# NOTIFIARR variables can be left blank if you don't use Notifiarr
# NOTIFIARR_WEBHOOK_URL should be from the passthrough integration
NOTIFIARR_WEBHOOK_URL=""
# DISCORD_CHANNEL_ID should be the channel ID of the channel you want to send the notifications to
DISCORD_CHANNEL_ID=""
# echo a log message to stdout and append it to the log file
log() {
echo "proton-port-forward-for-qbittorrent: [$(date "+%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
}
# Send a notification to Notifiarr
# $1 = title
# $2 = description
# $3 = color
notifiarr() {
title="$1"
description="$2"
color="$3"
# If NOTIFIARR_WEBHOOK_URL is empty or DISCORD_CHANNEL_ID is empty, then do not send a notification
if [ -z "$NOTIFIARR_WEBHOOK_URL" ] || [ -z "$DISCORD_CHANNEL_ID" ]; then
return
fi
json_body="{
\"notification\": {
\"update\": false,
\"name\": \"qBittorrent ProtonVPN Sync\"
},
\"discord\": {
\"color\": \"$color\",
\"text\": {
\"title\": \"$title\",
\"description\": \"$description\",
},
\"ids\": {
\"channel\": $DISCORD_CHANNEL_ID
}
}
}"
if [ "$(curl -si -XPOST -d "$json_body" -o /dev/null -w "%{http_code}" "$NOTIFIARR_WEBHOOK_URL")" = "200" ]; then
log "Successfully sent notification to Notifiarr"
else
log "Failed to send notification to Notifiarr" >&2
fi
}
# Wait 60sec for qBittorrent to start and be reachable
SECONDS=0
until (pgrep "qbittorrent-nox" &>/dev/null) && (curl -s "$QBITTORRENT_BASE_URL" &>/dev/null); do
if ((SECONDS > 60)); then
log "qBittorrent is not running!" >&2
notifiarr "qBittorrent is not running!" "qBittorrent is not running or is unreachable. Check your configuration variables or inspect your container" "ff0000"
kill -s TERM "$TOP_PID"
fi
log "qBittorrent is not running, retrying in 10 seconds..."
sleep 10
done
# Check if the required commands exist
commands=(curl tar python3 grep timeout)
for command in "${commands[@]}"; do
if ! command -v "$command" &>/dev/null; then
log "\"$command\" does not exist!" >&2
kill -s TERM "$TOP_PID"
fi
done
# Check if the $INSTALL_DIR/py-natpmp-master directory exists
log "Checking if py-natpmp exists..."
if [ ! -d "$INSTALL_DIR/py-natpmp-master" ]; then
log "py-natpmp does not exist, downloading..."
# Download and extract the natpmp client to $INSTALL_DIR
curl -sL https://github.com/yimingliu/py-natpmp/archive/master.tar.gz | tar xz -C "$INSTALL_DIR"
fi
log "py-natpmp exists"
# Get the qBittorrent auth cookie
qbittorrent_auth_cookie() {
cookie=$(curl -si --header "Referer: $QBITTORRENT_BASE_URL" --data "username=$QBITTORRENT_USERNAME&password=$QBITTORRENT_PASSWORD" "$QBITTORRENT_LOGIN_API_ENDPOINT" | grep -oP '(?<=set-cookie: )\S*(?=;)')
# If the cookie is empty and the username and password are not empty, then the login failed
if [ -z "$cookie" ] && { [ -n "$QBITTORRENT_USERNAME" ] && [ -n "$QBITTORRENT_PASSWORD" ]; }; then
log "Failed to login to qBittorrent" >&2
notifiarr "Failed to login to qBittorrent" "Check your username and password, or your configuration variables" "ff0000"
kill -s TERM "$TOP_PID"
else
echo "$cookie"
fi
}
# Save the qBittorrent auth cookie to a variable to prevent multiple calls to the function
qbittorrent_auth_cookie=$(qbittorrent_auth_cookie)
# Get the current active ProtonVPN port
active_proton_vpn_port() {
number_regex='^[0-9]+$'
# The timeout is to prevent the script from hanging if the port cannot be retrieved
port=$(timeout 10 python3 "$INSTALL_DIR"/py-natpmp-master/natpmp/natpmp_client.py -g 10.2.0.1 0 0 | grep -oP '(?<=public port ).*(?=,)')
# If the port is empty or not a number, then the port could not be retrieved
if [ -z "$port" ]; then
log "Failed to get the active ProtonVPN port, timeout" >&2
notifiarr "Failed to get the active ProtonVPN port" "Check your ProtonVPN configuration and connection" "ff0000"
kill -s TERM $TOP_PID
elif ! [[ "$port" =~ $number_regex ]]; then
log "Failed to get the active ProtonVPN port, port is not a number" >&2
notifiarr "Failed to get the active ProtonVPN port" "An unexpected value was returned from py-natpmp" "ff0000"
kill -s TERM "$TOP_PID"
else
echo "$port"
fi
}
# Save the current active ProtonVPN port to a variable to prevent multiple calls to the function
active_proton_vpn_port="$(active_proton_vpn_port)"
# Get the current active qBittorrent port
active_qbittorrent_port() {
number_regex='^[0-9]+$'
port=$(curl -s -b "$qbittorrent_auth_cookie" "$QBITTORRENT_GET_PREFS_API_ENDPOINT" | grep -oP '(?<="listen_port":)\d+(?=,)')
if [ -z "$port" ]; then
log "Failed to get the active qBittorrent port" >&2
notifiarr "Failed to get the active qBittorrent port" "Check your username and password, or your configuration variables" "ff0000"
kill -s TERM "$TOP_PID"
elif ! [[ "$port" =~ $number_regex ]]; then
log "Failed to get the active qBittorrent port, port is not a number" >&2
notifiarr "Failed to get the active qBittorrent port" "An unexpected value was returned from qBittorrent" "ff0000"
kill -s TERM "$TOP_PID"
else
echo "$port"
fi
}
# Save the current active qBittorrent port to a variable to prevent multiple calls to the function
active_qbittorrent_port="$(active_qbittorrent_port)"
# Check if the current active ProtonVPN port is different from the current active qBittorrent port
# If they are different, update the qBittorrent port
if [ -z "$active_proton_vpn_port" ] || [ -z "$active_qbittorrent_port" ]; then
log "Failed to get the active ProtonVPN port or the active qBittorrent port" >&2
notifiarr "Failed to get the active ProtonVPN port or the active qBittorrent port" "Check your ProtonVPN configuration and connection, or your qBittorrent username and password, or your configuration variables" "ff0000"
kill -s TERM "$TOP_PID"
elif [ "$active_proton_vpn_port" != "$active_qbittorrent_port" ]; then
log "Current active ProtonVPN port: $active_proton_vpn_port"
log "Current active qBittorrent port: $active_qbittorrent_port"
log "qBittorrent port is different from the ProtonVPN port, updating the qBittorrent port..."
# Make a post request to the qBittorrent API to update the port
if [ "$(curl -si -b "$qbittorrent_auth_cookie" -XPOST -d "json={\"listen_port\":$active_proton_vpn_port}" -o /dev/null -w "%{http_code}" "$QBITTORRENT_SET_PREFS_API_ENDPOINT")" = "200" ]; then
log "qBittorrent port updated to $active_proton_vpn_port"
# notifiarr "qBittorrent port synced with ProtonVPN" "**Old Port:** \`$active_qbittorrent_port\`\n**New Port:** \`$active_proton_vpn_port\`" "00ff00"
else
log "Failed to update qBittorrent port" >&2
notifiarr "Failed to update qBittorrent port" "Check your username and password, or your configuration variables" "ff0000"
kill -s TERM "$TOP_PID"
fi
else
log "Current active ProtonVPN port: $active_proton_vpn_port"
log "Current active qBittorrent port: $active_qbittorrent_port"
log "qBittorrent port is the same as the ProtonVPN port, no need to update the qBittorrent port"
fi
@Kiser360
Copy link

Kiser360 commented Jul 25, 2023

For anyone else like me questioning the magic IP 10.2.0.1, considering it's not mentioned in the Proton Docs1:

When generating a Wireguard config at the ProtonVPN site 10.2.0.1 will be in the resulting config specifying the DNS for the to-be-created wireguard adapter. This gave me some confidence that it's just a ProtonVPN convention, maybe a coordination server or the VPN's gateway, probably safe to keep hard-coded.

Thank you for sharing @KaBankz , this was very helpful!

Footnotes

  1. They just use natpmpc directly without specifying any host

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