Files
olm/dns/platform/resolvconf.go
Laurence 8549dc8746 enhance(dns): expose stale cleanup functionality
When the tunnel is forced close an integration may want to manually call cleanup function to fix stale issues without having the knowledge of which configuration to cleanup
2026-02-26 11:30:12 +00:00

256 lines
7.1 KiB
Go

//go:build (linux && !android) || freebsd
package dns
import (
"bytes"
"fmt"
"net/netip"
"os/exec"
"strings"
)
const resolvconfCommand = "resolvconf"
// ResolvconfDNSConfigurator manages DNS settings using the resolvconf utility
type ResolvconfDNSConfigurator struct {
ifaceName string
implType string
originalState *DNSState
}
// NewResolvconfDNSConfigurator creates a new resolvconf DNS configurator
func NewResolvconfDNSConfigurator(ifaceName string) (*ResolvconfDNSConfigurator, error) {
if ifaceName == "" {
return nil, fmt.Errorf("interface name is required")
}
// Detect resolvconf implementation type
implType, err := detectResolvconfType()
if err != nil {
return nil, fmt.Errorf("detect resolvconf type: %w", err)
}
configurator := &ResolvconfDNSConfigurator{
ifaceName: ifaceName,
implType: implType,
}
// Call cleanup function to remove any stale DNS config for this interface
if err := configurator.CleanupUncleanShutdown(); err != nil {
return nil, fmt.Errorf("cleanup unclean shutdown: %w", err)
}
return configurator, nil
}
// Name returns the configurator name
func (r *ResolvconfDNSConfigurator) Name() string {
return fmt.Sprintf("resolvconf-%s", r.implType)
}
// SetDNS sets the DNS servers and returns the original servers
func (r *ResolvconfDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) {
// Get current DNS settings before overriding
originalServers, err := r.GetCurrentDNS()
if err != nil {
// If we can't get current DNS, proceed anyway
originalServers = []netip.Addr{}
}
// Store original state
r.originalState = &DNSState{
OriginalServers: originalServers,
ConfiguratorName: r.Name(),
}
// Apply new DNS servers
if err := r.applyDNSServers(servers); err != nil {
return nil, fmt.Errorf("apply DNS servers: %w", err)
}
return originalServers, nil
}
// RestoreDNS restores the original DNS configuration
func (r *ResolvconfDNSConfigurator) RestoreDNS() error {
var cmd *exec.Cmd
switch r.implType {
case "openresolv":
// Force delete with -f
cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName)
default:
cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName)
}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("delete resolvconf config: %w, output: %s", err, out)
}
return nil
}
// CleanupUncleanShutdown removes any DNS configuration left over from a previous crash
// For resolvconf, we attempt to delete any entry for the interface name.
// This ensures that if the process crashed while DNS was configured, the stale
// entry is removed on the next startup.
func (r *ResolvconfDNSConfigurator) CleanupUncleanShutdown() error {
// Try to delete any existing entry for this interface
// This is idempotent - if no entry exists, resolvconf will just return success
var cmd *exec.Cmd
switch r.implType {
case "openresolv":
cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName)
default:
cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName)
}
// Ignore errors - the entry may not exist, which is fine
_ = cmd.Run()
return nil
}
// GetCurrentDNS returns the currently configured DNS servers
func (r *ResolvconfDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) {
// resolvconf doesn't provide a direct way to query per-interface DNS
// We can try to read /etc/resolv.conf but it's merged from all sources
content, err := exec.Command(resolvconfCommand, "-l").CombinedOutput()
if err != nil {
// Fall back to reading resolv.conf
return readResolvConfServers()
}
// Parse the output (format varies by implementation)
return parseResolvconfOutput(string(content)), nil
}
// applyDNSServers applies DNS server configuration via resolvconf
func (r *ResolvconfDNSConfigurator) applyDNSServers(servers []netip.Addr) error {
if len(servers) == 0 {
return fmt.Errorf("no DNS servers provided")
}
// Build resolv.conf content
var content bytes.Buffer
content.WriteString("# Generated by Olm DNS Manager\n\n")
for _, server := range servers {
content.WriteString("nameserver ")
content.WriteString(server.String())
content.WriteString("\n")
}
// Apply via resolvconf
var cmd *exec.Cmd
switch r.implType {
case "openresolv":
// OpenResolv supports exclusive mode with -x
cmd = exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName)
default:
cmd = exec.Command(resolvconfCommand, "-a", r.ifaceName)
}
cmd.Stdin = &content
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apply resolvconf config: %w, output: %s", err, out)
}
return nil
}
// detectResolvconfType detects which resolvconf implementation is being used
func detectResolvconfType() (string, error) {
cmd := exec.Command(resolvconfCommand, "--version")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("detect resolvconf type: %w", err)
}
if strings.Contains(string(out), "openresolv") {
return "openresolv", nil
}
return "resolvconf", nil
}
// parseResolvconfOutput parses resolvconf -l output for DNS servers
func parseResolvconfOutput(output string) []netip.Addr {
var servers []netip.Addr
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip comments and empty lines
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Look for nameserver lines
if strings.HasPrefix(line, "nameserver") {
fields := strings.Fields(line)
if len(fields) >= 2 {
if addr, err := netip.ParseAddr(fields[1]); err == nil {
servers = append(servers, addr)
}
}
}
}
return servers
}
// readResolvConfServers reads DNS servers from /etc/resolv.conf
func readResolvConfServers() ([]netip.Addr, error) {
cmd := exec.Command("cat", "/etc/resolv.conf")
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("read resolv.conf: %w", err)
}
return parseResolvconfOutput(string(out)), nil
}
// IsResolvconfAvailable checks if resolvconf is available
func IsResolvconfAvailable() bool {
cmd := exec.Command(resolvconfCommand, "--version")
return cmd.Run() == nil
}
// CleanupStaleResolvconfDNS removes any stale DNS configuration left by the resolvconf
// configurator from a previous unclean shutdown. This is a static function that can be
// called without creating a configurator instance, useful for cleanup before network operations.
// The interfaceName parameter specifies which interface entry to clean up (typically "olm").
func CleanupStaleResolvconfDNS(interfaceName string) error {
if !IsResolvconfAvailable() {
// resolvconf not available, nothing to clean up
return nil
}
// Detect resolvconf implementation type
implType, err := detectResolvconfType()
if err != nil {
// Can't detect type, try default
implType = "resolvconf"
}
// Try to delete any existing entry for this interface
// This is idempotent - if no entry exists, resolvconf will just return success
var cmd *exec.Cmd
switch implType {
case "openresolv":
cmd = exec.Command(resolvconfCommand, "-f", "-d", interfaceName)
default:
cmd = exec.Command(resolvconfCommand, "-d", interfaceName)
}
// Ignore errors - the entry may not exist, which is fine
_ = cmd.Run()
return nil
}