Previous Section Next Section

7.1 The Local_check_ Rule Sets

The rapid spread of the Internet has led to an increase of mail abuses. Prior to V8.8 sendmail, detecting and rejecting abusive email required that you write C-language code for use in the checkcompat/( ) routine (see Appendix D). Beginning with V8.8 sendmail, important and useful checking and rejecting can be done from within four pairs of complementary rule sets. They are presented in the order that sendmail calls them:[2]

[2] See the delay_checks feature (Section 7.5.6) to see how that feature changes this order.

Local_check_relay and check_relay

Validate the host initiating the SMTP connection.

Local_check_mail and check_mail

Validate the envelope-sender address given to the SMTP MAIL command.

Local_check_rcpt and check_rcpt

Validate the envelope-recipient address given to the SMTP RCPT command.

check_compat

Compare or contrast each pair of envelope-sender and envelope-recipient addresses before delivery, and validate them based on the result.

These routines are all handled in the same manner. If the rule set returns anything other than a #error or a #discard delivery agent, the message is accepted. Otherwise, the #error delivery agent causes the message to be rejected or deferred (error), whereas the #discard delivery agent causes the message to be accepted, then discarded (discard).

7.1.1 Local_check_relay and check_relay

V8.8 sendmail supports two mechanisms for screening incoming SMTP connections. One is the libwrap.a mechanism, the other is this check_relay rule set. V8.9 sendmail adds a third mechanism, the access database (Section 7.5).

The Local_check_relay rule set provides a hook into the check_relay rule set, which is used to screen incoming network connections and accept or reject them based on the hostname, domain, or IP number. It is called just before the libwrap.a code and can be used even if that code was omitted from your release of sendmail. The check_relay rule set is not called if sendmail was run with the -bs command-line switch (-bs).

The check_relay rule set is called with a workspace that looks like this:

host $| IPnumber

The hostname and IP number are separated by the $| operator. The host is the fully qualified canonical name of the connecting host. The IPnumber is the IP number of that host in dotted-quad form without surrounding square brackets, or the IPv6 number prefixed with a literal IPv6:.

By default, the check_relay rule set allows all connections. This behavior can be overridden or enforced in the access database by prefixing leftmost keys with a literal Connect: (Section 7.5.3):

Connect:bad.host       REJECT

Here, for example, any connection from the host bad.host is rejected.

The default behavior of the check_relay rule set can also be overridden by the various DNS blacklist features (see Section 7.2).

In the event you need to add checks to this check_relay rule set, you can do so by adding a Local_check_relay rule set. Declaring this latter rule set gives you a hook into the start of check_relay, which means your rules are applied before the default rules.

One way to use Local_check_relay might be to list offensive sites in a database and reject any connections from those sites.[3] Consider a database that contains hostnames or addresses as its keys and descriptions of each host's offense as its values:

[3] We illustrate this scheme, despite the fact that it is available in the access database, because other meaningful uses for this rule set are rare.

hostA.edu      Spamming site
hostB.com      Mail Bombing site
123.45.6       Offensive domain
IPv6:2002:c0a8:51d2::23f4      Offending host

Notice that the keys can be hostnames, or IPv4 or IPv6 addresses. Such a database might be declared in the configuration file like this:

LOCAL_CONFIG
Kbadhosts dbm -a<> /etc/mail/badhosts

Now, each time a site connects to your running daemon, the following rule set will be called:

SLocal_check_relay
R $* $| $*            $: $(badhosts $1 $) $| $2             look up hostname
R $*<> $| $*          $#error $@ 5.1.3 $: 550 Sorry, $1 denied
R $* $|  $*           $: $2                                 select the IP number
R $-.$-.$-.$-         $: $(badhosts $1.$2.$3.$4 $)          look up host address
R IPv6 : $+           $: $(badhosts IPv6:$1 $)              look up host or network 
address
R $*<>                $#error $@ 5.1.3 $: 550 Sorry, $1 denied
R $*                  $@ ok                                 otherwise OK

