mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 12:59:08 +02:00
* [client] Split resolv.conf parsing from file reading Pure refactor, no behavior change. parseResolvConf parses content that was already read, so a caller can compare and parse the same snapshot of the file instead of reading it twice. * [client] Return the written content from the resolv.conf update function Pure refactor, no behavior change. The repair watcher needs to know what NetBird wrote so it can tell its own write apart from a third party's. Callers ignore the content for now. * [client] Keep the resolv.conf watch armed while repairing In file mode the repair watcher removed its inotify watch, rewrote resolv.conf and re-added the watch, without looking at the file again. A second third-party write landing in that window raised no event, so the file stayed on the upstream resolver until something else touched it. systemd-resolved with the stub listener off, the Amazon Linux 2023 default, writes the file twice in a row on restart and does not rewrite it on an unchanged DHCP renewal, so the host stayed off NetBird DNS while the client reported connected. The watch now stays armed during the repair. NetBird's own write is recognized by comparing the file with the content it wrote, which also covers a write whose search list was truncated to the resolv.conf limits and would otherwise fail the parameter check forever. The watcher checks the file once when it is armed, since a change made before that produced no event. * [client] Consume inotify errors in the resolv.conf watcher fsnotify blocks its reader until an error is received. The watcher never read the Errors channel, so a single error, such as an event queue overflow, stopped all further events and silently disabled the repair. The watcher now consumes errors and checks the file, since an overflow means events were dropped. * [client] Parse resolv.conf one line at a time Pure refactor, no behavior change. The per-line handling moves into parseResolvConfLine, which keeps parseResolvConf under the cognitive complexity limit. * [client] Drop the comment restating parseResolvConfLine
194 lines
5.0 KiB
Go
194 lines
5.0 KiB
Go
//go:build (linux && !android) || freebsd
|
|
|
|
package dns
|
|
|
|
import (
|
|
"bytes"
|
|
"net/netip"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
|
)
|
|
|
|
var (
|
|
eventTypes = []fsnotify.Op{
|
|
fsnotify.Create,
|
|
fsnotify.Write,
|
|
fsnotify.Remove,
|
|
fsnotify.Rename,
|
|
}
|
|
)
|
|
|
|
// repairConfFn writes the NetBird-managed resolv.conf and returns the exact content it wrote.
|
|
type repairConfFn func([]string, netip.Addr, *resolvConf, *statemanager.Manager) ([]byte, error)
|
|
|
|
type repair struct {
|
|
operationFile string
|
|
updateFn repairConfFn
|
|
watchDir string
|
|
|
|
inotify *fsnotify.Watcher
|
|
inotifyWg sync.WaitGroup
|
|
}
|
|
|
|
func newRepair(operationFile string, updateFn repairConfFn) *repair {
|
|
targetFile := targetFile(operationFile)
|
|
return &repair{
|
|
operationFile: targetFile,
|
|
watchDir: path.Dir(targetFile),
|
|
updateFn: updateFn,
|
|
}
|
|
}
|
|
|
|
// watchFileChanges keeps resolv.conf pointing at NetBird. written is the content NetBird last
|
|
// wrote to the file, used to recognize the echo of its own writes.
|
|
func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP netip.Addr, written []byte, stateManager *statemanager.Manager) {
|
|
if f.inotify != nil {
|
|
return
|
|
}
|
|
|
|
log.Infof("start to watch resolv.conf: %s", f.operationFile)
|
|
inotify, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Errorf("failed to start inotify watcher for resolv.conf: %s", err)
|
|
return
|
|
}
|
|
|
|
if err := inotify.Add(f.watchDir); err != nil {
|
|
log.Errorf("failed to add inotify watch for resolv.conf: %s", err)
|
|
if err := inotify.Close(); err != nil {
|
|
log.Debugf("failed to close resolv.conf inotify: %v", err)
|
|
}
|
|
return
|
|
}
|
|
f.inotify = inotify
|
|
|
|
f.inotifyWg.Add(1)
|
|
go func() {
|
|
defer f.inotifyWg.Done()
|
|
|
|
// A change that landed before the watch was armed produced no event.
|
|
lastWritten := f.checkAndRepair(nbSearchDomains, nbNameserverIP, written, stateManager)
|
|
for {
|
|
select {
|
|
case event, ok := <-inotify.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if !f.isEventRelevant(event) {
|
|
continue
|
|
}
|
|
case err, ok := <-inotify.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
// fsnotify stops delivering events until an error is received, and an
|
|
// error such as a queue overflow means events were dropped.
|
|
log.Warnf("resolv.conf inotify error, checking the file: %v", err)
|
|
}
|
|
lastWritten = f.checkAndRepair(nbSearchDomains, nbNameserverIP, lastWritten, stateManager)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// checkAndRepair rewrites resolv.conf when NetBird's parameters are missing from it and returns
|
|
// the content NetBird last wrote. The watch stays armed while repairing, so a third-party write
|
|
// racing the repair still raises an event; the echo of NetBird's own write is skipped by content.
|
|
func (f *repair) checkAndRepair(nbSearchDomains []string, nbNameserverIP netip.Addr, lastWritten []byte, stateManager *statemanager.Manager) []byte {
|
|
content, err := os.ReadFile(f.operationFile)
|
|
if err != nil {
|
|
log.Warnf("failed to read resolv.conf: %s", err)
|
|
return lastWritten
|
|
}
|
|
|
|
if bytes.Equal(content, lastWritten) {
|
|
log.Tracef("%s still holds the NetBird managed content, skip the check", f.operationFile)
|
|
return lastWritten
|
|
}
|
|
|
|
log.Tracef("%s changed, check if it is broken", f.operationFile)
|
|
|
|
rConf, err := parseResolvConf(content)
|
|
if err != nil {
|
|
log.Warnf("failed to parse resolv conf: %s", err)
|
|
return lastWritten
|
|
}
|
|
|
|
log.Debugf("check resolv.conf parameters: %s", rConf)
|
|
if !isNbParamsMissing(nbSearchDomains, nbNameserverIP, rConf) {
|
|
log.Tracef("resolv.conf still correct, skip the update")
|
|
return lastWritten
|
|
}
|
|
log.Info("broken params in resolv.conf, repairing it...")
|
|
|
|
written, err := f.updateFn(nbSearchDomains, nbNameserverIP, rConf, stateManager)
|
|
if err != nil {
|
|
log.Errorf("failed to repair resolv.conf: %v", err)
|
|
return lastWritten
|
|
}
|
|
return written
|
|
}
|
|
|
|
func (f *repair) stopWatchFileChanges() {
|
|
if f.inotify == nil {
|
|
return
|
|
}
|
|
err := f.inotify.Close()
|
|
if err != nil {
|
|
log.Warnf("failed to close resolv.conf inotify: %v", err)
|
|
}
|
|
f.inotifyWg.Wait()
|
|
f.inotify = nil
|
|
}
|
|
|
|
func (f *repair) isEventRelevant(event fsnotify.Event) bool {
|
|
var ok bool
|
|
for _, et := range eventTypes {
|
|
if event.Has(et) {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if event.Name == f.operationFile {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// nbParamsAreMissing checks if the resolv.conf file contains all the parameters that NetBird needs
|
|
// check the NetBird related nameserver IP at the first place
|
|
// check the NetBird related search domains in the search domains list
|
|
func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rConf *resolvConf) bool {
|
|
if !isContains(nbSearchDomains, rConf.searchDomains) {
|
|
return true
|
|
}
|
|
|
|
if len(rConf.nameServers) == 0 {
|
|
return true
|
|
}
|
|
|
|
if rConf.nameServers[0] != nbNameserverIP {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func targetFile(filename string) string {
|
|
target, err := filepath.EvalSymlinks(filename)
|
|
if err != nil {
|
|
log.Errorf("evarl err: %s", err)
|
|
}
|
|
return target
|
|
}
|