One Gluetun Tunnel, Two Forwarded Ports, and a 26-Server VPN Test
My qBittorrent behind Gluetun had no forwarded port. One WireGuard tunnel now forwards two ports to two clients. Plus a 26-server speed test, the MTU math, and a decluttarr re-grab loop.
HomeLab, Network and DNS. Updated . 7 min read.
For 18 days I cleaned up stuck torrents with a bot that removed them, blocklisted them, and searched again. The cause was one missing port. My VPN provider would have given me a second one on the same tunnel the whole time. I just never asked for it.
TL;DR: two qBittorrent clients sit behind one Gluetun container. Only one had a forwarded port, so the other could only talk to peers that accept incoming connections, and its downloads stalled. Proton forwards up to 5 ports on one WireGuard connection, so VPN_PORT_FORWARDING_PORTS_COUNT=2 plus a 13-line up script gives each client its own port. While I was in there I tested 26 port-forward servers in 13 cities, fixed the MTU for a PPPoE line, and learned not to run a second Gluetun with the live key.
The setup
Gluetun runs the WireGuard tunnel on my unRAID NUC. Two qBittorrent containers share its network namespace: one for a private tracker on port 8081, one for public torrents on 8080. Both clients talk to the internet only through the tunnel.
Gluetun already did port forwarding. Proton handed out one port, and VPN_PORT_FORWARDING_UP_COMMAND set it as the private client's listen port. The public client got nothing. My notes from 2026-09-05 say it plainly: "a fix needs a second Gluetun tunnel". That sentence was wrong, and it sat there for 18 days.
What no forwarded port does to a torrent
BitTorrent peers connect to each other in both directions. A client with no open port can still dial out, so it reaches every peer that has an open port. What it can't do is connect to another peer that is also closed. Two firewalled peers never meet.
On a popular swarm you barely notice, because plenty of peers have open ports. On a small one it's fatal. I had three public torrents that ran for 1.4 days at 0.2 to 1.7 KB/s, with 1 or 2 seeds each. If those seeds are firewalled too, the swarm is dead to you.
Even healthy swarms were slow. Through the port-less tunnel, two Ubuntu ISO torrents together ran 7 to 24 MB/s, 12 on average, over 4 minutes. A plain HTTP download through the same tunnel did 37 to 44 MB/s.
Treating the symptom: decluttarr
My first answer was queue hygiene. On 2026-09-05 I installed decluttarr for the public client only, and added the slow rule the next day. It checks the queue every 10 minutes and gives each bad torrent a strike:
| Rule | Strikes | Removed after |
|---|---|---|
remove_metadata_missing | 6 | about 1 hour |
remove_stalled | 18 | about 3 hours |
remove_slow (under 1.5 MB/s) | 6 | about 70 minutes |
A removal takes the torrent out of the client, blocklists the release in the arr app, and the arr searches again. The private client is on ignored_download_clients, so decluttarr never touches it.
The re-grab loop
The removals found a gotcha early. One minute after decluttarr removed a torrent and the arr blocklisted it, the arr grabbed the exact same hash again.
The blocklist didn't stop the same hash from coming back. That takes a separate indexer flag in the arr, "Reject Blocklisted Torrent Hashes While Grabbing", and it was off on all 12 indexers (6 per arr).
Then decluttarr made it worse. It keeps every hash it removed in an in-memory deleted list and won't remove the same hash again until the container restarts. The returning torrent sat at 56 of 18 strikes, and the only trace in the log was a "keeps coming back" tip.
Turning the flag on in the arr didn't stick. Prowlarr manages those indexers, and a Prowlarr sync writes its own value over the arr's. The flag is owned by a per-app setting in Prowlarr, syncRejectBlocklistedTorrentHashesWhileGrabbing, under Settings > Apps. I turned it on there and watched one ApplicationIndexerSync keep it. Set it in Prowlarr, never in the arr.
One more thing that cost me time: at INFO level decluttarr hides the strike that triggers the removal. Strike 19 of 18 logs at VERBOSE. The triggered removal line is the proof, and the arr's downloadFailed history event is the second one.
All of this was real work, and all of it was cleaning up after a port I didn't have.
The fix: two ports, one tunnel
The fix was one setting and a short script. Proton forwards up to 5 ports on one connection. Gluetun asks for as many as VPN_PORT_FORWARDING_PORTS_COUNT says and passes them to the up command as a comma-separated list. So the template got:
VPN_PORT_FORWARDING_PORTS_COUNT=2
VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh /gluetun/pf-up.sh {{PORTS}}
And the script, which lives in Gluetun's appdata folder:
#!/bin/sh
P1=${1%%,*}
P2=${1#*,}
set_port() {
wget -q -O- --retry-connrefused --tries=999 --waitretry=2 \
--post-data "json={\"listen_port\":$2,\"random_port\":false,\"upnp\":false}" \
"http://127.0.0.1:$1/api/v2/app/setPreferences" && echo "port $2 -> 127.0.0.1:$1"
}
set_port 8081 "$P1" &
[ "$P2" != "$1" ] && set_port 8080 "$P2" &
waitA few details in there are on purpose:
- localhost works because both clients live in Gluetun's network namespace. Both trust localhost, so there's no login step.
- The retries. Gluetun can run the script before a client's Web API is listening, so
wgetkeeps retrying a refused connection instead of giving up. random_portandupnpoff. The listen port has to be exactly the forwarded one, and nothing should move it.- The
[ "$P2" != "$1" ]guard. If only one port comes back, there's no comma,P2equals the whole argument, and the script leaves the public client alone instead of giving both clients the same port.
Proton hands out new ports after a reconnect, and Gluetun runs the script again each time. Both clients also got reannounce_when_address_changed on, so trackers hear about a new address right away. /tmp/gluetun/forwarded_port now lists both ports.
Picking the server: 26 of them
With the tunnel open anyway, I stopped trusting a random server. Before this, Gluetun was set to one country and picked a server at random. The last pick had a 135 ms round trip.
I tested 26 port-forward servers in 13 European cities from the NUC. For each one: ping, a 200 MB download from a UK host, 100 MB from a French host, and a 100 MB upload to Cloudflare.
| Where | Result |
|---|---|
| Marseille | 106 ms, all 4 servers near 24 MB/s upload |
| Madrid | 121 to 124 ms, 3 of 4 good (one failed to connect once) |
| Paris | 3.6 MB/s upload on both servers |
| Frankfurt, Zurich | 9 to 10 MB/s upload |
| Milan, Brussels | some servers routed at 170 to 190 ms with 3 MB/s upload |
| UK | 117 to 149 ms |
The surprise was how little the peak mattered. Every good server hit my line limit on a single HTTP stream, 37 to 41 MB/s down and 20 to 25 MB/s up. What differed was latency and whether the upload stayed steady. Paris and Marseille are in the same country, and one did 3.6 MB/s up while the other did 24.
So Gluetun now gets SERVER_CITIES=Marseille,Madrid with PORT_FORWARD_ONLY=on, and an empty SERVER_COUNTRIES. That's 8 servers, all of them measured.
The MTU math
The line is PPPoE, which leaves a path MTU of 1492. WireGuard over IPv4 adds 60 bytes of headers. So anything above 1432 fragments.
I tried path MTU discovery first. It gave 1133 on the live tunnel and 1388 to 1440 in the tests, and 1440 is already too big for the line. A number that moves that much isn't one I want to feed a config, so I fixed it: WIREGUARD_MTU=1420. That's under the 1432 ceiling with a little room, and it gave the best result on the server I tuned it on.
Don't test with the live key
One gotcha from the server test. I ran a second Gluetun container with the same WireGuard key while the live one was up. Same key, two tunnels. The live server started losing its NAT-PMP mapping every 1 to 2 minutes:
UDP external port requested as X but received Y
That line showed up 17 times, and the private client stayed firewalled until I rebuilt the live container. If you test servers, generate a separate key for the test, or stop the live tunnel first.
Also worth knowing: after any Gluetun rebuild, rebuild every container that uses its network. They keep a handle to the old container's namespace and sit there with no network until you do.
How I know it took
Checked the same night, at 23:27:
- round trip 105 ms, down from 135
wg0at MTU 1420- both listen ports answer from outside, tested with TCP from the NUC host to the VPN exit address
- both clients show
connected; the private one saidfirewalledbefore - 200 MB down at 37.3 MB/s, 100 MB up at 23.0 MB/s
- all 7 Prowlarr indexers still valid from the new exit
The decluttarr rules stay. A dead swarm is still dead with an open port. But the note that said "a fix needs a second Gluetun tunnel" is gone, and I'd check the provider's port limit before building a second tunnel for anything.
Questions
- Can Gluetun forward more than one port on a single VPN connection?
- Yes, with Proton VPN. Set VPN_PORT_FORWARDING_PORTS_COUNT to the number you need (Proton allows up to 5). Gluetun passes them to VPN_PORT_FORWARDING_UP_COMMAND as a comma-separated list in {{PORTS}}, and writes them to /tmp/gluetun/forwarded_port. You do not need a second tunnel for a second client.
- How do I give each qBittorrent client its own forwarded port?
- Point VPN_PORT_FORWARDING_UP_COMMAND at a small script that splits {{PORTS}} and calls each client's /api/v2/app/setPreferences with listen_port set, random_port false and upnp false. Both clients share Gluetun's network namespace, so the script reaches them on localhost. Gluetun runs the script again after every reconnect, when Proton hands out new ports.
- What WireGuard MTU should I use on a PPPoE line?
- PPPoE leaves a path MTU of 1492, and WireGuard over IPv4 adds 60 bytes, so the tunnel MTU must be 1432 or less. I set WIREGUARD_MTU=1420 as a fixed value, because path MTU discovery through the tunnel gave unstable results between 1133 and 1440.
- Why does Sonarr or Radarr grab a torrent again right after decluttarr removed it?
- Blocklisting a release does not stop a grab of the same hash from another indexer unless Reject Blocklisted Torrent Hashes While Grabbing is on. If Prowlarr manages the indexers, a sync resets that flag in the arr, so set syncRejectBlocklistedTorrentHashesWhileGrabbing in Prowlarr under Settings > Apps instead.