Merge pull request #2399 from litinoveweedle/qbittorrent_vpn_overhaul

Qbittorrent vpn overhaul
This commit is contained in:
Alexandre
2026-02-05 07:32:46 +01:00
committed by GitHub
17 changed files with 643 additions and 824 deletions

View File

@@ -1,15 +1,27 @@
{
"name": "Example devcontainer for add-on repositories",
"image": "ghcr.io/home-assistant/devcontainer:addons",
"appPort": ["7123:8123", "7357:4357"],
"postStartCommand": "sudo -E bash devcontainer_bootstrap",
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
"image": "ghcr.io/home-assistant/devcontainer:2-addons",
"appPort": [
"7123:8123",
"7357:4357"
],
"postStartCommand": "bash devcontainer_bootstrap",
"runArgs": [
"-e",
"GIT_EDITOR=code --wait",
"--privileged"
],
"workspaceFolder": "/mnt/supervisor/addons/local/${localWorkspaceFolderBasename}",
"workspaceMount": "source=${localWorkspaceFolder},target=${containerWorkspaceFolder},type=bind,consistency=cached",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"customizations": {
"vscode": {
"extensions": ["timonwong.shellcheck", "esbenp.prettier-vscode"],
"extensions": [
"timonwong.shellcheck",
"esbenp.prettier-vscode"
],
"settings": {
"terminal.integrated.profiles.linux": {
"zsh": {
@@ -24,5 +36,8 @@
}
}
},
"mounts": [ "type=volume,target=/var/lib/docker" ]
}
"mounts": [
"type=volume,target=/var/lib/docker",
"type=volume,target=/mnt/supervisor"
]
}

4
.vscode/tasks.json vendored
View File

@@ -4,7 +4,7 @@
{
"label": "Start Home Assistant",
"type": "shell",
"command": "sudo chmod a+x /usr/bin/supervisor* && sudo -E supervisor_run",
"command": "supervisor_run",
"group": {
"kind": "test",
"isDefault": true
@@ -16,4 +16,4 @@
"problemMatcher": []
}
]
}
}

View File

@@ -1,3 +1,5 @@
- Rewrite the openvpn and wireguard scripts in order to make them more robust, secure, and compatible with more suppliers @litinoveweedle
## 5.1.4-6 (03-02-2026)
- Minor bugs fixed
## 5.1.4-5 (23-01-2026)

View File

@@ -92,6 +92,7 @@ RUN \
# Copy local files
COPY rootfs/ /
RUN find /etc -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -exec chmod +x {} +
RUN chmod +x /usr/local/sbin/vpn
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
@@ -112,7 +113,7 @@ RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_auto
# && chmod a+x /etc/s6-overlay/s6-rc.d/$SCRIPTSNAME/* ; done; fi
# Manual apps
ARG PACKAGES="wireguard-tools iptables ip6tables iptables-legacy"
ARG PACKAGES="ipcalc wireguard-tools"
# Automatic apps & bashio
ADD "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/ha_autoapps.sh" "/ha_autoapps.sh"
@@ -126,8 +127,6 @@ RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.
# 4 Entrypoint #
################
RUN chmod +x /usr/local/bin/* /usr/local/sbin/*
# Add entrypoint
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
ADD "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/ha_entrypoint.sh" "/ha_entrypoint.sh"

View File

@@ -79,6 +79,7 @@ map:
- ssl
name: qBittorrent
options:
log_level: info
env_vars: []
DNS_server: 8.8.8.8,1.1.1.1
PGID: "0"
@@ -88,10 +89,7 @@ options:
certfile: fullchain.pem
customUI: vuetorrent
keyfile: privkey.pem
qbit_manage: false
ssl: false
wireguard_enabled: false
wireguard_config: ""
whitelist: localhost,127.0.0.1,172.30.0.0/16,192.168.0.0/16
panel_admin: false
panel_icon: mdi:progress-download
@@ -112,6 +110,7 @@ privileged:
- DAC_READ_SEARCH
- NET_ADMIN
schema:
log_level: list(trace|debug|info|notice|warning|error|fatal)?
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
@@ -129,8 +128,7 @@ schema:
keyfile: str
localdisks: str?
networkdisks: str?
openvpn_alt_mode: bool?
openvpn_config: str?
openvpn_config: match(^\w+\.(ovpn|conf)$)?
openvpn_enabled: bool?
openvpn_password: str?
openvpn_username: str?
@@ -138,10 +136,10 @@ schema:
run_duration: str?
silent: bool?
ssl: bool
wireguard_config: str?
wireguard_config: match(^\w+\.conf$)?
wireguard_enabled: bool?
whitelist: str?
slug: qbittorrent
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "5.1.4-6"
version: "5.1.4-7"

View File

@@ -2,269 +2,112 @@
# shellcheck shell=bash
set -e
declare openvpn_config
OPENVPN_STATE_DIR="/var/run/openvpn"
QBT_CONFIG_FILE="/config/qBittorrent/qBittorrent.conf"
declare openvpn_config=""
declare openvpn_runtime_config=""
declare interface_name=""
declare openvpn_username
declare openvpn_password
QBT_CONFIG_FILE="/config/qBittorrent/qBittorrent.conf"
if bashio::config.true 'openvpn_enabled'; then
bashio::log.info "----------------------------"
bashio::log.info "Openvpn enabled, configuring"
bashio::log.info "----------------------------"
# Function to check for files path
function check_path() {
# Get variable
file="$1"
# Double check exists
if [ ! -f "$file" ]; then
bashio::warning "$file not found"
return 1
fi
# Check each lines
cp "$file" /tmpfile
line_number=0
while read -r line; do
# Increment the line number
((line_number = line_number + 1))
# Check if lines starting with auth-user-pass have a valid argument
###################################################################
if [[ "$line" == "auth-user-pass"* ]]; then
# Extract the second argument
file_name="$(echo "$line" | awk -F' ' '{print $2}')"
# If second argument is null or -
if [ -z "$file_name" ] || [[ "$file_name" == -* ]]; then
# Insert to explain why a comment is made
sed -i "${line_number}i # The following line is commented out as does not contain a valid argument" "$file"
# Increment as new line added
((line_number = line_number + 1))
# Comment out the line
sed -i "${line_number}s/^/# /" "$file"
# Go to next line
continue
fi
fi
# Check if the line contains a txt file
#######################################
if [[ ! $line =~ ^"#" ]] && [[ ! $line =~ ^";" ]] && [[ ! $line =~ ^"remote" ]] && [[ "$line" == *" "*"."* ]] || [[ "$line" == "auth-user-pass"* ]]; then
# Extract the txt file name from the line
file_name="$(echo "$line" | awk -F' ' '{print $2}')"
# if contains only numbers and dots it is likely an ip, don't check it
if [[ "$file_name" =~ ^[0-9\.]+$ ]]; then
continue
fi
# Check if the txt file exists
if [[ "$file_name" != *"/etc/openvpn/credentials"* ]] && [ ! -f "$file_name" ]; then
# Check if the txt file exists in the /config/openvpn/ directory
if [ -f "/config/openvpn/${file_name##*/}" ]; then
# Append /config/openvpn/ in front of the original txt file in the ovpn file
sed -i "${line_number}s|$file_name|/config/openvpn/${file_name##*/}|" "$file"
# Print a success message
bashio::log.warning "Appended /config/openvpn/ to ${file_name##*/} in $file"
else
# Print an error message
bashio::log.warning "$file_name is referenced in your ovpn file but does not exist, and can't be found either in the /config/openvpn/ directory"
fi
fi
fi
done < /tmpfile
rm /tmpfile
# Standardize lf
dos2unix "$file"
# Remove custom up & down
sed -i '/^up /s/^/#/' "$file"
sed -i '/^down /s/^/#/' "$file"
# Remove blank lines
sed -i '/^[[:blank:]]*$/d' "$file"
# Ensure config ends with a line feed
sed -i "\$q" "$file"
# Correct paths
sed -i "s=/etc/openvpn=/config/openvpn=g" "$file"
sed -i "s=/config/openvpn/credentials=/etc/openvpn/credentials=g" "$file"
}
#####################
# CONFIGURE OPENVPN #
#####################
# If openvpn_config option used
if bashio::config.has_value "openvpn_config"; then
openvpn_config=$(bashio::config 'openvpn_config')
# If file found
if [ -f /config/openvpn/"$openvpn_config" ]; then
# If correct type
if [[ "$openvpn_config" == *".ovpn" ]] || [[ "$openvpn_config" == *".conf" ]]; then
echo "... configured ovpn file : using /addon_configs/$HOSTNAME/openvpn/$openvpn_config"
else
bashio::exit.nok "Configured ovpn file : $openvpn_config is set but does not end by .ovpn ; it can't be used!"
fi
else
bashio::exit.nok "Configured ovpn file : $openvpn_config not found! Are you sure you added it in /addon_configs/$HOSTNAME/openvpn using the Filebrowser addon ?"
fi
# If openvpn_config not set, but folder is not empty
elif ls /config/openvpn/*.ovpn > /dev/null 2>&1; then
# Look for openvpn files
# Wildcard search for openvpn config files and store results in array
mapfile -t VPN_CONFIGS < <(find /config/openvpn -maxdepth 1 -name "*.ovpn" -print)
# Choose random config
VPN_CONFIG="${VPN_CONFIGS[$RANDOM % ${#VPN_CONFIGS[@]}]}"
# Get the VPN_CONFIG name without the path and extension
openvpn_config="${VPN_CONFIG##*/}"
echo "... Openvpn enabled, but openvpn_config option empty. Selecting a random ovpn file : ${openvpn_config}. Other available files :"
printf '%s\n' "${VPN_CONFIGS[@]}"
# If openvpn_enabled set, config not set, and openvpn folder empty
else
bashio::exit.nok "openvpn_enabled is set, however, your openvpn folder is empty ! Are you sure you added it in /addon_configs/$HOSTNAME/openvpn using the Filebrowser addon ?"
fi
# Send to openvpn script
sed -i "s|/config/openvpn/config.ovpn|/config/openvpn/$openvpn_config|g" /etc/s6-overlay/s6-rc.d/svc-qbittorrent/run
# Check path
check_path /config/openvpn/"${openvpn_config}"
# Set credentials
if bashio::config.has_value "openvpn_username"; then
openvpn_username=$(bashio::config 'openvpn_username')
echo "${openvpn_username}" > /etc/openvpn/credentials
else
bashio::exit.nok "Openvpn is enabled, but openvpn_username option is empty! Exiting"
fi
if bashio::config.has_value "openvpn_password"; then
openvpn_password=$(bashio::config 'openvpn_password')
echo "${openvpn_password}" >> /etc/openvpn/credentials
else
bashio::exit.nok "Openvpn is enabled, but openvpn_password option is empty! Exiting"
fi
# Add credentials file
if grep -q ^auth-user-pass /config/openvpn/"$openvpn_config"; then
# Credentials specified are they custom ?
file_name="$(sed -n "/^auth-user-pass/p" /config/openvpn/"$openvpn_config" | awk -F' ' '{print $2}')"
file_name="${file_name:-null}"
if [[ "$file_name" != *"/etc/openvpn/credentials"* ]] && [[ "$file_name" != "null" ]]; then
if [ -f "$file_name" ]; then
# If credential specified, exists, and is not the addon default
bashio::log.warning "auth-user-pass specified in the ovpn file, addon username and passwords won't be used !"
else
# Credential referenced but doesn't exist
bashio::log.warning "auth-user-pass $file_name is referenced in your ovpn file but does not exist, and can't be found either in the /config/openvpn/ directory. The addon will attempt to use it's own username and password instead."
# Comment previous lines
sed -i '/^auth-user-pass/i # specified auth-user-pass file not found, disabling' /config/openvpn/"$openvpn_config"
sed -i '/^auth-user-pass/s/^/#/' /config/openvpn/"$openvpn_config"
# No credentials specified, using addons username and password
echo "# Please do not remove the line below, it allows using the addon username and password" >> /config/openvpn/"$openvpn_config"
echo "auth-user-pass /etc/openvpn/credentials" >> /etc/openvpn/"$openvpn_config"
fi
else
# Standardize just to be sure
sed -i "/\/etc\/openvpn\/credentials/c auth-user-pass \/etc\/openvpn\/credentials" /config/openvpn/"$openvpn_config"
fi
else
# No credentials specified, using addons username and password
echo "# Please do not remove the line below, it allows using the addon username and password" >> /config/openvpn/"$openvpn_config"
echo "auth-user-pass /etc/openvpn/credentials" >> /config/openvpn/"$openvpn_config"
fi
# Permissions
chmod 755 /config/openvpn/*
chmod 755 /etc/openvpn/*
chmod 600 /etc/openvpn/credentials
chmod 755 /etc/openvpn/up.sh
chmod 755 /etc/openvpn/down.sh
chmod 755 /etc/openvpn/up-qbittorrent.sh
chmod +x /etc/openvpn/up.sh
chmod +x /etc/openvpn/down.sh
chmod +x /etc/openvpn/up-qbittorrent.sh
echo "... openvpn correctly set, qbittorrent will run tunnelled through openvpn"
#########################
# CONFIGURE QBITTORRENT #
#########################
# WITH CONTAINER BINDING
#########################
# If alternative mode enabled, bind container
if bashio::config.true 'openvpn_alt_mode'; then
echo "Using container binding"
# Remove interface
echo "... deleting previous interface settings"
sed -i '/Interface/d' "$QBT_CONFIG_FILE"
# Modify ovpn config
if grep -q route-nopull /config/openvpn/"$openvpn_config"; then
echo "... removing route-nopull from your config.ovpn"
sed -i '/route-nopull/d' /config/openvpn/"$openvpn_config"
fi
# Exit
exit 0
fi
# WITH INTERFACE BINDING
#########################
# Connection with interface binding
echo "Using interface binding in the qBittorrent app"
# Define preferences line
cd /config/qBittorrent/ || exit 1
# If qBittorrent.conf exists
if [ -f "$QBT_CONFIG_FILE" ]; then
# Remove previous line and bind tun0
echo "... deleting previous interface settings"
sed -i '/Interface/d' "$QBT_CONFIG_FILE"
# Bind tun0
echo "... binding tun0 interface in qBittorrent configuration"
sed -i "/\[Preferences\]/ i\Connection\\\Interface=tun0" "$QBT_CONFIG_FILE"
sed -i "/\[Preferences\]/ i\Connection\\\InterfaceName=tun0" "$QBT_CONFIG_FILE"
# Add to ongoing session
sed -i "/\[BitTorrent\]/a \Session\\\Interface=tun0" "$QBT_CONFIG_FILE"
sed -i "/\[BitTorrent\]/a \Session\\\InterfaceName=tun0" "$QBT_CONFIG_FILE"
else
bashio::log.error "qBittorrent config file doesn't exist, openvpn must be added manually to qbittorrent options "
exit 1
fi
# Modify ovpn config
if ! grep -q route-nopull /config/openvpn/"$openvpn_config"; then
echo "... adding route-nopull to your config.ovpn"
sed -i "1a route-nopull" /config/openvpn/"$openvpn_config"
fi
else
##################
# REMOVE OPENVPN #
##################
if ! bashio::config.true 'wireguard_enabled'; then
# Ensure no redirection by removing the direction tag when no VPN is used
if [ -f "$QBT_CONFIG_FILE" ]; then
sed -i '/Interface/d' "$QBT_CONFIG_FILE"
fi
bashio::log.info "Direct connection without VPN enabled"
else
bashio::log.info "OpenVPN disabled. WireGuard handling network binding."
fi
if bashio::fs.directory_exists "${OPENVPN_STATE_DIR}"; then
bashio::log.warning "Previous OpenVPN state directory found, cleaning up."
rm -Rf "${OPENVPN_STATE_DIR}"
fi
if ! bashio::config.true 'openvpn_enabled'; then
bashio::exit.ok 'OpenVPN is disabled.'
elif bashio::config.true 'wireguard_enabled'; then
bashio::exit.nok 'OpenVPN and WireGuard cannot be enabled simultaneously. Disable one of them.'
fi
mkdir -p "${OPENVPN_STATE_DIR}"
bashio::log.info "----------------------------"
bashio::log.info "Openvpn enabled, configuring"
bashio::log.info "----------------------------"
# Set credentials
if bashio::config.has_value "openvpn_username"; then
openvpn_username=$(bashio::config 'openvpn_username')
else
bashio::exit.nok "Openvpn is enabled, but openvpn_username option is empty! Exiting"
fi
if bashio::config.has_value "openvpn_password"; then
openvpn_password=$(bashio::config 'openvpn_password')
else
bashio::exit.nok "Openvpn is enabled, but openvpn_password option is empty! Exiting"
fi
echo -e "${openvpn_username}\n${openvpn_password}" > "${OPENVPN_STATE_DIR}/credentials.conf"
chmod 600 "${OPENVPN_STATE_DIR}/credentials.conf"
if bashio::config.has_value "openvpn_config"; then
openvpn_config="$(bashio::config 'openvpn_config')"
openvpn_config="${openvpn_config##*/}"
fi
if [[ -z "${openvpn_config}" ]]; then
bashio::log.info 'openvpn_config option left empty. Attempting automatic selection.'
mapfile -t configs < <(find /config/openvpn -maxdepth 1 \( -type f -name '*.conf' -o -name '*.ovpn' \) -print)
if [ "${#configs[@]}" -eq 0 ]; then
bashio::exit.nok 'OpenVPN is enabled but no .conf or .ovpn file was found in /config/openvpn.'
elif [ "${#configs[@]}" -eq 1 ]; then
openvpn_config="${configs[0]}"
bashio::log.info "OpenVPN configuration not specified. Using ${openvpn_config##*/}."
elif bashio::fs.file_exists '/config/openvpn/config.conf'; then
openvpn_config='/config/openvpn/config.conf'
bashio::log.info 'Using default OpenVPN configuration config.conf.'
else
bashio::exit.nok "Multiple OpenVPN configuration files detected. Please set the 'openvpn_config' option."
fi
elif bashio::fs.file_exists "/config/openvpn/${openvpn_config}"; then
openvpn_config="/config/openvpn/${openvpn_config}"
else
bashio::exit.nok "OpenVPN configuration '/config/openvpn/${openvpn_config}' not found."
fi
interface_name="$(sed -n "/^dev tun/p" "${openvpn_config}" | awk -F' ' '{print $2}')"
if [[ -z "${interface_name}" ]]; then
bashio::exit.nok "OpenVPN configuration '${openvpn_config}' misses device directive."
elif [[ ${interface_name} = "tun" ]]; then
interface_name='tun0'
elif [[ ${interface_name} = "tap" ]]; then
interface_name='tap0'
fi
openvpn_runtime_config="${OPENVPN_STATE_DIR}/${interface_name}.conf"
cp "${openvpn_config}" "${openvpn_runtime_config}"
chmod 600 "${openvpn_runtime_config}"
dos2unix "${openvpn_runtime_config}" >/dev/null 2>&1 || true
sed -i '/^[[:space:]]*[;#]/d' "${openvpn_runtime_config}"
sed -i 's/#.*//' "${openvpn_runtime_config}"
sed -i '/^[[:space:]]*$/d' "${openvpn_runtime_config}"
sed -i '/^[[:blank:]]*$/d' "${openvpn_runtime_config}"
sed -i '/^up/d' "${openvpn_runtime_config}"
sed -i '/^down/d' "${openvpn_runtime_config}"
sed -i '/^route/d' "${openvpn_runtime_config}"
sed -i '/^auth-user-pass /d' "${openvpn_runtime_config}"
sed -i '/^cd /d' "${openvpn_runtime_config}"
sed -i '/^chroot /d' "${openvpn_runtime_config}"
sed -i '$q' "${openvpn_runtime_config}"
bashio::log.info 'Prepared OpenVPN runtime configuration for initial connection attempt.'
echo "${openvpn_runtime_config}" > "${OPENVPN_STATE_DIR}/config"
echo "${interface_name}" > "${OPENVPN_STATE_DIR}/interface"
bashio::log.info "Using interface binding in the qBittorrent app"
if bashio::fs.file_exists "${QBT_CONFIG_FILE}"; then
sed -i '/Interface/d' "${QBT_CONFIG_FILE}"
sed -i "/\\[Preferences\\]/ i\\Connection\\\\Interface=${interface_name}" "${QBT_CONFIG_FILE}"
sed -i "/\\[Preferences\\]/ i\\Connection\\\\InterfaceName=${interface_name}" "${QBT_CONFIG_FILE}"
sed -i "/\\[BitTorrent\\]/a \\Session\\\\Interface=${interface_name}" "${QBT_CONFIG_FILE}"
sed -i "/\\[BitTorrent\\]/a \\Session\\\\InterfaceName=${interface_name}" "${QBT_CONFIG_FILE}"
else
bashio::log.warning "qBittorrent config file not found. Bind the client manually to interface ${interface_name}."
fi
bashio::log.info "OpenVPN prepared with interface ${interface_name} using configuration ${openvpn_config##*/}."

