watcher: save and notify nothing for a cut-short port or TLS check (closes #185) #188
@@ -618,7 +618,8 @@ repository's `Dockerfile` and runs it. The app needs:
|
|||||||
abandoned, and the number abandoned is logged at warn level rather
|
abandoned, and the number abandoned is logged at warn level rather
|
||||||
than dropped silently. Notifications generated after shutdown has
|
than dropped silently. Notifications generated after shutdown has
|
||||||
begun are refused and logged, so a late burst cannot extend the
|
begun are refused and logged, so a late burst cannot extend the
|
||||||
shutdown.
|
shutdown. A DNS lookup, port check or TLS check that shutdown cuts
|
||||||
|
short saves nothing and sends no notification.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ nameserver IP address changes: https://git.eeqj.de/sneak/dnswatcher/issues/105
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: a port or TLS check that shutdown cuts short saves nothing and
|
||||||
|
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
||||||
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
||||||
is not a trusted proxy, not the first, which the client sets (closes #181).
|
is not a trusted proxy, not the first, which the client sets (closes #181).
|
||||||
- 2026-10-01: a nameserver that does not answer is saved as `error` with the
|
- 2026-10-01: a nameserver that does not answer is saved as `error` with the
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package watcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/state"
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/tlscheck"
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCancelledCheckSavesNothing runs a check with its context already
|
||||||
|
// cancelled, which is how the rest of a check runs once shutdown cuts it
|
||||||
|
// short. The real resolver drops the DNS lookup without sending a query,
|
||||||
|
// and the real port and TLS checkers fail without connecting. The port
|
||||||
|
// and certificate state the last check saved must stay as it was, and
|
||||||
|
// nothing may be notified.
|
||||||
|
func TestCancelledCheckSavesNothing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := defaultTestConfig(t)
|
||||||
|
cfg.Hostnames = []string{host}
|
||||||
|
|
||||||
|
// newTestWatcher's watcher has stand-in checkers. This one, on the
|
||||||
|
// same state and notifier, has the real ones.
|
||||||
|
_, deps := newTestWatcher(t, cfg)
|
||||||
|
w := watcher.NewForTest(
|
||||||
|
cfg,
|
||||||
|
deps.state,
|
||||||
|
resolver.NewFromLogger(slog.Default()),
|
||||||
|
portcheck.NewStandalone(),
|
||||||
|
tlscheck.NewStandalone(),
|
||||||
|
deps.notifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The last check found host at a local address, with both ports
|
||||||
|
// open and a good certificate.
|
||||||
|
const localIP = "127.0.0.1"
|
||||||
|
|
||||||
|
deps.state.SetHostnameState(host, hostnameState(
|
||||||
|
map[string]map[string][]string{nsA: {"A": {localIP}}},
|
||||||
|
))
|
||||||
|
|
||||||
|
ports := map[string]*state.PortState{
|
||||||
|
localIP + ":80": {Open: true, Hostnames: []string{host}},
|
||||||
|
localIP + ":443": {Open: true, Hostnames: []string{host}},
|
||||||
|
}
|
||||||
|
for key, ps := range ports {
|
||||||
|
deps.state.SetPortState(key, ps)
|
||||||
|
}
|
||||||
|
|
||||||
|
certKey := localIP + ":443:" + host
|
||||||
|
cert := &state.CertificateState{CommonName: host, Status: "ok"}
|
||||||
|
deps.state.SetCertificateState(certKey, cert)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
w.RunOnce(ctx)
|
||||||
|
|
||||||
|
for key, want := range ports {
|
||||||
|
got, _ := deps.state.GetPortState(key)
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("port %s saved as %+v, want %+v", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
got, _ := deps.state.GetCertificateState(certKey)
|
||||||
|
if !reflect.DeepEqual(got, cert) {
|
||||||
|
t.Errorf("certificate saved as %+v, want %+v", got, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications := deps.notifier.getNotifications()
|
||||||
|
if len(notifications) != 0 {
|
||||||
|
t.Errorf("sent %v, want no notifications", notifications)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -658,6 +658,13 @@ func (w *Watcher) checkSinglePort(
|
|||||||
hostnames []string,
|
hostnames []string,
|
||||||
) {
|
) {
|
||||||
result, err := w.portCheck.CheckPort(ctx, ip, port)
|
result, err := w.portCheck.CheckPort(ctx, ip, port)
|
||||||
|
|
||||||
|
// A check the context cut short says nothing about the port, so it
|
||||||
|
// is neither saved nor notified.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.log.Error(
|
w.log.Error(
|
||||||
"port check failed",
|
"port check failed",
|
||||||
@@ -733,6 +740,13 @@ func (w *Watcher) checkTLSCert(
|
|||||||
hostname string,
|
hostname string,
|
||||||
) {
|
) {
|
||||||
cert, err := w.tlsCheck.CheckCertificate(ctx, ip, hostname)
|
cert, err := w.tlsCheck.CheckCertificate(ctx, ip, hostname)
|
||||||
|
|
||||||
|
// A check the context cut short says nothing about the certificate,
|
||||||
|
// so it is neither saved nor notified.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
certKey := fmt.Sprintf("%s:%d:%s", ip, tlsPort, hostname)
|
certKey := fmt.Sprintf("%s:%d:%s", ip, tlsPort, hostname)
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
prev, hasPrev := w.state.GetCertificateState(certKey)
|
prev, hasPrev := w.state.GetCertificateState(certKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user