Documentation

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

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.

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