Adds support for establishing SSH sessions over an existing "ControlMaster" [unix domain] socket in proxy mode. Details of the protocol can be found here: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux More details about ControlMaster sockets can be found here: https://linux.die.net/man/5/ssh_config Fixes golang/go#32958 Co-authored-by: Cyrus Katrak <ckatrak@slack-corp.com> Change-Id: Ia3ae8893262f5060ed3fadfcbe97619c9659145b Reviewed-on: https://go-review.googlesource.com/c/crypto/+/733040 Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Nicola Murino <nicola.murino@gmail.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com>
276 lines
6.5 KiB
Go
276 lines
6.5 KiB
Go
// Copyright 2026 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.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestControlClientHandshake(t *testing.T) {
|
|
reqs := [][]byte{
|
|
// Hello request.
|
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04},
|
|
// Client proxy request.
|
|
{0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00},
|
|
}
|
|
respsNormal := [][]byte{
|
|
// Hello response.
|
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04},
|
|
// Server proxy response.
|
|
{0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00},
|
|
}
|
|
for _, tt := range []struct {
|
|
name string
|
|
resps [][]byte
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "normal handshake",
|
|
resps: respsNormal,
|
|
},
|
|
{
|
|
name: "length greater than max",
|
|
resps: [][]byte{
|
|
{0xff, 0xff, 0xff, 0xff},
|
|
respsNormal[1],
|
|
},
|
|
expectedErr: "message length 4294967295 exceeds maximum",
|
|
},
|
|
{
|
|
name: "missing hello response",
|
|
resps: [][]byte{
|
|
{},
|
|
},
|
|
expectedErr: "use of closed network connection",
|
|
},
|
|
{
|
|
name: "hello response too short",
|
|
resps: [][]byte{
|
|
{0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00},
|
|
respsNormal[1],
|
|
},
|
|
expectedErr: "EOF",
|
|
},
|
|
{
|
|
name: "bad hello response type",
|
|
resps: [][]byte{
|
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04},
|
|
respsNormal[1],
|
|
},
|
|
expectedErr: "expected hello response, got 0",
|
|
},
|
|
{
|
|
name: "bad protocol version",
|
|
resps: [][]byte{
|
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00},
|
|
respsNormal[1],
|
|
},
|
|
expectedErr: "mux server has unsupported version 0",
|
|
},
|
|
{
|
|
name: "missing server proxy response",
|
|
resps: [][]byte{
|
|
respsNormal[0],
|
|
},
|
|
expectedErr: "use of closed network connection",
|
|
},
|
|
{
|
|
name: "server proxy response too short",
|
|
resps: [][]byte{
|
|
respsNormal[0],
|
|
{0x00, 0x00, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00},
|
|
},
|
|
expectedErr: "EOF",
|
|
},
|
|
{
|
|
name: "bad server proxy response type",
|
|
resps: [][]byte{
|
|
respsNormal[0],
|
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
},
|
|
expectedErr: "expected server proxy response, got 0",
|
|
},
|
|
{
|
|
name: "bad request id",
|
|
resps: [][]byte{
|
|
respsNormal[0],
|
|
{0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x01},
|
|
},
|
|
expectedErr: "expected request id 0, got 1",
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
done := make(chan error, 1)
|
|
|
|
ok := func() bool {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
go func() {
|
|
defer close(done)
|
|
_, _, _, err := NewControlClientConn(c2)
|
|
c2.Write([]byte{0}) // Dummy message to unblock the final read.
|
|
done <- err
|
|
}()
|
|
|
|
i := 0
|
|
for ; i < len(reqs) && i < len(tt.resps); i++ {
|
|
expected := reqs[i]
|
|
buf := make([]byte, len(expected))
|
|
if _, err := io.ReadFull(c1, buf); err != nil {
|
|
t.Errorf("error reading message %d: %v", i+1, err)
|
|
return false
|
|
}
|
|
if !bytes.Equal(buf, expected) {
|
|
t.Errorf(
|
|
"unexpected message %d: got %v, want %v",
|
|
i+1, buf, expected,
|
|
)
|
|
return false
|
|
}
|
|
_, err = c1.Write(tt.resps[i])
|
|
if err != nil {
|
|
t.Errorf("error writing message %d: %v", i+1, err)
|
|
return false
|
|
}
|
|
}
|
|
// Wait for the next message so that the final response can be read.
|
|
buf := make([]byte, 1)
|
|
c1.Read(buf)
|
|
return true
|
|
}()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
err := <-done
|
|
if tt.expectedErr != "" {
|
|
if err == nil || !strings.Contains(err.Error(), tt.expectedErr) {
|
|
t.Fatalf("got err %q; want err containing %q", err, tt.expectedErr)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("got err %q; want no err", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControlClientTransport(t *testing.T) {
|
|
type response struct {
|
|
status bool
|
|
payload []byte
|
|
err error
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
resp []byte
|
|
respStatus bool
|
|
respPayload []byte
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "successful request",
|
|
resp: []byte{0x00, 0x00, 0x00, 0x02, 0x00, 0x51},
|
|
respStatus: true,
|
|
},
|
|
{
|
|
name: "failed request",
|
|
resp: []byte{0x00, 0x00, 0x00, 0x02, 0x00, 0x52},
|
|
respStatus: false,
|
|
},
|
|
{
|
|
name: "short response",
|
|
resp: []byte{0x00, 0x00, 0x00, 0x00},
|
|
expectedErr: "EOF",
|
|
},
|
|
{
|
|
name: "response with payload",
|
|
resp: []byte{0x00, 0x00, 0x00, 0x05, 0x00, 0x51, 0x01, 0x02, 0x03},
|
|
respStatus: true,
|
|
respPayload: []byte{1, 2, 3},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c1, c2, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
defer c1.Close()
|
|
defer c2.Close()
|
|
|
|
// Handshake responses.
|
|
c1.Write([]byte{
|
|
0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04,
|
|
0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00,
|
|
})
|
|
|
|
conn, chans, reqs, err := NewControlClientConn(c2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := NewClient(conn, chans, reqs)
|
|
|
|
done := make(chan response, 1)
|
|
go func() {
|
|
defer close(done)
|
|
status, payload, err := client.SendRequest("hello", true, nil)
|
|
if err != nil {
|
|
done <- response{err: err}
|
|
return
|
|
}
|
|
done <- response{
|
|
status: status,
|
|
payload: payload,
|
|
}
|
|
}()
|
|
|
|
// Discard handshake.
|
|
io.CopyN(io.Discard, c1, 24)
|
|
|
|
expectedReq := []byte{
|
|
0x00, 0x00, 0x00, 0x0c, 0x00, 0x50,
|
|
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o',
|
|
0x01,
|
|
}
|
|
buf := make([]byte, len(expectedReq))
|
|
if _, err := io.ReadFull(c1, buf); err != nil {
|
|
t.Fatalf("reading request: %v", err)
|
|
}
|
|
if !bytes.Equal(buf, expectedReq) {
|
|
t.Fatalf("got request %v; want %v", buf, expectedReq)
|
|
}
|
|
|
|
c1.Write(tt.resp)
|
|
|
|
resp := <-done
|
|
if tt.expectedErr != "" {
|
|
if resp.err == nil || !strings.Contains(resp.err.Error(), tt.expectedErr) {
|
|
t.Fatalf("got err %q; want err containing %q", resp.err, tt.expectedErr)
|
|
}
|
|
return
|
|
}
|
|
if resp.err != nil {
|
|
t.Fatalf("got err %q; want no err", resp.err)
|
|
}
|
|
if resp.status != tt.respStatus {
|
|
t.Fatalf("got status %v; want %v", resp.status, tt.respStatus)
|
|
}
|
|
if !bytes.Equal(resp.payload, tt.respPayload) {
|
|
t.Errorf("got payload %v; want %v", resp.payload, tt.respPayload)
|
|
}
|
|
})
|
|
}
|
|
}
|