The first rule looks up the host part in the database. If it is found, the value (reason for rejection) is returned and the two characters < > are appended. The second rule looks for anything to the left of the $| that ends in < > and, if anything is found, issues the error:[4]

[4] Actually, the message is not printed; instead, the SMTP daemon goes into a "reject everything" mode. This prevents some SMTP implementations from retrying the connection.

550 5.1.3 Sorry, reason for rejectdenied

Rejected connections are handled in the same way as connections rejected by the access database (Section 7.5).

The rest of the rules do the same thing, but also check for the IP number.

If the Local_check_relay rule set returns a #error or #discard delivery agent, the connection is rejected. If it returns a $#OK,[5] the connection is accepted and subsequent check_relay rule set rules are skipped:

[5] Actually $#anything will have the same effect, but you should use $#OK only to remain compatible with future releases of sendmail.

SLocal_check_relay
R $*              $# OK      skip check_relay rule set rules

But if it returns a $@OK, further check_relay rule set rules are allowed which might themselves reject the connection:

SLocal_check_relay
R $*              $@ OK      allow check_relay rule set rules

Note that the rules presented here are not nearly as complex or sophisticated as your site will likely need. They do not, for example, reject on the basis of the domain part of the hostname, nor do they reject on the basis of the individual host IP addresses.

Note that the rules in the Local_check_relay and check_relay rule sets cannot be tested in rule-testing mode because that mode wrongly interprets the expression $| (when you enter it at the > prompt) as two separate text characters instead of as a single operator. To test an address that contains an embedded $| operator, we suggest that you create a translation rule set something like this:

LOCAL_RULESETS
STranslate
R $* $$| $*              $: $1 $| $2                            fake for -bt mode

This rule set changes a literal $ and | into a $| operator so that you can test rule sets such as Local_check_relay from rule-testing mode:

ADDRESS TEST MODE (ruleset 3 NOT automatically invoked)
Enter <ruleset> <address>
> Translate,Local_check_relay bogus.host.domain $| 123.45.67.89

Here, the comma-separated list of rule sets begins with Translate, which changes the two-character text expression "$|" into the single operator $|. The result, an address expression that is suitable for the Local_check_relay rule set, can then be successfully tested.[6]

[6] Don't be tempted to put this rule directly into the Local_check_relay rule set. You might someday encounter an address that has the two adjacent characters $ and | as a legal part of it. Also be aware that such addresses might be intentionally sent to circumvent your checks.

7.1.2 Local_check_mail and check_mail

The Local_check_mail rule set provides a hook into the check_mail rule set, which is used to validate the envelope-sender address given in the MAIL command of the SMTP dialog:

MAIL From:<sender@host.domain>

The check_mail rule set is called immediately after the MAIL command is read. The workspace passed to check_mail is the address following the colon in the MAIL command. That envelope-sender address might or might not be surrounded by angle braces.

If sendmail's delivery mode is anything other than deferred (-bd), the check_mail rule set performs the following default actions:

  • Calls the tls_client rule set (Section 10.10.8.2) to perform TLS verification, if needed.

  • Accepts all envelope-sender addresses of the form < >.

  • Makes certain that the host and domain part of the envelope-sender address exists.

  • If the access database (Section 7.5) is used, it looks up the envelope-sender in that database and rejects, accepts, or defers the message based on the returned lookup value.

The Local_check_mail rule set provides a hook into check_mail before the preceding checks are made, and provides a place for you to insert your own rules.

To illustrate one use for the Local_check_mail rule set, consider the need to accept all mail from an internal domain, even when many of the hosts in that domain cannot be looked up with DNS.[7] One method might look like this:

[7] Normally, sendmail rejects mail from a site whose name cannot be found with DNS with the error "domain of sender must exist."

LOCAL_RULESETS
SLocal_check_mail
R $*                   $: $>canonify $1       focus on the host
R $* <@ $+. > $*       $1 <@ $2> $3           strip trailing dots
R $* <@ $+ > $*        $: $2                  isolate the host
R $* . $+ . $+         $2 . $3                strip subdomains
R internal.org         $# OK

Here, we force the rule set named canonify to preprocess the address so that any RFC2822 comments will be thrown away and the host part of the address will be focused.[8] We then strip any trailing dots from the hostname to prevent a trailing dot from wrongly affecting our validation. In the third line, we throw away everything but the hostname. In the fourth line, we throw away all but the rightmost two components of the hostname to eliminate the host part and any subdomain prefixes. What remains is the domain name. We then compare that domain name to the hostname internal.org. If they match, we accept the sender. If they don't match, the default rules in the check_mail rule set continue to process the address.

[8] The name canonify corresponds to rule set 3.

Note that if this Local_check_mail rule set returns $#OK,[9] all subsequent check_mail rule set checks of the envelope-sender will be suppressed:

[9] Actually $#anything will have the same effect, but you should use $#OK only to remain compatible with future releases of sendmail.Other rules might still reject it, possibly for other reasons, so always test new rules carefully.

SLocal_check_mail
R $*              $# OK      skip check_mail rule set checks

But if it returns $@OK, further envelope-sender check_mail rule set checks are processed (such as looking up the user and host parts in the access database, or trying to resolve the host part):

SLocal_check_mail
R $*              $@ OK      allow check_mail rule set checks

After this rule set is installed (and the sendmail daemon had been restarted), all mail from internal.org will be accepted during the SMTP dialog even if the hostname does not exist.

Other uses for the Local_check_mail rule set might include limiting certain senders to only a few outbound messages per day, by using an external database to record attempts; rejecting the user part of sender addresses for special reasons, such as being all numeric; and rejecting mail from a specific list of users at a given site.

If you need to base a decision to reject mail on both the sender and the recipient, you might be able to use the check_compat rule set described next, or design your own rules for this rule set using $&f ($f).

7.1.3 Local_check_rcpt and check_rcpt

The Local_check_rcpt rule set provides a hook into the check_rcpt rule set, which is used to validate the recipient-sender address given in the RCPT command in the SMTP dialog:

RCPT To:<recipient@host.domain>

The check_rcpt rule set is called immediately after the RCPT command is read. The workspace that is passed to check_rcpt is the address following the colon. The envelope-recipient address might or might not be surrounded by angle brackets and might or might not have other RFC2822 comments associated with it.

The check_rcpt rule set has default rules that do the following:

  • Reject empty envelope-recipient addresses, such as < >, and those which have nothing following the RCPT TO:.

  • Ensure that the envelope-recipient address is either local, or one that is allowed to be relayed.

  • If the access database (Section 7.5) is used, looks up the envelope-recipient's host in that database and rejects, accepts, or defers the message based on the returned lookup value. If the blacklist_recipients feature (Section 7.5.5) is declared, it also looks up the envelope-recipient in that database.

The Local_check_rcpt gives you a hook into the check_rcpt rule set before any of the default rules are called. To illustrate one use for the Local_check_rcpt rule set, consider the need to reject all incoming mail destined for the recipient named fax. One method might look like this:

LOCAL_RULESETS
SLocal_check_rcpt
R $*                   $: $>canonify $1        focus on host
R fax <@ $=w . > $*    $#error $@ 5.1.3 $: "cannot send mail to fax"

Here, the first rule calls the rule set named canonify to focus on the host part of the address and normalize it. The second rule rejects anything to fax in any of our local domains (the $=w). A recipient address of fax at any other domain will pass through these rules and be accepted:

RCPT To: <fax@ourhost>
553 5.1.3 <fax@ourhost>... cannot send mail to fax

Other uses for this Local_check_rcpt rule set (documented elsewhere in this book) include the following:

  • Create a special bounce-handling machine that accepts all bounced mail, then logs and discards it (Section 6.3.2)

  • Create a special performance-testing blackhole machine that accepts all outside mail and silently discards it (Section 6.3.3)

Note that if this Local_check_rcpt rule set returns $#OK,[10] all subsequent checks with the check_rcpt rule set will be suppressed:

[10] Actually $#anything will have the same effect, but you should use $#OK only to remain compatible with future releases of sendmail.

SLocal_check_rcpt
R $*              $# OK      skip check_rcpt rule set checks

But if it returns $@OK, further checks with the check_rcpt rule set are processed (such as looking up the user and host parts in the access database, and such as validating that the host part is local):

SLocal_check_rcpt
R $*              $@ OK      allow check_rcpt rule set checks

If you need to base a decision to reject mail on both the sender and the recipient, you can either use the check_compat rule set described next, or design you own rules for this rule set using $&f ($f).

Note that check_rcpt rule set rules apply only to mail that arrives via SMTP. If your site submits mail using SMTP you might find locally originating mail being wrongly rejected. If yours is such a site, you can add the following rules to Local_check_rcpt, which should fix the problem:

SLocal_check_rcpt
R $*                 $: $&{client_addr}
R 127.0.0.1          $@ OK

7.1.4 The check_compat Rule Set

Not all situations can be resolved by simply checking the RCPT TO: or MAIL FROM: address. Sometimes you will need to make judgments based on pairs of addresses, or non-SMTP addresses or other information. To handle this situation, V8.8 introduced the check_compat rule set. Unlike check_mail and check_rcpt, check_compat is called for all deliveries, not just SMTP transactions. It is called after an address has undergone aliases translation, just after the check for too large a size (as defined by M=; see M=), and just before the checkcompat( ) routine (see Appendix D).

Note that although with V8.12 and above you can still write you own check_compat rule set, doing so has been made unnecessary by the compat_check feature (Section 7.5.7). But also note that, as of V8.12, you cannot both declare the check_compat feature and use this check_compat rule set.

The check_compat rule set is called with a workspace that looks like this:

sender $| recipient

The sender and recipient addresses are separated by the $| operator. Each has undergone aliasing and ~/.forward file processing.

One use for the check_compat rule set is to prevent a certain user (here operator/) from sending mail offsite:

LOCAL_RULESETS
SGet_domain
R $*                     $: $>canonify $1        focus on host
R $* <@ $+. > $*         $1 <@ $2> $3            strip trailing dots
R $* <@ $+ > $*          $: $2                   isolate the host
R $* . $+ . $+           $2 . $3                 strip host and subdomains

SGet_user
R $*                     $: $>3 $1               focus on host
R $* <@ $+ > $*          $@ $1                   discard host

Scheck_compat
R $* $| $*               $: $1 $|  $>Get_domain $2       fetch recipient domain
R $* $|  $=w             $@ ok                           local is OK
R $* $|  $=m             $@ ok                           local is OK
R $* $|  $*              $: $>Get_user $1                fetch sender user
R operator               $#error $@ 5.1.3 $: "operator might not mail off site"

First, we set up two subroutines patterned after the code in the previous two sections. The first reduces its workspace to just the domain part of an address. The second reduces an address to just the user part. These two subroutines are called by check_compat.

The first rule in check_compat uses the Get_domain subroutine to convert the address on the right of the $| (the recipient) into just a domain name. That right side is compared to the local hostnames ($=w and $=m). If the domain is a local one, delivery is allowed (we return anything but a #error or a $#discard).

But if the domain is an offsite one, we call Get_user to fetch the user part of the address to the left of the $| (the sender). If that user is operator, delivery is denied and the message bounces.

Other uses for the check_compat rule set might include the following:

  • Logging a record of when a DSN NOTIFY request of success is requested (${dsn_notify})

  • Creating a class of users who, possibly for security reasons, might send mail inside the organization but not outside it

  • Screening a particular recipient to prevent that user from receiving objectionable mail from a specific source

Note that such rule sets cannot be tested in rule-testing mode because that mode wrongly interprets the expression $| (when you enter it at the > prompt) as two separate text characters instead of a single one. See Section 7.1.1 for one suggested solution to this problem.

    Previous Section Next Section