mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-02-03 10:27:43 +01:00
201 lines
5.5 KiB
Plaintext
201 lines
5.5 KiB
Plaintext
#!/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: "<ip> <country>" 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 "<ip> <country>" 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
|