mirror of
https://github.com/fosrl/olm.git
synced 2026-02-08 05:56:41 +00:00
222 lines
6.0 KiB
Go
222 lines
6.0 KiB
Go
//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)
|
|
}
|
|
|
|
configurator := &ResolvconfDNSConfigurator{
|
|
ifaceName: ifaceName,
|
|
implType: implType,
|
|
}
|
|
|
|
// Call cleanup function to remove any stale DNS config for this interface
|
|
if err := configurator.CleanupUncleanShutdown(); err != nil {
|
|
return nil, fmt.Errorf("cleanup unclean shutdown: %w", err)
|
|
}
|
|
|
|
return configurator, 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
|
|
}
|
|
|
|
// CleanupUncleanShutdown removes any DNS configuration left over from a previous crash
|
|
// For resolvconf, we attempt to delete any entry for the interface name.
|
|
// This ensures that if the process crashed while DNS was configured, the stale
|
|
// entry is removed on the next startup.
|
|
func (r *ResolvconfDNSConfigurator) CleanupUncleanShutdown() error {
|
|
// Try to delete any existing entry for this interface
|
|
// This is idempotent - if no entry exists, resolvconf will just return success
|
|
var cmd *exec.Cmd
|
|
|
|
switch r.implType {
|
|
case "openresolv":
|
|
cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName)
|
|
default:
|
|
cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName)
|
|
}
|
|
|
|
// Ignore errors - the entry may not exist, which is fine
|
|
_ = cmd.Run()
|
|
|
|
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
|
|
}
|