#!/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 -oiP 'set-cookie:.*SID[^=]*=\K[^;]+'") 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