mirror of
https://github.com/fosrl/olm.git
synced 2026-05-13 11: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
364 lines
11 KiB
Go
364 lines
11 KiB
Go
//go:build (linux && !android) || freebsd
|
|
|
|
package dns
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
dbus "github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
// NetworkManager D-Bus constants
|
|
networkManagerDest = "org.freedesktop.NetworkManager"
|
|
networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager"
|
|
networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager"
|
|
networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager"
|
|
networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode"
|
|
networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version"
|
|
|
|
// NetworkManager dispatcher script path
|
|
networkManagerDispatcherDir = "/etc/NetworkManager/dispatcher.d"
|
|
networkManagerConfDir = "/etc/NetworkManager/conf.d"
|
|
networkManagerDNSConfFile = "olm-dns.conf"
|
|
networkManagerDispatcherFile = "01-olm-dns"
|
|
)
|
|
|
|
// NetworkManagerDNSConfigurator manages DNS settings using NetworkManager configuration files
|
|
// This approach works with unmanaged interfaces by modifying NetworkManager's global DNS settings
|
|
type NetworkManagerDNSConfigurator struct {
|
|
ifaceName string
|
|
originalState *DNSState
|
|
confPath string
|
|
dispatchPath string
|
|
}
|
|
|
|
// NewNetworkManagerDNSConfigurator creates a new NetworkManager DNS configurator
|
|
func NewNetworkManagerDNSConfigurator(ifaceName string) (*NetworkManagerDNSConfigurator, error) {
|
|
if ifaceName == "" {
|
|
return nil, fmt.Errorf("interface name is required")
|
|
}
|
|
|
|
// Check that NetworkManager conf.d directory exists
|
|
if _, err := os.Stat(networkManagerConfDir); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("NetworkManager conf.d directory not found: %s", networkManagerConfDir)
|
|
}
|
|
|
|
configurator := &NetworkManagerDNSConfigurator{
|
|
ifaceName: ifaceName,
|
|
confPath: networkManagerConfDir + "/" + networkManagerDNSConfFile,
|
|
dispatchPath: networkManagerDispatcherDir + "/" + networkManagerDispatcherFile,
|
|
}
|
|
|
|
// Clean up any stale configuration from a previous unclean shutdown
|
|
if err := configurator.CleanupUncleanShutdown(); err != nil {
|
|
return nil, fmt.Errorf("cleanup unclean shutdown: %w", err)
|
|
}
|
|
|
|
return configurator, nil
|
|
}
|
|
|
|
// Name returns the configurator name
|
|
func (n *NetworkManagerDNSConfigurator) Name() string {
|
|
return "network-manager"
|
|
}
|
|
|
|
// 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 {
|
|
// If we can't get current DNS, proceed anyway
|
|
originalServers = []netip.Addr{}
|
|
}
|
|
|
|
// 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 {
|
|
// Remove our configuration file
|
|
if err := os.Remove(n.confPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove DNS config file: %w", err)
|
|
}
|
|
|
|
// Reload NetworkManager to apply the change
|
|
if err := n.reloadNetworkManager(); err != nil {
|
|
return fmt.Errorf("reload NetworkManager: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupUncleanShutdown removes any DNS configuration left over from a previous crash
|
|
// For NetworkManager, we check if our config file exists and remove it if so.
|
|
// This ensures that if the process crashed while DNS was configured, the stale
|
|
// configuration is removed on the next startup.
|
|
func (n *NetworkManagerDNSConfigurator) CleanupUncleanShutdown() error {
|
|
// Check if our config file exists from a previous session
|
|
if _, err := os.Stat(n.confPath); os.IsNotExist(err) {
|
|
// No config file, nothing to clean up
|
|
return nil
|
|
}
|
|
|
|
// Remove the stale configuration file
|
|
if err := os.Remove(n.confPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove stale DNS config file: %w", err)
|
|
}
|
|
|
|
// Reload NetworkManager to apply the change
|
|
if err := n.reloadNetworkManager(); err != nil {
|
|
return fmt.Errorf("reload NetworkManager after cleanup: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetCurrentDNS returns the currently configured DNS servers by reading /etc/resolv.conf
|
|
func (n *NetworkManagerDNSConfigurator) GetCurrentDNS() ([]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 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
|
|
}
|
|
|
|
// applyDNSServers applies DNS server configuration via NetworkManager config file
|
|
func (n *NetworkManagerDNSConfigurator) applyDNSServers(servers []netip.Addr) error {
|
|
if len(servers) == 0 {
|
|
return fmt.Errorf("no DNS servers provided")
|
|
}
|
|
|
|
// Build DNS server list
|
|
var dnsServers []string
|
|
for _, server := range servers {
|
|
dnsServers = append(dnsServers, server.String())
|
|
}
|
|
|
|
// Create NetworkManager configuration file that sets global DNS
|
|
// This overrides DNS for all connections
|
|
configContent := fmt.Sprintf(`# Generated by Olm DNS Manager - DO NOT EDIT
|
|
# This file configures NetworkManager to use Olm's DNS proxy
|
|
|
|
[global-dns-domain-*]
|
|
servers=%s
|
|
`, strings.Join(dnsServers, ","))
|
|
|
|
// Write the configuration file
|
|
if err := os.WriteFile(n.confPath, []byte(configContent), 0644); err != nil {
|
|
return fmt.Errorf("write DNS config file: %w", err)
|
|
}
|
|
|
|
// Reload NetworkManager to apply the new configuration
|
|
if err := n.reloadNetworkManager(); err != nil {
|
|
// Try to clean up
|
|
os.Remove(n.confPath)
|
|
return fmt.Errorf("reload NetworkManager: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// reloadNetworkManager tells NetworkManager to reload its configuration
|
|
func (n *NetworkManagerDNSConfigurator) reloadNetworkManager() error {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return fmt.Errorf("connect to system bus: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
// Call Reload method with flags=0 (reload everything)
|
|
// See: https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.html#gdbus-method-org-freedesktop-NetworkManager.Reload
|
|
err = obj.CallWithContext(ctx, networkManagerDest+".Reload", 0, uint32(0)).Store()
|
|
if err != nil {
|
|
return fmt.Errorf("call Reload: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// IsNetworkManagerDNSModeSupported checks if NetworkManager's DNS mode is one we can work with
|
|
// Some DNS modes delegate to other systems (like systemd-resolved) which we should use directly
|
|
func IsNetworkManagerDNSModeSupported() bool {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
|
|
obj := conn.Object(networkManagerDest, networkManagerDbusDNSManagerObjectNode)
|
|
|
|
modeVariant, err := obj.GetProperty(networkManagerDbusDNSManagerModeProperty)
|
|
if err != nil {
|
|
// If we can't get the mode, assume it's not supported
|
|
return false
|
|
}
|
|
|
|
mode, ok := modeVariant.Value().(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// If NetworkManager is delegating DNS to systemd-resolved, we should use
|
|
// systemd-resolved directly for better control
|
|
switch mode {
|
|
case "systemd-resolved":
|
|
// NetworkManager is delegating to systemd-resolved
|
|
// We should use systemd-resolved configurator instead
|
|
return false
|
|
case "dnsmasq", "unbound":
|
|
// NetworkManager is using a local resolver that it controls
|
|
// We can configure DNS through NetworkManager
|
|
return true
|
|
case "default", "none", "":
|
|
// NetworkManager is managing DNS directly or not at all
|
|
// We can configure DNS through NetworkManager
|
|
return true
|
|
default:
|
|
// Unknown mode, try to use it
|
|
return true
|
|
}
|
|
}
|
|
|
|
// GetNetworkManagerDNSMode returns the current DNS mode of NetworkManager
|
|
func GetNetworkManagerDNSMode() (string, error) {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return "", fmt.Errorf("connect to system bus: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
obj := conn.Object(networkManagerDest, networkManagerDbusDNSManagerObjectNode)
|
|
|
|
modeVariant, err := obj.GetProperty(networkManagerDbusDNSManagerModeProperty)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get DNS mode property: %w", err)
|
|
}
|
|
|
|
mode, ok := modeVariant.Value().(string)
|
|
if !ok {
|
|
return "", errors.New("DNS mode is not a string")
|
|
}
|
|
|
|
return mode, nil
|
|
}
|
|
|
|
// GetNetworkManagerVersion returns the version of NetworkManager
|
|
func GetNetworkManagerVersion() (string, error) {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return "", fmt.Errorf("connect to system bus: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode)
|
|
|
|
versionVariant, err := obj.GetProperty(networkManagerDbusVersionProperty)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get version property: %w", err)
|
|
}
|
|
|
|
version, ok := versionVariant.Value().(string)
|
|
if !ok {
|
|
return "", errors.New("version is not a string")
|
|
}
|
|
|
|
return version, nil
|
|
}
|
|
|
|
// CleanupStaleNetworkManagerDNS removes any stale DNS configuration left by NetworkManager
|
|
// 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.
|
|
func CleanupStaleNetworkManagerDNS() error {
|
|
confPath := networkManagerConfDir + "/" + networkManagerDNSConfFile
|
|
|
|
// Check if our config file exists from a previous session
|
|
if _, err := os.Stat(confPath); os.IsNotExist(err) {
|
|
// No config file, nothing to clean up
|
|
return nil
|
|
}
|
|
|
|
// Remove the stale configuration file
|
|
if err := os.Remove(confPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove stale DNS config file: %w", err)
|
|
}
|
|
|
|
// Try to reload NetworkManager if it's available
|
|
if IsNetworkManagerAvailable() {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return fmt.Errorf("connect to system bus for reload: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := obj.CallWithContext(ctx, networkManagerDest+".Reload", 0, uint32(0)).Store(); err != nil {
|
|
return fmt.Errorf("reload NetworkManager after cleanup: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|