mirror of
https://github.com/fosrl/olm.git
synced 2026-02-08 05:56:41 +00:00
263
dns/platform/README.md
Normal file
263
dns/platform/README.md
Normal 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
|
||||
174
dns/platform/REFACTORING_SUMMARY.md
Normal file
174
dns/platform/REFACTORING_SUMMARY.md
Normal 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
240
dns/platform/darwin.go
Normal 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
|
||||
}
|
||||
30
dns/platform/detect_darwin.go
Normal file
30
dns/platform/detect_darwin.go
Normal 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
|
||||
}
|
||||
92
dns/platform/detect_unix.go
Normal file
92
dns/platform/detect_unix.go
Normal 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()
|
||||
}
|
||||
34
dns/platform/detect_windows.go
Normal file
34
dns/platform/detect_windows.go
Normal 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
|
||||
}
|
||||
236
dns/platform/examples/example_usage.go
Normal file
236
dns/platform/examples/example_usage.go
Normal 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
192
dns/platform/file.go
Normal 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
|
||||
}
|
||||
256
dns/platform/networkmanager.go
Normal file
256
dns/platform/networkmanager.go
Normal 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
192
dns/platform/resolvconf.go
Normal 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
186
dns/platform/systemd.go
Normal 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
41
dns/platform/types.go
Normal 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
247
dns/platform/windows.go
Normal 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
3
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
Reference in New Issue
Block a user