//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 }