mirror of
https://github.com/fosrl/olm.git
synced 2026-02-24 22:06:42 +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/Microsoft/go-winio v0.6.2
|
||||||
github.com/fosrl/newt v0.0.0
|
github.com/fosrl/newt v0.0.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/miekg/dns v1.1.68
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
@@ -15,8 +16,8 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||||
github.com/google/btree v1.1.3 // 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
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // 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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
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 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
|||||||
Reference in New Issue
Block a user