Forked from KaBankz/99-enable-proton-port-forward-cron.sh
Created
October 16, 2024 22:40
-
-
Save dneto82/8dcae87f68e874a4675f657d17382595 to your computer and use it in GitHub Desktop.
Enable ProtonVPN port forwarding with qBittorrent on docker
This file contains 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
#!/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 |
This file contains 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
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment