diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index 10cbc981c7..853ff206c2 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -6,7 +6,6 @@ #include "esphome/components/network/util.h" #include "esphome/core/log.h" #include -#include namespace esphome::async_tcp { @@ -42,7 +41,15 @@ bool AsyncClient::connect(const char *host, uint16_t port) { return false; } - socket_->setblocking(false); + if (socket_->setblocking(false) != 0) { + // Capture before the log and close() clobber errno + const int saved_errno = errno; + ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno); + close(); + if (error_cb_) + error_cb_(error_arg_, this, saved_errno); + return false; + } int err = socket_->connect((struct sockaddr *) &addr, addrlen); if (err == 0) { @@ -97,45 +104,22 @@ void AsyncClient::loop() { return; if (connecting_) { - // For connecting, we need to check writability, not readability - // The Application's select() only monitors read FDs, so we do our own check here - // For ESP platforms lwip_select() might be faster, but this code isn't used - // on those platforms anyway. If it was, we'd fix the Application select() - // to report writability instead of doing it this way. - int fd = socket_->get_fd(); - if (fd < 0) { - ESP_LOGW(TAG, "Invalid socket fd"); - close(); - return; - } - - fd_set writefds; - FD_ZERO(&writefds); - FD_SET(fd, &writefds); - - struct timeval tv = {0, 0}; - int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv); - - if (ret > 0 && FD_ISSET(fd, &writefds)) { - int error = 0; - socklen_t len = sizeof(error); - if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) { + int err = 0; + switch (socket::poll_connect(*socket_, err)) { + case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING: + break; + case socket::ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED: connecting_ = false; connected_ = true; if (connect_cb_) connect_cb_(connect_arg_, this); - } else { - ESP_LOGW(TAG, "Connection failed: %d", error); + break; + case socket::ConnectPollResult::CONNECT_POLL_RESULT_ERROR: + ESP_LOGW(TAG, "Connection failed: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, error); - } - } else if (ret < 0) { - const int err = errno; - ESP_LOGE(TAG, "Select error: %d", err); - close(); - if (error_cb_) - error_cb_(error_arg_, this, err); + error_cb_(error_arg_, this, err); + break; } } else if (connected_) { // For connected sockets, use the Application's select() results diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 6248474624..22580343e8 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -459,7 +459,10 @@ void ESPHomeOTAComponent::handle_data_() { tv.tv_usec = 0; this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); - this->client_->setblocking(true); + if (this->client_->setblocking(true) != 0) { + this->log_socket_error_(LOG_STR("blocking")); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } // Acknowledge auth OK - 1 byte this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK); diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 0d4284f145..cec3b3498a 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -59,13 +59,15 @@ int BSDSocketImpl::close() { int BSDSocketImpl::setblocking(bool blocking) { int fl = ::fcntl(this->fd_, F_GETFL, 0); + if (fl < 0) { + return fl; + } if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - ::fcntl(this->fd_, F_SETFL, fl); - return 0; + return ::fcntl(this->fd_, F_SETFL, fl); } size_t BSDSocketImpl::getpeername_to(std::span buf) { diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 528d201799..f4597f2522 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -205,6 +205,13 @@ static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN #endif +/// Outcome of polling a non-blocking connect(); see socket::poll_connect(). +enum class ConnectPollResult : uint8_t { + CONNECT_POLL_RESULT_PENDING, + CONNECT_POLL_RESULT_CONNECTED, + CONNECT_POLL_RESULT_ERROR, +}; + } // namespace esphome::socket #endif diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 8d00dbede2..b411445b98 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -48,8 +48,33 @@ static const char *const TAG = "socket"; #ifdef USE_ESP8266 // optimistic_yield() rate limit in microseconds of CONT time; cheap when hot. static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000; +// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative +static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); } +#else +static inline void yield_to_sys() {} #endif +// errno for a failed tcp_* call +static int lwip_err_to_errno(err_t err) { + switch (err) { + case ERR_MEM: + return ENOMEM; + case ERR_BUF: + return EAGAIN; // transient, e.g. no free local port + case ERR_RTE: + return EHOSTUNREACH; // no route, e.g. no address yet + case ERR_VAL: + case ERR_ARG: + return EINVAL; + case ERR_USE: + return EADDRINUSE; + case ERR_ISCONN: + return EISCONN; + default: + return EIO; + } +} + // set to 1 to enable verbose lwip logging #if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if) #define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) @@ -62,8 +87,8 @@ static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000; // Must be called before destroying the object that tcp_arg points to — // tcp_abort() triggers the err callback synchronously, which would // otherwise call back into a partially-destroyed object. -// tcp_sent/tcp_poll are not cleared because this implementation -// never registers them. +// tcp_sent/tcp_poll are never registered and the connect callback cannot +// fire after abort or close, so neither is cleared. static void pcb_detach_abort(struct tcp_pcb *pcb) { tcp_arg(pcb, nullptr); tcp_recv(pcb, nullptr); @@ -76,8 +101,7 @@ static void pcb_detach_abort(struct tcp_pcb *pcb) { // After tcp_close(), the PCB remains alive during the TCP close handshake // (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP // would call recv/err on a destroyed socket object, corrupting the heap. -// tcp_sent/tcp_poll are not cleared because this implementation -// never registers them. +// Callbacks are left as in pcb_detach_abort(). // Returns ERR_OK on success; on failure the PCB is aborted instead. static err_t pcb_detach_close(struct tcp_pcb *pcb) { tcp_arg(pcb, nullptr); @@ -101,67 +125,51 @@ LWIPRawCommon::~LWIPRawCommon() { } } +bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const { + if (name == nullptr) { + errno = EINVAL; + return false; + } +#if LWIP_IPV6 + if (this->family_ == AF_INET6) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return false; + } + auto *addr6 = reinterpret_cast(name); + *port = ntohs(addr6->sin6_port); + inet6_addr_to_ip6addr(ip_2_ip6(ip), &addr6->sin6_addr); + // ANY lets bind() accept both families; connect() picks the concrete type + IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY); + return true; + } +#endif + if (this->family_ != AF_INET || addrlen < sizeof(sockaddr_in)) { + errno = EINVAL; + return false; + } + auto *addr4 = reinterpret_cast(name); + *port = ntohs(addr4->sin_port); + ip_addr_set_ip4_u32(ip, addr4->sin_addr.s_addr); + return true; +} + int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return -1; } - if (name == nullptr) { - errno = EINVAL; - return -1; - } ip_addr_t ip; - in_port_t port; -#if LWIP_IPV6 - if (this->family_ == AF_INET) { - if (addrlen < sizeof(sockaddr_in)) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.type = IPADDR_TYPE_V4; - ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); - } else if (this->family_ == AF_INET6) { - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return -1; - } - auto *addr6 = reinterpret_cast(name); - port = ntohs(addr6->sin6_port); - ip.type = IPADDR_TYPE_ANY; - memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); - } else { - errno = EINVAL; + uint16_t port; + if (!this->sockaddr2ip_(name, addrlen, &ip, &port)) { return -1; } -#else - if (this->family_ != AF_INET) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port); -#endif + LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port); err_t err = tcp_bind(this->pcb_, &ip, port); - if (err == ERR_USE) { - LWIP_LOG(" -> err ERR_USE"); - errno = EADDRINUSE; - return -1; - } - if (err == ERR_VAL) { - LWIP_LOG(" -> err ERR_VAL"); - errno = EINVAL; - return -1; - } if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - errno = EIO; + errno = lwip_err_to_errno(err); return -1; } return 0; @@ -178,7 +186,7 @@ int LWIPRawCommon::close() { this->pcb_ = nullptr; if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - errno = err == ERR_MEM ? ENOMEM : EIO; + errno = lwip_err_to_errno(err); return -1; } return 0; @@ -205,7 +213,7 @@ int LWIPRawCommon::shutdown(int how) { err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx); if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - errno = err == ERR_MEM ? ENOMEM : EIO; + errno = lwip_err_to_errno(err); return -1; } return 0; @@ -425,7 +433,82 @@ void LWIPRawImpl::s_err_fn(void *arg, err_t err) { // ERR_ABRT: aborted through tcp_abort or TCP timer auto *arg_this = reinterpret_cast(arg); ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); + if (arg_this->connect_err_ == EINPROGRESS) { + // Refused (RST) or SYN retries exhausted; written before pcb_ so + // poll_connect() never sees a dead pcb without its reason + arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT; + } arg_this->pcb_ = nullptr; + esphome::wake_loop_any_context(); +} + +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 + auto *arg_this = reinterpret_cast(arg); + arg_this->connect_err_ = EISCONN; + esphome::wake_loop_any_context(); + return ERR_OK; +} + +int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) { + LWIP_LOCK(); + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) { + errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN; + return -1; + } + ip_addr_t ip; + uint16_t port; + if (!this->sockaddr2ip_(addr, addrlen, &ip, &port)) { + return -1; + } +#if LWIP_IPV6 + // tcp_connect needs a concrete type; a remembered IPv4 peer arrives v4-mapped + if (IP_IS_ANY_TYPE_VAL(ip)) { + if (ip6_addr_isipv4mappedipv6(ip_2_ip6(&ip))) { + unmap_ipv4_mapped_ipv6(ip_2_ip4(&ip), ip_2_ip6(&ip)); + IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V4); + } else { + IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V6); + } + } +#endif + LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port); + err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = lwip_err_to_errno(err); + return -1; + } + this->connect_err_ = EINPROGRESS; + errno = EINPROGRESS; + return -1; +} + +ConnectPollResult LWIPRawImpl::poll_connect(int &err_out) const { + // pcb_ first; see the ordering note on the declaration + if (this->pcb_ == nullptr) { + // Only a recorded connect failure carries its own reason + const bool failed = this->connect_err_ == ECONNREFUSED || this->connect_err_ == ETIMEDOUT; + err_out = failed ? this->connect_err_ : ECONNRESET; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } + switch (this->connect_err_) { + case EINPROGRESS: + yield_to_sys(); // so the SYN-ACK is processed between polls + return ConnectPollResult::CONNECT_POLL_RESULT_PENDING; + case EISCONN: + return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED; + case 0: + err_out = EINVAL; // no connect was started + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + default: + err_out = this->connect_err_; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } } err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { @@ -540,14 +623,11 @@ ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) { } ssize_t LWIPRawImpl::read(void *buf, size_t len) { -#ifdef USE_ESP8266 - // Would block: yield to SYS so queued WiFi RX reaches lwip and this read - // may succeed. Without this, inbound segments can sit unprocessed for - // seconds while the main loop polls (CONT/SYS are cooperative on ESP8266). + // Let queued WiFi RX reach lwip first; otherwise inbound segments can + // sit unprocessed for seconds while the main loop polls if (this->waiting_for_data_()) { - optimistic_yield(ESP8266_YIELD_INTERVAL_US); + yield_to_sys(); } -#endif // See waiting_for_data_() for safety of unlocked reads. if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { this->wait_for_data_(); @@ -636,12 +716,10 @@ int LWIPRawImpl::internal_output_() { return -1; } } -#ifdef USE_ESP8266 // Flushed: yield to SYS so the queued segments reach the WiFi driver // instead of waiting seconds for an unrelated SYS slot. Callers only get // here after a successful tcp_write, so idle paths never yield. - optimistic_yield(ESP8266_YIELD_INTERVAL_US); -#endif + yield_to_sys(); return 0; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 917b5b2f7a..ed8057e289 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -50,6 +50,8 @@ class LWIPRawCommon { protected: int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen); + /// sockaddr of this socket's family to lwip address and port; false with errno on mismatch + bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const; // Member ordering optimized to minimize padding on 32-bit systems struct tcp_pcb *pcb_; @@ -58,7 +60,14 @@ 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) + // 0 before connect(), EINPROGRESS while pending, EISCONN once established, + // else the failure errno the callbacks recorded; fills the padding byte + uint8_t connect_err_ = 0; + 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 in the padding so no socket pays RAM for it +static_assert(sizeof(LWIPRawCommon) == sizeof(struct tcp_pcb *) + 4, "LWIPRawCommon grew past one word of flags"); /// Connected socket implementation for LWIP raw TCP. /// No virtual methods — callers always use the concrete type. @@ -83,6 +92,12 @@ class LWIPRawImpl : public LWIPRawCommon { errno = EOPNOTSUPP; return -1; } + /// Non-blocking: returns -1/EINPROGRESS once the SYN is queued, see poll_connect(). + /// addr must match the socket family; an IPv4 peer on AF_INET6 arrives v4-mapped. + int connect(const struct sockaddr *addr, socklen_t addrlen); + // Unlocked like ready(): the callbacks write the error byte before pcb_, + // so a torn read only costs one extra poll + ConnectPollResult poll_connect(int &err_out) const; ssize_t read(void *buf, size_t len); ssize_t readv(const struct iovec *iov, int iovcnt); ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) { @@ -120,6 +135,7 @@ class LWIPRawImpl : public LWIPRawCommon { static void s_err_fn(void *arg, err_t err); static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); + static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err); protected: // True when the socket could receive data but none has arrived yet. @@ -137,6 +153,9 @@ class LWIPRawImpl : public LWIPRawCommon { size_t rx_buf_offset_ = 0; bool rx_closed_ = false; }; +// 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. /// Separate from LWIPRawImpl — no virtual dispatch needed. diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index a6bd639c10..f87c5377e4 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -49,13 +49,15 @@ int LwIPSocketImpl::close() { int LwIPSocketImpl::setblocking(bool blocking) { int fl = lwip_fcntl(this->fd_, F_GETFL, 0); + if (fl < 0) { + return fl; + } if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - lwip_fcntl(this->fd_, F_SETFL, fl); - return 0; + return lwip_fcntl(this->fd_, F_SETFL, fl); } size_t LwIPSocketImpl::getpeername_to(std::span buf) { diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 567937a4db..f2d428af77 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -2,6 +2,9 @@ #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) #include #include +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +#include +#endif #include #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -165,7 +168,10 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_ #else // Use LWIP-specific functions ip6_addr_t ip6; - inet6_aton(ip_address, &ip6); + if (inet6_aton(ip_address, &ip6) == 0) { + errno = EINVAL; + return 0; + } memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); #endif return sizeof(sockaddr_in6); @@ -195,6 +201,47 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_ return sizeof(sockaddr_in); } +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +ConnectPollResult poll_connect(Socket &sock, int &err_out) { + int fd = sock.get_fd(); + if (fd < 0 || fd >= FD_SETSIZE) { + // FD_SET on either is undefined behavior + err_out = EBADF; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } + // Connect completion is a write event; the main loop only selects on reads + fd_set writefds; + 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) { + err_out = errno; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } + if (ret == 0) { + return ConnectPollResult::CONNECT_POLL_RESULT_PENDING; + } + int error = 0; + socklen_t len = sizeof(error); + if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) { + err_out = errno; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } + if (error != 0) { + err_out = error; + return ConnectPollResult::CONNECT_POLL_RESULT_ERROR; + } + return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED; +} +#endif + socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { #if USE_NETWORK_IPV6 if (addrlen < sizeof(sockaddr_in6)) { diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index eb8870786d..8ce392ff1d 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -145,6 +145,14 @@ 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); +/// Poll a connect() that returned EINPROGRESS. On error, err_out is SO_ERROR (or +/// errno) on fd implementations and the failure the callbacks recorded on raw lwip. +#ifdef USE_SOCKET_IMPL_LWIP_TCP +inline ConnectPollResult poll_connect(Socket &sock, int &err_out) { return sock.poll_connect(err_out); } +#else +ConnectPollResult poll_connect(Socket &sock, int &err_out); +#endif + /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span buf); diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 858516c746..f662cba4cf 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -13,7 +13,12 @@ void UDPComponent::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) for (const auto &address : this->addresses_) { struct sockaddr saddr {}; - socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_); + if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) { + ESP_LOGW(TAG, "Invalid address %s", address); + // A dropped address silently receives nothing; surface the misconfiguration + this->status_set_warning(LOG_STR("invalid address")); + continue; + } this->sockaddrs_.push_back(saddr); } // set up broadcast socket @@ -94,7 +99,11 @@ void UDPComponent::setup() { // 8266 and RP2040 `Duino for (const auto &address : this->addresses_) { auto ipaddr = IPAddress(); - ipaddr.fromString(address); + if (!ipaddr.fromString(address)) { + ESP_LOGW(TAG, "Invalid address %s", address); + this->status_set_warning(LOG_STR("invalid address")); + continue; + } this->ipaddrs_.push_back(ipaddr); } if (this->should_listen_) diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index e46b96c86a..fd94f56b75 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -34,6 +34,10 @@ void WakeOnLanButton::press_action() { struct sockaddr_storage saddr {}; auto addr_len = socket::set_sockaddr(reinterpret_cast(&saddr), sizeof(saddr), "255.255.255.255", this->port_); + if (addr_len == 0) { + ESP_LOGW(TAG, "Invalid broadcast address"); + return; + } uint8_t buffer[6 + sizeof this->macaddr_ * 16]; memcpy(buffer, PREFIX, sizeof(PREFIX)); for (size_t i = 0; i != 16; i++) { diff --git a/tests/components/socket/test-ipv6.rp2040-ard.yaml b/tests/components/socket/test-ipv6.rp2040-ard.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/socket/test-ipv6.rp2040-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml