diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 642dc1c6..4db2aa24 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -513,6 +513,7 @@ func nodesToPtables( "Expiration", "Connected", "Expired", + "Client Version", } tableData := pterm.TableData{tableHeader} @@ -623,6 +624,7 @@ func nodesToPtables( expiryTime, online, expired, + node.GetClientVersion(), } tableData = append( tableData, diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index b4b7e8f6..73d1ddb6 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -98,6 +98,7 @@ type Node struct { AvailableRoutes []string `protobuf:"bytes,24,rep,name=available_routes,json=availableRoutes,proto3" json:"available_routes,omitempty"` SubnetRoutes []string `protobuf:"bytes,25,rep,name=subnet_routes,json=subnetRoutes,proto3" json:"subnet_routes,omitempty"` Tags []string `protobuf:"bytes,26,rep,name=tags,proto3" json:"tags,omitempty"` + ClientVersion string `protobuf:"bytes,27,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -258,6 +259,13 @@ func (x *Node) GetTags() []string { return nil } +func (x *Node) GetClientVersion() string { + if x != nil { + return x.ClientVersion + } + return "" +} + type RegisterNodeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index e705a33a..94089106 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -424,6 +424,10 @@ func (node *Node) Proto() *v1.Node { nodeProto.Expiry = timestamppb.New(*node.Expiry) } + if node.Hostinfo != nil { + nodeProto.ClientVersion = node.Hostinfo.View().IPNVersion() + } + return nodeProto } diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 40634525..29401ffa 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -969,3 +969,46 @@ func TestHasNetworkChanges(t *testing.T) { }) } } + +func TestNodeProto_ClientVersion(t *testing.T) { + tests := []struct { + name string + hostinfo *tailcfg.Hostinfo + wantVersion string + }{ + { + name: "node-with-client-version", + hostinfo: &tailcfg.Hostinfo{IPNVersion: "1.50.0"}, + wantVersion: "1.50.0", + }, + { + name: "node-with-different-version", + hostinfo: &tailcfg.Hostinfo{IPNVersion: "1.76.1"}, + wantVersion: "1.76.1", + }, + { + name: "node-without-hostinfo", + hostinfo: nil, + wantVersion: "", + }, + { + name: "node-with-empty-version", + hostinfo: &tailcfg.Hostinfo{IPNVersion: ""}, + wantVersion: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := &Node{ + ID: 1, + Hostname: "test-node", + Hostinfo: tt.hostinfo, + } + proto := node.Proto() + if got := proto.GetClientVersion(); got != tt.wantVersion { + t.Errorf("Proto().GetClientVersion() = %q, want %q", got, tt.wantVersion) + } + }) + } +} diff --git a/proto/headscale/v1/node.proto b/proto/headscale/v1/node.proto index 3ce83c4b..08c17972 100644 --- a/proto/headscale/v1/node.proto +++ b/proto/headscale/v1/node.proto @@ -53,6 +53,7 @@ message Node { repeated string available_routes = 24; repeated string subnet_routes = 25; repeated string tags = 26; + string client_version = 27; } message RegisterNodeRequest {