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

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)
}