Archive discontinued selective SRS implementation

This commit is contained in:
2026-10-20 14:41:48 +02:00
parent 3af1c29a16
commit 39b933b798
25 changed files with 488 additions and 0 deletions
+4
View File
@@ -21,3 +21,7 @@ log*
# - roundcube # - roundcube
/log/* /log/*
roundcubemail-* roundcubemail-*
# Python
__pycache__/
*.py[cod]
+51
View File
@@ -0,0 +1,51 @@
# Mailsystem
Dieses Repository enthaelt Installations-, Update- und
Verwaltungsskripte sowie Konfigurationsvorlagen und
Dokumentationen fuer Linux-basierte Mailserver.
Zu den unterstuetzten Komponenten gehoeren unter anderem:
- Postfix
- Dovecot
- AmaViS
- SpamAssassin und ClamAV
- OpenDKIM und OpenDMARC
- PostfixAdmin
- Roundcube
- weitere Dienste und Hilfsprogramme
## Installation und Dokumentation
Die Installations- und Verwaltungsskripte befinden sich
ueberwiegend im Hauptverzeichnis.
Ergaenzende Dateien liegen unter anderem in:
- conf/
- files/
- helpers/
- DOC/
Weitere Informationen enthalten die vorhandenen
README-Dateien, insbesondere README.install.
## Archivierte Entwicklungen
Abgeschlossene oder eingestellte Entwicklungsarbeiten
werden unter archive/ aufbewahrt.
### Selective SRS (2026)
Die im Oktober 2026 entwickelte selektive SRS-Loesung
wurde nach abschliessender Architekturpruefung
eingestellt und von den Mailservern entfernt.
Der vollstaendige historische Entwicklungsstand ist
hier dokumentiert:
[Selective SRS 2026](archive/selective-srs-2026/README.md)
Die archivierten SRS-Skripte sind nicht fuer den
produktiven Einsatz freigegeben und duerfen nicht
unveraendert zur Installation verwendet werden.
+170
View File
@@ -0,0 +1,170 @@
# Selective Sender Rewriting Scheme (SRS)
## Projektstatus
**Archiviert am 20. Oktober 2026.**
Die Entwicklung wurde eingestellt. Diese Implementierung ist nicht
fuer den produktiven Einsatz freigegeben.
Dieses Verzeichnis dient ausschliesslich der Archivierung und
Dokumentation des bisherigen Entwicklungsstands.
Die enthaltenen Installations- und Verwaltungsskripte duerfen
nicht unveraendert fuer Neuinstallationen verwendet werden.
## Urspruengliches Ziel
Ziel war eine selektive SRS-Implementierung fuer Postfix.
SRS sollte ausschliesslich bei externen Weiterleitungen zum
Einsatz kommen, die durch lokale virtuelle Mail-Aliase entstehen.
Normale eingehende und ausgehende E-Mails sollten dagegen
unveraendert verarbeitet werden.
Die Verwaltung virtueller Aliase erfolgt unter anderem ueber
PostfixAdmin und PostgreSQL.
## Technische Implementierung
Entwickelt wurde eine Loesung auf Basis von:
- Postfix
- PostSRSd 2.4.1
- einer zusaetzlichen Postfix-Instanz
- eigenen Python-Programmen zur Alias-Entscheidung,
Weiterleitung und Rueckuebersetzung
- Shell-Skripten fuer Installation, Aktivierung und
Deaktivierung
Die Implementierung enthielt unter anderem:
- selektive SRS-Entscheidungslogik
- SRS0-/SRS1-Umschreibung
- Verarbeitung von SRS-Ruecklaeufern
- persistente Speicherung des vorherigen Postfix-Zustands
- kontrollierte Aktivierung und Deaktivierung
- Sicherungs- und Rollback-Funktionen
- DNS- und SPF-Pruefungen
- Offline-Tests
## Entwicklungs- und Testergebnis
Die Implementierung wurde im Oktober 2026 entwickelt und
auf e.mx.oopen.de getestet.
Dabei wurden unter anderem folgende Funktionen erfolgreich
geprueft:
- Aktivierung und Deaktivierung
- externe Alias-Weiterleitungen mit SRS
- SPF-Pruefung weitergeleiteter Nachrichten
- Rueckuebersetzung von SRS-Ruecklaeufern
- verschiedene Fehler- und Rollback-Szenarien
Die vorhandenen Tests belegen keine vollstaendige
Produktionsreife.
Die Loesung wurde zeitweise auch auf mx.warenform.de
produktiv eingesetzt.
## Grund fuer die Einstellung
Bei der abschliessenden Architekturpruefung wurden
grundlegende betriebliche Risiken festgestellt.
Insbesondere:
1. Die Verwendung von
default_transport = srs-forward
fuehrte dazu, dass auch regulaere Postfix-Zustellungen
von der eigenen Python-SRS-Verarbeitung und der
zusaetzlichen Postfix-Instanz abhaengig waren.
2. Die globale Einbindung der SRS-Reverse-Socketmap
in die Empfaengeradressverarbeitung erzeugte eine
weitere Abhaengigkeit von einem eigenen Dienst.
3. Fehler in der SRS-Entscheidungslogik konnten auch
Nachrichten beeintraechtigen, fuer die ueberhaupt
keine SRS-Umschreibung erforderlich war.
4. Der Betriebs- und Wartungsaufwand der Gesamtkonstruktion
war fuer die urspruenglich begrenzte Aufgabenstellung
zu hoch.
Damit wurde die wichtigste betriebliche Anforderung
nicht erfuellt:
Der regulaere Mailbetrieb muss auch bei einem Ausfall
der optionalen SRS-Verarbeitung unabhaengig weiterlaufen.
Eine weitere Ausweitung der eigenen Fehlerbehandlung
wurde nicht als sinnvoll angesehen.
## Entscheidung
Am 20. Oktober 2026 wurde entschieden, diese
SRS-Implementierung nicht weiterzuentwickeln
und nicht fuer weitere produktive Installationen
zu verwenden.
Stattdessen wurde der bisherige Postfix-Betrieb
ohne SRS wiederhergestellt.
## Rueckbau
Die SRS-Integration wurde am 20. Oktober 2026 auf
folgenden Systemen deaktiviert und entfernt:
- e.mx.oopen.de
- mx.warenform.de
Entfernt wurden insbesondere:
- SRS-spezifische Postfix-Konfigurationseintraege
- die zusaetzliche Postfix-Instanz
- SRS-spezifische systemd-Units
- PostSRSd und die eigenen SRS-Python-Programme
- nicht mehr benoetigte SRS-Socket- und PID-Dateien
Die Konfiguration und das jeweilige SRS-Secret
wurden vor der Entfernung lokal gesichert.
Diese Sicherungen sind nicht Bestandteil des
Git-Repositories.
## Inhalt des Archivs
Die bisherigen Programme, Konfigurationsvorlagen,
Verwaltungsskripte, Tests und Dokumentationen
bleiben hier erhalten.
Die urspruenglichen README-Dateien beschreiben
den damaligen Entwicklungsstand und sind als
historische Dokumentation zu verstehen.
Da die Dateien in dieses Archivverzeichnis
verschoben wurden, stimmen einige relative Pfade
und Annahmen der Skripte nicht mehr mit der
urspruenglichen Repository-Struktur ueberein.
Die Skripte und Tests sind daher nicht ohne
weitere Anpassungen ausfuehrbar.
## Moegliche spaetere Neubewertung
Eine erneute Beschaeftigung mit SRS sollte
nur erfolgen, wenn eine deutlich einfachere
und besser isolierte Architektur verfuegbar ist.
Dabei muss insbesondere gewaehrleistet sein,
dass Fehler oder Ausfaelle der SRS-Verarbeitung
keine Auswirkungen auf den regulaeren
Postfix-Mailbetrieb haben.
Eine unveraenderte Wiederaufnahme der hier
archivierten Implementierung wird nicht empfohlen.
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""Offline, path-relocated installer contract tests. All external commands mocked.
Do not equate these tests with genuine Postfix/PostSRSd integration.
"""
from pathlib import Path
import os, subprocess, tempfile, zipfile, re, sys, shutil
SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
def putexe(file, content):
file.parent.mkdir(parents=True, exist_ok=True)
file.write_text('#!/usr/bin/env bash\n' + content)
file.chmod(0o755)
def setup(root):
root.mkdir(parents=True, exist_ok=True)
repo = root / 'repo'
repo.mkdir()
if SOURCE.is_file():
with zipfile.ZipFile(SOURCE) as z:
z.extractall(repo)
else:
for source_file in SOURCE.rglob('*'):
if source_file.is_file():
dst = repo / source_file.relative_to(SOURCE)
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_file, dst)
for path in (repo/'install-update-srs.sh', repo/'helpers/srs-common.sh',
*list((repo/'files/srs').glob('*.py')),
*list((repo/'files/srs/templates').glob('*'))):
s = path.read_text()
# Keep all managed install/config/queue paths in an unprivileged fixture tree.
# The postconf absolute command must also be relocated.
s = s.replace('/usr/sbin/postconf', str(root/'bin/postconf'))
for prefix in ['/etc/', '/usr/local/', '/var/', '/run/lock/', '/root/srs-installer-backups']:
new = str(root) + prefix
s = s.replace(prefix, new)
# prevent reading the real original root Postfix config in our fixtures
path.write_text(s)
(repo/'install-update-srs.sh').chmod(0o755)
for p in ['etc/postfix','usr/local/sbin','run/lock','var/spool','var/lib','etc/systemd/system','flags']:
(root/p).mkdir(parents=True, exist_ok=True)
(root/'etc/postfix/main.cf').write_text('myhostname = mail.example.org\n')
(root/'etc/postfix/master.cf').write_text('smtp inet n - y - - smtpd\nlocalhost:10025 inet n - y - - smtpd\n -o receive_override_options=no_unknown_recipient_checks\n')
(repo/'conf').mkdir(exist_ok=True)
# All expected lookup paths are extracted from the (relocated) reference source.
alias = (repo/'files/srs/srs-alias-decision.py').read_text()
for typ,name in set(re.findall(r'"(pgsql|btree):([^" ]+)"', alias)):
f = Path(name + ('.db' if typ == 'btree' else ''))
f.parent.mkdir(parents=True,exist_ok=True)
f.touch()
bin = root/'bin'
bin.mkdir()
putexe(bin/'postconf', '''
name="${@: -1}"
# e.mx.oopen.de has two pre-existing unrelated warnings in main.cf.
# They are emitted on every postconf call while the requested value is valid.
echo "postconf: warning: ${SRS_TEST_ROOT}/etc/postfix/main.cf: unused parameter: policyd-spf_time_limit=3600" >&2
# The warning path matches the source main.cf in the relocated fixture.
# Existing warnings must be distinguished from queried-param errors.
if [[ -f "${SRS_TEST_ROOT}/flags/unknown_parameter_warning" ]]; then
echo "postconf: warning: smtp_tls_cafile: unknown parameter" >&2
fi
case "$name" in
myhostname) echo mail.example.org;;
mydomain) echo example.org;;
myorigin) echo /etc/mailname;;
mydestination) echo 'mail.example.org localhost';;
relay_domains) echo '\u0024mydestination';;
virtual_alias_maps) echo 'pgsql:some-alias';;
virtual_alias_domains) echo 'btree:some-domain';;
virtual_mailbox_maps) echo 'pgsql:some-mailbox';;
virtual_mailbox_domains) echo 'pgsql:some-domain';;
smtp_bind_address) echo 192.0.2.22;;
smtp_bind_address6) echo 2001:db8::22;;
smtp_dns_support_level) echo dnssec;;
smtp_host_lookup) echo dns;;
smtp_tls_CAfile) echo /etc/ssl/certs/ca-certificates.crt;;
smtp_tls_mandatory_protocols|smtp_tls_protocols) echo '>=TLSv1.2';;
smtp_tls_security_level) echo dane;;
*) echo "unknown $name" >&2; exit 1;;
esac
''')
putexe(bin/'systemctl', '''
if [[ "$1" == is-active ]]; then exit 3; fi
exit 0
''')
putexe(bin/'git', '''
if [[ "$1" == clone ]]; then
mkdir -p "${@: -1}"; exit 0
fi
exec /usr/bin/git "$@"
''')
putexe(bin/'cmake', '''
if [[ "$1" == --version ]]; then echo 'cmake version 3.31.6'; exit 0; fi
if [[ "$1" == -S ]]; then
for ((i=1; i<=$#; i++)); do
arg="${!i}"
if [[ "$arg" == -B ]]; then n=$((i+1)); build="${!n}"; fi
if [[ "$arg" == -DCMAKE_INSTALL_PREFIX=* ]]; then prefix="${arg#*=}"; fi
done
mkdir -p "$build"; printf '%s\\n' "$prefix" > "$build/prefix"; exit 0
fi
if [[ "$1" == --build ]]; then exit 0; fi
if [[ "$1" == --install ]]; then
prefix="$(cat "$2/prefix")"
mkdir -p "${DESTDIR}${prefix}/sbin"
printf '#!/bin/sh\\nexit 0\\n' > "${DESTDIR}${prefix}/sbin/postsrsd"
chmod +x "${DESTDIR}${prefix}/sbin/postsrsd"
exit 0
fi
exit 9
''')
putexe(bin/'ctest','exit 0\n')
putexe(bin/'dig','exit 0\n')
putexe(bin/'postmap','''
case "$1" in
btree:*) dest="${1#btree:}"; touch "${dest}.db";;
*) exit 1;;
esac
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postmap" ]]; then exit 1; fi
exit 0
''')
putexe(bin/'postfix','''
if [[ "$*" == *check* ]]; then
mkdir -p "${SRS_TEST_ROOT}/var/spool/postfix-srs-delivery/private" "${SRS_TEST_ROOT}/var/lib/postfix-srs-delivery"
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postfix" ]]; then echo 'forced postfix check error' >&2; exit 1; fi
fi
exit 0
''')
(repo/'helpers/srs-dns-check.py').write_text('print(" Simulated authoritative DNS: OK")\n')
return repo
def run(root, repo, *, fail=None, version=None):
if fail: (root/'flags'/('fail_'+fail)).touch()
cmd = f"{repo/'install-update-srs.sh'} --install"
if version: cmd += f' --version {version}'
env = os.environ.copy()
env.update({'PATH':str(root/'bin') + ':' + env['PATH'], 'SRS_TEST_ROOT':str(root), 'LC_ALL':'C'})
# Running inside util-linux script supplies a real PTY for init_srs_runtime.
result = subprocess.run(['script','-q','-e','-c',cmd,'/dev/null'], input='srs.example.org\nyes\nyes\n',
text=True, capture_output=True, timeout=35, env=env)
return result
def assert_ok(condition, msg):
if not condition: raise AssertionError(msg)
with tempfile.TemporaryDirectory(prefix='oopen-srs-test-') as tmp:
base=Path(tmp)
os.chmod(base, 0o2770) # Deliberately inherit SGID on fixture subdirectories.
root=base/'initial'
repo=setup(root)
result=run(root,repo)
print('initial install', result.returncode)
if result.returncode: print(result.stdout[-2500:])
assert_ok(result.returncode == 0,'initial installation failed')
assert_ok((root/'usr/local/postsrsd').is_symlink(), 'missing symlink')
assert_ok((root/'usr/local/sbin/srs-forward.py').exists(), 'missing srs forward')
assert_ok((root/'etc/postsrsd-srs/secret').stat().st_mode & 0o777 == 0o600, 'secret mode')
assert_ok('smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt' in (root/'etc/postfix-srs/delivery/main.cf').read_text(),'rendered TLS CA')
print(" secret dir mode:", oct((root/"etc/postsrsd-srs").stat().st_mode & 0o777), "uid", (root/"etc/postsrsd-srs").stat().st_uid)
print(" stat check:", subprocess.check_output(["stat", "-c", "%u:%g:%a", str(root/"etc/postsrsd-srs")], text=True).strip())
print(' initial install: state, secret, config, symlink, CAfile verified')
original_secret=(root/'etc/postsrsd-srs/secret').read_bytes()
original_forward=(root/'usr/local/sbin/srs-forward.py').read_bytes()
original_link=os.readlink(root/'usr/local/postsrsd')
result=run(root,repo)
print('maintenance update', result.returncode)
if result.returncode: print(result.stdout[-2500:])
assert_ok(result.returncode == 0,'maintenance update failed')
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'secret changed')
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'link changed')
print(' maintenance update: original secret and binary retained')
# Force late failure, after Postfix configuration and all units have been deployed.
source=repo/'files/srs/srs-forward.py'
source.write_text(source.read_text()+'\n# injected test change\n')
result=run(root,repo,fail='postfix')
print('failed maintenance rollback', result.returncode)
if result.returncode==0: print(result.stdout[-2500:])
assert_ok(result.returncode != 0,'injected failure not detected')
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes()==original_forward,'rollback did not restore file')
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'rollback changed secret')
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'rollback changed symlink')
print(' failure rollback: file restored; secret and symlink preserved')
(root/'flags/fail_postfix').unlink()
result=run(root,repo,version='2.4.2')
print('version upgrade', result.returncode)
if result.returncode: print(result.stdout[-2500:])
assert_ok(result.returncode == 0,'version upgrade failed')
assert_ok((root/'usr/local/postsrsd-2.4.1').exists(),'prior prefix missing')
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','new link incorrect')
print(' version upgrade: previous prefix retained; link updated')
(root/'flags/fail_postfix').touch()
result=run(root,repo,version='2.4.3')
print('failed version upgrade rollback', result.returncode)
assert_ok(result.returncode != 0,'injected failure not detected')
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','previous version link not restored')
assert_ok(not (root/'usr/local/postsrsd-2.4.3').exists(),'failed version prefix not removed')
print(' failed version upgrade: symlink restored, new prefix removed')
# Active SRS configuration must prohibit any maintenance update before writes.
(root/'flags/fail_postfix').unlink()
active_marker = '# BEGIN OOPEN-SRS-MANAGED'
existing_main=(root/'etc/postfix/main.cf')
original_main=existing_main.read_text()
existing_main.write_text(original_main + active_marker + '\n')
before=(root/'usr/local/sbin/srs-forward.py').read_bytes()
result=run(root,repo,version='2.4.2')
print('active integration guard', result.returncode)
assert_ok(result.returncode != 0,'active SRS was not refused')
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes() == before, 'active check changed scripts')
existing_main.write_text(original_main)
print(' active integration guard: blocked before file changes')
# Independent initial installation with late failure. New SRS files must be removed.
root=base/'failed-first'
repo=setup(root)
result=run(root,repo,fail='postfix')
print('failed first install', result.returncode)
assert_ok(result.returncode != 0,'injected first-install failure not detected')
assert_ok(not (root/'usr/local/postsrsd').exists(), 'first-install rollback left symlink')
assert_ok(not (root/'usr/local/postsrsd-2.4.1').exists(), 'first-install rollback left prefix')
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(), 'first-install rollback left secret')
assert_ok(not (root/'etc/postfix-srs/delivery/main.cf').exists(), 'first-install rollback left config')
assert_ok((root/'var/spool/postfix-srs-delivery/private').is_dir(), 'test Postfix runtime queue unexpectedly deleted')
print(' failed first install: tracked files removed; Postfix runtime directory safely preserved')
# Fail at postmap, before systemd and Postfix checks.
root=base/'failed-map'
repo=setup(root)
result=run(root,repo,fail='postmap')
print('failed postmap rollback', result.returncode)
assert_ok(result.returncode != 0, 'injected postmap failure not detected')
assert_ok(not (root/'etc/postfix-srs/delivery/transport.db').exists(),'failed postmap index remains')
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(),'failed postmap secret remains')
assert_ok(not (root/'usr/local/postsrsd').exists(),'failed postmap link remains')
print(' failed postmap: generated index and secret removed')
# Unexpected warnings must never be silently swallowed.
root=base/'unexpected-warning'
repo=setup(root)
(root/'flags/unknown_parameter_warning').touch()
result=run(root, repo)
print('unexpected postconf warning', result.returncode)
assert_ok(result.returncode != 0, 'unexpected postconf warning accepted')
assert_ok(not (root/'usr/local/postsrsd').exists(), 'unexpected warning allowed installation')
print(' unrelated existing warnings tolerated, queried-parameter warning rejected')
# A template referencing an unknown Postfix parameter must fail closed.
root=base/'bad-template-parameter'
repo=setup(root)
template=root/'repo/files/srs/templates/delivery-main.cf.in'
contents=template.read_text()
assert_ok('smtp_tls_CAfile = @SMTP_TLS_CAFILE@' in contents, 'missing expected template parameter')
template.write_text(contents.replace('smtp_tls_CAfile = @SMTP_TLS_CAFILE@',
'smtp_tls_cafile = @SMTP_TLS_CAFILE@'))
result=run(root, repo)
print('invalid template parameter', result.returncode)
assert_ok(result.returncode != 0, 'unknown template Postfix parameter accepted')
assert_ok(not (root/'usr/local/postsrsd').exists(), 'invalid template allowed installation')
print(' unknown requested Postfix parameter rejected')
print('ALL OFFLINE MOCK CONTRACT TESTS PASSED')