Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,5 @@ RUN apk add --no-cache --update -l wget && \
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
deluser openvpn && \
mkdir /gluetun
COPY extras/scripts/qbittorrent-port-update.sh /scripts/qbittorrent-port-update.sh
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
183 changes: 183 additions & 0 deletions extras/scripts/qbittorrent-port-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/bin/sh

build_default_url() {
port="${1:-$WEBUI_PORT}"
echo "http://127.0.0.1:${port}/api"
}

# default values
VPN_PORT=""
VPN_INTERFACE="tun0"
VPN_ADDRESS=""
WEBUI_PORT="8080"
WEBUI_URL=$(build_default_url "$WEBUI_PORT")

# it might take a few tries for qBittorrent to be available (e.g. slow loading with many torrents)
WGET_OPTS="--retry-connrefused --tries=5"

usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Update qBittorrent listening port, network interface, and address via its WebUI API."
echo "This script is designed to work with Gluetun's VPN_PORT_FORWARDING_UP_COMMAND"
echo "and VPN_PORT_FORWARDING_DOWN_COMMAND."
echo ""
echo "WARNING: If you do not provide --iface and --addr, they will be set to default values on every run"
echo ""
echo "Options:"
echo " --help Show this help message and exit"
echo " --user USER Specify the qBittorrent username"
echo " (Omit if authentication is disabled for localhost)"
echo " --pass PASS Specify the qBittorrent password"
echo " (Omit if authentication is disabled for localhost)"
echo " --port PORT Specify the qBittorrent listening port (peer-port)"
echo " REQUIRED"
echo " --iface IFACE Specify the network interface to bind to"
echo " Examples: \"\" (any interface), \"lo\", \"eth0\", \"tun0\", etc."
echo " Default: \"${VPN_INTERFACE}\""
echo " --addr ADDR Specify the network address to bind to"
echo " Examples: \"\" (all addresses), \"0.0.0.0\" (all IPv4), \"::\" (all IPv6), or a specific IP"
echo " Default: \"${VPN_ADDRESS}\""
echo " --webui-port PORT Specify the qBittorrent WebUI Port. Not compatible with --url"
echo " Default: \"${WEBUI_PORT}\""
echo " --url URL Specify the qBittorrent API URL. Not compatible with --webui-port"
echo " Default: \"${WEBUI_URL}\""
echo ""
echo "Examples:"
echo "# With authentication:"
echo "VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c \"/scripts/qbittorrent-port-update.sh --user ADMIN --pass **** --port {{PORT}} --iface {{VPN_INTERFACE}} --webui-port 8080\""
echo "VPN_PORT_FORWARDING_DOWN_COMMAND=/bin/sh -c \"/scripts/qbittorrent-port-update.sh --user ADMIN --pass **** --port 0 --iface lo --webui-port 8080\""
echo "# Without authentication (\"Bypass authentication for clients on localhost\" enabled in qBittorrent):"
echo "VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c \"/scripts/qbittorrent-port-update.sh --port {{PORT}} --iface {{VPN_INTERFACE}} --webui-port 8080\""
echo "VPN_PORT_FORWARDING_DOWN_COMMAND=/bin/sh -c \"/scripts/qbittorrent-port-update.sh --port 0 --iface lo --webui-port 8080\""
}

while [ $# -gt 0 ]; do
case "$1" in
--help)
usage
exit 0
;;
--user)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --user requires a non-empty argument."
usage
exit 1
fi
USERNAME="$2"
_USECRED=true
shift 2
;;
--pass)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --pass requires a non-empty argument."
usage
exit 1
fi
PASSWORD="$2"
_USECRED=true
shift 2
;;
--port)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --port requires a non-empty argument."
usage
exit 1
fi
VPN_PORT=$(echo "$2" | cut -d',' -f1)
shift 2
;;
--iface)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --iface requires a non-empty argument."
usage
exit 1
fi
VPN_INTERFACE="$2"
Comment on lines +103 to +108
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --iface option block rejects an explicitly empty value (--iface ""), but the usage text lists "" (any interface) as a valid example. Consider allowing an empty string by only erroring when the argument is missing (i.e., $# -lt 2), not when $2 is empty, and ensure downstream JSON generation handles the empty value correctly.

Copilot uses AI. Check for mistakes.
shift 2
;;
--addr)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --addr requires a non-empty argument."
usage
exit 1
fi
VPN_ADDRESS="$2"
Comment on lines +112 to +117
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --addr option block rejects an explicitly empty value (--addr ""), but the usage text lists "" (all addresses) as a valid example. Consider allowing an empty string by only erroring when the argument is missing (i.e., $# -lt 2), not when $2 is empty, and ensure the JSON payload remains valid when the address is empty.

Copilot uses AI. Check for mistakes.
shift 2
;;
--webui-port)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --webui-port requires a non-empty argument."
usage
exit 1
fi
WEBUI_PORT="$2"
WEBUI_URL=$(build_default_url "$WEBUI_PORT")
shift 2
;;
--url)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "Error: --url requires a non-empty argument."
usage
exit 1
fi
WEBUI_URL="$2"
shift 2
Comment on lines +120 to +137
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text says --webui-port is not compatible with --url (and vice-versa), but the parser currently allows both and whichever appears last silently wins. Please add an explicit validation after argument parsing to fail fast when both are provided, or otherwise define and document precedence.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done

if [ -z "${VPN_PORT}" ]; then
echo "ERROR: --port is required but not provided"
exit 1
fi

if [ "${_USECRED}" = "true" ]; then
# make sure username AND password were provided
if [ -z "${USERNAME}" ]; then
echo "ERROR: qBittorrent username not provided"
exit 1
fi
if [ -z "${PASSWORD}" ]; then
echo "ERROR: qBittorrent password not provided"
exit 1
fi

cookie=$(wget ${WGET_OPTS} -qO- \
--header "Referer: ${WEBUI_URL}" \
--post-data "username=${USERNAME}&password=${PASSWORD}" \
"${WEBUI_URL}/v2/auth/login" \
--server-response 2>&1 | \
grep -i "set-cookie:" | \
sed 's/.*set-cookie: //I;s/;.*//')

if [ -z "${cookie}" ]; then
echo "ERROR: Failed to authenticate with qBittorrent. Check username/password or verify WebUI is accessible"
exit 1
fi

# set cookie for future requests
WGET_OPTS="${WGET_OPTS} --header=Cookie:$cookie"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cookie is derived from potentially multiple Set-Cookie headers; if multiple lines are returned, the variable may contain newlines and later be appended into WGET_OPTS, producing an invalid --header argument. Consider constraining this to the specific cookie qBittorrent requires (e.g., first match for the session cookie) and/or stripping newlines before adding it to request headers.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still a problem now that is uses a cookie jar?

fi

# update qBittorrent preferences via API, the first call disabled everything and sets safe defaults
# This is required as per https://github.com/qdm12/gluetun-wiki/pull/147 and https://github.com/qdm12/gluetun/issues/2997#issuecomment-3566749335
wget ${WGET_OPTS} -qO- --post-data="json={\"random_port\":false,\"upnp\":false,\"listen_port\":0,\"current_network_interface\":\"lo\",\"current_interface_address\":\"127.0.0.1\"}" "$WEBUI_URL/v2/app/setPreferences"
if [ $? -ne 0 ]; then
echo "ERROR: Failed to reset qBittorrent settings"
exit 1
fi

# second call to set the actual port, interface and address
wget ${WGET_OPTS} -qO- --post-data="json={\"listen_port\":$VPN_PORT,\"current_network_interface\":\"$VPN_INTERFACE\",\"current_interface_address\":\"$VPN_ADDRESS\"}" "$WEBUI_URL/v2/app/setPreferences"
if [ $? -ne 0 ]; then
Comment on lines +194 to +200
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON payload is constructed via string interpolation without escaping VPN_INTERFACE / VPN_ADDRESS. If either contains a quote/backslash (or other unexpected characters), the request JSON will be invalid and could potentially change the intended preferences. Consider either JSON-escaping these values before building the payload, or strictly validating them against an allowlist pattern (interface name / IP literal / empty) before sending.

Copilot uses AI. Check for mistakes.
echo "ERROR: Failed to apply qBittorrent port/interface settings"
exit 1
fi

echo "qBittorrent updated to use peer-port: ${VPN_PORT}, interface: \"${VPN_INTERFACE}\", address: \"${VPN_ADDRESS}\""