Harden dial target validation and keep a fresh address on flash failure

This commit is contained in:
J. Nick Koston
2026-08-31 21:31:06 -04:00
parent bf380037a6
commit dae9a5d1a6
2 changed files with 17 additions and 9 deletions
@@ -22,11 +22,10 @@ void OutgoingConnectionManager::setup() {
#ifndef API_OUTGOING_CONNECTION_HOST
this->target_pref_ = global_preferences->make_preference<SavedOutgoingTarget>(629847102UL, true);
if (!this->target_pref_.load(&this->saved_)) {
ESP_LOGD(TAG, "No saved target");
this->saved_ = {};
this->saved_ = {}; // dump_config() reports the empty state
}
// Defend against a corrupt or truncated preference blob
this->saved_.host[socket::SOCKADDR_STR_LEN - 1] = '\0';
this->saved_.host[sizeof(this->saved_.host) - 1] = '\0';
// The macro is defined with a true/false value, not conditionally
#if !USE_NETWORK_IPV6
if (strchr(this->saved_.host, ':') != nullptr) {
@@ -92,7 +91,10 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
struct sockaddr_storage addr;
socklen_t addr_len =
socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, API_OUTGOING_CONNECTION_PORT);
if (addr_len == 0) {
// inet_addr() cannot signal failure: unparsable IPv4 text yields the
// broadcast address, which is never a valid target either
if (addr_len == 0 || (((struct sockaddr *) &addr)->sa_family == AF_INET &&
((struct sockaddr_in *) &addr)->sin_addr.s_addr == INADDR_NONE)) {
ESP_LOGW(TAG, "Invalid target %s", host);
this->schedule_retry_(now);
return;
@@ -131,8 +133,9 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
}
this->last_poll_ = now;
int fd = this->dial_socket_->get_fd();
if (fd >= FD_SETSIZE) {
ESP_LOGW(TAG, "fd %d out of select range", fd);
if (fd < 0 || fd >= FD_SETSIZE) {
// FD_SET on either is undefined behavior
ESP_LOGW(TAG, "fd %d unusable for select", fd);
this->schedule_retry_(now);
return;
}
@@ -233,12 +236,12 @@ void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
if (strcmp(target.host, this->saved_.host) == 0) {
return; // unchanged; avoid flash wear
}
// Use the fresh address this boot even if the flash write fails
this->saved_ = target;
if (!this->target_pref_.save(&target) || !global_preferences->sync()) {
// Keep the old value so the save is retried on the next flagged hello
ESP_LOGW(TAG, "Failed to save target");
return;
}
this->saved_ = target;
ESP_LOGD(TAG, "Saved %s as outgoing connection target", this->saved_.host);
#endif
}
@@ -20,10 +20,15 @@ namespace esphome::api {
class APIServer;
class APIConnection;
// Follows the build's address family (ifdef'd in socket/headers.h): toggling
// enable_ipv6 changes the blob size, load() rejects the old blob, and the
// target is simply relearned
static constexpr size_t SAVED_TARGET_HOST_LEN = socket::SOCKADDR_STR_LEN;
struct SavedOutgoingTarget {
// IP as text so the socket component's v4-mapped-IPv6 normalization is
// reused on both ends; empty = none remembered
char host[socket::SOCKADDR_STR_LEN];
char host[SAVED_TARGET_HOST_LEN];
} PACKED; // NOLINT
/// Dials out when no dial-back target client is connected. Only the TCP