This is an unofficial patch for Postfix stable release 2.0 that
back-ports a new feature from the after-2.0 snapshot releases.

This feature can be used to block mail from so-called spammer
havens, from sender addresses that resolve to Verisign's wild-card
mail responder, or from domains that claim to have mail servers in
reserved networks such as 127.0.0.1.

This patch is a revised version; the first implementation of the
check_{helo,sender,recipient}_ns_access feature did not correctly
look up name server records and caused mail to be deferred with a
450 status code; the second revision no longer defers mail when
some NS or MX host lookup fails.

-----------------

The check_{helo,sender,recipient}_{ns,mx}_access maptype:mapname
restriction applies the specified access table to the NS or MX
hosts of the host/domain given in HELO, EHLO, MAIL FROM or RCPT TO
commands.

    /etc/postfix/main.cf:
        smtpd_mumble_restrictions =
            ...
            reject_unknown_sender_domain
            check_sender_mx_access hash:/etc/postfix/mx_access
            ...

    /etc/postfix/mx_access:
        spammer.haven.tld reject spammer mx host
        64.94.110.11      reject mail server in verisign wild-card domain
        127               reject mail server in loopback network
        0                 reject mail server in broadcast network

The following entries block mail from domains that claim to have
have mail servers or domain servers in reserved networks.

        10                reject mail server in RFC 1918 private network
        169.254           reject mail server in link local network
        172.16            reject mail server in RFC 1918 private network
        ...similar entries for 172.17...172.31
        192.0.2           reject mail server in TEST-NET network
        192.168           reject mail server in RFC 1918 private network
        224               reject mail server in class D multicast network
        ...similar entries for 225...239
        240               reject mail server in class E reserved network
        ...similar entries for 241...247
        248               reject mail server in reserved network
        ...similar entries for 249...255

diff -cr /tmp/postfix-2.0.16/conf/sample-smtpd.cf ./conf/sample-smtpd.cf
*** /tmp/postfix-2.0.16/conf/sample-smtpd.cf	Tue Aug 12 12:28:46 2003
--- ./conf/sample-smtpd.cf	Fri Sep 19 11:17:34 2003
***************
*** 311,316 ****
--- 311,321 ----
  #   check_helo_access maptype:mapname
  #	look up HELO hostname or parent domains.
  #	see access(5) for possible lookup results.
+ #   check_helo_mx_access maptype:mapname
+ #   check_helo_ns_access maptype:mapname
+ #	look up the helo hostname MX hosts (or name servers) and apply the
+ #	specified access table to those hosts.
+ #	Note: the OK result is not allowed here for security reasons.
  #   reject: reject the request. Place this at the end of a restriction.
  #   permit: permit the request. Place this at the end of a restriction.
  #   warn_if_reject: next restriction logs a warning instead of rejecting.
***************
*** 343,348 ****
--- 348,358 ----
  #   check_sender_access maptype:mapname
  #	look up sender address, parent domain, or localpart@.
  #	see access(5) for possible lookup results.
+ #   check_sender_mx_access maptype:mapname
+ #   check_sender_ns_access maptype:mapname
+ #	look up sender address MX hosts (or name servers) and apply the
+ #	specified access table to those hosts.
+ #	Note: the OK result is not allowed here for security reasons.
  #   reject_sender_login_mismatch: reject if $smtpd_sender_login_maps specifies
  #	a MAIL FROM address owner, but the client is not (SASL) logged in as
  #	that MAIL FROM address owner; or if the client is (SASL) logged in, but
***************
*** 409,414 ****
--- 419,429 ----
  #   check_recipient_access maptype:mapname
  #	look up recipient address, parent domain, or localpart@.
  #	see access(5) for possible lookup results.
+ #   check_recipient_mx_access maptype:mapname
+ #   check_recipient_ns_access maptype:mapname
+ #	look up the recipient address MX hosts (or name servers) and apply the
+ #	specified access table to those hosts.
+ #	Note: the OK result is not allowed here for security reasons.
  #   reject_non_fqdn_recipient: reject recipient address that is not in FQDN form
  #   reject: reject the request. Place this at the end of a restriction.
  #   permit: permit the request. Place this at the end of a restriction.
