mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-22 16:46:29 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user