From 56c4395a394c48b42eea5ebf6e8e49127d9f29b4 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 02:56:11 +0200 Subject: [PATCH] config: watch a target listed twice only once (closes #207) ClassifyTargets kept a name every time it appeared in DNSWATCHER_TARGETS, so example.com,Example.com. put example.com in the domain list twice and every check looked it up and checked its certificates twice, sending two expiry warnings per TLS check (port checks were already grouped by address and port). It now skips a name it has already kept, comparing after the lower-casing and trailing-dot removal it already did; the list keeps the order of first appearance. Model: opus-5-5 --- README.md | 3 ++- TODO.md | 2 ++ internal/config/classify.go | 9 +++++++-- internal/config/classify_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1ec4db..c195381 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,8 @@ following precedence (highest to lowest): monitoring targets are configured. A monitoring daemon with nothing to monitor is a misconfiguration, so dnswatcher fails fast with a clear error message rather than running silently. Set `DNSWATCHER_TARGETS` to a comma-separated list -of DNS names before starting. +of DNS names before starting. A name listed more than once, in any letter case +or with a trailing dot, is watched once. **`/metrics` is rate limited.** Each client address may send it 30 requests a minute, failed logins included; beyond that it answers `429 Too Many Requests` diff --git a/TODO.md b/TODO.md index 6b45c81..f08c72d 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # Completed Steps +- 2026-10-02: a name listed more than once in `DNSWATCHER_TARGETS`, in any + letter case or with a trailing dot, is watched once (closes #207). - 2026-10-01: a certificate within the expiry warning period is warned about on every TLS check, where some checks used to skip it at random (closes #204). - 2026-10-01: a domain's NS set is its delegation from the parent zone's diff --git a/internal/config/classify.go b/internal/config/classify.go index 1076215..ef10036 100644 --- a/internal/config/classify.go +++ b/internal/config/classify.go @@ -57,17 +57,22 @@ func ClassifyDNSName(name string) (DNSNameType, error) { // ClassifyTargets splits a list of DNS names into apex domains and // hostnames using the Public Suffix List. It returns an error if any -// name cannot be classified. +// name cannot be classified. A name given more than once, in any letter +// case or with a trailing dot, is kept once. func ClassifyTargets(targets []string) ([]string, []string, error) { var domains, hostnames []string + seen := make(map[string]bool) + for _, t := range targets { normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(t), ".")) - if normalized == "" { + if normalized == "" || seen[normalized] { continue } + seen[normalized] = true + typ, classErr := ClassifyDNSName(normalized) if classErr != nil { return nil, nil, classErr diff --git a/internal/config/classify_test.go b/internal/config/classify_test.go index a9c03af..a82c853 100644 --- a/internal/config/classify_test.go +++ b/internal/config/classify_test.go @@ -1,6 +1,7 @@ package config_test import ( + "slices" "testing" "sneak.berlin/go/dnswatcher/internal/config" @@ -93,6 +94,29 @@ func TestClassifyTargets(t *testing.T) { } } +func TestClassifyTargetsKeepsEachNameOnce(t *testing.T) { + t.Parallel() + + domains, hostnames, err := config.ClassifyTargets([]string{ + "example.org", + "Example.org.", + "www.example.org", + "EXAMPLE.ORG", + "WWW.Example.org.", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !slices.Equal(domains, []string{"example.org"}) { + t.Errorf("domains = %v, want [example.org]", domains) + } + + if !slices.Equal(hostnames, []string{"www.example.org"}) { + t.Errorf("hostnames = %v, want [www.example.org]", hostnames) + } +} + func TestClassifyTargetsRejectsPublicSuffix(t *testing.T) { t.Parallel()