Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
Cannot retrieve contributors at this time.
Cannot retrieve contributors at this time
| 'use strict'; | |
| var dns = require('dns'); | |
| var net = require('net'); | |
| var async = require('async'); | |
| var tlds = require('haraka-tld'); | |
| var utils = require('./utils'); | |
| var net_utils = require('./net_utils'); | |
| exports.register = function () { | |
| var plugin = this; | |
| plugin.load_fcrdns_ini(); | |
| }; | |
| exports.load_fcrdns_ini = function () { | |
| var plugin = this; | |
| plugin.cfg = plugin.config.get('connect.fcrdns.ini', { | |
| booleans: [ | |
| '-reject.no_rdns', | |
| '-reject.no_fcrdns', | |
| '-reject.invalid_tld', | |
| '-reject.generic_rdns', | |
| ] | |
| }, function () { | |
| plugin.load_fcrdns_ini(); | |
| }); | |
| }; | |
| exports.hook_connect_init = function (next, connection) { | |
| var plugin = this; | |
| // always init, so results.get is deterministic | |
| connection.results.add(plugin, { | |
| fcrdns: [], // PTR host names that resolve to this IP | |
| invalid_tlds: [], // rDNS names with invalid TLDs | |
| other_ips: [], // IPs from names that didn't match | |
| ptr_names: [], // Array of host names from PTR query | |
| ptr_multidomain: false, // Multiple host names in different domains | |
| has_rdns: false, // does IP have PTR records? | |
| ptr_name_has_ips: false, // PTR host has IP address(es) | |
| ptr_name_to_ip: {}, // host names and their IP addresses | |
| }); | |
| next(); | |
| }; | |
| exports.hook_lookup_rdns = function (next, connection) { | |
| var plugin = this; | |
| var rip = connection.remote_ip; | |
| if (net_utils.is_private_ip(rip)) { | |
| connection.results.add(plugin, {skip: 'private_ip'}); | |
| return next(); | |
| } | |
| plugin.refresh_config(connection); | |
| var called_next = 0; | |
| var timer; | |
| var do_next = function (code, msg) { | |
| if (called_next) return; | |
| called_next++; | |
| clearTimeout(timer); | |
| return next(code, msg); | |
| }; | |
| // Set-up timer | |
| timer = setTimeout(function () { | |
| connection.results.add(plugin, {err: 'timeout', emit: true}); | |
| if (plugin.cfg.reject.no_rdns) { | |
| return do_next(DENYSOFT, 'client [' + rip + '] rDNS lookup timeout'); | |
| } | |
| return do_next(); | |
| }, (plugin.cfg.main.timeout || 30) * 1000); | |
| dns.reverse(rip, function (err, ptr_names) { | |
| connection.logdebug(plugin, 'rdns lookup: ' + rip); | |
| if (err) return plugin.handle_ptr_error(connection, err, do_next); | |
| connection.results.add(plugin, {ptr_names: ptr_names}); | |
| connection.results.add(plugin, {has_rdns: true}); | |
| // Fetch A & AAAA records for each PTR host name | |
| var pending_queries = 0; | |
| var queries_run = false; | |
| var results = {}; | |
| for (var i=0; i<ptr_names.length; i++) { | |
| var ptr_domain = ptr_names[i].toLowerCase(); | |
| results[ptr_domain] = []; | |
| // Make sure TLD is valid | |
| if (!tlds.get_organizational_domain(ptr_domain)) { | |
| connection.results.add(plugin, {fail: 'valid_tld(' + ptr_domain +')'}); | |
| if (!plugin.cfg.reject.invalid_tld) continue; | |
| if (net_utils.is_private_ip(rip)) continue; | |
| return do_next(DENY, 'client [' + rip + | |
| '] rejected; invalid TLD in rDNS (' + ptr_domain + ')'); | |
| } | |
| queries_run = true; | |
| connection.logdebug(plugin, 'domain: ' + ptr_domain); | |
| pending_queries++; | |
| net_utils.get_ips_by_host(ptr_domain, function (err2, ips) { | |
| pending_queries--; | |
| if (err2) { | |
| for (var e=0; e < err2.length; e++) { | |
| switch (err[e]) { | |
| case 'queryAaaa ENODATA': | |
| case 'queryAaaa ENOTFOUND': | |
| break; | |
| default: | |
| connection.results.add(plugin, {err: err2[e]}); | |
| } | |
| } | |
| } | |
| connection.logdebug(plugin, ptr_domain + ' => ' + ips); | |
| results[ptr_domain] = ips; | |
| if (pending_queries > 0) return; | |
| if (ips.length === 0) { | |
| connection.results.add(plugin, | |
| { fail: 'ptr_valid('+ptr_domain+')' }); | |
| } | |
| // Got all DNS results | |
| connection.results.add(plugin, {ptr_name_to_ip: results}); | |
| return plugin.check_fcrdns(connection, results, do_next); | |
| }); | |
| } | |
| // No valid PTR | |
| if (!queries_run || (queries_run && pending_queries === 0)) { | |
| return do_next(); | |
| } | |
| }); | |
| }; | |
| exports.hook_data_post = function (next, connection) { | |
| var plugin = this; | |
| var txn = connection.transaction; | |
| txn.remove_header('X-Haraka-rDNS'); | |
| txn.remove_header('X-Haraka-FCrDNS'); | |
| txn.remove_header('X-Haraka-rDNS-OtherIPs'); | |
| txn.remove_header('X-Haraka-HostID'); | |
| var fcrdns = connection.results.get('connect.fcrdns'); | |
| if (!fcrdns) { | |
| connection.results.add(plugin, {err: "no fcrdns results!?"}); | |
| return next(); | |
| } | |
| if (fcrdns.name && fcrdns.name.length) { | |
| txn.add_header('X-Haraka-rDNS', fcrdns.name.join(' ')); | |
| } | |
| if (fcrdns.fcrdns && fcrdns.fcrdns.length) { | |
| txn.add_header('X-Haraka-FCrDNS', fcrdns.fcrdns.join(' ')); | |
| } | |
| if (fcrdns.other_ips && fcrdns.other_ips.length) { | |
| txn.add_header('X-Haraka-rDNS-OtherIPs', fcrdns.other_ips.join(' ')); | |
| } | |
| return next(); | |
| }; | |
| exports.handle_ptr_error = function (connection, err, do_next) { | |
| var plugin = this; | |
| var rip = connection.remote_ip; | |
| switch (err.code) { | |
| case 'ENOTFOUND': | |
| case dns.NOTFOUND: | |
| case dns.NXDOMAIN: | |
| connection.results.add(plugin, {fail: 'has_rdns', emit: true}); | |
| if (plugin.cfg.reject.no_rdns) { | |
| return do_next(DENY, 'client [' + rip + '] rejected; no rDNS'); | |
| } | |
| return do_next(); | |
| default: | |
| connection.results.add(plugin, {err: err.code}); | |
| if (plugin.cfg.reject.no_rdns) { | |
| return do_next(DENYSOFT, 'client [' + rip + '] rDNS lookup error (' + err + ')'); | |
| } | |
| return do_next(); | |
| } | |
| }; | |
| exports.check_fcrdns = function (connection, results, do_next) { | |
| var plugin = this; | |
| for (var fdom in results) { // mail.example.com | |
| if (!fdom) continue; | |
| var org_domain = tlds.get_organizational_domain(fdom); // example.com | |
| // Multiple domains? | |
| if (last_domain && last_domain !== org_domain) { | |
| connection.results.add(plugin, {ptr_multidomain: true}); | |
| } | |
| else { | |
| var last_domain = org_domain; | |
| } | |
| // FCrDNS? PTR -> (A | AAAA) 3. PTR comparison | |
| plugin.ptr_compare(results[fdom], connection, fdom); | |
| connection.results.add(plugin, {ptr_name_has_ips: true}); | |
| if (plugin.is_generic_rdns(connection, fdom) && | |
| plugin.cfg.reject.generic_rdns) { | |
| return do_next(DENY, 'client ' + fdom + ' [' + connection.remote_ip + | |
| '] rejected; generic rDNS, please use your ISPs SMTP relay'); | |
| } | |
| } | |
| plugin.log_summary(connection); | |
| plugin.save_auth_results(connection); | |
| var r = connection.results.get('connect.fcrdns'); | |
| if (!r.fcrdns.length && plugin.cfg.reject.no_fcrdns) { | |
| return do_next(DENY, 'Sorry, no FCrDNS match found'); | |
| } | |
| return do_next(); | |
| }; | |
| exports.ptr_compare = function (ip_list, connection, domain) { | |
| var plugin = this; | |
| if (!ip_list) return false; | |
| if (!ip_list.length) return false; | |
| if (ip_list.indexOf(connection.remote_ip) !== -1) { | |
| connection.results.add(plugin, {pass: 'fcrdns' }); | |
| connection.results.push(plugin, {fcrdns: domain}); | |
| return true; | |
| } | |
| if (net_utils.same_ipv4_network(connection.remote_ip, ip_list)) { | |
| connection.results.add(plugin, {pass: 'fcrdns(net)' }); | |
| connection.results.push(plugin, {fcrdns: domain}); | |
| return true; | |
| } | |
| for (var j=0; j<ip_list.length; j++) { | |
| connection.results.push(plugin, {other_ips: ip_list[j]}); | |
| } | |
| return false; | |
| }; | |
| exports.save_auth_results = function (connection) { | |
| var r = connection.results.get('connect.fcrdns'); | |
| if (!r) return; | |
| if (r.fcrdns && r.fcrdns.length) { | |
| connection.auth_results('iprev=pass'); | |
| return true; | |
| } | |
| if (!r.has_rdns) { | |
| connection.auth_results('iprev=permerror'); | |
| return false; | |
| } | |
| if (r.err.length) { | |
| connection.auth_results('iprev=temperror'); | |
| return false; | |
| } | |
| connection.auth_results('iprev=fail'); | |
| return false; | |
| }; | |
| exports.is_generic_rdns = function (connection, domain) { | |
| var plugin = this; | |
| // IP in rDNS? (Generic rDNS) | |
| if (!domain) return false; | |
| if (!net_utils.is_ip_in_str(connection.remote_ip, domain)) { | |
| connection.results.add(plugin, {pass: 'is_generic_rdns'}); | |
| return false; | |
| } | |
| connection.results.add(plugin, {fail: 'is_generic_rdns'}); | |
| var orgDom = tlds.get_organizational_domain(domain); | |
| if (!orgDom) { | |
| connection.loginfo(this, 'no org domain for: ' + domain); | |
| return false; | |
| } | |
| var host_part = domain.split('.').slice(0,orgDom.split('.').length+1); | |
| if (/(?:static|business)/.test(host_part)) { | |
| // Allow some obvious generic but static ranges | |
| // EHLO/HELO checks will still catch out hosts that use generic rDNS there | |
| connection.loginfo(this, 'allowing generic static rDNS'); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| exports.log_summary = function (connection) { | |
| if (!connection) return; // connection went away | |
| var note = connection.results.get('connect.fcrdns'); | |
| if (!note) return; | |
| connection.loginfo(this, | |
| ['ip=' + connection.remote_ip, | |
| 'rdns="' + ((note.ptr_names.length > 2) ? note.ptr_names.slice(0,2).join(',') + '...' : note.ptr_names.join(',')) + '"', | |
| 'rdns_len=' + note.ptr_names.length, | |
| 'fcrdns="' + ((note.fcrdns.length > 2) ? note.fcrdns.slice(0,2).join(',') + '...' : note.fcrdns.join(',')) + '"', | |
| 'fcrdns_len=' + note.fcrdns.length, | |
| 'other_ips_len=' + note.other_ips.length, | |
| 'invalid_tlds=' + note.invalid_tlds.length, | |
| 'generic_rdns=' + ((note.ptr_name_has_ips) ? 'true' : 'false'), | |
| ].join(' ')); | |
| }; | |
| exports.refresh_config = function (connection) { | |
| var plugin = this; | |
| // allow rdns_acccess whitelist to override | |
| if (connection.notes.rdns_access && connection.notes.rdns_access === 'white') { | |
| plugin.cfg.reject.no_rdns = 0; | |
| plugin.cfg.reject.invalid_tld = 0; | |
| plugin.cfg.reject.generic_rdns = 0; | |
| } | |
| return plugin.cfg; | |
| }; |