mirror of
https://github.com/fosrl/olm.git
synced 2026-10-02 18:59:09 +02:00
Groundwork for monitoring and updating the dns
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
)
|
||||
|
||||
const defaultPollInterval = 30 * time.Second
|
||||
|
||||
// SystemDNSMonitor monitors the host system's DNS configuration and notifies
|
||||
// callers when it changes. The reported servers are in "host:port" format
|
||||
// (e.g. "8.8.8.8:53") and can be used directly as UpstreamDNS and PublicDNS.
|
||||
//
|
||||
// Platform behaviour:
|
||||
// - Linux: reads /run/systemd/resolve/resolv.conf when present (updated by
|
||||
// systemd-resolved on every DHCP change), then falls back to
|
||||
// /etc/resolv.conf.olm.backup (written before olm overrides DNS), and
|
||||
// finally /etc/resolv.conf.
|
||||
// - macOS: reads /etc/resolv.conf, which is never modified by olm's
|
||||
// supplemental scutil DNS override.
|
||||
// - Windows: enumerates DHCP-assigned DNS servers from every network adapter
|
||||
// in the registry.
|
||||
// - Other platforms: returns an empty list (no-op monitor).
|
||||
type SystemDNSMonitor struct {
|
||||
mu sync.RWMutex
|
||||
current []string
|
||||
onChange func(servers []string)
|
||||
interval time.Duration
|
||||
stopCh chan struct{}
|
||||
excludeMu sync.RWMutex
|
||||
excludeIPs map[netip.Addr]bool
|
||||
}
|
||||
|
||||
// NewSystemDNSMonitor creates a new monitor. onChange is called with the new
|
||||
// server list whenever a change is detected; it is also called once from Start
|
||||
// with the initial values. A zero interval uses the 30-second default.
|
||||
func NewSystemDNSMonitor(interval time.Duration, onChange func(servers []string)) *SystemDNSMonitor {
|
||||
if interval <= 0 {
|
||||
interval = defaultPollInterval
|
||||
}
|
||||
return &SystemDNSMonitor{
|
||||
interval: interval,
|
||||
onChange: onChange,
|
||||
stopCh: make(chan struct{}),
|
||||
excludeIPs: make(map[netip.Addr]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// SetExcludeIP registers an IP address that must never appear in the reported
|
||||
// DNS server list. Call this after olm's DNS proxy is created to prevent the
|
||||
// proxy's own IP from being returned as an upstream server when the OS DNS has
|
||||
// been overridden to point at the proxy.
|
||||
func (m *SystemDNSMonitor) SetExcludeIP(ip netip.Addr) {
|
||||
m.excludeMu.Lock()
|
||||
m.excludeIPs[ip.Unmap()] = true
|
||||
m.excludeMu.Unlock()
|
||||
}
|
||||
|
||||
// Start reads the current system DNS immediately, fires onChange, then polls
|
||||
// in the background until Stop is called or ctx is cancelled.
|
||||
func (m *SystemDNSMonitor) Start(ctx context.Context) {
|
||||
servers := m.readFiltered()
|
||||
m.mu.Lock()
|
||||
m.current = servers
|
||||
m.mu.Unlock()
|
||||
|
||||
if m.onChange != nil && len(servers) > 0 {
|
||||
m.onChange(servers)
|
||||
}
|
||||
|
||||
go m.run(ctx)
|
||||
}
|
||||
|
||||
// Stop halts the background polling goroutine.
|
||||
func (m *SystemDNSMonitor) Stop() {
|
||||
select {
|
||||
case <-m.stopCh:
|
||||
default:
|
||||
close(m.stopCh)
|
||||
}
|
||||
}
|
||||
|
||||
// Current returns the most recently observed system DNS servers.
|
||||
func (m *SystemDNSMonitor) Current() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]string, len(m.current))
|
||||
copy(out, m.current)
|
||||
return out
|
||||
}
|
||||
|
||||
// readFiltered calls the platform-specific readSystemDNS and removes any
|
||||
// addresses that have been excluded via SetExcludeIP. If all addresses are
|
||||
// excluded the function returns nil so the caller can retain the last
|
||||
// known-good value.
|
||||
func (m *SystemDNSMonitor) readFiltered() []string {
|
||||
raw := readSystemDNS()
|
||||
|
||||
m.excludeMu.RLock()
|
||||
excludeIPs := m.excludeIPs
|
||||
m.excludeMu.RUnlock()
|
||||
|
||||
if len(excludeIPs) == 0 {
|
||||
return raw
|
||||
}
|
||||
|
||||
var filtered []string
|
||||
for _, s := range raw {
|
||||
host, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
filtered = append(filtered, s)
|
||||
continue
|
||||
}
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil || excludeIPs[addr.Unmap()] {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (m *SystemDNSMonitor) run(ctx context.Context) {
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-m.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
servers := m.readFiltered()
|
||||
if len(servers) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
changed := !dnsSlicesEqual(m.current, servers)
|
||||
if changed {
|
||||
m.current = servers
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
if changed && m.onChange != nil {
|
||||
logger.Info("System DNS changed: %v", servers)
|
||||
m.onChange(servers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dnsSlicesEqual reports whether two server lists are equal regardless of order.
|
||||
func dnsSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
ac := make([]string, len(a))
|
||||
bc := make([]string, len(b))
|
||||
copy(ac, a)
|
||||
copy(bc, b)
|
||||
sort.Strings(ac)
|
||||
sort.Strings(bc)
|
||||
for i := range ac {
|
||||
if ac[i] != bc[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user