Qualifier handling on non-all mechanisms |
BROKEN |
Yes |
Core #169 bug. -ip4:1.2.3.4 returns hard-fail even when 1.2.3.4 does NOT match. Code saves prefix but only applies it correctly for all. For every other mechanism, a non-match at the top level with prefix=='-' returns DASH_FORCED_HARD_FAIL instead of continuing. |
all with qualifiers (-/~/?) |
Yes |
Yes |
Only mechanism handled correctly. |
| Multiple v=spf1 TXT records = permerror |
No |
Yes |
opendmarc_spf_dns_get_record returns the first matching TXT record and discards the rest. RFC 7208 sec.3.2 requires permerror. Core #304 bug. |
| 10 DNS lookup limit |
Yes |
Yes |
MAX_SPF_DNS_LOOKUPS=10, checked before each lookup. |
| Void lookup limit (max 2, RFC sec.4.6.4) |
No |
Likely yes |
Only total lookups tracked. No void counter. #304 bug. |
ip4 mechanism - basic match |
Partial |
Yes |
Matches correctly, but qualifier ignored: -ip4:x matching yields pass instead of fail. |
ip6 mechanism - basic match |
Partial |
Yes |
Same qualifier problem as ip4. |
a mechanism - A/AAAA lookup |
Partial |
Yes |
Looks up A and AAAA, but CIDR on the a mechanism is only single IPv4. |
a/cidr (IPv4 prefix only) |
Partial |
Yes |
CIDR applied via opendmarc_spf_cidr_address, but only IPv4. |
a//cidr6 dual CIDR (RFC sec.5.3) |
No |
Yes |
Comment in source: "Don't know what to do with a/24." |
mx mechanism |
Partial |
Yes |
Looks up MX then A; no dual CIDR; qualifier ignored on non-match. |
mx//cidr6 dual CIDR (RFC sec.5.4) |
No |
Yes |
Not implemented. |
| MX host cap (10 max, RFC sec.5.4) |
No |
Yes |
Iterates all MX results with no limit. |
ptr mechanism |
Partial |
Yes |
PTR + forward-confirm logic exists, but PTR hostname cap (10 max, RFC sec.5.5) not enforced. |
include mechanism - basic recursion |
Partial |
Yes |
Stack-based recursion works; loop detection exists. |
include - qualifier applied to include result |
No |
Yes |
RFC sec.5.2: if included record results in fail, include mechanism does not match (processing continues); if neutral or softfail, also no match. Internal code recurses and returns pass-or-fail without honoring this. |
redirect modifier semantics (RFC sec.6.1) |
No |
Yes |
Code comment says "treating them the same [as include] is no harm." RFC sec.6.1 requires redirect to only activate if no mechanism matched, and the redirected record's result becomes the overall result directly. |
exists mechanism - basic |
Partial |
Yes |
Does an A lookup; no qualifier handling on the match result. |
| Unknown mechanism = permerror (RFC sec.4.7) |
No |
Yes |
Code logs "Unrecognized SPF keyword, WARNING" and continues. RFC requires permerror. |
| Macro %{s}, %{l}, %{o}, %{d}, %{h} |
Yes |
Yes |
|
| Macro %{i} for IPv6 |
Broken |
Yes |
For IPv6, RFC sec.7.3 requires dot-separated nibble format. Code passes raw IPv6 string. |
| Macro %{v} |
Broken |
Yes |
Hardcodes "in-addr" for all IP versions. For IPv6 senders it must be "ip6". |
| Macro %{p} |
Yes |
Yes |
PTR validated domain. |
| Macro %{c}, %{r}, %{t} |
Yes (exp-only) |
Yes |
Correctly restricted to exp= context. |
| Macro delimiter list (RFC sec.7.1) |
Partial |
Yes |
Code only splits on . and :. RFC allows ., -, +, ,, /, _, = as delimiters. |
| IPv4-mapped IPv6 (::ffff:x.x.x.x) |
No |
Uncertain |
Code comment: "we don't care at this point if it is ipv6 or ipv4". ip4 mechanisms won't match against an IPv6-form address. libspf2 wrapper calls both set_ipv4_str and set_ipv6_str with the same string, which likely handles this. |
none result (no record found) |
Yes |
Yes |
|
temperror result |
Yes |
Yes |
|
permerror result |
Partial |
Yes |
Internal has no permerror state; bad-syntax conditions map to SPF_RETURN_BAD_SYNTAX_* which are all treated as fail. |
| SPF2.0/SID record handling |
Yes (skip) |
Yes |
Correctly ignored for DMARC. |
Context
There are two code paths: the internal implementation in
libopendmarc/opendmarc_spf.c(compiled when libspf2 is absent, under!HAVE_SPF2_H) and the libspf2 wrapper (compiled whenHAVE_SPF2_H). The header-reading path inopendmarc/opendmarc-spf-parse.cis shared and correct.Related to issues #169 and #304.
Part 1: Internal vs libspf2 feature matrix
allmechanisms-ip4:1.2.3.4returns hard-fail even when1.2.3.4does NOT match. Code savesprefixbut only applies it correctly forall. For every other mechanism, a non-match at the top level withprefix=='-'returnsDASH_FORCED_HARD_FAILinstead of continuing.allwith qualifiers (-/~/?)opendmarc_spf_dns_get_recordreturns the first matching TXT record and discards the rest. RFC 7208 sec.3.2 requires permerror. Core #304 bug.MAX_SPF_DNS_LOOKUPS=10, checked before each lookup.ip4mechanism - basic match-ip4:xmatching yields pass instead of fail.ip6mechanism - basic matchamechanism - A/AAAA lookupamechanism is only single IPv4.a/cidr(IPv4 prefix only)opendmarc_spf_cidr_address, but only IPv4.a//cidr6dual CIDR (RFC sec.5.3)mxmechanismmx//cidr6dual CIDR (RFC sec.5.4)ptrmechanismincludemechanism - basic recursioninclude- qualifier applied to include resultfail,includemechanism does not match (processing continues); ifneutralorsoftfail, also no match. Internal code recurses and returns pass-or-fail without honoring this.redirectmodifier semantics (RFC sec.6.1)existsmechanism - basicexp=context..and:. RFC allows.,-,+,,,/,_,=as delimiters.ip4mechanisms won't match against an IPv6-form address. libspf2 wrapper calls bothset_ipv4_strandset_ipv6_strwith the same string, which likely handles this.noneresult (no record found)temperrorresultpermerrorresultpermerrorstate; bad-syntax conditions map toSPF_RETURN_BAD_SYNTAX_*which are all treated as fail.Part 2: What's missing from BOTH vs RFC 7208
RFC 7208 (April 2014) is the current normative SPF spec. RFC 8616 (2019) adds internationalized domain name support but only adjusts macro handling.
HELOidentity SPF check (RFC sec.2.3)MAIL FROMidentity. DMARC (RFC 7489 sec.4.1) requires MAIL FROM, so this is a deliberate scope limit, not a bug. Worth documenting.MAIL FROMnull sender handling (RFC sec.4.5.2)<>, HELO should be used as the identity. The internal code copieshelo_domainintomailfrom_domain. libspf2 wrapper setsenv_fromonly ifused_mfrom==TRUE; falls back to HELO. Both handle this but inconsistently.dns_countfor them.permerrorpropagation fromincludeincluded record yieldspermerror, the including record must also yieldpermerror. Internal returns fail. libspf2 wrapper maps any non-pass toDMARC_POLICY_SPF_OUTCOME_FAILwithout distinguishingpermerror.temperrorpropagation fromincludetemperrorfrom an included record must propagate astemperror. Internal wraps all errors as fail. libspf2 wrapper mapsSPF_RESULT_TEMPERRORtoTMPFAILfor the top-level result, but does not verify it propagates from includes.Summary
The internal implementation has two categories of defects: the qualifier-handling bug from #169 (prefix is parsed but not applied to mechanism match results - only applied on non-match at top level, which is the opposite of correct), and the structural gaps from #304 (void lookups, duplicate TXT records, dual CIDR). libspf2 handles all of these correctly. The only defect both share is the lack of
permerror/temperrorpropagation fromincludechains in the result-mapping wrapper, and missing DNS caching.