diff --git a/CHANGES b/CHANGES index 9b077cb59..19f42e478 100644 --- a/CHANGES +++ b/CHANGES @@ -2899,3 +2899,7 @@ * firewall: add masquerading rules for containers to reach each other via public IP * docker: fix parsing of optional namespace in image refs +[8.3.0] +* UI redesign +* mail: add ipv6 rdns check + diff --git a/dashboard/public/views/email.html b/dashboard/public/views/email.html index 92b15df7f..d2059846f 100644 --- a/dashboard/public/views/email.html +++ b/dashboard/public/views/email.html @@ -671,7 +671,7 @@
{{ 'email.dnsStatus.namecheapInfo' | tr }}
-{{ 'email.dnsStatus.ptrInfo' | tr }}
+{{ 'email.dnsStatus.ptrInfo' | tr }}
{{ 'email.dnsStatus.hostname' | tr }}: {{ expectedDnsRecords[record.value].name }}
{{ 'email.dnsStatus.domain' | tr }}: {{ expectedDnsRecords[record.value].domain }}
{{ 'email.dnsStatus.type' | tr }}: {{ expectedDnsRecords[record.value].type }}
diff --git a/dashboard/public/views/email.js b/dashboard/public/views/email.js index 9287c0bde..6be181517 100644 --- a/dashboard/public/views/email.js +++ b/dashboard/public/views/email.js @@ -45,11 +45,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio $scope.storageQuotaTicks = [ 500*1000*1000, 5*1000*1000*1000, 15*1000*1000*1000, 50*1000*1000*1000, 100*1000*1000*1000 ]; $scope.expectedDnsRecords = { - mx: { }, - dkim: { }, - spf: { }, - dmarc: { }, - ptr: { } + mx: {}, + dkim: {}, + spf: {}, + dmarc: {}, + ptr4: {}, + ptr6: {} }; $scope.expectedDnsRecordsTypes = [ @@ -57,7 +58,9 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio { name: 'DKIM', value: 'dkim' }, { name: 'SPF', value: 'spf' }, { name: 'DMARC', value: 'dmarc' }, - { name: 'PTR', value: 'ptr' } + { name: 'PTR4', value: 'ptr4' }, + { name: 'PTR6', value: 'ptr6' } + ]; $scope.openSubscriptionSetup = function () { diff --git a/src/mail.js b/src/mail.js index ed877748c..1e6fa5019 100644 --- a/src/mail.js +++ b/src/mail.js @@ -397,8 +397,60 @@ async function checkDmarc(domain) { return dmarc; } -// TODO: check ip6.arpa for IPv6 PTR -async function checkPtr(mailFqdn) { +async function checkPtr6(mailFqdn) { + assert.strictEqual(typeof mailFqdn, 'string'); + + const ptr = { + domain: null, + name: null, + type: 'PTR', + value: null, + expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918) + status: false, + errorMessage: '' + }; + + const [error, ip] = await safe(network.getIPv6()); + if (error) { + ptr.errorMessage = error.message; + return ptr; + } + if (ip === null) { + ptr.errorMessage = 'Server has no IPv6'; + return ptr; + } + + function expandIPv6(ipv6) { + const parts = ipv6.split('::'); + const left = parts[0].split(':'); + const right = parts[1] ? parts[1].split(':') : []; + const fill = new Array(8 - left.length - right.length).fill('0'); + const full = [...left, ...fill, ...right]; + return full.map(part => part.padStart(4, '0')).join(''); + } + + const expanded = expandIPv6(ip); + const reversed = expanded.split('').reverse().join(''); + const reversedWithDots = reversed.split('').join('.'); + + ptr.domain = `${reversedWithDots}.ip6.arpa`; + ptr.name = ip; + + const [error2, ptrRecords] = await safe(dig.resolve(ptr.domain, 'PTR', DNS_OPTIONS)); + if (error2) { + ptr.errorMessage = error2.message; + return ptr; + } + + if (ptrRecords.length !== 0) { + ptr.value = ptrRecords.join(' '); + ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; }); + } + + return ptr; +} + +async function checkPtr4(mailFqdn) { assert.strictEqual(typeof mailFqdn, 'string'); const ptr = { @@ -416,6 +468,10 @@ async function checkPtr(mailFqdn) { ptr.errorMessage = error.message; return ptr; } + if (ip === null) { + ptr.errorMessage = 'Server has no IPv4'; + return ptr; + } ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa'; ptr.name = ip; @@ -550,7 +606,8 @@ async function getStatus(domain) { checks.push( { what: 'dns.spf', promise: checkSpf(domain, fqdn) }, { what: 'dns.dkim', promise: checkDkim(mailDomain) }, - { what: 'dns.ptr', promise: checkPtr(fqdn) }, + { what: 'dns.ptr4', promise: checkPtr4(fqdn) }, + { what: 'dns.ptr6', promise: checkPtr6(fqdn) }, { what: 'relay', promise: checkOutboundPort25() }, { what: 'rbl', promise: checkRblStatus(domain) }, ); diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index cb3cbbf8e..141986d9d 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -138,11 +138,11 @@ describe('Mail API', function () { expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`); expect(response.body.dns.mx.status).to.eql(false); - expect(response.body.dns.ptr).to.be.an('object'); - expect(response.body.dns.ptr.type).to.eql('PTR'); + expect(response.body.dns.ptr4).to.be.an('object'); + expect(response.body.dns.ptr4.type).to.eql('PTR'); // expect(response.body.ptr.value).to.eql(null); this will be anything random - expect(response.body.dns.ptr.expected).to.eql(mailFqdn); - expect(response.body.dns.ptr.status).to.eql(false); + expect(response.body.dns.ptr4.expected).to.eql(mailFqdn); + expect(response.body.dns.ptr4.status).to.eql(false); }); it('succeeds with "undefined" spf, dkim, dmarc, mx, ptr records', async function () { @@ -178,9 +178,9 @@ describe('Mail API', function () { expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`); expect(response.body.dns.mx.value).to.eql(null); - expect(response.body.dns.ptr).to.be.an('object'); - expect(response.body.dns.ptr.expected).to.eql(mailFqdn); - expect(response.body.dns.ptr.status).to.eql(false); + expect(response.body.dns.ptr4).to.be.an('object'); + expect(response.body.dns.ptr4.expected).to.eql(mailFqdn); + expect(response.body.dns.ptr4.status).to.eql(false); // expect(response.body.ptr.value).to.eql(null); this will be anything random }); @@ -217,9 +217,9 @@ describe('Mail API', function () { expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`); expect(response.body.dns.mx.value).to.eql(`20 ${mailFqdn}. 10 some.other.server.`); - expect(response.body.dns.ptr).to.be.an('object'); - expect(response.body.dns.ptr.expected).to.eql(mailFqdn); - expect(response.body.dns.ptr.status).to.eql(false); + expect(response.body.dns.ptr4).to.be.an('object'); + expect(response.body.dns.ptr4.expected).to.eql(mailFqdn); + expect(response.body.dns.ptr4.status).to.eql(false); // expect(response.body.ptr.value).to.eql(null); this will be anything random expect(response.body.relay).to.be.an('object');