RFC Implementation Guide for Ze
This guide provides a step-by-step checklist for implementing an RFC in Ze. Use it alongside planning.md to ensure complete implementations.
Overview
An RFC implementation typically touches these areas (not all apply to every RFC):
| Component | Package | When Needed |
|---|---|---|
| Capability | internal/core/bgp/capability/ |
RFC introduces a capability |
| Attribute | internal/core/bgp/attribute/ |
RFC introduces path attributes |
| NLRI | internal/core/bgp/nlri/ |
RFC introduces new AFI/SAFI |
| Message | internal/component/bgp/message/ |
RFC modifies message format |
| FSM | internal/component/bgp/fsm/ |
RFC affects state machine |
| Config | internal/component/config/ |
RFC needs configuration |
| Plugin | internal/component/plugin/ |
RFC needs plugin commands |
| Context | internal/core/bgp/context/ |
RFC affects encoding context |
Phase 0: Preparation
0.1 RFC Analysis
[ ] Download RFC: curl -o rfc/rfcNNNN.txt https://www.rfc-editor.org/rfc/rfcNNNN.txt
[ ] Create summary: rfc/short/rfcNNNN.md (use /rfc-summarisation)
[ ] Identify RFC dependencies (other RFCs this one references)
[ ] Check dependency RFCs are implemented or summarized
[ ] Identify which components this RFC affects (table above)
0.2 Codebase Analysis
[ ] Search for existing partial implementation: grep -r "RFC NNNN" internal/
[ ] Check if related capabilities exist: internal/core/bgp/capability/
[ ] Check if related attributes exist: internal/core/bgp/attribute/
[ ] Check if related NLRI types exist: internal/core/bgp/nlri/
[ ] Read architecture docs for affected areas (see planning.md keyword table)
0.3 ExaBGP Migration Consideration
If this RFC adds features that ExaBGP users might rely on, check if migration support is needed:
| RFC Affects | Migration Impact | Action |
|---|---|---|
| API commands/events | ExaBGP plugins expect different JSON format | Update. internal/exabgp/bridge/ |
| Config syntax | ExaBGP configs have different syntax | Update. internal/exabgp/migration/ |
| Capabilities | ExaBGP may configure differently | Check migration handles it |
[ ] Does ExaBGP support this RFC feature?
[ ] If yes: is config migration needed? (internal/exabgp/migration/)
[ ] If yes: is API bridge update needed? (internal/exabgp/bridge/)
See ai/rules/go-standards.md for architecture details.
0.4 Spec Creation
[ ] Create spec: plan/spec-rfcNNNN-<feature>.md
[ ] Fill Required Reading section with identified docs
[ ] git add the spec immediately
Phase 1: Capability (if applicable)
When: RFC introduces a BGP capability (advertised in OPEN message)
1.1 Define Capability
[ ] Add capability code constant to internal/core/bgp/capability/capability.go
- Code<Name> Code = NN // RFC NNNN
[ ] Create capability struct in appropriate file (or new file)
- encoding.go for wire-format affecting caps
- session.go for session behavior caps
- new file for complex capabilities
[ ] Implement Capability interface:
- Code() Code
- Len() int
- WriteTo(buf []byte, off int) int
[ ] Implement ConfigProvider interface if cap provides plugin config:
- ConfigValues() map[string]string
1.2 Wire Format
[ ] Document wire format with ASCII diagram in code comment
[ ] Implement constructor: New<Name>(...) *<Name>
[ ] Implement parser: parse<Name>(data []byte) (*<Name>, error)
[ ] Add case to parseCapability() switch in capability.go
[ ] Handle malformed data gracefully (return error, don't panic)
1.3 Negotiation
[ ] If cap affects encoding: add field to EncodingCaps (encoding.go)
[ ] If cap affects session: add field to SessionCaps (session.go)
[ ] Update Negotiate() to handle intersection logic
[ ] Update Negotiated accessors if needed
[ ] Document negotiation rules in code comments with RFC section refs
1.4 Tests
[ ] Unit test: WriteTo round-trips correctly
[ ] Unit test: Parse valid wire bytes
[ ] Unit test: Parse rejects malformed bytes
[ ] Unit test: Negotiation logic (both have, one has, neither has)
[ ] Boundary test: min/max values for any numeric fields
Phase 2: Attribute (if applicable)
When: RFC introduces new BGP path attribute(s)
2.1 Define Attribute
[ ] Add attribute code constant to internal/core/bgp/attribute/attribute.go
- Attr<Name> AttributeCode = NN // RFC NNNN
[ ] Create attribute struct (new file if complex, or add to existing)
[ ] Implement Attribute interface:
- Code() AttributeCode
- Flags() AttributeFlags
- Len() int
- WriteTo(buf []byte, off int) int
- WriteToWithContext(buf []byte, off int, ctx *PackContext) int (if context-dependent)
- String() string
2.2 Wire Format
[ ] Document wire format with ASCII diagram
[ ] Implement constructor: New<Name>(...) *<Name>
[ ] Implement parser: Parse<Name>(data []byte) (*<Name>, error)
[ ] Handle optional/transitive flags per RFC
[ ] If context-dependent: implement WriteToWithContext()
2.3 Builder Integration
[ ] Add setter to Builder: Set<Name>(...) *Builder
[ ] Add parser support in builder_parse.go if text syntax needed
[ ] Document text syntax in builder_parse.go
2.4 Iterator Support
[ ] Ensure Iterator can return attribute via existing pattern
[ ] Add helper to extract typed attribute if frequently needed
2.5 Tests
[ ] Unit test: WriteTo produces correct wire bytes
[ ] Unit test: Parse valid wire bytes
[ ] Unit test: Parse rejects malformed bytes
[ ] Unit test: Flags are set correctly
[ ] Unit test: Builder integration
[ ] Boundary test: length limits, value ranges
Phase 3: NLRI (if applicable)
When: RFC introduces new AFI/SAFI or NLRI encoding
3.1 Define Family
[ ] Add AFI constant if new: internal/core/bgp/nlri/constants.go
[ ] Add SAFI constant if new: internal/core/bgp/nlri/constants.go
[ ] Add Family constant: var <Name> = Family{AFI: ..., SAFI: ...}
[ ] Register in familyNames map for string parsing
3.2 Define NLRI Type
[ ] Create NLRI struct (new file for complex types)
[ ] Implement NLRI interface:
- Family() Family
- Bytes() []byte (payload only, no path ID; returned slice may be shared)
- Len() int (payload length, no path ID)
- PathID() uint32
- WriteTo(buf []byte, off int) int (payload only, no path ID)
- SupportsAddPath() bool
- String() string
[ ] If ADD-PATH supported:
- SupportsAddPath() returns true
- Use WriteNLRI() helper for ADD-PATH aware encoding
3.3 Wire Format
[ ] Document wire format with ASCII diagram
[ ] Implement constructor(s) for creating NLRI
[ ] Implement parser for wire bytes
[ ] Handle variable-length fields correctly
[ ] Add UPDATE builder in internal/component/bgp/message/update_build_<type>.go if needed
3.4 Iterator Support
[ ] Add parsing support to NLRI iterator if needed
[ ] Ensure family-specific parsing in iterator works
3.5 Tests
[ ] Unit test: WriteTo produces correct wire bytes
[ ] Unit test: Parse valid wire bytes
[ ] Unit test: Round-trip (create -> write -> parse -> compare)
[ ] Unit test: ADD-PATH handling (with/without path ID)
[ ] Unit test: String() produces readable output
[ ] Boundary test: max prefix length, label values, etc.
Phase 4: Message Changes (if applicable)
When: RFC modifies BGP message format or introduces new message type
4.1 New Message Type
[ ] Add message type constant to internal/component/bgp/message/message.go
[ ] Create message struct implementing Message interface:
- Type() MessageType
- Len(ctx *EncodingContext) int
- WriteTo(buf []byte, off int, ctx *EncodingContext) int
[ ] Add case to message dispatcher in message.go
4.2 Message Modification
[ ] Update affected message struct
[ ] Update Len() calculation
[ ] Update WriteTo() implementation
[ ] Update parser if receiving this message
[ ] Document changes with RFC section references
4.3 Tests
[ ] Unit test: Message builds correctly
[ ] Unit test: Message parses correctly
[ ] Unit test: Round-trip encoding
[ ] Boundary test: max lengths, extended message handling
Phase 5: FSM Changes (if applicable)
When: RFC affects BGP state machine behavior
5.1 State/Event Changes
[ ] Add new states if needed (rare)
[ ] Add new events if needed
[ ] Update state transition table
[ ] Document RFC section for each change
5.2 Timer Changes
[ ] Add new timers if needed
[ ] Update timer handling logic
[ ] Document timer semantics with RFC refs
5.3 Tests
[ ] Unit test: State transitions
[ ] Unit test: Timer behavior
[ ] Integration test if complex
Phase 6: Configuration
When: RFC feature needs user configuration
6.1 Schema Definition
[ ] Add schema nodes to internal/component/config/schema.go
[ ] Define value types and constraints
[ ] Add validation rules
[ ] Document config syntax in schema comments
6.2 Parsing
[ ] Update parser if new syntax patterns needed
[ ] Add to appropriate config section (global, peer, family)
[ ] Handle defaults appropriately
6.3 Validation
[ ] Config rejects unknown keys (Ze rule)
[ ] Config validates value ranges
[ ] Config validates inter-field dependencies
6.4 Tests
[ ] Valid config test: test/parse/<feature>.ci (expect=exit:code=0)
[ ] Invalid config test: test/parse/<feature>-invalid.ci (expect=exit:code=1 + expect=stderr:contains=)
[ ] Test all validation rules trigger appropriately
Phase 7: API Commands (if applicable)
When: RFC feature needs plugin control/visibility
7.1 Text Commands
[ ] Design command syntax (see docs/architecture/api/update-syntax.md)
[ ] Implement parser in appropriate location
[ ] Implement handler
[ ] Document syntax in architecture docs
7.2 Response Format
[ ] Define JSON response structure if applicable
[ ] Ensure consistency with existing response patterns
7.3 Tests
[ ] Functional test: test/plugin/<feature>/
[ ] Test valid command variations
[ ] Test error handling for invalid commands
Phase 8: Engine Integration
When: RFC affects how router processes messages
8.1 Reactor Changes
[ ] Update message handling in reactor
[ ] Add new handlers if needed
[ ] Update routing logic if affected
8.2 Peer Changes
[ ] Update peer state management
[ ] Update capability negotiation handling
[ ] Update message sending logic
8.3 Tests
[ ] Integration tests with test peer
[ ] Functional tests for end-to-end behavior
Phase 9: Functional Tests
When: Always (every RFC implementation needs functional tests)
Purpose: Verify the feature works as users expect, end-to-end. Unit tests verify internal correctness; functional tests verify user-facing behavior.
9.1 User Scenario Tests
Think from the user's perspective: "If I configure X and send command Y, what should happen?"
[ ] Identify user-facing scenarios this RFC enables
[ ] For each scenario, create a functional test that:
-> Configures the feature as a user would
-> Exercises the feature through normal usage (API commands, peer interaction)
-> Verifies the observable outcome (wire bytes sent, events received, state changes)
Example scenarios by RFC type:
| RFC Type | User Scenario |
|---|---|
| Capability | "When I enable X, peer receives capability in OPEN" |
| Attribute | "When I announce route with X, UPDATE contains correct attribute" |
| NLRI | "When I announce prefix in family X, wire encoding is correct" |
| FSM | "When peer does X, session transitions to correct state" |
| Error | "When I receive malformed X, session handles it correctly" |
9.2 Encoding Tests
[ ] Create test/encode/<feature>.conf (peer config with feature enabled)
[ ] Create test/encode/<feature>.ci (command/wire pairs)
[ ] Test happy path: feature used correctly produces correct wire bytes
[ ] Test variations: different parameter combinations
[ ] Test boundaries: min/max values that affect encoding
9.3 Plugin Tests
[ ] Create test/plugin/<feature>/ directory
[ ] Test plugin receives correct JSON events when feature is active
[ ] Test plugin commands produce correct behavior
[ ] Test error responses when plugin sends invalid commands
9.4 Config Tests
[ ] Create test/parse/<feature>.ci - valid configurations (expect=exit:code=0)
[ ] Create test/parse/<feature>-invalid.ci - invalid configs (expect=exit:code=1 + expect=stderr:contains=)
[ ] Test feature enables/disables correctly via config
[ ] Test config validation catches user mistakes
9.5 Integration Tests
[ ] Test end-to-end with ze-peer
[ ] Test capability negotiation (both peers support, one supports, neither supports)
[ ] Test message exchange in realistic scenarios
[ ] Test interop edge cases (if applicable)
9.6 Negative Tests
[ ] Test graceful handling of malformed input
[ ] Test behavior when feature is disabled but peer uses it
[ ] Test error messages are helpful to users
9.7 RFC Requirement Coverage Tags
Implementing a MUST is only half the job: the enforcing test must be bound to the requirement, so the coverage gate can prove the link exists and catch a later regression. When a test proves an RFC 2119 MUST-level obligation, tag it. In Go:
// RFC requirement: RFC7606-7.1-1 positive -- valid ORIGIN length 1 is accepted
// RFC requirement: RFC7606-7.1-1 negative -- ORIGIN length 2 is treated as withdraw
A .ci functional test uses a line-start # comment with the same fields (never
inside a terminator= block):
# RFC requirement: RFC7606-7.1-1 negative -- malformed ORIGIN withdraws the route
- Allocate the id with
/ze-rfc <rfc>. Every Compliance Checklist line inrfc/short/rfcNNNN.mdgets a permanentRFC<n>-<section>-<ordinal>id, and that id is the contract the tag references. Never renumber or reuse one. - Provide BOTH polarities. Every gated MUST needs a
positiveAND anegativetest; a one-sided test passes on blanket accept or blanket reject. If a requirement is genuinely testable only one way, annotate its summary line{single-polarity: positive|negative; why}instead.{gap: why; ref}and{not-applicable: why}cover deliberate divergence and inapplicability,{lower-layer: <layer>; <path>.go::<Symbol> ...}covers a behavior a layer under Ze performs on state Ze installs, where Ze's own boundary carries no value to assert, and{feature-declined: "<RFC sentence>"; <path>.go::<Symbol> ...}covers an obligation conditional on a feature the RFC makes optional and Ze declined. Each needs a reason (a bare annotation is rejected), and the last two need the facts their gates check: the layer and the producer, or the quoted sentence and the producer (docs/contributing/rfc-conformance-gates.md). - Place the tag inline at the table case when one function covers many requirements. One id per line, polarity mandatory. The tag is the only authored half of the binding, so it dies with the test.
- A NEW tag owes a discrimination proof in the same change.
./le rfc checkrefuses a tagged unit the tip commit added againstHEAD^and that carries no recorded break under which that unit itself goes red. Both sides are committed, so the violation reaches you at your own commit rather than every other session at theirs. Record one with./le rfc discriminate-record;./le rfc discriminate stem <stem>proposes the candidates. The full contract isrfc/discrimination/README.md. - The prose after the polarity is the CLAIM, and it is fingerprinted. It is what the proof is a proof OF, so rewording it stales the record and owes a re-record. The claim runs to the end of the comment paragraph, so a sentence continued on the next comment line is part of it. Write what the test demonstrates and nothing wider: a claim that says more than the body checks is the defect the record exists to refuse.
What each carrier earns in the ledger
Evidence has two axes: KIND (which layer the test exercises) and TIER (whether anything executes it). Both are DERIVED from the carrier table rather than declared by the test.
| Carrier | Kind | Executed by | Tier |
|---|---|---|---|
*_test.go outside. internal/le/ |
unit |
./le test-unit all |
verify, on every push |
test/<suite>/*.ci |
functional |
./le functional gating |
verify, but only from a suite the functional run actually gates. suiteCarriers builds one prefixed row per name in functional.GatingNames, so a .ci in a non-gating suite (static, traffic, flow-export, vpp, vrrp) earns no verify tier, and is skipped entirely. test/draft/ |
test/editor/*.et |
editor |
./le functional editor |
verify, on the same earned-per-suite basis |
internal/le/interoplab/bgp/*.go |
interop |
./le integration interop |
nightly when a scheduled workflow names that runner, unrun otherwise |
internal/le/interoplab/ipsec/*.go |
interop |
./le integration interop-ipsec |
same derivation |
internal/le/interoplab/l2tp/*.go |
interop |
./le deployment docker-l2tp-ppp-test |
same derivation |
internal/le/interoplab/pppoe/*.go |
interop |
./le deployment docker-pppoe-accel-test |
same derivation |
The four legacy trees under test/interop*/ carry the same interop kinds through
legacyInteropCarriers, keyed on a /check.py suffix.
A requirement whose only evidence is nightly-tier is marked **nightly-only** on
its ledger row and counted in its own rollup column. The rollup deliberately
never sums the two, because a nightly tier is not merge-gate proof.
-
Five native RFC actions clear different failures.
./le rfc checkgates coverage and validates the audit records../le rfc index-updaterenders one ledger per RFC andai/RFC-REQUIREMENTS.md../le rfc extraction-create stem <stem>writes an extraction skeleton. It reachesrfc/extraction/only when every site and section already carries a disposition; otherwise it lands in this session's scratch and the command prints themvthat ends the walk../le rfc extraction-statusprints the sign-off counts../le rfc resealre-stamps an audit verdict after a mechanical edit.
For an enrolled RFC the gate fails unless every MUST has its pair or a reasoned annotation. Writing a summary does not enrol an RFC.
-
Declare enrolment in the summary, and the public row with it. Both live in the
## Metatable ofrfc/short/<stem>.md, and nowhere else.rfc/enrolled.txt,rfc/not-enrolled.txtanddocs/features/rfc-status.mdare generated from those tables by./le rfc index-update. A hand edit to one of the three is lost at the next run. Write| Enrolment | <kind> |beside| Enrolment reason | <why> |, kind one ofenrolled,non-normative,backlog,blocked,out-of-scopeorsource-restricted. A summary that declares neither row does not parse, so the state that used to red the gate cannot be written now.Un-enrolment used to be the one state that carried no information. So "the RFC imposes nothing", "nobody extracted it" and "we do not even have the text" all looked identical.
Only
non-normativeis a claim about conformance. Its reason must state a property of the DOCUMENT: its category, a missing RFC 2119 section, a keyword scan. It must never say the obligation does not apply to Ze.source-restrictedis the permanent kind: the standard's own text cannot be redistributed, so no enrolment is ever reachable. Its reason names the publishing body or the license, and it excuses no public support claim, because being unable to bound a claim is a reason to stop making it.out-of-scopesays the extraction is DONE and the owner declined the feature, so its reason carries the decision date and its row readsFutureorUnsupported.backlogandblockedare debt, and the ledger renders them as debt.Three related reds come from the same place. First, a public row that claims support over a summary with zero gated requirements. The escape is evidence that zero is real: a
non-normativedisposition, or amanual-walkextraction sign-off whoseregister-reasonsays why. Second, a row that PROMISES conformance --Supported, alone or with a scope after it -- over gated requirements of which not one carries a test in both polarities. Neither escape reaches that one: each says the DOCUMENT imposes no MUST, and this red is about a MUST that exists and is unproven. Prove one requirement, or write| Support status | Partial |, which is what the public page's own vocabulary calls "not proven". Third, aSupport remainingcell that spells a gap count immediately before MUST or SHALL must agree with the summary's{gap}count. -
Audit letter and spirit with
/ze-rfc-audit <rfc>. The gate proves a link exists, but it cannot read the test. The audit reads the RFC itself and each tagged test. It then judges whether the test would fail if the code stopped complying, and records a per-requirement verdict inrfc/audit/<rfc>.json. The verdict is one of five closed values, and the gate reads it:enforcedis the only one that means proven.weak,wrong,unimplementedandnot-applicableeach subtract the requirement from the published proven count. That count is in the ledger's Audit coverage section, and the gate still exits 0.
Recording a finding is free, and deleting one is not.
./le rfc checkre-stales a verdict when the requirement text, the tagged test's own function, or a cited producer changes. -
A
SHIFTEDverdict is not your problem to re-read. When the gate says a verdict is SHIFTED, the tagged unit is byte-identical and only the file around it moved: a line shift, a sibling test, or a rewritten import. Run./le rfc resealthen./le rfc index-update. It is the only command that writesrfc/audit/, and that is deliberate. A check that also wrote cannot be trusted to report. And a regen target that wrote evidence would re-stamp hand-authored judgements during unrelated work. -
A
STALEverdict is. The tagged unit itself changed, so re-run/ze-rfc-audit <rfc>. The re-seal refuses that case by design. -
Never change a tagged test to make it pass. Once a test carries an
RFC requirement:tag it is the requirement: fix your code, not the test. Changing its behavior needs the owner's approval. Write it as one row intest/rfc-changed/<session>.md, your own session's shard of the approval ledger, and commit that shard with the change. The native weakening check reads it and blocks the edit until a row names the test.
Who writes an owner-approval row
This is the one difference between the two ledgers, and it decides everything
else. A weakening row in test/weakened/<session>.md is the author's own
justification, and a reviewer reads it to judge the author. A row in
test/rfc-changed/<session>.md is the OWNER's decision, written down by the
author who asked for it.
An author cannot approve their own change. ai/rules/testing.md says it in one
line: a weakening row does not authorize changing a tagged test, because
self-service justification is not user approval. A row here with no answer
from the owner behind it is a forgery, not a shortcut. So the Reason column
holds what the owner approved, not what the author wanted.
The shard layout serves that rule rather than bending it. A row you did not
write sits in a file named after its writer, no commit but that writer's own can
carry it (./le commit create refuses a foreign shard), and the gate drops a
row only after proving git holds it at HEAD. Reason: an approval published under
the wrong commit's subject is a forged record of an owner decision, and it is
the one failure of this file that leaves no trace.
Name what the owner approved, and say why the tagged requirement is still proven
after the change. Quote the requirement id, so a reader can open rfc/short/
beside it. A reason that does not answer the second question approves a
compliance claim losing its evidence.
Full rules: ai/skills/ze-rfc.md; audit method: ai/skills/ze-rfc-audit.md.
[ ] Every MUST-level line in rfc/short/rfcNNNN.md has an id (allocated by /ze-rfc)
[ ] Each gated MUST has a positive AND a negative tagged test, or a reasoned annotation
[ ] ./le rfc check passes; the summary's Meta table declares Enrolment: enrolled once its tests exist
[ ] If not enrolled: that same row carries the kind, and Enrolment reason says why
Phase 10: Documentation
10.1 Architecture Docs
[ ] Update relevant docs in docs/architecture/
[ ] Add wire format documentation if new formats
[ ] Update capability list if new capability
[ ] Update attribute list if new attribute
[ ] Update NLRI list if new family
10.2 RFC Summary
[ ] Ensure rfc/short/rfcNNNN.md is complete
[ ] Add Ze implementation notes section
[ ] Cross-reference related RFCs
[ ] Every MUST-level line has a stable id (see 9.7); disclose any {gap} in docs/features/rfc-status.md
[ ] Extraction sign-off recorded: ./le rfc extraction-create stem rfcNNNN, then classify
every derived site and section in rfc/extraction/rfcNNNN.json (see 10.4)
10.3 Config Examples
[ ] Add example configs showing feature usage
[ ] Document in relevant architecture docs
10.4 Extraction Sign-Off
The checklist above proves the requirements you WROTE DOWN are enforced. Nothing
in it bounds what the summary MISSED, and a green ./le rfc check is bounded
by what was extracted. Record the walk in an artifact the gate re-checks:
./le rfc extraction-create stem rfcNNNN # skeleton to session scratch, never to rfc/extraction/
# classify every site and section by hand,
# then move the file in as the command says
./le rfc extraction-classify decisions <path> # or write the decisions in one file and apply them
./le rfc check # re-derives the inventory and judges it
Each derived site (<section>:<n>, with the sentence it came from) is mapped
to a requirement id or excluded with a kind from a closed set and a reason;
each section is walked or skipped. An unclassified site fails the gate, so
generating the skeleton cannot produce a sign-off, only the walk can. Enrolling
a stem that was not enrolled at HEAD REQUIRES this artifact. Contract and field
reference: rfc/extraction/README.md.
One exclusion kind does not dismiss its sentence. relocated-to-spec says the
obligation is owed by a named spec, under an id reserved there, because an owner
ruling moved it out of the summary. It authors relocated-to
(plan/spec-<name>.md) and reserved-id, and the gate refuses the sign-off
unless that spec exists and still names that id.
Read the signed, enrolled, relocated, and unsigned counts with
./le rfc extraction-status. The action emits structured output directly.
Final Checklist
Before marking implementation complete:
[ ] Pre-commit verification passes: ./le verify current mode full
[ ] Fuzz targets pass: ./le fuzz run
[ ] Functional tests pass: ./le functional gating
[ ] RFC MUST tests have both polarities: ./le rfc check
[ ] RFC section comments on all protocol code
[ ] RFC constraint comments with quoted requirements
[ ] RFC requirement coverage tags on tests (see 9.7): ./le rfc check passes
[ ] No backwards-compatibility shims (Ze rule)
[ ] No version numbers in config (Ze rule)
[ ] Architecture docs updated
[ ] Write learned summary to plan/learned/NNN-<name>.md
[ ] All changes in single commit
Quick Reference: Common Patterns
Wire Writing Pattern
// All wire types implement this
func (x *Type) WriteTo(buf []byte, off int) int {
// Write directly to buf at offset
// Return number of bytes written
}
// Context-dependent types add this
func (x *Type) WriteToWithContext(buf []byte, off int, ctx *PackContext) int {
// Use ctx for ASN4, ADD-PATH decisions
}
Capability Pattern
type MyCap struct {
// fields
}
func (c *MyCap) Code() Code { return CodeMyCap }
func (c *MyCap) Len() int { /* TLV value length */ }
func (c *MyCap) WriteTo(buf []byte, off int) int { /* write into buf */ }
func parseMyCap(data []byte) (*MyCap, error) { /* parse */ }
Attribute Pattern
type MyAttr struct {
// fields
}
func (a *MyAttr) Code() AttributeCode { return AttrMyAttr }
func (a *MyAttr) Flags() AttributeFlags { return FlagTransitive | FlagOptional }
func (a *MyAttr) Len() int { /* payload length */ }
func (a *MyAttr) WriteTo(buf []byte, off int) int { /* write */ }
NLRI Pattern
type MyNLRI struct {
family Family
pathID uint32
// fields
}
func (n *MyNLRI) Family() Family { return n.family }
func (n *MyNLRI) PathID() uint32 { return n.pathID }
func (n *MyNLRI) Bytes() []byte { /* payload only, no path ID */ }
func (n *MyNLRI) Len() int { /* payload length, no path ID */ }
func (n *MyNLRI) WriteTo(buf []byte, off int) int { /* write payload */ }
func (n *MyNLRI) SupportsAddPath() bool { return true }
Test Pattern
func TestMyFeature(t *testing.T) {
tests := []struct {
name string
input []byte
want *MyType
wantErr bool
}{
{"valid", []byte{...}, &MyType{...}, false},
{"invalid", []byte{...}, nil, true},
{"boundary-min", []byte{...}, &MyType{...}, false},
{"boundary-max", []byte{...}, &MyType{...}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test logic
})
}
}
RFC Implementation Examples
| RFC | Components | Good Reference |
|---|---|---|
| RFC 4724 (GR) | Capability, FSM | internal/core/bgp/capability/session.go |
| RFC 7911 (ADD-PATH) | Capability, NLRI encoding | internal/core/bgp/capability/encoding.go |
| RFC 4760 (MP) | Capability, NLRI, Attributes | internal/core/bgp/nlri/internal/core/bgp/attribute/mpnlri.go |
| RFC 8955 (FlowSpec) | NLRI, UPDATE builder | internal/component/bgp/message/update_build_flowspec.go |
| RFC 7432 (EVPN) | NLRI, UPDATE builder | internal/component/bgp/message/update_build_evpn.go |