Skip to content

SPF implementation compliance matrix: internal vs libspf2 vs RFC 7208 #370

@thegushi

Description

@thegushi

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 when HAVE_SPF2_H). The header-reading path in opendmarc/opendmarc-spf-parse.c is shared and correct.

Related to issues #169 and #304.


Part 1: Internal vs libspf2 feature matrix

RFC 7208 Requirement Internal libspf2 Notes
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.

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.

RFC 7208 Requirement Both gap Notes
HELO identity SPF check (RFC sec.2.3) Partial Both evaluate MAIL FROM identity. DMARC (RFC 7489 sec.4.1) requires MAIL FROM, so this is a deliberate scope limit, not a bug. Worth documenting.
MAIL FROM null sender handling (RFC sec.4.5.2) Partial When MAIL FROM is <>, HELO should be used as the identity. The internal code copies helo_domain into mailfrom_domain. libspf2 wrapper sets env_from only if used_mfrom==TRUE; falls back to HELO. Both handle this but inconsistently.
DNS result caching between mechanisms No RFC sec.4.6.4 says implementations "SHOULD" cache. Neither the internal DNS layer nor the wrapper does any cross-mechanism caching within a single evaluation. Can cause redundant lookups and inflated lookup counts.
CNAME chaining counts against lookup limit Unknown RFC sec.4.6.4: every DNS lookup that follows a CNAME chain counts as 1. The internal DNS layer follows CNAMEs but does not increment dns_count for them.
Internationalized domain names (RFC 8616) No Neither path does IDNA2008 encoding/decoding before DNS lookups.
permerror propagation from include No RFC sec.5.2 table: if an included record yields permerror, the including record must also yield permerror. Internal returns fail. libspf2 wrapper maps any non-pass to DMARC_POLICY_SPF_OUTCOME_FAIL without distinguishing permerror.
temperror propagation from include No Same table: temperror from an included record must propagate as temperror. Internal wraps all errors as fail. libspf2 wrapper maps SPF_RESULT_TEMPERROR to TMPFAIL for 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/temperror propagation from include chains in the result-mapping wrapper, and missing DNS caching.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions