mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-02-05 19:34:52 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}" "$@"
|
||||
363
qbittorrent/rootfs/usr/local/sbin/vpn
Normal file
363
qbittorrent/rootfs/usr/local/sbin/vpn
Normal 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
|
||||
Reference in New Issue
Block a user