From e5d0ed8c4c092f8a63b697c41e2f5408b9267028 Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:33:46 -0700 Subject: [PATCH] [Web] use absolute RHS names in generated DNS zonefile The DNS overview "Download" produces a $ORIGIN zonefile, but the right-hand side of MX, CNAME and SRV records was emitted as a relative name. A target such as mail.example.net is then read relative to the origin and expands to mail.example.net.example.org., which is wrong. The only prior attempt at making names absolute was str_replace($domain, $domain . '.', ...), which appended a dot only to targets that happened to contain the origin domain, so cross-domain targets stayed relative. That same replace also corrupted any TXT value containing the origin (e.g. a DMARC rua=mailto:x@example.org became ...@example.org.). Absolutize the RHS per record type at export time only: MX and CNAME targets, and the SRV target token, get a trailing dot; ports, the SRV root target ".", IP addresses and TXT character strings are left as is. The records used for the on-page DNS validation are untouched, so matching against dns_get_record() output still works. Fixes #6984 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/inc/ajax/dns_diagnostics.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/data/web/inc/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 95e34e886..1987a88cd 100644 --- a/data/web/inc/ajax/dns_diagnostics.php +++ b/data/web/inc/ajax/dns_diagnostics.php @@ -442,6 +442,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm unset($record); + // Make a hostname RHS absolute so it is not read relative to $ORIGIN. + // Already-absolute names, the SRV root target "." and IP addresses are left alone. + $absolutize = function($host) { + $host = trim($host); + if ($host === '' || $host === '.' || substr($host, -1) === '.' || filter_var($host, FILTER_VALIDATE_IP)) { + return $host; + } + return $host . '.'; + }; + $dns_data = sprintf("\$ORIGIN %s.\n", $domain); foreach ($records as $record) { if ($domain == substr($record[0], -strlen($domain))) { @@ -462,16 +472,26 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm $val = str_replace(state_optional, '', $val); $val = str_replace(state_good, '', $val); if (strlen($val) > 0) { + // these are all TXT values, their RHS is a character string, not a name $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val); } } } else { + if ($record[1] == 'MX' || $record[1] == 'CNAME') { + $val = $absolutize($val); + } + elseif ($record[1] == 'SRV') { + // format here is "target port"; only the target is a name + $parts = explode(' ', $val, 2); + $parts[0] = $absolutize($parts[0]); + $val = implode(' ', $parts); + } $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val); } foreach ($vals as $val) { - $dns_data .= str_replace($domain, $domain . '.', $val); + $dns_data .= $val; } } }