stricter hostname validation and replace (#2383)

This commit is contained in:
Kristoffer Dalby
2025-10-22 13:50:39 +02:00
committed by GitHub
parent 2c9e98d3f5
commit 1cdea7ed9b
16 changed files with 888 additions and 193 deletions

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/netip"
"regexp"
"slices"
"sort"
"strconv"
@@ -27,6 +28,8 @@ var (
ErrHostnameTooLong = errors.New("hostname too long, cannot except 255 ASCII chars")
ErrNodeHasNoGivenName = errors.New("node has no given name")
ErrNodeUserHasNoName = errors.New("node user has no name")
invalidDNSRegex = regexp.MustCompile("[^a-z0-9-.]+")
)
type (
@@ -144,7 +147,10 @@ func (ns Nodes) ViewSlice() views.Slice[NodeView] {
// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
func (node *Node) GivenNameHasBeenChanged() bool {
return node.GivenName == util.ConvertWithFQDNRules(node.Hostname)
// Strip invalid DNS characters for givenName comparison
normalised := strings.ToLower(node.Hostname)
normalised = invalidDNSRegex.ReplaceAllString(normalised, "")
return node.GivenName == normalised
}
// IsExpired returns whether the node registration has expired.
@@ -531,20 +537,34 @@ func (node *Node) ApplyHostnameFromHostInfo(hostInfo *tailcfg.Hostinfo) {
return
}
if node.Hostname != hostInfo.Hostname {
newHostname := strings.ToLower(hostInfo.Hostname)
if err := util.ValidateHostname(newHostname); err != nil {
log.Warn().
Str("node.id", node.ID.String()).
Str("current_hostname", node.Hostname).
Str("rejected_hostname", hostInfo.Hostname).
Err(err).
Msg("Rejecting invalid hostname update from hostinfo")
return
}
if node.Hostname != newHostname {
log.Trace().
Str("node.id", node.ID.String()).
Str("old_hostname", node.Hostname).
Str("new_hostname", hostInfo.Hostname).
Str("new_hostname", newHostname).
Str("old_given_name", node.GivenName).
Bool("given_name_changed", node.GivenNameHasBeenChanged()).
Msg("Updating hostname from hostinfo")
if node.GivenNameHasBeenChanged() {
node.GivenName = util.ConvertWithFQDNRules(hostInfo.Hostname)
// Strip invalid DNS characters for givenName display
givenName := strings.ToLower(newHostname)
givenName = invalidDNSRegex.ReplaceAllString(givenName, "")
node.GivenName = givenName
}
node.Hostname = hostInfo.Hostname
node.Hostname = newHostname
log.Trace().
Str("node.id", node.ID.String()).

View File

@@ -369,7 +369,7 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
},
want: Node{
GivenName: "manual-test.local",
Hostname: "NewHostName.Local",
Hostname: "newhostname.local",
},
},
{
@@ -383,7 +383,245 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
},
want: Node{
GivenName: "newhostname.local",
Hostname: "NewHostName.Local",
Hostname: "newhostname.local",
},
},
{
name: "invalid-hostname-with-emoji-rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "hostname-with-💩",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname", // Should reject and keep old hostname
},
},
{
name: "invalid-hostname-with-unicode-rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "我的电脑",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname", // Should keep old hostname
},
},
{
name: "invalid-hostname-with-special-chars-rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "node-with-special!@#$%",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname", // Should reject and keep old hostname
},
},
{
name: "invalid-hostname-too-short-rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "a",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname", // Should keep old hostname
},
},
{
name: "invalid-hostname-uppercase-accepted-lowercased",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "ValidHostName",
},
want: Node{
GivenName: "validhostname", // GivenName follows hostname when it changes
Hostname: "validhostname", // Uppercase is lowercased, not rejected
},
},
{
name: "uppercase_to_lowercase_accepted",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "User2-Host",
},
want: Node{
GivenName: "user2-host",
Hostname: "user2-host",
},
},
{
name: "at_sign_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "Test@Host",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "chinese_chars_with_dash_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "server-北京-01",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "chinese_only_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "我的电脑",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "emoji_with_text_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "laptop-🚀",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "mixed_chinese_emoji_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "测试💻机器",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "only_emojis_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "🎉🎊",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "only_at_signs_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "@@@",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "starts_with_dash_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "-test",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "ends_with_dash_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "test-",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "too_long_hostname_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: strings.Repeat("t", 65),
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
{
name: "underscore_rejected",
nodeBefore: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "test_node",
},
want: Node{
GivenName: "valid-hostname",
Hostname: "valid-hostname",
},
},
}