diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index cf573b5dff..019a4e2ee1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1823,13 +1823,18 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { this->complete_authentication_(); #ifdef USE_API_OUTGOING_CONNECTION - // Only honor the flag once a real key is active: with a PSK set, plaintext - // and the all-zeros provisioning PSK are rejected at the transport, so a - // client reaching this point has proven possession of the key. - if (msg.outgoing_connection_target && !this->flags_.outgoing_connection_target && - this->parent_->get_noise_ctx().has_psk()) { - this->flags_.outgoing_connection_target = true; - this->parent_->on_outgoing_target_client(this); + // Only honor the flag once a real key is active. With a PSK set, plaintext + // and the all-zeros provisioning PSK are rejected at the transport, and any + // session that predates key activation is force-closed when the key is + // applied (see update_noise_psk_), so a client reaching this point has + // proven possession of the key. + if (msg.outgoing_connection_target && !this->flags_.outgoing_connection_target) { + if (this->parent_->get_noise_ctx().has_psk()) { + this->flags_.outgoing_connection_target = true; + this->parent_->on_outgoing_target_client(this); + } else { + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Dial-back target refused; no key active")); + } } #endif diff --git a/esphome/components/api/api_outgoing_connection.cpp b/esphome/components/api/api_outgoing_connection.cpp index d3487e5bfe..4aee8dcf59 100644 --- a/esphome/components/api/api_outgoing_connection.cpp +++ b/esphome/components/api/api_outgoing_connection.cpp @@ -10,6 +10,9 @@ #include #include +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +#include +#endif namespace esphome::api { @@ -76,6 +79,7 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { } this->dial_socket_ = socket::socket_loop_monitored(((struct sockaddr *) &addr)->sa_family, SOCK_STREAM, IPPROTO_TCP); if (!this->dial_socket_ || this->dial_socket_->setblocking(false) != 0) { + ESP_LOGW(TAG, "Socket create failed: errno %d", errno); this->schedule_retry_(now); return; } @@ -84,7 +88,11 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { if (err == 0) { // Immediate success (possible for localhost) this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_)); - this->schedule_retry_(now); + if (this->dialed_conn_ == nullptr) { + this->schedule_retry_(now); + } else { + this->schedule_wait_(now, PRECONDITION_RETRY_MS); + } return; } if (errno != EINPROGRESS) { @@ -118,8 +126,13 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) { FD_ZERO(&writefds); FD_SET(fd, &writefds); struct timeval tv = {0, 0}; +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS + // LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly + int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv); +#else // Global-scope select: the entity namespace esphome::select shadows it here int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv); +#endif if (ret < 0) { ESP_LOGW(TAG, "Connect poll failed: errno %d", errno); this->schedule_retry_(now); @@ -136,9 +149,14 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) { return; } this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_)); - // Stay in a retry wait until the peer proves itself by sending a flagged - // hello; on_target_client() then resets to idle and clears the backoff. - this->schedule_retry_(now); + if (this->dialed_conn_ == nullptr) { + this->schedule_retry_(now); + return; + } + // The dial worked; hold without escalating until the peer proves itself + // with a flagged hello (on_target_client() resets to idle) or dies + // unproven (on_client_removed() escalates the backoff). + this->schedule_wait_(now, PRECONDITION_RETRY_MS); } void OutgoingConnectionManager::schedule_retry_(uint32_t now) { @@ -151,6 +169,13 @@ void OutgoingConnectionManager::schedule_retry_(uint32_t now) { this->backoff_ = std::min(this->backoff_ * 2, BACKOFF_MAX_MS); } +void OutgoingConnectionManager::on_client_removed(APIConnection *conn) { + if (conn == this->dialed_conn_) { + this->dialed_conn_ = nullptr; + this->schedule_retry_(App.get_loop_component_start_time()); + } +} + void OutgoingConnectionManager::on_target_client(APIConnection *conn) { // The target is connected; stop any dial in flight and reset the backoff. this->dial_socket_.reset(); diff --git a/esphome/components/api/api_outgoing_connection.h b/esphome/components/api/api_outgoing_connection.h index 1fc2c54c65..ece7a0882d 100644 --- a/esphome/components/api/api_outgoing_connection.h +++ b/esphome/components/api/api_outgoing_connection.h @@ -37,12 +37,10 @@ class OutgoingConnectionManager { /// Called when a key-verified client declares itself a dial-back target; /// the last such client wins as the remembered address. void on_target_client(APIConnection *conn); - /// Called for every removed connection so a dialed one stops gating re-dials - void on_client_removed(APIConnection *conn) { - if (conn == this->dialed_conn_) { - this->dialed_conn_ = nullptr; - } - } + /// Called for every removed connection so a dialed one stops gating + /// re-dials. A dialed connection dying without ever sending the flagged + /// hello is the unproven-peer case, so the backoff escalates here. + void on_client_removed(APIConnection *conn); void on_shutdown() { this->dial_socket_.reset(); } void dump_config() const; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index cec27639d3..b5d7b96f2b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -616,6 +616,10 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString if (!c->send_message(req)) { API_LOG_MSG_DROPPED(TAG, "Disconnect request"); } + // Force the disconnect: a session opened before the key was active + // (plaintext or zero-PSK) must not survive activation, or its peer + // could later claim capabilities reserved for key-verified clients. + c->flags_.next_close = true; } }); }