Skip to content

Commit a3316c2

Browse files
committed
Merge pull request #541 from msimerson/relay
Relay
2 parents 3496ad3 + 3af5a31 commit a3316c2

File tree

7 files changed

+717
-0
lines changed

7 files changed

+717
-0
lines changed

TODO

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ Remove the following deprecated plugins
3434
- mail_from.access (replaced by access.js)
3535
- rcpt_to.access ""
3636
- connect.rdns_access ""
37+
- relay_acl
38+
- relay_all
39+
- relay_force_routing
3740

3841
Rename the following plugins
3942
- toobusy -> connect.toobusy

docs/plugins/relay.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# relay
2+
3+
[MTAs](http://en.wikipedia.org/wiki/Mail_transfer_agent) generally only accept mail for _local_ domains they can deliver to. In Haraka, the `rcpt_to.*` plugins usually decide which domains and/or email addresses are deliverable. By default, everything else is rejected.
4+
5+
**Relaying** is when a MTA accepts mail that is destined elsewhere. Back in the day (1980s), most MTAs permitted open relaying. Soon spammers abused our open relays (1990s) and left us with soiled mail queues. Now nearly all MTAs have relaying disabled and [MUAs](http://en.wikipedia.org/wiki/Mail_user_agent) are required to use a [MSA](http://en.wikipedia.org/wiki/Message_submission_agent) to relay. Most MTAs (including Haraka) have MSA features and can serve both purposes.
6+
7+
This **relay** plugin provides Haraka with options for managing relay permissions.
8+
9+
## Authentication
10+
11+
One way to enable relaying is [authentication](http://haraka.github.io/manual.html) via the auth plugins. Successful authentication enables relaying during _that_ SMTP connection. To securely offer SMTP AUTH, the [tls](http://haraka.github.io/manual/plugins/tls.html) plugin and at least one auth plugin must be enabled and properly configured. When that requirement is met, the AUTH SMTP extension will be advertised to SMTP clients.
12+
13+
% nc mail.example.com 587
14+
220 mail.example.com ESMTP Haraka 2.4.0 ready
15+
ehlo client.example.com
16+
250-mail.example.com Hello client.example.com [192.168.0.1], Haraka is at your service.
17+
250-PIPELINING
18+
250-8BITMIME
19+
250-SIZE 10000000
20+
250 STARTTLS
21+
quit
22+
221 mail.example.com closing connection. Have a jolly good day.
23+
24+
Notice that there's no AUTH advertised. We only permit authentication when the
25+
connection is secured with TLS:
26+
27+
% openssl s_client -connect mail.example.com:587 -starttls smtp
28+
CONNECTED(00000003)
29+
<snip long SSL certificate details>
30+
---
31+
250 STARTTLS
32+
ehlo client.example.com
33+
250-mail.example.com Hello client.example.com [192.168.1.1], Haraka is at your service.
34+
250-PIPELINING
35+
250-8BITMIME
36+
250-SIZE 10000000
37+
250 AUTH PLAIN LOGIN
38+
quit
39+
221 mail.example.com closing connection. Have a jolly good day.
40+
closed
41+
42+
To avoid port 25 restrictions, in 1998 we developed [SMTP submission](http://tools.ietf.org/html/rfc2476) on port 587. For optimal security and reliability, [MUAs](http://en.wikipedia.org/wiki/Mail_user_agent) should be configured to send mail to port 587 with TLS/SSL and AUTH enabled.
43+
44+
## ACL (Access Control List)
45+
46+
ACL processing is enabled by setting acl=true in the [relay] section of
47+
relay.ini:
48+
49+
[relay]
50+
acl=true
51+
52+
With the Access Control List feature, relaying can be enabled for IPv4 and
53+
IPv6 networks. IP ranges listed in the ACL file are allowed to send mails
54+
without furthur checks.
55+
56+
* `config/relay_acl_allow`
57+
58+
Allowed IP ranges in CIDR notation, one per line.
59+
60+
Back in the day, ISPs enabled all of their IP space to relay. That proved
61+
problematic for users who took their laptops and mobile phones elsewhere and
62+
then couldn't send mail. For end users therefore, use SMTP AUTH described
63+
above. If you reside somewhere technology evolves more slowly, you can still
64+
add IP allocations to `relay_acl_allow` like so:
65+
66+
echo 'N.N.N.N/24' >> /path/to/haraka/config/relay_acl_allow
67+
68+
A common use case for IP based relaying is to relay messages on behalf of
69+
another mail server. If your organization has an Exchange server, using Haraka
70+
to filter inbound messages is a great choice. You might also want to relay
71+
outbound messages via Haraka as well, so they can be DKIM signed on their way
72+
to the internet. For such a use case, you would set 'acl=true' (the default)
73+
in the [relay] section of `access.ini` and then add the external IP address
74+
of the corporate firewall to `config/relay_acl_allow`:
75+
76+
echo 'N.N.N.N/32' >> /path/to/haraka/config/relay_acl_allow
77+
78+
79+
## Force Route / Dest[ination] Domains
80+
81+
Force routes and Destination Domains are enabled by setting in the [relay]
82+
section of relay.ini:
83+
84+
[relay]
85+
force_routing=false (default: false)
86+
dest_domains=false (default: false)
87+
88+
These two features share another common config file:
89+
90+
* `config/relay_dest_domains.ini`
91+
92+
The format is ini and entries are within the [domains] section. The key for each entry is the domain and the value is a JSON string. Within the JSON string, the currently supported keys are:
93+
94+
* action (Dest Domains)
95+
* nexthop (Force Route)
96+
97+
### Force Route
98+
99+
Think of force route as the equivalent of the transport map in Postfix or the smtproutes file in Qmail. Rather than looking up the MX for a host, the *nexthop* value from the entry in the config file is used.
100+
101+
The value of "nexthop": can be a hostname or an IP, optionally follow by :port.
102+
103+
Example:
104+
105+
[domains]
106+
test.com = { "action": "continue", "nexthop": "127.0.0.1:2525" }
107+
108+
### Destination Domains
109+
110+
Allowed destination/recipient domains. The field within the JSON value used
111+
by Dest Domains is "action": and the possible values are accept, continue, or
112+
deny.
113+
114+
* accept (accept the mail without further checks)
115+
116+
Example:
117+
118+
[domains]
119+
test.com = { "action": "accept" }
120+
121+
I think of *accept* as the equivalent of qmail's *rcpthosts*, or a misplaced Haraka `rcpt_to.*` plugin. The *accept* mechanism is another way to tell Haraka that a particular domain is one we accept mail for. The difference between this and and the [rcpt_to.in_host_list](http://haraka.github.io/manual/plugins/rcpt_to.in_host_list.html) plugin is that this one also enables relaying.
122+
123+
* continue (mails are subject to further checks)
124+
125+
Example:
126+
127+
[domains]
128+
test.com = { "action": "continue" }
129+
130+
Because the default behavior of Dest Routes is to deny, the *continue* option provides an escape, permitting another Haraka plugin to validate the recipient. Like the *accept* option, it too enables relaying.
131+
132+
* deny (mails are rejected)
133+
134+
This deny option baffles me. The default behavior of Haraka is to reject emails for
135+
which a recipient validation plugin hasn't vouched. Adding it here prevents
136+
any subsequent recipient validation plugin from getting a chance. It also
137+
necessitates the continue option.
138+
139+
140+
## all
141+
142+
Relay all is enabled by setting all=true in the [relay] section of
143+
relay.ini:
144+
145+
[relay]
146+
all=true (default: false)
147+
148+
Relay all is useful for spamtraps to accept all mail.
149+
150+
Do NOT use this on a real mail server, unless you really know what you are
151+
doing. If you use the all feature with anything that relays mail (such
152+
as forwarding to a real mail server, or the `deliver` plugin), your mail
153+
server is now an open relay.
154+
155+
This is BAD. Hence the big letters. In short: DO NOT USE THIS FEATURE.
156+
157+
It is useful for testing and spamtraps, hence its presence.

plugins/relay.js

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// relay
2+
//
3+
// documentation via: haraka -h relay
4+
5+
var ipaddr = require('ipaddr.js'),
6+
net = require('net');
7+
8+
exports.register = function() {
9+
var plugin = this;
10+
plugin.refresh_config();
11+
if (plugin.cfg.relay.acl ) { plugin.register_hook('connect', 'acl' ); }
12+
if (plugin.cfg.relay.dest_domains ) { plugin.register_hook('rcpt', 'dest_domains'); }
13+
if (plugin.cfg.relay.all ) { plugin.register_hook('rcpt', 'all' ); }
14+
if (plugin.cfg.relay.force_routing) { plugin.register_hook('get_mx', 'force_routing'); }
15+
};
16+
17+
exports.refresh_config = function() {
18+
var plugin = this;
19+
20+
var load_relay_ini = function () {
21+
plugin.cfg = plugin.config.get('relay.ini', {
22+
booleans: [
23+
'+relay.acl',
24+
'+relay.force_routing',
25+
'-relay.all',
26+
'-relay.dest_domains',
27+
],
28+
}, function () {
29+
load_relay_ini();
30+
});
31+
};
32+
33+
var load_dest_domains = function () {
34+
plugin.loginfo(plugin, "loading relay_dest_domain.ini");
35+
plugin.dest = plugin.config.get('relay_dest_domains.ini', 'ini', function() {
36+
load_dest_domains();
37+
});
38+
};
39+
40+
var load_acls = function () {
41+
var file_name = 'relay_acl_allow';
42+
plugin.loginfo(plugin, "loading " + file_name);
43+
44+
// load with a self-referential callback
45+
plugin.acl_allow = plugin.config.get(file_name, 'list', function () {
46+
load_acls();
47+
});
48+
49+
for (var i=0; i<plugin.acl_allow.length; i++) {
50+
var cidr = plugin.acl_allow[i].split('/');
51+
if (!net.isIP(cidr[0])) {
52+
plugin.logerror(plugin, "invalid entry in " + file_name + ": " + cidr[0]);
53+
}
54+
if (!cidr[1]) {
55+
plugin.logerror(plugin, "appending missing CIDR suffix in: " + file_name);
56+
plugin.acl_allow[i] = cidr[0] + '/32';
57+
}
58+
}
59+
};
60+
61+
load_relay_ini(); // plugin.cfg = { }
62+
63+
if (plugin.cfg.relay.acl) {
64+
load_acls(); // plugin.acl_allow = [..]
65+
}
66+
67+
if (plugin.cfg.relay.force_routing || plugin.cfg.relay.dest_domains) {
68+
load_dest_domains(); // plugin.dest.domains = { }
69+
}
70+
};
71+
72+
exports.acl = function (next, connection) {
73+
var plugin = this;
74+
if (!plugin.cfg.relay.acl) { return next(); }
75+
76+
connection.logdebug(this, 'checking ' + connection.remote_ip + ' in relay_acl_allow');
77+
78+
if (!plugin.is_acl_allowed(connection)) {
79+
connection.results.add(plugin, {skip: 'acl(unlisted)'});
80+
return next();
81+
}
82+
83+
connection.results.add(plugin, {pass: 'acl'});
84+
connection.relaying = true;
85+
return next(OK);
86+
};
87+
88+
exports.is_acl_allowed = function (connection) {
89+
var plugin = this;
90+
if (!plugin.acl_allow) { return false; }
91+
if (!plugin.acl_allow.length) { return false; }
92+
93+
var ip = connection.remote_ip;
94+
95+
for (var i=0; i < plugin.acl_allow.length; i++) {
96+
var item = plugin.acl_allow[i];
97+
connection.logdebug(plugin, 'checking if ' + ip + ' is in ' + item);
98+
var cidr = plugin.acl_allow[i].split('/');
99+
var c_net = cidr[0];
100+
var c_mask = cidr[1] || 32;
101+
102+
if (!net.isIP(c_net)) continue; // bad config entry
103+
if (net.isIPv4(ip) && net.isIPv6(c_net)) continue;
104+
if (net.isIPv6(ip) && net.isIPv4(c_net)) continue;
105+
106+
if (ipaddr.parse(ip).match(ipaddr.parse(c_net), c_mask)) {
107+
connection.logdebug(plugin, 'checking if ' + ip + ' is in ' + item + ": yes");
108+
return true;
109+
}
110+
}
111+
return false;
112+
};
113+
114+
exports.dest_domains = function (next, connection, params) {
115+
var plugin = this;
116+
if (!plugin.cfg.relay.dest_domains) { return next(); }
117+
var transaction = connection.transaction;
118+
119+
// Skip this if the host is already allowed to relay
120+
if (connection.relaying) {
121+
transaction.results.add(plugin, {skip: 'relay_dest_domain(relay)'});
122+
return next();
123+
}
124+
125+
if (!plugin.dest) {
126+
transaction.results.add(plugin, {err: 'relay_dest_domain(no config!)'});
127+
return next();
128+
}
129+
130+
if (!plugin.dest.domains) {
131+
transaction.results.add(plugin, {skip: 'relay_dest_domain(config)'});
132+
return next();
133+
}
134+
135+
var dest_domain = params[0].host;
136+
connection.logdebug(plugin, 'dest_domain = ' + dest_domain);
137+
138+
var dst_cfg = plugin.dest.domains[dest_domain];
139+
if (!dst_cfg) {
140+
transaction.results.add(plugin, {fail: 'relay_dest_domain'});
141+
return next(DENY, "You are not allowed to relay");
142+
}
143+
144+
var action = JSON.parse(dst_cfg).action;
145+
connection.logdebug(plugin, 'found config for ' + dest_domain + ': ' + action);
146+
147+
switch(action) {
148+
case "accept":
149+
// why enable relaying here? Returning next(OK) will allow the
150+
// address to be considered 'local'. What advantage does relaying
151+
// bring?
152+
connection.relaying = true;
153+
transaction.results.add(plugin, {pass: 'relay_dest_domain'});
154+
return next(OK);
155+
case "continue":
156+
// why oh why? Only reason I can think of is to enable outbound.
157+
connection.relaying = true;
158+
transaction.results.add(plugin, {pass: 'relay_dest_domain'});
159+
return next(CONT); // same as next()
160+
case "deny":
161+
transaction.results.add(plugin, {fail: 'relay_dest_domain'});
162+
return next(DENY, "You are not allowed to relay");
163+
}
164+
165+
transaction.results.add(plugin, {fail: 'relay_dest_domain'});
166+
return next(DENY, "Mail for that recipient is not accepted here.");
167+
};
168+
169+
exports.force_routing = function (next, hmail, domain) {
170+
var plugin = this;
171+
if (!plugin.cfg.relay.force_routing) { return next(); }
172+
if (!plugin.dest) { return next(); }
173+
if (!plugin.dest.domains) { return next(); }
174+
var route = plugin.dest.domains[domain];
175+
176+
if (!route) {
177+
plugin.logdebug(plugin, 'using normal MX lookup for: ' + domain);
178+
return next();
179+
}
180+
181+
var c = JSON.parse(route);
182+
var nexthop = JSON.parse(route).nexthop;
183+
if (!nexthop) {
184+
plugin.logdebug(plugin, 'using normal MX lookup for: ' + domain);
185+
return next();
186+
}
187+
188+
plugin.logdebug(plugin, 'using ' + nexthop + ' for: ' + domain);
189+
return next(OK, nexthop);
190+
};
191+
192+
exports.all = function(next, connection, params) {
193+
// relay everything - could be useful for a spamtrap
194+
var plugin = this;
195+
if (!plugin.cfg.relay.all) { return next(); }
196+
// TODO: This looks like a bug (shortening the recipient array)
197+
var recipient = params.shift();
198+
connection.loginfo(plugin, "confirming recipient " + recipient);
199+
connection.relaying = true;
200+
next(OK);
201+
};
202+

plugins/relay_acl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var ipaddr = require('ipaddr.js'),
66
net = require('net');
77

88
exports.register = function() {
9+
this.logerror(this, "deprecated. see 'haraka -h relay'");
910
this.register_hook('lookup_rdns', 'refresh_config');
1011
this.register_hook('connect', 'relay_acl');
1112
this.register_hook('rcpt', 'relay_dest_domains');

plugins/relay_all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Just relay everything - could be useful for a spamtrap
22

33
exports.register = function() {
4+
this.logerror(this, "deprecated. see 'haraka -h relay'");
45
this.register_hook('rcpt', 'confirm_all');
56
};
67

0 commit comments

Comments
 (0)