mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-04-03 04:30:37 +02:00
This is implementation of the UPnP port opening for qBittorrent running on VPN. Implementation also includes simple firewall for incoming connections.
266 lines
9.4 KiB
Plaintext
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
|