View File

@@ -6,37 +6,32 @@ WIREGUARD_STATE_DIR="/var/run/wireguard"
QBT_CONFIG_FILE="/config/qBittorrent/qBittorrent.conf"
declare wireguard_config=""
declare wireguard_runtime_config=""
declare configured_name
declare interface_name=""
mkdir -p "${WIREGUARD_STATE_DIR}"
if ! bashio::config.true 'wireguard_enabled'; then
rm -f "${WIREGUARD_STATE_DIR}/config" "${WIREGUARD_STATE_DIR}/interface"
exit 0
if bashio::fs.directory_exists "${WIREGUARD_STATE_DIR}"; then
bashio::log.warning "Previous WireGuard state directory found, cleaning up."
rm -Rf "${WIREGUARD_STATE_DIR}"
fi
if bashio::config.true 'openvpn_enabled'; then
if ! bashio::config.true 'wireguard_enabled'; then
bashio::exit.ok 'WireGuard is disabled.'
elif bashio::config.true 'openvpn_enabled'; then
bashio::exit.nok 'OpenVPN and WireGuard cannot be enabled simultaneously. Disable one of them.'
fi
if bashio::config.true 'openvpn_alt_mode'; then
bashio::log.warning 'The openvpn_alt_mode option is ignored when WireGuard is enabled.'
fi
mkdir -p "${WIREGUARD_STATE_DIR}"
if bashio::config.has_value 'wireguard_config'; then
configured_name="$(bashio::config 'wireguard_config')"
configured_name="${configured_name##*/}"
if [[ -z "${configured_name}" ]]; then
bashio::log.info 'wireguard_config option left empty. Attempting automatic selection.'
elif bashio::fs.file_exists "/config/wireguard/${configured_name}"; then
wireguard_config="/config/wireguard/${configured_name}"
else
bashio::exit.nok "WireGuard configuration '/config/wireguard/${configured_name}' not found."
fi
fi
bashio::log.info "------------------------------"
bashio::log.info "Wireguard enabled, configuring"
bashio::log.info "------------------------------"
if [ -z "${wireguard_config:-}" ]; then
mapfile -t configs < <(find /config/wireguard -maxdepth 1 -type f -name '*.conf' -print)
if bashio::config.has_value "wireguard_config"; then
wireguard_config="$(bashio::config 'wireguard_config')"
wireguard_config="${wireguard_config##*/}"
fi
if [[ -z "${wireguard_config}" ]]; then
bashio::log.info 'wireguard_config option left empty. Attempting automatic selection.'
mapfile -t configs < <(find /config/wireguard -maxdepth 1 -type f -name '*.conf' -print)
if [ "${#configs[@]}" -eq 0 ]; then
bashio::exit.nok 'WireGuard is enabled but no .conf file was found in /config/wireguard.'
elif [ "${#configs[@]}" -eq 1 ]; then
@@ -48,10 +43,12 @@ if [ -z "${wireguard_config:-}" ]; then
else
bashio::exit.nok "Multiple WireGuard configuration files detected. Please set the 'wireguard_config' option."
fi
elif bashio::fs.file_exists "/config/wireguard/${wireguard_config}"; then
wireguard_config="/config/wireguard/${wireguard_config}"
else
bashio::exit.nok "WireGuard configuration '/config/wireguard/${wireguard_config}' not found."
fi
dos2unix "${wireguard_config}" >/dev/null 2>&1 || true
interface_name="$(basename "${wireguard_config}" .conf)"
if [[ -z "${interface_name}" ]]; then
interface_name='wg0'
@@ -60,12 +57,26 @@ fi
wireguard_runtime_config="${WIREGUARD_STATE_DIR}/${interface_name}.conf"
cp "${wireguard_config}" "${wireguard_runtime_config}"
chmod 600 "${wireguard_runtime_config}" 2>/dev/null || true
chmod 600 "${wireguard_runtime_config}"
dos2unix "${wireguard_runtime_config}" >/dev/null 2>&1 || true
sed -i '/^[[:space:]]*[;#]/d' "${wireguard_runtime_config}"
sed -i 's/#.*//' "${wireguard_runtime_config}"
sed -i '/^[[:space:]]*$/d' "${wireguard_runtime_config}"
sed -i '/^[[:blank:]]*$/d' "${wireguard_runtime_config}"
sed -i '/DNS/d' "${wireguard_runtime_config}"
sed -i '/PostUp/d' "${wireguard_runtime_config}"
sed -i '/PostDown/d' "${wireguard_runtime_config}"
sed -i '/SaveConfig/d' "${wireguard_runtime_config}"
sed -i "\$q" "${wireguard_runtime_config}"
bashio::log.info 'Prepared WireGuard runtime configuration for initial connection attempt.'
echo "${wireguard_runtime_config}" > "${WIREGUARD_STATE_DIR}/config"
echo "${interface_name}" > "${WIREGUARD_STATE_DIR}/interface"
bashio::log.info "Using interface binding in the qBittorrent app"
if bashio::fs.file_exists "${QBT_CONFIG_FILE}"; then
sed -i '/Interface/d' "${QBT_CONFIG_FILE}"
sed -i "/\\[Preferences\\]/ i\\Connection\\\\Interface=${interface_name}" "${QBT_CONFIG_FILE}"

