mirror of
https://github.com/tailscale/tailscale.git
synced 2026-04-13 02:47:56 -04:00
ipn/ipnlocal, feature/ssh: move SSH code out of LocalBackend to feature
This makes tsnet apps not depend on x/crypto/ssh and locks that in with a test. It also paves the wave for tsnet apps to opt-in to SSH support via a blank feature import in the future. Updates #12614 Change-Id: Ica85628f89c8f015413b074f5001b82b27c953a9 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
99e3e9af51
commit
f905871fb1
@@ -1000,10 +1000,9 @@ 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 golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/ssh+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
|
||||
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
|
||||
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
@@ -1011,8 +1010,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
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+
|
||||
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 tailscale.com/tsweb/varz+
|
||||
golang.org/x/exp/maps from sigs.k8s.io/controller-runtime/pkg/cache+
|
||||
golang.org/x/exp/slices from tailscale.com/cmd/k8s-operator+
|
||||
@@ -1078,7 +1075,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
crypto/aes from crypto/tls+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdh from crypto/ecdsa+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
@@ -1127,9 +1124,9 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
crypto/internal/randutil from crypto/internal/rand
|
||||
crypto/internal/sysrand from crypto/internal/fips140/drbg
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/mlkem from golang.org/x/crypto/ssh+
|
||||
crypto/mlkem from crypto/hpke+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
|
||||
@@ -303,6 +303,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/feature/posture from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
|
||||
L tailscale.com/feature/sdnotify from tailscale.com/feature/condregister
|
||||
LD tailscale.com/feature/ssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
||||
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
|
||||
L tailscale.com/feature/tap from tailscale.com/feature/condregister
|
||||
@@ -387,7 +388,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/local+
|
||||
LD tailscale.com/sessionrecording from tailscale.com/ssh/tailssh
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/feature/ssh
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
package main
|
||||
|
||||
// Force registration of tailssh with LocalBackend.
|
||||
import _ "tailscale.com/ssh/tailssh"
|
||||
// Register implementations of various SSH hooks.
|
||||
import _ "tailscale.com/feature/ssh"
|
||||
|
||||
@@ -401,8 +401,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
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 golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/ed25519 from gopkg.in/square/go-jose.v2
|
||||
@@ -414,8 +413,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
golang.org/x/crypto/pbkdf2 from gopkg.in/square/go-jose.v2
|
||||
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+
|
||||
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 tailscale.com/tsweb/varz+
|
||||
golang.org/x/exp/maps from tailscale.com/ipn/store/mem+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
@@ -476,7 +473,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
crypto/aes from crypto/tls+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdh from crypto/ecdsa+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
@@ -525,9 +522,9 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
crypto/internal/randutil from crypto/internal/rand
|
||||
crypto/internal/sysrand from crypto/internal/fips140/drbg
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/mlkem from golang.org/x/crypto/ssh+
|
||||
crypto/mlkem from crypto/hpke+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
|
||||
@@ -67,6 +67,11 @@ func TPMAvailable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HookGetSSHHostKeyPublicStrings is a hook for the ssh/hostkeys package to
|
||||
// provide SSH host key public strings to ipn/ipnlocal without ipnlocal needing
|
||||
// to import golang.org/x/crypto/ssh.
|
||||
var HookGetSSHHostKeyPublicStrings Hook[func(varRoot string, logf logger.Logf) ([]string, error)]
|
||||
|
||||
// HookHardwareAttestationAvailable is a hook that reports whether hardware
|
||||
// attestation is supported and available.
|
||||
var HookHardwareAttestationAvailable Hook[func() bool]
|
||||
|
||||
11
feature/ssh/ssh.go
Normal file
11
feature/ssh/ssh.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ((linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9) && !ts_omit_ssh
|
||||
|
||||
// Package ssh registers the Tailscale SSH feature, including host key
|
||||
// management and the SSH server.
|
||||
package ssh
|
||||
|
||||
// Register implementations of various SSH hooks.
|
||||
import _ "tailscale.com/ssh/tailssh"
|
||||
@@ -44,9 +44,6 @@ func init() {
|
||||
// several candidate nodes is reachable and actually alive.
|
||||
RegisterC2N("/echo", handleC2NEcho)
|
||||
}
|
||||
if buildfeatures.HasSSH {
|
||||
RegisterC2N("/ssh/usernames", handleC2NSSHUsernames)
|
||||
}
|
||||
if buildfeatures.HasLogTail {
|
||||
RegisterC2N("POST /logtail/flush", handleC2NLogtailFlush)
|
||||
}
|
||||
@@ -290,26 +287,6 @@ func handleC2NPprof(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
c2nPprof(w, r, profile)
|
||||
}
|
||||
|
||||
func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasSSH {
|
||||
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := b.getSSHUsernames(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
writeJSON(w, res)
|
||||
}
|
||||
|
||||
func handleC2NSockStats(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if b.sockstatLogger == nil {
|
||||
|
||||
@@ -5646,10 +5646,12 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
||||
// TODO(bradfitz): this is called with b.mu held. Not ideal.
|
||||
// If the filesystem gets wedged or something we could block for
|
||||
// a long time. But probably fine.
|
||||
var err error
|
||||
sshHostKeys, err = b.getSSHHostKeyPublicStrings()
|
||||
if err != nil {
|
||||
b.logf("warning: unable to get SSH host keys, SSH will appear as disabled for this node: %v", err)
|
||||
if f, ok := feature.HookGetSSHHostKeyPublicStrings.GetOk(); ok {
|
||||
var err error
|
||||
sshHostKeys, err = f(b.TailscaleVarRoot(), b.logf)
|
||||
if err != nil {
|
||||
b.logf("warning: unable to get SSH host keys, SSH will appear as disabled for this node: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
hi.SSH_HostKeys = sshHostKeys
|
||||
@@ -6439,9 +6441,9 @@ func (b *LocalBackend) maybeSentHostinfoIfChangedLocked(prefs ipn.PrefsView) {
|
||||
}
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// OperatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
func (b *LocalBackend) OperatorUserName() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
@@ -6454,7 +6456,7 @@ func (b *LocalBackend) operatorUserName() string {
|
||||
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||
// os/user.User.Uid string form), or the empty string if none.
|
||||
func (b *LocalBackend) OperatorUserID() string {
|
||||
opUserName := b.operatorUserName()
|
||||
opUserName := b.OperatorUserName()
|
||||
if opUserName == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ((linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9) && !ts_omit_ssh
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/lineiter"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// keyTypes are the SSH key types that we either try to read from the
|
||||
// system's OpenSSH keys or try to generate for ourselves when not
|
||||
// running as root.
|
||||
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||
|
||||
// getSSHUsernames discovers and returns the list of usernames that are
|
||||
// potential Tailscale SSH user targets.
|
||||
//
|
||||
// Invariant: must not be called with b.mu held.
|
||||
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
res := new(tailcfg.C2NSSHUsernamesResponse)
|
||||
if !b.tailscaleSSHEnabled() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
max := 10
|
||||
if req != nil && req.Max != 0 {
|
||||
max = req.Max
|
||||
}
|
||||
|
||||
add := func(u string) {
|
||||
if req != nil && req.Exclude[u] {
|
||||
return
|
||||
}
|
||||
switch u {
|
||||
case "nobody", "daemon", "sync":
|
||||
return
|
||||
}
|
||||
if slices.Contains(res.Usernames, u) {
|
||||
return
|
||||
}
|
||||
if len(res.Usernames) > max {
|
||||
// Enough for a hint.
|
||||
return
|
||||
}
|
||||
res.Usernames = append(res.Usernames, u)
|
||||
}
|
||||
|
||||
if opUser := b.operatorUserName(); opUser != "" {
|
||||
add(opUser)
|
||||
}
|
||||
|
||||
// Check popular usernames and see if they exist with a real shell.
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for line := range lineiter.Bytes(out) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '_' {
|
||||
continue
|
||||
}
|
||||
add(string(line))
|
||||
}
|
||||
default:
|
||||
for lr := range lineiter.File("/etc/passwd") {
|
||||
line, err := lr.Value()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
|
||||
continue
|
||||
}
|
||||
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
|
||||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
|
||||
continue
|
||||
}
|
||||
before, _, ok := bytes.Cut(line, []byte{':'})
|
||||
if ok {
|
||||
add(string(before))
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
|
||||
var existing map[string]ssh.Signer
|
||||
if os.Geteuid() == 0 {
|
||||
existing = b.getSystemSSH_HostKeys()
|
||||
}
|
||||
return b.getTailscaleSSH_HostKeys(existing)
|
||||
}
|
||||
|
||||
// getTailscaleSSH_HostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
||||
// keys, reusing the provided ones in existing if present in the map.
|
||||
func (b *LocalBackend) getTailscaleSSH_HostKeys(existing map[string]ssh.Signer) (keys []ssh.Signer, err error) {
|
||||
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
||||
for _, typ := range keyTypes {
|
||||
if s, ok := existing[typ]; ok {
|
||||
keys = append(keys, s)
|
||||
continue
|
||||
}
|
||||
if keyDir == "" {
|
||||
root := b.TailscaleVarRoot()
|
||||
if root == "" {
|
||||
return nil, errors.New("no var root for ssh keys")
|
||||
}
|
||||
keyDir = filepath.Join(root, "ssh")
|
||||
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
hostKey, err := b.hostKeyFileOrCreate(keyDir, typ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
||||
}
|
||||
keys = append(keys, signer)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
var keyGenMu sync.Mutex
|
||||
|
||||
func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
keyGenMu.Lock()
|
||||
defer keyGenMu.Unlock()
|
||||
|
||||
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
||||
v, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
var priv any
|
||||
switch typ {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type %q", typ)
|
||||
case "ed25519":
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
case "ecdsa":
|
||||
// curve is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
curve := elliptic.P256()
|
||||
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
|
||||
case "rsa":
|
||||
// keySize is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
const keySize = 2048
|
||||
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
||||
err = os.WriteFile(path, pemGen, 0700)
|
||||
return pemGen, err
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
|
||||
for _, typ := range keyTypes {
|
||||
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
||||
hostKey, err := os.ReadFile(filename)
|
||||
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
||||
continue
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
b.logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
||||
continue
|
||||
}
|
||||
mak.Set(&ret, typ, signer)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getSSHHostKeyPublicStrings() ([]string, error) {
|
||||
signers, err := b.GetSSH_HostKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keyStrings []string
|
||||
for _, signer := range signers {
|
||||
keyStrings = append(keyStrings, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))))
|
||||
}
|
||||
return keyStrings, nil
|
||||
}
|
||||
|
||||
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
|
||||
// on prefs. It returns false if there are no prefs set.
|
||||
func (b *LocalBackend) tailscaleSSHEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
p := b.pm.CurrentPrefs()
|
||||
return p.Valid() && p.RunSSH()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_ssh || ios || android || (!linux && !darwin && !freebsd && !openbsd && !plan9)
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) getSSHHostKeyPublicStrings() ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getSSHUsernames(*tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || (darwin && !ios)
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestSSHKeyGen(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lb := &LocalBackend{varRoot: dir}
|
||||
keys, err := lb.getTailscaleSSH_HostKeys(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
got[k.PublicKey().Type()] = true
|
||||
}
|
||||
want := map[string]bool{
|
||||
"ssh-rsa": true,
|
||||
"ecdsa-sha2-nistp256": true,
|
||||
"ssh-ed25519": true,
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("keys = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
keys2, err := lb.getTailscaleSSH_HostKeys(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(keys, keys2) {
|
||||
t.Errorf("got different keys on second call")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSSHServer struct {
|
||||
SSHServer
|
||||
}
|
||||
|
||||
func TestGetSSHUsernames(t *testing.T) {
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, health.NewTracker(eventbustest.NewBus(t))))
|
||||
b := &LocalBackend{pm: pm, store: pm.Store()}
|
||||
b.sshServer = fakeSSHServer{}
|
||||
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", must.Get(json.Marshal(res)))
|
||||
}
|
||||
109
ssh/tailssh/c2n.go
Normal file
109
ssh/tailssh/c2n.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/lineiter"
|
||||
)
|
||||
|
||||
func handleC2NSSHUsernames(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := getSSHUsernames(b, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
// getSSHUsernames discovers and returns the list of usernames that are
|
||||
// potential Tailscale SSH user targets.
|
||||
func getSSHUsernames(b *ipnlocal.LocalBackend, req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
res := new(tailcfg.C2NSSHUsernamesResponse)
|
||||
if b == nil || !b.ShouldRunSSH() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
max := 10
|
||||
if req != nil && req.Max != 0 {
|
||||
max = req.Max
|
||||
}
|
||||
|
||||
add := func(u string) {
|
||||
if req != nil && req.Exclude[u] {
|
||||
return
|
||||
}
|
||||
switch u {
|
||||
case "nobody", "daemon", "sync":
|
||||
return
|
||||
}
|
||||
if slices.Contains(res.Usernames, u) {
|
||||
return
|
||||
}
|
||||
if len(res.Usernames) > max {
|
||||
// Enough for a hint.
|
||||
return
|
||||
}
|
||||
res.Usernames = append(res.Usernames, u)
|
||||
}
|
||||
|
||||
if opUser := b.OperatorUserName(); opUser != "" {
|
||||
add(opUser)
|
||||
}
|
||||
|
||||
// Check popular usernames and see if they exist with a real shell.
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for line := range lineiter.Bytes(out) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '_' {
|
||||
continue
|
||||
}
|
||||
add(string(line))
|
||||
}
|
||||
default:
|
||||
for lr := range lineiter.File("/etc/passwd") {
|
||||
line, err := lr.Value()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
|
||||
continue
|
||||
}
|
||||
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
|
||||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
|
||||
continue
|
||||
}
|
||||
before, _, ok := bytes.Cut(line, []byte{':'})
|
||||
if ok {
|
||||
add(string(before))
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
155
ssh/tailssh/hostkeys.go
Normal file
155
ssh/tailssh/hostkeys.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// keyTypes are the SSH key types that we either try to read from the
|
||||
// system's OpenSSH keys or try to generate for ourselves when not
|
||||
// running as root.
|
||||
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||
|
||||
// getHostKeys returns the SSH host keys, using system keys when running as root
|
||||
// and generating Tailscale-specific keys as needed.
|
||||
func getHostKeys(varRoot string, logf logger.Logf) ([]gossh.Signer, error) {
|
||||
var existing map[string]gossh.Signer
|
||||
if os.Geteuid() == 0 {
|
||||
existing = getSystemHostKeys(logf)
|
||||
}
|
||||
return getTailscaleHostKeys(varRoot, existing)
|
||||
}
|
||||
|
||||
// getHostKeyPublicStrings returns the SSH host key public key strings.
|
||||
func getHostKeyPublicStrings(varRoot string, logf logger.Logf) ([]string, error) {
|
||||
signers, err := getHostKeys(varRoot, logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keyStrings []string
|
||||
for _, signer := range signers {
|
||||
keyStrings = append(keyStrings, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(signer.PublicKey()))))
|
||||
}
|
||||
return keyStrings, nil
|
||||
}
|
||||
|
||||
// getTailscaleHostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
||||
// keys, reusing the provided ones in existing if present in the map.
|
||||
func getTailscaleHostKeys(varRoot string, existing map[string]gossh.Signer) (keys []gossh.Signer, err error) {
|
||||
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
||||
for _, typ := range keyTypes {
|
||||
if s, ok := existing[typ]; ok {
|
||||
keys = append(keys, s)
|
||||
continue
|
||||
}
|
||||
if keyDir == "" {
|
||||
if varRoot == "" {
|
||||
return nil, errors.New("no var root for ssh keys")
|
||||
}
|
||||
keyDir = filepath.Join(varRoot, "ssh")
|
||||
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
hostKey, err := hostKeyFileOrCreate(keyDir, typ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
||||
}
|
||||
keys = append(keys, signer)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// keyGenMu protects concurrent generation of host keys with
|
||||
// [hostKeyFileOrCreate], making sure two callers don't try to concurrently find
|
||||
// a missing key and generate it at the same time, returning different keys to
|
||||
// their callers.
|
||||
//
|
||||
// Technically we actually want to have a mutex per directory (the keyDir
|
||||
// passed), but that's overkill for how rarely keys are loaded or generated.
|
||||
var keyGenMu sync.Mutex
|
||||
|
||||
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
keyGenMu.Lock()
|
||||
defer keyGenMu.Unlock()
|
||||
|
||||
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
||||
v, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
var priv any
|
||||
switch typ {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type %q", typ)
|
||||
case "ed25519":
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
case "ecdsa":
|
||||
// curve is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
curve := elliptic.P256()
|
||||
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
|
||||
case "rsa":
|
||||
// keySize is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
const keySize = 2048
|
||||
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
||||
err = os.WriteFile(path, pemGen, 0700)
|
||||
return pemGen, err
|
||||
}
|
||||
|
||||
func getSystemHostKeys(logf logger.Logf) (ret map[string]gossh.Signer) {
|
||||
for _, typ := range keyTypes {
|
||||
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
||||
hostKey, err := os.ReadFile(filename)
|
||||
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
||||
continue
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
||||
continue
|
||||
}
|
||||
mak.Set(&ret, typ, signer)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
39
ssh/tailssh/hostkeys_test.go
Normal file
39
ssh/tailssh/hostkeys_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || (darwin && !ios)
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSSHKeyGen(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keys, err := getTailscaleHostKeys(dir, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
got[k.PublicKey().Type()] = true
|
||||
}
|
||||
want := map[string]bool{
|
||||
"ssh-rsa": true,
|
||||
"ecdsa-sha2-nistp256": true,
|
||||
"ssh-ed25519": true,
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("keys = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
keys2, err := getTailscaleHostKeys(dir, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(keys, keys2) {
|
||||
t.Errorf("got different keys on second call")
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,6 @@
|
||||
// ipnLocalBackend is the subset of ipnlocal.LocalBackend that we use.
|
||||
// It is used for testing.
|
||||
type ipnLocalBackend interface {
|
||||
GetSSH_HostKeys() ([]gossh.Signer, error)
|
||||
ShouldRunSSH() bool
|
||||
NetMap() *netmap.NetworkMap
|
||||
WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
@@ -107,6 +106,8 @@ func (srv *server) now() time.Time {
|
||||
}
|
||||
|
||||
func init() {
|
||||
feature.HookGetSSHHostKeyPublicStrings.Set(getHostKeyPublicStrings)
|
||||
ipnlocal.RegisterC2N("/ssh/usernames", handleC2NSSHUsernames)
|
||||
ipnlocal.RegisterNewSSHServer(func(logf logger.Logf, lb *ipnlocal.LocalBackend) (ipnlocal.SSHServer, error) {
|
||||
tsd, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -504,7 +505,7 @@ func (srv *server) newConn() (*conn, error) {
|
||||
maps.Copy(ss.RequestHandlers, ssh.DefaultRequestHandlers)
|
||||
maps.Copy(ss.ChannelHandlers, ssh.DefaultChannelHandlers)
|
||||
maps.Copy(ss.SubsystemHandlers, ssh.DefaultSubsystemHandlers)
|
||||
keys, err := srv.lb.GetSSH_HostKeys()
|
||||
keys, err := getHostKeys(srv.lb.TailscaleVarRoot(), srv.logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -631,28 +630,6 @@ type testBackend struct {
|
||||
allowSendEnv bool
|
||||
}
|
||||
|
||||
func (tb *testBackend) GetSSH_HostKeys() ([]gossh.Signer, error) {
|
||||
var result []gossh.Signer
|
||||
var priv any
|
||||
var err error
|
||||
const keySize = 2048
|
||||
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, signer)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (tb *testBackend) ShouldRunSSH() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
@@ -34,7 +33,6 @@
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
@@ -381,6 +379,7 @@ func TestEvalSSHPolicy(t *testing.T) {
|
||||
type localState struct {
|
||||
sshEnabled bool
|
||||
matchingRule *tailcfg.SSHRule
|
||||
varRoot string // if empty, TailscaleVarRoot returns ""
|
||||
|
||||
// serverActions is a map of the action name to the action.
|
||||
// It is served for paths like https://unused/ssh-action/<action-name>.
|
||||
@@ -388,31 +387,12 @@ type localState struct {
|
||||
serverActions map[string]*tailcfg.SSHAction
|
||||
}
|
||||
|
||||
var (
|
||||
currentUser = os.Getenv("USER") // Use the current user for the test.
|
||||
testSigner gossh.Signer
|
||||
testSignerOnce sync.Once
|
||||
)
|
||||
var currentUser = os.Getenv("USER") // Use the current user for the test.
|
||||
|
||||
func (ts *localState) Dialer() *tsdial.Dialer {
|
||||
return &tsdial.Dialer{}
|
||||
}
|
||||
|
||||
func (ts *localState) GetSSH_HostKeys() ([]gossh.Signer, error) {
|
||||
testSignerOnce.Do(func() {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s, err := gossh.NewSignerFromSigner(priv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testSigner = s
|
||||
})
|
||||
return []gossh.Signer{testSigner}, nil
|
||||
}
|
||||
|
||||
func (ts *localState) ShouldRunSSH() bool {
|
||||
return ts.sshEnabled
|
||||
}
|
||||
@@ -468,7 +448,7 @@ func (ts *localState) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (ts *localState) TailscaleVarRoot() string {
|
||||
return ""
|
||||
return ts.varRoot
|
||||
}
|
||||
|
||||
func (ts *localState) NodeKey() key.NodePublic {
|
||||
@@ -505,6 +485,7 @@ func TestSSHRecordingCancelsSessionsOnUploadFailure(t *testing.T) {
|
||||
logf: tstest.WhileTestRunningLogger(t),
|
||||
lb: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: t.TempDir(),
|
||||
matchingRule: newSSHRule(
|
||||
&tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
@@ -633,6 +614,7 @@ func TestMultipleRecorders(t *testing.T) {
|
||||
logf: tstest.WhileTestRunningLogger(t),
|
||||
lb: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: t.TempDir(),
|
||||
matchingRule: newSSHRule(
|
||||
&tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
@@ -724,6 +706,7 @@ func TestSSHRecordingNonInteractive(t *testing.T) {
|
||||
logf: tstest.WhileTestRunningLogger(t),
|
||||
lb: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: t.TempDir(),
|
||||
matchingRule: newSSHRule(
|
||||
&tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
@@ -792,6 +775,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||
t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
|
||||
}
|
||||
varRoot := t.TempDir()
|
||||
acceptRule := newSSHRule(&tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
Message: "Welcome to Tailscale SSH!",
|
||||
@@ -818,6 +802,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "no-policy",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
},
|
||||
authErr: true,
|
||||
wantBanners: []string{"tailscale: tailnet policy does not permit you to SSH to this node\n"},
|
||||
@@ -826,6 +811,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "user-mismatch",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: bobRule,
|
||||
},
|
||||
authErr: true,
|
||||
@@ -835,6 +821,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "accept",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: acceptRule,
|
||||
},
|
||||
wantBanners: []string{"Welcome to Tailscale SSH!"},
|
||||
@@ -843,6 +830,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "reject",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: rejectRule,
|
||||
},
|
||||
wantBanners: []string{"Go Away!"},
|
||||
@@ -852,6 +840,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "simple-check",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: newSSHRule(&tailcfg.SSHAction{
|
||||
HoldAndDelegate: "https://unused/ssh-action/accept",
|
||||
}),
|
||||
@@ -865,6 +854,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "multi-check",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: newSSHRule(&tailcfg.SSHAction{
|
||||
Message: "First",
|
||||
HoldAndDelegate: "https://unused/ssh-action/check1",
|
||||
@@ -883,6 +873,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
name: "check-reject",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: newSSHRule(&tailcfg.SSHAction{
|
||||
Message: "First",
|
||||
HoldAndDelegate: "https://unused/ssh-action/reject",
|
||||
@@ -899,6 +890,7 @@ func TestSSHAuthFlow(t *testing.T) {
|
||||
sshUser: "alice+password",
|
||||
state: &localState{
|
||||
sshEnabled: true,
|
||||
varRoot: varRoot,
|
||||
matchingRule: acceptRule,
|
||||
},
|
||||
usesPassword: true,
|
||||
|
||||
@@ -396,8 +396,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
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 golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
|
||||
@@ -407,8 +406,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
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+
|
||||
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 tailscale.com/tsweb/varz+
|
||||
golang.org/x/exp/maps from tailscale.com/ipn/store/mem+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
@@ -469,7 +466,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
crypto/aes from crypto/tls+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/ecdh from crypto/ecdsa+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
@@ -518,9 +515,9 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
crypto/internal/randutil from crypto/internal/rand
|
||||
crypto/internal/sysrand from crypto/internal/fips140/drbg
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/mlkem from golang.org/x/crypto/ssh+
|
||||
crypto/mlkem from crypto/hpke+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
|
||||
@@ -2631,6 +2631,10 @@ func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
BadDeps: map[string]string{
|
||||
"golang.org/x/crypto/ssh": "tsnet should not depend on SSH",
|
||||
"golang.org/x/crypto/ssh/internal/bcrypt_pbkdf": "tsnet should not depend on SSH",
|
||||
},
|
||||
OnDep: func(dep string) {
|
||||
if strings.Contains(dep, "portlist") {
|
||||
t.Errorf("unexpected dep: %q", dep)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
_ "tailscale.com/feature"
|
||||
_ "tailscale.com/feature/buildfeatures"
|
||||
_ "tailscale.com/feature/condregister"
|
||||
_ "tailscale.com/feature/ssh"
|
||||
_ "tailscale.com/health"
|
||||
_ "tailscale.com/hostinfo"
|
||||
_ "tailscale.com/ipn"
|
||||
@@ -40,7 +41,6 @@
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/syncs"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsd"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
_ "tailscale.com/feature"
|
||||
_ "tailscale.com/feature/buildfeatures"
|
||||
_ "tailscale.com/feature/condregister"
|
||||
_ "tailscale.com/feature/ssh"
|
||||
_ "tailscale.com/health"
|
||||
_ "tailscale.com/hostinfo"
|
||||
_ "tailscale.com/ipn"
|
||||
@@ -40,7 +41,6 @@
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/syncs"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsd"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
_ "tailscale.com/feature"
|
||||
_ "tailscale.com/feature/buildfeatures"
|
||||
_ "tailscale.com/feature/condregister"
|
||||
_ "tailscale.com/feature/ssh"
|
||||
_ "tailscale.com/health"
|
||||
_ "tailscale.com/hostinfo"
|
||||
_ "tailscale.com/ipn"
|
||||
@@ -40,7 +41,6 @@
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/syncs"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsd"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
_ "tailscale.com/feature"
|
||||
_ "tailscale.com/feature/buildfeatures"
|
||||
_ "tailscale.com/feature/condregister"
|
||||
_ "tailscale.com/feature/ssh"
|
||||
_ "tailscale.com/health"
|
||||
_ "tailscale.com/hostinfo"
|
||||
_ "tailscale.com/ipn"
|
||||
@@ -40,7 +41,6 @@
|
||||
_ "tailscale.com/net/tstun"
|
||||
_ "tailscale.com/paths"
|
||||
_ "tailscale.com/safesocket"
|
||||
_ "tailscale.com/ssh/tailssh"
|
||||
_ "tailscale.com/syncs"
|
||||
_ "tailscale.com/tailcfg"
|
||||
_ "tailscale.com/tsd"
|
||||
|
||||
Reference in New Issue
Block a user