From 8c51eead714a52b81651352b5db4b985d17c3148 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Sun, 20 Apr 2014 16:44:52 +0100 Subject: [PATCH] Add options dnssec_request_domains, dnssec_require_domains to the dnslookup router Note there are no testsuite cases included. TODO in this area: - dnssec during verify-callouts - dnssec during dnsdb expansions - dnssec on the forward lookup of a verify=helo and verify=reverse_host_lookup - observability of status of requested dnssec --- doc/doc-docbook/spec.xfpt | 23 ++++++ doc/doc-txt/ChangeLog | 3 + src/src/dns.c | 10 ++- src/src/functions.h | 4 +- src/src/host.c | 112 ++++++++++++++++++++------- src/src/lookups/dnsdb.c | 2 +- src/src/match.c | 2 + src/src/routers/dnslookup.c | 12 ++- src/src/routers/dnslookup.h | 2 + src/src/routers/rf_lookup_hostlist.c | 6 +- src/src/transports/smtp.c | 1 + src/src/verify.c | 3 +- 12 files changed, 143 insertions(+), 37 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 86090290b..68ebf8ad6 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -17622,6 +17622,29 @@ when there is a DNS lookup error. +.option dnssec_request_domains dnslookup "domain list&!!" unset +.cindex "MX record" "security" +.cindex "DNSSEC" "MX lookup" +.cindex "security" "MX lookup" +.cindex "DNS" "DNSSEC" +DNS lookups for domains matching &%dnssec_request_domains%& will be done with +the dnssec request bit set. +This applies to all of the SRV, MX A6, AAAA, A lookup sequence. + + + +.option dnssec_require_domains dnslookup "domain list&!!" unset +.cindex "MX record" "security" +.cindex "DNSSEC" "MX lookup" +.cindex "security" "MX lookup" +.cindex "DNS" "DNSSEC" +DNS lookups for domains matching &%dnssec_request_domains%& will be done with +the dnssec request bit set. Any returns not having the Authenticated Data bit +(AD bit) set wil be ignored and logged as a host-lookup failure. +This applies to all of the SRV, MX A6, AAAA, A lookup sequence. + + + .option mx_domains dnslookup "domain list&!!" unset .cindex "MX record" "required to exist" .cindex "SRV record" "required to exist" diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index 56ff713cb..6252956a6 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -84,6 +84,9 @@ TL/07 Add new dmarc expansion variable $dmarc_domain_policy to directly JH/13 Fix handling of $tls_cipher et.al. in (non-verify) transport. Bug 1455. +JH/14 New options dnssec_request_domains, dnssec_require_domains on the + dnslookup router (applying to the forward lookup). + Exim version 4.82 ----------------- diff --git a/src/src/dns.c b/src/src/dns.c index 185522e58..a15eb033a 100644 --- a/src/src/dns.c +++ b/src/src/dns.c @@ -159,12 +159,13 @@ the first time we have been here, and set the resolver options. Arguments: qualify_single TRUE to set the RES_DEFNAMES option search_parents TRUE to set the RES_DNSRCH option + use_dnssec TRUE to set the RES_USE_DNSSEC option Returns: nothing */ void -dns_init(BOOL qualify_single, BOOL search_parents) +dns_init(BOOL qualify_single, BOOL search_parents, BOOL use_dnssec) { res_state resp = os_get_dns_resolver_res(); @@ -206,6 +207,8 @@ if (dns_use_edns0 >= 0) # ifndef RES_USE_EDNS0 # error Have RES_USE_DNSSEC but not RES_USE_EDNS0? Something hinky ... # endif +if (use_dnssec) + resp->options |= RES_USE_DNSSEC; if (dns_dnssec_ok >= 0) { if (dns_use_edns0 == 0 && dns_dnssec_ok != 0) @@ -228,6 +231,9 @@ if (dns_dnssec_ok >= 0) DEBUG(D_resolver) debug_printf("Unable to %sset DNSSEC without resolver support.\n", dns_dnssec_ok ? "" : "un"); +if (use_dnssec) + DEBUG(D_resolver) + debug_printf("Unable to set DNSSEC without resolver support.\n") # endif #endif /* DISABLE_DNSSEC */ @@ -1249,4 +1255,6 @@ else return yield; } +/* vi: aw ai sw=2 +*/ /* End of dns.c */ diff --git a/src/src/functions.h b/src/src/functions.h index be71345a1..599afd206 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -115,7 +115,7 @@ extern BOOL dkim_transport_write_message(address_item *, int, int, #endif extern dns_address *dns_address_from_rr(dns_answer *, dns_record *); extern void dns_build_reverse(uschar *, uschar *); -extern void dns_init(BOOL, BOOL); +extern void dns_init(BOOL, BOOL, BOOL); extern int dns_basic_lookup(dns_answer *, uschar *, int); extern BOOL dns_is_secure(dns_answer *); extern int dns_lookup(dns_answer *, uschar *, int, uschar **); @@ -157,7 +157,7 @@ extern void host_build_log_info(void); extern void host_build_sender_fullhost(void); extern BOOL host_find_byname(host_item *, uschar *, int, uschar **, BOOL); extern int host_find_bydns(host_item *, uschar *, int, uschar *, uschar *, - uschar *,uschar **, BOOL *); + uschar *, uschar *, uschar *, uschar **, BOOL *); extern ip_address_item *host_find_interfaces(void); extern BOOL host_is_in_net(uschar *, uschar *, int); extern BOOL host_is_tls_on_connect_port(int); diff --git a/src/src/host.c b/src/src/host.c index 785eea412..495a44d58 100644 --- a/src/src/host.c +++ b/src/src/host.c @@ -1622,7 +1622,7 @@ while ((ordername = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) { if (strcmpic(ordername, US"bydns") == 0) { - dns_init(FALSE, FALSE); + dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */ dns_build_reverse(sender_host_address, buffer); rc = dns_lookup(&dnsa, buffer, T_PTR, NULL); @@ -1919,7 +1919,8 @@ if (running_in_test_harness) some circumstances when the get..byname() function actually calls the DNS. */ dns_init((flags & HOST_FIND_QUALIFY_SINGLE) != 0, - (flags & HOST_FIND_SEARCH_PARENTS) != 0); + (flags & HOST_FIND_SEARCH_PARENTS) != 0, + FALSE); /*XXX dnssec? */ /* In an IPv6 world, unless IPv6 has been disabled, we need to scan for both kinds of address, so go round the loop twice. Note that we have ensured that @@ -2195,6 +2196,7 @@ Arguments: fully_qualified_name if not NULL, return fully qualified name here if the contents are different (i.e. it must be preset to something) + dnnssec_require if TRUE check the DNS result AD bit Returns: HOST_FIND_FAILED couldn't find A record HOST_FIND_AGAIN try again later @@ -2204,7 +2206,8 @@ Returns: HOST_FIND_FAILED couldn't find A record static int set_address_from_dns(host_item *host, host_item **lastptr, - uschar *ignore_target_hosts, BOOL allow_ip, uschar **fully_qualified_name) + uschar *ignore_target_hosts, BOOL allow_ip, uschar **fully_qualified_name, + BOOL dnssec_require) { dns_record *rr; host_item *thishostlast = NULL; /* Indicates not yet filled in anything */ @@ -2287,6 +2290,12 @@ for (; i >= 0; i--) if (rc != DNS_NOMATCH && rc != DNS_NODATA) v6_find_again = TRUE; continue; } + if (dnssec_require && !dns_is_secure(&dnsa)) + { + log_write(L_host_lookup_failed, LOG_MAIN, "dnssec fail on %s for %.256s", + i>1 ? "A6" : i>0 ? "AAAA" : "A", host->name); + continue; + } /* Lookup succeeded: fill in the given host item with the first non-ignored address found; create additional items for any others. A single A6 record @@ -2433,6 +2442,8 @@ Arguments: srv_service when SRV used, the service name srv_fail_domains DNS errors for these domains => assume nonexist mx_fail_domains DNS errors for these domains => assume nonexist + dnssec_request_domains => make dnssec request + dnssec_require_domains => ditto and nonexist failures fully_qualified_name if not NULL, return fully-qualified name removed set TRUE if local host was removed from the list @@ -2450,6 +2461,7 @@ Returns: HOST_FIND_FAILED Failed to find the host or domain; int host_find_bydns(host_item *host, uschar *ignore_target_hosts, int whichrrs, uschar *srv_service, uschar *srv_fail_domains, uschar *mx_fail_domains, + uschar *dnssec_request_domains, uschar *dnssec_require_domains, uschar **fully_qualified_name, BOOL *removed) { host_item *h, *last; @@ -2459,6 +2471,10 @@ int ind_type = 0; int yield; dns_answer dnsa; dns_scan dnss; +BOOL dnssec_request = match_isinlist(host->name, &dnssec_request_domains, + 0, NULL, NULL, MCL_DOMAIN, TRUE, NULL) == OK; +BOOL dnssec_require = match_isinlist(host->name, &dnssec_require_domains, + 0, NULL, NULL, MCL_DOMAIN, TRUE, NULL) == OK; /* Set the default fully qualified name to the incoming name, initialize the resolver if necessary, set up the relevant options, and initialize the flag @@ -2466,7 +2482,9 @@ that gets set for DNS syntax check errors. */ if (fully_qualified_name != NULL) *fully_qualified_name = host->name; dns_init((whichrrs & HOST_FIND_QUALIFY_SINGLE) != 0, - (whichrrs & HOST_FIND_SEARCH_PARENTS) != 0); + (whichrrs & HOST_FIND_SEARCH_PARENTS) != 0, + dnssec_request || dnssec_require + ); host_find_failed_syntax = FALSE; /* First, if requested, look for SRV records. The service name is given; we @@ -2494,13 +2512,19 @@ if ((whichrrs & HOST_FIND_BY_SRV) != 0) /* On DNS failures, we give the "try again" error unless the domain is listed as one for which we continue. */ + if (rc == DNS_SUCCEED && dnssec_require && !dns_is_secure(&dnsa)) + { + log_write(L_host_lookup_failed, LOG_MAIN, + "dnssec fail on SRV for %.256s", host->name); + rc = DNS_FAIL; + } if (rc == DNS_FAIL || rc == DNS_AGAIN) { #ifndef STAND_ALONE if (match_isinlist(host->name, &srv_fail_domains, 0, NULL, NULL, MCL_DOMAIN, TRUE, NULL) != OK) #endif - return HOST_FIND_AGAIN; + { yield = HOST_FIND_AGAIN; goto out; } DEBUG(D_host_lookup) debug_printf("DNS_%s treated as DNS_NODATA " "(domain in srv_fail_domains)\n", (rc == DNS_FAIL)? "FAIL":"AGAIN"); } @@ -2517,16 +2541,29 @@ if (rc != DNS_SUCCEED && (whichrrs & HOST_FIND_BY_MX) != 0) { ind_type = T_MX; rc = dns_lookup(&dnsa, host->name, ind_type, fully_qualified_name); - if (rc == DNS_NOMATCH) return HOST_FIND_FAILED; - if (rc == DNS_FAIL || rc == DNS_AGAIN) - { - #ifndef STAND_ALONE - if (match_isinlist(host->name, &mx_fail_domains, 0, NULL, NULL, MCL_DOMAIN, - TRUE, NULL) != OK) - #endif - return HOST_FIND_AGAIN; - DEBUG(D_host_lookup) debug_printf("DNS_%s treated as DNS_NODATA " - "(domain in mx_fail_domains)\n", (rc == DNS_FAIL)? "FAIL":"AGAIN"); + switch (rc) + { + case DNS_NOMATCH: + yield = HOST_FIND_FAILED; goto out; + + case DNS_SUCCEED: + if (!dnssec_require || dns_is_secure(&dnsa)) + break; + log_write(L_host_lookup_failed, LOG_MAIN, + "dnssec fail on MX for %.256s", host->name); + rc = DNS_FAIL; + /*FALLTRHOUGH*/ + + case DNS_FAIL: + case DNS_AGAIN: + #ifndef STAND_ALONE + if (match_isinlist(host->name, &mx_fail_domains, 0, NULL, NULL, MCL_DOMAIN, + TRUE, NULL) != OK) + #endif + { yield = HOST_FIND_AGAIN; goto out; } + DEBUG(D_host_lookup) debug_printf("DNS_%s treated as DNS_NODATA " + "(domain in mx_fail_domains)\n", (rc == DNS_FAIL)? "FAIL":"AGAIN"); + break; } } @@ -2539,14 +2576,15 @@ if (rc != DNS_SUCCEED) if ((whichrrs & HOST_FIND_BY_A) == 0) { DEBUG(D_host_lookup) debug_printf("Address records are not being sought\n"); - return HOST_FIND_FAILED; + yield = HOST_FIND_FAILED; + goto out; } last = host; /* End of local chainlet */ host->mx = MX_NONE; host->port = PORT_NONE; rc = set_address_from_dns(host, &last, ignore_target_hosts, FALSE, - fully_qualified_name); + fully_qualified_name, dnssec_require); /* If one or more address records have been found, check that none of them are local. Since we know the host items all have their IP addresses @@ -2573,7 +2611,8 @@ if (rc != DNS_SUCCEED) } } - return rc; + yield = rc; + goto out; } /* We have found one or more MX or SRV records. Sort them according to @@ -2757,7 +2796,8 @@ if (ind_type == T_SRV) if (host == last && host->name[0] == 0) { DEBUG(D_host_lookup) debug_printf("the single SRV record is \".\"\n"); - return HOST_FIND_FAILED; + yield = HOST_FIND_FAILED; + goto out; } DEBUG(D_host_lookup) @@ -2867,12 +2907,14 @@ otherwise invalid host names obtained from MX or SRV records can cause trouble if they happen to match something local. */ yield = HOST_FIND_FAILED; /* Default yield */ -dns_init(FALSE, FALSE); /* Disable qualify_single and search_parents */ +dns_init(FALSE, FALSE, /* Disable qualify_single and search_parents */ + dnssec_request || dnssec_require); for (h = host; h != last->next; h = h->next) { if (h->address != NULL) continue; /* Inserted by a multihomed host */ - rc = set_address_from_dns(h, &last, ignore_target_hosts, allow_mx_to_ip, NULL); + rc = set_address_from_dns(h, &last, ignore_target_hosts, allow_mx_to_ip, + NULL, dnssec_require); if (rc != HOST_FOUND) { h->status = hstatus_unusable; @@ -2981,6 +3023,9 @@ DEBUG(D_host_lookup) } } +out: + +dns_init(FALSE, FALSE, FALSE); /* clear the dnssec bit for getaddrbyname */ return yield; } @@ -3002,6 +3047,8 @@ int whichrrs = HOST_FIND_BY_MX | HOST_FIND_BY_A; BOOL byname = FALSE; BOOL qualify_single = TRUE; BOOL search_parents = FALSE; +BOOL request_dnssec = FALSE; +BOOL require_dnssec = FALSE; uschar **argv = USS cargv; uschar buffer[256]; @@ -3021,7 +3068,7 @@ if (argc > 1) primary_hostname = argv[1]; /* So that debug level changes can be done first */ -dns_init(qualify_single, search_parents); +dns_init(qualify_single, search_parents, FALSE); printf("Testing host lookup\n"); printf("> "); @@ -3047,10 +3094,14 @@ while (Ufgets(buffer, 256, stdin) != NULL) whichrrs = HOST_FIND_BY_SRV | HOST_FIND_BY_MX; else if (Ustrcmp(buffer, "srv+mx+a") == 0) whichrrs = HOST_FIND_BY_SRV | HOST_FIND_BY_MX | HOST_FIND_BY_A; - else if (Ustrcmp(buffer, "qualify_single") == 0) qualify_single = TRUE; + else if (Ustrcmp(buffer, "qualify_single") == 0) qualify_single = TRUE; else if (Ustrcmp(buffer, "no_qualify_single") == 0) qualify_single = FALSE; - else if (Ustrcmp(buffer, "search_parents") == 0) search_parents = TRUE; + else if (Ustrcmp(buffer, "search_parents") == 0) search_parents = TRUE; else if (Ustrcmp(buffer, "no_search_parents") == 0) search_parents = FALSE; + else if (Ustrcmp(buffer, "request_dnssec") == 0) request_dnssec = TRUE; + else if (Ustrcmp(buffer, "no_request_dnssec") == 0) request_dnssec = FALSE; + else if (Ustrcmp(buffer, "require_dnssec") == 0) require_dnssec = TRUE; + else if (Ustrcmp(buffer, "no_reqiret_dnssec") == 0) require_dnssec = FALSE; else if (Ustrcmp(buffer, "test_harness") == 0) running_in_test_harness = !running_in_test_harness; else if (Ustrcmp(buffer, "ipv6") == 0) disable_ipv6 = !disable_ipv6; @@ -3083,11 +3134,12 @@ while (Ufgets(buffer, 256, stdin) != NULL) if (qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; if (search_parents) flags |= HOST_FIND_SEARCH_PARENTS; - rc = byname? - host_find_byname(&h, NULL, flags, &fully_qualified_name, TRUE) - : - host_find_bydns(&h, NULL, flags, US"smtp", NULL, NULL, - &fully_qualified_name, NULL); + rc = byname + ? host_find_byname(&h, NULL, flags, &fully_qualified_name, TRUE) + : host_find_bydns(&h, NULL, flags, US"smtp", NULL, NULL, + request_dnssec ? &h.name : NULL, + require_dnssec ? &h.name : NULL, + &fully_qualified_name, NULL); if (rc == HOST_FIND_FAILED) printf("Failed\n"); else if (rc == HOST_FIND_AGAIN) printf("Again\n"); @@ -3146,4 +3198,6 @@ return 0; } #endif /* STAND_ALONE */ +/* vi: aw ai sw=2 +*/ /* End of host.c */ diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c index e2e0f7f0d..a1eb2b658 100644 --- a/src/src/lookups/dnsdb.c +++ b/src/src/lookups/dnsdb.c @@ -241,7 +241,7 @@ if ((equals = Ustrchr(keystring, '=')) != NULL) /* Initialize the resolver in case this is the first time it has been used. */ -dns_init(FALSE, FALSE); +dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */ /* The remainder of the string must be a list of domains. As long as the lookup for at least one of them succeeds, we return success. Failure means that none diff --git a/src/src/match.c b/src/src/match.c index 66ae3dddb..97a098205 100644 --- a/src/src/match.c +++ b/src/src/match.c @@ -221,6 +221,8 @@ if (cb->at_is_special && pattern[0] == '@') NULL, /* service name not relevant */ NULL, /* srv_fail_domains not relevant */ NULL, /* mx_fail_domains not relevant */ + NULL, /* no dnssec request XXX ? */ + NULL, /* no dnssec require XXX ? */ NULL, /* no feedback FQDN */ &removed); /* feedback if local removed */ diff --git a/src/src/routers/dnslookup.c b/src/src/routers/dnslookup.c index 057a2a15d..c8fd3f991 100644 --- a/src/src/routers/dnslookup.c +++ b/src/src/routers/dnslookup.c @@ -18,6 +18,10 @@ optionlist dnslookup_router_options[] = { (void *)(offsetof(dnslookup_router_options_block, check_secondary_mx)) }, { "check_srv", opt_stringptr, (void *)(offsetof(dnslookup_router_options_block, check_srv)) }, + { "dnssec_request_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, dnssec_request_domains)) }, + { "dnssec_require_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, dnssec_require_domains)) }, { "mx_domains", opt_stringptr, (void *)(offsetof(dnslookup_router_options_block, mx_domains)) }, { "mx_fail_domains", opt_stringptr, @@ -53,7 +57,9 @@ dnslookup_router_options_block dnslookup_router_option_defaults = { NULL, /* mx_domains */ NULL, /* mx_fail_domains */ NULL, /* srv_fail_domains */ - NULL /* check_srv */ + NULL, /* check_srv */ + NULL, /* dnssec_request_domains */ + NULL /* dnssec_require_domains */ }; @@ -261,7 +267,9 @@ for (;;) } rc = host_find_bydns(&h, rblock->ignore_target_hosts, flags, srv_service, - ob->srv_fail_domains, ob->mx_fail_domains, &fully_qualified_name, &removed); + ob->srv_fail_domains, ob->mx_fail_domains, + ob->dnssec_request_domains, ob->dnssec_require_domains, + &fully_qualified_name, &removed); if (removed) setflag(addr, af_local_host_removed); /* If host found with only address records, test for the domain's being in diff --git a/src/src/routers/dnslookup.h b/src/src/routers/dnslookup.h index b0c384367..518b7f478 100644 --- a/src/src/routers/dnslookup.h +++ b/src/src/routers/dnslookup.h @@ -17,6 +17,8 @@ typedef struct { uschar *mx_fail_domains; uschar *srv_fail_domains; uschar *check_srv; + uschar *dnssec_request_domains; + uschar *dnssec_require_domains; } dnslookup_router_options_block; /* Data for reading the private options. */ diff --git a/src/src/routers/rf_lookup_hostlist.c b/src/src/routers/rf_lookup_hostlist.c index eadcd5df7..0eae31e61 100644 --- a/src/src/routers/rf_lookup_hostlist.c +++ b/src/src/routers/rf_lookup_hostlist.c @@ -94,6 +94,8 @@ for (h = addr->host_list; h != NULL; h = next_h) NULL, /* SRV service not relevant */ NULL, /* failing srv domains not relevant */ NULL, /* no special mx failing domains */ + NULL, /* no dnssec request XXX ? */ + NULL, /* no dnssec require XXX ? */ NULL, /* fully_qualified_name */ NULL); /* indicate local host removed */ } @@ -117,7 +119,9 @@ for (h = addr->host_list; h != NULL; h = next_h) BOOL removed; DEBUG(D_route|D_host_lookup) debug_printf("doing DNS lookup\n"); rc = host_find_bydns(h, ignore_target_hosts, HOST_FIND_BY_A, NULL, NULL, - NULL, &canonical_name, &removed); + NULL, + NULL, NULL, /*XXX dnssec? */ + &canonical_name, &removed); if (rc == HOST_FOUND) { if (removed) setflag(addr, af_local_host_removed); diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index 0aa95a448..57b66b881 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -2816,6 +2816,7 @@ for (cutoff_retry = 0; expired && rc = host_find_byname(host, NULL, flags, &canonical_name, TRUE); else rc = host_find_bydns(host, NULL, flags, NULL, NULL, NULL, + NULL, NULL, /*XXX todo: smtp tpt hosts_require_dnssec */ &canonical_name, NULL); /* Update the host (and any additional blocks, resulting from diff --git a/src/src/verify.c b/src/src/verify.c index 690bb8f01..f799ff1de 100644 --- a/src/src/verify.c +++ b/src/src/verify.c @@ -1750,6 +1750,7 @@ while (addr_new != NULL) (void)host_find_byname(host, NULL, flags, &canonical_name, TRUE); else (void)host_find_bydns(host, NULL, flags, NULL, NULL, NULL, + NULL, NULL, /*XXX todo: dnssec */ &canonical_name, NULL); } } @@ -3546,7 +3547,7 @@ revadd[0] = 0; /* In case this is the first time the DNS resolver is being used. */ -dns_init(FALSE, FALSE); +dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */ /* Loop through all the domains supplied, until something matches */ -- 2.25.1