
rationale Link to heading
- seedbox behind homerouter/firewall/vps
- access via lan/vpn/ssh
- no open ports router/firewall
- airvpn+gluetun handle port forwarding tunnel
- hidden traffic from isp/provider
- nice webui to handle several wireguard clients
- using docker/podman for portability (debian/rhel)
- works with truenas scale 25.10 (as docker custom yaml)
- static version of transmission (for private trackers)
- caddy handla dns via dns-01 challenge
- caddy reverse proxy webuis
context Link to heading
i needed to modernize an old seedbox after migrating from alpine lxc with transmission to truenas scale’s docker
this stack should live on any vps or homeserver whithout the need to open any ports (on friend’s computers) and not using proprietary networks such as tailscale.
headscale need a second server + a little overhead. being a very simple seedbox without any other *arr’s purpose.
using airvpn’s port forwarding magic (this is not sponsored) to handle stack’s in/out.
the excellent gluetun can handle all of this.
truenas scale being debian under the hood it is no so different, any gnu/linux distrubution will do
podman or docker Link to heading
podman is presented here but to be docker compatbile this compose.yaml need
- remove security-opt from gluetun
- change 0 uid/guid to your real guid or 568 for truenas
appsuser - eventually remve Zz from volumes
todo Link to heading
- adding
*arr services as needed setup wg-easy to limit access to seedboxfully portable conf for easy migrationtest on rhel host with rootless podman- migrate from podman to docker truenas scale compatible
- implementing warpgate as bastion instead of simple wireguard
- automate update and caddy build
- operate yaml’s stack via api/webhook midclt (truenas scale only)
insert diagram
users files and permissions Link to heading
user Link to heading
- for docker on truescale default
appsuser has id/gid568
for podman gnu/linux distro it’s highly recommended to use a different user with no ssh/sudo privileges
your user should be in tun group for wg-easy
sudo useradd -m -G tun seedstack
sudo passwd seedstack
sudo loginctl enable-linger seedstack
then login with this user
su --login seedstack
directories Link to heading
create needed dir/files
mkdir -p /mnt/drum/lab/sharestack/{caddy/{config,data},files/{complete,downloads,incomplete,watch},flood,gluetun,transmission}
env file Link to heading
most of secrets are stored in
/mnt/drum/lab/sharestack/.env
generate wg-easy password with
then double the $$
podman run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'redacted'
double all single $ from output
airvpn port forwarding Link to heading
client conf Link to heading
generate wireguard conf in webui
- client area > config generator > wireguard > choose your countries/server
extract those from conf file
you’ll need those in .env or compose.yaml
- presharedkey
- ptrivatekey
- public ip
bittorent port forward conf Link to heading
this port is used for bittorent traffic in/out tunnel
- once stack is up do
test portin webui to confirm it’s open
open a port forwarding for this device
client area > ports > manage
- ports: 51822
- enabled : yes
- tcp+udp
- ipv4/6 (your choice)
- local port: 51822 (depending on your topology)
wireguard port forward conf Link to heading
this port is used for wireguard clients to enter from outside
open a port forwarding for the same device
client area > ports > manage
- ports:
- enabled : yes
- udp
- ipv4/6 (your choice)
- local port: 52522 (depending on your topology)
gluetun vpn/network Link to heading
gluetun does
- vpn connection to airvpn
- update airvpns’s list of server /24h
- handle port forwarding for bittorrent :51822/tcp/udp and wireguard :52522/udp (no need to open any firewall/router ports)
- use privacy respectful dns over tls servers for trackers
- expose only caddy’s :443 for flood access to lan/vpn clients
to allow wireguard clients to get in though gluetun add those iptables post rules
/mnt/drum/lab/sharestack/gluetun/post-rules.sh
chmod x /mnt/drum/lab/sharestack/gluetun/post-rules.sh
label=disable is needed for this conainter otherwise wg-easy will not work
gluetun podman block
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
group_add:
- keep-groups
# remove security_opt to be docker compatible
security_opt:
- label=disable
ports:
- '443:443'
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- VPN_SERVICE_PROVIDER=airvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=redacted
- WIREGUARD_PRESHARED_KEY=redacted
- WIREGUARD_ADDRESSES=redacted/32
- SERVER_COUNTRIES=redacted
- SERVER_CITIES=redacted
- UPDATER_VPN_SERVICE_PROVIDERS=airvpn
- UPDATER_PERIOD=24h
- FIREWALL_VPN_INPUT_PORTS=65170,52509
- DNS_SERVER=on
- DNS_UPSTREAM_RESOLVER_TYPE=dot
- DNS_UPSTREAM_RESOLVERS=quad9,libredns
- DNS_UPDATE_PERIOD=24h
- BLOCK_SURVEILLANCE=on
- FIREWALL_ALLOW_LAN=true
- FIREWALL_INPUT_PORTS=443
volumes:
- ./gluetun:/gluetun:Z
- ./gluetun:/tmp/gluetun:Z
- ./gluetun/post-rules.sh:/iptables/post-rules.sh:ro,z
wg-easy Link to heading
using a nice webui to manage clients access to seedbox
the ports needed
- FIREWALL_VPN_INPUT_PORTS=65170,52509
here are the ports that wg-easy will give flow
- FIREWALL_INPUT_PORTS=443,51821
transmission bittorrent Link to heading
conf file is needed for
- defining our port :51822
- enabling rpc for webui flood access
- many other options you should be aware of for private trackers dht pex …
- setup password in cear text first then will be hashed automatically
- every torrent in
watchwill start automatically
/mnt/drum/lab/sharestack/transmission/settings.json
{
"alt-speed-down": 50,
"alt-speed-enabled": false,
"alt-speed-time-begin": 540,
"alt-speed-time-day": 127,
"alt-speed-time-enabled": false,
"alt-speed-time-end": 1020,
"alt-speed-up": 50,
"announce-ip": "",
"announce-ip-enabled": false,
"anti-brute-force-enabled": false,
"anti-brute-force-threshold": 100,
"bind-address-ipv4": "0.0.0.0",
"bind-address-ipv6": "::",
"blocklist-enabled": false,
"blocklist-url": "http://www.example.com/blocklist",
"cache-size-mb": 4,
"default-trackers": "",
"dht-enabled": false,
"download-dir": "/downloads/complete",
"download-queue-enabled": true,
"download-queue-size": 5,
"encryption": 1,
"idle-seeding-limit": 30,
"idle-seeding-limit-enabled": false,
"incomplete-dir": "/downloads/incomplete",
"incomplete-dir-enabled": true,
"lpd-enabled": false,
"message-level": 2,
"peer-congestion-algorithm": "",
"peer-id-ttl-hours": 6,
"peer-limit-global": 200,
"peer-limit-per-torrent": 50,
"peer-port": 51822,
"peer-port-random-high": 65535,
"peer-port-random-low": 52152,
"peer-port-random-on-start": false,
"peer-socket-tos": "le",
"pex-enabled": false,
"port-forwarding-enabled": false,
"preallocation": 1,
"prefetch-enabled": true,
"queue-stalled-enabled": true,
"queue-stalled-minutes": 30,
"ratio-limit": 2,
"ratio-limit-enabled": false,
"rename-partial-files": true,
"rpc-authentication-required": false,
"rpc-bind-address": "0.0.0.0",
"rpc-enabled": true,
"rpc-host-whitelist": "",
"rpc-host-whitelist-enabled": false,
"rpc-password": "redacted",
"rpc-port": 9091,
"rpc-socket-mode": "0750",
"rpc-url": "/transmission/",
"rpc-username": "",
"rpc-whitelist": "",
"rpc-whitelist-enabled": false,
"scrape-paused-torrents-enabled": true,
"script-torrent-added-enabled": false,
"script-torrent-added-filename": "",
"script-torrent-done-enabled": false,
"script-torrent-done-filename": "",
"script-torrent-done-seeding-enabled": false,
"script-torrent-done-seeding-filename": "",
"seed-queue-enabled": true,
"seed-queue-size": 10,
"speed-limit-down": 0,
"speed-limit-down-enabled": false,
"speed-limit-up": 0,
"speed-limit-up-enabled": false,
"start-added-torrents": true,
"tcp-enabled": true,
"torrent-added-verify-mode": "fast",
"trash-original-torrent-files": false,
"umask": "002",
"upload-slots-per-torrent": 14,
"utp-enabled": false,
"watch-dir": "/watch",
"watch-dir-enabled": true
}
transmission podman block
transmission:
image: lscr.io/linuxserver/transmission:amd64-4.1.0-r0-ls329
container_name: transmission
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
environment:
- TZ=Europe/Paris
- PEERPORT=51822
- PUID=0
- PGID=0
volumes:
- ./transmission:/config:z
- ./files:/downloads:z
- ./files/watch:/watch:z
- ./redistribute:/media:z
flood webui Link to heading
need to know transmission’s rpc url defined in settings.json
it also need to use the same directory as transmission
check this security precautions
flood block
flood:
container_name: flood
environment:
- HOME=/config
- TRANSMISSION_RPC_URL=http://localhost:9091/transmission/rpc
image: jesec/flood
network_mode: service:gluetun
restart: unless-stopped
user: '0:0'
volumes:
- /mnt/drum/lab/sharestack/transmission:/config
- /mnt/drum/lab/sharestack/files:/downloads
caddy Link to heading
dns provider (optionnal) Link to heading
-
if you choose http challenge at some point you’ll need to open :80 tcp or play with gluetun
-
this example use ovh’s api token to manage
dns-01challenge
A record is *.homelab.domain.tld > localip
go to https://eu.api.ovh.com/createToken/
create an app for caddy
- GET /domain/zone/domain.tld/*
- PUSH /domain/zone/domain.tld/*
- DELETE /domain/zone/domain.tld/*
building caddy (optionnal) Link to heading
if you need some plugins as dns plugin you’ll need to build caddy
/mnt/drum/lab/sharestack/caddy.build
ARG CADDY_VERSION=latest
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/ovh
FROM caddy:${CADDY_VERSION}
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
reverse proxy Link to heading
caddy act as a simple reverse proxy for flood
caddy block
caddy:
build:
context: .
dockerfile: ./caddy.build
image: caddy_custom:latest
container_name: caddy
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro,z
- ./caddy/data:/data:Z
- ./caddy/config:/config:Z
- ./logs:/srv/data:Z
all logins are logged in ~/sharestack/logs/download_access.log
Caddyfile
(tls_ovh) {
tls {
dns ovh {
endpoint https://eu.api.ovh.com/v1
application_key redacted
application_secret redacted
consumer_key redacted
}
}
}
(log_common) {
log {
format json
output file data/{args[0]}_access.log {
roll_local_time
roll_keep 12
roll_keep_for 365d
}
}
}
download.home.domain.tld {
reverse_proxy localhost:3000
encode zstd gzip
import tls_ovh
import log_common download
}
vpn.home.domain.tld {
reverse_proxy localhost:52509
encode zstd gzip
import tls_ovh
import log_common vpn
}
logging in flood Link to heading
login url is
https://download.home.domain.tld
login with
user/password setup in settings.json
use this url as rpc url (yes it’s clear http but insde torrentnet network not exposed to lan)
http://gluetun:9091/transmission/rpc
all logins are logged in ~/sharestack/logs/download_access.log
full podman stack Link to heading
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
group_add:
- keep-groups
security_opt:
- label=disable
ports:
- '443:443'
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- VPN_SERVICE_PROVIDER=airvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=redacted
- WIREGUARD_PRESHARED_KEY=redacted
- WIREGUARD_ADDRESSES=redacted/32
- SERVER_COUNTRIES=redacted
- SERVER_CITIES=redacted
- UPDATER_VPN_SERVICE_PROVIDERS=airvpn
- UPDATER_PERIOD=24h
- FIREWALL_VPN_INPUT_PORTS=65170,52509
- DNS_SERVER=on
- DNS_UPSTREAM_RESOLVER_TYPE=dot
- DNS_UPSTREAM_RESOLVERS=quad9,libredns
- DNS_UPDATE_PERIOD=24h
- BLOCK_SURVEILLANCE=on
- FIREWALL_ALLOW_LAN=true
- FIREWALL_INPUT_PORTS=443
volumes:
- ./gluetun:/gluetun:Z
- ./gluetun:/tmp/gluetun:Z
- ./gluetun/post-rules.txt:/iptables/post-rules.txt:ro,z
wg-easy:
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
environment:
- LANG=en
- TZ=Europe/Paris
- UI_TRAFFIC_STATS=true
- UI_CHART_TYPE=1
- WG_HOST=redacted.airdns.org
- FIREWALL_VPN_INPUT_PORTS=65170,52509
- FIREWALL_INPUT_PORTS=443,51821
- WG_PORT=52509
- WG_DEFAULT_DNS=10.64.0.1
- WG_DEFAULT_ADDRESS=10.8.0.x
- WG_ALLOWED_IPS=10.8.0.0/24,10.64.0.0/24
- WG_POST_UP=echo "no iptables"
- WG_POST_DOWN=echo "no iptables"
- LANG=en
- PASSWORD_HASH=redacted
- PORT=51821
- WG_PERSISTENT_KEEPALIVE=25
- WG_USERSPACE=false
volumes:
- ./wg-easy:/etc/wireguard:Z
caddy:
build:
context: .
dockerfile: ./caddy.build
image: caddy_custom:latest
container_name: caddy
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro,z
- ./caddy/data:/data:Z
- ./caddy/config:/config:Z
- ./logs:/srv/data:Z
transmission:
image: lscr.io/linuxserver/transmission:amd64-4.1.0-r0-ls329
container_name: transmission
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
environment:
- TZ=Europe/Paris
- PEERPORT=65170
- PUID=0
- PGID=0
volumes:
- ./transmission:/config:z
- ./files:/downloads:z
- ./files/watch:/watch:z
- ./redistribute:/media:z
flood:
image: jesec/flood
container_name: flood
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
- transmission
user: '0:0'
environment:
- HOME=/config
- TRANSMISSION_RPC_URL=http://localhost:9091/transmission/rpc
volumes:
- ./transmission:/config:z
- ./files:/downloads:z
- ./redistribute:/media:z
jellyfin:
image: jellyfin/jellyfin
container_name: jellyfin
network_mode: service:gluetun
restart: unless-stopped
depends_on:
- gluetun
user: '0:0'
environment:
- JELLYFIN_PublishedServerUrl=https://watch.home.domain.tld
volumes:
- ./jellyfin/config:/config:Z
- ./jellyfin/cache:/cache:Z
- ./files:/downloads:z
update stack Link to heading
for caddy you need to rebuild it for each upgrade
cd /mnt/.ix-apps/app_configs/sharestack/versions/1.0.0/templates/rendered sudo docker build
then
sudo docker pull
there’s an error for caddy nevermind
! caddy Warning pull access denied for caddy_custom, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
troubleshooting Link to heading
at containers start soem debug can be dound here on truenas scale
/var/log/app_lifecycle.log
- check port 51822 is ok for tranmisssion
to add a counter rule on specific tcp/udp 51822
sudo docker exec gluetun iptables -I INPUT 1 -p tcp --dport 51822 -j ACCEPT
sudo docker exec gluetun iptables -I INPUT 1 -p udp --dport 51822 -j ACCEPT
check if there’s some packets
sudo docker exec gluetun iptables -L INPUT -v -n | grep 51822
erase counters
sudo docker exec gluetun iptables -Z INPUT
drink some tea
check again
sudo docker exec gluetun iptables -L INPUT -v -n | grep 51822
check fromm flood if rpc is working it rejects auth 3 failed attempts. restart daemon if several tests
curl -u username:password https://localhost/transmission/rpc
ressources Link to heading
https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/airvpn.md
https://docs.linuxserver.io/images/docker-transmission/#via-docker-compose
https://deepwiki.com/qdm12/gluetun-wiki/3.6-airvpn
https://github.com/haugene/docker-transmission-openvpn/discussions/2660
https://www.reddit.com/r/truenas/comments/vjmvmp/comment/idkga5i/?force-legacy-sct=1
https://airvpn.org/faq/port_forwarding/
https://github.com/jesec/flood/issues/500
https://github.com/jesec/flood/wiki/Security-precautions
- side note you can use your own wireguard server to do port forwarding