inbound journal: one row per recipient, and every row counts to the cap (audit M3)
Security audit 2026-10-17, finding M3, stage 1. inboundSession.RcptTo appended every RCPT (the outbound session de-duplicates), and the end of the message inserted a row per recipient one statement at a time inside Postfix's callback; the cut to 10 000 rows every 500 counted only the log tailer's rows. Postfix accepts a repeated recipient up to smtpd_recipient_limit (1 000) and sends each to the milter, so one message was a thousand rows, uncut until the six-hourly prune. Now: the inbound milter folds a repeated recipient (case-insensitive, as cleanup's been_here() with BH_FLAG_FOLD does, and as session.RcptTo does); a message's rows go in one transaction (store.InsertInboundSpamMessage, which replaces InsertInboundSpam in milter.InboundStore); the count and the cut moved from the tailer into the store (countInboundWrites, an atomic counter on Store) so the milter's rows, the tailer's and the tailer's start-up re-reads all count. store.InboundSpamLogMaxRows is the one cap. Tests: TestInboundRecipientRepeatedIsOneRow (1 000 RCPT in two cases, one row); TestInboundJournalIsCutWhicheverWriterWrites (a small cap: a message's rows and refusals together cross the mark, the table is cut); the tailer's own counting test went with its counter. go test ./internal/store ./internal/milter ./internal/logtail (Windows-only Follow failures aside). Also gofmt of a test line from the M2 commit. Docs: security.md § Mail path, architecture.md; CHANGELOG; plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,15 +146,6 @@ const inboundDetailMax = 255
|
||||
// cap bounds what is left - messages the client abandoned - and a flood.
|
||||
const inboundQueueIDs = 4096
|
||||
|
||||
// inboundPruneEvery is how many rows the tailer writes before it has the
|
||||
// journal cut back to its row cap, without waiting for the six-hourly prune.
|
||||
// Refusals are written at the rate strangers send RCPT TO. Postfix's one
|
||||
// limit on that, smtpd_client_recipient_rate_limit, is per client address and
|
||||
// off by default (postconf(5), Postfix 3.10.13) and not set here, so between
|
||||
// two prunes the journal would otherwise grow as fast as someone cared to
|
||||
// make it.
|
||||
const inboundPruneEvery = 500
|
||||
|
||||
// inboundJournal is the per-run state of the inbound half.
|
||||
type inboundJournal struct {
|
||||
st InboundStore
|
||||
@@ -166,8 +157,6 @@ type inboundJournal struct {
|
||||
port25 map[string]uint64
|
||||
order []queued
|
||||
seq uint64
|
||||
// written counts the rows since the journal was last cut to its cap.
|
||||
written int
|
||||
}
|
||||
|
||||
type queued struct {
|
||||
@@ -287,13 +276,9 @@ func (j *inboundJournal) handle(line string, now time.Time, catchingUp bool) {
|
||||
log.Printf("log-tailer: inbound journal: record %s -> %s: %v", from, to, err)
|
||||
return
|
||||
}
|
||||
if j.written++; j.written >= inboundPruneEvery {
|
||||
j.written = 0
|
||||
// The zero time removes nothing by age: this is the cap alone.
|
||||
if _, err := j.st.PruneInboundSpam(time.Time{}, inboundSpamLogMaxRows); err != nil {
|
||||
log.Printf("log-tailer: inbound journal: cut to %d rows: %v", inboundSpamLogMaxRows, err)
|
||||
}
|
||||
}
|
||||
// Between the six-hourly prunes the store cuts the journal to its cap
|
||||
// every so many rows, the journal milter's and these together
|
||||
// (store.InboundSpamLogMaxRows).
|
||||
}
|
||||
|
||||
func (j *inboundJournal) remember(queueID string) {
|
||||
@@ -354,9 +339,8 @@ func lineTime(line string, now time.Time) (time.Time, string) {
|
||||
return now, line
|
||||
}
|
||||
|
||||
// The journal keeps at most this many rows whatever their age: refusals are
|
||||
// written at the rate strangers knock on port 25.
|
||||
const inboundSpamLogMaxRows = 10000
|
||||
// The journal keeps at most this many rows whatever their age.
|
||||
const inboundSpamLogMaxRows = store.InboundSpamLogMaxRows
|
||||
|
||||
// inboundRetentionLoop prunes the inbound journal at once and then on the
|
||||
// cadence of the send log.
|
||||
|
||||
Reference in New Issue
Block a user