ESPHome 2026.10.2
Loading...
Searching...
No Matches
lwip_raw_tcp_impl.cpp
Go to the documentation of this file.
1#include "socket.h"
3
4#ifdef USE_SOCKET_IMPL_LWIP_TCP
5
6#include <cerrno>
7#include <cstring>
8#include <sys/time.h>
9
11#include "esphome/core/wake.h"
12#include "esphome/core/log.h"
13
14#ifdef USE_OTA_PLATFORM_ESPHOME
16#endif
17
18#ifdef USE_ESP8266
19#include <coredecls.h> // For esp_schedule()
20#elif defined(USE_RP2)
21#include <hardware/sync.h> // For __sev(), __wfe()
22#include <pico/time.h> // For add_alarm_in_ms(), cancel_alarm()
23#endif
24
25namespace esphome::socket {
26
27// ---- LWIP thread safety ----
28//
29// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1.
30// This means lwip callbacks (recv_fn, accept_fn, err_fn) run from a low-priority
31// user IRQ context, not the main loop (see low_priority_irq_handler() in pico-sdk
32// async_context_threadsafe_background.c). They can preempt main-loop code at any point.
33//
34// Without locking, this causes race conditions between recv_fn and read() on the
35// shared rx_buf_ pbuf chain — recv_fn calls pbuf_cat() while read() is freeing
36// nodes, leading to use-after-free and infinite-loop crashes. See esphome#10681.
37//
38// On ESP8266, lwip callbacks run from the SYS context which cooperates with user
39// code (CONT context) — they never preempt each other, so no locking is needed.
40//
41// esphome::LwIPLock is the platform-provided RAII guard (see helpers.h/helpers.cpp).
42// On RP2040, it acquires cyw43_arch_lwip_begin/end (WiFi) or ethernet_arch_lwip_begin/end
43// (Ethernet). On ESP8266, it's a no-op.
44#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT
45
46static const char *const TAG = "socket";
47
48#ifdef USE_ESP8266
49// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
50static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
51// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative
52static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); }
53#else
54static inline void yield_to_sys() {}
55#endif
56
57// errno for a failed tcp_* call
58static int lwip_err_to_errno(err_t err) {
59 switch (err) {
60 case ERR_MEM:
61 return ENOMEM;
62 case ERR_BUF:
63 return EAGAIN; // transient, e.g. no free local port
64 case ERR_RTE:
65 return EHOSTUNREACH; // no route, e.g. no address yet
66 case ERR_VAL:
67 case ERR_ARG:
68 return EINVAL;
69 case ERR_USE:
70 return EADDRINUSE;
71 case ERR_ISCONN:
72 return EISCONN;
73 default:
74 return EIO;
75 }
76}
77
78// set to 1 to enable verbose lwip logging
79#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
80#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
81#else
82#define LWIP_LOG(msg, ...)
83#endif
84
85// Clear arg, recv, and err callbacks, then abort a connected PCB.
86// Only valid for full tcp_pcb (not tcp_pcb_listen).
87// Must be called before destroying the object that tcp_arg points to —
88// tcp_abort() triggers the err callback synchronously, which would
89// otherwise call back into a partially-destroyed object.
90// tcp_sent/tcp_poll are never registered and the connect callback cannot
91// fire after abort or close, so neither is cleared.
92static void pcb_detach_abort(struct tcp_pcb *pcb) {
93 tcp_arg(pcb, nullptr);
94 tcp_recv(pcb, nullptr);
95 tcp_err(pcb, nullptr);
96 tcp_abort(pcb);
97}
98
99// Clear arg, recv, and err callbacks, then gracefully close a connected PCB.
100// Only valid for full tcp_pcb (not tcp_pcb_listen).
101// After tcp_close(), the PCB remains alive during the TCP close handshake
102// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
103// would call recv/err on a destroyed socket object, corrupting the heap.
104// Callbacks are left as in pcb_detach_abort().
105// Returns ERR_OK on success; on failure the PCB is aborted instead.
106static err_t pcb_detach_close(struct tcp_pcb *pcb) {
107 tcp_arg(pcb, nullptr);
108 tcp_recv(pcb, nullptr);
109 tcp_err(pcb, nullptr);
110 err_t err = tcp_close(pcb);
111 if (err != ERR_OK) {
112 tcp_abort(pcb);
113 }
114 return err;
115}
116
117// ---- LWIPRawCommon methods ----
118
120 LWIP_LOCK();
121 if (this->pcb_ != nullptr) {
122 LWIP_LOG("tcp_abort(%p)", this->pcb_);
123 pcb_detach_abort(this->pcb_);
124 this->pcb_ = nullptr;
125 }
126}
127
128bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const {
129 if (name == nullptr) {
130 errno = EINVAL;
131 return false;
132 }
133#if LWIP_IPV6
134 if (this->family_ == AF_INET6) {
135 if (addrlen < sizeof(sockaddr_in6)) {
136 errno = EINVAL;
137 return false;
138 }
139 auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
140 *port = ntohs(addr6->sin6_port);
141 inet6_addr_to_ip6addr(ip_2_ip6(ip), &addr6->sin6_addr);
142 // ANY lets bind() accept both families; connect() picks the concrete type
143 IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY);
144 return true;
145 }
146#endif
147 if (this->family_ != AF_INET || addrlen < sizeof(sockaddr_in)) {
148 errno = EINVAL;
149 return false;
150 }
151 auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
152 *port = ntohs(addr4->sin_port);
153 ip_addr_set_ip4_u32(ip, addr4->sin_addr.s_addr);
154 return true;
155}
156
157int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) {
158 LWIP_LOCK();
159 if (this->pcb_ == nullptr) {
160 errno = EBADF;
161 return -1;
162 }
163 ip_addr_t ip;
164 uint16_t port;
165 if (!this->sockaddr2ip_(name, addrlen, &ip, &port)) {
166 return -1;
167 }
168 LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
169 err_t err = tcp_bind(this->pcb_, &ip, port);
170 if (err != ERR_OK) {
171 LWIP_LOG(" -> err %d", err);
172 errno = lwip_err_to_errno(err);
173 return -1;
174 }
175 return 0;
176}
177
179 LWIP_LOCK();
180 if (this->pcb_ == nullptr) {
181 errno = ECONNRESET;
182 return -1;
183 }
184 LWIP_LOG("tcp_close(%p)", this->pcb_);
185 err_t err = pcb_detach_close(this->pcb_);
186 this->pcb_ = nullptr;
187 if (err != ERR_OK) {
188 LWIP_LOG(" -> err %d", err);
189 errno = lwip_err_to_errno(err);
190 return -1;
191 }
192 return 0;
193}
194
196 LWIP_LOCK();
197 if (this->pcb_ == nullptr) {
198 errno = ECONNRESET;
199 return -1;
200 }
201 bool shut_rx = false, shut_tx = false;
202 if (how == SHUT_RD) {
203 shut_rx = true;
204 } else if (how == SHUT_WR) {
205 shut_tx = true;
206 } else if (how == SHUT_RDWR) {
207 shut_rx = shut_tx = true;
208 } else {
209 errno = EINVAL;
210 return -1;
211 }
212 LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", this->pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0);
213 err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx);
214 if (err != ERR_OK) {
215 LWIP_LOG(" -> err %d", err);
216 errno = lwip_err_to_errno(err);
217 return -1;
218 }
219 return 0;
220}
221
222int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) {
223 LWIP_LOCK();
224 if (this->pcb_ == nullptr) {
225 errno = ECONNRESET;
226 return -1;
227 }
228 if (name == nullptr || addrlen == nullptr) {
229 errno = EINVAL;
230 return -1;
231 }
232 return this->ip2sockaddr_(&this->pcb_->remote_ip, this->pcb_->remote_port, name, addrlen);
233}
234
235int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) {
236 LWIP_LOCK();
237 if (this->pcb_ == nullptr) {
238 errno = ECONNRESET;
239 return -1;
240 }
241 if (name == nullptr || addrlen == nullptr) {
242 errno = EINVAL;
243 return -1;
244 }
245 return this->ip2sockaddr_(&this->pcb_->local_ip, this->pcb_->local_port, name, addrlen);
246}
247
248size_t LWIPRawCommon::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
249 struct sockaddr_storage storage;
250 socklen_t len = sizeof(storage);
251 if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
252 buf[0] = '\0';
253 return 0;
254 }
255 return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
256}
257
258size_t LWIPRawCommon::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
259 struct sockaddr_storage storage;
260 socklen_t len = sizeof(storage);
261 if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
262 buf[0] = '\0';
263 return 0;
264 }
265 return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
266}
267
268int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
269 LWIP_LOCK();
270 if (this->pcb_ == nullptr) {
271 errno = ECONNRESET;
272 return -1;
273 }
274 if (optlen == nullptr || optval == nullptr) {
275 errno = EINVAL;
276 return -1;
277 }
278 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
279 if (*optlen < 4) {
280 errno = EINVAL;
281 return -1;
282 }
283 // lwip doesn't seem to have this feature. Don't send an error
284 // to prevent warnings
285 *reinterpret_cast<int *>(optval) = 1;
286 *optlen = 4;
287 return 0;
288 }
289 if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
290 if (*optlen < sizeof(struct timeval)) {
291 errno = EINVAL;
292 return -1;
293 }
294 uint32_t ms = this->recv_timeout_cs_ * 10;
295 auto *tv = reinterpret_cast<struct timeval *>(optval);
296 tv->tv_sec = ms / 1000;
297 tv->tv_usec = (ms % 1000) * 1000;
298 *optlen = sizeof(struct timeval);
299 return 0;
300 }
301 if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
302 if (*optlen < 4) {
303 errno = EINVAL;
304 return -1;
305 }
306 *reinterpret_cast<int *>(optval) = this->nodelay_;
307 *optlen = 4;
308 return 0;
309 }
310
311 errno = EINVAL;
312 return -1;
313}
314
315int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
316 LWIP_LOCK();
317 if (this->pcb_ == nullptr) {
318 errno = ECONNRESET;
319 return -1;
320 }
321 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
322 if (optlen != 4) {
323 errno = EINVAL;
324 return -1;
325 }
326 // lwip doesn't seem to have this feature. Don't send an error
327 // to prevent warnings
328 return 0;
329 }
330 if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
331 if (optlen < sizeof(struct timeval)) {
332 errno = EINVAL;
333 return -1;
334 }
335 const auto *tv = reinterpret_cast<const struct timeval *>(optval);
336 uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000;
337 uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond
338 this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs);
339 return 0;
340 }
341 if (level == SOL_SOCKET && optname == SO_SNDTIMEO) {
342 // Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op.
343 return 0;
344 }
345 if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
346 if (optlen != 4) {
347 errno = EINVAL;
348 return -1;
349 }
350 int val = *reinterpret_cast<const int *>(optval);
351 this->nodelay_ = val;
352 return 0;
353 }
354
355 errno = EINVAL;
356 return -1;
357}
358
359int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {
360 if (this->family_ == AF_INET) {
361 if (*addrlen < sizeof(struct sockaddr_in)) {
362 errno = EINVAL;
363 return -1;
364 }
365
366 struct sockaddr_in *addr = reinterpret_cast<struct sockaddr_in *>(name);
367 addr->sin_family = AF_INET;
368 *addrlen = addr->sin_len = sizeof(struct sockaddr_in);
369 addr->sin_port = port;
370 inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip));
371 return 0;
372 }
373#if LWIP_IPV6
374 else if (this->family_ == AF_INET6) {
375 if (*addrlen < sizeof(struct sockaddr_in6)) {
376 errno = EINVAL;
377 return -1;
378 }
379
380 struct sockaddr_in6 *addr = reinterpret_cast<struct sockaddr_in6 *>(name);
381 addr->sin6_family = AF_INET6;
382 *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6);
383 addr->sin6_port = port;
384
385 // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6.
386 if (IP_IS_V4(ip)) {
387 ip_addr_t mapped;
388 ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip));
389 inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped));
390 } else {
391 inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip));
392 }
393 return 0;
394 }
395#endif
396 return -1;
397}
398
399// ---- LWIPRawImpl methods ----
400
402 LWIP_LOCK();
403 // Free any received pbufs that LWIP transferred ownership of via recv_fn.
404 // tcp_abort() in the base destructor won't free these since LWIP considers
405 // ownership transferred once the recv callback accepts them.
406 if (this->rx_buf_ != nullptr) {
407 pbuf_free(this->rx_buf_);
408 this->rx_buf_ = nullptr;
409 }
410 // Base class destructor handles pcb_ cleanup via tcp_abort
411}
412
413void LWIPRawImpl::init(struct pbuf *initial_rx, bool initial_rx_closed) {
414 LWIP_LOCK();
415 LWIP_LOG("init(%p)", this->pcb_);
416 tcp_arg(this->pcb_, this);
417 tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn);
418 tcp_err(this->pcb_, LWIPRawImpl::s_err_fn);
419 if (initial_rx != nullptr) {
420 this->rx_buf_ = initial_rx;
421 this->rx_buf_offset_ = 0;
422 }
423 this->rx_closed_ = initial_rx_closed;
424}
425
426void LWIPRawImpl::s_err_fn(void *arg, err_t err) {
427 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
428 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
429 // No LWIP_LOCK() needed — lwip core already holds the async_context lock.
430 //
431 // pcb is already freed when this callback is called
432 // ERR_RST: connection was reset by remote host
433 // ERR_ABRT: aborted through tcp_abort or TCP timer
434 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
435 ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
436 if (arg_this->connect_err_ == EINPROGRESS) {
437 // Refused (RST) or SYN retries exhausted; written before pcb_ so
438 // poll_connect() never sees a dead pcb without its reason
439 arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT;
440 }
441 arg_this->pcb_ = nullptr;
443}
444
445err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) {
446 // LWIP CALLBACK, same constraints as s_err_fn; err is always ERR_OK
447 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
448 arg_this->connect_err_ = EISCONN;
450 return ERR_OK;
451}
452
453int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) {
454 LWIP_LOCK();
455 if (this->pcb_ == nullptr) {
456 errno = EBADF;
457 return -1;
458 }
459 if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) {
460 errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN;
461 return -1;
462 }
463 ip_addr_t ip;
464 uint16_t port;
465 if (!this->sockaddr2ip_(addr, addrlen, &ip, &port)) {
466 return -1;
467 }
468#if LWIP_IPV6
469 // tcp_connect needs a concrete type; a remembered IPv4 peer arrives v4-mapped
470 if (IP_IS_ANY_TYPE_VAL(ip)) {
471 if (ip6_addr_isipv4mappedipv6(ip_2_ip6(&ip))) {
472 unmap_ipv4_mapped_ipv6(ip_2_ip4(&ip), ip_2_ip6(&ip));
473 IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V4);
474 } else {
475 IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V6);
476 }
477 }
478#endif
479 LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
480 err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn);
481 if (err != ERR_OK) {
482 LWIP_LOG(" -> err %d", err);
483 errno = lwip_err_to_errno(err);
484 return -1;
485 }
486 this->connect_err_ = EINPROGRESS;
487 errno = EINPROGRESS;
488 return -1;
489}
490
492 // pcb_ first; see the ordering note on the declaration
493 if (this->pcb_ == nullptr) {
494 // Only a recorded connect failure carries its own reason
495 const bool failed = this->connect_err_ == ECONNREFUSED || this->connect_err_ == ETIMEDOUT;
496 err_out = failed ? this->connect_err_ : ECONNRESET;
498 }
499 switch (this->connect_err_) {
500 case EINPROGRESS:
501 yield_to_sys(); // so the SYN-ACK is processed between polls
503 case EISCONN:
505 case 0:
506 err_out = EINVAL; // no connect was started
508 default:
509 err_out = this->connect_err_;
511 }
512}
513
514err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
515 auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
516 return arg_this->recv_fn(pb, err);
517}
518
519err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) {
520 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
521 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
522 LWIP_LOG("recv(pb=%p err=%d)", pb, err);
523 if (err != 0) {
524 // "An error code if there has been an error receiving Only return ERR_ABRT if you have
525 // called tcp_abort from within the callback function!"
526 if (pb != nullptr) {
527 pbuf_free(pb);
528 }
529 this->rx_closed_ = true;
530 return ERR_OK;
531 }
532 if (pb == nullptr) {
533 this->rx_closed_ = true;
534 return ERR_OK;
535 }
536 if (this->rx_buf_ == nullptr) {
537 // no need to copy because lwIP gave control of it to us
538 this->rx_buf_ = pb;
539 this->rx_buf_offset_ = 0;
540 } else {
541 pbuf_cat(this->rx_buf_, pb);
542 }
543 // Wake the main loop immediately so it can process the received data.
545 return ERR_OK;
546}
547
549 // Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040
550 // (needs async_context lock).
551 //
552 // Loop until data arrives, connection closes, or the full timeout elapses.
553 // wakeable_delay() may return early due to any wake source,
554 // so we re-enter for the remaining time.
555 uint32_t timeout_ms = this->recv_timeout_cs_ * 10;
556 uint32_t start = millis();
557 while (this->waiting_for_data_()) {
558 uint32_t elapsed = millis() - start;
559 if (elapsed >= timeout_ms)
560 break;
561 esphome::internal::wakeable_delay(timeout_ms - elapsed);
562 }
563}
564
566 // Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf.
567 if (this->pcb_ == nullptr) {
568 errno = ECONNRESET;
569 return -1;
570 }
571 if (this->rx_closed_ && this->rx_buf_ == nullptr) {
572 return 0;
573 }
574 if (len == 0) {
575 return 0;
576 }
577 if (this->rx_buf_ == nullptr) {
578 errno = EWOULDBLOCK;
579 return -1;
580 }
581
582 size_t read = 0;
583 uint8_t *buf8 = reinterpret_cast<uint8_t *>(buf);
584 while (len && this->rx_buf_ != nullptr) {
585 size_t pb_len = this->rx_buf_->len;
586 size_t pb_left = pb_len - this->rx_buf_offset_;
587 if (pb_left == 0)
588 break;
589 size_t copysize = std::min(len, pb_left);
590 memcpy(buf8, reinterpret_cast<uint8_t *>(this->rx_buf_->payload) + this->rx_buf_offset_, copysize);
591
592 if (pb_left == copysize) {
593 // full pb copied, free it
594 if (this->rx_buf_->next == nullptr) {
595 // last buffer in chain
596 pbuf_free(this->rx_buf_);
597 this->rx_buf_ = nullptr;
598 this->rx_buf_offset_ = 0;
599 } else {
600 auto *old_buf = this->rx_buf_;
601 this->rx_buf_ = this->rx_buf_->next;
602 pbuf_ref(this->rx_buf_);
603 pbuf_free(old_buf);
604 this->rx_buf_offset_ = 0;
605 }
606 } else {
607 this->rx_buf_offset_ += copysize;
608 }
609 LWIP_LOG("tcp_recved(%p %u)", this->pcb_, copysize);
610 tcp_recved(this->pcb_, copysize);
611
612 buf8 += copysize;
613 len -= copysize;
614 read += copysize;
615 }
616
617 if (read == 0) {
618 errno = EWOULDBLOCK;
619 return -1;
620 }
621
622 return read;
623}
624
625ssize_t LWIPRawImpl::read(void *buf, size_t len) {
626 // Let queued WiFi RX reach lwip first; otherwise inbound segments can
627 // sit unprocessed for seconds while the main loop polls
628 if (this->waiting_for_data_()) {
629 yield_to_sys();
630 }
631 // See waiting_for_data_() for safety of unlocked reads.
632 if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
633 this->wait_for_data_();
634 }
635
636 LWIP_LOCK();
637 return this->read_locked_(buf, len);
638}
639
640ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) {
641 // No ESP8266 SYS yield here: only read() needs it today. If a consumer
642 // switches to scatter-gather reads, mirror the yield from read().
643 // See waiting_for_data_() for safety of unlocked reads.
644 if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
645 this->wait_for_data_();
646 }
647
648 LWIP_LOCK(); // Hold for entire scatter-gather operation
649 ssize_t ret = 0;
650 for (int i = 0; i < iovcnt; i++) {
651 ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
652 if (err == -1) {
653 if (ret != 0) {
654 // if we already read some don't return an error
655 break;
656 }
657 return err;
658 }
659 ret += err;
660 if ((size_t) err != iov[i].iov_len)
661 break;
662 }
663 return ret;
664}
665
666ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) {
667 LWIP_LOCK();
668 if (this->pcb_ == nullptr) {
669 errno = ECONNRESET;
670 return -1;
671 }
672 if (len == 0)
673 return 0;
674 if (buf == nullptr) {
675 errno = EINVAL;
676 return 0;
677 }
678 auto space = tcp_sndbuf(this->pcb_);
679 if (space == 0) {
680 errno = EWOULDBLOCK;
681 return -1;
682 }
683 size_t to_send = std::min((size_t) space, len);
684 LWIP_LOG("tcp_write(%p buf=%p %u)", this->pcb_, buf, to_send);
685 err_t err = tcp_write(this->pcb_, buf, to_send, TCP_WRITE_FLAG_COPY);
686 if (err == ERR_MEM) {
687 LWIP_LOG(" -> err ERR_MEM");
688 errno = EWOULDBLOCK;
689 return -1;
690 }
691 if (err != ERR_OK) {
692 LWIP_LOG(" -> err %d", err);
693 errno = ECONNRESET;
694 return -1;
695 }
696 return to_send;
697}
698
700 LWIP_LOCK();
701 if (this->pcb_ == nullptr) {
702 errno = ECONNRESET;
703 return -1;
704 }
705 LWIP_LOG("tcp_output(%p)", this->pcb_);
706 err_t err = tcp_output(this->pcb_);
707 if (err != ERR_OK) {
708 LWIP_LOG(" -> err %d", err);
709 // ERR_ABRT: sometimes lwip returns it for no apparent reason; the
710 // connection works fine afterwards, and back with ESPAsyncTCP we
711 // indirectly also ignored this error, so treat it as success for
712 // flush purposes too.
713 // FIXME: figure out where this is returned and what it means in this context
714 if (err != ERR_ABRT) {
715 errno = ECONNRESET;
716 return -1;
717 }
718 }
719 // Flushed: yield to SYS so the queued segments reach the WiFi driver
720 // instead of waiting seconds for an unrelated SYS slot. Callers only get
721 // here after a successful tcp_write, so idle paths never yield.
722 yield_to_sys();
723 return 0;
724}
725
726ssize_t LWIPRawImpl::write(const void *buf, size_t len) {
727 LWIP_LOCK(); // Hold for write + optional output
728 ssize_t written = this->internal_write_(buf, len);
729 if (written == -1)
730 return -1;
731 if (written == 0) {
732 // no need to output if nothing written
733 return 0;
734 }
735 if (this->nodelay_) {
736 int err = this->internal_output_();
737 if (err == -1)
738 return -1;
739 }
740 return written;
741}
742
743ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) {
744 LWIP_LOCK(); // Hold for entire scatter-gather operation
745 ssize_t written = 0;
746 for (int i = 0; i < iovcnt; i++) {
747 ssize_t err = this->internal_write_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
748 if (err == -1) {
749 if (written != 0) {
750 // if we already read some don't return an error
751 break;
752 }
753 return err;
754 }
755 written += err;
756 if ((size_t) err != iov[i].iov_len)
757 break;
758 }
759 if (written == 0) {
760 // no need to output if nothing written
761 return 0;
762 }
763 if (this->nodelay_) {
764 int err = this->internal_output_();
765 if (err == -1)
766 return -1;
767 }
768 return written;
769}
770
771// ---- LWIPRawListenImpl methods ----
772
774 LWIP_LOCK();
775 // Abort any queued PCBs that were never accepted by the main loop.
776 for (uint8_t i = 0; i < this->accepted_socket_count_; i++) {
777 auto &entry = this->accepted_pcbs_[i];
778 if (entry.pcb != nullptr) {
779 pcb_detach_abort(entry.pcb);
780 entry.pcb = nullptr;
781 }
782 if (entry.rx_buf != nullptr) {
783 pbuf_free(entry.rx_buf);
784 entry.rx_buf = nullptr;
785 }
786 }
787 this->accepted_socket_count_ = 0;
788 // Listen PCBs must use tcp_close(), not tcp_abort().
789 // tcp_abandon() asserts pcb->state != LISTEN and would access
790 // fields that don't exist in the smaller tcp_pcb_listen struct.
791 // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access
792 // fields that only exist in the full tcp_pcb, not tcp_pcb_listen.
793 // tcp_close() on a listen PCB is synchronous (frees immediately),
794 // so there are no async callbacks to worry about.
795 // Close here and null pcb_ so the base destructor skips tcp_abort.
796 if (this->pcb_ != nullptr) {
797 tcp_close(this->pcb_);
798 this->pcb_ = nullptr;
799 }
800}
801
803 LWIP_LOCK();
804 LWIP_LOG("init(%p)", this->pcb_);
805 tcp_arg(this->pcb_, this);
806 tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn);
807 tcp_err(this->pcb_, LWIPRawListenImpl::s_err_fn);
808}
809
810void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) {
811 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
812 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
813 auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
814 ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
815 arg_this->pcb_ = nullptr;
816}
817
818void LWIPRawListenImpl::s_queued_err_fn(void *arg, err_t err) {
819 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
820 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
821 // Called when a queued (not yet accepted) PCB errors — e.g., remote sent RST.
822 // The PCB is already freed by lwip. Null our pointer so accept() skips it.
823 (void) err;
824 auto *entry = reinterpret_cast<QueuedPcb *>(arg);
825 entry->pcb = nullptr;
826 // Don't free rx_buf here — accept() will clean it up when it sees pcb==nullptr
827}
828
829err_t LWIPRawListenImpl::s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
830 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
831 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
832 // Temporary recv callback for PCBs queued between accept_fn_ and accept().
833 // Without this, lwip's default tcp_recv_null handler would ACK and drop the data,
834 // causing the API handshake to silently fail (client sends Hello, server never sees it).
835 (void) pcb;
836 auto *entry = reinterpret_cast<QueuedPcb *>(arg);
837 if (pb == nullptr || err != ERR_OK) {
838 // Remote closed or error
839 if (pb != nullptr) {
840 pbuf_free(pb);
841 }
842 entry->rx_closed = true;
843 return ERR_OK;
844 }
845 // Buffer the data — tcp_recved() is deferred to read() after accept() creates the socket.
846 if (entry->rx_buf == nullptr) {
847 entry->rx_buf = pb;
848 } else {
849 pbuf_cat(entry->rx_buf, pb);
850 }
851 return ERR_OK;
852}
853
854err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
855 auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
856 return arg_this->accept_fn_(newpcb, err);
857}
858
859std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) {
860 LWIP_LOCK();
861 if (this->pcb_ == nullptr) {
862 errno = EBADF;
863 return nullptr;
864 }
865 // Dequeue front entry, skipping any null entries (PCBs freed by lwip while queued).
866 // The error callback nulled their pcb pointers; clean up buffered data and discard.
867 while (this->accepted_socket_count_ > 0) {
868 QueuedPcb entry = this->accepted_pcbs_[0];
869 // Shift remaining entries forward, updating tcp_arg pointers as we go.
870 // Safe because we hold LWIP_LOCK, so err/recv callbacks can't fire during the update.
871 for (uint8_t i = 1; i < this->accepted_socket_count_; i++) {
872 this->accepted_pcbs_[i - 1] = this->accepted_pcbs_[i];
873 if (this->accepted_pcbs_[i - 1].pcb != nullptr) {
874 tcp_arg(this->accepted_pcbs_[i - 1].pcb, &this->accepted_pcbs_[i - 1]);
875 }
876 }
877 this->accepted_pcbs_[this->accepted_socket_count_ - 1] = {};
878 this->accepted_socket_count_--;
879 if (entry.pcb == nullptr) {
880 // PCB was freed by lwip (RST/timeout) while queued — discard and try next
881 if (entry.rx_buf != nullptr) {
882 pbuf_free(entry.rx_buf);
883 }
884 continue;
885 }
886 LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_);
887 // Create socket wrapper on the main loop (not in accept callback) to avoid
888 // heap allocation in IRQ context on RP2040. Transfer any data received while queued.
889 auto sock = make_unique<LWIPRawImpl>(this->family_, entry.pcb);
890 sock->init(entry.rx_buf, entry.rx_closed);
891 if (addr != nullptr) {
892 sock->getpeername(addr, addrlen);
893 }
894 LWIP_LOG("accept(%p)", sock.get());
895 return sock;
896 }
897 errno = EWOULDBLOCK;
898 return nullptr;
899}
900
902 LWIP_LOCK();
903 if (this->pcb_ == nullptr) {
904 errno = EBADF;
905 return -1;
906 }
907 LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", this->pcb_, backlog);
908 struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(this->pcb_, backlog);
909 if (listen_pcb == nullptr) {
910 tcp_abort(this->pcb_);
911 this->pcb_ = nullptr;
912 errno = EOPNOTSUPP;
913 return -1;
914 }
915 // tcp_listen reallocates the pcb, replace ours
916 this->pcb_ = listen_pcb;
917 // set callbacks on new pcb
918 LWIP_LOG("tcp_arg(%p)", this->pcb_);
919 tcp_arg(this->pcb_, this);
920 tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn);
921 // Note: tcp_err() is NOT re-registered here. tcp_listen_with_backlog() converts the
922 // full tcp_pcb to a smaller tcp_pcb_listen struct that lacks the errf field.
923 // Calling tcp_err() on a listen PCB writes past the struct boundary (undefined behavior).
924 return 0;
925}
926
927err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
928 // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
929 // No heap allocation allowed — malloc is not IRQ-safe (see #14687).
930 LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
931 if (err != ERR_OK || newpcb == nullptr) {
932 // "An error code if there has been an error accepting. Only return ERR_ABRT if you have
933 // called tcp_abort from within the callback function!"
934 // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
935 // nothing to do here, we just don't push it to the queue
936 return ERR_OK;
937 }
938 // Check if we've reached the maximum accept queue size
939 if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
940 LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_);
941 // Abort the connection when queue is full
942 tcp_abort(newpcb);
943 // Must return ERR_ABRT since we called tcp_abort()
944 return ERR_ABRT;
945 }
946 // Store the raw PCB — LWIPRawImpl creation is deferred to the main-loop accept().
947 // This avoids heap allocation in this callback, which is unsafe from IRQ context on RP2040.
948 uint8_t idx = this->accepted_socket_count_++;
949 this->accepted_pcbs_[idx] = {newpcb, nullptr, false};
950 // Register temporary callbacks so that while the PCB is queued:
951 // - err: nulls our pointer if the connection errors (RST, timeout)
952 // - recv: buffers any data that arrives before accept() creates the LWIPRawImpl
953 // (without this, lwip's default tcp_recv_null would ACK and drop the data)
954 // tcp_arg points to our queue entry; accept() updates these pointers after shifting.
955 tcp_arg(newpcb, &this->accepted_pcbs_[idx]);
956 tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
957 tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
958 LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
959#ifdef USE_OTA_PLATFORM_ESPHOME
960 // Must run before wake_loop_any_context() so flags are visible when the main task wakes.
962#endif
963 // Wake the main loop immediately so it can accept the new connection.
965 return ERR_OK;
966}
967
968// ---- Factory functions ----
969
970std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
971 if (type != SOCK_STREAM) {
972 ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP");
973 errno = EPROTOTYPE;
974 return nullptr;
975 }
976 LWIP_LOCK();
977 auto *pcb = tcp_new();
978 if (pcb == nullptr)
979 return nullptr;
980 auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
981 sock->init();
982 return std::unique_ptr<Socket>{sock};
983}
984
985std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol) {
986 // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable
987 return socket(domain, type, protocol);
988}
989
990std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
991 if (type != SOCK_STREAM) {
992 ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP");
993 errno = EPROTOTYPE;
994 return nullptr;
995 }
996 LWIP_LOCK();
997 auto *pcb = tcp_new();
998 if (pcb == nullptr)
999 return nullptr;
1000 auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
1001 sock->init();
1002 return std::unique_ptr<ListenSocket>{sock};
1003}
1004
1005std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
1006 // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable
1007 return socket_listen(domain, type, protocol);
1008}
1009
1010#undef LWIP_LOCK
1011
1012} // namespace esphome::socket
1013
1014#endif // USE_SOCKET_IMPL_LWIP_TCP
int getsockname(struct sockaddr *name, socklen_t *addrlen)
size_t getsockname_to(std::span< char, SOCKADDR_STR_LEN > buf)
Format local address into a fixed-size buffer (no heap allocation)
int bind(const struct sockaddr *name, socklen_t addrlen)
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const
sockaddr of this socket's family to lwip address and port; false with errno on mismatch
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
int getsockopt(int level, int optname, void *optval, socklen_t *optlen)
int getpeername(struct sockaddr *name, socklen_t *addrlen)
size_t getpeername_to(std::span< char, SOCKADDR_STR_LEN > buf)
Format peer address into a fixed-size buffer (no heap allocation)
Connected socket implementation for LWIP raw TCP.
ssize_t read_locked_(void *buf, size_t len)
ConnectPollResult poll_connect(int &err_out) const
int connect(const struct sockaddr *addr, socklen_t addrlen)
Non-blocking: returns -1/EINPROGRESS once the SYN is queued, see poll_connect().
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err)
void init(struct pbuf *initial_rx=nullptr, bool initial_rx_closed=false)
ssize_t readv(const struct iovec *iov, int iovcnt)
static void s_err_fn(void *arg, err_t err)
err_t recv_fn(struct pbuf *pb, err_t err)
ssize_t internal_write_(const void *buf, size_t len)
ssize_t write(const void *buf, size_t len)
ssize_t read(void *buf, size_t len)
static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err)
ssize_t writev(const struct iovec *iov, int iovcnt)
Listening socket implementation for LWIP raw TCP.
static void s_err_fn(void *arg, err_t err)
std::unique_ptr< LWIPRawImpl > accept(struct sockaddr *addr, socklen_t *addrlen)
uint16_t type
uint32_t socklen_t
Definition headers.h:99
uint8_t sa_family_t
Definition headers.h:59
__int64 ssize_t
Definition httplib.h:178
@ IPADDR_TYPE_V6
Definition ip_address.h:32
@ IPADDR_TYPE_V4
Definition ip_address.h:32
int ret
void esphome_wake_ota_component_any_context()
mopeka_std_values val[3]
void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms)
Host wakeable_delay uses select() over the registered fds — defined in wake_host.cpp.
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span< char, SOCKADDR_STR_LEN > buf)
Format sockaddr into caller-provided buffer, returns length written (excluding null)
Definition socket.cpp:101
std::unique_ptr< ListenSocket > socket_listen(int domain, int type, int protocol)
Create a listening socket of the given domain, type and protocol.
ConnectPollResult
Outcome of polling a non-blocking connect(); see socket::poll_connect().
Definition headers.h:209
std::unique_ptr< ListenSocket > socket_listen_loop_monitored(int domain, int type, int protocol)
std::unique_ptr< Socket > socket(int domain, int type, int protocol)
Create a socket of the given domain, type and protocol.
std::unique_ptr< Socket > socket_loop_monitored(int domain, int type, int protocol)
Create a socket and monitor it for data in the main loop.
void esphome_wake_ota_component_any_context()
const void size_t len
Definition hal.h:64
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
int written
Definition helpers.h:1193
void IRAM_ATTR wake_loop_any_context()
IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp.
static void uint32_t
uint8_t sin6_len
Definition headers.h:75
in_port_t sin6_port
Definition headers.h:77
struct in6_addr sin6_addr
Definition headers.h:79
sa_family_t sin6_family
Definition headers.h:76
struct in_addr sin_addr
Definition headers.h:67
uint8_t sin_len
Definition headers.h:64
sa_family_t sin_family
Definition headers.h:65
in_port_t sin_port
Definition headers.h:66
Platform-specific main loop wake primitives.