aboutsummaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 23:38:54 +0530
committerBobby <[email protected]>2026-03-08 23:38:54 +0530
commit94d5561e7cc39eb2909bdc36d4ef4972cd21e56d (patch)
tree98d9792dda80f76f185ab2eb37c1de005be9ea0f /utils
parent1136af49815be77a0aca151f3b8ec7348bf4b4c8 (diff)
downloaddove-94d5561e7cc39eb2909bdc36d4ef4972cd21e56d.tar.xz
dove-94d5561e7cc39eb2909bdc36d4ef4972cd21e56d.zip
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.
Diffstat (limited to 'utils')
-rw-r--r--utils/collections/orderedmap.go37
-rw-r--r--utils/dns/server.go2
-rw-r--r--utils/dns/system.go358
-rw-r--r--utils/email/submit.go13
-rw-r--r--utils/smtp/messages.go2
-rw-r--r--utils/smtp/server.go16
-rw-r--r--utils/urls/attach.go2
-rw-r--r--utils/urls/path.go6
-rw-r--r--utils/urls/registry.go4
9 files changed, 413 insertions, 27 deletions
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) {