mirror of
https://github.com/tailscale/tailscale.git
synced 2026-03-28 19:21:13 -04:00
This adds a new node capability 'dns-subdomain-resolve' that signals that all of hosts' subdomains should resolve to the same IP address. It allows wildcard matching on any node marked with this capability. This change also includes an util/dnsname utility function that lets us access the parent of a full qualified domain name. MagicDNS takes this function and recursively searchs for a matching real node name. One important thing to observe is that, in this context, a subdomain can have multiple sub labels. This means that for a given node named machine, both my.machine and be.my.machine will be a positive match. Updates #1196 Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
293 lines
7.6 KiB
Go
293 lines
7.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package dnsname
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFQDN(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want FQDN
|
|
wantErr bool
|
|
wantLabels int
|
|
}{
|
|
{"", ".", false, 0},
|
|
{".", ".", false, 0},
|
|
{"foo.com", "foo.com.", false, 2},
|
|
{"foo.com.", "foo.com.", false, 2},
|
|
{".foo.com.", "foo.com.", false, 2},
|
|
{".foo.com", "foo.com.", false, 2},
|
|
{"com", "com.", false, 1},
|
|
{"www.tailscale.com", "www.tailscale.com.", false, 3},
|
|
{"_ssh._tcp.tailscale.com", "_ssh._tcp.tailscale.com.", false, 4},
|
|
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0},
|
|
{strings.Repeat("aaaaa.", 60) + "com", "", true, 0},
|
|
{"foo..com", "", true, 0},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.in, func(t *testing.T) {
|
|
got, err := ToFQDN(test.in)
|
|
if got != test.want {
|
|
t.Errorf("ToFQDN(%q) got %q, want %q", test.in, got, test.want)
|
|
}
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("ToFQDN(%q) err %v, wantErr=%v", test.in, err, test.wantErr)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
gotDot := got.WithTrailingDot()
|
|
if gotDot != string(test.want) {
|
|
t.Errorf("ToFQDN(%q).WithTrailingDot() got %q, want %q", test.in, gotDot, test.want)
|
|
}
|
|
gotNoDot := got.WithoutTrailingDot()
|
|
wantNoDot := string(test.want)[:len(test.want)-1]
|
|
if gotNoDot != wantNoDot {
|
|
t.Errorf("ToFQDN(%q).WithoutTrailingDot() got %q, want %q", test.in, gotNoDot, wantNoDot)
|
|
}
|
|
|
|
if gotLabels := got.NumLabels(); gotLabels != test.wantLabels {
|
|
t.Errorf("ToFQDN(%q).NumLabels() got %v, want %v", test.in, gotLabels, test.wantLabels)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFQDNTooLong(t *testing.T) {
|
|
// RFC 1035 says a dns name has a max size of 255 octets, and is represented as labels of len+ASCII chars so
|
|
// example.com
|
|
// is represented as
|
|
// 7example3com0
|
|
// which is to say that if we have a trailing dot then the dots cancel out all the len bytes except the first and
|
|
// we can accept 254 chars.
|
|
|
|
// This name is max length
|
|
name := "aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaa.example.com."
|
|
if len(name) != 254 {
|
|
t.Fatalf("name should be 254 chars including trailing . (len is %d)", len(name))
|
|
}
|
|
got, err := ToFQDN(name)
|
|
if err != nil {
|
|
t.Fatalf("want: no error, got: %v", err)
|
|
}
|
|
if string(got) != name {
|
|
t.Fatalf("want: %s, got: %s", name, got)
|
|
}
|
|
|
|
// This name is too long
|
|
name = "x" + name
|
|
got, err = ToFQDN(name)
|
|
if got != "" {
|
|
t.Fatalf("want: \"\", got: %s", got)
|
|
}
|
|
if err == nil || !strings.HasSuffix(err.Error(), "is too long to be a DNS name") {
|
|
t.Fatalf("want: error to end with \"is too long to be a DNS name\", got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFQDNContains(t *testing.T) {
|
|
tests := []struct {
|
|
a, b string
|
|
want bool
|
|
}{
|
|
{"", "", true},
|
|
{"", "foo.com", true},
|
|
{"foo.com", "", false},
|
|
{"tailscale.com", "www.tailscale.com", true},
|
|
{"www.tailscale.com", "tailscale.com", false},
|
|
{"scale.com", "tailscale.com", false},
|
|
{"foo.com", "foo.com", true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.a+"_"+test.b, func(t *testing.T) {
|
|
a, err := ToFQDN(test.a)
|
|
if err != nil {
|
|
t.Fatalf("ToFQDN(%q): %v", test.a, err)
|
|
}
|
|
b, err := ToFQDN(test.b)
|
|
if err != nil {
|
|
t.Fatalf("ToFQDN(%q): %v", test.b, err)
|
|
}
|
|
|
|
if got := a.Contains(b); got != test.want {
|
|
t.Errorf("ToFQDN(%q).Contains(%q) got %v, want %v", a, b, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFQDNParent(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want FQDN
|
|
}{
|
|
{"", ""},
|
|
{".", ""},
|
|
{"com.", ""},
|
|
{"foo.com.", "com."},
|
|
{"www.foo.com.", "foo.com."},
|
|
{"a.b.c.d.", "b.c.d."},
|
|
{"sub.node.tailnet.ts.net.", "node.tailnet.ts.net."},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.in, func(t *testing.T) {
|
|
in, err := ToFQDN(test.in)
|
|
if err != nil {
|
|
t.Fatalf("ToFQDN(%q): %v", test.in, err)
|
|
}
|
|
got := in.Parent()
|
|
if got != test.want {
|
|
t.Errorf("ToFQDN(%q).Parent() = %q, want %q", test.in, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeLabel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"empty", "", ""},
|
|
{"space", " ", ""},
|
|
{"upper", "OBERON", "oberon"},
|
|
{"mixed", "Avery's iPhone 4(SE)", "averys-iphone-4se"},
|
|
{"dotted", "mon.ipn.dev", "mon-ipn-dev"},
|
|
{"email", "admin@example.com", "admin-example-com"},
|
|
{"boundary", ".bound.ary.", "bound-ary"},
|
|
{"bad_trailing", "a-", "a"},
|
|
{"bad_leading", "-a", "a"},
|
|
{"bad_both", "-a-", "a"},
|
|
{
|
|
"overlong",
|
|
strings.Repeat("test.", 20),
|
|
"test-test-test-test-test-test-test-test-test-test-test-test-tes",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := SanitizeLabel(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("want %q; got %q", tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTrimCommonSuffixes(t *testing.T) {
|
|
tests := []struct {
|
|
hostname string
|
|
want string
|
|
}{
|
|
{"computer.local", "computer"},
|
|
{"computer.localdomain", "computer"},
|
|
{"computer.lan", "computer"},
|
|
{"computer.mynetwork", "computer.mynetwork"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := TrimCommonSuffixes(tt.hostname)
|
|
if got != tt.want {
|
|
t.Errorf("TrimCommonSuffixes(%q) = %q; want %q", tt.hostname, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHasSuffix(t *testing.T) {
|
|
tests := []struct {
|
|
name, suffix string
|
|
want bool
|
|
}{
|
|
{"foo.com", "com", true},
|
|
{"foo.com.", "com", true},
|
|
{"foo.com.", "com.", true},
|
|
{"foo.com", ".com", true},
|
|
|
|
{"", "", false},
|
|
{"foo.com.", "", false},
|
|
{"foo.com.", "o.com", false},
|
|
}
|
|
for _, tt := range tests {
|
|
got := HasSuffix(tt.name, tt.suffix)
|
|
if got != tt.want {
|
|
t.Errorf("HasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrimSuffix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
suffix string
|
|
want string
|
|
}{
|
|
{"foo.magicdnssuffix.", "magicdnssuffix", "foo"},
|
|
{"foo.magicdnssuffix", "magicdnssuffix", "foo"},
|
|
{"foo.magicdnssuffix", ".magicdnssuffix", "foo"},
|
|
{"foo.anothersuffix", "magicdnssuffix", "foo.anothersuffix"},
|
|
{"foo.anothersuffix.", "magicdnssuffix", "foo.anothersuffix"},
|
|
{"a.b.c.d", "c.d", "a.b"},
|
|
{"name.", "foo", "name"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := TrimSuffix(tt.name, tt.suffix)
|
|
if got != tt.want {
|
|
t.Errorf("TrimSuffix(%q, %q) = %q; want %q", tt.name, tt.suffix, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidHostname(t *testing.T) {
|
|
tests := []struct {
|
|
hostname string
|
|
wantErr string
|
|
}{
|
|
{"example", ""},
|
|
{"example.com", ""},
|
|
{" example", `must start with a letter or number`},
|
|
{"example-.com", `must end with a letter or number`},
|
|
{strings.Repeat("a", 63), ""},
|
|
{strings.Repeat("a", 64), `is too long, max length is 63 bytes`},
|
|
{strings.Repeat(strings.Repeat("a", 63)+".", 4), "is too long to be a DNS name"},
|
|
{"www.what🤦lol.example.com", "contains invalid character"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.hostname, func(t *testing.T) {
|
|
err := ValidHostname(test.hostname)
|
|
if (err == nil) != (test.wantErr == "") || (err != nil && !strings.Contains(err.Error(), test.wantErr)) {
|
|
t.Fatalf("ValidHostname(%s)=%v; expected %v", test.hostname, err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var sinkFQDN FQDN
|
|
|
|
func BenchmarkToFQDN(b *testing.B) {
|
|
tests := []string{
|
|
"www.tailscale.com.",
|
|
"www.tailscale.com",
|
|
".www.tailscale.com",
|
|
"_ssh._tcp.www.tailscale.com.",
|
|
"_ssh._tcp.www.tailscale.com",
|
|
}
|
|
|
|
for _, test := range tests {
|
|
b.Run(test, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
sinkFQDN, _ = ToFQDN(test)
|
|
}
|
|
})
|
|
}
|
|
}
|