Plugin Handlers
Handlers process engine-initiated callbacks during startup and at runtime.
They are registered on the sdk.Plugin before calling Run().
Registration
Register handlers before calling Run():
p := sdk.NewWithConn("my-plugin", conn)
// Startup handlers (named handler types defined in sdk package)
p.OnConfigure(func(sections []sdk.ConfigSection) error { ... })
p.OnShareRegistry(func(commands []sdk.RegistryCommand) { ... })
// Runtime handlers
p.OnEvent(func(event string) error { ... })
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (string, any, error) { ... })
p.OnConfigVerify(func(sections []sdk.ConfigSection) error { ... })
p.OnConfigApply(func(diffs []sdk.ConfigDiffSection) error { ... })
p.OnValidateOpen(func(input *sdk.ValidateOpenInput) *sdk.ValidateOpenOutput { ... })
p.OnBye(func(reason string) { ... })
// Post-startup callback (safe to make engine calls here)
p.OnStarted(func(ctx context.Context) error { ... })
// NLRI handlers
p.OnEncodeNLRI(func(family string, args []string) (string, error) { ... })
p.OnDecodeNLRI(func(family string, hex string) (any, error) { ... })
p.OnDecodeCapability(func(code uint8, hex string) (any, error) { ... })
p.Run(ctx, sdk.Registration{...})
Handler Reference
OnConfigure
Called during Stage 2 when the engine delivers config sections.
p.OnConfigure(func(sections []sdk.ConfigSection) error {
for _, s := range sections {
// s.Root: config root name (e.g., "bgp")
// s.Data: JSON-encoded config data
var cfg MyConfig
if err := json.Unmarshal([]byte(s.Data), &cfg); err != nil {
return fmt.Errorf("parse config: %w", err)
}
// Store config for later use
}
return nil
})
ConfigSection fields:
| Field | Type | Description |
|---|---|---|
Root |
string |
Config root name (e.g., "bgp") |
Data |
string |
JSON-encoded config data |
Return nil to accept the config, or an error to reject (aborts startup).
OnShareRegistry
Called during Stage 4 when the engine shares the command registry.
p.OnShareRegistry(func(commands []sdk.RegistryCommand) {
for _, cmd := range commands {
// cmd.Name: command name (e.g., "peer")
// cmd.Plugin: plugin that registered it
// cmd.Encoding: encoding format
}
})
RegistryCommand fields:
| Field | Type | Description |
|---|---|---|
Name |
string |
Command name |
Plugin |
string |
Plugin that registered it |
Encoding |
string |
Encoding format |
This handler does not return a value. The registry is informational.
OnEvent
Called at runtime when the engine delivers a BGP event.
p.OnEvent(func(event string) error {
// event is a JSON string containing the BGP event
var evt map[string]any
if err := json.Unmarshal([]byte(event), &evt); err != nil {
return fmt.Errorf("parse event: %w", err)
}
eventType, _ := evt["type"].(string)
switch eventType {
case "state":
// Peer state change (up/down)
case "update":
// BGP UPDATE received
case "sent":
// Route sent to peer
}
return nil
})
Return nil to acknowledge, or an error to signal failure.
OnExecuteCommand
Called at runtime when the engine routes a command to the plugin.
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (status string, data any, err error) {
// serial: request correlation ID
// command: the command name (e.g., "rib adjacent status")
// args: additional arguments
// peer: peer address (if command is peer-scoped)
switch command {
case "rib adjacent status":
result, _ := json.Marshal(map[string]any{
"running": true,
"peers": 2,
})
return "done", string(result), nil
default:
return "", "", fmt.Errorf("unknown command: %s", command)
}
})
Parameters:
| Parameter | Type | Description |
|---|---|---|
serial |
string |
Request correlation ID |
command |
string |
Full command name |
args |
[]string |
Additional arguments |
peer |
string |
Peer address (may be empty) |
Return values:
| Return | Type | Description |
|---|---|---|
status |
string |
"done" or "error" |
data |
string |
JSON-encoded response data |
err |
error |
Non-nil triggers error response |
OnConfigVerify
Called during config reload to validate a candidate config. The plugin receives the full candidate config sections and returns nil to accept or an error to reject. If no handler is registered, config-verify returns OK (no-op).
p.OnConfigVerify(func(sections []sdk.ConfigSection) error {
for _, s := range sections {
var cfg MyConfig
if err := json.Unmarshal([]byte(s.Data), &cfg); err != nil {
return fmt.Errorf("invalid config: %w", err)
}
// Validate semantic constraints
if cfg.HoldTime > 0 && cfg.HoldTime < 3 {
return fmt.Errorf("hold-time must be 0 or >= 3, got %d", cfg.HoldTime)
}
}
return nil // Accept
})
Return nil to accept the candidate config, or an error to reject the reload.
OnConfigApply
Called during config reload to apply config changes. The plugin receives diff sections describing what changed (added, removed, changed) and returns nil to accept or an error to reject. If no handler is registered, config-apply returns OK (no-op).
p.OnConfigApply(func(diffs []sdk.ConfigDiffSection) error {
for _, d := range diffs {
// d.Root: config root name
// d.Added: JSON-encoded added config (may be empty)
// d.Removed: JSON-encoded removed config (may be empty)
// d.Changed: JSON-encoded changed config (may be empty)
if d.Added != "" {
// Apply additions
}
if d.Removed != "" {
// Apply removals
}
if d.Changed != "" {
// Apply modifications
}
}
return nil
})
ConfigDiffSection fields:
| Field | Type | Description |
|---|---|---|
Root |
string |
Config root name (e.g., "bgp") |
Added |
string |
JSON-encoded added config |
Removed |
string |
JSON-encoded removed config |
Changed |
string |
JSON-encoded changed config |
OnValidateOpen
Called when the engine receives an OPEN message and wants the plugin to validate
the session. When this handler is registered, WantsValidateOpen is automatically
set to true in the Stage 1 registration.
p.OnValidateOpen(func(input *sdk.ValidateOpenInput) *sdk.ValidateOpenOutput {
// input.Peer: peer address
// input.Local: local OPEN message (ASN, RouterID, HoldTime, Capabilities)
// input.Remote: remote OPEN message
// Check remote capabilities
for _, cap := range input.Remote.Capabilities {
if cap.Code == 64 { // Graceful Restart
// Validate GR parameters
}
}
// Accept or reject
return &sdk.ValidateOpenOutput{Accept: true}
// To reject:
// return &sdk.ValidateOpenOutput{
// Accept: false,
// NotifyCode: 2, // OPEN Message Error
// NotifySubcode: 6, // Unacceptable Hold Time
// Reason: "hold time too low",
// }
})
ValidateOpenInput fields:
| Field | Type | Description |
|---|---|---|
Peer |
string |
Peer address |
Local |
ValidateOpenMessage |
Local OPEN message |
Remote |
ValidateOpenMessage |
Remote OPEN message |
ValidateOpenMessage fields:
| Field | Type | Description |
|---|---|---|
ASN |
uint32 |
AS number |
RouterID |
string |
Router ID (IP string) |
HoldTime |
uint16 |
Hold time in seconds |
Capabilities |
[]ValidateOpenCapability |
Capabilities (code + hex value) |
ValidateOpenOutput fields:
| Field | Type | Description |
|---|---|---|
Accept |
bool |
true to accept, false to reject |
NotifyCode |
uint8 |
NOTIFICATION code (on reject) |
NotifySubcode |
uint8 |
NOTIFICATION subcode (on reject) |
Reason |
string |
Human-readable reason (on reject) |
If no handler is registered, OPEN validation returns {accept: true} (no-op).
OnBye
Called when the engine sends a shutdown notification. On the pipe transport, the SDK responds OK before invoking this callback. On the bridge transport, the callback runs and then the result is sent back through the channel.
p.OnBye(func(reason string) {
log.Printf("shutting down: %s", reason)
// Clean up resources
})
OnStarted
Called after the 5-stage startup completes but before the event loop begins.
This is the safe place to make engine calls (e.g., SubscribeEvents).
Do NOT make engine calls inside OnShareRegistry or OnConfigure -- those
run while the engine is waiting for the response, causing a deadlock.
p.OnStarted(func(ctx context.Context) error {
// Safe to call engine methods here
return p.SubscribeEvents(ctx, []string{"update", "state"}, []string{"*"}, "json")
})
NLRI Handlers
Plugins that handle address families register encode/decode handlers:
// Encode NLRI from text arguments to hex
p.OnEncodeNLRI(func(family string, args []string) (string, error) {
// args: component keywords (e.g., ["destination", "10.0.0.0/24"])
hex, err := encodeFlowSpec(family, args)
return hex, err
})
// Decode NLRI from hex to data structure (SDK marshals once)
p.OnDecodeNLRI(func(family string, hex string) (any, error) {
result, err := decodeFlowSpec(family, hex)
return result, err
})
// Decode capability from hex to data structure (SDK marshals once)
p.OnDecodeCapability(func(code uint8, hex string) (any, error) {
if code == 73 { // FQDN
return decodeFQDN(hex)
}
return nil, fmt.Errorf("unknown capability code: %d", code)
})
Pre-Run Configuration
These methods configure the plugin before calling Run():
SetStartupSubscriptions
Sets event subscriptions included in the Stage 5 "ready" RPC, registered
atomically before SignalAPIReady. This avoids the race between the engine
sending routes and the plugin subscribing.
p.SetStartupSubscriptions(
[]string{"update", "state"}, // event types
[]string{"*"}, // peer filter
"json", // format
)
SetEncoding
Sets the event encoding preference ("json" or "text"). Text encoding uses
space-delimited output parseable by strings.Fields instead of nested JSON.
Must be called after SetStartupSubscriptions and before Run().
p.SetEncoding("text")
SetCapabilities
Sets the capabilities to declare during Stage 3. Must be called before Run().
p.SetCapabilities([]sdk.CapabilityDecl{
{Code: 64, Encoding: "hex", Payload: "0078", Peers: []string{"192.168.1.1"}},
})
Engine Methods (Plugin to Engine)
These methods are available at runtime (after startup) to call the engine:
UpdateRoute
Injects a route update to matching peers.
peers, routes, err := p.UpdateRoute(ctx, "192.168.1.1", "update text nhop set 10.0.0.1 nlri ipv4/unicast add 10.0.1.0/24")
DispatchCommand
Dispatches a command through the engine's command dispatcher (inter-plugin communication).
status, data, err := p.DispatchCommand(ctx, "rib adjacent inbound show")
EmitEvent
Pushes an event into the engine's delivery pipeline for plugin-to-plugin communication.
delivered, err := p.EmitEvent(ctx, "bgp", "rpki", "received", "192.168.1.1", eventJSON)
SubscribeEvents / UnsubscribeEvents
Subscribe or unsubscribe from event delivery.
err := p.SubscribeEvents(ctx, []string{"update"}, []string{"*"}, "json")
err = p.UnsubscribeEvents(ctx)
DecodeNLRI / EncodeNLRI
Request NLRI encoding/decoding from the engine via the plugin registry.
hex, err := p.EncodeNLRI(ctx, "ipv4/flow", []string{"destination", "10.0.0.0/24"})
json, err := p.DecodeNLRI(ctx, "ipv4/flow", "0701180A0000")
DecodeMPReach / DecodeMPUnreach
Request MP_REACH_NLRI or MP_UNREACH_NLRI decoding from the engine.
out, err := p.DecodeMPReach(ctx, hexAttrValue, false)
// out.Family, out.NextHop, out.NLRI
out, err := p.DecodeMPUnreach(ctx, hexAttrValue, false)
// out.Family, out.NLRI
DecodeUpdate
Request full UPDATE message decoding from the engine.
json, err := p.DecodeUpdate(ctx, hexUpdateBody, false)
Error Handling
Return clear, actionable errors:
// Good: tells user what's wrong and how to fix
return fmt.Errorf("hold-time must be at least %d seconds, got %d",
minHoldTime, cfg.HoldTime)
// Bad: cryptic
return fmt.Errorf("validation failed")
Config verify/apply responses use structured status:
| Situation | Response |
|---|---|
| Handler returns nil | {"status":"ok"} |
| Handler returns error | {"status":"error","error":"reason"} |
| ✕ | {"status":"ok"} (graceful no-op) |
Unknown methods are rejected with an error response:
#5 error {"code":"error","message":"unknown method: ze-plugin-callback:foo"}