From 77bcc32d3c62468fcb981af1f34a903c92dc4db7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Dec 2024 08:38:04 +0100 Subject: [PATCH] adjust Signed-off-by: Kristoffer Dalby --- Dockerfile.integration | 2 +- config-example.yaml | 5 ++ hscontrol/app.go | 3 ++ hscontrol/dns/extrarecords.go | 35 +++++++++----- integration/dns_test.go | 91 +++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 12 deletions(-) diff --git a/Dockerfile.integration b/Dockerfile.integration index cf55bd7476b..735cdba5887 100644 --- a/Dockerfile.integration +++ b/Dockerfile.integration @@ -8,7 +8,7 @@ ENV GOPATH /go WORKDIR /go/src/headscale RUN apt-get update \ - && apt-get install --no-install-recommends --yes less jq sqlite3 \ + && apt-get install --no-install-recommends --yes less jq sqlite3 dnsutils \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean RUN mkdir -p /var/run/headscale diff --git a/config-example.yaml b/config-example.yaml index 93204398040..521e3a3e08e 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -311,6 +311,11 @@ dns: # # you can also put it in one line # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } + # Path to a JSON file containing a list of extra DNS records, on the format of the `extra_records` field. + # Updates to the file will automatically be picked up by headscale and propagated to the nodes + # This option is mutual exclusive with the `extra_records` field. + # extra_records_path: "" + # DEPRECATED # Use the username as part of the DNS name for nodes, with this option enabled: # node1.username.example.com diff --git a/hscontrol/app.go b/hscontrol/app.go index 52b565174ee..86f21327a7f 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -603,6 +603,9 @@ func (h *Headscale) Serve() error { if err != nil { return fmt.Errorf("setting up : %w", err) } + h.cfg.TailcfgDNSConfig.ExtraRecords = h.extraRecordMan.Records() + go h.extraRecordMan.Run() + defer h.extraRecordMan.Close() } // Start all scheduled tasks, e.g. expiring nodes, derp updates and diff --git a/hscontrol/dns/extrarecords.go b/hscontrol/dns/extrarecords.go index 55a393d0807..16232b7f7ff 100644 --- a/hscontrol/dns/extrarecords.go +++ b/hscontrol/dns/extrarecords.go @@ -45,11 +45,20 @@ func NewExtraRecordsMan(path string) (*ExtraRecordsMan, error) { return nil, fmt.Errorf("adding path to watcher: %w", err) } + records, hash, err := readExtraRecordsFromPath(path) + if err != nil { + return nil, fmt.Errorf("reading extra records from path: %w", err) + } + + log.Trace().Caller().Strs("watching", watcher.WatchList()).Msg("started filewatcher") + return &ExtraRecordsMan{ watcher: watcher, path: path, - records: set.Set[tailcfg.DNSRecord]{}, - hashes: map[string][32]byte{}, + records: set.SetOf(records), + hashes: map[string][32]byte{ + path: hash, + }, closeCh: make(chan struct{}), }, nil } @@ -70,19 +79,23 @@ func (e *ExtraRecordsMan) Run() { select { case <-e.closeCh: return - case _, ok := <-e.watcher.Events: + case event, ok := <-e.watcher.Events: + log.Trace().Caller().Str("path", event.Name).Str("op", event.Op.String()).Msg("extra records received filewatch event") if !ok { - log.Error().Msgf("error reading file watcher event of channel, records watcher closing") + log.Error().Caller().Msgf("error reading file watcher event of channel, records watcher closing") return } + if event.Name != e.path { + continue + } e.updateRecords() - case err, ok := <-e.watcher.Errors: - if !ok { - log.Error().Msgf("error reading file watcher event of channel, records watcher closing") - return - } - log.Error().Err(err).Msgf("extra records filewatcher returned error") + // case err, ok := <-e.watcher.Errors: + // if !ok { + // log.Error().Caller().Msgf("error reading file watcher error of channel, records watcher closing") + // return + // } + // log.Error().Caller().Err(err).Msgf("extra records filewatcher returned error: %q", err) } } } @@ -94,7 +107,7 @@ func (e *ExtraRecordsMan) Close() { func (e *ExtraRecordsMan) updateRecords() { records, newHash, err := readExtraRecordsFromPath(e.path) if err != nil { - log.Error().Err(err).Msgf("reading extra records from path: %s", e.path) + log.Error().Caller().Err(err).Msgf("reading extra records from path: %s", e.path) return } diff --git a/integration/dns_test.go b/integration/dns_test.go index efe702e9d9c..d7bbc175da6 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -1,6 +1,7 @@ package integration import ( + "encoding/json" "fmt" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" + "tailscale.com/tailcfg" ) func TestResolveMagicDNS(t *testing.T) { @@ -81,6 +83,95 @@ func TestResolveMagicDNS(t *testing.T) { } } +func TestResolveMagicDNSExtraRecordsPath(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + // defer scenario.ShutdownAssertNoPanics(t) + + spec := map[string]int{ + "magicdns1": 1, + "magicdns2": 1, + } + + const erPath = "/tmp/extra_records.json" + + extraRecords := []tailcfg.DNSRecord{ + { + Name: "test.myvpn.example.com", + Type: "A", + Value: "6.6.6.6", + }, + } + b, _ := json.Marshal(extraRecords) + + err = scenario.CreateHeadscaleEnv(spec, []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("extrarecords"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_DNS_EXTRA_RECORDS_PATH": erPath, + }), + hsic.WithFileInContainer(erPath, b), + ) + 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 { + stdout, stderr, err := client.Execute([]string{"dig", "test.myvpn.example.com"}) + if err != nil { + t.Errorf("stderr: %s", stderr) + t.Errorf("executing dig command: %s", err) + } + + assert.Contains(t, stdout, "6.6.6.6") + } + + extraRecords = append(extraRecords, tailcfg.DNSRecord{ + Name: "otherrecord.myvpn.example.com", + Type: "A", + Value: "7.7.7.7", + }) + b2, _ := json.Marshal(extraRecords) + + hs, err := scenario.Headscale() + assertNoErr(t, err) + + err = hs.WriteFile(erPath, b2) + assertNoErr(t, err) + + time.Sleep(5 * time.Second) + for _, client := range allClients { + stdout, stderr, err := client.Execute([]string{"dig", "test.myvpn.example.com"}) + assert.Errorf(t, err, "stderr: %s", stderr) + assert.Contains(t, stdout, "6.6.6.6") + + stdout, stderr, err = client.Execute([]string{"dig", "otherrecord.myvpn.example.com"}) + assert.Errorf(t, err, "stderr: %s", stderr) + assert.Contains(t, stdout, "7.7.7.7") + } +} + // TestValidateResolvConf validates that the resolv.conf file // ends up as expected in our Tailscale containers. // All the containers are based on Alpine, meaning Tailscale