ensure final dot on node name (#2503)

* ensure final dot on node name

This ensures that nodes which have a base domain set, will have a dot appended to their FQDN.

Resolves: https://github.com/juanfont/headscale/issues/2501

* improve OIDC TTL expire test

Waiting a bit more than the TTL of the OIDC token seems to remove some flakiness of this test. This furthermore makes use of a go func safe buffer which should avoid race conditions.
This commit is contained in:
Nick 2025-04-11 12:39:08 +02:00 committed by GitHub
parent 0d3134720b
commit 109989005d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 67 additions and 13 deletions

View File

@ -87,7 +87,11 @@ The new policy can be used by setting the environment variable
[#2493](https://github.com/juanfont/headscale/pull/2493) [#2493](https://github.com/juanfont/headscale/pull/2493)
- If a OIDC provider doesn't include the `email_verified` claim in its ID - If a OIDC provider doesn't include the `email_verified` claim in its ID
tokens, Headscale will attempt to get it from the UserInfo endpoint. tokens, Headscale will attempt to get it from the UserInfo endpoint.
- Improve performance by only querying relevant nodes from the database for node updates [#2509](https://github.com/juanfont/headscale/pull/2509) - Improve performance by only querying relevant nodes from the database for node
updates [#2509](https://github.com/juanfont/headscale/pull/2509)
- node FQDNs in the netmap will now contain a dot (".") at the end. This aligns
with behaviour of tailscale.com
[#2503](https://github.com/juanfont/headscale/pull/2503)
## 0.25.1 (2025-02-25) ## 0.25.1 (2025-02-25)

View File

@ -169,6 +169,32 @@ func TestTailNode(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "check-dot-suffix-on-node-name",
node: &types.Node{
GivenName: "minimal",
Hostinfo: &tailcfg.Hostinfo{},
},
dnsConfig: &tailcfg.DNSConfig{},
baseDomain: "example.com",
want: &tailcfg.Node{
// a node name should have a dot appended
Name: "minimal.example.com.",
StableID: "0",
HomeDERP: 0,
LegacyDERPString: "127.3.3.40:0",
Hostinfo: hiview(tailcfg.Hostinfo{}),
Tags: []string{},
MachineAuthorized: true,
CapMap: tailcfg.NodeCapMap{
tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
tailcfg.CapabilityAdmin: []tailcfg.RawMessage{},
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
},
},
wantErr: false,
},
// TODO: Add tests to check other aspects of the node conversion: // TODO: Add tests to check other aspects of the node conversion:
// - With tags and policy // - With tags and policy
// - dnsconfig and basedomain // - dnsconfig and basedomain

View File

@ -364,7 +364,7 @@ func (node *Node) GetFQDN(baseDomain string) (string, error) {
if baseDomain != "" { if baseDomain != "" {
hostname = fmt.Sprintf( hostname = fmt.Sprintf(
"%s.%s", "%s.%s.",
node.GivenName, node.GivenName,
baseDomain, baseDomain,
) )

View File

@ -142,7 +142,7 @@ func TestNodeFQDN(t *testing.T) {
}, },
}, },
domain: "example.com", domain: "example.com",
want: "test.example.com", want: "test.example.com.",
}, },
{ {
name: "all-set", name: "all-set",
@ -153,7 +153,7 @@ func TestNodeFQDN(t *testing.T) {
}, },
}, },
domain: "example.com", domain: "example.com",
want: "test.example.com", want: "test.example.com.",
}, },
{ {
name: "no-given-name", name: "no-given-name",
@ -171,7 +171,7 @@ func TestNodeFQDN(t *testing.T) {
GivenName: strings.Repeat("a", 256), GivenName: strings.Repeat("a", 256),
}, },
domain: "example.com", domain: "example.com",
wantErr: fmt.Sprintf("failed to create valid FQDN (%s.example.com): hostname too long, cannot except 255 ASCII chars", strings.Repeat("a", 256)), wantErr: fmt.Sprintf("failed to create valid FQDN (%s.example.com.): hostname too long, cannot except 255 ASCII chars", strings.Repeat("a", 256)),
}, },
{ {
name: "no-dnsconfig", name: "no-dnsconfig",
@ -182,7 +182,7 @@ func TestNodeFQDN(t *testing.T) {
}, },
}, },
domain: "example.com", domain: "example.com",
want: "test.example.com", want: "test.example.com.",
}, },
} }

View File

@ -170,10 +170,11 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps)) t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps))
// This is not great, but this sadly is a time dependent test, so the // This is not great, but this sadly is a time dependent test, so the
// safe thing to do is wait out the whole TTL time before checking if // safe thing to do is wait out the whole TTL time (and a bit more out
// the clients have logged out. The Wait function can't do it itself // of safety reasons) before checking if the clients have logged out.
// as it has an upper bound of 1 min. // The Wait function can't do it itself as it has an upper bound of 1
time.Sleep(shortAccessTTL) // min.
time.Sleep(shortAccessTTL + 10*time.Second)
assertTailscaleNodesLogout(t, allClients) assertTailscaleNodesLogout(t, allClients)
} }

View File

@ -49,7 +49,7 @@ func TestResolveMagicDNS(t *testing.T) {
// It is safe to ignore this error as we handled it when caching it // It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN() peerFQDN, _ := peer.FQDN()
assert.Equal(t, fmt.Sprintf("%s.headscale.net", peer.Hostname()), peerFQDN) assert.Equal(t, fmt.Sprintf("%s.headscale.net.", peer.Hostname()), peerFQDN)
command := []string{ command := []string{
"tailscale", "tailscale",

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
@ -29,14 +30,36 @@ func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
}) })
} }
// buffer is a goroutine safe bytes.buffer
type buffer struct {
store bytes.Buffer
mutex sync.Mutex
}
// Write appends the contents of p to the buffer, growing the buffer as needed. It returns
// the number of bytes written.
func (b *buffer) Write(p []byte) (n int, err error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.store.Write(p)
}
// String returns the contents of the unread portion of the buffer
// as a string.
func (b *buffer) String() string {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.store.String()
}
func ExecuteCommand( func ExecuteCommand(
resource *dockertest.Resource, resource *dockertest.Resource,
cmd []string, cmd []string,
env []string, env []string,
options ...ExecuteCommandOption, options ...ExecuteCommandOption,
) (string, string, error) { ) (string, string, error) {
var stdout bytes.Buffer var stdout = buffer{}
var stderr bytes.Buffer var stderr = buffer{}
execConfig := ExecuteCommandConfig{ execConfig := ExecuteCommandConfig{
timeout: dockerExecuteTimeout, timeout: dockerExecuteTimeout,