#!/usr/bin/with-contenv bashio # shellcheck shell=bash set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:${PATH}" # 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 bashio::log.warning "Unable to query external IP (attempt ${attempt}/20)." fi sleep 5 done return 1 } start_nginx_background() { bashio::log.info "Starting nginx..." nginx & 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 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