ssh,tempfork/gliderlabs/ssh: replace github.com/tailscale/golang-x-crypto/ssh with golang.org/x/crypto/ssh

The upstream crypto package now supports sending banners at any time during
authentication, so the Tailscale fork of crypto/ssh is no longer necessary.

github.com/tailscale/golang-x-crypto is still needed for some custom ACME
autocert functionality.

tempfork/gliderlabs is still necessary because of a few other customizations,
mostly related to TTY handling.

Updates #8593

Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
Percy Wegmann 2025-01-27 13:05:27 -06:00 committed by Percy Wegmann
parent 3abfbf50ae
commit 46fd4e58a2
22 changed files with 177 additions and 236 deletions

View File

@ -197,9 +197,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
LD github.com/tailscale/golang-x-crypto/internal/poly1305 from github.com/tailscale/golang-x-crypto/ssh
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@ -986,12 +983,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/ssh+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
@ -1000,6 +997,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
LD golang.org/x/crypto/ssh from tailscale.com/ipn/ipnlocal
LD golang.org/x/crypto/ssh/internal/bcrypt_pbkdf from golang.org/x/crypto/ssh
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from sigs.k8s.io/controller-runtime/pkg/cache+
golang.org/x/exp/slices from tailscale.com/cmd/k8s-operator+

View File