View File

@@ -1,36 +0,0 @@
#!/bin/sh
# shellcheck disable=SC2154,SC2004,SC2059,SC2086
# Copyright (c) 2006-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# Contributed by Roy Marples (uberlord@gentoo.org)
# If we have a service specific script, run this now
if [ -x /etc/openvpn/"${RC_SVCNAME}"-down.sh ]; then
/etc/openvpn/"${RC_SVCNAME}"-down.sh "$@"
fi
# Restore resolv.conf to how it was
if [ "${PEER_DNS}" != "no" ]; then
if [ -x /sbin/resolvconf ]; then
/sbin/resolvconf -d "${dev}"
elif [ -e /etc/resolv.conf-"${dev}".sv ]; then
# Important that we cat instead of move incase resolv.conf is
# a symlink and not an actual file
cat /etc/resolv.conf-"${dev}".sv > /etc/resolv.conf
rm -f /etc/resolv.conf-"${dev}".sv
fi
fi
if [ -n "${RC_SVCNAME}" ]; then
# Re-enter the init script to start any dependant services
if /etc/init.d/"${RC_SVCNAME}" --quiet status; then
export IN_BACKGROUND=true
if [ -d /var/run/s6/container_environment ]; then printf "%s" "true" > /var/run/s6/container_environment/IN_BACKGROUND; fi
printf "%s\n" "IN_BACKGROUND=\"true\"" >> ~/.bashrc
/etc/init.d/"${RC_SVCNAME}" --quiet stop
fi
fi
exit 0
# vim: ts=4 :

