[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) <noreply@anthropic.com>
This commit is contained in:
Stephen Ritz 2026-07-14 16:33:46 -07:00
parent c1d75cf808
commit e5d0ed8c4c

View file

@ -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;
}
}
}