Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type wrappedConn struct {

func (c *wrappedConn) Write(p []byte) (n int, err error) {
n, err = c.Conn.Write(p)
atomic.AddInt32(&(c.written), int32(n)) //nolint:gosec // test code, overflow not possible
atomic.AddInt32(&c.written, int32(n)) //nolint:gosec // test code, overflow not possible
return
}

Expand All @@ -108,7 +108,7 @@ func TestConnWrapping(t *testing.T) {
if err := session.Shell(); err != nil {
t.Fatal(err)
}
if atomic.LoadInt32(&(wrapped.written)) == 0 {
if atomic.LoadInt32(&wrapped.written) == 0 {
t.Fatal("wrapped conn not written to")
}
}
13 changes: 8 additions & 5 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,22 +571,25 @@ func (srv *Server) connectionKeepAlive(
// next tick enforces the deadline, and if SendRequest
// hangs forever it will be unblocked when the TimeIsUp
// branch closes sshConn.
var err error
var (
ok bool
err error
)
ch := openChans.any()
if ch != nil {
_, err = ch.SendRequest(keepAliveRequestType, true, nil)
ok, err = ch.SendRequest(keepAliveRequestType, true, nil)
if err != nil {
openChans.remove(ch)
ch = nil
}
}
if ch == nil {
_, _, err = sshConn.SendRequest(keepAliveRequestType, true, nil)
ok, _, err = sshConn.SendRequest(keepAliveRequestType, true, nil)
}
if err == nil {
if err == nil && ok {
keepAlive.Reset()
} else {
log.Printf("ssh: keepalive request failed: %v", err)
log.Printf("ssh: keepalive request failed: ok=%t err=%v", ok, err)
}
}()
}
Expand Down
123 changes: 123 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,129 @@ func TestConnectionKeepAliveUsesChannelRequestWhenSessionOpen(t *testing.T) {
}
}

// TestConnectionKeepAliveNegativeGlobalReplyDoesNotReset verifies that a
// protocol-level negative response is not counted as a successful keepalive.
func TestConnectionKeepAliveNegativeGlobalReplyDoesNotReset(t *testing.T) {
t.Parallel()

closingFired := make(chan struct{})
srv := &Server{
Handler: func(_ Session) {},
ClientAliveInterval: 100 * time.Millisecond,
ClientAliveCountMax: 2,
ConnectionClosingCallback: func(_ Context, _ *gossh.ServerConn) {
close(closingFired)
},
}

l := newLocalTCPListener()
defer func() { _ = l.Close() }()
go func() { _ = srv.serveOnce(l) }()

cfg := &gossh.ClientConfig{
User: "testuser",
Auth: []gossh.AuthMethod{gossh.Password("testpass")},
HostKeyCallback: gossh.InsecureIgnoreHostKey(), //nolint:gosec // test code
}
netConn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
sshConn, chans, reqs, err := gossh.NewClientConn(netConn, l.Addr().String(), cfg)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
defer func() { _ = sshConn.Close() }()

go func() {
for range chans { //nolint:revive // intentional drain
}
}()
go func() {
for req := range reqs {
if req.Type == keepAliveRequestType {
_ = req.Reply(false, nil)
} else if req.WantReply {
_ = req.Reply(true, nil)
}
}
}()

select {
case <-closingFired:
case <-time.After(5 * time.Second):
t.Fatal("negative global keepalive replies incorrectly reset the deadline")
}
}

// TestConnectionKeepAliveNegativeChannelReplyDoesNotReset is the channel
// request counterpart to TestConnectionKeepAliveNegativeGlobalReplyDoesNotReset.
func TestConnectionKeepAliveNegativeChannelReplyDoesNotReset(t *testing.T) {
t.Parallel()

closingFired := make(chan struct{})
srv := &Server{
Handler: func(s Session) { <-s.Context().Done() },
ClientAliveInterval: 100 * time.Millisecond,
ClientAliveCountMax: 2,
ConnectionClosingCallback: func(_ Context, _ *gossh.ServerConn) {
close(closingFired)
},
}

l := newLocalTCPListener()
defer func() { _ = l.Close() }()
go func() { _ = srv.serveOnce(l) }()

cfg := &gossh.ClientConfig{
User: "testuser",
Auth: []gossh.AuthMethod{gossh.Password("testpass")},
HostKeyCallback: gossh.InsecureIgnoreHostKey(), //nolint:gosec // test code
}
netConn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
sshConn, chans, reqs, err := gossh.NewClientConn(netConn, l.Addr().String(), cfg)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
defer func() { _ = sshConn.Close() }()

go func() {
for range chans { //nolint:revive // intentional drain
}
}()
go func() {
for req := range reqs {
if req.WantReply {
_ = req.Reply(true, nil)
}
}
}()

ch, chReqs, err := sshConn.OpenChannel("session", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
defer func() { _ = ch.Close() }()
go func() {
for req := range chReqs {
if req.Type == keepAliveRequestType {
_ = req.Reply(false, nil)
} else if req.WantReply {
_ = req.Reply(true, nil)
}
}
}()

select {
case <-closingFired:
case <-time.After(5 * time.Second):
t.Fatal("negative channel keepalive replies incorrectly reset the deadline")
}
}

// TestConnectionKeepAlivePrunesClosedChannels verifies that the
// per-channel close hook prunes channels from the openChannelSet as
// soon as the client closes them, BEFORE the next keepalive probe
Expand Down
Loading