/*++ /* NAME /* smtp_addr 3 /* SUMMARY /* SMTP server address lookup /* SYNOPSIS /* #include "smtp_addr.h" /* /* DNS_RR *smtp_domain_addr(name, mxrr, misc_flags, why, found_myself) /* char *name; /* DNS_RR **mxrr; /* int misc_flags; /* DSN_BUF *why; /* int *found_myself; /* /* DNS_RR *smtp_host_addr(name, misc_flags, why) /* char *name; /* int misc_flags; /* DSN_BUF *why; /* DESCRIPTION /* This module implements Internet address lookups. By default, /* lookups are done via the Internet domain name service (DNS). /* A reasonable number of CNAME indirections is permitted. When /* DNS lookups are disabled, host address lookup is done with /* getnameinfo() or gethostbyname(). /* /* smtp_domain_addr() looks up the network addresses for mail /* exchanger hosts listed for the named domain. Addresses are /* returned in most-preferred first order. The result is truncated /* so that it contains only hosts that are more preferred than the /* local mail server itself. The found_myself result parameter /* is updated when the local MTA is MX host for the specified /* destination. If MX records were found, the rname, qname, /* and dnssec validation status of the MX RRset are returned /* via mxrr, which the caller must free with dns_rr_free(). /* /* When no mail exchanger is listed in the DNS for \fIname\fR, the /* request is passed to smtp_host_addr(). /* /* It is an error to call smtp_domain_addr() when DNS lookups are /* disabled. /* /* smtp_host_addr() looks up all addresses listed for the named /* host. The host can be specified as a numerical Internet network /* address, or as a symbolic host name. /* /* Results from smtp_domain_addr() or smtp_host_addr() are /* destroyed by dns_rr_free(), including null lists. /* DIAGNOSTICS /* Panics: interface violations. For example, calling smtp_domain_addr() /* when DNS lookups are explicitly disabled. /* /* All routines either return a DNS_RR pointer, or return a null /* pointer and update the \fIwhy\fR argument accordingly. /* LICENSE /* .ad /* .fi /* The Secure Mailer license must be distributed with this software. /* AUTHOR(S) /* Wietse Venema /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA /* /* Wietse Venema /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA /*--*/ /* System library. */ #include #include #include #include #include #include #include #include #include #include /* Utility library. */ #include #include #include #include #include #include #include #include /* Global library. */ #include #include #include /* DNS library. */ #include /* Application-specific. */ #include "smtp.h" #include "smtp_addr.h" /* smtp_print_addr - print address list */ static void smtp_print_addr(const char *what, DNS_RR *addr_list) { DNS_RR *addr; MAI_HOSTADDR_STR hostaddr; msg_info("begin %s address list", what); for (addr = addr_list; addr; addr = addr->next) { if (dns_rr_to_pa(addr, &hostaddr) == 0) { msg_warn("skipping record type %s: %m", dns_strtype(addr->type)); } else { msg_info("pref %4d host %s/%s", addr->pref, SMTP_HNAME(addr), hostaddr.buf); } } msg_info("end %s address list", what); } /* smtp_addr_one - address lookup for one host name */ static DNS_RR *smtp_addr_one(DNS_RR *addr_list, const char *host, int res_opt, unsigned pref, DSN_BUF *why) { const char *myname = "smtp_addr_one"; DNS_RR *addr = 0; DNS_RR *rr; int aierr; struct addrinfo *res0; struct addrinfo *res; INET_PROTO_INFO *proto_info = inet_proto_info(); unsigned char *proto_family_list = proto_info->sa_family_list; int found; if (msg_verbose) msg_info("%s: host %s", myname, host); /* * Interpret a numerical name as an address. */ if (hostaddr_to_sockaddr(host, (char *) 0, 0, &res0) == 0) { if (strchr((char *) proto_family_list, res0->ai_family) != 0) { if ((addr = dns_sa_to_rr(host, pref, res0->ai_addr)) == 0) msg_fatal("host %s: conversion error for address family " "%d: %m", host, res0->ai_addr->sa_family); addr_list = dns_rr_append(addr_list, addr); freeaddrinfo(res0); return (addr_list); } freeaddrinfo(res0); } /* * Use DNS lookup, but keep the option open to use native name service. * * XXX A soft error dominates past and future hard errors. Therefore we * should not clobber a soft error text and status code. */ if (smtp_host_lookup_mask & SMTP_HOST_FLAG_DNS) { res_opt |= smtp_dns_res_opt; switch (dns_lookup_v(host, res_opt, &addr, (VSTRING *) 0, why->reason, DNS_REQ_FLAG_NONE, proto_info->dns_atype_list)) { case DNS_OK: for (rr = addr; rr; rr = rr->next) rr->pref = pref; addr_list = dns_rr_append(addr_list, addr); return (addr_list); default: dsb_status(why, "4.4.3"); return (addr_list); case DNS_FAIL: dsb_status(why, SMTP_HAS_SOFT_DSN(why) ? "4.4.3" : "5.4.3"); return (addr_list); case DNS_INVAL: dsb_status(why, SMTP_HAS_SOFT_DSN(why) ? "4.4.4" : "5.4.4"); return (addr_list); case DNS_POLICY: dsb_status(why, "4.7.0"); return (addr_list); case DNS_NOTFOUND: dsb_status(why, SMTP_HAS_SOFT_DSN(why) ? "4.4.4" : "5.4.4"); /* maybe native naming service will succeed */ break; } } /* * Use the native name service which also looks in /etc/hosts. * * XXX A soft error dominates past and future hard errors. Therefore we * should not clobber a soft error text and status code. */ #define RETRY_AI_ERROR(e) \ ((e) == EAI_AGAIN || (e) == EAI_MEMORY || (e) == EAI_SYSTEM) #ifdef EAI_NODATA #define DSN_NOHOST(e) \ ((e) == EAI_AGAIN || (e) == EAI_NODATA || (e) == EAI_NONAME) #else #define DSN_NOHOST(e) \ ((e) == EAI_AGAIN || (e) == EAI_NONAME) #endif if (smtp_host_lookup_mask & SMTP_HOST_FLAG_NATIVE) { if ((aierr = hostname_to_sockaddr(host, (char *) 0, 0, &res0)) != 0) { dsb_simple(why, (SMTP_HAS_SOFT_DSN(why) || RETRY_AI_ERROR(aierr)) ? (DSN_NOHOST(aierr) ? "4.4.4" : "4.3.0") : (DSN_NOHOST(aierr) ? "5.4.4" : "5.3.0"), "unable to look up host %s: %s", host, MAI_STRERROR(aierr)); } else { for (found = 0, res = res0; res != 0; res = res->ai_next) { if (strchr((char *) proto_family_list, res->ai_family) == 0) { msg_info("skipping address family %d for host %s", res->ai_family, host); continue; } found++; if ((addr = dns_sa_to_rr(host, pref, res->ai_addr)) == 0) msg_fatal("host %s: conversion error for address family " "%d: %m", host, res0->ai_addr->sa_family); addr_list = dns_rr_append(addr_list, addr); if (DNS_RR_IS_TRUNCATED(addr_list)) break; } freeaddrinfo(res0); if (found == 0) { dsb_simple(why, SMTP_HAS_SOFT_DSN(why) ? "4.4.4" : "5.4.4", "%s: host not found", host); } return (addr_list); } } /* * No further alternatives for host lookup. */ return (addr_list); } /* smtp_addr_list - address lookup for a list of mail exchangers */ static DNS_RR *smtp_addr_list(DNS_RR *mx_names, DSN_BUF *why) { DNS_RR *addr_list = 0; DNS_RR *rr; int res_opt = 0; if (mx_names->dnssec_valid) res_opt = RES_USE_DNSSEC; #ifdef USE_TLS else if (smtp_tls_insecure_mx_policy > TLS_LEV_MAY) res_opt = RES_USE_DNSSEC; #endif /* * As long as we are able to look up any host address, we ignore problems * with DNS lookups (except if we're backup MX, and all the better MX * hosts can't be found). * * XXX 2821: update the error status (0->FAIL upon unrecoverable lookup * error, any->RETRY upon temporary lookup error) so that we can * correctly handle the case of no resolvable MX host. Currently this is * always treated as a soft error. RFC 2821 wants a more precise * response. * * XXX dns_lookup() enables RES_DEFNAMES. This is wrong for names found in * MX records - we should not append the local domain to dot-less names. * * XXX However, this is not the only problem. If we use the native name * service for host lookup, then it will usually enable RES_DNSRCH which * appends local domain information to all lookups. In particular, * getaddrinfo() may invoke a resolver that runs in a different process * (NIS server, nscd), so we can't even reliably turn this off by * tweaking the in-process resolver flags. */ for (rr = mx_names; rr; rr = rr->next) { if (rr->type != T_MX) msg_panic("smtp_addr_list: bad resource type: %d", rr->type); addr_list = smtp_addr_one(addr_list, (char *) rr->data, res_opt, rr->pref, why); if (addr_list && DNS_RR_IS_TRUNCATED(addr_list)) break; } return (addr_list); } /* smtp_find_self - spot myself in a crowd of mail exchangers */ static DNS_RR *smtp_find_self(DNS_RR *addr_list) { const char *myname = "smtp_find_self"; INET_ADDR_LIST *self; INET_ADDR_LIST *proxy; DNS_RR *addr; int i; self = own_inet_addr_list(); proxy = proxy_inet_addr_list(); for (addr = addr_list; addr; addr = addr->next) { /* * Find out if this mail system is listening on this address. */ for (i = 0; i < self->used; i++) if (DNS_RR_EQ_SA(addr, (struct sockaddr *) (self->addrs + i))) { if (msg_verbose) msg_info("%s: found self at pref %d", myname, addr->pref); return (addr); } /* * Find out if this mail system has a proxy listening on this * address. */ for (i = 0; i < proxy->used; i++) if (DNS_RR_EQ_SA(addr, (struct sockaddr *) (proxy->addrs + i))) { if (msg_verbose) msg_info("%s: found proxy at pref %d", myname, addr->pref); return (addr); } } /* * Didn't find myself, or my proxy. */ if (msg_verbose) msg_info("%s: not found", myname); return (0); } /* smtp_truncate_self - truncate address list at self and equivalents */ static DNS_RR *smtp_truncate_self(DNS_RR *addr_list, unsigned pref) { DNS_RR *addr; DNS_RR *last; for (last = 0, addr = addr_list; addr; last = addr, addr = addr->next) { if (pref == addr->pref) { if (msg_verbose) smtp_print_addr("truncated", addr); dns_rr_free(addr); if (last == 0) { addr_list = 0; } else { last->next = 0; } break; } } return (addr_list); } /* smtp_balance_inet_proto - balance IPv4/6 protocols within address limit */ static DNS_RR *smtp_balance_inet_proto(DNS_RR *addr_list, int misc_flags, int addr_limit) { const char myname[] = "smtp_balance_inet_proto"; DNS_RR *rr; DNS_RR *stripped_list; DNS_RR *next; int v6_count; int v4_count; int v6_target, v4_target; int *p; /* * Precondition: the input is sorted by MX preference (not necessarily IP * address family preference), and addresses with the same or worse * preference than 'myself' have been eliminated. Postcondition: the * relative list order is unchanged, but some elements are removed. */ /* * Ensure that dns_rr_append() won't interfere with the protocol * balancing goals. */ if (addr_limit > var_dns_rr_list_limit) addr_limit = var_dns_rr_list_limit; /* * Count the number of IPv6 and IPv4 addresses. */ for (v4_count = v6_count = 0, rr = addr_list; rr != 0; rr = rr->next) { if (rr->type == T_A) { v4_count++; } else if (rr->type == T_AAAA) { v6_count++; } else { msg_panic("%s: unexpected record type: %s", myname, dns_strtype(rr->type)); } } /* * Ensure that one address type will not out-crowd the other, while * enforcing the address count limit. This works around a current problem * where some destination announces primarily IPv6 MX addresses, the * smtp_address_limit eliminates most or all IPv4 addresses, and the * destination is not reachable over IPv6. * * Maybe: do all smtp_mx_address_limit enforcement here, and remove * pre-existing enforcement elsewhere. That would obsolete the * smtp_balance_inet_protocols configuration parameter. */ if (v4_count > 0 && v6_count > 0 && v4_count + v6_count > addr_limit) { /*- * Decide how many IPv6 and IPv4 addresses to keep. The code below * has three branches, corresponding to the regions R1, R2 and R3 * in the figure. * * L = addr_limit * X = excluded by condition (v4_count + v6_count > addr_limit) * * v4_count * ^ * | * L \ R1 * |X\ | * |XXX\ | * |XXXXX\ | R2 * L/2 +-------\------- * |XXXXXXX|X\ * |XXXXXXX|XXX\ R3 * |XXXXXXX|XXXXX\ * 0 +-------+-------\--> v6_count * 0 L/2 L */ if (v6_count <= addr_limit / 2) { /* Region R1 */ v6_target = v6_count; v4_target = addr_limit - v6_target; } else if (v4_count <= addr_limit / 2) {/* Region R3 */ v4_target = v4_count; v6_target = addr_limit - v4_target; } else { /* Region R2 */ /* v4_count > addr_limit / 2 && v6_count > addr_limit / 2 */ v4_target = (addr_limit + (addr_list->type == T_A)) / 2; v6_target = addr_limit - v4_target; } if (msg_verbose) msg_info("v6_target=%d, v4_target=%d", v6_target, v4_target); /* Enforce the address count targets. */ stripped_list = 0; for (rr = addr_list; rr != 0; rr = next) { next = rr->next; rr->next = 0; if (rr->type == T_A) { p = &v4_target; } else if (rr->type == T_AAAA) { p = &v6_target; } else { msg_panic("%s: unexpected record type: %s", myname, dns_strtype(rr->type)); } if (*p > 0) { stripped_list = dns_rr_append(stripped_list, rr); *p -= 1; } else { dns_rr_free(rr); } } if (v4_target > 0 || v6_target > 0) msg_panic("%s: bad target count: v4_target=%d, v6_target=%d", myname, v4_target, v6_target); if (msg_verbose) smtp_print_addr("smtp_balance_inet_proto result", stripped_list); return (stripped_list); } else { return (addr_list); } } /* smtp_domain_addr - mail exchanger address lookup */ DNS_RR *smtp_domain_addr(const char *name, DNS_RR **mxrr, int misc_flags, DSN_BUF *why, int *found_myself) { DNS_RR *mx_names; DNS_RR *addr_list = 0; DNS_RR *self = 0; unsigned best_pref; unsigned best_found; int r = 0; /* Resolver flags */ const char *aname; dsb_reset(why); /* Paranoia */ /* * Preferences from DNS use 0..32767, fall-backs use 32768+. */ #define IMPOSSIBLE_PREFERENCE (~0) /* * Sanity check. */ if (smtp_dns_support == SMTP_DNS_DISABLED) msg_panic("smtp_domain_addr: DNS lookup is disabled"); if (smtp_dns_support == SMTP_DNS_DNSSEC) r |= RES_USE_DNSSEC; /* * IDNA support. */ #ifndef NO_EAI if (!allascii(name) && (aname = midna_domain_to_ascii(name)) != 0) { if (msg_verbose) msg_info("%s asciified to %s", name, aname); } else #endif aname = name; /* * Look up the mail exchanger hosts listed for this name. Sort the * results by preference. Look up the corresponding host addresses, and * truncate the list so that it contains only hosts that are more * preferred than myself. When no MX resource records exist, look up the * addresses listed for this name. * * According to RFC 974: "It is possible that the list of MXs in the * response to the query will be empty. This is a special case. If the * list is empty, mailers should treat it as if it contained one RR, an * MX RR with a preference value of 0, and a host name of REMOTE. (I.e., * REMOTE is its only MX). In addition, the mailer should do no further * processing on the list, but should attempt to deliver the message to * REMOTE." * * Normally it is OK if an MX host cannot be found in the DNS; we'll just * use a backup one, and silently ignore the better MX host. However, if * the best backup that we can find in the DNS is the local machine, then * we must remember that the local machine is not the primary MX host, or * else we will claim that mail loops back. * * XXX Optionally do A lookups even when the MX lookup didn't complete. * Unfortunately with some DNS servers this is not a transient problem. * * XXX Ideally we would perform A lookups only as far as needed. But as long * as we're looking up all the hosts, it would be better to look up the * least preferred host first, so that DNS lookup error messages make * more sense. * * XXX 2821: RFC 2821 says that the sender must shuffle equal-preference MX * hosts, whereas multiple A records per hostname must be used in the * order as received. They make the bogus assumption that a hostname with * multiple A records corresponds to one machine with multiple network * interfaces. * * XXX 2821: Postfix recognizes the local machine by looking for its own IP * address in the list of mail exchangers. RFC 2821 says one has to look * at the mail exchanger hostname as well, making the bogus assumption * that an IP address is listed only under one hostname. However, looking * at hostnames provides a partial solution for MX hosts behind a NAT * gateway. */ switch (dns_lookup(aname, T_MX, r, &mx_names, (VSTRING *) 0, why->reason)) { default: dsb_status(why, "4.4.3"); if (var_ign_mx_lookup_err) addr_list = smtp_host_addr(aname, misc_flags, why); break; case DNS_INVAL: dsb_status(why, "5.4.4"); if (var_ign_mx_lookup_err) addr_list = smtp_host_addr(aname, misc_flags, why); break; case DNS_NULLMX: dsb_status(why, "5.1.0"); break; case DNS_POLICY: dsb_status(why, "4.7.0"); break; case DNS_FAIL: dsb_status(why, "5.4.3"); if (var_ign_mx_lookup_err) addr_list = smtp_host_addr(aname, misc_flags, why); break; case DNS_OK: mx_names = dns_rr_sort(mx_names, dns_rr_compare_pref_any); best_pref = (mx_names ? mx_names->pref : IMPOSSIBLE_PREFERENCE); addr_list = smtp_addr_list(mx_names, why); if (mxrr) *mxrr = dns_rr_copy(mx_names); /* copies one record! */ dns_rr_free(mx_names); if (addr_list == 0) { /* Text does not change. */ if (var_smtp_defer_mxaddr) { /* Don't clobber the null terminator. */ if (SMTP_HAS_HARD_DSN(why)) SMTP_SET_SOFT_DSN(why); /* XXX */ /* Require some error status. */ else if (!SMTP_HAS_SOFT_DSN(why)) msg_panic("smtp_domain_addr: bad status"); } msg_warn("no MX host for %s has a valid address record", name); break; } best_found = (addr_list ? addr_list->pref : IMPOSSIBLE_PREFERENCE); if (msg_verbose) smtp_print_addr(name, addr_list); if ((misc_flags & SMTP_MISC_FLAG_LOOP_DETECT) && (self = smtp_find_self(addr_list)) != 0) { addr_list = smtp_truncate_self(addr_list, self->pref); if (addr_list == 0) { if (best_pref != best_found) { dsb_simple(why, "4.4.4", "unable to find primary relay for %s", name); } else { dsb_simple(why, "5.4.6", "mail for %s loops back to myself", name); } } } #define SMTP_COMPARE_ADDR(flags) \ (((flags) & SMTP_MISC_FLAG_PREF_IPV6) ? dns_rr_compare_pref_ipv6 : \ ((flags) & SMTP_MISC_FLAG_PREF_IPV4) ? dns_rr_compare_pref_ipv4 : \ dns_rr_compare_pref_any) if (addr_list && addr_list->next) { if (var_smtp_rand_addr) addr_list = dns_rr_shuffle(addr_list); addr_list = dns_rr_sort(addr_list, SMTP_COMPARE_ADDR(misc_flags)); if (var_smtp_mxaddr_limit > 0 && var_smtp_balance_inet_proto) addr_list = smtp_balance_inet_proto(addr_list, misc_flags, var_smtp_mxaddr_limit); } break; case DNS_NOTFOUND: addr_list = smtp_host_addr(aname, misc_flags, why); break; } /* * Clean up. */ *found_myself |= (self != 0); return (addr_list); } /* smtp_host_addr - direct host lookup */ DNS_RR *smtp_host_addr(const char *host, int misc_flags, DSN_BUF *why) { DNS_RR *addr_list; int res_opt = 0; const char *ahost; dsb_reset(why); /* Paranoia */ if (smtp_dns_support == SMTP_DNS_DNSSEC) res_opt |= RES_USE_DNSSEC; /* * IDNA support. */ #ifndef NO_EAI if (!allascii(host) && (ahost = midna_domain_to_ascii(host)) != 0) { if (msg_verbose) msg_info("%s asciified to %s", host, ahost); } else #endif ahost = host; /* * If the host is specified by numerical address, just convert the * address to internal form. Otherwise, the host is specified by name. */ #define PREF0 0 addr_list = smtp_addr_one((DNS_RR *) 0, ahost, res_opt, PREF0, why); if (addr_list && (misc_flags & SMTP_MISC_FLAG_LOOP_DETECT) && smtp_find_self(addr_list) != 0) { dns_rr_free(addr_list); dsb_simple(why, "5.4.6", "mail for %s loops back to myself", host); return (0); } if (addr_list && addr_list->next) { if (var_smtp_rand_addr) addr_list = dns_rr_shuffle(addr_list); /* The following changes the order of equal-preference hosts. */ if (inet_proto_info()->ai_family_list[1] != 0) addr_list = dns_rr_sort(addr_list, SMTP_COMPARE_ADDR(misc_flags)); if (var_smtp_mxaddr_limit > 0 && var_smtp_balance_inet_proto) addr_list = smtp_balance_inet_proto(addr_list, misc_flags, var_smtp_mxaddr_limit); } if (msg_verbose) smtp_print_addr(host, addr_list); return (addr_list); }