1 Commits
Author SHA1 Message Date
sneak 48684f9e69 watcher: keep port state when no nameserver of a name answered (closes #193)
check / check (push) Failing after 2m13s
The port check removed the saved port state of every address no
configured name resolves to. A name whose nameservers all timed out
or failed is saved with no records, so its addresses looked gone and
their port state was removed; when the nameservers answered again the
port state was recorded afresh, and a port that opened or closed in
the meantime was not notified.

The removal now keeps an entry when one of the names saved on it is
configured and none of its nameservers answered on its last check. A
name whose nameservers answer with no addresses still loses it, and so
does a name no longer configured, whose saved state is never checked
again.

Model: opus-5-5
2026-10-01 22:04:12 +00:00
2 changed files with 14 additions and 85 deletions
+13 -69
View File
@@ -333,9 +333,9 @@ func TestNameserverThatRefuses(t *testing.T) {
}
// TestPortStateWhenNoNameserverAnswered runs the port checks on
// hostname state built here, which gives the name no address. The port
// state saved for its old address is kept only when the name is a
// configured hostname or domain and none of its nameservers answered.
// hostname state built here, which gives the hostname no address. The
// port state saved for its old address is kept only when the hostname
// is configured and none of its nameservers answered.
func TestPortStateWhenNoNameserverAnswered(t *testing.T) {
t.Parallel()
@@ -346,25 +346,15 @@ func TestPortStateWhenNoNameserverAnswered(t *testing.T) {
nsA: answered(map[string][]string{}), nsB: failed(),
})
configured := []string{host}
tests := []struct {
name string
hostname *state.HostnameState
hostnames []string
domains []string
wantKept bool
name string
hostname *state.HostnameState
configured bool
wantKept bool
}{
{"no nameserver answered", noneAnswered, configured, nil, true},
{
"no nameserver answered, configured as a domain",
noneAnswered, nil, configured, true,
},
{
"one answered with no address",
oneAnsweredNoAddress, configured, nil, false,
},
{"no nameserver answered, not configured", noneAnswered, nil, nil, false},
{"no nameserver answered", noneAnswered, true, true},
{"one answered with no address", oneAnsweredNoAddress, true, false},
{"no nameserver answered, not configured", noneAnswered, false, false},
}
for _, tt := range tests {
@@ -372,8 +362,9 @@ func TestPortStateWhenNoNameserverAnswered(t *testing.T) {
t.Parallel()
cfg := defaultTestConfig(t)
cfg.Hostnames = tt.hostnames
cfg.Domains = tt.domains
if tt.configured {
cfg.Hostnames = []string{host}
}
// The port checks read the saved hostname state and look
// nothing up, so the watcher has no resolver.
@@ -399,50 +390,3 @@ func TestPortStateWhenNoNameserverAnswered(t *testing.T) {
})
}
}
// TestPortStateWhenNoNameserverAnsweredAndOtherNameMovesAway saves the
// port state of an address two configured hostnames resolve to. While
// none of the first one's nameservers answer, the port checks run with
// the other one still at that address, then after it moved away; the
// port state is kept both times.
func TestPortStateWhenNoNameserverAnsweredAndOtherNameMovesAway(
t *testing.T,
) {
t.Parallel()
const other = "mail.example.net"
cfg := defaultTestConfig(t)
cfg.Hostnames = []string{host, other}
// The port checks read the saved hostname state and look nothing
// up, so the watcher has no resolver.
deps := newTestDeps(t, cfg)
w := watcher.NewForTest(
cfg, deps.state, nil,
deps.portChecker, deps.tlsChecker, deps.notifier,
)
key := ip1 + ":443"
deps.state.SetPortState(key, &state.PortState{
Open: true, Hostnames: []string{host, other},
})
deps.state.SetHostnameState(host, saved(
map[string]*state.NameserverRecordState{nsA: failed(), nsB: failed()},
))
for _, otherIP := range []string{ip1, ip2} {
deps.state.SetHostnameState(other, saved(
map[string]*state.NameserverRecordState{
nsA: answered(map[string][]string{"A": {otherIP}}),
},
))
w.CheckAllPorts(t.Context())
if _, kept := deps.state.GetPortState(key); !kept {
t.Fatalf("port state %s removed with %s at %s", key, other, otherIP)
}
}
}
+1 -16
View File
@@ -828,24 +828,9 @@ func (w *Watcher) checkSinglePort(
)
}
// A configured name on the saved list none of whose nameservers
// answered stays on it, so the entry is kept when the other names
// stop resolving to this address.
savedHostnames := slices.Clone(hostnames)
if hasPrev {
for _, name := range prev.Hostnames {
if !slices.Contains(hostnames, name) && w.noNameserverAnswered(name) {
savedHostnames = append(savedHostnames, name)
}
}
sort.Strings(savedHostnames)
}
w.state.SetPortState(key, &state.PortState{
Open: result.Open,
Hostnames: savedHostnames,
Hostnames: hostnames,
LastChecked: now,
})
}