mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-04-04 05:10:05 +02:00
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:
@@ -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"
|
||||
|
||||
@@ -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?
|
||||
|
||||
15
qbittorrent/rootfs/etc/cont-init.d/96-vpn-upnp.sh
Normal file
15
qbittorrent/rootfs/etc/cont-init.d/96-vpn-upnp.sh
Normal 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
|
||||
@@ -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."
|
||||
|
||||
265
qbittorrent/rootfs/etc/services.d/vpn-upnp/run
Normal file
265
qbittorrent/rootfs/etc/services.d/vpn-upnp/run
Normal 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
|
||||
@@ -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'."
|
||||
|
||||
Reference in New Issue
Block a user