From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 8ADBD1FF0AA for ; Tue, 03 Nov 2026 16:52:35 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id A0E32215DA; Tue, 03 Nov 2026 16:52:34 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH http-server 1/1] apiserver: send TLS close notify at the end of streamed responses Date: Tue, 3 Nov 2026 16:52:23 +0100 Message-ID: <20261103155223.1960754-1-m.koeppl@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793721144682 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.220 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 4ZN6UQO2Y7WP5TI6J5AYAEC4M5H67KC7 X-Message-ID-Hash: 4ZN6UQO2Y7WP5TI6J5AYAEC4M5H67KC7 X-MailFrom: m.koeppl@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: A streamed response (e.g. for the single file restore download), has no Content-Length nor chunked encoding, so the client can only tell where the body ends by the connection being closed. The response accounced 'Connection: Keep-Alive' and, at the end of the stream, push_shutdown() shut down the socket directly, bypassing the TLS layer. The client then receives a TCP FIN without a TLS close notify [0]. Without a close notify, a client cannot tell a complete body from a truncated one. OpenSSL 3 reports this as an unexpected EOF, which makes curl fail with error 56 even though all data arrived. Even though browsers usually accept this, this affects users who might use this for scripted downloads from endpoints such as the file-restore one. Do not keep streamed response alive. finish_response() then disconnects the client once all data is written and, since commit 07e56cc ("fix unexpected EOF for client when closing TLS session"), client_do_disconnect() sends a close notify before shutting down the socket. Drop the push_shutdown() call, as it would shut down the socket before that if the write buffer is already empty. [0] https://www.rfc-editor.org/info/rfc9846/#name-closure-alerts Fixes: 389ad88 ("support streaming data form fh to client") Signed-off-by: Michael Köppl --- Ran into this while writing an E2E test that downloads a single file from a PBS backup through the file-restore API endpoint with curl, which failed with exit code 56 even though the file was complete. I tested this manually with curl, as well as the GUI file restore. On a 3-node cluster, downloaded a single file from a backup on node 2 proxied through node 1, without and with this patch applied. Node 1 receives the stream completely and without error in both cases. src/PVE/APIServer/AnyEvent.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm index 449b1c0..9258b6e 100644 --- a/src/PVE/APIServer/AnyEvent.pm +++ b/src/PVE/APIServer/AnyEvent.pm @@ -274,7 +274,6 @@ sub response_stream { # write out any remaining data $reqhdl->push_write($hdl->{rbuf}) if length($hdl->{rbuf}) > 0; $hdl->{rbuf} = ""; - $reqhdl->push_shutdown(); $self->finish_response($reqstate); } }; @@ -366,6 +365,8 @@ sub response { $resp->remove_header("Content-Length"); } + $reqstate->{keep_alive} = 0 if $stream_fh; + if ($reqstate->{keep_alive} > 0) { $resp->push_header('Connection' => 'Keep-Alive'); } else { -- 2.47.3