msimerson/Haraka

View on GitHub
plugins/relay.js

Summary

Maintainability
C
1 day
Test Coverage
// relay
//
// documentation via: haraka -h relay

const ipaddr = require('ipaddr.js');
const net    = require('net');

exports.register = function () {

    this.load_relay_ini();             // plugin.cfg = { }

    if (this.cfg.relay.acl) {
        this.load_acls();             // plugin.acl_allow = [..]
        this.register_hook('connect_init', 'acl');
        this.register_hook('connect', 'pass_relaying');
    }

    if (this.cfg.relay.force_routing || this.cfg.relay.dest_domains) {
        this.load_dest_domains();      // plugin.dest.domains = { }
    }

    if (this.cfg.relay.force_routing) {
        this.register_hook('get_mx', 'force_routing');
    }

    if (this.cfg.relay.dest_domains) {
        this.register_hook('rcpt', 'dest_domains');
    }

    if (this.cfg.relay.all) {
        this.register_hook('rcpt', 'all');
    }
}

exports.load_relay_ini = function () {
    this.cfg = this.config.get('relay.ini', {
        booleans: [
            '+relay.acl',
            '+relay.force_routing',
            '-relay.all',
            '-relay.dest_domains',
        ],
    }, () => {
        this.load_relay_ini();
    });
}

exports.load_dest_domains = function () {
    this.dest = this.config.get(
        'relay_dest_domains.ini',
        'ini',
        () => { this.load_dest_domains(); }
    );
}

exports.load_acls = function () {
    const file_name = 'relay_acl_allow';

    // load with a self-referential callback
    this.acl_allow = this.config.get(file_name, 'list', () => {
        this.load_acls();
    });

    for (let i=0; i<this.acl_allow.length; i++) {
        const cidr = this.acl_allow[i].split('/');
        if (!net.isIP(cidr[0])) {
            this.logerror(this, `invalid entry in ${file_name}: ${cidr[0]}`);
        }
        if (!cidr[1]) {
            this.logerror(this, `appending missing CIDR suffix in: ${file_name}`);
            this.acl_allow[i] = `${cidr[0]  }/32`;
        }
    }
}

exports.acl = function (next, connection) {
    if (!this.cfg.relay.acl) { return next(); }

    connection.logdebug(this, `checking ${connection.remote.ip} in relay_acl_allow`);

    if (!this.is_acl_allowed(connection)) {
        connection.results.add(this, {skip: 'acl(unlisted)'});
        return next();
    }

    connection.results.add(this, {pass: 'acl'});
    connection.relaying = true;
    return next(OK);
}

exports.pass_relaying = (next, connection) => {
    if (connection.relaying) {
        return next(OK);
    }

    return next();
}

exports.is_acl_allowed = function (connection) {
    if (!this.acl_allow) { return false; }
    if (!this.acl_allow.length) { return false; }

    const { ip } = connection.remote;

    for (const item of this.acl_allow) {
        connection.logdebug(this, `checking if ${ip} is in ${item}`);
        const cidr = item.split('/');
        const c_net  = cidr[0];
        const c_mask = cidr[1] || 32;

        if (!net.isIP(c_net)) continue;  // bad config entry
        if (net.isIPv4(ip) && net.isIPv6(c_net)) continue;
        if (net.isIPv6(ip) && net.isIPv4(c_net)) continue;

        if (ipaddr.parse(ip).match(ipaddr.parse(c_net), c_mask)) {
            connection.logdebug(this, `checking if ${ip} is in ${item}: yes`);
            return true;
        }
    }
    return false;
}

exports.dest_domains = function (next, connection, params) {
    if (!this.cfg.relay.dest_domains) { return next(); }
    const { relaying, transaction } = connection ?? {}
    if (!transaction) return next();

    // Skip this if the host is already allowed to relay
    if (relaying) {
        transaction.results.add(this, {skip: 'relay_dest_domain(relay)'});
        return next();
    }

    if (!this.dest) {
        transaction.results.add(this, {err: 'relay_dest_domain(no config!)'});
        return next();
    }

    if (!this.dest.domains) {
        transaction.results.add(this, {skip: 'relay_dest_domain(config)'});
        return next();
    }

    const dest_domain = params[0].host;
    connection.logdebug(this, `dest_domain = ${dest_domain}`);

    const dst_cfg = this.dest.domains[dest_domain];
    if (!dst_cfg) {
        transaction.results.add(this, {fail: 'relay_dest_domain'});
        return next(DENY, "You are not allowed to relay");
    }

    const { action } = JSON.parse(dst_cfg);
    connection.logdebug(this, `found config for ${dest_domain}: ${action}`);

    switch (action) {
        case "accept":
            // why enable relaying here? Returning next(OK) will allow the
            // address to be considered 'local'. What advantage does relaying
            // bring?
            connection.relaying = true;
            transaction.results.add(this, {pass: 'relay_dest_domain'});
            return next(OK);
        case "continue":
            // why oh why? Only reason I can think of is to enable outbound.
            connection.relaying = true;
            transaction.results.add(this, {pass: 'relay_dest_domain'});
            return next(CONT);  // same as next()
        case "deny":
            transaction.results.add(this, {fail: 'relay_dest_domain'});
            return next(DENY, "You are not allowed to relay");
    }

    transaction.results.add(this, {fail: 'relay_dest_domain'});
    return next(DENY, "Mail for that recipient is not accepted here.");
}

exports.force_routing = function (next, hmail, domain) {
    if (!this.cfg.relay.force_routing) { return next(); }
    if (!this.dest) { return next(); }
    if (!this.dest.domains) { return next(); }
    let route = this.dest.domains[domain];

    if (!route) {
        route = this.dest.domains.any;
        if (!route) {
            this.logdebug(this, `using normal MX lookup for: ${domain}`);
            return next();
        }
    }

    const { nexthop } = JSON.parse(route);
    if (!nexthop) {
        this.logdebug(this, `using normal MX lookup for: ${domain}`);
        return next();
    }

    this.logdebug(this, `using ${nexthop} for: ${domain}`);
    return next(OK, nexthop);
}

exports.all = function (next, connection, params) {
    if (!this.cfg.relay.all) { return next(); }

    connection.loginfo(this, `confirming recipient ${params[0]}`);
    connection.relaying = true;
    next(OK);
}