diff -cr /tmp/postfix-2.0.16/html/uce.html ./html/uce.html
*** /tmp/postfix-2.0.16/html/uce.html	Mon Aug 25 09:54:11 2003
--- ./html/uce.html	Fri Sep 19 11:17:34 2003
***************
*** 567,572 ****
--- 567,588 ----
  
  <p>
  
+ <a name="check_helo_ns_access">
+ 
+ <dt> <b>check_helo_ns_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <a name="check_helo_mx_access">
+ 
+ <dt> <b>check_helo_mx_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <dd> Apply the specified <a href="access.5.html">access database</a>
+ to the DNS (or MX) servers for the host or domain name given with
+ the HELO (or EHLO) command.
+ 
+ <dd> Note: an OK result is not allowed for safety reasons.
+ 
+ <p>
+ 
  <dt> <b><a href="#permit">permit</a></b>
  
  <dt> <b><a href="#defer">defer</a></b>
***************
*** 714,719 ****
--- 730,751 ----
  
  <p>
  
+ <a name="check_sender_ns_access">
+ 
+ <dt> <b>check_sender_ns_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <a name="check_sender_mx_access">
+ 
+ <dt> <b>check_sender_mx_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <dd> Apply the specified <a href="access.5.html">access database</a>
+ to the DNS (or MX) servers for the host or domain name given with
+ the MAIL FROM command.
+ 
+ <dd> Note: an OK result is not allowed for safety reasons.
+ 
+ <p>
+ 
  <a name="reject_non_fqdn_sender">
  
  <dt> <b>reject_non_fqdn_sender</b> <dd> Reject the request when
***************
*** 921,926 ****
--- 953,974 ----
  <dt> <i>maptype</i>:<i>mapname</i> <dd> Search the named <a
  href="access.5.html">access database</a> for the resolved destination
  address, recipient domain or parent domain, or <i>localpart</i>@. 
+ 
+ <p>
+ 
+ <a name="check_recipient_ns_access">
+ 
+ <dt> <b>check_recipient_ns_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <a name="check_recipient_mx_access">
+ 
+ <dt> <b>check_recipient_mx_access</b> <i>maptype</i>:<i>mapname</i>
+ 
+ <dd> Apply the specified <a href="access.5.html">access database</a>
+ to the DNS servers (or MX hosts) for the host or domain name given
+ with the RCPT TO command.
+ 
+ <dd> Note: an OK result is not allowed for safety reasons.
  
  <p>
  
diff -cr /tmp/postfix-2.0.16/src/dns/dns_lookup.c ./src/dns/dns_lookup.c
*** /tmp/postfix-2.0.16/src/dns/dns_lookup.c	Sun Dec  8 09:09:11 2002
--- ./src/dns/dns_lookup.c	Fri Sep 19 11:17:34 2003
***************
*** 509,514 ****
--- 509,515 ----
  	    vstring_sprintf(why,
  		   "Name service error for %s: invalid host or domain name",
  			    name);
+ 	h_errno = HOST_NOT_FOUND;
  	return (DNS_NOTFOUND);
      }
  
***************
*** 520,525 ****
--- 521,527 ----
  	    vstring_sprintf(why,
  		   "Name service error for %s: invalid host or domain name",
  			    name);
+ 	h_errno = HOST_NOT_FOUND;
  	return (DNS_NOTFOUND);
      }
  
diff -cr /tmp/postfix-2.0.16/src/global/mail_params.h ./src/global/mail_params.h
*** /tmp/postfix-2.0.16/src/global/mail_params.h	Mon Mar  3 17:07:03 2003
--- ./src/global/mail_params.h	Fri Sep 19 11:17:35 2003
***************
*** 1249,1254 ****
--- 1249,1261 ----
  #define CHECK_RECIP_ACL		"check_recipient_access"
  #define CHECK_ETRN_ACL		"check_etrn_access"
  
+ #define CHECK_HELO_MX_ACL	"check_helo_mx_access"
+ #define CHECK_SENDER_MX_ACL	"check_sender_mx_access"
+ #define CHECK_RECIP_MX_ACL	"check_recipient_mx_access"
+ #define CHECK_HELO_NS_ACL	"check_helo_ns_access"
+ #define CHECK_SENDER_NS_ACL	"check_sender_ns_access"
+ #define CHECK_RECIP_NS_ACL	"check_recipient_ns_access"
+ 
  #define WARN_IF_REJECT		"warn_if_reject"
  
  #define REJECT_RBL		"reject_rbl"	/* LaMont compatibility */
