|
|
@@ -48,6 +48,13 @@ var (
|
|
|
sshVerboseLogging = envknob.RegisterBool("TS_DEBUG_SSH_VLOG")
|
|
|
)
|
|
|
|
|
|
+const (
|
|
|
+ // forcePasswordSuffix is the suffix at the end of a username that forces
|
|
|
+ // Tailscale SSH into password authentication mode to work around buggy SSH
|
|
|
+ // clients that get confused by successful replies to auth type "none".
|
|
|
+ forcePasswordSuffix = "+password"
|
|
|
+)
|
|
|
+
|
|
|
// ipnLocalBackend is the subset of ipnlocal.LocalBackend that we use.
|
|
|
// It is used for testing.
|
|
|
type ipnLocalBackend interface {
|
|
|
@@ -197,7 +204,10 @@ type conn struct {
|
|
|
idH string
|
|
|
connID string // ID that's shared with control
|
|
|
|
|
|
- noPubKeyPolicyAuthError error // set by BannerCallback
|
|
|
+ // anyPasswordIsOkay is whether the client is authorized but has requested
|
|
|
+ // password-based auth to work around their buggy SSH client. When set, we
|
|
|
+ // accept any password in the PasswordHandler.
|
|
|
+ anyPasswordIsOkay bool // set by NoClientAuthCallback
|
|
|
|
|
|
action0 *tailcfg.SSHAction // set by doPolicyAuth; first matching action
|
|
|
currentAction *tailcfg.SSHAction // set by doPolicyAuth, updated by resolveNextAction
|
|
|
@@ -273,7 +283,43 @@ func (c *conn) NoClientAuthCallback(ctx ssh.Context) error {
|
|
|
if err := c.doPolicyAuth(ctx, nil /* no pub key */); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- return c.isAuthorized(ctx)
|
|
|
+ if err := c.isAuthorized(ctx); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Let users specify a username ending in +password to force password auth.
|
|
|
+ // This exists for buggy SSH clients that get confused by success from
|
|
|
+ // "none" auth.
|
|
|
+ if strings.HasSuffix(ctx.User(), forcePasswordSuffix) {
|
|
|
+ c.anyPasswordIsOkay = true
|
|
|
+ return errors.New("any password please") // not shown to users
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *conn) nextAuthMethodCallback(cm gossh.ConnMetadata, prevErrors []error) (nextMethod []string) {
|
|
|
+ switch {
|
|
|
+ case c.anyPasswordIsOkay:
|
|
|
+ nextMethod = append(nextMethod, "password")
|
|
|
+ case len(prevErrors) > 0 && prevErrors[len(prevErrors)-1] == errPubKeyRequired:
|
|
|
+ nextMethod = append(nextMethod, "publickey")
|
|
|
+ }
|
|
|
+
|
|
|
+ // The fake "tailscale" method is always appended to next so OpenSSH renders
|
|
|
+ // that in parens as the final failure. (It also shows up in "ssh -v", etc)
|
|
|
+ nextMethod = append(nextMethod, "tailscale")
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// fakePasswordHandler is our implementation of the PasswordHandler hook that
|
|
|
+// checks whether the user's password is correct. But we don't actually use
|
|
|
+// passwords. This exists only for when the user's username ends in "+password"
|
|
|
+// to signal that their SSH client is buggy and gets confused by auth type
|
|
|
+// "none" succeeding and they want our SSH server to require a dummy password
|
|
|
+// prompt instead. We then accept any password since we've already authenticated
|
|
|
+// & authorized them.
|
|
|
+func (c *conn) fakePasswordHandler(ctx ssh.Context, password string) bool {
|
|
|
+ return c.anyPasswordIsOkay
|
|
|
}
|
|
|
|
|
|
// PublicKeyHandler implements ssh.PublicKeyHandler is called by the
|
|
|
@@ -345,9 +391,8 @@ func (c *conn) doPolicyAuth(ctx ssh.Context, pubKey ssh.PublicKey) error {
|
|
|
// ServerConfig implements ssh.ServerConfigCallback.
|
|
|
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
|
|
return &gossh.ServerConfig{
|
|
|
- // OpenSSH presents this on failure as `Permission denied (tailscale).`
|
|
|
- ImplicitAuthMethod: "tailscale",
|
|
|
- NoClientAuth: true, // required for the NoClientAuthCallback to run
|
|
|
+ NoClientAuth: true, // required for the NoClientAuthCallback to run
|
|
|
+ NextAuthMethodCallback: c.nextAuthMethodCallback,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -370,6 +415,7 @@ func (srv *server) newConn() (*conn, error) {
|
|
|
|
|
|
NoClientAuthHandler: c.NoClientAuthCallback,
|
|
|
PublicKeyHandler: c.PublicKeyHandler,
|
|
|
+ PasswordHandler: c.fakePasswordHandler,
|
|
|
|
|
|
Handler: c.handleSessionPostSSHAuth,
|
|
|
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
|
|
|
@@ -495,7 +541,7 @@ func (c *conn) setInfo(ctx ssh.Context) error {
|
|
|
return nil
|
|
|
}
|
|
|
ci := &sshConnInfo{
|
|
|
- sshUser: ctx.User(),
|
|
|
+ sshUser: strings.TrimSuffix(ctx.User(), forcePasswordSuffix),
|
|
|
src: toIPPort(ctx.RemoteAddr()),
|
|
|
dst: toIPPort(ctx.LocalAddr()),
|
|
|
}
|