Files
olm/dns/platform/network_manager.go
2025-11-26 12:13:29 -05:00

295 lines
8.7 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)
}
return &NetworkManagerDNSConfigurator{
ifaceName: ifaceName,
confPath: networkManagerConfDir + "/" + networkManagerDNSConfFile,
dispatchPath: networkManagerDispatcherDir + "/" + networkManagerDispatcherFile,
}, 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
}
// 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
}