diff --git a/qbittorrent/Dockerfile b/qbittorrent/Dockerfile index 6f7fc4b13..42fbfce90 100644 --- a/qbittorrent/Dockerfile +++ b/qbittorrent/Dockerfile @@ -113,7 +113,7 @@ RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_auto # && chmod a+x /etc/s6-overlay/s6-rc.d/$SCRIPTSNAME/* ; done; fi # Manual apps -ARG PACKAGES="ipcalc wireguard-tools" +ARG PACKAGES="ipcalc wireguard-tools libnatpmp iptables ip6tables" # Automatic apps & bashio ADD "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/ha_autoapps.sh" "/ha_autoapps.sh" diff --git a/qbittorrent/config.yaml b/qbittorrent/config.yaml index d38b2aa05..749b0972e 100644 --- a/qbittorrent/config.yaml +++ b/qbittorrent/config.yaml @@ -131,6 +131,7 @@ schema: openvpn_enabled: bool? openvpn_password: str? openvpn_username: str? + vpn_upnp_enabled: bool? qbit_manage: bool? run_duration: str? silent: bool? @@ -142,4 +143,4 @@ schema: slug: qbittorrent udev: true url: https://github.com/alexbelgium/hassio-addons -version: "5.1.4-17" +version: "5.1.4-18" diff --git a/qbittorrent/rootfs/etc/cont-init.d/96-vpn-upnp.sh b/qbittorrent/rootfs/etc/cont-init.d/96-vpn-upnp.sh new file mode 100644 index 000000000..07f2ef38f --- /dev/null +++ b/qbittorrent/rootfs/etc/cont-init.d/96-vpn-upnp.sh @@ -0,0 +1,15 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -euo pipefail + +if ! bashio::config.true 'openvpn_enabled' && ! bashio::config.true 'wireguard_enabled'; then + # No VPN enabled: remove UPnP service to avoid unnecessary restarts + rm -rf /etc/services.d/vpn-upnp + exit 0 +fi + +if ! bashio::config.true 'vpn_upnp_enabled'; then + # UPnP not enabled: remove UPnP service to avoid unnecessary restarts + rm -rf /etc/services.d/vpn-upnp + exit 0 +fi diff --git a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run index 52091f91a..707b20f62 100755 --- a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run +++ b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run @@ -178,7 +178,7 @@ read -r VPN_IP VPN_COUNTRY <<< "${VPN_INFO_OUT}" bashio::log.info "VPN external IP: ${VPN_IP} (${VPN_COUNTRY})" while true; do - sleep "${VPN_CHECK_INTERVAL}" + sleep "${VPN_CHECK_INTERVAL}" & wait $! if ! current_out="$(get_ip_info)"; then bashio::log.warning "Failed to refresh external IP; keeping previous assumptions." diff --git a/qbittorrent/rootfs/etc/services.d/vpn-upnp/run b/qbittorrent/rootfs/etc/services.d/vpn-upnp/run new file mode 100644 index 000000000..83d5d0cec --- /dev/null +++ b/qbittorrent/rootfs/etc/services.d/vpn-upnp/run @@ -0,0 +1,265 @@ +#!/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 diff --git a/qbittorrent/rootfs/usr/local/sbin/vpn b/qbittorrent/rootfs/usr/local/sbin/vpn index 7960b84de..2972a8f3d 100755 --- a/qbittorrent/rootfs/usr/local/sbin/vpn +++ b/qbittorrent/rootfs/usr/local/sbin/vpn @@ -40,6 +40,7 @@ _parse_config() { } _parse_dns() { + local dns_ip local -a dns_conf=() local -a dns_backup_ipv4=("8.8.8.8" "1.1.1.1") local -a dns_backup_ipv6=("2001:4860:4860::8888" "2606:4700:4700::1111") @@ -155,19 +156,24 @@ _routing_add() { local local_ipv6=$(ip addr show ${config["Interface"]} | grep 'inet6 ' | awk '{print $2}' | cut -d'/' -f1) local ipv4 local ipv6 + local dns_ip # add routing rules for local IPs for ipv4 in ${local_ipv4}; do config["IPv4Enabled"]="true" - _cmd "ip -4 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1 _cmd "ip -4 rule add priority 1 from ${ipv4} table ${config["Table"]}" || return 1 + _cmd "ip -4 rule add priority 1 to ${ipv4}/24 table ${config["Table"]}" || return 1 done + if [ "${config["IPv4Enabled"]}" = "true" ]; then + _cmd "ip -4 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1 + fi for ipv6 in ${local_ipv6}; do config["IPv6Enabled"]="true" _cmd "ip -6 rule add priority 1 from ${ipv6} table ${config["Table"]}" || return 1 + _cmd "ip -6 rule add priority 1 to ${ipv6}/64 table ${config["Table"]}" || return 1 done if [ "${config["IPv6Enabled"]}" = "true" ]; then - _cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || true + _cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1 fi # get valid DNS servers @@ -182,15 +188,11 @@ _routing_add() { #_cmd "ip -6 route add ${dns_ip} dev ${config["Interface"]}" || return 1 _cmd "ip -6 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1 done - - # Update resolv.conf with VPN DNS servers - _resolvconf "update" } _routing_del() { bashio::log.info "Removing routing rules for VPN interface ${config["Interface"]}..." - _resolvconf "reset" while _cmd "ip -4 rule del priority 1 from all table ${config["Table"]} 2>/dev/null"; do :; done while _cmd "ip -4 rule del priority 1 to all table ${config["Table"]} 2>/dev/null"; do :; done while _cmd "ip -4 route del default dev ${config["Interface"]} table ${config["Table"]} 2>/dev/null"; do :; done @@ -199,20 +201,52 @@ _routing_del() { while _cmd "ip -6 route del default dev ${config["Interface"]} table ${config["Table"]} 2>/dev/null"; do :; done } +# --- Firewall Specific Functions --- + +_firewall_add() { + if [ "${config["IPv4Enabled"]}" = "true" ]; then + _cmd "iptables -N pnat" || return 1 + _cmd "iptables -A INPUT -i ${config["Interface"]} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" || return 1 + _cmd "iptables -A INPUT -i ${config["Interface"]} -p icmp -j ACCEPT" || return 1 + _cmd "iptables -A INPUT -i ${config["Interface"]} -j pnat" || return 1 + _cmd "iptables -A INPUT -i ${config["Interface"]} -j DROP" || return 1 + fi + + if [ "${config["IPv6Enabled"]}" = "true" ]; then + _cmd "ip6tables -N pnat" || return 1 + _cmd "ip6tables -A INPUT -i ${config["Interface"]} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" || return 1 + _cmd "ip6tables -A INPUT -i ${config["Interface"]} -p icmpv6 -j ACCEPT" || return 1 + _cmd "ip6tables -A INPUT -i ${config["Interface"]} -j pnat" || return 1 + _cmd "ip6tables -A INPUT -i ${config["Interface"]} -j DROP" || return 1 + fi +} + +_firewall_del() { + if [ "${config["IPv4Enabled"]}" = "true" ]; then + _cmd "iptables -F INPUT" || true + _cmd "iptables -F pnat" || true + _cmd "iptables -X pnat" || true + fi + + if [ "${config["IPv6Enabled"]}" = "true" ]; then + _cmd "ip6tables -F INPUT" || true + _cmd "ip6tables -F pnat" || true + _cmd "ip6tables -X pnat" || true + fi +} + # --- WireGuard Specific Logic --- -_wg_wait_handshake() { +_wireguard_check() { local timeout="${1:-20}" - local iface="${config["Interface"]}" - local peer_pk="${config["PublicKey"]}" local deadline ts deadline=$(( $(date +%s) + timeout )) while [ "$(date +%s)" -lt "${deadline}" ]; do - ping -I "${iface}" -c1 -W1 1.1.1.1 >/dev/null 2>&1 || true + ping -I "${config["Interface"]}" -c1 -W1 1.1.1.1 >/dev/null 2>&1 || true - ts="$(wg show "${iface}" latest-handshakes 2>/dev/null | awk -v pk="${peer_pk}" '$1==pk{print $2; exit}')" + ts="$(wg show "${config["Interface"]}" latest-handshakes 2>/dev/null | awk -v pk="${config["PublicKey"]}" '$1==pk{print $2; exit}')" if [ -n "${ts}" ] && [ "${ts}" -gt 0 ] 2>/dev/null; then return 0 fi @@ -220,17 +254,25 @@ _wg_wait_handshake() { done bashio::log.error "WireGuard handshake not established after ${timeout}s (latest-handshake=${ts:-0})." - wg show "${iface}" 2>&1 | while IFS= read -r l; do bashio::log.error "${l}"; done + wg show "${config["Interface"]}" 2>&1 | while IFS= read -r l; do bashio::log.error "${l}"; done return 1 } _wireguard_up() { + local local_ip + local -a local_ips=() + local -A local_ip_types=() + local allowed_ip + local -a allowed_ips=() + local -A allowed_ip_types=() + local key + bashio::log.warning "This script force Wireguard to ignore any routes and DNS settings." bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}" bashio::log.warning "This routing table will be used for traffic from the VPN interface and to the configured DNS servers." bashio::log.warning "Qbittorrent bittorrent client shall be set to use the VPN interface ${config["Interface"]} only." - for key in "Interface" "ListenPort" "PrivateKey" "PublicKey" "EndpointIP" "EndpointPort" ; do + for key in "Interface" "ListenPort" "PrivateKey" "PublicKey" "EndpointIP" "EndpointPort" "Address"; do if [ ! -v config[$key] ] || [ -z "${config[$key]}" ]; then bashio::log.error "Missing required WireGuard configuration parameter: ${key}" return 1 @@ -238,28 +280,51 @@ _wireguard_up() { done _cmd "ip link add ${config["Interface"]} type wireguard" || return 1 - local allowed_ips="" - local -a local_ips=() + mapfile -d ',' -t local_ips < <(echo "${config["Address"]}" | tr -d ' ') for local_ip in ${local_ips[@]}; do local result=0 _check_host "${local_ip}" || result=$? if [ "${result}" -eq 1 ]; then - allowed_ips="${allowed_ips},0.0.0.0/0" + config["IPv4Enabled"]="true" + local_ip_types["${local_ip}"]="ipv4" + allowed_ip_types["0.0.0.0/0"]="ipv4" _cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1 elif [ "${result}" -eq 2 ]; then - allowed_ips="${allowed_ips},::/0" + config["IPv6Enabled"]="true" + local_ip_types["${local_ip}"]="ipv6" _cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1 else bashio::log.warning "Ignoring invalid local IP address: ${local_ip}" fi done - allowed_ips="${allowed_ips#,}" - if [ -z "${allowed_ips}" ]; then - bashio::log.error "No valid local IP addresses configured." + if [ ${#local_ip_types[@]} -eq 0 ]; then + bashio::log.error "No valid local IP addresses configured for WireGuard interface." return 1 fi + mapfile -d ',' -t allowed_ips < <(echo "${config["Address"]}" | tr -d ' ') + for allowed_ip in ${allowed_ips[@]}; do + local result=0 + _check_host "${allowed_ip}" || result=$? + if [ "${result}" -eq 1 ] && [ "${config["IPv4Enabled"]}" == "true" ]; then + allowed_ip_types["${allowed_ip}"]="ipv4" + #allowed_ip_types["0.0.0.0/0"]="ipv4" + elif [ "${result}" -eq 2 ] && [ "${config["IPv6Enabled"]}" == "true" ]; then + allowed_ip_types["${allowed_ip}"]="ipv6" + #allowed_ip_types["::/0"]="ipv6" + else + bashio::log.error "Invalid allowed IP address: ${allowed_ip}" + return 1 + fi + done + if [ ${#allowed_ip_types[@]} -eq 0 ]; then + bashio::log.error "No valid allowed IP addresses configured for WireGuard peer." + return 1 + fi + printf -v allowed_ips '%s,' "${!allowed_ip_types[@]}" + allowed_ips="${allowed_ips%,}" + _cmd "wg set ${config["Interface"]} listen-port ${config["ListenPort"]} private-key ${config["PrivateKey"]}" || return 1 local endpoint="${config["EndpointIP"]}:${config["EndpointPort"]}" if [[ "${config["EndpointIP"]}" == *:* ]]; then @@ -279,18 +344,32 @@ _wireguard_up() { fi _cmd "ip link set ${config["Interface"]} up" || return 1 - _routing_add - _wg_wait_handshake 10 || return 1 + + # Add routing rules for VPN interface and DNS servers + _routing_add || return 1 + # Add firewall rules for VPN interface + _firewall_add || return 1 + # Update resolv.conf with VPN DNS servers + _resolvconf "update" || return 1 + # Wait for handshake to be established before returning success + _wireguard_check 10 || return 1 } _wireguard_down() { - _routing_del + # Update resolv.conf to remove VPN DNS servers + _resolvconf "reset" || true + # Remove routing rules for VPN interface and DNS servers + _routing_del || true + # Remove firewall rules for VPN interface + _firewall_del || true + _cmd "ip link set ${config["Interface"]} down" 2>/dev/null || true _cmd "ip link del ${config["Interface"]}" 2>/dev/null || true } wireguard() { local mode=$1 + local key local interface local config_file local WIREGUARD_STATE_DIR="/var/run/wireguard" @@ -328,7 +407,7 @@ wireguard() { printf '%s\n' "${config["PrivateKey"]}" > "${WIREGUARD_STATE_DIR}/privatekey" chmod 600 "${WIREGUARD_STATE_DIR}/privatekey" || true config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey" - + if [ -n "${config["PresharedKey"]:-}" ]; then printf '%s\n' "${config["PresharedKey"]}" > "${WIREGUARD_STATE_DIR}/presharedkey" chmod 600 "${WIREGUARD_STATE_DIR}/presharedkey" || true @@ -384,6 +463,23 @@ wireguard() { # --- OpenVPN Specific Logic --- +_openvpn_check() { + local timeout="${1:-20}" + local deadline ts + + deadline=$(( $(date +%s) + timeout )) + + while [ "$(date +%s)" -lt "${deadline}" ]; do + if ip link show "${config["Interface"]}" > /dev/null 2>&1 ; then + return 0 + fi + sleep 2 + done + + bashio::log.error "OpenVPN interface ${config["Interface"]} failed to come up after ${timeout}s." + return 1 +} + _openvpn_up() { bashio::log.warning "This script force OpenvPN to ignore any routes and DNS settings pushed by the server." bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}" @@ -418,21 +514,31 @@ _openvpn_up() { --route-nopull \ --route-noexec" || return 1 - #wait for slow OpenVPN interface to come up - for i in {1..10}; do - if ip link show "${config["Interface"]}" > /dev/null 2>&1 ; then - return 0 - fi - sleep 2 - done - bashio::log.error "OpenVPN interface ${config["Interface"]} failed to come up." - return 1 + # Wait for the VPN interface to come up + _openvpn_check 30 || return 1 } _openvpn_down() { # Terminate OpenVPN process pkill -f "openvpn --config ${config["ConfigFile"]}" || true - _routing_del +} + +_openpvn_postup() { + # Add routing rules for VPN interface and DNS servers + _routing_add || return 1 + # Add firewall rules for VPN interface + _firewall_add || return 1 + # Update resolv.conf with VPN DNS servers + _resolvconf "update" || return 1 +} + +_openpvn_postdown() { + # Update resolv.conf to remove VPN DNS servers + _resolvconf "reset" || true + # Remove routing rules for VPN interface and DNS servers + _routing_del || true + # Remove firewall rules for VPN interface + _firewall_del || true } openvpn() { @@ -480,10 +586,10 @@ openvpn() { bashio::log.info "OpenVPN on interface ${config["Interface"]} is down." bashio::exit.ok 'OpenVPN stopped.' elif [ "${mode}" = "postup" ]; then - _routing_add + _openpvn_postup bashio::exit.ok 'OpenVPN routes added.' elif [ "${mode}" = "postdown" ]; then - _routing_del + _openpvn_postdown bashio::exit.ok 'OpenVPN routes deleted.' else bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'."