| // Copyright 2014 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 ( |
| "context" |
| "io" |
| "net" |
| "testing" |
| "time" |
| ) |
| |
| func TestAutoPortListenBroken(t *testing.T) { |
| broken := "SSH-2.0-OpenSSH_5.9hh11" |
| works := "SSH-2.0-OpenSSH_6.1" |
| if !isBrokenOpenSSHVersion(broken) { |
| t.Errorf("version %q not marked as broken", broken) |
| } |
| if isBrokenOpenSSHVersion(works) { |
| t.Errorf("version %q marked as broken", works) |
| } |
| } |
| |
| func TestClientImplementsDialContext(t *testing.T) { |
| type ContextDialer interface { |
| DialContext(context.Context, string, string) (net.Conn, error) |
| } |
| // Belt and suspenders assertion, since package net does not |
| // declare a ContextDialer type. |
| var _ ContextDialer = &net.Dialer{} |
| var _ ContextDialer = &Client{} |
| } |
| |
| func TestClientDialContextWithCancel(t *testing.T) { |
| c := &Client{} |
| ctx, cancel := context.WithCancel(context.Background()) |
| cancel() |
| _, err := c.DialContext(ctx, "tcp", "localhost:1000") |
| if err != context.Canceled { |
| t.Errorf("DialContext: got nil error, expected %v", context.Canceled) |
| } |
| } |
| |
| func TestClientDialContextWithDeadline(t *testing.T) { |
| c := &Client{} |
| ctx, cancel := context.WithDeadline(context.Background(), time.Now()) |
| defer cancel() |
| _, err := c.DialContext(ctx, "tcp", "localhost:1000") |
| if err != context.DeadlineExceeded { |
| t.Errorf("DialContext: got nil error, expected %v", context.DeadlineExceeded) |
| } |
| } |
| |
| // forwardingPair establishes a client/server connection pair over an |
| // in-memory pipe. The server grants all global requests and accepts all |
| // channels opened by the client, delivering them on the returned channel. |
| func forwardingPair(t *testing.T) (*Client, *ServerConn, <-chan Channel) { |
| t.Helper() |
| |
| c1, c2, err := netPipe() |
| if err != nil { |
| t.Fatalf("netPipe: %v", err) |
| } |
| serverDone := make(chan struct{}) |
| t.Cleanup(func() { <-serverDone }) |
| t.Cleanup(func() { |
| c1.Close() |
| c2.Close() |
| }) |
| |
| serverConf := ServerConfig{ |
| NoClientAuth: true, |
| } |
| serverConf.AddHostKey(testSigners["rsa"]) |
| incoming := make(chan *ServerConn, 1) |
| serverChans := make(chan Channel, 1) |
| go func() { |
| defer close(serverDone) |
| conn, chans, reqs, err := NewServerConn(c1, &serverConf) |
| incoming <- conn |
| if err != nil { |
| t.Errorf("NewServerConn error: %v", err) |
| return |
| } |
| go func() { |
| for r := range reqs { |
| r.Reply(true, nil) |
| } |
| }() |
| for newCh := range chans { |
| ch, chanReqs, err := newCh.Accept() |
| if err != nil { |
| t.Errorf("Accept: %v", err) |
| continue |
| } |
| go DiscardRequests(chanReqs) |
| serverChans <- ch |
| } |
| }() |
| |
| conf := ClientConfig{ |
| HostKeyCallback: InsecureIgnoreHostKey(), |
| } |
| conn, chans, reqs, err := NewClientConn(c2, "", &conf) |
| if err != nil { |
| t.Fatalf("NewClientConn: %v", err) |
| } |
| client := NewClient(conn, chans, reqs) |
| t.Cleanup(func() { client.Close() }) |
| |
| server := <-incoming |
| if server == nil { |
| t.Fatal("Unable to get server") |
| } |
| t.Cleanup(func() { server.Close() }) |
| |
| return client, server, serverChans |
| } |
| |
| // floodStderr writes more than the default channel receive window (2 MiB) |
| // to w, which must be the stderr stream of an SSH channel. Stderr data is |
| // buffered and its window credit is only returned when the stream is |
| // read, so if the peer does not drain the stream the write blocks forever |
| // once the remote window is exhausted. |
| func floodStderr(t *testing.T, w io.Writer) { |
| t.Helper() |
| |
| const payload = 4 * 1024 * 1024 |
| done := make(chan error, 1) |
| go func() { |
| buf := make([]byte, 32*1024) |
| remaining := payload |
| for remaining > 0 { |
| n := min(len(buf), remaining) |
| nw, err := w.Write(buf[:n]) |
| if err != nil { |
| done <- err |
| return |
| } |
| remaining -= nw |
| } |
| done <- nil |
| }() |
| |
| select { |
| case err := <-done: |
| if err != nil { |
| t.Fatalf("write to stderr: %v", err) |
| } |
| case <-time.After(10 * time.Second): |
| t.Fatal("write to stderr blocked: the stderr stream is not drained") |
| } |
| } |
| |
| // verifyMainStream checks that the main stream is still usable after |
| // stderr traffic by sending a payload from the server channel and |
| // reading it back from the client connection. |
| func verifyMainStream(t *testing.T, serverCh Channel, clientConn net.Conn) { |
| t.Helper() |
| |
| want := []byte("main stream after stderr flood") |
| if _, err := serverCh.Write(want); err != nil { |
| t.Fatalf("write to main stream: %v", err) |
| } |
| got := make([]byte, len(want)) |
| if _, err := io.ReadFull(clientConn, got); err != nil { |
| t.Fatalf("read from main stream: %v", err) |
| } |
| if string(got) != string(want) { |
| t.Fatalf("read %q from main stream, want %q", got, want) |
| } |
| } |
| |
| func TestClientDialDiscardsStderr(t *testing.T) { |
| for _, network := range []string{"tcp", "unix"} { |
| t.Run(network, func(t *testing.T) { |
| client, _, serverChans := forwardingPair(t) |
| |
| addr := "127.0.0.1:8022" |
| if network == "unix" { |
| addr = "/tmp/forwarded.sock" |
| } |
| conn, err := client.Dial(network, addr) |
| if err != nil { |
| t.Fatalf("Dial(%q, %q): %v", network, addr, err) |
| } |
| defer conn.Close() |
| serverCh := <-serverChans |
| defer serverCh.Close() |
| |
| floodStderr(t, serverCh.Stderr()) |
| verifyMainStream(t, serverCh, conn) |
| }) |
| } |
| } |
| |
| func TestClientListenerDiscardsStderr(t *testing.T) { |
| for _, network := range []string{"tcp", "unix"} { |
| t.Run(network, func(t *testing.T) { |
| client, server, _ := forwardingPair(t) |
| |
| var ( |
| ln net.Listener |
| payload []byte |
| chType string |
| err error |
| ) |
| if network == "tcp" { |
| ln, err = client.Listen("tcp", "127.0.0.1:8022") |
| if err != nil { |
| t.Fatalf("Listen: %v", err) |
| } |
| chType = "forwarded-tcpip" |
| payload = Marshal(&forwardedTCPPayload{ |
| Addr: "127.0.0.1", |
| Port: 8022, |
| OriginAddr: "127.0.0.5", |
| OriginPort: 1234, |
| }) |
| } else { |
| ln, err = client.ListenUnix("/tmp/forwarded.sock") |
| if err != nil { |
| t.Fatalf("ListenUnix: %v", err) |
| } |
| chType = "forwarded-streamlocal@openssh.com" |
| payload = Marshal(&forwardedStreamLocalPayload{ |
| SocketPath: "/tmp/forwarded.sock", |
| }) |
| } |
| defer ln.Close() |
| |
| // OpenChannel does not return until the channel is |
| // accepted, so run Accept concurrently. |
| accepted := make(chan net.Conn, 1) |
| go func() { |
| conn, err := ln.Accept() |
| if err != nil { |
| t.Errorf("Accept: %v", err) |
| close(accepted) |
| return |
| } |
| accepted <- conn |
| }() |
| |
| serverCh, reqs, err := server.OpenChannel(chType, payload) |
| if err != nil { |
| t.Fatalf("OpenChannel(%q): %v", chType, err) |
| } |
| go DiscardRequests(reqs) |
| defer serverCh.Close() |
| |
| conn := <-accepted |
| if conn == nil { |
| t.Fatal("no connection accepted") |
| } |
| defer conn.Close() |
| |
| floodStderr(t, serverCh.Stderr()) |
| verifyMainStream(t, serverCh, conn) |
| }) |
| } |
| } |