Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions libopendmarc/dmarc.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ extern "C" {
#define DMARC_RECORD_FO_1 (0x2) /* fo, a bitmap */
#define DMARC_RECORD_FO_D (0x4) /* fo, a bitmap */
#define DMARC_RECORD_FO_S (0x8) /* fo, a bitmap */
#define DMARC_RECORD_T_UNSPECIFIED ('\0') /* t, RFC 9989 test mode; absent behaves as "n" */
#define DMARC_RECORD_T_N ('n') /* t */
#define DMARC_RECORD_T_Y ('y') /* t */

#define DMARC_PARSE_OKAY (0) /* Okay to continue */
#define DMARC_PARSE_ERROR_EMPTY (1) /* Nothing to parse */
Expand Down Expand Up @@ -161,6 +164,7 @@ OPENDMARC_STATUS_T opendmarc_policy_fetch_adkim(DMARC_POLICY_T *pctx, int *adkim
OPENDMARC_STATUS_T opendmarc_policy_fetch_aspf(DMARC_POLICY_T *pctx, int *aspf);
OPENDMARC_STATUS_T opendmarc_policy_fetch_p(DMARC_POLICY_T *pctx, int *p);
OPENDMARC_STATUS_T opendmarc_policy_fetch_sp(DMARC_POLICY_T *pctx, int *sp);
OPENDMARC_STATUS_T opendmarc_policy_fetch_t(DMARC_POLICY_T *pctx, int *t);
OPENDMARC_STATUS_T opendmarc_policy_fetch_fo(DMARC_POLICY_T *pctx, int *fo);
u_char ** opendmarc_policy_fetch_rua(DMARC_POLICY_T *pctx, u_char *list_buf, size_t size_of_buf, int constant);
u_char ** opendmarc_policy_fetch_ruf(DMARC_POLICY_T *pctx, u_char *list_buf, size_t size_of_buf, int constant);
Expand Down
1 change: 1 addition & 0 deletions libopendmarc/opendmarc_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ typedef struct dmarc_policy_t {
int sp;
int np; /* RFC 9989: non-existent subdomain policy */
int psd; /* RFC 9989: DMARC_RECORD_PSD_* */
int t; /* RFC 9989: DMARC_RECORD_T_*, test mode */
int pct;
int rf;
uint32_t ri;
Expand Down
25 changes: 25 additions & 0 deletions libopendmarc/opendmarc_policy.c
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,16 @@ opendmarc_policy_parse_dmarc(DMARC_POLICY_T *pctx, u_char *domain, u_char *recor
else
return DMARC_PARSE_ERROR_BAD_VALUE;
}
else if (strcasecmp((char *)cp, "t") == 0)
{
/* RFC 9989: test mode, replaces most of pct='s former role */
if (strncasecmp((char *)vp, "y", 1) == 0)
pctx->t = DMARC_RECORD_T_Y;
else if (strncasecmp((char *)vp, "n", 1) == 0)
pctx->t = DMARC_RECORD_T_N;
else
return DMARC_PARSE_ERROR_BAD_VALUE;
}
else if (strcasecmp((char *)cp, "adkim") == 0)
{
/*
Expand Down Expand Up @@ -1807,6 +1817,21 @@ opendmarc_policy_fetch_sp(DMARC_POLICY_T *pctx, int *sp)
return DMARC_PARSE_OKAY;
}

OPENDMARC_STATUS_T
opendmarc_policy_fetch_t(DMARC_POLICY_T *pctx, int *t)
{
if (pctx == NULL)
{
return DMARC_PARSE_ERROR_NULL_CTX;
}
if (t == NULL)
{
return DMARC_PARSE_ERROR_EMPTY;
}
*t = pctx->t;
return DMARC_PARSE_OKAY;
}

u_char **
opendmarc_policy_fetch_rua(DMARC_POLICY_T *pctx, u_char *list_buf, size_t size_of_buf, int constant)
{
Expand Down
39 changes: 38 additions & 1 deletion libopendmarc/tests/test_dmarc_fetch.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
int
main(int argc, char **argv)
{
static char *record= "v=DMARC1; p=none; sp=none; adkim=s; aspf=s; pct=50; ri=300; rf=afrf; rua=mailto:dmarc-a@abuse.net; ruf=mailto:dmarc-f@abuse.net";
static char *record= "v=DMARC1; p=none; sp=none; adkim=s; aspf=s; pct=50; t=y; ri=300; rf=afrf; rua=mailto:dmarc-a@abuse.net; ruf=mailto:dmarc-f@abuse.net";
DMARC_POLICY_T *pctx;
OPENDMARC_STATUS_T status;
int pass, fails, count;
int pct;
int adkim;
int aspf;
int t;

pass = fails = count = 0;
pctx = opendmarc_policy_connect_init((u_char *)"1.2.3.4", 0);
Expand Down Expand Up @@ -60,6 +61,17 @@ main(int argc, char **argv)
printf("\t%s(%d): opendmarc_policy_fetch_adkim: expected %d got %d: FAIL\n", __FILE__, __LINE__, DMARC_RECORD_A_STRICT, aspf);
fails += 1;
}
status = opendmarc_policy_fetch_t(pctx, &t);
if (status != DMARC_PARSE_OKAY)
{
printf("\t%s(%d): opendmarc_policy_fetch_t: %s: FAIL\n", __FILE__, __LINE__, opendmarc_policy_status_to_str(status));
fails += 1;
}
if (t != DMARC_RECORD_T_Y)
{
printf("\t%s(%d): opendmarc_policy_fetch_t: expected %d got %d: FAIL\n", __FILE__, __LINE__, DMARC_RECORD_T_Y, t);
fails += 1;
}

/*
** Regression tests for issue #256: opendmarc_policy_fetch_ruf() and
Expand Down Expand Up @@ -115,5 +127,30 @@ main(int argc, char **argv)
}
}

pctx = opendmarc_policy_connect_shutdown(pctx);

/* RFC 9989: t= absent defaults to "n" behavior (DMARC_RECORD_T_UNSPECIFIED) */
count++;
pctx = opendmarc_policy_connect_init((u_char *)"1.2.3.4", 0);
if (pctx == NULL)
{
(void) fprintf(stderr, "opendmarc_policy_connect_init: %s\n", strerror(errno));
return 1;
}
status = opendmarc_policy_parse_dmarc(pctx, (u_char *)"abuse.net",
(u_char *)"v=DMARC1; p=reject;");
if (status != DMARC_PARSE_OKAY)
{
printf("\t%s(%d): opendmarc_policy_parse_dmarc: %s: FAIL\n", __FILE__, __LINE__, opendmarc_policy_status_to_str(status));
fails += 1;
}
status = opendmarc_policy_fetch_t(pctx, &t);
if (status != DMARC_PARSE_OKAY || t != DMARC_RECORD_T_UNSPECIFIED)
{
printf("\t%s(%d): opendmarc_policy_fetch_t: expected unspecified, got %d: FAIL\n", __FILE__, __LINE__, t);
fails += 1;
}
pctx = opendmarc_policy_connect_shutdown(pctx);

return fails;
}
3 changes: 3 additions & 0 deletions libopendmarc/tests/test_dmarc_parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ main(int argc, char **argv)
/* 17 */ {"v=DMARC1; p=none; ruf=mailto://abuse.com", DMARC_PARSE_OKAY},
/* 18 */ {"v=DMARC1; p=none; ruf=mailto://abuse.com; foo=bar; buzz=happy;", DMARC_PARSE_OKAY},
/* 19 */ {"v=DMARC1; p=none; rf=000000000000000000000000000000000", DMARC_PARSE_ERROR_BAD_VALUE},
/* 20 */ {"v=DMARC1; p=reject; t=y;", DMARC_PARSE_OKAY},
/* 21 */ {"v=DMARC1; p=reject; t=n;", DMARC_PARSE_OKAY},
/* 22 */ {"v=DMARC1; p=reject; t=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
{NULL, 0},
};
int pass, fails, count;
Expand Down
34 changes: 31 additions & 3 deletions opendmarc/opendmarc.c
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ struct dmarcf_config
_Bool conf_ignoreauthclients;
_Bool conf_holdquarantinedmessages;
_Bool conf_reject_multi_from;
_Bool conf_ignorepct;
unsigned int conf_refcnt;
unsigned int conf_dnstimeout;
struct config * conf_data;
Expand Down Expand Up @@ -1283,6 +1284,10 @@ dmarcf_config_load(struct config *data, struct dmarcf_config *conf,
&conf->conf_rejectfail,
sizeof conf->conf_rejectfail);

(void) config_get(data, "DMARCbisIgnorePct",
&conf->conf_ignorepct,
sizeof conf->conf_ignorepct);

(void) config_get(data, "RejectString",
&conf->conf_rejectstring,
sizeof conf->conf_rejectstring);
Expand Down Expand Up @@ -2270,13 +2275,15 @@ mlfi_eom(SMFICTX *ctx)
int c;
int pc;
int policy;
int enforce_policy;
int skiphistory = 0;
int status;
int adkim;
int aspf;
int pct;
int p;
int sp;
int t;
int align_dkim;
int align_spf;
int limit_arc = 0;
Expand Down Expand Up @@ -3316,6 +3323,8 @@ mlfi_eom(SMFICTX *ctx)
opendmarc_policy_fetch_sp(cc->cctx_dmarc, &sp);
dmarcf_dstring_printf(dfc->mctx_histbuf, "sp %d\n", sp);

opendmarc_policy_fetch_t(cc->cctx_dmarc, &t);

{
int fo = DMARC_RECORD_FO_UNSPECIFIED;
opendmarc_policy_fetch_fo(cc->cctx_dmarc, &fo);
Expand Down Expand Up @@ -3349,12 +3358,31 @@ mlfi_eom(SMFICTX *ctx)

/*
** Enact policy based on DMARC results.
**
** RFC 9989 S 3.2.7/Appendix A.6: t=y is a request that the policy
** actually applied be one level below the published Domain Owner
** Assessment Policy ("reject" -> "quarantine", "quarantine" -> "none"),
** not a request to suppress enforcement outright. It has no effect
** when the policy is already "none". This is kept in a separate
** variable rather than overwriting "policy": t= "does not affect the
** generation of DMARC reports" per RFC 9989, and "policy" is read
** again below (failure report eligibility) where the unadjusted,
** published policy must still apply.
*/

enforce_policy = policy;
if (t == DMARC_RECORD_T_Y)
{
if (enforce_policy == DMARC_POLICY_REJECT)
enforce_policy = DMARC_POLICY_QUARANTINE;
else if (enforce_policy == DMARC_POLICY_QUARANTINE)
enforce_policy = DMARC_POLICY_NONE;
}

result = DMARC_RESULT_ACCEPT;
ret = SMFIS_ACCEPT;

switch (policy)
switch (enforce_policy)
{
case DMARC_POLICY_ABSENT: /* No DMARC record found */
case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */
Expand All @@ -3374,7 +3402,7 @@ mlfi_eom(SMFICTX *ctx)
ret = SMFIS_CONTINUE;

if (conf->conf_rejectfail &&
random() % 100 < pct)
(conf->conf_ignorepct || random() % 100 < pct))
{
if (strstr(conf->conf_rejectstring, "%s") != NULL)
snprintf((char *)replybuf, sizeof replybuf,
Expand Down Expand Up @@ -3412,7 +3440,7 @@ mlfi_eom(SMFICTX *ctx)
ret = SMFIS_CONTINUE;

if (conf->conf_holdquarantinedmessages &&
random() % 100 < pct)
(conf->conf_ignorepct || random() % 100 < pct))
{
/* quarantine will be deferred until after the ARC policy eval */

Expand Down
30 changes: 30 additions & 0 deletions opendmarc/opendmarc.conf.5.in
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,36 @@ is unchanged from before this setting existed.
.IR Auto 's
own PSL-then-RFC7489 fallback behaviour is unaffected by this setting.

.TP
.I DMARCbisIgnorePct (Boolean)
RFC 9989 removes the
.I pct
tag. By default (false), this filter keeps honoring it exactly as before:
a published
.I pct
value gates whether
.I RejectFailures
or
.I HoldQuarantinedMessages
actually act on a given message, via a per-message random draw against
.IR pct .
This default exists because
.I pct=0
and
.I pct=100
remain common in real published DMARC records (notably as a Domain
Owner's deliberate way to publish a stricter
.I p
value, such as during pre-production testing, without yet enforcing it).
Setting this to true ignores
.I pct
entirely, equivalent to treating every record as
.IR pct=100 ,
for full RFC 9989 compliance. This does not affect the
.I t
tag, which is honored unconditionally per RFC 9989 regardless of this
setting.

.TP
.I DNSTimeout (integer)
Sets the DNS timeout in seconds. A value of 0 causes an infinite wait.
Expand Down
17 changes: 17 additions & 0 deletions opendmarc/opendmarc.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,23 @@
#
# DMARCbisWalkModeFallback RFC9989

## DMARCbisIgnorePct { true | false }
## default false
##
## RFC 9989 removes the pct tag. By default, this filter keeps honoring
## it exactly as before: a published pct value gates whether
## RejectFailures or HoldQuarantinedMessages actually act on a given
## message, via a per-message random draw against pct. This default
## exists because pct=0 and pct=100 remain common in real published
## DMARC records (notably as a Domain Owner's deliberate way to publish a
## stricter p value, such as during pre-production testing, without yet
## enforcing it). Setting this to true ignores pct entirely, equivalent
## to treating every record as pct=100, for full RFC 9989 compliance.
## This does not affect the t tag, which is honored unconditionally per
## RFC 9989 regardless of this setting.
#
# DMARCbisIgnorePct false

## DNSTimeout (integer)
## default 5
##
Expand Down
Loading