// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package dns import ( "bytes" "context" "errors" "github.com/illarion/gonotify" "tailscale.com/health" ) func (m *directManager) runFileWatcher() { in, err := gonotify.NewInotify() if err != nil { // Oh well, we tried. This is all best effort for now, to // surface warnings to users. m.logf("dns: inotify new: %v", err) return } ctx, cancel := context.WithCancel(m.ctx) defer cancel() go m.closeInotifyOnDone(ctx, in) const events = gonotify.IN_ATTRIB | gonotify.IN_CLOSE_WRITE | gonotify.IN_CREATE | gonotify.IN_DELETE | gonotify.IN_MODIFY | gonotify.IN_MOVE if err := in.AddWatch("/etc/", events); err != nil { m.logf("dns: inotify addwatch: %v", err) return } for { events, err := in.Read() if ctx.Err() != nil { return } if err != nil { m.logf("dns: inotify read: %v", err) return } var match bool for _, ev := range events { if ev.Name == resolvConf { match = true break } } if !match { continue } m.checkForFileTrample() } } var warnTrample = health.NewWarnable() // checkForFileTrample checks whether /etc/resolv.conf has been trampled // by another program on the system. (e.g. a DHCP client) func (m *directManager) checkForFileTrample() { m.mu.Lock() want := m.wantResolvConf lastWarn := m.lastWarnContents m.mu.Unlock() if want == nil { return } cur, err := m.fs.ReadFile(resolvConf) if err != nil { m.logf("trample: read error: %v", err) return } if bytes.Equal(cur, want) { m.health.SetWarnable(warnTrample, nil) if lastWarn != nil { m.mu.Lock() m.lastWarnContents = nil m.mu.Unlock() m.logf("trample: resolv.conf again matches expected content") } return } if bytes.Equal(cur, lastWarn) { // We already logged about this, so not worth doing it again. return } m.mu.Lock() m.lastWarnContents = cur m.mu.Unlock() show := cur if len(show) > 1024 { show = show[:1024] } m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show) m.health.SetWarnable(warnTrample, errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight")) } func (m *directManager) closeInotifyOnDone(ctx context.Context, in *gonotify.Inotify) { <-ctx.Done() in.Close() }