-
Notifications
You must be signed in to change notification settings - Fork 2
go plugins
Pre-Alpha. This page describes behavior that may change.
This page is the five-minute version of writing a Go plugin against the Ze SDK. It covers the skeleton, the lifecycle, registration, the callback model, and how to make engine calls. See YANG schema, Handlers, Commands, and Testing for the deeper material, or the in-tree plugin development guide for the authoritative reference.
Read the plugin development index first if you want the trade-off between the Go path and the external path. This page assumes you have already picked Go.
package main
import (
"context"
"fmt"
"os"
"github.com/ze-software/ze/pkg/plugin/sdk"
)
func main() {
p, err := sdk.NewFromEnv("my-plugin")
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer p.Close()
p.OnEvent(func(event string) error {
fmt.Println("event:", event)
return nil
})
p.SetStartupSubscriptions([]string{"update"}, nil, "")
err = p.Run(context.Background(), sdk.Registration{
Families: []sdk.FamilyDecl{
{Name: "ipv4/unicast", Mode: "both"},
},
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}That is a complete plugin. It connects to the engine over TLS, subscribes to UPDATE events at startup, prints each event to stdout, and runs until the engine sends bye.
go build -o my-plugin
ze plugin validate binary ./my-pluginsdk.NewFromEnv(name) reads the environment variables Ze sets when it launches a plugin.
| 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 cert pinning. |
ZE_PLUGIN_NAME |
Plugin name as configured in Ze. |
The token is bound to the plugin name. A plugin cannot impersonate a different plugin with its own token. The certificate fingerprint is verified during the TLS handshake to prevent man-in-the-middle attacks. The token is cleared from /proc/<pid>/environ after the SDK has read it.
For testing or for in-process plugins, use sdk.NewWithConn(name, conn) to pass an existing net.Conn.
The SDK runs the five-stage startup protocol for you. You register callbacks before Run. The SDK invokes them at the right stage.
| Stage | Method | Purpose |
|---|---|---|
| 1 | argument to Run
|
Plugin declares its families, commands, and schema. |
| 2 | OnConfigure |
Engine delivers the plugin's config sections. |
| 3 | argument to Run
|
Plugin declares BGP capabilities to inject. |
| 4 | OnShareRegistry |
Engine delivers the full command registry. |
| 5 | (implicit) | Plugin enters the runtime event loop. |
After ready, the runtime callbacks are the ones that matter.
| Callback | Signature | When called |
|---|---|---|
OnEvent |
func(string) error |
A BGP event is delivered to the plugin. |
OnExecuteCommand |
func(serial, command string, args []string, peer string) (status, data string, err error) |
A command from another surface targets this plugin. |
OnEncodeNLRI |
func(family string, args []string) (string, error) |
NLRI encode request for a registered family. |
OnDecodeNLRI |
func(family, hex string) (string, error) |
NLRI decode request for a registered family. |
OnConfigVerify |
func([]ConfigSection) error |
Engine asks the plugin to validate a candidate config (reload time). |
OnConfigApply |
func([]ConfigDiffSection) error |
Engine asks the plugin to apply a config diff. |
OnValidateOpen |
func(*ValidateOpenInput) *ValidateOpenOutput |
Plugin can accept or reject a peer's OPEN. |
OnBye |
func(string) |
Shutdown reason. |
OnStarted |
func(ctx) error |
Local setup: start goroutines, register subscriptions, initialise per-plugin state. |
OnAllPluginsReady |
func(ctx) error |
Fires once every plugin registry is frozen. The only safe place to dispatch a command at another plugin. |
Pick the right one of the last two. OnStarted runs during the handshake,
so a command dispatched from it can be written into the stage-5 stream and
leave the engine reading a dispatch frame where it expected ready. That kills
the plugin for the rest of the process lifetime. Anything that calls
DispatchCommand, or that starts a goroutine which will, belongs in
OnAllPluginsReady. The shipped bgp-healthcheck plugin starts its probe
goroutines there for exactly this reason.
Each handshake stage is bounded by progress, not by wall clock. The engine
fails a startup tier only when the whole tier goes ze.plugin.stage.timeout
(default 5s) with no plugin completing a stage; every completion re-arms the
window. A BGP config loads twenty-plus plugins in one tier, so a flat
per-tier budget meant a CPU-starved or slow-disk box could lose all of them at
once and come up with its BGP plugins missing. Uniform slowdown no longer
reaches the barrier. A genuinely wedged tier still trips within one timeout of
the last progress, and a plugin that loops without advancing cannot hold the
window open, because a repeated stage completion is ignored.
Two plugins can both implement a behavior where only one should run it. The plugin that takes over declares the role in its static registration; the other stands down.
reg := registry.Registration{
Name: "bgp-rs",
Claims: []string{"bgp-peer-up-replay"},
}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 from its OnConfigure handler:
if p.ClaimActive("bgp-peer-up-replay") {
// another plugin owns this; do not also do it
}Stage 2 is part of the sequential handshake, so the answer is recorded before this 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.
Do not re-derive a role decision from OnAllPluginsReady: that callback is
fanned out on detached goroutines and races session establishment. An
unclaimed or unresolvable role reads false, which must be the fail-closed
direction for the caller ("nobody promised to do this, keep doing it
yourself").
bgp-rs and bgp-adj-rib-in use this for peer-up replay ownership. Without
it both replayed, and a peer received a byte-identical duplicate UPDATE.
A plugin that decides, on the peer-up event, whether a peer is eligible to
receive traffic sets PeerUpBarrier: true:
reg := registry.Registration{
Name: "bgp-rs",
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. This makes "End-of-RIB sent" mean "every barrier plugin has registered this peer", which is the property a peer, or a test, needs before treating the End-of-RIB as a go-ahead to send.
Without it, an UPDATE arriving between the peer coming up and the plugin registering it as a forward target 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 count is taken over the plugins the peer-up event is actually delivered to, so declaring the field without subscribing to state events does not stall anything.
After startup, the plugin can call back into the engine through methods on the *sdk.Plugin.
| Method | Purpose |
|---|---|
UpdateRoute(ctx, peerSelector, command) |
Inject route updates to matching peers. |
DispatchCommand(ctx, command) |
Route a command through the engine's dispatcher. |
EmitEvent(ctx, namespace, eventType, direction, peer, payload) |
Push an event onto the bus for other plugins to consume. |
SubscribeEvents(ctx, events, peers, format) |
Subscribe to events at runtime (SetStartupSubscriptions is the race-free version for stage 5). |
DecodeNLRI / EncodeNLRI
|
Use the engine's family registry. |
DecodeMPReach, DecodeMPUnreach, DecodeUpdate
|
UPDATE message codec helpers. |
Plugins can register their own ze doctor checks via the DoctorChecks field on the Registration struct. Each check is a function that returns a diagnostic code, severity, and message. Plugin-registered checks run alongside the built-in doctor checks.
reg := registry.Registration{
Name: "my-plugin",
DoctorChecks: []registry.DoctorCheck{
{
Code: "doctor-myplugin-reachable",
Severity: "error",
Check: func() (bool, string) { /* ... */ },
},
},
}Plugins can contribute data to show commands through the show enricher registry. In-process plugins register via show.MustRegister() in init(). When a show command calls show.Enrich(), registered enrichers are invoked to add plugin-contributed fields to the output. External plugins use the ze-plugin-callback:enrich-show callback protocol instead. See External Plugins for the wire-level details.
A Go plugin that wants to expose Prometheus metrics declares a ConfigureMetrics callback in its Registration. The engine calls it with a metrics.Registry before RunEngine. The plugin uses the registry to register counters and gauges, then holds them in package-level state:
import "github.com/ze-software/ze/internal/core/metrics"
var metricsPtr atomic.Pointer[pluginMetrics]
type pluginMetrics struct {
peersUp metrics.Gauge
routesSent metrics.Counter
}
func SetMetricsRegistry(reg metrics.Registry) {
metricsPtr.Store(&pluginMetrics{
peersUp: reg.Gauge("ze_myplugin_peers_up",
"Peers currently up."),
routesSent: reg.Counter("ze_myplugin_routes_sent_total",
"Routes sent by this plugin."),
})
}Then in pluginSetup:
reg := registry.Registration{
Name: "my-plugin",
// ...
ConfigureMetrics: func(reg any) {
if r, ok := reg.(metrics.Registry); ok {
SetMetricsRegistry(r)
}
},
}Metrics land in Ze's Prometheus endpoint automatically, alongside the engine's own metrics. No second scrape target, no separate exporter.
Naming convention. ze_{scope}_{subject}_{detail}. The scope is the plugin area (watchdog, rpki, fib_kernel, sysrib, persist). The subject is what is being measured (peers, routes, cache). The detail is the specific metric (up, sent_total, items). Counters end in _total. Gauges do not.
Shipped examples. The in-tree plugins bgp-rib, bgp-watchdog, bgp-rpki, bgp-persist, fib-kernel, and sysrib each export their own metrics through ConfigureMetrics. Grep for SetMetricsRegistry in the source tree for concrete patterns.
Plugins are bound to peers under the process block, and each plugin has its own plugin { internal <name> { use <plugin-name> } } declaration. Built-in plugins use internal with the use keyword; external process plugins use external with run.
plugin {
hub {
server local {
ip 127.0.0.1;
port 12700;
secret "shared-token-min-32-characters!!!";
}
}
external my-plugin {
run "/usr/local/bin/my-plugin";
encoder json;
}
}
bgp {
peer upstream {
connection { remote { ip 10.0.0.1; } }
session { asn { remote 65001; } }
process my-plugin {
receive [ update state ]
}
}
}
The receive list is the set of base events the plugin wants. Plugins can also register custom event types they emit themselves.
- Plugin development index for the trade-off between Go and external plugins.
- Plugins for the catalogue of plugins already shipped.
- The Go example plugin for a complete worked example.
Adapted from main/docs/plugin-development/README.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