From 94d5561e7cc39eb2909bdc36d4ef4972cd21e56d Mon Sep 17 00:00:00 2001 From: Bobby Date: Sun, 8 Mar 2026 23:38:54 +0530 Subject: Refactor DNS and SMTP configurations; add system DNS management - Updated DNS server address configuration to use BindAddress and DnsPort. - Enhanced email submission to utilize BindAddress for SMTP server address. - Improved error messages for unknown recipient domains. - Introduced a new OrderedMap structure for route management. - Added system DNS management functions for Linux, Darwin, and Windows platforms. - Created new dashboard services for DNS configuration and overview. - Updated UI to include Proxy Rules section and improved descriptions. - Added new utility functions for handling DNS configurations. --- utils/collections/orderedmap.go | 37 +++++ utils/dns/server.go | 2 +- utils/dns/system.go | 358 ++++++++++++++++++++++++++++++++++++++++ utils/email/submit.go | 13 +- utils/smtp/messages.go | 2 +- utils/smtp/server.go | 16 +- utils/urls/attach.go | 2 +- utils/urls/path.go | 6 +- utils/urls/registry.go | 4 +- 9 files changed, 413 insertions(+), 27 deletions(-) create mode 100644 utils/collections/orderedmap.go create mode 100644 utils/dns/system.go (limited to 'utils') diff --git a/utils/collections/orderedmap.go b/utils/collections/orderedmap.go new file mode 100644 index 0000000..b9f17b7 --- /dev/null +++ b/utils/collections/orderedmap.go @@ -0,0 +1,37 @@ +package collections + +type OrderedMap[K comparable, V any] struct { + keys []K + values map[K]V +} + +func OrderedMapOf[K comparable, V any]() OrderedMap[K, V] { + return OrderedMap[K, V]{ + keys: make([]K, 0), + values: make(map[K]V), + } +} + +func (orderedMap *OrderedMap[K, V]) Set(key K, value V) { + if _, exists := orderedMap.values[key]; !exists { + orderedMap.keys = append(orderedMap.keys, key) + } + orderedMap.values[key] = value +} + +func (orderedMap *OrderedMap[K, V]) Get(key K) (V, bool) { + value, exists := orderedMap.values[key] + return value, exists +} + +func (orderedMap *OrderedMap[K, V]) All() []V { + result := make([]V, 0, len(orderedMap.keys)) + for _, key := range orderedMap.keys { + result = append(result, orderedMap.values[key]) + } + return result +} + +func (orderedMap *OrderedMap[K, V]) Len() int { + return len(orderedMap.keys) +} diff --git a/utils/dns/server.go b/utils/dns/server.go index 7ae8770..1cd5623 100644 --- a/utils/dns/server.go +++ b/utils/dns/server.go @@ -14,7 +14,7 @@ import ( var activeServer *mdns.Server func Start() { - address := fmt.Sprintf("%s:%d", config.DNS.Host, config.DNS.Port) + address := fmt.Sprintf("%s:%d", config.BindAddress, config.DnsPort) mdns.HandleFunc(".", handleQuery) diff --git a/utils/dns/system.go b/utils/dns/system.go new file mode 100644 index 0000000..85f2a85 --- /dev/null +++ b/utils/dns/system.go @@ -0,0 +1,358 @@ +package dns + +import ( + "dove/config" + "fmt" + "os" + "os/exec" + "runtime" + "strings" +) + +type SystemDnsStatus struct { + Configured bool `json:"configured"` + Address string `json:"address"` + Platform string `json:"platform"` +} + +func CheckSystemDns() SystemDnsStatus { + doveAddress := fmt.Sprintf("%s", config.BindAddress) + status := SystemDnsStatus{ + Address: fmt.Sprintf("%s:%d", config.BindAddress, config.DnsPort), + Platform: runtime.GOOS, + } + + switch runtime.GOOS { + case "linux": + status.Configured = checkLinuxDns(doveAddress) + case "darwin": + status.Configured = checkDarwinDns(doveAddress) + case "windows": + status.Configured = checkWindowsDns(doveAddress) + } + + return status +} + +func ConfigureSystemDns() error { + doveAddress := config.BindAddress + + switch runtime.GOOS { + case "linux": + return configureLinuxDns(doveAddress) + case "darwin": + return configureDarwinDns(doveAddress) + case "windows": + return configureWindowsDns(doveAddress) + } + + return fmt.Errorf("Unsupported platform: %s.", runtime.GOOS) +} + +func DisableSystemDns() error { + doveAddress := config.BindAddress + + switch runtime.GOOS { + case "linux": + return disableLinuxDns(doveAddress) + case "darwin": + return disableDarwinDns(doveAddress) + case "windows": + return disableWindowsDns(doveAddress) + } + + return fmt.Errorf("Unsupported platform: %s.", runtime.GOOS) +} + +func checkLinuxDns(doveAddress string) bool { + if isSystemdResolved() { + output, commandError := exec.Command("resolvectl", "dns").Output() + if commandError != nil { + return false + } + return strings.Contains(string(output), doveAddress) + } + + resolvContents, readError := os.ReadFile("/etc/resolv.conf") + if readError != nil { + return false + } + + expectedLine := fmt.Sprintf("nameserver %s", doveAddress) + for _, line := range strings.Split(string(resolvContents), "\n") { + if strings.TrimSpace(line) == expectedLine { + return true + } + } + return false +} + +func configureLinuxDns(doveAddress string) error { + if isSystemdResolved() { + activeInterfaces, interfaceError := getActiveLinuxInterfaces() + if interfaceError != nil { + return interfaceError + } + for _, networkInterface := range activeInterfaces { + configureError := exec.Command("resolvectl", "dns", networkInterface, doveAddress).Run() + if configureError != nil { + return fmt.Errorf("Failed to configure DNS on %s: %w.", networkInterface, configureError) + } + } + return nil + } + + return prependNameserverToResolvConf(doveAddress) +} + +func disableLinuxDns(doveAddress string) error { + if isSystemdResolved() { + activeInterfaces, interfaceError := getActiveLinuxInterfaces() + if interfaceError != nil { + return interfaceError + } + for _, networkInterface := range activeInterfaces { + exec.Command("resolvectl", "revert", networkInterface).Run() + } + return nil + } + + return removeNameserverFromResolvConf(doveAddress) +} + +func isSystemdResolved() bool { + _, lookupError := exec.LookPath("resolvectl") + if lookupError != nil { + return false + } + serviceCheckError := exec.Command("systemctl", "is-active", "--quiet", "systemd-resolved").Run() + return serviceCheckError == nil +} + +func getActiveLinuxInterfaces() ([]string, error) { + output, commandError := exec.Command("ip", "-o", "link", "show", "up").Output() + if commandError != nil { + return nil, fmt.Errorf("Failed to list network interfaces: %w.", commandError) + } + + var activeInterfaces []string + for _, line := range strings.Split(string(output), "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 { + interfaceName := strings.TrimSuffix(fields[1], ":") + if interfaceName != "lo" { + activeInterfaces = append(activeInterfaces, interfaceName) + } + } + } + + if len(activeInterfaces) == 0 { + return nil, fmt.Errorf("No active network interfaces found.") + } + return activeInterfaces, nil +} + +func prependNameserverToResolvConf(doveAddress string) error { + resolvContents, readError := os.ReadFile("/etc/resolv.conf") + if readError != nil { + return fmt.Errorf("Failed to read /etc/resolv.conf: %w.", readError) + } + + newLine := fmt.Sprintf("nameserver %s", doveAddress) + existingLines := strings.Split(string(resolvContents), "\n") + + for _, line := range existingLines { + if strings.TrimSpace(line) == newLine { + return nil + } + } + + updatedContents := newLine + "\n" + string(resolvContents) + return os.WriteFile("/etc/resolv.conf", []byte(updatedContents), 0644) +} + +func removeNameserverFromResolvConf(doveAddress string) error { + resolvContents, readError := os.ReadFile("/etc/resolv.conf") + if readError != nil { + return fmt.Errorf("Failed to read /etc/resolv.conf: %w.", readError) + } + + targetLine := fmt.Sprintf("nameserver %s", doveAddress) + existingLines := strings.Split(string(resolvContents), "\n") + var filteredLines []string + + for _, line := range existingLines { + if strings.TrimSpace(line) != targetLine { + filteredLines = append(filteredLines, line) + } + } + + return os.WriteFile("/etc/resolv.conf", []byte(strings.Join(filteredLines, "\n")), 0644) +} + +func checkDarwinDns(doveAddress string) bool { + output, commandError := exec.Command("scutil", "--dns").Output() + if commandError != nil { + return false + } + return strings.Contains(string(output), doveAddress) +} + +func configureDarwinDns(doveAddress string) error { + output, commandError := exec.Command("networksetup", "-listallnetworkservices").Output() + if commandError != nil { + return fmt.Errorf("Failed to list network services: %w.", commandError) + } + + lines := strings.Split(string(output), "\n") + for _, serviceName := range lines { + serviceName = strings.TrimSpace(serviceName) + if serviceName == "" || strings.HasPrefix(serviceName, "An asterisk") { + continue + } + + existingDns, _ := exec.Command("networksetup", "-getdnsservers", serviceName).Output() + existingServers := strings.TrimSpace(string(existingDns)) + + var dnsServers []string + dnsServers = append(dnsServers, doveAddress) + if existingServers != "" && !strings.Contains(existingServers, "aren't any") { + for _, server := range strings.Split(existingServers, "\n") { + server = strings.TrimSpace(server) + if server != "" && server != doveAddress { + dnsServers = append(dnsServers, server) + } + } + } + + configureError := exec.Command("networksetup", "-setdnsservers", serviceName, strings.Join(dnsServers, " ")).Run() + if configureError != nil { + return fmt.Errorf("Failed to configure DNS on %s: %w.", serviceName, configureError) + } + } + + return nil +} + +func disableDarwinDns(doveAddress string) error { + output, commandError := exec.Command("networksetup", "-listallnetworkservices").Output() + if commandError != nil { + return fmt.Errorf("Failed to list network services: %w.", commandError) + } + + lines := strings.Split(string(output), "\n") + for _, serviceName := range lines { + serviceName = strings.TrimSpace(serviceName) + if serviceName == "" || strings.HasPrefix(serviceName, "An asterisk") { + continue + } + + existingDns, _ := exec.Command("networksetup", "-getdnsservers", serviceName).Output() + existingServers := strings.TrimSpace(string(existingDns)) + + var remainingServers []string + if existingServers != "" && !strings.Contains(existingServers, "aren't any") { + for _, server := range strings.Split(existingServers, "\n") { + server = strings.TrimSpace(server) + if server != "" && server != doveAddress { + remainingServers = append(remainingServers, server) + } + } + } + + if len(remainingServers) == 0 { + exec.Command("networksetup", "-setdnsservers", serviceName, "Empty").Run() + } else { + exec.Command("networksetup", "-setdnsservers", serviceName, strings.Join(remainingServers, " ")).Run() + } + } + + return nil +} + +func checkWindowsDns(doveAddress string) bool { + output, commandError := exec.Command("powershell", "-Command", + "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses").Output() + if commandError != nil { + return false + } + return strings.Contains(string(output), doveAddress) +} + +func configureWindowsDns(doveAddress string) error { + output, commandError := exec.Command("powershell", "-Command", + "Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -ExpandProperty InterfaceAlias").Output() + if commandError != nil { + return fmt.Errorf("Failed to list network adapters: %w.", commandError) + } + + adapters := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, adapterName := range adapters { + adapterName = strings.TrimSpace(adapterName) + if adapterName == "" { + continue + } + + existingOutput, _ := exec.Command("powershell", "-Command", + fmt.Sprintf("(Get-DnsClientServerAddress -InterfaceAlias '%s' -AddressFamily IPv4).ServerAddresses -join ','", adapterName)).Output() + existingServers := strings.TrimSpace(string(existingOutput)) + + var dnsServers []string + dnsServers = append(dnsServers, fmt.Sprintf("'%s'", doveAddress)) + if existingServers != "" { + for _, server := range strings.Split(existingServers, ",") { + server = strings.TrimSpace(server) + if server != "" && server != doveAddress { + dnsServers = append(dnsServers, fmt.Sprintf("'%s'", server)) + } + } + } + + setCommand := fmt.Sprintf("Set-DnsClientServerAddress -InterfaceAlias '%s' -ServerAddresses (%s)", + adapterName, strings.Join(dnsServers, ",")) + configureError := exec.Command("powershell", "-Command", setCommand).Run() + if configureError != nil { + return fmt.Errorf("Failed to configure DNS on %s: %w.", adapterName, configureError) + } + } + + return nil +} + +func disableWindowsDns(doveAddress string) error { + output, commandError := exec.Command("powershell", "-Command", + "Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -ExpandProperty InterfaceAlias").Output() + if commandError != nil { + return fmt.Errorf("Failed to list network adapters: %w.", commandError) + } + + adapters := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, adapterName := range adapters { + adapterName = strings.TrimSpace(adapterName) + if adapterName == "" { + continue + } + + existingOutput, _ := exec.Command("powershell", "-Command", + fmt.Sprintf("(Get-DnsClientServerAddress -InterfaceAlias '%s' -AddressFamily IPv4).ServerAddresses -join ','", adapterName)).Output() + + var remainingServers []string + for _, server := range strings.Split(strings.TrimSpace(string(existingOutput)), ",") { + server = strings.TrimSpace(server) + if server != "" && server != doveAddress { + remainingServers = append(remainingServers, fmt.Sprintf("'%s'", server)) + } + } + + if len(remainingServers) == 0 { + exec.Command("powershell", "-Command", + fmt.Sprintf("Set-DnsClientServerAddress -InterfaceAlias '%s' -ResetServerAddresses", adapterName)).Run() + } else { + setCommand := fmt.Sprintf("Set-DnsClientServerAddress -InterfaceAlias '%s' -ServerAddresses (%s)", + adapterName, strings.Join(remainingServers, ",")) + exec.Command("powershell", "-Command", setCommand).Run() + } + } + + return nil +} diff --git a/utils/email/submit.go b/utils/email/submit.go index 255c630..82589e7 100644 --- a/utils/email/submit.go +++ b/utils/email/submit.go @@ -8,13 +8,13 @@ import ( ) func Submit(senderAddress string, recipients []string, rawMessage []byte) error { - serverAddress := resolveLocalSMTPAddress() + serverAddress := fmt.Sprintf("%s:%d", config.BindAddress, config.SmtpPort) logger.Debugf(LogPrefix, SubmittingMessage, senderAddress, recipients, serverAddress) var smtpAuth smtp.Auth if config.SMTP.AuthRequired { - smtpAuth = smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Host) + smtpAuth = smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.BindAddress) } sendError := smtp.SendMail(serverAddress, smtpAuth, senderAddress, recipients, rawMessage) @@ -26,12 +26,3 @@ func Submit(senderAddress string, recipients []string, rawMessage []byte) error logger.Infof(LogPrefix, SubmitSuccess, senderAddress, recipients) return nil } - -func resolveLocalSMTPAddress() string { - host := config.SMTP.Host - if host == "0.0.0.0" || host == "::" { - host = "127.0.0.1" - } - - return fmt.Sprintf("%s:%d", host, config.SMTP.Port) -} diff --git a/utils/smtp/messages.go b/utils/smtp/messages.go index 0fd280b..8b14f8d 100644 --- a/utils/smtp/messages.go +++ b/utils/smtp/messages.go @@ -19,5 +19,5 @@ const ( ShutdownComplete = "All listeners stopped." ShutdownFailed = "Failed to shutdown %s listener: %v" TLSCertLoadFailed = "Failed to load TLS certificate: %v" - UnknownRecipientDomain = "Recipient domain %s is not managed by this server." + UnknownRecipientDomain = "No MX records found for recipient domain %s." ) diff --git a/utils/smtp/server.go b/utils/smtp/server.go index 0518ed7..572aea5 100644 --- a/utils/smtp/server.go +++ b/utils/smtp/server.go @@ -23,12 +23,12 @@ func Start() { tlsConfig = loadTLSConfig() } - plainAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.Port) + plainAddress := fmt.Sprintf("%s:%d", config.BindAddress, config.SmtpPort) plainServer := createServer(plainAddress) activeServers = append(activeServers, ServerInstance{Server: plainServer, Label: "SMTP"}) go startListener(plainServer, "SMTP", plainAddress) - smtpsAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.SMTPSPort) + smtpsAddress := fmt.Sprintf("%s:%d", config.BindAddress, config.SmtpsPort) smtpsServer := createServer(smtpsAddress) activeServers = append(activeServers, ServerInstance{Server: smtpsServer, Label: "SMTPS"}) if tlsConfig != nil { @@ -38,14 +38,14 @@ func Start() { go startListener(smtpsServer, "SMTPS", smtpsAddress) } - starttlsAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.StartTLSPort) - starttlsServer := createServer(starttlsAddress) - starttlsServer.EnableSMTPUTF8 = true - activeServers = append(activeServers, ServerInstance{Server: starttlsServer, Label: "STARTTLS"}) + submissionAddress := fmt.Sprintf("%s:%d", config.BindAddress, config.SubmissionPort) + submissionServer := createServer(submissionAddress) + submissionServer.EnableSMTPUTF8 = true + activeServers = append(activeServers, ServerInstance{Server: submissionServer, Label: "Submission"}) if tlsConfig != nil { - starttlsServer.TLSConfig = tlsConfig + submissionServer.TLSConfig = tlsConfig } - go startListener(starttlsServer, "STARTTLS", starttlsAddress) + go startListener(submissionServer, "Submission", submissionAddress) } func Shutdown() { diff --git a/utils/urls/attach.go b/utils/urls/attach.go index baf9929..906d765 100644 --- a/utils/urls/attach.go +++ b/utils/urls/attach.go @@ -6,7 +6,7 @@ func Attach(application *fiber.App) { registry.Mutex.Lock() defer registry.Mutex.Unlock() - for _, route := range registry.Routes { + for _, route := range registry.Routes.All() { bindRoute(application, route) } } diff --git a/utils/urls/path.go b/utils/urls/path.go index a3b3ec9..28f400f 100644 --- a/utils/urls/path.go +++ b/utils/urls/path.go @@ -26,21 +26,21 @@ func Path(method HTTPMethod, path string, handler fiber.Handler, name string) { fullName := resolveFullName(namespace, name) fullPath := resolveFullPath(namespace, path) - registry.Routes[fullName] = RegisteredRoute{ + registry.Routes.Set(fullName, RegisteredRoute{ Method: method, Path: path, Handler: handler, Namespace: namespace, Name: name, FullPath: fullPath, - } + }) } func GetFullPath(routeName string) (string, bool) { registry.Mutex.Lock() defer registry.Mutex.Unlock() - route, exists := registry.Routes[routeName] + route, exists := registry.Routes.Get(routeName) if !exists { return "", false } diff --git a/utils/urls/registry.go b/utils/urls/registry.go index e95d30c..e384a1e 100644 --- a/utils/urls/registry.go +++ b/utils/urls/registry.go @@ -20,11 +20,11 @@ type RegisteredRoute struct { type RouteRegistry struct { Mutex sync.Mutex CurrentNamespace string - Routes collections.Record[string, RegisteredRoute] + Routes collections.OrderedMap[string, RegisteredRoute] } var registry = &RouteRegistry{ - Routes: make(collections.Record[string, RegisteredRoute]), + Routes: collections.OrderedMapOf[string, RegisteredRoute](), } func SetNamespace(namespace string) { -- cgit v1.2.3