Files
selfpost/internal/syslogrelay/syslogrelay_test.go
T
mixandClaude Opus 5.5 a39eda5a0b audit 5: findings of the two reviews
- The e2e's numeric-settings test ran a container that passed every gate
  and never ended: the good values are checked beside a bad one after
  them; each run is named, bounded to a minute and removed.
- The gates refuse a value under the parameter's minimum (a zero window
  kills anvil, a zero milter timeout smtpd) and one past an int, in
  seconds for a time value (src/global/conv_time.c, src/anvil/anvil.c).
- OpenDKIM waits, five seconds at most, for the relay's socket.
- The relay marks its lines `syslog: `, writes control characters out,
  reads into a queue at once and drops past 200 lines a second, saying
  how many (security F1, F2; a stuck relay would hold OpenDKIM in
  syslog(3)).
- smtp_tls_mandatory_protocols >=TLSv1.2 too.
- extract-cert.sh keeps the old files' group and sets its trap before
  mktemp.
- crashexit spares cert-reload; architecture.md "four times running" and
  what else exits the container; security.md the restart after five panel
  exits and the /dev/log socket.
- Measured against the technical review: smtpd has the selfpost group
  (Groups 104 998), so it did reach the supervisor socket before this
  stage.
Accepted, reasons in the plan: the logrotate state file in the postfix
user's directory, envdoc_test's reading of `${VAR:-}` only, crashexit's
window pruning untested.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:53:14 +03:00

111 lines
3.4 KiB
Go

package syslogrelay
import (
"bytes"
"net"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
)
func TestLine(t *testing.T) {
for in, want := range map[string]string{
"<22>Oct 15 09:00:00 opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)\n": "syslog: opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)",
"<19>Oct 5 09:00:00 opendkim[45]: key retrieval failed": "syslog: error: opendkim[45]: key retrieval failed",
"<20>opendkim[45]: no timestamp": "syslog: opendkim[45]: no timestamp",
"not syslog at all": "syslog: not syslog at all",
"<x>Oct 15 09:00:00 odd": "syslog: <x>Oct 15 09:00:00 odd",
// A line of its own cannot be made: a newline inside is written out.
"<14>opendkim[1]: one\npanel: forged": `syslog: opendkim[1]: one\x0apanel: forged`,
} {
if got := Line([]byte(in)); got != want {
t.Errorf("Line(%q) = %q, want %q", in, got, want)
}
}
}
// syncBuffer is a bytes.Buffer the relay and the test may share.
type syncBuffer struct {
mu sync.Mutex
b bytes.Buffer
}
func (s *syncBuffer) Write(p []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.b.Write(p)
}
func (s *syncBuffer) String() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.b.String()
}
// startRelay runs the relay on a socket of its own and returns a connection
// to it.
func startRelay(t *testing.T, w *syncBuffer) net.Conn {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("no unixgram sockets on Windows; the container is Linux")
}
path := filepath.Join(t.TempDir(), "log")
go Run(path, w)
var conn net.Conn
var err error
for i := 0; i < 50; i++ {
if conn, err = net.Dial("unixgram", path); err == nil {
t.Cleanup(func() { conn.Close() })
return conn
}
time.Sleep(20 * time.Millisecond)
}
t.Fatal(err)
return nil
}
// What a program sends to the socket comes out as a line.
func TestRunRelaysADatagram(t *testing.T) {
out := &syncBuffer{}
conn := startRelay(t, out)
if _, err := conn.Write([]byte("<19>Oct 15 09:00:00 opendkim[7]: can't load key")); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) && !strings.Contains(out.String(), "\n") {
time.Sleep(10 * time.Millisecond)
}
if got := out.String(); got != "syslog: error: opendkim[7]: can't load key\n" {
t.Errorf("relayed %q", got)
}
}
// A flood is cut at the rate, and what was dropped is said.
func TestRunDropsAFlood(t *testing.T) {
out := &syncBuffer{}
conn := startRelay(t, out)
for i := 0; i < ratePerSecond+100; i++ {
if _, err := conn.Write([]byte("<14>flood")); err != nil {
t.Fatal(err)
}
}
time.Sleep(1100 * time.Millisecond)
if _, err := conn.Write([]byte("<14>after")); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) && !strings.Contains(out.String(), "syslog: after") {
time.Sleep(10 * time.Millisecond)
}
got := out.String()
if n := strings.Count(got, "syslog: flood\n"); n != ratePerSecond {
t.Errorf("%d flood lines written, want %d", n, ratePerSecond)
}
if !strings.Contains(got, "syslog: relay: dropped 100 lines") {
t.Errorf("the drop was not said:\n%s", got)
}
}