Ensure that DMARC entries are generated as subdomain, Allow overwriting of DMARC and SPF subdomain records (#1237)

* Ensure that DMARC entries are generated as subdomain
- see https://datatracker.ietf.org/doc/html/rfc7489#section-6.1

* Add tests for DNS DMARC

* Allow custom SPF and DMARC subdomain records to replace default records

* Improve tests for DMARC, add DMARC tests for subdomain
This commit is contained in:
sro0 2024-02-09 08:11:41 +01:00 committed by GitHub
parent 953baec023
commit 686ca84a30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 163 additions and 7 deletions

View File

@ -212,7 +212,7 @@ class Dns
} }
if (Settings::Get('dmarc.use_dmarc') == '1') { if (Settings::Get('dmarc.use_dmarc') == '1') {
// check for DMARC content later // check for DMARC content later
self::addRequiredEntry('@DMARC@.' . $sub_record, 'TXT', $required_entries); self::addRequiredEntry('@DMARC@', 'TXT', $required_entries);
} }
if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') { if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') {
// check for DKIM content later // check for DKIM content later
@ -239,19 +239,28 @@ class Dns
} }
if (Settings::Get('spf.use_spf') == '1' if (Settings::Get('spf.use_spf') == '1'
&& $entry['type'] == 'TXT' && $entry['type'] == 'TXT'
&& $entry['record'] == '@'
&& (strtolower(substr($entry['content'], 0, 7)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1') && (strtolower(substr($entry['content'], 0, 7)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1')
) { ) {
// unset special spf required-entry // unset special spf required-entry
unset($required_entries[$entry['type']][md5("@SPF@")]); if ($entry['record'] == '@') {
unset($required_entries[$entry['type']][md5("@SPF@")]);
} else {
// subdomain
unset($required_entries[$entry['type']][md5("@SPF@." . $entry['record'])]);
}
} }
if (Settings::Get('dmarc.use_dmarc') == '1' if (Settings::Get('dmarc.use_dmarc') == '1'
&& $entry['type'] == 'TXT' && $entry['type'] == 'TXT'
&& $entry['record'] == '@' && ($entry['record'] == '_dmarc' || substr($entry['record'], 0, 7) == '_dmarc.')
&& (strtolower(substr($entry['content'], 0, 9)) == '"v=dmarc1' || strtolower(substr($entry['content'], 0, 8)) == 'v=dmarc1') && (strtolower(substr($entry['content'], 0, 9)) == '"v=dmarc1' || strtolower(substr($entry['content'], 0, 8)) == 'v=dmarc1')
) { ) {
// unset special dmarc required-entry // unset special dmarc required-entry
unset($required_entries[$entry['type']][md5("@DMARC@")]); if ($entry['record'] == '_dmarc') {
unset($required_entries[$entry['type']][md5("@DMARC@")]);
} else {
// subdomain
unset($required_entries[$entry['type']][md5("@DMARC@" . substr($entry['record'], 6))]);
}
} }
if (empty($primary_ns) && $entry['record'] == '@' && $entry['type'] == 'NS') { if (empty($primary_ns) && $entry['record'] == '@' && $entry['type'] == 'NS') {
// use the first NS entry pertaining to the current domain as primary ns // use the first NS entry pertaining to the current domain as primary ns
@ -392,12 +401,12 @@ class Dns
} elseif ($record == '@DMARC@') { } elseif ($record == '@DMARC@') {
// dmarc for main-domain // dmarc for main-domain
$txt_content = Settings::Get('dmarc.dmarc_entry'); $txt_content = Settings::Get('dmarc.dmarc_entry');
$zonerecords[] = new DnsEntry('@', 'TXT', self::encloseTXTContent($txt_content)); $zonerecords[] = new DnsEntry('_dmarc', 'TXT', self::encloseTXTContent($txt_content));
} elseif (strlen($record) > 8 && substr($record, 0, 8) == '@DMARC@.') { } elseif (strlen($record) > 8 && substr($record, 0, 8) == '@DMARC@.') {
// dmarc for subdomain // dmarc for subdomain
$txt_content = Settings::Get('dmarc.dmarc_entry'); $txt_content = Settings::Get('dmarc.dmarc_entry');
$sub_record = substr($record, 8); $sub_record = substr($record, 8);
$zonerecords[] = new DnsEntry($sub_record, 'TXT', self::encloseTXTContent($txt_content)); $zonerecords[] = new DnsEntry('_dmarc.' . $sub_record, 'TXT', self::encloseTXTContent($txt_content));
} elseif (!empty($dkim_entries)) { } elseif (!empty($dkim_entries)) {
// DKIM entries // DKIM entries
$dkim_record = 'dkim' . $domain['dkim_id'] . '._domainkey'; $dkim_record = 'dkim' . $domain['dkim_id'] . '._domainkey';

View File

@ -2,9 +2,11 @@
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Froxlor\Settings; use Froxlor\Settings;
use Froxlor\Api\Commands\Admins;
use Froxlor\Api\Commands\Customers; use Froxlor\Api\Commands\Customers;
use Froxlor\Api\Commands\DomainZones; use Froxlor\Api\Commands\DomainZones;
use Froxlor\Api\Commands\Domains; use Froxlor\Api\Commands\Domains;
use Froxlor\Api\Commands\SubDomains;
/** /**
* *
@ -63,6 +65,89 @@ class DomainZonesTest extends TestCase
DomainZones::getLocal($customer_userdata, $data)->get(); DomainZones::getLocal($customer_userdata, $data)->get();
} }
/**
*
* @depends testCustomerDomainZonesGet
*/
public function testCustomerDomainZonesGetWithDMARC()
{
global $admin_userdata;
Settings::Set('dmarc.use_dmarc', 1, true);
// get customer
$json_result = Customers::getLocal($admin_userdata, array(
'loginname' => 'test1'
))->get();
$customer_userdata = json_decode($json_result, true)['data'];
$data = [
'domainname' => 'test2.local'
];
$json_result = DomainZones::getLocal($customer_userdata, $data)->get();
$result = json_decode($json_result, true)['data'];
$this->assertTrue(count($result) > 1);
$foundCnt = 0;
$foundStr = '';
foreach ($result as $entry) {
if (substr($entry, 0, 7) == '_dmarc ') {
$foundCnt++;
$foundStr = $entry;
}
}
$this->assertEquals(1, $foundCnt);
$resstr = preg_replace('/\s+/', '', $foundStr);
$against = preg_replace('/\s+/', '', '_dmarc 604800 IN TXT v=DMARC1;p=none;');
$this->assertEquals($against, $resstr);
}
/**
*
* @depends testCustomerDomainZonesGetWithDMARC
*/
public function testCustomerDomainZonesGetWithDMARCSubdomain()
{
global $admin_userdata;
// enable isemaildomain for subdomain
$json_result = Admins::getLocal($admin_userdata, array(
'loginname' => 'reseller'
))->get();
$reseller_userdata = json_decode($json_result, true)['data'];
$reseller_userdata['adminsession'] = 1;
$data = [
'domainname' => 'mysub2.test2.local',
'isemaildomain' => 1,
'customerid' => 1
];
$json_result = SubDomains::getLocal($reseller_userdata, $data)->update();
// get customer
$json_result = Customers::getLocal($admin_userdata, array(
'loginname' => 'test1'
))->get();
$customer_userdata = json_decode($json_result, true)['data'];
$data = [
'domainname' => 'test2.local'
];
$json_result = DomainZones::getLocal($customer_userdata, $data)->get();
$result = json_decode($json_result, true)['data'];
$foundCnt = 0;
$foundStr = '';
$this->assertTrue(count($result) > 1);
foreach ($result as $entry) {
if (substr($entry, 0, 14) == '_dmarc.mysub2 ') {
$foundCnt++;
$foundStr = $entry;
}
}
$this->assertEquals(1, $foundCnt);
$resstr = preg_replace('/\s+/', '', $foundStr);
$against = preg_replace('/\s+/', '', '_dmarc.mysub2 604800 IN TXT v=DMARC1;p=none;');
$this->assertEquals($against, $resstr);
}
public function testAdminDomainZonesUpdate() public function testAdminDomainZonesUpdate()
{ {
global $admin_userdata; global $admin_userdata;
@ -872,6 +957,68 @@ class DomainZonesTest extends TestCase
$this->assertEquals('_test1 18000 IN TXT aw yeah', $entry); $this->assertEquals('_test1 18000 IN TXT aw yeah', $entry);
} }
/**
*
* @depends testCustomerDomainZonesGetWithDMARC
*/
public function testAdminDomainZonesAddTXTCustomDMARC()
{
global $admin_userdata;
$data = [
'domainname' => 'test2.local',
'record' => '_dmarc',
'type' => 'TXT',
'content' => 'v=DMARC1;p=none;overwrite=TRUE'
];
$json_result = DomainZones::getLocal($admin_userdata, $data)->add();
$result = json_decode($json_result, true)['data'];
$this->assertTrue(count($result) > 1);
$foundCnt = 0;
$foundStr = '';
foreach ($result as $entry) {
if (substr($entry, 0, 7) == '_dmarc ') {
$foundCnt++;
$foundStr = $entry;
}
}
$this->assertEquals(1, $foundCnt);
$resstr = preg_replace('/\s+/', '', $foundStr);
$against = preg_replace('/\s+/', '', '_dmarc 18000 IN TXT v=DMARC1;p=none;overwrite=TRUE');
$this->assertEquals($against, $resstr);
}
/**
*
* @depends testCustomerDomainZonesGetWithDMARCSubdomain
*/
public function testAdminDomainZonesAddTXTCustomDMARCSubdomain()
{
global $admin_userdata;
$data = [
'domainname' => 'test2.local',
'record' => '_dmarc.mysub2',
'type' => 'TXT',
'content' => 'v=DMARC1;p=none;overwrite=TRUE'
];
$json_result = DomainZones::getLocal($admin_userdata, $data)->add();
$result = json_decode($json_result, true)['data'];
$this->assertTrue(count($result) > 1);
$foundCnt = 0;
$foundStr = '';
foreach ($result as $entry) {
if (substr($entry, 0, 14) == '_dmarc.mysub2 ') {
$foundCnt++;
$foundStr = $entry;
}
}
$this->assertEquals(1, $foundCnt);
$resstr = preg_replace('/\s+/', '', $foundStr);
$against = preg_replace('/\s+/', '', '_dmarc.mysub2 18000 IN TXT v=DMARC1;p=none;overwrite=TRUE');
$this->assertEquals($against, $resstr);
}
public function testAdminDomainZonesAddSRV() public function testAdminDomainZonesAddSRV()
{ {
global $admin_userdata; global $admin_userdata;