1
0
mirror of https://github.com/smallstep/cli.git synced 2026-10-24 11:19:19 +03:00
Files
step-ca-cli/command/ssh/proxycommand_test.go
Carl Tashian 5b50ed3b49 Fix proxycommand hang when server closes before stdin closes
proxyDirect waited for both copy goroutines via wg.Wait(). When the
server closes the connection mid-session, the stdin->conn goroutine
stays blocked reading a stdin that never reaches EOF (the ssh client
holds the proxycommand's stdin open until it exits), deadlocking until
an OS-level timeout reaps the process.

Return as soon as either direction finishes instead. Extracted a
testable proxyDirectWithIO and added a regression test.

Fixes #1641

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:27:53 -07:00

56 lines
1.5 KiB
Go

package ssh
import (
"bytes"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// Test_proxyDirectWithIO_serverClosesBeforeStdin reproduces smallstep/cli#1641:
// when the server closes the connection before the client has closed stdin, the
// proxycommand must still return promptly. Previously it would block in
// wg.Wait() forever because the stdin->conn goroutine stayed blocked reading a
// stdin that never reaches EOF (the ssh client keeps it open until the
// proxycommand exits).
func Test_proxyDirectWithIO_serverClosesBeforeStdin(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
// Server sends some data and immediately closes the connection.
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
conn.Write([]byte("hello"))
conn.Close()
}()
host, port, err := net.SplitHostPort(ln.Addr().String())
require.NoError(t, err)
// stdin that never reaches EOF, simulating the ssh client keeping the
// proxycommand's stdin open for the lifetime of the session.
stdinR, stdinW := io.Pipe()
defer stdinW.Close() // write end intentionally left open during the call
var stdout bytes.Buffer
done := make(chan error, 1)
go func() {
done <- proxyDirectWithIO(host, port, stdinR, &stdout)
}()
select {
case err := <-done:
require.NoError(t, err)
require.Equal(t, "hello", stdout.String())
case <-time.After(5 * time.Second):
t.Fatal("proxyDirectWithIO did not return after the server closed the connection")
}
}