Skip to content

Commit 84e6cf4

Browse files
committed
client/daemon: route liveness peer client version
1 parent 3cbd2fc commit 84e6cf4

21 files changed

Lines changed: 541 additions & 75 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
1818
- Increase default controller request timeout in config agent
1919
- Client
2020
- Route liveness treats peers that advertise passive mode as selectively passive; does not manage their routes directly.
21+
- Advertise peer client version with route liveness control packets.
2122

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

client/doublezero/src/routes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod tests {
3737
liveness_state: None,
3838
liveness_state_reason: None,
3939
peer_ip: peer_ip.to_string(),
40+
peer_client_version: None,
4041
}
4142
}
4243

client/doublezero/src/servicecontroller.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ pub struct RouteRecord {
130130
pub liveness_state: Option<String>,
131131
#[tabled(rename = "Liveness State Reason")]
132132
pub liveness_state_reason: Option<String>,
133+
#[tabled(rename = "Peer Client Version")]
134+
pub peer_client_version: Option<String>,
133135
}
134136

135137
impl fmt::Display for RouteRecord {

client/doublezerod/cmd/doublezerod/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ var (
5353
routeLivenessEnableActive = flag.Bool("route-liveness-enable-active", false, "enables route liveness in active mode (experimental)")
5454

5555
// set by LDFLAGS
56-
version = "dev"
56+
version = "0.0.0-dev"
5757
commit = "none"
5858
date = "unknown"
5959
)
@@ -154,9 +154,10 @@ func main() {
154154
log = newLogger(slog.LevelDebug)
155155
}
156156
lmc = &liveness.ManagerConfig{
157-
Logger: log,
158-
BindIP: defaultRouteLivenessBindIP,
159-
Port: liveness.DefaultLivenessPort,
157+
Logger: log,
158+
BindIP: defaultRouteLivenessBindIP,
159+
Port: liveness.DefaultLivenessPort,
160+
ClientVersion: version,
160161

161162
// If active mode is enabled, set passive mode to false.
162163
// The manager only knows about passive mode, with the negation of it being active mode.

client/doublezerod/internal/api/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Route struct {
3535
LivenessStateReason string `json:"liveness_state_reason,omitempty"`
3636
LivenessExpectedKernelState string `json:"liveness_expected_kernel_state,omitempty"`
3737
LivenessPeerMode string `json:"liveness_peer_mode,omitempty"`
38+
PeerClientVersion string `json:"peer_client_version,omitempty"`
3839
}
3940

4041
type routeKey struct {
@@ -134,6 +135,7 @@ func ServeRoutesHandler(nlr bgp.RouteReaderWriter, lm LivenessManager, db DBRead
134135
LivenessStateReason: stateReason,
135136
LivenessExpectedKernelState: sess.ExpectedKernelState.String(),
136137
LivenessPeerMode: sess.PeerAdvertisedMode.String(),
138+
PeerClientVersion: sess.PeerClientVersion.String(),
137139
}
138140
break
139141
}

client/doublezerod/internal/api/routes_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_KernelOnly(t *testing.T) {
242242
require.Empty(t, rt.LivenessStateReason)
243243
require.Empty(t, rt.LivenessExpectedKernelState)
244244
require.Empty(t, rt.LivenessPeerMode)
245+
require.Empty(t, rt.PeerClientVersion)
245246
}
246247

247248
func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T) {
@@ -265,6 +266,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
265266
State: liveness.StateUp,
266267
ExpectedKernelState: liveness.KernelStatePresent,
267268
PeerAdvertisedMode: liveness.PeerModeActive,
269+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
268270
}
269271

270272
svc := &ProvisionRequest{
@@ -315,6 +317,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
315317
require.Empty(t, rt.LivenessStateReason)
316318
require.Equal(t, liveness.KernelStatePresent.String(), rt.LivenessExpectedKernelState)
317319
require.Equal(t, LivenessPeerModeActive.String(), rt.LivenessPeerMode)
320+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
318321
}
319322

320323
func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T) {
@@ -338,6 +341,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
338341
State: liveness.StateDown,
339342
ExpectedKernelState: liveness.KernelStateAbsent,
340343
PeerAdvertisedMode: liveness.PeerModePassive,
344+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
341345
}
342346

343347
svc := &ProvisionRequest{
@@ -385,6 +389,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
385389
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
386390
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
387391
require.Equal(t, liveness.DownReasonNone.String(), rt.LivenessStateReason)
392+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
388393
}
389394

390395
func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *testing.T) {
@@ -409,6 +414,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
409414
LastDownReason: liveness.DownReasonRemoteAdmin,
410415
ExpectedKernelState: liveness.KernelStateAbsent,
411416
PeerAdvertisedMode: liveness.PeerModePassive,
417+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
412418
}
413419

414420
svc := &ProvisionRequest{
@@ -459,6 +465,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
459465
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
460466
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
461467
require.Equal(t, liveness.DownReasonRemoteAdmin.String(), rt.LivenessStateReason)
468+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
462469
}
463470

