mirror of
https://github.com/fosrl/olm.git
synced 2026-07-21 15:21:26 +02:00
178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
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
|
|
}
|