VPP load-balancer dataplane integration: state, sync, and global conf

This commit wires maglevd through to VPP's LB plugin end-to-end, using
locally-generated GoVPP bindings for the newer v2 API messages.

VPP binapi (vendored)
- New package internal/vpp/binapi/ containing lb, lb_types, ip_types, and
  interface_types, generated from a local VPP build (~/src/vpp) via a new
  'make vpp-binapi' target. GoVPP v0.12.0 upstream lacks the v2 messages we
  need (lb_conf_get, lb_add_del_vip_v2, lb_add_del_as_v2, lb_as_v2_dump,
  lb_as_set_weight), so we commit the generated output in-tree.
- All generated files go through our loggedChannel wrapper; every VPP API
  send/receive is recorded at DEBUG via slog (vpp-api-send / vpp-api-recv /
  vpp-api-send-multi / vpp-api-recv-multi) so the full wire-level trail is
  auditable. NewAPIChannel is unexported — callers must use c.apiChannel().

Read path: GetLBState{All,VIP}
- GetLBStateAll returns a full snapshot (global conf + every VIP with its
  attached application servers).
- GetLBStateVIP looks up a single VIP by (prefix, protocol, port) and
  returns (nil, nil) when the VIP doesn't exist in VPP. This is the
  efficient path for targeted updates on a busy LB.
- Helpers factored out: getLBConf, dumpAllVIPs, dumpASesForVIP, lookupVIP,
  vipFromDetails.

Write path: SyncLBState{All,VIP}
- SyncLBStateAll reconciles every configured frontend with VPP: creates
  missing VIPs, removes stale ones (with AS flush), and reconciles AS
  membership and weights within VIPs that exist on both sides.
- SyncLBStateVIP targets a single frontend by name. Never removes VIPs.
  Returns ErrFrontendNotFound (wrapped with the name) when the frontend
  isn't in config, so callers can use errors.Is.
- Shared reconcileVIP helper does the per-VIP AS diff; removeVIP is used
  only by the full-sync pass.
- LbAddDelVipV2 requests always set NewFlowsTableLength=1024. The .api
  default=1024 annotation is only applied by VAT/CLI parsers, not wire-
  level marshalling — sending 0 caused VPP to vec_validate with mask
  0xFFFFFFFF and OOM-panic.
- Pool semantics: backends in the primary (first) pool of a frontend get
  their configured weight; backends in secondary pools get weight 0. All
  backends are installed so higher layers can flip weights on failover
  without add/remove churn.
- Every individual change emits a DEBUG slog (vpp-lbsync-vip-add/del,
  vpp-lbsync-as-add/del, vpp-lbsync-as-weight). Start/done INFO logs
  carry a scope=all|vip label plus aggregate counts.

Global conf push: SetLBConf
- New SetLBConf(cfg) sends lb_conf with ipv4-src, ipv6-src, sticky-buckets,
  and flow-timeout. Called automatically on VPP (re)connect and after
  every config reload (via doReloadConfig). Results are cached on the
  Client so redundant pushes are silently skipped — only actual changes
  produce a vpp-lb-conf-set INFO log line.

Periodic drift reconciliation
- vpp.Client.lbSyncLoop runs in a goroutine tied to each VPP connection's
  lifetime. Its first tick is immediate (startup and post-reconnect
  sync quickly); subsequent ticks fire every vpp.lb.sync-interval from
  config (default 30s). Purpose: catch drift if something/someone
  modifies VPP state by hand. The loop uses a ConfigSource interface
  (satisfied by checker.Checker via its new Config() accessor) to avoid
  an import cycle with the checker package.

Config schema additions (maglev.vpp.lb)
- sync-interval: positive Go duration, default 30s.
- ipv4-src-address: REQUIRED. Used as the outer source for GRE4 encap
  to application servers. Missing this is a hard semantic error —
  maglevd --check exits 2 and the daemon refuses to start. VPP GRE
  needs a source address and every VIP we program uses GRE, so there
  is no meaningful config without it.
- ipv6-src-address: REQUIRED. Same treatment as ipv4-src-address.
- sticky-buckets-per-core: default 65536, must be a power of 2.
- flow-timeout: default 40s, must be a whole number of seconds in [1s, 120s].
- VPP validation runs at the end of convert() so structural errors in
  healthchecks/backends/frontends surface first — operators fix those,
  then get the VPP-specific requirements.

gRPC API
- New GetVPPLBState RPC returning VPPLBState: global conf + VIPs with
  ASes. Mirrors the read-path but strips fields irrelevant to our
  GRE-only deployment (srv_type, dscp, target_port).
- New SyncVPPLBState RPC with optional frontend_name. Unset → full sync
  (may remove stale VIPs). Set → single-VIP sync (never removes).
  Returns codes.NotFound for unknown frontends, codes.Unavailable when
  VPP integration is disabled or disconnected.

maglevc (CLI)
- New 'show vpp lbstate' command displaying the LB plugin state. VPP-only
  fields the dataplane irrelevant to GRE are suppressed. Per-AS lines use
  a key-value format ("address X  weight Y  flow-table-buckets Z")
  instead of a tabwriter column, which avoids the ANSI-color alignment
  issue we hit with mixed label/data rows.
- New 'sync vpp lbstate [<name>]' command. Without a name, triggers a
  full reconciliation; with a name, targets one frontend.
- Previous 'show vpp lb' renamed to 'show vpp lbstate' for consistency
  with the new sync command.

Test fixtures
- validConfig and all ad-hoc config_test.go fixtures that reach the end
  of convert() now include the two required vpp.lb src addresses.
- tests/01-maglevd/maglevd-lab/maglev.yaml gains a vpp.lb section so the
  robot integration tests can still load the config.
- cmd/maglevc/tree_test.go gains expected paths for the new commands.

Docs
- config-guide.md: new 'vpp' section in the basic structure, detailed
  vpp.lb field reference, noting ipv4/ipv6 src addresses as REQUIRED
  (hard error) with no defaults; example config updated.
- user-guide.md: documented 'show vpp info', 'show vpp lbstate',
  'sync vpp lbstate [<name>]', new --vpp-api-addr and --vpp-stats-addr
  flags, the vpp-lb-conf-set log line, and corrected the pause/resume
  description to reflect that pause cancels the probe goroutine.
- debian/maglev.yaml: example config gains a vpp.lb block with src
  addresses and commented optional overrides.
This commit is contained in:
2026-04-12 10:58:39 +02:00
parent 3227263d68
commit d3c5c86037
24 changed files with 4900 additions and 161 deletions

View File

@@ -14,7 +14,9 @@ LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
TEST ?= tests/
.PHONY: all build build-amd64 build-arm64 test proto lint fixstyle pkg-deb robot-test clean
VPP_API_DIR ?= $(HOME)/src/vpp/build-root/install-vpp_debug-native/vpp/share/vpp/api
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle pkg-deb robot-test clean
all: build
@@ -48,6 +50,27 @@ $(GEN_FILES): $(PROTO_FILE)
--go-grpc_out=. --go-grpc_opt=module=$(MODULE) \
$(PROTO_FILE)
# vpp-binapi regenerates the Go bindings for VPP API files used by maglevd
# from a local VPP build. The LB plugin ships with upstream VPP; any newer
# messages (e.g. lb_conf_get, lb_as_v2_dump) require a VPP build that has
# them. Override VPP_API_DIR on the command line to point at another tree:
# make vpp-binapi VPP_API_DIR=/path/to/share/vpp/api
vpp-binapi:
@command -v binapi-generator >/dev/null 2>&1 || { \
echo "installing binapi-generator..."; \
go install go.fd.io/govpp/cmd/binapi-generator@v0.12.0; \
}
rm -rf internal/vpp/binapi
mkdir -p internal/vpp/binapi
binapi-generator \
--input=$(VPP_API_DIR) \
--output-dir=internal/vpp/binapi \
--import-prefix=$(MODULE)/internal/vpp/binapi \
--no-source-path-info \
--no-version-info \
lb lb_types
rm -f internal/vpp/binapi/lb/lb_rpc.ba.go
fixstyle:
gofmt -w .

View File

@@ -71,12 +71,13 @@ func buildTree() *Node {
Children: []*Node{showHealthCheckName},
}
// show vpp info
// show vpp info / lbstate
showVPPInfo := &Node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
showVPPLBState := &Node{Word: "lbstate", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState}
showVPP := &Node{
Word: "vpp",
Help: "VPP dataplane information",
Children: []*Node{showVPPInfo},
Children: []*Node{showVPPInfo, showVPPLBState},
}
show.Children = []*Node{
@@ -174,7 +175,34 @@ func buildTree() *Node {
Children: []*Node{configCheck, configReload},
}
root.Children = []*Node{show, set, watch, configNode, quit, exit}
// sync vpp lbstate [<name>]
//
// Without a name: run SyncLBStateAll (may remove stale VIPs).
// With a name: run SyncLBStateVIP(name) for just that frontend (no removals).
syncVPPLBStateName := &Node{
Word: "<name>",
Help: "Sync a single frontend's VIP to VPP",
Dynamic: dynFrontends,
Run: runSyncVPPLBState,
}
syncVPPLBState := &Node{
Word: "lbstate",
Help: "Sync the VPP load-balancer dataplane from the running config",
Run: runSyncVPPLBState,
Children: []*Node{syncVPPLBStateName},
}
syncVPP := &Node{
Word: "vpp",
Help: "VPP dataplane sync commands",
Children: []*Node{syncVPPLBState},
}
syncNode := &Node{
Word: "sync",
Help: "Reconcile dataplane state from the running config",
Children: []*Node{syncVPP},
}
root.Children = []*Node{show, set, watch, configNode, syncNode, quit, exit}
return root
}
@@ -235,6 +263,89 @@ func runShowVPPInfo(ctx context.Context, client grpcapi.MaglevClient, _ []string
return w.Flush()
}
func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
state, err := client.GetVPPLBState(ctx, &grpcapi.GetVPPLBStateRequest{})
if err != nil {
return err
}
// ---- global config ----
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "%s\n", label("global"))
if state.Conf.Ip4SrcAddress != "" {
fmt.Fprintf(w, " %s\t%s\n", label("ip4-src"), state.Conf.Ip4SrcAddress)
}
if state.Conf.Ip6SrcAddress != "" {
fmt.Fprintf(w, " %s\t%s\n", label("ip6-src"), state.Conf.Ip6SrcAddress)
}
fmt.Fprintf(w, " %s\t%d\n", label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore)
fmt.Fprintf(w, " %s\t%ds\n", label("flow-timeout"), state.Conf.FlowTimeout)
if err := w.Flush(); err != nil {
return err
}
if len(state.Vips) == 0 {
fmt.Println(label("vips") + " (none)")
return nil
}
// ---- per-VIP details ----
for _, v := range state.Vips {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "%s\t%s\n", label("vip"), v.Prefix)
fmt.Fprintf(w, " %s\t%s\n", label("protocol"), protoString(v.Protocol))
fmt.Fprintf(w, " %s\t%d\n", label("port"), v.Port)
fmt.Fprintf(w, " %s\t%s\n", label("encap"), v.Encap)
fmt.Fprintf(w, " %s\t%d\n", label("flow-table-length"), v.FlowTableLength)
fmt.Fprintf(w, " %s\t%d\n", label("application-servers"), len(v.ApplicationServers))
if err := w.Flush(); err != nil {
return err
}
for _, a := range v.ApplicationServers {
fmt.Printf(" %s %s %s %d %s %d\n",
label("address"), a.Address,
label("weight"), a.Weight,
label("flow-table-buckets"), a.NumBuckets)
}
}
return nil
}
// protoString renders an IP protocol number as a name (tcp, udp, any, or numeric).
func protoString(p uint32) string {
switch p {
case 6:
return "tcp"
case 17:
return "udp"
case 255:
return "any"
}
return fmt.Sprintf("%d", p)
}
func runSyncVPPLBState(ctx context.Context, client grpcapi.MaglevClient, args []string) error {
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
req := &grpcapi.SyncVPPLBStateRequest{}
if len(args) > 0 && args[0] != "" {
name := args[0]
req.FrontendName = &name
}
if _, err := client.SyncVPPLBState(ctx, req); err != nil {
return err
}
if req.FrontendName != nil {
fmt.Printf("synced frontend %q to VPP\n", *req.FrontendName)
} else {
fmt.Println("synced full LB state to VPP")
}
return nil
}
func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
fmt.Printf("maglevc %s (commit %s, built %s)\n",
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())

View File

@@ -29,7 +29,10 @@ func TestExpandPathsRoot(t *testing.T) {
"watch events <opt>",
"config check",
"show vpp info",
"show vpp lbstate",
"config reload",
"sync vpp lbstate",
"sync vpp lbstate <name>",
"quit",
"exit",
}
@@ -60,9 +63,9 @@ func TestExpandPathsShow(t *testing.T) {
}
}
// version, frontends, frontends <name>, backends, backends <name>,
// healthchecks, healthchecks <name>, vpp info = 8 lines
if len(lines) != 8 {
t.Errorf("expected exactly 8 show subcommands, got %d", len(lines))
// healthchecks, healthchecks <name>, vpp info, vpp lb = 9 lines
if len(lines) != 9 {
t.Errorf("expected exactly 9 show subcommands, got %d", len(lines))
}
}

View File

@@ -99,6 +99,7 @@ func run() error {
var vppClient *vpp.Client
if *vppAPIAddr != "" {
vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr)
vppClient.SetConfigSource(chkr)
go vppClient.Run(ctx)
}

7
debian/maglev.yaml vendored
View File

