From bda1ffedde69ea6cb187867e8ea5ade485f8238d Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:45:37 +0100 Subject: [PATCH] Enhance VPN leak monitoring and IP fetching methods Refactor public IP fetching and VPN leak monitoring logic. Introduce new helper functions for fetching public IP and country code with improved error handling and randomization of URL sources. Update WireGuard and OpenVPN integration to include leak monitoring. --- .../s6-overlay/s6-rc.d/svc-qbittorrent/run | 159 +++++++++++++++--- 1 file changed, 132 insertions(+), 27 deletions(-) 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 b99392d0a..f414e9c95 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 @@ -5,23 +5,134 @@ WEBUI_PORT=${WEBUI_PORT:-8080} export PATH="/usr/local/sbin:/usr/local/bin:${PATH}" -# --- New helper: get public IP with rate-limiting fallbacks --- -_get_public_ip() { - local ip - ip=$( - curl -fsS --max-time 10 https://ifconfig.co/ip \ - || curl -fsS --max-time 10 https://api64.ipify.org \ - || curl -fsS --max-time 10 https://ipecho.net/plain - ) || return 1 - - printf '%s\n' "${ip}" -} +# Global variable used by WireGuard bring-up helper +output="" if bashio::config.true 'silent'; then sed -i 's|/proc/1/fd/1 hassio;|off;|g' /etc/nginx/nginx.conf fi +_fetch_public_ip() { + local resp + local url + local urls=( + "https://icanhazip.com" + "https://ifconfig.me/ip" + "https://api64.ipify.org" + "https://checkip.amazonaws.com" + "https://domains.google.com/checkip" + "https://ipinfo.io/ip" + ) + local shuffled_urls + mapfile -t shuffled_urls < <(printf "%s\n" "${urls[@]}" | shuf) + # Loop through the now-randomized list + for url in "${shuffled_urls[@]}"; do + resp=$(curl -fsS --max-time 5 "${url}" 2>/dev/null || true) + resp="${resp//[[:space:]]/}" + + # Validation (IPv4 or IPv6 regex) + if [[ "${resp}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || \ + [[ "${resp}" =~ ^[0-9a-fA-F:]+$ ]]; then + printf '%s\n' "${resp}" + return 0 + fi + done + return 1 +} + +_fetch_country_code() { + local resp + local url + local urls=( + "https://ipapi.co/country/" + "http://ip-api.com/line/?fields=countryCode" + "https://ifconfig.co/country-iso" + "https://ipinfo.io/country" + "https://www.icloud.com/geo/country_code/" + ) + local shuffled_urls_output + shuffled_urls_output=$(printf '%s\n' "${urls[@]}" | shuf) + while IFS= read -r url; do + # Skip empty lines if any + [[ -z "${url}" ]] && continue + # Fetch the response with a 5-second timeout + resp=$(curl -fsS --max-time 5 "${url}" 2>/dev/null || true) + # Clean whitespace/newlines + resp="${resp//[[:space:]]/}" + # Validation: Ensure the response is exactly 2 letters (ISO 3166-1 alpha-2) + if [[ "${resp}" =~ ^[A-Za-z]{2}$ ]]; then + # Convert to uppercase and print + printf '%s\n' "${resp^^}" + return 0 + fi + done <<< "${shuffled_urls_output}" # Process the shuffled output + return 1 +} + +_vpn_monitor_public_ip() { + # Arg1: label ("OpenVPN", "WireGuard", etc.) + local vpn_label="${1:-VPN}" + local current_ip_file="/currentip" + local baseline_ip + local vpn_ip + local country + local interval + local initial_delay + + interval=${VPN_LEAK_CHECK_INTERVAL:-300} + initial_delay=${VPN_LEAK_INITIAL_DELAY:-60} + + if ! command -v curl >/dev/null 2>&1; then + bashio::log.warning "${vpn_label}: curl not found; VPN leak monitoring disabled." + return 0 + fi + + if ! bashio::fs.file_exists "${current_ip_file}"; then + bashio::log.warning "${vpn_label}: baseline IP file ${current_ip_file} not found; VPN leak monitoring disabled." + return 0 + fi + + baseline_ip="$(tr -d '[:space:]' < "${current_ip_file}")" + if [[ -z "${baseline_ip}" ]]; then + bashio::log.warning "${vpn_label}: baseline IP in ${current_ip_file} is empty; VPN leak monitoring disabled." + return 0 + fi + + bashio::log.info "${vpn_label}: baseline (non-VPN) IP for leak detection: ${baseline_ip}" + bashio::log.debug "${vpn_label}: waiting ${initial_delay}s before first leak check." + sleep "${initial_delay}" + + while true; do + vpn_ip="$(_fetch_public_ip || true)" + if [[ -z "${vpn_ip}" ]]; then + bashio::log.warning "${vpn_label}: failed to obtain VPN public IP from all providers (rate limited or unreachable)." + else + if country="$(_fetch_country_code || true)"; then + bashio::log.info "${vpn_label}: public IP: ${vpn_ip} (${country})." + else + bashio::log.info "${vpn_label}: public IP: ${vpn_ip} (country unknown)." + fi + + if [[ "${vpn_ip}" == "${baseline_ip}" ]]; then + bashio::log.error "${vpn_label}: VPN leak detected: public IP ${vpn_ip} matches baseline ${baseline_ip}. Stopping add-on." + # Try to terminate the service tree cleanly. + s6-svscanctl -t /var/run/s6/services 2>/dev/null || true + exit 1 + fi + fi + + sleep "${interval}" + done +} + +# +# --- Main logic: OpenVPN / WireGuard / qBittorrent --- +# + if bashio::config.true 'openvpn_enabled'; then + # Start leak monitor for OpenVPN in the background + _vpn_monitor_public_ip "OpenVPN" & + exec /usr/sbin/openvpn \ --config /config/openvpn/config.ovpn \ --script-security 2 \ @@ -55,6 +166,7 @@ else 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}" @@ -68,13 +180,11 @@ else _wireguard_up_with_iptables_fallback() { local config_path="$1" - local status + local status=0 - output="" - output=$(wg-quick up "${config_path}" 2>&1) - status=$? + output="$(wg-quick up "${config_path}" 2>&1)" || status=$? - if [ "$status" -eq 0 ]; then + if [ "${status}" -eq 0 ]; then return 0 fi @@ -82,8 +192,7 @@ else if command -v iptables-legacy >/dev/null 2>&1 || command -v ip6tables-legacy >/dev/null 2>&1; then wg-quick down "${config_path}" >/dev/null 2>&1 || true _wireguard_prepare_iptables_legacy - output=$(wg-quick up "${config_path}" 2>&1) - status=$? + output="$(wg-quick up "${config_path}" 2>&1)" || status=$? else bashio::log.warning 'iptables errors detected but iptables-legacy binaries are unavailable in the image.' status=1 @@ -98,9 +207,10 @@ else bashio::log.warning "First attempt output:${bashio::constants.LF}${output}" ipv4_config="${WIREGUARD_STATE_DIR}/${wireguard_interface}-ipv4.conf" - echo -n > "${ipv4_config}" + : > "${ipv4_config}" chmod 600 "${ipv4_config}" 2>/dev/null || true + local line endpoint endpoint_host endpoint_port while IFS= read -r line; do if [[ "${line}" =~ ^Endpoint ]]; then endpoint="${line#Endpoint = }" @@ -140,14 +250,6 @@ else bashio::log.info "WireGuard interface ${wireguard_interface} is up." - # Example usage of the helper: get VPN-protected IP and store it - if vpn_ip="$(_get_public_ip)"; then - echo "${vpn_ip}" > /vpn_ip - bashio::log.info "Detected VPN public IP: ${vpn_ip}" - else - bashio::log.warning 'Unable to determine VPN public IP (all IP services failed).' - fi - # Refresh DNS resolver configuration if resolvconf is present if command -v resolvconf >/dev/null 2>&1; then bashio::log.info 'Refreshing DNS resolver configuration via resolvconf -u.' @@ -157,6 +259,9 @@ else else bashio::log.debug 'resolvconf not found in PATH; skipping DNS refresh.' fi + + # Start VPN leak monitor for WireGuard in background + _vpn_monitor_public_ip "WireGuard" & fi if bashio::config.true 'silent'; then