diff -cr /tmp/postfix-2.0.16/src/smtpd/smtpd_check.c ./src/smtpd/smtpd_check.c
*** /tmp/postfix-2.0.16/src/smtpd/smtpd_check.c	Tue Aug 12 10:53:25 2003
--- ./src/smtpd/smtpd_check.c	Fri Sep 19 11:17:52 2003
***************
*** 86,91 ****
--- 86,103 ----
  /* .IP "check_recipient_access maptype:mapname"
  /*	Look up the resolved recipient address in the named access table,
  /*	any parent domains of the recipient domain, and the localpart@.
+ /* .IP "check_helo_mx_access maptype:mapname"
+ /* .IP "check_sender_mx_access maptype:mapname"
+ /* .IP "check_recipient_mx_access maptype:mapname"
+ /*	Apply the specified access table to the MX server host name and IP
+ /*	addresses for the helo hostname, sender, or recipient, respectively.
+ /*	If no MX record is found the A record is used instead.
+ /* .IP "check_helo_ns_access maptype:mapname"
+ /* .IP "check_sender_ns_access maptype:mapname"
+ /* .IP "check_recipient_ns_access maptype:mapname"
+ /*	Apply the specified access table to the DNS server host name and IP
+ /*	addresses for the helo hostname, sender, or recipient, respectively.
+ /*	If no NS record is found, the parent domain is used instead.
  /* .IP "check_recipient_maps"
  /*	Reject recipients not listed as valid local, virtual or relay
  /*	recipients.
***************
*** 455,460 ****
--- 467,484 ----
      else \
  	(void) smtpd_check_reject((state), (class), (fmt), (a1), (a2)); \
      } while (0)
+ #define DEFER_IF_PERMIT3(state, class, fmt, a1, a2, a3) do { \
+     if ((state)->warn_if_reject == 0) \
+ 	defer_if(&(state)->defer_if_permit, (class), (fmt), (a1), (a2), (a3)); \
+     else \
+ 	(void) smtpd_check_reject((state), (class), (fmt), (a1), (a2), (a3)); \
+     } while (0)
+ #define DEFER_IF_PERMIT4(state, class, fmt, a1, a2, a3, a4) do { \
+     if ((state)->warn_if_reject == 0) \
+ 	defer_if(&(state)->defer_if_permit, (class), (fmt), (a1), (a2), (a3), (a4)); \
+     else \
+ 	(void) smtpd_check_reject((state), (class), (fmt), (a1), (a2), (a3), (a4)); \
+     } while (0)
  
   /*
    * Cached RBL lookup state.
***************
*** 2005,2010 ****
--- 2029,2151 ----
      return (SMTPD_CHECK_DUNNO);
  }
  
+ /* check_server_access - access control by server host name or address */
+ 
+ static int check_server_access(SMTPD_STATE *state, const char *table,
+ 			               const char *name,
+ 			               int type,
+ 			               const char *reply_name,
+ 			               const char *reply_class,
+ 			               const char *def_acl)
+ {
+     const char *myname = "check_server_access";
+     const char *domain;
+     int     dns_status;
+     DNS_RR *server_list;
+     DNS_RR *server;
+     int     found = 0;
+     struct in_addr addr;
+     struct hostent *hp;
+     char   *addr_string;
+     int     status;
+     char  **cpp;
+     static DNS_FIXED fixed;
+ 
+     /*
+      * Sanity check.
+      */
+     if (type != T_MX && type != T_NS)
+ 	msg_panic("%s: unexpected resource type \"%s\" in request",
+ 		  myname, dns_strtype(type));
+ 
+     if (msg_verbose)
+ 	msg_info("%s: %s %s", myname, dns_strtype(type), name);
+ 
+     /*
+      * Skip over local-part.
+      */
+     if ((domain = strrchr(name, '@')) != 0)
+ 	domain += 1;
+     else
+ 	domain = name;
+ 
+     /*
+      * If the domain name does not exist then we apply no restriction.
+      * 
+      * If the domain name exists but no MX record exists, fabricate an MX record
+      * that points to the domain name itself.
+      * 
+      * If the domain name exists but no NS record exists, look up parent domain
+      * NS records.
+      */
+     dns_status = dns_lookup(domain, type, 0, &server_list,
+ 			    (VSTRING *) 0, (VSTRING *) 0);
+     if (dns_status == DNS_NOTFOUND && h_errno == NO_DATA) {
+ 	if (type == T_MX) {
+ 	    server_list = dns_rr_create(domain, &fixed, 0,
+ 					domain, strlen(domain) + 1);
+ 	    dns_status = DNS_OK;
+ 	} else if (type == T_NS) {
+ 	    while ((domain = strchr(domain, '.')) != 0 && domain[1]) {
+ 		domain += 1;
+ 		dns_status = dns_lookup(domain, type, 0, &server_list,
+ 					(VSTRING *) 0, (VSTRING *) 0);
+ 		if (dns_status != DNS_NOTFOUND || h_errno != NO_DATA)
+ 		    break;
+ 	    }
+ 	}
+     }
+     if (dns_status != DNS_OK) {
+ 	msg_warn("Unable to look up %s host for %s", dns_strtype(type),
+ 		 domain && domain[1] ? domain : reply_name);
+ 	return (SMTPD_CHECK_DUNNO);
+     }
+ 
+     /*
+      * No bare returns after this point or we have a memory leak.
+      */
+ #define CHECK_SERVER_RETURN(x) { dns_rr_free(server_list); return(x); }
+ 
+     /*
+      * Check the hostnames first, then the addresses.
+      */
+     for (server = server_list; server != 0; server = server->next) {
+ 	if ((hp = gethostbyname((char *) server->data)) == 0) {
+ 	    msg_warn("Unable to look up %s host %s for %s %s",
+ 		     dns_strtype(type), (char *) server->data,
+ 		     reply_class, reply_name);
+ 	    continue;
+ 	}
+ 	if (hp->h_addrtype != AF_INET || hp->h_length != sizeof(addr)) {
+ 	    if (msg_verbose)
+ 		msg_warn("address type %d length %d for %s",
+ 		       hp->h_addrtype, hp->h_length, (char *) server->data);
+ 	    continue;				/* XXX */
+ 	}
+ 	if (msg_verbose)
+ 	    msg_info("%s: %s hostname check: %s",
+ 		     myname, dns_strtype(type), (char *) server->data);
+ 	if ((status = check_domain_access(state, table, (char *) server->data,
+ 				      FULL, &found, reply_name, reply_class,
+ 					  def_acl)) != 0 || found)
+ 	    CHECK_SERVER_RETURN(status);
+ 	if (msg_verbose)
+ 	    msg_info("%s: %s host address check: %s",
+ 		     myname, dns_strtype(type), (char *) server->data);
+ 	for (cpp = hp->h_addr_list; *cpp; cpp++) {
+ 	    memcpy((char *) &addr, *cpp, sizeof(addr));
+ 	    addr_string = mystrdup(inet_ntoa(addr));
+ 	    status = check_addr_access(state, table, addr_string, FULL,
+ 				       &found, reply_name, reply_class,
+ 				       def_acl);
+ 	    myfree(addr_string);
+ 	    if (status != 0 || found)
+ 		CHECK_SERVER_RETURN(status);
+ 	}
+     }
+     CHECK_SERVER_RETURN(SMTPD_CHECK_DUNNO);
+ }
+ 
  /* check_mail_access - OK/FAIL based on mail address lookup */
  
  static int check_mail_access(SMTPD_STATE *state, const char *table,
***************
*** 2568,2573 ****
--- 2709,2728 ----
      }
  }
  
