audit 4: DNS outages unknown, DMARC limits said, typed Health, one TLS mode per upstream
test / test (push) Canceled after 0s
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:
@@ -38,7 +38,7 @@ func socketsServer(t *testing.T) *Handlers {
|
||||
|
||||
func socketsOf(t *testing.T, h *Handlers) []health.Socket {
|
||||
t.Helper()
|
||||
return h.healthChecks()["Sockets"].([]health.Socket)
|
||||
return h.healthChecks().Sockets
|
||||
}
|
||||
|
||||
func socketCard(t *testing.T, h *Handlers) view.HealthCard {
|
||||
@@ -143,8 +143,8 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
|
||||
}
|
||||
|
||||
c := h.healthChecks()
|
||||
if c["SocketStatus"].(health.Status) != tt.status {
|
||||
t.Errorf("action %q: SocketStatus = %q, want %q", tt.action, c["SocketStatus"], tt.status)
|
||||
if c.SocketStatus != tt.status {
|
||||
t.Errorf("action %q: SocketStatus = %q, want %q", tt.action, c.SocketStatus, tt.status)
|
||||
}
|
||||
card := socketCard(t, h)
|
||||
if card.Value != "3 of 4 answering" || card.Level != tt.level || !strings.HasSuffix(card.Sub, "Spam filter") {
|
||||
|
||||
@@ -16,18 +16,18 @@ import (
|
||||
|
||||
// healthReading is one plausible reading of every check Health shows, so a test
|
||||
// can render the page and vary the one part it is about.
|
||||
func healthReading() map[string]any {
|
||||
func healthReading() healthReadings {
|
||||
c := checksFixture()
|
||||
c["Processes"] = []health.Process{
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Detail: "pid 21", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "FATAL", Detail: "exited too quickly", Status: health.StatusError},
|
||||
}
|
||||
c["Cert"] = health.Certificate{
|
||||
c.Cert = health.Certificate{
|
||||
Path: "/etc/postfix/tls/fullchain.pem", Subject: "mail.example.com",
|
||||
NotAfter: time.Date(2026, 11, 8, 9, 14, 0, 0, time.UTC), DaysLeft: 30,
|
||||
Status: health.StatusOK, Detail: "Valid for another 30 day(s).",
|
||||
}
|
||||
c["Machine"] = health.Machine{
|
||||
c.Machine = health.Machine{
|
||||
CPU: health.CPU{
|
||||
Measured: true, BusyPct: 12.4, Cores: 4, Threads: 4,
|
||||
Status: health.StatusOK, Detail: "4 cores · 4 threads",
|
||||
@@ -43,13 +43,13 @@ func healthReading() map[string]any {
|
||||
Window: 5 * time.Second,
|
||||
Status: health.StatusOK,
|
||||
}
|
||||
c["Sockets"] = []health.Socket{
|
||||
c.Sockets = []health.Socket{
|
||||
{Name: "OpenDKIM", Path: "/run/opendkim/opendkim.sock", Present: true, Status: health.StatusOK, Detail: "Listening"},
|
||||
{Name: "send-log", Path: "/run/selfpost/journal.sock", Status: health.StatusWarn, Detail: "send-log socket missing: the log stops being written."},
|
||||
}
|
||||
c["SocketStatus"] = health.StatusWarn
|
||||
c["Hostname"] = "mail.example.com"
|
||||
c["PTR"] = dnscheck.Result{
|
||||
c.SocketStatus = health.StatusWarn
|
||||
c.Hostname = "mail.example.com"
|
||||
c.PTR = dnscheck.Result{
|
||||
Status: health.StatusError,
|
||||
Detail: "No address has a reverse record.",
|
||||
Records: []string{"203.0.113.10 → no PTR record"},
|
||||
@@ -57,7 +57,7 @@ func healthReading() map[string]any {
|
||||
return c
|
||||
}
|
||||
|
||||
func renderHealth(t *testing.T, h *Handlers, c map[string]any, flash string) string {
|
||||
func renderHealth(t *testing.T, h *Handlers, c healthReadings, flash string) string {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
h.view.Render(rec, 200, "health", healthPage(view.Meta{User: "admin", IsGlobal: true}, c, flash))
|
||||
@@ -111,7 +111,7 @@ func TestHealthPageRendersEveryCheck(t *testing.T) {
|
||||
func TestHealthPageWithoutMachineMetrics(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
c := healthReading()
|
||||
c["Machine"] = health.Machine{
|
||||
c.Machine = health.Machine{
|
||||
CPU: health.CPU{Status: health.StatusUnknown, Detail: "The kernel's processor counters (/proc/stat) could not be read here."},
|
||||
Memory: health.Memory{Status: health.StatusUnknown, Detail: "The kernel's memory counters (/proc/meminfo) could not be read here."},
|
||||
Network: health.Network{Status: health.StatusUnknown, Detail: "The kernel's network counters (/proc/net/dev) could not be read here."},
|
||||
@@ -131,12 +131,12 @@ func TestHealthPageWithoutMachineMetrics(t *testing.T) {
|
||||
func TestHealthPageStatesOfTheOtherChecks(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
c := healthReading()
|
||||
c["ProcessError"] = true
|
||||
c["ProcessStatus"] = health.StatusUnknown
|
||||
c["Processes"] = []health.Process(nil)
|
||||
c["Cert"] = health.Certificate{Status: health.StatusUnknown, Detail: "No certificate path is configured (TLS_CERT_FILE)."}
|
||||
c["Hostname"] = ""
|
||||
c["PTR"] = dnscheck.Result{Status: health.StatusUnknown}
|
||||
c.ProcessError = true
|
||||
c.ProcessStatus = health.StatusUnknown
|
||||
c.Processes = []health.Process(nil)
|
||||
c.Cert = health.Certificate{Status: health.StatusUnknown, Detail: "No certificate path is configured (TLS_CERT_FILE)."}
|
||||
c.Hostname = ""
|
||||
c.PTR = dnscheck.Result{Status: health.StatusUnknown}
|
||||
out := renderHealth(t, h, c, "DNS re-checked.")
|
||||
for _, want := range []string{
|
||||
"Could not ask supervisord for the process list.",
|
||||
@@ -155,13 +155,13 @@ func TestHealthPageStatesOfTheOtherChecks(t *testing.T) {
|
||||
func TestHealthPageNamesARenewalWaiting(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
c := healthReading()
|
||||
cert := c["Cert"].(health.Certificate)
|
||||
cert := c.Cert
|
||||
cert.Renewal = &health.Renewal{
|
||||
NotAfter: time.Date(2027, 1, 31, 8, 2, 0, 0, time.UTC),
|
||||
Mounted: time.Date(2026, 10, 15, 3, 12, 0, 0, time.UTC),
|
||||
}
|
||||
cert.Detail = "Postfix still serves the certificate above. A renewed one is waiting in the mounted file."
|
||||
c["Cert"] = cert
|
||||
c.Cert = cert
|
||||
out := renderHealth(t, h, c, "")
|
||||
for _, want := range []string{
|
||||
"<dt>Expires</dt>", "2026-11-08 09:14 UTC",
|
||||
|
||||
@@ -186,20 +186,20 @@ func senderCheckCard(s store.SenderCheckStats, now time.Time) (rows []view.Sende
|
||||
// healthPage turns one reading of the checks (healthChecks) into the Health
|
||||
// page: a table row per resource, process and socket, and the verdict of each
|
||||
// box in its head.
|
||||
func healthPage(m view.Meta, c map[string]any, flash string) *view.Health {
|
||||
func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
page := view.NewHealth(m, flash)
|
||||
|
||||
machine := c["Machine"].(health.Machine)
|
||||
machine := c.Machine
|
||||
page.Machine.End = view.Verdict(string(machine.Status))
|
||||
page.MachineRows = machineRows(machine)
|
||||
|
||||
page.Processes.End = view.Verdict(string(c["ProcessStatus"].(health.Status)))
|
||||
page.ProcessError = c["ProcessError"].(bool)
|
||||
for _, p := range c["Processes"].([]health.Process) {
|
||||
page.Processes.End = view.Verdict(string(c.ProcessStatus))
|
||||
page.ProcessError = c.ProcessError
|
||||
for _, p := range c.Processes {
|
||||
page.ProcessRows = append(page.ProcessRows, processRow(p))
|
||||
}
|
||||
|
||||
cert := c["Cert"].(health.Certificate)
|
||||
cert := c.Cert
|
||||
page.Certificate.End = view.Verdict(string(cert.Status))
|
||||
if !cert.NotAfter.IsZero() {
|
||||
page.CertFacts = append(page.CertFacts, view.Fact{Label: "Expires", Value: view.Plain(cert.NotAfter.UTC().Format("2006-01-02 15:04 UTC")), Mono: true})
|
||||
@@ -214,8 +214,8 @@ func healthPage(m view.Meta, c map[string]any, flash string) *view.Health {
|
||||
}
|
||||
page.CertDetail, page.CertProblem = cert.Detail, cert.Status != health.StatusOK
|
||||
|
||||
page.Sockets.End = view.Verdict(string(c["SocketStatus"].(health.Status)))
|
||||
for _, s := range c["Sockets"].([]health.Socket) {
|
||||
page.Sockets.End = view.Verdict(string(c.SocketStatus))
|
||||
for _, s := range c.Sockets {
|
||||
row := view.SocketRow{Name: s.Name, Note: s.Note, Path: s.Path, State: view.Tag{Status: string(s.Status)}}
|
||||
if s.Status != health.StatusOK {
|
||||
row.Detail = s.Detail
|
||||
@@ -223,9 +223,9 @@ func healthPage(m view.Meta, c map[string]any, flash string) *view.Health {
|
||||
page.SocketRows = append(page.SocketRows, row)
|
||||
}
|
||||
|
||||
ptr := c["PTR"].(dnscheck.Result)
|
||||
ptr := c.PTR
|
||||
page.Hostname.End = view.Verdict(string(ptr.Status))
|
||||
host := c["Hostname"].(string)
|
||||
host := c.Hostname
|
||||
if host == "" {
|
||||
host = "(SELFPOST_HOSTNAME is not set)"
|
||||
}
|
||||
@@ -293,12 +293,12 @@ func lines(s string) []string {
|
||||
|
||||
// healthCards turns the checks into the six cards of Overview, in the order of
|
||||
// the tables on Health. Each card links to the table behind it.
|
||||
func healthCards(c map[string]any) []view.HealthCard {
|
||||
machine := c["Machine"].(health.Machine)
|
||||
procs := c["Processes"].([]health.Process)
|
||||
cert := c["Cert"].(health.Certificate)
|
||||
sockets := c["Sockets"].([]health.Socket)
|
||||
ptr := c["PTR"].(dnscheck.Result)
|
||||
func healthCards(c healthReadings) []view.HealthCard {
|
||||
machine := c.Machine
|
||||
procs := c.Processes
|
||||
cert := c.Cert
|
||||
sockets := c.Sockets
|
||||
ptr := c.PTR
|
||||
|
||||
card := func(name, icon, href string, status health.Status, value, sub string) view.HealthCard {
|
||||
return view.HealthCard{Name: name, Icon: icon, Href: href, Level: cardLevel(status), Value: value, Sub: sub}
|
||||
@@ -331,7 +331,7 @@ func healthCards(c map[string]any) []view.HealthCard {
|
||||
if len(notRunning) > 0 {
|
||||
procSub = strings.Join(notRunning, ", ")
|
||||
}
|
||||
if c["ProcessError"].(bool) {
|
||||
if c.ProcessError {
|
||||
procValue, procSub = "Could not be read", "supervisord"
|
||||
}
|
||||
|
||||
@@ -340,8 +340,8 @@ func healthCards(c map[string]any) []view.HealthCard {
|
||||
certValue = fmt.Sprintf("Expires in %d days", cert.DaysLeft)
|
||||
}
|
||||
|
||||
queueValue := queueCardValue(c["QueueSummary"].(string))
|
||||
if e := c["QueueError"].(string); e != "" {
|
||||
queueValue := queueCardValue(c.QueueSummary)
|
||||
if e := c.QueueError; e != "" {
|
||||
queueValue = "Could not be read"
|
||||
}
|
||||
|
||||
@@ -378,12 +378,12 @@ func healthCards(c map[string]any) []view.HealthCard {
|
||||
|
||||
return []view.HealthCard{
|
||||
card("Machine", "ti-cpu", "/server/health", machine.Status, cpu, ram),
|
||||
card("Processes", "ti-server-cog", "/server/health", c["ProcessStatus"].(health.Status), procValue, procSub),
|
||||
card("Processes", "ti-server-cog", "/server/health", c.ProcessStatus, procValue, procSub),
|
||||
card("TLS certificate", "ti-certificate", "/server/health", cert.Status, certValue, cert.Subject),
|
||||
card("Queue", "ti-stack-2", "/outbound/queue", c["QueueStatus"].(health.Status), queueValue, "outbound"),
|
||||
card("Milter sockets", "ti-plug-connected", "/server/health", c["SocketStatus"].(health.Status),
|
||||
card("Queue", "ti-stack-2", "/outbound/queue", c.QueueStatus, queueValue, "outbound"),
|
||||
card("Milter sockets", "ti-plug-connected", "/server/health", c.SocketStatus,
|
||||
fmt.Sprintf("%d of %d answering", answering, len(sockets)), socketSub),
|
||||
card("Reverse DNS", "ti-arrows-exchange", "/server/health", ptr.Status, ptrValue, c["Hostname"].(string)),
|
||||
card("Reverse DNS", "ti-arrows-exchange", "/server/health", ptr.Status, ptrValue, c.Hostname),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +417,26 @@ func cardLevel(s health.Status) string {
|
||||
}
|
||||
}
|
||||
|
||||
// healthReadings are what Health and Overview are drawn from, read once per
|
||||
// page. A struct, not a map: a mistyped name is a compile error and not an
|
||||
// empty card (owner, 2026-10-13; the last of template-data-typing).
|
||||
type healthReadings struct {
|
||||
Processes []health.Process
|
||||
ProcessError bool
|
||||
ProcessStatus health.Status
|
||||
QueueSummary string
|
||||
QueueError string
|
||||
QueueStatus health.Status
|
||||
Machine health.Machine
|
||||
Cert health.Certificate
|
||||
Sockets []health.Socket
|
||||
SocketStatus health.Status
|
||||
Hostname string
|
||||
PTR dnscheck.Result
|
||||
}
|
||||
|
||||
// healthChecks runs every server check once.
|
||||
func (h *Handlers) healthChecks() map[string]any {
|
||||
func (h *Handlers) healthChecks() healthReadings {
|
||||
procs, procErr := health.Processes()
|
||||
procStatus := health.StatusUnknown
|
||||
if procErr != nil {
|
||||
@@ -452,19 +470,19 @@ func (h *Handlers) healthChecks() map[string]any {
|
||||
machine := h.machine.Sample()
|
||||
srv := h.dns.Server(h.cfg.Hostname, false)
|
||||
|
||||
return map[string]any{
|
||||
"Processes": procs,
|
||||
"ProcessError": procErr != nil,
|
||||
"ProcessStatus": procStatus,
|
||||
"QueueSummary": queueSummary(queueText),
|
||||
"QueueError": queueErr,
|
||||
"QueueStatus": queueStatus,
|
||||
"Machine": machine,
|
||||
"Cert": cert,
|
||||
"Sockets": sockets,
|
||||
"SocketStatus": socketStatus,
|
||||
"Hostname": h.cfg.Hostname,
|
||||
"PTR": srv.PTR,
|
||||
return healthReadings{
|
||||
Processes: procs,
|
||||
ProcessError: procErr != nil,
|
||||
ProcessStatus: procStatus,
|
||||
QueueSummary: queueSummary(queueText),
|
||||
QueueError: queueErr,
|
||||
QueueStatus: queueStatus,
|
||||
Machine: machine,
|
||||
Cert: cert,
|
||||
Sockets: sockets,
|
||||
SocketStatus: socketStatus,
|
||||
Hostname: h.cfg.Hostname,
|
||||
PTR: srv.PTR,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,26 +10,26 @@ import (
|
||||
|
||||
// checksFixture is one reading of every server check, shaped as healthChecks
|
||||
// returns it.
|
||||
func checksFixture() map[string]any {
|
||||
return map[string]any{
|
||||
"Processes": []health.Process{
|
||||
func checksFixture() healthReadings {
|
||||
return healthReadings{
|
||||
Processes: []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "FATAL", Status: health.StatusError},
|
||||
},
|
||||
"ProcessError": false,
|
||||
"ProcessStatus": health.StatusError,
|
||||
"QueueSummary": "Mail queue is empty",
|
||||
"QueueError": "",
|
||||
"QueueStatus": health.StatusOK,
|
||||
"Machine": health.Machine{Status: health.StatusUnknown},
|
||||
"Cert": health.Certificate{Subject: "mail.example.org", DaysLeft: 12, Status: health.StatusWarn, Detail: "Expires soon."},
|
||||
"Sockets": []health.Socket{
|
||||
ProcessError: false,
|
||||
ProcessStatus: health.StatusError,
|
||||
QueueSummary: "Mail queue is empty",
|
||||
QueueError: "",
|
||||
QueueStatus: health.StatusOK,
|
||||
Machine: health.Machine{Status: health.StatusUnknown},
|
||||
Cert: health.Certificate{Subject: "mail.example.org", DaysLeft: 12, Status: health.StatusWarn, Detail: "Expires soon."},
|
||||
Sockets: []health.Socket{
|
||||
{Name: "OpenDKIM", Present: true, Status: health.StatusOK},
|
||||
{Name: "send-log", Present: false, Status: health.StatusWarn},
|
||||
},
|
||||
"SocketStatus": health.StatusWarn,
|
||||
"Hostname": "mail.example.org",
|
||||
"PTR": dnscheck.Result{Status: health.StatusOK},
|
||||
SocketStatus: health.StatusWarn,
|
||||
Hostname: "mail.example.org",
|
||||
PTR: dnscheck.Result{Status: health.StatusOK},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ func TestHealthCards(t *testing.T) {
|
||||
|
||||
// A check that could not run must not look fine.
|
||||
c := checksFixture()
|
||||
c["ProcessError"] = true
|
||||
c["ProcessStatus"] = health.StatusUnknown
|
||||
c["PTR"] = dnscheck.Result{Status: health.StatusUnknown}
|
||||
c.ProcessError = true
|
||||
c.ProcessStatus = health.StatusUnknown
|
||||
c.PTR = dnscheck.Result{Status: health.StatusUnknown}
|
||||
cards = healthCards(c)
|
||||
if cards[1].Level != "warn" || cards[1].Value != "Could not be read" || cards[5].Level != "warn" || cards[5].Value != "Not checked" {
|
||||
t.Errorf("unverified checks render as %+v and %+v", cards[1], cards[5])
|
||||
@@ -86,18 +86,18 @@ func TestQueueCardValue(t *testing.T) {
|
||||
// reload helper idles between runs: it is neither running nor missing.
|
||||
func TestProcessesCardLeavesOutTheIdleOneShot(t *testing.T) {
|
||||
c := checksFixture()
|
||||
c["Processes"] = []health.Process{
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix-reload", State: "STOPPED", Status: health.StatusOK, OneShot: true},
|
||||
}
|
||||
c["ProcessStatus"] = health.StatusOK
|
||||
c.ProcessStatus = health.StatusOK
|
||||
card := healthCards(c)[1]
|
||||
if card.Value != "2 of 2 running" || card.Sub != "all programs" {
|
||||
t.Errorf("card = %q / %q, want 2 of 2 running / all programs", card.Value, card.Sub)
|
||||
}
|
||||
|
||||
c["Processes"] = []health.Process{
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix-reload", State: "FATAL", Status: health.StatusError, OneShot: true},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user