Refresh the watchdog on every authenticated removal

This commit is contained in:
J. Nick Koston
2026-08-31 21:01:03 -04:00
parent 493c7265ab
commit bf380037a6
4 changed files with 48 additions and 9 deletions
+19 -1
View File
@@ -458,6 +458,22 @@ def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
return config
def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
"""A raw lwip_tcp socket can be selected explicitly on any platform."""
if CONF_OUTGOING_CONNECTION not in config:
return config
from esphome.components import socket
socket_conf = fv.full_config.get().get("socket") or {}
if socket_conf.get(socket.CONF_IMPLEMENTATION) == socket.IMPLEMENTATION_LWIP_TCP:
raise cv.Invalid(
"outgoing_connection is not supported with the lwip_tcp socket "
"implementation because it cannot make outgoing connections",
path=[CONF_OUTGOING_CONNECTION],
)
return config
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host silently dials 255.255.255.255 on a build without IPv6."""
if (
@@ -477,7 +493,9 @@ def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_esp8266_action_strings, _validate_outgoing_host_ipv6
_validate_esp8266_action_strings,
_validate_outgoing_socket_implementation,
_validate_outgoing_host_ipv6,
)
@@ -174,7 +174,9 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
void OutgoingConnectionManager::handoff_(APIServer *server, uint32_t now) {
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
if (this->dialed_conn_ == nullptr) {
this->schedule_retry_(now);
// Only preconditions (slot limit, key cleared) refuse the handoff; the
// peer is reachable, so do not escalate the backoff
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
return;
}
// Connected; dialed_conn_ gates further dialing until the session settles
+7 -6
View File
@@ -239,14 +239,15 @@ void APIServer::remove_client_(uint8_t client_index) {
// Last client disconnected - set warning and start tracking for reboot timeout
// (suppressed while provisioning is pending - see loop()).
// Refresh on every authenticated removal, not just the last one, so an
// unauthenticated straggler removed later (e.g. a port scan, or a dial to
// a host that accepts TCP but never speaks the API) cannot discard a
// healthy session's timestamp and trigger a spurious reboot
if (was_authenticated) {
this->last_connected_ = App.get_loop_component_start_time();
}
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
this->status_set_warning(LOG_STR("waiting for client connection"));
// A session that never authenticated (e.g. a port scan, or a dial to a
// host that accepts TCP but never speaks the API) must not reset the
// reboot watchdog
if (was_authenticated) {
this->last_connected_ = App.get_loop_component_start_time();
}
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
@@ -5,7 +5,11 @@ from pathlib import Path
import pytest
from esphome.components.api import CONFIG_SCHEMA, _validate_outgoing_host_ipv6
from esphome.components.api import (
CONFIG_SCHEMA,
_validate_outgoing_host_ipv6,
_validate_outgoing_socket_implementation,
)
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
import esphome.config_validation as cv
from esphome.const import PlatformFramework
@@ -68,6 +72,20 @@ def test_outgoing_connection_rejected_on_raw_lwip_platforms(
CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"}))
def test_outgoing_connection_rejects_lwip_tcp_socket(
set_core_config: SetCoreConfigCallable,
) -> None:
"""An explicit lwip_tcp socket selection is rejected at final validate."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data=ESP32_PLATFORM_DATA,
full_config={"socket": {"implementation": "lwip_tcp"}},
)
config = CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"}))
with pytest.raises(cv.Invalid, match="lwip_tcp"):
_validate_outgoing_socket_implementation(config)
def test_outgoing_connection_rejects_hostnames(
set_core_config: SetCoreConfigCallable,
) -> None: