Zone11
Status: Final
Purpose
- Validate SPF policy publication at zone apex:
- ability to retrieve authoritative TXT data;
- consistency of SPF policy sets across nameserver IPs;
- single-policy expectation per nameserver IP;
- SPF syntax and non-mail-domain policy handling.
Preconditions And Inputs
- Preconditions:
- A
zone.Zoneobject is available. - Nameserver resolution context is available for nsdiscovery calls.
- A
- Required inputs:
- Nameserver name/IP items from
DelegationNameserversandZoneNameservers. - Apex TXT responses for SPF extraction.
- Nameserver name/IP items from
- Profile/config knobs that affect behavior:
resolver.defaults.parallel: parallel nameserver query fanout.net.ipv4andnet.ipv6: disabled transports are skipped.
Algorithm And Decision Flow
- Build nameserver set from nsdiscovery delegation+zone items, then group by distinct IP.
- For each IP group (parallelized):
- Skip disabled transports.
- Query apex
TXT. - Accept response only when response exists,
RCODE=NOERROR, andAA=true. - Extract TXT records for apex, concatenate fragments per record, lowercase text, and keep only SPF records (
v=spf1with end/space/tab boundary). - Store per-IP SPF policy list plus associated nameserver
name/iplist.
- If no IP produced an accepted authoritative response, emit
Z11_UNABLE_TO_CHECK_FOR_SPF. - Else group per-IP policy sets by a normalized key:
- If all policy keys are empty:
- emit
Z11_NO_SPF_NON_MAIL_DOMAINfor root/TLD/.arpazones; - otherwise emit
Z11_NO_SPF_FOUND(domain).
- emit
- Else if more than one distinct policy-set key exists:
- emit
Z11_INCONSISTENT_SPF_POLICIES; - emit
Z11_DIFFERENT_SPF_POLICIES_FOUNDper policy-set group.
- emit
- Else if any single IP has more than one SPF policy, emit
Z11_SPF_MULTIPLE_RECORDS. - Else evaluate the single effective SPF policy text against the grammar in SPF Syntax Check
:
- if syntax invalid, emit
Z11_SPF_SYNTAX_ERROR; - if syntax valid and zone is root/TLD/
.arpa:- emit
Z11_NULL_SPF_NON_MAIL_DOMAINfor null SPF (v=spf1 -all); - else emit
Z11_NON_NULL_SPF_NON_MAIL_DOMAIN;
- emit
- if syntax valid and zone is regular mail domain, emit
Z11_SPF_SYNTAX_OK; - if syntax valid, emit
Z11_SPF_UNKNOWN_MODIFIERafter the verdict above, once per distinct unknown modifier name in record order, for every zone class.
- if syntax invalid, emit
- If all policy keys are empty:
Per-IP SPF Collection and Policy Classification (steps 1-4)
Emitted Tags (Possible Set)
| Tag | Emitted when |
|---|---|
Z11_DIFFERENT_SPF_POLICIES_FOUND | A policy-set group is emitted during SPF inconsistency reporting. |
Z11_INCONSISTENT_SPF_POLICIES | At least two distinct SPF policy-set groups exist across checked IPs. |
Z11_NO_SPF_FOUND | No SPF policy found for a domain expected to carry mail policy. |
Z11_NO_SPF_NON_MAIL_DOMAIN | No SPF policy found for root/TLD/.arpa domain class. |
Z11_NON_NULL_SPF_NON_MAIL_DOMAIN | Non-null SPF policy found for root/TLD/.arpa domain class. |
Z11_NULL_SPF_NON_MAIL_DOMAIN | Null SPF policy found for root/TLD/.arpa domain class. |
Z11_SPF_MULTIPLE_RECORDS | At least one checked IP returned more than one SPF policy. |
Z11_SPF_SYNTAX_ERROR | Effective SPF policy failed local syntax validation. |
Z11_SPF_SYNTAX_OK | Effective SPF policy passed local syntax validation. |
Z11_SPF_UNKNOWN_MODIFIER | Effective SPF policy passed local syntax validation and carries a modifier other than redirect and exp; one entry per distinct modifier name. |
Z11_UNABLE_TO_CHECK_FOR_SPF | No nameserver IP yielded an authoritative TXT response suitable for SPF evaluation. |
Tag Arguments
| Tag | Argument key | Type | Meaning |
|---|---|---|---|
Z11_DIFFERENT_SPF_POLICIES_FOUND | servers | array<object> | Structured nameserver {ns,address} object list for one policy-set group. |
Z11_INCONSISTENT_SPF_POLICIES | - | - | No arguments. |
Z11_NO_SPF_FOUND | domain | string | Tested zone name. |
Z11_NO_SPF_NON_MAIL_DOMAIN | domain | string | Tested zone name. |
Z11_NON_NULL_SPF_NON_MAIL_DOMAIN | domain | string | Tested zone name. |
Z11_NULL_SPF_NON_MAIL_DOMAIN | domain | string | Tested zone name. |
Z11_SPF_MULTIPLE_RECORDS | servers | array<object> | Structured nameserver {ns,address} object list with multi-policy responses. |
Z11_SPF_SYNTAX_ERROR | servers | array<object> | Structured nameserver {ns,address} object list used for evaluated policy. |
Z11_SPF_SYNTAX_ERROR | domain | string | Tested zone name. |
Z11_SPF_SYNTAX_OK | domain | string | Tested zone name. |
Z11_SPF_UNKNOWN_MODIFIER | domain | string | Tested zone name. |
Z11_SPF_UNKNOWN_MODIFIER | spf_modifier | string | Lowercased modifier name, without the = and the value. |
Z11_UNABLE_TO_CHECK_FOR_SPF | - | - | No arguments. |
Severity Levels Per Tag
| Tag | Level | Notes |
|---|---|---|
Z11_DIFFERENT_SPF_POLICIES_FOUND | NOTICE | Default from share/profile.json (test_levels.ZONE). |
Z11_INCONSISTENT_SPF_POLICIES | WARNING | Default from share/profile.json (test_levels.ZONE). |
Z11_NO_SPF_FOUND | NOTICE | Default from share/profile.json (test_levels.ZONE). |
Z11_NO_SPF_NON_MAIL_DOMAIN | INFO | Default from share/profile.json (test_levels.ZONE). |
Z11_NON_NULL_SPF_NON_MAIL_DOMAIN | NOTICE | Default from share/profile.json (test_levels.ZONE). |
Z11_NULL_SPF_NON_MAIL_DOMAIN | INFO | Default from share/profile.json (test_levels.ZONE). |
Z11_SPF_MULTIPLE_RECORDS | WARNING | Default from share/profile.json (test_levels.ZONE). |
Z11_SPF_SYNTAX_ERROR | WARNING | Default from share/profile.json (test_levels.ZONE). |
Z11_SPF_SYNTAX_OK | INFO | Default from share/profile.json (test_levels.ZONE). |
Z11_SPF_UNKNOWN_MODIFIER | NOTICE | Default from share/profile.json (test_levels.ZONE); carries zero score penalty (scoring TagPenalties). The record is valid: RFC 7208 section 6 requires receivers to ignore modifiers they do not recognize. Surfaced because not every receiver does. |
Z11_UNABLE_TO_CHECK_FOR_SPF | WARNING | Default from share/profile.json (test_levels.ZONE). |
Effect On Zone13
Z11_SPF_SYNTAX_OK, Z11_NULL_SPF_NON_MAIL_DOMAIN and Z11_NON_NULL_SPF_NON_MAIL_DOMAIN are the three verdicts Zone11 emits once the policy passes the syntax check. Beyond reporting that verdict they signal to the module runner that the apex policy was accepted, which is what starts Zone13
. Any other Zone11 outcome leaves Zone13 unstarted. Changing which of these tags a branch emits therefore changes Zone13 coverage.
Differences From Upstream
- Differences (Upstream vs Gonemaster):
- Upstream: defines SPF syntax against RFC 7208 ABNF semantics. Gonemaster: uses a local check (
spfCheckSyntax/spfTermOk) that follows the RFC 7208 term grammar for mechanisms, modifiers and CIDR lengths, but validates domain targets with a permissive name check; see SPF Syntax Check . Gonemaster additionally reports modifiers outside RFC 7208 withZ11_SPF_UNKNOWN_MODIFIER. - Upstream: defines no testcase boundary markers. Gonemaster: the runtime emits the shared
TEST_CASE_START/TEST_CASE_END, which are not part of the Zone11 metadata inventory.
- Upstream: defines SPF syntax against RFC 7208 ABNF semantics. Gonemaster: uses a local check (
- Potential upstream report:
no
SPF Syntax Check
The effective policy is checked against the record grammar of RFC 7208 section 12, transcribed below in the parts this check relies on. The text is lowercased before the check, so matching is case insensitive. A record is valid when it consists of v=spf1 followed by zero or more terms separated by space or tab, and every term is a directive or a modifier.
record = version terms *SP
version = "v=spf1"
terms = *( 1*SP ( directive / modifier ) )
directive = [ qualifier ] mechanism
qualifier = "+" / "-" / "?" / "~"
mechanism = ( all / include / a / mx / ptr / ip4 / ip6 / exists )
modifier = redirect / explanation / unknown-modifier
unknown-modifier = name "=" macro-string
name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." )
dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ]
ip4-cidr-length = "/" ("0" / %x31-39 0*1DIGIT) ; 0 to 32
ip6-cidr-length = "/" ("0" / %x31-39 0*2DIGIT) ; 0 to 128
macro-string = *( macro-expand / macro-literal )
macro-expand = ( "%{" macro-letter transformers *delimiter "}" )
/ "%%" / "%_" / "%-"
macro-literal = %x21-24 / %x26-7E ; visible characters except "%"
macro-letter = "s" / "l" / "o" / "d" / "i" / "p" / "v" / "h" / "c" / "r" / "t"
transformers = *DIGIT [ "r" ]
delimiter = "." / "-" / "+" / "," / "/" / "_" / "="Consequences relied on by this testcase:
aandmxacceptdual-cidr-length, soa:example.com/24,a:example.com//64anda:example.com/24//64are all valid. The IPv4 length is bounded to 32 and the IPv6 length to 128. A length with a leading zero, such as/08, is outside the grammar and is rejected. The same bounds and the same leading zero rule apply to theip4andip6lengths.- A term containing
=before any:is matched as a modifier, before any qualifier is considered, because a qualifier is only permitted on a directive.-redirect=example.comis therefore a syntax error, and so is a verification token appended to a directive, such as-allfoo=bar.exists:foo=bar.example.comis a mechanism, since its:precedes the=. redirectandexptake a domain value. Every othernameis an unknown modifier whose value must matchmacro-string. RFC 7208 section 6 states that “Unrecognized modifiers MUST be ignored no matter where, or how often, they appear in a record”, so the record stays valid and each distinct name is reported once withZ11_SPF_UNKNOWN_MODIFIER. The RFC 6652 modifiersra,rpandrrfall in this class.- The name
vis not accepted as a modifier name. A secondv=spf1among the terms results from two policies merged into one record, and such a record is reported withZ11_SPF_SYNTAX_ERRORrather than as an unknown modifier. macro-literalexcludes%, so a%that does not begin amacro-expand, such as%zor%{q}, makes the record invalid.
Edge Cases And Limitations
- Distinct nameserver names sharing one IP are grouped and represented together in
serversoutputs. - TXT responses with authoritative
NOERRORbut without SPF TXT records are treated as empty-policy results. Z11_SPF_UNKNOWN_MODIFIERis emitted once per distinct modifier name, so a name repeated in the record yields one entry.- Unknown modifier values are validated only as
macro-string. The RFC 6652 values are not checked against the RFC 6652 grammar: a receiver never rejects a record on the content of a modifier it ignores, and the publishedrpgrammar disagrees with its own prose (errata 6579, held for document update). - Domain targets of
include,exists,redirect,exp,a,mxandptrare validated with a permissive name check. Macro expansions in a domain target are accepted without being checked againstmacro-string. ptraccepts adual-cidr-lengthsuffix although RFC 7208 defines none for it.- Duplicate
redirectorexpmodifiers are not detected. RFC 7208 section 6 treats them as a permanent error. - Runtime boundary markers (
TEST_CASE_START/TEST_CASE_END) are emitted by shared testcase wrappers but omitted from current Zone11 metadata tag contract.