From 89cd183a9f180d2d55ade80cd7e6eb2678425d24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:34:20 +0200 Subject: [PATCH] [noise] Gate the refill on the noise handshake, cover its loop time on ESP8266 The refill now waits only for api clients still in their noise handshake, not for any client that has yet to send its hello, so a stale half open connection cannot keep the slot empty for a minute. On ESP8266 the api server raises its blocking warning threshold to 80 ms in setup(), since the refill takes about 60 ms there and used to run inside every handshake anyway; and prepare_spare_ephemeral() clears the ready flag before filling, so a random source failure can never leave a mismatched pair. --- esphome/components/api/api_connection.h | 1 + esphome/components/api/api_frame_helper.h | 3 +++ esphome/components/api/api_server.cpp | 18 ++++++++++-------- esphome/components/noise/noise.cpp | 2 ++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a4c49dccf4..55f3bdc1f3 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -319,6 +319,7 @@ class APIConnection final : public APIServerConnectionBase { bool is_authenticated() { return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; } + bool is_handshake_complete() const { return this->helper_->is_handshake_complete(); } bool is_connection_setup() { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index ff8aa7834c..3319fad391 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -118,6 +118,9 @@ class APIFrameHelper { virtual APIError loop() = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); } + /// True once the transport handshake is done and the connection carries + /// api messages (immediately for plaintext, after the noise handshake). + bool is_handshake_complete() const { return this->state_ == State::DATA; } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { if (state_ == State::CLOSED) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1d9eb297a9..7fab3697aa 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -41,6 +41,12 @@ void APIServer::setup() { ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE +#ifdef USE_ESP8266 + // Refilling the spare ephemeral key blocks the loop for about 60 ms on + // this core; the same generation used to run inside every handshake. + // Cover it so the first refill does not log a blocking warning at boot. + this->warn_if_blocking_over_ = 8; // centiseconds +#endif // Always reserve the slot: flash preferences are positional on esp8266, so // a yaml key build must keep the layout of a runtime key build uint32_t hash = 88491486UL; @@ -193,19 +199,15 @@ void APIServer::loop() { #ifdef USE_API_NOISE // Refill the spare ephemeral key while nobody is waiting for it: not before -// the network is up, and not while an api client is still in its handshake -// (an OTA handshake is not visible here; it took the previous spare and -// pays the refill instead, no worse than generating its own key). The -// generation blocks the loop for about 60 ms on ESP8266, over the 50 ms -// blocking warning, so the api component's warning threshold ratchets up -// on the first refill; the same generation used to run inside the -// handshake, where it tripped the warning for longer. +// the network is up, and not while an api client is still in its noise +// handshake (an OTA handshake is not visible here; it took the previous +// spare and pays the refill instead, no worse than generating its own key). void APIServer::prepare_spare_ephemeral_() { if (noise::has_spare_ephemeral() || !network::is_connected()) { return; } for (auto &client : this->active_clients()) { - if (!client->is_connection_setup()) { + if (!client->is_handshake_complete()) { return; } } diff --git a/esphome/components/noise/noise.cpp b/esphome/components/noise/noise.cpp index f4743bbe03..2fc0f2787c 100644 --- a/esphome/components/noise/noise.cpp +++ b/esphome/components/noise/noise.cpp @@ -33,6 +33,8 @@ static bool spare_ephemeral_ready = false; // NOLINT(cppcoreguidel bool has_spare_ephemeral() { return spare_ephemeral_ready; } void prepare_spare_ephemeral() { + // A partial fill must never look ready + spare_ephemeral_ready = false; uint8_t *private_key = spare_ephemeral; uint8_t *public_key = spare_ephemeral + EPHEMERAL_PRIVATE_KEY_SIZE; // Same generation as noise-c's curve25519 backend: random bytes, X25519