From 945986000ece3ceb82a764737c899c2dc0b10ee0 Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sun, 21 Jun 2026 14:57:07 -0400 Subject: [PATCH] feat: add walk-mode selection and cross-mode comparison to opendmarc-check Adds -m to opendmarc-check to run a single DMARCbis walk mode (auto/psl/rfc7489/rfc9989), -p to load a Public Suffix List file (PSL mode and auto otherwise have nothing to query), and -a to query a domain under all four modes in one pass and print a comparison line per mode (status, org domain, p=, sp=, whether the RFC 7489 fallback walk was used). This exercises the actual opendmarc_policy_query_dmarc() path the milter uses, not a re-implementation, so it can be used to spot real divergence between walk modes for a given label rather than relying on the Perl survey scripts in contrib/. Extracted policy_to_str()/alignment_to_str() helpers out of the existing inline switch statements so both the classic single-domain report and the new comparison output share one source of truth for p=/sp=/adkim=/aspf= string conversion. --- opendmarc/opendmarc-check.8.in | 42 ++++ opendmarc/opendmarc-check.c | 339 +++++++++++++++++++++++---------- 2 files changed, 285 insertions(+), 96 deletions(-) diff --git a/opendmarc/opendmarc-check.8.in b/opendmarc/opendmarc-check.8.in index 9265b5ea..eddc202d 100644 --- a/opendmarc/opendmarc-check.8.in +++ b/opendmarc/opendmarc-check.8.in @@ -4,6 +4,8 @@ \- DMARC record check tool .SH SYNOPSIS .B opendmarc-check +[\-p pslfile] +[\-m mode | \-a] domain [domain [...]] .SH DESCRIPTION .B opendmarc-check @@ -11,6 +13,46 @@ queries the DNS for a DMARC record for the named domain(s) and then translates the content found to a human-readable form. This can be used to ensure that the DMARC policy you have placed in the nameserver is what you intended for others to see. + +.B opendmarc-check +uses the same libopendmarc query path as the milter itself, so its output +reflects exactly what the filter would do for a given domain, including +which organizational domain it would land on and by what mode. +.SH OPTIONS +.TP +.I \-p pslfile +Load the named Public Suffix List file before querying. Required for the +.I psl +walk mode to find an organizational domain; also affects +.I auto +mode. Without this option no PSL is loaded, matching the longstanding +default behavior of this tool. +.TP +.I \-m mode +Use the named walk mode to determine the organizational domain when the +queried domain itself has no DMARC record. +.I mode +is one of +.IR auto " (the default)," +.IR psl ", " rfc7489 ", or " rfc9989 . +See +.I DMARCbisWalkMode +in +.I opendmarc.conf(5) +for what each mode does. Mutually exclusive with +.I \-a. +.TP +.I \-a +For each domain, query using all four walk modes in turn and print one +line of results per mode, instead of the normal detailed single-mode +report. Useful for comparing how +.I psl, +.I rfc7489, +and +.I rfc9989 +diverge or agree on a given domain's organizational domain and policy. +Mutually exclusive with +.I \-m. .SH VERSION This man page covers version @VERSION@ of .I opendmarc. diff --git a/opendmarc/opendmarc-check.c b/opendmarc/opendmarc-check.c index ca20183f..28a53401 100644 --- a/opendmarc/opendmarc-check.c +++ b/opendmarc/opendmarc-check.c @@ -11,6 +11,7 @@ #include #include #include +#include /* libbsd if found */ #ifdef USE_BSD_H @@ -42,6 +43,117 @@ /* globals */ char *progname; +/* +** USAGE -- print a usage message +*/ + +static void +usage(void) +{ + fprintf(stderr, + "%s: usage: %s [-p pslfile] [-m mode | -a] domain [domain ...]\n" + "\t-p pslfile path to a Public Suffix List file\n" + "\t-m mode walk mode: auto (default), psl, rfc7489, rfc9989\n" + "\t-a query all four walk modes per domain and compare\n", + progname, progname); +} + +/* +** PARSE_WALK_MODE -- translate a mode name to an OPENDMARC_WALK_MODE_* value +** +** Returns TRUE on success, FALSE if the name is not recognized. +*/ + +static int +parse_walk_mode(const char *name, int *modep) +{ + if (strcasecmp(name, "auto") == 0) + *modep = OPENDMARC_WALK_MODE_AUTO; + else if (strcasecmp(name, "psl") == 0) + *modep = OPENDMARC_WALK_MODE_PSL; + else if (strcasecmp(name, "rfc7489") == 0) + *modep = OPENDMARC_WALK_MODE_RFC7489; + else if (strcasecmp(name, "rfc9989") == 0) + *modep = OPENDMARC_WALK_MODE_RFC9989; + else + return FALSE; + + return TRUE; +} + +/* +** WALK_MODE_TO_STR -- name of an OPENDMARC_WALK_MODE_* value +*/ + +static const char * +walk_mode_to_str(int mode) +{ + switch (mode) + { + case OPENDMARC_WALK_MODE_PSL: + return "psl"; + + case OPENDMARC_WALK_MODE_RFC7489: + return "rfc7489"; + + case OPENDMARC_WALK_MODE_RFC9989: + return "rfc9989"; + + case OPENDMARC_WALK_MODE_AUTO: + default: + return "auto"; + } +} + +/* +** POLICY_TO_STR -- human-readable name of a DMARC_RECORD_P_* value +*/ + +static const char * +policy_to_str(int p) +{ + switch (p) + { + case DMARC_RECORD_P_NONE: + return "none"; + + case DMARC_RECORD_P_QUARANTINE: + return "quarantine"; + + case DMARC_RECORD_P_REJECT: + return "reject"; + + case DMARC_RECORD_P_UNSPECIFIED: + return "unspecified"; + + default: + return "unknown"; + } +} + +/* +** ALIGNMENT_TO_STR -- human-readable name of a DMARC_RECORD_A_* value +*/ + +static const char * +alignment_to_str(int a) +{ + switch (a) + { + case DMARC_RECORD_A_STRICT: + return "strict"; + + case DMARC_RECORD_A_RELAXED: + return "relaxed"; + + case DMARC_RECORD_A_UNSPECIFIED: + return "unspecified"; + + default: + return "unknown"; + } +} + /* ** MAIN -- program mainline ** @@ -56,24 +168,77 @@ int main(int argc, char **argv) { int c; + int i; int n; + int ch; int pct; + int compare_all = FALSE; + int mode_given = FALSE; + int walk_mode = OPENDMARC_WALK_MODE_AUTO; + char *pslfile = NULL; OPENDMARC_STATUS_T status; char *p; - char *sp; - char *adkim; - char *aspf; + const char *adkim; + const char *aspf; + const char *pstr; + const char *spstr; unsigned char **rua; unsigned char **ruf; DMARC_POLICY_T *dmarc; OPENDMARC_LIB_T lib; + static const int all_modes[] = { OPENDMARC_WALK_MODE_AUTO, + OPENDMARC_WALK_MODE_PSL, + OPENDMARC_WALK_MODE_RFC7489, + OPENDMARC_WALK_MODE_RFC9989 }; progname = (p = strrchr(argv[0], '/')) == NULL ? argv[0] : p + 1; - if (argc == 1) + while ((ch = getopt(argc, argv, "am:p:h")) != -1) { - fprintf(stderr, "%s: usage: %s [domain [...]]\n", progname, - progname); + switch (ch) + { + case 'a': + compare_all = TRUE; + break; + + case 'm': + if (!parse_walk_mode(optarg, &walk_mode)) + { + fprintf(stderr, "%s: unknown walk mode '%s'\n", + progname, optarg); + usage(); + + return EX_USAGE; + } + mode_given = TRUE; + break; + + case 'p': + pslfile = optarg; + break; + + case 'h': + default: + usage(); + + return EX_USAGE; + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) + { + usage(); + + return EX_USAGE; + } + + if (compare_all && mode_given) + { + fprintf(stderr, "%s: -m and -a are mutually exclusive\n", progname); + usage(); return EX_USAGE; } @@ -81,6 +246,13 @@ main(int argc, char **argv) memset(&lib, '\0', sizeof lib); lib.tld_type = OPENDMARC_TLD_TYPE_NONE; lib.nscount = 0; + if (pslfile != NULL) + { + lib.tld_type = OPENDMARC_TLD_TYPE_MOZILLA; + (void) strlcpy((char *)lib.tld_source_file, pslfile, + sizeof lib.tld_source_file); + } + lib.walk_mode = walk_mode; status = opendmarc_policy_library_init(&lib); if (status != DMARC_PARSE_OKAY) @@ -101,18 +273,71 @@ main(int argc, char **argv) return EX_SOFTWARE; } - for (c = 1; c < argc; c++) + for (c = 0; c < argc; c++) { + if (c != 0) + fprintf(stdout, "\n"); + + if (compare_all) + { + fprintf(stdout, "%s\n", argv[c]); + + for (i = 0; i < (int) (sizeof all_modes / sizeof all_modes[0]); i++) + { + u_char orgbuf[256]; + int fallback = 0; + int p_n = DMARC_RECORD_P_UNSPECIFIED; + int sp_n = DMARC_RECORD_P_UNSPECIFIED; + + /* + * Re-init with the next mode; this exercises the same + * connect/store/query path the milter uses, just with + * walk_mode swapped between runs. + */ + lib.walk_mode = all_modes[i]; + (void) opendmarc_policy_library_init(&lib); + (void) opendmarc_policy_connect_rset(dmarc); + + status = opendmarc_policy_store_from_domain(dmarc, + (u_char *) argv[c]); + if (status == DMARC_PARSE_OKAY) + status = opendmarc_policy_query_dmarc(dmarc, NULL); + + memset(orgbuf, '\0', sizeof orgbuf); + if (status == DMARC_PARSE_OKAY) + { + (void) opendmarc_policy_fetch_utilized_domain(dmarc, + orgbuf, + sizeof orgbuf); + (void) opendmarc_policy_fetch_org_domain_from_fallback(dmarc, + &fallback); + (void) opendmarc_policy_fetch_p(dmarc, &p_n); + (void) opendmarc_policy_fetch_sp(dmarc, &sp_n); + } + + fprintf(stdout, + " %-8s %-26s org=%-28s p=%-12s sp=%-12s fallback=%s\n", + walk_mode_to_str(all_modes[i]), + opendmarc_policy_status_to_str(status), + status == DMARC_PARSE_OKAY ? (char *) orgbuf : "-", + status == DMARC_PARSE_OKAY ? policy_to_str(p_n) : "-", + status == DMARC_PARSE_OKAY ? policy_to_str(sp_n) : "-", + fallback ? "yes" : "no"); + } + + continue; + } + (void) opendmarc_policy_connect_rset(dmarc); - status = opendmarc_policy_store_from_domain(dmarc, (u_char *)argv[c]); + status = opendmarc_policy_store_from_domain(dmarc, (u_char *) argv[c]); if (status != DMARC_PARSE_OKAY) { fprintf(stderr, "%s: opendmarc_policy_store_from_domain(%s): %s\n", progname, argv[c], opendmarc_policy_status_to_str(status)); - + return EX_SOFTWARE; } @@ -123,102 +348,24 @@ main(int argc, char **argv) "%s: opendmarc_policy_query_dmarc(%s): %s\n", progname, argv[c], opendmarc_policy_status_to_str(status)); - + return EX_SOFTWARE; } - if (c != 1) - fprintf(stdout, "\n"); - - (void) opendmarc_policy_fetch_pct(dmarc, &pct); + (void) opendmarc_policy_fetch_pct(dmarc, &n); + pct = n; (void) opendmarc_policy_fetch_adkim(dmarc, &n); - switch (n) - { - case DMARC_RECORD_A_UNSPECIFIED: - adkim = "unspecified"; - break; - - case DMARC_RECORD_A_STRICT: - adkim = "strict"; - break; - - case DMARC_RECORD_A_RELAXED: - adkim = "relaxed"; - break; - - default: - adkim = "unknown"; - break; - } + adkim = alignment_to_str(n); (void) opendmarc_policy_fetch_aspf(dmarc, &n); - switch (n) - { - case DMARC_RECORD_A_UNSPECIFIED: - aspf = "unspecified"; - break; - - case DMARC_RECORD_A_STRICT: - aspf = "strict"; - break; - - case DMARC_RECORD_A_RELAXED: - aspf = "relaxed"; - break; - - default: - aspf = "unknown"; - break; - } + aspf = alignment_to_str(n); (void) opendmarc_policy_fetch_p(dmarc, &n); - switch (n) - { - case DMARC_RECORD_P_UNSPECIFIED: - p = "unspecified"; - break; - - case DMARC_RECORD_P_NONE: - p = "none"; - break; - - case DMARC_RECORD_P_QUARANTINE: - p = "quarantine"; - break; - - case DMARC_RECORD_P_REJECT: - p = "reject"; - break; - - default: - p = "unknown"; - break; - } + pstr = policy_to_str(n); (void) opendmarc_policy_fetch_sp(dmarc, &n); - switch (n) - { - case DMARC_RECORD_P_UNSPECIFIED: - sp = "unspecified"; - break; - - case DMARC_RECORD_P_NONE: - sp = "none"; - break; - - case DMARC_RECORD_P_QUARANTINE: - sp = "quarantine"; - break; - - case DMARC_RECORD_P_REJECT: - sp = "reject"; - break; - - default: - sp = "unknown"; - break; - } + spstr = policy_to_str(n); rua = opendmarc_policy_fetch_rua(dmarc, NULL, 0, 1); ruf = opendmarc_policy_fetch_ruf(dmarc, NULL, 0, 1); @@ -227,8 +374,8 @@ main(int argc, char **argv) fprintf(stdout, "\tSample percentage: %d\n", pct); fprintf(stdout, "\tDKIM alignment: %s\n", adkim); fprintf(stdout, "\tSPF alignment: %s\n", aspf); - fprintf(stdout, "\tDomain policy: %s\n", p); - fprintf(stdout, "\tSubdomain policy: %s\n", sp); + fprintf(stdout, "\tDomain policy: %s\n", pstr); + fprintf(stdout, "\tSubdomain policy: %s\n", spstr); fprintf(stdout, "\tAggregate report URIs:\n"); for (n = 0; rua != NULL && rua[n] != NULL; n++) fprintf(stdout, "\t\t%s\n", rua[n]);