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.
This commit is contained in:
litinoveweedle
2026-03-21 15:24:35 +01:00
parent ceab896b46
commit 7af8610a25
6 changed files with 425 additions and 38 deletions

View File

@@ -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 # && chmod a+x /etc/s6-overlay/s6-rc.d/$SCRIPTSNAME/* ; done; fi
# Manual apps # Manual apps
ARG PACKAGES="ipcalc wireguard-tools" ARG PACKAGES="ipcalc wireguard-tools libnatpmp iptables ip6tables"
# Automatic apps & bashio # Automatic apps & bashio
ADD "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/ha_autoapps.sh" "/ha_autoapps.sh" ADD "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/ha_autoapps.sh" "/ha_autoapps.sh"

View File

@@ -131,6 +131,7 @@ schema:
openvpn_enabled: bool? openvpn_enabled: bool?
openvpn_password: str? openvpn_password: str?
openvpn_username: str? openvpn_username: str?
vpn_upnp_enabled: bool?
qbit_manage: bool? qbit_manage: bool?
run_duration: str? run_duration: str?
silent: bool? silent: bool?

View File

@@ -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

View File

@@ -178,7 +178,7 @@ read -r VPN_IP VPN_COUNTRY <<< "${VPN_INFO_OUT}"
bashio::log.info "VPN external IP: ${VPN_IP} (${VPN_COUNTRY})" bashio::log.info "VPN external IP: ${VPN_IP} (${VPN_COUNTRY})"
while true; do while true; do
sleep "${VPN_CHECK_INTERVAL}" sleep "${VPN_CHECK_INTERVAL}" & wait $!
if ! current_out="$(get_ip_info)"; then if ! current_out="$(get_ip_info)"; then
bashio::log.warning "Failed to refresh external IP; keeping previous assumptions." bashio::log.warning "Failed to refresh external IP; keeping previous assumptions."

View File

@@ -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

View File

@@ -40,6 +40,7 @@ _parse_config() {
} }
_parse_dns() { _parse_dns() {
local dns_ip
local -a dns_conf=() local -a dns_conf=()
local -a dns_backup_ipv4=("8.8.8.8" "1.1.1.1") 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") 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 local_ipv6=$(ip addr show ${config["Interface"]} | grep 'inet6 ' | awk '{print $2}' | cut -d'/' -f1)
local ipv4 local ipv4
local ipv6 local ipv6
local dns_ip
# add routing rules for local IPs # add routing rules for local IPs
for ipv4 in ${local_ipv4}; do for ipv4 in ${local_ipv4}; do
config["IPv4Enabled"]="true" 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 from ${ipv4} table ${config["Table"]}" || return 1
_cmd "ip -4 rule add priority 1 to ${ipv4}/24 table ${config["Table"]}" || return 1
done 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 for ipv6 in ${local_ipv6}; do
config["IPv6Enabled"]="true" config["IPv6Enabled"]="true"
_cmd "ip -6 rule add priority 1 from ${ipv6} table ${config["Table"]}" || return 1 _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 done
if [ "${config["IPv6Enabled"]}" = "true" ]; then 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 fi
# get valid DNS servers # 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 route add ${dns_ip} dev ${config["Interface"]}" || return 1
_cmd "ip -6 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1 _cmd "ip -6 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1
done done
# Update resolv.conf with VPN DNS servers
_resolvconf "update"
} }
_routing_del() { _routing_del() {
bashio::log.info "Removing routing rules for VPN interface ${config["Interface"]}..." 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 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 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 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 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 --- # --- WireGuard Specific Logic ---
_wg_wait_handshake() { _wireguard_check() {
local timeout="${1:-20}" local timeout="${1:-20}"
local iface="${config["Interface"]}"
local peer_pk="${config["PublicKey"]}"
local deadline ts local deadline ts
deadline=$(( $(date +%s) + timeout )) deadline=$(( $(date +%s) + timeout ))
while [ "$(date +%s)" -lt "${deadline}" ]; do 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 if [ -n "${ts}" ] && [ "${ts}" -gt 0 ] 2>/dev/null; then
return 0 return 0
fi fi
@@ -220,17 +254,25 @@ _wg_wait_handshake() {
done done
bashio::log.error "WireGuard handshake not established after ${timeout}s (latest-handshake=${ts:-0})." 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 return 1
} }
_wireguard_up() { _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 "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 "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 "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." 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 if [ ! -v config[$key] ] || [ -z "${config[$key]}" ]; then
bashio::log.error "Missing required WireGuard configuration parameter: ${key}" bashio::log.error "Missing required WireGuard configuration parameter: ${key}"
return 1 return 1
@@ -238,28 +280,51 @@ _wireguard_up() {
done done
_cmd "ip link add ${config["Interface"]} type wireguard" || return 1 _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 ' ') mapfile -d ',' -t local_ips < <(echo "${config["Address"]}" | tr -d ' ')
for local_ip in ${local_ips[@]}; do for local_ip in ${local_ips[@]}; do
local result=0 local result=0
_check_host "${local_ip}" || result=$? _check_host "${local_ip}" || result=$?
if [ "${result}" -eq 1 ]; then 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 _cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
elif [ "${result}" -eq 2 ]; then 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 _cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
else else
bashio::log.warning "Ignoring invalid local IP address: ${local_ip}" bashio::log.warning "Ignoring invalid local IP address: ${local_ip}"
fi fi
done done
allowed_ips="${allowed_ips#,}" if [ ${#local_ip_types[@]} -eq 0 ]; then
if [ -z "${allowed_ips}" ]; then bashio::log.error "No valid local IP addresses configured for WireGuard interface."
bashio::log.error "No valid local IP addresses configured."
return 1 return 1
fi 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 _cmd "wg set ${config["Interface"]} listen-port ${config["ListenPort"]} private-key ${config["PrivateKey"]}" || return 1
local endpoint="${config["EndpointIP"]}:${config["EndpointPort"]}" local endpoint="${config["EndpointIP"]}:${config["EndpointPort"]}"
if [[ "${config["EndpointIP"]}" == *:* ]]; then if [[ "${config["EndpointIP"]}" == *:* ]]; then
@@ -279,18 +344,32 @@ _wireguard_up() {
fi fi
_cmd "ip link set ${config["Interface"]} up" || return 1 _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() { _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 set ${config["Interface"]} down" 2>/dev/null || true
_cmd "ip link del ${config["Interface"]}" 2>/dev/null || true _cmd "ip link del ${config["Interface"]}" 2>/dev/null || true
} }
wireguard() { wireguard() {
local mode=$1 local mode=$1
local key
local interface local interface
local config_file local config_file
local WIREGUARD_STATE_DIR="/var/run/wireguard" local WIREGUARD_STATE_DIR="/var/run/wireguard"
@@ -328,7 +407,7 @@ wireguard() {
printf '%s\n' "${config["PrivateKey"]}" > "${WIREGUARD_STATE_DIR}/privatekey" printf '%s\n' "${config["PrivateKey"]}" > "${WIREGUARD_STATE_DIR}/privatekey"
chmod 600 "${WIREGUARD_STATE_DIR}/privatekey" || true chmod 600 "${WIREGUARD_STATE_DIR}/privatekey" || true
config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey" config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey"
if [ -n "${config["PresharedKey"]:-}" ]; then if [ -n "${config["PresharedKey"]:-}" ]; then
printf '%s\n' "${config["PresharedKey"]}" > "${WIREGUARD_STATE_DIR}/presharedkey" printf '%s\n' "${config["PresharedKey"]}" > "${WIREGUARD_STATE_DIR}/presharedkey"
chmod 600 "${WIREGUARD_STATE_DIR}/presharedkey" || true chmod 600 "${WIREGUARD_STATE_DIR}/presharedkey" || true
@@ -384,6 +463,23 @@ wireguard() {
# --- OpenVPN Specific Logic --- # --- 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() { _openvpn_up() {
bashio::log.warning "This script force OpenvPN to ignore any routes and DNS settings pushed by the server." 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"]}" bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}"
@@ -418,21 +514,31 @@ _openvpn_up() {
--route-nopull \ --route-nopull \
--route-noexec" || return 1 --route-noexec" || return 1
#wait for slow OpenVPN interface to come up # Wait for the VPN interface to come up
for i in {1..10}; do _openvpn_check 30 || return 1
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
} }
_openvpn_down() { _openvpn_down() {
# Terminate OpenVPN process # Terminate OpenVPN process
pkill -f "openvpn --config ${config["ConfigFile"]}" || true 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() { openvpn() {
@@ -480,10 +586,10 @@ openvpn() {
bashio::log.info "OpenVPN on interface ${config["Interface"]} is down." bashio::log.info "OpenVPN on interface ${config["Interface"]} is down."
bashio::exit.ok 'OpenVPN stopped.' bashio::exit.ok 'OpenVPN stopped.'
elif [ "${mode}" = "postup" ]; then elif [ "${mode}" = "postup" ]; then
_routing_add _openpvn_postup
bashio::exit.ok 'OpenVPN routes added.' bashio::exit.ok 'OpenVPN routes added.'
elif [ "${mode}" = "postdown" ]; then elif [ "${mode}" = "postdown" ]; then
_routing_del _openpvn_postdown
bashio::exit.ok 'OpenVPN routes deleted.' bashio::exit.ok 'OpenVPN routes deleted.'
else else
bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'." bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'."