watcher: a name removed from the targets leaves the state (closes #223)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
At startup, before the first check, Run removes from the loaded state the domain, hostname and certificate entries of names no longer in DNSWATCHER_TARGETS, takes those names off each port entry's list of names and removes a port entry left with none, so the dashboard, /api/v1/status and the startup notification count only configured names. A configured domain's own records, saved as a hostname entry under its name, are kept. Nothing is notified. Each port check, next to the removal of stale port entries, now also removes the certificate entries for an address a name no longer resolves to, except while none of its nameservers answered, as port entries already were. Model: opus-5-5
This commit was merged in pull request #246.
This commit is contained in:
@@ -245,6 +245,14 @@ clears them.
|
|||||||
false-positive change notifications.
|
false-positive change notifications.
|
||||||
- State is written atomically (write to temp file, then rename) to prevent
|
- State is written atomically (write to temp file, then rename) to prevent
|
||||||
corruption.
|
corruption.
|
||||||
|
- A name removed from `DNSWATCHER_TARGETS` is removed from the state at startup,
|
||||||
|
before the first check, without a notification: its domain, hostname and
|
||||||
|
certificate entries go, it is taken off each port entry's list of names, and a
|
||||||
|
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
|
||||||
|
of addresses no configured name has.
|
||||||
|
- Each port check also removes the certificate entries for an address their name
|
||||||
|
no longer resolves to, except while none of the name's nameservers answer.
|
||||||
|
|
||||||
### Web Dashboard
|
### Web Dashboard
|
||||||
|
|
||||||
@@ -734,7 +742,8 @@ docker run -d \
|
|||||||
1. **Startup**: Check that the data directory can be written, and exit with an
|
1. **Startup**: Check that the data directory can be written, and exit with an
|
||||||
error naming it if not. Load state from disk. If no state file exists, start
|
error naming it if not. Load state from disk. If no state file exists, start
|
||||||
with empty state (first check will establish baseline without triggering
|
with empty state (first check will establish baseline without triggering
|
||||||
change notifications).
|
change notifications). Remove from the state the names no longer in
|
||||||
|
`DNSWATCHER_TARGETS` (see State Management).
|
||||||
2. **Initial check**: Immediately perform all DNS, port, and TLS checks on
|
2. **Initial check**: Immediately perform all DNS, port, and TLS checks on
|
||||||
startup.
|
startup.
|
||||||
3. **Periodic checks** (DNS always runs first):
|
3. **Periodic checks** (DNS always runs first):
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-02: a name removed from `DNSWATCHER_TARGETS` leaves the state, and so
|
||||||
|
the dashboard and API, at startup, before the first check (closes #223).
|
||||||
- 2026-10-02: a record type whose query to a nameserver fails keeps its previous
|
- 2026-10-02: a record type whose query to a nameserver fails keeps its previous
|
||||||
records and alerts nothing; the other types are still saved (closes #231).
|
records and alerts nothing; the other types are still saved (closes #231).
|
||||||
- 2026-10-02: a Port Change notification lists the port's domains on a
|
- 2026-10-02: a Port Change notification lists the port's domains on a
|
||||||
|
|||||||
@@ -298,6 +298,27 @@ func (s *State) GetDomainState(
|
|||||||
return ds, ok
|
return ds, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteDomainState removes a domain state entry.
|
||||||
|
func (s *State) DeleteDomainState(domain string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.snapshot.Domains, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDomainNames returns the names of all domain state entries.
|
||||||
|
func (s *State) GetAllDomainNames() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
names := make([]string, 0, len(s.snapshot.Domains))
|
||||||
|
for name := range s.snapshot.Domains {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
// SetHostnameState updates the state for a hostname.
|
// SetHostnameState updates the state for a hostname.
|
||||||
func (s *State) SetHostnameState(
|
func (s *State) SetHostnameState(
|
||||||
hostname string,
|
hostname string,
|
||||||
@@ -321,6 +342,28 @@ func (s *State) GetHostnameState(
|
|||||||
return hs, ok
|
return hs, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteHostnameState removes a hostname state entry.
|
||||||
|
func (s *State) DeleteHostnameState(hostname string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.snapshot.Hostnames, hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllHostnames returns the names of all hostname state entries,
|
||||||
|
// which include each apex domain's own records.
|
||||||
|
func (s *State) GetAllHostnames() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
names := make([]string, 0, len(s.snapshot.Hostnames))
|
||||||
|
for name := range s.snapshot.Hostnames {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
// SetPortState updates the state for a port.
|
// SetPortState updates the state for a port.
|
||||||
func (s *State) SetPortState(key string, ps *PortState) {
|
func (s *State) SetPortState(key string, ps *PortState) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -383,6 +426,27 @@ func (s *State) GetCertificateState(
|
|||||||
return cs, ok
|
return cs, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteCertificateState removes a certificate state entry.
|
||||||
|
func (s *State) DeleteCertificateState(key string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
delete(s.snapshot.Certificates, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCertificateKeys returns all certificate state keys.
|
||||||
|
func (s *State) GetAllCertificateKeys() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(s.snapshot.Certificates))
|
||||||
|
for k := range s.snapshot.Certificates {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
// checkDataDirWritable creates the data directory if needed, then writes
|
// checkDataDirWritable creates the data directory if needed, then writes
|
||||||
// and removes the temp file that Save uses. It runs at startup so that an
|
// and removes the temp file that Save uses. It runs at startup so that an
|
||||||
// unwritable directory stops the process, instead of the process running
|
// unwritable directory stops the process, instead of the process running
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ func (w *Watcher) MaybeSendTestNotification(ctx context.Context) {
|
|||||||
w.maybeSendTestNotification(ctx)
|
w.maybeSendTestNotification(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CleanupRemovedTargets exports cleanupRemovedTargets for testing.
|
||||||
|
func (w *Watcher) CleanupRemovedTargets() {
|
||||||
|
w.cleanupRemovedTargets()
|
||||||
|
}
|
||||||
|
|
||||||
// CheckAllPorts exports checkAllPorts for testing.
|
// CheckAllPorts exports checkAllPorts for testing.
|
||||||
func (w *Watcher) CheckAllPorts(ctx context.Context) {
|
func (w *Watcher) CheckAllPorts(ctx context.Context) {
|
||||||
w.checkAllPorts(ctx)
|
w.checkAllPorts(ctx)
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package watcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/state"
|
||||||
|
"sneak.berlin/go/dnswatcher/internal/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRemovedTargetsLeaveTheState loads a state saved while a domain
|
||||||
|
// and a hostname now removed from the configuration were still in it,
|
||||||
|
// and runs the removal that Run does before the first check. The
|
||||||
|
// removed names' domain, hostname and certificate entries are gone,
|
||||||
|
// the configured names' are kept, and nothing is notified. Nothing is
|
||||||
|
// looked up: the watcher has no resolver.
|
||||||
|
func TestRemovedTargetsLeaveTheState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
removedDomain = "example.com"
|
||||||
|
removedHost = "www.example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg := defaultTestConfig(t)
|
||||||
|
cfg.Domains = []string{domain}
|
||||||
|
cfg.Hostnames = []string{host}
|
||||||
|
|
||||||
|
deps := newTestDeps(t, cfg)
|
||||||
|
w := watcher.NewForTest(
|
||||||
|
cfg, deps.state, nil,
|
||||||
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The state a check of all four names saves, each name at ip1.
|
||||||
|
for _, name := range []string{domain, removedDomain} {
|
||||||
|
deps.state.SetDomainState(name, &state.DomainState{
|
||||||
|
Nameservers: []string{nsA},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{domain, host, removedDomain, removedHost} {
|
||||||
|
deps.state.SetHostnameState(name, saved(
|
||||||
|
map[string]*state.NameserverRecordState{
|
||||||
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
deps.state.SetCertificateState(
|
||||||
|
ip1+":443:"+name, &state.CertificateState{Status: "ok"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := deps.state.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("saving the state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = deps.state.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loading the state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.CleanupRemovedTargets()
|
||||||
|
|
||||||
|
snap := deps.state.GetSnapshot()
|
||||||
|
|
||||||
|
got := slices.Sorted(maps.Keys(snap.Domains))
|
||||||
|
if want := []string{domain}; !slices.Equal(got, want) {
|
||||||
|
t.Errorf("domain entries %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = slices.Sorted(maps.Keys(snap.Hostnames))
|
||||||
|
if want := []string{domain, host}; !slices.Equal(got, want) {
|
||||||
|
t.Errorf("hostname entries %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = slices.Sorted(maps.Keys(snap.Certificates))
|
||||||
|
if want := []string{
|
||||||
|
ip1 + ":443:" + domain, ip1 + ":443:" + host,
|
||||||
|
}; !slices.Equal(got, want) {
|
||||||
|
t.Errorf("certificate entries %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
||||||
|
t.Errorf("sent %v, want nothing", sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRemovedTargetsLeaveThePortEntries loads a state whose port
|
||||||
|
// entries name a domain and a hostname now removed from the
|
||||||
|
// configuration, and runs the removal that Run does before the first
|
||||||
|
// check. The removed names are off each port entry's list of names, the
|
||||||
|
// entry only they had is gone, the entry that also names configured
|
||||||
|
// names is kept for the port checks, and nothing is notified.
|
||||||
|
func TestRemovedTargetsLeaveThePortEntries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
removedDomain = "example.com"
|
||||||
|
removedHost = "www.example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg := defaultTestConfig(t)
|
||||||
|
cfg.Domains = []string{domain}
|
||||||
|
cfg.Hostnames = []string{host}
|
||||||
|
|
||||||
|
deps := newTestDeps(t, cfg)
|
||||||
|
w := watcher.NewForTest(
|
||||||
|
cfg, deps.state, nil,
|
||||||
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The port 443 entries a check of all four names saves: each name
|
||||||
|
// at ip1, except the removed hostname, at ip2.
|
||||||
|
deps.state.SetPortState(ip1+":443", &state.PortState{
|
||||||
|
Open: true, Hostnames: []string{removedDomain, domain, host},
|
||||||
|
})
|
||||||
|
deps.state.SetPortState(ip2+":443", &state.PortState{
|
||||||
|
Open: true, Hostnames: []string{removedHost},
|
||||||
|
})
|
||||||
|
|
||||||
|
err := deps.state.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("saving the state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = deps.state.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loading the state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.CleanupRemovedTargets()
|
||||||
|
|
||||||
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
||||||
|
t.Errorf("sent %v, want nothing", sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := deps.state.GetSnapshot()
|
||||||
|
|
||||||
|
got := slices.Sorted(maps.Keys(snap.Ports))
|
||||||
|
if want := []string{ip1 + ":443"}; !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("port entries %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = snap.Ports[ip1+":443"].Hostnames
|
||||||
|
if want := []string{domain, host}; !slices.Equal(got, want) {
|
||||||
|
t.Errorf("names of port entry %s:443 %v, want %v", ip1, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCertificateStateForAnAddressGone runs the port checks on hostname
|
||||||
|
// state built here for a configured hostname, with certificate entries
|
||||||
|
// saved for it at ip1, ip2 and an IPv6 address. When its nameservers
|
||||||
|
// answered with ip1 and the IPv6 address, the entry for ip2 is removed.
|
||||||
|
// When none of them answered, its addresses are not known, and every
|
||||||
|
// entry is kept. Nothing is notified, and nothing is looked up.
|
||||||
|
func TestCertificateStateForAnAddressGone(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const ip6 = "2001:db8::1"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hostname *state.HostnameState
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"answered without ip2",
|
||||||
|
saved(map[string]*state.NameserverRecordState{
|
||||||
|
nsA: answered(map[string][]string{
|
||||||
|
"A": {ip1}, "AAAA": {ip6},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[]string{ip1 + ":443:" + host, ip6 + ":443:" + host},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no nameserver answered",
|
||||||
|
saved(map[string]*state.NameserverRecordState{
|
||||||
|
nsA: failed(), nsB: failed(),
|
||||||
|
}),
|
||||||
|
[]string{
|
||||||
|
ip1 + ":443:" + host,
|
||||||
|
ip2 + ":443:" + host,
|
||||||
|
ip6 + ":443:" + host,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(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)
|
||||||
|
|
||||||
|
deps.state.SetHostnameState(host, tt.hostname)
|
||||||
|
|
||||||
|
for _, ip := range []string{ip1, ip2, ip6} {
|
||||||
|
deps.state.SetCertificateState(
|
||||||
|
ip+":443:"+host, &state.CertificateState{Status: "ok"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.CheckAllPorts(t.Context())
|
||||||
|
|
||||||
|
got := slices.Sorted(maps.Keys(deps.state.GetSnapshot().Certificates))
|
||||||
|
if !slices.Equal(got, tt.want) {
|
||||||
|
t.Errorf("certificate entries %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
||||||
|
t.Errorf("sent %v, want nothing", sent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,7 @@ func (w *Watcher) Run(ctx context.Context) {
|
|||||||
"tlsInterval", w.config.TLSInterval.String(),
|
"tlsInterval", w.config.TLSInterval.String(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
w.cleanupRemovedTargets()
|
||||||
w.RunOnce(ctx)
|
w.RunOnce(ctx)
|
||||||
w.maybeSendTestNotification(ctx)
|
w.maybeSendTestNotification(ctx)
|
||||||
|
|
||||||
@@ -201,6 +202,59 @@ func (w *Watcher) detectFirstRun() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupRemovedTargets removes from the loaded state the domain,
|
||||||
|
// hostname and certificate entries of names no longer in the
|
||||||
|
// configuration, which changes only at a restart, and takes those names
|
||||||
|
// off each port entry's list of names, removing a port entry left with
|
||||||
|
// none. Nothing is notified. A configured domain's own records are
|
||||||
|
// saved as a hostname entry under its name, which is kept.
|
||||||
|
func (w *Watcher) cleanupRemovedTargets() {
|
||||||
|
for _, name := range w.state.GetAllDomainNames() {
|
||||||
|
if !w.isDomain(name) {
|
||||||
|
w.state.DeleteDomainState(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range w.state.GetAllHostnames() {
|
||||||
|
if !w.isConfigured(name) {
|
||||||
|
w.state.DeleteHostnameState(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range w.state.GetAllCertificateKeys() {
|
||||||
|
if _, hostname := parseCertKey(key); !w.isConfigured(hostname) {
|
||||||
|
w.state.DeleteCertificateState(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range w.state.GetAllPortKeys() {
|
||||||
|
ps, ok := w.state.GetPortState(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
for _, name := range ps.Hostnames {
|
||||||
|
if w.isConfigured(name) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
w.state.DeletePortState(key)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
w.state.SetPortState(key, &state.PortState{
|
||||||
|
Open: ps.Open,
|
||||||
|
Hostnames: names,
|
||||||
|
LastChecked: ps.LastChecked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runDNSChecks performs DNS resolution for all configured domains
|
// runDNSChecks performs DNS resolution for all configured domains
|
||||||
// and hostnames, updating state with freshly resolved records.
|
// and hostnames, updating state with freshly resolved records.
|
||||||
// This must complete before port or TLS checks run so those
|
// This must complete before port or TLS checks run so those
|
||||||
@@ -861,8 +915,10 @@ 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.
|
// any hostname referencing them, and certificate entries for
|
||||||
|
// an address their name no longer has.
|
||||||
w.cleanupStalePorts(associations)
|
w.cleanupStalePorts(associations)
|
||||||
|
w.cleanupStaleCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPortAssociations constructs a map from IP:port keys to
|
// buildPortAssociations constructs a map from IP:port keys to
|
||||||
@@ -946,11 +1002,45 @@ func (w *Watcher) cleanupStalePorts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupStaleCertificates removes the certificate entries for an
|
||||||
|
// address their name no longer resolves to. An entry saved for a name
|
||||||
|
// none of whose nameservers answered is kept: that name's addresses are
|
||||||
|
// not known, not gone.
|
||||||
|
func (w *Watcher) cleanupStaleCertificates() {
|
||||||
|
for _, key := range w.state.GetAllCertificateKeys() {
|
||||||
|
ip, hostname := parseCertKey(key)
|
||||||
|
|
||||||
|
if slices.Contains(w.collectIPs(hostname), ip) ||
|
||||||
|
w.noNameserverAnswered(hostname) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
w.state.DeleteCertificateState(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCertKey splits an "ip:port:hostname" certificate key into its
|
||||||
|
// address and hostname.
|
||||||
|
func parseCertKey(key string) (string, string) {
|
||||||
|
lastColon := strings.LastIndex(key, ":")
|
||||||
|
if lastColon < 0 {
|
||||||
|
return "", key
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, _ := parsePortKey(key[:lastColon])
|
||||||
|
|
||||||
|
return ip, key[lastColon+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isConfigured reports whether name is a configured domain or hostname.
|
||||||
|
func (w *Watcher) isConfigured(name string) bool {
|
||||||
|
return w.isDomain(name) || slices.Contains(w.config.Hostnames, name)
|
||||||
|
}
|
||||||
|
|
||||||
// noNameserverAnswered reports whether name is a configured domain or
|
// noNameserverAnswered reports whether name is a configured domain or
|
||||||
// hostname and none of its nameservers answered on its last check.
|
// hostname and none of its nameservers answered on its last check.
|
||||||
func (w *Watcher) noNameserverAnswered(name string) bool {
|
func (w *Watcher) noNameserverAnswered(name string) bool {
|
||||||
if !slices.Contains(w.config.Hostnames, name) &&
|
if !w.isConfigured(name) {
|
||||||
!slices.Contains(w.config.Domains, name) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user