tailscale/net/dns/direct_linux_test.go
Brad Fitzpatrick 49eb1b5d26 net/dns: fix TestDNSTrampleRecovery failure under flakestress
The test had two problems:

1. runFileWatcher passed hardcoded "/etc/" to the inotify watcher,
   but the test filesystem uses a temp directory prefix. The watcher
   was watching the real /etc/, never seeing the test's file writes.

2. The test's watchFile used gonotify.NewDirWatcher which creates
   goroutines that block on real inotify syscalls. These don't work
   inside synctest's fake-time bubble. The test only passed standalone
   by accident: gonotify walks /etc/ on startup producing fake events
   that happened to trigger trample detection at the right time.

Fix the path issue by adding ActualPath to the wholeFileFS interface,
which translates logical paths (like "/etc/resolv.conf") to real
filesystem paths (respecting any test prefix). Use it in
runFileWatcher so the inotify watch targets the correct directory.

Replace gonotify in the test with a one-shot timer that synctest can
advance through fake time, reliably triggering the trample check.

Fixes #19400

Change-Id: Idb252881ec24d0ab3b3c1d154dbdaf532db837d4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-14 06:55:35 -07:00

95 lines
2.5 KiB
Go

// 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()
}