//go:build (linux && !android) || freebsd package dns import ( "fmt" "net/netip" "os" "strings" ) const ( resolvConfPath = "/etc/resolv.conf" resolvConfBackupPath = "/etc/resolv.conf.olm.backup" resolvConfHeader = "# Generated by Olm DNS Manager\n# Original file backed up to " + resolvConfBackupPath + "\n\n" ) // FileDNSConfigurator manages DNS settings by directly modifying /etc/resolv.conf type FileDNSConfigurator struct { originalState *DNSState } // NewFileDNSConfigurator creates a new file-based DNS configurator func NewFileDNSConfigurator() (*FileDNSConfigurator, error) { f := &FileDNSConfigurator{} if err := f.CleanupUncleanShutdown(); err != nil { return nil, fmt.Errorf("cleanup unclean shutdown: %w", err) } return f, nil } // Name returns the configurator name func (f *FileDNSConfigurator) Name() string { return "file-resolv.conf" } // SetDNS sets the DNS servers and returns the original servers func (f *FileDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) { // Get current DNS settings before overriding originalServers, err := f.GetCurrentDNS() if err != nil { return nil, fmt.Errorf("get current DNS: %w", err) } // Backup original resolv.conf if not already backed up if !f.isBackupExists() { if err := f.backupResolvConf(); err != nil { return nil, fmt.Errorf("backup resolv.conf: %w", err) } } // Store original state f.originalState = &DNSState{ OriginalServers: originalServers, ConfiguratorName: f.Name(), } // Write new resolv.conf if err := f.writeResolvConf(servers); err != nil { return nil, fmt.Errorf("write resolv.conf: %w", err) } return originalServers, nil } // RestoreDNS restores the original DNS configuration func (f *FileDNSConfigurator) RestoreDNS() error { if !f.isBackupExists() { return fmt.Errorf("no backup file exists") } // Copy backup back to original location if err := copyFile(resolvConfBackupPath, resolvConfPath); err != nil { return fmt.Errorf("restore from backup: %w", err) } // Remove backup file if err := os.Remove(resolvConfBackupPath); err != nil { return fmt.Errorf("remove backup file: %w", err) } return nil } // CleanupUncleanShutdown removes any DNS configuration left over from a previous crash // For the file-based configurator, we check if a backup file exists (indicating a crash // happened while DNS was configured) and restore from it if so. func (f *FileDNSConfigurator) CleanupUncleanShutdown() error { // Check if backup file exists from a previous session if !f.isBackupExists() { // No backup file, nothing to clean up return nil } // A backup exists, which means we crashed while DNS was configured // Restore the original resolv.conf if err := copyFile(resolvConfBackupPath, resolvConfPath); err != nil { return fmt.Errorf("restore from backup during cleanup: %w", err) } // Remove backup file if err := os.Remove(resolvConfBackupPath); err != nil { return fmt.Errorf("remove backup file during cleanup: %w", err) } return nil } // GetCurrentDNS returns the currently configured DNS servers func (f *FileDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) { content, err := os.ReadFile(resolvConfPath) if err != nil { return nil, fmt.Errorf("read resolv.conf: %w", err) } return f.parseNameservers(string(content)), nil } // backupResolvConf creates a backup of the current resolv.conf func (f *FileDNSConfigurator) backupResolvConf() error { // Get file info for permissions info, err := os.Stat(resolvConfPath) if err != nil { return fmt.Errorf("stat resolv.conf: %w", err) } if err := copyFile(resolvConfPath, resolvConfBackupPath); err != nil { return fmt.Errorf("copy file: %w", err) } // Preserve permissions if err := os.Chmod(resolvConfBackupPath, info.Mode()); err != nil { return fmt.Errorf("chmod backup: %w", err) } return nil } // writeResolvConf writes a new resolv.conf with the specified DNS servers func (f *FileDNSConfigurator) writeResolvConf(servers []netip.Addr) error { if len(servers) == 0 { return fmt.Errorf("no DNS servers provided") } // Get file info for permissions info, err := os.Stat(resolvConfPath) if err != nil { return fmt.Errorf("stat resolv.conf: %w", err) } var content strings.Builder content.WriteString(resolvConfHeader) // Write nameservers for _, server := range servers { content.WriteString("nameserver ") content.WriteString(server.String()) content.WriteString("\n") } // Write the file if err := os.WriteFile(resolvConfPath, []byte(content.String()), info.Mode()); err != nil { return fmt.Errorf("write resolv.conf: %w", err) } return nil } // isBackupExists checks if a backup file exists func (f *FileDNSConfigurator) isBackupExists() bool { _, err := os.Stat(resolvConfBackupPath) return err == nil } // parseNameservers extracts nameserver entries from resolv.conf content func (f *FileDNSConfigurator) parseNameservers(content string) []netip.Addr { var servers []netip.Addr lines := strings.Split(content, "\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 } // copyFile copies a file from src to dst func copyFile(src, dst string) error { content, err := os.ReadFile(src) if err != nil { return fmt.Errorf("read source: %w", err) } // Get source file permissions info, err := os.Stat(src) if err != nil { return fmt.Errorf("stat source: %w", err) } if err := os.WriteFile(dst, content, info.Mode()); err != nil { return fmt.Errorf("write destination: %w", err) } return nil }