ssh/tailssh: add support for remote/reverse port forwarding

This basically allows running services on the SSH client and reaching
them from the SSH server during the session.

Updates #6575

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/8319/head
Maisem Ali 1 year ago committed by Maisem Ali
parent 62130e6b68
commit 2e0aa151c9

@ -422,6 +422,7 @@ func (srv *server) newConn() (*conn, error) {
c := &conn{srv: srv} c := &conn{srv: srv}
now := srv.now() now := srv.now()
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5)) c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
fwdHandler := &ssh.ForwardedTCPHandler{}
c.Server = &ssh.Server{ c.Server = &ssh.Server{
Version: "Tailscale", Version: "Tailscale",
ServerConfigCallback: c.ServerConfig, ServerConfigCallback: c.ServerConfig,
@ -432,6 +433,7 @@ func (srv *server) newConn() (*conn, error) {
Handler: c.handleSessionPostSSHAuth, Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo, LocalPortForwardingCallback: c.mayForwardLocalPortTo,
ReversePortForwardingCallback: c.mayReversePortForwardTo,
SubsystemHandlers: map[string]ssh.SubsystemHandler{ SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": c.handleSessionPostSSHAuth, "sftp": c.handleSessionPostSSHAuth,
}, },
@ -441,7 +443,10 @@ func (srv *server) newConn() (*conn, error) {
ChannelHandlers: map[string]ssh.ChannelHandler{ ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler, "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 ss := c.Server
for k, v := range ssh.DefaultRequestHandlers { for k, v := range ssh.DefaultRequestHandlers {
@ -463,6 +468,17 @@ func (srv *server) newConn() (*conn, error) {
return c, nil 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 // mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
// to the specified host and port. // to the specified host and port.
// TODO(bradfitz/maisem): should we have more checks on host/port? // TODO(bradfitz/maisem): should we have more checks on host/port?
@ -1860,6 +1876,7 @@ var (
metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick") metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick")
metricSFTP = clientmetric.NewCounter("ssh_sftp_requests") metricSFTP = clientmetric.NewCounter("ssh_sftp_requests")
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_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 // userVisibleError is a wrapper around an error that implements

@ -99,7 +99,8 @@ type CapabilityVersion int
// - 60: 2023-04-06: Client understands IsWireGuardOnly // - 60: 2023-04-06: Client understands IsWireGuardOnly
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction // - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events // - 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 type StableID string
@ -2048,6 +2049,10 @@ type SSHAction struct {
// to use local port forwarding if requested. // to use local port forwarding if requested.
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"` 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. // Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record. // The recording will be uploaded to http://addr:port/record.
Recorders []netip.AddrPort `json:"recorders,omitempty"` Recorders []netip.AddrPort `json:"recorders,omitempty"`

@ -415,6 +415,7 @@ var _SSHActionCloneNeedsRegeneration = SSHAction(struct {
AllowAgentForwarding bool AllowAgentForwarding bool
HoldAndDelegate string HoldAndDelegate string
AllowLocalPortForwarding bool AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction OnRecordingFailure *SSHRecorderFailureAction
}{}) }{})

@ -940,6 +940,7 @@ func (v SSHActionView) SessionDuration() time.Duration { return v.ж.Ses
func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding } func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding }
func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate }
func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } 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) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) }
func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction { func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
if v.ж.OnRecordingFailure == nil { if v.ж.OnRecordingFailure == nil {
@ -958,6 +959,7 @@ var _SSHActionViewNeedsRegeneration = SSHAction(struct {
AllowAgentForwarding bool AllowAgentForwarding bool
HoldAndDelegate string HoldAndDelegate string
AllowLocalPortForwarding bool AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction OnRecordingFailure *SSHRecorderFailureAction
}{}) }{})

Loading…
Cancel
Save