@@ -3,6 +3,13 @@ maglev:
transition-history: 5
# netns: dataplane # run probes inside a named network namespace
vpp:
lb:
ipv4-src-address: 192.0.2.254 # source for GRE4 encap to application servers
ipv6-src-address: 2001:db8::254 # source for GRE6 encap to application servers
# sticky-buckets-per-core: 65536 # power of 2, default 65536
# flow-timeout: 40s # 1s-120s, default 40s
healthchecks:
http-check:
type: http

View File

@@ -11,7 +11,7 @@ in two stages:
ensuring that every backend referenced by a frontend exists, that address families are
consistent within a frontend, and that IP source addresses are the correct family.
If you want to get started quickly, take a look at the [[example config](../debian/mavleg.yaml)].
If you want to get started quickly, take a look at the [example config](../debian/maglev.yaml).
## Basic structure
@@ -22,6 +22,10 @@ maglev:
healthchecker:
[ Global health checker settings ]
vpp:
lb:
[ VPP load-balancer integration settings ]
healthchecks:
my-check:
[ Health check definition ]
@@ -35,9 +39,11 @@ maglev:
[ Frontend (VIP) definition ]
```
All four sections live under the top-level `maglev:` key. The `healthchecks`, `backends`,
All five sections live under the top-level `maglev:` key. The `healthchecks`, `backends`,
and `frontends` sections are maps keyed by an arbitrary name of your choosing. Names must be
unique within their section and are case-sensitive.
unique within their section and are case-sensitive. The `vpp` section is required when
`maglevd` has a working VPP connection — its `lb.ipv4-src-address` and `lb.ipv6-src-address`
fields are mandatory and `maglevd` will refuse to start without them.
---
@@ -61,6 +67,50 @@ maglev:
---
## vpp
Settings controlling the integration with a locally running VPP instance. The
`vpp` section is a map with a single sub-section, `lb`. Both `lb.ipv4-src-address`
and `lb.ipv6-src-address` are **required**`maglevd --check` exits with a
semantic error and the daemon refuses to start when either is missing, because
VPP's GRE encap needs a source address and every VIP `maglevd` programs uses GRE.
* ***lb.ipv4-src-address***: Required. The IPv4 source address VPP uses when
encapsulating IPv4 traffic into GRE4 tunnels to application servers. Must
be a valid IPv4 address. No default.
* ***lb.ipv6-src-address***: Required. The IPv6 source address VPP uses when
encapsulating IPv6 traffic into GRE6 tunnels. Must be a valid IPv6 address.
No default.
* ***lb.sync-interval***: A positive Go duration (e.g. `30s`, `1m`) controlling
how often `maglevd` reconciles the VPP load-balancer dataplane against its
running configuration. On startup, an immediate full sync runs; subsequent
syncs fire at this interval as long as the VPP connection is up. Defaults
to `30s`. The purpose is to catch drift — for example, a VIP added to VPP
by hand — and bring VPP back in line with the maglev config.
* ***lb.sticky-buckets-per-core***: The number of buckets per worker thread in
the established-flow table. Must be a power of 2. Defaults to `65536` (64k).
* ***lb.flow-timeout***: Idle time after which an established flow is removed
from the table. Must be a whole number of seconds between `1s` and `120s`
inclusive. Defaults to `40s`.
These four values are pushed to VPP via `lb_conf` when `maglevd` connects to
VPP and again after every config reload (whenever they change). A log line
`vpp-lb-conf-set` records the effective values.
Example:
```yaml
maglev:
vpp:
lb:
sync-interval: 60s
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
sticky-buckets-per-core: 65536
flow-timeout: 40s
```
---
## healthchecks
A named map of health check definitions. Each health check describes *how* to probe a backend.
@@ -278,6 +328,6 @@ frontends:
---
For a detailed description of the health state machine, probe intervals, and all transition events,
see [[healthchecks.md](healthchecks.md)]. For a user guide on how to use the maglev daemon and client,
see the [[user-guide.md](user-guide.md)].
see [healthchecks.md](healthchecks.md). For a user guide on how to use the maglev daemon and client,
see the [user-guide.md](user-guide.md).

View File

@@ -88,10 +88,26 @@ show healthchecks [<name>] Without name: list all health-check names.
show vpp info Show VPP version, build date, PID, uptime, and when
maglevd connected. Returns an error if VPP is not
connected.
show vpp lbstate Show the VPP load-balancer plugin state: global
configuration, configured VIPs, and their attached
application servers (address, weight, bucket count).
Returns an error if VPP is not connected.
set backend <name> pause Suspend health checking for a backend, freezing its state.
set backend <name> resume Resume health checking; backend re-enters unknown state
and is probed immediately.
sync vpp lbstate [<name>] Reconcile the VPP load-balancer dataplane from the
running config. Without a name: runs a full sync —
creates missing VIPs, removes stale VIPs, and adjusts
application-server membership and weights across all
frontends. With a name: only the named frontend's VIP
is reconciled, and no VIPs are removed. A full sync
also runs automatically every
maglev.vpp.lb.sync-interval (default 30s) to catch
drift, and once on startup.
set backend <name> pause Stop health checking for a backend. Cancels the probe
goroutine so no further traffic is sent, and freezes
the state at whatever it was when paused.
set backend <name> resume Resume health checking. A fresh probe goroutine is
started and the backend re-enters unknown state.
set backend <name> disable Stop probing entirely and remove the backend from rotation.
The backend remains visible (state: disabled) and can be
re-enabled without reloading configuration.

View File

@@ -152,6 +152,15 @@ func (c *Checker) Subscribe() (<-chan Event, func()) {
}
}
// Config returns the live config pointer held by the checker. Callers must
// treat the returned value as read-only. The pointer is swapped on Reload,
// so callers that cache it across reloads may see stale data.
func (c *Checker) Config() *config.Config {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg
}
// ListFrontends returns the names of all configured frontends.
func (c *Checker) ListFrontends() []string {
c.mu.RLock()

View File

@@ -17,6 +17,7 @@ import (
// Config is the top-level parsed and validated configuration.
type Config struct {
HealthChecker HealthCheckerConfig
VPP VPPConfig
HealthChecks map[string]HealthCheck
Backends map[string]Backend
Frontends map[string]Frontend
@@ -28,6 +29,37 @@ type HealthCheckerConfig struct {
Netns string // network namespace for probes; "" = current netns
}
// VPPConfig holds VPP-related configuration.
type VPPConfig struct {
LB VPPLBConfig
}
// VPPLBConfig holds load-balancer integration settings.
type VPPLBConfig struct {
// SyncInterval is how often the full dataplane reconciliation runs,
// catching drift (e.g. manual changes to VPP). Defaults to 30s.
SyncInterval time.Duration
// IPv4SrcAddress is the source address VPP uses when encapsulating
// IPv4 traffic into GRE4 tunnels to application servers. Required
// when any frontend uses an IPv4 VIP; VPP GRE encap will fail if unset.
IPv4SrcAddress net.IP
// IPv6SrcAddress is the source address VPP uses when encapsulating
// IPv6 traffic into GRE6 tunnels. Required when any frontend uses an
// IPv6 VIP; VPP GRE encap will fail if unset.
IPv6SrcAddress net.IP
// StickyBucketsPerCore is the number of buckets (per worker thread) in
// the established-flow table. Must be a power of 2. Defaults to 65536.
StickyBucketsPerCore uint32
// FlowTimeout is the idle time after which an established flow is
// removed from the table. Must be between 1 and 120 seconds inclusive.
// Defaults to 40s.
FlowTimeout time.Duration
}
// HealthCheck describes how to probe a backend.
type HealthCheck struct {
Type string
@@ -97,6 +129,7 @@ type rawConfig struct {
type rawMaglev struct {
HealthChecker rawHealthCheckerCfg `yaml:"healthchecker"`
VPP rawVPPCfg `yaml:"vpp"`
HealthChecks map[string]rawHealthCheck `yaml:"healthchecks"`
Backends map[string]rawBackend `yaml:"backends"`
Frontends map[string]rawFrontend `yaml:"frontends"`
@@ -107,6 +140,18 @@ type rawHealthCheckerCfg struct {
Netns string `yaml:"netns"`
}
type rawVPPCfg struct {
LB rawVPPLBCfg `yaml:"lb"`
}
type rawVPPLBCfg struct {
SyncInterval string `yaml:"sync-interval"` // Go duration; default 30s
IPv4SrcAddress string `yaml:"ipv4-src-address"`
IPv6SrcAddress string `yaml:"ipv6-src-address"`
StickyBucketsPerCore *uint32 `yaml:"sticky-buckets-per-core"` // default 65536
FlowTimeout string `yaml:"flow-timeout"` // Go duration; default 40s, [1-120]s
}
type rawHealthCheck struct {
Type string `yaml:"type"`
Port uint16 `yaml:"port"`
@@ -255,9 +300,93 @@ func convert(r *rawMaglev) (*Config, error) {
cfg.Frontends[name] = fe
}
// ---- vpp ------------------------------------------------------------------
// Runs last so structural errors in healthchecks/backends/frontends are
// reported first; operators fix those, then we tell them about the VPP
// src-address requirements.
if err := convertVPP(&r.VPP, &cfg.VPP); err != nil {
return nil, err
}
return cfg, nil
}
// convertVPP parses and validates the maglev.vpp section. Missing src-address
// fields are tolerated but logged at ERROR level so operators notice that VPP
// GRE encap will fail without them.
func convertVPP(r *rawVPPCfg, cfg *VPPConfig) error {
// sync-interval: default 30s, must be > 0.
if s := r.LB.SyncInterval; s != "" {
d, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("vpp.lb.sync-interval: %w", err)
}
if d <= 0 {
return fmt.Errorf("vpp.lb.sync-interval must be > 0")
}
cfg.LB.SyncInterval = d
} else {
cfg.LB.SyncInterval = 30 * time.Second
}
// ipv4-src-address: optional here, but warned below if missing.
if s := r.LB.IPv4SrcAddress; s != "" {
ip := net.ParseIP(s)
if ip == nil || ip.To4() == nil {
return fmt.Errorf("vpp.lb.ipv4-src-address: %q is not a valid IPv4 address", s)
}
cfg.LB.IPv4SrcAddress = ip.To4()
}
// ipv6-src-address: optional here, but warned below if missing.
if s := r.LB.IPv6SrcAddress; s != "" {
ip := net.ParseIP(s)
if ip == nil || ip.To4() != nil {
return fmt.Errorf("vpp.lb.ipv6-src-address: %q is not a valid IPv6 address", s)
}
cfg.LB.IPv6SrcAddress = ip.To16()
}
// sticky-buckets-per-core: default 65536, must be power of 2.
if p := r.LB.StickyBucketsPerCore; p != nil {
n := *p
if n == 0 || n&(n-1) != 0 {
return fmt.Errorf("vpp.lb.sticky-buckets-per-core: %d must be a power of 2", n)
}
cfg.LB.StickyBucketsPerCore = n
} else {
cfg.LB.StickyBucketsPerCore = 65536
}
// flow-timeout: default 40s, must be 1-120s inclusive and a whole number of seconds.
if s := r.LB.FlowTimeout; s != "" {
d, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("vpp.lb.flow-timeout: %w", err)
}
if d%time.Second != 0 {
return fmt.Errorf("vpp.lb.flow-timeout: %s must be a whole number of seconds", d)
}
if d < time.Second || d > 120*time.Second {
return fmt.Errorf("vpp.lb.flow-timeout: %s out of range [1s, 120s]", d)
}
cfg.LB.FlowTimeout = d
} else {
cfg.LB.FlowTimeout = 40 * time.Second
}
// A missing src address is a hard error: VPP's GRE encap needs a source,
// and every VIP we program uses GRE. Fail the config check so the
// operator cannot start maglevd with a broken setup.
if cfg.LB.IPv4SrcAddress == nil {
return fmt.Errorf("vpp.lb.ipv4-src-address must be set; VPP GRE4 encap will fail for IPv4 VIPs")
}
if cfg.LB.IPv6SrcAddress == nil {
return fmt.Errorf("vpp.lb.ipv6-src-address must be set; VPP GRE6 encap will fail for IPv6 VIPs")
}
return nil
}
func convertHealthCheck(r *rawHealthCheck) (HealthCheck, error) {
h := HealthCheck{Type: r.Type, Port: r.Port}

View File

@@ -12,6 +12,10 @@ maglev:
healthchecker:
transition-history: 5
netns: dataplane
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
http-check:
type: http
@@ -150,6 +154,10 @@ func TestValidConfig(t *testing.T) {
func TestDefaults(t *testing.T) {
raw := `
maglev:
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
icmp:
type: icmp
@@ -196,6 +204,10 @@ func TestBackendNoHealthcheck(t *testing.T) {
// A backend with no healthcheck reference is valid; probe is skipped.
raw := `
maglev:
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks: {}
backends:
be:
@@ -220,6 +232,10 @@ maglev:
func TestOptionalIntervals(t *testing.T) {
raw := `
maglev:
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
icmp:
type: icmp
@@ -259,6 +275,10 @@ func TestValidationErrors(t *testing.T) {
base := func(hcExtra, beExtra, feExtra string) string {
return `
maglev:
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
c:
type: icmp
@@ -294,6 +314,10 @@ maglev:
name: "mixed backend address families in pool",
yaml: `
maglev:
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
c:
type: icmp

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,8 @@ const (
Maglev_CheckConfig_FullMethodName = "/maglev.Maglev/CheckConfig"
Maglev_ReloadConfig_FullMethodName = "/maglev.Maglev/ReloadConfig"
Maglev_GetVPPInfo_FullMethodName = "/maglev.Maglev/GetVPPInfo"
Maglev_GetVPPLBState_FullMethodName = "/maglev.Maglev/GetVPPLBState"
Maglev_SyncVPPLBState_FullMethodName = "/maglev.Maglev/SyncVPPLBState"
)
// MaglevClient is the client API for Maglev service.
@@ -57,6 +59,8 @@ type MaglevClient interface {
CheckConfig(ctx context.Context, in *CheckConfigRequest, opts ...grpc.CallOption) (*CheckConfigResponse, error)
ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error)
GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, opts ...grpc.CallOption) (*VPPInfo, error)
GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error)
SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error)
}
type maglevClient struct {
@@ -226,6 +230,26 @@ func (c *maglevClient) GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, op
return out, nil
}
func (c *maglevClient) GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(VPPLBState)
err := c.cc.Invoke(ctx, Maglev_GetVPPLBState_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *maglevClient) SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SyncVPPLBStateResponse)
err := c.cc.Invoke(ctx, Maglev_SyncVPPLBState_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MaglevServer is the server API for Maglev service.
// All implementations must embed UnimplementedMaglevServer
// for forward compatibility.
@@ -247,6 +271,8 @@ type MaglevServer interface {
CheckConfig(context.Context, *CheckConfigRequest) (*CheckConfigResponse, error)
ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error)
GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error)
GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error)
SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error)
mustEmbedUnimplementedMaglevServer()
}
@@ -302,6 +328,12 @@ func (UnimplementedMaglevServer) ReloadConfig(context.Context, *ReloadConfigRequ
func (UnimplementedMaglevServer) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) {
return nil, status.Error(codes.Unimplemented, "method GetVPPInfo not implemented")
}
func (UnimplementedMaglevServer) GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error) {
return nil, status.Error(codes.Unimplemented, "method GetVPPLBState not implemented")
}
func (UnimplementedMaglevServer) SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SyncVPPLBState not implemented")
}
func (UnimplementedMaglevServer) mustEmbedUnimplementedMaglevServer() {}
func (UnimplementedMaglevServer) testEmbeddedByValue() {}
@@ -586,6 +618,42 @@ func _Maglev_GetVPPInfo_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler)
}
func _Maglev_GetVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetVPPLBStateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MaglevServer).GetVPPLBState(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Maglev_GetVPPLBState_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MaglevServer).GetVPPLBState(ctx, req.(*GetVPPLBStateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Maglev_SyncVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SyncVPPLBStateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MaglevServer).SyncVPPLBState(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Maglev_SyncVPPLBState_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MaglevServer).SyncVPPLBState(ctx, req.(*SyncVPPLBStateRequest))
}
return interceptor(ctx, in, info, handler)
}
// Maglev_ServiceDesc is the grpc.ServiceDesc for Maglev service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -649,6 +717,14 @@ var Maglev_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetVPPInfo",
Handler: _Maglev_GetVPPInfo_Handler,
},
{
MethodName: "GetVPPLBState",
Handler: _Maglev_GetVPPLBState_Handler,
},
{
MethodName: "SyncVPPLBState",
Handler: _Maglev_SyncVPPLBState_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -4,6 +4,7 @@ package grpcapi
import (
"context"
"errors"
"log/slog"
"net"
@@ -255,6 +256,13 @@ func (s *Server) doReloadConfig() *ReloadConfigResponse {
ReloadError: err.Error(),
}
}
// Push new global LB conf to VPP if anything changed. SetLBConf is a
// no-op when VPP isn't connected or when the values are unchanged.
if s.vppClient != nil {
if err := s.vppClient.SetLBConf(newCfg); err != nil {
slog.Warn("vpp-lb-conf-set-failed", "err", err)
}
}
slog.Info("config-reload-done", "frontends", len(newCfg.Frontends))
return &ReloadConfigResponse{Ok: true}
}
@@ -282,6 +290,84 @@ func (s *Server) GetVPPInfo(_ context.Context, _ *GetVPPInfoRequest) (*VPPInfo,
}, nil
}
// GetVPPLBState returns a snapshot of the VPP load-balancer plugin state.
func (s *Server) GetVPPLBState(_ context.Context, _ *GetVPPLBStateRequest) (*VPPLBState, error) {
if s.vppClient == nil {
return nil, status.Error(codes.Unavailable, "VPP integration is disabled")
}
state, err := s.vppClient.GetLBStateAll()
if err != nil {
return nil, status.Errorf(codes.Unavailable, "%v", err)
}
return lbStateToProto(state), nil
}
// SyncVPPLBState runs the LB reconciler. With frontend_name unset it does a
// full sync (SyncLBStateAll), which may remove stale VIPs. With frontend_name
// set it does a single-VIP sync (SyncLBStateVIP) that only adds/updates.
func (s *Server) SyncVPPLBState(_ context.Context, req *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) {
if s.vppClient == nil {
return nil, status.Error(codes.Unavailable, "VPP integration is disabled")
}
cfg := s.checker.Config()
if req.FrontendName != nil && *req.FrontendName != "" {
if err := s.vppClient.SyncLBStateVIP(cfg, *req.FrontendName); err != nil {
if errors.Is(err, vpp.ErrFrontendNotFound) {
return nil, status.Errorf(codes.NotFound, "%v", err)
}
return nil, status.Errorf(codes.Unavailable, "%v", err)
}
return &SyncVPPLBStateResponse{}, nil
}
if err := s.vppClient.SyncLBStateAll(cfg); err != nil {
return nil, status.Errorf(codes.Unavailable, "%v", err)
}
return &SyncVPPLBStateResponse{}, nil
}
// lbStateToProto converts the vpp package's LBState into the proto message.
func lbStateToProto(s *vpp.LBState) *VPPLBState {
out := &VPPLBState{
Conf: &VPPLBConf{
Ip4SrcAddress: ipStringOrEmpty(s.Conf.IP4SrcAddress),
Ip6SrcAddress: ipStringOrEmpty(s.Conf.IP6SrcAddress),
StickyBucketsPerCore: s.Conf.StickyBucketsPerCore,
FlowTimeout: s.Conf.FlowTimeout,
},
}
for _, v := range s.VIPs {
pv := &VPPLBVIP{
Prefix: v.Prefix.String(),
Protocol: uint32(v.Protocol),
Port: uint32(v.Port),
Encap: v.Encap,
FlowTableLength: uint32(v.FlowTableLength),
}
for _, a := range v.ASes {
var ts int64
if !a.InUseSince.IsZero() {
ts = a.InUseSince.UnixNano()
}
pv.ApplicationServers = append(pv.ApplicationServers, &VPPLBAS{
Address: a.Address.String(),
Weight: uint32(a.Weight),
Flags: uint32(a.Flags),
NumBuckets: a.NumBuckets,
InUseSinceNs: ts,
})
}
out.Vips = append(out.Vips, pv)
}
return out
}
func ipStringOrEmpty(ip net.IP) string {
if len(ip) == 0 || ip.IsUnspecified() {
return ""
}
return ip.String()
}
// ---- conversion helpers ----------------------------------------------------
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {

122
internal/vpp/apilog.go Normal file
View File

@@ -0,0 +1,122 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"fmt"
"log/slog"
"go.fd.io/govpp/api"
)
// loggedChannel wraps an api.Channel so that every VPP request/reply is
// recorded via slog at DEBUG level. All code in this package MUST send VPP
// messages through a loggedChannel (via Client.apiChannel) so we have a
// complete audit trail of what was sent to the dataplane.
type loggedChannel struct {
ch api.Channel
}
// apiChannel opens a new API channel wrapped in logging. This is the only
// approved way to talk to VPP; do not call conn.NewAPIChannel directly.
func (c *Client) apiChannel() (*loggedChannel, error) {
c.mu.Lock()
conn := c.apiConn
c.mu.Unlock()
if conn == nil {
return nil, errNotConnected
}
ch, err := conn.NewAPIChannel()
if err != nil {
return nil, err
}
return &loggedChannel{ch: ch}, nil
}
// Close closes the underlying channel.
func (lc *loggedChannel) Close() { lc.ch.Close() }
// SendRequest logs the outgoing message and returns a wrapped request context.
func (lc *loggedChannel) SendRequest(msg api.Message) *loggedRequestCtx {
slog.Debug("vpp-api-send",
"msg", msg.GetMessageName(),
"crc", msg.GetCrcString(),
"payload", fmt.Sprintf("%+v", msg),
)
return &loggedRequestCtx{
ctx: lc.ch.SendRequest(msg),
name: msg.GetMessageName(),
}
}
// SendMultiRequest logs the outgoing message and returns a wrapped multi-request context.
func (lc *loggedChannel) SendMultiRequest(msg api.Message) *loggedMultiRequestCtx {
slog.Debug("vpp-api-send-multi",
"msg", msg.GetMessageName(),
"crc", msg.GetCrcString(),
"payload", fmt.Sprintf("%+v", msg),
)
return &loggedMultiRequestCtx{
ctx: lc.ch.SendMultiRequest(msg),
name: msg.GetMessageName(),
}
}
// loggedRequestCtx wraps api.RequestCtx and logs the reply on ReceiveReply.
type loggedRequestCtx struct {
ctx api.RequestCtx
name string
}
func (r *loggedRequestCtx) ReceiveReply(msg api.Message) error {
err := r.ctx.ReceiveReply(msg)
if err != nil {
slog.Debug("vpp-api-recv",
"req", r.name,
"reply", msg.GetMessageName(),
"err", err,
)
return err
}
slog.Debug("vpp-api-recv",
"req", r.name,
"reply", msg.GetMessageName(),
"payload", fmt.Sprintf("%+v", msg),
)
return nil
}
// loggedMultiRequestCtx wraps api.MultiRequestCtx and logs each reply.
type loggedMultiRequestCtx struct {
ctx api.MultiRequestCtx
name string
seq int
}
func (r *loggedMultiRequestCtx) ReceiveReply(msg api.Message) (bool, error) {
stop, err := r.ctx.ReceiveReply(msg)
if err != nil {
slog.Debug("vpp-api-recv-multi",
"req", r.name,
"reply", msg.GetMessageName(),
"seq", r.seq,
"err", err,
)
return stop, err
}
if stop {
slog.Debug("vpp-api-recv-multi-done",
"req", r.name,
"count", r.seq,
)
return stop, nil
}
slog.Debug("vpp-api-recv-multi",
"req", r.name,
"reply", msg.GetMessageName(),
"seq", r.seq,
"payload", fmt.Sprintf("%+v", msg),
)
r.seq++
return stop, nil
}

