Files
hassio-addons/qbittorrent/rootfs/etc/services.d/vpn-monitor/run
litinoveweedle 7af8610a25 qBittorrent upnp and firewall for VPN
This is implementation of the UPnP port opening for qBittorrent running on VPN. Implementation also includes simple firewall for incoming connections.
2026-03-21 15:24:35 +01:00

204 lines
5.7 KiB
Plaintext
Executable File

#!/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 [[ "${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
if [[ "${vpn_openvpn}" == true ]]; then
VPN_INTERFACE=$(cat "/var/run/openvpn/interface")
bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)."
elif [[ "${vpn_wireguard}" == true ]]; then
VPN_INTERFACE=$(cat "/var/run/wireguard/interface")
bashio::log.info "VPN monitor set to query external IP through interface ${VPN_INTERFACE} (interface binding)."
fi
if [[ -z "${VPN_INTERFACE}" ]] || ! ip link show "${VPN_INTERFACE}" > /dev/null 2>&1 ; then
bashio::log.error "VPN interface not found."
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}" & wait $!
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