blob: 5ec7ed9c2789a6f1f009e1753dd5ebd27f079aa9 [file]
// 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)
})
}
}