Skip to content

Commit 83508ca

Browse files
committed
client/daemon: route liveness peer client version (#2366)
- Advertise peer client version in route liveness control packets - Update daemon API to include peer client version, and show on CLI command output - Closes #2270 - Added test coverage for new functionality
1 parent a0038fd commit 83508ca

20 files changed

Lines changed: 540 additions & 75 deletions

client/doublezero/src/routes.rs

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

client/doublezero/src/servicecontroller.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub struct RouteRecord {
128128
pub liveness_state: Option<String>,
129129
#[tabled(rename = "Liveness State Reason")]
130130
pub liveness_state_reason: Option<String>,
131+
#[tabled(rename = "Peer Client Version")]
132+
pub peer_client_version: Option<String>,
131133
}
132134

133135
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
@@ -34,6 +34,7 @@ type Route struct {
3434
LivenessStateReason string `json:"liveness_state_reason,omitempty"`
3535
LivenessExpectedKernelState string `json:"liveness_expected_kernel_state,omitempty"`
3636
LivenessPeerMode string `json:"liveness_peer_mode,omitempty"`
37+
PeerClientVersion string `json:"peer_client_version,omitempty"`
3738
}
3839

3940
type routeKey struct {
@@ -131,6 +132,7 @@ func ServeRoutesHandler(nlr bgp.RouteReaderWriter, lm LivenessManager, db DBRead
131132
LivenessStateReason: stateReason,
132133
LivenessExpectedKernelState: sess.ExpectedKernelState.String(),
133134
LivenessPeerMode: sess.PeerAdvertisedMode.String(),
135+
PeerClientVersion: sess.PeerClientVersion.String(),
134136
}
135137
break
136138
}

client/doublezerod/internal/api/routes_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_KernelOnly(t *testing.T) {
235235
require.Empty(t, rt.LivenessStateReason)
236236
require.Empty(t, rt.LivenessExpectedKernelState)
237237
require.Empty(t, rt.LivenessPeerMode)
238+
require.Empty(t, rt.PeerClientVersion)
238239
}
239240

240241
func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T) {
@@ -258,6 +259,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
258259
State: liveness.StateUp,
259260
ExpectedKernelState: liveness.KernelStatePresent,
260261
PeerAdvertisedMode: liveness.PeerModeActive,
262+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
261263
}
262264

263265
svc := &ProvisionRequest{
@@ -306,6 +308,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_PresentInBoth(t *testing.T)
306308
require.Empty(t, rt.LivenessStateReason)
307309
require.Equal(t, liveness.KernelStatePresent.String(), rt.LivenessExpectedKernelState)
308310
require.Equal(t, LivenessPeerModeActive.String(), rt.LivenessPeerMode)
311+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
309312
}
310313

311314
func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T) {
@@ -329,6 +332,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
329332
State: liveness.StateDown,
330333
ExpectedKernelState: liveness.KernelStateAbsent,
331334
PeerAdvertisedMode: liveness.PeerModePassive,
335+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
332336
}
333337

334338
svc := &ProvisionRequest{
@@ -374,6 +378,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_AbsentInKernel(t *testing.T)
374378
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
375379
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
376380
require.Equal(t, liveness.DownReasonNone.String(), rt.LivenessStateReason)
381+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
377382
}
378383

379384
func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *testing.T) {
@@ -398,6 +403,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
398403
LastDownReason: liveness.DownReasonRemoteAdmin,
399404
ExpectedKernelState: liveness.KernelStateAbsent,
400405
PeerAdvertisedMode: liveness.PeerModePassive,
406+
PeerClientVersion: liveness.ClientVersion{Major: 1, Minor: 2, Patch: 3, Channel: liveness.VersionChannelStable},
401407
}
402408