+ /* forbid_whitelist - disallow whitelisting */
+ 
+ static void forbid_whitelist(SMTPD_STATE *state, const char *name,
+ 			             int status, const char *target)
+ {
+     if (status == SMTPD_CHECK_OK) {
+ 	msg_warn("restriction %s returns OK for %s", name, target);
+ 	msg_warn("this is not allowed for security reasons");
+ 	msg_warn("use DUNNO instead of OK if you want to make an exception");
+ 	longjmp(smtpd_check_buf, smtpd_check_reject(state, MAIL_ERROR_SOFTWARE,
+ 					 "451 Server configuration error"));
+     }
+ }
+ 
  /* generic_checks - generic restrictions */
  
  static int generic_checks(SMTPD_STATE *state, ARGV *restrictions,
***************
*** 2722,2727 ****
--- 2877,2896 ----
  				   state->helo_name, SMTPD_NAME_HELO)) == 0)
  		    status = SMTPD_CHECK_OK;
  	    }
+ 	} else if (is_map_command(state, name, CHECK_HELO_NS_ACL, &cpp)) {
+ 	    if (state->helo_name) {
+ 		status = check_server_access(state, *cpp, state->helo_name,
+ 					     T_NS, state->helo_name,
+ 					     SMTPD_NAME_HELO, def_acl);
+ 		forbid_whitelist(state, name, status, state->helo_name);
+ 	    }
+ 	} else if (is_map_command(state, name, CHECK_HELO_MX_ACL, &cpp)) {
+ 	    if (state->helo_name) {
+ 		status = check_server_access(state, *cpp, state->helo_name,
+ 					     T_MX, state->helo_name,
+ 					     SMTPD_NAME_HELO, def_acl);
+ 		forbid_whitelist(state, name, status, state->helo_name);
+ 	    }
  	} else if (strcasecmp(name, REJECT_NON_FQDN_HOSTNAME) == 0) {
  	    if (state->helo_name) {
  		if (*state->helo_name != '[')
***************
*** 2760,2765 ****
--- 2929,2948 ----
  	} else if (strcasecmp(name, REJECT_SENDER_LOGIN_MISMATCH) == 0) {
  	    if (state->sender && *state->sender)
  		status = reject_sender_login_mismatch(state, state->sender);
+ 	} else if (is_map_command(state, name, CHECK_SENDER_NS_ACL, &cpp)) {
+ 	    if (state->sender && *state->sender) {
+ 		status = check_server_access(state, *cpp, state->sender,
+ 					     T_NS, state->sender,
+ 					     SMTPD_NAME_SENDER, def_acl);
+ 		forbid_whitelist(state, name, status, state->sender);
+ 	    }
+ 	} else if (is_map_command(state, name, CHECK_SENDER_MX_ACL, &cpp)) {
+ 	    if (state->sender && *state->sender) {
+ 		status = check_server_access(state, *cpp, state->sender,
+ 					     T_MX, state->sender,
+ 					     SMTPD_NAME_SENDER, def_acl);
+ 		forbid_whitelist(state, name, status, state->sender);
+ 	    }
  	} else if (strcasecmp(name, REJECT_RHSBL_SENDER) == 0) {
  	    if (cpp[1] == 0)
  		msg_warn("restriction %s requires domain name argument", name);
***************
*** 2812,2817 ****
--- 2995,3014 ----
  	    if (state->recipient)
  		status = reject_non_fqdn_address(state, state->recipient,
  				    state->recipient, SMTPD_NAME_RECIPIENT);
+ 	} else if (is_map_command(state, name, CHECK_RECIP_NS_ACL, &cpp)) {
+ 	    if (state->recipient && *state->recipient) {
+ 		status = check_server_access(state, *cpp, state->recipient,
+ 					     T_NS, state->recipient,
+ 					     SMTPD_NAME_RECIPIENT, def_acl);
+ 		forbid_whitelist(state, name, status, state->recipient);
+ 	    }
+ 	} else if (is_map_command(state, name, CHECK_RECIP_MX_ACL, &cpp)) {
+ 	    if (state->recipient && *state->recipient) {
+ 		status = check_server_access(state, *cpp, state->recipient,
+ 					     T_MX, state->recipient,
+ 					     SMTPD_NAME_RECIPIENT, def_acl);
+ 		forbid_whitelist(state, name, status, state->recipient);
+ 	    }
  	} else if (strcasecmp(name, REJECT_RHSBL_RECIPIENT) == 0) {
  	    if (cpp[1] == 0)
  		msg_warn("restriction %s requires domain name argument", name);
