openvibers/openvibe.tools Commits

Tools reputation: a null MX (RFC 7505) reads as 'accepts no mail', not a nameless server (#65)

OP OpenVibers committed 29 days ago · 2388621162 · 2 files, +10 −2

Domain Reputation, as seen in production: example.com read "1 mail server: ." because it publishes a null MX (RFC 7505, `0 .`), which Node returns as an empty exchange.

- **Null MX:** a null MX (an empty exchange or `.`) now reads as a good finding, "the domain says it accepts no mail", instead of a mail server with no name.
- **Real servers:** only real exchanges are counted and listed.

`net-reputation.test.js` covers a null MX.

Changes

ddiff --git a/apps/gateway/server/net/checks.js b/apps/gateway/server/net/checks.js
iindex 99a3abe..8b28485 100644
---- a/apps/gateway/server/net/checks.js
++++ b/apps/gateway/server/net/checks.js
@@@ -600,7 +600,9 @@ function createChecks({ egress, resolverFor = defaultResolver, tlsUpgrade = tls.
if (domain) {
// Mail: MX, SPF, DMARC
-- if (mx.length) add('Mail servers (MX)', 'good', `${mx.length} mail ${mx.length === 1 ? 'server' : 'servers'}: ${mx.sort((a, b) => a.priority - b.priority).slice(0, 3).map(m => m.exchange).join(', ')}.`);
++ const real = mx.filter(m => m.exchange && m.exchange !== '.');
++ if (real.length) add('Mail servers (MX)', 'good', `${real.length} mail ${real.length === 1 ? 'server' : 'servers'}: ${real.sort((a, b) => a.priority - b.priority).slice(0, 3).map(m => m.exchange).join(', ')}.`);
++ else if (mx.length) add('Mail servers (MX)', 'good', 'A null MX (RFC 7505): the domain says it accepts no mail, so nobody waits on a mail server that is not there.');
else add('Mail servers (MX)', 'info', 'No MX record: this domain does not receive mail. If it never sends mail either, SPF "v=spf1 -all" and DMARC p=reject stop others sending as it.');
if (!spf.length) add('SPF', 'warn', 'No SPF record: anyone can send mail claiming to be from this domain, and receivers trust its mail less.', 15);
else if (spf.length > 1) add('SPF', 'bad', `${spf.length} SPF records: receivers treat more than one as a permanent error, as if there were none.`, 15);
ddiff --git a/apps/gateway/test/net-reputation.test.js b/apps/gateway/test/net-reputation.test.js
iindex cdda70a..f5cc898 100644
---- a/apps/gateway/test/net-reputation.test.js
++++ b/apps/gateway/test/net-reputation.test.js
@@@ -15,7 +15,7 @@ const TXT = {
'_dmarc.good.example': [['v=DMARC1; p=reject; rua=mailto:[email protected]']],
'bad.example': [['v=spf1 +all']],
};
--const MX = { 'good.example': [{ exchange: 'mail.good.example', priority: 10 }] };
++const MX = { 'good.example': [{ exchange: 'mail.good.example', priority: 10 }], 'nomail.example': [{ exchange: '', priority: 0 }] };
const fail = (code) => Promise.reject(Object.assign(new Error(code), { code }));
const resolverFor = () => ({
resolve4: (q) => (LISTED[q] ? Promise.resolve(LISTED[q]) : A[q] ? Promise.resolve(A[q]) : fail('ENOTFOUND')),
@@@ -71,6 +71,12 @@ const reverse = async (ip) => ({ '93.184.216.31': ['host.isp.example'], '93.184.
assert.strictEqual(r.grade, 'poor');
assert.strictEqual(r.score, Math.max(0, 100 - r.findings.reduce((s, x) => s + x.cost, 0)));
++ // A null MX (RFC 7505: "0 ." comes back as an empty exchange) says the domain takes no mail.
++ A['nomail.example'] = ['93.184.216.40'];
++ f = byCheck(await checks.reputation('nomail.example', { rdap, reverse }));
++ assert.strictEqual(f['Mail servers (MX)'].status, 'good');
++ assert.match(f['Mail servers (MX)'].detail, /null MX/);
++
// An address: blacklists and reverse DNS.
r = await checks.reputation('93.184.216.31', { rdap, reverse });
f = byCheck(r);