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.
This commit is contained in:
litinoveweedle
2026-03-21 15:24:35 +01:00
parent ceab896b46
commit 7af8610a25
6 changed files with 425 additions and 38 deletions

View File

@@ -40,6 +40,7 @@ _parse_config() {
}
_parse_dns() {
local dns_ip
local -a dns_conf=()
local -a dns_backup_ipv4=("8.8.8.8" "1.1.1.1")
local -a dns_backup_ipv6=("2001:4860:4860::8888" "2606:4700:4700::1111")
@@ -155,19 +156,24 @@ _routing_add() {
local local_ipv6=$(ip addr show ${config["Interface"]} | grep 'inet6 ' | awk '{print $2}' | cut -d'/' -f1)
local ipv4
local ipv6
local dns_ip
# add routing rules for local IPs
for ipv4 in ${local_ipv4}; do
config["IPv4Enabled"]="true"
_cmd "ip -4 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1
_cmd "ip -4 rule add priority 1 from ${ipv4} table ${config["Table"]}" || return 1
_cmd "ip -4 rule add priority 1 to ${ipv4}/24 table ${config["Table"]}" || return 1
done
if [ "${config["IPv4Enabled"]}" = "true" ]; then
_cmd "ip -4 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1
fi
for ipv6 in ${local_ipv6}; do
config["IPv6Enabled"]="true"
_cmd "ip -6 rule add priority 1 from ${ipv6} table ${config["Table"]}" || return 1
_cmd "ip -6 rule add priority 1 to ${ipv6}/64 table ${config["Table"]}" || return 1
done
if [ "${config["IPv6Enabled"]}" = "true" ]; then
_cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || true
_cmd "ip -6 route add default dev ${config["Interface"]} table ${config["Table"]}" || return 1
fi
# get valid DNS servers
@@ -182,15 +188,11 @@ _routing_add() {
#_cmd "ip -6 route add ${dns_ip} dev ${config["Interface"]}" || return 1
_cmd "ip -6 rule add priority 1 to ${dns_ip} table ${config["Table"]}" || return 1
done
# Update resolv.conf with VPN DNS servers
_resolvconf "update"
}
_routing_del() {
bashio::log.info "Removing routing rules for VPN interface ${config["Interface"]}..."
_resolvconf "reset"
while _cmd "ip -4 rule del priority 1 from all table ${config["Table"]} 2>/dev/null"; do :; done
while _cmd "ip -4 rule del priority 1 to all table ${config["Table"]} 2>/dev/null"; do :; done
while _cmd "ip -4 route del default dev ${config["Interface"]} table ${config["Table"]} 2>/dev/null"; do :; done
@@ -199,20 +201,52 @@ _routing_del() {
while _cmd "ip -6 route del default dev ${config["Interface"]} table ${config["Table"]} 2>/dev/null"; do :; done
}
# --- Firewall Specific Functions ---
_firewall_add() {
if [ "${config["IPv4Enabled"]}" = "true" ]; then
_cmd "iptables -N pnat" || return 1
_cmd "iptables -A INPUT -i ${config["Interface"]} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" || return 1
_cmd "iptables -A INPUT -i ${config["Interface"]} -p icmp -j ACCEPT" || return 1
_cmd "iptables -A INPUT -i ${config["Interface"]} -j pnat" || return 1
_cmd "iptables -A INPUT -i ${config["Interface"]} -j DROP" || return 1
fi
if [ "${config["IPv6Enabled"]}" = "true" ]; then
_cmd "ip6tables -N pnat" || return 1
_cmd "ip6tables -A INPUT -i ${config["Interface"]} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" || return 1
_cmd "ip6tables -A INPUT -i ${config["Interface"]} -p icmpv6 -j ACCEPT" || return 1
_cmd "ip6tables -A INPUT -i ${config["Interface"]} -j pnat" || return 1
_cmd "ip6tables -A INPUT -i ${config["Interface"]} -j DROP" || return 1
fi
}
_firewall_del() {
if [ "${config["IPv4Enabled"]}" = "true" ]; then
_cmd "iptables -F INPUT" || true
_cmd "iptables -F pnat" || true
_cmd "iptables -X pnat" || true
fi
if [ "${config["IPv6Enabled"]}" = "true" ]; then
_cmd "ip6tables -F INPUT" || true
_cmd "ip6tables -F pnat" || true
_cmd "ip6tables -X pnat" || true
fi
}
# --- WireGuard Specific Logic ---
_wg_wait_handshake() {
_wireguard_check() {
local timeout="${1:-20}"
local iface="${config["Interface"]}"
local peer_pk="${config["PublicKey"]}"
local deadline ts
deadline=$(( $(date +%s) + timeout ))
while [ "$(date +%s)" -lt "${deadline}" ]; do
ping -I "${iface}" -c1 -W1 1.1.1.1 >/dev/null 2>&1 || true
ping -I "${config["Interface"]}" -c1 -W1 1.1.1.1 >/dev/null 2>&1 || true
ts="$(wg show "${iface}" latest-handshakes 2>/dev/null | awk -v pk="${peer_pk}" '$1==pk{print $2; exit}')"
ts="$(wg show "${config["Interface"]}" latest-handshakes 2>/dev/null | awk -v pk="${config["PublicKey"]}" '$1==pk{print $2; exit}')"
if [ -n "${ts}" ] && [ "${ts}" -gt 0 ] 2>/dev/null; then
return 0
fi
@@ -220,17 +254,25 @@ _wg_wait_handshake() {
done
bashio::log.error "WireGuard handshake not established after ${timeout}s (latest-handshake=${ts:-0})."
wg show "${iface}" 2>&1 | while IFS= read -r l; do bashio::log.error "${l}"; done
wg show "${config["Interface"]}" 2>&1 | while IFS= read -r l; do bashio::log.error "${l}"; done
return 1
}
_wireguard_up() {
local local_ip
local -a local_ips=()
local -A local_ip_types=()
local allowed_ip
local -a allowed_ips=()
local -A allowed_ip_types=()
local key
bashio::log.warning "This script force Wireguard to ignore any routes and DNS settings."
bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}"
bashio::log.warning "This routing table will be used for traffic from the VPN interface and to the configured DNS servers."
bashio::log.warning "Qbittorrent bittorrent client shall be set to use the VPN interface ${config["Interface"]} only."
for key in "Interface" "ListenPort" "PrivateKey" "PublicKey" "EndpointIP" "EndpointPort" ; do
for key in "Interface" "ListenPort" "PrivateKey" "PublicKey" "EndpointIP" "EndpointPort" "Address"; do
if [ ! -v config[$key] ] || [ -z "${config[$key]}" ]; then
bashio::log.error "Missing required WireGuard configuration parameter: ${key}"
return 1
@@ -238,28 +280,51 @@ _wireguard_up() {
done
_cmd "ip link add ${config["Interface"]} type wireguard" || return 1
local allowed_ips=""
local -a local_ips=()
mapfile -d ',' -t local_ips < <(echo "${config["Address"]}" | tr -d ' ')
for local_ip in ${local_ips[@]}; do
local result=0
_check_host "${local_ip}" || result=$?
if [ "${result}" -eq 1 ]; then
allowed_ips="${allowed_ips},0.0.0.0/0"
config["IPv4Enabled"]="true"
local_ip_types["${local_ip}"]="ipv4"
allowed_ip_types["0.0.0.0/0"]="ipv4"
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
elif [ "${result}" -eq 2 ]; then
allowed_ips="${allowed_ips},::/0"
config["IPv6Enabled"]="true"
local_ip_types["${local_ip}"]="ipv6"
_cmd "ip addr add ${local_ip} dev ${config["Interface"]}" || return 1
else
bashio::log.warning "Ignoring invalid local IP address: ${local_ip}"
fi
done
allowed_ips="${allowed_ips#,}"
if [ -z "${allowed_ips}" ]; then
bashio::log.error "No valid local IP addresses configured."
if [ ${#local_ip_types[@]} -eq 0 ]; then
bashio::log.error "No valid local IP addresses configured for WireGuard interface."
return 1
fi
mapfile -d ',' -t allowed_ips < <(echo "${config["Address"]}" | tr -d ' ')
for allowed_ip in ${allowed_ips[@]}; do
local result=0
_check_host "${allowed_ip}" || result=$?
if [ "${result}" -eq 1 ] && [ "${config["IPv4Enabled"]}" == "true" ]; then
allowed_ip_types["${allowed_ip}"]="ipv4"
#allowed_ip_types["0.0.0.0/0"]="ipv4"
elif [ "${result}" -eq 2 ] && [ "${config["IPv6Enabled"]}" == "true" ]; then
allowed_ip_types["${allowed_ip}"]="ipv6"
#allowed_ip_types["::/0"]="ipv6"
else
bashio::log.error "Invalid allowed IP address: ${allowed_ip}"
return 1
fi
done
if [ ${#allowed_ip_types[@]} -eq 0 ]; then
bashio::log.error "No valid allowed IP addresses configured for WireGuard peer."
return 1
fi
printf -v allowed_ips '%s,' "${!allowed_ip_types[@]}"
allowed_ips="${allowed_ips%,}"
_cmd "wg set ${config["Interface"]} listen-port ${config["ListenPort"]} private-key ${config["PrivateKey"]}" || return 1
local endpoint="${config["EndpointIP"]}:${config["EndpointPort"]}"
if [[ "${config["EndpointIP"]}" == *:* ]]; then
@@ -279,18 +344,32 @@ _wireguard_up() {
fi
_cmd "ip link set ${config["Interface"]} up" || return 1
_routing_add
_wg_wait_handshake 10 || return 1
# Add routing rules for VPN interface and DNS servers
_routing_add || return 1
# Add firewall rules for VPN interface
_firewall_add || return 1
# Update resolv.conf with VPN DNS servers
_resolvconf "update" || return 1
# Wait for handshake to be established before returning success
_wireguard_check 10 || return 1
}
_wireguard_down() {
_routing_del
# Update resolv.conf to remove VPN DNS servers
_resolvconf "reset" || true
# Remove routing rules for VPN interface and DNS servers
_routing_del || true
# Remove firewall rules for VPN interface
_firewall_del || true
_cmd "ip link set ${config["Interface"]} down" 2>/dev/null || true
_cmd "ip link del ${config["Interface"]}" 2>/dev/null || true
}
wireguard() {
local mode=$1
local key
local interface
local config_file
local WIREGUARD_STATE_DIR="/var/run/wireguard"
@@ -328,7 +407,7 @@ wireguard() {
printf '%s\n' "${config["PrivateKey"]}" > "${WIREGUARD_STATE_DIR}/privatekey"
chmod 600 "${WIREGUARD_STATE_DIR}/privatekey" || true
config["PrivateKey"]="${WIREGUARD_STATE_DIR}/privatekey"
if [ -n "${config["PresharedKey"]:-}" ]; then
printf '%s\n' "${config["PresharedKey"]}" > "${WIREGUARD_STATE_DIR}/presharedkey"
chmod 600 "${WIREGUARD_STATE_DIR}/presharedkey" || true
@@ -384,6 +463,23 @@ wireguard() {
# --- OpenVPN Specific Logic ---
_openvpn_check() {
local timeout="${1:-20}"
local deadline ts
deadline=$(( $(date +%s) + timeout ))
while [ "$(date +%s)" -lt "${deadline}" ]; do
if ip link show "${config["Interface"]}" > /dev/null 2>&1 ; then
return 0
fi
sleep 2
done
bashio::log.error "OpenVPN interface ${config["Interface"]} failed to come up after ${timeout}s."
return 1
}
_openvpn_up() {
bashio::log.warning "This script force OpenvPN to ignore any routes and DNS settings pushed by the server."
bashio::log.warning "Default route will be inserted into custom routing table: ${config["Table"]}"
@@ -418,21 +514,31 @@ _openvpn_up() {
--route-nopull \
--route-noexec" || return 1
#wait for slow OpenVPN interface to come up
for i in {1..10}; do
if ip link show "${config["Interface"]}" > /dev/null 2>&1 ; then
return 0
fi
sleep 2
done
bashio::log.error "OpenVPN interface ${config["Interface"]} failed to come up."
return 1
# Wait for the VPN interface to come up
_openvpn_check 30 || return 1
}
_openvpn_down() {
# Terminate OpenVPN process
pkill -f "openvpn --config ${config["ConfigFile"]}" || true
_routing_del
}
_openpvn_postup() {
# Add routing rules for VPN interface and DNS servers
_routing_add || return 1
# Add firewall rules for VPN interface
_firewall_add || return 1
# Update resolv.conf with VPN DNS servers
_resolvconf "update" || return 1
}
_openpvn_postdown() {
# Update resolv.conf to remove VPN DNS servers
_resolvconf "reset" || true
# Remove routing rules for VPN interface and DNS servers
_routing_del || true
# Remove firewall rules for VPN interface
_firewall_del || true
}
openvpn() {
@@ -480,10 +586,10 @@ openvpn() {
bashio::log.info "OpenVPN on interface ${config["Interface"]} is down."
bashio::exit.ok 'OpenVPN stopped.'
elif [ "${mode}" = "postup" ]; then
_routing_add
_openpvn_postup
bashio::exit.ok 'OpenVPN routes added.'
elif [ "${mode}" = "postdown" ]; then
_routing_del
_openpvn_postdown
bashio::exit.ok 'OpenVPN routes deleted.'
else
bashio::log.error "Invalid OpenVPN mode specified. Use 'up', 'down', 'postup', or 'postdown'."