| // 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) | |
| } | |
| }) | |
| } | |
| } |