-
Notifications
You must be signed in to change notification settings - Fork 2
rfc implementation
Pre-Alpha. This page describes behavior that may change.
Ze implements a lot of standards documents. This page describes what stands behind that claim, including the parts that do not flatter it.
The short form: for every document Ze enrols, every MUST-level requirement in that document is either bound to tests that prove it in both directions, or carries a written reason why it is not. make ze-rfc-check refuses the build the moment that stops being true, and it runs inside make ze-verify on every push.
Figures below are from 2026-08-01. Each one names the file or command that produces it, so you can check whether this page has drifted from the tree.
| Question | Answer | Where to check |
|---|---|---|
| Which documents are held to this? | 168 enrolled: 164 RFCs, 3 IETF drafts, and the sFlow v5 specification | rfc/enrolled.txt |
| Held to what? | 2,909 MUST-level requirements, one per extracted obligation | header of ai/RFC-REQUIREMENTS.md
|
| What counts as proof? | a positive and a negative test, each naming the requirement identifier |
RFC requirement: tags in the tests |
| How many are proven that way? | 1,164. The other 1,745 each carry a written reason, and 370 of those are proven in the one direction that exists |
Both column of the ledger |
| What happens when an obligation is not met? | a {gap} annotation saying so, and a matching disclosure on the public status page |
rfc/short/<stem>.md, rfc-status.md
|
| What enforces it? | 23 checks in make ze-rfc-check, a stage of make ze-verify in both modes |
grep -c '^def check_' scripts/dev/rfc_requirements.py |
| Is any enforced requirement unaccounted for? | no. Within the enrolled set, every one is proven or reasoned | arithmetic below |
Two files are worth opening before the rest of this page makes sense. ai/RFC-REQUIREMENTS.md is the generated ledger: one row per requirement, with the tests that prove it and the pipeline that runs them. docs/features/rfc-status.md is the plain-English view: one row per RFC, with the named gaps written out in prose.
A tracked document gets a summary under rfc/short/<stem>.md. The summary is a compression of the normative text, not a paraphrase of it: each MUST-level line quotes the obligation and carries a permanent identifier of the form RFC7606-7.1-1, meaning RFC 7606, section 7.1, first obligation.
That identifier is the contract. A test claims a requirement by naming it in a comment:
// RFC requirement: RFC9234-4.1-1 positive -- a peer with a configured role advertises
// exactly one Role capability (code 9) in its OPEN.
// RFC requirement: RFC9234-4.1-1 negative -- a peer with no role, or an invalid or empty
// role, advertises no Role capability.Both directions are required. A test that only proves the accept case still passes when the code accepts everything, and a test that only proves the reject case still passes when the code rejects everything. Demanding both means the pair fails on a blanket answer in either direction.
The links in the ledger are derived from those tags, never written by hand. Nobody maintains a table of which test covers which requirement, so nobody can quietly fix the table instead of the code. Identifiers are also allocated on a ratchet, which here means a check that only lets a number move one way: a new identifier cannot reuse one a section has already issued.
A tag is worth exactly what runs it. Ze records two things about every piece of evidence: which layer the test exercises, and whether any pipeline executes it.
| Evidence | Test file | Run by | On every push? | Tags today |
|---|---|---|---|---|
unit/verify |
*_test.go |
make ze-unit-test |
yes | 3,060 |
functional/verify |
*.ci |
make ze-functional-test, make ze-exabgp-test
|
yes, for the suites those targets name | 19 |
editor/verify |
*.et |
make ze-editor-test |
yes | 0 |
interop/nightly |
test/interop/scenarios/*/check.py |
make ze-interop-test |
no, scheduled and advisory | 2 |
A unit test proves the algorithm. A .ci test proves the daemon exposes the behavior to a user. An interop scenario proves a foreign implementation accepts it. These are not interchangeable, so the ledger prints the distinction on every row.
Read that last column honestly: 3,060 of the 3,081 tags are unit tests. RFC evidence in Ze is overwhelmingly algorithm-level. The daemon-level and peer-level suites are large and run constantly, but they are mostly not bound to requirement identifiers yet, so they do not appear here. That is the biggest single weakness in this system today.
Three rules follow, and each exists because the alternative was tried:
- A tag in a suite no pipeline runs is refused outright. Not accepted with a caveat: refused, with an error naming the file. Moving a tagged test into an unrun suite is a build failure, not a quiet downgrade.
- A tag in a package that does not compile is refused too. The scanner reads text, so it once credited six requirements to a package holding an undefined symbol, where not a single test could run. Every package carrying a tag is now type-checked.
- Evidence cannot be silently downgraded. Swapping a functional test for a unit test on the same requirement fails the check even though the tag count is unchanged. The same applies to trading a proof that runs on every push for one that runs only nightly.
The checker reads the working tree, and a tree cannot tell "never proven" from "stopped being proven". About half the checks therefore compare against the previous commit rather than judging the tree alone. Seven are the named ratchets:
| Ratchet | Fires when |
|---|---|
| Enrolment only moves forward | a document whose requirements were enforced stops being enforced |
| Proof only moves forward | a requirement loses a direction it had at HEAD. Annotating it {gap} is not the escape, it is the move being blocked |
| Requirements do not vanish | a requirement identifier disappears from its summary |
| Adding a document adds checking | a summary new since HEAD declares MUST-level requirements and is not enrolled |
| Evidence only moves forward | a requirement loses an evidence layer or pipeline it had at HEAD
|
| Extraction sign-off only moves forward | a signed document loses its sign-off, or its excluded-site count rises with no fresh reason and date |
| Public disclosure only moves forward | a newly enrolled RFC arrives with no row on the public status page, or an existing row is deleted while its RFC stays enrolled |
Further one-way checks cover the audit verdicts described below and the identifier allocation above.
The third one exists for an unglamorous reason. Without it, deleting a checklist line would be the cheapest route from red to green, cheaper than declaring a gap, because a gap costs a public disclosure row. The check would then be pressuring contributors to hide obligations rather than declare them. Correcting a misquote is still allowed: you edit the text under the same identifier.
The last one closes the matching hole on the public side. A gap admitted privately in a summary must be disclosed publicly. When a status row spells out how many requirements are missing, that number must equal the real count of gap annotations. And every summary in the tree is either enrolled or carries a recorded reason in rfc/not-enrolled.txt. Four properties of the public page are machine-checked. The rest of it, including the Status word itself, is a human judgement, and the page says so at the top.
An enrolled RFC is one where every MUST-level obligation has been accounted for. That is not the same as one where every obligation is met, and the difference is neither small nor hidden.
| Annotation | Count | Meaning |
|---|---|---|
{gap: why; ref} |
534, across 84 of the 168 enrolled documents | Ze does not meet this. 525 of them name the source file that fails it |
{not-applicable: why} |
841 | an obligation for a role Ze never plays, with the reason stated |
{single-polarity: dir; why} |
370 | genuinely testable one way only, with the reason stated |
Those three plus the 1,164 proven in both directions account for all 2,909 enforced requirements exactly. Nothing in the enrolled set is silently untested.
A bare annotation is rejected: the checker requires a reason on every one. The convention is to name the function and file that fail the obligation, which most do, though the checker cannot enforce that the anchor stays current.
Three real gaps from rfc/short/rfc4271.md, chosen because the base BGP specification is the least comfortable place to have any:
| Requirement | What Ze does instead |
|---|---|
RFC4271-5-3 |
an unrecognized transitive optional attribute is re-advertised without setting the Partial bit. The stored flags octet is replayed verbatim |
RFC4271-4.3-5 |
a prefix appearing in both the withdrawn and the announced field of one UPDATE is removed rather than installed, because the announce is applied first and the withdrawal second |
RFC4271-9.2.1.1-2 |
there is no MinRouteAdvertisementIntervalTimer |
RFC 4271 carries 21 such annotations today, and all 21 are named in prose on the public status page, most with the source location. Somebody deciding whether Ze suits their network can find out what it does not do without reading the code.
The checker can only judge requirements somebody wrote down. An obligation never extracted from the RFC is invisible to it, and silence is not conformance.
That hole has its own artifact. Enrolling a document now requires an extraction sign-off under rfc/extraction/<stem>.json, in which a reviewer classifies every normative site and every section of the full text. Only the classifications are written by hand: the sites, the sections, the quotes and every published count are derived from the source text when the check runs, and an artifact that edits a derived field is refused. A generated skeleton can never pass, because every site starts unclassified and an unclassified site fails. Mass-producing sign-offs makes the tree redder, not greener.
Two patterns are worth naming, because both were caught this way:
-
A
{not-applicable}reading "Ze has no X at all." That admission is often itself the violation of a separate MUST requiring X to exist. RFC 4271 section 5.1.4 requires a mechanism to remove MULTI_EXIT_DISC. That obligation was unextracted, and two other requirements were citing its absence as their own exemption. - A section whose siblings are enumerated but one clause is not. RFC 8666 section 5's "MUST be ignored on reception" was missing while sections 6, 7.1 and 7.2 each carried it. An enumeration hole, not a style choice.
The backlog here is real and stated plainly. Two documents of the 168 carry a sign-off. The other 166 were enrolled before the artifact existed and are exempt for that reason. New enrolments are not. make ze-rfc-extraction-status prints the count. A drain schedule lives in rfc/drain-budget.txt and ships at rate 0, which means inert: arming it is a deliberate one-line commit, taken once a sign-off pass has been measured rather than guessed.
There is a second exempt set on the public side: 32 enrolled RFCs have no row on the status page at all. Their requirements are enforced, so the obligation holds while the public page says nothing about the RFC. New enrolments are refused without a row, so that number can only shrink. It is derived and published in the ledger rather than kept in a list somebody could forget.
Once a test carries an RFC requirement: tag it stops being a test and becomes a requirement's proof, so the usual rule reverses: when it goes red, you fix the code, never the test.
An edit that changes the behavior of a tagged test is blocked at write time, in all four test file types. In Go the protected scope is the whole enclosing test function rather than the lines you touched, so moving an assertion out of reach does not help. For .ci, .et and check.py files it widens to the whole file. Removing the tag is checked first, since a tag is only a comment. Reformatting and comment edits pass. Renaming or re-asserting does not.
The escape is an approval recorded in the source, rfc-test-change-approved: <date> <what was approved>. Being a comment, it is a convention and an auditable trail rather than a mechanical permission check: the project rule is that only the owner authorizes one, and grep -rn 'rfc-test-change-approved:' lists every time it has been used.
The checks prove a link exists. They cannot judge whether the link is meaningful, because a test can keep its tag while its assertions are weakened into vacuity. That is what /ze-rfc-audit is for: it reads the RFC text and each tagged test and asks whether the test would actually fail if the code stopped complying. A verdict is fingerprinted against both the requirement text and the tested code, so it goes stale on its own when either moves, and a stale assurance becomes a request to re-read instead of sitting there looking green. A verdict may never be deleted, and upgrading one without the tested code changing requires a written reason.
The audit is sampled, not total, and its coverage today is small: of 1,534 requirements it could judge, 49 have been judged, all in RFC 7606. The mechanical checks cover the rest.
The workflow, in order:
-
Summarise first. Write
rfc/short/<stem>.mdbefore any spec, giving every MUST-level line a permanent identifier. This forces "is this document worth implementing here" to be answered against a real list rather than an impression. -
Spec it. A spec under
plan/names the sections in scope, the acceptance criteria, and what is out of scope. - Write the tests first, tagged with the requirement identifiers, in both directions.
- Write the code, citing the section in a comment at each non-trivial decision.
-
Enrol it in
rfc/enrolled.txtonce every requirement is proven or reasoned. Writing a summary does not enrol anything: enrolment is a separate, deliberate act, and it is the moment the checks start holding you to the document permanently. It also needs the source text underrfc/full/orrfc/drafts/, and, for a new enrolment, a public status row and an extraction sign-off.
A new summary cannot sit there unenrolled: once written, it must be enrolled in the same change. An older summary can sit in the declared remainder instead, recorded in rfc/not-enrolled.txt with a reason, and those two files between them account for every summary in the tree. Seven sit in that remainder today, four marked backlog and three blocked, and the ledger labels both kinds DEBT rather than letting them read as neutral. Between them they hold the 34 requirements that still owe work, 27 of which are in RFC 1035.
Worth stating plainly, since the rest of the page is a list of things that are checked:
- It does not prove interoperability. These tests prove Ze behaves as the document says. Whether Ze and a given vendor agree is what the interop suites are for, against FRR, BIRD, GoBGP, strongSwan and others.
- It is mostly unit-level evidence. See the tag split above. A requirement can be proven at the algorithm and still be unreachable through the daemon.
- It does not cover SHOULD-level text. Only MUST-level obligations are enforced. Weaker statements are recorded in the summaries and read during review.
-
It does not make the
Statuscolumn true.Supported,Partialand the rest are a product-support judgement made by a human. - It says nothing about documents nobody summarised. An RFC with no summary is on no list here.
- It does not check this page. The wiki lives outside the repository, so the figures above are a snapshot somebody typed. The commands below are what to trust.
make ze-rfc-check # the checks themselves, and a stage of make ze-verify
make ze-rfc-index # regenerate ai/RFC-REQUIREMENTS.md
make ze-rfc-extraction-status # extraction sign-off counts, as JSONRun them on a clean checkout. The check self-tests against its own fixtures before it judges the tree, and it refuses to run against a stale ledger, so a dirty working tree reports a failure that is about the tree rather than about compliance.
- Testing for the suites the evidence layers refer to.
- Coding standards for the test-first rule this depends on.
- Contributing for the spec-first workflow.
-
Claude Code for
/ze-rfcand/ze-rfc-audit. -
ai/rules/rfc-compliance.md,scripts/dev/rfc_requirements.pyandrfc/extraction/README.mdin the repo are the authoritative versions of everything above.
Unreviewed draft. This wiki was authored in bulk and has not been reviewed. File corrections on the issue tracker.
- Overview
- YANG Model
- Editor Workflow
- Archive and Rollback
- System
- Interfaces
- VRRP
- BFD
- FIB
- OSPF
- IS-IS
- MPLS / LDP / RSVP-TE
- RSVP-TE
- SRv6
- Static Routes
- Policy Routing
- Firewall
- Traffic Control
- Class of Service
- L2TP/PPP
- PPPoE
- VPP Data Plane
- RPKI
- IPsec VPN
- TACACS+ AAA
- RADIUS AAA
- AS112 DNS
- Authorization
- Fleet
- BGP
- Starting and Stopping
- Show Commands
- Monitoring
- Flow Export
- DDoS Mitigation
- Anomaly Detection
- Health Checks
- Audit Trail
- Production Diagnostics
- Logging
- Operational Reports
- Healthcheck
- Self-Update
- Zero-Touch Provisioning
- MRT Analysis
- Upgrade and Restart
- Storage
- Policy
- Core
- Resilience
- Validation
- Capabilities
- Address Families
- Protocol
- Subsystems
- Infrastructure
- Route Server at an IXP
- Transit Edge with RPKI
- Public Looking Glass
- ExaBGP Migration Walkthrough
- FlowSpec Injection
- Chaos-Tested Peering
- AS Path Topology