Merge branch 'dev'
This commit is contained in:
commit
e6bf893c05
5 changed files with 152 additions and 43 deletions
10
.github/workflows/docker-image.yml
vendored
10
.github/workflows/docker-image.yml
vendored
|
@ -17,6 +17,7 @@ on:
|
|||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
TARGET_PLATFORMS: linux/amd64,linux/arm64
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
|
@ -29,6 +30,14 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: ${{ env.TARGET_PLATFORMS }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
|
@ -49,3 +58,4 @@ jobs:
|
|||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ env.TARGET_PLATFORMS }}
|
||||
|
|
22
Dockerfile
22
Dockerfile
|
@ -15,24 +15,24 @@ FROM ubuntu:jammy
|
|||
LABEL org.opencontainers.image.source="https://github.com/soxfor/qbittorrent-natmap"
|
||||
LABEL org.opencontainers.image.base.name="ubuntu:jammy"
|
||||
LABEL description="Map port via NAT-PMP and update qBittorrent configuration"
|
||||
LABEL version="1.0.2"
|
||||
LABEL version="1.0.5"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt update
|
||||
RUN apt install --no-install-suggests --no-install-recommends -y natpmpc curl bc
|
||||
RUN apt install --no-install-suggests --no-install-recommends -y natpmpc curl bc netcat-openbsd
|
||||
RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
RUN apt clean
|
||||
|
||||
ENV QBITTORRENT_SERVER=''
|
||||
ENV QBITTORRENT_PORT='8080'
|
||||
ENV QBITTORRENT_USER='admin'
|
||||
ENV QBITTORRENT_PASS='adminadmin'
|
||||
ENV VPN_GATEWAY=''
|
||||
ENV VPN_CT_NAME='gluetun'
|
||||
ENV VPN_IF_NAME='tun0'
|
||||
ENV CHECK_INTERVAL='300'
|
||||
ENV NAT_LEASE_LIFETIME='300'
|
||||
ENV QBITTORRENT_SERVER=
|
||||
ENV QBITTORRENT_PORT=8080
|
||||
ENV QBITTORRENT_USER=admin
|
||||
ENV QBITTORRENT_PASS=adminadmin
|
||||
ENV VPN_GATEWAY=
|
||||
ENV VPN_CT_NAME=gluetun
|
||||
ENV VPN_IF_NAME=tun0
|
||||
ENV CHECK_INTERVAL=300
|
||||
ENV NAT_LEASE_LIFETIME=300
|
||||
|
||||
COPY --from=docker-cli /usr/bin/docker /usr/bin/docker
|
||||
COPY data/start.sh /start.sh
|
||||
|
|
65
README.md
65
README.md
|
@ -1,21 +1,56 @@
|
|||
# qbittorrent-natmap
|
||||
# qBittorrent-NatMap
|
||||
|
||||
Docker container to maintain and update the NAT-PMP/UPnP forwarded port to qBittorrent.
|
||||
The objective of this container is to run a script that requests a port forward (via NAT-PMP) from the VPN provider and upon success changes the listening port of the qBittorrent client when running in Docker
|
||||
|
||||
Tested with Gluetun using ProtonVPN (Wireguard) and qBittorrent.
|
||||
This solution is currently in use and tested with [Gluetun](https://github.com/qdm12/gluetun) and [qBittorrent](https://github.com/linuxserver/docker-qbittorrent) from Linuxserver.io and with VPN fron ProtonVPN using Wireguard.
|
||||
|
||||
This solution can probably need a few tweaks and error handling, also some work in opening only the needed port on the VPN container.
|
||||
## What made me do this?
|
||||
|
||||
In the repo a sample docker-compose.yml can be found.
|
||||
The need to improve the seeding/upload performance and not finding any work done for this scenario (qBittorrent using docker'ized VPN), but finding [this post on reddit](https://old.reddit.com/r/ProtonVPN/comments/10owypt/successful_port_forward_on_debian_wdietpi_using/) by u/TennesseeTater for Deluge made me try and do something similar. His post is also referenced in the [ProtonVPN Guide][1].
|
||||
|
||||
The following variables can be set:
|
||||
## Why not modify the Gluetun image and include natpmpc there?
|
||||
|
||||
- QBITTORRENT_SERVER (Default: <not set>)*
|
||||
- QBITTORRENT_PORT (Default: 8080)
|
||||
- QBITTORRENT_USER (Default: admin)
|
||||
- QBITTORRENT_PASS (Default: adminadmin)
|
||||
- VPN_CT_NAME (Default: gluetun)
|
||||
- VPN_IF_NAME (Default: tun0)
|
||||
- VPN_GATEWAY* (Default: <not set>)*
|
||||
- CHECK_INTERVAL (Default: 300s)
|
||||
- NAT_LEASE_TIME (Default: 300s)
|
||||
Well, as far as I could find, Alpine Linux doesn't have natively the binary for **natpmpc**, the NAT-PMP client used to request the *port forward* as per the instructions for [manual mapping][1] on ProtonVPN. Gluetun is using alpine as it's base image.
|
||||
|
||||
On AlpineLinux package info: [natpmpc binary not found][2] and [here][3] a request still in open state.
|
||||
|
||||
If I had the binary needed on the Gluetun container a script running on the host system instead of the container would probably suffice, allowing the following action: doing "docker exec <container> natpmpc <args>"
|
||||
|
||||
## What does the script do/modify?
|
||||
|
||||
So far:
|
||||
|
||||
* Evaluates the required variables for execution (for now, if they're set) and if **docker.sock** was mapped from the host into the container
|
||||
* If the above succeeds:
|
||||
* Get the VPN public IP
|
||||
* Grab the SessionID cookie from qBittorrent
|
||||
* Grab the current listen port from qBittorrent
|
||||
* After the configured checks pass, a function to request and verify the port mapping starts
|
||||
* Using *natpmpc* a port mapping request is made to the address defined in `VPN_GATEWAY` for udp and tcp
|
||||
* Comparison is made between the current configured port in qBittorrent and the currently active mapped port from the VPN
|
||||
* If a condition of different port is found:
|
||||
* The new port is configured in qBittorrent, along with that it's also disabled the random port setting and UPnP mapping from the torrent client
|
||||
* A couple of commands are executed to add/remove iptables rules regarding the previous active and new active mapped port on the VPN container
|
||||
|
||||
These actions are performed continuously (in a loop, every 5 minutes (default, can be lowered/increased)), most likely an option to set a failure count will be added in the future.
|
||||
|
||||
## Configurable variables:
|
||||
|
||||
* QBITTORRENT_SERVER (Defaults to empty, **needs to be set**)
|
||||
* If setting here an address not related to the `VPN_IF_NAME` (default: tun0) a few users have [reported](https://old.reddit.com/r/ProtonVPN/comments/11ubgvi/port_forward_with_qbittorrent_and_protonvpn_on/jcxirts/) needing to set `FIREWALL_OUTBOUND_SUBNETS` for the Gluetun/VPN container
|
||||
* For ProtonVPN using Wireguard and qBittorrent container using `VPN_CT_NAME` as network_mode this would be set to **10.2.0.2**
|
||||
* QBITTORRENT_PORT (Defaults to **8080**)
|
||||
* QBITTORRENT_USER (Defaults to **admin**)
|
||||
* QBITTORRENT_PASS (Defaults to **adminadmin**)
|
||||
* VPN_GATEWAY (Defaults to empty, **needs to be set**)
|
||||
* The value for this variable will be the `VPN_IF_NAME` (default: tun0) gateway address, not the `VPN_ENDPOINT_IP` from the Gluetun/VPN Container when using Wireguard, [more info here](https://github.com/qdm12/gluetun/wiki/Custom-provider#wireguard-only).
|
||||
* For ProtonVPN using Wireguard this would be set to **10.2.0.1**
|
||||
* VPN_CT_NAME (Defaults to **gluetun**)
|
||||
* VPN_IF_NAME (Defaults to **tun0**)
|
||||
* CHECK_INTERVAL (Defaults to **300s**)
|
||||
* NAT_LEASE_LIFETIME (Defaults to **300s**)
|
||||
* Ideally both `CHECK_INTERVAL` and `NAT_LEASE_LIFETIME` should be set equal or the check interval lower than the lease lifetime, but never above.
|
||||
|
||||
[1]: https://protonvpn.com/support/port-forwarding-manual-setup/
|
||||
[2]: https://pkgs.alpinelinux.org/contents?file=natpmpc&path=&name=&branch=edge
|
||||
[3]: https://gitlab.alpinelinux.org/alpine/awall/-/issues/2220
|
||||
|
|
|
@ -13,6 +13,7 @@ findconfiguredport() {
|
|||
}
|
||||
|
||||
findactiveport() {
|
||||
natpmpc -g ${VPN_GATEWAY} -a 0 0 udp ${NAT_LEASE_LIFETIME} >/dev/null 2>&1
|
||||
natpmpc -g ${VPN_GATEWAY} -a 0 0 tcp ${NAT_LEASE_LIFETIME} | grep -oP '(?<=Mapped public port.).*(?=.protocol.*)'
|
||||
}
|
||||
|
||||
|
@ -25,18 +26,44 @@ qbt_changeport(){
|
|||
return $?
|
||||
}
|
||||
|
||||
public_ip=$(getpublicip)
|
||||
qbt_sid=$(qbt_login)
|
||||
configured_port=$(findconfiguredport ${qbt_sid})
|
||||
active_port=''
|
||||
qbt_checksid(){
|
||||
if echo $(curl -s --header "Referer: http://${QBITTORRENT_SERVER}:${QBITTORRENT_PORT}" --cookie "${qbt_sid}" http://${QBITTORRENT_SERVER}:${QBITTORRENT_PORT}/api/v2/app/version) | grep -qi forbidden; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
qbt_isreachable(){
|
||||
nc -4 -vw 5 ${QBITTORRENT_SERVER} ${QBITTORRENT_PORT} 2>&1 &>/dev/null
|
||||
}
|
||||
|
||||
fw_delrule(){
|
||||
if (docker exec ${VPN_CT_NAME} /sbin/iptables -L INPUT -n | grep -qP "^ACCEPT.*${configured_port}.*"); then
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -D INPUT -i ${VPN_IF_NAME} -p tcp --dport ${configured_port} -j ACCEPT
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -D INPUT -i ${VPN_IF_NAME} -p udp --dport ${configured_port} -j ACCEPT
|
||||
fi
|
||||
}
|
||||
|
||||
fw_addrule(){
|
||||
if ! (docker exec ${VPN_CT_NAME} /sbin/iptables -L INPUT -n | grep -qP "^ACCEPT.*${active_port}.*"); then
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -A INPUT -i ${VPN_IF_NAME} -p tcp --dport ${active_port} -j ACCEPT
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -A INPUT -i ${VPN_IF_NAME} -p udp --dport ${active_port} -j ACCEPT
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_portmap() {
|
||||
res=0
|
||||
public_ip=$(getpublicip)
|
||||
|
||||
if echo $(curl -s --header "Referer: http://${QBITTORRENT_SERVER}:${QBITTORRENT_PORT}" --cookie "${qbt_sid}" http://${QBITTORRENT_SERVER}:${QBITTORRENT_PORT}/api/v2/app/version) | grep -qi forbidden; then
|
||||
if ! qbt_checksid; then
|
||||
echo "$(timestamp) | qBittorrent Cookie invalid, getting new SessionID"
|
||||
qbt_sid=$(qbt_login)
|
||||
else
|
||||
echo "$(timestamp) | qBittorrent SessionID Ok!"
|
||||
fi
|
||||
|
||||
configured_port=$(findconfiguredport ${qbt_sid})
|
||||
|
@ -48,12 +75,9 @@ get_portmap() {
|
|||
|
||||
if [ ${configured_port} != ${active_port} ]; then
|
||||
if qbt_changeport ${qbt_sid} ${active_port}; then
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -A INPUT -i ${VPN_IF_NAME} -p tcp --dport ${active_port} -j ACCEPT
|
||||
docker exec ${VPN_CT_NAME} /sbin/iptables -D INPUT -i ${VPN_IF_NAME} -p tcp --dport ${configured_port} -j ACCEPT
|
||||
if docker exec ${VPN_CT_NAME} /sbin/iptables -L INPUT -n | grep -qP "^ACCEPT.*${active_port}.*"; then
|
||||
echo "$(timestamp) | IPTables rule added for port ${active_port} on ${VPN_CT_NAME} container"
|
||||
if fw_delrule; then
|
||||
echo "$(timestamp) | IPTables rule deleted for port ${configured_port} on ${VPN_CT_NAME} container"
|
||||
fi
|
||||
sleep 3
|
||||
echo "$(timestamp) | Port Changed to: $(findconfiguredport ${qbt_sid})"
|
||||
else
|
||||
echo "$(timestamp) | Port Change failed."
|
||||
|
@ -63,9 +87,14 @@ get_portmap() {
|
|||
echo "$(timestamp) | Port OK (Act: ${active_port} Cfg: ${configured_port})"
|
||||
fi
|
||||
|
||||
if fw_addrule; then
|
||||
echo "$(timestamp) | IPTables rule added for port ${active_port} on ${VPN_CT_NAME} container"
|
||||
fi
|
||||
|
||||
return $res
|
||||
}
|
||||
|
||||
pre_reqs() {
|
||||
while read var; do
|
||||
[ -z "${!var}" ] && { echo "$(timestamp) | ${var} is empty or not set."; exit 1; }
|
||||
done << EOF
|
||||
|
@ -82,6 +111,27 @@ EOF
|
|||
|
||||
[ ! -S /var/run/docker.sock ] && { echo "$(timestamp) | Docker socket doesn't exist or is inaccessible"; exit 2; }
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
load_vals(){
|
||||
public_ip=$(getpublicip)
|
||||
if qbt_isreachable; then
|
||||
qbt_sid=$(qbt_login)
|
||||
configured_port=$(findconfiguredport ${qbt_sid})
|
||||
else
|
||||
echo "$(timestamp) | Unable to reach qBittorrent at ${QBITTORRENT_SERVER}:${QBITTORRENT_PORT}"
|
||||
exit 6
|
||||
fi
|
||||
active_port=''
|
||||
}
|
||||
|
||||
if pre_reqs; then load_vals; fi
|
||||
|
||||
[ -z ${public_ip} ] && { echo "$(timestamp) | Unable to grab VPN Public IP. Please check configuration"; exit 3; }
|
||||
[ -z ${configured_port} ] && { echo "$(timestamp) | qBittorrent configured port value is empty(?). Please check configuration"; exit 4; }
|
||||
[ -z ${qbt_sid} ] && { echo "$(timestamp) | Unable to grab qBittorrent SessionID. Please check configuration"; exit 5; }
|
||||
|
||||
while true;
|
||||
do
|
||||
if get_portmap; then
|
||||
|
|
|
@ -8,12 +8,26 @@ services:
|
|||
environment:
|
||||
- TZ=Etc/UTC
|
||||
- QBITTORRENT_SERVER=ip.a.dd.r
|
||||
# - QBITTORRENT_PORT= # Defaults to 8080
|
||||
# - QBITTORRENT_USER= # Defaults to admin
|
||||
# - QBITTORRENT_PASS= # Defaults to adminadmin
|
||||
# - VPN_CT_NAME= # Defaults to gluetun
|
||||
# - QBITTORRENT_PORT=
|
||||
# Defaults to 8080
|
||||
# - QBITTORRENT_USER=
|
||||
# Defaults to admin
|
||||
# - QBITTORRENT_PASS=
|
||||
# Defaults to adminadmin
|
||||
# - VPN_CT_NAME=
|
||||
# Defaults to gluetun
|
||||
- VPN_GATEWAY=ip.a.dd.r
|
||||
# - VPN_IF_NAME= # Defaults to tun0
|
||||
# - CHECK_INTERVAL= # Defaults to 300sec
|
||||
# - NAT_LEASE_LIFETIME= # Defaults to 300sec
|
||||
# - VPN_IF_NAME=
|
||||
# Defaults to tun0
|
||||
# - CHECK_INTERVAL=
|
||||
# Defaults to 300sec
|
||||
# - NAT_LEASE_LIFETIME=
|
||||
# Defaults to 300sec
|
||||
depends_on:
|
||||
# VPN Container Name
|
||||
- gluetun
|
||||
# qBittorrent Container Name
|
||||
- qbittorrent
|
||||
network_mode: "container:gluetun" # Specify the VPN container name here
|
||||
# or
|
||||
# network_mode: "service:gluetun" # if defined on the same docker-compose file
|
||||
|
|
Reference in a new issue