// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause //go:build linux package dns import ( "context" "net/netip" "os" "path/filepath" "testing" "testing/synctest" "time" "tailscale.com/util/dnsname" "tailscale.com/util/eventbus/eventbustest" ) func TestDNSTrampleRecovery(t *testing.T) { t.Cleanup(HookWatchFile.SetForTest(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) } const 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.Errorf("Reading DNS config: %v", 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) } const 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) const 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 a test implementation of the file watcher that uses a timer // instead of inotify. Real inotify (gonotify.NewDirWatcher) creates goroutines // that block on real syscalls, which don't work inside synctest's fake-time // bubble. Instead, we use a one-shot timer that synctest.Wait() will advance, // triggering a callback to check for file trampling. func watchFile(ctx context.Context, dir, filename string, cb func()) error { timer := time.NewTimer(time.Millisecond) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: cb() } <-ctx.Done() return ctx.Err() }