Record outbound relay and LMTP live tests

This commit is contained in:
Mario Fetka
2026-07-27 08:35:59 +02:00
parent 6d78fbd535
commit 4e7f33ac58
4 changed files with 64 additions and 17 deletions
+3 -1
View File
@@ -60,7 +60,9 @@ one-off files in `/tmp`:
envelope plus an unsigned, non-SRS internal LMTP message while an
unreachable global relayhost is enabled. This proves that internal LMTP
routing takes precedence, after which the test restores the exact SMTP
configuration and active service.
configuration and active service. The fixture itself is unprivileged; the
script uses sudo only for the already-authorized `bongo-admin` and
`systemctl` operations.
- `smtp-client-interoperability-check.py` repeats real delivery with curl,
swaks, Xeams Email Sender, GNU Mailutils, and paced Telnet input. It covers
authenticated STARTTLS submission plus unauthenticated local delivery on
+28 -7
View File
@@ -52,6 +52,12 @@ def load_helpers():
SMTP07 = load_helpers()
def privileged(arguments: list[str]) -> list[str]:
if os.geteuid() == 0:
return arguments
return ["/usr/bin/sudo", "-n", *arguments]
@dataclass
class LMTPCapture:
lock: threading.Lock = field(default_factory=threading.Lock)
@@ -167,7 +173,9 @@ class ThreadingLMTPServer(
def read_configuration(name: str) -> dict:
completed = SMTP07.run([SMTP07.ADMIN, "__config-read", name])
completed = SMTP07.run(
privileged([SMTP07.ADMIN, "__config-read", name])
)
try:
value = json.loads(completed.stdout)
except json.JSONDecodeError as error:
@@ -181,11 +189,26 @@ def read_configuration(name: str) -> dict:
def replace_configuration(name: str, value: dict) -> None:
SMTP07.run(
[SMTP07.ADMIN, "__config-replace", name],
privileged([SMTP07.ADMIN, "__config-replace", name]),
input_text=json.dumps(value, separators=(",", ":")),
)
def service_active() -> bool:
return SMTP07.run(
privileged(
[SMTP07.SYSTEMCTL, "is-active", "--quiet", "bongo.service"]
),
check=False,
).returncode == 0
def restart_bongo() -> None:
SMTP07.run(
privileged([SMTP07.SYSTEMCTL, "restart", "bongo.service"])
)
def submit(host: str, port: int) -> None:
message = (
f"From: Mailman return <{SENDER}>\r\n"
@@ -254,9 +277,7 @@ def validate(delivery: tuple[bytes, bytes, bytes]) -> None:
def perform_test() -> None:
if os.geteuid() != 0:
raise SMTP20Error("the live LMTP test must run as root")
originally_active = SMTP07.service_active()
originally_active = service_active()
if not originally_active:
raise SMTP20Error("bongo.service must be active before this test")
@@ -305,7 +326,7 @@ def perform_test() -> None:
try:
replace_configuration("smtp", test_smtp)
configuration_changed = True
SMTP07.run([SMTP07.SYSTEMCTL, "restart", "bongo.service"])
restart_bongo()
SMTP07.INTERNAL_HOST = internal_host
SMTP07.INTERNAL_PORT = internal_port
SMTP07.wait_for_bongo()
@@ -331,7 +352,7 @@ def perform_test() -> None:
restoration_errors.append(f"LMTP fixture shutdown failed: {error}")
if originally_active:
try:
SMTP07.run([SMTP07.SYSTEMCTL, "restart", "bongo.service"])
restart_bongo()
SMTP07.INTERNAL_HOST = internal_host
SMTP07.INTERNAL_PORT = internal_port
SMTP07.wait_for_bongo()
+1 -1
View File
@@ -148,7 +148,7 @@ inputs and outputs is absent from the ZIP.
| SMTP-26 | IDNA2008 domains use one canonical A-label in config, aliases, DNS lookup, SMTP and Web compose | | | |
| SMTP-18 | Recipient validation, multiple recipients, partial refusal, aliases | | | |
| SMTP-19 | Domain/address aliases, forwarding loops, and SRS forwarding | | | |
| SMTP-20 | Trusted internal SMTP to disposable LMTP preserves the envelope and adds neither DKIM nor SRS | | | |
| SMTP-20 | Trusted internal SMTP to disposable LMTP preserves the envelope and adds neither DKIM nor SRS | [PASS](test-evidence/0.7-r1.md#smtp-20) | | |
| SMTP-21 | LMTP per-recipient 2xx, 4xx, 5xx, retry, and duplicate prevention | | | |
| SMTP-22 | XCLIENT/XFORWARD negotiation only to trusted destinations | | | |
| SMTP-23 | Fallback client-address header only for configured trusted peers | | | |
+32 -8
View File
@@ -2452,22 +2452,46 @@ This models mail returned from Mailman over the trusted internal SMTP
listener: internal ingress is not signed merely on receipt, but the message
is signed before it leaves Bongo for an external relay or recipient.
The recovery phase deliberately installs an incorrect relay password,
verifies that the message remains in Queue 7 and is not accepted by the
relay, repairs the secret, explicitly retries the retained entry, and
requires successful signed delivery. Commit `8f0c1518` makes this
administrative retry reliable by reserving the queue entry before `QRUN`
acknowledges it; a concurrent attempt is now reported as busy instead of
silently losing the retry.
The two recovery phases first select a bound but non-listening loopback port
and then install an incorrect relay password. In both cases the test verifies
that the message remains in Queue 7 and is not accepted by the relay. It then
repairs the endpoint or secret, retries the retained entry, and requires
successful signed delivery. Commit `8f0c1518` makes this administrative retry
reliable by reserving the queue entry before `QRUN` acknowledges it; a
concurrent attempt is now reported as busy instead of silently losing the
retry.
```text
SMTP-09 PASS hostname=localhost ports=12525/12465 starttls=no-auth implicit-tls=auth failure=retained recovery=delivered trusted-port26=dkim-signed dkim=verified queue-clean=yes config-restored=yes
SMTP-09 PASS hostname=localhost ports=12525/12465 starttls=no-auth implicit-tls=auth unreachable=retained/recovered auth-failure=retained/recovered trusted-port26=dkim-signed dkim=verified queue-clean=yes config-restored=yes
```
The test restored the exact SMTP Store document, removed its queue entries,
password and CA files, and returned `bongo.service` to its original active
state.
## SMTP-20
Result: **PASS**
The reusable `smtp-lmtp-delivery-check.py` starts a loopback-only,
status-aware LMTP fixture on an unprivileged port. It adds one temporary
transport-domain mapping, enables an intentionally unreachable global
relayhost, and submits a Mailman-style message through trusted port 26.
The installed Bongo client selected the internal LMTP transport before the
global relayhost, preserved the original envelope sender and recipient, and
delivered exactly one message. The captured internal copy had neither a
`DKIM-Signature` nor an SRS envelope. This proves both the internal-delivery
exception and that enabling a global relayhost cannot redirect LMTP traffic
outside Bongo.
```text
SMTP-20 PASS ingress=trusted-internal-smtp transport=lmtp relayhost=bypassed envelope=original dkim=absent srs=absent duplicate=no queue-clean=yes config-restored=yes service-restored=active
```
The test removed its marked Queue data, stopped the fixture, restored the
exact SMTP Store document, and returned `bongo.service` to the active state.
## SMTP-27
Result: **PASS**