// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause //go:build linux package dns import ( "context" "fmt" "net/netip" "os" "path/filepath" "testing" "testing/synctest" "github.com/illarion/gonotify/v3" "tailscale.com/util/dnsname" "tailscale.com/util/eventbus/eventbustest" ) func TestDNSTrampleRecovery(t *testing.T) { HookWatchFile.Set(watchFile) synctest.Test(t, func(t *testing.T) { tmp := t.TempDir() if err := os.MkdirAll(filepath.Join(tmp, "etc"), 0700); err != nil { t.Fatal(err) } resolvPath := "/etc/resolv.conf" fs := directFS{prefix: tmp} readFile := func(t *testing.T, path string) string { t.Helper() b, err := fs.ReadFile(path) if err != nil { t.Fatal(err) } return string(b) } bus := eventbustest.NewBus(t) eventbustest.LogAllEvents(t, bus) m := newDirectManagerOnFS(t.Logf, nil, bus, fs) defer m.Close() if err := m.SetDNS(OSConfig{ Nameservers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")}, SearchDomains: []dnsname.FQDN{"ts.net.", "ts-dns.test."}, MatchDomains: []dnsname.FQDN{"ignored."}, }); err != nil { t.Fatal(err) } want := `# 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 nameserver 8.8.8.8 nameserver 8.8.4.4 search ts.net ts-dns.test ` if got := readFile(t, resolvPath); got != want { t.Fatalf("resolv.conf:\n%s, want:\n%s", got, want) } tw := eventbustest.NewWatcher(t, bus) trample := "Hvem er det som tramper på min bro?" if err := fs.WriteFile(resolvPath, []byte(trample), 0644); err != nil { t.Fatal(err) } synctest.Wait() if err := eventbustest.Expect(tw, eventbustest.Type[TrampleDNS]()); err != nil { t.Errorf("did not see trample event: %s", err) } }) } // watchFile is generally copied from linuxtrample, but cancels the context // after one trample to end the test. func watchFile(ctx context.Context, dir, filename string, cb func()) error { ctx, cancel := context.WithCancel(ctx) defer cancel() const events = gonotify.IN_ATTRIB | gonotify.IN_CLOSE_WRITE | gonotify.IN_CREATE | gonotify.IN_DELETE | gonotify.IN_MODIFY | gonotify.IN_MOVE watcher, err := gonotify.NewDirWatcher(ctx, events, dir) if err != nil { return fmt.Errorf("NewDirWatcher: %w", err) } for { select { case event := <-watcher.C: if event.Name == filename { cb() cancel() } case <-ctx.Done(): return ctx.Err() } } }