Files
olm/dns/platform/file.go
2025-12-18 11:33:59 -05:00

221 lines
5.8 KiB
Go

//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
}