util/dnsname: add wildcard FQDN utilities

This adds helper functions for DNS name handling. They were propeled by
a demand to manipulate and validate names containing wildcard prefixes.
The new helper functions provide the ability to traverse parents of DNS
names, validate wildcard labels, and normalize input names into FQDN by
other components of Tailscale that will soon support wildcard prefixes.

Updates #1196

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
This commit is contained in:
Fernando Serboncini
2025-12-09 14:02:30 -05:00
parent 1dfdee8521
commit 5895c43d70
2 changed files with 141 additions and 0 deletions

View File

@@ -65,6 +65,24 @@ func ToFQDN(s string) (FQDN, error) {
return FQDN(raw), nil
}
// ToFQDNAllowWildcard converts s to an FQDN, allowing a leading "*." for
// wildcard DNS records. For example, "*.example.com" is valid and returns
// "*.example.com.".
func ToFQDNAllowWildcard(s string) (FQDN, error) {
if strings.HasPrefix(s, "*.") {
rest := s[2:]
if rest == "" || rest == "." {
return "", vizerror.New("wildcard must have a parent domain")
}
parent, err := ToFQDN(rest)
if err != nil {
return "", err
}
return FQDN("*." + string(parent)), nil
}
return ToFQDN(s)
}
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
@@ -94,6 +112,35 @@ func (f FQDN) Contains(other FQDN) bool {
return strings.HasSuffix(other.WithTrailingDot(), cmp)
}
// Parent returns the parent domain of f by removing the first label.
// For example, Parent of "www.example.com." returns "example.com.".
// For single-label domains like "com." or the root ".", it returns "".
func (f FQDN) Parent() FQDN {
s := f.WithTrailingDot()
if s == "." {
return ""
}
idx := strings.Index(s, ".")
if idx == -1 || idx == len(s)-1 {
return ""
}
parent := s[idx+1:]
if parent == "." {
return ""
}
return FQDN(parent)
}
// ValidWildcardLabel reports whether label is a valid DNS label, allowing
// a leading "*." for wildcard records. For example, "*.foo" is valid.
// All errors are [vizerror.Error].
func ValidWildcardLabel(label string) error {
if strings.HasPrefix(label, "*.") {
return ValidLabel(label[2:])
}
return ValidLabel(label)
}
// ValidLabel reports whether label is a valid DNS label. All errors are
// [vizerror.Error].
func ValidLabel(label string) error {

View File

@@ -123,6 +123,100 @@ func TestFQDNContains(t *testing.T) {
}
}
func TestFQDNParent(t *testing.T) {
tests := []struct {
in string
wantParent string
}{
{"www.example.com", "example.com."},
{"foo.bar.baz.com", "bar.baz.com."},
{"example.com", "com."},
{"com", ""}, // single label, parent would be root
{".", ""}, // root has no parent
{"", ""}, // empty -> root, no parent
{"*.example.com", "example.com."}, // wildcard label
}
for _, test := range tests {
t.Run(test.in, func(t *testing.T) {
fqdn, err := ToFQDN(test.in)
if err != nil {
t.Fatalf("ToFQDN(%q): %v", test.in, err)
}
gotParent := fqdn.Parent()
if string(gotParent) != test.wantParent {
t.Errorf("FQDN(%q).Parent() = %q, want %q", fqdn, gotParent, test.wantParent)
}
})
}
}
func TestValidWildcardLabel(t *testing.T) {
tests := []struct {
label string
valid bool
}{
// Valid regular labels
{"foo", true},
{"foo-bar", true},
{"a", true},
{"123", true},
// Valid wildcard labels
{"*.foo", true},
{"*.foo-bar", true},
// Invalid labels
{"", false},
{"*", false},
{"-foo", false},
{"foo-", false},
{"foo..bar", false},
{"*.foo-", false},
{"*.", false},
{"**", false},
{"*.*.foo", false},
}
for _, tt := range tests {
t.Run(tt.label, func(t *testing.T) {
err := ValidWildcardLabel(tt.label)
if (err == nil) != tt.valid {
t.Errorf("ValidWildcardLabel(%q) = %v, want valid=%v", tt.label, err, tt.valid)
}
})
}
}
func TestToFQDNAllowWildcard(t *testing.T) {
tests := []struct {
in string
want string
valid bool
}{
{"example.com", "example.com.", true},
{"foo.bar.baz", "foo.bar.baz.", true},
{"*.example.com", "*.example.com.", true},
{"*.foo.bar.com", "*.foo.bar.com.", true},
{"*.example.com.", "*.example.com.", true},
{"*.", "", false},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
got, err := ToFQDNAllowWildcard(tt.in)
if (err == nil) != tt.valid {
t.Errorf("ToFQDNAllowWildcard(%q) error = %v, want valid=%v", tt.in, err, tt.valid)
return
}
if tt.valid && string(got) != tt.want {
t.Errorf("ToFQDNAllowWildcard(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestSanitizeLabel(t *testing.T) {
tests := []struct {
name string