mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Decide the listener failure policy in setup, enforce the slot limit once, and reject lwip_tcp through the resolved socket implementation only
This commit is contained in:
@@ -296,15 +296,6 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
|
||||
if CONF_OUTGOING_CONNECTION not in config:
|
||||
return config
|
||||
# Platform default check here for a friendly early error; an explicit
|
||||
# lwip_tcp selection on other platforms is caught against the resolved
|
||||
# implementation in _validate_outgoing_socket_implementation
|
||||
if CORE.is_esp8266 or CORE.is_rp2:
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection is not supported on this platform because its "
|
||||
"socket layer cannot make outgoing connections",
|
||||
path=[CONF_OUTGOING_CONNECTION],
|
||||
)
|
||||
if CONF_ENCRYPTION not in config:
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection requires 'encryption' so the peer is verified by key",
|
||||
@@ -474,7 +465,11 @@ def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
|
||||
"""A raw lwip_tcp socket can be selected explicitly on any platform."""
|
||||
"""Reject the raw lwip_tcp socket, the only option on ESP8266 and RP2040.
|
||||
|
||||
Checked against the resolved implementation so an explicit selection on
|
||||
another platform is caught the same way as the platform default.
|
||||
"""
|
||||
if CONF_OUTGOING_CONNECTION not in config:
|
||||
return config
|
||||
from esphome.components import socket
|
||||
@@ -485,7 +480,8 @@ def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
|
||||
) in socket.IMPLEMENTATIONS_WITHOUT_CONNECT:
|
||||
raise cv.Invalid(
|
||||
f"outgoing_connection is not supported with the {impl} socket "
|
||||
"implementation because it cannot make outgoing connections",
|
||||
"implementation (the only one on ESP8266 and RP2040) because it "
|
||||
"cannot make outgoing connections",
|
||||
path=[CONF_OUTGOING_CONNECTION],
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
namespace esphome::api {
|
||||
|
||||
// uncomment to log raw packets
|
||||
// #define HELPER_LOG_PACKETS
|
||||
//#define HELPER_LOG_PACKETS
|
||||
|
||||
// Maximum message size limits to prevent OOM on constrained devices
|
||||
// Handshake messages are limited to a small size for security
|
||||
|
||||
@@ -178,13 +178,12 @@ void OutgoingConnectionManager::on_client_removed(APIConnection *conn, bool was_
|
||||
return;
|
||||
}
|
||||
this->dialed_conn_ = nullptr;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (was_authenticated) {
|
||||
// A working peer (e.g. a host: target that never sends the flag)
|
||||
// disconnected normally; state is IDLE, so loop() applies the delay
|
||||
this->backoff_ = BACKOFF_MIN_MS;
|
||||
} else {
|
||||
this->schedule_retry_(now);
|
||||
this->schedule_retry_(App.get_loop_component_start_time());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,6 @@ APIServer::APIServer() { global_api_server = this; }
|
||||
void APIServer::socket_failed_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
this->destroy_socket_();
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// Dial-out needs no listener; degrade instead of stopping the component
|
||||
this->status_set_error(LOG_STR("listen socket failed"));
|
||||
#else
|
||||
this->mark_failed();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIServer::create_listen_socket_() {
|
||||
@@ -98,14 +92,15 @@ void APIServer::setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// A dead listener degrades to an error status; dial-out still runs
|
||||
this->create_listen_socket_();
|
||||
#else
|
||||
if (!this->create_listen_socket_()) {
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// Dial-out needs no listener; degrade instead of stopping the component
|
||||
this->status_set_error(LOG_STR("listen socket failed"));
|
||||
#else
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
@@ -309,7 +304,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
|
||||
bool APIServer::add_client_(APIConnection *conn) {
|
||||
if (this->at_client_limit_()) {
|
||||
// Callers check first; enforce the array bound where the write happens
|
||||
// The accept path checks first to skip the allocation; the outgoing
|
||||
// handoff relies on this check
|
||||
ESP_LOGW(TAG, "Max connections (%d), dropping client", MAX_API_CONNECTIONS);
|
||||
delete conn;
|
||||
return false;
|
||||
@@ -328,11 +324,11 @@ bool APIServer::add_client_(APIConnection *conn) {
|
||||
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
|
||||
// Re-check at the handoff: inbound clients may have filled the slots and
|
||||
// the PSK may have been cleared (mark_outgoing() needs the noise helper)
|
||||
const bool at_limit = this->at_client_limit_();
|
||||
if (at_limit || !this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGW(TAG, "Dropping outgoing connection (%s)", at_limit ? "max connections" : "no key");
|
||||
// Re-check at the handoff: the PSK may have been cleared since the dial
|
||||
// started (mark_outgoing() needs the noise helper); add_client_ re-checks
|
||||
// the slot limit
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGW(TAG, "Dropping outgoing connection (no key)");
|
||||
return nullptr;
|
||||
}
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
|
||||
@@ -81,25 +81,24 @@ def test_outgoing_connection_requires_encryption(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"platform_framework",
|
||||
[PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO],
|
||||
("platform_framework", "platform_data"),
|
||||
[
|
||||
# The platform default on these two
|
||||
(PlatformFramework.ESP8266_ARDUINO, None),
|
||||
(PlatformFramework.RP2040_ARDUINO, None),
|
||||
# An explicit selection elsewhere
|
||||
(PlatformFramework.ESP32_IDF, ESP32_PLATFORM_DATA),
|
||||
],
|
||||
)
|
||||
def test_outgoing_connection_rejected_on_raw_lwip_platforms(
|
||||
def test_outgoing_connection_rejects_lwip_tcp(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
platform_framework: PlatformFramework,
|
||||
platform_data: ConfigType | None,
|
||||
) -> None:
|
||||
set_core_config(platform_framework)
|
||||
with pytest.raises(cv.Invalid, match="not supported on this platform"):
|
||||
CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"}))
|
||||
|
||||
|
||||
def test_outgoing_connection_rejects_lwip_tcp_selected_on_esp32(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""An explicit lwip_tcp selection is caught at final validate."""
|
||||
"""The resolved lwip_tcp socket is rejected at final validate."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data=ESP32_PLATFORM_DATA,
|
||||
platform_framework,
|
||||
platform_data=platform_data,
|
||||
full_config={"socket": {"implementation": "lwip_tcp"}},
|
||||
)
|
||||
config = CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"}))
|
||||
|
||||
Reference in New Issue
Block a user