logtail, health, panel: cite the source of what they rely on (assumptions part A)
Comments only, no behaviour changed. Each place the log tailer, Health and cmd/panel rely on another program now says where that is written, for the version the image runs: Postfix 3.7.11 (log_adhoc(), msg_logger_print(), smtpd_whatsup(), cleanup_milter.c, qmgr_active.c, postlogd.c, showq.c, pipe.c), logrotate 3.21.0, supervisor 4.2.5, proc(5). Measurements stay measurements and say where and when; test fixtures say whether they were seen or composed. Where the source contradicts the code the comment does not bless it but names the register entry (A-8 expiry, A-12 the ISO stamp no Postfix writes, A-15 the last recipient, A-21/A-23 sasl_username, A-33 postlogd's old inode, A-38 a socket file is not a listener, A-65 the certificate served); those are fixed in their own commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,12 @@ func runDMARCIngestMode() error {
|
||||
}
|
||||
// Not a report this relay keeps. It is dropped: deferring would have
|
||||
// Postfix try it again for days, and a bounce would go to an envelope
|
||||
// sender anyone can write - the report addresses are public.
|
||||
// sender anyone can write - the report addresses are public. Postfix
|
||||
// 3.7.11: exit 0 is PIPE_STAT_OK, logged "status=sent (delivered via
|
||||
// ... service ...)" (src/global/pipe_command.c, pipe_command();
|
||||
// src/pipe/pipe.c, eval_command_status()); a 4.x.x status defers until
|
||||
// maximal_queue_lifetime and a 5.x.x one bounces, either way a notice
|
||||
// to the sender (src/qmgr/qmgr_active.c, src/global/bounce.c).
|
||||
if incrErr := st.IncrDMARCParseFailures(); incrErr != nil {
|
||||
log.Printf("dmarc ingest: record failure: %v", incrErr)
|
||||
}
|
||||
@@ -47,8 +52,13 @@ func runDMARCIngestMode() error {
|
||||
// what a delivery command wrote - about a hundred bytes - into its own line
|
||||
// for the message ("status=sent (delivered via dmarc-ingest service (...))"),
|
||||
// so the reason goes out first and bare: no prefix, no date, no recipient,
|
||||
// all of which that line already has. postlog is no use here: pipe(8) starts
|
||||
// the command without the group that may write to Postfix's log socket.
|
||||
// all of which that line already has. Postfix 3.7.11, src/pipe/pipe.c,
|
||||
// eval_command_status(): " (%.100s)" of the output, which
|
||||
// src/global/pipe_command.c takes from stdout and stderr together. postlog is
|
||||
// no use here: pipe(8) starts the command without the group that may write to
|
||||
// Postfix's log socket (src/util/set_ugid.c, set_ugid(): setgroups() to the
|
||||
// user's one group). Whether Debian's postlog is set-gid, which would bring
|
||||
// the group with it, is not recorded - assumptions A-43.
|
||||
func logRejected(reason error) {
|
||||
fmt.Fprintln(os.Stderr, "dropped: "+logSafe(strings.TrimPrefix(reason.Error(), dmarc.ErrRejected.Error()+": ")))
|
||||
}
|
||||
@@ -73,6 +83,9 @@ func logSafe(s string) string {
|
||||
// ingestRecipient is the report address the message was delivered to. Postfix's
|
||||
// pipe(8) hands it over as an argument (${recipient} in the service's argv,
|
||||
// build/postfix-config.sh) - it sets no RECIPIENT variable, that is local(8).
|
||||
// Postfix 3.7.11, src/pipe/pipe.c, deliver_message(): pipe_command() gets only
|
||||
// CA_PIPE_CMD_EXPORT(export_environment), no extra environment;
|
||||
// src/local/command.c sets "RECIPIENT".
|
||||
// Without it the check that a report came to its own domain's address is
|
||||
// skipped.
|
||||
func ingestRecipient(args []string) string {
|
||||
|
||||
@@ -99,7 +99,9 @@ func newPanel(cfg config, st *store.Store) (*web.Server, error) {
|
||||
ms := newMailStack(cfg, st)
|
||||
// postfix-config.sh has already run (entrypoint). postconf -h is the
|
||||
// effective config, including a manual override; the panel keeps this
|
||||
// snapshot for the process lifetime (architecture.md).
|
||||
// snapshot for the process lifetime (architecture.md). Postfix 3.7.11,
|
||||
// postconf(1): without -d or -n it shows main.cf's value or the default,
|
||||
// and -h drops the "name = " label; values are not $-expanded without -x.
|
||||
retryPolicy := postfix.LoadRetryPolicy()
|
||||
return web.New(st, ms.Domains, ms.Apps, ms.Inbound, ms.Lists, ms.DMARC, web.Config{
|
||||
Hostname: cfg.hostname,
|
||||
|
||||
@@ -54,7 +54,9 @@ func listenMilter(socketPath string) (net.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
// Clear a stale socket left behind by an unclean shutdown, otherwise the
|
||||
// listen below fails with "address already in use".
|
||||
// listen below fails with "address already in use" (unix(7): the socket
|
||||
// file "must be deleted by the caller when it is no longer needed"; bind(2)
|
||||
// EADDRINUSE).
|
||||
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,6 +69,9 @@ func listenMilter(socketPath string) (net.Listener, error) {
|
||||
// group access. The socket inherits group `selfpost` from the setgid parent
|
||||
// dir entrypoint.sh prepares; make it group read/write so postfix can reach
|
||||
// it (connecting to a Unix socket needs write permission on the node).
|
||||
// unix(7): "connecting to a stream socket object requires write
|
||||
// permission on that socket"; inode(7): a file made in a set-group-ID
|
||||
// directory takes the directory's group.
|
||||
if err := os.Chmod(socketPath, 0o660); err != nil {
|
||||
ln.Close()
|
||||
return nil, err
|
||||
|
||||
+14
-2
@@ -34,7 +34,12 @@ func main() {
|
||||
log.SetPrefix("dmarc-ingest: ")
|
||||
if err := runDMARCIngestMode(); err != nil {
|
||||
log.Printf("ingest failed: %v", err)
|
||||
os.Exit(75) // EX_TEMPFAIL — ask Postfix to defer
|
||||
// EX_TEMPFAIL — ask Postfix to defer. Postfix 3.7.11,
|
||||
// src/global/pipe_command.c, pipe_command(): a sysexits code maps
|
||||
// to its DSN (src/global/sys_exits.c: 75 is "4.3.0") and a 4 defers,
|
||||
// unless the output starts with a "D.S.N text" of its own - the
|
||||
// log prefix above makes sure it does not.
|
||||
os.Exit(75)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -200,7 +205,14 @@ func antispamAction(raw string) string {
|
||||
|
||||
// saslRealm chooses the realm new SASL accounts live under. It mirrors the
|
||||
// hostname Postfix's SASL layer uses so a client authenticating with a bare
|
||||
// login resolves to the right account.
|
||||
// login resolves to the right account. Postfix 3.7.11,
|
||||
// src/xsasl/xsasl_cyrus_server.c, xsasl_cyrus_server_create(): serverFQDN is
|
||||
// $myhostname (= SELFPOST_HOSTNAME, build/postfix-config.sh) and user_realm
|
||||
// NULL while smtpd_sasl_local_domain is empty; Cyrus SASL 2.1.28,
|
||||
// plugins/sasldb.c, sasldb_auxprop_lookup(), and common/plugin_common.c,
|
||||
// _plug_parseuser(): a login
|
||||
// without "@" is then looked up in the realm serverFQDN. A SASL_REALM other
|
||||
// than SELFPOST_HOSTNAME does not follow that - assumptions A-48.
|
||||
func saslRealm() string {
|
||||
if r := os.Getenv("SASL_REALM"); r != "" {
|
||||
return r
|
||||
|
||||
@@ -22,7 +22,12 @@ func runNotifier(ctx context.Context, cfg config, st *store.Store) error {
|
||||
|
||||
// OpenDKIM's tables are rebuilt when a domain is added or deleted. On the
|
||||
// first start of a version that signs the server's own name they do not
|
||||
// have its line yet. A failure here costs the signature, not the message.
|
||||
// have its line yet. A failure here costs the signature, not the message:
|
||||
// OpenDKIM 2.11.0~beta2, opendkim/opendkim.c, mlfi_eoh(): with no
|
||||
// SigningTable match and Mode s (build/opendkim.conf) the message is
|
||||
// accepted unsigned. A line whose key the KeyTable lacks or that does not
|
||||
// load is another matter - dkimf_apply_signtable() < 0 there answers
|
||||
// SMFIS_TEMPFAIL (assumptions A-47).
|
||||
if rebuilt, err := ms.Domains.EnsureServerSigned(); err != nil {
|
||||
log.Printf("notifier: sign as %s: %v", cfg.hostname, err)
|
||||
} else if rebuilt {
|
||||
|
||||
@@ -19,8 +19,11 @@ import (
|
||||
const aliveEvery = 2 * time.Second
|
||||
|
||||
// serveSenderCheck runs the sender check (internal/milter/sender.go) on a
|
||||
// socket of its own. Postfix attaches that socket strictly, so while this
|
||||
// role is not serving, mail is deferred: what that costs is counted here and
|
||||
// socket of its own. Postfix attaches that socket strictly
|
||||
// (default_action=tempfail in build/postfix-config.sh; Postfix 3.7.11,
|
||||
// src/milter/milter8.c, milter8_comm_error(): "451 4.7.1 Service unavailable -
|
||||
// try again later"), so while this role is not serving, mail is deferred:
|
||||
// what that costs is counted here and
|
||||
// by the log tailer, and shown on Server > Health
|
||||
// (docs/plans/from-header-binding.md § Statistics).
|
||||
func serveSenderCheck(ctx context.Context, cfg config, st *store.Store) error {
|
||||
@@ -131,7 +134,12 @@ func byDay(from, to time.Time) []dayPart {
|
||||
var postfixAskFor, postfixAskEvery = 30 * time.Second, 2 * time.Second
|
||||
|
||||
// postfixStarted asks supervisord since when Postfix has been running, again
|
||||
// and again for postfixAskFor while it does not answer.
|
||||
// and again for postfixAskFor while it does not answer. supervisor 4.2.5,
|
||||
// supervisor/rpcinterface.py, _interpretProcessInfo(): the uptime runs from
|
||||
// the spawn of program "postfix", which is build/postfix-wrapper.sh; it waits
|
||||
// for the milter sockets (up to MILTER_WAIT_TIMEOUT) before it execs
|
||||
// "postfix start-fg", so the time given is up to that much early
|
||||
// (assumptions A-46).
|
||||
func postfixStarted() postfixState {
|
||||
deadline := time.Now().Add(postfixAskFor)
|
||||
for {
|
||||
|
||||
Reference in New Issue
Block a user