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