From f66ddb544cfc568125b1afedd1da97c9ecc61b6b Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Wed, 19 Apr 2023 21:10:55 -0700 Subject: [PATCH] tailcfg: add SSHRecorderFailureAction and SSHRecordingFailureNotifyRequest This allows control to specify how to handle situations where the recorder isn't available. Updates tailscale/corp#9967 Signed-off-by: Maisem Ali --- tailcfg/tailcfg.go | 56 +++++++++++++++++++++++++++++++++++++++- tailcfg/tailcfg_clone.go | 5 ++++ tailcfg/tailcfg_view.go | 8 ++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 620151f11..95057d490 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -2048,7 +2048,61 @@ type SSHAction struct { // Recorders defines the destinations of the SSH session recorders. // The recording will be uploaded to http://addr:port/record. - Recorders []netip.AddrPort `json:"recorders"` + Recorders []netip.AddrPort `json:"recorders,omitempty"` + + // OnRecorderFailure is the action to take if recording fails. + // If nil, the default action is to fail open. + OnRecordingFailure *SSHRecorderFailureAction `json:"onRecordingFailure,omitempty"` +} + +// SSHRecorderFailureAction is the action to take if recording fails. +type SSHRecorderFailureAction struct { + // RejectSessionWithMessage, if not empty, specifies that the session should + // be rejected if the recording fails to start. + // The message will be shown to the user before the session is rejected. + RejectSessionWithMessage string `json:",omitempty"` + + // TerminateSessionWithMessage, if not empty, specifies that the session + // should be terminated if the recording fails after it has started. The + // message will be shown to the user before the session is terminated. + TerminateSessionWithMessage string `json:",omitempty"` + + // NotifyURL, if non-empty, specifies a HTTP POST URL to notify when the + // recording fails. The payload is the JSON encoded + // SSHRecordingFailureNotifyRequest struct. The host field in the URL is + // ignored, and it will be sent to control over the Noise transport. + NotifyURL string `json:",omitempty"` +} + +// SSHRecordingFailureNotifyRequest is the JSON payload sent to the NotifyURL +// when a recording fails. +type SSHRecordingFailureNotifyRequest struct { + // CapVersion is the client's current CapabilityVersion. + CapVersion CapabilityVersion + + // NodeKey is the client's current node key. + NodeKey key.NodePublic + + // SrcNode is the ID of the node that initiated the SSH session. + SrcNode NodeID + + // SSHUser is the user that was presented to the SSH server. + SSHUser string + + // LocalUser is the user that was resolved from the SSHUser for the local machine. + LocalUser string + + // Attempts is the list of recorders that were attempted, in order. + Attempts []SSHRecordingAttempt +} + +// SSHRecordingAttempt is a single attempt to start a recording. +type SSHRecordingAttempt struct { + // Recorder is the address of the recorder that was attempted. + Recorder netip.AddrPort + + // FailureMessage is the error message of the failed attempt. + FailureMessage string } // OverTLSPublicKeyResponse is the JSON response to /key?v= diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 3f54d96d7..a678cb47b 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -399,6 +399,10 @@ func (src *SSHAction) Clone() *SSHAction { dst := new(SSHAction) *dst = *src dst.Recorders = append(src.Recorders[:0:0], src.Recorders...) + if dst.OnRecordingFailure != nil { + dst.OnRecordingFailure = new(SSHRecorderFailureAction) + *dst.OnRecordingFailure = *src.OnRecordingFailure + } return dst } @@ -412,6 +416,7 @@ var _SSHActionCloneNeedsRegeneration = SSHAction(struct { HoldAndDelegate string AllowLocalPortForwarding bool Recorders []netip.AddrPort + OnRecordingFailure *SSHRecorderFailureAction }{}) // Clone makes a deep copy of SSHPrincipal. diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 3771881f6..b4ff3b738 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -941,6 +941,13 @@ func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.All func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) } +func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction { + if v.ж.OnRecordingFailure == nil { + return nil + } + x := *v.ж.OnRecordingFailure + return &x +} // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _SSHActionViewNeedsRegeneration = SSHAction(struct { @@ -952,6 +959,7 @@ var _SSHActionViewNeedsRegeneration = SSHAction(struct { HoldAndDelegate string AllowLocalPortForwarding bool Recorders []netip.AddrPort + OnRecordingFailure *SSHRecorderFailureAction }{}) // View returns a readonly view of SSHPrincipal.