mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-07-27 00:10:32 +02:00
This is implementation of the UPnP port opening for qBittorrent running on VPN. Implementation also includes simple firewall for incoming connections.
204 lines
5.7 KiB
Plaintext
Executable File
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
|