Plugins
Ze uses a plugin architecture for all features beyond core BGP session management. Plugins handle RIB storage, route reflection, graceful restart, RPKI validation, NLRI encoding, and more.
Which Plugins Do I Need?
| Use case | Plugins | Why |
|---|---|---|
| Announce routes to upstream | bgp-rib |
Stores routes and sends them to peers |
| Route server (IXP) | bgp-rib + bgp-rs + bgp-adj-rib-in |
Forward routes between clients, replay on reconnect |
| With RPKI validation | Add bgp-rpki + bgp-adj-rib-in |
Validate origin AS against ROA cache |
| With merged RPKI events | Add bgp-rpki-decorator (+ above) |
Receive UPDATE events pre-merged with RPKI state |
| With graceful restart | Add bgp-gr |
Hold routes across restarts (RFC 4724) |
| Service healthcheck | bgp-healthcheck + bgp-watchdog |
Monitor services, control route announcement via MED or withdraw. Guide |
| Monitor only (no RIB) | None | Ze runs without plugins -- peers connect, events fire, no routes stored |
| Interface-aware BGP | iface + bgp-rib |
React to OS interface changes -- start/stop BGP listeners when addresses appear/disappear |
| Static routes | (auto-loaded) | Config-driven static routes with ECMP, weighted load balancing, BFD failover. Guide |
| OSPFv2 edge plugin | (auto-loaded) | ospf {} starts the native OSPFv2 edge plugin, validates router-id/area/interface config, opens raw IPv4 sockets for active links, runs the Interface and Neighbor State Machines, and handles LSDB flooding |
NLRI family plugins (bgp-nlri-evpn, bgp-nlri-vpn, etc.) are loaded automatically when you configure the corresponding address family. You don't need to declare them.
Config-Driven Loading
BGP itself is a config-driven plugin. If your config has a bgp { } section, BGP loads automatically. If it doesn't, ze starts without BGP (useful for interface-only or FIB-only deployments). Native OSPFv2 follows the same pattern: an ospf { } section auto-loads the ospf edge plugin through ConfigRoots ["ospf"].
Loading Plugins
Plugins are declared in the plugin { } block. Built-in plugins use internal, external processes use external:
plugin {
internal rib {
use bgp-rib
}
internal adj-rib-in {
use bgp-adj-rib-in
}
internal gr {
use bgp-gr
}
}
For external processes (scripts, custom binaries):
plugin {
external collector {
run ./collector.py
encoder json
}
}
Plugin Block Settings
| List | Setting | Description |
|---|---|---|
internal |
use |
Name of a built-in plugin to run in-process |
external |
run |
Command to start an external plugin process |
external |
encoder |
Wire encoding: json (default) or text |
Binding Plugins to Peers
Each peer declares which plugins receive its events via process blocks. The process name must match the plugin's internal or external name in the plugin { } block:
plugin {
internal rib { ... } # <-- this name
}
peer transit-a {
process rib { ... } # <-- must match
}
Plugins receive BGP events through process bindings on each peer:
peer transit-a {
...
process rib {
receive [ state ]
send [ update ]
}
process adj-rib-in {
receive [ update state ]
}
}
Event Types
| Event | Description |
|---|---|
update |
Route announcements and withdrawals |
open |
OPEN message |
notification |
NOTIFICATION message |
keepalive |
KEEPALIVE message |
refresh |
Route refresh request |
state |
Peer state changes (up/down) |
negotiated |
Capability negotiation results |
eor |
End-of-RIB marker |
rpki |
RPKI validation results |
update-rpki |
Merged UPDATE + RPKI validation (from bgp-rpki-decorator) |
Plugins can register custom event types via the EventTypes field in their registration.
These become valid in receive config directives and subscribe-events RPCs.
Runtime subscriptions
The Go SDK can subscribe at startup with SetStartupSubscriptions or after
startup with SubscribeEvents. Each subscription carries its own namespace,
event list, peer selector, format, and envelope preference. An empty namespace
uses the protocol component's default namespace, normally bgp.
The event name "*" expands at registration time to every event type currently
registered in that namespace. This avoids a wildcard check on every delivered
event. Events registered later require a new subscription.
By default, OnEvent receives the original payload string. Call
SetEnvelope(true) before startup when one handler needs to distinguish events
from several namespaces or event types. Delivery then wraps the payload:
{
"namespace": "vpn-ipsec",
"event": "sa-up",
"payload": {
"peer": "branch-a"
}
}
The envelope sits inside the existing event string, so single-event and batch delivery use the same callback. Subscribers that do not opt in remain byte-compatible with earlier releases.
Directions
process my-plugin {
receive [ update ] # events FROM the peer
send [ update ] # ability to send TO the peer
}
Invocation Modes
| Mode | Config Syntax | Description |
|---|---|---|
| Internal | internal rib { use bgp-rib } |
Compiled-in plugin using net.Pipe for startup and DirectBridge for hot paths |
| External | external feed { run "/usr/local/bin/my-plugin" } |
External binary or script using TLS connect-back |
Internal mode (use pluginname) runs a compiled-in plugin as a goroutine within the ze process. Startup still uses the same YANG RPC handshake as external plugins, then DirectBridge bypasses socket I/O for supported hot paths. External mode starts a separate process; that process connects back to the plugin hub over TLS and authenticates with its per-plugin token.
Built-In Plugins
List available plugins:
ze --plugins
Storage and Policy
| Plugin | Purpose | Typical Binding |
|---|---|---|
bgp-rib |
Route Information Base | receive [ state ] send [ update ] |
bgp-adj-rib-in |
Adj-RIB-In (raw hex replay, auto-replays on peer-up) | receive [ update state ] |
bgp-persist |
Route persistence across restarts | receive [ update state ] send [ update ] |
bgp-rs |
Route server (forward-all) | receive [ update ] send [ update ] |
bgp-watchdog |
Deferred route announcement | receive [ update ] |
Protocol
| Plugin | Purpose | Typical Binding |
|---|---|---|
bgp-gr |
Graceful Restart (RFC 4724) and Long-Lived GR (RFC 9494) | receive [ state eor ] |
bgp-rpki |
RPKI origin validation (RFC 6811) | receive [ update ] |
bgp-rpki-decorator |
Merged UPDATE+RPKI events | receive [ update rpki ] |
bgp-route-refresh |
Route Refresh (RFC 2918) | receive [ refresh ] |
bgp-role |
BGP Role (RFC 9234) | -- |
bgp-hostname |
FQDN capability | -- |
bgp-softver |
Software version capability | -- |
bgp-llnh |
Link-local next-hop (RFC 2545) | -- |
bgp-bmp |
BMP receiver + sender (RFC 7854) | receive [ state update ] |
Infrastructure
| Plugin | Description | Process Binding |
|---|---|---|
iface |
OS interface orchestration: loads backend, dispatches operations | -- (Bus events, no peer binding) |
iface-netlink |
Netlink backend for iface: manage, monitor, bridge, sysctl, mirror | -- (registered as iface backend) |
iface-dhcp |
DHCP client: DHCPv4/DHCPv6 lease acquisition and renewal | -- (Bus events) |
rib |
System RIB: selects best route across protocols by admin distance | -- (Bus events, no peer binding) |
fib-kernel |
FIB kernel: programs OS routes from system RIB via netlink | -- (Bus events, no peer binding) |
fib-p4 |
FIB P4: programs P4 switch from system RIB via gRPC/P4Runtime (noop backend) | -- (Bus events, no peer binding) |
fib-vpp |
FIB VPP: programs VPP FIB from system RIB via GoVPP. MPLS label push (IPRouteAddDel with LabelStack), swap/pop (MplsRouteAddDel), interface enable (SwInterfaceSetMplsEnable). | -- (Bus events, no peer binding) |
firewall-vpp |
VPP ACL backend for firewall: translates ze Match/Action types to VPP ACL rules, read-merge-write bindings preserving foreign ACLs | -- (registered as firewall backend) |
policy-routes |
Policy-based routing via nftables packet marking and kernel ip rules. Steers traffic to alternate routing tables or next-hops based on L3/L4 match criteria. Guide | -- (Config-driven, depends on firewall) |
sysctl |
Kernel tunable management: three-layer precedence (config > transient > default), restore on stop. Named profiles (dsr, router, hardened, multihomed, proxy) for interface units. User-defined profiles. | -- (Bus events, CLI commands) |
dhcpserver |
DHCP server (RFC 2131/2132) with PXE boot support (RFC 4578): pool management, lease tracking, static mappings, PXE option injection (options 43/60/66/67/93) for BIOS/UEFI bootfile selection | -- (Config-driven, UDP listener) |
tftpserver |
Read-only TFTP server (RFC 1350): serves bootloader files for PXE provisioning in 512-byte blocks with stop-and-wait ACK, concurrent transfer limiting, path traversal protection | -- (Config-driven, UDP listener on port 69) |
imageserver |
HTTP image server for PXE provisioning: serves gokrazy disk images, installer boot files, and pre-provisioned zefs databases with SSH credentials. Own HTTP listener, path traversal protection, Range request support | -- (Config-driven, HTTP listener) |
geodns |
GeoDNS server (RFC 1035): DNS answers selected by client source IP. Client IP from EDNS0 client-subnet (RFC 7871) or packet source; CIDR longest-prefix selects a named host-set (A/AAAA/SRV records); synthesizes SOA/NS/glue. show geodns, ze_geodns_* metrics, UDP+TCP listeners (default 127.0.0.1:5300); compile-out-able with the ze_geodns build tag |
-- (Config-driven, UDP+TCP listeners) |
as112 |
AS112 anycast DNS node (RFC 7534, RFC 7535): authoritative-only sink for misdirected RFC 1918 / link-local reverse-DNS queries plus the EMPTY.AS112.ARPA DNAME-redirection zone. Four fixed anycast host addresses (never operator-typed) registered via the iface address-ownership registry, IP_FREEBIND listeners, optional allow-from client-source access list (loopback always permitted). show as112, as112 health (one-shot query for the healthcheck probe), ze_as112_* metrics, UDP+TCP listeners on port 53. Now also a BGP redistribute source: redistribute { destination bgp { import as112 } } originates the four covering prefixes into BGP, with asn (origin AS, default 112), community, and a watchdog health-gate under service { as112 }. |
-- (Config-driven, UDP+TCP listeners) |
traffic-usage |
eBPF TCX per-(port, protocol) and opt-in per-IP byte accounting (IPv4, monitoring only). Pure-Go assembled eBPF (cilium/ebpf asm.Instructions, no C/clang). Prometheus ze_traffic_usage_* metrics, show traffic usage [name <interface>]. Linux >= 6.6. Guide |
-- (Config-driven, Bus events) |
L2TP
| Plugin | Description |
|---|---|
l2tp-auth-local |
Static user/password authentication for L2TP PPP sessions (PAP/CHAP-MD5/MS-CHAPv2) |
l2tp-auth-radius |
RADIUS authentication (Access-Request), accounting (Start/Stop/Interim-Update), CoA/DM listener, and Access-Accept attribute extraction (Framed-IP-Address, Framed-Pool, Session-Timeout, Idle-Timeout, Filter-Id, Acct-Interim-Interval) |
l2tp-pool |
Bitmap-backed IPv4 address pool with default and named pools; Framed-IP-Address bypasses pool (direct RADIUS IP assignment), Framed-Pool selects a named pool |
l2tp-shaper |
TC traffic shaping (TBF/HTB) on pppN interfaces with configured default rates, RADIUS CoA rate updates, and initial rate from Filter-Id at session establishment |
These plugins register via the L2TP handler registry (RegisterAuthHandler,
RegisterPoolHandler) at init time. Only one auth handler is active at a
time (last registered wins). The pool and shaper plugins subscribe to
session lifecycle events via the EventBus.
See L2TP guide for configuration details.
The iface plugin defines a Backend interface and loads a backend by name (YANG
backend leaf, default netlink). The iface-netlink backend handles all Linux
interface operations. iface-dhcp is a separate plugin for DHCP client lifecycle.
BGP reacts to address events by starting/stopping listeners. Uses a JunOS-style
two-layer model: physical interfaces + logical units (VLANs).
Bus topics published:
| Topic | When |
|---|---|
interface/created |
Interface appeared |
interface/deleted |
Interface removed |
interface/up |
Link state to up |
interface/down |
Link state to down |
interface/addr/added |
IP assigned |
interface/addr/removed |
IP removed |
The rib plugin aggregates best routes from all protocol RIBs and selects
the system-wide best per prefix by administrative distance (lower wins).
Subscribes to bgp-rib/best-change/ Bus topic prefix, publishes system-rib/best-change.
The fib-kernel plugin programs OS routes from the system RIB into the kernel
via netlink (Linux). Uses a custom rtm_protocol ID (RTPROT_ZE=250) to identify
ze-installed routes. Crash recovery marks existing ze routes as stale at startup
and sweeps them after reconvergence. A kernel route monitor detects external
changes and re-asserts ze routes when overwritten.
Bus topics in the FIB pipeline:
| Topic | Publisher | Subscriber | Payload |
|---|---|---|---|
bgp-rib/best-change/bgp |
bgp-rib |
rib |
Batch of per-prefix best-path changes |
system-rib/best-change |
rib |
fib-kernel, fib-p4 |
Batch of system-wide best route changes |
fib/external-change |
fib-kernel |
monitoring | External route change on ze-managed prefix |
Bus topics in the sysctl pipeline:
| Topic | Publisher | Subscriber | Payload |
|---|---|---|---|
sysctl/default |
fib-kernel, iface |
sysctl |
Plugin-required kernel default (key, value, source) |
sysctl/set |
CLI | sysctl |
Transient value from user (key, value) |
sysctl/applied |
sysctl |
any | Notification after kernel write (key, value, source) |
sysctl/show-request |
CLI | sysctl |
Request active keys table (request-id) |
sysctl/show-result |
sysctl |
requester | Active keys JSON (request-id, entries) |
sysctl/list-request |
CLI | sysctl |
Request known keys table (request-id) |
sysctl/list-result |
sysctl |
requester | Known keys JSON (request-id, entries) |
sysctl/clear-profile-defaults |
iface |
sysctl |
Clear stale profile defaults for an interface before re-emission (interface) |
Route Filters
Plugins can declare named filters at stage 1 for import and/or export filtering. Each filter specifies which attributes it needs, and the engine sends only those attributes as text for each UPDATE. Filters respond accept, reject, or modify (delta-only). See Route Filters for configuration.
A single plugin can offer multiple named filters. Config references them as
<plugin>:<filter> (e.g., rpki:validate, community:scrub).
| Category | Behavior | Example |
|---|---|---|
| Mandatory | Always on, cannot be overridden | rfc:otc |
| Default | On by default, overridable per-peer | rfc:no-self-as |
| User | Explicit in filter {} config |
rpki:validate |
Filters can declare overrides to remove default filters from the chain
(e.g., allow-own-as:relaxed overrides rfc:no-self-as for a specific peer).
Cross-Protocol Redistribute (redistribute-orchestrator)
redistribute-orchestrator is the single subscriber that dispatches non-consumer
protocol route-change events to registered RedistConsumer implementations.
Unlike the route filter chain above (which gates intra-BGP traffic),
the orchestrator lets operators redistribute locally-originated routes from
other protocols (L2TP sessions, connected interface prefixes, static routes,
future OSPF / ISIS) into destination protocols (BGP, future OSPF/ISIS).
Config:
redistribute {
destination bgp {
import connected;
import static;
import l2tp { family [ ipv4/unicast ipv6/unicast ]; }
}
}
Each destination <protocol> names a registered consumer. Under it,
import <source> enables one non-consumer protocol. The import rule's
source is the protocol's canonical name registered via
redistribute.RegisterSource. Per-source family lists narrow which
address families are redistributed; an empty list means "all families".
An import is scoped to its enclosing destination: an import under
destination bgp feeds only BGP, not OSPF/IS-IS.
The orchestrator auto-loads when redistribute {} appears in the
config. No plugin { internal redistribute-orchestrator { use redistribute-orchestrator } }
block is required.
Reactor per-peer NEXT_HOP substitution applies: when the producer leaves
NextHop zero, the reactor stamps each peer's local session address as the
NEXT_HOP. Producers that have an explicit address pass it through verbatim.
Late-join replay: a route injected by a source into destination bgp also
reaches a BGP peer that establishes AFTER the injection. On a peer's down->up
edge the orchestrator emits a redistevents.ReplayRequest carrying an opaque
ReplayID token; each producer re-emits its current set with the token echoed,
and the orchestrator targets only the newly-established peer. Producers stay
peer-agnostic; the orchestrator holds the ReplayID -> peer mapping. Out-of-process
producers re-emit asynchronously, so the mapping is held for a TTL. This closes the
gap for dynamic/inbound peers not present in the reactor map at injection time.
Counters: ze_bgp_redistribute_events_received, _announcements,
_withdrawals, _filtered_protocol_total, _filtered_rule_total,
ze_bgp_redistribute_replay_total{source} (routes replayed to a newly-established peer).
Prefix-List Filter (bgp-filter-prefix)
bgp-filter-prefix is a built-in filter plugin that matches IPv4 and IPv6
routes against ordered prefix lists defined in bgp { policy { prefix-list
NAME { ... } } }. Each list is a sequence of entry <CIDR> blocks with
optional ge / le bounds and an action of accept or reject; the
first matching entry wins and no match is an implicit deny.
Filter chain references use the standard <plugin>:<filter> form:
bgp-filter-prefix:CUSTOMERS. The shorter form prefix-list:CUSTOMERS also
resolves to the same plugin via the filter-type registration, and a bare
CUSTOMERS resolves if no other filter plugin claims a filter of that name.
| UPDATE content | Filter action |
|---|---|
| Single prefix, accepted | accept (passes through) |
| Single prefix, denied | reject (update dropped) |
| Multi-prefix, all accepted | accept (passes through) |
| Multi-prefix, all denied | reject (update dropped) |
| Multi-prefix, mixed | modify -- rewrites the UPDATE NLRI section to carry only the accepted prefixes, denied prefixes are silently removed. cmd-4 phase 2. |
The mixed case supports only IPv4 unicast legacy NLRI in v1. For
multiprotocol families (MP_REACH_NLRI), the plugin falls back to whole-
update accept when any prefix passes -- implementing per-NLRI rewriting for
MP_REACH requires declaring raw=true on the filter registration and
rewriting the attribute value directly.
AS-Path Filter (bgp-filter-aspath)
bgp-filter-aspath matches the UPDATE's AS-path against ordered regex
entries defined in bgp { policy { as-path-list NAME { entry REGEX { action
accept|reject; } } } }. The AS-path is converted to a space-separated
decimal string (e.g., "65001 65002 65003") and each entry's regex is
matched using Go's RE2 engine (linear time, inherently ReDoS-safe). First
match wins; no match is implicit deny.
Chain references: bgp-filter-aspath:NAME, as-path-list:NAME, or bare
NAME. Config authors should use [0-9] instead of \d in regex strings
because ze's config parser interprets backslash as an escape character.
Community Match Filter (bgp-filter-community-match)
bgp-filter-community-match checks for presence of a specific community
value in the route's standard, large, or extended community attributes.
Defined in bgp { policy { community-match NAME { entry COMMUNITY { type
standard|large|extended; action accept|reject; } } } }. First match wins;
no match is implicit deny.
Separate from the tag/strip community plugin (bgp-filter-community) because
intent differs: this plugin filters (accept/reject), that one modifies
(tag/strip). They can coexist in the same deployment.
Chain references: bgp-filter-community-match:NAME or community-match:NAME.
Well-known community names (no-export, no-advertise, blackhole, etc.)
work as match values because the filter text format renders them as names.
Route Attribute Modifier (bgp-filter-modify)
bgp-filter-modify unconditionally applies declared operations on every route
that reaches it in the filter chain. Three operation types are supported:
Set (absolute value): set { local-preference 200; med 50; origin igp;
next-hop 10.0.0.1; as-path-prepend 3; }. Only present leaves are applied.
Increment/Decrement (relative adjustment): increment { local-preference 50; }
or decrement { med 30; }. Supported attributes: local-preference, med, aigp.
Increment saturates at uint32 max (4294967295). Decrement floors at 0.
Set and increment/decrement for the same attribute are mutually exclusive.
Community Add/Remove: set { community-add [ 65000:200 ]; community-remove
[ 65000:100 ]; large-community-add [ 65000:100:200 ]; }. Adds or removes
individual community values (standard, large, extended) without replacing the
entire attribute. The engine maps these to AttrModAdd/AttrModRemove operations.
For conditional modification, compose with match filters earlier in the chain:
filter import [ prefix-list:CUSTOMERS modify:PREFER-LOCAL ].
Chain references: bgp-filter-modify:NAME or modify:NAME.
AS-Path Length Filter (bgp-filter-aspath-length)
bgp-filter-aspath-length accepts or rejects routes based on AS_PATH hop count.
Configure named filters with min and/or max bounds:
bgp { policy { as-path-length REJECT-LONG { max 30; } } }.
Routes outside the configured range are rejected. At least one of max or min
is required. Path length counts AS_SEQUENCE entries individually and AS_SET as 1,
following RFC 4271 Section 9.1.2.2.
Chain references: bgp-filter-aspath-length:NAME or as-path-length:NAME.
Remove Private AS (bgp-filter-remove-private-as)
bgp-filter-remove-private-as removes RFC 6996 Private Use ASNs from AS path
attributes in an import or export policy chain. Define named actions in
bgp { policy { remove-private-as NAME { ... } } } and reference them from a
peer, group, or global filter chain by their unique name.
Default mode strips private ASNs from AS_PATH and AS4_PATH. The optional
replace-with peer-as mode replaces each private ASN with the destination peer
AS on export, or the source peer AS on import.
bgp {
policy {
remove-private-as STRIP {
}
remove-private-as REPLACE {
replace-with peer-as
}
}
peer transit-a {
filter {
export [ STRIP ]
}
}
}
Filter instance names are globally unique under bgp policy. When a name is
unique, reference it directly (e.g. STRIP). The prefixed forms
remove-private-as:STRIP and bgp-filter-remove-private-as:STRIP remain
accepted for disambiguation or advanced use.
The plugin emits policy intent only. The reactor performs the wire rewrite so AS_SEQUENCE, AS_SET, and confederation segment structure is preserved. On export to EBGP peers, private-AS removal runs before the normal local-AS prepend.
To test what a filter would do without sending traffic, use show policy test:
ze show policy test peer upstream1 export filter STRIP update <BGP-UPDATE-HEX>
This returns per-filter trace output showing accept/reject/modify decisions and changed attributes. See the command reference for full syntax.
NLRI Encoders/Decoders
NLRI plugins register address family support at init time via family.MustRegister(afi, safi, afiStr, safiStr). The four base families (ipv4/unicast, ipv6/unicast, ipv4/multicast, ipv6/multicast) live in internal/core/family/registry.go itself; everything else is owned by its plugin's types.go. Plugins are loaded automatically when the corresponding family is configured.
| Plugin | Families |
|---|---|
bgp-nlri-vpn |
ipv4/mpls-vpn, ipv6/mpls-vpn |
bgp-nlri-evpn |
l2vpn/evpn |
bgp-nlri-vpls |
l2vpn/vpls |
bgp-nlri-flowspec |
ipv4/flow, ipv6/flow, ipv4/flow-vpn, ipv6/flow-vpn |
bgp-nlri-labeled |
ipv4/mpls-label, ipv6/mpls-label |
bgp-nlri-mup |
ipv4/mup, ipv6/mup |
bgp-nlri-mvpn |
ipv4/mvpn, ipv6/mvpn |
bgp-nlri-rtc |
ipv4/rtc |
bgp-nlri-ls |
bgp-ls/bgp-ls, bgp-ls/bgp-ls-vpn |
Hub Configuration
For external plugins that connect over TLS (non-internal mode), configure the hub:
plugin {
hub {
server local {
host 127.0.0.1;
port 0; # auto-assign port
secret change-this-token-to-at-least-32-chars; # TLS auth token
}
}
}
Writing External Plugins
External plugins communicate with ze using the same newline-framed YANG RPC protocol as internal plugins: #<id> <verb> [json]. External processes connect back to the plugin hub over TLS using the ZE_PLUGIN_HUB_* environment variables set by the engine. The Go SDK in pkg/plugin/sdk is the reference implementation; the functional-test helper test/scripts/ze_api.py shows the Python shape:
from ze_api import API
api = API()
api.declare_done()
api.wait_for_config()
api.capability_done()
api.wait_for_registry()
api.subscribe(['update direction received'])
api.ready()
# Event loop
while True:
event = api.read_line(timeout=1.0)
if event:
# process event JSON
pass
See plugin-development/protocol.md for the full protocol reference.
Dependencies
Plugins can declare dependencies on other plugins. The engine starts plugins in dependency order and delivers state/EOR events to dependents first.
# bgp-gr depends on bgp-rib
# bgp-rpki depends on bgp-adj-rib-in
# bgp-rs optionally uses bgp-adj-rib-in
Dependencies are declared in the plugin's registration, not in config. The engine resolves them automatically. Two kinds:
| Kind | Field | Behaviour if missing |
|---|---|---|
| Hard | Dependencies |
Startup fails with ErrMissingDependency. |
| Optional | OptionalDependencies |
Silently skipped. Plugin owner handles runtime absence (typically a one-shot WARN + feature disabled). |
bgp-rs uses bgp-adj-rib-in optionally: when both are loaded, replay-on-peer-up works; when bgp-adj-rib-in is absent, forwarding still works and a single WARN log announces that replay is disabled. bgp-rs forwards via the typed Plugin.ForwardCached / ReleaseCached fast path (rs-fastpath-3) instead of the legacy text-RPC bgp cache forward <id> <sel> pipeline. See architecture/api/commands for the full SDK surface.
Exclusive Roles
When two plugins both implement a behaviour but only one should run it, the plugin that takes over declares the role in its static registration (Claims), and the other stands down. The engine unions the claims of every plugin in the startup set and delivers the union on each plugin's Stage-2 configure callback; the standing-down plugin reads it with sdk.Plugin.ClaimActive(role) from its OnConfigure handler.
Stage 2 is part of the sequential handshake, so the decision is recorded before any plugin sends Stage-5 ready and therefore before the engine starts peers. A handler reading it during a runtime event always sees the final answer.
bgp-rs claims bgp-peer-up-replay; bgp-adj-rib-in stands its own replay down when that claim is active. The decision must not be re-derived from OnAllPluginsReady: that callback is fanned out on detached goroutines that race session establishment, and when the ownership decision was taken there both plugins replayed, so a peer received a byte-identical duplicate UPDATE. An unclaimed or unresolvable role reads false, which is the fail-closed direction: nobody promised to do this, so keep doing it yourself.
Peer-Up Barrier
A plugin that decides on the peer-up event whether a peer may receive traffic declares PeerUpBarrier: true. The engine then holds that peer's initial-sync End-of-RIB until every barrier-declaring plugin subscribed to state events has taken delivery of the peer-up event, so "End-of-RIB sent" means "every barrier plugin has registered this peer".
bgp-rs declares it: it registers the peer as a forward target on that event, and an UPDATE arriving before that is forwarded nowhere. The wait is bounded and never blocks establishment. A plugin that does not acknowledge only delays the End-of-RIB to the timeout, which logs a WARN naming the peer and the shortfall. The expected count is taken over the plugins the event is actually delivered to, so declaring the field without subscribing to state events does not stall anything. It is a separate counter from the API-sync wait, which counts plugins that send routes: merging them would let a route sender's signal satisfy a registrar's obligation.
Startup Timing
Each handshake stage is bounded by progress, not by wall clock. A stage fails only when the whole startup tier goes ze.plugin.stage.timeout (default 5s) without any plugin completing a stage; every completion re-arms the window. A BGP config puts twenty or more plugins in one tier (bgp, bgp-bmp, bgp-rpki, every bgp-filter-*), and a flat per-tier budget meant a CPU-starved or slow-disk host could lose all of them at once and come up with its BGP plugins missing.
The wait stays bounded three ways: a repeated stage completion is ignored, so a looping plugin cannot hold the window open; a wedged tier trips within one timeout of the last progress; and shutdown ends the wait. Worst case is (plugins + 1) x timeout per stage.
Debugging Plugins
The plugin debug shell lets you manually interact with the engine using the plugin protocol. This is useful when debugging plugin code -- you can send individual commands and inspect responses.
ze bgp plugin cli
The debug shell:
- Asks about handshake parameters (plugin name, families) with defaults -- hit Enter to accept
- Connects to the daemon via SSH
- Runs the 5-stage plugin handshake over the SSH channel
- Enters interactive command mode
Available post-handshake commands:
| Command | Description |
|---|---|
dispatch-command <cmd> |
Dispatch an engine command |
subscribe-events <events> |
Subscribe to events |
unsubscribe-events |
Unsubscribe from events |
decode-nlri <family> <hex> |
Decode NLRI from hex |
encode-nlri <family> <args> |
Encode NLRI |
bye |
Disconnect |
Use --name <name> to set a custom plugin name for the session.