ssh: replace tempfork with tailscale/gliderssh
Brings in a newer version of Gliderlabs SSH with added socket forwarding support. Fixes #12409 Fixes #5295 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
committed by
Kristoffer Dalby
parent
82fa218c4a
commit
dd3b613787
+72
-70
@@ -30,7 +30,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
gliderssh "github.com/tailscale/gliderssh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
@@ -38,7 +39,6 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -54,10 +54,10 @@ var (
|
||||
sshDisableForwarding = envknob.RegisterBool("TS_SSH_DISABLE_FORWARDING")
|
||||
sshDisablePTY = envknob.RegisterBool("TS_SSH_DISABLE_PTY")
|
||||
|
||||
// errTerminal is an empty gossh.PartialSuccessError (with no 'Next'
|
||||
// errTerminal is an empty ssh.PartialSuccessError (with no 'Next'
|
||||
// authentication methods that may proceed), which results in the SSH
|
||||
// server immediately disconnecting the client.
|
||||
errTerminal = &gossh.PartialSuccessError{}
|
||||
errTerminal = &ssh.PartialSuccessError{}
|
||||
|
||||
// hookSSHLoginSuccess is called after successful SSH authentication.
|
||||
// It is set by platform-specific code (e.g., auditd_linux.go).
|
||||
@@ -204,7 +204,7 @@ func (srv *server) OnPolicyChange() {
|
||||
}
|
||||
|
||||
// conn represents a single SSH connection and its associated
|
||||
// ssh.Server.
|
||||
// gliderssh.Server.
|
||||
//
|
||||
// During the lifecycle of a connection, the following are called in order:
|
||||
// Setup and discover server info
|
||||
@@ -220,9 +220,9 @@ func (srv *server) OnPolicyChange() {
|
||||
// channels concurrently. At which point any of the following can be called
|
||||
// in any order.
|
||||
// - c.handleSessionPostSSHAuth
|
||||
// - c.mayForwardLocalPortTo followed by ssh.DirectTCPIPHandler
|
||||
// - c.mayForwardLocalPortTo followed by gliderssh.DirectTCPIPHandler
|
||||
type conn struct {
|
||||
*ssh.Server
|
||||
*gliderssh.Server
|
||||
srv *server
|
||||
|
||||
insecureSkipTailscaleAuth bool // used by tests.
|
||||
@@ -234,9 +234,9 @@ type conn struct {
|
||||
idH string
|
||||
connID string // ID that's shared with control
|
||||
|
||||
// spac is a [gossh.ServerPreAuthConn] used for sending auth banners.
|
||||
// spac is a [ssh.ServerPreAuthConn] used for sending auth banners.
|
||||
// Banners cannot be sent after auth completes.
|
||||
spac gossh.ServerPreAuthConn
|
||||
spac ssh.ServerPreAuthConn
|
||||
|
||||
// The following fields are set during clientAuth and are used for policy
|
||||
// evaluation and session management. They are immutable after clientAuth
|
||||
@@ -280,7 +280,7 @@ func (c *conn) vlogf(format string, args ...any) {
|
||||
|
||||
// errDenied is returned by auth callbacks when a connection is denied by the
|
||||
// policy. It writes the message to an auth banner and then returns an empty
|
||||
// gossh.PartialSuccessError in order to stop processing authentication
|
||||
// ssh.PartialSuccessError in order to stop processing authentication
|
||||
// attempts and immediately disconnect the client.
|
||||
func (c *conn) errDenied(message string) error {
|
||||
if message == "" {
|
||||
@@ -293,7 +293,7 @@ func (c *conn) errDenied(message string) error {
|
||||
}
|
||||
|
||||
// errBanner writes the given message to an auth banner and then returns an
|
||||
// empty gossh.PartialSuccessError in order to stop processing authentication
|
||||
// empty ssh.PartialSuccessError in order to stop processing authentication
|
||||
// attempts and immediately disconnect the client. The contents of err is not
|
||||
// leaked in the auth banner, but it is logged to the server's log.
|
||||
func (c *conn) errBanner(message string, err error) error {
|
||||
@@ -308,7 +308,7 @@ func (c *conn) errBanner(message string, err error) error {
|
||||
|
||||
// errUnexpected is returned by auth callbacks that encounter an unexpected
|
||||
// error, such as being unable to send an auth banner. It sends an empty
|
||||
// gossh.PartialSuccessError to tell gossh.Server to stop processing
|
||||
// ssh.PartialSuccessError to tell ssh.Server to stop processing
|
||||
// authentication attempts and instead disconnect immediately.
|
||||
func (c *conn) errUnexpected(err error) error {
|
||||
c.logf("terminal error: %s", err)
|
||||
@@ -319,11 +319,11 @@ func (c *conn) errUnexpected(err error) error {
|
||||
//
|
||||
// If policy evaluation fails, it returns an error.
|
||||
// If access is denied, it returns an error. This must always be an empty
|
||||
// gossh.PartialSuccessError to prevent further authentication methods from
|
||||
// ssh.PartialSuccessError to prevent further authentication methods from
|
||||
// being tried.
|
||||
func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retErr error) {
|
||||
func (c *conn) clientAuth(cm ssh.ConnMetadata) (perms *ssh.Permissions, retErr error) {
|
||||
defer func() {
|
||||
if pse, ok := retErr.(*gossh.PartialSuccessError); ok {
|
||||
if pse, ok := retErr.(*ssh.PartialSuccessError); ok {
|
||||
if pse.Next.GSSAPIWithMICConfig != nil ||
|
||||
pse.Next.KeyboardInteractiveCallback != nil ||
|
||||
pse.Next.PasswordCallback != nil ||
|
||||
@@ -336,7 +336,7 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
||||
}()
|
||||
|
||||
if c.insecureSkipTailscaleAuth {
|
||||
return &gossh.Permissions{}, nil
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
|
||||
if err := c.setInfo(cm); err != nil {
|
||||
@@ -384,7 +384,7 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
||||
}
|
||||
c.finalAction = action
|
||||
c.authCompleted.Store(true)
|
||||
return &gossh.Permissions{}, nil
|
||||
return &ssh.Permissions{}, nil
|
||||
case action.Reject:
|
||||
metricTerminalReject.Add(1)
|
||||
c.finalAction = action
|
||||
@@ -417,14 +417,14 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
||||
}
|
||||
}
|
||||
|
||||
// ServerConfig implements ssh.ServerConfigCallback.
|
||||
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
return &gossh.ServerConfig{
|
||||
PreAuthConnCallback: func(spac gossh.ServerPreAuthConn) {
|
||||
// ServerConfig implements gliderssh.ServerConfigCallback.
|
||||
func (c *conn) ServerConfig(ctx gliderssh.Context) *ssh.ServerConfig {
|
||||
return &ssh.ServerConfig{
|
||||
PreAuthConnCallback: func(spac ssh.ServerPreAuthConn) {
|
||||
c.spac = spac
|
||||
},
|
||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
NoClientAuthCallback: func(cm ssh.ConnMetadata) (*ssh.Permissions, error) {
|
||||
// First perform client authentication, which can potentially
|
||||
// involve multiple steps (for example prompting user to log in to
|
||||
// Tailscale admin panel to confirm identity).
|
||||
@@ -438,10 +438,10 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
// specify a username ending in "+password" to force password auth.
|
||||
// The actual value of the password doesn't matter.
|
||||
if strings.HasSuffix(cm.User(), forcePasswordSuffix) {
|
||||
return nil, &gossh.PartialSuccessError{
|
||||
Next: gossh.ServerAuthCallbacks{
|
||||
PasswordCallback: func(_ gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
||||
return &gossh.Permissions{}, nil
|
||||
return nil, &ssh.PartialSuccessError{
|
||||
Next: ssh.ServerAuthCallbacks{
|
||||
PasswordCallback: func(_ ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
return &ssh.Permissions{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -449,14 +449,14 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
|
||||
return perms, nil
|
||||
},
|
||||
PasswordCallback: func(cm gossh.ConnMetadata, pword []byte) (*gossh.Permissions, error) {
|
||||
PasswordCallback: func(cm ssh.ConnMetadata, pword []byte) (*ssh.Permissions, error) {
|
||||
// Some clients don't request 'none' authentication. Instead, they
|
||||
// immediately supply a password. We humor them by accepting the
|
||||
// password, but authenticate as usual, ignoring the actual value of
|
||||
// the password.
|
||||
return c.clientAuth(cm)
|
||||
},
|
||||
PublicKeyCallback: func(cm gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
PublicKeyCallback: func(cm ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
// Some clients don't request 'none' authentication. Instead, they
|
||||
// immediately supply a public key. We humor them by accepting the
|
||||
// key, but authenticate as usual, ignoring the actual content of
|
||||
@@ -479,9 +479,9 @@ 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{}
|
||||
streamLocalFwdHandler := &ssh.ForwardedUnixHandler{}
|
||||
c.Server = &ssh.Server{
|
||||
fwdHandler := &gliderssh.ForwardedTCPHandler{}
|
||||
streamLocalFwdHandler := &gliderssh.ForwardedUnixHandler{}
|
||||
c.Server = &gliderssh.Server{
|
||||
Version: "Tailscale",
|
||||
ServerConfigCallback: c.ServerConfig,
|
||||
|
||||
@@ -492,14 +492,14 @@ func (srv *server) newConn() (*conn, error) {
|
||||
LocalUnixForwardingCallback: c.mayForwardLocalUnixTo,
|
||||
ReverseUnixForwardingCallback: c.mayReverseUnixForwardTo,
|
||||
|
||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
||||
SubsystemHandlers: map[string]gliderssh.SubsystemHandler{
|
||||
"sftp": c.handleSessionPostSSHAuth,
|
||||
},
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
"direct-streamlocal@openssh.com": ssh.DirectStreamLocalHandler,
|
||||
ChannelHandlers: map[string]gliderssh.ChannelHandler{
|
||||
"direct-tcpip": gliderssh.DirectTCPIPHandler,
|
||||
"direct-streamlocal@openssh.com": gliderssh.DirectStreamLocalHandler,
|
||||
},
|
||||
RequestHandlers: map[string]ssh.RequestHandler{
|
||||
RequestHandlers: map[string]gliderssh.RequestHandler{
|
||||
"tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||
"streamlocal-forward@openssh.com": streamLocalFwdHandler.HandleSSHRequest,
|
||||
@@ -507,9 +507,9 @@ func (srv *server) newConn() (*conn, error) {
|
||||
},
|
||||
}
|
||||
ss := c.Server
|
||||
maps.Copy(ss.RequestHandlers, ssh.DefaultRequestHandlers)
|
||||
maps.Copy(ss.ChannelHandlers, ssh.DefaultChannelHandlers)
|
||||
maps.Copy(ss.SubsystemHandlers, ssh.DefaultSubsystemHandlers)
|
||||
maps.Copy(ss.RequestHandlers, gliderssh.DefaultRequestHandlers)
|
||||
maps.Copy(ss.ChannelHandlers, gliderssh.DefaultChannelHandlers)
|
||||
maps.Copy(ss.SubsystemHandlers, gliderssh.DefaultSubsystemHandlers)
|
||||
keys, err := getHostKeys(srv.lb.TailscaleVarRoot(), srv.logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -523,7 +523,7 @@ func (srv *server) newConn() (*conn, error) {
|
||||
// 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 {
|
||||
func (c *conn) mayReversePortForwardTo(ctx gliderssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
if sshDisableForwarding() {
|
||||
return false
|
||||
}
|
||||
@@ -537,7 +537,7 @@ func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string,
|
||||
// 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?
|
||||
func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
func (c *conn) mayForwardLocalPortTo(ctx gliderssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
if sshDisableForwarding() {
|
||||
return false
|
||||
}
|
||||
@@ -548,42 +548,44 @@ func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, de
|
||||
return false
|
||||
}
|
||||
|
||||
// mayForwardLocalUnixTo reports whether the ctx should be allowed to forward
|
||||
// to the specified Unix domain socket path. This is the server-side handler for
|
||||
// direct-streamlocal@openssh.com (SSH -L with Unix sockets).
|
||||
func (c *conn) mayForwardLocalUnixTo(ctx ssh.Context, socketPath string) (net.Conn, error) {
|
||||
// mayForwardLocalUnixTo is the server-side handler for
|
||||
// direct-streamlocal@openssh.com (SSH -L with Unix sockets). It returns a
|
||||
// connection to the specified Unix domain socket path if forwarding is
|
||||
// permitted, or an error if not.
|
||||
func (c *conn) mayForwardLocalUnixTo(ctx gliderssh.Context, socketPath string) (net.Conn, error) {
|
||||
if sshDisableForwarding() {
|
||||
return nil, ssh.ErrRejected
|
||||
return nil, gliderssh.ErrRejected
|
||||
}
|
||||
if c.finalAction != nil && c.finalAction.AllowLocalPortForwarding {
|
||||
metricLocalPortForward.Add(1)
|
||||
cb := ssh.NewLocalUnixForwardingCallback(c.unixForwardingOptions())
|
||||
cb := gliderssh.NewLocalUnixForwardingCallback(c.unixForwardingOptions())
|
||||
return cb(ctx, socketPath)
|
||||
}
|
||||
return nil, ssh.ErrRejected
|
||||
return nil, gliderssh.ErrRejected
|
||||
}
|
||||
|
||||
// mayReverseUnixForwardTo reports whether the ctx should be allowed to create
|
||||
// a reverse Unix domain socket forward. This is the server-side handler for
|
||||
// streamlocal-forward@openssh.com (SSH -R with Unix sockets).
|
||||
func (c *conn) mayReverseUnixForwardTo(ctx ssh.Context, socketPath string) (net.Listener, error) {
|
||||
// mayReverseUnixForwardTo is the server-side handler for
|
||||
// streamlocal-forward@openssh.com (SSH -R with Unix sockets). It returns a
|
||||
// listener for the specified Unix domain socket path if reverse forwarding is
|
||||
// permitted, or an error if not.
|
||||
func (c *conn) mayReverseUnixForwardTo(ctx gliderssh.Context, socketPath string) (net.Listener, error) {
|
||||
if sshDisableForwarding() {
|
||||
return nil, ssh.ErrRejected
|
||||
return nil, gliderssh.ErrRejected
|
||||
}
|
||||
if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding {
|
||||
metricRemotePortForward.Add(1)
|
||||
cb := ssh.NewReverseUnixForwardingCallback(c.unixForwardingOptions())
|
||||
cb := gliderssh.NewReverseUnixForwardingCallback(c.unixForwardingOptions())
|
||||
return cb(ctx, socketPath)
|
||||
}
|
||||
return nil, ssh.ErrRejected
|
||||
return nil, gliderssh.ErrRejected
|
||||
}
|
||||
|
||||
// unixForwardingOptions returns the Unix forwarding options scoped to the
|
||||
// authenticated local user. Socket paths are restricted to the user's home
|
||||
// directory, /tmp, and /run/user/<uid>.
|
||||
func (c *conn) unixForwardingOptions() ssh.UnixForwardingOptions {
|
||||
return ssh.UnixForwardingOptions{
|
||||
AllowedDirectories: ssh.UserSocketDirectories(c.localUser.HomeDir, c.localUser.Uid),
|
||||
func (c *conn) unixForwardingOptions() gliderssh.UnixForwardingOptions {
|
||||
return gliderssh.UnixForwardingOptions{
|
||||
AllowedDirectories: gliderssh.UserSocketDirectories(c.localUser.HomeDir, c.localUser.Uid),
|
||||
BindUnlink: true,
|
||||
}
|
||||
}
|
||||
@@ -635,7 +637,7 @@ func toIPPort(a net.Addr) (ipp netip.AddrPort) {
|
||||
|
||||
// connInfo populates the sshConnInfo from the provided arguments,
|
||||
// validating only that they represent a known Tailscale identity.
|
||||
func (c *conn) setInfo(cm gossh.ConnMetadata) error {
|
||||
func (c *conn) setInfo(cm ssh.ConnMetadata) error {
|
||||
if c.info != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -685,7 +687,7 @@ func (c *conn) evaluatePolicy() (_ *tailcfg.SSHAction, localUser string, acceptE
|
||||
// handleSessionPostSSHAuth runs an SSH session after the SSH-level authentication,
|
||||
// but not necessarily before all the Tailscale-level extra verification has
|
||||
// completed. It also handles SFTP requests.
|
||||
func (c *conn) handleSessionPostSSHAuth(s ssh.Session) {
|
||||
func (c *conn) handleSessionPostSSHAuth(s gliderssh.Session) {
|
||||
// Do this check after auth, but before starting the session.
|
||||
switch s.Subsystem() {
|
||||
case "sftp":
|
||||
@@ -734,7 +736,7 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string {
|
||||
|
||||
// sshSession is an accepted Tailscale SSH session.
|
||||
type sshSession struct {
|
||||
ssh.Session
|
||||
gliderssh.Session
|
||||
sharedID string // ID that's shared with control
|
||||
logf logger.Logf
|
||||
|
||||
@@ -747,8 +749,8 @@ type sshSession struct {
|
||||
cmd *exec.Cmd
|
||||
wrStdin io.WriteCloser
|
||||
rdStdout io.ReadCloser
|
||||
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
|
||||
ptyReq *ssh.Pty // non-nil for pty sessions
|
||||
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
|
||||
ptyReq *gliderssh.Pty // non-nil for pty sessions
|
||||
|
||||
// childPipes is a list of pipes that need to be closed when the process exits.
|
||||
// For pty sessions, this is the tty fd.
|
||||
@@ -772,7 +774,7 @@ func (ss *sshSession) vlogf(format string, args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) newSSHSession(s ssh.Session) *sshSession {
|
||||
func (c *conn) newSSHSession(s gliderssh.Session) *sshSession {
|
||||
sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format("20060102T150405"), randBytes(5))
|
||||
c.logf("starting session: %v", sharedID)
|
||||
ctx, cancel := context.WithCancelCause(s.Context())
|
||||
@@ -907,10 +909,10 @@ func (c *conn) detachSession(ss *sshSession) {
|
||||
var errSessionDone = errors.New("session is done")
|
||||
|
||||
// handleSSHAgentForwarding starts a Unix socket listener and in the background
|
||||
// forwards agent connections between the listener and the ssh.Session.
|
||||
// forwards agent connections between the listener and the gliderssh.Session.
|
||||
// On success, it assigns ss.agentListener.
|
||||
func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) error {
|
||||
if !ssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
||||
func (ss *sshSession) handleSSHAgentForwarding(s gliderssh.Session, lu *userMeta) error {
|
||||
if !gliderssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
||||
return nil
|
||||
}
|
||||
if sshDisableForwarding() {
|
||||
@@ -920,7 +922,7 @@ func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) erro
|
||||
return nil
|
||||
}
|
||||
ss.logf("ssh: agent forwarding requested")
|
||||
ln, err := ssh.NewAgentListener()
|
||||
ln, err := gliderssh.NewAgentListener()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -952,7 +954,7 @@ func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) erro
|
||||
return err
|
||||
}
|
||||
|
||||
go ssh.ForwardAgentConnections(ln, s)
|
||||
go gliderssh.ForwardAgentConnections(ln, s)
|
||||
ss.agentListener = ln
|
||||
return nil
|
||||
}
|
||||
@@ -1325,7 +1327,7 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
var w ssh.Window
|
||||
var w gliderssh.Window
|
||||
if ptyReq, _, isPtyReq := ss.Pty(); isPtyReq {
|
||||
w = ptyReq.Window
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user