From 2b034ae65acfb71e4379ac5127bc216b8ebd29da Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 2 Oct 2026 00:20:37 +0000 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, checked its ports and its certificates twice, sending two expiry warnings per TLS check. 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..3fbab63 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.com", + "Example.com.", + "www.example.com", + "EXAMPLE.COM", + "WWW.Example.com.", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !slices.Equal(domains, []string{"example.com"}) { + t.Errorf("domains = %v, want [example.com]", domains) + } + + if !slices.Equal(hostnames, []string{"www.example.com"}) { + t.Errorf("hostnames = %v, want [www.example.com]", hostnames) + } +} + func TestClassifyTargetsRejectsPublicSuffix(t *testing.T) { t.Parallel()