Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file.
- Client
- Route liveness treats peers that advertise passive mode as selectively passive; does not manage their routes directly.
- Route liveness runs in passive mode for IBRL with allocated IP, if global passive mode is enabled.
- Advertise peer client version with route liveness control packets.

## [v0.8.0](https://github.com/malbeclabs/doublezero/compare/client/v0.7.1...client/v0.8.0) – 2025-12-02

Expand Down
1 change: 1 addition & 0 deletions client/doublezero/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod tests {
liveness_state: None,
liveness_state_reason: None,
peer_ip: peer_ip.to_string(),
peer_client_version: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions client/doublezero/src/servicecontroller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ pub struct RouteRecord {
pub liveness_state: Option<String>,
#[tabled(rename = "Liveness State Reason")]
pub liveness_state_reason: Option<String>,
#[tabled(rename = "Peer Client Version")]
pub peer_client_version: Option<String>,
}

impl fmt::Display for RouteRecord {
Expand Down
9 changes: 5 additions & 4 deletions client/doublezerod/cmd/doublezerod/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ var (
routeLivenessEnableActive = flag.Bool("route-liveness-enable-active", false, "enables route liveness in active mode (experimental)")

// set by LDFLAGS
version = "dev"
version = "0.0.0-dev"
commit = "none"
date = "unknown"
)
Expand Down Expand Up @@ -154,9 +154,10 @@ func main() {
log = newLogger(slog.LevelDebug)
}
lmc = &liveness.ManagerConfig{
Logger: log,
BindIP: defaultRouteLivenessBindIP,
Port: liveness.DefaultLivenessPort,
Logger: log,
BindIP: defaultRouteLivenessBindIP,
Port: liveness.DefaultLivenessPort,
ClientVersion: version,

// If active mode is enabled, set passive mode to false.
// The manager only knows about passive mode, with the negation of it being active mode.
Expand Down
2 changes: 2 additions & 0 deletions client/doublezerod/internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Route struct {
LivenessStateReason string `json:"liveness_state_reason,omitempty"`
LivenessExpectedKernelState string `json:"liveness_expected_kernel_state,omitempty"`
LivenessPeerMode string `json:"liveness_peer_mode,omitempty"`
PeerClientVersion string `json:"peer_client_version,omitempty"`
}

type routeKey struct {
Expand Down Expand Up @@ -131,6 +132,7 @@ func ServeRoutesHandler(nlr bgp.RouteReaderWriter, lm LivenessManager, db DBRead
LivenessStateReason: stateReason,
LivenessExpectedKernelState: sess.ExpectedKernelState.String(),
LivenessPeerMode: sess.PeerAdvertisedMode.String(),
PeerClientVersion: sess.PeerClientVersion.String(),
}
break
}
Expand Down
7 changes: 7 additions & 0 deletions client/doublezerod/internal/api/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_KernelOnly(t *testing.T) {
require.Empty(t, rt.LivenessStateReason)
require.Empty(t, rt.LivenessExpectedKernelState)
require.Empty(t, rt.LivenessPeerMode)
require.Empty(t, rt.PeerClientVersion)
}

func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T) {
Expand All @@ -261,6 +262,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
State: liveness.StateUp,
ExpectedKernelState: liveness.KernelStatePresent,
PeerAdvertisedMode: liveness.PeerModeActive,
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
}

svc := &ProvisionRequest{
Expand Down Expand Up @@ -310,6 +312,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
require.Empty(t, rt.LivenessStateReason)
require.Equal(t, liveness.KernelStatePresent.String(), rt.LivenessExpectedKernelState)
require.Equal(t, LivenessPeerModeActive.String(), rt.LivenessPeerMode)
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
}

func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T) {
Expand All @@ -333,6 +336,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
State: liveness.StateDown,
ExpectedKernelState: liveness.KernelStateAbsent,
PeerAdvertisedMode: liveness.PeerModePassive,
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
}

svc := &ProvisionRequest{
Expand Down Expand Up @@ -379,6 +383,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
require.Equal(t, liveness.DownReasonNone.String(), rt.LivenessStateReason)
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
}

func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *testing.T) {
Expand All @@ -403,6 +408,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
LastDownReason: liveness.DownReasonRemoteAdmin,
ExpectedKernelState: liveness.KernelStateAbsent,
PeerAdvertisedMode: liveness.PeerModePassive,
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
}

svc := &ProvisionRequest{
Expand Down Expand Up @@ -452,6 +458,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
require.Equal(t, liveness.DownReasonRemoteAdmin.String(), rt.LivenessStateReason)
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
}

