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
|
# Sicherheit
|
||||||
- ENABLE_FAIL2BAN=1
|
- 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
|
# Sonstige
|
||||||
- ENABLE_MANAGESIEVE=0
|
- ENABLE_MANAGESIEVE=0
|
||||||
@@ -130,11 +133,47 @@ services:
|
|||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_PTRACE
|
- SYS_PTRACE
|
||||||
restart: unless-stopped
|
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:
|
networks:
|
||||||
mail_network:
|
mail_network:
|
||||||
aliases:
|
aliases:
|
||||||
- mailserver
|
- mailserver
|
||||||
- ${NODE_HOSTNAME}
|
- ${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:
|
roundcube:
|
||||||
image: roundcube/roundcubemail:latest
|
image: roundcube/roundcubemail:latest
|
||||||
@@ -190,3 +229,16 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
mail_network:
|
mail_network:
|
||||||
external: true
|
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