View File

@@ -0,0 +1,304 @@
// Code generated by GoVPP's binapi-generator. DO NOT EDIT.
// Package interface_types contains generated bindings for API file interface_types.api.
//
// Contents:
// - 1 alias
// - 7 enums
package interface_types
import (
"strconv"
api "go.fd.io/govpp/api"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the GoVPP api package it is being compiled against.
// A compilation error at this line likely means your copy of the
// GoVPP api package needs to be updated.
const _ = api.GoVppAPIPackageIsVersion2
const (
APIFile = "interface_types"
APIVersion = "1.0.0"
VersionCrc = 0x7f2ba79a
)
// Direction defines enum 'direction'.
type Direction uint8
const (
RX Direction = 0
TX Direction = 1
)
var (
Direction_name = map[uint8]string{
0: "RX",
1: "TX",
}
Direction_value = map[string]uint8{
"RX": 0,
"TX": 1,
}
)
func (x Direction) String() string {
s, ok := Direction_name[uint8(x)]
if ok {
return s
}
return "Direction(" + strconv.Itoa(int(x)) + ")"
}
// IfStatusFlags defines enum 'if_status_flags'.
type IfStatusFlags uint32
const (
IF_STATUS_API_FLAG_ADMIN_UP IfStatusFlags = 1
IF_STATUS_API_FLAG_LINK_UP IfStatusFlags = 2
)
var (
IfStatusFlags_name = map[uint32]string{
1: "IF_STATUS_API_FLAG_ADMIN_UP",
2: "IF_STATUS_API_FLAG_LINK_UP",
}
IfStatusFlags_value = map[string]uint32{
"IF_STATUS_API_FLAG_ADMIN_UP": 1,
"IF_STATUS_API_FLAG_LINK_UP": 2,
}
)
func (x IfStatusFlags) String() string {
s, ok := IfStatusFlags_name[uint32(x)]
if ok {
return s
}
str := func(n uint32) string {
s, ok := IfStatusFlags_name[uint32(n)]
if ok {
return s
}
return "IfStatusFlags(" + strconv.Itoa(int(n)) + ")"
}
for i := uint32(0); i <= 32; i++ {
val := uint32(x)
if val&(1<<i) != 0 {
if s != "" {
s += "|"
}
s += str(1 << i)
}
}
if s == "" {
return str(uint32(x))
}
return s
}
// IfType defines enum 'if_type'.
type IfType uint32
const (
IF_API_TYPE_HARDWARE IfType = 0
IF_API_TYPE_SUB IfType = 1
IF_API_TYPE_P2P IfType = 2
IF_API_TYPE_PIPE IfType = 3
)
var (
IfType_name = map[uint32]string{
0: "IF_API_TYPE_HARDWARE",
1: "IF_API_TYPE_SUB",
2: "IF_API_TYPE_P2P",
3: "IF_API_TYPE_PIPE",
}
IfType_value = map[string]uint32{
"IF_API_TYPE_HARDWARE": 0,
"IF_API_TYPE_SUB": 1,
"IF_API_TYPE_P2P": 2,
"IF_API_TYPE_PIPE": 3,
}
)
func (x IfType) String() string {
s, ok := IfType_name[uint32(x)]
if ok {
return s
}
return "IfType(" + strconv.Itoa(int(x)) + ")"
}
// LinkDuplex defines enum 'link_duplex'.
type LinkDuplex uint32
const (
LINK_DUPLEX_API_UNKNOWN LinkDuplex = 0
LINK_DUPLEX_API_HALF LinkDuplex = 1
LINK_DUPLEX_API_FULL LinkDuplex = 2
)
var (
LinkDuplex_name = map[uint32]string{
0: "LINK_DUPLEX_API_UNKNOWN",
1: "LINK_DUPLEX_API_HALF",
2: "LINK_DUPLEX_API_FULL",
}
LinkDuplex_value = map[string]uint32{
"LINK_DUPLEX_API_UNKNOWN": 0,
"LINK_DUPLEX_API_HALF": 1,
"LINK_DUPLEX_API_FULL": 2,
}
)
func (x LinkDuplex) String() string {
s, ok := LinkDuplex_name[uint32(x)]
if ok {
return s
}
return "LinkDuplex(" + strconv.Itoa(int(x)) + ")"
}
// MtuProto defines enum 'mtu_proto'.
type MtuProto uint32
const (
MTU_PROTO_API_L3 MtuProto = 0
MTU_PROTO_API_IP4 MtuProto = 1
MTU_PROTO_API_IP6 MtuProto = 2
MTU_PROTO_API_MPLS MtuProto = 3
)
var (
MtuProto_name = map[uint32]string{
0: "MTU_PROTO_API_L3",
1: "MTU_PROTO_API_IP4",
2: "MTU_PROTO_API_IP6",
3: "MTU_PROTO_API_MPLS",
}
MtuProto_value = map[string]uint32{
"MTU_PROTO_API_L3": 0,
"MTU_PROTO_API_IP4": 1,
"MTU_PROTO_API_IP6": 2,
"MTU_PROTO_API_MPLS": 3,
}
)
func (x MtuProto) String() string {
s, ok := MtuProto_name[uint32(x)]
if ok {
return s
}
return "MtuProto(" + strconv.Itoa(int(x)) + ")"
}
// RxMode defines enum 'rx_mode'.
type RxMode uint32
const (
RX_MODE_API_UNKNOWN RxMode = 0
RX_MODE_API_POLLING RxMode = 1
RX_MODE_API_INTERRUPT RxMode = 2
RX_MODE_API_ADAPTIVE RxMode = 3
RX_MODE_API_DEFAULT RxMode = 4
)
var (
RxMode_name = map[uint32]string{
0: "RX_MODE_API_UNKNOWN",
1: "RX_MODE_API_POLLING",
2: "RX_MODE_API_INTERRUPT",
3: "RX_MODE_API_ADAPTIVE",
4: "RX_MODE_API_DEFAULT",
}
RxMode_value = map[string]uint32{
"RX_MODE_API_UNKNOWN": 0,
"RX_MODE_API_POLLING": 1,
"RX_MODE_API_INTERRUPT": 2,
"RX_MODE_API_ADAPTIVE": 3,
"RX_MODE_API_DEFAULT": 4,
}
)
func (x RxMode) String() string {
s, ok := RxMode_name[uint32(x)]
if ok {
return s
}
return "RxMode(" + strconv.Itoa(int(x)) + ")"
}
// SubIfFlags defines enum 'sub_if_flags'.
type SubIfFlags uint32
const (
SUB_IF_API_FLAG_NO_TAGS SubIfFlags = 1
SUB_IF_API_FLAG_ONE_TAG SubIfFlags = 2
SUB_IF_API_FLAG_TWO_TAGS SubIfFlags = 4
SUB_IF_API_FLAG_DOT1AD SubIfFlags = 8
SUB_IF_API_FLAG_EXACT_MATCH SubIfFlags = 16
SUB_IF_API_FLAG_DEFAULT SubIfFlags = 32
SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY SubIfFlags = 64
SUB_IF_API_FLAG_INNER_VLAN_ID_ANY SubIfFlags = 128
SUB_IF_API_FLAG_MASK_VNET SubIfFlags = 254
SUB_IF_API_FLAG_DOT1AH SubIfFlags = 256
)
var (
SubIfFlags_name = map[uint32]string{
1: "SUB_IF_API_FLAG_NO_TAGS",
2: "SUB_IF_API_FLAG_ONE_TAG",
4: "SUB_IF_API_FLAG_TWO_TAGS",
8: "SUB_IF_API_FLAG_DOT1AD",
16: "SUB_IF_API_FLAG_EXACT_MATCH",
32: "SUB_IF_API_FLAG_DEFAULT",
64: "SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY",
128: "SUB_IF_API_FLAG_INNER_VLAN_ID_ANY",
254: "SUB_IF_API_FLAG_MASK_VNET",
256: "SUB_IF_API_FLAG_DOT1AH",
}
SubIfFlags_value = map[string]uint32{
"SUB_IF_API_FLAG_NO_TAGS": 1,
"SUB_IF_API_FLAG_ONE_TAG": 2,
"SUB_IF_API_FLAG_TWO_TAGS": 4,
"SUB_IF_API_FLAG_DOT1AD": 8,
"SUB_IF_API_FLAG_EXACT_MATCH": 16,
"SUB_IF_API_FLAG_DEFAULT": 32,
"SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY": 64,
"SUB_IF_API_FLAG_INNER_VLAN_ID_ANY": 128,
"SUB_IF_API_FLAG_MASK_VNET": 254,
"SUB_IF_API_FLAG_DOT1AH": 256,
}
)
func (x SubIfFlags) String() string {
s, ok := SubIfFlags_name[uint32(x)]
if ok {
return s
}
str := func(n uint32) string {
s, ok := SubIfFlags_name[uint32(n)]
if ok {
return s
}
return "SubIfFlags(" + strconv.Itoa(int(n)) + ")"
}
for i := uint32(0); i <= 32; i++ {
val := uint32(x)
if val&(1<<i) != 0 {
if s != "" {
s += "|"
}
s += str(1 << i)
}
}
if s == "" {
return str(uint32(x))
}
return s
}
// InterfaceIndex defines alias 'interface_index'.
type InterfaceIndex uint32

View File

@@ -0,0 +1,717 @@
// Code generated by GoVPP's binapi-generator. DO NOT EDIT.
// Package ip_types contains generated bindings for API file ip_types.api.
//
// Contents:
// - 5 aliases
// - 5 enums
// - 8 structs
// - 1 union
package ip_types
import (
"fmt"
"net"
"strconv"
"strings"
api "go.fd.io/govpp/api"
codec "go.fd.io/govpp/codec"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the GoVPP api package it is being compiled against.
// A compilation error at this line likely means your copy of the
// GoVPP api package needs to be updated.
const _ = api.GoVppAPIPackageIsVersion2
const (
APIFile = "ip_types"
APIVersion = "3.0.0"
VersionCrc = 0xfee023ed
)
// AddressFamily defines enum 'address_family'.
type AddressFamily uint8
const (
ADDRESS_IP4 AddressFamily = 0
ADDRESS_IP6 AddressFamily = 1
)
var (
AddressFamily_name = map[uint8]string{
0: "ADDRESS_IP4",
1: "ADDRESS_IP6",
}
AddressFamily_value = map[string]uint8{
"ADDRESS_IP4": 0,
"ADDRESS_IP6": 1,
}
)
func (x AddressFamily) String() string {
s, ok := AddressFamily_name[uint8(x)]
if ok {
return s
}
return "AddressFamily(" + strconv.Itoa(int(x)) + ")"
}
// IPDscp defines enum 'ip_dscp'.
type IPDscp uint8
const (
IP_API_DSCP_CS0 IPDscp = 0
IP_API_DSCP_CS1 IPDscp = 8
IP_API_DSCP_AF11 IPDscp = 10
IP_API_DSCP_AF12 IPDscp = 12
IP_API_DSCP_AF13 IPDscp = 14
IP_API_DSCP_CS2 IPDscp = 16
IP_API_DSCP_AF21 IPDscp = 18
IP_API_DSCP_AF22 IPDscp = 20
IP_API_DSCP_AF23 IPDscp = 22
IP_API_DSCP_CS3 IPDscp = 24
IP_API_DSCP_AF31 IPDscp = 26
IP_API_DSCP_AF32 IPDscp = 28
IP_API_DSCP_AF33 IPDscp = 30
IP_API_DSCP_CS4 IPDscp = 32
IP_API_DSCP_AF41 IPDscp = 34
IP_API_DSCP_AF42 IPDscp = 36
IP_API_DSCP_AF43 IPDscp = 38
IP_API_DSCP_CS5 IPDscp = 40
IP_API_DSCP_EF IPDscp = 46
IP_API_DSCP_CS6 IPDscp = 48
IP_API_DSCP_CS7 IPDscp = 50
)
var (
IPDscp_name = map[uint8]string{
0: "IP_API_DSCP_CS0",
8: "IP_API_DSCP_CS1",
10: "IP_API_DSCP_AF11",
12: "IP_API_DSCP_AF12",
14: "IP_API_DSCP_AF13",
16: "IP_API_DSCP_CS2",
18: "IP_API_DSCP_AF21",
20: "IP_API_DSCP_AF22",
22: "IP_API_DSCP_AF23",
24: "IP_API_DSCP_CS3",
26: "IP_API_DSCP_AF31",
28: "IP_API_DSCP_AF32",
30: "IP_API_DSCP_AF33",
32: "IP_API_DSCP_CS4",
34: "IP_API_DSCP_AF41",
36: "IP_API_DSCP_AF42",
38: "IP_API_DSCP_AF43",
40: "IP_API_DSCP_CS5",
46: "IP_API_DSCP_EF",
48: "IP_API_DSCP_CS6",
50: "IP_API_DSCP_CS7",
}
IPDscp_value = map[string]uint8{
"IP_API_DSCP_CS0": 0,
"IP_API_DSCP_CS1": 8,
"IP_API_DSCP_AF11": 10,
"IP_API_DSCP_AF12": 12,
"IP_API_DSCP_AF13": 14,
"IP_API_DSCP_CS2": 16,
"IP_API_DSCP_AF21": 18,
"IP_API_DSCP_AF22": 20,
"IP_API_DSCP_AF23": 22,
"IP_API_DSCP_CS3": 24,
"IP_API_DSCP_AF31": 26,
"IP_API_DSCP_AF32": 28,
"IP_API_DSCP_AF33": 30,
"IP_API_DSCP_CS4": 32,
"IP_API_DSCP_AF41": 34,
"IP_API_DSCP_AF42": 36,
"IP_API_DSCP_AF43": 38,
"IP_API_DSCP_CS5": 40,
"IP_API_DSCP_EF": 46,
"IP_API_DSCP_CS6": 48,
"IP_API_DSCP_CS7": 50,
}
)
func (x IPDscp) String() string {
s, ok := IPDscp_name[uint8(x)]
if ok {
return s
}
return "IPDscp(" + strconv.Itoa(int(x)) + ")"
}
// IPEcn defines enum 'ip_ecn'.
type IPEcn uint8
const (
IP_API_ECN_NONE IPEcn = 0
IP_API_ECN_ECT0 IPEcn = 1
IP_API_ECN_ECT1 IPEcn = 2
IP_API_ECN_CE IPEcn = 3
)
var (
IPEcn_name = map[uint8]string{
0: "IP_API_ECN_NONE",
1: "IP_API_ECN_ECT0",
2: "IP_API_ECN_ECT1",
3: "IP_API_ECN_CE",
}
IPEcn_value = map[string]uint8{
"IP_API_ECN_NONE": 0,
"IP_API_ECN_ECT0": 1,
"IP_API_ECN_ECT1": 2,
"IP_API_ECN_CE": 3,
}
)
func (x IPEcn) String() string {
s, ok := IPEcn_name[uint8(x)]
if ok {
return s
}
return "IPEcn(" + strconv.Itoa(int(x)) + ")"
}
// IPFeatureLocation defines enum 'ip_feature_location'.
type IPFeatureLocation uint8
const (
IP_API_FEATURE_INPUT IPFeatureLocation = 0
IP_API_FEATURE_OUTPUT IPFeatureLocation = 1
IP_API_FEATURE_LOCAL IPFeatureLocation = 2
IP_API_FEATURE_PUNT IPFeatureLocation = 3
IP_API_FEATURE_DROP IPFeatureLocation = 4
)
var (
IPFeatureLocation_name = map[uint8]string{
0: "IP_API_FEATURE_INPUT",
1: "IP_API_FEATURE_OUTPUT",
2: "IP_API_FEATURE_LOCAL",
3: "IP_API_FEATURE_PUNT",
4: "IP_API_FEATURE_DROP",
}
IPFeatureLocation_value = map[string]uint8{
"IP_API_FEATURE_INPUT": 0,
"IP_API_FEATURE_OUTPUT": 1,
"IP_API_FEATURE_LOCAL": 2,
"IP_API_FEATURE_PUNT": 3,
"IP_API_FEATURE_DROP": 4,
}
)
func (x IPFeatureLocation) String() string {
s, ok := IPFeatureLocation_name[uint8(x)]
if ok {
return s
}
return "IPFeatureLocation(" + strconv.Itoa(int(x)) + ")"
}
// IPProto defines enum 'ip_proto'.
type IPProto uint8
const (
IP_API_PROTO_HOPOPT IPProto = 0
IP_API_PROTO_ICMP IPProto = 1
IP_API_PROTO_IGMP IPProto = 2
IP_API_PROTO_TCP IPProto = 6
IP_API_PROTO_UDP IPProto = 17
IP_API_PROTO_GRE IPProto = 47
IP_API_PROTO_ESP IPProto = 50
IP_API_PROTO_AH IPProto = 51
IP_API_PROTO_ICMP6 IPProto = 58
IP_API_PROTO_EIGRP IPProto = 88
IP_API_PROTO_OSPF IPProto = 89
IP_API_PROTO_SCTP IPProto = 132
IP_API_PROTO_RESERVED IPProto = 255
)
var (
IPProto_name = map[uint8]string{
0: "IP_API_PROTO_HOPOPT",
1: "IP_API_PROTO_ICMP",
2: "IP_API_PROTO_IGMP",
6: "IP_API_PROTO_TCP",
17: "IP_API_PROTO_UDP",
47: "IP_API_PROTO_GRE",
50: "IP_API_PROTO_ESP",
51: "IP_API_PROTO_AH",
58: "IP_API_PROTO_ICMP6",
88: "IP_API_PROTO_EIGRP",
89: "IP_API_PROTO_OSPF",
132: "IP_API_PROTO_SCTP",
255: "IP_API_PROTO_RESERVED",
}
IPProto_value = map[string]uint8{
"IP_API_PROTO_HOPOPT": 0,
"IP_API_PROTO_ICMP": 1,
"IP_API_PROTO_IGMP": 2,
"IP_API_PROTO_TCP": 6,
"IP_API_PROTO_UDP": 17,
"IP_API_PROTO_GRE": 47,
"IP_API_PROTO_ESP": 50,
"IP_API_PROTO_AH": 51,
"IP_API_PROTO_ICMP6": 58,
"IP_API_PROTO_EIGRP": 88,
"IP_API_PROTO_OSPF": 89,
"IP_API_PROTO_SCTP": 132,
"IP_API_PROTO_RESERVED": 255,
}
)
func (x IPProto) String() string {
s, ok := IPProto_name[uint8(x)]
if ok {
return s
}
return "IPProto(" + strconv.Itoa(int(x)) + ")"
}
// AddressWithPrefix defines alias 'address_with_prefix'.
type AddressWithPrefix Prefix
func NewAddressWithPrefix(network net.IPNet) AddressWithPrefix {
prefix := NewPrefix(network)
return AddressWithPrefix(prefix)
}
func ParseAddressWithPrefix(s string) (AddressWithPrefix, error) {
prefix, err := ParsePrefix(s)
if err != nil {
return AddressWithPrefix{}, err
}
return AddressWithPrefix(prefix), nil
}
func (x AddressWithPrefix) ToIPNet() *net.IPNet {
return Prefix(x).ToIPNet()
}
func (x AddressWithPrefix) String() string {
return Prefix(x).String()
}
func (x *AddressWithPrefix) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *AddressWithPrefix) UnmarshalText(text []byte) error {
prefix, err := ParseAddressWithPrefix(string(text))
if err != nil {
return err
}
*x = prefix
return nil
}
// IP4Address defines alias 'ip4_address'.
type IP4Address [4]uint8
func NewIP4Address(ip net.IP) IP4Address {
var ipaddr IP4Address
copy(ipaddr[:], ip.To4())
return ipaddr
}
func ParseIP4Address(s string) (IP4Address, error) {
ip := net.ParseIP(s).To4()
if ip == nil {
return IP4Address{}, fmt.Errorf("invalid IP4 address: %s", s)
}
var ipaddr IP4Address
copy(ipaddr[:], ip.To4())
return ipaddr, nil
}
func (x IP4Address) ToIP() net.IP {
return net.IP(x[:]).To4()
}
func (x IP4Address) String() string {
return x.ToIP().String()
}
func (x *IP4Address) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *IP4Address) UnmarshalText(text []byte) error {
ipaddr, err := ParseIP4Address(string(text))
if err != nil {
return err
}
*x = ipaddr
return nil
}
// IP4AddressWithPrefix defines alias 'ip4_address_with_prefix'.
type IP4AddressWithPrefix IP4Prefix
// IP6Address defines alias 'ip6_address'.
type IP6Address [16]uint8
func NewIP6Address(ip net.IP) IP6Address {
var ipaddr IP6Address
copy(ipaddr[:], ip.To16())
return ipaddr
}
func ParseIP6Address(s string) (IP6Address, error) {
ip := net.ParseIP(s).To16()
if ip == nil {
return IP6Address{}, fmt.Errorf("invalid IP6 address: %s", s)
}
var ipaddr IP6Address
copy(ipaddr[:], ip.To16())
return ipaddr, nil
}
func (x IP6Address) ToIP() net.IP {
return net.IP(x[:]).To16()
}
func (x IP6Address) String() string {
return x.ToIP().String()
}
func (x *IP6Address) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *IP6Address) UnmarshalText(text []byte) error {
ipaddr, err := ParseIP6Address(string(text))
if err != nil {
return err
}
*x = ipaddr
return nil
}
// IP6AddressWithPrefix defines alias 'ip6_address_with_prefix'.
type IP6AddressWithPrefix IP6Prefix
// Address defines type 'address'.
type Address struct {
Af AddressFamily `binapi:"address_family,name=af" json:"af,omitempty"`
Un AddressUnion `binapi:"address_union,name=un" json:"un,omitempty"`
}
func NewAddress(ip net.IP) Address {
var addr Address
if ip.To4() == nil {
addr.Af = ADDRESS_IP6
var ip6 IP6Address
copy(ip6[:], ip.To16())
addr.Un.SetIP6(ip6)
} else {
addr.Af = ADDRESS_IP4
var ip4 IP4Address
copy(ip4[:], ip.To4())
addr.Un.SetIP4(ip4)
}
return addr
}
func ParseAddress(s string) (Address, error) {
ip := net.ParseIP(s)
if ip == nil {
return Address{}, fmt.Errorf("invalid IP address: %s", s)
}
return NewAddress(ip), nil
}
func (x Address) ToIP() net.IP {
if x.Af == ADDRESS_IP6 {
ip6 := x.Un.GetIP6()
return net.IP(ip6[:]).To16()
} else {
ip4 := x.Un.GetIP4()
return net.IP(ip4[:]).To4()
}
}
func (x Address) String() string {
return x.ToIP().String()
}
func (x *Address) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *Address) UnmarshalText(text []byte) error {
addr, err := ParseAddress(string(text))
if err != nil {
return err
}
*x = addr
return nil
}
// IP4AddressAndMask defines type 'ip4_address_and_mask'.
type IP4AddressAndMask struct {
Addr IP4Address `binapi:"ip4_address,name=addr" json:"addr,omitempty"`
Mask IP4Address `binapi:"ip4_address,name=mask" json:"mask,omitempty"`
}
// IP4Prefix defines type 'ip4_prefix'.
type IP4Prefix struct {
Address IP4Address `binapi:"ip4_address,name=address" json:"address,omitempty"`
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
}
func NewIP4Prefix(network net.IPNet) IP4Prefix {
var prefix IP4Prefix
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address = NewIP4Address(network.IP)
return prefix
}
func ParseIP4Prefix(s string) (prefix IP4Prefix, err error) {
hasPrefix := strings.Contains(s, "/")
if hasPrefix {
ip, network, err := net.ParseCIDR(s)
if err != nil {
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
}
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address, err = ParseIP4Address(ip.String())
if err != nil {
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
}
} else {
ip := net.ParseIP(s)
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
if ip.To4() == nil {
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
}
prefix.Len = byte(defaultMaskSize)
prefix.Address, err = ParseIP4Address(ip.String())
if err != nil {
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
}
}
return prefix, nil
}
func (x IP4Prefix) ToIPNet() *net.IPNet {
mask := net.CIDRMask(int(x.Len), 32)
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
return ipnet
}
func (x IP4Prefix) String() string {
ip := x.Address.String()
return ip + "/" + strconv.Itoa(int(x.Len))
}
func (x *IP4Prefix) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *IP4Prefix) UnmarshalText(text []byte) error {
prefix, err := ParseIP4Prefix(string(text))
if err != nil {
return err
}
*x = prefix
return nil
}
// IP6AddressAndMask defines type 'ip6_address_and_mask'.
type IP6AddressAndMask struct {
Addr IP6Address `binapi:"ip6_address,name=addr" json:"addr,omitempty"`
Mask IP6Address `binapi:"ip6_address,name=mask" json:"mask,omitempty"`
}
// IP6Prefix defines type 'ip6_prefix'.
type IP6Prefix struct {
Address IP6Address `binapi:"ip6_address,name=address" json:"address,omitempty"`
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
}
func NewIP6Prefix(network net.IPNet) IP6Prefix {
var prefix IP6Prefix
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address = NewIP6Address(network.IP)
return prefix
}
func ParseIP6Prefix(s string) (prefix IP6Prefix, err error) {
hasPrefix := strings.Contains(s, "/")
if hasPrefix {
ip, network, err := net.ParseCIDR(s)
if err != nil {
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
}
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address, err = ParseIP6Address(ip.String())
if err != nil {
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
}
} else {
ip := net.ParseIP(s)
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
if ip.To4() == nil {
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
}
prefix.Len = byte(defaultMaskSize)
prefix.Address, err = ParseIP6Address(ip.String())
if err != nil {
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
}
}
return prefix, nil
}
func (x IP6Prefix) ToIPNet() *net.IPNet {
mask := net.CIDRMask(int(x.Len), 128)
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
return ipnet
}
func (x IP6Prefix) String() string {
ip := x.Address.String()
return ip + "/" + strconv.Itoa(int(x.Len))
}
func (x *IP6Prefix) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *IP6Prefix) UnmarshalText(text []byte) error {
prefix, err := ParseIP6Prefix(string(text))
if err != nil {
return err
}
*x = prefix
return nil
}
// Mprefix defines type 'mprefix'.
type Mprefix struct {
Af AddressFamily `binapi:"address_family,name=af" json:"af,omitempty"`
GrpAddressLength uint16 `binapi:"u16,name=grp_address_length" json:"grp_address_length,omitempty"`
GrpAddress AddressUnion `binapi:"address_union,name=grp_address" json:"grp_address,omitempty"`
SrcAddress AddressUnion `binapi:"address_union,name=src_address" json:"src_address,omitempty"`
}
// Prefix defines type 'prefix'.
type Prefix struct {
Address Address `binapi:"address,name=address" json:"address,omitempty"`
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
}
func NewPrefix(network net.IPNet) Prefix {
var prefix Prefix
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address = NewAddress(network.IP)
return prefix
}
func ParsePrefix(ip string) (prefix Prefix, err error) {
hasPrefix := strings.Contains(ip, "/")
if hasPrefix {
netIP, network, err := net.ParseCIDR(ip)
if err != nil {
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
}
maskSize, _ := network.Mask.Size()
prefix.Len = byte(maskSize)
prefix.Address, err = ParseAddress(netIP.String())
if err != nil {
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
}
} else {
netIP := net.ParseIP(ip)
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
if netIP.To4() == nil {
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
}
prefix.Len = byte(defaultMaskSize)
prefix.Address, err = ParseAddress(netIP.String())
if err != nil {
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
}
}
return prefix, nil
}
func (x Prefix) ToIPNet() *net.IPNet {
var mask net.IPMask
if x.Address.Af == ADDRESS_IP4 {
mask = net.CIDRMask(int(x.Len), 32)
} else {
mask = net.CIDRMask(int(x.Len), 128)
}
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
return ipnet
}
func (x Prefix) String() string {
ip := x.Address.String()
return ip + "/" + strconv.Itoa(int(x.Len))
}
func (x *Prefix) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
func (x *Prefix) UnmarshalText(text []byte) error {
prefix, err := ParsePrefix(string(text))
if err != nil {
return err
}
*x = prefix
return nil
}
// PrefixMatcher defines type 'prefix_matcher'.
type PrefixMatcher struct {
Le uint8 `binapi:"u8,name=le" json:"le,omitempty"`
Ge uint8 `binapi:"u8,name=ge" json:"ge,omitempty"`
}
// AddressUnion defines union 'address_union'.
type AddressUnion struct {
// AddressUnion can be one of:
// - IP4 *IP4Address
// - IP6 *IP6Address
XXX_UnionData [16]byte
}
func AddressUnionIP4(a IP4Address) (u AddressUnion) {
u.SetIP4(a)
return
}
func (u *AddressUnion) SetIP4(a IP4Address) {
buf := codec.NewBuffer(u.XXX_UnionData[:])
buf.EncodeBytes(a[:], 4)
}
func (u *AddressUnion) GetIP4() (a IP4Address) {
buf := codec.NewBuffer(u.XXX_UnionData[:])
copy(a[:], buf.DecodeBytes(4))
return
}
func AddressUnionIP6(a IP6Address) (u AddressUnion) {
u.SetIP6(a)
return
}
func (u *AddressUnion) SetIP6(a IP6Address) {
buf := codec.NewBuffer(u.XXX_UnionData[:])
buf.EncodeBytes(a[:], 16)
}
func (u *AddressUnion) GetIP6() (a IP6Address) {
buf := codec.NewBuffer(u.XXX_UnionData[:])
copy(a[:], buf.DecodeBytes(16))
return
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
// Code generated by GoVPP's binapi-generator. DO NOT EDIT.
// Package lb_types contains generated bindings for API file lb_types.api.
//
// Contents:
// - 5 enums
// - 1 struct
package lb_types
import (
"strconv"
ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types"
api "go.fd.io/govpp/api"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the GoVPP api package it is being compiled against.
// A compilation error at this line likely means your copy of the
// GoVPP api package needs to be updated.
const _ = api.GoVppAPIPackageIsVersion2
const (
APIFile = "lb_types"
APIVersion = "1.0.0"
VersionCrc = 0xba19340c
)
// LbEncapType defines enum 'lb_encap_type'.
type LbEncapType uint32
const (
LB_API_ENCAP_TYPE_GRE4 LbEncapType = 0
LB_API_ENCAP_TYPE_GRE6 LbEncapType = 1
LB_API_ENCAP_TYPE_L3DSR LbEncapType = 2
LB_API_ENCAP_TYPE_NAT4 LbEncapType = 3
LB_API_ENCAP_TYPE_NAT6 LbEncapType = 4
LB_API_ENCAP_N_TYPES LbEncapType = 5
)
var (
LbEncapType_name = map[uint32]string{
0: "LB_API_ENCAP_TYPE_GRE4",
1: "LB_API_ENCAP_TYPE_GRE6",
2: "LB_API_ENCAP_TYPE_L3DSR",
3: "LB_API_ENCAP_TYPE_NAT4",
4: "LB_API_ENCAP_TYPE_NAT6",
5: "LB_API_ENCAP_N_TYPES",
}
LbEncapType_value = map[string]uint32{
"LB_API_ENCAP_TYPE_GRE4": 0,
"LB_API_ENCAP_TYPE_GRE6": 1,
"LB_API_ENCAP_TYPE_L3DSR": 2,
"LB_API_ENCAP_TYPE_NAT4": 3,
"LB_API_ENCAP_TYPE_NAT6": 4,
"LB_API_ENCAP_N_TYPES": 5,
}
)
func (x LbEncapType) String() string {
s, ok := LbEncapType_name[uint32(x)]
if ok {
return s
}
return "LbEncapType(" + strconv.Itoa(int(x)) + ")"
}
// LbLkpTypeT defines enum 'lb_lkp_type_t'.
type LbLkpTypeT uint32
const (
LB_API_LKP_SAME_IP_PORT LbLkpTypeT = 0
LB_API_LKP_DIFF_IP_PORT LbLkpTypeT = 1
LB_API_LKP_ALL_PORT_IP LbLkpTypeT = 2
LB_API_LKP_N_TYPES LbLkpTypeT = 3
)
var (
LbLkpTypeT_name = map[uint32]string{
0: "LB_API_LKP_SAME_IP_PORT",
1: "LB_API_LKP_DIFF_IP_PORT",
2: "LB_API_LKP_ALL_PORT_IP",
3: "LB_API_LKP_N_TYPES",
}
LbLkpTypeT_value = map[string]uint32{
"LB_API_LKP_SAME_IP_PORT": 0,
"LB_API_LKP_DIFF_IP_PORT": 1,
"LB_API_LKP_ALL_PORT_IP": 2,
"LB_API_LKP_N_TYPES": 3,
}
)
func (x LbLkpTypeT) String() string {
s, ok := LbLkpTypeT_name[uint32(x)]
if ok {
return s
}
return "LbLkpTypeT(" + strconv.Itoa(int(x)) + ")"
}
// LbNatProtocol defines enum 'lb_nat_protocol'.
type LbNatProtocol uint32
const (
LB_API_NAT_PROTOCOL_UDP LbNatProtocol = 6
LB_API_NAT_PROTOCOL_TCP LbNatProtocol = 23
LB_API_NAT_PROTOCOL_ANY LbNatProtocol = 4294967295
)
var (
LbNatProtocol_name = map[uint32]string{
6: "LB_API_NAT_PROTOCOL_UDP",
23: "LB_API_NAT_PROTOCOL_TCP",
4294967295: "LB_API_NAT_PROTOCOL_ANY",
}
LbNatProtocol_value = map[string]uint32{
"LB_API_NAT_PROTOCOL_UDP": 6,
"LB_API_NAT_PROTOCOL_TCP": 23,
"LB_API_NAT_PROTOCOL_ANY": 4294967295,
}
)
func (x LbNatProtocol) String() string {
s, ok := LbNatProtocol_name[uint32(x)]
if ok {
return s
}
return "LbNatProtocol(" + strconv.Itoa(int(x)) + ")"
}
// LbSrvType defines enum 'lb_srv_type'.
type LbSrvType uint32
const (
LB_API_SRV_TYPE_CLUSTERIP LbSrvType = 0
LB_API_SRV_TYPE_NODEPORT LbSrvType = 1
LB_API_SRV_N_TYPES LbSrvType = 2
)
var (
LbSrvType_name = map[uint32]string{
0: "LB_API_SRV_TYPE_CLUSTERIP",
1: "LB_API_SRV_TYPE_NODEPORT",
2: "LB_API_SRV_N_TYPES",
}
LbSrvType_value = map[string]uint32{
"LB_API_SRV_TYPE_CLUSTERIP": 0,
"LB_API_SRV_TYPE_NODEPORT": 1,
"LB_API_SRV_N_TYPES": 2,
}
)
func (x LbSrvType) String() string {
s, ok := LbSrvType_name[uint32(x)]
if ok {
return s
}
return "LbSrvType(" + strconv.Itoa(int(x)) + ")"
}
// LbVipType defines enum 'lb_vip_type'.
type LbVipType uint32
const (
LB_API_VIP_TYPE_IP6_GRE6 LbVipType = 0
LB_API_VIP_TYPE_IP6_GRE4 LbVipType = 1
LB_API_VIP_TYPE_IP4_GRE6 LbVipType = 2
LB_API_VIP_TYPE_IP4_GRE4 LbVipType = 3
LB_API_VIP_TYPE_IP4_L3DSR LbVipType = 4
LB_API_VIP_TYPE_IP4_NAT4 LbVipType = 5
LB_API_VIP_TYPE_IP6_NAT6 LbVipType = 6
LB_API_VIP_N_TYPES LbVipType = 7
)
var (
LbVipType_name = map[uint32]string{
0: "LB_API_VIP_TYPE_IP6_GRE6",
1: "LB_API_VIP_TYPE_IP6_GRE4",
2: "LB_API_VIP_TYPE_IP4_GRE6",
3: "LB_API_VIP_TYPE_IP4_GRE4",
4: "LB_API_VIP_TYPE_IP4_L3DSR",
5: "LB_API_VIP_TYPE_IP4_NAT4",
6: "LB_API_VIP_TYPE_IP6_NAT6",
7: "LB_API_VIP_N_TYPES",
}
LbVipType_value = map[string]uint32{
"LB_API_VIP_TYPE_IP6_GRE6": 0,
"LB_API_VIP_TYPE_IP6_GRE4": 1,
"LB_API_VIP_TYPE_IP4_GRE6": 2,
"LB_API_VIP_TYPE_IP4_GRE4": 3,
"LB_API_VIP_TYPE_IP4_L3DSR": 4,
"LB_API_VIP_TYPE_IP4_NAT4": 5,
"LB_API_VIP_TYPE_IP6_NAT6": 6,
"LB_API_VIP_N_TYPES": 7,
}
)
func (x LbVipType) String() string {
s, ok := LbVipType_name[uint32(x)]
if ok {
return s
}
return "LbVipType(" + strconv.Itoa(int(x)) + ")"
}
// LbVip defines type 'lb_vip'.
type LbVip struct {
Pfx ip_types.AddressWithPrefix `binapi:"address_with_prefix,name=pfx" json:"pfx,omitempty"`
Protocol ip_types.IPProto `binapi:"ip_proto,name=protocol" json:"protocol,omitempty"`
Port uint16 `binapi:"u16,name=port" json:"port,omitempty"`
}

View File

@@ -14,13 +14,23 @@ import (
"go.fd.io/govpp/adapter"
"go.fd.io/govpp/adapter/socketclient"
"go.fd.io/govpp/adapter/statsclient"
"go.fd.io/govpp/api"
"go.fd.io/govpp/binapi/vpe"
"go.fd.io/govpp/core"
"git.ipng.ch/ipng/vpp-maglev/internal/config"
lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb"
)
// ConfigSource provides a snapshot of the current maglev config to the VPP
// sync loop. checker.Checker satisfies this interface via its Config() method.
// Decoupling via an interface avoids an import cycle with the checker package.
type ConfigSource interface {
Config() *config.Config
}
const retryInterval = 5 * time.Second
const pingInterval = 10 * time.Second
const defaultLBSyncInterval = 30 * time.Second
// Info holds VPP version and connection metadata, populated on connect.
type Info struct {
@@ -44,6 +54,17 @@ type Client struct {
statsConn *core.StatsConnection
statsClient adapter.StatsAPI // raw adapter for DumpStats
info Info // populated on successful connect
cfgSrc ConfigSource // optional; enables periodic LB sync
lastLBConf *lb.LbConf // cached last-pushed lb_conf (dedup)
}
// SetConfigSource attaches a live config source. When set, the VPP client
// runs a periodic SyncLBStateAll loop (at the interval from cfg.VPP.LB.SyncInterval)
// for as long as the VPP connection is up. Must be called before Run.
func (c *Client) SetConfigSource(src ConfigSource) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfgSrc = src
}
// New creates a Client for the given socket paths.
@@ -77,8 +98,43 @@ func (c *Client) Run(ctx context.Context) {
"pid", c.info.PID,
"api", c.apiAddr, "stats", c.statsAddr)
// Read the current LB plugin state so we can log what's programmed.
if state, err := c.GetLBStateAll(); err != nil {
slog.Warn("vpp-lb-read-failed", "err", err)
} else {
totalAS := 0
for _, v := range state.VIPs {
totalAS += len(v.ASes)
}
slog.Info("vpp-lb-state",
"vips", len(state.VIPs),
"application-servers", totalAS,
"sticky-buckets-per-core", state.Conf.StickyBucketsPerCore,
"flow-timeout", state.Conf.FlowTimeout)
}
// Push global LB conf (src addresses, buckets, timeout) from the
// running config. On startup this is the initial set; on reconnect
// (VPP restart) VPP has forgotten everything, so we set it again.
c.mu.Lock()
src := c.cfgSrc
c.mu.Unlock()
if src != nil {
if cfg := src.Config(); cfg != nil {
if err := c.SetLBConf(cfg); err != nil {
slog.Warn("vpp-lb-conf-set-failed", "err", err)
}
}
}
// Start the LB sync loop for as long as the connection is up.
// It exits when connCtx is cancelled (on disconnect or shutdown).
connCtx, connCancel := context.WithCancel(ctx)
go c.lbSyncLoop(connCtx)
// Hold the connection, pinging periodically to detect VPP restarts.
c.monitor(ctx)
connCancel()
// If ctx is done we're shutting down; otherwise VPP dropped and we retry.
c.disconnect()
@@ -89,6 +145,49 @@ func (c *Client) Run(ctx context.Context) {
}
}
// lbSyncLoop periodically runs SyncLBStateAll to catch drift between the
// maglev config and the VPP dataplane. The first run happens immediately
// on loop start (VPP has just connected, so any pre-existing state needs
// reconciliation). Subsequent runs fire every cfg.VPP.LB.SyncInterval.
// Exits when ctx is cancelled.
func (c *Client) lbSyncLoop(ctx context.Context) {
c.mu.Lock()
src := c.cfgSrc
c.mu.Unlock()
if src == nil {
return // no config source registered; nothing to sync
}
// next-run timestamp starts at "now" so the first tick is immediate.
next := time.Now()
for {
wait := time.Until(next)
if wait < 0 {
wait = 0
}
select {
case <-ctx.Done():
return
case <-time.After(wait):
}
cfg := src.Config()
if cfg == nil {
next = time.Now().Add(defaultLBSyncInterval)
continue
}
interval := cfg.VPP.LB.SyncInterval
if interval <= 0 {
interval = defaultLBSyncInterval
}
if err := c.SyncLBStateAll(cfg); err != nil {
slog.Warn("vpp-lbsync-error", "err", err)
}
next = time.Now().Add(interval)
}
}
// IsConnected returns true if both API and stats connections are active.
func (c *Client) IsConnected() bool {
c.mu.Lock()
@@ -96,25 +195,6 @@ func (c *Client) IsConnected() bool {
return c.apiConn != nil && c.statsConn != nil
}
// NewAPIChannel creates a new API channel for sending VPP binary API requests.
// Returns an error if the API connection is not established.
func (c *Client) NewAPIChannel() (api.Channel, error) {
c.mu.Lock()
conn := c.apiConn
c.mu.Unlock()
if conn == nil {
return nil, errNotConnected
}
return conn.NewAPIChannel()
}
// StatsConnection returns the stats connection, or nil if not connected.
func (c *Client) StatsConnection() *core.StatsConnection {
c.mu.Lock()
defer c.mu.Unlock()
return c.statsConn
}
// GetInfo returns the VPP version and connection metadata, or an error
// if VPP is not connected.
func (c *Client) GetInfo() (Info, error) {
@@ -160,6 +240,7 @@ func (c *Client) disconnect() {
c.statsConn = nil
c.statsClient = nil
c.info = Info{}
c.lastLBConf = nil // force re-push of lb_conf on reconnect
c.mu.Unlock()
safeDisconnectAPI(apiConn)
@@ -184,7 +265,7 @@ func (c *Client) monitor(ctx context.Context) {
// ping sends a control_ping to VPP and returns true if it succeeds.
func (c *Client) ping() bool {
ch, err := c.NewAPIChannel()
ch, err := c.apiChannel()
if err != nil {
return false
}
@@ -204,7 +285,7 @@ func (c *Client) ping() bool {
func (c *Client) fetchInfo() Info {
info := Info{ConnectedSince: time.Now()}
ch, err := c.NewAPIChannel()
ch, err := c.apiChannel()
if err != nil {
return info
}

103
internal/vpp/lbconf.go Normal file
View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"bytes"
"fmt"
"log/slog"
"net"
"git.ipng.ch/ipng/vpp-maglev/internal/config"
ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types"
lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb"
)
// SetLBConf sends lb_conf to VPP with the global load-balancer settings from
// cfg. Called on VPP connect (startup and reconnect) and after every
// successful config reload. Returns nil if VPP is not connected (silently
// skipped — the next connect will push the conf).
//
// The values sent are cached on the Client; if SetLBConf is called twice in
// a row with unchanged values, no API call is made and no log is emitted.
func (c *Client) SetLBConf(cfg *config.Config) error {
if !c.IsConnected() {
return nil
}
req := &lb.LbConf{
IP4SrcAddress: ip_types.IP4Address(ip4Bytes(cfg.VPP.LB.IPv4SrcAddress)),
IP6SrcAddress: ip_types.IP6Address(ip6Bytes(cfg.VPP.LB.IPv6SrcAddress)),
StickyBucketsPerCore: cfg.VPP.LB.StickyBucketsPerCore,
FlowTimeout: uint32(cfg.VPP.LB.FlowTimeout.Seconds()),
}
// Skip if nothing changed since the last successful push.
c.mu.Lock()
prev := c.lastLBConf
c.mu.Unlock()
if prev != nil &&
bytes.Equal(prev.IP4SrcAddress[:], req.IP4SrcAddress[:]) &&
bytes.Equal(prev.IP6SrcAddress[:], req.IP6SrcAddress[:]) &&
prev.StickyBucketsPerCore == req.StickyBucketsPerCore &&
prev.FlowTimeout == req.FlowTimeout {
return nil
}
ch, err := c.apiChannel()
if err != nil {
return err
}
defer ch.Close()
reply := &lb.LbConfReply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_conf: %w", err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_conf: retval=%d", reply.Retval)
}
c.mu.Lock()
c.lastLBConf = req
c.mu.Unlock()
slog.Info("vpp-lb-conf-set",
"ipv4-src", ipStringFromCfg(cfg.VPP.LB.IPv4SrcAddress),
"ipv6-src", ipStringFromCfg(cfg.VPP.LB.IPv6SrcAddress),
"sticky-buckets-per-core", req.StickyBucketsPerCore,
"flow-timeout", cfg.VPP.LB.FlowTimeout.String())
return nil
}
// ip4Bytes returns the 4-byte representation of an IPv4 address, or all-zero
// if ip is nil/unset.
func ip4Bytes(ip net.IP) [4]byte {
var out [4]byte
if ip == nil {
return out
}
if b := ip.To4(); b != nil {
copy(out[:], b)
}
return out
}
// ip6Bytes returns the 16-byte representation of an IPv6 address, or all-zero
// if ip is nil/unset.
func ip6Bytes(ip net.IP) [16]byte {
var out [16]byte
if ip == nil {
return out
}
copy(out[:], ip.To16())
return out
}
// ipStringFromCfg renders an IP for logging; returns "unset" if nil.
func ipStringFromCfg(ip net.IP) string {
if ip == nil {
return "unset"
}
return ip.String()
}

267
internal/vpp/lbstate.go Normal file
View File

@@ -0,0 +1,267 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"fmt"
"net"
"time"
lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb"
lb_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb_types"
)
// LBConf mirrors VPP's lb_conf_get_reply: global LB plugin settings.
type LBConf struct {
IP4SrcAddress net.IP
IP6SrcAddress net.IP
StickyBucketsPerCore uint32
FlowTimeout uint32
}
// LBVIP mirrors VPP's lb_vip_details plus the set of application servers
// attached to this VIP (from lb_as_v2_details).
type LBVIP struct {
Prefix *net.IPNet // VIP address + prefix length
Protocol uint8 // IP proto (6=TCP, 17=UDP, 255=any)
Port uint16 // 0 = all-port VIP
Encap string // gre4|gre6|l3dsr|nat4|nat6
SrvType string // clusterip|nodeport
Dscp uint8
TargetPort uint16
FlowTableLength uint16
ASes []LBAS
}
// LBAS mirrors VPP's lb_as_v2_details: one application server bound to a VIP.
type LBAS struct {
Address net.IP
Weight uint8
Flags uint8 // bit 0 = used (alive), bit 1 = flushed
NumBuckets uint32
InUseSince time.Time // from VPP seconds-since-epoch (0 = never)
}
// LBState is a snapshot of the VPP LB plugin state.
type LBState struct {
Conf LBConf
VIPs []LBVIP
}
// GetLBStateAll fetches a full snapshot of the LB plugin state (global config
// plus every VIP and its application servers).
// Returns an error if VPP is not connected.
func (c *Client) GetLBStateAll() (*LBState, error) {
ch, err := c.apiChannel()
if err != nil {
return nil, err
}
defer ch.Close()
state := &LBState{}
conf, err := getLBConf(ch)
if err != nil {
return nil, err
}
state.Conf = conf
vips, err := dumpAllVIPs(ch)
if err != nil {
return nil, err
}
for i := range vips {
ases, err := dumpASesForVIP(ch, vips[i].Protocol, vips[i].Port)
if err != nil {
return nil, err
}
vips[i].ASes = ases
}
state.VIPs = vips
return state, nil
}
// GetLBStateVIP fetches a single VIP from VPP. Returns (nil, nil) if the VIP
// does not exist in VPP (caller must treat absence as "needs to be added").
// Returns an error only on transport/VPP failures.
func (c *Client) GetLBStateVIP(prefix *net.IPNet, protocol uint8, port uint16) (*LBVIP, error) {
ch, err := c.apiChannel()
if err != nil {
return nil, err
}
defer ch.Close()
return lookupVIP(ch, prefix, protocol, port)
}
// ---- low-level helpers (used by both Get and Sync paths) -------------------
func getLBConf(ch *loggedChannel) (LBConf, error) {
reply := &lb.LbConfGetReply{}
if err := ch.SendRequest(&lb.LbConfGet{}).ReceiveReply(reply); err != nil {
return LBConf{}, fmt.Errorf("lb_conf_get: %w", err)
}
return LBConf{
IP4SrcAddress: ip4ToNetIP(reply.IP4SrcAddress),
IP6SrcAddress: ip6ToNetIP(reply.IP6SrcAddress),
StickyBucketsPerCore: reply.StickyBucketsPerCore,
FlowTimeout: reply.FlowTimeout,
}, nil
}
// dumpAllVIPs returns every VIP known to VPP (metadata only — ASes not populated).
func dumpAllVIPs(ch *loggedChannel) ([]LBVIP, error) {
reqCtx := ch.SendMultiRequest(&lb.LbVipDump{})
var out []LBVIP
for {
reply := &lb.LbVipDetails{}
stop, err := reqCtx.ReceiveReply(reply)
if err != nil {
return nil, fmt.Errorf("lb_vip_dump: %w", err)
}
if stop {
break
}
out = append(out, vipFromDetails(reply))
}
return out, nil
}
// lookupVIP finds a single VIP by (prefix, protocol, port) and returns it
// populated with its application servers, or nil if the VIP does not exist.
func lookupVIP(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16) (*LBVIP, error) {
all, err := dumpAllVIPs(ch)
if err != nil {
return nil, err
}
want := prefix.String()
for i := range all {
if all[i].Prefix.String() != want {
continue
}
if all[i].Protocol != protocol || all[i].Port != port {
continue
}
ases, err := dumpASesForVIP(ch, protocol, port)
if err != nil {
return nil, err
}
all[i].ASes = ases
return &all[i], nil
}
return nil, nil
}
// dumpASesForVIP returns the application servers bound to the VIP identified
// by (protocol, port). VPP's lb_as_v2_dump filter is used; we also guard
// defensively against replies for other VIPs.
func dumpASesForVIP(ch *loggedChannel, protocol uint8, port uint16) ([]LBAS, error) {
req := &lb.LbAsV2Dump{
Protocol: protocol,
Port: port,
}
reqCtx := ch.SendMultiRequest(req)
var out []LBAS
for {
reply := &lb.LbAsV2Details{}
stop, err := reqCtx.ReceiveReply(reply)
if err != nil {
return nil, fmt.Errorf("lb_as_v2_dump: %w", err)
}
if stop {
break
}
if reply.Vip.Port != port || uint8(reply.Vip.Protocol) != protocol {
continue
}
var inUse time.Time
if reply.InUseSince != 0 {
inUse = time.Unix(int64(reply.InUseSince), 0)
}
out = append(out, LBAS{
Address: reply.AppSrv.ToIP(),
Weight: reply.Weight,
Flags: reply.Flags,
NumBuckets: reply.NumBuckets,
InUseSince: inUse,
})
}
return out, nil
}
// vipFromDetails builds an LBVIP (without ASes) from a VPP lb_vip_details reply.
func vipFromDetails(reply *lb.LbVipDetails) LBVIP {
return LBVIP{
Prefix: lbVipPrefix(reply.Vip),
Protocol: uint8(reply.Vip.Protocol),
Port: reply.Vip.Port,
Encap: encapString(reply.Encap),
SrvType: srvTypeString(reply.SrvType),
Dscp: uint8(reply.Dscp),
TargetPort: reply.TargetPort,
FlowTableLength: reply.FlowTableLength,
}
}
// lbVipPrefix converts a VPP lb_vip's address+prefix to a *net.IPNet.
func lbVipPrefix(v lb_types.LbVip) *net.IPNet {
ip := v.Pfx.Address.ToIP()
bits := 32
if ip.To4() == nil {
bits = 128
}
return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(v.Pfx.Len), bits),
}
}
func ip4ToNetIP(a [4]byte) net.IP {
// VPP reports 255.255.255.255 when no IPv4 src is configured.
if a == [4]byte{0xff, 0xff, 0xff, 0xff} {
return nil
}
return net.IPv4(a[0], a[1], a[2], a[3]).To4()
}
func ip6ToNetIP(a [16]byte) net.IP {
// VPP reports all-ones when no IPv6 src is configured.
allOnes := true
for _, b := range a {
if b != 0xff {
allOnes = false
break
}
}
if allOnes {
return nil
}
ip := make(net.IP, 16)
copy(ip, a[:])
return ip
}
func encapString(e lb_types.LbEncapType) string {
switch e {
case lb_types.LB_API_ENCAP_TYPE_GRE4:
return "gre4"
case lb_types.LB_API_ENCAP_TYPE_GRE6:
return "gre6"
case lb_types.LB_API_ENCAP_TYPE_L3DSR:
return "l3dsr"
case lb_types.LB_API_ENCAP_TYPE_NAT4:
return "nat4"
case lb_types.LB_API_ENCAP_TYPE_NAT6:
return "nat6"
}
return fmt.Sprintf("unknown(%d)", e)
}
func srvTypeString(t lb_types.LbSrvType) string {
switch t {
case lb_types.LB_API_SRV_TYPE_CLUSTERIP:
return "clusterip"
case lb_types.LB_API_SRV_TYPE_NODEPORT:
return "nodeport"
}
return fmt.Sprintf("unknown(%d)", t)
}

477
internal/vpp/lbsync.go Normal file
View File

@@ -0,0 +1,477 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"errors"
"fmt"
"log/slog"
"net"
"git.ipng.ch/ipng/vpp-maglev/internal/config"
ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types"
lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb"
lb_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb_types"
)
// ErrFrontendNotFound is returned by SyncLBStateVIP when the caller asks for
// a frontend name that does not exist in the config.
var ErrFrontendNotFound = errors.New("frontend not found in config")
// vipKey uniquely identifies a VPP LB VIP by its prefix, protocol, and port.
type vipKey struct {
prefix string // canonical CIDR form
protocol uint8
port uint16
}
// desiredVIP is the sync's view of one VIP derived from the maglev config.
type desiredVIP struct {
Prefix *net.IPNet
Protocol uint8 // 6=TCP, 17=UDP, 255=any
Port uint16
ASes map[string]desiredAS // keyed by AS IP string
}
// desiredAS is one application server to be installed under a VIP.
type desiredAS struct {
Address net.IP
Weight uint8 // 0-100
}
// syncStats counts changes made to the dataplane during a sync run.
type syncStats struct {
vipAdd int
vipDel int
asAdd int
asDel int
asWeight int
}
// SyncLBStateAll reconciles the full VPP load-balancer state with the given
// config. For every frontend in cfg:
// - if the VIP does not exist in VPP, create it;
// - for every pool backend, add the application server if missing, or
// update its weight if different.
//
// VIPs and ASes present in VPP but absent from the config are removed.
// Returns an error if any VPP API call fails.
func (c *Client) SyncLBStateAll(cfg *config.Config) error {
if !c.IsConnected() {
return errNotConnected
}
cur, err := c.GetLBStateAll()
if err != nil {
return fmt.Errorf("read VPP LB state: %w", err)
}
desired := desiredFromConfig(cfg)
ch, err := c.apiChannel()
if err != nil {
return err
}
defer ch.Close()
slog.Info("vpp-lbsync-start",
"scope", "all",
"vips-desired", len(desired),
"vips-current", len(cur.VIPs))
// Index both sides by (prefix, protocol, port).
curByKey := make(map[vipKey]LBVIP, len(cur.VIPs))
for _, v := range cur.VIPs {
curByKey[makeVIPKey(v.Prefix, v.Protocol, v.Port)] = v
}
desByKey := make(map[vipKey]desiredVIP, len(desired))
for _, d := range desired {
desByKey[makeVIPKey(d.Prefix, d.Protocol, d.Port)] = d
}
var st syncStats
// ---- pass 1: remove VIPs that are in VPP but not in config ----
for k, v := range curByKey {
if _, keep := desByKey[k]; keep {
continue
}
if err := removeVIP(ch, v, &st); err != nil {
return err
}
}
// ---- pass 2: add/update VIPs that are in config ----
for k, d := range desByKey {
cur, existing := curByKey[k]
var curPtr *LBVIP
if existing {
curPtr = &cur
}
if err := reconcileVIP(ch, d, curPtr, &st); err != nil {
return err
}
}
slog.Info("vpp-lbsync-done",
"scope", "all",
"vip-added", st.vipAdd,
"vip-removed", st.vipDel,
"as-added", st.asAdd,
"as-removed", st.asDel,
"as-weight-updated", st.asWeight)
return nil
}
// SyncLBStateVIP reconciles a single VIP (identified by frontend name) with
// the given config. Unlike SyncLBStateAll, it never removes VIPs: if the
// frontend is missing from cfg, SyncLBStateVIP returns ErrFrontendNotFound.
// This is the right tool for targeted updates on a busy load-balancer with
// many VIPs — only one VIP is read from VPP and only its ASes are modified.
func (c *Client) SyncLBStateVIP(cfg *config.Config, feName string) error {
if !c.IsConnected() {
return errNotConnected
}
fe, ok := cfg.Frontends[feName]
if !ok {
return fmt.Errorf("%q: %w", feName, ErrFrontendNotFound)
}
d := desiredFromFrontend(cfg, fe)
cur, err := c.GetLBStateVIP(d.Prefix, d.Protocol, d.Port)
if err != nil {
return fmt.Errorf("read VPP VIP state: %w", err)
}
ch, err := c.apiChannel()
if err != nil {
return err
}
defer ch.Close()
slog.Info("vpp-lbsync-start",
"scope", "vip",
"frontend", feName,
"prefix", d.Prefix.String(),
"protocol", protocolName(d.Protocol),
"port", d.Port)
var st syncStats
if err := reconcileVIP(ch, d, cur, &st); err != nil {
return err
}
slog.Info("vpp-lbsync-done",
"scope", "vip",
"frontend", feName,
"vip-added", st.vipAdd,
"as-added", st.asAdd,
"as-removed", st.asDel,
"as-weight-updated", st.asWeight)
return nil
}
// reconcileVIP brings one VIP's state in VPP into alignment with the desired
// state. If cur is nil the VIP is added from scratch; otherwise ASes are
// added, removed, and reweighted individually. Stats are accumulated into st.
func reconcileVIP(ch *loggedChannel, d desiredVIP, cur *LBVIP, st *syncStats) error {
if cur == nil {
if err := addVIP(ch, d); err != nil {
return err
}
st.vipAdd++
for _, as := range d.ASes {
if err := addAS(ch, d.Prefix, d.Protocol, d.Port, as); err != nil {
return err
}
st.asAdd++
}
return nil
}
// VIP exists in both — reconcile ASes.
curASes := make(map[string]LBAS, len(cur.ASes))
for _, a := range cur.ASes {
curASes[a.Address.String()] = a
}
// Remove ASes that are in VPP but not desired.
for addr, a := range curASes {
if _, keep := d.ASes[addr]; keep {
continue
}
if err := delAS(ch, cur.Prefix, cur.Protocol, cur.Port, a.Address); err != nil {
return err
}
st.asDel++
}
// Add new ASes, update weights on existing ones.
for addr, a := range d.ASes {
c, hit := curASes[addr]
if !hit {
if err := addAS(ch, d.Prefix, d.Protocol, d.Port, a); err != nil {
return err
}
st.asAdd++
continue
}
if c.Weight != a.Weight {
if err := setASWeight(ch, d.Prefix, d.Protocol, d.Port, a); err != nil {
return err
}
st.asWeight++
}
}
return nil
}
// removeVIP flushes all ASes from a VIP and then deletes the VIP itself.
func removeVIP(ch *loggedChannel, v LBVIP, st *syncStats) error {
for _, as := range v.ASes {
if err := delAS(ch, v.Prefix, v.Protocol, v.Port, as.Address); err != nil {
return err
}
st.asDel++
}
if err := delVIP(ch, v.Prefix, v.Protocol, v.Port); err != nil {
return err
}
st.vipDel++
return nil
}
// desiredFromConfig flattens every frontend in cfg into a desired VIP set.
func desiredFromConfig(cfg *config.Config) []desiredVIP {
out := make([]desiredVIP, 0, len(cfg.Frontends))
for _, fe := range cfg.Frontends {
out = append(out, desiredFromFrontend(cfg, fe))
}
return out
}
// desiredFromFrontend builds the desired VIP for a single frontend.
//
// All backends across all pools of a frontend are merged into a single
// application-server list so VPP knows about every backend that could ever
// receive traffic. Weights are assigned as follows:
//
// - primary (first) pool: the backend's configured weight
// - any subsequent pool: weight 0 (backend is known but receives no traffic)
//
// This preserves the pool priority model: higher layers can later flip
// secondary-pool backends to non-zero weights on failover without needing to
// add/remove ASes in the dataplane. When the same backend appears in multiple
// pools, the first pool it appears in wins.
func desiredFromFrontend(cfg *config.Config, fe config.Frontend) desiredVIP {
bits := 32
if fe.Address.To4() == nil {
bits = 128
}
d := desiredVIP{
Prefix: &net.IPNet{IP: fe.Address, Mask: net.CIDRMask(bits, bits)},
Protocol: protocolFromConfig(fe.Protocol),
Port: fe.Port,
ASes: make(map[string]desiredAS),
}
for poolIdx, pool := range fe.Pools {
for bName, pb := range pool.Backends {
b, ok := cfg.Backends[bName]
if !ok || !b.Enabled || b.Address == nil {
continue
}
addr := b.Address.String()
if _, already := d.ASes[addr]; already {
continue
}
var w uint8
if poolIdx == 0 {
w = clampWeight(pb.Weight)
} // secondary pools: weight 0 (default)
d.ASes[addr] = desiredAS{Address: b.Address, Weight: w}
}
}
return d
}
// ---- API call helpers ------------------------------------------------------
// defaultFlowsTableLength is sent as NewFlowsTableLength in lb_add_del_vip_v2.
// The .api file declares default=1024 but that default is only applied by VAT/
// the CLI parser, not when a raw message is marshalled over the socket. If we
// send 0, the plugin's vec_validate explodes (OOM / panic). Must be a power of
// two — 1024 matches the default that would have been applied via CLI.
const defaultFlowsTableLength = 1024
func addVIP(ch *loggedChannel, d desiredVIP) error {
encap := encapForIP(d.Prefix.IP)
req := &lb.LbAddDelVipV2{
Pfx: ip_types.NewAddressWithPrefix(*d.Prefix),
Protocol: d.Protocol,
Port: d.Port,
Encap: encap,
Type: lb_types.LB_API_SRV_TYPE_CLUSTERIP,
NewFlowsTableLength: defaultFlowsTableLength,
IsDel: false,
}
reply := &lb.LbAddDelVipV2Reply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_add_del_vip_v2 add %s: %w", d.Prefix, err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_add_del_vip_v2 add %s: retval=%d", d.Prefix, reply.Retval)
}
slog.Debug("vpp-lbsync-vip-add",
"prefix", d.Prefix.String(),
"protocol", protocolName(d.Protocol),
"port", d.Port,
"encap", encapName(encap))
return nil
}
func delVIP(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16) error {
req := &lb.LbAddDelVipV2{
Pfx: ip_types.NewAddressWithPrefix(*prefix),
Protocol: protocol,
Port: port,
IsDel: true,
}
reply := &lb.LbAddDelVipV2Reply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_add_del_vip_v2 del %s: %w", prefix, err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_add_del_vip_v2 del %s: retval=%d", prefix, reply.Retval)
}
slog.Debug("vpp-lbsync-vip-del",
"prefix", prefix.String(),
"protocol", protocolName(protocol),
"port", port)
return nil
}
func addAS(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, a desiredAS) error {
req := &lb.LbAddDelAsV2{
Pfx: ip_types.NewAddressWithPrefix(*prefix),
Protocol: protocol,
Port: port,
AsAddress: ip_types.NewAddress(a.Address),
Weight: a.Weight,
IsDel: false,
}
reply := &lb.LbAddDelAsV2Reply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_add_del_as_v2 add %s@%s: %w", a.Address, prefix, err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_add_del_as_v2 add %s@%s: retval=%d", a.Address, prefix, reply.Retval)
}
slog.Debug("vpp-lbsync-as-add",
"vip", prefix.String(),
"protocol", protocolName(protocol),
"port", port,
"address", a.Address.String(),
"weight", a.Weight)
return nil
}
func delAS(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, addr net.IP) error {
req := &lb.LbAddDelAsV2{
Pfx: ip_types.NewAddressWithPrefix(*prefix),
Protocol: protocol,
Port: port,
AsAddress: ip_types.NewAddress(addr),
IsDel: true,
IsFlush: true,
}
reply := &lb.LbAddDelAsV2Reply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_add_del_as_v2 del %s@%s: %w", addr, prefix, err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_add_del_as_v2 del %s@%s: retval=%d", addr, prefix, reply.Retval)
}
slog.Debug("vpp-lbsync-as-del",
"vip", prefix.String(),
"protocol", protocolName(protocol),
"port", port,
"address", addr.String())
return nil
}
func setASWeight(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, a desiredAS) error {
req := &lb.LbAsSetWeight{
Pfx: ip_types.NewAddressWithPrefix(*prefix),
Protocol: protocol,
Port: port,
AsAddress: ip_types.NewAddress(a.Address),
Weight: a.Weight,
}
reply := &lb.LbAsSetWeightReply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
return fmt.Errorf("lb_as_set_weight %s@%s: %w", a.Address, prefix, err)
}
if reply.Retval != 0 {
return fmt.Errorf("lb_as_set_weight %s@%s: retval=%d", a.Address, prefix, reply.Retval)
}
slog.Debug("vpp-lbsync-as-weight",
"vip", prefix.String(),
"protocol", protocolName(protocol),
"port", port,
"address", a.Address.String(),
"weight", a.Weight)
return nil
}
// ---- utility ---------------------------------------------------------------
func makeVIPKey(prefix *net.IPNet, protocol uint8, port uint16) vipKey {
return vipKey{prefix: prefix.String(), protocol: protocol, port: port}
}
func protocolFromConfig(s string) uint8 {
switch s {
case "tcp":
return 6
case "udp":
return 17
}
return 255 // any
}
func protocolName(p uint8) string {
switch p {
case 6:
return "tcp"
case 17:
return "udp"
case 255:
return "any"
}
return fmt.Sprintf("%d", p)
}
func encapForIP(ip net.IP) lb_types.LbEncapType {
if ip.To4() != nil {
return lb_types.LB_API_ENCAP_TYPE_GRE4
}
return lb_types.LB_API_ENCAP_TYPE_GRE6
}
func encapName(e lb_types.LbEncapType) string {
switch e {
case lb_types.LB_API_ENCAP_TYPE_GRE4:
return "gre4"
case lb_types.LB_API_ENCAP_TYPE_GRE6:
return "gre6"
}
return fmt.Sprintf("%d", e)
}
func clampWeight(w int) uint8 {
if w < 0 {
return 0
}
if w > 100 {
return 100
}
return uint8(w)
}