403409
svc := &ProvisionRequest{
@@ -446,6 +452,7 @@ func TestClient_API_ServeRoutesHandler_WithLiveness_SetsLivenessStateReason(t *t
446452
require.Equal(t, liveness.KernelStateAbsent.String(), rt.LivenessExpectedKernelState)
447453
require.Equal(t, LivenessPeerModePassive.String(), rt.LivenessPeerMode)
448454
require.Equal(t, liveness.DownReasonRemoteAdmin.String(), rt.LivenessStateReason)
455+
require.Equal(t, sess.PeerClientVersion.String(), rt.PeerClientVersion)
449456
}
450457

451458
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

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

212+
clientVersion, err := ParseClientVersion(cfg.ClientVersion)
213+
if err != nil {
214+
return nil, fmt.Errorf("error parsing client version: %v", err)
215+
}
216+
206217
udp := cfg.UDP
207218
if udp == nil {
208219
var err error
@@ -249,7 +260,7 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*manager, error) {
249260

250261
// Wire up IO loops.
251262
m.recv = NewReceiver(m.log, m.udp, m.HandleRx, m.metrics)
252-
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode)
263+
m.sched = NewScheduler(m.log, m.udp, m.onSessionDown, m.cfg.MaxEvents, m.cfg.EnablePeerMetrics, m.metrics, m.cfg.PassiveMode, clientVersion)
253264

254265
// Receiver goroutine: parses control packets and dispatches to HandleRx.
255266
m.wg.Add(1)
@@ -737,7 +748,10 @@ func (m *manager) onSessionUp(sess *Session) {
737748
"peer", peer.String(),
738749
"route", snap.Route.String(),
739750
"convergence", convergence.String(),
740-
"upSince", snap.UpSince.UTC().String())
751+
"upSince", snap.UpSince.UTC().String(),
752+
"peerAdvertisedMode", snap.PeerAdvertisedMode.String(),
753+
"peerClientVersion", snap.PeerClientVersion.String(),
754+
)
741755
}
742756

743757
// onSessionDown withdraws the route if currently installed (unless PassiveMode
@@ -779,7 +793,9 @@ func (m *manager) onSessionDown(sess *Session) {
779793
"peer", peer.String(),
780794
"routePresent", route != nil,
781795
"downSince", snap.DownSince.UTC().String(),
782-
"downReason", snap.LastDownReason.String())
796+
"downReason", snap.LastDownReason.String(),
797+
"peerClientVersion", snap.PeerClientVersion.String(),
798+
)
783799
return
784800
}
785801

@@ -788,7 +804,9 @@ func (m *manager) onSessionDown(sess *Session) {
788804
"peer", peer.String(),
789805
"route", snap.Route.String(),
790806
"downSince", snap.DownSince.UTC().String(),
791-
"downReason", snap.LastDownReason.String())
807+
"downReason", snap.LastDownReason.String(),
808+
"peerClientVersion", snap.PeerClientVersion.String(),
809+
)
792810
return
793811
}
794812

@@ -797,7 +815,9 @@ func (m *manager) onSessionDown(sess *Session) {
797815
"peer", peer.String(),
798816
"route", snap.Route.String(),
799817
"downSince", snap.DownSince.UTC().String(),
800-
"downReason", snap.LastDownReason.String())
818+
"downReason", snap.LastDownReason.String(),
819+
"peerClientVersion", snap.PeerClientVersion.String(),
820+
)
801821
return
802822
}
803823

@@ -816,7 +836,9 @@ func (m *manager) onSessionDown(sess *Session) {
816836
"route", snap.Route.String(),
817837
"convergence", convergence.String(),
818838
"downSince", snap.DownSince.UTC().String(),
819-
"downReason", snap.LastDownReason.String())
839+
"downReason", snap.LastDownReason.String(),
840+
"peerClientVersion", snap.PeerClientVersion.String(),
841+
)
820842
}
821843

822844
// 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)
@@ -1234,6 +1237,7 @@ func newTestManagerWithMetrics(t *testing.T, mutate func(*ManagerConfig)) (*mana
12341237
MinTxFloor: 50 * time.Millisecond,
12351238
MaxTxCeil: 1 * time.Second,
12361239
BackoffMax: 1 * time.Second,
1240+
ClientVersion: "1.2.3-dev",
12371241
}
12381242
if mutate != nil {
12391243
mutate(cfg)

client/doublezerod/internal/liveness/metrics.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import (
88

99
const (
1010
// Labels.
11-
LabelIface = "iface"
12-
LabelLocalIP = "local_ip"
13-
LabelPeerIP = "peer_ip"
14-
LabelState = "state"
15-
LabelStateTo = "state_to"
16-
LabelStateFrom = "state_from"
17-
LabelReason = "reason"
18-
LabelOperation = "operation"
11+
LabelIface = "iface"
12+
LabelLocalIP = "local_ip"
13+
LabelPeerIP = "peer_ip"
14+
LabelState = "state"
15+
LabelStateTo = "state_to"
16+
LabelStateFrom = "state_from"
17+
LabelReason = "reason"
18+
LabelOperation = "operation"
19+
LabelPeerClientVersion = "peer_client_version"
1920
)
2021

2122
type Metrics struct {
@@ -154,7 +155,7 @@ func newMetrics() *Metrics {
154155
Name: "doublezero_liveness_control_packets_rx_total",
155156
Help: "Total control packets received.",
156157
},
157-
serviceLabels,
158+
withServiceLabels(LabelPeerClientVersion),
158159
),
159160
ControlPacketsRxInvalid: prometheus.NewCounterVec(
160161
prometheus.CounterOpts{

0 commit comments

Comments
 (0)