diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c701e2778..d0c285560 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f426c2c94..6f51015fc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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": [] } ] -} +} \ No newline at end of file diff --git a/qbittorrent/CHANGELOG.md b/qbittorrent/CHANGELOG.md index c604054de..1bc6d0465 100644 --- a/qbittorrent/CHANGELOG.md +++ b/qbittorrent/CHANGELOG.md @@ -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) diff --git a/qbittorrent/Dockerfile b/qbittorrent/Dockerfile index f9e7b9af5..6f7fc4b13 100644 --- a/qbittorrent/Dockerfile +++ b/qbittorrent/Dockerfile @@ -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" diff --git a/qbittorrent/config.yaml b/qbittorrent/config.yaml index 23e3d2986..1c1cb97f4 100644 --- a/qbittorrent/config.yaml +++ b/qbittorrent/config.yaml @@ -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" diff --git a/qbittorrent/rootfs/etc/cont-init.d/93-openvpn.sh b/qbittorrent/rootfs/etc/cont-init.d/93-openvpn.sh index d1dff3c77..b82afb701 100755 --- a/qbittorrent/rootfs/etc/cont-init.d/93-openvpn.sh +++ b/qbittorrent/rootfs/etc/cont-init.d/93-openvpn.sh @@ -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##*/}." diff --git a/qbittorrent/rootfs/etc/cont-init.d/94-wireguard.sh b/qbittorrent/rootfs/etc/cont-init.d/94-wireguard.sh index 056f704ba..3afa12619 100755 --- a/qbittorrent/rootfs/etc/cont-init.d/94-wireguard.sh +++ b/qbittorrent/rootfs/etc/cont-init.d/94-wireguard.sh @@ -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}" diff --git a/qbittorrent/rootfs/etc/openvpn/down.sh b/qbittorrent/rootfs/etc/openvpn/down.sh deleted file mode 100755 index 57d18e39b..000000000 --- a/qbittorrent/rootfs/etc/openvpn/down.sh +++ /dev/null @@ -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 : diff --git a/qbittorrent/rootfs/etc/openvpn/up-qbittorrent.sh b/qbittorrent/rootfs/etc/openvpn/up-qbittorrent.sh deleted file mode 100755 index 0f0101dbc..000000000 --- a/qbittorrent/rootfs/etc/openvpn/up-qbittorrent.sh +++ /dev/null @@ -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}" diff --git a/qbittorrent/rootfs/etc/openvpn/up.sh b/qbittorrent/rootfs/etc/openvpn/up.sh deleted file mode 100755 index fe5da2bda..000000000 --- a/qbittorrent/rootfs/etc/openvpn/up.sh +++ /dev/null @@ -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 : diff --git a/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run b/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run index b0f1a99cf..ec0e73b49 100644 --- a/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run +++ b/qbittorrent/rootfs/etc/s6-overlay/s6-rc.d/svc-qbittorrent/run @@ -10,125 +10,6 @@ if bashio::config.true 'silent'; then sed -i 's|/proc/1/fd/1 hassio;|off;|g' /etc/nginx/nginx.conf fi -# --- WireGuard Specific Logic --- - -_setup_wireguard() { - local WIREGUARD_STATE_DIR="/var/run/wireguard" - local output="" - local status=0 - - if ! bashio::fs.file_exists "${WIREGUARD_STATE_DIR}/config"; then - bashio::exit.nok 'WireGuard runtime configuration not prepared. Please restart the add-on.' - fi - - local wireguard_config - wireguard_config="$(cat "${WIREGUARD_STATE_DIR}/config")" - local wireguard_interface - wireguard_interface="$(cat "${WIREGUARD_STATE_DIR}/interface" 2>/dev/null || echo 'wg0')" - - if ip link show "${wireguard_interface}" >/dev/null 2>&1; then - bashio::log.warning "WireGuard interface ${wireguard_interface} already exists. Resetting." - wg-quick down "${wireguard_config}" >/dev/null 2>&1 || true - fi - - bashio::log.info "Starting WireGuard interface ${wireguard_interface}..." - - # Internal helper: fallback for iptables-legacy - _wg_prepare_legacy() { - local legacy_bin_dir="${WIREGUARD_STATE_DIR}/iptables-legacy-bin" - mkdir -p "${legacy_bin_dir}" - local cmd - for cmd in iptables iptables-save iptables-restore ip6tables ip6tables-save ip6tables-restore; do - if command -v "${cmd}-legacy" >/dev/null 2>&1; then - ln -sf "$(command -v "${cmd}-legacy")" "${legacy_bin_dir}/${cmd}" - fi - done - chmod 700 "${legacy_bin_dir}" 2>/dev/null || true - export PATH="${legacy_bin_dir}:${PATH}" - bashio::log.warning 'Retrying WireGuard using iptables-legacy wrappers.' - } - - # Internal helper: Attempt connection - _wg_up_attempt() { - local config_path="$1" - output="$(wg-quick up "${config_path}" 2>&1)" || status=$? - - if [ "${status}" -eq 0 ]; then return 0; fi - - # Allow sysctl failures on read-only hosts while keeping the interface up - if echo "${output}" | grep -qi 'net\.ipv4\.conf\.all\.src_valid_mark=1'; then - if echo "${output}" | grep -qiE 'read-only file system|operation not permitted'; then - if ip link show "${wireguard_interface}" >/dev/null 2>&1; then - bashio::log.warning 'WireGuard applied but sysctl net.ipv4.conf.all.src_valid_mark=1 could not be set (read-only). Continuing.' - status=0 - return 0 - fi - fi - fi - - # Check for iptables errors and try legacy fallback - if echo "${output}" | grep -qiE 'iptables-restore|ip6tables-restore|xtables'; then - if command -v iptables-legacy >/dev/null 2>&1; then - wg-quick down "${config_path}" >/dev/null 2>&1 || true - _wg_prepare_legacy - output="$(wg-quick up "${config_path}" 2>&1)" || status=$? - else - bashio::log.warning 'iptables errors detected but iptables-legacy missing.' - status=1 - fi - fi - return "${status}" - } - - # 1. First Attempt - if ! _wg_up_attempt "${wireguard_config}"; then - bashio::log.warning 'Initial WireGuard connection failed. Trying IPv4-only endpoints.' - bashio::log.debug "Output: ${output}" - - # 2. IPv4 Fallback Preparation - local ipv4_config="${WIREGUARD_STATE_DIR}/${wireguard_interface}-ipv4.conf" - : > "${ipv4_config}" - chmod 600 "${ipv4_config}" 2>/dev/null || true - - local line endpoint endpoint_host endpoint_port - while IFS= read -r line || [ -n "$line" ]; do - if [[ "${line}" =~ ^Endpoint ]]; then - endpoint="${line#Endpoint = }" - endpoint_host="${endpoint%:*}" - endpoint_port="${endpoint##*:}" - - # Resolve hostname to IPv4 - mapfile -t ipv4_candidates < <(getent ahostsv4 "${endpoint_host}" | awk '{print $1}' | uniq) - - if [ ${#ipv4_candidates[@]} -gt 0 ]; then - bashio::log.debug "Resolved ${endpoint_host} to ${ipv4_candidates[0]}" - echo "Endpoint = ${ipv4_candidates[0]}:${endpoint_port}" >> "${ipv4_config}" - else - echo "${line}" >> "${ipv4_config}" - fi - else - echo "${line}" >> "${ipv4_config}" - fi - done < "${wireguard_config}" - - wg-quick down "${wireguard_config}" >/dev/null 2>&1 || true - - # 3. Second Attempt (IPv4 only) - if ! _wg_up_attempt "${ipv4_config}"; then - bashio::log.error 'WireGuard failed to establish connection.' - bashio::log.error "${output}" - bashio::exit.nok 'WireGuard start failed.' - fi - fi - - bashio::log.info "WireGuard interface ${wireguard_interface} is up." - - # DNS Refresh - if command -v resolvconf >/dev/null 2>&1; then - resolvconf -u >/dev/null 2>&1 || bashio::log.warning 'resolvconf -u failed.' - fi -} - # --- Main Execution --- openvpn_enabled=false @@ -148,24 +29,9 @@ if [[ "${openvpn_enabled}" == true && "${wireguard_enabled}" == true ]]; then fi if [[ "${openvpn_enabled}" == true ]]; then - - exec /usr/sbin/openvpn \ - --config /config/openvpn/config.ovpn \ - --script-security 2 \ - --up /etc/openvpn/up.sh \ - --down /etc/openvpn/down.sh \ - --pull-filter ignore "route-ipv6" \ - --pull-filter ignore "ifconfig-ipv6" \ - --pull-filter ignore "tun-ipv6" \ - --pull-filter ignore "redirect-gateway ipv6" \ - --pull-filter ignore "dhcp-option DNS6" \ - & - + /usr/local/sbin/vpn openvpn up elif [[ "${wireguard_enabled}" == true ]]; then - - # Run modularized WireGuard setup - _setup_wireguard - + /usr/local/sbin/vpn wireguard up fi # --- Launch qBittorrent --- diff --git a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run index 5117c5fa3..52091f91a 100755 --- a/qbittorrent/rootfs/etc/services.d/vpn-monitor/run +++ b/qbittorrent/rootfs/etc/services.d/vpn-monitor/run @@ -129,13 +129,6 @@ if bashio::config.true 'openvpn_enabled'; then vpn_openvpn=true fi -if [[ "${vpn_openvpn}" == true ]] && ! bashio::config.true 'openvpn_alt_mode'; then - VPN_INTERFACE="tun0" - bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." -else - VPN_INTERFACE="" -fi - if bashio::config.true 'wireguard_enabled'; then vpn_wireguard=true fi @@ -151,6 +144,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 diff --git a/qbittorrent/rootfs/usr/local/bin/resolvconf b/qbittorrent/rootfs/usr/local/bin/resolvconf deleted file mode 100644 index 3e2a91880..000000000 --- a/qbittorrent/rootfs/usr/local/bin/resolvconf +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh -set -eu - -STATE_DIR="/var/run/wireguard/resolvconf" -BACKUP_FILE="${STATE_DIR}/resolv.conf.backup" - -mkdir -p "${STATE_DIR}" - -if [ "$#" -eq 0 ]; then - exit 0 -fi - -command="$1" -shift || true - -restore_backup() { - if [ -f "${BACKUP_FILE}" ]; then - cat "${BACKUP_FILE}" > /etc/resolv.conf - fi -} - -apply_dns() { - iface="$1" - shift || true - - # Skip optional arguments such as -m or -x - while [ "$#" -gt 0 ]; do - case "$1" in - -m|-p|-w) - shift 2 || true - ;; - -x|-y|-Z) - shift 1 || true - ;; - --) - shift - break - ;; - *) - break - ;; - esac - done - - tmp_file="${STATE_DIR}/${iface}.conf" - cat > "${tmp_file}" - - if [ ! -f "${BACKUP_FILE}" ]; then - cp /etc/resolv.conf "${BACKUP_FILE}" 2>/dev/null || true - fi - - { - echo "# Generated by WireGuard add-on resolvconf shim" - cat "${tmp_file}" - } > /etc/resolv.conf -} - -case "${command}" in - -a) - if [ "$#" -eq 0 ]; then - exit 0 - fi - apply_dns "$@" - ;; - -d) - if [ "$#" -gt 0 ]; then - rm -f "${STATE_DIR}/$1.conf" - fi - restore_backup - ;; - -u) - latest_conf="$(find "${STATE_DIR}" -maxdepth 1 -type f -name '*.conf' -print | head -n 1 || true)" - if [ -n "${latest_conf}" ] && [ -f "${latest_conf}" ]; then - { - echo "# Generated by WireGuard add-on resolvconf shim" - cat "${latest_conf}" - } > /etc/resolv.conf - else - restore_backup - fi - ;; - *) - # Treat other commands as successful no-ops to remain compatible with wg-quick. - exit 0 - ;; -esac diff --git a/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore b/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore deleted file mode 100644 index 23bff73d3..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/ip6tables-restore +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_IP6TABLES_RESTORE="/sbin/ip6tables-restore" -if [[ ! -x "${REAL_IP6TABLES_RESTORE}" ]]; then - REAL_IP6TABLES_RESTORE="/usr/sbin/ip6tables-restore" -fi - -cleanup() { - local exit_code=$? - [[ -n "${RULES_FILE:-}" && -f "${RULES_FILE}" ]] && rm -f "${RULES_FILE}" - [[ -n "${SANITIZED_FILE:-}" && -f "${SANITIZED_FILE}" ]] && rm -f "${SANITIZED_FILE}" - return $exit_code -} -trap cleanup EXIT - -RULES_FILE="$(mktemp)" -cat > "${RULES_FILE}" - -ipv6_unavailable() { - local message="$1" - [[ $message =~ [Tt]able[[:space:]]does[[:space:]]not[[:space:]]exist ]] && return 0 - [[ $message =~ address[[:space:]]family[[:space:]]not[[:space:]]supported ]] && return 0 - [[ $message =~ can[[:punct:]]t[[:space:]]initialize[[:space:]]ip6tables[[:space:]]table ]] && return 0 - [[ $message =~ IPv6[[:space:]]support[[:space:]]not[[:space:]]available ]] && return 0 - return 1 -} - -# First attempt with the original ruleset -output="" -if output="$(${REAL_IP6TABLES_RESTORE} "$@" < "${RULES_FILE}" 2>&1)"; then - [[ -n "${output}" ]] && printf '%s\n' "${output}" >&2 - exit 0 -fi -status=$? - -# Retry without comment matches if the kernel is missing the comment module -SANITIZED_FILE="$(mktemp)" -sed -E 's/-m[[:space:]]+comment[[:space:]]+--comment[[:space:]]+"[^"]*"//g' "${RULES_FILE}" > "${SANITIZED_FILE}" - -retry_output="" -if retry_output="$(${REAL_IP6TABLES_RESTORE} "$@" < "${SANITIZED_FILE}" 2>&1)"; then - printf '%s\n' "ip6tables-restore failed with comment matches; reapplied without comments." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "${retry_output}" >&2 - exit 0 -fi -retry_status=$? - -# Final fallback: try legacy backend if available -legacy_output="" -for legacy in /sbin/ip6tables-restore-legacy /usr/sbin/ip6tables-restore-legacy; do - if [[ -x "${legacy}" ]]; then - if legacy_output="$(${legacy} "$@" < "${RULES_FILE}" 2>&1)"; then - printf '%s\n' "ip6tables-restore failed; succeeded using legacy backend." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "${legacy_output}" >&2 - exit 0 - fi - fi - -done - -if ipv6_unavailable "${output}" || ipv6_unavailable "${retry_output}" || ipv6_unavailable "${legacy_output}"; then - printf '%s\n' "IPv6 firewall support not detected; skipping IPv6 ruleset restore and continuing." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "Legacy backend error: ${legacy_output}" >&2 - exit 0 -fi - -printf '%s\n' "ip6tables-restore failed and fallbacks were unsuccessful." >&2 -printf '%s\n' "Original error: ${output}" >&2 -printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 -exit ${retry_status} diff --git a/qbittorrent/rootfs/usr/local/sbin/iptables-restore b/qbittorrent/rootfs/usr/local/sbin/iptables-restore deleted file mode 100644 index 2219b563c..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/iptables-restore +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_IPTABLES_RESTORE="/sbin/iptables-restore" -if [[ ! -x "${REAL_IPTABLES_RESTORE}" ]]; then - REAL_IPTABLES_RESTORE="/usr/sbin/iptables-restore" -fi - -cleanup() { - local exit_code=$? - [[ -n "${RULES_FILE:-}" && -f "${RULES_FILE}" ]] && rm -f "${RULES_FILE}" - [[ -n "${SANITIZED_FILE:-}" && -f "${SANITIZED_FILE}" ]] && rm -f "${SANITIZED_FILE}" - return $exit_code -} -trap cleanup EXIT - -RULES_FILE="$(mktemp)" -cat > "${RULES_FILE}" - -# First attempt with the original ruleset -if output="$(${REAL_IPTABLES_RESTORE} "$@" < "${RULES_FILE}" 2>&1)"; then - [[ -n "${output}" ]] && printf '%s\n' "${output}" >&2 - exit 0 -fi -status=$? - -# Retry without comment matches if the kernel is missing the comment module -SANITIZED_FILE="$(mktemp)" -sed -E 's/-m[[:space:]]+comment[[:space:]]+--comment[[:space:]]+"[^"]*"//g' "${RULES_FILE}" > "${SANITIZED_FILE}" - -if retry_output="$(${REAL_IPTABLES_RESTORE} "$@" < "${SANITIZED_FILE}" 2>&1)"; then - printf '%s\n' "iptables-restore failed with comment matches; reapplied without comments." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${retry_output}" ]] && printf '%s\n' "${retry_output}" >&2 - exit 0 -fi -retry_status=$? - -# Final fallback: try legacy backend if available -for legacy in /sbin/iptables-restore-legacy /usr/sbin/iptables-restore-legacy; do - if [[ -x "${legacy}" ]]; then - if legacy_output="$(${legacy} "$@" < "${RULES_FILE}" 2>&1)"; then - printf '%s\n' "iptables-restore failed; succeeded using legacy backend." >&2 - printf '%s\n' "Original error: ${output}" >&2 - [[ -n "${legacy_output}" ]] && printf '%s\n' "${legacy_output}" >&2 - exit 0 - fi - fi -done - -printf '%s\n' "iptables-restore failed and fallbacks were unsuccessful." >&2 -printf '%s\n' "Original error: ${output}" >&2 -printf '%s\n' "Sanitized retry error: ${retry_output}" >&2 -exit ${retry_status} diff --git a/qbittorrent/rootfs/usr/local/sbin/sysctl b/qbittorrent/rootfs/usr/local/sbin/sysctl deleted file mode 100644 index b76c18c88..000000000 --- a/qbittorrent/rootfs/usr/local/sbin/sysctl +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REAL_SYSCTL="/sbin/sysctl" -if [[ ! -x "${REAL_SYSCTL}" ]]; then - REAL_SYSCTL="/usr/sbin/sysctl" -fi - -if [[ "$#" -ge 2 && "$1" == "-q" && "$2" == "net.ipv4.conf.all.src_valid_mark=1" ]]; then - if "${REAL_SYSCTL}" "$@" >/dev/null 2>&1; then - exit 0 - fi - # Suppress failure for this specific key to keep wg-quick from aborting in unprivileged environments. - exit 0 -fi - -exec "${REAL_SYSCTL}" "$@" diff --git a/qbittorrent/rootfs/usr/local/sbin/vpn b/qbittorrent/rootfs/usr/local/sbin/vpn new file mode 100755 index 000000000..2d2bd6290 --- /dev/null +++ b/qbittorrent/rootfs/usr/local/sbin/vpn @@ -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 " + 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