func TestServeRoutesHandler_UsesDoubleZeroIP_NotTunnelSrc(t *testing.T) {
Expand Down
19 changes: 10 additions & 9 deletions client/doublezerod/internal/bgp/bgp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,16 @@ func (p *dummyPlugin) handleUpdate(peer corebgp.PeerConfig, u []byte) *corebgp.N
func TestBgpServer(t *testing.T) {
nlr := &mockRouteReaderWriter{}
lm, err := liveness.NewManager(t.Context(), &liveness.ManagerConfig{
Logger: slog.Default(),
Netlinker: nlr,
BindIP: "127.0.0.1",
Port: 0,
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 50 * time.Millisecond,
MaxTxCeil: 1 * time.Second,
Logger: slog.Default(),
Netlinker: nlr,
BindIP: "127.0.0.1",
Port: 0,
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 50 * time.Millisecond,
MaxTxCeil: 1 * time.Second,
ClientVersion: "1.2.3-dev",
})
require.NoError(t, err)
t.Cleanup(func() { _ = lm.Close() })
Expand Down
2 changes: 2 additions & 0 deletions client/doublezerod/internal/liveness/faults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ func setupLivenessClients(t *testing.T, basePort int) ([]*testClient, *fakeUDPSw
MinTxFloor: cfg.txMin,
MaxTxCeil: cfg.txMin,
BackoffMax: cfg.backoffMax,
ClientVersion: "1.2.3-dev",
})
require.NoError(t, err)

Expand Down Expand Up @@ -759,6 +760,7 @@ func setupMixedPassiveLivenessClients(t *testing.T, basePort int) ([]*testClient
MinTxFloor: cfg.txMin,
MaxTxCeil: cfg.txMin,
BackoffMax: cfg.backoffMax,
ClientVersion: "1.2.3-dev",
}

switch i {
Expand Down
34 changes: 28 additions & 6 deletions client/doublezerod/internal/liveness/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ type ManagerConfig struct {
// liveness still tracks state and metrics, but will not install or
// uninstall its routes in the kernel for that session.
HonorPeerAdvertisedPassive bool

// Client version to advertise to peers in control packets.
ClientVersion string
}

// Validate fills defaults and enforces constraints for ManagerConfig.
Expand Down Expand Up @@ -142,6 +145,9 @@ func (c *ManagerConfig) Validate() error {
if c.MaxEvents < 0 {
return errors.New("maxEvents must be greater than 0")
}
if c.ClientVersion == "" {
return errors.New("clientVersion is required")
}
return nil
}

Expand Down Expand Up @@ -203,6 +209,11 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*manager, error) {
return nil, fmt.Errorf("error validating manager config: %v", err)
}

clientVersion, err := ParseClientVersion(cfg.ClientVersion)
if err != nil {
return nil, fmt.Errorf("error parsing client version: %v", err)
}

udp := cfg.UDP
if udp == nil {
var err error
Expand Down Expand Up @@ -249,7 +260,7 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*manager, error) {

// Wire up IO loops.
m.recv = NewReceiver(m.log, m.udp, m.HandleRx, m.metrics)
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode)
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode, clientVersion)

// Receiver goroutine: parses control packets and dispatches to HandleRx.
m.wg.Add(1)
Expand Down Expand Up @@ -737,7 +748,10 @@ func (m *manager) onSessionUp(sess *Session) {
"peer", peer.String(),
"route", snap.Route.String(),
"convergence", convergence.String(),
"upSince", snap.UpSince.UTC().String())
"upSince", snap.UpSince.UTC().String(),
"peerAdvertisedMode", snap.PeerAdvertisedMode.String(),
"peerClientVersion", snap.PeerClientVersion.String(),
)
}

// onSessionDown withdraws the route if currently installed (unless PassiveMode
Expand Down Expand Up @@ -779,7 +793,9 @@ func (m *manager) onSessionDown(sess *Session) {
"peer", peer.String(),
"routePresent", route != nil,
"downSince", snap.DownSince.UTC().String(),
"downReason", snap.LastDownReason.String())
"downReason", snap.LastDownReason.String(),
"peerClientVersion", snap.PeerClientVersion.String(),
)
return
}

Expand All @@ -788,7 +804,9 @@ func (m *manager) onSessionDown(sess *Session) {
"peer", peer.String(),
"route", snap.Route.String(),
"downSince", snap.DownSince.UTC().String(),
"downReason", snap.LastDownReason.String())
"downReason", snap.LastDownReason.String(),
"peerClientVersion", snap.PeerClientVersion.String(),
)
return
}

