mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-08 00:38:34 +00:00
controlclient,health,ipnlocal,tailcfg: add DisplayMessage support
Updates tailscale/corp#27759 Signed-off-by: James Sanderson <jsanderson@tailscale.com>
This commit is contained in:
parent
5b670eb3a5
commit
11e83f9da5
@ -90,6 +90,7 @@ type mapSession struct {
|
|||||||
lastDomain string
|
lastDomain string
|
||||||
lastDomainAuditLogID string
|
lastDomainAuditLogID string
|
||||||
lastHealth []string
|
lastHealth []string
|
||||||
|
lastDisplayMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
||||||
lastPopBrowserURL string
|
lastPopBrowserURL string
|
||||||
lastTKAInfo *tailcfg.TKAInfo
|
lastTKAInfo *tailcfg.TKAInfo
|
||||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||||
@ -412,6 +413,21 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
|||||||
if resp.Health != nil {
|
if resp.Health != nil {
|
||||||
ms.lastHealth = resp.Health
|
ms.lastHealth = resp.Health
|
||||||
}
|
}
|
||||||
|
if resp.DisplayMessages != nil {
|
||||||
|
if v, ok := resp.DisplayMessages["*"]; ok && v == nil {
|
||||||
|
ms.lastDisplayMessages = nil
|
||||||
|
}
|
||||||
|
for k, v := range resp.DisplayMessages {
|
||||||
|
if k == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v != nil {
|
||||||
|
mak.Set(&ms.lastDisplayMessages, k, *v)
|
||||||
|
} else {
|
||||||
|
delete(ms.lastDisplayMessages, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if resp.TKAInfo != nil {
|
if resp.TKAInfo != nil {
|
||||||
ms.lastTKAInfo = resp.TKAInfo
|
ms.lastTKAInfo = resp.TKAInfo
|
||||||
}
|
}
|
||||||
@ -831,15 +847,20 @@ func (ms *mapSession) sortedPeers() []tailcfg.NodeView {
|
|||||||
func (ms *mapSession) netmap() *netmap.NetworkMap {
|
func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||||
peerViews := ms.sortedPeers()
|
peerViews := ms.sortedPeers()
|
||||||
|
|
||||||
// Convert all ms.lastHealth to the new [netmap.NetworkMap.DisplayMessages].
|
|
||||||
var msgs map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
var msgs map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
||||||
|
if len(ms.lastDisplayMessages) != 0 {
|
||||||
|
msgs = ms.lastDisplayMessages
|
||||||
|
} else if len(ms.lastHealth) > 0 {
|
||||||
|
// Convert all ms.lastHealth to the new [netmap.NetworkMap.DisplayMessages]
|
||||||
for _, h := range ms.lastHealth {
|
for _, h := range ms.lastHealth {
|
||||||
mak.Set(&msgs, tailcfg.DisplayMessageID("control-health-"+strhash(h)), tailcfg.DisplayMessage{
|
id := "control-health-" + strhash(h) // Unique ID in case there is more than one health message
|
||||||
|
mak.Set(&msgs, tailcfg.DisplayMessageID(id), tailcfg.DisplayMessage{
|
||||||
Title: "Coordination server reports an issue",
|
Title: "Coordination server reports an issue",
|
||||||
Severity: tailcfg.SeverityMedium,
|
Severity: tailcfg.SeverityMedium,
|
||||||
Text: "The coordination server is reporting a health issue: " + h,
|
Text: "The coordination server is reporting a health issue: " + h,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nm := &netmap.NetworkMap{
|
nm := &netmap.NetworkMap{
|
||||||
NodeKey: ms.publicNodeKey,
|
NodeKey: ms.publicNodeKey,
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
"tailscale.com/control/controlknobs"
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
@ -1139,8 +1140,190 @@ func BenchmarkMapSessionDelta(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNetmapDisplayMessage checks that the various diff operations
|
||||||
|
// (add/update/delete/clear) for [tailcfg.DisplayMessage] in a
|
||||||
|
// [tailcfg.MapResponse] work as expected.
|
||||||
|
func TestNetmapDisplayMessage(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
initialState *tailcfg.MapResponse
|
||||||
|
mapResponse tailcfg.MapResponse
|
||||||
|
wantMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "basic-set",
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"test-message": {
|
||||||
|
Title: "Testing",
|
||||||
|
Text: "This is a test message",
|
||||||
|
Severity: tailcfg.SeverityHigh,
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
||||||
|
URL: "https://www.example.com",
|
||||||
|
Label: "Learn more",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"test-message": {
|
||||||
|
Title: "Testing",
|
||||||
|
Text: "This is a test message",
|
||||||
|
Severity: tailcfg.SeverityHigh,
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
||||||
|
URL: "https://www.example.com",
|
||||||
|
Label: "Learn more",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete-one",
|
||||||
|
initialState: &tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update-one",
|
||||||
|
initialState: &tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A updated",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A updated",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add-one",
|
||||||
|
initialState: &tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete-all",
|
||||||
|
initialState: &tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"*": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete-all-and-add",
|
||||||
|
initialState: &tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"message-a": {
|
||||||
|
Title: "Message A",
|
||||||
|
},
|
||||||
|
"message-b": {
|
||||||
|
Title: "Message B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mapResponse: tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"*": nil,
|
||||||
|
"message-c": {
|
||||||
|
Title: "Message C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"message-c": {
|
||||||
|
Title: "Message C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
ms := newTestMapSession(t, nil)
|
||||||
|
|
||||||
|
if test.initialState != nil {
|
||||||
|
ms.netmapForResponse(test.initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
nm := ms.netmapForResponse(&test.mapResponse)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(test.wantMessages, nm.DisplayMessages, cmpopts.EquateEmpty()); diff != "" {
|
||||||
|
t.Errorf("unexpected warnings (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestNetmapHealthIntegration checks that we get the expected health warnings
|
// TestNetmapHealthIntegration checks that we get the expected health warnings
|
||||||
// from processing a map response and passing the NetworkMap to a health tracker
|
// from processing a [tailcfg.MapResponse] containing health messages and passing the
|
||||||
|
// [netmap.NetworkMap] to a [health.Tracker].
|
||||||
func TestNetmapHealthIntegration(t *testing.T) {
|
func TestNetmapHealthIntegration(t *testing.T) {
|
||||||
ms := newTestMapSession(t, nil)
|
ms := newTestMapSession(t, nil)
|
||||||
ht := health.Tracker{}
|
ht := health.Tracker{}
|
||||||
@ -1182,3 +1365,56 @@ func TestNetmapHealthIntegration(t *testing.T) {
|
|||||||
t.Fatalf("CurrentStatus().Warnings[\"control-health*\"] different than expected (-want +got)\n%s", d)
|
t.Fatalf("CurrentStatus().Warnings[\"control-health*\"] different than expected (-want +got)\n%s", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNetmapDisplayMessageIntegration checks that we get the expected health
|
||||||
|
// warnings from processing a [tailcfg.MapResponse] that contains DisplayMessages and
|
||||||
|
// passing the [netmap.NetworkMap] to a [health.Tracker].
|
||||||
|
func TestNetmapDisplayMessageIntegration(t *testing.T) {
|
||||||
|
ms := newTestMapSession(t, nil)
|
||||||
|
ht := health.Tracker{}
|
||||||
|
|
||||||
|
ht.SetIPNState("NeedsLogin", true)
|
||||||
|
ht.GotStreamedMapResponse()
|
||||||
|
baseWarnings := ht.CurrentState().Warnings
|
||||||
|
|
||||||
|
nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
||||||
|
"test-message": {
|
||||||
|
Title: "Testing",
|
||||||
|
Text: "This is a test message",
|
||||||
|
Severity: tailcfg.SeverityHigh,
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
||||||
|
URL: "https://www.example.com",
|
||||||
|
Label: "Learn more",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ht.SetControlHealth(nm.DisplayMessages)
|
||||||
|
|
||||||
|
state := ht.CurrentState()
|
||||||
|
|
||||||
|
// Ignore warnings that aren't from the netmap
|
||||||
|
for k := range baseWarnings {
|
||||||
|
delete(state.Warnings, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := map[health.WarnableCode]health.UnhealthyState{
|
||||||
|
"test-message": {
|
||||||
|
WarnableCode: "test-message",
|
||||||
|
Title: "Testing",
|
||||||
|
Text: "This is a test message",
|
||||||
|
Severity: health.SeverityHigh,
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
PrimaryAction: &health.UnhealthyStateAction{
|
||||||
|
URL: "https://www.example.com",
|
||||||
|
Label: "Learn more",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, state.Warnings); diff != "" {
|
||||||
|
t.Errorf("unexpected message contents (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -34,6 +34,15 @@ type UnhealthyState struct {
|
|||||||
Args Args `json:",omitempty"`
|
Args Args `json:",omitempty"`
|
||||||
DependsOn []WarnableCode `json:",omitempty"`
|
DependsOn []WarnableCode `json:",omitempty"`
|
||||||
ImpactsConnectivity bool `json:",omitempty"`
|
ImpactsConnectivity bool `json:",omitempty"`
|
||||||
|
PrimaryAction *UnhealthyStateAction `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnhealthyStateAction represents an action (URL and link) to be presented to
|
||||||
|
// the user associated with an [UnhealthyState]. Analogous to
|
||||||
|
// [tailcfg.DisplayMessageAction].
|
||||||
|
type UnhealthyStateAction struct {
|
||||||
|
URL string
|
||||||
|
Label string
|
||||||
}
|
}
|
||||||
|
|
||||||
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
|
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
|
||||||
@ -102,15 +111,23 @@ func (t *Tracker) CurrentState() *State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for id, msg := range t.lastNotifiedControlMessages {
|
for id, msg := range t.lastNotifiedControlMessages {
|
||||||
code := WarnableCode(id)
|
state := UnhealthyState{
|
||||||
wm[code] = UnhealthyState{
|
WarnableCode: WarnableCode(id),
|
||||||
WarnableCode: code,
|
|
||||||
Severity: severityFromTailcfg(msg.Severity),
|
Severity: severityFromTailcfg(msg.Severity),
|
||||||
Title: msg.Title,
|
Title: msg.Title,
|
||||||
Text: msg.Text,
|
Text: msg.Text,
|
||||||
ImpactsConnectivity: msg.ImpactsConnectivity,
|
ImpactsConnectivity: msg.ImpactsConnectivity,
|
||||||
// TODO(tailscale/corp#27759): DependsOn?
|
// TODO(tailscale/corp#27759): DependsOn?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.PrimaryAction != nil {
|
||||||
|
state.PrimaryAction = &UnhealthyStateAction{
|
||||||
|
URL: msg.PrimaryAction.URL,
|
||||||
|
Label: msg.PrimaryAction.Label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wm[state.WarnableCode] = state
|
||||||
}
|
}
|
||||||
|
|
||||||
return &State{
|
return &State{
|
||||||
|
@ -5828,7 +5828,14 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
|||||||
b.pauseOrResumeControlClientLocked()
|
b.pauseOrResumeControlClientLocked()
|
||||||
|
|
||||||
if nm != nil {
|
if nm != nil {
|
||||||
b.health.SetControlHealth(nm.DisplayMessages)
|
messages := make(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage)
|
||||||
|
for id, msg := range nm.DisplayMessages {
|
||||||
|
if msg.PrimaryAction != nil && !b.validPopBrowserURL(msg.PrimaryAction.URL) {
|
||||||
|
msg.PrimaryAction = nil
|
||||||
|
}
|
||||||
|
messages[id] = msg
|
||||||
|
}
|
||||||
|
b.health.SetControlHealth(messages)
|
||||||
} else {
|
} else {
|
||||||
b.health.SetControlHealth(nil)
|
b.health.SetControlHealth(nil)
|
||||||
}
|
}
|
||||||
|
@ -5339,3 +5339,68 @@ func TestSrcCapPacketFilter(t *testing.T) {
|
|||||||
t.Error("IsDrop() for node without cap = false, want true")
|
t.Error("IsDrop() for node without cap = false, want true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisplayMessages(t *testing.T) {
|
||||||
|
b := newTestLocalBackend(t)
|
||||||
|
|
||||||
|
// Pretend we're in a map poll so health updates get processed
|
||||||
|
ht := b.HealthTracker()
|
||||||
|
ht.SetIPNState("NeedsLogin", true)
|
||||||
|
ht.GotStreamedMapResponse()
|
||||||
|
|
||||||
|
b.setNetMapLocked(&netmap.NetworkMap{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"test-message": {
|
||||||
|
Title: "Testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
state := ht.CurrentState()
|
||||||
|
_, ok := state.Warnings["test-message"]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Error("no warning found with id 'test-message'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDisplayMessagesURLFilter tests that we filter out any URLs that are not
|
||||||
|
// valid as a pop browser URL (see [LocalBackend.validPopBrowserURL]).
|
||||||
|
func TestDisplayMessagesURLFilter(t *testing.T) {
|
||||||
|
b := newTestLocalBackend(t)
|
||||||
|
|
||||||
|
// Pretend we're in a map poll so health updates get processed
|
||||||
|
ht := b.HealthTracker()
|
||||||
|
ht.SetIPNState("NeedsLogin", true)
|
||||||
|
ht.GotStreamedMapResponse()
|
||||||
|
|
||||||
|
b.setNetMapLocked(&netmap.NetworkMap{
|
||||||
|
DisplayMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
||||||
|
"test-message": {
|
||||||
|
Title: "Testing",
|
||||||
|
Severity: tailcfg.SeverityHigh,
|
||||||
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
||||||
|
URL: "https://www.evil.com",
|
||||||
|
Label: "Phishing Link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
state := ht.CurrentState()
|
||||||
|
got, ok := state.Warnings["test-message"]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no warning found with id 'test-message'")
|
||||||
|
}
|
||||||
|
|
||||||
|
want := health.UnhealthyState{
|
||||||
|
WarnableCode: "test-message",
|
||||||
|
Title: "Testing",
|
||||||
|
Severity: health.SeverityHigh,
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("Unexpected message content (-want/+got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -161,7 +161,8 @@ type CapabilityVersion int
|
|||||||
// - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829)
|
// - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829)
|
||||||
// - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
|
// - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
|
||||||
// - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
|
// - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 116
|
// - 117: 2025-05-28: Client understands DisplayMessages (structured health messages), but not necessarily PrimaryAction.
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 117
|
||||||
|
|
||||||
// ID is an integer ID for a user, node, or login allocated by the
|
// ID is an integer ID for a user, node, or login allocated by the
|
||||||
// control plane.
|
// control plane.
|
||||||
@ -2030,11 +2031,29 @@ type MapResponse struct {
|
|||||||
// known problems). A non-zero length slice are the list of problems that
|
// known problems). A non-zero length slice are the list of problems that
|
||||||
// the control plane sees.
|
// the control plane sees.
|
||||||
//
|
//
|
||||||
|
// Either this will be set, or DisplayMessages will be set, but not both.
|
||||||
|
//
|
||||||
// Note that this package's type, due its use of a slice and omitempty, is
|
// Note that this package's type, due its use of a slice and omitempty, is
|
||||||
// unable to marshal a zero-length non-nil slice. The control server needs
|
// unable to marshal a zero-length non-nil slice. The control server needs
|
||||||
// to marshal this type using a separate type. See MapResponse docs.
|
// to marshal this type using a separate type. See MapResponse docs.
|
||||||
Health []string `json:",omitempty"`
|
Health []string `json:",omitempty"`
|
||||||
|
|
||||||
|
// DisplayMessages sets the health state of the node from the control
|
||||||
|
// plane's perspective.
|
||||||
|
//
|
||||||
|
// Either this will be set, or Health will be set, but not both.
|
||||||
|
//
|
||||||
|
// The map keys are IDs that uniquely identify the type of health issue. The
|
||||||
|
// map values are the messages. If the server sends down a map with entries,
|
||||||
|
// the client treats it as a patch: new entries are added, keys with a value
|
||||||
|
// of nil are deleted, existing entries with new values are updated. A nil
|
||||||
|
// map and an empty map both mean no change has occurred since the last
|
||||||
|
// update.
|
||||||
|
//
|
||||||
|
// As a special case, the map key "*" with a value of nil means to clear all
|
||||||
|
// prior display messages before processing the other map entries.
|
||||||
|
DisplayMessages map[DisplayMessageID]*DisplayMessage `json:",omitempty"`
|
||||||
|
|
||||||
// SSHPolicy, if non-nil, updates the SSH policy for how incoming
|
// SSHPolicy, if non-nil, updates the SSH policy for how incoming
|
||||||
// SSH connections should be handled.
|
// SSH connections should be handled.
|
||||||
SSHPolicy *SSHPolicy `json:",omitempty"`
|
SSHPolicy *SSHPolicy `json:",omitempty"`
|
||||||
@ -2079,24 +2098,53 @@ type MapResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DisplayMessage represents a health state of the node from the control plane's
|
// DisplayMessage represents a health state of the node from the control plane's
|
||||||
// perspective. It is deliberately similar to health.Warnable as both get
|
// perspective. It is deliberately similar to [health.Warnable] as both get
|
||||||
// converted into health.UnhealthyState to be sent to the GUI.
|
// converted into [health.UnhealthyState] to be sent to the GUI.
|
||||||
type DisplayMessage struct {
|
type DisplayMessage struct {
|
||||||
// Title is a string that the GUI uses as title for this message. The title
|
// Title is a string that the GUI uses as title for this message. The title
|
||||||
// should be short and fit in a single line.
|
// should be short and fit in a single line. It should not end in a period.
|
||||||
|
//
|
||||||
|
// Example: "Network may be blocking Tailscale".
|
||||||
|
//
|
||||||
|
// See the various instantiations of [health.Warnable] for more examples.
|
||||||
Title string
|
Title string
|
||||||
|
|
||||||
// Text is an extended string that the GUI will display to the user.
|
// Text is an extended string that the GUI will display to the user. This
|
||||||
|
// could be multiple sentences explaining the issue in more detail.
|
||||||
|
//
|
||||||
|
// Example: "macOS Screen Time seems to be blocking Tailscale. Try disabling
|
||||||
|
// Screen Time in System Settings > Screen Time > Content & Privacy > Access
|
||||||
|
// to Web Content."
|
||||||
|
//
|
||||||
|
// See the various instantiations of [health.Warnable] for more examples.
|
||||||
Text string
|
Text string
|
||||||
|
|
||||||
// Severity is the severity of the DisplayMessage, which the GUI can use to
|
// Severity is the severity of the DisplayMessage, which the GUI can use to
|
||||||
// determine how to display it. Maps to health.Severity.
|
// determine how to display it. Maps to [health.Severity].
|
||||||
Severity DisplayMessageSeverity
|
Severity DisplayMessageSeverity
|
||||||
|
|
||||||
// ImpactsConnectivity is whether the health problem will impact the user's
|
// ImpactsConnectivity is whether the health problem will impact the user's
|
||||||
// ability to connect to the Internet or other nodes on the tailnet, which
|
// ability to connect to the Internet or other nodes on the tailnet, which
|
||||||
// the GUI can use to determine how to display it.
|
// the GUI can use to determine how to display it.
|
||||||
ImpactsConnectivity bool `json:",omitempty"`
|
ImpactsConnectivity bool `json:",omitempty"`
|
||||||
|
|
||||||
|
// Primary action, if present, represents the action to allow the user to
|
||||||
|
// take when interacting with this message. For example, if the
|
||||||
|
// DisplayMessage is shown via a notification, the action label might be a
|
||||||
|
// button on that notification and clicking the button would open the URL.
|
||||||
|
PrimaryAction *DisplayMessageAction `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayMessageAction represents an action (URL and link) to be presented to
|
||||||
|
// the user associated with a [DisplayMessage].
|
||||||
|
type DisplayMessageAction struct {
|
||||||
|
// URL is the URL to navigate to when the user interacts with this action
|
||||||
|
URL string
|
||||||
|
|
||||||
|
// Label is the call to action for the UI to display on the UI element that
|
||||||
|
// will open the URL (such as a button or link). For example, "Sign in" or
|
||||||
|
// "Learn more".
|
||||||
|
Label string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayMessageID is a string that uniquely identifies the kind of health
|
// DisplayMessageID is a string that uniquely identifies the kind of health
|
||||||
|
@ -163,6 +163,7 @@ func mapResponseContainsNonPatchFields(res *tailcfg.MapResponse) bool {
|
|||||||
res.PacketFilters != nil ||
|
res.PacketFilters != nil ||
|
||||||
res.UserProfiles != nil ||
|
res.UserProfiles != nil ||
|
||||||
res.Health != nil ||
|
res.Health != nil ||
|
||||||
|
res.DisplayMessages != nil ||
|
||||||
res.SSHPolicy != nil ||
|
res.SSHPolicy != nil ||
|
||||||
res.TKAInfo != nil ||
|
res.TKAInfo != nil ||
|
||||||
res.DomainDataPlaneAuditLogID != "" ||
|
res.DomainDataPlaneAuditLogID != "" ||
|
||||||
|
Loading…
x
Reference in New Issue
Block a user