Skip to content

go plugins

Thomas Mangin edited this page Apr 11, 2026 · 9 revisions

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.

A minimal plugin

package main

import (
    "context"
    "fmt"
    "os"

    "codeberg.org/thomas-mangin/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-plugin

Connecting

sdk.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 lifecycle

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 Post-startup hook. Safe place to make engine calls.

Calling the engine

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.

Prometheus metrics

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 "codeberg.org/thomas-mangin/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-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.

Configuration in the Ze tree

Plugins are bound to peers under the process block, and each plugin has its own plugin { external <name> { use <plugin-name> } } declaration. Built-in plugins use the use keyword; external process plugins use 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.

See also

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

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally