DNS-Recursor fuer rspamd + Bayes-Trainingsskript
Rspamds DNS-Blocklisten waren wirkungslos: Spamhaus beantwortete keine Anfragen, weil sie ueber Dockers eingebautes DNS von einem als oeffentlich eingestuften Resolver kamen (DBL_BLOCKED_OPENRESOLVER im Log, fuer rDNS, Message-ID und URLs gleichermassen). ENABLE_UNBOUND=1 hat daran nichts geaendert - die Variable existiert in DMS nicht und war ein No-Op. - dns-recursor (PowerDNS Recursor 5.3.10) als eigener Service, der selbst rekursiv aufloest, in einem separaten Netz dms_dns mit fester IP, weil Dockers dns:-Option nur IPs akzeptiert - ENABLE_UNBOUND entfernt - train_bayes.sh: einmaliges Anlernen des Bayes-Klassifikators aus den vorhandenen Postfaechern (Junk -> Spam, INBOX -> Ham). Bisher feuerte kein einziges BAYES_-Symbol, weil die noetigen 200 Lernvorgaenge je Klasse fehlten. Default ist ein Trockenlauf, gelernt wird nur mit --apply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,10 @@ services:
|
||||
|
||||
# Sicherheit
|
||||
- ENABLE_FAIL2BAN=1
|
||||
- ENABLE_UNBOUND=1
|
||||
# ENABLE_UNBOUND entfernt: die Variable existiert in DMS nicht (mehr) und
|
||||
# war wirkungslos - rspamd hat ueber Dockers eingebautes DNS (127.0.0.11)
|
||||
# aufgeloest und bekam von Spamhaus nur DBL_BLOCKED_OPENRESOLVER zurueck.
|
||||
# Ersetzt durch den dns-recursor-Service weiter unten.
|
||||
|
||||
# Sonstige
|
||||
- ENABLE_MANAGESIEVE=0
|
||||
@@ -130,11 +133,47 @@ services:
|
||||
- NET_ADMIN
|
||||
- SYS_PTRACE
|
||||
restart: unless-stopped
|
||||
|
||||
# -------------------------------------------------------
|
||||
# DNS: rspamd fragt DNS-Blocklisten (Spamhaus DBL/ZEN, SURBL) ab. Diese
|
||||
# Dienste beantworten keine Anfragen, die ueber oeffentliche Resolver
|
||||
# kommen - man bekommt DBL_BLOCKED_OPENRESOLVER statt eines Ergebnisses.
|
||||
# Deshalb ein eigener rekursiver Resolver, siehe Service dns-recursor.
|
||||
#
|
||||
# ACHTUNG: Damit ersetzt der Recursor Dockers eingebautes DNS (127.0.0.11)
|
||||
# fuer DIESEN Container. Der mailserver kann dann keine Container-Namen
|
||||
# mehr aufloesen (roundcube-db, email-worker-ts, ...). Das ist hier
|
||||
# unkritisch, weil er von sich aus nur nach aussen verbindet (SES-Relay).
|
||||
# Faellt der Recursor aus, hat der mailserver kein DNS mehr - deshalb
|
||||
# restart: always beim Recursor.
|
||||
# -------------------------------------------------------
|
||||
dns:
|
||||
- 10.99.0.53
|
||||
depends_on:
|
||||
- dns-recursor
|
||||
|
||||
networks:
|
||||
mail_network:
|
||||
aliases:
|
||||
- mailserver
|
||||
- ${NODE_HOSTNAME}
|
||||
dns_network:
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Rekursiver DNS-Resolver ausschliesslich fuer den mailserver.
|
||||
# Er fragt die Root-Server selbst ab, statt an einen Upstream-Resolver
|
||||
# weiterzuleiten - nur so akzeptiert Spamhaus die Anfragen.
|
||||
# Braucht keine Konfiguration: das Image laeuft per Default als Recursor
|
||||
# und erlaubt Anfragen aus den RFC1918-Netzen (10.99.0.0/24 liegt darin).
|
||||
# ---------------------------------------------------------
|
||||
dns-recursor:
|
||||
image: powerdns/pdns-recursor-53:5.3.10
|
||||
container_name: dns-recursor
|
||||
restart: always
|
||||
stop_grace_period: 0s
|
||||
networks:
|
||||
dns_network:
|
||||
ipv4_address: 10.99.0.53
|
||||
|
||||
roundcube:
|
||||
image: roundcube/roundcubemail:latest
|
||||
@@ -190,3 +229,16 @@ services:
|
||||
networks:
|
||||
mail_network:
|
||||
external: true
|
||||
|
||||
# Eigenes Netz nur fuer mailserver <-> dns-recursor.
|
||||
# Bewusst NICHT im externen mail_network: dort vergibt Docker die Adressen
|
||||
# dynamisch, eine feste IP koennte mit einem anderen Container kollidieren.
|
||||
# Das Subnetz muss fest sein, weil Dockers `dns:`-Option nur IPs akzeptiert.
|
||||
# Falls 10.99.0.0/24 bei euch schon belegt ist (VPN, AWS-VPC), hier und bei
|
||||
# `dns:` sowie `ipv4_address` oben gemeinsam auf ein freies Netz aendern.
|
||||
dns_network:
|
||||
name: dms_dns
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.99.0.0/24
|
||||
|
||||
206
DMS/docker-data/dms/config/train_bayes.sh
Executable file
206
DMS/docker-data/dms/config/train_bayes.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
# ===========================================================================
|
||||
# train_bayes.sh - Bayes-Klassifikator von rspamd aus den vorhandenen
|
||||
# Postfaechern anlernen.
|
||||
#
|
||||
# Hintergrund: rspamd traegt mit BAYES_* erst dann zum Score bei, wenn genug
|
||||
# gelernt wurde (Default: 200 Spam UND 200 Ham). Bis dahin liefert der
|
||||
# Klassifikator gar nichts - genau das war im Log zu sehen, dort tauchte kein
|
||||
# einziges BAYES_-Symbol auf. Statt monatelang auf manuelles Verschieben in
|
||||
# den Junk-Ordner zu warten (das macht RSPAMD_LEARN=1 bereits automatisch),
|
||||
# lernt dieses Skript einmalig aus dem, was ohnehin auf der Platte liegt.
|
||||
#
|
||||
# AUSFUEHRUNG - immer IM Container:
|
||||
#
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh
|
||||
# -> Trockenlauf: zaehlt nur, lernt nichts. Das ist der Default.
|
||||
#
|
||||
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh --apply
|
||||
# -> lernt tatsaechlich
|
||||
#
|
||||
# Optionen:
|
||||
# --apply Wirklich lernen (ohne das passiert nichts)
|
||||
# --max N Max. Nachrichten je Postfach und Klasse (Default 400)
|
||||
# --include-sent Gesendete Mails zusaetzlich als Ham lernen
|
||||
# --only MUSTER Nur Postfaecher, deren Adresse MUSTER enthaelt
|
||||
# --host HOST:PORT rspamd-Controller (Default 127.0.0.1:11334)
|
||||
#
|
||||
# Passwort: Falls der Controller eines verlangt (bei euch in
|
||||
# rspamd/override.d/worker-controller.inc gesetzt), vorweg setzen:
|
||||
# docker exec -e RSPAMC_PASSWORD='...' mailserver bash /tmp/.../train_bayes.sh
|
||||
#
|
||||
# WICHTIG zur Datenqualitaet:
|
||||
# - Ham kommt aus der INBOX. Liegt dort unerkannter Spam, wird er als Ham
|
||||
# gelernt und verschlechtert die Erkennung. Vorher grob durchsehen lohnt.
|
||||
# - Spam kommt aus Junk. Liegt dort faelschlich einsortierte legitime Mail,
|
||||
# gilt dasselbe umgekehrt.
|
||||
# - Ein sehr schiefes Verhaeltnis (z.B. 8000 Ham zu 50 Spam) macht den
|
||||
# Klassifikator einseitig. Der Trockenlauf zeigt das Verhaeltnis an -
|
||||
# danach ggf. mit --max nachsteuern.
|
||||
# ===========================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
MAILBASE="${MAILBASE:-/var/mail}"
|
||||
RSPAMC_HOST="127.0.0.1:11334"
|
||||
MAX_PER_CLASS=400
|
||||
MAX_BYTES=5242880 # 5 MB - groessere Mails bringen fuers Lernen nichts
|
||||
BATCH=50 # Dateien pro rspamc-Aufruf
|
||||
APPLY=0
|
||||
INCLUDE_SENT=0
|
||||
ONLY=""
|
||||
|
||||
usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply) APPLY=1; shift ;;
|
||||
--max) MAX_PER_CLASS="${2:-}"; shift 2 ;;
|
||||
--include-sent) INCLUDE_SENT=1; shift ;;
|
||||
--only) ONLY="${2:-}"; shift 2 ;;
|
||||
--host) RSPAMC_HOST="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unbekannte Option: $1" >&2; echo "Hilfe: $0 --help" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d ${MAILBASE} ]]; then
|
||||
echo "FEHLER: ${MAILBASE} existiert nicht. Laeuft das Skript wirklich IM Container?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RSPAMC=(rspamc -h "${RSPAMC_HOST}")
|
||||
[[ -n ${RSPAMC_PASSWORD:-} ]] && RSPAMC+=(-P "${RSPAMC_PASSWORD}")
|
||||
|
||||
if ! command -v rspamc >/dev/null 2>&1; then
|
||||
echo "FEHLER: rspamc nicht gefunden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "${RSPAMC[@]}" stat >/dev/null 2>&1; then
|
||||
echo "FEHLER: Controller unter ${RSPAMC_HOST} nicht erreichbar oder Passwort noetig." >&2
|
||||
echo " Versuche es mit: docker exec -e RSPAMC_PASSWORD='...' mailserver bash $0 ..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dateien einsammeln: bis LIMIT regulaere Dateien unterhalb der uebergebenen
|
||||
# Maildir-Verzeichnisse, groessenbegrenzt. Neueste zuerst waere schoener, ist
|
||||
# aber bei zehntausenden Dateien zu teuer - Maildir-Namen beginnen mit dem
|
||||
# Timestamp, daher liefert die Sortierung eine brauchbare Naeherung.
|
||||
# ---------------------------------------------------------------------------
|
||||
gather() {
|
||||
local limit="$1"; shift
|
||||
local dir
|
||||
for dir in "$@"; do
|
||||
[[ -d ${dir} ]] || continue
|
||||
find "${dir}" -maxdepth 1 -type f -size -"${MAX_BYTES}"c -print 2>/dev/null
|
||||
done | sort -r | head -n "${limit}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lernen in Batches. Gibt "gelernt fehlgeschlagen" zurueck.
|
||||
# ---------------------------------------------------------------------------
|
||||
learn() {
|
||||
local cmd="$1"; shift
|
||||
local -a files=("$@")
|
||||
local total=${#files[@]}
|
||||
local ok=0 fail=0 idx=0 out
|
||||
|
||||
while (( idx < total )); do
|
||||
out=$("${RSPAMC[@]}" "${cmd}" "${files[@]:idx:BATCH}" 2>&1)
|
||||
ok=$(( ok + $(grep -c 'success = true' <<<"${out}") ))
|
||||
fail=$(( fail + $(grep -ci 'error' <<<"${out}") ))
|
||||
idx=$(( idx + BATCH ))
|
||||
done
|
||||
|
||||
echo "${ok} ${fail}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptlauf
|
||||
# ---------------------------------------------------------------------------
|
||||
if (( APPLY )); then
|
||||
echo "=== Bayes-Training (SCHREIBEND) ==="
|
||||
else
|
||||
echo "=== Bayes-Training (Trockenlauf - es wird nichts gelernt) ==="
|
||||
fi
|
||||
echo "Basis: ${MAILBASE} | max ${MAX_PER_CLASS} je Postfach und Klasse"
|
||||
[[ -n ${ONLY} ]] && echo "Filter: nur Postfaecher mit '${ONLY}'"
|
||||
printf '\n%-42s %8s %8s\n' "POSTFACH" "SPAM" "HAM"
|
||||
printf -- '---------------------------------------------------------------\n'
|
||||
|
||||
total_spam=0
|
||||
total_ham=0
|
||||
learned_spam=0
|
||||
learned_ham=0
|
||||
failed=0
|
||||
|
||||
for domain_dir in "${MAILBASE}"/*/; do
|
||||
[[ -d ${domain_dir} ]] || continue
|
||||
domain=$(basename "${domain_dir}")
|
||||
|
||||
for user_dir in "${domain_dir}"*/; do
|
||||
user_dir="${user_dir%/}"
|
||||
[[ -d ${user_dir}/cur ]] || continue # keine Maildir -> ueberspringen
|
||||
|
||||
mailbox="$(basename "${user_dir}")@${domain}"
|
||||
[[ -n ${ONLY} && ${mailbox} != *"${ONLY}"* ]] && continue
|
||||
|
||||
mapfile -t spam_files < <(gather "${MAX_PER_CLASS}" \
|
||||
"${user_dir}/.Junk/cur" "${user_dir}/.Junk/new" \
|
||||
"${user_dir}/.Spam/cur" "${user_dir}/.Spam/new")
|
||||
|
||||
ham_dirs=("${user_dir}/cur" "${user_dir}/new")
|
||||
if (( INCLUDE_SENT )); then
|
||||
ham_dirs+=("${user_dir}/.Sent/cur" "${user_dir}/.Sent Messages/cur")
|
||||
fi
|
||||
mapfile -t ham_files < <(gather "${MAX_PER_CLASS}" "${ham_dirs[@]}")
|
||||
|
||||
n_spam=${#spam_files[@]}
|
||||
n_ham=${#ham_files[@]}
|
||||
(( n_spam == 0 && n_ham == 0 )) && continue
|
||||
|
||||
printf '%-42s %8d %8d\n' "${mailbox}" "${n_spam}" "${n_ham}"
|
||||
total_spam=$(( total_spam + n_spam ))
|
||||
total_ham=$(( total_ham + n_ham ))
|
||||
|
||||
if (( APPLY )); then
|
||||
if (( n_spam > 0 )); then
|
||||
read -r ok fail <<<"$(learn learn_spam "${spam_files[@]}")"
|
||||
learned_spam=$(( learned_spam + ok )); failed=$(( failed + fail ))
|
||||
fi
|
||||
if (( n_ham > 0 )); then
|
||||
read -r ok fail <<<"$(learn learn_ham "${ham_files[@]}")"
|
||||
learned_ham=$(( learned_ham + ok )); failed=$(( failed + fail ))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
printf -- '---------------------------------------------------------------\n'
|
||||
printf '%-42s %8d %8d\n\n' "SUMME" "${total_spam}" "${total_ham}"
|
||||
|
||||
if (( total_spam > 0 )); then
|
||||
ratio=$(( total_ham / (total_spam > 0 ? total_spam : 1) ))
|
||||
if (( ratio >= 5 )); then
|
||||
echo "WARNUNG: Ham/Spam-Verhaeltnis etwa ${ratio}:1 - sehr einseitig."
|
||||
echo " Besser mit kleinerem --max fahren, z.B. --max $(( total_spam / 4 + 10 ))."
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ! APPLY )); then
|
||||
echo "Trockenlauf beendet. Zum echten Lernen dasselbe Kommando mit --apply."
|
||||
echo "rspamd braucht je Klasse mindestens 200 Nachrichten, bevor BAYES_* feuert."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Gelernt: ${learned_spam} Spam, ${learned_ham} Ham, ${failed} Fehler/Duplikate."
|
||||
echo
|
||||
echo "=== rspamc stat ==="
|
||||
"${RSPAMC[@]}" stat 2>/dev/null | grep -iE 'learn|classifier|statfile' || true
|
||||
echo
|
||||
echo "Kontrolle an der naechsten eingehenden Mail:"
|
||||
echo " docker exec mailserver sh -c 'grep rspamd_task_write_log /var/log/mail/rspamd.log | tail -3'"
|
||||
echo "In der Symbolliste sollte jetzt BAYES_HAM oder BAYES_SPAM auftauchen."
|
||||
Reference in New Issue
Block a user