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