blob: 19397abdfdcde3196ea8d29006a9f6505e0049c9 [file]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || darwin
package remote
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/build/buildlet"
"golang.org/x/crypto/ssh"
"golang.org/x/net/nettest"
)
func TestSignPublicSSHKey(t *testing.T) {
signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate))
if err != nil {
t.Fatalf("ssh.ParsePrivateKey() = %s", err)
}
ownerID := "accounts.google.com:userIDvalue"
sessionID := "user-maria-linux-amd64-12"
gotPubKey, err := SignPublicSSHKey(context.Background(), signer, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(gotPubKey)
if err != nil {
t.Fatalf("ssh.ParseAuthorizedKey(...) = %s; want no error", err)
}
certChecker := &ssh.CertChecker{}
wantPrinciple := fmt.Sprintf("%s@farmer.golang.org", sessionID)
pubKeyCert := pubKey.(*ssh.Certificate)
if err := certChecker.CheckCert(wantPrinciple, pubKeyCert); err != nil {
t.Fatalf("certChecker.CheckCert(%s, %+v) = %s", wantPrinciple, pubKeyCert, err)
}
if diff := cmp.Diff(pubKeyCert.SignatureKey.Marshal(), signer.PublicKey().Marshal()); diff != "" {
t.Fatalf("Public Keys mismatch (-want +got):\n%s", diff)
}
}
func TestHandleCertificateAuthFunc(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
client, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil {
t.Fatalf("Dial(...) = _, %s; want no error", err)
}
client.Close()
}
func TestHandleCertificateAuthFuncErrors(t *testing.T) {
t.Run("no certificate", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
clientSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err := ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = client, nil; want error")
}
})
t.Run("wrong certificate signer", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertAlternateClientPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatalf("Dial(...) = _, %s; want no error", err)
}
})
t.Run("wrong user", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID + "_i_do_not_exist",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
t.Run("wrong principle", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID+"WRONG", ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
t.Run("wrong owner", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", "", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID+"WRONG", time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
}
func setupSSHServer(t *testing.T, ctx context.Context) (addr string, sp *SessionPool, s *SSHServer) {
sp = NewSessionPool(ctx)
l, err := nettest.NewLocalListener("tcp")
if err != nil {
t.Fatalf("nettest.NewLocalListener(tcp) = _, %s; want no error", err)
}
addr = l.Addr().String()
s, err = NewSSHServer(addr, []byte(devCertAlternateClientPrivate), []byte(devCertCAPublic), []byte(devCertCAPrivate), sp)
if err != nil {
t.Fatalf("NewSSHServer(...) = %s; want no error", err)
}
go s.serve(l)
if err != nil {
t.Fatalf("server.serve(l) = %s; want no error", err)
}
return
}
func parsePrivateKey(t *testing.T, pemEncoded []byte) ssh.Signer {
cert, err := ssh.ParsePrivateKey(pemEncoded)
if err != nil {
t.Fatalf("ssh.ParsePrivateKey() = _, %s; want no error", err)
}
return cert
}
const (
// devCertCAPrivate is a private SSH CA certificate to be used for development.
devCertCAPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WAAAAJhjy3ePY8t3
jwAAAAtzc2gtZWQyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WA
AAAEALuUJMb/rEaFNa+vn5RejeoBiiViyda7djgEvMnQ8fRJV3YUncNv+hXneJEO3VEuxx
OfjWFhdu6e4JYiVfN7ZYAAAAE3Rlc3R1c2VyQGdvbGFuZy5vcmcBAg==
-----END OPENSSH PRIVATE KEY-----`
// devCertCAPublic is a public SSH CA certificate to be used for development.
devCertCAPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3YUncNv+hXneJEO3VEuxxOfjWFhdu6e4JYiVfN7ZY testuser@golang.org`
// devCertClientPrivate is a private SSH certificate to be used for development.
devCertClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3owAAAKDFZ7xtxWe8
bQAAAAtzc2gtZWQyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3ow
AAAECidrOyYbTlYxyBSPP7W/UHk3Si2dgWSfkT+eEIETcvqHEIzoAN0eeNMgcb8ikxrfPf
YIvC27cENRHeTY01sjejAAAAFnRlc3RfY2xpZW50QGdvbGFuZy5vcmcBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----`
// devCertClientPublic is a public SSH certificate to be used for development.
devCertClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEIzoAN0eeNMgcb8ikxrfPfYIvC27cENRHeTY01sjej test_client@golang.org`
// devCertAlternateClientPrivate is a private SSH certificate to be used for development.
devCertAlternateClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQAAAKDYNRtZ2DUb
WQAAAAtzc2gtZWQyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQ
AAAEDS4G3tQt5S4v7CD+DVyT/mwOKgIScIgFOpFt/EsCXL9M6PwraVsJK/4uiM1ytR/Rcf
W+qSe4RkGQBx6IEe424RAAAAF3Rlc3RfZGlzY2FyZEBnb2xhbmcub3JnAQIDBAUG
-----END OPENSSH PRIVATE KEY-----`
// devCertAlternateClientPublic is a public SSH to be used for development.
devCertAlternateClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM6PwraVsJK/4uiM1ytR/RcfW+qSe4RkGQBx6IEe424R test_discard@golang.org`
)
func TestBuildletSSHArgs(t *testing.T) {
common := []string{"-p", "1234", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-i", "/key"}
tests := []struct {
subsystem string
rawCmd string
want []string
}{
{"", "", append(common, "swarming@localhost")},
{"", "hostname -f", append(common, "swarming@localhost", "hostname -f")},
{"sftp", "", append(common, "-s", "swarming@localhost", "sftp")},
}
for _, tt := range tests {
got := buildletSSHArgs(1234, "/key", "swarming", tt.subsystem, tt.rawCmd)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("buildletSSHArgs(subsystem=%q, cmd=%q) mismatch (-want +got):\n%s", tt.subsystem, tt.rawCmd, diff)
}
}
}
// TestSSHHandleDirect exercises the direct (no pty) proxy path end to
// end: a real ssh or scp client connects to a real SSHServer, which
// authenticates the client's certificate and proxies the session to a
// fake buildlet sshd (an in-process x/crypto/ssh server standing in
// for the sshd on a swarming bot).
func TestSSHHandleDirect(t *testing.T) {
// The proxy runs on Linux, and this test needs an ssh client that
// can connect to a local server and run commands through it. The
// macOS builders cannot even exec /usr/bin/scp, so limit the test
// to the system the proxy is deployed on.
if runtime.GOOS != "linux" {
t.Skipf("skipping on %s: the ssh proxy runs on linux", runtime.GOOS)
}
requireCommand(t, "ssh", "-V")
// The gomote key pair: the proxy's host key, its identity when
// connecting to the buildlet sshd, and the key that sshd authorizes.
gomotePriv, gomotePub, err := SSHKeyPair()
if err != nil {
t.Fatal(err)
}
authorizedGomoteKey, _, _, _, err := ssh.ParseAuthorizedKey(gomotePub)
if err != nil {
t.Fatal(err)
}
caPriv, _, err := SSHKeyPair()
if err != nil {
t.Fatal(err)
}
caSigner, err := ssh.ParsePrivateKey(caPriv)
if err != nil {
t.Fatal(err)
}
buildletHostPriv, _, err := SSHKeyPair()
if err != nil {
t.Fatal(err)
}
buildletSigner, err := ssh.ParsePrivateKey(buildletHostPriv)
if err != nil {
t.Fatal(err)
}
// A session pool holding one fake buildlet.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sp := NewSessionPool(ctx)
defer sp.Close()
fc := &directFakeClient{
t: t,
hostSigner: buildletSigner,
authorized: authorizedGomoteKey,
}
const ownerID = "accounts.google.com:tester"
sess := sp.AddSession(ownerID, "user", "gotip-linux-amd64", "host-linux-amd64", "task123", fc)
// The proxy under test.
ss, err := NewSSHServer("localhost:0", gomotePriv, gomotePub, caPriv, sp, EnableLUCIOption())
if err != nil {
t.Fatal(err)
}
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
go ss.serve(ln)
defer ss.Close()
port := strconv.Itoa(ln.Addr().(*net.TCPAddr).Port)
// A client key, certified by the CA for this session and owner,
// as the gomote command does via the SignSSHKey RPC.
dir := t.TempDir()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
block, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
t.Fatal(err)
}
keyFile := filepath.Join(dir, "id_ed25519")
if err := os.WriteFile(keyFile, pem.EncodeToMemory(block), 0o600); err != nil {
t.Fatal(err)
}
sshPub, err := ssh.NewPublicKey(pub)
if err != nil {
t.Fatal(err)
}
cert, err := SignPublicSSHKey(ctx, caSigner, ssh.MarshalAuthorizedKey(sshPub), sess, ownerID, time.Hour)
if err != nil {
t.Fatal(err)
}
certFile := filepath.Join(dir, "id_ed25519-cert.pub")
if err := os.WriteFile(certFile, cert, 0o600); err != nil {
t.Fatal(err)
}
opts := []string{
"-o", "CertificateFile=" + certFile,
"-i", keyFile,
"-o", "IdentitiesOnly=yes",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "BatchMode=yes",
"-o", "LogLevel=ERROR",
}
target := sess + "@localhost"
// The property the mote protocol needs: every byte passes through
// in both directions unmodified, including the ones a cooked pty
// would eat (\x00, \x03, \x13, \x7f, \r).
const binary = "mote server hello \x00\x01\xfe\xff\n\x03\x04\x11\x13\x7f\r\nrest"
tests := []struct {
name string
stdin string
cmd []string // remote command; none means ssh with no arguments, a shell reading stdin
stdout string
stderr string
code int
}{
{name: "exec", cmd: []string{"echo hello direct"}, stdout: "hello direct\n"},
{name: "noArgs", stdin: "echo from-shell\nexit 0\n", stdout: "from-shell\n"},
{name: "exitStatus", cmd: []string{"exit 7"}, code: 7},
{name: "stderr", cmd: []string{"echo out; echo err >&2"}, stdout: "out\n", stderr: "err\n"},
{name: "binarySafe", stdin: binary, cmd: []string{"cat"}, stdout: binary},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := append(append([]string{"-p", port}, opts...), target)
args = append(args, tt.cmd...)
cmd := exec.Command("ssh", args...)
var outb, errb bytes.Buffer
cmd.Stdin = strings.NewReader(tt.stdin)
cmd.Stdout = &outb
cmd.Stderr = &errb
err := cmd.Run()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
t.Fatalf("ssh: %v", err)
}
if code != tt.code || outb.String() != tt.stdout || errb.String() != tt.stderr {
t.Errorf("code=%d stdout=%q stderr=%q, want %d, %q, %q",
code, outb.String(), errb.String(), tt.code, tt.stdout, tt.stderr)
}
})
}
t.Run("scp", func(t *testing.T) {
if fc.sftpServer() == "" {
t.Skip("no sftp-server binary found")
}
requireCommand(t, "scp")
src := filepath.Join(dir, "src.bin")
data := []byte("scp test \x00\x01\xfe\xff\r\n data")
if err := os.WriteFile(src, data, 0o644); err != nil {
t.Fatal(err)
}
remote := filepath.Join(dir, "remote.bin") // "remote" is the local fs: sftp-server runs in-process
back := filepath.Join(dir, "back.bin")
for _, files := range [][]string{
{src, target + ":" + remote},
{target + ":" + remote, back},
} {
args := append(append([]string{"-P", port}, opts...), files...)
out, err := exec.Command("scp", args...).CombinedOutput()
if err != nil {
t.Fatalf("scp %v: %v\n%s", files, err, out)
}
}
got, err := os.ReadFile(back)
if err != nil || !bytes.Equal(got, data) {
t.Errorf("round trip = %q, %v; want %q", got, err, data)
}
})
}
// requireCommand skips the test unless the named command is installed
// and can be run. Running it is the part worth checking: some builders
// have an ssh and an scp in PATH that fail to exec.
func requireCommand(t *testing.T, name string, args ...string) {
t.Helper()
path, err := exec.LookPath(name)
if err != nil {
t.Skipf("no %s command: %v", name, err)
}
// The command may report usage and exit with a status, which is
// fine; only a failure to run it at all disqualifies the system.
var execErr *exec.Error
if err := exec.Command(path, args...).Run(); errors.As(err, &execErr) {
t.Skipf("cannot run %s: %v", path, err)
}
}
// A directFakeClient is a buildlet client whose ConnectSSH returns a
// connection to an in-process fake buildlet sshd.
type directFakeClient struct {
buildlet.FakeClient
t *testing.T
hostSigner ssh.Signer
authorized ssh.PublicKey
}
// sftpServer returns the path of the OpenSSH sftp-server binary,
// or "" if none is installed.
func (c *directFakeClient) sftpServer() string {
for _, p := range []string{
"/usr/libexec/sftp-server", // macOS
"/usr/lib/openssh/sftp-server", // Debian
"/usr/libexec/openssh/sftp-server", // Fedora
"/usr/lib/ssh/sftp-server", // Arch
} {
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func (c *directFakeClient) ConnectSSH(user, authorizedPubKey string) (net.Conn, error) {
if user != "swarming" {
return nil, fmt.Errorf("ConnectSSH user = %q, want swarming", user)
}
c1, c2 := net.Pipe()
go c.serveSSH(c2)
return c1, nil
}
// serveSSH runs the fake buildlet sshd on conn: public key
// authentication with the gomote key, and sessions that run exec and
// shell requests locally and hand the sftp subsystem to sftp-server.
// A session requesting a pty is an error: the direct proxy path must
// not allocate one.
func (c *directFakeClient) serveSSH(conn net.Conn) {
defer conn.Close()
config := &ssh.ServerConfig{
PublicKeyCallback: func(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if !bytes.Equal(key.Marshal(), c.authorized.Marshal()) {
return nil, fmt.Errorf("unauthorized key")
}
return nil, nil
},
}
config.AddHostKey(c.hostSigner)
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
c.t.Logf("fake sshd: handshake: %v", err)
return
}
defer sconn.Close()
go ssh.DiscardRequests(reqs)
for newCh := range chans {
if newCh.ChannelType() != "session" {
newCh.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
ch, chReqs, err := newCh.Accept()
if err != nil {
continue
}
go c.serveSession(ch, chReqs)
}
}
func (c *directFakeClient) serveSession(ch ssh.Channel, reqs <-chan *ssh.Request) {
for req := range reqs {
var cmd *exec.Cmd
switch req.Type {
default:
req.Reply(false, nil)
continue
case "env":
req.Reply(true, nil)
continue
case "pty-req":
c.t.Errorf("fake sshd: unexpected pty-req on direct session")
req.Reply(false, nil)
continue
case "exec":
var p struct{ Command string }
ssh.Unmarshal(req.Payload, &p)
cmd = exec.Command("/bin/sh", "-c", p.Command)
case "shell":
cmd = exec.Command("/bin/sh")
case "subsystem":
var p struct{ Name string }
ssh.Unmarshal(req.Payload, &p)
server := c.sftpServer()
if p.Name != "sftp" || server == "" {
req.Reply(false, nil)
continue
}
cmd = exec.Command(server)
}
req.Reply(true, nil)
// Drain any remaining requests so the connection loop
// does not stall while the command runs.
go func() {
for req := range reqs {
req.Reply(false, nil)
}
}()
c.runCommand(ch, cmd)
return
}
ch.Close()
}
func (c *directFakeClient) runCommand(ch ssh.Channel, cmd *exec.Cmd) {
defer ch.Close()
cmd.Stdout = ch
cmd.Stderr = ch.Stderr()
stdin, err := cmd.StdinPipe()
if err != nil {
c.t.Errorf("fake sshd: %v", err)
return
}
go func() {
io.Copy(stdin, ch)
stdin.Close()
}()
err = cmd.Run()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
c.t.Errorf("fake sshd: running command: %v", err)
return
}
ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Code uint32 }{uint32(code)}))
}