tranmsission

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 apps user
  • eventually remve Zz from volumes

todo Link to heading

  • adding *arr services as needed
  • setup wg-easy to limit access to seedbox
  • fully portable conf for easy migration
  • test 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 apps user has id/gid 568

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 port in 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 watch will 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-01 challenge

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://forums.truenas.com/t/guide-to-installing-transmission-with-pia-and-port-forwarding-on-truenas-24-10-electriceel-and-later/22664

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