Skip to content

external plugins

Thomas Mangin edited this page Apr 8, 2026 · 1 revision

Pre-Alpha. This page describes behavior that may change.

An external plugin is a separate process. Ze launches it, sets a handful of environment variables, and the plugin connects back over a single TLS connection and speaks the plugin protocol. Any language that can read and write lines and parse JSON can implement one. The choice to go external (rather than in-tree Go) is the subject of the plugin development index; this page covers the wire protocol and the startup flow.

The wire format

Every message between the engine and the plugin is a single newline-terminated line.

#<id> <method> [<json-params>]\n           # Request
#<id> ok      [<json-result>]\n            # Success response
#<id> error   [<json-error>]\n             # Error response
  • <id> is a monotonically increasing uint64 correlation id.
  • <method> uses YANG-style <module>:<rpc-name> naming (ze-plugin-engine:declare-registration, ze-plugin-callback:configure).
  • JSON payloads are optional, omitted when empty or null.
  • Responses use ok or error as the verb. Requests use the method name.

A single connection carries every RPC in both directions. MuxConn multiplexes the connection so plugin requests to the engine and engine requests to the plugin do not block each other. A background reader routes incoming lines by verb: ok and error go to the waiting CallRPC caller by id, method-name requests go to the plugin's request handler.

How the connection happens

The engine starts the plugin process with a fixed set of environment variables.

Variable Purpose
ZE_PLUGIN_HUB_HOST TLS host. Default 127.0.0.1.
ZE_PLUGIN_HUB_PORT TLS port. Default 12700.
ZE_PLUGIN_HUB_TOKEN Per-plugin auth token. Cleared from the environment after the SDK reads it.
ZE_PLUGIN_CERT_FP SHA-256 fingerprint of the engine's TLS certificate, for pinning.
ZE_PLUGIN_NAME The plugin name as configured in Ze.

The plugin opens a TLS connection to <HOST>:<PORT>, verifies the engine's certificate fingerprint against ZE_PLUGIN_CERT_FP, and authenticates with ZE_PLUGIN_HUB_TOKEN. The token is bound to the plugin name: a plugin cannot authenticate as a different plugin with its own token.

There is a standalone mode where the plugin uses stdin and stdout with inline MuxConn framing instead of TLS. That mode exists for debugging, for the ExaBGP bridge, and for running a plugin under ze exabgp plugin from the command line.

The 5-stage startup

Every plugin goes through five stages before it enters the runtime event loop. The Go SDK handles the flow for you through Plugin.Run(). For a non-Go external plugin, you implement the stages yourself.

  1. declare-registration (plugin → engine). The plugin declares its families, commands, dependencies, config roots it wants to receive, and its YANG schema. The engine validates the declaration and replies ok.
  2. configure (engine → plugin). The engine delivers the plugin's config sections as a list of {root, data} pairs. The plugin parses them and replies ok, or rejects the config with an error (which aborts startup).
  3. declare-capabilities (plugin → engine). The plugin declares any BGP capabilities it wants the engine to inject into OPEN messages to specific peers. Encoding is hex, b64, or text.
  4. share-registry (engine → plugin). The engine sends the full command registry from every other loaded plugin. Informational: the plugin uses it to route command dispatches.
  5. ready (plugin → engine). The plugin signals readiness with any startup subscriptions (event types, peer filters, format preference). The subscription is attached atomically to the ready signal so no event is lost in the race.

After stage 5, the engine can send deliver-event and execute-command requests at runtime. The plugin can send update-route, emit-event, subscribe-events, dispatch-command, and a handful of codec helpers back. Shutdown is a single bye request from the engine with a reason string.

A wire-level example

Stage 1 registration, then a stage-2 configure, then a runtime event delivery.

#1 ze-plugin-engine:declare-registration {"families":[{"name":"ipv4/flow","mode":"both"}],"commands":[{"name":"flowspec status","description":"Show FlowSpec status"}],"wants-config":["bgp"]}
#1 ok

#1 ze-plugin-callback:configure {"sections":[{"root":"bgp","data":"{\"bgp\":{\"peer\":{...}}}"}]}
#1 ok

#42 ze-plugin-callback:deliver-event {"event":"{\"type\":\"state\",\"bgp\":{\"peer\":{\"address\":\"10.0.0.1\"},\"state\":\"up\"}}"}
#42 ok

The correlation id is reset per direction: the engine and the plugin each allocate their own id space, and MuxConn routes by verb.

What an external plugin can do

Everything an in-tree Go plugin can do, with the exception that in-process optimisations (DirectBridge, shared memory access) are not available. From the API surface:

  • Receive BGP events as JSON.
  • Declare and serve commands (execute-command).
  • Encode and decode NLRI for a registered address family.
  • Extend the config tree with a YANG schema.
  • Verify and apply config diffs at reload time.
  • Inject BGP capabilities into OPEN.
  • Validate incoming OPEN messages.
  • Emit events onto the bus for other plugins to consume.
  • Dispatch commands through the engine.

Writing one in a language that is not Go

The protocol is text and JSON. Any language that can open a TLS socket (or use stdin/stdout in standalone mode), read newline-terminated lines, and parse JSON can implement it.

The moving parts to get right:

  • Line framing: every message is one line terminated with \n.
  • Correlation: maintain a monotonic id counter for outgoing requests and route incoming ok/error back to the caller.
  • Multiplexing: the same connection carries requests in both directions. Your reader must distinguish method-name verbs (which are incoming requests) from ok/error (which are responses to your outgoing requests).
  • The 5-stage startup flow, in order.
  • Cert pinning in the TLS handshake.

A Python implementation that mirrors the ExaBGP programmable model would read events from the connection, write update commands back, and use execute-command as the point where it extends the CLI with its own subcommands. A Rust implementation would use tokio for the async reader. A Ruby implementation would work through OpenSSL::SSL::SSLSocket. None of this requires generated stubs.

See also

Adapted from main/docs/plugin-development/protocol.md.

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally