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