diff --git a/cli/ssh.go b/cli/ssh.go index e6c7271503..4bcc40d4c0 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -1647,7 +1647,11 @@ func WithTestOnlyCoderConnectDialer(ctx context.Context, dialer coderConnectDial func testOrDefaultDialer(ctx context.Context) coderConnectDialer { dialer, ok := ctx.Value(coderConnectDialerContextKey{}).(coderConnectDialer) if !ok || dialer == nil { - return &net.Dialer{} + // Timeout prevents hanging on broken tunnels (OS default is very long). + return &net.Dialer{ + Timeout: 5 * time.Second, + KeepAlive: 30 * time.Second, + } } return dialer } diff --git a/cli/ssh_internal_test.go b/cli/ssh_internal_test.go index e0fe9bca65..62977c50a9 100644 --- a/cli/ssh_internal_test.go +++ b/cli/ssh_internal_test.go @@ -243,6 +243,26 @@ func TestCloserStack_PushAfterClose_ConnClosed(t *testing.T) { require.Equal(t, []*fakeCloser{fc}, *closes, "should close conn on failed push") } +func TestCoderConnectDialer_DefaultTimeout(t *testing.T) { + t.Parallel() + ctx := context.Background() + + dialer := testOrDefaultDialer(ctx) + d, ok := dialer.(*net.Dialer) + require.True(t, ok, "expected *net.Dialer") + assert.Equal(t, 5*time.Second, d.Timeout) + assert.Equal(t, 30*time.Second, d.KeepAlive) +} + +func TestCoderConnectDialer_Overridden(t *testing.T) { + t.Parallel() + custom := &net.Dialer{Timeout: 99 * time.Second} + ctx := WithTestOnlyCoderConnectDialer(context.Background(), custom) + + dialer := testOrDefaultDialer(ctx) + assert.Equal(t, custom, dialer) +} + func TestCoderConnectStdio(t *testing.T) { t.Parallel()