@ -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 go ssh.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 = & go ssh. 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.
// glider ssh.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 glider ssh.DirectTCPIPHandler
type conn struct {
* ssh . Server
* glider ssh. 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 [go ssh.ServerPreAuthConn] used for sending auth banners.
// spac is a [ssh.ServerPreAuthConn] used for sending auth banners.
// Banners cannot be sent after auth completes.
spac go ssh. 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
// go ssh.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 go ssh.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
// go ssh.PartialSuccessError to tell go ssh.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
// go ssh.PartialSuccessError to prevent further authentication methods from
// ssh.PartialSuccessError to prevent further authentication methods from
// being tried.
func ( c * conn ) clientAuth ( cm go ssh. ConnMetadata ) ( perms * go ssh. Permissions , retErr error ) {
func ( c * conn ) clientAuth ( cm ssh . ConnMetadata ) ( perms * ssh . Permissions , retErr error ) {
defer func ( ) {
if pse , ok := retErr . ( * go ssh. 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 & go ssh. 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 & go ssh. 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 ) * go ssh. ServerConfig {
return & go ssh. ServerConfig {
PreAuthConnCallback : func ( spac go ssh. ServerPreAuthConn ) {
// ServerConfig implements glider ssh.ServerConfigCallback.
func ( c * conn ) ServerConfig ( ctx glider ssh. 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 go ssh. ConnMetadata ) ( * go ssh. 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 , & go ssh. PartialSuccessError {
Next : go ssh. ServerAuthCallbacks {
PasswordCallback : func ( _ go ssh. ConnMetadata , password [ ] byte ) ( * go ssh. Permissions , error ) {
return & go ssh. 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 go ssh. ConnMetadata , pword [ ] byte ) ( * go ssh. 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 go ssh. ConnMetadata , key go ssh. PublicKey ) ( * go ssh. 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 := & glider ssh. ForwardedTCPHandler { }
streamLocalFwdHandler := & glider ssh. ForwardedUnixHandler { }
c . Server = & glider ssh. 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 ] glider ssh. SubsystemHandler {
"sftp" : c . handleSessionPostSSHAuth ,
} ,
ChannelHandlers : map [ string ] ssh . ChannelHandler {
"direct-tcpip" : ssh . DirectTCPIPHandler ,
"direct-streamlocal@openssh.com" : ssh . DirectStreamLocalHandler ,
ChannelHandlers : map [ string ] glider ssh. ChannelHandler {
"direct-tcpip" : glider ssh. DirectTCPIPHandler ,
"direct-streamlocal@openssh.com" : glider ssh. DirectStreamLocalHandler ,
} ,
RequestHandlers : map [ string ] ssh . RequestHandler {
RequestHandlers : map [ string ] glider ssh. 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 , glider ssh. DefaultRequestHandlers )
maps . Copy ( ss . ChannelHandlers , glider ssh. DefaultChannelHandlers )
maps . Copy ( ss . SubsystemHandlers , glider ssh. 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 glider ssh. 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 glider ssh. 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 , glider ssh. ErrRejected
}
if c . finalAction != nil && c . finalAction . AllowLocalPortForwarding {
metricLocalPortForward . Add ( 1 )
cb := ssh . NewLocalUnixForwardingCallback ( c . unixForwardingOptions ( ) )
cb := glider ssh. NewLocalUnixForwardingCallback ( c . unixForwardingOptions ( ) )
return cb ( ctx , socketPath )
}
return nil , ssh . ErrRejected
return nil , glider ssh. 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 , glider ssh. ErrRejected
}
if c . finalAction != nil && c . finalAction . AllowRemotePortForwarding {
metricRemotePortForward . Add ( 1 )
cb := ssh . NewReverseUnixForwardingCallback ( c . unixForwardingOptions ( ) )
cb := glider ssh. NewReverseUnixForwardingCallback ( c . unixForwardingOptions ( ) )
return cb ( ctx , socketPath )
}
return nil , ssh . ErrRejected
return nil , glider ssh. 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 ( ) glider ssh. UnixForwardingOptions {
return glider ssh. UnixForwardingOptions {
AllowedDirectories : glider ssh. 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 go ssh. 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 glider ssh. 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
glider ssh. 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 * glider ssh. 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 glider ssh. 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 glider ssh.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 glider ssh. Session , lu * userMeta ) error {
if ! glider ssh. 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 := glider ssh. 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 glider ssh. 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 glider ssh. Window
if ptyReq , _ , isPtyReq := ss . Pty ( ) ; isPtyReq {
w = ptyReq . Window
}