View File

@@ -1,9 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
WEBUI_PORT=${WEBUI_PORT:-8080}
exec \
s6-notifyoncheck -d -n 300 -w 1000 -c "nc -z localhost ${WEBUI_PORT}" \
s6-setuidgid abc /usr/bin/qbittorrent-nox --webui-port="${WEBUI_PORT}"

View File

@@ -1,97 +0,0 @@
#!/bin/sh
# shellcheck disable=SC2154,SC2004,SC2059,SC2086
# launch qbittorrent
/etc/openvpn/up-qbittorrent.sh "${4}" &
# Copyright (c) 2006-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# Contributed by Roy Marples (uberlord@gentoo.org)
# Setup our resolv.conf
# Vitally important that we use the domain entry in resolv.conf so we
# can setup the nameservers are for the domain ONLY in resolvconf if
# we're using a decent dns cache/forwarder like dnsmasq and NOT nscd/libc.
# nscd/libc users will get the VPN nameservers before their other ones
# and will use the first one that responds - maybe the LAN ones?
# non resolvconf users just the the VPN resolv.conf
# FIXME:- if we have >1 domain, then we have to use search :/
# We need to add a flag to resolvconf to say
# "these nameservers should only be used for the listed search domains
# if other global nameservers are present on other interfaces"
# This however, will break compatibility with Debians resolvconf
# A possible workaround would be to just list multiple domain lines
# and try and let resolvconf handle it
if [ "${PEER_DNS}" != "no" ]; then
NS=
DOMAIN=
SEARCH=
i=1
while true; do
eval opt=\$foreign_option_${i}
[ -z "${opt}" ] && break
if [ "${opt}" != "${opt#dhcp-option DOMAIN *}" ]; then
if [ -z "${DOMAIN}" ]; then
DOMAIN="${opt#dhcp-option DOMAIN *}"
else
SEARCH="${SEARCH}${SEARCH:+ }${opt#dhcp-option DOMAIN *}"
fi
elif [ "${opt}" != "${opt#dhcp-option DNS *}" ]; then
NS="${NS}nameserver ${opt#dhcp-option DNS *}\n"
fi
i=$((${i} + 1))
done
if [ -n "${NS}" ]; then
DNS="# Generated by openvpn for interface ${dev}\n"
if [ -n "${SEARCH}" ]; then
DNS="${DNS}search ${DOMAIN} ${SEARCH}\n"
elif [ -n "${DOMAIN}" ]; then
DNS="${DNS}domain ${DOMAIN}\n"
fi
DNS="${DNS}${NS}"
if [ -x /sbin/resolvconf ]; then
printf "${DNS}" | /sbin/resolvconf -a "${dev}"
else
# Preserve the existing resolv.conf
if [ -e /etc/resolv.conf ]; then
cp /etc/resolv.conf /etc/resolv.conf-"${dev}".sv
fi
printf "${DNS}" > /etc/resolv.conf
chmod 644 /etc/resolv.conf
fi
fi
fi
# Below section is Gentoo specific
# Quick summary - our init scripts are re-entrant and set the RC_SVCNAME env var
# as we could have >1 openvpn service
if [ -n "${RC_SVCNAME}" ]; then
# If we have a service specific script, run this now
if [ -x /etc/openvpn/"${RC_SVCNAME}"-up.sh ]; then
/etc/openvpn/"${RC_SVCNAME}"-up.sh "$@"
fi
# Re-enter the init script to start any dependant services
if ! /etc/init.d/"${RC_SVCNAME}" --quiet status; then
export IN_BACKGROUND=true
if [ -d /var/run/s6/container_environment ]; then printf "%s" "true" > /var/run/s6/container_environment/IN_BACKGROUND; fi
printf "%s\n" "IN_BACKGROUND=\"true\"" >> ~/.bashrc
/etc/init.d/${RC_SVCNAME} --quiet start
fi
fi
###############
# ALLOW WEBUI #
###############
ip route add 10.0.0.0/8 via 172.30.32.1
ip route add 192.168.0.0/16 via 172.30.32.1
ip route add 172.16.0.0/12 via 172.30.32.1
exit 0
# vim: ts=4 :

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,19 @@ if [[ "${vpn_openvpn}" == true && "${vpn_wireguard}" == true ]]; then
exit 1
fi
if [[ "${vpn_openvpn}" == true ]]; 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)."
fi
if [[ -z "${VPN_INTERFACE}" ]] || ! ip link show "${VPN_INTERFACE}" > /dev/null 2>&1 ; then
bashio::log.error "VPN interface not found."
bashio::addon.stop
exit 1
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,453 @@
#!/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=()
_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]}"
config_ref["$key"]="$value"
fi
done < "$config_file"
}
_parse_dns() {
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 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+=("${ipv4_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
# add routing rules for local IPs
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 ${ipv4} 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 ${ipv6} table ${config["Table"]}" || return 1
done
# 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
# Update resolv.conf with VPN DNS servers
_resolvconf "update"
}
_routing_del() {
bashio::log.info "Removing routing rules for VPN interface ${config["Interface"]}..."
_resolvconf "reset"
while _cmd "ip -4 rule del priority 1 from all table ${config["Table"]} 2>/dev/null"; do :; done
while _cmd "ip -4 rule del priority 1 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 Specific Logic ---
_wireguard_up() {
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" ; 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
local allowed_ips=""
local -a local_ips=()
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
allowed_ips="${allowed_ips},0.0.0.0/0"
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
elif [ "${result}" -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
if [ -v config["PersistentKeepalive"] ] && [ -n "${config["PersistentKeepalive"]}" ]; then
_cmd "wg set ${config["Interface"]} peer ${config["PublicKey"]} endpoint ${config["EndpointIP"]}:${config["EndpointPort"]} allowed-ips ${allowed_ips} persistent-keepalive ${config["PersistentKeepalive"]}" || return 1
else
_cmd "wg set ${config["Interface"]} peer ${config["PublicKey"]} endpoint ${config["EndpointIP"]}:${config["EndpointPort"]} allowed-ips ${allowed_ips}" || return 1
fi
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
_routing_add
}
_wireguard_down() {
_routing_del
_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 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
echo ${config["PrivateKey"]} > ${WIREGUARD_STATE_DIR}/privatekey
config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey"
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_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"]}
# Start OpenVPN in the background
_cmd "/usr/sbin/openvpn \
--config "${config["ConfigFile"]}" \
--client \
--daemon \
--log /dev/null \
--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 slow OpenVPN interface to come up
for i in {1..10}; 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."
return 1
}
_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
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
_routing_add
bashio::exit.ok 'OpenVPN routes added.'
elif [ "${mode}" = "postdown" ]; then
_routing_del
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