[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.
This commit is contained in:
J. Nick Koston
2026-09-07 00:34:20 +02:00
parent 370cfb8898
commit 89cd183a9f
4 changed files with 16 additions and 8 deletions
+1
View File
@@ -319,6 +319,7 @@ class APIConnection final : public APIServerConnectionBase {
bool is_authenticated() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
}
bool is_handshake_complete() const { return this->helper_->is_handshake_complete(); }
bool is_connection_setup() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
@@ -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)
+10 -8
View File
@@ -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;
}
}
+2
View File
@@ -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