diff --git a/dns/platform/README.md b/dns/platform/README.md new file mode 100644 index 0000000..0873c2f --- /dev/null +++ b/dns/platform/README.md @@ -0,0 +1,263 @@ +# DNS Platform Module + +A standalone Go module for managing system DNS settings across different platforms and DNS management systems. + +## Overview + +This module provides a unified interface for overriding system DNS servers on: +- **macOS**: Using `scutil` +- **Windows**: Using Windows Registry +- **Linux/FreeBSD**: Supporting multiple backends: + - systemd-resolved (D-Bus) + - NetworkManager (D-Bus) + - resolvconf utility + - Direct `/etc/resolv.conf` manipulation + +## Features + +- ✅ Cross-platform DNS override +- ✅ Automatic detection of best DNS management method +- ✅ Backup and restore original DNS settings +- ✅ Platform-specific optimizations +- ✅ No external dependencies for basic functionality + +## Architecture + +### Interface + +All configurators implement the `DNSConfigurator` interface: + +```go +type DNSConfigurator interface { + SetDNS(servers []netip.Addr) ([]netip.Addr, error) + RestoreDNS() error + GetCurrentDNS() ([]netip.Addr, error) + Name() string +} +``` + +### Platform-Specific Implementations + +Each platform has dedicated structs instead of using build tags at the file level: + +- `DarwinDNSConfigurator` - macOS using scutil +- `WindowsDNSConfigurator` - Windows using registry +- `FileDNSConfigurator` - Unix using /etc/resolv.conf +- `SystemdResolvedDNSConfigurator` - Linux using systemd-resolved +- `NetworkManagerDNSConfigurator` - Linux using NetworkManager +- `ResolvconfDNSConfigurator` - Linux using resolvconf utility + +## Usage + +### Automatic Detection + +```go +import "github.com/your-org/olm/dns/platform" + +// On Linux/Unix - provide interface name for best results +configurator, err := platform.DetectBestConfigurator("eth0") +if err != nil { + log.Fatal(err) +} + +// Set DNS servers +originalServers, err := configurator.SetDNS([]netip.Addr{ + netip.MustParseAddr("8.8.8.8"), + netip.MustParseAddr("8.8.4.4"), +}) +if err != nil { + log.Fatal(err) +} + +// Restore original DNS +defer configurator.RestoreDNS() +``` + +### Manual Selection + +```go +// Linux - Direct file manipulation +configurator, err := platform.NewFileDNSConfigurator() + +// Linux - systemd-resolved +configurator, err := platform.NewSystemdResolvedDNSConfigurator("eth0") + +// Linux - NetworkManager +configurator, err := platform.NewNetworkManagerDNSConfigurator("eth0") + +// Linux - resolvconf +configurator, err := platform.NewResolvconfDNSConfigurator("eth0") + +// macOS +configurator, err := platform.NewDarwinDNSConfigurator() + +// Windows (requires interface GUID) +configurator, err := platform.NewWindowsDNSConfigurator("{GUID-HERE}") +``` + +### Platform Detection Utilities + +```go +// Check if systemd-resolved is available +if platform.IsSystemdResolvedAvailable() { + // Use systemd-resolved +} + +// Check if NetworkManager is available +if platform.IsNetworkManagerAvailable() { + // Use NetworkManager +} + +// Check if resolvconf is available +if platform.IsResolvconfAvailable() { + // Use resolvconf +} + +// Get system DNS servers +servers, err := platform.GetSystemDNS() +``` + +## Implementation Details + +### macOS (Darwin) + +Uses `scutil` to create DNS configuration states in the system configuration database. DNS settings are applied via the Network Service state hierarchy. + +**Pros:** +- Native macOS API +- Proper integration with system preferences +- Supports DNS flushing + +**Cons:** +- Requires elevated privileges + +### Windows + +Modifies registry keys under `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{GUID}`. + +**Pros:** +- Direct registry manipulation +- Immediate effect after cache flush + +**Cons:** +- Requires interface GUID +- Requires administrator privileges +- May require restart of DNS client service + +### Linux: systemd-resolved + +Uses D-Bus API to communicate with systemd-resolved service. + +**Pros:** +- Modern standard on many distributions +- Proper per-interface configuration +- No file manipulation needed + +**Cons:** +- Requires D-Bus access +- Only available on systemd systems +- Interface-specific + +### Linux: NetworkManager + +Uses D-Bus API to modify NetworkManager connection settings. + +**Pros:** +- Common on desktop Linux +- Integrates with NetworkManager GUI +- Per-interface configuration + +**Cons:** +- Requires NetworkManager to be running +- D-Bus access required +- Interface-specific + +### Linux: resolvconf + +Uses the `resolvconf` utility to update DNS configuration. + +**Pros:** +- Works on many different systems +- Handles merging of multiple DNS sources +- Supports both openresolv and Debian resolvconf + +**Cons:** +- Requires resolvconf to be installed +- Interface-specific + +### Linux: Direct File + +Directly modifies `/etc/resolv.conf` with backup. + +**Pros:** +- Works everywhere +- No dependencies +- Simple and reliable + +**Cons:** +- May be overwritten by DHCP or other services +- No per-interface configuration +- Doesn't integrate with system tools + +## Build Tags + +The module uses build tags to compile platform-specific code: + +- `//go:build darwin && !ios` - macOS (non-iOS) +- `//go:build windows` - Windows +- `//go:build (linux && !android) || freebsd` - Linux and FreeBSD +- `//go:build linux && !android` - Linux only (for systemd) + +## Dependencies + +- `github.com/godbus/dbus/v5` - D-Bus communication (Linux only) +- `golang.org/x/sys` - System calls and registry access +- Standard library + +## Security Considerations + +- **Elevated Privileges**: Most DNS modification operations require root/administrator privileges +- **Backup Files**: Backup files contain original DNS configuration and should be protected +- **State Persistence**: DNS state is stored in memory; unexpected termination may require manual cleanup + +## Cleanup + +The module properly cleans up after itself: + +1. Backup files are created before modification +2. Original DNS servers are stored in memory +3. `RestoreDNS()` should be called to restore original settings +4. On Linux file-based systems, backup files are removed after restoration + +## Testing + +Each configurator can be tested independently: + +```go +func TestDNSOverride(t *testing.T) { + configurator, err := platform.NewFileDNSConfigurator() + require.NoError(t, err) + + servers := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + } + + original, err := configurator.SetDNS(servers) + require.NoError(t, err) + + defer configurator.RestoreDNS() + + current, err := configurator.GetCurrentDNS() + require.NoError(t, err) + require.Equal(t, servers, current) +} +``` + +## Future Enhancements + +- [ ] Support for search domains configuration +- [ ] Support for DNS options (timeout, attempts, etc.) +- [ ] Monitoring for external DNS changes +- [ ] Automatic restoration on process exit +- [ ] Windows NRPT (Name Resolution Policy Table) support +- [ ] IPv6 DNS server support on all platforms diff --git a/dns/platform/REFACTORING_SUMMARY.md b/dns/platform/REFACTORING_SUMMARY.md new file mode 100644 index 0000000..44786a8 --- /dev/null +++ b/dns/platform/REFACTORING_SUMMARY.md @@ -0,0 +1,174 @@ +# DNS Platform Module Refactoring Summary + +## Changes Made + +Successfully refactored the DNS platform directory from a NetBird-derived codebase into a standalone, simplified DNS override module. + +### Files Created + +**Core Interface & Types:** +- `types.go` - DNSConfigurator interface and shared types (DNSConfig, DNSState) + +**Platform Implementations:** +- `darwin.go` - macOS DNS configurator using scutil (replaces host_darwin.go) +- `windows.go` - Windows DNS configurator using registry (replaces host_windows.go) +- `file.go` - Linux/Unix file-based configurator (replaces file_unix.go + file_parser_unix.go + file_repair_unix.go) +- `networkmanager.go` - NetworkManager D-Bus configurator (replaces network_manager_unix.go) +- `systemd.go` - systemd-resolved D-Bus configurator (replaces systemd_linux.go) +- `resolvconf.go` - resolvconf utility configurator (replaces resolvconf_unix.go) + +**Detection & Helpers:** +- `detect_unix.go` - Automatic detection for Linux/FreeBSD +- `detect_darwin.go` - Automatic detection for macOS +- `detect_windows.go` - Automatic detection for Windows + +**Documentation:** +- `README.md` - Comprehensive module documentation +- `examples/example_usage.go` - Usage examples for all platforms + +### Files Removed + +**Old NetBird-specific files:** +- `dbus_unix.go` - D-Bus utilities (functionality moved into platform-specific files) +- `file_parser_unix.go` - resolv.conf parser (simplified and integrated into file.go) +- `file_repair_unix.go` - File watching/repair (removed - out of scope) +- `file_unix.go` - Old file configurator (replaced by file.go) +- `host_darwin.go` - Old macOS configurator (replaced by darwin.go) +- `host_unix.go` - Old Unix manager factory (replaced by detect_unix.go) +- `host_windows.go` - Old Windows configurator (replaced by windows.go) +- `network_manager_unix.go` - Old NetworkManager (replaced by networkmanager.go) +- `resolvconf_unix.go` - Old resolvconf (replaced by resolvconf.go) +- `systemd_linux.go` - Old systemd-resolved (replaced by systemd.go) +- `unclean_shutdown_*.go` - Unclean shutdown detection (removed - out of scope) + +### Key Architectural Changes + +1. **Removed Build Tags for Platform Selection** + - Old: Used `//go:build` tags at top of files to compile different code per platform + - New: Named structs differently per platform (e.g., `DarwinDNSConfigurator`, `WindowsDNSConfigurator`) + - Build tags kept only where necessary for cross-platform library imports + +2. **Simplified Interface** + - Removed complex domain routing, search domains, and port customization + - Focused on core functionality: Set DNS, Get DNS, Restore DNS + - Removed state manager dependencies + +3. **Removed External Dependencies** + - Removed: statemanager, NetBird-specific types, logging libraries + - Kept only: D-Bus (for Linux), x/sys (for Windows registry and Unix syscalls) + - Uses standard library where possible + +4. **Standalone Operation** + - No longer depends on NetBird types (HostDNSConfig, etc.) + - Uses standard library types (net/netip.Addr) + - Self-contained backup/restore logic + +5. **Improved Code Organization** + - Each platform has its own clearly-named file + - Detection logic separated into detect_*.go files + - Shared types in types.go + - Examples in dedicated examples/ directory + +### Feature Comparison + +**Removed (out of scope for basic DNS override):** +- Search domain management +- Match-only domains +- DNS port customization (except where natively supported) +- File watching and auto-repair +- Unclean shutdown detection +- State persistence +- Integration with external state managers + +**Retained (core DNS functionality):** +- Setting DNS servers +- Getting current DNS servers +- Restoring original DNS servers +- Automatic platform detection +- DNS cache flushing +- Backup and restore of original configuration + +### Platform-Specific Notes + +**macOS (Darwin):** +- Simplified to focus on DNS server override using scutil +- Removed complex domain routing and local DNS setup +- Removed GPO and state management +- Kept DNS cache flushing + +**Windows:** +- Simplified registry manipulation to just NameServer key +- Removed NRPT (Name Resolution Policy Table) support +- Removed DNS registration and WINS management +- Kept DNS cache flushing + +**Linux - File-based:** +- Direct /etc/resolv.conf manipulation with backup +- Removed file watching and auto-repair +- Removed complex search domain merging logic +- Simple nameserver-only configuration + +**Linux - systemd-resolved:** +- D-Bus API for per-link DNS configuration +- Simplified to just DNS server setting +- Uses Revert method for restoration + +**Linux - NetworkManager:** +- D-Bus API for connection settings modification +- Simplified to IPv4 DNS only +- Removed search/match domain complexity + +**Linux - resolvconf:** +- Uses resolvconf utility (openresolv or Debian resolvconf) +- Interface-specific configuration +- Simple nameserver configuration + +### Usage Pattern + +```go +// Automatic detection +configurator, err := platform.DetectBestConfigurator("eth0") + +// Set DNS +original, err := configurator.SetDNS([]netip.Addr{ + netip.MustParseAddr("8.8.8.8"), +}) + +// Restore +defer configurator.RestoreDNS() +``` + +### Maintenance Notes + +- Each platform implementation is independent +- No shared state between configurators +- Backups are file-based or in-memory only +- No external database or state management required +- Configurators can be tested independently + +## Migration Guide + +If you were using the old code: + +1. Replace `HostDNSConfig` with simple `[]netip.Addr` for DNS servers +2. Replace `newHostManager()` with `platform.DetectBestConfigurator()` +3. Replace `applyDNSConfig()` with `SetDNS()` +4. Replace `restoreHostDNS()` with `RestoreDNS()` +5. Remove state manager dependencies +6. Remove search domain configuration (can be added back if needed) + +## Dependencies + +Required: +- `github.com/godbus/dbus/v5` - For Linux D-Bus configurators +- `golang.org/x/sys` - For Windows registry and Unix syscalls +- Standard library + +## Testing Recommendations + +Each configurator should be tested on its target platform: +- macOS: Test darwin.go with scutil +- Windows: Test windows.go with actual interface GUID +- Linux: Test all variants (file, systemd, networkmanager, resolvconf) +- Verify backup/restore functionality +- Test with invalid input (empty servers, bad interface names) diff --git a/dns/platform/darwin.go b/dns/platform/darwin.go new file mode 100644 index 0000000..bbcedcf --- /dev/null +++ b/dns/platform/darwin.go @@ -0,0 +1,240 @@ +//go:build darwin && !ios + +package dns + +import ( + "bufio" + "bytes" + "fmt" + "net/netip" + "os/exec" + "strings" +) + +const ( + scutilPath = "/usr/sbin/scutil" + dscacheutilPath = "/usr/bin/dscacheutil" + + dnsStateKeyFormat = "State:/Network/Service/Olm-%s/DNS" + globalIPv4State = "State:/Network/Global/IPv4" + primaryServiceFormat = "State:/Network/Service/%s/DNS" + + keyServerAddresses = "ServerAddresses" + arraySymbol = "* " +) + +// DarwinDNSConfigurator manages DNS settings on macOS using scutil +type DarwinDNSConfigurator struct { + createdKeys map[string]struct{} + originalState *DNSState +} + +// NewDarwinDNSConfigurator creates a new macOS DNS configurator +func NewDarwinDNSConfigurator() (*DarwinDNSConfigurator, error) { + return &DarwinDNSConfigurator{ + createdKeys: make(map[string]struct{}), + }, nil +} + +// Name returns the configurator name +func (d *DarwinDNSConfigurator) Name() string { + return "darwin-scutil" +} + +// SetDNS sets the DNS servers and returns the original servers +func (d *DarwinDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) { + // Get current DNS settings before overriding + originalServers, err := d.GetCurrentDNS() + if err != nil { + return nil, fmt.Errorf("get current DNS: %w", err) + } + + // Store original state + d.originalState = &DNSState{ + OriginalServers: originalServers, + ConfiguratorName: d.Name(), + } + + // Set new DNS servers + if err := d.applyDNSServers(servers); err != nil { + return nil, fmt.Errorf("apply DNS servers: %w", err) + } + + // Flush DNS cache + if err := d.flushDNSCache(); err != nil { + // Non-fatal, just log + fmt.Printf("warning: failed to flush DNS cache: %v\n", err) + } + + return originalServers, nil +} + +// RestoreDNS restores the original DNS configuration +func (d *DarwinDNSConfigurator) RestoreDNS() error { + // Remove all created keys + for key := range d.createdKeys { + if err := d.removeKey(key); err != nil { + return fmt.Errorf("remove key %s: %w", key, err) + } + } + + // Flush DNS cache + if err := d.flushDNSCache(); err != nil { + fmt.Printf("warning: failed to flush DNS cache: %v\n", err) + } + + return nil +} + +// GetCurrentDNS returns the currently configured DNS servers +func (d *DarwinDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) { + primaryServiceKey, err := d.getPrimaryServiceKey() + if err != nil || primaryServiceKey == "" { + return nil, fmt.Errorf("get primary service: %w", err) + } + + dnsKey := fmt.Sprintf(primaryServiceFormat, primaryServiceKey) + cmd := fmt.Sprintf("show %s\n", dnsKey) + + output, err := d.runScutil(cmd) + if err != nil { + return nil, fmt.Errorf("run scutil: %w", err) + } + + servers := d.parseServerAddresses(output) + return servers, nil +} + +// applyDNSServers applies the DNS server configuration +func (d *DarwinDNSConfigurator) applyDNSServers(servers []netip.Addr) error { + if len(servers) == 0 { + return fmt.Errorf("no DNS servers provided") + } + + key := fmt.Sprintf(dnsStateKeyFormat, "Override") + + // Build server addresses array + var serverLines strings.Builder + for _, server := range servers { + serverLines.WriteString(arraySymbol) + serverLines.WriteString(server.String()) + serverLines.WriteString("\n") + } + + // Build scutil command + cmd := fmt.Sprintf(`d.init +d.add %s %s +set %s +`, keyServerAddresses, strings.TrimSpace(serverLines.String()), key) + + if _, err := d.runScutil(cmd); err != nil { + return fmt.Errorf("set DNS servers: %w", err) + } + + d.createdKeys[key] = struct{}{} + return nil +} + +// removeKey removes a DNS configuration key +func (d *DarwinDNSConfigurator) removeKey(key string) error { + cmd := fmt.Sprintf("remove %s\n", key) + + if _, err := d.runScutil(cmd); err != nil { + return fmt.Errorf("remove key: %w", err) + } + + delete(d.createdKeys, key) + return nil +} + +// getPrimaryServiceKey gets the primary network service key +func (d *DarwinDNSConfigurator) getPrimaryServiceKey() (string, error) { + cmd := fmt.Sprintf("show %s\n", globalIPv4State) + + output, err := d.runScutil(cmd) + if err != nil { + return "", fmt.Errorf("run scutil: %w", err) + } + + scanner := bufio.NewScanner(bytes.NewReader(output)) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "PrimaryService") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + return strings.TrimSpace(parts[1]), nil + } + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("scan output: %w", err) + } + + return "", fmt.Errorf("primary service not found") +} + +// parseServerAddresses parses DNS server addresses from scutil output +func (d *DarwinDNSConfigurator) parseServerAddresses(output []byte) []netip.Addr { + var servers []netip.Addr + inServerArray := false + + scanner := bufio.NewScanner(bytes.NewReader(output)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "ServerAddresses : {") { + inServerArray = true + continue + } + + if line == "}" { + inServerArray = false + continue + } + + if inServerArray { + // Line format: "0 : 8.8.8.8" + parts := strings.Split(line, " : ") + if len(parts) >= 2 { + if addr, err := netip.ParseAddr(parts[1]); err == nil { + servers = append(servers, addr) + } + } + } + } + + return servers +} + +// flushDNSCache flushes the system DNS cache +func (d *DarwinDNSConfigurator) flushDNSCache() error { + cmd := exec.Command(dscacheutilPath, "-flushcache") + if err := cmd.Run(); err != nil { + return fmt.Errorf("flush cache: %w", err) + } + + cmd = exec.Command("killall", "-HUP", "mDNSResponder") + if err := cmd.Run(); err != nil { + // Non-fatal, mDNSResponder might not be running + return nil + } + + return nil +} + +// runScutil executes an scutil command +func (d *DarwinDNSConfigurator) runScutil(commands string) ([]byte, error) { + // Wrap commands with open/quit + wrapped := fmt.Sprintf("open\n%squit\n", commands) + + cmd := exec.Command(scutilPath) + cmd.Stdin = strings.NewReader(wrapped) + + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("scutil command failed: %w, output: %s", err, output) + } + + return output, nil +} diff --git a/dns/platform/detect_darwin.go b/dns/platform/detect_darwin.go new file mode 100644 index 0000000..ee931f5 --- /dev/null +++ b/dns/platform/detect_darwin.go @@ -0,0 +1,30 @@ +//go:build darwin && !ios + +package dns + +import "fmt" + +// DetectBestConfigurator returns the macOS DNS configurator +func DetectBestConfigurator(ifaceName string) (DNSConfigurator, error) { + return NewDarwinDNSConfigurator() +} + +// GetSystemDNS returns the current system DNS servers +func GetSystemDNS() ([]string, error) { + configurator, err := NewDarwinDNSConfigurator() + if err != nil { + return nil, fmt.Errorf("create configurator: %w", err) + } + + servers, err := configurator.GetCurrentDNS() + if err != nil { + return nil, fmt.Errorf("get current DNS: %w", err) + } + + var result []string + for _, server := range servers { + result = append(result, server.String()) + } + + return result, nil +} diff --git a/dns/platform/detect_unix.go b/dns/platform/detect_unix.go new file mode 100644 index 0000000..53cc4e3 --- /dev/null +++ b/dns/platform/detect_unix.go @@ -0,0 +1,92 @@ +//go:build (linux && !android) || freebsd + +package dns + +import ( + "fmt" + "net/netip" + "os" + "strings" +) + +// DetectBestConfigurator detects and returns the most appropriate DNS configurator for the system +// ifaceName is optional and only used for NetworkManager, systemd-resolved, and resolvconf +func DetectBestConfigurator(ifaceName string) (DNSConfigurator, error) { + // Try systemd-resolved first (most modern) + if IsSystemdResolvedAvailable() && ifaceName != "" { + if configurator, err := NewSystemdResolvedDNSConfigurator(ifaceName); err == nil { + return configurator, nil + } + } + + // Try NetworkManager (common on desktops) + if IsNetworkManagerAvailable() && ifaceName != "" { + if configurator, err := NewNetworkManagerDNSConfigurator(ifaceName); err == nil { + return configurator, nil + } + } + + // Try resolvconf (common on older systems) + if IsResolvconfAvailable() && ifaceName != "" { + if configurator, err := NewResolvconfDNSConfigurator(ifaceName); err == nil { + return configurator, nil + } + } + + // Fall back to direct file manipulation + return NewFileDNSConfigurator() +} + +// Helper functions for checking system state + +// IsSystemdResolvedRunning checks if systemd-resolved is running +func IsSystemdResolvedRunning() bool { + // Check if stub resolver is configured + servers, err := readResolvConfDNS() + if err != nil { + return false + } + + // systemd-resolved uses 127.0.0.53 + stubAddr := netip.MustParseAddr("127.0.0.53") + for _, server := range servers { + if server == stubAddr { + return true + } + } + + return false +} + +// readResolvConfDNS reads DNS servers from /etc/resolv.conf +func readResolvConfDNS() ([]netip.Addr, error) { + content, err := os.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, fmt.Errorf("read resolv.conf: %w", err) + } + + var servers []netip.Addr + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + 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, nil +} + +// GetSystemDNS returns the current system DNS servers +func GetSystemDNS() ([]netip.Addr, error) { + return readResolvConfDNS() +} diff --git a/dns/platform/detect_windows.go b/dns/platform/detect_windows.go new file mode 100644 index 0000000..81576f4 --- /dev/null +++ b/dns/platform/detect_windows.go @@ -0,0 +1,34 @@ +//go:build windows + +package dns + +import "fmt" + +// DetectBestConfigurator returns the Windows DNS configurator +// guid is the network interface GUID +func DetectBestConfigurator(guid string) (DNSConfigurator, error) { + if guid == "" { + return nil, fmt.Errorf("interface GUID is required for Windows") + } + return NewWindowsDNSConfigurator(guid) +} + +// GetSystemDNS returns the current system DNS servers for the given interface +func GetSystemDNS(guid string) ([]string, error) { + configurator, err := NewWindowsDNSConfigurator(guid) + if err != nil { + return nil, fmt.Errorf("create configurator: %w", err) + } + + servers, err := configurator.GetCurrentDNS() + if err != nil { + return nil, fmt.Errorf("get current DNS: %w", err) + } + + var result []string + for _, server := range servers { + result = append(result, server.String()) + } + + return result, nil +} diff --git a/dns/platform/examples/example_usage.go b/dns/platform/examples/example_usage.go new file mode 100644 index 0000000..7ae331f --- /dev/null +++ b/dns/platform/examples/example_usage.go @@ -0,0 +1,236 @@ +package main + +import ( + "fmt" + "log" + "net/netip" + "os" + "os/signal" + "syscall" + "time" + + "github.com/your-org/olm/dns/platform" +) + +func main() { + // Example 1: Automatic detection and DNS override + exampleAutoDetection() + + // Example 2: Manual platform selection + // exampleManualSelection() + + // Example 3: Get current system DNS + // exampleGetCurrentDNS() +} + +// exampleAutoDetection demonstrates automatic detection of the best DNS configurator +func exampleAutoDetection() { + fmt.Println("=== Example 1: Automatic Detection ===") + + // On Linux/Unix, provide an interface name for better detection + // On macOS, the interface name is ignored + // On Windows, provide the interface GUID + ifaceName := "eth0" // Change this to your interface name + + configurator, err := platform.DetectBestConfigurator(ifaceName) + if err != nil { + log.Fatalf("Failed to detect DNS configurator: %v", err) + } + + fmt.Printf("Using DNS configurator: %s\n", configurator.Name()) + + // Get current DNS servers before changing + currentDNS, err := configurator.GetCurrentDNS() + if err != nil { + log.Printf("Warning: Could not get current DNS: %v", err) + } else { + fmt.Printf("Current DNS servers: %v\n", currentDNS) + } + + // Set new DNS servers + newDNS := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), // Cloudflare + netip.MustParseAddr("8.8.8.8"), // Google + } + + fmt.Printf("Setting DNS servers to: %v\n", newDNS) + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatalf("Failed to set DNS: %v", err) + } + + fmt.Printf("Original DNS servers (backed up): %v\n", originalDNS) + + // Set up signal handling for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Run for 30 seconds or until interrupted + fmt.Println("\nDNS override active. Press Ctrl+C to restore original DNS.") + fmt.Println("Waiting 30 seconds...") + + select { + case <-time.After(30 * time.Second): + fmt.Println("\nTimeout reached.") + case sig := <-sigChan: + fmt.Printf("\nReceived signal: %v\n", sig) + } + + // Restore original DNS + fmt.Println("Restoring original DNS servers...") + if err := configurator.RestoreDNS(); err != nil { + log.Fatalf("Failed to restore DNS: %v", err) + } + + fmt.Println("DNS restored successfully!") +} + +// exampleManualSelection demonstrates manual selection of DNS configurator +func exampleManualSelection() { + fmt.Println("=== Example 2: Manual Selection ===") + + // Linux - systemd-resolved + configurator, err := platform.NewSystemdResolvedDNSConfigurator("eth0") + if err != nil { + log.Fatalf("Failed to create systemd-resolved configurator: %v", err) + } + + fmt.Printf("Using: %s\n", configurator.Name()) + + newDNS := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + } + + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatalf("Failed to set DNS: %v", err) + } + + fmt.Printf("Changed from %v to %v\n", originalDNS, newDNS) + + // Restore after 10 seconds + time.Sleep(10 * time.Second) + configurator.RestoreDNS() +} + +// exampleGetCurrentDNS demonstrates getting current system DNS +func exampleGetCurrentDNS() { + fmt.Println("=== Example 3: Get Current DNS ===") + + configurator, err := platform.DetectBestConfigurator("eth0") + if err != nil { + log.Fatalf("Failed to detect configurator: %v", err) + } + + servers, err := configurator.GetCurrentDNS() + if err != nil { + log.Fatalf("Failed to get DNS: %v", err) + } + + fmt.Printf("Current DNS servers (%s):\n", configurator.Name()) + for i, server := range servers { + fmt.Printf(" %d. %s\n", i+1, server) + } +} + +// Platform-specific examples + +// exampleLinuxFile demonstrates direct file manipulation on Linux +func exampleLinuxFile() { + configurator, err := platform.NewFileDNSConfigurator() + if err != nil { + log.Fatal(err) + } + + newDNS := []netip.Addr{ + netip.MustParseAddr("8.8.8.8"), + } + + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatal(err) + } + + defer configurator.RestoreDNS() + + fmt.Printf("Changed from %v to %v\n", originalDNS, newDNS) + time.Sleep(10 * time.Second) +} + +// exampleLinuxNetworkManager demonstrates NetworkManager on Linux +func exampleLinuxNetworkManager() { + if !platform.IsNetworkManagerAvailable() { + fmt.Println("NetworkManager is not available") + return + } + + configurator, err := platform.NewNetworkManagerDNSConfigurator("eth0") + if err != nil { + log.Fatal(err) + } + + newDNS := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + } + + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatal(err) + } + + defer configurator.RestoreDNS() + + fmt.Printf("Changed from %v to %v\n", originalDNS, newDNS) + time.Sleep(10 * time.Second) +} + +// exampleMacOS demonstrates macOS DNS override +func exampleMacOS() { + configurator, err := platform.NewDarwinDNSConfigurator() + if err != nil { + log.Fatal(err) + } + + newDNS := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("1.0.0.1"), + } + + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatal(err) + } + + defer configurator.RestoreDNS() + + fmt.Printf("Changed from %v to %v\n", originalDNS, newDNS) + time.Sleep(10 * time.Second) +} + +// exampleWindows demonstrates Windows DNS override +func exampleWindows() { + // You need to get the interface GUID first + // This can be obtained from: + // - ipconfig /all (look for the interface's GUID) + // - registry: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces + guid := "{YOUR-INTERFACE-GUID-HERE}" + + configurator, err := platform.NewWindowsDNSConfigurator(guid) + if err != nil { + log.Fatal(err) + } + + newDNS := []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + } + + originalDNS, err := configurator.SetDNS(newDNS) + if err != nil { + log.Fatal(err) + } + + defer configurator.RestoreDNS() + + fmt.Printf("Changed from %v to %v\n", originalDNS, newDNS) + time.Sleep(10 * time.Second) +} diff --git a/dns/platform/file.go b/dns/platform/file.go new file mode 100644 index 0000000..8f6f766 --- /dev/null +++ b/dns/platform/file.go @@ -0,0 +1,192 @@ +//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) { + return &FileDNSConfigurator{}, 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 +} + +// 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 +} diff --git a/dns/platform/networkmanager.go b/dns/platform/networkmanager.go new file mode 100644 index 0000000..9a9a882 --- /dev/null +++ b/dns/platform/networkmanager.go @@ -0,0 +1,256 @@ +//go:build (linux && !android) || freebsd + +package dns + +import ( + "context" + "encoding/binary" + "fmt" + "net" + "net/netip" + "time" + + dbus "github.com/godbus/dbus/v5" +) + +const ( + networkManagerDest = "org.freedesktop.NetworkManager" + networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager" + networkManagerDbusGetDeviceByIPIface = networkManagerDest + ".GetDeviceByIpIface" + networkManagerDbusDeviceInterface = "org.freedesktop.NetworkManager.Device" + networkManagerDbusDeviceGetApplied = networkManagerDbusDeviceInterface + ".GetAppliedConnection" + networkManagerDbusDeviceReapply = networkManagerDbusDeviceInterface + ".Reapply" + networkManagerDbusIPv4Key = "ipv4" + networkManagerDbusDNSKey = "dns" + networkManagerDbusDNSPriorityKey = "dns-priority" + networkManagerDbusPrimaryDNSPriority = int32(-500) +) + +type networkManagerConnSettings map[string]map[string]dbus.Variant +type networkManagerConfigVersion uint64 + +// NetworkManagerDNSConfigurator manages DNS settings using NetworkManager D-Bus API +type NetworkManagerDNSConfigurator struct { + ifaceName string + dbusLinkObject dbus.ObjectPath + originalState *DNSState +} + +// NewNetworkManagerDNSConfigurator creates a new NetworkManager DNS configurator +func NewNetworkManagerDNSConfigurator(ifaceName string) (*NetworkManagerDNSConfigurator, error) { + // Get the D-Bus link object for this interface + conn, err := dbus.SystemBus() + if err != nil { + return nil, fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode) + + var linkPath string + if err := obj.Call(networkManagerDbusGetDeviceByIPIface, 0, ifaceName).Store(&linkPath); err != nil { + return nil, fmt.Errorf("get device by interface: %w", err) + } + + return &NetworkManagerDNSConfigurator{ + ifaceName: ifaceName, + dbusLinkObject: dbus.ObjectPath(linkPath), + }, nil +} + +// Name returns the configurator name +func (n *NetworkManagerDNSConfigurator) Name() string { + return "networkmanager-dbus" +} + +// SetDNS sets the DNS servers and returns the original servers +func (n *NetworkManagerDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) { + // Get current DNS settings before overriding + originalServers, err := n.GetCurrentDNS() + if err != nil { + return nil, fmt.Errorf("get current DNS: %w", err) + } + + // Store original state + n.originalState = &DNSState{ + OriginalServers: originalServers, + ConfiguratorName: n.Name(), + } + + // Apply new DNS servers + if err := n.applyDNSServers(servers); err != nil { + return nil, fmt.Errorf("apply DNS servers: %w", err) + } + + return originalServers, nil +} + +// RestoreDNS restores the original DNS configuration +func (n *NetworkManagerDNSConfigurator) RestoreDNS() error { + if n.originalState == nil { + return fmt.Errorf("no original state to restore") + } + + // Restore original DNS servers + if err := n.applyDNSServers(n.originalState.OriginalServers); err != nil { + return fmt.Errorf("restore DNS servers: %w", err) + } + + return nil +} + +// GetCurrentDNS returns the currently configured DNS servers +func (n *NetworkManagerDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) { + connSettings, _, err := n.getAppliedConnectionSettings() + if err != nil { + return nil, fmt.Errorf("get connection settings: %w", err) + } + + return n.extractDNSServers(connSettings), nil +} + +// applyDNSServers applies DNS server configuration via NetworkManager +func (n *NetworkManagerDNSConfigurator) applyDNSServers(servers []netip.Addr) error { + connSettings, configVersion, err := n.getAppliedConnectionSettings() + if err != nil { + return fmt.Errorf("get connection settings: %w", err) + } + + // Convert DNS servers to NetworkManager format (uint32 little-endian) + var dnsServers []uint32 + for _, server := range servers { + if server.Is4() { + dnsServers = append(dnsServers, binary.LittleEndian.Uint32(server.AsSlice())) + } + } + + if len(dnsServers) == 0 { + return fmt.Errorf("no valid IPv4 DNS servers provided") + } + + // Update DNS settings + connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant(dnsServers) + connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSPriorityKey] = dbus.MakeVariant(networkManagerDbusPrimaryDNSPriority) + + // Reapply connection settings + if err := n.reApplyConnectionSettings(connSettings, configVersion); err != nil { + return fmt.Errorf("reapply connection settings: %w", err) + } + + return nil +} + +// getAppliedConnectionSettings retrieves current NetworkManager connection settings +func (n *NetworkManagerDNSConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) { + conn, err := dbus.SystemBus() + if err != nil { + return nil, 0, fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(networkManagerDest, n.dbusLinkObject) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var connSettings networkManagerConnSettings + var configVersion networkManagerConfigVersion + + if err := obj.CallWithContext(ctx, networkManagerDbusDeviceGetApplied, 0, uint32(0)).Store(&connSettings, &configVersion); err != nil { + return nil, 0, fmt.Errorf("get applied connection: %w", err) + } + + return connSettings, configVersion, nil +} + +// reApplyConnectionSettings applies new connection settings via NetworkManager +func (n *NetworkManagerDNSConfigurator) reApplyConnectionSettings(connSettings networkManagerConnSettings, configVersion networkManagerConfigVersion) error { + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(networkManagerDest, n.dbusLinkObject) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := obj.CallWithContext(ctx, networkManagerDbusDeviceReapply, 0, connSettings, configVersion, uint32(0)).Store(); err != nil { + return fmt.Errorf("reapply connection: %w", err) + } + + return nil +} + +// extractDNSServers extracts DNS servers from connection settings +func (n *NetworkManagerDNSConfigurator) extractDNSServers(connSettings networkManagerConnSettings) []netip.Addr { + var servers []netip.Addr + + ipv4Settings, ok := connSettings[networkManagerDbusIPv4Key] + if !ok { + return servers + } + + dnsVariant, ok := ipv4Settings[networkManagerDbusDNSKey] + if !ok { + return servers + } + + dnsServers, ok := dnsVariant.Value().([]uint32) + if !ok { + return servers + } + + for _, dnsServer := range dnsServers { + // Convert uint32 back to IP address + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, dnsServer) + + if addr, ok := netip.AddrFromSlice(buf); ok { + servers = append(servers, addr) + } + } + + return servers +} + +// IsNetworkManagerAvailable checks if NetworkManager is available and responsive +func IsNetworkManagerAvailable() bool { + conn, err := dbus.SystemBus() + if err != nil { + return false + } + defer conn.Close() + + obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Try to ping NetworkManager + if err := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store(); err != nil { + return false + } + + return true +} + +// GetNetworkInterfaces returns available network interfaces +func GetNetworkInterfaces() ([]string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("get interfaces: %w", err) + } + + var names []string + for _, iface := range interfaces { + // Skip loopback + if iface.Flags&net.FlagLoopback != 0 { + continue + } + names = append(names, iface.Name) + } + + return names, nil +} diff --git a/dns/platform/resolvconf.go b/dns/platform/resolvconf.go new file mode 100644 index 0000000..4202c4c --- /dev/null +++ b/dns/platform/resolvconf.go @@ -0,0 +1,192 @@ +//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) + } + + return &ResolvconfDNSConfigurator{ + ifaceName: ifaceName, + implType: implType, + }, 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 +} + +// 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 +} diff --git a/dns/platform/systemd.go b/dns/platform/systemd.go new file mode 100644 index 0000000..4c0e323 --- /dev/null +++ b/dns/platform/systemd.go @@ -0,0 +1,186 @@ +//go:build linux && !android + +package dns + +import ( + "context" + "fmt" + "net" + "net/netip" + "time" + + dbus "github.com/godbus/dbus/v5" + "golang.org/x/sys/unix" +) + +const ( + systemdResolvedDest = "org.freedesktop.resolve1" + systemdDbusObjectNode = "/org/freedesktop/resolve1" + systemdDbusManagerIface = "org.freedesktop.resolve1.Manager" + systemdDbusGetLinkMethod = systemdDbusManagerIface + ".GetLink" + systemdDbusLinkInterface = "org.freedesktop.resolve1.Link" + systemdDbusSetDNSMethod = systemdDbusLinkInterface + ".SetDNS" + systemdDbusRevertMethod = systemdDbusLinkInterface + ".Revert" +) + +// systemdDbusDNSInput maps to (iay) dbus input for SetDNS method +type systemdDbusDNSInput struct { + Family int32 + Address []byte +} + +// SystemdResolvedDNSConfigurator manages DNS settings using systemd-resolved D-Bus API +type SystemdResolvedDNSConfigurator struct { + ifaceName string + dbusLinkObject dbus.ObjectPath + originalState *DNSState +} + +// NewSystemdResolvedDNSConfigurator creates a new systemd-resolved DNS configurator +func NewSystemdResolvedDNSConfigurator(ifaceName string) (*SystemdResolvedDNSConfigurator, error) { + // Get network interface + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return nil, fmt.Errorf("get interface: %w", err) + } + + // Connect to D-Bus + conn, err := dbus.SystemBus() + if err != nil { + return nil, fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(systemdResolvedDest, systemdDbusObjectNode) + + // Get the link object for this interface + var linkPath string + if err := obj.Call(systemdDbusGetLinkMethod, 0, iface.Index).Store(&linkPath); err != nil { + return nil, fmt.Errorf("get link: %w", err) + } + + return &SystemdResolvedDNSConfigurator{ + ifaceName: ifaceName, + dbusLinkObject: dbus.ObjectPath(linkPath), + }, nil +} + +// Name returns the configurator name +func (s *SystemdResolvedDNSConfigurator) Name() string { + return "systemd-resolved" +} + +// SetDNS sets the DNS servers and returns the original servers +func (s *SystemdResolvedDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) { + // Get current DNS settings before overriding + originalServers, err := s.GetCurrentDNS() + if err != nil { + // If we can't get current DNS, proceed anyway + originalServers = []netip.Addr{} + } + + // Store original state + s.originalState = &DNSState{ + OriginalServers: originalServers, + ConfiguratorName: s.Name(), + } + + // Apply new DNS servers + if err := s.applyDNSServers(servers); err != nil { + return nil, fmt.Errorf("apply DNS servers: %w", err) + } + + return originalServers, nil +} + +// RestoreDNS restores the original DNS configuration +func (s *SystemdResolvedDNSConfigurator) RestoreDNS() error { + // Call Revert method to restore systemd-resolved defaults + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(systemdResolvedDest, s.dbusLinkObject) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := obj.CallWithContext(ctx, systemdDbusRevertMethod, 0).Store(); err != nil { + return fmt.Errorf("revert DNS settings: %w", err) + } + + return nil +} + +// GetCurrentDNS returns the currently configured DNS servers +// Note: systemd-resolved doesn't easily expose current per-link DNS servers via D-Bus +// This is a placeholder that returns an empty list +func (s *SystemdResolvedDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) { + // systemd-resolved's D-Bus API doesn't have a simple way to query current DNS servers + // We would need to parse resolvectl status output or read from /run/systemd/resolve/ + // For now, return empty list + return []netip.Addr{}, nil +} + +// applyDNSServers applies DNS server configuration via systemd-resolved +func (s *SystemdResolvedDNSConfigurator) applyDNSServers(servers []netip.Addr) error { + if len(servers) == 0 { + return fmt.Errorf("no DNS servers provided") + } + + // Convert servers to systemd-resolved format + var dnsInputs []systemdDbusDNSInput + for _, server := range servers { + family := unix.AF_INET + if server.Is6() { + family = unix.AF_INET6 + } + + dnsInputs = append(dnsInputs, systemdDbusDNSInput{ + Family: int32(family), + Address: server.AsSlice(), + }) + } + + // Connect to D-Bus + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connect to system bus: %w", err) + } + defer conn.Close() + + obj := conn.Object(systemdResolvedDest, s.dbusLinkObject) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Call SetDNS method + if err := obj.CallWithContext(ctx, systemdDbusSetDNSMethod, 0, dnsInputs).Store(); err != nil { + return fmt.Errorf("set DNS servers: %w", err) + } + + return nil +} + +// IsSystemdResolvedAvailable checks if systemd-resolved is available and responsive +func IsSystemdResolvedAvailable() bool { + conn, err := dbus.SystemBus() + if err != nil { + return false + } + defer conn.Close() + + obj := conn.Object(systemdResolvedDest, systemdDbusObjectNode) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Try to ping systemd-resolved + if err := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store(); err != nil { + return false + } + + return true +} diff --git a/dns/platform/types.go b/dns/platform/types.go new file mode 100644 index 0000000..471ba29 --- /dev/null +++ b/dns/platform/types.go @@ -0,0 +1,41 @@ +package dns + +import "net/netip" + +// DNSConfigurator provides an interface for managing system DNS settings +// across different platforms and implementations +type DNSConfigurator interface { + // SetDNS overrides the system DNS servers with the specified ones + // Returns the original DNS servers that were replaced + SetDNS(servers []netip.Addr) ([]netip.Addr, error) + + // RestoreDNS restores the original DNS servers + RestoreDNS() error + + // GetCurrentDNS returns the currently configured DNS servers + GetCurrentDNS() ([]netip.Addr, error) + + // Name returns the name of this configurator implementation + Name() string +} + +// DNSConfig contains the configuration for DNS override +type DNSConfig struct { + // Servers is the list of DNS servers to use + Servers []netip.Addr + + // SearchDomains is an optional list of search domains + SearchDomains []string +} + +// DNSState represents the saved state of DNS configuration +type DNSState struct { + // OriginalServers are the DNS servers before override + OriginalServers []netip.Addr + + // OriginalSearchDomains are the search domains before override + OriginalSearchDomains []string + + // ConfiguratorName is the name of the configurator that saved this state + ConfiguratorName string +} diff --git a/dns/platform/windows.go b/dns/platform/windows.go new file mode 100644 index 0000000..c5f3f21 --- /dev/null +++ b/dns/platform/windows.go @@ -0,0 +1,247 @@ +//go:build windows + +package dns + +import ( + "errors" + "fmt" + "io" + "net/netip" + "syscall" + + "golang.org/x/sys/windows/registry" +) + +var ( + dnsapi = syscall.NewLazyDLL("dnsapi.dll") + dnsFlushResolverCacheFn = dnsapi.NewProc("DnsFlushResolverCache") +) + +const ( + interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces` + interfaceConfigNameServer = "NameServer" + interfaceConfigDhcpNameServer = "DhcpNameServer" +) + +// WindowsDNSConfigurator manages DNS settings on Windows using the registry +type WindowsDNSConfigurator struct { + guid string + originalState *DNSState +} + +// NewWindowsDNSConfigurator creates a new Windows DNS configurator +// guid is the network interface GUID +func NewWindowsDNSConfigurator(guid string) (*WindowsDNSConfigurator, error) { + if guid == "" { + return nil, fmt.Errorf("interface GUID is required") + } + + return &WindowsDNSConfigurator{ + guid: guid, + }, nil +} + +// Name returns the configurator name +func (w *WindowsDNSConfigurator) Name() string { + return "windows-registry" +} + +// SetDNS sets the DNS servers and returns the original servers +func (w *WindowsDNSConfigurator) SetDNS(servers []netip.Addr) ([]netip.Addr, error) { + // Get current DNS settings before overriding + originalServers, err := w.GetCurrentDNS() + if err != nil { + return nil, fmt.Errorf("get current DNS: %w", err) + } + + // Store original state + w.originalState = &DNSState{ + OriginalServers: originalServers, + ConfiguratorName: w.Name(), + } + + // Set new DNS servers + if err := w.setDNSServers(servers); err != nil { + return nil, fmt.Errorf("set DNS servers: %w", err) + } + + // Flush DNS cache + if err := w.flushDNSCache(); err != nil { + // Non-fatal, just log + fmt.Printf("warning: failed to flush DNS cache: %v\n", err) + } + + return originalServers, nil +} + +// RestoreDNS restores the original DNS configuration +func (w *WindowsDNSConfigurator) RestoreDNS() error { + if w.originalState == nil { + return fmt.Errorf("no original state to restore") + } + + // Clear the static DNS setting + if err := w.clearDNSServers(); err != nil { + return fmt.Errorf("clear DNS servers: %w", err) + } + + // Flush DNS cache + if err := w.flushDNSCache(); err != nil { + fmt.Printf("warning: failed to flush DNS cache: %v\n", err) + } + + return nil +} + +// GetCurrentDNS returns the currently configured DNS servers +func (w *WindowsDNSConfigurator) GetCurrentDNS() ([]netip.Addr, error) { + regKey, err := w.getInterfaceRegistryKey(registry.QUERY_VALUE) + if err != nil { + return nil, fmt.Errorf("get interface registry key: %w", err) + } + defer closeKey(regKey) + + // Try to get static DNS first + nameServer, _, err := regKey.GetStringValue(interfaceConfigNameServer) + if err == nil && nameServer != "" { + return w.parseServerList(nameServer), nil + } + + // Fall back to DHCP DNS + dhcpNameServer, _, err := regKey.GetStringValue(interfaceConfigDhcpNameServer) + if err == nil && dhcpNameServer != "" { + return w.parseServerList(dhcpNameServer), nil + } + + return []netip.Addr{}, nil +} + +// setDNSServers sets the DNS servers in the registry +func (w *WindowsDNSConfigurator) setDNSServers(servers []netip.Addr) error { + if len(servers) == 0 { + return fmt.Errorf("no DNS servers provided") + } + + regKey, err := w.getInterfaceRegistryKey(registry.SET_VALUE) + if err != nil { + return fmt.Errorf("get interface registry key: %w", err) + } + defer closeKey(regKey) + + // Build comma-separated or space-separated list of servers + var serverList string + for i, server := range servers { + if i > 0 { + serverList += "," + } + serverList += server.String() + } + + if err := regKey.SetStringValue(interfaceConfigNameServer, serverList); err != nil { + return fmt.Errorf("set NameServer: %w", err) + } + + return nil +} + +// clearDNSServers clears the static DNS server setting +func (w *WindowsDNSConfigurator) clearDNSServers() error { + regKey, err := w.getInterfaceRegistryKey(registry.SET_VALUE) + if err != nil { + return fmt.Errorf("get interface registry key: %w", err) + } + defer closeKey(regKey) + + // Set empty string to revert to DHCP + if err := regKey.SetStringValue(interfaceConfigNameServer, ""); err != nil { + return fmt.Errorf("clear NameServer: %w", err) + } + + return nil +} + +// getInterfaceRegistryKey opens the registry key for the network interface +func (w *WindowsDNSConfigurator) getInterfaceRegistryKey(access uint32) (registry.Key, error) { + regKeyPath := interfaceConfigPath + `\` + w.guid + + regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, access) + if err != nil { + return 0, fmt.Errorf("open HKEY_LOCAL_MACHINE\\%s: %w", regKeyPath, err) + } + + return regKey, nil +} + +// parseServerList parses a comma or space-separated list of DNS servers +func (w *WindowsDNSConfigurator) parseServerList(serverList string) []netip.Addr { + var servers []netip.Addr + + // Split by comma or space + parts := splitByDelimiters(serverList, []rune{',', ' '}) + + for _, part := range parts { + if addr, err := netip.ParseAddr(part); err == nil { + servers = append(servers, addr) + } + } + + return servers +} + +// flushDNSCache flushes the Windows DNS resolver cache +func (w *WindowsDNSConfigurator) flushDNSCache() error { + // dnsFlushResolverCacheFn.Call() may panic if the func is not found + defer func() { + if rec := recover(); rec != nil { + fmt.Printf("warning: DnsFlushResolverCache panicked: %v\n", rec) + } + }() + + ret, _, err := dnsFlushResolverCacheFn.Call() + if ret == 0 { + if err != nil && !errors.Is(err, syscall.Errno(0)) { + return fmt.Errorf("DnsFlushResolverCache failed: %w", err) + } + return fmt.Errorf("DnsFlushResolverCache failed") + } + + return nil +} + +// splitByDelimiters splits a string by multiple delimiters +func splitByDelimiters(s string, delimiters []rune) []string { + var result []string + var current []rune + + for _, char := range s { + isDelimiter := false + for _, delim := range delimiters { + if char == delim { + isDelimiter = true + break + } + } + + if isDelimiter { + if len(current) > 0 { + result = append(result, string(current)) + current = []rune{} + } + } else { + current = append(current, char) + } + } + + if len(current) > 0 { + result = append(result, string(current)) + } + + return result +} + +// closeKey closes a registry key and logs errors +func closeKey(closer io.Closer) { + if err := closer.Close(); err != nil { + fmt.Printf("warning: failed to close registry key: %v\n", err) + } +} diff --git a/go.mod b/go.mod index a5fc99c..586f5e7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/fosrl/newt v0.0.0 github.com/gorilla/websocket v1.5.3 + github.com/miekg/dns v1.1.68 github.com/vishvananda/netlink v1.3.1 golang.org/x/sys v0.38.0 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb @@ -15,8 +16,8 @@ require ( ) require ( + github.com/godbus/dbus/v5 v5.2.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/miekg/dns v1.1.68 // indirect github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/crypto v0.44.0 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect diff --git a/go.sum b/go.sum index c439800..275773c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8= +github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=