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 11 months ago committed by Maisem Ali
parent 62130e6b68
commit 2e0aa151c9

@ -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

@ -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"`

@ -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.

@ -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.

Loading…
Cancel
Save