From: Jeremy Harris Date: Wed, 28 Aug 2024 21:01:24 +0000 (+0100) Subject: dmarc dynamic module X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/7dc8d146a675f52b441310e731314d86c66b2114?ds=inline dmarc dynamic module --- diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 428cbc079..90fb6dd47 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -32055,12 +32055,20 @@ This control turns off DKIM verification processing entirely. For details on the operation and configuration of DKIM, see section &<>&. -.vitem &*control&~=&~dmarc_disable_verify*& +.vitem &*control&~=&~enforce_sync*& &&& + &*control&~=&~no_enforce_sync*& + +.vitem &*control&~=&~dmarc_disable_verify*& &&& + &*control&~=&~dmarc_enable_forensic*& .cindex "disable DMARC verify" -.cindex "DMARC" "disable verify" -This control turns off DMARC verification processing entirely. For details on +.cindex DMARC "disable verify" +.cindex DMARC controls +.cindex DMARC "forensic mails" +These control affect DMARC processing. For details on the operation and configuration of DMARC, see section &<>&. +The &"disable"& turns off DMARC verification processing entirely. + .vitem &*control&~=&~dscp/*&<&'value'&> .cindex "&ACL;" "setting DSCP value" diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base index caae1e536..43cec361c 100644 --- a/src/OS/Makefile-Base +++ b/src/OS/Makefile-Base @@ -499,7 +499,6 @@ OBJ_EXPERIMENTAL = arc.o \ bmi_spam.o \ dane.o \ dcc.o \ - dmarc.o \ imap_utf7.o \ utf8.o \ xclient.o @@ -901,7 +900,6 @@ arc.o: $(HDRS) pdkim/pdkim.h arc.c bmi_spam.o: $(HDRS) bmi_spam.c dane.o: $(HDRS) dane.c dane-openssl.c dcc.o: $(HDRS) dcc.h dcc.c -dmarc.o: $(HDRS) pdkim/pdkim.h dmarc.h dmarc.c imap_utf7.o: $(HDRS) imap_utf7.c utf8.o: $(HDRS) utf8.c xclient.o: $(HDRS) xclient.c diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile index af0de26e4..2b8a9bcb5 100755 --- a/src/scripts/Configure-Makefile +++ b/src/scripts/Configure-Makefile @@ -29,6 +29,16 @@ fi archtype=`../scripts/arch-type` || exit 1 +# Linux now whines about egrep, saying "use grep -E". +# Solarix doesn't support -E on grep. Thanks so much for +# going non-back-compatible, Linux. +if echo 1 | grep -E 1 >/dev/null; then + egrep="grep -E" +else + egrep="egrep" +fi + + # Now test for either the non-existence of Makefile, or for any of its # components being newer. Note that the "newer" script gives the right # answer (for our purposes) when the first file is non-existent. @@ -311,7 +321,7 @@ done <<-END routers ROUTER ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT transports TRANSPORT APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP auths AUTH CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS - miscmods SUPPORT SPF + miscmods SUPPORT SPF DMARC END # See if there is a definition of EXIM_PERL in what we have built so far. diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks index 09d18b63c..e45097243 100755 --- a/src/scripts/MakeLinks +++ b/src/scripts/MakeLinks @@ -94,7 +94,7 @@ d="miscmods" mkdir $d cd $d # Makefile is generated -for f in spf.c spf.h +for f in dmarc.c dmarc.h spf.c spf.h do ln -s ../../src/$d/$f $f done @@ -140,7 +140,7 @@ for f in blob.h dbfunctions.h exim.h functions.h globals.h \ string.c tls.c tlscert-gnu.c tlscert-openssl.c tls-cipher-stdname.c \ tls-gnu.c tls-openssl.c \ tod.c transport.c tree.c verify.c version.c xtextencode.c \ - dkim.c dkim.h dkim_transport.c dmarc.c dmarc.h \ + dkim.c dkim.h dkim_transport.c \ valgrind.h memcheck.h \ macro_predef.c macro_predef.h do diff --git a/src/src/EDITME b/src/src/EDITME index e507ab3cd..35c497697 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -642,9 +642,16 @@ DISABLE_MAL_MKS=yes # Uncomment the following line to add DMARC checking capability, implemented # using libopendmarc libraries. You must have SPF and DKIM support enabled also. +# +# If set to "2" instead of "yes" then the support will be +# built as a module and must be installed into LOOKUP_MODULE_DIR (the name +# is historic). The same rules as for other module builds apply; use +# SUPPORT_DMARC_{INCLUDE,LIBS}. +# # SUPPORT_DMARC=yes # CFLAGS += -I/usr/local/include # LDFLAGS += -lopendmarc +# # Uncomment the following if you need to change the default. You can # override it at runtime (main config option dmarc_tld_file) # DMARC_TLD_FILE=/etc/exim/opendmarc.tlds diff --git a/src/src/acl.c b/src/src/acl.c index e285da65c..30fc09174 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -218,7 +218,11 @@ static condition_def conditions[] = { }, #endif #ifdef SUPPORT_DMARC - [ACLC_DMARC_STATUS] = { US"dmarc_status", ACD_EXP, + [ACLC_DMARC_STATUS] = { US"dmarc_status", +# if SUPPORT_DMARC==2 + ACD_LOAD | +# endif + ACD_EXP, PERMITTED(ACL_BIT_DATA) }, #endif @@ -393,6 +397,9 @@ for (condition_def * c = conditions; c < conditions + nelem(conditions); c++) #ifndef MACRO_PREDEF +/* These tables support loading of dynamic modules triggered by an ACL +condition use, spotted during readconf. See acl_read(). */ + # ifdef LOOKUP_MODULE_DIR typedef struct condition_module { const uschar * mod_name; /* module for the givien conditions */ @@ -403,11 +410,17 @@ typedef struct condition_module { # if SUPPORT_SPF==2 static int spf_condx[] = { ACLC_SPF, ACLC_SPF_GUESS, -1 }; # endif +# if SUPPORT_DMARC==2 +static int dmarc_condx[] = { ACLC_DMARC_STATUS, -1 }; +# endif static condition_module condition_modules[] = { # if SUPPORT_SPF==2 {.mod_name = US"spf", .conditions = spf_condx}, # endif +# if SUPPORT_SPF==2 + {.mod_name = US"dmarc", .conditions = dmarc_condx}, +# endif }; # endif @@ -3942,14 +3955,36 @@ for (; cb; cb = cb->next) #ifdef SUPPORT_DMARC case ACLC_DMARC_STATUS: + /* See comment on ACLC_SPF wrt. coding issues */ + { + misc_module_info * mi = misc_mod_find(US"dmarc", &log_message); + typedef uschar * (*efn_t)(int); + uschar * expanded_query; + +debug_printf("%s %d\n", __FUNCTION__, __LINE__); + if (!mi) + { rc = DEFER; break; } /* shouldn't happen */ + +debug_printf("%s %d: mi %p\n", __FUNCTION__, __LINE__, mi); if (!f.dmarc_has_been_checked) - dmarc_process(); - f.dmarc_has_been_checked = TRUE; + { + typedef int (*pfn_t)(void); + (void) (((pfn_t *) mi->functions)[0]) (); /* dmarc_process */ + f.dmarc_has_been_checked = TRUE; + } +debug_printf("%s %d\n", __FUNCTION__, __LINE__); /* used long way of dmarc_exim_expand_query() in case we need more view into the process in the future. */ - rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS), + + /*XXX is this call used with any other arg? */ + expanded_query = (((efn_t *) mi->functions)[1]) (DMARC_VERIFY_STATUS); + +debug_printf("%s %d\n", __FUNCTION__, __LINE__); + rc = match_isinlist(expanded_query, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); +debug_printf("%s %d\n", __FUNCTION__, __LINE__); + } break; #endif @@ -4185,8 +4220,10 @@ for (; cb; cb = cb->next) #ifdef SUPPORT_SPF case ACLC_SPF: case ACLC_SPF_GUESS: - /* Hardwire the offset of the function in the module functions table - for now. Work out a more general mech later. */ + /* We have hardwired function-call numbers, and also prototypes for the + functions. We could do a function name table search for the number + but I can't see how to deal with prototypes. Is a K&R non-prototyped + function still usable with today's compilers? */ { misc_module_info * mi = misc_mod_find(US"spf", &log_message); typedef int (*fn_t)(const uschar **, const uschar *, int); @@ -4195,7 +4232,7 @@ for (; cb; cb = cb->next) if (!mi) { rc = DEFER; break; } /* shouldn't happen */ - fn = ((fn_t *) mi->functions)[1]; + fn = ((fn_t *) mi->functions)[0]; /* spf_process() */ rc = fn(&arg, sender_address, cb->type == ACLC_SPF ? SPF_PROCESS_NORMAL : SPF_PROCESS_GUESS); diff --git a/src/src/arc.c b/src/src/arc.c index 48f69a8cf..2dcbf2efb 100644 --- a/src/src/arc.c +++ b/src/src/arc.c @@ -19,7 +19,7 @@ # include "pdkim/signing.h" # ifdef SUPPORT_DMARC -# include "dmarc.h" +# include "miscmods/dmarc.h" # endif extern pdkim_ctx * dkim_verify_ctx; diff --git a/src/src/daemon.c b/src/src/daemon.c index 49bf74a11..456c586da 100644 --- a/src/src/daemon.c +++ b/src/src/daemon.c @@ -2578,9 +2578,6 @@ smtp_deliver_init(); /* Used for callouts */ #ifdef WITH_CONTENT_SCAN malware_init(); #endif -#ifdef SUPPORT_DMARC -dmarc_init(); -#endif #ifndef DISABLE_TLS tls_daemon_init(); #endif diff --git a/src/src/dmarc.c b/src/src/dmarc.c deleted file mode 100644 index 664daa737..000000000 --- a/src/src/dmarc.c +++ /dev/null @@ -1,717 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ -/* DMARC support. - Copyright (c) The Exim Maintainers 2019 - 2024 - Copyright (c) Todd Lyons 2012 - 2014 - License: GPL */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; - All rights reserved, licensed for use per LICENSE.opendmarc. */ - -/* Code for calling dmarc checks via libopendmarc. Called from acl.c. */ - -#include "exim.h" -#ifdef SUPPORT_DMARC -# if !defined SUPPORT_SPF -# error SPF must also be enabled for DMARC -# elif defined DISABLE_DKIM -# error DKIM must also be enabled for DMARC -# else - -# include "functions.h" -# include "dmarc.h" -# include "pdkim/pdkim.h" - -OPENDMARC_LIB_T dmarc_ctx; -DMARC_POLICY_T *dmarc_pctx = NULL; -OPENDMARC_STATUS_T libdm_status, action, dmarc_policy; -OPENDMARC_STATUS_T da, sa, action; -BOOL dmarc_abort = FALSE; -uschar *dmarc_pass_fail = US"skipped"; -header_line *from_header = NULL; - -misc_module_info * spf_mod_info; -SPF_response_t *spf_response_p; -int dmarc_spf_ares_result = 0; -uschar *spf_sender_domain = NULL; -uschar *spf_human_readable = NULL; -u_char *header_from_sender = NULL; -int history_file_status = DMARC_HIST_OK; - -typedef struct dmarc_exim_p { - uschar *name; - int value; -} dmarc_exim_p; - -static dmarc_exim_p dmarc_policy_description[] = { - /* name value */ - { US"", DMARC_RECORD_P_UNSPECIFIED }, - { US"none", DMARC_RECORD_P_NONE }, - { US"quarantine", DMARC_RECORD_P_QUARANTINE }, - { US"reject", DMARC_RECORD_P_REJECT }, - { NULL, 0 } -}; - - -int -dmarc_init(void) -{ -uschar * errstr; -if (!(spf_mod_info = misc_mod_find(US"spf", &errstr))) - log_write(0, LOG_MAIN|LOG_PANIC_DIE, - "dmarc: failed to find SPF module: %s", errstr); -return TRUE; -} - -gstring * -dmarc_version_report(gstring * g) -{ -return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n", - (OPENDMARC_LIB_VERSION & 0xff000000) >> 24, (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16, - (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8, OPENDMARC_LIB_VERSION & 0x000000ff); -} - - -/* Accept an error_block struct, initialize if empty, parse to the -end, and append the two strings passed to it. Used for adding -variable amounts of value:pair data to the forensic emails. */ - -static error_block * -add_to_eblock(error_block *eblock, uschar *t1, uschar *t2) -{ -error_block *eb = store_malloc(sizeof(error_block)); -if (!eblock) - eblock = eb; -else - { - /* Find the end of the eblock struct and point it at eb */ - error_block *tmp = eblock; - while(tmp->next) - tmp = tmp->next; - tmp->next = eb; - } -eb->text1 = t1; -eb->text2 = t2; -eb->next = NULL; -return eblock; -} - -/* dmarc_conn_init sets up a context that can be re-used for several -messages on the same SMTP connection (that come from the -same host with the same HELO string) */ - -int -dmarc_conn_init(void) -{ -int *netmask = NULL; /* Ignored */ -int is_ipv6 = 0; - -/* Set some sane defaults. Also clears previous results when -multiple messages in one connection. */ - -dmarc_pctx = NULL; -dmarc_status = US"none"; -dmarc_abort = FALSE; -dmarc_pass_fail = US"skipped"; -dmarc_used_domain = US""; -f.dmarc_has_been_checked = FALSE; -header_from_sender = NULL; -spf_response_p = NULL; -spf_sender_domain = NULL; -spf_human_readable = NULL; - -/* ACLs have "control=dmarc_disable_verify" */ -if (f.dmarc_disable_verify) - return OK; - -(void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx); -dmarc_ctx.nscount = 0; -libdm_status = opendmarc_policy_library_init(&dmarc_ctx); -if (libdm_status != DMARC_PARSE_OKAY) - { - log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to init library: %s", - opendmarc_policy_status_to_str(libdm_status)); - dmarc_abort = TRUE; - } -if (!dmarc_tld_file || !*dmarc_tld_file) - { - DEBUG(D_receive) debug_printf_indent("DMARC: no dmarc_tld_file\n"); - dmarc_abort = TRUE; - } -else if (opendmarc_tld_read_file(CS dmarc_tld_file, NULL, NULL, NULL)) - { - log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list '%s': %s", - dmarc_tld_file, strerror(errno)); - dmarc_abort = TRUE; - } -if (!sender_host_address) - { - DEBUG(D_receive) debug_printf_indent("DMARC: no sender_host_address\n"); - dmarc_abort = TRUE; - } -/* This catches locally originated email and startup errors above. */ -if (!dmarc_abort) - { - is_ipv6 = string_is_ip_address(sender_host_address, netmask) == 6; - if (!(dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6))) - { - log_write(0, LOG_MAIN|LOG_PANIC, - "DMARC failure creating policy context: ip=%s", sender_host_address); - dmarc_abort = TRUE; - } - } - -return OK; -} - - -/* dmarc_store_data stores the header data so that subsequent dmarc_process can -access the data. -Called after the entire message has been received, with the From: header. */ - -int -dmarc_store_data(header_line * hdr) -{ -/* No debug output because would change every test debug output */ -if (!f.dmarc_disable_verify) - from_header = hdr; -return OK; -} - - -static void -dmarc_send_forensic_report(u_char ** ruf) -{ -uschar *recipient, *save_sender; -BOOL send_status = FALSE; -error_block *eblock = NULL; -FILE *message_file = NULL; - -/* Earlier ACL does not have *required* control=dmarc_enable_forensic */ -if (!f.dmarc_enable_forensic) - return; - -if ( dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT - || dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE - || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_REJECT - || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_QUARANTINE - ) - if (ruf) - { - eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain); - eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address); - eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full)); - eblock = add_to_eblock(eblock, US"SPF Alignment", - sa == DMARC_POLICY_SPF_ALIGNMENT_PASS ? US"yes" : US"no"); - eblock = add_to_eblock(eblock, US"DKIM Alignment", - da == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? US"yes" : US"no"); - eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text); - - for (int c = 0; ruf[c]; c++) - { - recipient = string_copylc(ruf[c]); - if (Ustrncmp(recipient, "mailto:",7)) - continue; - /* Move to first character past the colon */ - recipient += 7; - DEBUG(D_receive) - debug_printf_indent("DMARC forensic report to %s%s\n", recipient, - (host_checking || f.running_in_test_harness) ? " (not really)" : ""); - if (host_checking || f.running_in_test_harness) - continue; - - if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock, - header_list, message_file, NULL)) - log_write(0, LOG_MAIN|LOG_PANIC, - "failure to send DMARC forensic report to %s", recipient); - } - } -} - - -/* Look up a DNS dmarc record for the given domain. Return it or NULL */ - -static uschar * -dmarc_dns_lookup(uschar * dom) -{ -dns_answer * dnsa = store_get_dns_answer(); -dns_scan dnss; -int rc = dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL); - -if (rc == DNS_SUCCEED) - for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr; - rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) - if (rr->type == T_TXT && rr->size > 3) - { - uschar *record = string_copyn_taint(US rr->data, rr->size, GET_TAINTED); - store_free_dns_answer(dnsa); - return record; - } -store_free_dns_answer(dnsa); -return NULL; -} - - -static int -dmarc_write_history_file(const gstring * dkim_history_buffer) -{ -int history_file_fd = 0; -ssize_t written_len; -int tmp_ans; -u_char ** rua; /* aggregate report addressees */ -gstring * g; - -if (!dmarc_history_file) - { - DEBUG(D_receive) debug_printf_indent("DMARC history file not set\n"); - return DMARC_HIST_DISABLED; - } -if (!host_checking) - { - uschar * s = string_copy(dmarc_history_file); /* need a writeable copy */ - if ((history_file_fd = log_open_as_exim(s)) < 0) - { - log_write(0, LOG_MAIN|LOG_PANIC, - "failure to create DMARC history file: %s: %s", - s, strerror(errno)); - return DMARC_HIST_FILE_ERR; - } - } - -/* Generate the contents of the history file entry */ - -g = string_fmt_append(NULL, - "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n", - message_id, primary_hostname, time(NULL), sender_host_address, - header_from_sender, expand_string(US"$sender_address_domain")); - -if (spf_response_p) - g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result); - -if (dkim_history_buffer) - g = string_fmt_append(g, "%Y", dkim_history_buffer); - -g = string_fmt_append(g, "pdomain %s\npolicy %d\n", - dmarc_used_domain, dmarc_policy); - -if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1))) - for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++) - g = string_fmt_append(g, "rua %s\n", rua[tmp_ans]); -else - g = string_catn(g, US"rua -\n", 6); - -opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans); -g = string_fmt_append(g, "pct %d\n", tmp_ans); - -opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans); -g = string_fmt_append(g, "adkim %d\n", tmp_ans); - -opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans); -g = string_fmt_append(g, "aspf %d\n", tmp_ans); - -opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); -g = string_fmt_append(g, "p %d\n", tmp_ans); - -opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans); -g = string_fmt_append(g, "sp %d\n", tmp_ans); - -g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n", - da, sa, action); - -#if DMARC_API >= 100400 -# ifdef EXPERIMENTAL_ARC -g = arc_dmarc_hist_append(g); -# else -g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n", - ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED); -# endif -#endif - -/* Write the contents to the history file */ -DEBUG(D_receive) - { - debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n", - host_checking ? " (not really)" : ""); - debug_printf_indent("DMARC history data for debugging:\n"); - expand_level++; - debug_printf_indent("%Y", g); - expand_level--; - } - -if (!host_checking) - { - written_len = write_to_fd_buf(history_file_fd, - g->s, - gstring_length(g)); - if (written_len == 0) - { - log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s", - dmarc_history_file); - return DMARC_HIST_WRITE_ERR; - } - (void)close(history_file_fd); - } -return DMARC_HIST_OK; -} - - -/* dmarc_process adds the envelope sender address to the existing -context (if any), retrieves the result, sets up expansion -strings and evaluates the condition outcome. -Called for the first ACL dmarc= condition. */ - -int -dmarc_process(void) -{ -int sr, origin; /* used in SPF section */ -int dmarc_spf_result = 0; /* stores spf into dmarc conn ctx */ -int tmp_ans, c; -pdkim_signature * sig = dkim_signatures; -uschar * rr; -BOOL has_dmarc_record = TRUE; -u_char ** ruf; /* forensic report addressees, if called for */ - -/* ACLs have "control=dmarc_disable_verify" */ -if (f.dmarc_disable_verify) - return OK; - -/* Store the header From: sender domain for this part of DMARC. -If there is no from_header struct, then it's likely this message -is locally generated and relying on fixups to add it. Just skip -the entire DMARC system if we can't find a From: header....or if -there was a previous error. */ - -if (!from_header) - { - DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n"); - dmarc_abort = TRUE; - } -else if (!dmarc_abort) - { - uschar * errormsg; - int dummy, domain; - uschar * p; - uschar saveend; - - f.parse_allow_group = TRUE; - p = parse_find_address_end(from_header->text, FALSE); - saveend = *p; *p = '\0'; - if ((header_from_sender = parse_extract_address(from_header->text, &errormsg, - &dummy, &dummy, &domain, FALSE))) - header_from_sender += domain; - *p = saveend; - - /* The opendmarc library extracts the domain from the email address, but - only try to store it if it's not empty. Otherwise, skip out of DMARC. */ - - if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0)) - dmarc_abort = TRUE; - libdm_status = dmarc_abort - ? DMARC_PARSE_OKAY - : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender); - if (libdm_status != DMARC_PARSE_OKAY) - { - log_write(0, LOG_MAIN|LOG_PANIC, - "failure to store header From: in DMARC: %s, header was '%s'", - opendmarc_policy_status_to_str(libdm_status), from_header->text); - dmarc_abort = TRUE; - } - } - -/* Skip DMARC if connection is SMTP Auth. Temporarily, admin should -instead do this in the ACLs. */ - -if (!dmarc_abort && !sender_host_authenticated) - { - uschar * dmarc_domain; - gstring * dkim_history_buffer = NULL; - - /* Use the envelope sender domain for this part of DMARC */ - - spf_sender_domain = expand_string(US"$sender_address_domain"); - - { - misc_module_info * mi = misc_mod_findonly(US"spf"); - typedef SPF_response_t * (*fn_t)(void); - if (mi) - spf_response_p = ((fn_t *) mi->functions)[3](); /* spf_get_response */ - } - - if (!spf_response_p) - { - /* No spf data means null envelope sender so generate a domain name - from the sender_helo_name */ - - if (!spf_sender_domain) - { - spf_sender_domain = sender_helo_name; - log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n", - spf_sender_domain); - DEBUG(D_receive) - debug_printf_indent("DMARC using synthesized SPF sender domain = %s\n", - spf_sender_domain); - } - dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE; - dmarc_spf_ares_result = ARES_RESULT_UNKNOWN; - origin = DMARC_POLICY_SPF_ORIGIN_HELO; - spf_human_readable = US""; - } - else - { - sr = spf_response_p->result; - dmarc_spf_result = sr == SPF_RESULT_NEUTRAL ? DMARC_POLICY_SPF_OUTCOME_NONE : - sr == SPF_RESULT_PASS ? DMARC_POLICY_SPF_OUTCOME_PASS : - sr == SPF_RESULT_FAIL ? DMARC_POLICY_SPF_OUTCOME_FAIL : - sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL : - DMARC_POLICY_SPF_OUTCOME_NONE; - dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL ? ARES_RESULT_NEUTRAL : - sr == SPF_RESULT_PASS ? ARES_RESULT_PASS : - sr == SPF_RESULT_FAIL ? ARES_RESULT_FAIL : - sr == SPF_RESULT_SOFTFAIL ? ARES_RESULT_SOFTFAIL : - sr == SPF_RESULT_NONE ? ARES_RESULT_NONE : - sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR : - sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR : - ARES_RESULT_UNKNOWN; - origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM; - spf_human_readable = US spf_response_p->header_comment; - DEBUG(D_receive) - debug_printf_indent("DMARC using SPF sender domain = %s\n", spf_sender_domain); - } - if (strcmp( CCS spf_sender_domain, "") == 0) - dmarc_abort = TRUE; - if (!dmarc_abort) - { - libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain, - dmarc_spf_result, origin, spf_human_readable); - if (libdm_status != DMARC_PARSE_OKAY) - log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s", - opendmarc_policy_status_to_str(libdm_status)); - } - - /* Now we cycle through the dkim signature results and put into - the opendmarc context, further building the DMARC reply. */ - - for(pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) - { - int dkim_result, dkim_ares_result, vs, ves; - - vs = sig->verify_status & ~PDKIM_VERIFY_POLICY; - ves = sig->verify_ext_status; - dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS : - vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL : - vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL : - DMARC_POLICY_DKIM_OUTCOME_NONE; - libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain, - -/* The opendmarc project broke its API in a way we can't detect easily. -The EDITME provides a DMARC_API variable */ -#if DMARC_API >= 100400 - sig->selector, -#endif - dkim_result, US""); - DEBUG(D_receive) - debug_printf_indent("DMARC adding DKIM sender domain = %s\n", sig->domain); - if (libdm_status != DMARC_PARSE_OKAY) - log_write(0, LOG_MAIN|LOG_PANIC, - "failure to store dkim (%s) for DMARC: %s", - sig->domain, opendmarc_policy_status_to_str(libdm_status)); - - dkim_ares_result = - vs == PDKIM_VERIFY_PASS ? ARES_RESULT_PASS : - vs == PDKIM_VERIFY_FAIL ? ARES_RESULT_FAIL : - vs == PDKIM_VERIFY_NONE ? ARES_RESULT_NONE : - vs == PDKIM_VERIFY_INVALID ? - ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR : - ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE ? ARES_RESULT_PERMERROR : - ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD ? ARES_RESULT_PERMERROR : - ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT ? ARES_RESULT_PERMERROR : - ARES_RESULT_UNKNOWN : - ARES_RESULT_UNKNOWN; -#if DMARC_API >= 100400 - dkim_history_buffer = string_fmt_append(dkim_history_buffer, - "dkim %s %s %d\n", sig->domain, sig->selector, dkim_ares_result); -#else - dkim_history_buffer = string_fmt_append(dkim_history_buffer, - "dkim %s %d\n", sig->domain, dkim_ares_result); -#endif - } - - /* Look up DMARC policy record in DNS. We do this explicitly, rather than - letting the dmarc library do it with opendmarc_policy_query_dmarc(), so that - our dns access path is used for debug tracing and for the testsuite - diversion. */ - - libdm_status = (rr = dmarc_dns_lookup(header_from_sender)) - ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL) - : DMARC_DNS_ERROR_NO_RECORD; - switch (libdm_status) - { - case DMARC_DNS_ERROR_NXDOMAIN: - case DMARC_DNS_ERROR_NO_RECORD: - DEBUG(D_receive) - debug_printf_indent("DMARC no record found for %s\n", header_from_sender); - has_dmarc_record = FALSE; - break; - case DMARC_PARSE_OKAY: - DEBUG(D_receive) - debug_printf_indent("DMARC record found for %s\n", header_from_sender); - break; - case DMARC_PARSE_ERROR_BAD_VALUE: - DEBUG(D_receive) - debug_printf_indent("DMARC record parse error for %s\n", header_from_sender); - has_dmarc_record = FALSE; - break; - default: - /* everything else, skip dmarc */ - DEBUG(D_receive) - debug_printf_indent("DMARC skipping (%s), unsure what to do with %s", - opendmarc_policy_status_to_str(libdm_status), - from_header->text); - has_dmarc_record = FALSE; - break; - } - - /* Store the policy string in an expandable variable. */ - - libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); - for (c = 0; dmarc_policy_description[c].name; c++) - if (tmp_ans == dmarc_policy_description[c].value) - { - dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name); - break; - } - - /* Can't use exim's string manipulation functions so allocate memory - for libopendmarc using its max hostname length definition. */ - - dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED); - libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx, - dmarc_domain, DMARC_MAXHOSTNAMELEN-1); - store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1); - dmarc_used_domain = dmarc_domain; - - if (libdm_status != DMARC_PARSE_OKAY) - log_write(0, LOG_MAIN|LOG_PANIC, - "failure to read domainname used for DMARC lookup: %s", - opendmarc_policy_status_to_str(libdm_status)); - - dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx); - switch(libdm_status) - { - case DMARC_POLICY_ABSENT: /* No DMARC record found */ - dmarc_status = US"norecord"; - dmarc_pass_fail = US"none"; - dmarc_status_text = US"No DMARC record"; - action = DMARC_RESULT_ACCEPT; - break; - case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */ - dmarc_status = US"nofrom"; - dmarc_pass_fail = US"temperror"; - dmarc_status_text = US"No From: domain found"; - action = DMARC_RESULT_ACCEPT; - break; - case DMARC_POLICY_NONE: /* Accept and report */ - dmarc_status = US"none"; - dmarc_pass_fail = US"none"; - dmarc_status_text = US"None, Accept"; - action = DMARC_RESULT_ACCEPT; - break; - case DMARC_POLICY_PASS: /* Explicit accept */ - dmarc_status = US"accept"; - dmarc_pass_fail = US"pass"; - dmarc_status_text = US"Accept"; - action = DMARC_RESULT_ACCEPT; - break; - case DMARC_POLICY_REJECT: /* Explicit reject */ - dmarc_status = US"reject"; - dmarc_pass_fail = US"fail"; - dmarc_status_text = US"Reject"; - action = DMARC_RESULT_REJECT; - break; - case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ - dmarc_status = US"quarantine"; - dmarc_pass_fail = US"fail"; - dmarc_status_text = US"Quarantine"; - action = DMARC_RESULT_QUARANTINE; - break; - default: - dmarc_status = US"temperror"; - dmarc_pass_fail = US"temperror"; - dmarc_status_text = US"Internal Policy Error"; - action = DMARC_RESULT_TEMPFAIL; - break; - } - - libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa); - if (libdm_status != DMARC_PARSE_OKAY) - log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s", - opendmarc_policy_status_to_str(libdm_status)); - - if (has_dmarc_record) - { - log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s " - "spf_align=%s dkim_align=%s enforcement='%s'", - spf_sender_domain, dmarc_used_domain, - sa==DMARC_POLICY_SPF_ALIGNMENT_PASS ?"yes":"no", - da==DMARC_POLICY_DKIM_ALIGNMENT_PASS ?"yes":"no", - dmarc_status_text); - history_file_status = dmarc_write_history_file(dkim_history_buffer); - /* Now get the forensic reporting addresses, if any */ - ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1); - dmarc_send_forensic_report(ruf); - } - } - -/* shut down libopendmarc */ -if (dmarc_pctx) - (void) opendmarc_policy_connect_shutdown(dmarc_pctx); -if (!f.dmarc_disable_verify) - (void) opendmarc_policy_library_shutdown(&dmarc_ctx); - -return OK; -} - -uschar * -dmarc_exim_expand_query(int what) -{ -if (f.dmarc_disable_verify || !dmarc_pctx) - return dmarc_exim_expand_defaults(what); - -if (what == DMARC_VERIFY_STATUS) - return dmarc_status; -return US""; -} - -uschar * -dmarc_exim_expand_defaults(int what) -{ -if (what == DMARC_VERIFY_STATUS) - return f.dmarc_disable_verify ? US"off" : US"none"; -return US""; -} - - -gstring * -authres_dmarc(gstring * g) -{ -if (f.dmarc_has_been_checked) - { - int start = 0; /* Compiler quietening */ - DEBUG(D_acl) start = gstring_length(g); - g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail); - if (header_from_sender) - g = string_append(g, 2, US" header.from=", header_from_sender); - DEBUG(D_acl) debug_printf("DMARC:\tauthres '%.*s'\n", - gstring_length(g) - start - 3, g->s + start + 3); - } -else - DEBUG(D_acl) debug_printf("DMARC:\tno authres\n"); -return g; -} - -# endif /* SUPPORT_SPF */ -#endif /* SUPPORT_DMARC */ -/* vi: aw ai sw=2 - */ diff --git a/src/src/dmarc.h b/src/src/dmarc.h deleted file mode 100644 index dcf289f2d..000000000 --- a/src/src/dmarc.h +++ /dev/null @@ -1,66 +0,0 @@ -/************************************************* -* Exim - an Internet mail transport agent * -*************************************************/ - -/* Experimental DMARC support. - Copyright (c) The Exim Maintainers 2021 - 2023 - Copyright (c) Todd Lyons 2012 - 2014 - License: GPL */ -/* SPDX-License-Identifier: GPL-2.0-or-later */ - -/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; - All rights reserved, licensed for use per LICENSE.opendmarc. */ - -#ifdef SUPPORT_DMARC - -# include "opendmarc/dmarc.h" -# ifdef SUPPORT_SPF -# include "spf2/spf.h" -# endif /* SUPPORT_SPF */ - -/* prototypes */ -gstring * dmarc_version_report(gstring *); -int dmarc_init(void); -int dmarc_conn_init(void); -int dmarc_store_data(header_line *); -int dmarc_process(void); -uschar *dmarc_exim_expand_query(int); -uschar *dmarc_exim_expand_defaults(int); - -#define DMARC_VERIFY_STATUS 1 - -#define DMARC_HIST_OK 1 -#define DMARC_HIST_DISABLED 2 -#define DMARC_HIST_EMPTY 3 -#define DMARC_HIST_FILE_ERR 4 -#define DMARC_HIST_WRITE_ERR 5 - -/* From opendmarc.c */ -#define DMARC_RESULT_REJECT 0 -#define DMARC_RESULT_DISCARD 1 -#define DMARC_RESULT_ACCEPT 2 -#define DMARC_RESULT_TEMPFAIL 3 -#define DMARC_RESULT_QUARANTINE 4 - -/* From opendmarc-ar.h */ -/* ARES_RESULT_T -- type for specifying an authentication result */ -#define ARES_RESULT_UNDEFINED (-1) -#define ARES_RESULT_PASS 0 -#define ARES_RESULT_UNUSED 1 -#define ARES_RESULT_SOFTFAIL 2 -#define ARES_RESULT_NEUTRAL 3 -#define ARES_RESULT_TEMPERROR 4 -#define ARES_RESULT_PERMERROR 5 -#define ARES_RESULT_NONE 6 -#define ARES_RESULT_FAIL 7 -#define ARES_RESULT_POLICY 8 -#define ARES_RESULT_NXDOMAIN 9 -#define ARES_RESULT_SIGNED 10 -#define ARES_RESULT_UNKNOWN 11 -#define ARES_RESULT_DISCARD 12 - -#define DMARC_ARC_POLICY_RESULT_PASS 0 -#define DMARC_ARC_POLICY_RESULT_UNUSED 1 -#define DMARC_ARC_POLICY_RESULT_FAIL 2 - -#endif /* SUPPORT_DMARC */ diff --git a/src/src/drtables.c b/src/src/drtables.c index 35a376dd1..9ed55e29a 100644 --- a/src/src/drtables.c +++ b/src/src/drtables.c @@ -432,8 +432,8 @@ static void misc_mod_add(misc_module_info * mi) { if (mi->init) mi->init(mi); -DEBUG(D_lookup) if (mi->lib_vers_report) - debug_printf_indent("%Y\n", mi->lib_vers_report(NULL)); +DEBUG(D_any) if (mi->lib_vers_report) + debug_printf_indent("%Y", mi->lib_vers_report(NULL)); mi->next = misc_module_list; misc_module_list = mi; @@ -459,8 +459,10 @@ mi = (struct misc_module_info *) dlsym(dl, CS string_sprintf("%s_module_info", name)); if ((errormsg = dlerror())) { - fprintf(stderr, "%s does not appear to be an spf module (%s)\n", name, errormsg); - log_write(0, LOG_MAIN|LOG_PANIC, "%s does not appear to be an spf module (%s)", name, errormsg); + fprintf(stderr, "%s does not appear to be a '%s' module (%s)\n", + name, name, errormsg); + log_write(0, LOG_MAIN|LOG_PANIC, + "%s does not contain the expected module info symbol (%s)", name, errormsg); dlclose(dl); return NULL; } @@ -491,6 +493,7 @@ misc_mod_findonly(const uschar * name) for (misc_module_info * mi = misc_module_list; mi; mi = mi->next) if (Ustrcmp(name, mi->name) == 0) return mi; +return NULL; } /* Find a "misc" module, possibly already loaded, by name. */ @@ -508,6 +511,42 @@ return NULL; } +/* For any "misc" module having a connection-init routine, call it. */ + +int +misc_mod_conn_init(const uschar * sender_helo_name, + const uschar * sender_host_address) +{ +for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next) + if (mi->conn_init) + if ((mi->conn_init) (sender_helo_name, sender_host_address) != OK) + return FAIL; +return OK; +} + +/* Ditto, smtp-reset */ + +void +misc_mod_smtp_reset(void) +{ +for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next) + if (mi->smtp_reset) + (mi->smtp_reset)(); +} + +/* Ditto, msg-init */ + +int +misc_mod_msg_init(void) +{ +for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next) + if (mi->msg_init) + if ((mi->msg_init)() != OK) + return FAIL; +return OK; +} + + @@ -658,6 +697,9 @@ DEBUG(D_lookup) debug_printf("Loaded %d lookup modules\n", countmodules); } +#if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2 +extern misc_module_info dmarc_module_info; +#endif #if defined(SUPPORT_SPF) && SUPPORT_SPF!=2 extern misc_module_info spf_module_info; #endif @@ -667,9 +709,15 @@ init_misc_mod_list(void) { static BOOL onetime = FALSE; if (onetime) return; + #if defined(SUPPORT_SPF) && SUPPORT_SPF!=2 +/* dmarc depends on spf so this add must go first, for the dmarc-static case */ misc_mod_add(&spf_module_info); #endif +#if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2 +misc_mod_add(&dmarc_module_info); +#endif + onetime = TRUE; } diff --git a/src/src/exim.c b/src/src/exim.c index ecc25d6bc..5ad54ffc1 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -1395,9 +1395,9 @@ DEBUG(D_any) #ifdef SUPPORT_I18N g = utf8_version_report(g); #endif -#ifdef SUPPORT_DMARC - g = dmarc_version_report(g); -#endif + +/*XXX do we need a "show misc-mods version-report" ? +Currently they are output in misc_mod_add() */ show_string(is_stdout, g); g = NULL; diff --git a/src/src/exim.h b/src/src/exim.h index 470adb351..284748c5d 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -549,7 +549,7 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly. # include "dkim.h" #endif #ifdef SUPPORT_DMARC -# include "dmarc.h" +# include "miscmods/dmarc.h" # include #endif diff --git a/src/src/expand.c b/src/src/expand.c index b3a1575a7..a6b05bd87 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -513,10 +513,10 @@ static var_entry var_table[] = { { "dkim_verify_status", vtype_stringptr, &dkim_verify_status }, #endif #ifdef SUPPORT_DMARC - { "dmarc_domain_policy", vtype_stringptr, &dmarc_domain_policy }, - { "dmarc_status", vtype_stringptr, &dmarc_status }, - { "dmarc_status_text", vtype_stringptr, &dmarc_status_text }, - { "dmarc_used_domain", vtype_stringptr, &dmarc_used_domain }, + { "dmarc_domain_policy", vtype_module, US"dmarc" }, + { "dmarc_status", vtype_module, US"dmarc" }, + { "dmarc_status_text", vtype_module, US"dmarc" }, + { "dmarc_used_domain", vtype_module, US"dmarc" }, #endif { "dnslist_domain", vtype_stringptr, &dnslist_domain }, { "dnslist_matched", vtype_stringptr, &dnslist_matched }, @@ -4888,7 +4888,7 @@ while (*s) if (mi) { typedef gstring * (*fn_t)(gstring *); - fn_t fn = ((fn_t *) mi->functions)[2]; /* authres_spf */ + fn_t fn = ((fn_t *) mi->functions)[1]; /* authres_spf */ yield = fn(yield); } } @@ -4897,7 +4897,16 @@ while (*s) yield = authres_dkim(yield); #endif #ifdef SUPPORT_DMARC - yield = authres_dmarc(yield); + { + misc_module_info * mi = misc_mod_findonly(US"dmarc"); + if (mi) + { + /*XXX is authres common enough to be generic? */ + typedef gstring * (*fn_t)(gstring *); + fn_t fn = ((fn_t *) mi->functions)[2]; /* authres_dmarc*/ + yield = fn(yield); + } + } #endif #ifdef EXPERIMENTAL_ARC yield = authres_arc(yield); diff --git a/src/src/functions.h b/src/src/functions.h index aaec6461f..3a980318f 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -147,9 +147,6 @@ extern gstring *authres_arc(gstring *); #ifndef DISABLE_DKIM extern gstring *authres_dkim(gstring *); #endif -#ifdef SUPPORT_DMARC -extern gstring *authres_dmarc(gstring *); -#endif extern gstring *authres_smtpauth(gstring *); extern uschar *b64encode(const uschar *, int); @@ -377,8 +374,13 @@ extern ssize_t mime_decode_base64(FILE *, FILE *, uschar *); extern int mime_regex(const uschar **, BOOL); extern void mime_set_anomaly(int); #endif + +extern int misc_mod_conn_init(const uschar *, const uschar *); extern misc_module_info * misc_mod_find(const uschar * modname, uschar **); extern misc_module_info * misc_mod_findonly(const uschar * modname); +extern int misc_mod_msg_init(void); +extern void misc_mod_smtp_reset(void); + extern uschar *moan_check_errorcopy(const uschar *); extern BOOL moan_skipped_syntax_errors(uschar *, error_block *, uschar *, BOOL, uschar *); diff --git a/src/src/globals.c b/src/src/globals.c index f2287d41c..cfa75f1d7 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -882,15 +882,6 @@ uschar *dkim_verify_signers = US"$dkim_signers"; uschar *dkim_verify_status = NULL; uschar *dkim_verify_reason = NULL; #endif -#ifdef SUPPORT_DMARC -uschar *dmarc_domain_policy = NULL; -uschar *dmarc_forensic_sender = NULL; -uschar *dmarc_history_file = NULL; -uschar *dmarc_status = NULL; -uschar *dmarc_status_text = NULL; -uschar *dmarc_tld_file = NULL; -uschar *dmarc_used_domain = NULL; -#endif uschar *dns_again_means_nonexist = NULL; int dns_csa_search_limit = 5; diff --git a/src/src/globals.h b/src/src/globals.h index 9b30e502c..8173d771e 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -563,15 +563,6 @@ extern uschar *dkim_verify_signers; /* Colon-separated list of domains for ea extern uschar *dkim_verify_status; /* result for this signature */ extern uschar *dkim_verify_reason; /* result for this signature */ #endif -#ifdef SUPPORT_DMARC -extern uschar *dmarc_domain_policy; /* Expansion for declared policy of used domain */ -extern uschar *dmarc_forensic_sender; /* Set sender address for forensic reports */ -extern uschar *dmarc_history_file; /* Expansion variable, file to store dmarc results */ -extern uschar *dmarc_status; /* Expansion variable, one word value */ -extern uschar *dmarc_status_text; /* Expansion variable, human readable value */ -extern uschar *dmarc_tld_file; /* Mozilla TLDs text file */ -extern uschar *dmarc_used_domain; /* Expansion variable, domain libopendmarc chose for DMARC policy lookup */ -#endif extern uschar *dns_again_means_nonexist; /* Domains that are badly set up */ extern int dns_csa_search_limit; /* How deep to search for CSA SRV records */ diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c index 4e0824911..a06f9117d 100644 --- a/src/src/lookups/spf.c +++ b/src/src/lookups/spf.c @@ -39,7 +39,7 @@ misc_module_info * mi = misc_mod_find(US"spf", errmsg); if (mi) { typedef void * (*fn_t)(const uschar *, uschar **); - return (((fn_t *) mi->functions)[5]) (filename, errmsg); + return (((fn_t *) mi->functions)[3]) (filename, errmsg); } return NULL; } @@ -52,7 +52,7 @@ misc_module_info * mi = misc_mod_find(US"spf", NULL); if (mi) { typedef void (*fn_t)(void *); - return (((fn_t *) mi->functions)[6]) (handle); + return (((fn_t *) mi->functions)[4]) (handle); } } @@ -67,7 +67,7 @@ if (mi) { typedef int (*fn_t) (void *, const uschar *, const uschar *, int, uschar **, uschar **, uint *, const uschar *); - return (((fn_t *) mi->functions)[7]) (handle, filename, keystring, key_len, + return (((fn_t *) mi->functions)[5]) (handle, filename, keystring, key_len, result, errmsg, do_cache, opts); } return FAIL; diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile index 59bf29836..d20b7a9f1 100644 --- a/src/src/miscmods/Makefile +++ b/src/src/miscmods/Makefile @@ -26,6 +26,7 @@ miscmods.a: $(OBJ) .c.so:; @echo "$(CC) -shared $*.c" $(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@ -spf.o spf.so: $(HDRS) spf.h spf.c +spf.o spf.so: $(HDRS) spf.h spf.c +dmarc.o dmarc.so: $(HDRS) ../pdkim/pdkim.h dmarc.h dmarc.c # End diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c new file mode 100644 index 000000000..0a97bf6b7 --- /dev/null +++ b/src/src/miscmods/dmarc.c @@ -0,0 +1,791 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ +/* DMARC support. + Copyright (c) The Exim Maintainers 2019 - 2024 + Copyright (c) Todd Lyons 2012 - 2014 + License: GPL */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +/* Code for calling dmarc checks via libopendmarc. Called from acl.c. */ + +#include "../exim.h" +#ifdef SUPPORT_DMARC +# if !defined SUPPORT_SPF +# error SPF must also be enabled for DMARC +# elif defined DISABLE_DKIM +# error DKIM must also be enabled for DMARC +# else + +# include "../functions.h" +# include "dmarc.h" +# include "../pdkim/pdkim.h" + +OPENDMARC_LIB_T dmarc_ctx; +DMARC_POLICY_T *dmarc_pctx = NULL; +OPENDMARC_STATUS_T libdm_status, action, dmarc_policy; +OPENDMARC_STATUS_T da, sa, action; +BOOL dmarc_abort = FALSE; +uschar *dmarc_pass_fail = US"skipped"; +header_line *from_header = NULL; + +misc_module_info * spf_mod_info; +SPF_response_t *spf_response_p; +int dmarc_spf_ares_result = 0; +uschar *spf_sender_domain = NULL; +uschar *spf_human_readable = NULL; +u_char *header_from_sender = NULL; +int history_file_status = DMARC_HIST_OK; + +typedef struct dmarc_exim_p { + uschar *name; + int value; +} dmarc_exim_p; + +static dmarc_exim_p dmarc_policy_description[] = { + /* name value */ + { US"", DMARC_RECORD_P_UNSPECIFIED }, + { US"none", DMARC_RECORD_P_NONE }, + { US"quarantine", DMARC_RECORD_P_QUARANTINE }, + { US"reject", DMARC_RECORD_P_REJECT }, + { NULL, 0 } +}; + + +/* $variables */ +uschar * dmarc_domain_policy = NULL; /* Declared policy of used domain */ +uschar * dmarc_status = NULL; /* One word value */ +uschar * dmarc_status_text = NULL; /* Human readable value */ +uschar * dmarc_used_domain = NULL; /* Domain libopendmarc chose for DMARC policy lookup */ + +/* options */ +uschar * dmarc_forensic_sender = NULL; /* Set sender address for forensic reports */ +uschar * dmarc_history_file = NULL; /* File to store dmarc results */ +uschar * dmarc_tld_file = NULL; /* Mozilla TLDs text file */ + + +/* One-time initialisation for dmarc. Ensure the spf module is available. */ + +static BOOL +dmarc_init(void *) +{ +uschar * errstr; +if (!(spf_mod_info = misc_mod_find(US"spf", &errstr))) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "dmarc: failed to find SPF module: %s", errstr); +return TRUE; +} + +static gstring * +dmarc_version_report(gstring * g) +{ +return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n", + (OPENDMARC_LIB_VERSION & 0xff000000) >> 24, + (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16, + (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8, + (OPENDMARC_LIB_VERSION & 0x000000ff)); +} + + +/* Accept an error_block struct, initialize if empty, parse to the +end, and append the two strings passed to it. Used for adding +variable amounts of value:pair data to the forensic emails. */ + +static error_block * +add_to_eblock(error_block *eblock, uschar *t1, uschar *t2) +{ +error_block *eb = store_malloc(sizeof(error_block)); +if (!eblock) + eblock = eb; +else + { + /* Find the end of the eblock struct and point it at eb */ + error_block *tmp = eblock; + while(tmp->next) + tmp = tmp->next; + tmp->next = eb; + } +eb->text1 = t1; +eb->text2 = t2; +eb->next = NULL; +return eblock; +} + +/* dmarc_msg_init sets up a context that can be re-used for several +messages on the same SMTP connection (that come from the +same host with the same HELO string). +However, we seem to only use it for one; we destroy some sort of context +at the tail end of dmarc_process(). */ + +static int +dmarc_msg_init() +{ +int *netmask = NULL; /* Ignored */ +int is_ipv6 = 0; + +/* Set some sane defaults. Also clears previous results when +multiple messages in one connection. */ + +dmarc_pctx = NULL; +dmarc_status = US"none"; +dmarc_abort = FALSE; +dmarc_pass_fail = US"skipped"; +dmarc_used_domain = US""; +f.dmarc_has_been_checked = FALSE; +header_from_sender = NULL; +spf_response_p = NULL; +spf_sender_domain = NULL; +spf_human_readable = NULL; + +/* ACLs have "control=dmarc_disable_verify" */ +if (f.dmarc_disable_verify) + return OK; + +(void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx); +dmarc_ctx.nscount = 0; +libdm_status = opendmarc_policy_library_init(&dmarc_ctx); +if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to init library: %s", + opendmarc_policy_status_to_str(libdm_status)); + dmarc_abort = TRUE; + } +if (!dmarc_tld_file || !*dmarc_tld_file) + { + DEBUG(D_receive) debug_printf_indent("DMARC: no dmarc_tld_file\n"); + dmarc_abort = TRUE; + } +else if (opendmarc_tld_read_file(CS dmarc_tld_file, NULL, NULL, NULL)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list '%s': %s", + dmarc_tld_file, strerror(errno)); + dmarc_abort = TRUE; + } +if (!sender_host_address) + { + DEBUG(D_receive) debug_printf_indent("DMARC: no sender_host_address\n"); + dmarc_abort = TRUE; + } +/* This catches locally originated email and startup errors above. */ +if (!dmarc_abort) + { + is_ipv6 = string_is_ip_address(sender_host_address, netmask) == 6; + if (!(dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6))) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "DMARC failure creating policy context: ip=%s", sender_host_address); + dmarc_abort = TRUE; + } + } + +return OK; +} + + +static void +dmarc_smtp_reset(void) +{ +dmarc_domain_policy = dmarc_status = dmarc_status_text = +dmarc_used_domain = NULL; +} + + +/* dmarc_store_data stores the header data so that subsequent dmarc_process can +access the data. +Called after the entire message has been received, with the From: header. */ + +static int +dmarc_store_data(header_line * hdr) +{ +/* No debug output because would change every test debug output */ +if (!f.dmarc_disable_verify) + from_header = hdr; +return OK; +} + + +static void +dmarc_send_forensic_report(u_char ** ruf) +{ +uschar *recipient, *save_sender; +BOOL send_status = FALSE; +error_block *eblock = NULL; +FILE *message_file = NULL; + +/* Earlier ACL does not have *required* control=dmarc_enable_forensic */ +if (!f.dmarc_enable_forensic) + return; + +if ( dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT + || dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE + || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_REJECT + || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_QUARANTINE + ) + if (ruf) + { + eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain); + eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address); + eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full)); + eblock = add_to_eblock(eblock, US"SPF Alignment", + sa == DMARC_POLICY_SPF_ALIGNMENT_PASS ? US"yes" : US"no"); + eblock = add_to_eblock(eblock, US"DKIM Alignment", + da == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? US"yes" : US"no"); + eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text); + + for (int c = 0; ruf[c]; c++) + { + recipient = string_copylc(ruf[c]); + if (Ustrncmp(recipient, "mailto:",7)) + continue; + /* Move to first character past the colon */ + recipient += 7; + DEBUG(D_receive) + debug_printf_indent("DMARC forensic report to %s%s\n", recipient, + (host_checking || f.running_in_test_harness) ? " (not really)" : ""); + if (host_checking || f.running_in_test_harness) + continue; + + if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock, + header_list, message_file, NULL)) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to send DMARC forensic report to %s", recipient); + } + } +} + + +/* Look up a DNS dmarc record for the given domain. Return it or NULL */ + +static uschar * +dmarc_dns_lookup(uschar * dom) +{ +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; +int rc = dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL); + +if (rc == DNS_SUCCEED) + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT && rr->size > 3) + { + uschar *record = string_copyn_taint(US rr->data, rr->size, GET_TAINTED); + store_free_dns_answer(dnsa); + return record; + } +store_free_dns_answer(dnsa); +return NULL; +} + + +static int +dmarc_write_history_file(const gstring * dkim_history_buffer) +{ +int history_file_fd = 0; +ssize_t written_len; +int tmp_ans; +u_char ** rua; /* aggregate report addressees */ +gstring * g; + +if (!dmarc_history_file) + { + DEBUG(D_receive) debug_printf_indent("DMARC history file not set\n"); + return DMARC_HIST_DISABLED; + } +if (!host_checking) + { + uschar * s = string_copy(dmarc_history_file); /* need a writeable copy */ + if ((history_file_fd = log_open_as_exim(s)) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to create DMARC history file: %s: %s", + s, strerror(errno)); + return DMARC_HIST_FILE_ERR; + } + } + +/* Generate the contents of the history file entry */ + +g = string_fmt_append(NULL, + "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n", + message_id, primary_hostname, time(NULL), sender_host_address, + header_from_sender, expand_string(US"$sender_address_domain")); + +if (spf_response_p) + g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result); + +if (dkim_history_buffer) + g = string_fmt_append(g, "%Y", dkim_history_buffer); + +g = string_fmt_append(g, "pdomain %s\npolicy %d\n", + dmarc_used_domain, dmarc_policy); + +if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1))) + for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++) + g = string_fmt_append(g, "rua %s\n", rua[tmp_ans]); +else + g = string_catn(g, US"rua -\n", 6); + +opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans); +g = string_fmt_append(g, "pct %d\n", tmp_ans); + +opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans); +g = string_fmt_append(g, "adkim %d\n", tmp_ans); + +opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans); +g = string_fmt_append(g, "aspf %d\n", tmp_ans); + +opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); +g = string_fmt_append(g, "p %d\n", tmp_ans); + +opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans); +g = string_fmt_append(g, "sp %d\n", tmp_ans); + +g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n", + da, sa, action); + +#if DMARC_API >= 100400 +# ifdef EXPERIMENTAL_ARC +g = arc_dmarc_hist_append(g); +# else +g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n", + ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED); +# endif +#endif + +/* Write the contents to the history file */ +DEBUG(D_receive) + { + debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n", + host_checking ? " (not really)" : ""); + debug_printf_indent("DMARC history data for debugging:\n"); + expand_level++; + debug_printf_indent("%Y", g); + expand_level--; + } + +if (!host_checking) + { + written_len = write_to_fd_buf(history_file_fd, + g->s, + gstring_length(g)); + if (written_len == 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s", + dmarc_history_file); + return DMARC_HIST_WRITE_ERR; + } + (void)close(history_file_fd); + } +return DMARC_HIST_OK; +} + + +/* dmarc_process adds the envelope sender address to the existing +context (if any), retrieves the result, sets up expansion +strings and evaluates the condition outcome. +Called for the first ACL dmarc= condition. */ + +static int +dmarc_process(void) +{ +int sr, origin; /* used in SPF section */ +int dmarc_spf_result = 0; /* stores spf into dmarc conn ctx */ +int tmp_ans, c; +pdkim_signature * sig = dkim_signatures; +uschar * rr; +BOOL has_dmarc_record = TRUE; +u_char ** ruf; /* forensic report addressees, if called for */ + +/* ACLs have "control=dmarc_disable_verify" */ +if (f.dmarc_disable_verify) + return OK; + +/* Store the header From: sender domain for this part of DMARC. +If there is no from_header struct, then it's likely this message +is locally generated and relying on fixups to add it. Just skip +the entire DMARC system if we can't find a From: header....or if +there was a previous error. */ + +if (!from_header) + { + DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n"); + dmarc_abort = TRUE; + } +else if (!dmarc_abort) + { + uschar * errormsg; + int dummy, domain; + uschar * p; + uschar saveend; + + f.parse_allow_group = TRUE; + p = parse_find_address_end(from_header->text, FALSE); + saveend = *p; *p = '\0'; + if ((header_from_sender = parse_extract_address(from_header->text, &errormsg, + &dummy, &dummy, &domain, FALSE))) + header_from_sender += domain; + *p = saveend; + + /* The opendmarc library extracts the domain from the email address, but + only try to store it if it's not empty. Otherwise, skip out of DMARC. */ + + if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0)) + dmarc_abort = TRUE; + libdm_status = dmarc_abort + ? DMARC_PARSE_OKAY + : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to store header From: in DMARC: %s, header was '%s'", + opendmarc_policy_status_to_str(libdm_status), from_header->text); + dmarc_abort = TRUE; + } + } + +/* Skip DMARC if connection is SMTP Auth. Temporarily, admin should +instead do this in the ACLs. */ + +if (!dmarc_abort && !sender_host_authenticated) + { + uschar * dmarc_domain; + gstring * dkim_history_buffer = NULL; + + /* Use the envelope sender domain for this part of DMARC */ + + spf_sender_domain = expand_string(US"$sender_address_domain"); + + { + typedef SPF_response_t * (*fn_t)(void); + if (spf_mod_info) + spf_response_p = ((fn_t *) spf_mod_info->functions)[2](); /* spf_get_response */ + } + + if (!spf_response_p) + { + /* No spf data means null envelope sender so generate a domain name + from the sender_helo_name */ + + if (!spf_sender_domain) + { + spf_sender_domain = sender_helo_name; + log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n", + spf_sender_domain); + DEBUG(D_receive) + debug_printf_indent("DMARC using synthesized SPF sender domain = %s\n", + spf_sender_domain); + } + dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE; + dmarc_spf_ares_result = ARES_RESULT_UNKNOWN; + origin = DMARC_POLICY_SPF_ORIGIN_HELO; + spf_human_readable = US""; + } + else + { + sr = spf_response_p->result; + dmarc_spf_result = sr == SPF_RESULT_NEUTRAL ? DMARC_POLICY_SPF_OUTCOME_NONE : + sr == SPF_RESULT_PASS ? DMARC_POLICY_SPF_OUTCOME_PASS : + sr == SPF_RESULT_FAIL ? DMARC_POLICY_SPF_OUTCOME_FAIL : + sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL : + DMARC_POLICY_SPF_OUTCOME_NONE; + dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL ? ARES_RESULT_NEUTRAL : + sr == SPF_RESULT_PASS ? ARES_RESULT_PASS : + sr == SPF_RESULT_FAIL ? ARES_RESULT_FAIL : + sr == SPF_RESULT_SOFTFAIL ? ARES_RESULT_SOFTFAIL : + sr == SPF_RESULT_NONE ? ARES_RESULT_NONE : + sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR : + sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR : + ARES_RESULT_UNKNOWN; + origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM; + spf_human_readable = US spf_response_p->header_comment; + DEBUG(D_receive) + debug_printf_indent("DMARC using SPF sender domain = %s\n", spf_sender_domain); + } + if (strcmp( CCS spf_sender_domain, "") == 0) + dmarc_abort = TRUE; + if (!dmarc_abort) + { + libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain, + dmarc_spf_result, origin, spf_human_readable); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s", + opendmarc_policy_status_to_str(libdm_status)); + } + + /* Now we cycle through the dkim signature results and put into + the opendmarc context, further building the DMARC reply. */ + + for(pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + { + int dkim_result, dkim_ares_result, vs, ves; + + vs = sig->verify_status & ~PDKIM_VERIFY_POLICY; + ves = sig->verify_ext_status; + dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS : + vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL : + vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL : + DMARC_POLICY_DKIM_OUTCOME_NONE; + libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain, + +/* The opendmarc project broke its API in a way we can't detect easily. +The EDITME provides a DMARC_API variable */ +#if DMARC_API >= 100400 + sig->selector, +#endif + dkim_result, US""); + DEBUG(D_receive) + debug_printf_indent("DMARC adding DKIM sender domain = %s\n", sig->domain); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to store dkim (%s) for DMARC: %s", + sig->domain, opendmarc_policy_status_to_str(libdm_status)); + + dkim_ares_result = + vs == PDKIM_VERIFY_PASS ? ARES_RESULT_PASS : + vs == PDKIM_VERIFY_FAIL ? ARES_RESULT_FAIL : + vs == PDKIM_VERIFY_NONE ? ARES_RESULT_NONE : + vs == PDKIM_VERIFY_INVALID ? + ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT ? ARES_RESULT_PERMERROR : + ARES_RESULT_UNKNOWN : + ARES_RESULT_UNKNOWN; +#if DMARC_API >= 100400 + dkim_history_buffer = string_fmt_append(dkim_history_buffer, + "dkim %s %s %d\n", sig->domain, sig->selector, dkim_ares_result); +#else + dkim_history_buffer = string_fmt_append(dkim_history_buffer, + "dkim %s %d\n", sig->domain, dkim_ares_result); +#endif + } + + /* Look up DMARC policy record in DNS. We do this explicitly, rather than + letting the dmarc library do it with opendmarc_policy_query_dmarc(), so that + our dns access path is used for debug tracing and for the testsuite + diversion. */ + + libdm_status = (rr = dmarc_dns_lookup(header_from_sender)) + ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL) + : DMARC_DNS_ERROR_NO_RECORD; + switch (libdm_status) + { + case DMARC_DNS_ERROR_NXDOMAIN: + case DMARC_DNS_ERROR_NO_RECORD: + DEBUG(D_receive) + debug_printf_indent("DMARC no record found for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + case DMARC_PARSE_OKAY: + DEBUG(D_receive) + debug_printf_indent("DMARC record found for %s\n", header_from_sender); + break; + case DMARC_PARSE_ERROR_BAD_VALUE: + DEBUG(D_receive) + debug_printf_indent("DMARC record parse error for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + default: + /* everything else, skip dmarc */ + DEBUG(D_receive) + debug_printf_indent("DMARC skipping (%s), unsure what to do with %s", + opendmarc_policy_status_to_str(libdm_status), + from_header->text); + has_dmarc_record = FALSE; + break; + } + + /* Store the policy string in an expandable variable. */ + + libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); + for (c = 0; dmarc_policy_description[c].name; c++) + if (tmp_ans == dmarc_policy_description[c].value) + { + dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name); + break; + } + + /* Can't use exim's string manipulation functions so allocate memory + for libopendmarc using its max hostname length definition. */ + + dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED); + libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx, + dmarc_domain, DMARC_MAXHOSTNAMELEN-1); + store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1); + dmarc_used_domain = dmarc_domain; + + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to read domainname used for DMARC lookup: %s", + opendmarc_policy_status_to_str(libdm_status)); + + dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx); + switch(libdm_status) + { + case DMARC_POLICY_ABSENT: /* No DMARC record found */ + dmarc_status = US"norecord"; + dmarc_pass_fail = US"none"; + dmarc_status_text = US"No DMARC record"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */ + dmarc_status = US"nofrom"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"No From: domain found"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_NONE: /* Accept and report */ + dmarc_status = US"none"; + dmarc_pass_fail = US"none"; + dmarc_status_text = US"None, Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_PASS: /* Explicit accept */ + dmarc_status = US"accept"; + dmarc_pass_fail = US"pass"; + dmarc_status_text = US"Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_REJECT: /* Explicit reject */ + dmarc_status = US"reject"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Reject"; + action = DMARC_RESULT_REJECT; + break; + case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ + dmarc_status = US"quarantine"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Quarantine"; + action = DMARC_RESULT_QUARANTINE; + break; + default: + dmarc_status = US"temperror"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"Internal Policy Error"; + action = DMARC_RESULT_TEMPFAIL; + break; + } + + libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s", + opendmarc_policy_status_to_str(libdm_status)); + + if (has_dmarc_record) + { + log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s " + "spf_align=%s dkim_align=%s enforcement='%s'", + spf_sender_domain, dmarc_used_domain, + sa==DMARC_POLICY_SPF_ALIGNMENT_PASS ?"yes":"no", + da==DMARC_POLICY_DKIM_ALIGNMENT_PASS ?"yes":"no", + dmarc_status_text); + history_file_status = dmarc_write_history_file(dkim_history_buffer); + /* Now get the forensic reporting addresses, if any */ + ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1); + dmarc_send_forensic_report(ruf); + } + } + +/* shut down libopendmarc */ +if (dmarc_pctx) + (void) opendmarc_policy_connect_shutdown(dmarc_pctx); +if (!f.dmarc_disable_verify) + (void) opendmarc_policy_library_shutdown(&dmarc_ctx); + +return OK; +} + +static uschar * +dmarc_exim_expand_defaults(int what) +{ +if (what == DMARC_VERIFY_STATUS) + return f.dmarc_disable_verify ? US"off" : US"none"; +return US""; +} + +static uschar * +dmarc_exim_expand_query(int what) +{ +if (f.dmarc_disable_verify || !dmarc_pctx) + return dmarc_exim_expand_defaults(what); + +if (what == DMARC_VERIFY_STATUS) + return dmarc_status; +return US""; +} + + +static gstring * +authres_dmarc(gstring * g) +{ +if (f.dmarc_has_been_checked) + { + int start = 0; /* Compiler quietening */ + DEBUG(D_acl) start = gstring_length(g); + g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail); + if (header_from_sender) + g = string_append(g, 2, US" header.from=", header_from_sender); + DEBUG(D_acl) debug_printf("DMARC:\tauthres '%.*s'\n", + gstring_length(g) - start - 3, g->s + start + 3); + } +else + DEBUG(D_acl) debug_printf("DMARC:\tno authres\n"); +return g; +} + +/******************************************************************************/ +/* Module API */ + +static optionlist dmarc_options[] = { + { "dmarc_forensic_sender", opt_stringptr, {&dmarc_forensic_sender} }, + { "dmarc_history_file", opt_stringptr, {&dmarc_history_file} }, + { "dmarc_tld_file", opt_stringptr, {&dmarc_tld_file} }, +}; + +static void * dmarc_functions[] = { + dmarc_process, + dmarc_exim_expand_query, + authres_dmarc, + dmarc_store_data, +}; + +/* dmarc_forensic_sender is provided for visibility of the the option setting +by moan_send_message. We do not document it as a config-visible $variable. +We could provide it via a function but there's little advantage. */ + +static var_entry dmarc_variables[] = { + { "dmarc_domain_policy", vtype_stringptr, &dmarc_domain_policy }, + { "dmarc_forensic_sender", vtype_stringptr, &dmarc_forensic_sender }, + { "dmarc_status", vtype_stringptr, &dmarc_status }, + { "dmarc_status_text", vtype_stringptr, &dmarc_status_text }, + { "dmarc_used_domain", vtype_stringptr, &dmarc_used_domain }, +}; + +misc_module_info dmarc_module_info = +{ + .name = US"dmarc", +# if SUPPORT_SPF==2 + .dyn_magic = MISC_MODULE_MAGIC, +# endif + .init = dmarc_init, + .lib_vers_report = dmarc_version_report, + .smtp_reset = dmarc_smtp_reset, + .msg_init = dmarc_msg_init, + + .options = dmarc_options, + .options_count = nelem(dmarc_options), + + .functions = dmarc_functions, + .functions_count = nelem(dmarc_functions), + + .variables = dmarc_variables, + .variables_count = nelem(dmarc_variables), +}; + +# endif /* SUPPORT_SPF */ +#endif /* SUPPORT_DMARC */ +/* vi: aw ai sw=2 + */ diff --git a/src/src/miscmods/dmarc.h b/src/src/miscmods/dmarc.h new file mode 100644 index 000000000..c1cafd0d1 --- /dev/null +++ b/src/src/miscmods/dmarc.h @@ -0,0 +1,57 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Experimental DMARC support. + Copyright (c) The Exim Maintainers 2021 - 2023 + Copyright (c) Todd Lyons 2012 - 2014 + License: GPL */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +#ifdef SUPPORT_DMARC + +# include +# ifdef SUPPORT_SPF +# include +# endif /* SUPPORT_SPF */ + +#define DMARC_VERIFY_STATUS 1 + +#define DMARC_HIST_OK 1 +#define DMARC_HIST_DISABLED 2 +#define DMARC_HIST_EMPTY 3 +#define DMARC_HIST_FILE_ERR 4 +#define DMARC_HIST_WRITE_ERR 5 + +/* From opendmarc.c */ +#define DMARC_RESULT_REJECT 0 +#define DMARC_RESULT_DISCARD 1 +#define DMARC_RESULT_ACCEPT 2 +#define DMARC_RESULT_TEMPFAIL 3 +#define DMARC_RESULT_QUARANTINE 4 + +/* From opendmarc-ar.h */ +/* ARES_RESULT_T -- type for specifying an authentication result */ +#define ARES_RESULT_UNDEFINED (-1) +#define ARES_RESULT_PASS 0 +#define ARES_RESULT_UNUSED 1 +#define ARES_RESULT_SOFTFAIL 2 +#define ARES_RESULT_NEUTRAL 3 +#define ARES_RESULT_TEMPERROR 4 +#define ARES_RESULT_PERMERROR 5 +#define ARES_RESULT_NONE 6 +#define ARES_RESULT_FAIL 7 +#define ARES_RESULT_POLICY 8 +#define ARES_RESULT_NXDOMAIN 9 +#define ARES_RESULT_SIGNED 10 +#define ARES_RESULT_UNKNOWN 11 +#define ARES_RESULT_DISCARD 12 + +#define DMARC_ARC_POLICY_RESULT_PASS 0 +#define DMARC_ARC_POLICY_RESULT_UNUSED 1 +#define DMARC_ARC_POLICY_RESULT_FAIL 2 + +#endif /* SUPPORT_DMARC */ diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c index a7b6c6a8d..f28fd0cbf 100644 --- a/src/src/miscmods/spf.c +++ b/src/src/miscmods/spf.c @@ -287,29 +287,30 @@ return TRUE; messages on the same SMTP connection (that come from the same host with the same HELO string). -Return: Boolean success +Return: OK/FAIL */ -static BOOL -spf_conn_init(uschar * spf_helo_domain, uschar * spf_remote_addr) +static int +spf_conn_init(const uschar * spf_helo_domain, const uschar * spf_remote_addr) { DEBUG(D_receive) debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr); -if (!spf_server && !spf_init(NULL)) return FALSE; +if (!spf_server && !spf_init(NULL)) + return FAIL; if (SPF_server_set_rec_dom(spf_server, CS primary_hostname)) { DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n", primary_hostname); spf_server = NULL; - return FALSE; + return FAIL; } spf_request = SPF_request_new(spf_server); -if ( SPF_request_set_ipv4_str(spf_request, CS spf_remote_addr) - && SPF_request_set_ipv6_str(spf_request, CS spf_remote_addr) +if ( SPF_request_set_ipv4_str(spf_request, CCS spf_remote_addr) + && SPF_request_set_ipv6_str(spf_request, CCS spf_remote_addr) ) { DEBUG(D_receive) @@ -317,19 +318,19 @@ if ( SPF_request_set_ipv4_str(spf_request, CS spf_remote_addr) "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr); spf_server = NULL; spf_request = NULL; - return FALSE; + return FAIL; } -if (SPF_request_set_helo_dom(spf_request, CS spf_helo_domain)) +if (SPF_request_set_helo_dom(spf_request, CCS spf_helo_domain)) { DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n", spf_helo_domain); spf_server = NULL; spf_request = NULL; - return FALSE; + return FAIL; } -return TRUE; +return OK; } static void @@ -564,11 +565,9 @@ static optionlist spf_options[] = { }; static void * spf_functions[] = { - spf_conn_init, spf_process, authres_spf, spf_get_response, /* ugly; for dmarc */ - spf_smtp_reset, spf_lookup_open, spf_lookup_close, @@ -592,6 +591,8 @@ misc_module_info spf_module_info = # endif .init = spf_init, .lib_vers_report = spf_lib_version_report, + .conn_init = spf_conn_init, + .smtp_reset = spf_smtp_reset, .options = spf_options, .options_count = nelem(spf_options), diff --git a/src/src/moan.c b/src/src/moan.c index 19d29190b..08258f5d1 100644 --- a/src/src/moan.c +++ b/src/src/moan.c @@ -178,8 +178,7 @@ header From: and grab the address from that for the envelope FROM. */ GET_OPTION("dmarc_forensic_sender"); if ( ident == ERRMESS_DMARC_FORENSIC - && dmarc_forensic_sender - && (s = expand_string(dmarc_forensic_sender)) + && (s = expand_string(US"$dmarc_forensic_sender")) /* a hack... */ && *s && (s2 = expand_string(string_sprintf("${address:%s}", s))) && *s2 diff --git a/src/src/readconf.c b/src/src/readconf.c index 1fe6b7341..ae7073229 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -129,9 +129,9 @@ static optionlist optionlist_config[] = { { "dkim_verify_signers", opt_stringptr, {&dkim_verify_signers} }, #endif #ifdef SUPPORT_DMARC - { "dmarc_forensic_sender", opt_stringptr, {&dmarc_forensic_sender} }, - { "dmarc_history_file", opt_stringptr, {&dmarc_history_file} }, - { "dmarc_tld_file", opt_stringptr, {&dmarc_tld_file} }, + { "dmarc_forensic_sender", opt_module, {US"dmarc"} }, + { "dmarc_history_file", opt_module, {US"dmarc"} }, + { "dmarc_tld_file", opt_module, {US"dmarc"} }, #endif { "dns_again_means_nonexist", opt_stringptr, {&dns_again_means_nonexist} }, { "dns_check_names_pattern", opt_stringptr, {&check_dns_names_pattern} }, @@ -1707,7 +1707,7 @@ static BOOL readconf_handle_option(uschar *buffer, optionlist *oltop, int last, void *data_block, uschar *unknown_txt) { -int ptr = 0; +int ptr; int offset = 0; int count, type, value; int issecure = 0; @@ -1721,11 +1721,16 @@ rmark reset_point; int intbase = 0; uschar *inttype = US""; uschar *sptr; -const uschar * s = buffer; +const uschar * s; uschar **str_target; uschar name[EXIM_DRIVERNAME_MAX]; uschar name2[EXIM_DRIVERNAME_MAX]; +sublist: + +s = buffer; +ptr = 0; + /* There may be leading spaces; thereafter, we expect an option name starting with a letter. */ @@ -1764,8 +1769,6 @@ if (Ustrncmp(name, "not_", 4) == 0) offset = 4; } -sublist: - /* Search the list for the given name. A non-existent name, or an option that is set twice, is a disaster. */ @@ -2454,7 +2457,6 @@ switch (type) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "failed to find %s module for %s: %s", US ol->v.value, name, errstr); -debug_printf("hunting for option %s in module %s\n", name, mi->name); oltop = mi->options; last = mi->options_count; goto sublist; @@ -3516,6 +3518,9 @@ if (!*spool_directory) /* Expand the spool directory name; it may, for example, contain the primary host name. Same comment about failure. */ +DEBUG(D_any) if (Ustrchr(spool_directory, '$')) + debug_printf("Expanding spool_directory option\n"); + if (!(s = expand_string(spool_directory))) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to expand spool_directory " "\"%s\": %s", spool_directory, expand_string_message); diff --git a/src/src/receive.c b/src/src/receive.c index 336b37410..37b152f48 100644 --- a/src/src/receive.c +++ b/src/src/receive.c @@ -17,7 +17,7 @@ extern int dcc_ok; #endif #ifdef SUPPORT_DMARC -# include "dmarc.h" +# include "miscmods/dmarc.h" #endif /************************************************* @@ -1835,9 +1835,8 @@ if (smtp_input && !smtp_batched_input && !f.dkim_disable_verify) dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED); #endif -#ifdef SUPPORT_DMARC -if (sender_host_address) dmarc_conn_init(); /* initialize libopendmarc */ -#endif +if (misc_mod_msg_init() != OK) + goto TIDYUP; /* In SMTP sessions we may receive several messages in one connection. Before each subsequent one, we wait for the clock to tick at the level of message-id @@ -3627,7 +3626,14 @@ else #endif /* WITH_CONTENT_SCAN */ #ifdef SUPPORT_DMARC - dmarc_store_data(dmarc_from_header); + { + misc_module_info * mi = misc_mod_findonly(US"dmarc"); + if (mi) + { + typedef int (*fn_t)(header_line *); + (((fn_t *) mi->functions)[3]) (dmarc_from_header); + } + } #endif #ifndef DISABLE_PRDR diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c index f9bd3ece8..adf6c59cb 100644 --- a/src/src/smtp_in.c +++ b/src/src/smtp_in.c @@ -1681,16 +1681,6 @@ bmi_run = 0; bmi_verdicts = NULL; #endif dnslist_domain = dnslist_matched = NULL; -#ifdef SUPPORT_SPF - { - misc_module_info * mi = misc_mod_findonly(US"spf"); - if (mi) - { - typedef void (*fn_t)(void); - (((fn_t *) mi->functions)[4])(); /* spf_smtp_reset*/ - } - } -#endif #ifndef DISABLE_DKIM dkim_cur_signer = dkim_signers = dkim_signing_domain = dkim_signing_selector = dkim_signatures = NULL; @@ -1701,8 +1691,6 @@ dkim_key_length = 0; #endif #ifdef SUPPORT_DMARC f.dmarc_has_been_checked = f.dmarc_disable_verify = f.dmarc_enable_forensic = FALSE; -dmarc_domain_policy = dmarc_status = dmarc_status_text = -dmarc_used_domain = NULL; #endif #ifdef EXPERIMENTAL_ARC arc_state = arc_state_reason = NULL; @@ -1742,6 +1730,7 @@ while (acl_warn_logged) store_free(this); } +misc_mod_smtp_reset(); message_tidyup(); store_reset(reset_point); @@ -4025,24 +4014,14 @@ while (done <= 0) } } -#ifdef SUPPORT_SPF - /* If we have an spf module, set up SPF context */ - { - misc_module_info * mi = misc_mod_findonly(US"spf"); - if (mi) + /* For any misc-module having a connection-init routine, call it. */ + + if (misc_mod_conn_init(sender_helo_name, sender_host_address) != OK) { - /* We have hardwired function-call numbers, and also prototypes for the - functions. We could do a function name table search for the number - but I can't see how to deal with prototypes. Is a K&R non-prototyped - function still usable with today's compilers? */ - - typedef BOOL (*fn_t)(uschar *, uschar *); - fn_t fn = ((fn_t *) mi->functions)[0]; /* spf_conn_init */ - - (void) fn(sender_helo_name, sender_host_address); + DEBUG(D_receive) debug_printf("A module conn-init routine failed\n"); + done = 1; + break; } - } -#endif /* Apply an ACL check if one is defined; afterwards, recheck synchronization in case the client started sending in a delay. */ diff --git a/src/src/structs.h b/src/src/structs.h index 2c8c77c43..46abac728 100644 --- a/src/src/structs.h +++ b/src/src/structs.h @@ -1023,6 +1023,9 @@ typedef struct misc_module_info { unsigned dyn_magic; BOOL (*init)(void *); /* arg is the misc_module_info ptr */ gstring * (*lib_vers_report)(gstring *); /* underlying library */ + int (*conn_init)(const uschar *, const uschar *); + void (*smtp_reset)(void); + int (*msg_init)(void); void * options; unsigned options_count; diff --git a/test/runtest b/test/runtest index dcf6d76b2..ae227810c 100755 --- a/test/runtest +++ b/test/runtest @@ -1561,8 +1561,8 @@ RESET_AFTER_EXTRA_LINE_READ: # Not all platforms build with SPF enabled next if /(^$time_pid?spf_conn_init|spf_compile\.c)/; next if /try option spf_smtp_comment_template$/; - next if /loading module 'spf'$/; - next if /^Loaded "spf"$/; + next if /loading module '(?:dmarc|spf)'$/; + next if /^$time_pid?Loaded "(?:dmarc|spf)"$/; # Not all platforms have sendfile support next if /^cannot use sendfile for body: no support$/; diff --git a/test/stderr/0437 b/test/stderr/0437 index 29241cfd3..514a770f8 100644 --- a/test/stderr/0437 +++ b/test/stderr/0437 @@ -1,5 +1,6 @@ Exim version x.yz .... Hints DB: +Expanding spool_directory option search_open: lsearch "TESTSUITE/aux-fixed/0437.ls" search_find: file="TESTSUITE/aux-fixed/0437.ls" key="spool" partial=-1 affix=NULL starflags=0 opts=NULL