@ -6,6 +6,9 @@
// highlight the unique parts of the Tailscale SSH server so SSH
// client authors can hit it easily and fix their SSH clients without
// needing to set up Tailscale and Tailscale SSH.
//
// Connections are allowed using any username except for "denyme". Connecting as
// "denyme" will result in an authentication failure with error message.
package main
import (
@ -16,6 +19,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
@ -24,7 +28,7 @@ import (
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
@ -62,13 +66,21 @@ func main() {
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
start := time.Now()
var spac gossh.ServerPreAuthConn
return &gossh.ServerConfig{
NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
return []string{"tailscale"}
PreAuthConnCallback: func(conn gossh.ServerPreAuthConn) {
spac = conn
},
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
if cm.User() == "denyme" {
return nil, &gossh.BannerError{
Err: errors.New("denying access"),
Message: "denyme is not allowed to access this machine\n",
}
}
totalBanners := 2
if cm.User() == "banners" {
@ -77,9 +89,9 @@ func main() {
for banner := 2; banner <= totalBanners; banner++ {
time.Sleep(time.Second)
if banner == totalBanners {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
} else {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
}
}
return nil, nil

View File

@ -152,9 +152,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
LD github.com/tailscale/golang-x-crypto/internal/poly1305 from github.com/tailscale/golang-x-crypto/ssh
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@ -439,12 +436,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+

View File

@ -17,7 +17,6 @@ func TestOmitSSH(t *testing.T) {
Tags: "ts_omit_ssh",
BadDeps: map[string]string{
"tailscale.com/ssh/tailssh": msg,
"golang.org/x/crypto/ssh": msg,
"tailscale.com/sessionrecording": msg,
"github.com/anmitsu/go-shlex": msg,
"github.com/creack/pty": msg,

2
go.mod
View File

@ -94,7 +94,7 @@ require (
go.uber.org/zap v1.27.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
golang.org/x/mod v0.22.0
golang.org/x/net v0.34.0

4
go.sum
View File

@ -1058,8 +1058,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07 h1:Z+Zg+aXJYq6f4TK2E4H+vZkQ4dJAWnInXDR6hM9znxo=
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

View File

@ -24,8 +24,8 @@ import (
"strings"
"sync"
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"golang.org/x/crypto/ssh"
"tailscale.com/tailcfg"
"tailscale.com/util/lineiter"
"tailscale.com/util/mak"

View File

@ -29,7 +29,7 @@ import (
"syscall"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/logtail/backoff"
@ -198,8 +198,11 @@ func (srv *server) OnPolicyChange() {
// Setup and discover server info
// - ServerConfigCallback
//
// Do the user auth
// - NoClientAuthHandler
// Get access to a ServerPreAuthConn (useful for sending banners)
//
// Do the user auth with a NoClientAuthCallback. If user specified
// a username ending in "+password", follow this with password auth
// (to work around buggy SSH clients that don't work with noauth).
//
// Once auth is done, the conn can be multiplexed with multiple sessions and
// channels concurrently. At which point any of the following can be called
@ -219,15 +222,12 @@ type conn struct {
idH string
connID string // ID that's shared with control
// 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
// spac is a [gossh.ServerPreAuthConn] used for sending auth banners.
// Banners cannot be sent after auth completes.
spac gossh.ServerPreAuthConn
action0 *tailcfg.SSHAction // set by doPolicyAuth; first matching action
currentAction *tailcfg.SSHAction // set by doPolicyAuth, updated by resolveNextAction
finalAction *tailcfg.SSHAction // set by doPolicyAuth or resolveNextAction
finalActionErr error // set by doPolicyAuth or resolveNextAction
action0 *tailcfg.SSHAction // set by clientAuth
finalAction *tailcfg.SSHAction // set by clientAuth
info *sshConnInfo // set by setInfo
localUser *userMeta // set by doPolicyAuth
@ -254,141 +254,142 @@ func (c *conn) vlogf(format string, args ...any) {
}
}
// isAuthorized walks through the action chain and returns nil if the connection
// is authorized. If the connection is not authorized, it returns
// errDenied. If the action chain resolution fails, it returns the
// resolution error.
func (c *conn) isAuthorized(ctx ssh.Context) error {
action := c.currentAction
for {
if action.Accept {
return nil
}
if action.Reject || action.HoldAndDelegate == "" {
return errDenied
}
var err error
action, err = c.resolveNextAction(ctx)
if err != nil {
return err
}
if action.Message != "" {
if err := ctx.SendAuthBanner(action.Message); err != nil {
return err
}
}
}
}
// errDenied is returned by auth callbacks when a connection is denied by the
// policy.
var errDenied = errors.New("ssh: access denied")
// NoClientAuthCallback implements gossh.NoClientAuthCallback and is called by
// the ssh.Server when the client first connects with the "none"
// authentication method.
//
// It is responsible for continuing policy evaluation from BannerCallback (or
// starting it afresh). It returns an error if the policy evaluation fails, or
// if the decision is "reject"
//
// It either returns nil (accept) or errDenied (reject). The errors may be wrapped.
func (c *conn) NoClientAuthCallback(ctx ssh.Context) error {
if c.insecureSkipTailscaleAuth {
return nil
// policy. It returns a gossh.BannerError to make sure the message gets
// displayed as an auth banner.
func errDenied(message string) error {
if message == "" {
message = "tailscale: access denied"
}
if err := c.doPolicyAuth(ctx); err != nil {
return err
return &gossh.BannerError{
Message: message,
}
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")
}
// 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
}
// doPolicyAuth verifies that conn can proceed.
// It returns nil if the matching policy action is Accept or
// HoldAndDelegate. Otherwise, it returns errDenied.
func (c *conn) doPolicyAuth(ctx ssh.Context) error {
if err := c.setInfo(ctx); err != nil {
c.logf("failed to get conninfo: %v", err)
return errDenied
}
a, localUser, acceptEnv, err := c.evaluatePolicy()
// bannerError creates a gossh.BannerError that will result in the given
// message being displayed to the client. If err != nil, this also logs
// message:error. The contents of err is not leaked to clients in the banner.
func (c *conn) bannerError(message string, err error) error {
if err != nil {
return fmt.Errorf("%w: %v", errDenied, err)
c.logf("%s: %s", message, err)
}
c.action0 = a
c.currentAction = a
c.acceptEnv = acceptEnv
if a.Message != "" {
if err := ctx.SendAuthBanner(a.Message); err != nil {
return fmt.Errorf("SendBanner: %w", err)
}
return &gossh.BannerError{
Err: err,
Message: fmt.Sprintf("tailscale: %s", message),
}
if a.Accept || a.HoldAndDelegate != "" {
if a.Accept {
c.finalAction = a
}
}
// clientAuth is responsible for performing client authentication.
//
// If policy evaluation fails, it returns an error.
// If access is denied, it returns an error.
func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
if c.insecureSkipTailscaleAuth {
return &gossh.Permissions{}, nil
}
if err := c.setInfo(cm); err != nil {
return nil, c.bannerError("failed to get connection info", err)
}
action, localUser, acceptEnv, err := c.evaluatePolicy()
if err != nil {
return nil, c.bannerError("failed to evaluate SSH policy", err)
}
c.action0 = action
if action.Accept || action.HoldAndDelegate != "" {
// Immediately look up user information for purposes of generating
// hold and delegate URL (if necessary).
lu, err := userLookup(localUser)
if err != nil {
c.logf("failed to look up %v: %v", localUser, err)
ctx.SendAuthBanner(fmt.Sprintf("failed to look up %v\r\n", localUser))
return err
return nil, c.bannerError(fmt.Sprintf("failed to look up local user %q ", localUser), err)
}
gids, err := lu.GroupIds()
if err != nil {
c.logf("failed to look up local user's group IDs: %v", err)
return err
return nil, c.bannerError("failed to look up local user's group IDs", err)
}
c.userGroupIDs = gids
c.localUser = lu
return nil
c.acceptEnv = acceptEnv
}
if a.Reject {
c.finalAction = a
return errDenied
for {
switch {
case action.Accept:
metricTerminalAccept.Add(1)
if action.Message != "" {
if err := c.spac.SendAuthBanner(action.Message); err != nil {
return nil, fmt.Errorf("error sending auth welcome message: %w", err)
}
}
c.finalAction = action
return &gossh.Permissions{}, nil
case action.Reject:
metricTerminalReject.Add(1)
c.finalAction = action
return nil, errDenied(action.Message)
case action.HoldAndDelegate != "":
if action.Message != "" {
if err := c.spac.SendAuthBanner(action.Message); err != nil {
return nil, fmt.Errorf("error sending hold and delegate message: %w", err)
}
}
url := action.HoldAndDelegate
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
metricHolds.Add(1)
url = c.expandDelegateURLLocked(url)
var err error
action, err = c.fetchSSHAction(ctx, url)
if err != nil {
metricTerminalFetchError.Add(1)
return nil, c.bannerError("failed to fetch next SSH action", fmt.Errorf("fetch failed from %s: %w", url, err))
}
default:
metricTerminalMalformed.Add(1)
return nil, c.bannerError("reached Action that had neither Accept, Reject, nor HoldAndDelegate", nil)
}
}
// Shouldn't get here, but:
return errDenied
}
// ServerConfig implements ssh.ServerConfigCallback.
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
NoClientAuth: true, // required for the NoClientAuthCallback to run
NextAuthMethodCallback: c.nextAuthMethodCallback,
PreAuthConnCallback: func(spac gossh.ServerPreAuthConn) {
c.spac = spac
},
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.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).
perms, err := c.clientAuth(cm)
if err != nil {
return nil, err
}
// Authentication succeeded. Buggy SSH clients get confused by
// success from the "none" auth method. As a workaround, let users
// 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 perms, nil
},
}
}
@ -399,7 +400,7 @@ func (srv *server) newConn() (*conn, error) {
// Stop accepting new connections.
// Connections in the auth phase are handled in handleConnPostSSHAuth.
// Existing sessions are terminated by Shutdown.
return nil, errDenied
return nil, errDenied("tailscale: server is shutting down")
}
srv.mu.Unlock()
c := &conn{srv: srv}
@ -410,9 +411,6 @@ func (srv *server) newConn() (*conn, error) {
Version: "Tailscale",
ServerConfigCallback: c.ServerConfig,
NoClientAuthHandler: c.NoClientAuthCallback,
PasswordHandler: c.fakePasswordHandler,
Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
ReversePortForwardingCallback: c.mayReversePortForwardTo,
@ -523,16 +521,16 @@ func toIPPort(a net.Addr) (ipp netip.AddrPort) {
return netip.AddrPortFrom(tanetaddr.Unmap(), uint16(ta.Port))
}
// connInfo returns a populated sshConnInfo from the provided arguments,
// connInfo populates the sshConnInfo from the provided arguments,
// validating only that they represent a known Tailscale identity.
func (c *conn) setInfo(ctx ssh.Context) error {
func (c *conn) setInfo(cm gossh.ConnMetadata) error {
if c.info != nil {
return nil
}
ci := &sshConnInfo{
sshUser: strings.TrimSuffix(ctx.User(), forcePasswordSuffix),
src: toIPPort(ctx.RemoteAddr()),
dst: toIPPort(ctx.LocalAddr()),
sshUser: strings.TrimSuffix(cm.User(), forcePasswordSuffix),
src: toIPPort(cm.RemoteAddr()),
dst: toIPPort(cm.LocalAddr()),
}
if !tsaddr.IsTailscaleIP(ci.dst.Addr()) {
return fmt.Errorf("tailssh: rejecting non-Tailscale local address %v", ci.dst)
@ -547,7 +545,7 @@ func (c *conn) setInfo(ctx ssh.Context) error {
ci.node = node
ci.uprof = uprof
c.idH = ctx.SessionID()
c.idH = string(cm.SessionID())
c.info = ci
c.logf("handling conn: %v", ci.String())
return nil
@ -594,62 +592,6 @@ func (c *conn) handleSessionPostSSHAuth(s ssh.Session) {
ss.run()
}
// resolveNextAction starts at c.currentAction and makes it way through the
// action chain one step at a time. An action without a HoldAndDelegate is
// considered the final action. Once a final action is reached, this function
// will keep returning that action. It updates c.currentAction to the next
// action in the chain. When the final action is reached, it also sets
// c.finalAction to the final action.
func (c *conn) resolveNextAction(sctx ssh.Context) (action *tailcfg.SSHAction, err error) {
if c.finalAction != nil || c.finalActionErr != nil {
return c.finalAction, c.finalActionErr
}
defer func() {
if action != nil {
c.currentAction = action
if action.Accept || action.Reject {
c.finalAction = action
}
}
if err != nil {
c.finalActionErr = err
}
}()
ctx, cancel := context.WithCancel(sctx)
defer cancel()
// Loop processing/fetching Actions until one reaches a
// terminal state (Accept, Reject, or invalid Action), or
// until fetchSSHAction times out due to the context being
// done (client disconnect) or its 30 minute timeout passes.
// (Which is a long time for somebody to see login
// instructions and go to a URL to do something.)
action = c.currentAction
if action.Accept || action.Reject {
if action.Reject {
metricTerminalReject.Add(1)
} else {
metricTerminalAccept.Add(1)
}
return action, nil
}
url := action.HoldAndDelegate
if url == "" {
metricTerminalMalformed.Add(1)
return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
}
metricHolds.Add(1)
url = c.expandDelegateURLLocked(url)
nextAction, err := c.fetchSSHAction(ctx, url)
if err != nil {
metricTerminalFetchError.Add(1)
return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
}
return nextAction, nil
}
func (c *conn) expandDelegateURLLocked(actionURL string) string {
nm := c.srv.lb.NetMap()
ci := c.info

View File

@ -32,8 +32,8 @@ import (
"github.com/bramvdbogaerde/go-scp"
"github.com/google/go-cmp/cmp"
"github.com/pkg/sftp"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"golang.org/x/crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"

View File

@ -31,7 +31,7 @@ import (
"testing"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"tailscale.com/ipn/ipnlocal"
@ -805,7 +805,8 @@ func TestSSHAuthFlow(t *testing.T) {
state: &localState{
sshEnabled: true,
},
authErr: true,
authErr: true,
wantBanners: []string{"tailscale: failed to evaluate SSH policy"},
},
{
name: "accept",

View File

@ -7,7 +7,7 @@ import (
"path"
"sync"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
const (

View File

@ -6,7 +6,7 @@ import (
"net"
"sync"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
// contextKey is a value for use with context.WithValue. It's used as
@ -55,8 +55,6 @@ var (
// ContextKeyPublicKey is a context key for use with Contexts in this package.
// The associated value will be of type PublicKey.
ContextKeyPublicKey = &contextKey{"public-key"}
ContextKeySendAuthBanner = &contextKey{"send-auth-banner"}
)
// Context is a package specific context interface. It exposes connection
@ -91,8 +89,6 @@ type Context interface {
// SetValue allows you to easily write new values into the underlying context.
SetValue(key, value interface{})
SendAuthBanner(banner string) error
}
type sshContext struct {
@ -121,7 +117,6 @@ func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
ctx.SetValue(ContextKeyUser, conn.User())
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
ctx.SetValue(ContextKeySendAuthBanner, conn.SendAuthBanner)
}
func (ctx *sshContext) SetValue(key, value interface{}) {
@ -158,7 +153,3 @@ func (ctx *sshContext) LocalAddr() net.Addr {
func (ctx *sshContext) Permissions() *Permissions {
return ctx.Value(ContextKeyPermissions).(*Permissions)
}
func (ctx *sshContext) SendAuthBanner(msg string) error {
return ctx.Value(ContextKeySendAuthBanner).(func(string) error)(msg)
}

View File

@ -3,7 +3,7 @@ package ssh
import (
"os"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
// PasswordAuth returns a functional option that sets PasswordHandler on the server.

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
func newTestSessionWithOptions(t *testing.T, srv *Server, cfg *gossh.ClientConfig, options ...Option) (*gossh.Session, *gossh.Client, func()) {

View File

@ -8,7 +8,7 @@ import (
"sync"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,

View File

@ -9,7 +9,7 @@ import (
"sync"
"github.com/anmitsu/go-shlex"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
// Session provides access to information about an SSH session and methods

View File

@ -9,7 +9,7 @@ import (
"net"
"testing"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
func (srv *Server) serveOnce(l net.Listener) error {

View File

@ -4,7 +4,7 @@ import (
"crypto/subtle"
"net"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
type Signal string
@ -105,7 +105,7 @@ type Pty struct {
// requested by the client as part of the pty-req. These are outlined as
// part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
//
// The opcodes are defined as constants in github.com/tailscale/golang-x-crypto/ssh (VINTR,VQUIT,etc.).
// The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.).
// Boolean opcodes have values 0 or 1.
Modes gossh.TerminalModes
}

View File

@ -7,7 +7,7 @@ import (
"strconv"
"sync"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
const (

View File

@ -10,7 +10,7 @@ import (
"strings"
"testing"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
)
var sampleServerResponse = []byte("Hello world")

View File

@ -5,7 +5,7 @@ import (
"crypto/rsa"
"encoding/binary"
"github.com/tailscale/golang-x-crypto/ssh"
"golang.org/x/crypto/ssh"
)
func generateSigner() (ssh.Signer, error) {

View File

@ -1,6 +1,6 @@
package ssh
import gossh "github.com/tailscale/golang-x-crypto/ssh"
import gossh "golang.org/x/crypto/ssh"
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {