mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 15:23:45 +00:00
tailcfg: dns name mangling
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
parent
d8e67ca2ab
commit
a641ca53eb
108
tailcfg/name.go
Normal file
108
tailcfg/name.go
Normal 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
63
tailcfg/name_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user