464471
func TestServeRoutesHandler_UsesDoubleZeroIP_NotTunnelSrc(t *testing.T) {

client/doublezerod/internal/bgp/bgp_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,16 @@ func (p *dummyPlugin) handleUpdate(peer corebgp.PeerConfig, u []byte) *corebgp.N
118118
func TestBgpServer(t *testing.T) {
119119
nlr := &mockRouteReaderWriter{}
120120
lm, err := liveness.NewManager(t.Context(), &liveness.ManagerConfig{
121-
Logger: slog.Default(),
122-
Netlinker: nlr,
123-
BindIP: "127.0.0.1",
124-
Port: 0,
125-
TxMin: 100 * time.Millisecond,
126-
RxMin: 100 * time.Millisecond,
127-
DetectMult: 3,
128-
MinTxFloor: 50 * time.Millisecond,
129-
MaxTxCeil: 1 * time.Second,
121+
Logger: slog.Default(),
122+
Netlinker: nlr,
123+
BindIP: "127.0.0.1",
124+
Port: 0,
125+
TxMin: 100 * time.Millisecond,
126+
RxMin: 100 * time.Millisecond,
127+
DetectMult: 3,
128+
MinTxFloor: 50 * time.Millisecond,
129+
MaxTxCeil: 1 * time.Second,
130+
ClientVersion: "1.2.3-dev",
130131
})
131132
require.NoError(t, err)
132133
t.Cleanup(func() { _ = lm.Close() })

client/doublezerod/internal/liveness/faults_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,7 @@ func setupLivenessClients(t *testing.T, basePort int) ([]*testClient, *fakeUDPSw
698698
MinTxFloor: cfg.txMin,
699699
MaxTxCeil: cfg.txMin,
700700
BackoffMax: cfg.backoffMax,
701+
ClientVersion: "1.2.3-dev",
701702
})
702703
require.NoError(t, err)
703704

@@ -759,6 +760,7 @@ func setupMixedPassiveLivenessClients(t *testing.T, basePort int) ([]*testClient
759760
MinTxFloor: cfg.txMin,
760761
MaxTxCeil: cfg.txMin,
761762
BackoffMax: cfg.backoffMax,
763+
ClientVersion: "1.2.3-dev",
762764
}
763765

764766
switch i {

client/doublezerod/internal/liveness/manager.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ type ManagerConfig struct {
8686
// liveness still tracks state and metrics, but will not install or
8787
// uninstall its routes in the kernel for that session.
8888
HonorPeerAdvertisedPassive bool
89+
90+
// Client version to advertise to peers in control packets.
91+
ClientVersion string
8992
}
9093

9194
// Validate fills defaults and enforces constraints for ManagerConfig.
@@ -142,6 +145,9 @@ func (c *ManagerConfig) Validate() error {
142145
if c.MaxEvents < 0 {
143146
return errors.New("maxEvents must be greater than 0")
144147
}
148+
if c.ClientVersion == "" {
149+
return errors.New("clientVersion is required")
150+
}
145151
return nil
146152
}
147153

@@ -196,6 +202,11 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*manager, error) {
196202
return nil, fmt.Errorf("error validating manager config: %v", err)
197203
}
198204

205+
clientVersion, err := ParseClientVersion(cfg.ClientVersion)
206+
if err != nil {
207+
return nil, fmt.Errorf("error parsing client version: %v", err)
208+
}
209+
199210
udp := cfg.UDP
200211
if udp == nil {
201212
var err error
@@ -242,7 +253,7 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*manager, error) {
242253

243254
// Wire up IO loops.
244255
m.recv = NewReceiver(m.log, m.udp, m.HandleRx, m.metrics)
245-
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode)
256+
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode, clientVersion)
246257

247258
// Receiver goroutine: parses control packets and dispatches to HandleRx.
248259
m.wg.Add(1)
@@ -730,7 +741,10 @@ func (m *manager) onSessionUp(sess *Session) {
730741
"peer", peer.String(),
731742
"route", snap.Route.String(),
732743
"convergence", convergence.String(),
733-
"upSince", snap.UpSince.UTC().String())
744+
"upSince", snap.UpSince.UTC().String(),
745+
"peerAdvertisedMode", snap.PeerAdvertisedMode.String(),
746+
"peerClientVersion", snap.PeerClientVersion.String(),
747+
)
734748
}
735749

736750
// onSessionDown withdraws the route if currently installed (unless PassiveMode
@@ -772,7 +786,9 @@ func (m *manager) onSessionDown(sess *Session) {
772786
"peer", peer.String(),
773787
"routePresent", route != nil,
774788
"downSince", snap.DownSince.UTC().String(),
775-
"downReason", snap.LastDownReason.String())
789+
"downReason", snap.LastDownReason.String(),
790+
"peerClientVersion", snap.PeerClientVersion.String(),
791+
)
776792
return
777793
}
778794

