mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-06-04 14:54:07 +02:00
Merge pull request #2601 from litinoveweedle/qbittorrent_vpn_upnp
qBittorrent upnp and firewall for VPN
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
|
# && 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"
|
||||||
|
|||||||
@@ -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?
|
||||||
@@ -142,4 +143,4 @@ schema:
|
|||||||
slug: qbittorrent
|
slug: qbittorrent
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "5.1.4-17"
|
version: "5.1.4-18"
|
||||||
|
|||||||
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})"
|
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."
|
||||||
|
|||||||
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() {
|
_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'."
|
||||||
|
|||||||
Reference in New Issue
Block a user