Add support for custom outbound IPs by domain
This commit is contained in:
parent
d907720cd0
commit
afe38a57eb
4 changed files with 137 additions and 20 deletions
50
README.md
50
README.md
|
@ -17,4 +17,52 @@ To use this image, just replace `analogic/poste.io` in your config with `dirtsim
|
|||
* You **must** configure the container with a fully-qualified hostname, whose IP address(es) **must** be listed in the public DNS system
|
||||
* The IP address(es) must be public IPs, and *should* have reverse DNS pointing to the container's hostname
|
||||
* You should be using **host-mode networking**, since in any other networking mode, the original `analogic/poste.io` image is sufficiently isolated without these patches!
|
||||
* By default, outgoing email to other mail servers will be sent via the first IP address returned by running `hostname -i` in the container. If you need to override this, configure the container with an `OUTBOUND_MAIL_IP` environment variable specifying the IP address to be used.
|
||||
* By default, outgoing email to other mail servers will be sent via the first IP address returned by running `hostname -i` in the container. If you need to override this, configure the container with an `OUTBOUND_MAIL_IP` environment variable specifying the IP address to be used, or create a `/data/outbound-hosts.yml` file as described below, with an appropriate `default` entry.
|
||||
|
||||
### Managing Sender IPs
|
||||
|
||||
In some environments, you may wish to use different sending IP addresses for different origin domains. To support this use case, you can add a file named `outbound-hosts.yml` to the `/data` volume, laid out like this:
|
||||
|
||||
```yaml
|
||||
# This info will be used for domains that don't have an entry of their own
|
||||
default:
|
||||
helo: poste.mygenericdomain.com
|
||||
ip: 1.2.3.4
|
||||
|
||||
exampledomain.com:
|
||||
helo: mx.exampledomain.com
|
||||
ip: 5.6.7.8
|
||||
```
|
||||
|
||||
With the above configuration, mails sent from `exampledomain.com` will be sent with a HELO of `mx.exampledomain.com`, using an outbound IP of `5.6.7.8`, and mail for any other domain will use the defaults.
|
||||
|
||||
Note that the information in this file is *not* validated against DNS or checked for security. It is your responsibility to ensure that all `helo` hostnames exist in DNS with the matching `ip` , and that all listed IP addresses are actually valid for the network interfaces on your server. In addition, for best deliverability, you should also:
|
||||
|
||||
* Ensure that SPF will pass for a given domain + `helo`/`ip` combination
|
||||
* Ensure that the reverse DNS for the given `ip` values has a reasonable result (preferably the same as the `helo`)
|
||||
* Ensure that each `helo` address used as an MX is listed in the "Alternative names" of your TLS certificate in the "Mailserver settings" of the poste admin interface, and that its corresponding `ip` is listed in an `A` or `AAAA` record for the *container's* hostname. (So that the container will listen for incoming mail on that address, and respond with a valid certificate.) This step is not necessary for domains that simply use the container's hostname as their MX.
|
||||
|
||||
And of course, you will need to update all of this information whenever any of the configuration changes. If you control DNS for all the relevant domains yourself, you may be able to generate this file automatically from your domain list and DNS: e.g. by looking up MX records and their corresponding addresses. (But you shouldn't trust the DNS for domains you don't control, as that would let your clients pick their own sending IPs!)
|
||||
|
||||
### Docker-Compose Example
|
||||
|
||||
Here's a trivial `docker-compose.yml` setup for using this image:
|
||||
|
||||
```yaml
|
||||
version: "2.3"
|
||||
services:
|
||||
poste:
|
||||
image: dirtsimple/poste.io
|
||||
restart: always
|
||||
network_mode: host
|
||||
|
||||
# to serve everything on `mail.example.com`:
|
||||
hostname: mail
|
||||
domainname: example.com
|
||||
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
```
|
||||
|
||||
This example assumes that `mail.example.com` is mapped in the public DNS to one or more IP addresses on the server where the container runs, and that *none* of those IP addresses have any other services listening on ports 25, 80, 110, 143, 443, 466, 587, 993, 995, or 4190. (You should, of course, replace `mail` and `example.com` with appropriate values for your installation.)
|
|
@ -17,12 +17,7 @@ listen587=${ipaddrs// /:587,}:587
|
|||
sed -i 's/^listen=.*:25$/listen='"$listen025/" /opt/haraka-smtp/config/smtp.ini
|
||||
sed -i 's/^listen=.*:587,.*:465$/listen='"$listen587,$listen465/" /opt/haraka-submission/config/smtp.ini
|
||||
|
||||
|
||||
# === Haraka should only do outbound connects on one IP ===
|
||||
|
||||
# If OUTBOUND_MAIL_IP is set, use that, otherwise use the host's first IP
|
||||
outbound=${OUTBOUND_MAIL_IP:-${ipaddrs%% *}}
|
||||
|
||||
echo "$outbound" >/opt/haraka-submission/config/my-ip
|
||||
echo "$outbound" >/opt/haraka-smtp/config/my-ip
|
||||
outbound=${ipaddrs// /,}
|
||||
echo "$outbound" >/opt/haraka-submission/config/my-ips
|
||||
echo "$outbound" >/opt/haraka-smtp/config/my-ips
|
||||
|
||||
|
|
82
files/opt/haraka-smtp/plugins/outbound_ips.js
Normal file
82
files/opt/haraka-smtp/plugins/outbound_ips.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
/*****
|
||||
|
||||
This plugin selects an outbound IP address for each piece of outgoing mail,
|
||||
using the following algorithm:
|
||||
|
||||
* If only one IP address is known for the container's hostname (as of container
|
||||
start), use that address for everything.
|
||||
* If an environment variable 'OUTBOUND_MAIL_IP' exists, use it for everything.
|
||||
* Check the contents of /data/outbound-hosts.yml for an entry for the sender's
|
||||
domain (which should be an object with 'helo' and 'ip' props), or a 'default'
|
||||
entry if there's no entry for the domain. If the file can't be read or parsed,
|
||||
or an entry is malformed, fall through to the next step.
|
||||
* If an environment variable 'OUTBOUND_DEFAULT_IP' exists, use that
|
||||
* Use the first IP address mapped to the container's hostname
|
||||
|
||||
In the future, the .yml file lookup would be best replaced with SQLite database
|
||||
columns on the domains table (for HELO name and IP). The configuration could
|
||||
then be done via the admin interface.
|
||||
|
||||
*****/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const yml = require('js-yaml');
|
||||
|
||||
const hostname = require('os').hostname();
|
||||
|
||||
const my_ips = require("haraka-config").get("my-ips").trim().split(/\s*,\s*/);
|
||||
const default_ip = process.env.OUTBOUND_MAIL_IP || process.env.OUTBOUND_DEFAULT_IP || my_ips[0];
|
||||
|
||||
|
||||
exports.hook_get_mx = async function(next, hmail, domain) {
|
||||
const plugin = this;
|
||||
|
||||
function set_outbound(items, key) {
|
||||
const target = items[key];
|
||||
if ( 'object' !== typeof target ||
|
||||
'string' !== typeof target.helo ||
|
||||
'string' !== typeof target.ip
|
||||
) {
|
||||
const errmsg = `${key} must be an object with 'helo' and 'ip' strings: got ${JSON.stringify(target)}`;
|
||||
throw new Error(errmsg);
|
||||
}
|
||||
plugin.loginfo(`Setting outbound HELO = ${target.helo}, IP = ${target.ip}`);
|
||||
hmail.todo.notes.outbound_helo = target.helo;
|
||||
hmail.todo.notes.outbound_ip = target.ip;
|
||||
}
|
||||
|
||||
function use_default() {
|
||||
set_outbound({"default": {helo: hostname, ip: default_ip}}, 'default');
|
||||
}
|
||||
|
||||
if ( process.env.OUTBOUND_MAIL_IP || my_ips.length === 1 ) {
|
||||
use_default();
|
||||
} else {
|
||||
const from_domain = hmail.todo.mail_from.host;
|
||||
|
||||
try {
|
||||
this.logdebug("loading /data/outbound-hosts.yml");
|
||||
const outbound = yml.safeLoad(
|
||||
await fs.readFile('/data/outbound-hosts.yml','utf8')
|
||||
);
|
||||
if ( outbound[from_domain] ) {
|
||||
set_outbound(outbound, from_domain);
|
||||
}
|
||||
else if ( outbound['default'] ) {
|
||||
set_outbound(outbound, 'default');
|
||||
}
|
||||
else {
|
||||
this.logerror(`Couldn't find an entry for ${from_domain} or 'default' in /data/outbound-hosts.yml`);
|
||||
use_default();
|
||||
}
|
||||
} catch (err) {
|
||||
// Fall back to default IP
|
||||
this.logerror(`Error loading /data/outbound-hosts.yml: ${err.message}`);
|
||||
use_default();
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
|
@ -49,17 +49,9 @@ shopt -q expand_aliases||{ unalias -a;shopt -s expand_aliases;};builtin alias +=
|
|||
- sub localhost '$hostname'
|
||||
}}
|
||||
|
||||
~ edit /opt/haraka-smtp/plugins/poste.js; {{
|
||||
# Force outbound smtp to go via container host IP, unless overridden by
|
||||
# another plugin. (We only edit the haraka-smtp version because the
|
||||
# haraka-submissions/plugins dir is a symlink.)
|
||||
+ after "plugin\.register_hook('init_child'"; {{
|
||||
- " plugin.register_hook('get_mx', 'get_mx');"
|
||||
}}
|
||||
+ append ""; {{
|
||||
- 'const my_outbound_ip = require("haraka-config").get("my-ip");'
|
||||
- 'exports.get_mx = function (next, hmail, domain) { hmail.todo.notes.outbound_ip=my_outbound_ip; next(); }'
|
||||
}}
|
||||
~ edit /opt/haraka-{smtp,submission}/config/plugins; {{
|
||||
# Add our outbound IP routing plugin
|
||||
- append 'outbound_ips'
|
||||
}}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue