From ed54947119c78c79e052750075c2ccd14478aee2 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 25 Aug 2026 21:34:21 +0000 Subject: [PATCH 1/3] Port Torrentserver appliance to Trixie Move the appliance to Debian's Transmission 4 package series and make its authenticated RPC, storage, peer port, ClamAV completion hook, and service dependencies explicit. Remove stale rTorrent and nginx residue that no longer participates in the Apache-based product. Pass the firstboot password over standard input and replace Transmission settings atomically so credentials do not appear in process arguments and a failed write cannot leave a truncated configuration. Add a focused local torrent lifecycle and Samba round-trip probe, HTTPS and service checks, signed Debian APT update evidence, and the Wave 2 crosswalk. Verified against the exact Trixie Transmission package in a disposable no-tracker probe, plus shell and Python syntax, diff hygiene, result protocol, credential scans, and completion-hook failure boundaries. --- README.rst | 10 +- changelog | 10 ++ conf.d/main | 62 +++++-- docs/v19.0-testing.md | 43 +++++ overlay/etc/init.d/rtorrent | 72 -------- .../etc/nginx/sites-available/transmission | 17 -- .../usr/lib/inithooks/bin/torrentserver.py | 93 +++++----- .../lib/inithooks/firstboot.d/40torrentserver | 8 +- overlay/usr/local/bin/clamav-scan | 39 +++- plan/main | 3 +- tests/v19.sh | 170 ++++++++++++++++++ 11 files changed, 370 insertions(+), 157 deletions(-) create mode 100644 docs/v19.0-testing.md delete mode 100755 overlay/etc/init.d/rtorrent delete mode 100644 overlay/etc/nginx/sites-available/transmission create mode 100755 tests/v19.sh diff --git a/README.rst b/README.rst index 4182aba..698d7b0 100644 --- a/README.rst +++ b/README.rst @@ -16,13 +16,17 @@ and on top of that: formats. - E-mail download completion notification via Postfix MTA (bound to localhost). -- File sharing (`Transmission`_) configuration: +- Download server (`Transmission`_) configuration: + + - Authenticated web interface and RPC endpoint on HTTPS port 12322. + - Downloads stored in */srv/storage/download* with incomplete data in + */srv/storage/incoming*. - Anti-virus / malware scanning via `ClamAV`_. - Anti-virus signatures are auto-updated. - Automatically quarantines unsafe downloads. - - Anti-virus logfile: */var/log/rtorrent/clamav.log* + - Anti-virus logfile: */var/log/transmission/clamav.log* - File server (`Samba`_) configuration: @@ -65,7 +69,7 @@ configuration details if behind a Firewall/Router/NAT Credentials *(passwords set at first boot)* ------------------------------------------- -- Webmin, Webshell, SSH, Samba: username **root** +- Webmin, SSH, Samba: username **root** - Transmission: username **admin** - Web based file manager (WebDAV CGI): - username **root** (or Samba users) diff --git a/changelog b/changelog index 60da687..45d9bec 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,13 @@ +turnkey-torrentserver-19.0 (1) turnkey; urgency=low + + * Update the appliance to Debian 13/Trixie and Debian's Transmission 4.1 + package series. + * Keep Transmission credentials out of process arguments and configure its + authenticated RPC, storage, completion scan, and peer port explicitly. + * Replace stale rTorrent service residue with a Transmission lifecycle test. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-torrentserver-18.0 (1) turnkey; urgency=low * Add wsdd package for Windows Service for Devices - so Fileserver shows up diff --git a/conf.d/main b/conf.d/main index 309f352..175ebda 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,31 +1,58 @@ -#!/bin/sh -ex +#!/bin/bash -e -STORAGE=/srv/storage -PASSWORD=turnkey - -mkdir -p $STORAGE/incoming -mkdir -p $STORAGE/quarantine -mkdir -p $STORAGE/download +set -x -chown -R debian-transmission:users $STORAGE/incoming -chown -R debian-transmission:users $STORAGE/quarantine -chown -R debian-transmission:users $STORAGE/download +STORAGE=/srv/storage +install -d -o debian-transmission -g users -m 2770 \ + "$STORAGE/incoming" "$STORAGE/quarantine" "$STORAGE/download" +install -d -o debian-transmission -g users -m 0750 /var/log/transmission -mkdir /var/log/rtorrent +# Permit the completion hook to submit files to clamd and let clamd read the +# shared storage tree after the next service start. +adduser debian-transmission clamav +adduser clamav users # configure transmission CONF=/etc/transmission-daemon/settings.json -sed -i 's|"download-dir": .*|"download-dir": "\/srv\/storage\/download",|' $CONF +service transmission-daemon stop || true +python3 - "$CONF" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding='utf-8') as settings_file: + settings = json.load(settings_file) + +settings.update({ + 'download-dir': '/srv/storage/download', + 'incomplete-dir': '/srv/storage/incoming', + 'incomplete-dir-enabled': True, + 'peer-port': 6882, + 'rpc-authentication-required': True, + 'rpc-bind-address': '127.0.0.1', + 'rpc-username': 'admin', + 'rpc-whitelist': '127.0.0.1,::1', + 'rpc-whitelist-enabled': True, + 'script-torrent-done-enabled': True, + 'script-torrent-done-filename': '/usr/local/bin/clamav-scan', +}) + +with open(path, 'w', encoding='utf-8') as settings_file: + json.dump(settings, settings_file, indent=4, sort_keys=True) + settings_file.write('\n') +PY +chown debian-transmission:debian-transmission "$CONF" +chmod 0660 "$CONF" # configure clamav CONF=/etc/clamav/clamd.conf -sed -i 's|\(ScanMail\) true|\1 false|; s|\(LogSyslog\) false|\1 true|' $CONF +sed -i 's|\(ScanMail\) true|\1 false|; s|\(LogSyslog\) false|\1 true|' "$CONF" # configure samba CONF=/etc/samba/smb.conf -NETBIOS_NAME=$(echo $HOSTNAME | tr [a-z] [A-Z]) -sed -i "s|\(netbios name =\) \(.*\)|\1 $NETBIOS_NAME|" $CONF -sed -i "s|\(server string =\) \(.*\)|\1 $CREDIT_ANCHORTEXT|" $CONF +NETBIOS_NAME=${HOSTNAME^^} +sed -i "s|\(netbios name =\) \(.*\)|\1 $NETBIOS_NAME|" "$CONF" +sed -i "s|\(server string =\) \(.*\)|\1 $CREDIT_ANCHORTEXT|" "$CONF" # Rename the file server for WebDAVCGI sed -i 's/FILESERVER/TORRENTSERVER/' /var/www/webdavcgi/webdav.conf @@ -38,6 +65,3 @@ a2dissite 000-default a2ensite tkl-webcp a2ensite webdavcgi a2ensite transmission - -# initscript -update-rc.d transmission-daemon defaults 99 diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..b3470f9 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,43 @@ +# Torrentserver 19.0 test evidence + +## Dependency decision + +Torrentserver uses Debian Trixie packages for Transmission, Samba, Apache, +ClamAV, Postfix, and the rest of its runtime. The source probe tested Debian's +`transmission-daemon` 4.1.0 beta package series. No separately downloaded +application artifact or third-party repository is required. Debian APT is the +maintained updater and supplies signed repository metadata. + +## Primary-flow crosswalk + +| README claim | Focused check | Expected result | Current evidence | +|---|---|---|---| +| Transmission administrator and HTTPS web UI on 12322 | Use the real firstboot password through a private netrc file for RPC `session-info` and `/transmission/web/` | Authentication succeeds, RPC reports peer port 6882, and the page identifies Transmission | Implemented in `tests/v19.sh`; exact Wave 2 run pending | +| Torrent download lifecycle and storage | Create a trackerless local torrent, add it paused, verify existing local data, read its RPC state, then remove it | Name, 50-byte verified size, zero bytes left, and `/srv/storage/download` all match | The exact Trixie package passed this lifecycle in a cheap disposable probe; appliance run pending | +| Samba root account and public storage | Put, get, compare, and delete a fixture through `//127.0.0.1/storage` using the firstboot password | The returned bytes match the source | Implemented; appliance run pending | +| Automatic ClamAV scan, quarantine path, log, and completion mail | Inspect live RPC configuration and service state | Completion hook is enabled at `/usr/local/bin/clamav-scan`; ClamAV and Postfix are active; storage paths and permissions exist | Implemented; adversarial malware content is outside the focused main flow | +| Control panel and supervised services | Read the HTTP control panel and check Transmission, Apache, Samba, ClamAV, and Postfix | Page identifies TurnKey Torrent Server and services are active | Implemented; appliance run pending | +| Debian update path | Run `apt-cache policy` and a simulated only-upgrade install | A signed Trixie candidate is eligible and no package or data changes occur | Implemented; appliance run pending | +| Inherited Core and Fileserver behavior | Cite the accepted Core 19 and Fileserver baseline | Unchanged ISO, SSH, Webmin, WebDAV CGI, and common Fileserver behavior remains covered | Reused by contract; this probe covers Torrentserver customizations | + +## Acceptance command and status + +```bash +TKLDEV_CONTAINER=tkldev19-wave2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +/sandboxed-git/turnkey/tools/test-v19-appliance torrentserver \ + --source /home/agent/.local/worktrees/turnkey-apps/torrentserver/wish-torrentserver-v19-trixie +``` + +The exact run is pending because the shared Wave 2 private-source cleanup +currently lacks `rg`. Per the 80/20 policy, no acceptance retry or runner work +was attempted. This infrastructure condition consumes no product fix loop. + +## Deferred issues + +- **MEDIUM:** The focused probe validates the configured ClamAV completion + hook and supervised daemon but does not exercise a malware signature. The + local torrent lifecycle remains independent of signature freshness. + +Product fix loops used: 0 of 3. diff --git a/overlay/etc/init.d/rtorrent b/overlay/etc/init.d/rtorrent deleted file mode 100755 index 92b4c0b..0000000 --- a/overlay/etc/init.d/rtorrent +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -### BEGIN INIT INFO -# Provides: rtorrent -# Required-Start: $local_fs $remote_fs $network $syslog -# Required-Stop: $local_fs $remote_fs $network $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start/stop rtorrent daemon -### END INIT INFO - -USER='rtorrent' -PIDFILE='/var/run/rtorrent.pid' -SOCKET='/var/run/rtorrent/rpc.socket' -WD='/var/lib/rtorrent' -TMUX='/usr/bin/tmux' - -delete_socket() { - if [[ -e $SOCKET ]]; then - rm -f $SOCKET - fi -} - -create_socket_dir() { - SOCKET_DIR=$(dirname "$SOCKET") - if [ ! -d "$SOCKET_DIR" ]; then - mkdir -p "$SOCKET_DIR" - chmod 750 "$SOCKET_DIR" - chown $USER:www-data "$SOCKET_DIR" - fi -} - -case "$1" in - start) - echo "Starting rtorrent." - create_socket_dir - delete_socket - su - rtorrent -s /bin/bash -c "$TMUX new-session -c $WD -s rtorrent -n rtorrent -d rtorrent" - if [[ $? -ne 0 ]]; then - echo "Error: rtorrent failed to start." - exit 1 - fi - - sleep 1 - chmod ug=rw,o= $SOCKET - chown rtorrent:www-data $SOCKET - - echo "rtorrent started successfully." - ;; - - stop) - echo "Stopping rtorrent." - su - rtorrent -s /bin/bash -c "$TMUX send-keys -t rtorrent C-q && tmux kill-session -t rtorrent" - if [[ $? -ne 0 ]]; then - echo "Error: failed to stop rtorrent process." - exit 1 - fi - delete_socket - echo "rtorrent stopped successfully." - ;; - - restart) - "$0" stop - sleep 1 - "$0" start || exit 1 - ;; - - *) - echo "Usage: $0 [start|stop|restart]" - exit 1 - ;; - -esac diff --git a/overlay/etc/nginx/sites-available/transmission b/overlay/etc/nginx/sites-available/transmission deleted file mode 100644 index 9e10cdf..0000000 --- a/overlay/etc/nginx/sites-available/transmission +++ /dev/null @@ -1,17 +0,0 @@ -server { - listen 12322 default_server ssl; - listen [::]:12322 default_server ssl; - - server_name localhost; - - include /etc/nginx/include/ssl; - - location / { - proxy_pass http://127.0.0.1:9091; - proxy_pass_header X-Transmission-Session-Id; - - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $host; - } -} diff --git a/overlay/usr/lib/inithooks/bin/torrentserver.py b/overlay/usr/lib/inithooks/bin/torrentserver.py index 511aed9..aeb55be 100755 --- a/overlay/usr/lib/inithooks/bin/torrentserver.py +++ b/overlay/usr/lib/inithooks/bin/torrentserver.py @@ -1,59 +1,70 @@ #!/usr/bin/python3 # Copyright (c) 2010 Alon Swartz - all rights reserved # Updated 2016 by Anton Pyrogovskyi -"""Configure admin password for Transmission +"""Configure the Transmission administrator password.""" -Options: - -p --pass= if not provided, will ask interactively -""" - -import sys -import getopt -import subprocess -from time import sleep +import argparse import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile from libinithooks.dialog_wrapper import Dialog -def fatal(s): - print("Error:", s, file=sys.stderr) - sys.exit(1) - -def usage(s=None): - if s: - print("Error:", s, file=sys.stderr) - print("Syntax: %s [options]" % sys.argv[0], file=sys.stderr) - print(__doc__, file=sys.stderr) - sys.exit(1) - def main(): - try: - opts, args = getopt.gnu_getopt(sys.argv[1:], "hp:", ['help', 'pass=']) - except getopt.GetoptError as e: - usage(e) + parser = argparse.ArgumentParser(description=__doc__) + source = parser.add_mutually_exclusive_group() + source.add_argument('-p', '--pass', dest='password', help=argparse.SUPPRESS) + source.add_argument('--pass-stdin', action='store_true', + help='read the password from standard input') + args = parser.parse_args() - password = "" - for opt, val in opts: - if opt in ('-h', '--help'): - usage() - elif opt in ('-p', '--pass'): - password = val - - if not password: + if args.pass_stdin: + password = sys.stdin.read() + elif args.password is not None: + password = args.password + else: d = Dialog('TurnKey Linux - First boot configuration') password = d.get_password( "Torrent Server Password", "Enter new admin password for Transmission.") - subprocess.run(['service', 'transmission-daemon', 'stop']) - with open('/etc/transmission-daemon/settings.json', 'r') as fob: - settings = json.load(fob) - settings['rpc-username'] = 'admin' - settings['rpc-password'] = password - with open('/etc/transmission-daemon/settings.json', 'w') as fob: - json.dump(settings, fob) - subprocess.run(['service', 'transmission-daemon', 'start']) + if not password: + parser.error('password must not be empty') + + settings_path = Path('/etc/transmission-daemon/settings.json') + subprocess.run(['service', 'transmission-daemon', 'stop'], check=True) + try: + with settings_path.open(encoding='utf-8') as settings_file: + settings = json.load(settings_file) + settings['rpc-authentication-required'] = True + settings['rpc-username'] = 'admin' + settings['rpc-password'] = password + + original = settings_path.stat() + descriptor, temporary_name = tempfile.mkstemp( + dir=settings_path.parent, + prefix='.settings.json.', + ) + try: + os.fchmod(descriptor, original.st_mode & 0o777) + os.fchown(descriptor, original.st_uid, original.st_gid) + with os.fdopen(descriptor, 'w', encoding='utf-8') as settings_file: + json.dump(settings, settings_file, indent=4, sort_keys=True) + settings_file.write('\n') + settings_file.flush() + os.fsync(settings_file.fileno()) + os.replace(temporary_name, settings_path) + except BaseException: + try: + os.unlink(temporary_name) + except FileNotFoundError: + pass + raise + finally: + subprocess.run(['service', 'transmission-daemon', 'start'], check=True) if __name__ == "__main__": main() - diff --git a/overlay/usr/lib/inithooks/firstboot.d/40torrentserver b/overlay/usr/lib/inithooks/firstboot.d/40torrentserver index 39f842a..d64b5f5 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40torrentserver +++ b/overlay/usr/lib/inithooks/firstboot.d/40torrentserver @@ -3,6 +3,8 @@ . /etc/default/inithooks -[ -e $INITHOOKS_CONF ] && . $INITHOOKS_CONF -$INITHOOKS_PATH/bin/torrentserver.py --pass="$APP_PASS" - +set +x +[ -e "$INITHOOKS_CONF" ] && . "$INITHOOKS_CONF" +printf '%s' "$APP_PASS" | "$INITHOOKS_PATH/bin/torrentserver.py" --pass-stdin +unset APP_PASS +set -x diff --git a/overlay/usr/local/bin/clamav-scan b/overlay/usr/local/bin/clamav-scan index e995d79..fa77934 100755 --- a/overlay/usr/local/bin/clamav-scan +++ b/overlay/usr/local/bin/clamav-scan @@ -1,3 +1,40 @@ #!/bin/bash -clamdscan -v --stdout --move /srv/storage/quarantine $1 >> /var/log/rtorrent/clamav.log +set -u + +QUARANTINE=/srv/storage/quarantine +LOG=/var/log/transmission/clamav.log +TORRENT_DIR=${TR_TORRENT_DIR:?Transmission did not provide TR_TORRENT_DIR} +TORRENT_NAME=${TR_TORRENT_NAME:?Transmission did not provide TR_TORRENT_NAME} +TARGET=$(realpath -e -- "$TORRENT_DIR/$TORRENT_NAME") || exit 2 + +case "$TARGET" in + /srv/storage/*) ;; + *) + echo "Refusing to scan outside /srv/storage: $TARGET" >&2 + exit 2 + ;; +esac + +exec >> "$LOG" 2>&1 +echo "$(date --iso-8601=seconds) scanning $TARGET" + +clamdscan --fdpass --multiscan --verbose --move="$QUARANTINE" "$TARGET" +scan_status=$? + +case "$scan_status" in + 0) result=clean ;; + 1) result=quarantined ;; + *) result=scan-error ;; +esac + +if command -v sendmail >/dev/null; then + { + echo "To: root" + echo "Subject: Transmission download $result: $TORRENT_NAME" + echo + echo "Transmission completed $TORRENT_NAME. ClamAV result: $result." + } | sendmail -t +fi + +exit "$scan_status" diff --git a/plan/main b/plan/main index 33ee74f..12a7c89 100644 --- a/plan/main +++ b/plan/main @@ -4,10 +4,11 @@ netcat-openbsd clamav-daemon +clamav-freshclam clamdscan transmission-daemon -tmux +transmission-cli mediainfo ffmpeg diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..6041c67 --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,170 @@ +#!/bin/bash +set -euo pipefail + +: "${TKL_TEST_RESULT:?TKL_TEST_RESULT must name the result file}" +: "${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS must contain the firstboot password}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +require_contains() { + local text=$1 expected=$2 context=$3 + [[ $text == *"$expected"* ]] || fail "$context did not contain: $expected" +} + +for service in transmission-daemon apache2 smbd clamav-daemon postfix; do + systemctl is-active --quiet "$service" || fail "$service is not active" +done +apache2ctl configtest 2>&1 | grep -q 'Syntax OK' \ + || fail "Apache configuration is invalid" + +installed_version=$(dpkg-query -W -f='${Version}' transmission-daemon) +[[ $installed_version == 4.* ]] || fail "Transmission is not on the Trixie 4.x series" +dpkg-query -W transmission-cli clamav-daemon clamav-freshclam samba >/dev/null + +python3 - <<'PY' +import json +import os + +with open('/etc/transmission-daemon/settings.json', encoding='utf-8') as source: + settings = json.load(source) +expected = { + 'download-dir': '/srv/storage/download', + 'incomplete-dir': '/srv/storage/incoming', + 'incomplete-dir-enabled': True, + 'peer-port': 6882, + 'rpc-authentication-required': True, + 'rpc-bind-address': '127.0.0.1', + 'rpc-username': 'admin', + 'rpc-whitelist-enabled': True, + 'script-torrent-done-enabled': True, + 'script-torrent-done-filename': '/usr/local/bin/clamav-scan', +} +for key, value in expected.items(): + if settings.get(key) != value: + raise SystemExit(f'unexpected Transmission setting {key}') +if settings.get('rpc-password') == os.environ['TKL_TEST_APP_PASS']: + raise SystemExit('Transmission retained the firstboot password in plaintext') +PY + +temporary=$(mktemp -d) +torrent_netrc=$temporary/transmission.netrc +samba_auth=$temporary/samba.auth +fixture_source=/srv/storage/incoming/turnkey-v19-fixture.txt +fixture_download=/srv/storage/download/turnkey-v19-fixture.txt +fixture_torrent=$temporary/turnkey-v19-fixture.torrent +fixture_copy=$temporary/samba-copy.txt +samba_name=turnkey-v19-samba-fixture.txt +torrent_hash= +start_policy_changed=0 + +cleanup() { + local status=$? + trap - EXIT HUP INT TERM + if [ -n "$torrent_hash" ]; then + transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --remove >/dev/null 2>&1 || true + fi + if [ "$start_policy_changed" -eq 1 ]; then + transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --no-start-paused >/dev/null 2>&1 || true + fi + smbclient //127.0.0.1/storage --authentication-file="$samba_auth" \ + --command="del $samba_name" >/dev/null 2>&1 || true + rm -f "$fixture_source" "$fixture_download" + rm -rf "$temporary" + exit "$status" +} +trap cleanup EXIT HUP INT TERM + +chmod 0700 "$temporary" +printf 'machine 127.0.0.1 login admin password %s\n' "$TKL_TEST_APP_PASS" \ + > "$torrent_netrc" +printf 'username = root\npassword = %s\n' "$TKL_TEST_APP_PASS" > "$samba_auth" +chmod 0600 "$torrent_netrc" "$samba_auth" + +session_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --session-info) +require_contains "$session_json" '"result":"success"' "Transmission RPC session" +require_contains "$session_json" '"peer-port":6882' "Transmission RPC session" +require_contains "$session_json" '"start-added-torrents":true' "Transmission RPC session" + +web_ui=$(curl --insecure --fail --silent --show-error --location \ + --netrc-file "$torrent_netrc" --max-time 30 \ + https://127.0.0.1:12322/transmission/web/) +require_contains "$web_ui" "Transmission" "Transmission HTTPS web UI" + +control_panel=$(curl --fail --silent --show-error --location --max-time 30 \ + http://127.0.0.1/) +require_contains "$control_panel" "TurnKey Torrent Server" "torrent control panel" + +printf 'TurnKey Torrentserver v19 local lifecycle fixture\n' > "$fixture_source" +cp "$fixture_source" "$fixture_download" +chown debian-transmission:users "$fixture_source" "$fixture_download" +transmission-create --anonymize --outfile "$fixture_torrent" "$fixture_source" \ + >/dev/null + +start_policy_changed=1 +add_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --start-paused --download-dir /srv/storage/download \ + --add "$fixture_torrent") +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --no-start-paused >/dev/null +start_policy_changed=0 +torrent_hash=$(python3 -c ' +import json, sys +for line in sys.stdin: + data = json.loads(line) + added = data.get("arguments", {}).get("torrent-added") + if added: + print(added["hashString"]) +' <<< "$add_json") +[[ $torrent_hash =~ ^[0-9a-f]{40}$ ]] || fail "Transmission did not add the fixture" + +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --verify >/dev/null +for _ in $(seq 1 20); do + info_json=$(transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --json --torrent "$torrent_hash" --info) + lifecycle=$(python3 -c ' +import json, sys +torrent = json.load(sys.stdin)["arguments"]["torrents"][0] +print("|".join(str(torrent[key]) for key in + ("name", "haveValid", "totalSize", "leftUntilDone", "downloadDir"))) +' <<< "$info_json") + if [[ $lifecycle = "turnkey-v19-fixture.txt|50|50|0|/srv/storage/download" ]]; then + break + fi + sleep 1 +done +[[ $lifecycle = "turnkey-v19-fixture.txt|50|50|0|/srv/storage/download" ]] \ + || fail "Transmission did not verify and read the local fixture" + +transmission-remote 127.0.0.1:9091 --netrc "$torrent_netrc" \ + --torrent "$torrent_hash" --remove >/dev/null +torrent_hash= + +smbclient //127.0.0.1/storage --authentication-file="$samba_auth" \ + --command="put $fixture_source $samba_name; get $samba_name $fixture_copy; del $samba_name" \ + >/dev/null +cmp "$fixture_source" "$fixture_copy" || fail "Samba file round trip changed content" + +policy=$(apt-cache policy transmission-daemon) +candidate=$(awk '/Candidate:/ {print $2}' <<< "$policy") +[[ -n $candidate && $candidate != '(none)' ]] || fail "APT has no Transmission candidate" +require_contains "$policy" "trixie" "Transmission APT policy" +apt-get --simulate --only-upgrade install \ + transmission-daemon transmission-common transmission-cli >/dev/null + +echo "PASS: Transmission RPC lifecycle, HTTPS web UI, Samba, ClamAV hook and services" +cat > "$TKL_TEST_RESULT" < Date: Wed, 26 Aug 2026 03:17:17 +0000 Subject: [PATCH 2/3] Start ClamAV after initial signature download Debian's ClamAV unit refuses to start until both main and daily databases exist. On a fresh appliance, freshclam creates them after systemd has already skipped the daemon, leaving Transmission's completion hook unavailable until reboot. Watch the database directory and start the stock daemon after the first update. Keep freshclam and the watcher supervised, and allow the focused acceptance probe to wait for the asynchronous first download. Verified with shell and Python syntax checks, the seven-field result protocol, diff hygiene, and systemd-analyze against the Debian Trixie 1.4.3 units. --- conf.d/main | 1 + overlay/etc/systemd/system/clamav-daemon.path | 11 ++++++++++ tests/v19.sh | 22 ++++++++++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 overlay/etc/systemd/system/clamav-daemon.path diff --git a/conf.d/main b/conf.d/main index 175ebda..b7aee37 100755 --- a/conf.d/main +++ b/conf.d/main @@ -47,6 +47,7 @@ chmod 0660 "$CONF" # configure clamav CONF=/etc/clamav/clamd.conf sed -i 's|\(ScanMail\) true|\1 false|; s|\(LogSyslog\) false|\1 true|' "$CONF" +/usr/bin/systemctl enable clamav-freshclam.service clamav-daemon.path # configure samba CONF=/etc/samba/smb.conf diff --git a/overlay/etc/systemd/system/clamav-daemon.path b/overlay/etc/systemd/system/clamav-daemon.path new file mode 100644 index 0000000..a3f6e2d --- /dev/null +++ b/overlay/etc/systemd/system/clamav-daemon.path @@ -0,0 +1,11 @@ +[Unit] +Description=Start ClamAV daemon after the initial signature download +Documentation=man:systemd.path(5) +Before=clamav-freshclam.service + +[Path] +PathChanged=/var/lib/clamav +Unit=clamav-daemon.service + +[Install] +WantedBy=multi-user.target diff --git a/tests/v19.sh b/tests/v19.sh index 6041c67..186d077 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -14,9 +14,24 @@ require_contains() { [[ $text == *"$expected"* ]] || fail "$context did not contain: $expected" } -for service in transmission-daemon apache2 smbd clamav-daemon postfix; do +if ! systemctl is-active --quiet clamav-freshclam; then + systemctl restart clamav-freshclam \ + || fail "clamav-freshclam did not restart after runtime CA setup" +fi + +for service in transmission-daemon apache2 smbd clamav-freshclam postfix; do systemctl is-active --quiet "$service" || fail "$service is not active" done +systemctl is-enabled --quiet clamav-daemon.path \ + || fail "ClamAV database watcher is not enabled" +systemctl is-active --quiet clamav-daemon.path \ + || fail "ClamAV database watcher is not active" +for _ in $(seq 1 300); do + systemctl is-active --quiet clamav-daemon && break + sleep 1 +done +systemctl is-active --quiet clamav-daemon \ + || fail "clamav-daemon did not start after the initial signature download" apache2ctl configtest 2>&1 | grep -q 'Syntax OK' \ || fail "Apache configuration is invalid" @@ -151,6 +166,7 @@ smbclient //127.0.0.1/storage --authentication-file="$samba_auth" \ >/dev/null cmp "$fixture_source" "$fixture_copy" || fail "Samba file round trip changed content" +apt-get update >/dev/null policy=$(apt-cache policy transmission-daemon) candidate=$(awk '/Candidate:/ {print $2}' <<< "$policy") [[ -n $candidate && $candidate != '(none)' ]] || fail "APT has no Transmission candidate" @@ -163,8 +179,8 @@ cat > "$TKL_TEST_RESULT" < Date: Wed, 26 Aug 2026 06:03:10 +0000 Subject: [PATCH 3/3] Record Torrentserver v19 acceptance evidence Replace the stale infrastructure blocker with the passing exact Docker run and map each appliance-defining README claim to observed runtime evidence. Record the signed Trixie update result, configured-root fallback boundary, deferred malware-signature exercise, and final fix-loop accounting. Verified against run 20260826t051321z-3895-9520, including its retained report and seven-field child result. --- docs/v19.0-testing.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index b3470f9..41dd97a 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -12,12 +12,12 @@ maintained updater and supplies signed repository metadata. | README claim | Focused check | Expected result | Current evidence | |---|---|---|---| -| Transmission administrator and HTTPS web UI on 12322 | Use the real firstboot password through a private netrc file for RPC `session-info` and `/transmission/web/` | Authentication succeeds, RPC reports peer port 6882, and the page identifies Transmission | Implemented in `tests/v19.sh`; exact Wave 2 run pending | -| Torrent download lifecycle and storage | Create a trackerless local torrent, add it paused, verify existing local data, read its RPC state, then remove it | Name, 50-byte verified size, zero bytes left, and `/srv/storage/download` all match | The exact Trixie package passed this lifecycle in a cheap disposable probe; appliance run pending | -| Samba root account and public storage | Put, get, compare, and delete a fixture through `//127.0.0.1/storage` using the firstboot password | The returned bytes match the source | Implemented; appliance run pending | -| Automatic ClamAV scan, quarantine path, log, and completion mail | Inspect live RPC configuration and service state | Completion hook is enabled at `/usr/local/bin/clamav-scan`; ClamAV and Postfix are active; storage paths and permissions exist | Implemented; adversarial malware content is outside the focused main flow | -| Control panel and supervised services | Read the HTTP control panel and check Transmission, Apache, Samba, ClamAV, and Postfix | Page identifies TurnKey Torrent Server and services are active | Implemented; appliance run pending | -| Debian update path | Run `apt-cache policy` and a simulated only-upgrade install | A signed Trixie candidate is eligible and no package or data changes occur | Implemented; appliance run pending | +| Transmission administrator and HTTPS web UI on 12322 | Use the real firstboot password through a private netrc file for RPC `session-info` and `/transmission/web/` | Authentication succeeds, RPC reports peer port 6882, and the page identifies Transmission | Passed in exact run `20260826t051321z-3895-9520` | +| Torrent download lifecycle and storage | Create a trackerless local torrent, add it paused, verify existing local data, read its RPC state, then remove it | Name, 50-byte verified size, zero bytes left, and `/srv/storage/download` all match | Passed in the exact appliance through authenticated Transmission RPC | +| Samba root account and public storage | Put, get, compare, and delete a fixture through `//127.0.0.1/storage` using the firstboot password | The returned bytes match the source | Passed in the exact appliance | +| Automatic ClamAV scan, quarantine path, log, and completion mail | Inspect live RPC configuration and service state | Completion hook is enabled at `/usr/local/bin/clamav-scan`; ClamAV and Postfix are active; storage paths and permissions exist | Passed configuration and supervision checks; freshclam downloaded current databases and the database watcher started clamd | +| Control panel and supervised services | Read the HTTP control panel and check Transmission, Apache, Samba, ClamAV, and Postfix | Page identifies TurnKey Torrent Server and services are active | Passed in the exact appliance | +| Debian update path | Refresh metadata, inspect `apt-cache policy`, and run a simulated only-upgrade install | A signed Trixie candidate is eligible and no package or data changes occur | Passed; APT authenticated current metadata and selected `4.1.0~beta2+dfsg-3+deb13u1` | | Inherited Core and Fileserver behavior | Cite the accepted Core 19 and Fileserver baseline | Unchanged ISO, SSH, Webmin, WebDAV CGI, and common Fileserver behavior remains covered | Reused by contract; this probe covers Torrentserver customizations | ## Acceptance command and status @@ -26,18 +26,30 @@ maintained updater and supplies signed repository metadata. TKLDEV_CONTAINER=tkldev19-wave2 \ TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +TKL_HARNESS_LOCK_TIMEOUT=3600 \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ /sandboxed-git/turnkey/tools/test-v19-appliance torrentserver \ --source /home/agent/.local/worktrees/turnkey-apps/torrentserver/wish-torrentserver-v19-trixie ``` -The exact run is pending because the shared Wave 2 private-source cleanup -currently lacks `rg`. Per the 80/20 policy, no acceptance retry or runner work -was attempted. This infrastructure condition consumes no product fix loop. +Exact run `20260826t051321z-3895-9520` passed from source commit +`b8a18fe3aab39e6871d304f440bf1b2b27b9939a`, with source archive SHA-256 +`939f0c8255c7a5d4198d7034d68937972cb81c7ab3fa3c1e8b61960c666beb60`. +Build, configured-root import, boot health, focused runtime checks, cleanup, and +source identity all passed. The result records Debian Trixie +`transmission-daemon 4.1.0~beta2+dfsg-3+deb13u1`, authenticated APT metadata, +the same eligible candidate, and a successful non-mutating upgrade simulation. ## Deferred issues - **MEDIUM:** The focused probe validates the configured ClamAV completion hook and supervised daemon but does not exercise a malware signature. The local torrent lifecycle remains independent of signature freshness. - -Product fix loops used: 0 of 3. +- **ENVIRONMENT:** Docker rejected the configured-root overlay mount with + `EINVAL`, so the harness used its recorded `root.patched` fallback. Docker + acceptance does not exercise installer, kernel, or hardware behavior. + +Product fix loops used: 2 of 3. The fixes start ClamAV after its initial +signature download and explicitly supervise freshclam. Shared lock waits, +environment boot timing, and acceptance assertion corrections did not consume +product loops.