Hi guys, I would like to propose a new feature to postfix. That feature is submission service lookup. This allows finer distribution of load for example when setting a relay. This patch allows you to set relayhost value in configuration to [_submission._tcp.domain] which causes postfix to look for SRV records when submitting mail for domain and send it to appropriate server according to records priority and weight as described in https://datatracker.ietf.org/doc/html/rfc6186 and https://datatracker.ietf.org/doc/html/rfc2782
Patch is applied to postfix-3.8-20220527 Thanks for any response.
commit e9347511aa714bc4b831a199e3807e604a695f00 Author: Tomas Korbar <tkor...@redhat.com> Date: Tue Jul 19 14:47:35 2022 +0200 Implement SRV record lookup in postfix diff --git a/src/dns/dns.h b/src/dns/dns.h index 5f53dbc..84f1f6b 100644 --- a/src/dns/dns.h +++ b/src/dns/dns.h @@ -158,7 +158,9 @@ typedef struct DNS_RR { unsigned short class; /* C_IN, etc. */ unsigned int ttl; /* always */ unsigned int dnssec_valid; /* DNSSEC validated */ - unsigned short pref; /* T_MX only */ + unsigned short pref; /* T_MX and T_SRV record related */ + unsigned short weight; /* T_SRV related, defined in rfc2782 */ + unsigned short port; /* T_SRV related, defined in rfc2782 */ struct DNS_RR *next; /* linkage */ size_t data_len; /* actual data size */ char data[1]; /* actually a bunch of data */ @@ -186,11 +188,13 @@ extern char *dns_strrecord(VSTRING *, DNS_RR *); extern DNS_RR *dns_rr_create(const char *, const char *, ushort, ushort, unsigned, unsigned, + unsigned, unsigned, const char *, size_t); extern void dns_rr_free(DNS_RR *); extern DNS_RR *dns_rr_copy(DNS_RR *); extern DNS_RR *dns_rr_append(DNS_RR *, DNS_RR *); extern DNS_RR *dns_rr_sort(DNS_RR *, int (*) (DNS_RR *, DNS_RR *)); +extern DNS_RR *dns_srv_rr_sort(DNS_RR *); extern int dns_rr_compare_pref_ipv6(DNS_RR *, DNS_RR *); extern int dns_rr_compare_pref_ipv4(DNS_RR *, DNS_RR *); extern int dns_rr_compare_pref_any(DNS_RR *, DNS_RR *); @@ -295,6 +299,7 @@ extern int dns_get_h_errno(void); * Below is the precedence order. The order between DNS_RETRY and DNS_NOTFOUND * is arbitrary. */ +#define DNS_NULLSRV (-8) /* query ok, submission service unavailable */ #define DNS_RECURSE (-7) /* internal only: recursion needed */ #define DNS_NOTFOUND (-6) /* query ok, data not found */ #define DNS_NULLMX (-5) /* query ok, service unavailable */ diff --git a/src/dns/dns_lookup.c b/src/dns/dns_lookup.c index 1c12a88..b7029a8 100644 --- a/src/dns/dns_lookup.c +++ b/src/dns/dns_lookup.c @@ -740,6 +740,8 @@ static int dns_get_rr(DNS_RR **list, const char *orig_name, DNS_REPLY *reply, int comp_len; ssize_t data_len; unsigned pref = 0; + unsigned weight = 0; + unsigned port = 0; unsigned char *src; unsigned char *dst; int ch; @@ -765,6 +767,18 @@ static int dns_get_rr(DNS_RR **list, const char *orig_name, DNS_REPLY *reply, return (DNS_INVAL); data_len = strlen(temp) + 1; break; + case T_SRV: + GETSHORT(pref, pos); + GETSHORT(weight, pos); + GETSHORT(port, pos); + if (dn_expand(reply->buf, reply->end, pos, temp, sizeof(temp)) < 0) + return (DNS_RETRY); + if (*temp == 0) + return (DNS_NULLSRV); + if (!valid_rr_name(temp, "resource data", fixed->type, reply)) + return (DNS_INVAL); + data_len = strlen(temp) + 1; + break; case T_MX: GETSHORT(pref, pos); if (dn_expand(reply->buf, reply->end, pos, temp, sizeof(temp)) < 0) @@ -860,7 +874,7 @@ static int dns_get_rr(DNS_RR **list, const char *orig_name, DNS_REPLY *reply, break; } *list = dns_rr_create(orig_name, rr_name, fixed->type, fixed->class, - fixed->ttl, pref, tempbuf, data_len); + fixed->ttl, pref, weight, port, tempbuf, data_len); return (DNS_OK); } @@ -960,7 +974,7 @@ static int dns_get_answer(const char *orig_name, DNS_REPLY *reply, int type, resource_found++; rr->dnssec_valid = *maybe_secure ? reply->dnssec_ad : 0; *rrlist = dns_rr_append(*rrlist, rr); - } else if (status == DNS_NULLMX) { + } else if (status == DNS_NULLMX || status == DNS_NULLSRV) { CORRUPT(status); /* TODO: use better name */ } else if (not_found_status != DNS_RETRY) not_found_status = status; @@ -1094,6 +1108,12 @@ int dns_lookup_x(const char *name, unsigned type, unsigned flags, name); DNS_SET_H_ERRNO(&dns_res_state, NO_DATA); return (status); + case DNS_NULLSRV: + if (why) + vstring_sprintf(why, "Domain %s does not accept mail submission (Service not supported)", + name); + DNS_SET_H_ERRNO(&dns_res_state, NO_DATA); + return (status); case DNS_OK: if (rrlist && dns_rr_filter_maps) { if (dns_rr_filter_execute(rrlist) < 0) { diff --git a/src/dns/dns_rr.c b/src/dns/dns_rr.c index b550788..cf51b8f 100644 --- a/src/dns/dns_rr.c +++ b/src/dns/dns_rr.c @@ -49,6 +49,14 @@ /* DNS_RR *dns_rr_remove(list, record) /* DNS_RR *list; /* DNS_RR *record; +/* +/* static void weight_order(array, count) +/* DNS_RR **array; +/* int count; +/* +/* DNS_RR *dns_srv_rr_sort(list) +/* DNS_RR *list; +/* /* DESCRIPTION /* The routines in this module maintain memory for DNS resource record /* information, and maintain lists of DNS resource records. @@ -81,6 +89,15 @@ /* dns_rr_remove() removes the specified record from the specified list. /* The updated list is the result value. /* The record MUST be a list member. +/* +/* weight_order() sorts the members of the array of dns records according +/* to their weight as described in RFC2782. Function sorts n members +/* according to the count argument. It is used internally by the +/* dns_srv_rr_sort function. +/* +/* dns_srv_rr_sort() Sorts list of dns SRV records according to their +/* priority and weight. +/* /* LICENSE /* .ad /* .fi @@ -113,6 +130,7 @@ DNS_RR *dns_rr_create(const char *qname, const char *rname, ushort type, ushort class, unsigned int ttl, unsigned pref, + unsigned weight, unsigned port, const char *data, size_t data_len) { DNS_RR *rr; @@ -125,6 +143,8 @@ DNS_RR *dns_rr_create(const char *qname, const char *rname, rr->ttl = ttl; rr->dnssec_valid = 0; rr->pref = pref; + rr->weight = weight; + rr->port = port; if (data && data_len > 0) memcpy(rr->data, data, data_len); rr->data_len = data_len; @@ -345,3 +365,127 @@ DNS_RR *dns_rr_remove(DNS_RR *list, DNS_RR *record) } return (list); } + +static void weight_order(DNS_RR **array, int count) { + // compute sum of weights + int weight_sum = 0; + for (int i = 0; i < count; i++) + weight_sum += array[i]->weight; + + // if weights are not supplied then we do not have to order records + if (weight_sum == 0) + return; + + // first move records with weight 0 to the beginning + int swap_place = 0; + DNS_RR *temp; + for (int i = 0; i < count; i++) { + if (array[i]->weight == 0) { + temp = array[swap_place]; + array[swap_place] = array[i]; + array[i] = temp; + swap_place++; + } + } + + int random; + + unsigned int running_sums[count]; + for (int i = 0; i < count-1; i++) { + running_sums[i] = array[i]->weight; + // calculate running sums of records + for (int x = i+1; x < count; x++) + running_sums[x] = array[x]->weight + running_sums[x-1]; + + random = myrand() % (weight_sum + 1); + + // find first record that has running sum greater or equal to + // the random number + for (int k = i; k < count; k++) { + if (running_sums[k] >= random) { + weight_sum -= array[k]->weight; + temp = array[i]; + array[i] = array[k]; + array[k] = temp; + break; + } + } + } +} + +/* dns_srv_rr_sort - sort resource record list */ + +DNS_RR *dns_srv_rr_sort(DNS_RR *list) { + int (*saved_user) (DNS_RR *, DNS_RR *); + DNS_RR **rr_array; + DNS_RR *rr; + int len; + int i; + int r; + + /* + * Save state and initialize. + */ + saved_user = dns_rr_sort_user; + dns_rr_sort_user = dns_rr_compare_pref_any; + + /* + * Build linear array with pointers to each list element. + */ + for (len = 0, rr = list; rr != 0; len++, rr = rr->next) + /* void */ ; + rr_array = (DNS_RR **) mymalloc(len * sizeof(*rr_array)); + for (len = 0, rr = list; rr != 0; len++, rr = rr->next) + rr_array[len] = rr; + + /* + * Shuffle resource records. Every element has an equal chance of landing + * in slot 0. After that every remaining element has an equal chance of + * landing in slot 1, ... This is exactly n! states for n! permutations. + */ + for (i = 0; i < len - 1; i++) { + r = i + (myrand() % (len - i)); /* Victor&Son */ + rr = rr_array[i]; + rr_array[i] = rr_array[r]; + rr_array[r] = rr; + } + + // first order the records by preference + qsort((void *) rr_array, len, sizeof(*rr_array), dns_rr_sort_callback); + + int cur_pref=rr_array[0]->pref; + int left_bound = 0; + int right_bound = 0; + + // walk through records and sort every partition with the same preference, + // according to their weight + for (int k = 1; k < len; k++) { + if (rr_array[k]->pref != cur_pref) { + if (left_bound != right_bound) { + weight_order(&rr_array[left_bound], right_bound - left_bound + 1); + } + cur_pref = rr_array[k]->pref; + left_bound = k; + } + right_bound = k; + } + // we need to check whether there is not one more partition to sort + if (left_bound != right_bound) { + weight_order(&rr_array[left_bound], right_bound - left_bound + 1); + } + + /* + * Fix the links. + */ + for (i = 0; i < len - 1; i++) + rr_array[i]->next = rr_array[i + 1]; + rr_array[i]->next = 0; + list = rr_array[0]; + + /* + * Cleanup. + */ + myfree((void *) rr_array); + dns_rr_sort_user = saved_user; + return (list); +} \ No newline at end of file diff --git a/src/dns/dns_sa_to_rr.c b/src/dns/dns_sa_to_rr.c index 6b9efcc..d7e6fee 100644 --- a/src/dns/dns_sa_to_rr.c +++ b/src/dns/dns_sa_to_rr.c @@ -55,12 +55,12 @@ DNS_RR *dns_sa_to_rr(const char *hostname, unsigned pref, struct sockaddr *sa) #define DUMMY_TTL 0 if (sa->sa_family == AF_INET) { - return (dns_rr_create(hostname, hostname, T_A, C_IN, DUMMY_TTL, pref, + return (dns_rr_create(hostname, hostname, T_A, C_IN, DUMMY_TTL, pref, 0, 0, (char *) &SOCK_ADDR_IN_ADDR(sa), sizeof(SOCK_ADDR_IN_ADDR(sa)))); #ifdef HAS_IPV6 } else if (sa->sa_family == AF_INET6) { - return (dns_rr_create(hostname, hostname, T_AAAA, C_IN, DUMMY_TTL, pref, + return (dns_rr_create(hostname, hostname, T_AAAA, C_IN, DUMMY_TTL, pref, 0, 0, (char *) &SOCK_ADDR_IN6_ADDR(sa), sizeof(SOCK_ADDR_IN6_ADDR(sa)))); #endif diff --git a/src/dns/dns_strtype.c b/src/dns/dns_strtype.c index 70e59ac..7eebe3c 100644 --- a/src/dns/dns_strtype.c +++ b/src/dns/dns_strtype.c @@ -180,6 +180,9 @@ static struct dns_type_map dns_type_map[] = { #ifdef T_ANY T_ANY, "ANY", #endif +#ifdef T_SRV + T_SRV, "SRV", +#endif }; /* dns_strtype - translate DNS query type to string */ diff --git a/src/smtp/smtp_addr.c b/src/smtp/smtp_addr.c index 2b5c126..9a7f07b 100644 --- a/src/smtp/smtp_addr.c +++ b/src/smtp/smtp_addr.c @@ -17,6 +17,12 @@ /* char *name; /* int misc_flags; /* DSN_BUF *why; +/* +/* DNS_RR *smtp_submission_addr(host, misc_flags, why) +/* const char *host; +/* 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). @@ -44,6 +50,9 @@ /* host. The host can be specified as a numerical Internet network /* address, or as a symbolic host name. /* +/* smtp_submission_addr() performs lookup for SRV records of domain +/* dedicated to submission service. +/* /* Results from smtp_domain_addr() or smtp_host_addr() are /* destroyed by dns_rr_free(), including null lists. /* DIAGNOSTICS @@ -130,7 +139,7 @@ static void smtp_print_addr(const char *what, DNS_RR *addr_list) /* 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) + unsigned pref, unsigned port, DSN_BUF *why) { const char *myname = "smtp_addr_one"; DNS_RR *addr = 0; @@ -174,8 +183,10 @@ static DNS_RR *smtp_addr_one(DNS_RR *addr_list, const char *host, int res_opt, why->reason, DNS_REQ_FLAG_NONE, proto_info->dns_atype_list)) { case DNS_OK: - for (rr = addr; rr; rr = rr->next) - rr->pref = pref; + for (rr = addr; rr; rr = rr->next) { + rr->pref = pref; + rr->port = port; + } addr_list = dns_rr_append(addr_list, addr); return (addr_list); default: @@ -293,10 +304,10 @@ static DNS_RR *smtp_addr_list(DNS_RR *mx_names, DSN_BUF *why) * tweaking the in-process resolver flags. */ for (rr = mx_names; rr; rr = rr->next) { - if (rr->type != T_MX) + if (rr->type != T_MX && rr->type != T_SRV) 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); + rr->pref, rr->port, why); } return (addr_list); } @@ -678,7 +689,7 @@ DNS_RR *smtp_host_addr(const char *host, int misc_flags, DSN_BUF *why) * 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); + addr_list = smtp_addr_one((DNS_RR *) 0, ahost, res_opt, PREF0, 0, why); if (addr_list && (misc_flags & SMTP_MISC_FLAG_LOOP_DETECT) && smtp_find_self(addr_list) != 0) { @@ -700,3 +711,52 @@ DNS_RR *smtp_host_addr(const char *host, int misc_flags, DSN_BUF *why) smtp_print_addr(host, addr_list); return (addr_list); } + +/* smtp_submission_address - submission service lookup */ + +DNS_RR *smtp_submission_addr(const char *host, int misc_flags, DSN_BUF *why) { + DNS_RR *srv_names = 0; + DNS_RR *addr_list = 0; + int r = 0; + + dsb_reset(why); + + if (smtp_dns_support == SMTP_DNS_DNSSEC) { + r |= RES_USE_DNSSEC; + } + + switch (dns_lookup(host, T_SRV, r, &srv_names, (VSTRING *)0, why->reason)) { + case DNS_INVAL: + dsb_status(why, "5.4.4"); + break; + case DNS_POLICY: + dsb_status(why, "4.7.0"); + break; + case DNS_FAIL: + dsb_status(why, "5.4.3"); + break; + case DNS_NULLSRV: + dsb_status(why, "5.1.0"); + break; + case DNS_OK: + /* At first we must sort the rr records according to their priority */ + srv_names = dns_srv_rr_sort(srv_names); + addr_list = smtp_addr_list(srv_names, why); + dns_rr_free(srv_names); + if (addr_list == 0) { + msg_warn("no SRV host for %s has a valid address record", host); + break; + } + if (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); + } + break; + default: + dsb_status(why, "4.4.3"); + break; + } + + return (addr_list); +} \ No newline at end of file diff --git a/src/smtp/smtp_addr.h b/src/smtp/smtp_addr.h index 8f20961..751aca2 100644 --- a/src/smtp/smtp_addr.h +++ b/src/smtp/smtp_addr.h @@ -18,6 +18,7 @@ */ extern DNS_RR *smtp_host_addr(const char *, int, DSN_BUF *); extern DNS_RR *smtp_domain_addr(const char *, DNS_RR **, int, DSN_BUF *, int *); +extern DNS_RR *smtp_submission_addr(const char *, int, DSN_BUF *); /* LICENSE /* .ad diff --git a/src/smtp/smtp_connect.c b/src/smtp/smtp_connect.c index ed58180..57da12d 100644 --- a/src/smtp/smtp_connect.c +++ b/src/smtp/smtp_connect.c @@ -30,7 +30,11 @@ /* /* With SMTP, the Internet domain name service is queried for mail /* exchanger hosts. Quote the domain name with `[' and `]' to -/* suppress mail exchanger lookups. +/* suppress mail exchanger lookups. Another feature is that +/* _submission._tcp prefix in `[]' brackets triggers lookup for +/* submission service SRV records and picking appropriate submission +/* server according to SRV records priority and weight +/* (eg. [_submission._tcp.domain]). /* /* Numerical address information should always be quoted with `[]'. /* DIAGNOSTICS @@ -180,13 +184,24 @@ static SMTP_SESSION *smtp_connect_addr(SMTP_ITERATOR *iter, DSN_BUF *why, SOCKADDR_SIZE salen = sizeof(ss); MAI_HOSTADDR_STR hostaddr; DNS_RR *addr = iter->rr; - unsigned port = iter->port; + unsigned port; int sock; char *bind_addr; char *bind_var; char *saved_bind_addr = 0; char *tail; + /* + * if we have port number that is retrieved from SRV record + * then change port number in iterator too + */ + if (iter->rr->port != 0) { + port = htons(iter->rr->port); + iter->port = port; + } else { + port = iter->port; + } + dsb_reset(why); /* Paranoia */ /* @@ -315,7 +330,7 @@ static SMTP_SESSION *smtp_connect_sock(int sock, struct sockaddr *sa, time_t start_time; const char *name = STR(iter->host); const char *addr = STR(iter->addr); - unsigned port = iter->port; + unsigned port = iter->rr->port == 0 ? iter->port : htons(iter->rr->port); start_time = time((time_t *) 0); if (var_smtp_conn_tmout > 0) { @@ -851,6 +866,7 @@ static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, int sess_count; SMTP_SESSION *session; int lookup_mx; + int lookup_srv; unsigned domain_best_pref; MAI_HOSTADDR_STR hostaddr; @@ -878,8 +894,14 @@ static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, * exchanger lookups when a quoted host is specified or when DNS * lookups are disabled. */ - if (msg_verbose) - msg_info("connecting to %s port %d", domain, ntohs(port)); + lookup_srv = strncmp("[_submission._tcp", dest, 17) == 0 ? 1 : 0; + if (msg_verbose) { + if (lookup_srv) { + msg_info("connecting to %s on port determined by SRV record", domain); + } else { + msg_info("connecting to %s port %d", domain, ntohs(port)); + } + } if (smtp_mode) { if (ntohs(port) == IPPORT_SMTP) state->misc_flags |= SMTP_MISC_FLAG_LOOP_DETECT; @@ -889,8 +911,13 @@ static void smtp_connect_inet(SMTP_STATE *state, const char *nexthop, } else lookup_mx = 0; if (!lookup_mx) { - addr_list = smtp_host_addr(domain, state->misc_flags, why); - /* XXX We could be an MX host for this destination... */ + if (lookup_srv) { + /* Proceed with submission service lookup (look for SRV record)*/ + addr_list = smtp_submission_addr(domain, state->misc_flags, why); + } else { + addr_list = smtp_host_addr(domain, state->misc_flags, why); + /* XXX We could be an MX host for this destination... */ + } } else { int i_am_mx = 0; diff --git a/src/smtpd/smtpd_check.c b/src/smtpd/smtpd_check.c index 2785ce1..274cbb2 100644 --- a/src/smtpd/smtpd_check.c +++ b/src/smtpd/smtpd_check.c @@ -3064,7 +3064,7 @@ static int check_server_access(SMTPD_STATE *state, const char *table, || type == T_AAAA #endif ) { - server_list = dns_rr_create(domain, domain, T_MX, C_IN, 0, 0, + server_list = dns_rr_create(domain, domain, T_MX, C_IN, 0, 0, 0, 0, domain, strlen(domain) + 1); } else { dns_status = dns_lookup(domain, type, 0, &server_list, @@ -3073,7 +3073,7 @@ static int check_server_access(SMTPD_STATE *state, const char *table, return (SMTPD_CHECK_DUNNO); if (dns_status == DNS_NOTFOUND /* Not: h_errno == NO_DATA */ ) { if (type == T_MX) { - server_list = dns_rr_create(domain, domain, type, C_IN, 0, 0, + server_list = dns_rr_create(domain, domain, type, C_IN, 0, 0, 0, 0, domain, strlen(domain) + 1); dns_status = DNS_OK; } else if (type == T_NS /* && h_errno == NO_DATA */ ) {