summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDmytro Shynkevych <dmytro@tailscale.com>2020-07-21 11:17:09 -0400
committerDmytro Shynkevych <dmytro@tailscale.com>2020-07-21 11:46:54 -0400
commita641ca53eba175785c31e415761ef1631712cab4 (patch)
tree76a5fb7b257ca5d63dd38efd958f926094fe7a95
parentd8e67ca2ab22274e02c2f4d87c1341e0a1ea2550 (diff)
downloadtailscale-dshynkev/dns-name.tar.xz
tailscale-dshynkev/dns-name.zip
tailcfg: dns name manglingdshynkev/dns-name
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
-rw-r--r--tailcfg/name.go108
-rw-r--r--tailcfg/name_test.go63
2 files changed, 171 insertions, 0 deletions
diff --git a/tailcfg/name.go b/tailcfg/name.go
new file mode 100644
index 000000000..a602cceec
--- /dev/null
+++ b/tailcfg/name.go
@@ -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, ".")
+}
diff --git a/tailcfg/name_test.go b/tailcfg/name_test.go
new file mode 100644
index 000000000..e64f663b5
--- /dev/null
+++ b/tailcfg/name_test.go
@@ -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)
+ }
+ })
+ }
+}