watcher: a port check that finds 443 closed removes its certificate entries (closes #265)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The TLS check runs only on an open port 443, and the certificate cleanup removed an entry only when its name stopped resolving to the address, so an entry stayed ok for as long as its port was closed. The cleanup after the port checks now also removes the entries of an address whose port 443 was found closed. Nothing is notified for the removal; the Port Change already says the port closed. When the port opens again, the next TLS check saves the certificate as new, with no change or recovery notification. The README says both. Judgement call: the entry goes even while none of the name's nameservers answer, since the port check reached the address itself. Model: opus-5-5
This commit was merged in pull request #280.
This commit is contained in:
@@ -223,8 +223,14 @@ notification endpoint set, changes show only on the dashboard; see
|
|||||||
- Certificate CN, issuer, or SANs changed (replacement detected, reports old
|
- Certificate CN, issuer, or SANs changed (replacement detected, reports old
|
||||||
and new CN and issuer).
|
and new CN and issuer).
|
||||||
- TLS connection failure to a previously-reachable IP:443 (handshake error,
|
- TLS connection failure to a previously-reachable IP:443 (handshake error,
|
||||||
timeout, connection refused after previously succeeding).
|
timeout, or connection refused when port 443 closed after the port check
|
||||||
|
that found it open).
|
||||||
- TLS recovery: a previously-failing IP:443 now completes a handshake again.
|
- TLS recovery: a previously-failing IP:443 now completes a handshake again.
|
||||||
|
- A port check that finds port 443 closed removes the certificate entries for
|
||||||
|
that IP address, without a notification of its own: the port change
|
||||||
|
notification reports the closed port. When the port opens again, the next TLS
|
||||||
|
check saves the certificate as new, without a certificate change or recovery
|
||||||
|
notification.
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
@@ -294,8 +300,10 @@ clears them.
|
|||||||
port entry left with no name goes, so the dashboard and `/api/v1/status` no
|
port entry left with no name goes, so the dashboard and `/api/v1/status` no
|
||||||
longer list or count it. The first check's port checks remove the port entries
|
longer list or count it. The first check's port checks remove the port entries
|
||||||
of addresses no configured name has.
|
of addresses no configured name has.
|
||||||
- Each port check also removes the certificate entries for an address their name
|
- Each port check also removes, without a notification, the certificate entries
|
||||||
no longer resolves to, except while none of the name's nameservers answer.
|
for an address whose port 443 it found closed, and those for an address their
|
||||||
|
name no longer resolves to, except while none of the name's nameservers
|
||||||
|
answer.
|
||||||
|
|
||||||
### Web Dashboard
|
### Web Dashboard
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-13: a port check that finds port 443 closed removes that address's
|
||||||
|
certificate entries, so none is left shown as `ok` (closes #265).
|
||||||
- 2026-10-13: a state file holding `{}`, or leaving out a section, loads with
|
- 2026-10-13: a state file holding `{}`, or leaving out a section, loads with
|
||||||
that section empty; the first check no longer crashes on it (closes #267).
|
that section empty; the first check no longer crashes on it (closes #267).
|
||||||
- 2026-10-13: a stop before the first scan completes sends no startup
|
- 2026-10-13: a stop before the first scan completes sends no startup
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package watcher_test
|
|||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/dnswatcher/internal/state"
|
"sneak.berlin/go/dnswatcher/internal/state"
|
||||||
@@ -222,3 +223,67 @@ func TestCertificateStateForAnAddressGone(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCertificateStateForAPortClosed runs the port checks on hostname
|
||||||
|
// and port state built here for a configured hostname at ip1, whose
|
||||||
|
// port 443 the previous check found open and whose certificate entry
|
||||||
|
// is ok. The port is closed now: the certificate entry is removed, and
|
||||||
|
// only the Port Change is notified. When the port opens again, the next
|
||||||
|
// TLS check saves the certificate as new and notifies nothing about it.
|
||||||
|
// Nothing is looked up.
|
||||||
|
func TestCertificateStateForAPortClosed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := defaultTestConfig(t)
|
||||||
|
cfg.Hostnames = []string{host}
|
||||||
|
|
||||||
|
deps := newTestDeps(t, cfg)
|
||||||
|
w := watcher.NewForTest(
|
||||||
|
cfg, deps.state, nil,
|
||||||
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
||||||
|
)
|
||||||
|
w.SetFirstRun(false)
|
||||||
|
|
||||||
|
certKey := ip1 + ":443:" + host
|
||||||
|
|
||||||
|
deps.state.SetHostnameState(host, saved(
|
||||||
|
map[string]*state.NameserverRecordState{
|
||||||
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
deps.state.SetPortState(ip1+":443", &state.PortState{
|
||||||
|
Open: true, Hostnames: []string{host},
|
||||||
|
})
|
||||||
|
deps.state.SetCertificateState(certKey, &state.CertificateState{
|
||||||
|
CommonName: host, Status: "ok",
|
||||||
|
})
|
||||||
|
deps.portChecker.closed = true
|
||||||
|
|
||||||
|
w.CheckAllPorts(t.Context())
|
||||||
|
|
||||||
|
if _, ok := deps.state.GetCertificateState(certKey); ok {
|
||||||
|
t.Errorf("certificate entry %s kept with port 443 closed", certKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
sent := deps.notifier.getNotifications()
|
||||||
|
if title := "Port Change: " + ip1 + ":443"; len(sent) != 1 ||
|
||||||
|
sent[0].Title != title {
|
||||||
|
t.Errorf("sent %v, want one %q", sent, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.portChecker.closed = false
|
||||||
|
|
||||||
|
w.CheckAllPorts(t.Context())
|
||||||
|
w.RunTLSChecks(t.Context())
|
||||||
|
|
||||||
|
cert, ok := deps.state.GetCertificateState(certKey)
|
||||||
|
if !ok || cert.Status != "ok" {
|
||||||
|
t.Errorf("certificate entry %s after port 443 opened: %+v", certKey, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range deps.notifier.getNotifications() {
|
||||||
|
if strings.HasPrefix(n.Title, "TLS") {
|
||||||
|
t.Errorf("sent %v, want no TLS notification", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -978,7 +978,8 @@ func (w *Watcher) checkAllPorts(ctx context.Context) {
|
|||||||
|
|
||||||
// Phase 3: Remove port state entries that no longer have
|
// Phase 3: Remove port state entries that no longer have
|
||||||
// any hostname referencing them, and certificate entries for
|
// any hostname referencing them, and certificate entries for
|
||||||
// an address their name no longer has.
|
// an address whose port 443 is closed or that their name no
|
||||||
|
// longer has.
|
||||||
w.cleanupStalePorts(associations)
|
w.cleanupStalePorts(associations)
|
||||||
w.cleanupStaleCertificates()
|
w.cleanupStaleCertificates()
|
||||||
}
|
}
|
||||||
@@ -1065,13 +1066,21 @@ func (w *Watcher) cleanupStalePorts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cleanupStaleCertificates removes the certificate entries for an
|
// cleanupStaleCertificates removes the certificate entries for an
|
||||||
// address their name no longer resolves to. An entry saved for a name
|
// address whose port 443 was found closed, and for an address their
|
||||||
// none of whose nameservers answered is kept: that name's addresses are
|
// name no longer resolves to. An entry saved for a name none of whose
|
||||||
// not known, not gone.
|
// nameservers answered is kept unless its port is closed: that name's
|
||||||
|
// addresses are not known, not gone.
|
||||||
func (w *Watcher) cleanupStaleCertificates() {
|
func (w *Watcher) cleanupStaleCertificates() {
|
||||||
for _, key := range w.state.GetAllCertificateKeys() {
|
for _, key := range w.state.GetAllCertificateKeys() {
|
||||||
ip, hostname := parseCertKey(key)
|
ip, hostname := parseCertKey(key)
|
||||||
|
|
||||||
|
ps, ok := w.state.GetPortState(fmt.Sprintf("%s:%d", ip, tlsPort))
|
||||||
|
if ok && !ps.Open {
|
||||||
|
w.state.DeleteCertificateState(key)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if slices.Contains(w.collectIPs(hostname), ip) ||
|
if slices.Contains(w.collectIPs(hostname), ip) ||
|
||||||
w.noNameserverAnswered(hostname) {
|
w.noNameserverAnswered(hostname) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user