watcher: Port Change notifications list domains apart from hostnames (closes #248) #249
@@ -201,7 +201,9 @@ includes:
|
|||||||
- **NS recoveries**: Which nameserver recovered, which hostname/domain.
|
- **NS recoveries**: Which nameserver recovered, which hostname/domain.
|
||||||
- **NS inconsistencies**: Which nameservers disagree, what each one returned,
|
- **NS inconsistencies**: Which nameservers disagree, what each one returned,
|
||||||
which hostname or domain affected.
|
which hostname or domain affected.
|
||||||
- **Port changes**: Which IP:port, its new state, all associated hostnames.
|
- **Port changes**: Which IP:port, its new state, and the domains and the
|
||||||
|
hostnames that resolve to it, on a `Domains:` line and a `Hostnames:` line. A
|
||||||
|
line that would name nothing is left out.
|
||||||
- **TLS expiry warnings**: Expiry date and days remaining, CN, associated
|
- **TLS expiry warnings**: Expiry date and days remaining, CN, associated
|
||||||
hostname and IP.
|
hostname and IP.
|
||||||
- **TLS certificate changes**: Old and new CN and issuer, associated hostname
|
- **TLS certificate changes**: Old and new CN and issuer, associated hostname
|
||||||
|
|||||||
@@ -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 Port Change notification lists the port's domains on a
|
||||||
|
`Domains:` line and its hostnames on a `Hostnames:` line (closes #248).
|
||||||
- 2026-10-02: the dashboard's Ports table and `/api/v1/status` port entries list
|
- 2026-10-02: the dashboard's Ports table and `/api/v1/status` port entries list
|
||||||
a port's domains apart from its hostnames (closes #245).
|
a port's domains apart from its hostnames (closes #245).
|
||||||
- 2026-10-02: nameservers a referral names without addresses are looked up,
|
- 2026-10-02: nameservers a referral names without addresses are looked up,
|
||||||
|
|||||||
@@ -165,3 +165,51 @@ func TestStartupNotificationCountsConfiguredNames(t *testing.T) {
|
|||||||
t.Errorf("sent %v, want one message with %q", notifications, counts)
|
t.Errorf("sent %v, want one message with %q", notifications, counts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Port Change notification lists the configured apex domain and the
|
||||||
|
// hostname that resolve to the port's address on separate lines. The
|
||||||
|
// port checks read the saved hostname state and look nothing up, so the
|
||||||
|
// watcher has no resolver.
|
||||||
|
func TestPortChangeListsDomainsApartFromHostnames(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
w.SetFirstRun(false)
|
||||||
|
|
||||||
|
// Both names resolve to ip1, whose port 443 the previous check
|
||||||
|
// found open. It is closed now.
|
||||||
|
for _, name := range []string{domain, host} {
|
||||||
|
deps.state.SetHostnameState(name, saved(
|
||||||
|
map[string]*state.NameserverRecordState{
|
||||||
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ip1 + ":443"
|
||||||
|
deps.state.SetPortState(key, &state.PortState{
|
||||||
|
Open: true, Hostnames: []string{domain, host},
|
||||||
|
})
|
||||||
|
deps.portChecker.closed = true
|
||||||
|
|
||||||
|
w.CheckAllPorts(t.Context())
|
||||||
|
|
||||||
|
title := "Port Change: " + key
|
||||||
|
want := `Domains: example.net
|
||||||
|
Hostnames: www.example.net
|
||||||
|
Address: 192.0.2.1:443
|
||||||
|
Port now closed`
|
||||||
|
|
||||||
|
got := deps.notifier.getNotifications()
|
||||||
|
if len(got) != 1 || got[0].Title != title || got[0].Message != want {
|
||||||
|
t.Errorf("sent %v, want one %q with message:\n%s", got, title, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -542,17 +542,50 @@ func (w *Watcher) detectHostnameChanges(
|
|||||||
w.detectCNAMEAddressChanges(ctx, hostname, prev, current)
|
w.detectCNAMEAddressChanges(ctx, hostname, prev, current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDomain reports whether name is a configured apex domain, whose own
|
||||||
|
// records are checked and saved as a hostname's are.
|
||||||
|
func (w *Watcher) isDomain(name string) bool {
|
||||||
|
return slices.Contains(w.config.Domains, name)
|
||||||
|
}
|
||||||
|
|
||||||
// nameLine is the line a notification about name's records starts with:
|
// nameLine is the line a notification about name's records starts with:
|
||||||
// "Domain: " and the name for a configured apex domain, whose own
|
// "Domain: " and the name for a configured apex domain, and
|
||||||
// records are checked as a hostname's are, and "Hostname: " otherwise.
|
// "Hostname: " otherwise.
|
||||||
func (w *Watcher) nameLine(name string) string {
|
func (w *Watcher) nameLine(name string) string {
|
||||||
if slices.Contains(w.config.Domains, name) {
|
if w.isDomain(name) {
|
||||||
return "Domain: " + name
|
return "Domain: " + name
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Hostname: " + name
|
return "Hostname: " + name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// portNameLines lists the names that resolve to a port's address, the
|
||||||
|
// configured apex domains on one line and the hostnames on the next,
|
||||||
|
// leaving out a line that would name nothing.
|
||||||
|
func (w *Watcher) portNameLines(names []string) string {
|
||||||
|
var domains, hostnames []string
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
if w.isDomain(name) {
|
||||||
|
domains = append(domains, name)
|
||||||
|
} else {
|
||||||
|
hostnames = append(hostnames, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
if len(domains) > 0 {
|
||||||
|
lines = append(lines, "Domains: "+strings.Join(domains, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostnames) > 0 {
|
||||||
|
lines = append(lines, "Hostnames: "+strings.Join(hostnames, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// detectCNAMEAddressChanges notifies when the addresses at the end of
|
// detectCNAMEAddressChanges notifies when the addresses at the end of
|
||||||
// hostname's CNAME chain differ from those the previous check saved,
|
// hostname's CNAME chain differ from those the previous check saved,
|
||||||
// including a change from or to none. When the previous addresses are
|
// including a change from or to none. When the previous addresses are
|
||||||
@@ -952,8 +985,8 @@ func (w *Watcher) checkSinglePort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"Hosts: %s\nAddress: %s\nPort now %s",
|
"%s\nAddress: %s\nPort now %s",
|
||||||
strings.Join(hostnames, ", "), key, stateStr,
|
w.portNameLines(hostnames), key, stateStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
w.notify.SendNotification(
|
w.notify.SendNotification(
|
||||||
|
|||||||
Reference in New Issue
Block a user