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