Skip to content

Commit

Permalink
initial dns integration tests
Browse files Browse the repository at this point in the history
Signed-off-by: Kristoffer Dalby <[email protected]>
  • Loading branch information
kradalby committed Jul 29, 2024
1 parent 66aa89a commit 9c814d7
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 73 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
- TestNodeRenameCommand
- TestNodeMoveCommand
- TestPolicyCommand
- TestResolveMagicDNS
- TestValidateResolvConf
- TestDERPServerScenario
- TestPingAllByIP
- TestPingAllByIPPublicDERP
Expand All @@ -45,7 +47,6 @@ jobs:
- TestEphemeral2006DeletedTooQuickly
- TestPingAllByHostname
- TestTaildrop
- TestResolveMagicDNS
- TestExpireNode
- TestNodeOnlineStatus
- TestPingAllByIPManyUpDown
Expand Down
217 changes: 217 additions & 0 deletions integration/dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package integration

import (
"fmt"
"strings"
"testing"

"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
)

func TestResolveMagicDNS(t *testing.T) {
IntegrationSkip(t)
t.Parallel()

scenario, err := NewScenario(dockertestMaxWait())
assertNoErr(t, err)
defer scenario.Shutdown()

spec := map[string]int{
"magicdns1": len(MustTestVersions),
"magicdns2": len(MustTestVersions),
}

err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns"))
assertNoErrHeadscaleEnv(t, err)

allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

// assertClientsState(t, allClients)

// Poor mans cache
_, err = scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)

_, err = scenario.ListTailscaleClientsIPs()
assertNoErrListClientIPs(t, err)

for _, client := range allClients {
for _, peer := range allClients {
// It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN()

assert.Equal(t, fmt.Sprintf("%s.headscale.net", peer.Hostname()), peerFQDN)

command := []string{
"tailscale",
"ip", peerFQDN,
}
result, _, err := client.Execute(command)
if err != nil {
t.Fatalf(
"failed to execute resolve/ip command %s from %s: %s",
peerFQDN,
client.Hostname(),
err,
)
}

ips, err := peer.IPs()
if err != nil {
t.Fatalf(
"failed to get ips for %s: %s",
peer.Hostname(),
err,
)
}

for _, ip := range ips {
if !strings.Contains(result, ip.String()) {
t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result)
}
}
}
}
}

// TestValidateResolvConf validates that the resolv.conf file
// ends up as expected in our Tailscale containers.
// All the containers are based on Alpine, meaning Tailscale
// will overwrite the resolv.conf file.
// On other platform, Tailscale will integrate with a dns manager
// if available (like Systemd).
func TestValidateResolvConf(t *testing.T) {
IntegrationSkip(t)

resolvconf := func(conf string) string {
return strings.ReplaceAll(`# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
`+conf, "\t", "")
}

tests := []struct {
name string
conf map[string]string
wantConfCompareFunc func(*testing.T, string)
}{
// New config
{
name: "base-integration-config",
conf: map[string]string{
"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net",
},
wantConfCompareFunc: func(t *testing.T, got string) {
want := resolvconf(`
nameserver 100.100.100.100
search very-unique-domain.net
`)
assert.Equal(t, want, got)
},
},
{
name: "base-magic-dns-off",
conf: map[string]string{
"HEADSCALE_DNS_MAGIC_DNS": "false",
"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net",
},
wantConfCompareFunc: func(t *testing.T, got string) {
want := resolvconf(`
nameserver 100.100.100.100
search very-unique-domain.net
`)
assert.Equal(t, want, got)
},
},
{
name: "base-extra-search-domains",
conf: map[string]string{
"HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no",
"HEADSCALE_DNS_BASE_DOMAIN": "with-local-dns.net",
},
wantConfCompareFunc: func(t *testing.T, got string) {
want := resolvconf(`
nameserver 100.100.100.100
search with-local-dns.net test1.no test2.no
`)
assert.Equal(t, want, got)
},
},
{
name: "base-nameservers-split",
conf: map[string]string{
"HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
"HEADSCALE_DNS_BASE_DOMAIN": "with-local-dns.net",
},
wantConfCompareFunc: func(t *testing.T, got string) {
want := resolvconf(`
nameserver 100.100.100.100
search with-local-dns.net
`)
assert.Equal(t, want, got)
},
},
{
name: "base-full-no-magic",
conf: map[string]string{
"HEADSCALE_DNS_MAGIC_DNS": "false",
"HEADSCALE_DNS_BASE_DOMAIN": "all-of.it",
"HEADSCALE_DNS_NAMESERVERS_GLOBAL": `8.8.8.8`,
"HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no",
// TODO(kradalby): this currently isnt working, need to fix it
// "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
// "HEADSCALE_DNS_EXTRA_RECORDS": `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`,
},
wantConfCompareFunc: func(t *testing.T, got string) {
want := resolvconf(`
nameserver 100.100.100.100
search all-of.it test1.no test2.no
`)
assert.Equal(t, want, got)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scenario, err := NewScenario(dockertestMaxWait())
assertNoErr(t, err)
defer scenario.Shutdown()

spec := map[string]int{
"resolvconf1": len(MustTestVersions),
"resolvconf2": len(MustTestVersions),
}

err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("resolvconf"), hsic.WithConfigEnv(tt.conf))
assertNoErrHeadscaleEnv(t, err)

allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

// Poor mans cache
_, err = scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)

_, err = scenario.ListTailscaleClientsIPs()
assertNoErrListClientIPs(t, err)

for _, client := range allClients {
b, err := client.ReadFile("/etc/resolv.conf")
assertNoErr(t, err)

tt.wantConfCompareFunc(t, string(b))
}
})
}

}
68 changes: 0 additions & 68 deletions integration/general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,74 +623,6 @@ func TestTaildrop(t *testing.T) {
}
}

