mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-24 13:46:53 +02:00
Merge 6ea0656b87
into be337c6a33
This commit is contained in:
commit
b00d10b304
1
.github/workflows/test-integration.yaml
vendored
1
.github/workflows/test-integration.yaml
vendored
@ -50,6 +50,7 @@ jobs:
|
|||||||
- TestDERPVerifyEndpoint
|
- TestDERPVerifyEndpoint
|
||||||
- TestResolveMagicDNS
|
- TestResolveMagicDNS
|
||||||
- TestResolveMagicDNSExtraRecordsPath
|
- TestResolveMagicDNSExtraRecordsPath
|
||||||
|
- TestMagicDNSPeerAAAA
|
||||||
- TestDERPServerScenario
|
- TestDERPServerScenario
|
||||||
- TestDERPServerWebsocketScenario
|
- TestDERPServerWebsocketScenario
|
||||||
- TestPingAllByIP
|
- TestPingAllByIP
|
||||||
|
@ -404,3 +404,11 @@ logtail:
|
|||||||
# default static port 41641. This option is intended as a workaround for some buggy
|
# default static port 41641. This option is intended as a workaround for some buggy
|
||||||
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
|
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
|
||||||
randomize_client_port: false
|
randomize_client_port: false
|
||||||
|
|
||||||
|
|
||||||
|
# Enables MagicDNS AAAA query resolution for tailnet nodes with IPv6.
|
||||||
|
# When this option is enabled, devices that have a valid overlay IPv6 address will
|
||||||
|
# automatically gain the `magicdns-aaaa` Node Attribute.
|
||||||
|
# This attribute allows them to resolve AAAA (IPv6) DNS queries through MagicDNS for
|
||||||
|
# other nodes within the tailnet.
|
||||||
|
magicdns_peer_aaaa: false
|
||||||
|
@ -78,14 +78,17 @@ func tailNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tags []string
|
var tags []string
|
||||||
|
|
||||||
for _, tag := range node.RequestTagsSlice().All() {
|
for _, tag := range node.RequestTagsSlice().All() {
|
||||||
if checker.NodeCanHaveTag(node, tag) {
|
if checker.NodeCanHaveTag(node, tag) {
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range node.ForcedTags().All() {
|
for _, tag := range node.ForcedTags().All() {
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = lo.Uniq(tags)
|
tags = lo.Uniq(tags)
|
||||||
|
|
||||||
routes := primaryRouteFunc(node.ID())
|
routes := primaryRouteFunc(node.ID())
|
||||||
@ -133,6 +136,12 @@ func tailNode(
|
|||||||
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable NodeAttrMagicDNSPeerAAAA only if requested in
|
||||||
|
// the config and for nodes with a valid v6 overlay address
|
||||||
|
if cfg.MagicDNSPeerAAAA && node.IPv6().Valid() {
|
||||||
|
tNode.CapMap[tailcfg.NodeAttrMagicDNSPeerAAAA] = []tailcfg.RawMessage{}
|
||||||
|
}
|
||||||
|
|
||||||
if !node.IsOnline().Valid() || !node.IsOnline().Get() {
|
if !node.IsOnline().Valid() || !node.IsOnline().Get() {
|
||||||
// LastSeen is only set when node is
|
// LastSeen is only set when node is
|
||||||
// not connected to the control server.
|
// not connected to the control server.
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
func TestTailNode(t *testing.T) {
|
func TestTailNode(t *testing.T) {
|
||||||
mustNK := func(str string) key.NodePublic {
|
mustNK := func(str string) key.NodePublic {
|
||||||
var k key.NodePublic
|
var k key.NodePublic
|
||||||
|
|
||||||
_ = k.UnmarshalText([]byte(str))
|
_ = k.UnmarshalText([]byte(str))
|
||||||
|
|
||||||
return k
|
return k
|
||||||
@ -27,6 +28,7 @@ func TestTailNode(t *testing.T) {
|
|||||||
|
|
||||||
mustDK := func(str string) key.DiscoPublic {
|
mustDK := func(str string) key.DiscoPublic {
|
||||||
var k key.DiscoPublic
|
var k key.DiscoPublic
|
||||||
|
|
||||||
_ = k.UnmarshalText([]byte(str))
|
_ = k.UnmarshalText([]byte(str))
|
||||||
|
|
||||||
return k
|
return k
|
||||||
@ -34,6 +36,7 @@ func TestTailNode(t *testing.T) {
|
|||||||
|
|
||||||
mustMK := func(str string) key.MachinePublic {
|
mustMK := func(str string) key.MachinePublic {
|
||||||
var k key.MachinePublic
|
var k key.MachinePublic
|
||||||
|
|
||||||
_ = k.UnmarshalText([]byte(str))
|
_ = k.UnmarshalText([]byte(str))
|
||||||
|
|
||||||
return k
|
return k
|
||||||
@ -204,6 +207,7 @@ func TestTailNode(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
polMan, err := policy.NewPolicyManager(tt.pol, []types.User{}, types.Nodes{tt.node}.ViewSlice())
|
polMan, err := policy.NewPolicyManager(tt.pol, []types.User{}, types.Nodes{tt.node}.ViewSlice())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
primary := routes.New()
|
primary := routes.New()
|
||||||
cfg := &types.Config{
|
cfg := &types.Config{
|
||||||
BaseDomain: tt.baseDomain,
|
BaseDomain: tt.baseDomain,
|
||||||
@ -215,6 +219,7 @@ func TestTailNode(t *testing.T) {
|
|||||||
// This is a hack to avoid having a second node to test the primary route.
|
// This is a hack to avoid having a second node to test the primary route.
|
||||||
// This should be baked into the test case proper if it is extended in the future.
|
// This should be baked into the test case proper if it is extended in the future.
|
||||||
_ = primary.SetRoutes(2, netip.MustParsePrefix("192.168.0.0/24"))
|
_ = primary.SetRoutes(2, netip.MustParsePrefix("192.168.0.0/24"))
|
||||||
|
|
||||||
got, err := tailNode(
|
got, err := tailNode(
|
||||||
tt.node.View(),
|
tt.node.View(),
|
||||||
0,
|
0,
|
||||||
@ -224,7 +229,6 @@ func TestTailNode(t *testing.T) {
|
|||||||
},
|
},
|
||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("tailNode() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("tailNode() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
|
||||||
@ -293,7 +297,9 @@ func TestNodeExpiry(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("nodeExpiry() error = %v", err)
|
t.Fatalf("nodeExpiry() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var deseri tailcfg.Node
|
var deseri tailcfg.Node
|
||||||
|
|
||||||
err = json.Unmarshal(seri, &deseri)
|
err = json.Unmarshal(seri, &deseri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("nodeExpiry() error = %v", err)
|
t.Fatalf("nodeExpiry() error = %v", err)
|
||||||
@ -309,3 +315,113 @@ func TestNodeExpiry(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMagicDNSPeerAAAAEnabled(t *testing.T) {
|
||||||
|
// A node with both IPv4 and IPv6 addresses.
|
||||||
|
node := &types.Node{
|
||||||
|
GivenName: "ipv6node",
|
||||||
|
IPv4: iap("100.64.0.1"),
|
||||||
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
tsaddr.AllIPv4(),
|
||||||
|
tsaddr.AllIPv6(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key configuration setting is enabled.
|
||||||
|
cfg := &types.Config{
|
||||||
|
MagicDNSPeerAAAA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
polMan, err := policy.NewPolicyManager(nil, nil, types.Nodes{node}.ViewSlice())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := tailNode(
|
||||||
|
node.View(),
|
||||||
|
0,
|
||||||
|
polMan,
|
||||||
|
func(id types.NodeID) []netip.Prefix { return []netip.Prefix{} },
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Assert that the magicdns-aaaa capability is present.
|
||||||
|
_, hasAAAA := got.CapMap[tailcfg.NodeAttrMagicDNSPeerAAAA]
|
||||||
|
require.True(t, hasAAAA, "expected magicdns-aaaa capability to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMagicDNSPeerAAAADisabled(t *testing.T) {
|
||||||
|
// A node with both IPv4 and IPv6 addresses.
|
||||||
|
node := &types.Node{
|
||||||
|
GivenName: "ipv6node",
|
||||||
|
IPv4: iap("100.64.0.1"),
|
||||||
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
tsaddr.AllIPv4(),
|
||||||
|
tsaddr.AllIPv6(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key configuration setting is disabled.
|
||||||
|
cfg := &types.Config{
|
||||||
|
MagicDNSPeerAAAA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
polMan, err := policy.NewPolicyManager(nil, nil, types.Nodes{node}.ViewSlice())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := tailNode(
|
||||||
|
node.View(),
|
||||||
|
0,
|
||||||
|
polMan,
|
||||||
|
func(id types.NodeID) []netip.Prefix { return []netip.Prefix{} },
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Assert that the magicdns-aaaa capability is NOT present.
|
||||||
|
_, hasAAAA := got.CapMap[tailcfg.NodeAttrMagicDNSPeerAAAA]
|
||||||
|
require.False(t, hasAAAA, "expected magicdns-aaaa capability NOT to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMagicDNSPeerAAAAEnabledWithIPv4OnlyNode(t *testing.T) {
|
||||||
|
// A node with a valid IPv4 address and no IPv6 address
|
||||||
|
node := &types.Node{
|
||||||
|
ID: 0,
|
||||||
|
IPv4: iap("100.64.0.1"),
|
||||||
|
GivenName: "ipv4only",
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
tsaddr.AllIPv4(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the configuration with the flag enabled
|
||||||
|
cfg := &types.Config{
|
||||||
|
MagicDNSPeerAAAA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
polMan, err := policy.NewPolicyManager(nil, nil, types.Nodes{node}.ViewSlice())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := tailNode(
|
||||||
|
node.View(),
|
||||||
|
0,
|
||||||
|
polMan,
|
||||||
|
func(id types.NodeID) []netip.Prefix {
|
||||||
|
return []netip.Prefix{}
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The feature is enabled but the node lacks a V6 address
|
||||||
|
// the node attribute should not be present
|
||||||
|
_, hasAAAA := got.CapMap[tailcfg.NodeAttrMagicDNSPeerAAAA]
|
||||||
|
require.False(t, hasAAAA, "expected magicdns-aaaa capability NOT to be present on IPv4 only node")
|
||||||
|
}
|
||||||
|
@ -98,6 +98,12 @@ type Config struct {
|
|||||||
Policy PolicyConfig
|
Policy PolicyConfig
|
||||||
|
|
||||||
Tuning Tuning
|
Tuning Tuning
|
||||||
|
|
||||||
|
// Controls the automatic addition of the `magicdns-aaaa` node attribute
|
||||||
|
// If set to true, the attributes will be set for all nodes that have
|
||||||
|
// a valid Overlay IPv6 Address.
|
||||||
|
// This is separate from the real DNS
|
||||||
|
MagicDNSPeerAAAA bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSConfig struct {
|
type DNSConfig struct {
|
||||||
@ -336,6 +342,8 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
|
|
||||||
viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))
|
viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))
|
||||||
|
|
||||||
|
viper.SetDefault("magicdns_peer_aaaa", false)
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
log.Warn().Msg("No config file found, using defaults")
|
log.Warn().Msg("No config file found, using defaults")
|
||||||
@ -873,6 +881,7 @@ func LoadServerConfig() (*Config, error) {
|
|||||||
derpConfig := derpConfig()
|
derpConfig := derpConfig()
|
||||||
logTailConfig := logtailConfig()
|
logTailConfig := logtailConfig()
|
||||||
randomizeClientPort := viper.GetBool("randomize_client_port")
|
randomizeClientPort := viper.GetBool("randomize_client_port")
|
||||||
|
magicDNSPeerAAAA := viper.GetBool("magicdns_peer_aaaa")
|
||||||
|
|
||||||
oidcClientSecret := viper.GetString("oidc.client_secret")
|
oidcClientSecret := viper.GetString("oidc.client_secret")
|
||||||
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
|
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
|
||||||
@ -1000,6 +1009,8 @@ func LoadServerConfig() (*Config, error) {
|
|||||||
return DefaultBatcherWorkers()
|
return DefaultBatcherWorkers()
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MagicDNSPeerAAAA: magicDNSPeerAAAA,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,15 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -225,3 +228,68 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||||||
assertCommandOutputContains(t, client, []string{"dig", "copy.myvpn.example.com"}, "8.8.8.8")
|
assertCommandOutputContains(t, client, []string{"dig", "copy.myvpn.example.com"}, "8.8.8.8")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMagicDNSPeerAAAA(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
spec := ScenarioSpec{
|
||||||
|
NodesPerUser: len(MustTestVersions),
|
||||||
|
Users: []string{"user1", "user2"},
|
||||||
|
MaxWait: dockertestMaxWait(),
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario, err := NewScenario(spec)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
[]tsic.Option{
|
||||||
|
tsic.WithDockerEntrypoint([]string{
|
||||||
|
"/bin/sh",
|
||||||
|
"-c",
|
||||||
|
"/bin/sleep 3 ; apk add python3 curl bind-tools ; update-ca-certificates ; tailscaled --tun=tsdev",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
hsic.WithTestName("magicdnspeeraaaa"),
|
||||||
|
hsic.WithEmbeddedDERPServerOnly(),
|
||||||
|
hsic.WithTLS(),
|
||||||
|
hsic.WithConfigEnv(map[string]string{
|
||||||
|
// The feature under test
|
||||||
|
"HEADSCALE_MAGICDNS_PEER_AAAA": "true",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assertNoErrHeadscaleEnv(t, err)
|
||||||
|
|
||||||
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
|
assertNoErrListClients(t, err)
|
||||||
|
|
||||||
|
err = scenario.WaitForTailscaleSync()
|
||||||
|
assertNoErrSync(t, err)
|
||||||
|
|
||||||
|
// Loop through all clients to perform the dig command.
|
||||||
|
for _, client := range allClients {
|
||||||
|
// Only run the test for Tailscale clients that support this feature (1.84+).
|
||||||
|
// The `tailscaled` container has no field or method GivenName().
|
||||||
|
// We obtain the name by splitting the FQDN and using the first part.
|
||||||
|
clientName := client.Hostname()
|
||||||
|
if util.TailscaleVersionNewerOrEqual("1.84", client.Version()) {
|
||||||
|
t.Logf("Running AAAA check for client: %s (version: %s)", clientName, client.Version())
|
||||||
|
|
||||||
|
ips, err := client.IPs()
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
targetIPv6 := lo.Filter(ips, func(ip netip.Addr, _ int) bool {
|
||||||
|
return ip.Is6()
|
||||||
|
})
|
||||||
|
assert.NotEmpty(t, targetIPv6, "expected client to have an IPv6 address")
|
||||||
|
|
||||||
|
// Construct the dig command as a string slice and assert the output.
|
||||||
|
fqdn := clientName + ".headscale.net"
|
||||||
|
digCmd := []string{"dig", "+short", "-t", "AAAA", fqdn}
|
||||||
|
|
||||||
|
assertCommandOutputContains(t, client, digCmd, targetIPv6[0].String())
|
||||||
|
} else {
|
||||||
|
t.Logf("Skipping AAAA check for client: %s (version: %s), requires at least 1.84", clientName, client.Version())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user