tailcfg: dns name mangling

Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
Dmytro Shynkevych 2020-07-21 11:17:09 -04:00
parent d8e67ca2ab
commit a641ca53eb
No known key found for this signature in database
GPG Key ID: FF5E2F3DAD97EA23
2 changed files with 171 additions and 0 deletions

108
tailcfg/name.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tailcfg
import "strings"
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
}
}
// maxLabelLength is the maximal length of a label permitted by RFC 1035.
const maxLabelLength = 63
// SanitizeNameLabel takes a string intended to be a DNS name label
// and turns it into a valid name label according to RFC 1035.
func SanitizeNameLabel(label string) string {
var sb strings.Builder
start, end := 0, len(label)
// This is technically stricter than necessary as some characters may be dropped,
// but labels have no business being anywhere near this long in any case.
if end > maxLabelLength {
end = maxLabelLength
}
// A label must start with a letter...
for ; start < end; start++ {
if isalpha(label[start]) {
break
}
}
// ...and end with a letter or number.
for ; start < end; end-- {
// This is safe because (start < end) implies (end >= 1).
if isalphanum(label[end-1]) {
break
}
}
for i := start; i < end; i++ {
// Consume a separator only if we are not at a boundary:
// then we can turn it into a hypen without breaking the rules.
boundary := (i == start) || (i == end-1)
if !boundary && separators[label[i]] {
sb.WriteByte('-')
} else if isdnschar(label[i]) {
sb.WriteByte(tolower(label[i]))
}
}
return sb.String()
}
// SanitizeName takes a string intended to be a DNS name
// and turns it into a valid name according to RFC 1035.
//
// All dots in the string are preserved, defining its division into labels,
// unless the string represents an email address,
// in which case the local part of the address is treated as a single label.
func SanitizeName(name string) string {
// The local part may be a quoted string containing @, so we split on the last @.
if idx := strings.LastIndexByte(name, '@'); idx != -1 {
localPart := SanitizeNameLabel(name[:idx])
domain := SanitizeName(name[idx+1:])
return localPart + "." + domain
}
labels := strings.Split(name, ".")
for i, label := range labels {
labels[i] = SanitizeNameLabel(label)
}
return strings.Join(labels, ".")
}

63
tailcfg/name_test.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tailcfg
import (
"strings"
"testing"
)
func TestSanitizeNameLabel(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"},
{"boudary", ".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 := SanitizeNameLabel(tt.in)
if got != tt.want {
t.Errorf("want %s; got %s", tt.want, got)
}
})
}
}
func TestSanitizeName(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"single_label", "OBERON", "oberon"},
{"dotted", "MON.IPN.DEV", "mon.ipn.dev"},
{"email", "first.last@example.com", "first-last.example.com"},
{"weird", "\"first..last(c+d)?\"@email.com", "first--lastcd.email.com"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SanitizeName(tt.in)
if got != tt.want {
t.Errorf("want %s; got %s", tt.want, got)
}
})
}
}