Basic platform?

Former-commit-id: 423e18edc3
This commit is contained in:
Owen
2025-11-23 21:26:15 -05:00
parent 24b5122cc1
commit 50008f3c12
15 changed files with 2187 additions and 1 deletions

263
dns/platform/README.md Normal file
View File

@@ -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

View File

@@ -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)

240
dns/platform/darwin.go Normal file
View File

@@ -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 : <array> {") {
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
}

View File

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

View File

@@ -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()
}

View File

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

View File

@@ -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)
}

192
dns/platform/file.go Normal file
View File

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

View File

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

192
dns/platform/resolvconf.go Normal file
View File

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

186
dns/platform/systemd.go Normal file
View File

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

41
dns/platform/types.go Normal file
View File

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

247
dns/platform/windows.go Normal file
View File

@@ -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)
}
}

3
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=