From 457e224e19a317407b16846269adeb6778981103 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Sep 2026 15:03:05 +0200 Subject: [PATCH] Track the established state explicitly so polling before connect() reports an error, and document the family contract --- .../components/socket/lwip_raw_tcp_impl.cpp | 30 +++++++++++-------- esphome/components/socket/lwip_raw_tcp_impl.h | 11 ++++--- esphome/components/socket/socket.h | 3 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index b6c37af336e..6746af9119c 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -60,9 +60,9 @@ static int lwip_err_to_errno(err_t err) { case ERR_MEM: return ENOMEM; case ERR_BUF: - return EAGAIN; // no free local port + return EAGAIN; // transient, e.g. no free local port case ERR_RTE: - return EHOSTUNREACH; // no route or no address yet + return EHOSTUNREACH; // no route, e.g. no address yet case ERR_VAL: case ERR_ARG: return EINVAL; @@ -449,7 +449,7 @@ err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) { // LWIP CALLBACK — same constraints as s_err_fn. err is always ERR_OK; a // failed connect arrives through s_err_fn instead. auto *arg_this = reinterpret_cast(arg); - arg_this->connect_err_ = 0; + arg_this->connect_err_ = EISCONN; esphome::wake_loop_any_context(); return ERR_OK; } @@ -460,8 +460,8 @@ int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) { errno = EBADF; return -1; } - if (this->connect_err_ != 0) { - errno = EALREADY; + if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) { + errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN; return -1; } ip_addr_t ip; @@ -499,15 +499,19 @@ ConnectPollResult LWIPRawImpl::poll_connect(int &err_out) const { err_out = this->connect_err_ == 0 || this->connect_err_ == EINPROGRESS ? ECONNRESET : this->connect_err_; return ConnectPollResult::CONNECT_POLL_ERROR; } - if (this->connect_err_ == EINPROGRESS) { - yield_to_sys(); // so the SYN-ACK is processed between polls - return ConnectPollResult::CONNECT_POLL_PENDING; + switch (this->connect_err_) { + case EINPROGRESS: + yield_to_sys(); // so the SYN-ACK is processed between polls + return ConnectPollResult::CONNECT_POLL_PENDING; + case EISCONN: + return ConnectPollResult::CONNECT_POLL_CONNECTED; + case 0: + err_out = EINVAL; // no connect was started + return ConnectPollResult::CONNECT_POLL_ERROR; + default: + err_out = this->connect_err_; + return ConnectPollResult::CONNECT_POLL_ERROR; } - if (this->connect_err_ != 0) { - err_out = this->connect_err_; - return ConnectPollResult::CONNECT_POLL_ERROR; - } - return ConnectPollResult::CONNECT_POLL_CONNECTED; } err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 7028723056a..5d3dd36536e 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -61,11 +61,11 @@ class LWIPRawCommon { bool nodelay_ = false; sa_family_t family_ = 0; uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s) - // State of a connect() started on this socket: 0 when none is pending (or - // it completed), EINPROGRESS while the SYN is out, otherwise the errno the + // State of connect() on this socket: 0 before one was started, EINPROGRESS + // while the SYN is out, EISCONN once established, otherwise the errno the // lwip callbacks recorded for its failure. Fits the padding byte here. uint8_t connect_err_ = 0; - static_assert(EINPROGRESS < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256, + static_assert(EINPROGRESS < 256 && EISCONN < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256, "connect_err_ stores errno values in a byte"); }; // The connect state must stay inside the padding: no socket, listening or @@ -97,6 +97,8 @@ class LWIPRawImpl : public LWIPRawCommon { } /// Start a non-blocking connect. Always returns -1 with errno EINPROGRESS /// when the SYN was queued; completion is reported by poll_connect(). + /// addr must be of the family the socket was created with; an IPv4 peer + /// on an AF_INET6 socket arrives as a v4-mapped sockaddr_in6. int connect(const struct sockaddr *addr, socklen_t addrlen); // Intentionally unlocked like ready(): reads one pointer and one byte that // the callbacks write in the order the checks depend on (error byte first, @@ -157,7 +159,8 @@ class LWIPRawImpl : public LWIPRawCommon { size_t rx_buf_offset_ = 0; bool rx_closed_ = false; }; -static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + sizeof(void *), +// rx_buf_, rx_buf_offset_, then rx_closed_ padded to a word +static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + 4, "LWIPRawImpl layout changed"); /// Listening socket implementation for LWIP raw TCP. diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 1e834948c34..86454d042cf 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -145,7 +145,8 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st /// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); -/// Check a non-blocking connect() for completion without blocking. On +/// Check a non-blocking connect() for completion without blocking. Only +/// meaningful after connect() returned -1 with errno EINPROGRESS. On /// CONNECT_POLL_ERROR, err_out holds the socket's SO_ERROR (or errno when the /// poll itself failed) on fd based implementations, and the failure recorded /// by the lwip callbacks on the raw lwip implementation.