mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-06-17 12:59:13 +02:00
fix(qbittorrent): prevent recursive routing with blackhole route in table 1000
Agent-Logs-Url: https://github.com/alexbelgium/hassio-addons/sessions/14f8993e-d073-4a6b-8c36-40ebf1b0e396 Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
96b21edf48
commit
07f5f82e5b
@@ -1,5 +1,5 @@
|
||||
## 5.2.0-18 (13-05-2026)
|
||||
- OpenVPN: fix "Recursive routing detected" — previous fix added the VPN server host route only to the main routing table, which qBittorrent's traffic (policy-routed via `from <tun_ip> -> table 1000`) never consults; the route must also be added to table 1000 so the kernel selects the physical device, causing EHOSTUNREACH for the tun-bound qBittorrent socket
|
||||
## 5.2.0-19 (13-05-2026)
|
||||
- OpenVPN: simplify recursive routing fix — use a single blackhole route for the VPN server IP in table 1000 (no AWK, no ipcalc, no physical device detection required)
|
||||
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
@@ -143,4 +143,4 @@ schema:
|
||||
slug: qbittorrent
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "5.2.0-18"
|
||||
version: "5.2.0-19"
|
||||
|
||||
@@ -535,60 +535,30 @@ _openvpn_down() {
|
||||
pkill -f "openvpn --config ${config["ConfigFile"]}" || true
|
||||
# Safety-net cleanup in case the --down callback was never invoked
|
||||
_routing_del || true
|
||||
# Safety-net: remove host routes for VPN server if postdown was never invoked
|
||||
# Safety-net: remove blackhole route for VPN server if postdown was never invoked
|
||||
if [ -f "${OPENVPN_STATE_DIR}/server_ip" ]; then
|
||||
local saved_ip
|
||||
saved_ip=$(cat "${OPENVPN_STATE_DIR}/server_ip" 2>/dev/null || true)
|
||||
if [ -n "${saved_ip}" ]; then
|
||||
ip -4 route del "${saved_ip}/32" 2>/dev/null || true
|
||||
ip -4 route del "${saved_ip}/32" table "${config["Table"]}" 2>/dev/null || true
|
||||
ip -4 route del blackhole "${saved_ip}/32" table "${config["Table"]}" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "${OPENVPN_STATE_DIR}/server_ip"
|
||||
fi
|
||||
}
|
||||
|
||||
_openvpn_postup() {
|
||||
# Add explicit host routes for VPN server endpoint to prevent recursive routing.
|
||||
# With --route-noexec, OpenVPN skips adding its own host route for the remote
|
||||
# endpoint. Without it, sockets bound to the VPN interface (e.g. qBittorrent
|
||||
# bound to tun0) can send traffic destined for the VPN server IP through the
|
||||
# tunnel, which OpenVPN detects as recursive routing and logs as an error.
|
||||
#
|
||||
# qBittorrent uses SO_BINDTODEVICE=tun0, so all its traffic matches the policy
|
||||
# rule "from <tun_ip> -> table 1000". Table 1000 has "default dev tun0", so
|
||||
# even a /32 host route for the VPN server in the *main* table is irrelevant –
|
||||
# it is never consulted for qBittorrent's traffic. The /32 route must be added
|
||||
# to table 1000 as well, pointing to the physical device. When the kernel
|
||||
# selects the physical device as output but the socket is bound to tun0, it
|
||||
# returns EHOSTUNREACH and the packet never reaches the tun device.
|
||||
# Prevent recursive routing: add a blackhole route for the VPN server IP in
|
||||
# table 1000. qBittorrent is bound to tun0, so its traffic is policy-routed
|
||||
# into table 1000 (rule: from <tun_ip> -> table 1000). Without this, packets
|
||||
# destined for the VPN server go through tun0, OpenVPN detects the loop and
|
||||
# drops them. The blackhole makes qBittorrent's traffic to the server IP fail
|
||||
# immediately (EHOSTUNREACH) instead of looping. OpenVPN itself is not bound
|
||||
# to tun0, so its traffic uses the main table and reaches the server normally.
|
||||
if [ -n "${trusted_ip:-}" ]; then
|
||||
# Validate that trusted_ip is a well-formed IPv4 address before using it
|
||||
if ! ipcalc -c -4 "${trusted_ip}" >/dev/null 2>&1; then
|
||||
bashio::log.warning "trusted_ip '${trusted_ip}' is not a valid IPv4 address; VPN server host route not added. Recursive routing may occur."
|
||||
else
|
||||
local physical_gw physical_dev
|
||||
# Find physical default gateway and device, excluding the VPN interface.
|
||||
# Field order from `ip route show default`: default via GW dev DEV ...
|
||||
physical_gw=$(ip -4 route show default table main | awk -v iface="${config["Interface"]}" '$0 !~ iface && /via/ {print $3; exit}')
|
||||
physical_dev=$(ip -4 route show default table main | awk -v iface="${config["Interface"]}" '$0 !~ iface && /dev/ {for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
|
||||
if [ -n "${physical_gw}" ] && [ -n "${physical_dev}" ]; then
|
||||
bashio::log.info "Adding host route for VPN server ${trusted_ip} via ${physical_gw} (${physical_dev}) to prevent recursive routing."
|
||||
local route_err
|
||||
# Add to main routing table (belt)
|
||||
route_err=$(ip -4 route add "${trusted_ip}/32" via "${physical_gw}" 2>&1) || \
|
||||
bashio::log.debug "Could not add main table route for ${trusted_ip} (may already exist): ${route_err}"
|
||||
# Add to VPN routing table (suspenders): qBittorrent traffic from
|
||||
# the tun IP is policy-routed into table 1000. Adding a /32 here
|
||||
# for the VPN server overrides the "default dev tun0" catch-all and
|
||||
# directs those packets to the physical device, causing EHOSTUNREACH
|
||||
# for the tun-bound socket so no packet ever reaches the tunnel.
|
||||
route_err=$(ip -4 route add "${trusted_ip}/32" via "${physical_gw}" dev "${physical_dev}" table "${config["Table"]}" 2>&1) || \
|
||||
bashio::log.debug "Could not add VPN table route for ${trusted_ip} (may already exist): ${route_err}"
|
||||
echo "${trusted_ip}" > "${OPENVPN_STATE_DIR}/server_ip"
|
||||
else
|
||||
bashio::log.warning "Could not determine physical gateway/device; VPN server host route not added. Recursive routing may occur."
|
||||
fi
|
||||
fi
|
||||
bashio::log.info "Adding blackhole route for VPN server ${trusted_ip} in table ${config["Table"]} to prevent recursive routing."
|
||||
ip -4 route add blackhole "${trusted_ip}/32" table "${config["Table"]}" 2>/dev/null \
|
||||
&& echo "${trusted_ip}" > "${OPENVPN_STATE_DIR}/server_ip" \
|
||||
|| bashio::log.warning "Could not add blackhole route for VPN server ${trusted_ip}."
|
||||
fi
|
||||
|
||||
# Add routing rules for VPN interface and DNS servers
|
||||
@@ -602,7 +572,7 @@ _openvpn_postup() {
|
||||
}
|
||||
|
||||
_openvpn_postdown() {
|
||||
# Remove host routes for VPN server (added in postup to prevent recursive routing)
|
||||
# Remove blackhole route for VPN server (added in postup to prevent recursive routing)
|
||||
local server_ip=""
|
||||
if [ -n "${trusted_ip:-}" ]; then
|
||||
server_ip="${trusted_ip}"
|
||||
@@ -610,8 +580,7 @@ _openvpn_postdown() {
|
||||
server_ip=$(cat "${OPENVPN_STATE_DIR}/server_ip" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "${server_ip}" ]; then
|
||||
ip -4 route del "${server_ip}/32" 2>/dev/null || true
|
||||
ip -4 route del "${server_ip}/32" table "${config["Table"]}" 2>/dev/null || true
|
||||
ip -4 route del blackhole "${server_ip}/32" table "${config["Table"]}" 2>/dev/null || true
|
||||
rm -f "${OPENVPN_STATE_DIR}/server_ip"
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user