The tutorial
In this section we show several examples of possible uses for the checkcompat() routine. Among those we illustrate are the following:
- Accept mail only from our domain. (see "Accept Mail Only From Our Domain").
- Cause your workstation to refuse to work as a mail gateway (see "Workstation Refuses to Act as a Mail Gateway").
- Limit the size of guest account messages (see "Limit the Size of Guest Messages").
- Verify that identd information is correct (see "Verify identd Information").
- Prune
Received:
headers at a firewall (see "Prune Received: Headers at Firewall"). - Reject mail from spamming or mail-bombing sites (see "Reject Mail from Spamming or Mail-bombing Sites").
Note that in all of the following examples the numbers to the left indicate line numbers for discussion and are not a part of the code.
Accept Mail Only From Our Domain
If your site lives behind a firewall, [2] you might want to use checkcompat() to configure the internal sendmail so that it accepts only mail that is generated locally. The external sendmail (outside the firewall or part of it) acts as a proxy. That is, it accepts external mail that is destined for internal delivery from the outside and forwards it to the internal sendmail. Because the external sendmail is part of the local domain, its envelope always appears to be local. Any external mail that somehow bypasses the firewall needs to be bounced. The way to do this in checkcompat() looks like this:
[2] A firewall is a machine that lies between the local network and the outside world. It intercepts and filters all network traffic and rejects any that are considered inappropriate.
# define OUR_NET_IN_HEX 0x7b2d4300 /* 123.45.67.0 in hex */ # define OUR_NETMASK 0xffffff00 checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { if (tTd(49, 1)) printf("checkcompat(to=%s, from=%s)\n", to->q_paddr, e->e_from.q_paddr); if (RealHostAddr.sa.sa_family == 0) { /* this is a locally submitted message */ return EX_OK; } if (RealHostAddr.sa.sa_family != AF_INET || (RealHostAddr.sin.sin_addr.s_addr & OUR_NETMASK)!= OUR_NET_IN_HEX) {usrerr("553 End run mail not allowed");
e->e_flags |= EF_NO_BODY_RETN;
to->q_status = "5.7.1";
return (EX_UNAVAILABLE);
} return (EX_OK); }
The usrerr
() routine (line ) causes a warning to be printed at the sending site, and returning EX_UNAVAILABLE (line ) causes the mail message to be bounced. Bounced mail is sent back to the originating sender. A copy may also be sent to the local postmaster depending on the setting of PostmasterCopy
(P
) option (see PostmasterCopy (P)).
The EF_NO_BODY_RETN (line ) causes only the headers from the message to be returned in bounced mail, not the original message body. Other envelope flags of interest can be found in Table 37.3 of .
The to->q_status
(line ) conveys the DSN error status in the bounced mail message (see RFC1893). Here, indicates a permanent failure () of policy status (), where delivery is not authorized and the message is refused ().
Also note that this code sample is only a suggestion. It doesn't take into account that RealHostAddr
may contain x7f000001
( for localhost).
Workstation Refuses to Act as a Mail Gateway
If you've spent many months getting your workstation set up and running perfectly, you might not want outsiders using it as a knowledgeable mail relay. One way to prevent such unwanted use is to set up checkcompat() in conf.c so that it rejects any mail from outside your machine that is destined to another site outside your machine. A desirable side effect is that this will also prevent outsiders from directly posting into your internal mailing lists.
checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { if (tTd(49, 1)) printf("checkcompat(to=%s, from=%s)\n", to->q_paddr, e->e_from.q_paddr); if (RealHostAddr.sa.sa_family == 0) { /* this is a locally submitted message */ return (EX_OK); } /* only accept local delivery from outside */if (!bitnset(M_LOCALMAILER, to->q_mailer->m_flags))
{ usrerr("553 External gateway use prohibited"); e->e_flags |= EF_NO_BODY_RETN; to->q_status = "5.7.1"; return (EX_UNAVAILABLE); } return (EX_OK); }
Although to
(line ) is really a linked list of recipients, we check only the current recipient to prevent spurious warnings. This is done because checkcompat() is called once for every recipient. The check in line is to see whether F=l
delivery agent flag is not set (see F=l (lowercase L)) thus implying that the recipient is not local.
Note that this form of rejecting messages will not work on a mail hub. In that case more sophisticated checks need to be made. Among them are the following:
- Check all the IP domains for your site. If you have only one, the check in will work. If you have several (as in an assortment of class
C
domains), the check will be more complex. If the connecting host is in your domain or one of your domains, you should accept the message. - The envelope sender's host (
e->e_from->q_host
) should be checked to see whether it is in the class$=w
(see $=w). You can use the wordinclass() routine (see wordinclass()) to look it up. If it is in$=w
, you should accept the message. This prevents a message from being forwarded through a workstation. - If the delivery agent for a recipient is
*include*
, the message is destined for a mailing list. You might wish to screen further at this point.
Limit the Size of Guest Messages
Suppose your site has reserved uids numbered from 900 to 999 for guest users. Because guests are sometimes inconsiderate, you might want to limit the size of their messages and the number of simultaneous recipients they may specify. One way to do this is with the checkcompat() routine:
#define MAXGUESTSIZE 8000 #define MAXGUESTNRCP 4 checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { if (tTd(49, 1)) printf("checkcompat(to=%s, from=%s)\n", to->q_paddr, e->e_from.q_paddr); /* does q_uid contain a valid uid? - no external */if (! bitset(QGOODUID, e->e_from.q_flags))
return (EX_OK);if (e->e_from.q_uid < 900 || e->e_from.q_uid > 999)
return (EX_OK); if (e->e_msgsize > MAXGUESTSIZE) { syslog(LOG_NOTICE, "Guest %s attempted to send %d size", e->e_from.q_user, e->e_msgsize); usrerr("553 Message too large, %d max", MAXGUESTSIZE);e->e_flags |= EF_NO_BODY_RETN;
to->q_status = "5.7.1"; return (EX_UNAVAILABLE); } if (e->e_nrcpts > MAXGUESTNRCP) { syslog(LOG_NOTICE, "Guest %s attempted to send %d recipients", e->e_from.q_user, e->e_nrcpts); usrerr("553 Too many recipients for guest, %d max", MAXGUESTNRCP);e->e_flags &= ~EF_NO_BODY_RETN;
to->q_status = "5.7.1"; return (EX_UNAVAILABLE); } return (EX_OK); }
Note that q_uid
will have a valid uid (QGOODUID will be set) only if the sender is local (line ). For external mail coming in, QGOODUID will be clear.
Also note that we specifically do not return the message body (EF_NO_BODY_RETN) if the message was returned because it was too large (line ). But we do return the message body if the message was rejected for too many recipients (line ). Other envelope flags of interest can be found in Table 37.3 of .
Verify identd Information
When an outside host connects to the local sendmail via SMTP, its hostname is saved in the $s
macro (see $s). If the Timeout.ident
option (see Timeout (r)) is nonzero, sendmail uses the RFC1413 identification protocol to record the identity of the host at the other end, that is, the identity of the host that made the connection. That identity is recorded in the $_
macro (see $-).
If you are unusually picky about the identity of other hosts, you may wish to confirm that the host in $s
is the same as the host in $_
. One way to perform such a check is with the checkcompat() routine:
checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { char *s, *u, *v; int len; static char old_s[MAXHOSTNAMELEN]; if (tTd(49, 1)) printf("checkcompat(to=%s, from=%s)\n", to->q_paddr, e->e_from.q_paddr); /* if $s is localhost or in $=w, accept it */ if ((s = macvalue('s', e)) == NULL) return (EX_OK);if (strncasecmp(s, old_s, MAXHOSTNAMELEN-1) == 0)
return (EX_OK); else (void)sprintf(old_s, "%.*s", MAXHOSTNAMELEN-1, s);if (strcasecmp(s, "localhost") == 0)
return (EX_OK);if (wordinclass(s, 'w') == TRUE)
return (EX_OK); if ((u = macvalue('_', e)) == NULL) return (EX_OK); if ((u = strchr(u, '@')) == NULL) return (EX_OK); if ((v = strchr(u, ' ')) != NULL) *v = ' '; len = strlen(u); if (v != NULL) *v = ' ';if (strncasecmp(s, u, len) != 0)
{auth_warning(e, "$s=%s doesn't match $_=%.*s", s, len, u);
} return (EX_OK); }
First (line ) we check to see whether we have already checked this value of $s
. If so, we don't check again because checkcompat() is called once for each recipient. If $s
is new, we save a copy of its value for next time.
Then we make sure that the local host (no matter what its name) is acceptable (lines and ). If this is an offsite host, we compare the values of $s
and the host part of $_
(line ). If they don't match, we insert an X-Authentication-Warning:
header (line ). This keeps such warnings under the control of the PrivacyOptions.authwarnings
(p
) option (see PrivacyOptions (p)).
Prune Received: Headers at Firewall
In routing mail outward from a firewall (see ), it may be advantageous to replace all the internal Received:
headers with one master header. A way to do this with checkcompat() looks like this:
# define OUR_NET_IN_HEX 0x7b2d4300 /* 123.45.67.0 in hex */ # define OUR_NETMASK 0xffffff00 # define LOOP_CHECK "X-Loop-Check" checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { HDR *h; int cnt;if (RealHostAddr.sa.sa_family == 0)
{ /* this is a locally submitted message */ return EX_OK; }if (RealHostAddr.sa.sa_family != AF_INET ||
(RealHostAddr.sin.sin_addr.s_addr & OUR_NETMASK) != OUR_NET_IN_HEX)
{ /* not received from the internal network */ return EX_OK; }if (hvalue(LOOP_CHECK, e->e_header) != NULL)
{ /* We've stripped them once already */ return EX_OK; }addheader(LOOP_CHECK, "", &e->e_header);
for (cnt = 0, h = e->e_header; h != NULL; h = h->h_link)
{if (strcasecmp(h->h_field, "received") != 0)
continue;if (cnt++ == 0)
continue;clrbitmap(h->h_mflags);
h->h_flags |= H_ACHECK;
} return (EX_OK); }
Because we are stripping the message of Received:
headers, we need to be careful. We shouldn't do it if the message originated on the firewall machine (line ). We also shouldn't do it if the message originated from outside the internal (firewalled) network (lines and ). To prevent possibly disastrous mail loops, we check for a special header (line ) and skip stripping again if that header is found. We then add that special header (line ), just in case the mail flows though this firewall again.
If it is okay to do so, we scan all the headers (line ) looking for all Received:
headers (line ). We skip deleting the first one because it was placed there by the firewall (line ). We delete all the others by clearing their ?
flags
?
bits (line ) and setting the H_ACHECK flag (line ). See for a general discussion of this technique.
Be aware that this is only one possible approach and that, depending on what other hosts on the Internet do to the message, this loop detection may break. A safer but more difficult approach is to rewrite the Received:
headers themselves and to mask out sensitive information in them.
Reject Mail from Spamming or Mail-bombing Sites
As the Internet grows, your site may become more and more subject to advertising and vengeful attacks from the outside. Advertising attacks are called "spams" and are symptomized by advertisers sending multiple copies of advertisements through your internal mail lists or to several of your users. Vengeful attacks are called "mail bombs" and usually are detected by your mail spool directory filling with a huge number of messages from a single sender. [3]
[3] Often in response to one of your users sending an offensive spam.
To limit your vulnerability to such events (and to others of a similar nature that may be invented in the future), you may screen mail from outside hosts using a combination of a database and checkcompat(). First we show you how to set up such a database, then we show you a checkcompat() routine for using it. [4]
[4] You may also screen sender addresses at the SMTP MAIL command with the new V8.8
check_mail
rule set (see "The check_mail Rule Set"). Although it can be easier to designcheck_mail
rules, the checkcompat() routine can be more powerful.
The source file for the database will look like this:
user@spam.host spam user@bomb.host bomb
Here, each left-hand side entry is an email address with a user part, an @
, and a host part. We will be screening on the basis of individual sender addresses rather than screening at a sitewide level. The right-hand side is either the word spam
to represent a spamming sender or bomb
to represent a mail-bombing sender.
If the source file is called /etc/mail/blockusers, the database will be created like this:
%makemap hash /etc/mail/blockusers.db < /etc/mail/blockusers
Here, we create a hash
db style database. For other available styles, see "Create Files with makemap".
Once the database is in place, your configuration file needs to be told of its existence. To do that, we use the K
configuration command (see "The K Configuration Command"):
Kbadusers hash -o /etc/mail/blockusers.db
For the m4 configuration technique you would place this declaration under the LOCAL_CONFIG line in your mc file (see LOCAL-CONFIG).
One possible checkcompat() routine to handle all this will look like this:
checkcompat(to, e) register ADDRESS *to; register ENVELOPE *e; { STAB *map; char *p; int ret = 0;map = stab("badusers", ST_MAP, ST_FIND);
if (map == (STAB *)NULL)return (EX_OK);
p = (*map->s_map.map_class->map_lookup)
(&map->s_map, e->e_from.q_paddr, NULL, &ret); if (p == NULL)return (EX_OK);
if (strcasecmp(p, "spam") == 0)
{usrerr("553 Spamming mail rejected from %s",
e->e_from.q_paddr); to->q_status = "5.7.1"; return (EX_UNAVAILABLE); }if (strcasecmp(p, "bomb") == 0)
{ usrerr("553 Message rejected from mail-bomber %s", e->e_from.q_paddr); e->e_flags &= ~EF_NO_BODY_RETN; to->q_status = "5.7.1"; return (EX_UNAVAILABLE); } return (EX_OK); }
Here we first look up the database named badusers
in the symbol table (line ). It is okay for the database not to exist (line ). If the database exists, we look up the sender's address in it (line ). If the address is not found, all is okay (line ).
If the address was found in the database, we have a potential bad person. So we first check to see whether the address was marked as a spam
(line ). If it was, we bounce it with an appropriate error message (line ).
We also bounce the message if it is a mail bomb (line ). This is fraught with risk however. The bounced mail can fill up the outgoing queue, thereby accomplishing the bomber's ends in a different way. A better approach might be to drop the mail on the floor (see dropenvelope() in envelope.c), but we leave this as an exercise for the reader.