Bound the certificate check to 3 s a handshake and 7 s, and answer waits while printer is not given (issue 1510)

This commit is contained in:
2026-10-21 01:58:50 +02:00
parent 95c47a09c9
commit 65e67abb7d
4 changed files with 85 additions and 12 deletions
+38 -6
View File
@@ -34,6 +34,13 @@ var keyKinds = []struct {
}},
}
// certDial bounds one handshake and certTotal the whole check (two handshakes), so the node-engine's look, which has
// ten seconds, is always answered (issue 1510).
var (
certDial = 3 * time.Second
certTotal = 7 * time.Second
)
// Served is a certificate the printer served to a handshake of one key type.
type Served struct {
Key string `json:"key"`
@@ -45,9 +52,30 @@ type Served struct {
// CertHealth is brother_health's answer: healthy, why when it is not (or what could not be compared), and what the
// printer served.
type CertHealth struct {
Healthy bool `json:"healthy"`
Why string `json:"why"`
Served []Served `json:"served"`
Healthy bool `json:"healthy"`
State string `json:"state"` // healthy, unhealthy or waits (ADR 0283)
Why string `json:"why"`
// Waits names the config values the operator has yet to give, when State is waits.
Waits []map[string]string `json:"waits,omitempty"`
Served []Served `json:"served"`
}
// CertWaits is the answer while printer or printer-certificate is not given: the module waits for the operator, which
// is not a fault (ADR 0283), and names the setting.
func CertWaits(s Settings) (CertHealth, bool) {
var waits []map[string]string
var names []string
for _, c := range []struct{ name, value string }{{"printer", s.Printer}, {"printer-certificate", s.PrinterCertificate}} {
if c.value == "" {
waits = append(waits, map[string]string{"config": c.name})
names = append(names, c.name)
}
}
if len(waits) == 0 {
return CertHealth{}, false
}
return CertHealth{Healthy: true, State: "waits", Waits: waits, Served: []Served{},
Why: "waits for the operator to give the config value " + strings.Join(names, " and ")}, true
}
// serveAddr is the host with the HTTPS port when it names none.
@@ -60,7 +88,9 @@ func serveAddr(host string) string {
// served is the leaf's fingerprint the printer gives a handshake that offers only these suites, or why it gave none.
func served(ctx context.Context, addr string, suites []uint16) (string, error) {
d := tls.Dialer{NetDialer: &net.Dialer{Timeout: 5 * time.Second}, Config: &tls.Config{
ctx, cancel := context.WithTimeout(ctx, certDial)
defer cancel()
d := tls.Dialer{NetDialer: &net.Dialer{Timeout: certDial}, Config: &tls.Config{
MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12, CipherSuites: suites,
// Nothing is sent over this connection, and the certificate is only read, to be compared with the pins.
InsecureSkipVerify: true,
@@ -79,9 +109,11 @@ func served(ctx context.Context, addr string, suites []uint16) (string, error) {
// CheckCertificates compares what the printer serves with the pins. The login is never used.
func CheckCertificates(ctx context.Context, host, pins string) CertHealth {
ctx, cancel := context.WithTimeout(ctx, certTotal)
defer cancel()
want := pinList(pins)
addr := serveAddr(host)
h := CertHealth{Healthy: true}
h := CertHealth{Healthy: true, State: "healthy"}
var changed, unreachable []string
for _, k := range keyKinds {
s := Served{Key: k.Name}
@@ -99,7 +131,7 @@ func CheckCertificates(ctx context.Context, host, pins string) CertHealth {
}
switch {
case len(changed) > 0:
h.Healthy = false
h.Healthy, h.State = false, "unhealthy"
h.Why = fmt.Sprintf("the printer serves a certificate that is not pinned: %s; the pinned printer-certificate is %s. If the printer was reset or given new certificates, set printer-certificate to the new fingerprint(s) (with the old ones kept if they still serve)",
strings.Join(changed, "; "), strings.Join(want, ", "))
case len(unreachable) == len(keyKinds):
+43 -2
View File
@@ -103,7 +103,48 @@ func TestBrotherHealthIsAToolThatReadsTheSettings(t *testing.T) {
if h := got.(CertHealth); h.Healthy {
t.Errorf("a wrong pin is healthy: %+v", h)
}
if _, err := tool(t, newRig(t, `{}`, "pw").tl, "brother_health").Run(map[string]any{}); err == nil {
t.Error("an unset printer is healthy")
}
// 1510: with the printer not given the answer is waits, naming the setting (ADR 0283), not an error; and so is
// the pin.
func TestHealthWaitsForTheSettingsTheOperatorHasYetToGive(t *testing.T) {
got, err := tool(t, newRig(t, `{}`, "pw").tl, "brother_health").Run(map[string]any{})
h, ok := got.(CertHealth)
if err != nil || !ok || !h.Healthy || h.State != "waits" || len(h.Waits) != 2 || h.Waits[0]["config"] != "printer" || h.Waits[1]["config"] != "printer-certificate" {
t.Errorf("answer = %+v, %v", got, err)
}
got, _ = tool(t, newRig(t, `{"printer":"192.0.2.10"}`, "pw").tl, "brother_health").Run(map[string]any{})
if h := got.(CertHealth); h.State != "waits" || len(h.Waits) != 1 || h.Waits[0]["config"] != "printer-certificate" {
t.Errorf("no pin: %+v", h)
}
}
// 1510: each handshake is bounded by 3 s and the whole check by 7 s, and a printer that accepts and never answers
// ends the check at the bound with the module healthy (nothing compared).
func TestTheCheckIsBoundedAndAHungPrinterEndsItAtTheBound(t *testing.T) {
if certDial != 3*time.Second || certTotal != 7*time.Second {
t.Fatalf("bounds = %s and %s, want 3 s and 7 s", certDial, certTotal)
}
ln, _ := net.Listen("tcp", "127.0.0.1:0")
defer ln.Close()
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
defer c.Close() // held open, never answered
}
}()
oldD, oldT := certDial, certTotal
certDial, certTotal = 200*time.Millisecond, 500*time.Millisecond
t.Cleanup(func() { certDial, certTotal = oldD, oldT })
start := time.Now()
h := CheckCertificates(context.Background(), ln.Addr().String(), strings.Repeat("AB:", 31)+"AB")
if took := time.Since(start); took > 700*time.Millisecond {
t.Errorf("the check took %s", took)
}
if !h.Healthy || !strings.Contains(h.Why, "none was compared") {
t.Errorf("%+v", h)
}
}
+3 -3
View File
@@ -339,10 +339,10 @@ func tools(settingsFile string, now func() time.Time) []stdio.Tool {
}
}()
}
if err := s.Settings.MissingPrinter(); err != nil {
return nil, err
if w, waits := CertWaits(s.Settings); waits {
return w, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 9*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), certTotal+time.Second)
defer cancel()
return CheckCertificates(ctx, s.Settings.Printer, s.Settings.PrinterCertificate), nil
},