Plugin Protocol
Ze plugins communicate with the engine via newline-framed YANG RPCs over a single
bidirectional connection. Internal plugins use net.Pipe() for startup; external
plugins connect back via TLS.
All messages use newline-delimited framing with the wire format #<id> <verb> [<json>]\n.
Wire Format
Every message is a single newline-terminated line:
| Message type | Format |
|---|---|
| Request | #<id> <method> [<json-params>]\n |
| Success response | #<id> ok [<json-result>]\n |
| Error response | #<id> error [<json-error>]\n |
<id>is a monotonically increasing uint64 correlation ID<method>uses YANG-style<module>:<rpc-name>naming (e.g.,ze-plugin-engine:declare-registration)- JSON payloads are optional (omitted when empty or null)
- Responses use
okorerroras the verb; requests use the method name
Routing: MuxConn multiplexes a single connection for concurrent RPCs. A background
reader goroutine routes incoming lines by verb: ok/error responses go to the waiting
CallRPC caller by #<id>, while method-name requests go to the Requests() channel.
Examples:
# Plugin sends declare-registration (Stage 1)
#1 ze-plugin-engine:declare-registration {"families":[{"name":"ipv4/flow","mode":"both","afi":1,"safi":133}]}
# Engine responds OK
#1 ok
# Engine sends configure to plugin (Stage 2)
#1 ze-plugin-callback:configure {"sections":[{"root":"bgp","data":"{...}"}]}
# Plugin responds OK
#1 ok
# Engine sends event at runtime
#42 ze-plugin-callback:deliver-event {"event":"{\"type\":\"state\",...}"}
# Plugin responds OK
#42 ok
# Error response with payload
#5 error {"code":"error","message":"unknown family: ipv4/unknown"}
Protocol Stages
The SDK handles the 5-stage startup protocol automatically via Plugin.Run().
Stage 1: Registration (Plugin to Engine)
Plugin sends ze-plugin-engine:declare-registration with a DeclareRegistrationInput:
| Field | Type | Description |
|---|---|---|
families |
[]FamilyDecl |
Address families the plugin handles (name, mode, AFI, and SAFI) |
commands |
[]CommandDecl |
Commands the plugin provides |
dependencies |
[]string |
Plugin names that must also be loaded |
wants-config |
[]string |
Config roots the plugin wants to receive |
config-operations |
[]ConfigOperationDecl |
Config operation callbacks the plugin supports |
verify-budget |
int |
Estimated verify time in seconds (0 means trivial) |
apply-budget |
int |
Estimated apply time in seconds (0 means trivial) |
schema |
*SchemaDecl |
YANG schema (module, namespace, yang-text, handlers) |
wants-validate-open |
bool |
Whether plugin wants OPEN validation callbacks |
cache-consumer |
bool |
Whether plugin consumes cached events |
cache-consumer-unordered |
bool |
Whether unordered cache delivery is acceptable |
filters |
[]FilterDecl |
Named route filters the plugin provides |
doctor-checks |
[]DoctorCheckDecl |
Doctor checks the plugin provides |
enrichers |
[]EnricherDecl |
Show enrichers the plugin provides |
claims |
[]string |
Exclusive runtime roles the plugin takes over |
Each FamilyDecl has these fields:
| Field | Type | Description |
|---|---|---|
name |
string |
Canonical afi/safi name |
mode |
string |
encode, decode, or both |
afi |
uint16 |
RFC 4760 Address Family Identifier |
safi |
uint8 |
RFC 4760 Subsequent Address Family Identifier |
Set afi and safi for a custom family. A built-in family can omit both numeric fields.
Wire example:
#1 ze-plugin-engine:declare-registration {"families":[{"name":"ipv4/flow","mode":"both","afi":1,"safi":133}],"commands":[{"name":"flowspec status","description":"Show FlowSpec status"}],"wants-config":["bgp"]}
#1 ok
Stage 2: Config (Engine to Plugin)
Engine sends ze-plugin-callback:configure with a ConfigureInput:
| Field | Type | Description |
|---|---|---|
sections |
[]ConfigSection |
Config sections (root name + JSON data) |
Each ConfigSection has:
| Field | Type | Description |
|---|---|---|
root |
string |
Config root name (e.g., "bgp") |
data |
string |
JSON-encoded config data |
Wire example:
#1 ze-plugin-callback:configure {"sections":[{"root":"bgp","data":"{\"bgp\":{\"peer\":{...}}}"}]}
#1 ok
Stage 3: Capabilities (Plugin to Engine)
Plugin sends ze-plugin-engine:declare-capabilities with a DeclareCapabilitiesInput:
| Field | Type | Description |
|---|---|---|
capabilities |
[]CapabilityDecl |
BGP capabilities for OPEN injection |
Each CapabilityDecl has:
| Field | Type | Description |
|---|---|---|
code |
uint8 |
Capability code (e.g., 64 for Graceful Restart) |
encoding |
string |
"hex", "b64", or "text" |
payload |
string |
Encoded capability value |
peers |
[]string |
Peer addresses to inject into (empty = all peers) |
Wire example:
#2 ze-plugin-engine:declare-capabilities {"capabilities":[{"code":64,"encoding":"hex","payload":"0078","peers":["192.168.1.1"]}]}
#2 ok
Stage 4: Registry (Engine to Plugin)
Engine sends ze-plugin-callback:share-registry with a ShareRegistryInput:
| Field | Type | Description |
|---|---|---|
commands |
[]RegistryCommand |
Registered commands from all plugins |
Each RegistryCommand has:
| Field | Type | Description |
|---|---|---|
name |
string |
Command name |
plugin |
string |
Plugin that registered it |
encoding |
string |
Encoding format |
Wire example:
#2 ze-plugin-callback:share-registry {"commands":[{"name":"rib adjacent status","plugin":"bgp-adj-rib-in"},{"name":"peer","plugin":"bgp"}]}
#2 ok
Stage 5: Ready (Plugin to Engine)
Plugin sends ze-plugin-engine:ready with an optional ReadyInput:
| Field | Type | Description |
|---|---|---|
subscribe |
SubscribeEventsInput |
Optional startup event subscription |
transport |
string |
"bridge" for internal plugins; pipe closed after ack |
The subscribe field allows plugins to register event subscriptions atomically with
startup completion. This avoids a race where SignalAPIReady triggers route sends before
a separate subscribe-events RPC could be processed.
When transport is "bridge", the engine activates bridge callbacks on the PluginConn
and the SDK closes the pipe after receiving the OK response. All subsequent engine-to-plugin
callbacks flow through bridge.CallbackCh() instead of the MuxConn.
Wire example:
#3 ze-plugin-engine:ready {"subscribe":{"events":["update","state"],"peers":["*"],"format":"json"},"transport":"bridge"}
#3 ok
After Stage 5, the SDK activates the DirectBridge (for internal plugins) and enters the event loop.
Runtime Callbacks (Engine to Plugin)
After startup, the engine sends runtime RPCs to the plugin. The SDK dispatches each wire method to its registered handler.
| Method | SDK handler | Input | Output | Purpose |
|---|---|---|---|---|
ze-plugin-callback:deliver-event |
OnEvent |
DeliverEventInput |
None | Deliver one event |
ze-plugin-callback:deliver-batch |
OnEvent |
{"events":[]} |
None | Deliver an event batch |
ze-plugin-callback:execute-command |
OnExecuteCommand |
ExecuteCommandInput |
ExecuteCommandOutput |
Run a command |
ze-plugin-callback:encode-nlri |
OnEncodeNLRI |
EncodeNLRIInput |
EncodeNLRIOutput |
Encode NLRI |
ze-plugin-callback:decode-nlri |
OnDecodeNLRI |
DecodeNLRIInput |
DecodeNLRIOutput |
Decode NLRI |
ze-plugin-callback:decode-capability |
OnDecodeCapability |
DecodeCapabilityInput |
{"json":...} |
Decode a capability |
ze-plugin-callback:config-verify |
OnConfigVerify |
ConfigVerifyInput |
ConfigVerifyOutput |
Verify candidate config |
ze-plugin-callback:config-apply |
OnConfigApply |
ConfigApplyInput |
ConfigApplyOutput |
Apply a config diff |
ze-plugin-callback:config-rollback |
OnConfigRollback |
{"transaction-id":"..."} |
None | Roll back a config transaction |
ze-plugin-callback:config-operation-decompose |
OnConfigOperationDecompose |
ConfigOperationDecomposeInput |
ConfigOperationDecomposeOutput |
Decompose a config transaction |
ze-plugin-callback:config-operation-verify |
OnConfigOperationVerify |
ConfigOperationVerifyInput |
ConfigOperationVerifyOutput |
Verify a config operation |
ze-plugin-callback:config-operation-apply |
OnConfigOperationApply |
ConfigOperationApplyInput |
ConfigOperationApplyOutput |
Apply a config operation |
ze-plugin-callback:config-operation-rollback |
OnConfigOperationRollback |
ConfigOperationRollbackInput |
ConfigOperationRollbackOutput |
Roll back config operations |
ze-plugin-callback:config-operation-commit |
OnConfigOperationCommit |
ConfigOperationCommitInput |
ConfigOperationCommitOutput |
Commit operation journals |
ze-plugin-callback:validate-open |
OnValidateOpen |
ValidateOpenInput |
ValidateOpenOutput |
Validate an OPEN message |
ze-plugin-callback:filter-update |
OnFilterUpdate |
FilterUpdateInput |
FilterUpdateOutput |
Filter a route update |
ze-plugin-callback:doctor-check |
OnDoctorCheck |
DoctorCheckInput |
DoctorCheckOutput |
Run a doctor check |
ze-plugin-callback:enrich-show |
OnEnrichShow |
EnrichShowInput |
EnrichShowOutput |
Add data to show output |
ze-plugin-callback:post-startup |
OnAllPluginsReady |
None | None | Signal that all plugins are ready |
| ✕ | OnStructuredEvent |
[]any of *rpc.StructuredEvent |
None | Deliver structured events without JSON |
ze-plugin-callback:bye |
OnBye |
ByeInput |
None | Notify the plugin of shutdown |
Runtime RPCs (Plugin to Engine)
Plugins call the engine during runtime through these SDK methods:
| Method | SDK method | Input | Output | Purpose |
|---|---|---|---|---|
ze-plugin-engine:update-route |
UpdateRoute, UpdateRouteWithMeta, UpdateRouteSel, UpdateRouteSelWithMeta |
UpdateRouteInput |
UpdateRouteOutput |
Send a route update |
ze-plugin-engine:forward-cached |
ForwardCached |
ForwardCachedInput |
None | Forward cached UPDATEs |
ze-plugin-engine:release-cached |
ReleaseCached |
ReleaseCachedInput |
None | Release cached UPDATEs |
ze-plugin-engine:relay-stored-route |
RelayStoredRoute |
RelayStoredRouteInput |
None | Relay stored routes |
ze-plugin-engine:route-install |
RouteInstall |
RouteInstallInput |
RouteInstallOutput |
Install routes in the Loc-RIB |
ze-plugin-engine:route-remove |
RouteRemove |
RouteRemoveInput |
RouteRemoveOutput |
Remove routes from the Loc-RIB |
ze-plugin-engine:inject-wire-route |
InjectWireRoute |
InjectWireRouteInput |
None | Inject a raw BGP UPDATE |
ze-plugin-engine:batch-validate |
BatchValidate |
BatchValidateInput |
BatchValidateResult |
Submit route validation decisions |
ze-plugin-engine:dispatch-command |
DispatchCommand |
DispatchCommandInput |
DispatchCommandOutput |
Dispatch a command string |
ze-plugin-engine:dispatch-command-args |
DispatchCommandArgs |
DispatchCommandArgsInput |
DispatchCommandOutput |
Dispatch a command and pre-tokenized arguments |
ze-plugin-engine:emit-event |
EmitEvent |
EmitEventInput |
EmitEventOutput |
Emit an event |
ze-plugin-engine:subscribe-events |
SubscribeEvents |
SubscribeEventsInput |
None | Subscribe to events |
ze-plugin-engine:unsubscribe-events |
UnsubscribeEvents |
None | None | Unsubscribe from events |
ze-plugin-engine:decode-nlri |
DecodeNLRI |
DecodeNLRIInput |
DecodeNLRIOutput |
Decode NLRI through the registry |
ze-plugin-engine:encode-nlri |
EncodeNLRI |
EncodeNLRIInput |
EncodeNLRIOutput |
Encode NLRI through the registry |
ze-plugin-engine:decode-mp-reach |
DecodeMPReach |
DecodeMPReachInput |
DecodeMPReachOutput |
Decode MP_REACH_NLRI |
ze-plugin-engine:decode-mp-unreach |
DecodeMPUnreach |
DecodeMPUnreachInput |
DecodeMPUnreachOutput |
Decode MP_UNREACH_NLRI |
ze-plugin-engine:decode-update |
DecodeUpdate |
DecodeUpdateInput |
DecodeUpdateOutput |
Decode a BGP UPDATE |
Message Flow Example
Plugin Engine
| |
| STAGE 1: declare-registration |
|-- #1 ze-plugin-engine:declare-registration {...}->|
|<- #1 ok ---------------------------------------- |
| |
| STAGE 2: configure |
|<- #1 ze-plugin-callback:configure {...} ---------|
|-- #1 ok ---------------------------------------->|
| |
| STAGE 3: declare-capabilities |
|-- #2 ze-plugin-engine:declare-capabilities {...}->|
|<- #2 ok ---------------------------------------- |
| |
| STAGE 4: share-registry |
|<- #2 ze-plugin-callback:share-registry {...} ----|
|-- #2 ok ---------------------------------------->|
| |
| STAGE 5: ready |
|-- #3 ze-plugin-engine:ready {...} -------------->|
|<- #3 ok ---------------------------------------- |
| |
| RUNTIME: event delivery |
|<- #42 ze-plugin-callback:deliver-batch {...} ----|
|-- #42 ok ---------------------------------------->|
| |
| RUNTIME: plugin sends route update |
|-- #4 ze-plugin-engine:update-route {...} -------->|
|<- #4 ok {"peers-affected":2,"routes-sent":2} --- |
| |
| RUNTIME: command execution |
|<- #43 ze-plugin-callback:execute-command {...} ---|
|-- #43 ok {"status":"done","data":"..."} -------->|
| |
| SHUTDOWN: bye |
|<- #99 ze-plugin-callback:bye {"reason":"..."} ---|
|-- #99 ok ---------------------------------------->|
| (plugin exits) |
Error Handling
Stage errors: If any stage RPC fails (error response or timeout), the SDK returns
an error from Run() with context like "stage 1 (declare-registration): ...".
Runtime errors: Callback handlers return errors via #<id> error {"code":"...","message":"..."}.
Unknown methods are rejected with "unknown method: <method>".
Connection errors: EOF or closed connection during the event loop is treated as clean shutdown (engine closes socket to signal exit).
Config reload errors: config-verify and config-apply return structured results
with {"status":"ok"} or {"status":"error","error":"..."}. If no handler is registered,
the response is {"status":"ok"} (graceful no-op).
Batched Event Delivery
Events are batched for efficiency. The engine collects pending events from a per-process
channel, JSON-quotes each one, and sends them in a single deliver-batch RPC.
#42 ze-plugin-callback:deliver-batch {"events":["<json-event-1>","<json-event-2>",...]}
#42 ok
The SDK unpacks the batch and dispatches each event to the OnEvent handler individually.
Both deliver-event and deliver-batch handlers are registered in the callback map
when OnEvent is called.
For internal plugins with an active DirectBridge, event delivery bypasses the callback
channel entirely: bridge.DeliverEvents(events) calls the onEvent handler directly
(hot path). The callback channel is only used for non-event callbacks (execute-command,
config-verify, etc.) and bye.