func TestResolveMagicDNS(t *testing.T) {
IntegrationSkip(t)
t.Parallel()

scenario, err := NewScenario(dockertestMaxWait())
assertNoErr(t, err)
defer scenario.Shutdown()

spec := map[string]int{
"magicdns1": len(MustTestVersions),
"magicdns2": len(MustTestVersions),
}

err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns"))
assertNoErrHeadscaleEnv(t, err)

allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)

// assertClientsState(t, allClients)

// Poor mans cache
_, err = scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)

_, err = scenario.ListTailscaleClientsIPs()
assertNoErrListClientIPs(t, err)

for _, client := range allClients {
for _, peer := range allClients {
// It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN()

command := []string{
"tailscale",
"ip", peerFQDN,
}
result, _, err := client.Execute(command)
if err != nil {
t.Fatalf(
"failed to execute resolve/ip command %s from %s: %s",
peerFQDN,
client.Hostname(),
err,
)
}

ips, err := peer.IPs()
if err != nil {
t.Fatalf(
"failed to get ips for %s: %s",
peer.Hostname(),
err,
)
}

for _, ip := range ips {
if !strings.Contains(result, ip.String()) {
t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result)
}
}
}
}
}

func TestExpireNode(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
Expand Down
7 changes: 3 additions & 4 deletions integration/hsic/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ func DefaultConfigEnv() map[string]string {
"HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m",
"HEADSCALE_PREFIXES_V4": "100.64.0.0/10",
"HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48",
"HEADSCALE_DNS_CONFIG_BASE_DOMAIN": "headscale.net",
"HEADSCALE_DNS_CONFIG_MAGIC_DNS": "true",
"HEADSCALE_DNS_CONFIG_DOMAINS": "",
"HEADSCALE_DNS_CONFIG_NAMESERVERS": "127.0.0.11 1.1.1.1",
"HEADSCALE_DNS_BASE_DOMAIN": "headscale.net",
"HEADSCALE_DNS_MAGIC_DNS": "true",
"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "127.0.0.11 1.1.1.1",
"HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key",
"HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key",
"HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080",
Expand Down
1 change: 1 addition & 0 deletions integration/tailscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type TailscaleClient interface {
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Curl(url string, opts ...tsic.CurlOption) (string, error)
ID() string
ReadFile(path string) ([]byte, error)

// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
// and a bool indicating if the clients online count and peer count is equal.
Expand Down
40 changes: 40 additions & 0 deletions integration/tsic/tsic.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tsic

import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -998,3 +1000,41 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
func (t *TailscaleInContainer) SaveLog(path string) error {
return dockertestutil.SaveLog(t.pool, t.container, path)
}

// ReadFile reads a file from the Tailscale container.
// It returns the content of the file as a byte slice.
func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) {
tarBytes, err := integrationutil.FetchPathFromContainer(t.pool, t.container, path)
if err != nil {
return nil, fmt.Errorf("reading file from container: %w", err)
}

var out bytes.Buffer
tr := tar.NewReader(bytes.NewReader(tarBytes))
for {
hdr, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return nil, fmt.Errorf("reading tar header: %w", err)
}

if !strings.Contains(path, hdr.Name) {
return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name)
}

if _, err := io.Copy(&out, tr); err != nil {
return nil, fmt.Errorf("copying file to buffer: %w", err)
}

// Only support reading the first tile
break
}

if out.Len() == 0 {
return nil, fmt.Errorf("file is empty")
}

return out.Bytes(), nil
}

0 comments on commit 9c814d7

Please sign in to comment.