From 12720140b5ee17155fda2d7ea9fca3dfe8503f90 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Fri, 9 Oct 2026 12:02:08 -0300 Subject: [PATCH 1/2] fix: quic keepalive --- tests/test_connection.nim | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_connection.nim b/tests/test_connection.nim index 51af172..bd49652 100644 --- a/tests/test_connection.nim +++ b/tests/test_connection.nim @@ -454,6 +454,37 @@ proc runChunkedReadTest(peers: ConnectedPeers, bufSize, payloadSize: int) {.asyn await allFuturesRaising(sender(), receiver()).wait(streamTimeout) +proc idleConnectionSurvives(address: TransportAddress): Future[bool] {.async.} = + let idleTimeout = 3.seconds + let client = QuicClient.new( + makeTLSConfig(), + engineConfig = QuicEngineConfig( + idleTimeout: Opt.some(idleTimeout), pingPeriod: Opt.some(1.seconds) + ), + ) + let server = QuicServer.new( + makeTLSConfig(), engineConfig = QuicEngineConfig(idleTimeout: Opt.some(idleTimeout)) + ) + let listener = server.listen(address) + defer: + await allFutures(client.stop(), listener.stop()) + let accepting = listener.accept() + let outgoingConn = await client.dial(listener.localAddress()) + let incomingConn = await accepting + + # Wait past the 7 s of NEW_CONNECTION_ID frames, which also keep the connection alive. + await sleepAsync(idleTimeout * 4) + + let survived = + not outgoingConn.closedFuture().finished() and + not incomingConn.closedFuture().finished() + + outgoingConn.close() + incomingConn.close() + await allFutures(outgoingConn.closedFuture(), incomingConn.closedFuture()) + + survived + suite "connection": teardown: checkTrackers() @@ -473,6 +504,9 @@ suite "connection": asyncTest "connection survives client address migration": await runClientAddressMigrationTest(AutoAddressIP4) + asyncTest "connection without streams outlives the idle timeout": + check await idleConnectionSurvives(AutoAddressIP4) + asyncTest "multiple concurrent stream opens": await runConcurrentStreamOpenTest(AutoAddressIP4) From cb1b76b4c3a74bc9764120f1b6461ae1529ce905 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Fri, 9 Oct 2026 12:08:34 -0300 Subject: [PATCH 2/2] chore(lsquic): bump to keepalive fix --- libs/lsquic | 2 +- lsquic/engine_config.nim | 1 + tests/test_connection.nim | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/libs/lsquic b/libs/lsquic index 47b1aae..fd0d75c 160000 --- a/libs/lsquic +++ b/libs/lsquic @@ -1 +1 @@ -Subproject commit 47b1aaeb5b6eea0df8975dec4fa6d12a249723c7 +Subproject commit fd0d75c6584eb4c20dfa8b4b865bfe3daf5a2e52 diff --git a/lsquic/engine_config.nim b/lsquic/engine_config.nim index a3ba9f2..8e99486 100644 --- a/lsquic/engine_config.nim +++ b/lsquic/engine_config.nim @@ -101,6 +101,7 @@ proc initSettings*( settings.es_base_plpmtu = 1280 settings.es_init_max_streams_bidi = 100 settings.es_honor_prst = 1 + settings.es_ping_period = 15 # both sides ping, as in go-libp2p settings.es_max_cfcw = if server: 1536 * 1024 diff --git a/tests/test_connection.nim b/tests/test_connection.nim index bd49652..5ed8603 100644 --- a/tests/test_connection.nim +++ b/tests/test_connection.nim @@ -454,7 +454,29 @@ proc runChunkedReadTest(peers: ConnectedPeers, bufSize, payloadSize: int) {.asyn await allFuturesRaising(sender(), receiver()).wait(streamTimeout) -proc idleConnectionSurvives(address: TransportAddress): Future[bool] {.async.} = +proc exchangeOnStream(outgoingConn, incomingConn: Connection): Future[bool] {.async.} = + let payload = @[1'u8, 2, 3] + let sender = proc() {.async.} = + let stream = await outgoingConn.openStream() + await stream.write(payload) + await stream.close() + discard await readStreamTillEOF(stream) + await stream.closed.wait() + + let receiver = proc(): Future[seq[byte]] {.async.} = + let stream = await incomingConn.incomingStream() + let received = await readStreamTillEOF(stream) + await stream.close() + await stream.closed.wait() + received + + let receiving = receiver() + await allFuturesRaising(sender(), receiving).wait(streamTimeout) + receiving.read() == payload + +proc idleConnectionSurvives( + address: TransportAddress, closeStreamFirst: bool +): Future[bool] {.async.} = let idleTimeout = 3.seconds let client = QuicClient.new( makeTLSConfig(), @@ -472,12 +494,16 @@ proc idleConnectionSurvives(address: TransportAddress): Future[bool] {.async.} = let outgoingConn = await client.dial(listener.localAddress()) let incomingConn = await accepting + let usedBefore = + not closeStreamFirst or await exchangeOnStream(outgoingConn, incomingConn) + # Wait past the 7 s of NEW_CONNECTION_ID frames, which also keep the connection alive. await sleepAsync(idleTimeout * 4) let survived = - not outgoingConn.closedFuture().finished() and - not incomingConn.closedFuture().finished() + usedBefore and not outgoingConn.closedFuture().finished() and + not incomingConn.closedFuture().finished() and + await exchangeOnStream(outgoingConn, incomingConn) outgoingConn.close() incomingConn.close() @@ -505,7 +531,10 @@ suite "connection": await runClientAddressMigrationTest(AutoAddressIP4) asyncTest "connection without streams outlives the idle timeout": - check await idleConnectionSurvives(AutoAddressIP4) + check await idleConnectionSurvives(AutoAddressIP4, closeStreamFirst = false) + + asyncTest "connection outlives the idle timeout after its last stream closes": + check await idleConnectionSurvives(AutoAddressIP4, closeStreamFirst = true) asyncTest "multiple concurrent stream opens": await runConcurrentStreamOpenTest(AutoAddressIP4)