Files
hassio-addons/qbittorrent/rootfs/etc/services.d/vpn-upnp/run
litinoveweedle 7af8610a25 qBittorrent upnp and firewall for VPN
This is implementation of the UPnP port opening for qBittorrent running on VPN. Implementation also includes simple firewall for incoming connections.
2026-03-21 15:24:35 +01:00

266 lines
9.4 KiB
Plaintext

#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:${PATH}"
VPN_UPNP_INTERVAL="${VPN_UPNP_INTERVAL:-90}"
# -------------------------------
# Helpers
# -------------------------------
declare -A config
config["MySelf"]="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
_parse_config() {
local config_file="$1"
local -A config_keys=(
["UseHttps"]="HTTPS\\\\Enabled"
["Server"]="WebUI\\\\Address"
["Port"]="WebUI\\\\Port"
["User"]="WebUI\\\\Username"
["Pass"]="WebUI\\\\Password"
["Interface"]="Connection\\\\Interface"
["UPnP"]="Connection\\\\UPnP"
["TorrentPort"]="Session\\\\Port"
)
config["UseHttps"]="false"
config["Schema"]="http"
config["Server"]="127.0.0.1"
config["Port"]="8080"
config["User"]="admin"
config["Pass"]="adminadmin"
config["Interface"]=""
config["UPnP"]="false"
config["SID"]=""
config["UPnPPort"]="0"
config["IPv4Gateway"]=""
config["IPv4Enabled"]="false"
config["IPv6Enabled"]="false"
config["TorrentPort"]=$(shuf -i 49152-65535 -n 1)
for key in "${!config_keys[@]}"; do
if ! grep -qP "^\s*${config_keys[$key]}\s*=\s*\S+" "${config_file}"; then
continue
fi
config["${key}"]=$(grep -oP "^\s*${config_keys[$key]}\s*=\s*\K\S+" "${config_file}")
done
if [ "${config["UseHttps"]}" = "true" ]; then
config["Schema"]="https"
else
config["Schema"]="http"
fi
if [ "${config["Server"]}" = "*" ]; then
config["Server"]="127.0.0.1"
fi
if [ "${config["TorrentPort"]}" = "6881" ]; then
bashio::log.debug 'Torrent Port is default (6881), selecting a random port.'
config["TorrentPort"]=$(shuf -i 49152-65535 -n 1)
fi
for key in "${!config[@]}"; do
bashio::log.debug "Config key: $key, value: ${config[$key]}"
done
if [ -z "${config["Server"]}" ] || [ -z "${config["Port"]}" ] || [ -z "${config["User"]}" ] || [ -z "${config["Pass"]}" ] || [ -z "${config["Interface"]}" ]; then
bashio::exit.nok 'qBittorrent WebUI configuration not found or incomplete. Please check your qBittorrent.conf.'
fi
}
_cmd() {
cmd="$1"
bashio::log.debug "Executing command: ${cmd}"
eval "${cmd}"
}
_pnat_get_gw() {
local vpn_if_hex_addr=''
local ipv4
local vpn_if_hex_addr=$(grep ${config["Interface"]} /proc/net/route | awk '$2 == "00000000" { print $3 }')
local local_ipv4=$(ip addr show ${config["Interface"]} | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
local local_ipv6=$(ip addr show ${config["Interface"]} | grep 'inet6 ' | awk '{print $2}' | cut -d'/' -f1)
if [ -n "${local_ipv4}" ]; then
config["IPv4Enabled"]="true"
fi
if [ -n "${local_ipv6}" ]; then
config["IPv6Enabled"]="true"
fi
if [ -n "${vpn_if_hex_addr}" ]; then
#shellcheck disable=SC2046
config["IPv4Gateway"]=$(printf "%d." $(echo "${vpn_if_hex_addr}" | sed 's/../0x& /g' | tr ' ' '\n' | tac) | sed 's/\.$/\n/')
return 0
elif [ -n "${local_ipv4}" ]; then
for ipv4 in ${local_ipv4}; do
for n in {1..254}; do
local try_ip="$(echo "${ipv4}" | cut -d'.' -f1-3).${n}"
if [ "${try_ip}" != "${ipv4}" ]; then
if nc -4 -vw1 "${try_ip}" 1 &>/dev/null 2>&1; then
config["IPv4Gateway"]=${try_ip}
return 0
fi
fi
done
done
fi
return 1
}
_pnat_get_port() {
if [ "${config["IPv4Enabled"]}" = "true" ]; then
bashio::log.debug "Attempting to set uPNP port mapping for IPv4 via NAT-PMP"
# shellcheck disable=SC2086
_cmd "timeout 10s natpmpc -g ${config["IPv4Gateway"]} -a 1 0 udp ${config["Interval"]} >/dev/null 2>&1" || return 1
# shellcheck disable=SC2086
if [ "${config["UPnPPort"]}" = "0" ]; then
config["TorrentPort"]="0"
fi
config["UPnPPort"]=$(_cmd "timeout 10s natpmpc -g ${config["IPv4Gateway"]} -a 1 0 tcp ${config["Interval"]} | grep -oP '(?<=Mapped public port.).*(?=.protocol.*)'")
return $?
elif [ "${config["IPv6Enabled"]}" = "true" ]; then
if [ "${config["UPnPPort"]}" = "0" ]; then
config["UPnPPort"]=${config["TorrentPort"]}
config["TorrentPort"]="0"
else
config["UPnPPort"]=${config["TorrentPort"]}
fi
return 0
else
return 1
fi
}
_pnat_set_firewall() {
if [ "${config["IPv4Enabled"]}" = "true" ]; then
_cmd "iptables -F pnat 2>/dev/null" || true
_cmd "iptables -A pnat -p tcp --dport ${config["UPnPPort"]} -j ACCEPT" || return 1
_cmd "iptables -A pnat -p udp --dport ${config["UPnPPort"]} -j ACCEPT" || return 1
fi
if [ "${config["IPv6Enabled"]}" = "true" ]; then
_cmd "ip6tables -F pnat 2>/dev/null" || true
_cmd "ip6tables -A pnat -p tcp --dport ${config["UPnPPort"]} -j ACCEPT" || return 1
_cmd "ip6tables -A pnat -p udp --dport ${config["UPnPPort"]} -j ACCEPT" || return 1
fi
}
# --- qBittorrent Specific Logic ---
_qbt_login() {
config["SID"]=$(_cmd "curl -s -k -i --header \"Referer: ${config["Schema"]}://${config["Server"]}:${config["Port"]}\" --data \"username=${config["User"]}\" --data-urlencode \"password=${config["Pass"]}\" \"${config["Schema"]}://${config["Server"]}:${config["Port"]}/api/v2/auth/login\" | grep -oP '(?!set-cookie:.)SID=.*(?=\;.HttpOnly\;)'")
return $?
}
_qbt_set_port() {
_cmd "curl -s -k -i --header \"Referer: ${config["Schema"]}://${config["Server"]}:${config["Port"]}\" --cookie \"${config["SID"]}\" --data-urlencode \"json={\\\"listen_port\\\":${config["UPnPPort"]},\\\"random_port\\\":false,\\\"upnp\\\":false}\" \"${config["Schema"]}://${config["Server"]}:${config["Port"]}/api/v2/app/setPreferences\" >/dev/null 2>&1"
return $?
}
_qbt_get_port() {
config["TorrentPort"]=$(_cmd "curl -s -k -i --header \"Referer: ${config["Schema"]}://${config["Server"]}:${config["Port"]}\" --cookie \"${config["SID"]}\" \"${config["Schema"]}://${config["Server"]}:${config["Port"]}/api/v2/app/preferences\" | grep -oP '(?<=\"listen_port\"\\:)(\\d{1,5})'")
return $?
}
_qbt_check_sid() {
if _cmd "curl -s -k --header \"Referer: ${config["Schema"]}://${config["Server"]}:${config["Port"]}\" --cookie \"${config["SID"]}\" \"${config["Schema"]}://${config["Server"]}:${config["Port"]}/api/v2/app/version\" | grep -qi forbidden"; then
return 1
else
return 0
fi
}
_qbt_isreachable(){
_cmd "nc -4 -vw 5 ${config["Server"]} ${config["Port"]} &>/dev/null 2>&1"
return $?
}
# --- Main Logic ---
pnat_set() {
if [ -z "${config["SID"]}" ]; then
bashio::log.info "qBittorrent SessionID not set, getting new SessionID"
if ! _qbt_login; then
bashio::log.error "Failed getting new SessionID from qBittorrent"
return 1
fi
else
if ! _qbt_check_sid; then
bashio::log.info "qBittorrent Cookie invalid, getting new SessionID"
if ! _qbt_login; then
bashio::log.error "Failed getting new SessionID from qBittorrent"
return 1
fi
else
bashio::log.debug "qBittorrent SessionID Ok!"
fi
fi
if _qbt_get_port; then
bashio::log.debug "Configured Torrent Port: ${config["TorrentPort"]}"
else
bashio::log.error "Failed to get current Torrent Port configuration"
return 1
fi
if [ -z "${config["IPv4Gateway"]}" ]; then
bashio::log.info "VPN Gateway not know, will try to detect it."
if ! _pnat_get_gw; then
bashio::log.error "Failed to determine VPN gateway IP address. Cannot perform NAT-PMP/UPnP port mapping."
return 1
else
bashio::log.info "VPN Gateway detected: ${config["IPv4Gateway"]}"
fi
fi
if _pnat_get_port; then
bashio::log.debug "Active uPNP Port: ${config["UPnPPort"]}"
else
bashio::log.error "Failed to get current uPNP port mapping"
return 1
fi
if [ "${config["TorrentPort"]}" != "${config["UPnPPort"]}" ]; then
bashio::log.info "Changing Torrent port"
if _qbt_set_port; then
if _pnat_set_firewall; then
bashio::log.info "IPTables rule added for Torrent port ${config["UPnPPort"]}"
else
bashio::log.error "Failed to set firewall rules for Torrent port ${config["UPnPPort"]}"
return 1
fi
bashio::log.info "Torrent Port Changed to: ${config["UPnPPort"]}"
else
bashio::log.error "Torrent Port Change failed."
return 1
fi
else
bashio::log.debug "Torrent Port OK (Act: ${config["UPnPPort"]} Cfg: ${config["TorrentPort"]})"
fi
return 0
}
# -------------------------------
# Main logic
# -------------------------------
_parse_config "/config/qBittorrent/qBittorrent.conf"
config["Interval"]=$(( ${VPN_UPNP_INTERVAL} + (${VPN_UPNP_INTERVAL} / 2) ))
bashio::log.info "VPN UPnP enabled, starting NAT-PMP/UPnP monitor with an interval of ${VPN_UPNP_INTERVAL} seconds (UPnP lease interval: ${config["Interval"]} seconds)."
while true;
do
sleep ${VPN_UPNP_INTERVAL} & wait $!
if pnat_set; then
bashio::log.info "NAT-PMP/UPnP Ok!"
else
bashio::log.error "NAT-PMP/UPnP Failed"
fi
done