Match DNS zones case-insensitively and replace only the zone suffix

This commit is contained in:
Viktor Liu
2026-09-04 12:25:29 +02:00
parent 70d16d7987
commit 864a846b85
2 changed files with 71 additions and 9 deletions
+20 -9
View File
@@ -349,12 +349,21 @@ func (a *Anonymizer) anonymizeDomain(domain string, newZone bool) string {
a.mapDomain(baseForLookup, anonymized)
}
result := strings.Replace(baseDomain, baseForLookup, anonymized, 1)
if a.level >= LevelStrict && baseDomain != baseForLookup {
prefix := strings.TrimSuffix(baseDomain, "."+baseForLookup)
result = a.anonymizeLabels(prefix, "host") + "." + anonymized
// The full mapping feeds AnonymizeString so seeded FQDNs are caught
// in log lines as a whole, labels included.
// baseForLookup is the tail of baseDomain up to case, so cut by length: a
// substring match would rewrite an earlier occurrence and leave the real
// zone in place, as with a name whose own label repeats the zone.
prefix := baseDomain[:len(baseDomain)-len(baseForLookup)]
result := prefix + anonymized
if a.level >= LevelStrict && prefix != "" {
result = a.anonymizeLabels(strings.TrimSuffix(prefix, "."), "host") + "." + anonymized
}
// A dotless zone is held out of the substring pass, so a name under it
// needs its own mapping to be replaced where a log line mentions it as
// free text rather than as a DNS name. Strict level records every name
// anyway, labels included.
if prefix != "" && (a.level >= LevelStrict || !strings.Contains(baseForLookup, ".")) {
a.mapDomain(baseDomain, result)
}
return withTrailingDot(result, hasDot)
@@ -368,7 +377,9 @@ func (a *Anonymizer) mapDomain(key, anonymized string) {
}
// baseForLookup returns the key under which baseDomain's anonymized base is
// stored, or empty to leave baseDomain alone.
// stored, or empty to leave baseDomain alone. The key is lower case, since DNS
// labels compare case-insensitively, and is always the tail of baseDomain so a
// caller can cut it off by length.
//
// The key is normally the last two labels, so every name under a domain shares
// one anonymized base. A single-label zone is its own key: names under it then
@@ -377,7 +388,7 @@ func (a *Anonymizer) mapDomain(key, anonymized string) {
// needs newZone to become one, since free text and address forms also reach
// here and must not be rewritten on a guess.
func (a *Anonymizer) baseForLookup(baseDomain string, newZone bool) string {
parts := strings.Split(baseDomain, ".")
parts := strings.Split(strings.ToLower(baseDomain), ".")
last := parts[len(parts)-1]
if _, known := a.domainAnonymizer[last]; known {
@@ -387,7 +398,7 @@ func (a *Anonymizer) baseForLookup(baseDomain string, newZone bool) string {
return parts[len(parts)-2] + "." + last
}
if !newZone || last == "" || strings.EqualFold(last, "localhost") {
if !newZone || last == "" || last == "localhost" {
return ""
}
return last
+51
View File
@@ -388,6 +388,57 @@ func TestAnonymizeDomain_SingleLabelZone(t *testing.T) {
}
}
// TestAnonymizeDomainName_ZoneLabelRepeated covers a name whose own label
// repeats its zone. Cutting the key by a substring match would rewrite the
// leading occurrence and leave the real zone in the output.
func TestAnonymizeDomainName_ZoneLabelRepeated(t *testing.T) {
for _, level := range []anonymize.Level{anonymize.LevelDefault, anonymize.LevelStrict} {
t.Run(level.String(), func(t *testing.T) {
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
anonymizer.SetLevel(level)
zone := anonymizer.AnonymizeDomainName("corp.")
record := anonymizer.AnonymizeDomainName("corp.corp.")
// The zone is the tail, so it is the tail that must be replaced.
// The leading label is a host label, which the default level
// preserves like any other and strict level renumbers.
assert.True(t, strings.HasSuffix(record, "."+zone), "the record should sit under the anonymized zone, got %q for zone %q", record, zone)
assert.NotContains(t, strings.TrimSuffix(record, "."+zone), ".", "only the host label should remain in front of the zone, got %q", record)
if level >= anonymize.LevelStrict {
assert.NotContains(t, record, "corp", "strict level should also replace the host label")
}
})
}
}
// TestAnonymizeDomainName_ZoneCaseInsensitive covers a zone and a record that
// disagree on case. DNS labels compare case-insensitively, so both must resolve
// to one anonymized base.
func TestAnonymizeDomainName_ZoneCaseInsensitive(t *testing.T) {
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
zone := anonymizer.AnonymizeDomainName("Corp.")
record := anonymizer.AnonymizeDomainName("host.corp.")
assert.True(t, strings.HasSuffix(record, "."+zone), "a differently cased record should share the zone's base, got %q for zone %q", record, zone)
assert.NotContains(t, strings.ToLower(record), "corp", "the original zone name should not survive")
}
// TestAnonymizeString_NameUnderSingleLabelZoneInLogText covers a name under a
// dotless zone appearing as free text. The zone itself is held out of the
// substring pass, so the name needs its own mapping to be replaced.
func TestAnonymizeString_NameUnderSingleLabelZoneInLogText(t *testing.T) {
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
anonymizer.AnonymizeDomainName("corp")
record := anonymizer.AnonymizeDomainName("host.corp")
result := anonymizer.AnonymizeString("registering handler for [host.corp]")
assert.Equal(t, "registering handler for ["+record+"]", result,
"a name under a dotless zone should be replaced in free log text")
}
// TestAnonymizeString_KnownZoneInLogLine covers the bundle order: the DNS
// config establishes the zone, then log lines naming it are anonymized through
// the domain= key even though the name carries no dot of its own.