test / test (push) Canceled after 0s
Audit 2026-10-12, stage 2c, "Health says what is served"; the pages of
ea8b6bf's mockup.
Sockets (A-38). A unix socket's file outlives a killed OpenDKIM or panel
and refuses every connect() (unix(7)); Health said "Listening" from a
stat. CheckSocket and CheckMilter's unix case now dial: refused is
"Nothing listens on the ... socket: the program that made it has
stopped", EACCES is unknown (the panel may not connect), anything else
"does not answer". The probe closes before a byte, which no milter logs:
libmilter takes the EOF as SMFIC_EOF without a line (sendmail
8.17.1.9-2+deb12u2, the image's libmilter1.0.1, libmilter/comm.c
mi_rd_cmd(), engine.c mi_engine() - its source is now in dev/upstream),
OpenDKIM's mlfi_close() has no connection to free, go-milter v0.4.1
returns quietly on io.EOF (session.go, HandleMilterCommands()). The page
polls every 5 s while in view, so this mattered.
Certificate (A-65). Postfix serves build/postfix-config.sh's copy,
refreshed by cert-reload every TLS_RELOAD_INTERVAL_SECONDS. The panel now
reads smtpd_tls_cert_file from postconf -h at its start (as the retry
policy) and TLS_RELOAD_INTERVAL_SECONDS; CheckServedCertificate checks the
served copy and, when the mounted file differs byte for byte (cert-reload's
own cmp -s), names it as Renewal: its expiry and mtime, two facts on the
card. Waiting is fine for the interval: a nearing expiry of the served one
is then no warning (an expiry still an error); past interval + 1 h it is a
warning pointing at cert-reload's lines. The notifier sends no expiry
notice while a renewal waits on time.
Wording (D-P1, D-P3, and the same for the spam filter): Sender check
note, help topics sender-check, upstream, filter, certificate - word for
word as in the mockup; the filter row's detail too.
Tests: stale socket file (Linux) refused for CheckSocket and CheckMilter;
CheckServedCertificate (same, waiting, late, expired + waiting, no served
path); intervalText; the card's facts; the notifier quiet while a renewal
waits, speaking once it is late. Stand (2026-10-15, make e2e, exit 0):
new health_shows_what_is_served - the three sockets dialled ok by the
panel's user; a certificate mounted without its key is refused by
cert-reload ("do not belong together") and the card shows the served
expiry, "Renewal expires" with the mounted one's, "Renewal mounted", and
"Postfix still serves the certificate above."; a matching pair is then
served again. go vet and go test ./... pass on the server (24 packages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.4 KiB
Go
63 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/mixeme/selfpost/internal/dnscheck"
|
|
"github.com/mixeme/selfpost/internal/health"
|
|
"github.com/mixeme/selfpost/internal/notify"
|
|
"github.com/mixeme/selfpost/internal/postfix"
|
|
"github.com/mixeme/selfpost/internal/store"
|
|
)
|
|
|
|
// runNotifier writes to the panel's users when something needs them
|
|
// (architecture.md § Notifications), until ctx is cancelled. It reads what the
|
|
// pages read - a domain's published records against the key this server signs
|
|
// with, the certificate file, Postfix's queue - with a DNS checker of its own,
|
|
// so that its hourly look is never a page's cached one.
|
|
func runNotifier(ctx context.Context, cfg config, st *store.Store) error {
|
|
ms := newMailStack(cfg, st)
|
|
dns := dnscheck.New(cfg.dnsResolvers)
|
|
|
|
// 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:
|
|
// 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 {
|
|
log.Printf("notifier: OpenDKIM now signs mail of %s", cfg.hostname)
|
|
}
|
|
|
|
n := notify.New(st, notify.Checks{
|
|
Domain: func(d store.Domain) (dnscheck.Domain, error) {
|
|
rec, err := ms.Domains.DKIMRecord(d)
|
|
if err != nil {
|
|
return dnscheck.Domain{}, err
|
|
}
|
|
// The server's own addresses are cached for a minute: one lookup
|
|
// for a pass over all the domains.
|
|
srv := dns.Server(cfg.hostname, false)
|
|
return dns.Domain(dnscheck.Query{
|
|
Name: d.Name,
|
|
Selector: d.DKIMSelector,
|
|
ExpectedDKIM: rec.Value,
|
|
Hostname: srv.Hostname,
|
|
ServerIPs: srv.IPs,
|
|
DMARCReportEmail: d.DMARCRua,
|
|
}, true), nil
|
|
},
|
|
Certificate: func() health.Certificate {
|
|
return health.CheckServedCertificate(cfg.tlsServedCertFile, cfg.tlsCertFile, cfg.tlsReloadInterval, time.Now())
|
|
},
|
|
Queue: postfix.QueueMessages,
|
|
}, notify.NewMailer(cfg.hostname), cfg.hostname)
|
|
return n.Run(ctx)
|
|
}
|