mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-03-29 01:02:33 +01:00
This is implementation of the UPnP port opening for qBittorrent running on VPN. Implementation also includes simple firewall for incoming connections.
616 lines
24 KiB
Plaintext
Executable File
616 lines
24 KiB
Plaintext
Executable File
#!/usr/bin/with-contenv bashio
|
|
# shellcheck shell=bash
|
|
|
|
# --- Common Functions ---
|
|
|
|
declare -A config
|
|
config["MySelf"]="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
|
|
declare -a dns_servers_ipv4=()
|
|
declare -a dns_servers_ipv6=()
|
|
|
|
log_level="$(bashio::config "log_level")"
|
|
[[ "$log_level" == "debug" ]] && bashio::log.warning "--- Debug mode is active ---"
|
|
[[ "$log_level" == "debug" ]] && set -x
|
|
|
|
_parse_config() {
|
|
local -n config_ref="$1"
|
|
local config_file="$2"
|
|
local line
|
|
|
|
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
|
|
local key="${BASH_REMATCH[1]}"
|
|
local value="${BASH_REMATCH[2]}"
|
|
if [[ "$key" == "Address" ]]; then
|
|
if [[ -n "${config_ref["Address"]:-}" ]]; then
|
|
config_ref["Address"]+=",${value}"
|
|
else
|
|
config_ref["Address"]="${value}"
|
|
fi
|
|
else
|
|
config_ref["$key"]="$value"
|
|
fi
|
|
fi
|
|
done < "$config_file"
|
|
}
|
|
|
|
_parse_dns() {
|
|
local dns_ip
|
|
local -a dns_conf=()
|
|
local -a dns_backup_ipv4=("8.8.8.8" "1.1.1.1")
|
|
local -a dns_backup_ipv6=("2001:4860:4860::8888" "2606:4700:4700::1111")
|
|
local dns_servers=$(bashio::config 'DNS_server')
|
|
|
|
mapfile -d ',' -t dns_conf < <(echo "${dns_servers}" | tr -d ' ' | tr -d '\n')
|
|
if [ ${config["IPv4Enabled"]} = "true" ]; then
|
|
for dns_ip in "${dns_conf[@]}"; do
|
|
local result=0
|
|
_check_host "${dns_ip}" || result=$?
|
|
if [ "${result}" -eq 1 ]; then
|
|
dns_servers_ipv4+=("${dns_ip}")
|
|
fi
|
|
done
|
|
if [ ${#dns_servers_ipv4[@]} -eq 0 ]; then
|
|
bashio::log.warning "No valid IPv4 DNS servers configured. Using addon defaults ${dns_backup_ipv4[@]}"
|
|
dns_servers_ipv4=("${dns_backup_ipv4[@]}")
|
|
fi
|
|
fi
|
|
if [ ${config["IPv6Enabled"]} = "true" ]; then
|
|
for dns_ip in "${dns_conf[@]}"; do
|
|
local result=0
|
|
_check_host "${dns_ip}" || result=$?
|
|
if [ "${result}" -eq 2 ]; then
|
|
dns_servers_ipv6+=("${dns_ip}")
|
|
fi
|
|
done
|
|
if [ ${#dns_servers_ipv6[@]} -eq 0 ]; then
|
|
bashio::log.warning "No valid IPv6 DNS servers configured. Using addon defaults ${dns_backup_ipv6[@]}"
|
|
dns_servers_ipv6=("${dns_backup_ipv6[@]}")
|
|
fi
|
|
fi
|
|
}
|
|
|
|
_cmd() {
|
|
cmd="$1"
|
|
bashio::log.debug "Executing command: ${cmd}"
|
|
eval "${cmd}"
|
|
}
|
|
|
|
_check_host() {
|
|
if ipcalc -c -4 "$1" >/dev/null 2>&1; then
|
|
return 1 # IPv4
|
|
elif ipcalc -c -6 "$1" >/dev/null 2>&1; then
|
|
return 2 # IPv6
|
|
elif getent ahosts "$1" >/dev/null 2>&1; then
|
|
return 3 # resolvable hostnamee
|
|
else
|
|
return 0 # neither IP nor resolvable hostname
|
|
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 VPN DNS servers."
|
|
if ! bashio::fs.file_exists "${resolv_backup}"; then
|
|
bashio::log.debug "Creating backup of original resolv.conf at ${resolv_backup}"
|
|
cp "${resolv_conf}" "${resolv_backup}" 2>/dev/null || true
|
|
fi
|
|
bashio::log.debug "Updating ${resolv_conf} with DNS servers: ${dns_servers_ipv4[*]} ${dns_servers_ipv6[*]}"
|
|
{
|
|
echo "# Generated by vpn script"
|
|
local dns_ip
|
|
for dns_ip in ${dns_servers_ipv4[@]} ${dns_servers_ipv6[@]}; do
|
|
echo "nameserver ${dns_ip}"
|
|
done
|
|
} > "${resolv_conf}"
|
|
else
|
|
bashio::exit.nok "Invalid resolvconf mode specified. Use 'update' or 'reset'."
|
|
fi
|
|
}
|
|
|
|
_resolve_hostname() {
|
|
local hostname=$1
|
|
local -a ips=()
|
|
local -a ipv4_candidates=()
|
|
local -a ipv6_candidates=()
|
|
|
|
# Resolve hostname to IPv4
|
|
mapfile -t ipv4_candidates < <(getent ahostsv4 "${hostname}" | awk '{print $1}' | uniq)
|
|
|
|
# Resolve hostname to IPv6
|
|
mapfile -t ipv6_candidates < <(getent ahostsv6 "${hostname}" | awk '{print $1}' | uniq)
|
|
|
|
if [ ${#ipv4_candidates[@]} -gt 0 ]; then
|
|
bashio::log.debug "Resolved ${hostname} to ${ipv4_candidates[@]}"
|
|
ips+=("${ipv4_candidates[@]}")
|
|
fi
|
|
|
|
if [ ${#ipv6_candidates[@]} -gt 0 ]; then
|
|
bashio::log.debug "Resolved ${hostname} to ${ipv6_candidates[@]}"
|
|
ips+=("${ipv6_candidates[@]}")
|
|
fi
|
|
|
|
echo "${ips[@]}"
|
|
}
|
|
|
|
_routing_add() {
|
|
bashio::log.info "Adding routing rules for VPN interface ${config["Interface"]}..."
|
|
|
|
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)
|
|
local ipv4
|
|
local ipv6
|
|
local dns_ip
|
|
|
|
# add routing rules for local IPs
|
|
for ipv4 in ${local_ipv4}; do
|
|
config["IPv4Enabled"]="true"
|
|
_cmd "ip -4 rule add priority 1 from ${ipv4} table ${config["Table"]}" || return 1
|
|
_cmd "ip -4 rule add priority 1 to ${ipv4}/24 table ${config["Table"]}" || return 1
|
|
done
|
|
if [ "${config["IPv4Enabled"]}" = "true" ]; then
|
|
_cmd "ip -4 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1
|
|
fi
|
|
for ipv6 in ${local_ipv6}; do
|
|
config["IPv6Enabled"]="true"
|
|
_cmd "ip -6 rule add priority 1 from ${ipv6} table ${config["Table"]}" || return 1
|
|
_cmd "ip -6 rule add priority 1 to ${ipv6}/64 table ${config["Table"]}" || return 1
|
|
done
|
|
if [ "${config["IPv6Enabled"]}" = "true" ]; then
|
|
_cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1
|
|
fi
|
|
|
|
# get valid DNS servers
|
|
_parse_dns
|
|
|
|
# add routing rules for DNS servers
|
|
for dns_ip in "${dns_servers_ipv4[@]}"; do
|
|
#_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
|
|
done
|
|
for dns_ip in "${dns_servers_ipv6[@]}"; do
|
|
#_cmd "ip -6 route add ${dns_ip} dev ${config["Interface"]}" || return 1
|
|
_cmd "ip -6 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1
|
|
done
|
|
}
|
|
|
|
_routing_del() {
|
|
bashio::log.info "Removing routing rules for VPN interface ${config["Interface"]}..."
|
|
|
|
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
|
|
}
|
|
|
|
# --- 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_check() {
|
|
local timeout="${1:-20}"
|
|
local deadline ts
|
|
|
|
deadline=$(( $(date +%s) + timeout ))
|
|
|
|
while [ "$(date +%s)" -lt "${deadline}" ]; do
|
|
ping -I "${config["Interface"]}" -c1 -W1 1.1.1.1 >/dev/null 2>&1 || true
|
|
|
|
ts="$(wg show "${config["Interface"]}" latest-handshakes 2>/dev/null | awk -v pk="${config["PublicKey"]}" '$1==pk{print $2; exit}')"
|
|
if [ -n "${ts}" ] && [ "${ts}" -gt 0 ] 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
bashio::log.error "WireGuard handshake not established after ${timeout}s (latest-handshake=${ts:-0})."
|
|
wg show "${config["Interface"]}" 2>&1 | while IFS= read -r l; do bashio::log.error "${l}"; done
|
|
return 1
|
|
}
|
|
|
|
_wireguard_up() {
|
|
local local_ip
|
|
local -a local_ips=()
|
|
local -A local_ip_types=()
|
|
local allowed_ip
|
|
local -a allowed_ips=()
|
|
local -A allowed_ip_types=()
|
|
local key
|
|
|
|
bashio::log.warning "This script force Wireguard to ignore any routes and DNS settings."
|
|
bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}"
|
|
bashio::log.warning "This routing table will be used for traffic from the VPN interface and to the configured DNS servers."
|
|
bashio::log.warning "Qbittorrent bittorrent client shall be set to use the VPN interface ${config["Interface"]} only."
|
|
|
|
for key in "Interface" "ListenPort" "PrivateKey" "PublicKey" "EndpointIP" "EndpointPort" "Address"; do
|
|
if [ ! -v config[$key] ] || [ -z "${config[$key]}" ]; then
|
|
bashio::log.error "Missing required WireGuard configuration parameter: ${key}"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
_cmd "ip link add ${config["Interface"]} type wireguard" || return 1
|
|
|
|
mapfile -d ',' -t local_ips < <(echo "${config["Address"]}" | tr -d ' ')
|
|
for local_ip in ${local_ips[@]}; do
|
|
local result=0
|
|
_check_host "${local_ip}" || result=$?
|
|
if [ "${result}" -eq 1 ]; then
|
|
config["IPv4Enabled"]="true"
|
|
local_ip_types["${local_ip}"]="ipv4"
|
|
allowed_ip_types["0.0.0.0/0"]="ipv4"
|
|
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
|
|
elif [ "${result}" -eq 2 ]; then
|
|
config["IPv6Enabled"]="true"
|
|
local_ip_types["${local_ip}"]="ipv6"
|
|
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
|
|
else
|
|
bashio::log.warning "Ignoring invalid local IP address: ${local_ip}"
|
|
fi
|
|
done
|
|
if [ ${#local_ip_types[@]} -eq 0 ]; then
|
|
bashio::log.error "No valid local IP addresses configured for WireGuard interface."
|
|
return 1
|
|
fi
|
|
|
|
mapfile -d ',' -t allowed_ips < <(echo "${config["Address"]}" | tr -d ' ')
|
|
for allowed_ip in ${allowed_ips[@]}; do
|
|
local result=0
|
|
_check_host "${allowed_ip}" || result=$?
|
|
if [ "${result}" -eq 1 ] && [ "${config["IPv4Enabled"]}" == "true" ]; then
|
|
allowed_ip_types["${allowed_ip}"]="ipv4"
|
|
#allowed_ip_types["0.0.0.0/0"]="ipv4"
|
|
elif [ "${result}" -eq 2 ] && [ "${config["IPv6Enabled"]}" == "true" ]; then
|
|
allowed_ip_types["${allowed_ip}"]="ipv6"
|
|
#allowed_ip_types["::/0"]="ipv6"
|
|
else
|
|
bashio::log.error "Invalid allowed IP address: ${allowed_ip}"
|
|
return 1
|
|
fi
|
|
done
|
|
if [ ${#allowed_ip_types[@]} -eq 0 ]; then
|
|
bashio::log.error "No valid allowed IP addresses configured for WireGuard peer."
|
|
return 1
|
|
fi
|
|
printf -v allowed_ips '%s,' "${!allowed_ip_types[@]}"
|
|
allowed_ips="${allowed_ips%,}"
|
|
|
|
_cmd "wg set ${config["Interface"]} listen-port ${config["ListenPort"]} private-key ${config["PrivateKey"]}" || return 1
|
|
local endpoint="${config["EndpointIP"]}:${config["EndpointPort"]}"
|
|
if [[ "${config["EndpointIP"]}" == *:* ]]; then
|
|
endpoint="[${config["EndpointIP"]}]:${config["EndpointPort"]}"
|
|
fi
|
|
local peer_cmd="wg set ${config["Interface"]} peer ${config["PublicKey"]} endpoint ${endpoint} allowed-ips ${allowed_ips}"
|
|
if [ -n "${config["PresharedKey"]:-}" ]; then
|
|
peer_cmd="${peer_cmd} preshared-key ${config["PresharedKey"]}"
|
|
fi
|
|
if [ -n "${config["PersistentKeepalive"]:-}" ]; then
|
|
peer_cmd="${peer_cmd} persistent-keepalive ${config["PersistentKeepalive"]}"
|
|
fi
|
|
_cmd "${peer_cmd}" || return 1
|
|
|
|
if [ -v config["MTU"] ] && [ -n "${config["MTU"]}" ]; then
|
|
_cmd "ip link set ${config["Interface"]} mtu ${config["MTU"]}" || return 1
|
|
fi
|
|
|
|
_cmd "ip link set ${config["Interface"]} up" || 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() {
|
|
# Update resolv.conf to remove VPN DNS servers
|
|
_resolvconf "reset" || true
|
|
# Remove routing rules for VPN interface and DNS servers
|
|
_routing_del || true
|
|
# Remove firewall rules for VPN interface
|
|
_firewall_del || true
|
|
|
|
_cmd "ip link set ${config["Interface"]} down" 2>/dev/null || true
|
|
_cmd "ip link del ${config["Interface"]}" 2>/dev/null || true
|
|
}
|
|
|
|
wireguard() {
|
|
local mode=$1
|
|
local key
|
|
local interface
|
|
local config_file
|
|
local WIREGUARD_STATE_DIR="/var/run/wireguard"
|
|
|
|
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
|
|
|
|
bashio::log.info "Using Wireguard configuration file: ${config_file}"
|
|
|
|
_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
|
|
|
|
printf '%s\n' "${config["PrivateKey"]}" > "${WIREGUARD_STATE_DIR}/privatekey"
|
|
chmod 600 "${WIREGUARD_STATE_DIR}/privatekey" || true
|
|
config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey"
|
|
|
|
if [ -n "${config["PresharedKey"]:-}" ]; then
|
|
printf '%s\n' "${config["PresharedKey"]}" > "${WIREGUARD_STATE_DIR}/presharedkey"
|
|
chmod 600 "${WIREGUARD_STATE_DIR}/presharedkey" || true
|
|
config["PresharedKey"]="${WIREGUARD_STATE_DIR}/presharedkey"
|
|
fi
|
|
|
|
if [ "${mode}" = "up" ]; then
|
|
bashio::log.info "Starting WireGuard on interface ${config["Interface"]}..."
|
|
local result=0
|
|
_check_host "${config["EndpointHost"]}" || result=$?
|
|
if [ "${result}" -eq 0 ]; then
|
|
bashio::log.error "WireGuard endpoint ${config["EndpointHost"]} is neither a valid IP address nor a resolvable hostname."
|
|
bashio::exit.nok 'WireGuard start failed.'
|
|
elif [ "${result}" -eq 3 ]; then
|
|
local -a endpoint_ips=()
|
|
mapfile -d ' ' -t 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.debug "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 on interface ${config["Interface"]}..."
|
|
_wireguard_down
|
|
bashio::log.info "WireGuard on 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 Specific Logic ---
|
|
|
|
_openvpn_check() {
|
|
local timeout="${1:-20}"
|
|
local deadline ts
|
|
|
|
deadline=$(( $(date +%s) + timeout ))
|
|
|
|
while [ "$(date +%s)" -lt "${deadline}" ]; do
|
|
if ip link show "${config["Interface"]}" > /dev/null 2>&1 ; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
bashio::log.error "OpenVPN interface ${config["Interface"]} failed to come up after ${timeout}s."
|
|
return 1
|
|
}
|
|
|
|
_openvpn_up() {
|
|
bashio::log.warning "This script force OpenvPN to ignore any routes and DNS settings pushed by the server."
|
|
bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}"
|
|
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."
|
|
|
|
# Register this script as OpenVPN up/down handlers to manage routing
|
|
echo '#!/bin/bash' > ${config["PostUpScript"]}
|
|
echo "${config["MySelf"]} openvpn postup" >> ${config["PostUpScript"]}
|
|
chmod 755 ${config["PostUpScript"]}
|
|
echo '#!/bin/bash' > ${config["PostDownScript"]}
|
|
echo "${config["MySelf"]} openvpn postdown" >> ${config["PostDownScript"]}
|
|
chmod 755 ${config["PostDownScript"]}
|
|
|
|
# Define logging
|
|
log_path="/dev/null"
|
|
[[ "$log_level" == "debug" ]] && log_path="/proc/1/fd/1"
|
|
|
|
# Start OpenVPN in the background
|
|
_cmd "/usr/sbin/openvpn \
|
|
--config "${config["ConfigFile"]}" \
|
|
--client \
|
|
--daemon \
|
|
--log "$log_path" \
|
|
--script-security 2 \
|
|
--auth-user-pass "${OPENVPN_STATE_DIR}/credentials.conf" \
|
|
--auth-retry none \
|
|
--up ${config["PostUpScript"]} \
|
|
--down ${config["PostDownScript"]} \
|
|
--up-delay \
|
|
--up-restart \
|
|
--route-nopull \
|
|
--route-noexec" || return 1
|
|
|
|
# Wait for the VPN interface to come up
|
|
_openvpn_check 30 || return 1
|
|
}
|
|
|
|
_openvpn_down() {
|
|
# Terminate OpenVPN process
|
|
pkill -f "openvpn --config ${config["ConfigFile"]}" || true
|
|
}
|
|
|
|
_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() {
|
|
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
|
|
|
|
bashio::log.warning "Using OpenVPN configuration file: ${config_file}"
|
|
|
|
_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 on interface ${config["Interface"]}..."
|
|
if _openvpn_up; then
|
|
bashio::log.info "OpenVPN interface ${config["Interface"]} is up."
|
|
bashio::exit.ok 'OpenVPN started.'
|
|
fi
|
|
bashio::log.error 'OpenVPN failed to establish connection.'
|
|
_openvpn_down
|
|
elif [ "${mode}" = "down" ]; then
|
|
bashio::log.info "Stopping OpenVPN on interface ${config["Interface"]}..."
|
|
_openvpn_down
|
|
bashio::log.info "OpenVPN on interface ${config["Interface"]} is down."
|
|
bashio::exit.ok 'OpenVPN stopped.'
|
|
elif [ "${mode}" = "postup" ]; then
|
|
_openpvn_postup
|
|
bashio::exit.ok 'OpenVPN routes added.'
|
|
elif [ "${mode}" = "postdown" ]; then
|
|
_openpvn_postdown
|
|
bashio::exit.ok 'OpenVPN routes deleted.'
|
|
else
|
|
bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'."
|
|
bashio::exit.nok 'OpenVPN start failed.'
|
|
fi
|
|
|
|
bashio::exit.nok 'OpenVPN start failed.'
|
|
}
|
|
|
|
# --- Entry Point ---
|
|
|
|
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
|