@@ -781,7 +797,9 @@ func (m *manager) onSessionDown(sess *Session) {
781797
"peer", peer.String(),
782798
"route", snap.Route.String(),
783799
"downSince", snap.DownSince.UTC().String(),
784-
"downReason", snap.LastDownReason.String())
800+
"downReason", snap.LastDownReason.String(),
801+
"peerClientVersion", snap.PeerClientVersion.String(),
802+
)
785803
return
786804
}
787805

@@ -790,7 +808,9 @@ func (m *manager) onSessionDown(sess *Session) {
790808
"peer", peer.String(),
791809
"route", snap.Route.String(),
792810
"downSince", snap.DownSince.UTC().String(),
793-
"downReason", snap.LastDownReason.String())
811+
"downReason", snap.LastDownReason.String(),
812+
"peerClientVersion", snap.PeerClientVersion.String(),
813+
)
794814
return
795815
}
796816

@@ -807,7 +827,9 @@ func (m *manager) onSessionDown(sess *Session) {
807827
"route", snap.Route.String(),
808828
"convergence", convergence.String(),
809829
"downSince", snap.DownSince.UTC().String(),
810-
"downReason", snap.LastDownReason.String())
830+
"downReason", snap.LastDownReason.String(),
831+
"peerClientVersion", snap.PeerClientVersion.String(),
832+
)
811833
}
812834

813835
// isPeerEffectivelyPassive returns true when this session should not have its

client/doublezerod/internal/liveness/manager_test.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,50 @@ func TestClient_Liveness_Manager_ConfigValidate(t *testing.T) {
1616
t.Parallel()
1717
log := newTestLogger(t)
1818

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

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

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

28-
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MinTxFloor: -1}).Validate()
28+
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MinTxFloor: -1, ClientVersion: "1.2.3-dev"}).Validate()
2929
require.Error(t, err)
30-
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MaxTxCeil: -1}).Validate()
30+
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", MaxTxCeil: -1, ClientVersion: "1.2.3-dev"}).Validate()
3131
require.Error(t, err)
32-
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", BackoffMax: -1}).Validate()
32+
err = (&ManagerConfig{Logger: log, Netlinker: &MockRouteReaderWriter{}, BindIP: "127.0.0.1", BackoffMax: -1, ClientVersion: "1.2.3-dev"}).Validate()
3333
require.Error(t, err)
3434

3535
err = (&ManagerConfig{
36-
Logger: log,
37-
Netlinker: &MockRouteReaderWriter{},
38-
BindIP: "127.0.0.1",
39-
TxMin: 100 * time.Millisecond,
40-
RxMin: 100 * time.Millisecond,
41-
DetectMult: 3,
42-
MinTxFloor: 200 * time.Millisecond,
43-
MaxTxCeil: 100 * time.Millisecond,
44-
Port: -1, // invalid port
36+
Logger: log,
37+
Netlinker: &MockRouteReaderWriter{},
38+
BindIP: "127.0.0.1",
39+
ClientVersion: "1.2.3-dev",
40+
TxMin: 100 * time.Millisecond,
41+
RxMin: 100 * time.Millisecond,
42+
DetectMult: 3,
43+
MinTxFloor: 200 * time.Millisecond,
44+
MaxTxCeil: 100 * time.Millisecond,
45+
Port: -1, // invalid port
4546
}).Validate()
4647
require.EqualError(t, err, "port must be greater than or equal to 0")
4748

4849
cfg := &ManagerConfig{
49-
Logger: log,
50-
Netlinker: &MockRouteReaderWriter{},
51-
BindIP: "127.0.0.1",
52-
TxMin: 100 * time.Millisecond,
53-
RxMin: 100 * time.Millisecond,
54-
DetectMult: 3,
55-
MinTxFloor: 50 * time.Millisecond,
56-
MaxTxCeil: 1 * time.Second,
50+
Logger: log,
51+
Netlinker: &MockRouteReaderWriter{},
52+
ClientVersion: "1.2.3-dev",
53+
BindIP: "127.0.0.1",
54+
TxMin: 100 * time.Millisecond,
55+
RxMin: 100 * time.Millisecond,
56+
DetectMult: 3,
57+
MinTxFloor: 50 * time.Millisecond,
58+
MaxTxCeil: 1 * time.Second,
5759
}
5860
err = cfg.Validate()
5961
require.NoError(t, err)
62+
require.Equal(t, "1.2.3-dev", cfg.ClientVersion)
6063
require.NotZero(t, cfg.MinTxFloor)
6164
require.NotZero(t, cfg.MaxTxCeil)
6265
require.NotZero(t, cfg.BackoffMax)
@@ -1128,6 +1131,7 @@ func newTestManagerWithMetrics(t *testing.T, mutate func(*ManagerConfig)) (*mana
11281131
MinTxFloor: 50 * time.Millisecond,
11291132
MaxTxCeil: 1 * time.Second,
11301133
BackoffMax: 1 * time.Second,
1134+
ClientVersion: "1.2.3-dev",
11311135
}
11321136
if mutate != nil {
11331137
mutate(cfg)

0 commit comments

Comments
 (0)