audit 4: DNS outages unknown, DMARC limits said, typed Health, one TLS mode per upstream
test / test (push) Canceled after 0s

M14, M15: queryEach gives each resolver 2 s within the check's context and
asks the one that answered last first; an address or PTR lookup not
answered is Unknown, an answered mismatch beside it still an error;
Checker.Server keeps the last answer's addresses, so SPF does not go Unknown
with it. Notifications report faults only, now tested with SPF unknown
alone and beside a wrong DKIM.

DMARC reports: tests of malformed XML, gzip and zip bombs, a message over
the limit, the service's Resync, hosted suggestion and validation. The
finding: both limits cut silently and an oversized report failed as
damaged; readAtMost refuses it for its size (MaxMessage 10 MiB, moved into
IngestMessage).

healthReadings replaces map[string]any for Health and Overview.

L5: an inbound upstream has one TLS mode - a second domain to the same
[host]:port with another mode is refused, naming the first; the TLS policy
map holds each next hop once.

Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 631 s) on this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-15 12:15:58 +03:00
co-authored by Claude Opus 5.5
parent eee089ad4b
commit 006bed7022
22 changed files with 593 additions and 202 deletions
+33 -4
View File
@@ -1326,7 +1326,7 @@ command whose leftover is the owner's alone.
retried - the subject is gone with it, as before. Tests: a failed copy
goes out at the next check, once; the ceiling holds one back; a user
added mid-subject hears nothing of it, its end included.
- [ ] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
- [x] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
2026-10-14 - the two findings had no step). Each resolver gets a time
budget of its own, so a silent first resolver no longer leaves the second
an expired context ([dnscheck/resolver.go](../internal/dnscheck/resolver.go));
@@ -1338,7 +1338,17 @@ command whose leftover is the owner's alone.
and tested. Tests: a first resolver that drops packets, a PTR timeout.
architecture.md:339-340 and the `DefaultResolvers` comment say what is
true. — **Opus**
- [ ] **DMARC reports tested where they come in** (owner, 2026-10-14; rows of
*Done 2026-10-15:* `queryEach` gives each resolver
`perResolverTimeout` (2 s) within the check's context and asks the one
that answered last first (a dropping first resolver costs one 2 s wait,
then is asked last); an address or PTR lookup not answered is Unknown
(`lookupFailed`), an answered mismatch beside it still the error it is;
`Checker.Server` keeps the last answer's addresses when the lookup was
not answered. `notify.dnsReading` already reported faults only - now
tested with SPF unknown alone and beside a wrong DKIM. architecture.md
no longer has the sentence the audit cited; the guide's
`SELFPOST_DNS_RESOLVERS` row says the budget and the order.
- [x] **DMARC reports tested where they come in** (owner, 2026-10-14; rows of
review-2026-08-followups). The panel parses reports that arrive by mail
from anyone, and the `internal/dmarc` service (`service.go`,
`addresses.go`) and the DMARC handlers have no tests; no other step
@@ -1346,18 +1356,37 @@ command whose leftover is the owner's alone.
oversized, compressed past its limit, or names a domain the instance does
not have: refused or skipped with a reason, nothing stored, the panel
alive. A finding is fixed in this step. — **Opus**
- [ ] **The last untyped page data** (owner, 2026-10-13): `healthChecks`
*Done 2026-10-15.* Since the audit the package had gained tests (bounds,
unknown domain, tag mismatch, not a report, a store failure, the
allow-list) and the handlers theirs. Added: malformed XML, a gzip bomb
and a zip bomb (9 MiB of zeroes), a message over the limit - each
refused with its reason, nothing stored; the service's Resync on and
off, the hosted suggestion and its validation. The finding: both limits
cut silently (`io.LimitReader`), so an oversized report failed as
damaged; `readAtMost` refuses it for its size (`MaxMessage` 10 MiB, the
message limit moved from cmd/panel into `IngestMessage`). The panel
is not in the path: ingest is its own process per message, and a
refusal exits 0 with the reason in `mail.log`.
- [x] **The last untyped page data** (owner, 2026-10-13): `healthChecks`
([handlers_status.go](../internal/web/handlers/handlers_status.go)) returns
a struct instead of `map[string]any`, so a mistyped key in `healthPage` /
`healthCards` is a compile error and not an empty card on Server › Health.
No template change. This closes what is left of the roadmap's former
`template-data-typing`. — **Sonnet**
- [ ] **Upstream TLS mode validated at save** (L5): saving an inbound domain
*Done 2026-10-15:* `healthReadings`, read by `healthPage` and
`healthCards` and the tests' fixtures; no `map[string]any` is left in
internal/web outside tests.
- [x] **Upstream TLS mode validated at save** (L5): saving an inbound domain
whose `[host]:port` is already used by another domain with a different
TLS mode is refused with the validator's text naming that domain and its
mode; `TestRenderInboundMaps` gets the two-routes-one-next-hop case and the
map is asserted to hold one key. Guide § Inbound domains says a next hop
has one TLS mode. — **Opus**
*Done 2026-10-15:* `inbound.Service.checkOneTLSModePerUpstream` (the
domain itself may change its own mode; another port is another next
hop); `renderInboundMaps` writes a next hop's policy line once (rows
saved before keep the first by domain name, as Postfix did). Tests in
inbound and postfix (`TestRenderInboundMapsOneTLSLinePerNextHop`).
### Stage 5 — image and Postfix hardening (M17–M19, M21, L30–L32) → `2.6.0`