diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go index 0f6f1b783..4253b2471 100644 --- a/ssh/tailssh/tailssh.go +++ b/ssh/tailssh/tailssh.go @@ -422,6 +422,7 @@ func (srv *server) newConn() (*conn, error) { c := &conn{srv: srv} now := srv.now() c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5)) + fwdHandler := &ssh.ForwardedTCPHandler{} c.Server = &ssh.Server{ Version: "Tailscale", ServerConfigCallback: c.ServerConfig, @@ -430,8 +431,9 @@ func (srv *server) newConn() (*conn, error) { PublicKeyHandler: c.PublicKeyHandler, PasswordHandler: c.fakePasswordHandler, - Handler: c.handleSessionPostSSHAuth, - LocalPortForwardingCallback: c.mayForwardLocalPortTo, + Handler: c.handleSessionPostSSHAuth, + LocalPortForwardingCallback: c.mayForwardLocalPortTo, + ReversePortForwardingCallback: c.mayReversePortForwardTo, SubsystemHandlers: map[string]ssh.SubsystemHandler{ "sftp": c.handleSessionPostSSHAuth, }, @@ -441,7 +443,10 @@ func (srv *server) newConn() (*conn, error) { ChannelHandlers: map[string]ssh.ChannelHandler{ "direct-tcpip": ssh.DirectTCPIPHandler, }, - RequestHandlers: map[string]ssh.RequestHandler{}, + RequestHandlers: map[string]ssh.RequestHandler{ + "tcpip-forward": fwdHandler.HandleSSHRequest, + "cancel-tcpip-forward": fwdHandler.HandleSSHRequest, + }, } ss := c.Server for k, v := range ssh.DefaultRequestHandlers { @@ -463,6 +468,17 @@ func (srv *server) newConn() (*conn, error) { return c, nil } +// mayReversePortPortForwardTo reports whether the ctx should be allowed to port forward +// to the specified host and port. +// TODO(bradfitz/maisem): should we have more checks on host/port? +func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool { + if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding { + metricRemotePortForward.Add(1) + return true + } + return false +} + // mayForwardLocalPortTo reports whether the ctx should be allowed to port forward // to the specified host and port. // TODO(bradfitz/maisem): should we have more checks on host/port? @@ -1860,6 +1876,7 @@ var ( metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick") metricSFTP = clientmetric.NewCounter("ssh_sftp_requests") metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_requests") + metricRemotePortForward = clientmetric.NewCounter("ssh_remote_port_forward_requests") ) // userVisibleError is a wrapper around an error that implements diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 7787ba407..4c6f9a39f 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -99,7 +99,8 @@ type CapabilityVersion int // - 60: 2023-04-06: Client understands IsWireGuardOnly // - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction // - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events -const CurrentCapabilityVersion CapabilityVersion = 62 +// - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding. +const CurrentCapabilityVersion CapabilityVersion = 63 type StableID string @@ -2048,6 +2049,10 @@ type SSHAction struct { // to use local port forwarding if requested. AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"` + // AllowRemotePortForwarding, if true, allows accepted connections + // to use remote port forwarding if requested. + AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"` + // Recorders defines the destinations of the SSH session recorders. // The recording will be uploaded to http://addr:port/record. Recorders []netip.AddrPort `json:"recorders,omitempty"` diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index a678cb47b..9d72124b4 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -408,15 +408,16 @@ func (src *SSHAction) Clone() *SSHAction { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _SSHActionCloneNeedsRegeneration = SSHAction(struct { - Message string - Reject bool - Accept bool - SessionDuration time.Duration - AllowAgentForwarding bool - HoldAndDelegate string - AllowLocalPortForwarding bool - Recorders []netip.AddrPort - OnRecordingFailure *SSHRecorderFailureAction + Message string + Reject bool + Accept bool + SessionDuration time.Duration + AllowAgentForwarding bool + HoldAndDelegate string + AllowLocalPortForwarding bool + AllowRemotePortForwarding 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 b4ff3b738..9c195da1c 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -940,6 +940,7 @@ func (v SSHActionView) SessionDuration() time.Duration { return v.ж.Ses func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding } func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } +func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding } func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) } func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction { if v.ж.OnRecordingFailure == nil { @@ -951,15 +952,16 @@ func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _SSHActionViewNeedsRegeneration = SSHAction(struct { - Message string - Reject bool - Accept bool - SessionDuration time.Duration - AllowAgentForwarding bool - HoldAndDelegate string - AllowLocalPortForwarding bool - Recorders []netip.AddrPort - OnRecordingFailure *SSHRecorderFailureAction + Message string + Reject bool + Accept bool + SessionDuration time.Duration + AllowAgentForwarding bool + HoldAndDelegate string + AllowLocalPortForwarding bool + AllowRemotePortForwarding bool + Recorders []netip.AddrPort + OnRecordingFailure *SSHRecorderFailureAction }{}) // View returns a readonly view of SSHPrincipal.