mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
debugging debugging debugging debugging use default route interface IP for OS resolver queries remove retries fix resolv.conf clobbering on MacOS, set custom local addr for os resolver queries remove the client info discovery logic on network change, this was overkill just for the IP, and was causing service failure after switching networks many times rapidly handle ipv6 local addresses guard ciTable from nil pointer debugging failure count
165 lines
4.5 KiB
Go
165 lines
4.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
// parseResolvConfNameservers reads the resolv.conf file and returns the nameservers found.
|
|
// Returns nil if no nameservers are found.
|
|
func (p *prog) parseResolvConfNameservers(path string) ([]string, error) {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the file for "nameserver" lines
|
|
var currentNS []string
|
|
lines := strings.Split(string(content), "\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "nameserver") {
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) >= 2 {
|
|
currentNS = append(currentNS, parts[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
return currentNS, nil
|
|
}
|
|
|
|
// watchResolvConf watches any changes to /etc/resolv.conf file,
|
|
// and reverting to the original config set by ctrld.
|
|
func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) {
|
|
resolvConfPath := "/etc/resolv.conf"
|
|
// Evaluating symbolics link to watch the target file that /etc/resolv.conf point to.
|
|
if rp, _ := filepath.EvalSymlinks(resolvConfPath); rp != "" {
|
|
resolvConfPath = rp
|
|
}
|
|
mainLog.Load().Debug().Msgf("start watching %s file", resolvConfPath)
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf")
|
|
return
|
|
}
|
|
defer watcher.Close()
|
|
|
|
// We watch /etc instead of /etc/resolv.conf directly,
|
|
// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
|
|
watchDir := filepath.Dir(resolvConfPath)
|
|
if err := watcher.Add(watchDir); err != nil {
|
|
mainLog.Load().Warn().Err(err).Msgf("could not add %s to watcher list", watchDir)
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-p.dnsWatcherStopCh:
|
|
return
|
|
case <-p.stopCh:
|
|
mainLog.Load().Debug().Msgf("stopping watcher for %s", resolvConfPath)
|
|
return
|
|
case event, ok := <-watcher.Events:
|
|
if p.leakingQueryReset.Load() {
|
|
return
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes.
|
|
continue
|
|
}
|
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
|
mainLog.Load().Debug().Msgf("/etc/resolv.conf changes detected, reading changes...")
|
|
|
|
// Convert expected nameservers to strings for comparison
|
|
expectedNS := make([]string, len(ns))
|
|
for i, addr := range ns {
|
|
expectedNS[i] = addr.String()
|
|
}
|
|
|
|
var foundNS []string
|
|
var err error
|
|
|
|
maxRetries := 1
|
|
for retry := 0; retry < maxRetries; retry++ {
|
|
foundNS, err = p.parseResolvConfNameservers(resolvConfPath)
|
|
if err != nil {
|
|
mainLog.Load().Error().Err(err).Msg("failed to read resolv.conf content")
|
|
break
|
|
}
|
|
|
|
// If we found nameservers, break out of retry loop
|
|
if len(foundNS) > 0 {
|
|
break
|
|
}
|
|
|
|
// Only retry if we found no nameservers
|
|
if retry < maxRetries-1 {
|
|
mainLog.Load().Debug().Msgf("resolv.conf has no nameserver entries, retry %d/%d in 2 seconds", retry+1, maxRetries)
|
|
select {
|
|
case <-p.stopCh:
|
|
return
|
|
case <-p.dnsWatcherStopCh:
|
|
return
|
|
case <-time.After(2 * time.Second):
|
|
continue
|
|
}
|
|
} else {
|
|
mainLog.Load().Debug().Msg("resolv.conf remained empty after all retries")
|
|
}
|
|
}
|
|
|
|
// If we found nameservers, check if they match what we expect
|
|
if len(foundNS) > 0 {
|
|
// Check if the nameservers match exactly what we expect
|
|
matches := len(foundNS) == len(expectedNS)
|
|
if matches {
|
|
for i := range foundNS {
|
|
if foundNS[i] != expectedNS[i] {
|
|
matches = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
mainLog.Load().Debug().
|
|
Strs("found", foundNS).
|
|
Strs("expected", expectedNS).
|
|
Bool("matches", matches).
|
|
Msg("checking nameservers")
|
|
|
|
// Only revert if the nameservers don't match
|
|
if !matches {
|
|
if err := watcher.Remove(watchDir); err != nil {
|
|
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
|
continue
|
|
}
|
|
|
|
if err := setDnsFn(iface, ns); err != nil {
|
|
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
|
}
|
|
|
|
if err := watcher.Add(watchDir); err != nil {
|
|
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf")
|
|
}
|
|
}
|
|
}
|