From 1961c6543bdaf149305460049ec1d05ec547e9bc Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Wed, 18 Feb 2026 10:29:14 +0000 Subject: [PATCH] tailcfg,tsconst: add structured error field to MapResponse Add an Error field to MapResponse to allow the control server to communicate errors to clients in a structured way. The error includes a machine-readable code and a human-readable message. This enables clients to programmatically handle specific error conditions. The immediate use case is returning an explicit error when a node is not found on the tailnet, so that a message can be sent on the ipn bus to inform the client. Updates #18830 Signed-off-by: chaosinthecrd --- cmd/vet/jsontags_allowlist | 1 + tailcfg/tailcfg.go | 18 ++++++++++++++++++ tsconst/tsconst.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/cmd/vet/jsontags_allowlist b/cmd/vet/jsontags_allowlist index b9f91d562..7233d42bc 100644 --- a/cmd/vet/jsontags_allowlist +++ b/cmd/vet/jsontags_allowlist @@ -114,6 +114,7 @@ OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.MapResponse.Node OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.MapResponse.PingRequest OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.MapResponse.SSHPolicy OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.MapResponse.TKAInfo +OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.MapResponse.Error OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.NetPortRange.Bits OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.Node.Online OmitEmptyShouldBeOmitZero tailscale.com/tailcfg.Node.SelfNodeV4MasqAddrForThisPeer diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b49791be6..9265cf6d5 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -22,6 +22,7 @@ import ( "time" "tailscale.com/feature/buildfeatures" + "tailscale.com/tsconst" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -2161,6 +2162,23 @@ type MapResponse struct { // Deprecated: use NodeAttrDefaultAutoUpdate instead. See // https://github.com/tailscale/tailscale/issues/11502. DeprecatedDefaultAutoUpdate opt.Bool `json:"DefaultAutoUpdate,omitempty"` + + // Error, if non-nil, contains detail about the error that is being + // returned from control. Clients should attempt to parse this from + // non-2xx responses with Content-Type "application/json". For plaintext + // responses, assume "internal-error". + Error *MapResponseError `json:"err,omitempty"` +} + +// MapResponseError is the error response body that is sent to the client +// in the event that there is an error. +type MapResponseError struct { + // Message is the human readable error message detailing the cause of + // the error. + Message string `json:"message"` + // Code is a unique string identifier for the particular error that has + // taken place. + Code tsconst.MapResponseErrorCode `json:"code"` } // DisplayMessage represents a health state of the node from the control plane's diff --git a/tsconst/tsconst.go b/tsconst/tsconst.go index 85f05e549..8b08c8c87 100644 --- a/tsconst/tsconst.go +++ b/tsconst/tsconst.go @@ -13,3 +13,34 @@ const WintunInterfaceDesc0_14 = "Wintun Userspace Tunnel" // TailnetLockNotTrustedMsg is the error message used by network lock // and sniffed (via substring) out of an error sent over the network. const TailnetLockNotTrustedMsg = "this node is not trusted by network lock" + +// MapResponseErrorCode is a unique string identifier for map response errors. +type MapResponseErrorCode string + +// ErrCodeNodeNotFound indicates that the requested node was not found +// in the control plane database. +const ErrCodeNodeNotFound MapResponseErrorCode = "node-not-found" + +// ErrCodeInvalidRequest indicates that the client sent a malformed +// or invalid request. +const ErrCodeInvalidRequest MapResponseErrorCode = "invalid-request" + +// ErrCodeInternalError indicates that an internal server error +// occurred while processing the request. +const ErrCodeInternalError MapResponseErrorCode = "internal-error" + +// ErrCodeMaintenanceMode indicates that the server is currently +// in maintenance mode and cannot process requests. +const ErrCodeMaintenanceMode MapResponseErrorCode = "maintenance-mode" + +// ErrCodeRequestSuperseded indicates that the request was replaced +// by a newer request from the same client before it could complete. +const ErrCodeRequestSuperseded MapResponseErrorCode = "request-superseded" + +// ErrCodeUserNotFound indicates that the requested user was not found +// in the control plane database. +const ErrCodeUserNotFound MapResponseErrorCode = "user-not-found" + +// ErrCodeUnauthorized indicates that the client is not authorized +// to perform the requested operation. +const ErrCodeUnauthorized MapResponseErrorCode = "unauthorized"