diff --git a/qbittorrent/config.yaml b/qbittorrent/config.yaml index 432146422..9bbae572b 100644 --- a/qbittorrent/config.yaml +++ b/qbittorrent/config.yaml @@ -130,7 +130,7 @@ schema: localdisks: str? networkdisks: str? openvpn_alt_mode: bool? - openvpn_config: str? + openvpn_config: match(^\w+\.conf$)? openvpn_enabled: bool? openvpn_password: str? openvpn_username: str? @@ -138,7 +138,7 @@ schema: run_duration: str? silent: bool? ssl: bool - wireguard_config: str? + wireguard_config: match(^\w+\.conf$)? wireguard_enabled: bool? whitelist: str? slug: qbittorrent diff --git a/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run b/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run index b0f1a99cf..ec0e73b49 100644 --- a/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run +++ b/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run @@ -10,125 +10,6 @@ if bashio::config.true 'silent'; then sed -i 's|/proc/1/fd/1 hassio;|off;|g' /etc/nginx/nginx.conf fi -# --- WireGuard Specific Logic --- - -_setup_wireguard() { - local WIREGUARD_STATE_DIR="/var/run/wireguard" - local output="" - local status=0 - - if ! bashio::fs.file_exists "${WIREGUARD_STATE_DIR}/config"; then - bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' - fi - - local wireguard_config - wireguard_config="$(cat "${WIREGUARD_STATE_DIR}/config")" - local wireguard_interface - wireguard_interface="$(cat "${WIREGUARD_STATE_DIR}/interface" 2>/dev/null || echo 'wg0')" - - if ip link show "${wireguard_interface}" >/dev/null 2>&1; then - bashio::log.warning "WireGuard interface ${wireguard_interface} already exists. Resetting." - wg-quick down "${wireguard_config}" >/dev/null 2>&1 || true - fi - - bashio::log.info "Starting WireGuard interface ${wireguard_interface}..." - - # Internal helper: fallback for iptables-legacy - _wg_prepare_legacy() { - local legacy_bin_dir="${WIREGUARD_STATE_DIR}/iptables-legacy-bin" - mkdir -p "${legacy_bin_dir}" - local cmd - for cmd in iptables iptables-save iptables-restore ip6tables ip6tables-save ip6tables-restore; do - if command -v "${cmd}-legacy" >/dev/null 2>&1; then - ln -sf "$(command -v "${cmd}-legacy")" "${legacy_bin_dir}/${cmd}" - fi - done - chmod 700 "${legacy_bin_dir}" 2>/dev/null || true - export PATH="${legacy_bin_dir}:${PATH}" - bashio::log.warning 'Retrying WireGuard using iptables-legacy wrappers.' - } - - # Internal helper: Attempt connection - _wg_up_attempt() { - local config_path="$1" - output="$(wg-quick up "${config_path}" 2>&1)" || status=$? - - if [ "${status}" -eq 0 ]; then return 0; fi - - # Allow sysctl failures on read-only hosts while keeping the interface up - if echo "${output}" | grep -qi 'net\.ipv4\.conf\.all\.src_valid_mark=1'; then - if echo "${output}" | grep -qiE 'read-only file system|operation not permitted'; then - if ip link show "${wireguard_interface}" >/dev/null 2>&1; then - bashio::log.warning 'WireGuard applied but sysctl net.ipv4.conf.all.src_valid_mark=1 could not be set (read-only). Continuing.' - status=0 - return 0 - fi - fi - fi - - # Check for iptables errors and try legacy fallback - if echo "${output}" | grep -qiE 'iptables-restore|ip6tables-restore|xtables'; then - if command -v iptables-legacy >/dev/null 2>&1; then - wg-quick down "${config_path}" >/dev/null 2>&1 || true - _wg_prepare_legacy - output="$(wg-quick up "${config_path}" 2>&1)" || status=$? - else - bashio::log.warning 'iptables errors detected but iptables-legacy missing.' - status=1 - fi - fi - return "${status}" - } - - # 1. First Attempt - if ! _wg_up_attempt "${wireguard_config}"; then - bashio::log.warning 'Initial WireGuard connection failed. Trying IPv4-only endpoints.' - bashio::log.debug "Output: ${output}" - - # 2. IPv4 Fallback Preparation - local ipv4_config="${WIREGUARD_STATE_DIR}/${wireguard_interface}-ipv4.conf" - : > "${ipv4_config}" - chmod 600 "${ipv4_config}" 2>/dev/null || true - - local line endpoint endpoint_host endpoint_port - while IFS= read -r line || [ -n "$line" ]; do - if [[ "${line}" =~ ^Endpoint ]]; then - endpoint="${line#Endpoint = }" - endpoint_host="${endpoint%:*}" - endpoint_port="${endpoint##*:}" - - # Resolve hostname to IPv4 - mapfile -t ipv4_candidates < <(getent ahostsv4 "${endpoint_host}" | awk '{print $1}' | uniq) - - if [ ${#ipv4_candidates[@]} -gt 0 ]; then - bashio::log.debug "Resolved ${endpoint_host} to ${ipv4_candidates[0]}" - echo "Endpoint = ${ipv4_candidates[0]}:${endpoint_port}" >> "${ipv4_config}" - else - echo "${line}" >> "${ipv4_config}" - fi - else - echo "${line}" >> "${ipv4_config}" - fi - done < "${wireguard_config}" - - wg-quick down "${wireguard_config}" >/dev/null 2>&1 || true - - # 3. Second Attempt (IPv4 only) - if ! _wg_up_attempt "${ipv4_config}"; then - bashio::log.error 'WireGuard failed to establish connection.' - bashio::log.error "${output}" - bashio::exit.nok 'WireGuard start failed.' - fi - fi - - bashio::log.info "WireGuard interface ${wireguard_interface} is up." - - # DNS Refresh - if command -v resolvconf >/dev/null 2>&1; then - resolvconf -u >/dev/null 2>&1 || bashio::log.warning 'resolvconf -u failed.' - fi -} - # --- Main Execution --- openvpn_enabled=false @@ -148,24 +29,9 @@ if [[ "${openvpn_enabled}" == true && "${wireguard_enabled}" == true ]]; then fi if [[ "${openvpn_enabled}" == true ]]; then - - exec /usr/sbin/openvpn \ - --config /config/openvpn/config.ovpn \ - --script-security 2 \ - --up /etc/openvpn/up.sh \ - --down /etc/openvpn/down.sh \ - --pull-filter ignore "route-ipv6" \ - --pull-filter ignore "ifconfig-ipv6" \ - --pull-filter ignore "tun-ipv6" \ - --pull-filter ignore "redirect-gateway ipv6" \ - --pull-filter ignore "dhcp-option DNS6" \ - & - + /usr/local/sbin/vpn openvpn up elif [[ "${wireguard_enabled}" == true ]]; then - - # Run modularized WireGuard setup - _setup_wireguard - + /usr/local/sbin/vpn wireguard up fi # --- Launch qBittorrent --- diff --git a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run index 5117c5fa3..4ed9f4812 100755 --- a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run +++ b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run @@ -129,13 +129,6 @@ if bashio::config.true 'openvpn_enabled'; then vpn_openvpn=true fi -if [[ "${vpn_openvpn}" == true ]] && ! bashio::config.true 'openvpn_alt_mode'; then - VPN_INTERFACE="tun0" - bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." -else - VPN_INTERFACE="" -fi - if bashio::config.true 'wireguard_enabled'; then vpn_wireguard=true fi @@ -151,6 +144,16 @@ if [[ "${vpn_openvpn}" == true && "${vpn_wireguard}" == true ]]; then exit 1 fi +if [[ "${vpn_openvpn}" == true ]] && ! bashio::config.true 'openvpn_alt_mode'; then + VPN_INTERFACE=$(cat "/var/run/openvpn/interface") + bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." +elif [[ "${vpn_wireguard}" == true ]]; then + VPN_INTERFACE=$(cat "/var/run/wireguard/interface") + bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." +else + VPN_INTERFACE="" +fi + REAL_IP="$(read_real_ip)" if [[ -n "${REAL_IP}" ]]; then diff --git a/qbittorrent/rootfs/usr/local/bin/resolvconf b/qbittorrent/rootfs/usr/local/bin/resolvconf deleted file mode 100644 index 3e2a91880..000000000 --- a/qbittorrent/rootfs/usr/local/bin/resolvconf +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh -set -eu - -STATE_DIR="/var/run/wireguard/resolvconf" -BACKUP_FILE="${STATE_DIR}/resolv.conf.backup" - -mkdir -p "${STATE_DIR}" - -if [ "$#" -eq 0 ]; then - exit 0 -fi - -command="$1" -shift || true - -restore_backup() { - if [ -f "${BACKUP_FILE}" ]; then - cat "${BACKUP_FILE}" > /etc/resolv.conf - fi -} - -apply_dns() { - iface="$1" - shift || true - - # Skip optional arguments such as -m or -x - while [ "$#" -gt 0 ]; do - case "$1" in - -m|-p|-w) - shift 2 || true - ;; - -x|-y|-Z) - shift 1 || true - ;; - --) - shift - break - ;; - *) - break - ;; - esac - done - - tmp_file="${STATE_DIR}/${iface}.conf" - cat > "${tmp_file}" - - if [ ! -f "${BACKUP_FILE}" ]; then - cp /etc/resolv.conf "${BACKUP_FILE}" 2>/dev/null || true - fi - - { - echo "# Generated by WireGuard add-on resolvconf shim" - cat "${tmp_file}" - } > /etc/resolv.conf -} - -case "${command}" in - -a) - if [ "$#" -eq 0 ]; then - exit 0 - fi - apply_dns "$@" - ;; - -d) - if [ "$#" -gt 0 ]; then - rm -f "${STATE_DIR}/$1.conf" - fi - restore_backup - ;; - -u) - latest_conf="$(find "${STATE_DIR}" -maxdepth 1 -type f -name '*.conf' -print | head -n 1 || true)" - if [ -n "${latest_conf}" ] && [ -f "${latest_conf}" ]; then - { - echo "# Generated by WireGuard add-on resolvconf shim" - cat "${latest_conf}" - } > /etc/resolv.conf - else - restore_backup - fi - ;; - *) - # Treat other commands as successful no-ops to remain compatible with wg-quick. - exit 0 - ;; -esac diff --git a/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore b/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore deleted file mode 100644 index 23bff73d3..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_IP6TABLES_RESTORE="/sbin/ip6tables-restore" -if [[ ! -x "${REAL_IP6TABLES_RESTORE}" ]]; then - REAL_IP6TABLES_RESTORE="/usr/sbin/ip6tables-restore" -fi - -cleanup() { - local exit_code=$? - [[ -n "${RULES_FILE:-}" && -f "${RULES_FILE}" ]] && rm -f "${RULES_FILE}" - [[ -n "${SANITIZED_FILE:-}" && -f "${SANITIZED_FILE}" ]] && rm -f "${SANITIZED_FILE}" - return $exit_code -} -trap cleanup EXIT - -RULES_FILE="$(mktemp)" -cat > "${RULES_FILE}" - -ipv6_unavailable() { - local message="$1" - [[ $message =~ [Tt]able[[:space:]]does[[:space:]]not[[:space:]]exist ]] && return 0 - [[ $message =~ address[[:space:]]family[[:space:]]not[[:space:]]supported ]] && return 0 - [[ $message =~ can[[:punct:]]t[[:space:]]initialize[[:space:]]ip6tables[[:space:]]table ]] && return 0 - [[ $message =~ IPv6[[:space:]]support[[:space:]]not[[:space:]]available ]] && return 0 - return 1 -} - -# First attempt with the original ruleset -output="" -if output="$(${REAL_IP6TABLES_RESTORE} "$@" < "${RULES_FILE}" 2>&1)"; then - [[ -n "${output}" ]] && printf '%s\n' "${output}" >&2 - exit 0 -fi -status=$? - -# Retry without comment matches if the kernel is missing the comment module -SANITIZED_FILE="$(mktemp)" -sed -E 's/-m[[:space:]]+comment[[:space:]]+--comment[[:space:]]+"[^"]*"//g' "${RULES_FILE}" > "${SANITIZED_FILE}" - -retry_output="" -if retry_output="$(${REAL_IP6TABLES_RESTORE} "$@" < "${SANITIZED_FILE}" 2>&1)"; then - printf '%s\n' "ip6tables-restore failed with comment matches; reapplied without comments." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "${retry_output}" >&2 - exit 0 -fi -retry_status=$? - -# Final fallback: try legacy backend if available -legacy_output="" -for legacy in /sbin/ip6tables-restore-legacy /usr/sbin/ip6tables-restore-legacy; do - if [[ -x "${legacy}" ]]; then - if legacy_output="$(${legacy} "$@" < "${RULES_FILE}" 2>&1)"; then - printf '%s\n' "ip6tables-restore failed; succeeded using legacy backend." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "${legacy_output}" >&2 - exit 0 - fi - fi - -done - -if ipv6_unavailable "${output}" || ipv6_unavailable "${retry_output}" || ipv6_unavailable "${legacy_output}"; then - printf '%s\n' "IPv6 firewall support not detected; skipping IPv6 ruleset restore and continuing." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "Legacy backend error: ${legacy_output}" >&2 - exit 0 -fi - -printf '%s\n' "ip6tables-restore failed and fallbacks were unsuccessful." >&2 -printf '%s\n' "Original error: ${output}" >&2 -printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 -exit ${retry_status} diff --git a/qbittorrent/rootfs/usr/local/sbin/iptables-restore b/qbittorrent/rootfs/usr/local/sbin/iptables-restore deleted file mode 100644 index 2219b563c..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/iptables-restore +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_IPTABLES_RESTORE="/sbin/iptables-restore" -if [[ ! -x "${REAL_IPTABLES_RESTORE}" ]]; then - REAL_IPTABLES_RESTORE="/usr/sbin/iptables-restore" -fi - -cleanup() { - local exit_code=$? - [[ -n "${RULES_FILE:-}" && -f "${RULES_FILE}" ]] && rm -f "${RULES_FILE}" - [[ -n "${SANITIZED_FILE:-}" && -f "${SANITIZED_FILE}" ]] && rm -f "${SANITIZED_FILE}" - return $exit_code -} -trap cleanup EXIT - -RULES_FILE="$(mktemp)" -cat > "${RULES_FILE}" - -# First attempt with the original ruleset -if output="$(${REAL_IPTABLES_RESTORE} "$@" < "${RULES_FILE}" 2>&1)"; then - [[ -n "${output}" ]] && printf '%s\n' "${output}" >&2 - exit 0 -fi -status=$? - -# Retry without comment matches if the kernel is missing the comment module -SANITIZED_FILE="$(mktemp)" -sed -E 's/-m[[:space:]]+comment[[:space:]]+--comment[[:space:]]+"[^"]*"//g' "${RULES_FILE}" > "${SANITIZED_FILE}" - -if retry_output="$(${REAL_IPTABLES_RESTORE} "$@" < "${SANITIZED_FILE}" 2>&1)"; then - printf '%s\n' "iptables-restore failed with comment matches; reapplied without comments." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "${retry_output}" >&2 - exit 0 -fi -retry_status=$? - -# Final fallback: try legacy backend if available -for legacy in /sbin/iptables-restore-legacy /usr/sbin/iptables-restore-legacy; do - if [[ -x "${legacy}" ]]; then - if legacy_output="$(${legacy} "$@" < "${RULES_FILE}" 2>&1)"; then - printf '%s\n' "iptables-restore failed; succeeded using legacy backend." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "${legacy_output}" >&2 - exit 0 - fi - fi -done - -printf '%s\n' "iptables-restore failed and fallbacks were unsuccessful." >&2 -printf '%s\n' "Original error: ${output}" >&2 -printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 -exit ${retry_status} diff --git a/qbittorrent/rootfs/usr/local/sbin/sysctl b/qbittorrent/rootfs/usr/local/sbin/sysctl deleted file mode 100644 index b76c18c88..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/sysctl +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_SYSCTL="/sbin/sysctl" -if [[ ! -x "${REAL_SYSCTL}" ]]; then - REAL_SYSCTL="/usr/sbin/sysctl" -fi - -if [[ "$#" -ge 2 && "$1" == "-q" && "$2" == "net.ipv4.conf.all.src_valid_mark=1" ]]; then - if "${REAL_SYSCTL}" "$@" >/dev/null 2>&1; then - exit 0 - fi - # Suppress failure for this specific key to keep wg-quick from aborting in unprivileged environments. - exit 0 -fi - -exec "${REAL_SYSCTL}" "$@" diff --git a/qbittorrent/rootfs/usr/local/sbin/vpn b/qbittorrent/rootfs/usr/local/sbin/vpn new file mode 100644 index 000000000..b3ac0f7fd --- /dev/null +++ b/qbittorrent/rootfs/usr/local/sbin/vpn @@ -0,0 +1,363 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bas + +# --- WireGuard Specific Logic --- + +declare -A config +config["MySelf"]="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" +IFS=',' read -ra dns_servers <<< $(bashio::config 'DNS_server' | tr -d ' ') +config["DnsServers"]="${dns_servers[*]}" + +_parse_config() { + local -n config_ref="$1" + local config_file="$2" + + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip comments and empty lines + [[ "$line" =~ ^[#!] ]] && continue + # Extract key and value using regex (trim spaces) + #if [[ "$line" =~ ^[[:space:]]*([^ =]+)[[:space:]]*=[[:space:]]*(.*)[[:space:]]* ]]; then + if [[ "$line" =~ ^[[:space:]]*([^=[:space:]]+)[=[:space:]]+(.*)[[:space:]]* ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + config_ref["$key"]="$value" + fi + done < "$config_file" +} + +_cmd() { + cmd="$1" + bashio::log.info "Executing command: ${cmd}" + eval "${cmd}" +} + +_is_ip_address() { + if [ "$1" != "${1#*[0-9].[0-9]}" ]; then + return 1 # IPv4 + elif [ "$1" != "${1#*:[0-9a-fA-F]}" ]; then + return 2 # IPv6 + else + return 0 # Not an IP address + fi +} + +_resolvconf() { + local mode=$1 + local resolv_conf="/etc/resolv.conf" + local resolv_backup="/etc/resolv.conf.bak" + + if [ "${mode}" = "reset" ]; then + bashio::log.info "Resetting ${resolv_conf} to default DNS servers." + if bashio::fs.file_exists "${resolv_backup}"; then + cp "${resolv_backup}" "${resolv_conf}" + else + bashio::log.warning "No original resolv.conf backup found. Leaving as is." + fi + elif [ "${mode}" = "update" ]; then + bashio::log.info "Updating ${resolv_conf} with DNS servers: ${config["DnsServers"]}" + if ! bashio::fs.file_exists "${resolv_backup}"; then + cp "${resolv_conf}" "${resolv_backup}" 2>/dev/null || true + fi + { + local dns_ip + echo "# Generated by addon VPN script" + for dns_ip in ${config["DnsServers"]}; do + _is_ip_address "${dns_ip}" + local is_ip=$? + if [ "${is_ip}" -eq 0 ]; then + bashio::log.warning "Ignoring invalid DNS server address: ${dns_ip}" + continue + else + echo "nameserver ${dns_ip}" + fi + done + } > "${resolv_conf}" + fi +} + +_resolve_hostname() { + local hostname=$1 + local ips="" + + # Resolve hostname to IPv6 + mapfile -t ipv6_candidates < <(getent ahostsv6 "${hostname}" | awk '{print $1}' | uniq) + + # Resolve hostname to IPv4 + mapfile -t ipv4_candidates < <(getent ahostsv4 "${hostname}" | awk '{print $1}' | uniq) + + if [ ${#ipv6_candidates[@]} -gt 0 ]; then + bashio::log.debug "Resolved ${hostname} to ${ipv6_candidates[@]}" + ips=${ipv6_candidates[@]} + fi + + if [ ${#ipv4_candidates[@]} -gt 0 ]; then + bashio::log.debug "Resolved ${hostname} to ${ipv4_candidates[@]}" + ips="${ips} ${ipv4_candidates[@]}" + fi + + return $ips +} + +_routing_add() { + 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) + 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 ${local_ip} table ${config["Table"]}" || return 1 + done + for ipv6 in ${local_ipv6}; do + config["IPv6Enabled"]="true" + _cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1 + _cmd "ip -6 rule add priority 1 from ${local_ip} table ${config["Table"]}" || return 1 + done + + local dns_ip + for dns_ip in ${config["DnsServers"]}; do + _is_ip_address "${dns_ip}" + local is_ip=$? + if [ "${is_ip}" -eq 0 ]; then + bashio::log.warning "Ignoring invalid DNS server address: ${dns_ip}" + continue + elif [ "${is_ip}" -eq 1 ] && [ ${config["IPv4Enabled"]} = "true" ]; then + #_cmd "ip -4 route add ${dns_ip} dev ${config["Interface"]}" || return 1 + _cmd "ip -4 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1 + elif [ "${is_ip}" -eq 2 ] && [ "${config["IPv6Enabled"]}" = "true" ]; then + #_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 + else + bashio::log.warning "Failed to add route for DNS server: ${dns_ip}" + fi + done + _resolvconf "update" +} + +_routing_del() { + _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 + while _cmd "ip -6 rule del priority 1 from all table ${config["Table"]} 2>/dev/null"; do :; done + while _cmd "ip -6 rule del priority 1 to all 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 +} + +_wireguard_up() { + bashio::log.warn "Bringing up Wireguard interface on ${config["Interface"]}..." + bashio::log.warn "Using Wireguard configuration file: ${config["ConfigFile"]}" + bashio::log.warn "This script force Wireguard to ignore any routes and DNS settings." + bashio::log.warn "Default route will be inserted into custom routing table: ${config["Table"]}" + bashio::log.warn "This routing table will be used for traffic from the VPN interface and to configured DNS servers." + bashio::log.warn "Qbittorrent bittorrent client shall be set to use the VPN interface ${config["Interface"]} only." + + _cmd "ip link add dev ${config["Interface"]} type wireguard" || return 1 + local allowed_ips="" + for local_ip in ${config["Address"]}; do + _is_ip_address "${local_ip}" + local is_ip=$? + if [ "${is_ip}" -eq 1 ]; then + allowed_ips="${allowed_ips},0.0.0.0/0" + _cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1 + elif [ "${is_ip}" -eq 2 ]; then + allowed_ips="${allowed_ips},::/0" + _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." + return 1 + fi + _cmd "wg set ${config["Interface"]} listen-port ${config["ListenPort"]} private-key ${config["PrivateKey"]}" || return 1 + _cmd "wg set ${config["Interface"]} peer ${config["PublicKey"]} endpoint ${config["EndpointIP"]}:${config["EndpointPort"]} allowed-ips ${allowed_ips}" || return 1 + if [ -n "${config["PersistentKeepalive"]}" ]; then + _cmd "wg set ${config["Interface"]} peer ${config["PublicKey"]} persistent-keepalive ${config["PersistentKeepalive"]}" || return 1 + fi + _cmd "ip link set ${config["Interface"]} up" || return 1 + _routing_add +} + +_wireguard_down() { + _routing_del + _cmd "ip link set ${config["Interface"]} down" 2>/dev/null || true + _cmd "ip link del dev ${config["Interface"]}" 2>/dev/null || true +} + +wireguard() { + local mode=$1 + local interface + local config_file + local WIREGUARD_STATE_DIR="/var/run/wireguard" + + if ! bashio::fs.file_exists "${WIREGUARD_STATE_DIR}/interface"; then + bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' + fi + interface=$(cat "${WIREGUARD_STATE_DIR}/interface") + if [ -z "${interface}" ]; then + bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' + fi + if ! bashio::fs.file_exists "${WIREGUARD_STATE_DIR}/config"; then + bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' + fi + config_file=$(cat "${WIREGUARD_STATE_DIR}/config") + if [ -z "${config_file}" ]; then + bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' + fi + + _parse_config config "${config_file}" + config["Interface"]="${interface}" + config["ConfigFile"]="${config_file}" + config["Table"]="${config["Table"]:-1000}" + config["ListenPort"]="${config["ListenPort"]:-51820}" + config["EndpointHost"]="${config["Endpoint"]%:*}" + config["EndpointPort"]="${config["Endpoint"]##*:}" + config["IPv4Enabled"]="false" + config["IPv6Enabled"]="false" + for key in "${!config[@]}"; do + bashio::log.debug "${key}: ${config[$key]}" + done + + echo ${config["PrivateKey"]} > ${WIREGUARD_STATE_DIR}/privatekey + config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey" + + if [ "${mode}" = "up" ]; then + bashio::log.info "Starting WireGuard interface ${config["Interface"]}..." + if _is_ip_address ${config["EndpointHost"]}; then + local endpoint_ips=$(_resolve_hostname ${config["EndpointHost"]}) + if [ ${#endpoint_ips[@]} -eq 0 ]; then + bashio::log.error "Failed to resolve WireGuard endpoint hostname: ${config["EndpointHost"]}" + bashio::exit.nok 'WireGuard start failed.' + fi + for endpoint_ip in "${endpoint_ips[@]}"; do + bashio::log.info "Resolved WireGuard endpoint hostname ${config["EndpointHost"]} to IP: ${endpoint_ip}" + config["EndpointIP"]="${endpoint_ip}" + if _wireguard_up; then + bashio::log.info "WireGuard interface ${config["Interface"]} is up." + bashio::exit.ok 'WireGuard started.' + fi + bashio::log.error 'WireGuard failed to establish connection.' + _wireguard_down + done + else + bashio::log.info "WireGuard endpoint ${config["EndpointHost"]} is a valid IP address. Using as is." + config["EndpointIP"]="${config["EndpointHost"]}" + if _wireguard_up; then + bashio::log.info "WireGuard interface ${config["Interface"]} is up." + bashio::exit.ok 'WireGuard started.' + fi + bashio::log.error 'WireGuard failed to establish connection.' + _wireguard_down + fi + elif [ "${mode}" = "down" ]; then + bashio::log.info "Stopping WireGuard interface ${config["Interface"]}..." + _wireguard_down + bashio::log.info "WireGuard interface ${config["Interface"]} is down." + bashio::exit.ok 'WireGuard stopped.' + else + bashio::log.error "Invalid WireGuard mode specified. Use 'up' or 'down'." + bashio::exit.nok 'WireGuard start failed.' + fi + + bashio::exit.nok 'WireGuard start failed.' +} + +_openvpn_up() { + bashio::log.warn "Bringing up OpenVPN interface on ${config["Interface"]}..." + bashio::log.warn "Using OpenVPN configuration file: ${config["ConfigFile"]}" + bashio::log.warn "This script force OpenvPN to ignore any routes and DNS settings pushed by the server." + bashio::log.warn "Default route will be inserted into custom routing table: ${config["Table"]}" + bashio::log.warn "This routing table will be used for traffic from the VPN interface and to configured DNS servers." + bashio::log.warn "Qbittorrent bittorrent client shall be set to use the VPN interface ${config["Interface"]} only." + + # Register this script as OpenVPN up/down handlers to manage routing + echo "${config["MySelf"]} openvpn postup" > ${config["PostUpScript"]} + chmod 755 ${config["PostUpScript"]} + echo "${config["MySelf"]} openvpn postdown" > ${config["PostDownScript"]} + chmod 755 ${config["PostDownScript"]} + + # Start OpenVPN in the background + # (maybe use setsid instead of nohup to detach completely?) + nohup /usr/sbin/openvpn \ + --config "${config["ConfigFile"]}" \ + --script-security 2 \ + --up ${config["PostUpScript"]} \ + --down ${config["PostDownScript"]} \ + --route-nopull \ + --pull-filter ignore "route" \ + --pull-filter ignore "redirect-gateway" \ + --pull-filter ignore "dhcp-option DNS" \ + --pull-filter ignore "route-ipv6" \ + --pull-filter ignore "redirect-gateway ipv6" \ + --pull-filter ignore "dhcp-option DNS6" \ + & +} + +_openvpn_down() { + # Terminate OpenVPN process + pkill -f "openvpn --config ${config["ConfigFile"]}" || true + _routing_del +} + +openvpn() { + local mode=$1 + local interface + local config_file + local OPENVPN_STATE_DIR="/var/run/openvpn" + + if ! bashio::fs.file_exists "${OPENVPN_STATE_DIR}/interface"; then + bashio::exit.nok 'OpenVPN runtime configuration not prepared. Please restart the add-on.' + fi + interface=$(cat "${OPENVPN_STATE_DIR}/interface") + if [ -z "${interface}" ]; then + bashio::exit.nok 'OpenVPN runtime configuration not prepared. Please restart the add-on.' + fi + if ! bashio::fs.file_exists "${OPENVPN_STATE_DIR}/config"; then + bashio::exit.nok 'OpenVPN runtime configuration not prepared. Please restart the add-on.' + fi + config_file=$(cat "${OPENVPN_STATE_DIR}/config") + if [ -z "${config_file}" ]; then + bashio::exit.nok 'OpenVPN runtime configuration not prepared. Please restart the add-on.' + fi + + _parse_config config "${config_file}" + config["Interface"]="${interface}" + config["ConfigFile"]="${config_file}" + config["Table"]="${config["Table"]:-1000}" + config["PostUpScript"]="${OPENVPN_STATE_DIR}/up.sh" + config["PostDownScript"]="${OPENVPN_STATE_DIR}/down.sh" + + if [ "${mode}" = "up" ]; then + # register up and down scripts + bashio::log.info "Starting OpenVPN with configuration file: ${config_file}..." + _openvpn_up + bashio::exit.ok 'OpenVPN started.' + elif [ "${mode}" = "down" ]; then + bashio::log.info "Stopping OpenVPN..." + _openvpn_down + bashio::exit.ok 'OpenVPN stopped.' + elif [ "${mode}" = "postup" ]; then + _routing_add + elif [ "${mode}" = "postdown" ]; then + _routing_del + else + bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'." + bashio::exit.nok 'OpenVPN start failed.' + fi + bashio::log.info "Starting OpenVPN with configuration file: ${config_file}" +} + + +if [ $# -ne 2 ]; then + bashio::log.error "Invalid number of arguments. Usage: vpn.sh " + bashio::exit.nok 'VPN start failed.' +fi +if [[ "$1" == "wireguard" ]]; then + wireguard "$2" +elif [[ "$1" == "openvpn" ]]; then + openvpn "$2" +else + bashio::log.error "Invalid VPN type specified. Use 'wireguard' or 'openvpn'." + bashio::exit.nok 'VPN start failed.' +fi