-
Notifications
You must be signed in to change notification settings - Fork 2
architecture
Pre-Alpha. This page describes behavior that may change.
This page is the mental model. It covers the four things that make up Ze, how a BGP UPDATE travels through the system, and where plugins get to intercept it. It is deliberately short. If you want the deep version, read core-design.md, overview.md, and system-architecture.md in the main repo. A rendered SVG of the full diagram lives at ze-architecture.svg.
The engine knows nothing about BGP. It is a supervisor that wires four components together and starts them in the right order.
The Supervisor (internal/component/engine/) owns the startup and shutdown sequence. It creates the bus, the config provider, and the plugin manager, then starts subsystems. It has no protocol knowledge of its own. Adding a new protocol later does not require touching the engine.
The Bus (pkg/ze/eventbus.go) is a content-agnostic pub/sub backbone. Topics use flat (namespace, eventType) pairs, and payloads are opaque strings (JSON for the FIB pipeline, empty for simple notifications). The bus is used for cross-component signals that do not need to be strongly typed. Hot-path BGP data does not go through the bus.
The ConfigProvider is the single authority for the config tree. A YANG-parsed tree is pushed in via SetRoot(). Subsystems and plugins read their own slice of the tree out, by path. SIGHUP triggers a re-parse and a SetRoot(), which is how add-remove-update-a-peer at runtime works without a restart.
The PluginManager (internal/component/plugin/manager/) owns plugin lifecycle: spawning in-process plugins via DirectBridge, spawning external plugins via a ProcessSpawner, running the five-stage handshake, dispatching events, and routing commands back. Every plugin declares a Registration with its config roots, event types, send types, and dependencies, and the manager handles the rest.
BGP itself is a subsystem (internal/component/bgp/reactor/) that plugs into those four components the same way any other protocol does. Interface management (interface) and the FIB pipeline (rib, fib-kernel) follow the same pattern. If your config has no bgp { } block, BGP does not load. If you add one and SIGHUP, it loads. This is the "modular deployment" property of the engine.
The BGP subsystem is the part that most operators think of as "Ze". It is built from five layers, which run in-process and talk through direct calls rather than the bus.
Peer FSMs implement the RFC 4271 state machine: Idle, Connect, Active, OpenSent, OpenConfirm, Established. One goroutine per peer handles timers, TCP, and the read loop.
The Wire layer (internal/component/bgp/wire/, wireu/) parses BGP messages lazily. A received UPDATE becomes a WireUpdate: a thin struct holding a byte slice reference into the TCP read buffer plus a ContextID identifying the peer's negotiated capabilities. Nothing is decoded into intermediate structs on the receive path. Iterators parse attributes and NLRI on demand.
Capability negotiation (internal/component/bgp/capability/) tracks what each peer agreed to (ASN4, ADD-PATH, Extended Messages, Extended Next-Hop, Route Refresh, Graceful Restart, Role, Hostname, Software Version). The ContextID lets the reactor decide at forward time whether wire bytes can be sent to a destination peer unchanged (same context) or must be re-encoded (different context).
The Reactor (internal/component/bgp/reactor/) is the event loop and BGP message cache. Every received UPDATE gets a message-id and is cached as WireUpdate + metadata, so that plugins can ask to forward or withdraw by id later without paying the parse cost again.
The EventDispatcher (internal/component/bgp/server/) is the plugin-facing bridge. Internal plugins receive a *rpc.StructuredEvent via DirectBridge, which is a *RawMessage containing the cached WireUpdate, not a JSON round-trip. External plugins receive formatted JSON text on a pipe, with base64-encoded wire bytes inside. The format negotiation happens once at handshake.
The receive path is the hot path, so the reactor works hard to avoid copies and parsing. Two redistribution models share the same cache.
In the ExaBGP model, plugins control redistribution themselves. A received UPDATE is parsed lazily into a WireUpdate, cached against a message-id, and delivered as an event to subscribed plugins. Each plugin decides what to do with it: store it, drop it, forward it to other peers by id, or rewrite and forward it. There is no automatic best-path selection on this path.
In the route-server model, the bgp-rib plugin generates a RIB entry from the same cached WireUpdate. From there, route generation runs the way operators expect: best-path selection picks a winner, the result is fanned out to the eligible peers, and the egress filter chain runs per destination.
TCP recv -> WireUpdate (lazy) -> Reactor cache --+--> EventDispatcher --> ExaBGP-style plugins
| (forward by id, drop, rewrite)
|
+--> bgp-rib --> RIB entry --> Best path
|
Per-peer egress
|
Wire out
Both flows share the same copy-on-need optimisation. When a forward target shares the source peer's ContextID, the cached wire bytes go straight back out. No parsing, no re-encoding. When the contexts differ, or when an egress filter on the destination peer queues a modification against its ModAccumulator, a progressive build walks the cached attributes, applies the modifications, and emits a freshly encoded UPDATE. The accumulator is lazy: if nothing needs to change, no allocation happens at all.
The announce path goes the other way. A plugin command (text or hex) goes through ParseUpdate, which builds a WireUpdate once, and the reactor sends it to the selected peers.
Plugins are the interesting part. There are five hook points, and a plugin can use any combination of them.
Event subscribers. Any plugin can subscribe to events the reactor emits: session state changes, received UPDATE, sent UPDATE, withdrawals, NOTIFICATION, cache eviction. This is how bgp-rib builds its store, how bgp-adj-rib-in replays raw hex, and how monitoring plugins get their data.
Ingress and egress filters. Ze runs filters on routes in two directions, declared per peer or per group in the filter { import [...] export [...] } block. Ingress filters run when a route is received and can attach read-only metadata to the cached entry. Egress filters run when a route is sent and emit attribute modifications against a ModAccumulator. The RFC 9234 Role plugin is implemented as a mandatory egress filter: it stamps the OTC attribute when the destination peer is non-customer (provider, peer, or route-server-client).
Command handlers. A plugin can register CLI and API commands. The same schema drives the CLI, the web UI, the REST API, the Looking Glass, and the MCP server, so exposing a command exposes it on every surface at once.
NLRI family handlers. Every non-unicast address family ships as a plugin (EVPN, FlowSpec, VPNv4, VPNv6, labeled unicast, BGP-LS, VPLS, MVPN, RTC, MUP). The plugin registers decode and encode functions against the family identifier, and the reactor dispatches to them by family. IPv4 and IPv6 unicast and multicast are built into the engine itself because they predate the plugin interface.
Config schema augmentation. A plugin can augment another plugin's schema. Graceful Restart, for example, augments the BGP peer schema so that peer x { capability { graceful-restart { ... } } } is a valid path even though the bgp-gr plugin is logically separate.
Every CLI command, schema definition, and show handler is owned by the plugin or component that implements the feature. Commands register themselves in register.go via the leaf registry pattern. The verb-first grammar (show peer list, request peer teardown, clear dns cache) is consistent across all 50+ command-owning plugins.
For the architectural details, see command-ownership.md.
- core-design.md. The canonical architecture reference.
- overview.md. The guided tour.
- system-architecture.md. Hub mode and external plugin orchestration.
- command-ownership.md. Command surface ownership and the leaf registry.
- mrt.md. MRT recording component design.
- pool-architecture.md. AttrPool sharding and deduplication.
- runner-architecture.md. Functional test runner design.
- ze-architecture.svg. The full-resolution diagram.
- What is Ze for the product pitch.
- Status for what actually works.
- Plugin development for writing your own.
Adapted from main/docs/architecture/overview.md, main/docs/architecture/core-design.md, and main/docs/architecture/system-architecture.md.
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