initial VPN logic implementation

both Wireguard and OpenVPN are  now handled by single service file
This remove dependency to external tools
Simplify vpn routing by using dedicated routing table which is used by qbittorrent torrent client listenning directly on the vpn interface.
To prevent DNS leeks traffic to addon configured DNS servers is forced to use same dedicated routing table
This commit is contained in:
litinoveweedle
2026-01-25 09:21:49 +01:00
parent 18f6519f00
commit 47a43c82b4
8 changed files with 377 additions and 377 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}" "$@"

View File

@@ -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 <wireguard|openvpn> <up|down>"
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