mirror of
https://github.com/fosrl/olm.git
synced 2026-05-15 12:49:56 +00:00
When the tunnel is forced close an integration may want to manually call cleanup function to fix stale issues without having the knowledge of which configuration to cleanup
256 lines
7.1 KiB
Go
256 lines
7.1 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
|
|
}
|
|
|
|
// CleanupStaleResolvconfDNS removes any stale DNS configuration left by the resolvconf
|
|
// configurator from a previous unclean shutdown. This is a static function that can be
|
|
// called without creating a configurator instance, useful for cleanup before network operations.
|
|
// The interfaceName parameter specifies which interface entry to clean up (typically "olm").
|
|
func CleanupStaleResolvconfDNS(interfaceName string) error {
|
|
if !IsResolvconfAvailable() {
|
|
// resolvconf not available, nothing to clean up
|
|
return nil
|
|
}
|
|
|
|
// Detect resolvconf implementation type
|
|
implType, err := detectResolvconfType()
|
|
if err != nil {
|
|
// Can't detect type, try default
|
|
implType = "resolvconf"
|
|
}
|
|
|
|
// 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 implType {
|
|
case "openresolv":
|
|
cmd = exec.Command(resolvconfCommand, "-f", "-d", interfaceName)
|
|
default:
|
|
cmd = exec.Command(resolvconfCommand, "-d", interfaceName)
|
|
}
|
|
|
|
// Ignore errors - the entry may not exist, which is fine
|
|
_ = cmd.Run()
|
|
|
|
return nil
|
|
}
|