View File

@@ -21,6 +21,8 @@ service Maglev {
rpc CheckConfig(CheckConfigRequest) returns (CheckConfigResponse);
rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse);
rpc GetVPPInfo(GetVPPInfoRequest) returns (VPPInfo);
rpc GetVPPLBState(GetVPPLBStateRequest) returns (VPPLBState);
rpc SyncVPPLBState(SyncVPPLBStateRequest) returns (SyncVPPLBStateResponse);
}
// ---- requests ---------------------------------------------------------------
@@ -75,6 +77,55 @@ message VPPInfo {
int64 connecttime_ns = 6; // unix timestamp (ns) when maglevd connected to VPP
}
// ---- VPP load-balancer state ------------------------------------------------
message GetVPPLBStateRequest {}
// VPPLBConf mirrors VPP's lb_conf_get_reply: global LB plugin settings.
message VPPLBConf {
string ip4_src_address = 1;
string ip6_src_address = 2;
uint32 sticky_buckets_per_core = 3;
uint32 flow_timeout = 4;
}
// VPPLBAS is one application server attached to a VIP.
message VPPLBAS {
string address = 1;
uint32 weight = 2; // 0-100
uint32 flags = 3; // VPP AS flags (bit 0 = used, bit 1 = flushed)
uint32 num_buckets = 4;
int64 in_use_since_ns = 5; // unix timestamp (ns), 0 if never used
}
// VPPLBVIP mirrors VPP's lb_vip_details plus the attached application servers.
// Note: srv_type, dscp, and target_port are intentionally omitted — maglevd
// only supports GRE encap, so NAT/L3DSR-specific fields don't apply.
message VPPLBVIP {
string prefix = 1; // CIDR, e.g. 192.0.2.1/32
uint32 protocol = 2; // 6=TCP, 17=UDP, 255=any
uint32 port = 3; // 0 = all-port VIP
string encap = 4; // gre4|gre6|l3dsr|nat4|nat6
uint32 flow_table_length = 5;
repeated VPPLBAS application_servers = 6;
}
message VPPLBState {
VPPLBConf conf = 1;
repeated VPPLBVIP vips = 2;
}
// SyncVPPLBStateRequest triggers a reconciliation between the maglev config
// and the VPP load-balancer dataplane. When frontend_name is set, only that
// frontend's VIP is synced (SyncLBStateVIP) and no VIPs are removed. When
// unset, a full reconciliation runs (SyncLBStateAll), which will also remove
// stale VIPs from VPP.
message SyncVPPLBStateRequest {
optional string frontend_name = 1;
}
message SyncVPPLBStateResponse {}
message SetWeightRequest {
string frontend = 1;
string pool = 2;

View File

@@ -2,6 +2,11 @@ maglev:
healthchecker:
transition-history: 5
vpp:
lb:
ipv4-src-address: 10.0.0.1
ipv6-src-address: 2001:db8::1
healthchecks:
http-check:
type: http