Skip to content

Commit f80e595

Browse files
committed
test/refactor: add stalled-with-session test, unexport notePeerActivity
- New TestConnectionKeepAliveClosesStalledConnWithOpenSession exercises the channel-request path of dead-peer detection. Opens a session and has the client drain all request streams without replying; the server should still detect the stall via TimeIsUp and tear the transport down. Complements the existing no-session variant which exercises the global-request path. - Unexported NotePeerActivity -> notePeerActivity. It is an internal hook driven by this package's request loops; external callers have no reason to invoke it. Doing this before v1.2.0 is cut avoids a v2 break later.
1 parent e16d653 commit f80e595

4 files changed

Lines changed: 82 additions & 7 deletions

File tree

keepalive.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ func (ska *SessionKeepAlive) Reset() {
7171
}
7272
}
7373

74-
// NotePeerActivity records inbound traffic from the peer. It bumps
74+
// notePeerActivity records inbound traffic from the peer. It bumps
7575
// lastReceived (clearing the dead-peer deadline used by TimeIsUp) and
7676
// resets the ticker so the next probe fires `interval` after the most
7777
// recent activity. Matches OpenSSH sshd, which clears keep_alive_timeouts
7878
// on every successfully-received packet and defers the next probe on
79-
// inbound traffic.
80-
func (ska *SessionKeepAlive) NotePeerActivity() {
79+
// inbound traffic. Internal — driven by the package's request loops.
80+
func (ska *SessionKeepAlive) notePeerActivity() {
8181
ska.m.Lock()
8282
defer ska.m.Unlock()
8383
if ska.ticker != nil && !ska.closed {

server.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ func (srv *Server) HandleConn(newConn net.Conn) {
485485
onClose: openChans.remove,
486486
notePeerActivity: func() {
487487
if ka := ctx.KeepAlive(); ka != nil {
488-
ka.NotePeerActivity()
488+
ka.notePeerActivity()
489489
}
490490
},
491491
ctx: ctx,
@@ -526,7 +526,7 @@ func (srv *Server) connectionKeepAlive(
526526
// Do NOT Close() the SessionKeepAlive when this function returns: other
527527
// goroutines (Server.handleRequests, trackingNewChannel.Accept's
528528
// forwarder, and any in-flight probe goroutine spawned below) may still
529-
// be calling NotePeerActivity / Reset after we return. The ticker is
529+
// be calling notePeerActivity / Reset after we return. The ticker is
530530
// no longer referenced once HandleConn returns and is GC'd.
531531
keepAlive := ctx.KeepAlive()
532532

@@ -591,7 +591,7 @@ func (srv *Server) connectionKeepAlive(
591591
func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
592592
for req := range in {
593593
if ka := ctx.KeepAlive(); ka != nil {
594-
ka.NotePeerActivity()
594+
ka.notePeerActivity()
595595
}
596596
handler := srv.RequestHandlers[req.Type]
597597
if handler == nil {

server_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,81 @@ func TestConnectionKeepAliveClosesStalledConn(t *testing.T) {
213213
}
214214
}
215215

216+
// TestConnectionKeepAliveClosesStalledConnWithOpenSession is the counterpart
217+
// to TestConnectionKeepAliveClosesStalledConn: when a session channel is open
218+
// (so probes go as channel requests per client_alive_check), and the peer
219+
// stops replying to those probes, the connection-level keep-alive must still
220+
// detect the stall and tear the transport down. This exercises the
221+
// channel-request path of the deadline check; the no-session variant exercises
222+
// the global-request path.
223+
func TestConnectionKeepAliveClosesStalledConnWithOpenSession(t *testing.T) {
224+
t.Parallel()
225+
226+
closingFired := make(chan struct{})
227+
228+
srv := &Server{
229+
Handler: func(s Session) { <-s.Context().Done() },
230+
ClientAliveInterval: 100 * time.Millisecond,
231+
ClientAliveCountMax: 3,
232+
ConnectionClosingCallback: func(_ Context, _ *gossh.ServerConn) {
233+
close(closingFired)
234+
},
235+
}
236+
237+
l := newLocalTCPListener()
238+
defer func() { _ = l.Close() }()
239+
go func() { _ = srv.serveOnce(l) }()
240+
241+
cfg := &gossh.ClientConfig{
242+
User: "testuser",
243+
Auth: []gossh.AuthMethod{gossh.Password("testpass")},
244+
HostKeyCallback: gossh.InsecureIgnoreHostKey(), //nolint:gosec // test code
245+
}
246+
netConn, err := net.Dial("tcp", l.Addr().String())
247+
if err != nil {
248+
t.Fatalf("dial: %v", err)
249+
}
250+
sshConn, chans, reqs, err := gossh.NewClientConn(netConn, l.Addr().String(), cfg)
251+
if err != nil {
252+
t.Fatalf("NewClientConn: %v", err)
253+
}
254+
defer func() { _ = sshConn.Close() }()
255+
256+
ch, chReqs, err := sshConn.OpenChannel("session", nil)
257+
if err != nil {
258+
t.Fatalf("OpenChannel: %v", err)
259+
}
260+
defer func() { _ = ch.Close() }()
261+
262+
// Drain all three request streams (global, per-channel, and the
263+
// NewChannel stream) WITHOUT replying. The server's channel-typed
264+
// keepalive probes go on chReqs and will never get a reply, so
265+
// notePeerActivity is never called on the session-request side and
266+
// TimeIsUp eventually fires.
267+
go func() {
268+
for range chans { //nolint:revive // intentional drain
269+
}
270+
}()
271+
go func() {
272+
for req := range reqs {
273+
_ = req
274+
}
275+
}()
276+
go func() {
277+
for req := range chReqs {
278+
_ = req
279+
}
280+
}()
281+
282+
// 100ms * 3 = 300ms expected; allow generous slack for CI.
283+
select {
284+
case <-closingFired:
285+
case <-time.After(5 * time.Second):
286+
t.Fatal("ConnectionClosingCallback did not fire; " +
287+
"stalled-with-session client was not torn down by connection-level keep-alive")
288+
}
289+
}
290+
216291
// TestConnectionKeepAliveUsesChannelRequestWhenSessionOpen verifies that the
217292
// server's connection-level keepalive mirrors OpenSSH's client_alive_check():
218293
// while at least one channel is open it sends keepalive@openssh.com as a

session.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func (sess *session) handleRequests(_ Context, reqs <-chan *gossh.Request) {
294294
}()
295295
for req := range reqs {
296296
if ka := sess.ctx.KeepAlive(); ka != nil {
297-
ka.NotePeerActivity()
297+
ka.notePeerActivity()
298298
}
299299
switch req.Type {
300300
case "shell", "exec":

0 commit comments

Comments
 (0)