Expand All @@ -797,7 +815,9 @@ func (m *manager) onSessionDown(sess *Session) {
"peer", peer.String(),
"route", snap.Route.String(),
"downSince", snap.DownSince.UTC().String(),
"downReason", snap.LastDownReason.String())
"downReason", snap.LastDownReason.String(),
"peerClientVersion", snap.PeerClientVersion.String(),
)
return
}

Expand All @@ -816,7 +836,9 @@ func (m *manager) onSessionDown(sess *Session) {
"route", snap.Route.String(),
"convergence", convergence.String(),
"downSince", snap.DownSince.UTC().String(),
"downReason", snap.LastDownReason.String())
"downReason", snap.LastDownReason.String(),
"peerClientVersion", snap.PeerClientVersion.String(),
)
}

// isPeerEffectivelyPassive returns true when this session should not have its
Expand Down
50 changes: 27 additions & 23 deletions client/doublezerod/internal/liveness/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,50 @@ func TestClient_Liveness_Manager_ConfigValidate(t *testing.T) {
t.Parallel()
log := newTestLogger(t)

err := (&ManagerConfig{Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1"}).Validate()
err := (&ManagerConfig{Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)

err = (&ManagerConfig{Logger: log, BindIP: "127.0.0.1"}).Validate()
err = (&ManagerConfig{Logger: log, BindIP: "127.0.0.1", ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)

err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: ""}).Validate()
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "", ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)

err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MinTxFloor: -1}).Validate()
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MinTxFloor: -1, ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MaxTxCeil: -1}).Validate()
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MaxTxCeil: -1, ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", BackoffMax: -1}).Validate()
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", BackoffMax: -1, ClientVersion: "1.2.3-dev"}).Validate()
require.Error(t, err)

err = (&ManagerConfig{
Logger: log,
Netlinker: &MockRouteReaderWriter{},
BindIP: "127.0.0.1",
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 200 * time.Millisecond,
MaxTxCeil: 100 * time.Millisecond,
Port: -1, // invalid port
Logger: log,
Netlinker: &MockRouteReaderWriter{},
BindIP: "127.0.0.1",
ClientVersion: "1.2.3-dev",
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 200 * time.Millisecond,
MaxTxCeil: 100 * time.Millisecond,
Port: -1, // invalid port
}).Validate()
require.EqualError(t, err, "port must be greater than or equal to 0")

cfg := &ManagerConfig{
Logger: log,
Netlinker: &MockRouteReaderWriter{},
BindIP: "127.0.0.1",
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 50 * time.Millisecond,
MaxTxCeil: 1 * time.Second,
Logger: log,
Netlinker: &MockRouteReaderWriter{},
ClientVersion: "1.2.3-dev",
BindIP: "127.0.0.1",
TxMin: 100 * time.Millisecond,
RxMin: 100 * time.Millisecond,
DetectMult: 3,
MinTxFloor: 50 * time.Millisecond,
MaxTxCeil: 1 * time.Second,
}
err = cfg.Validate()
require.NoError(t, err)
require.Equal(t, "1.2.3-dev", cfg.ClientVersion)
require.NotZero(t, cfg.MinTxFloor)
require.NotZero(t, cfg.MaxTxCeil)
require.NotZero(t, cfg.BackoffMax)
Expand Down Expand Up @@ -1234,6 +1237,7 @@ func newTestManagerWithMetrics(t *testing.T, mutate func(*ManagerConfig)) (*mana
MinTxFloor: 50 * time.Millisecond,
MaxTxCeil: 1 * time.Second,
BackoffMax: 1 * time.Second,
ClientVersion: "1.2.3-dev",
}
if mutate != nil {
mutate(cfg)
Expand Down
19 changes: 10 additions & 9 deletions client/doublezerod/internal/liveness/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (

const (
// Labels.
LabelIface = "iface"
LabelLocalIP = "local_ip"
LabelPeerIP = "peer_ip"
LabelState = "state"
LabelStateTo = "state_to"
LabelStateFrom = "state_from"
LabelReason = "reason"
LabelOperation = "operation"
LabelIface = "iface"
LabelLocalIP = "local_ip"
LabelPeerIP = "peer_ip"
LabelState = "state"
LabelStateTo = "state_to"
LabelStateFrom = "state_from"
LabelReason = "reason"
LabelOperation = "operation"
LabelPeerClientVersion = "peer_client_version"
)

type Metrics struct {
Expand Down Expand Up @@ -154,7 +155,7 @@ func newMetrics() *Metrics {
Name: "doublezero_liveness_control_packets_rx_total",
Help: "Total control packets received.",
},
serviceLabels,
withServiceLabels(LabelPeerClientVersion),
),
ControlPacketsRxInvalid: prometheus.NewCounterVec(
prometheus.CounterOpts{
Expand Down
Loading
Loading