From 9809407dd7a0d5cfb9c0db64d788876a5498ba63 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:38:25 +0100 Subject: [PATCH] Improve VPN IP monitoring in nginx run script Refactor nginx run script to enhance VPN IP monitoring and logging. --- qbittorrent/rootfs/etc/services.d/nginx/run | 224 +++++++++++++++++--- 1 file changed, 190 insertions(+), 34 deletions(-) diff --git a/qbittorrent/rootfs/etc/services.d/nginx/run b/qbittorrent/rootfs/etc/services.d/nginx/run index 28704a0a9..6a835edc2 100644 --- a/qbittorrent/rootfs/etc/services.d/nginx/run +++ b/qbittorrent/rootfs/etc/services.d/nginx/run @@ -1,44 +1,200 @@ #!/usr/bin/with-contenv bashio # shellcheck shell=bash -set -e -# ============================================================================== -# Wait for transmission to become available -bashio::net.wait_for 8080 localhost 900 +set -euo pipefail -bashio::log.info "Starting NGinx..." +export PATH="/usr/local/sbin:/usr/local/bin:${PATH}" -# Check vpn is working -if [ -f /currentip ]; then - nginx || nginx -s reload & - while true; do - # Get vpn ip - if bashio::config.true 'wireguard_enabled'; then - wireguard_interface="$(cat /var/run/wireguard/interface 2>/dev/null || echo 'wg0')" - curl -s ipecho.net/plain --interface "${wireguard_interface}" > /vpnip - elif bashio::config.true 'openvpn_alt_mode'; then - curl -s ipecho.net/plain > /vpnip +# File where the real (non-VPN) public IP is stored +REAL_IP_FILE="/currentip" + +# How often to re-check VPN IP (seconds) +VPN_CHECK_INTERVAL="${VPN_CHECK_INTERVAL:-300}" + +# Service used to get IP + country (JSON) +VPN_INFO_URL="${VPN_INFO_URL:-https://ipinfo.io/json}" + +# ------------------------------- +# Helpers +# ------------------------------- + +read_real_ip() { + # Reads the "real" IP saved before VPN start + if [[ -f "${REAL_IP_FILE}" ]]; then + # Strip whitespace/newlines just in case + local ip + ip="$(tr -d '[:space:]' < "${REAL_IP_FILE}")" + if [[ -n "${ip}" ]]; then + echo "${ip}" + return 0 + fi + fi + # No baseline is not fatal: just return empty + echo "" + return 0 +} + +get_ip_info() { + # Outputs: " " on success + local json ip country + + if ! json="$(curl -fsS --max-time 10 "${VPN_INFO_URL}" 2>/dev/null)"; then + bashio::log.warning "Unable to reach VPN info service at ${VPN_INFO_URL}." + return 1 + fi + + # Extract "ip" and "country" without jq + ip="$( + printf '%s\n' "${json}" \ + | sed -n 's/.*"ip"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + | head -n1 + )" + country="$( + printf '%s\n' "${json}" \ + | sed -n 's/.*"country"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + | head -n1 + )" + + if [[ -z "${ip}" ]]; then + bashio::log.warning "Failed to parse IP from VPN info response." + return 1 + fi + + printf '%s %s\n' "${ip}" "${country:-Unknown}" +} + +wait_for_vpn_ip() { + # Waits until: + # - we can read an external IP, AND + # - it is different from REAL_IP (if REAL_IP is known) + # Outputs " " when ready. + local attempt out ip country + + for attempt in {1..20}; do + if out="$(get_ip_info)"; then + read -r ip country <<< "${out}" + + if [[ -n "${REAL_IP:-}" ]] && [[ "${ip}" == "${REAL_IP}" ]]; then + bashio::log.warning \ + "External IP still equals real IP (${ip}); VPN not ready yet (attempt ${attempt}/20)." + else + printf '%s %s\n' "${ip}" "${country}" + return 0 + fi else - curl -s ipecho.net/plain --interface tun0 > /vpnip + bashio::log.warning "Unable to query external IP (attempt ${attempt}/20)." fi - # Verify ip has changed - if [[ "$(cat /vpnip)" = "$(cat /currentip)" ]]; then - bashio::log.fatal "VPN is not properly configured. Your ip is exposed. Please fix this, or do not use the vpn alt mode" - bashio::exit.nok - fi - - # Get ip location - COUNTRY=$(curl -s https://ipinfo.io/$(cat /vpnip) | grep country -i -m 1 | cut -d ':' -f 2 | xargs | awk 'gsub(/,$/,x)' || true) - - # Inform by message - bashio::log.info "VPN is up and running with ip $(cat /vpnip), based in country : $COUNTRY" - - # Check every 15m - sleep 15m - - true + sleep 5 done -else - nginx || nginx -s reload + + return 1 +} + +start_nginx_background() { + bashio::log.info "Starting nginx..." + nginx -g 'daemon off;' & + echo $! +} + +# ------------------------------- +# Main logic +# ------------------------------- + +# Detect VPN mode(s) +vpn_openvpn=false +vpn_wireguard=false + +if bashio::config.true 'openvpn_enabled'; then + vpn_openvpn=true fi + +if bashio::config.true 'wireguard_enabled'; then + vpn_wireguard=true +fi + +# If both are enabled, that's a configuration error +if [[ "${vpn_openvpn}" == true && "${vpn_wireguard}" == true ]]; then + bashio::log.error "Both OpenVPN and WireGuard are enabled. Only one VPN mode is supported." + exit 1 +fi + +vpn_enabled=false +vpn_mode="none" + +if [[ "${vpn_openvpn}" == true ]]; then + vpn_enabled=true + vpn_mode="OpenVPN" +fi + +if [[ "${vpn_wireguard}" == true ]]; then + vpn_enabled=true + vpn_mode="WireGuard" +fi + +if [[ "${vpn_enabled}" != true ]]; then + # No VPN: just boot nginx, no IP leak monitoring + bashio::log.info "No VPN enabled (OpenVPN/WireGuard). Starting nginx without IP monitoring." + exec nginx -g 'daemon off;' +fi + +# Read the baseline "real" IP from /currentip +REAL_IP="$(read_real_ip)" + +if [[ -n "${REAL_IP}" ]]; then + bashio::log.info "Real (non-VPN) IP from ${REAL_IP_FILE}: ${REAL_IP}" +else + bashio::log.warning "Real IP file ${REAL_IP_FILE} missing or empty; IP leak detection will be less strict." +fi + +bashio::log.info "VPN mode detected: ${vpn_mode}. Enabling IP leak protection and periodic monitoring." + +# Wait until VPN is up and external IP != REAL_IP +if ! VPN_INFO_OUT="$(wait_for_vpn_ip)"; then + bashio::log.error "Unable to obtain a VPN external IP different from real IP. Exiting to avoid leaking traffic." + exit 1 +fi + +read -r VPN_IP VPN_COUNTRY <<< "${VPN_INFO_OUT}" + +bashio::log.info "VPN external IP: ${VPN_IP} (${VPN_COUNTRY})" + +# Start nginx in background and monitor +nginx_pid="$(start_nginx_background)" + +# Forward termination signals +trap ' + bashio::log.info "Signal received, stopping nginx..." + kill "'"${nginx_pid}"'" 2>/dev/null || true + wait "'"${nginx_pid}"'" 2>/dev/null || true + exit 0 +' SIGTERM SIGINT SIGHUP + +# Monitoring loop +while true; do + # If nginx died, stop this service and let s6 handle restart policy + if ! kill -0 "${nginx_pid}" 2>/dev/null; then + bashio::log.error "nginx process exited unexpectedly; leaving service." + exit 1 + fi + + sleep "${VPN_CHECK_INTERVAL}" + + if ! current_out="$(get_ip_info)"; then + bashio::log.warning "Failed to refresh external IP; keeping previous assumptions." + continue + fi + + read -r current_ip current_country <<< "${current_out}" + + bashio::log.info "Current external IP: ${current_ip} (${current_country})" + + # If we know the real IP, treat equality as a leak + if [[ -n "${REAL_IP}" ]] && [[ "${current_ip}" == "${REAL_IP}" ]]; then + bashio::log.error "IP LEAK DETECTED: current external IP ${current_ip} matches real IP ${REAL_IP}." + bashio::log.error "Stopping nginx and exiting so the supervisor can restart the add-on." + kill "${nginx_pid}" 2>/dev/null || true + wait "${nginx_pid}" 2>/dev/null || true + exit 1 + fi +done