#!/usr/bin/with-contenv bashio # shellcheck shell=bash set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:${PATH}" REAL_IP_FILE="/currentip" VPN_CHECK_INTERVAL="${VPN_CHECK_INTERVAL:-300}" VPN_INFO_URL="${VPN_INFO_URL:-https://ipinfo.io}" # ------------------------------- # Helpers # ------------------------------- _fetch_public_ip() { local resp local url local curl_iface_opts=() 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 if [[ -n "${VPN_INTERFACE:-}" ]]; then curl_iface_opts+=(--interface "${VPN_INTERFACE}") fi mapfile -t shuffled_urls < <(printf "%s\n" "${urls[@]}" | shuf) for url in "${shuffled_urls[@]}"; do resp=$(curl -fsS --max-time 5 "${curl_iface_opts[@]}" "${url}" 2>/dev/null || true) resp="${resp//[[:space:]]/}" 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 } read_real_ip() { local attempt ip for attempt in {1..6}; do if [[ -f "${REAL_IP_FILE}" ]]; then ip="$(tr -d '[:space:]' < "${REAL_IP_FILE}")" if [[ -n "${ip}" ]]; then echo "${ip}" return 0 fi fi sleep 5 done echo "" return 0 } get_ip_info() { local ip country json info_url url if ! ip="$(_fetch_public_ip)"; then bashio::log.warning "Unable to determine external IP from fallback IP services." return 1 fi country="Unknown" info_url="${VPN_INFO_URL:-}" if [[ -n "${info_url}" ]]; then if [[ "${info_url}" == *"%s"* ]]; then printf -v url "${info_url}" "${ip}" else url="${info_url%/}/${ip}/json" fi if json="$(curl -fsS --max-time 10 "${url}" 2>/dev/null || true)" && [[ -n "${json}" ]]; then country="$( printf '%s\n' "${json}" \ | sed -n 's/.*"country"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -n1 )" [[ -z "${country}" ]] && country="Unknown" fi fi printf '%s %s\n' "${ip}" "${country}" } wait_for_vpn_ip() { local attempt out ip country for attempt in {1..5}; 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}/5)." else printf '%s %s\n' "${ip}" "${country}" return 0 fi else bashio::log.warning "Unable to query external IP (attempt ${attempt}/5)." fi sleep 5 done return 1 } # ------------------------------- # Main logic # ------------------------------- 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 bashio::config.true 'openvpn_alt_mode'; then if [[ "${vpn_openvpn}" == true ]]; then VPN_INTERFACE="tun0" bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." elif [[ "${vpn_wireguard}" == true ]]; then if [[ -f /var/run/wireguard/interface ]]; then VPN_INTERFACE="$(cat /var/run/wireguard/interface)" else VPN_INTERFACE="wg0" fi bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)." else VPN_INTERFACE="" fi else VPN_INTERFACE="" fi if [[ "${vpn_openvpn}" != true && "${vpn_wireguard}" != true ]]; then bashio::log.info "VPN leak monitor not started because no VPN is enabled." exit 0 fi if [[ "${vpn_openvpn}" == true && "${vpn_wireguard}" == true ]]; then bashio::log.error "Both OpenVPN and WireGuard are enabled. Only one VPN mode is supported." bashio::addon.stop exit 1 fi 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 detected; enabling IP leak protection and periodic monitoring." if ! VPN_INFO_OUT="$(wait_for_vpn_ip)"; then bashio::log.error "Unable to obtain a VPN external IP different from real IP. Stopping add-on." bashio::addon.stop exit 1 fi read -r VPN_IP VPN_COUNTRY <<< "${VPN_INFO_OUT}" bashio::log.info "VPN external IP: ${VPN_IP} (${VPN_COUNTRY})" while true; do 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 [[ -n "${REAL_IP}" ]] && [[ "${current_ip}" == "${REAL_IP}" ]]; then bashio::log.error "IP LEAK DETECTED: current external IP ${current_ip} matches real IP ${REAL_IP}. Stopping add-on." bashio::addon.stop exit 1 fi if [[ -z "${REAL_IP}" ]] && [[ "${current_ip}" == "${VPN_IP}" ]]; then # No baseline to compare; only report changes in IP for visibility VPN_IP="${current_ip}" fi done