ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
socket.cpp
Go to the documentation of this file.
1#include "socket.h"
2#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
3#include <cerrno>
4#include <cstring>
5#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
6#include <sys/select.h>
7#endif
8#include <string>
9#include "esphome/core/log.h"
12#ifdef USE_HOST
13#include "esphome/core/wake.h"
14#endif
15#if defined(USE_HOST)
16#include <ifaddrs.h>
17#include <net/if.h>
18#elif defined(USE_ZEPHYR)
19#ifndef CLANG_TIDY // NCS 2.9.2 net_if.h has a BUILD_ASSERT clang rejects; gone in NCS 3.4
20#include <zephyr/net/net_if.h>
21#endif
22#else
23#include "lwip/netif.h"
24#endif
25
26namespace esphome::socket {
27
28#ifdef USE_HOST
29// Host: ready when the wake select() loop has flagged this fd (or it isn't monitored).
30bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || wake_fd_ready(fd); }
31#elif defined(USE_ZEPHYR)
32// Zephyr (nRF52): fd monitoring isn't wired into the esphome select loop
33// (wake_register_fd is USE_HOST-only), so loop_monitored is always false. Always
34// return true — the caller handles EAGAIN/EWOULDBLOCK on read.
35//
36// Cost (known trade-off, not an oversight): loop-monitored sockets (API, web_server)
37// are read every loop() iteration and bail on EAGAIN; there is no event-driven wake,
38// so the main loop busy-polls at loop frequency and cannot idle between packets.
39// TODO: wire Zephyr fds into an event-driven wake source (e.g. zsock_poll/k_poll) so
40// the loop can sleep between packets on battery/OpenThread targets.
41bool socket_ready_fd(int /*fd*/, bool /*loop_monitored*/) { return true; }
42#endif
43
44// Platform-specific inet_ntop wrappers
45#if USE_NETWORK_IPV4
46#if defined(USE_SOCKET_IMPL_LWIP_TCP)
47// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
48static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
49 inet_ntoa_r(*reinterpret_cast<const struct in_addr *>(addr), buf, size);
50 return buf;
51}
52#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
53// LWIP sockets (LibreTiny, ESP32 Arduino)
54static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
55 return lwip_inet_ntop(AF_INET, addr, buf, size);
56}
57#elif defined(USE_ZEPHYR)
58// Zephyr BSD sockets — use Zephyr native address formatting via POSIX-subset wrappers.
59// <zephyr/net/socket.h> is already included transitively through <sys/socket.h>.
60static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
61 return zsock_inet_ntop(AF_INET, addr, buf, size);
62}
63#else
64// BSD sockets (host, ESP32-IDF)
65static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
66 return inet_ntop(AF_INET, addr, buf, size);
67}
68#endif
69#endif /* USE_NETWORK_IPV4 */
70
71#if USE_NETWORK_IPV6
72#if defined(USE_SOCKET_IMPL_LWIP_TCP)
73static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
74 inet6_ntoa_r(*reinterpret_cast<const ip6_addr_t *>(addr), buf, size);
75 return buf;
76}
77#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
78static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
79 return lwip_inet_ntop(AF_INET6, addr, buf, size);
80}
81#elif defined(USE_ZEPHYR)
82static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
83 return zsock_inet_ntop(AF_INET6, addr, buf, size);
84}
85#else
86static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
87 return inet_ntop(AF_INET6, addr, buf, size);
88}
89#endif
90#endif /* USE_NETWORK_IPV6 */
91
92bool sockaddr_to_ipv4(const struct sockaddr *addr, uint32_t *out) {
93#if USE_NETWORK_IPV4
94 if (addr->sa_family == AF_INET) {
95 *out = reinterpret_cast<const struct sockaddr_in *>(addr)->sin_addr.s_addr;
96 return true;
97 }
98#if USE_NETWORK_IPV6
99 if (addr->sa_family == AF_INET6) {
100 // ::ffff:a.b.c.d; s6_addr is the portable byte view on every stack.
101 static constexpr uint8_t V4_MAPPED_PREFIX[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF};
102 const uint8_t *bytes = reinterpret_cast<const struct sockaddr_in6 *>(addr)->sin6_addr.s6_addr;
103 if (memcmp(bytes, V4_MAPPED_PREFIX, sizeof(V4_MAPPED_PREFIX)) == 0) {
104 memcpy(out, bytes + sizeof(V4_MAPPED_PREFIX), sizeof(*out));
105 return true;
106 }
107 }
108#endif
109#else
110 (void) addr;
111 (void) out;
112#endif /* USE_NETWORK_IPV4 */
113 return false;
114}
115
116// Format sockaddr into caller-provided buffer, returns length written (excluding null)
117size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf) {
118#if USE_NETWORK_IPV4
119 if (addr_ptr->sa_family == AF_INET && len >= sizeof(const struct sockaddr_in)) {
120 const auto *addr = reinterpret_cast<const struct sockaddr_in *>(addr_ptr);
121 if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr)
122 return strlen(buf.data());
123 }
124#endif /* USE_NETWORK_IPV4 */
125#if USE_NETWORK_IPV6
126 if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
127 const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
128#if USE_NETWORK_IPV4
129 uint32_t v4;
130 // Format a v4 mapped peer as plain IPv4.
131 if (sockaddr_to_ipv4(addr_ptr, &v4) && esphome_inet_ntop4(&v4, buf.data(), buf.size()) != nullptr)
132 return strlen(buf.data());
133#endif /* USE_NETWORK_IPV4 */
134 if (esphome_inet_ntop6(&addr->sin6_addr, buf.data(), buf.size()) != nullptr)
135 return strlen(buf.data());
136 }
137#endif
138 buf[0] = '\0';
139 return 0;
140}
141
142std::unique_ptr<Socket> socket_ip(int type, int protocol) { return socket(IP_DOMAIN, type, protocol); }
143
144#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
145// Callback must be a temporary lambda
146template<typename F> static bool foreach_eligible_ipv6_if(F &&callback) {
147#if defined(USE_HOST)
148 struct ifaddrs *ifaddr;
149 if (getifaddrs(&ifaddr) != 0) {
150 return false;
151 }
152 bool found_any = false;
153 for (struct ifaddrs *ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
154 if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET6) {
155 continue;
156 }
157 if ((ifa->ifa_flags & IFF_LOOPBACK) || !(ifa->ifa_flags & IFF_UP) || !(ifa->ifa_flags & IFF_MULTICAST)) {
158 continue;
159 }
160 unsigned int idx = if_nametoindex(ifa->ifa_name);
161 if (idx == 0) {
162 continue;
163 }
164 found_any = true;
165 if (callback(idx)) {
166 break;
167 }
168 }
169 freeifaddrs(ifaddr);
170 if (!found_any) {
171 errno = EADDRNOTAVAIL;
172 }
173 return found_any;
174#elif defined(USE_ZEPHYR) && defined(CLANG_TIDY)
175 (void) callback;
176 return false;
177#elif defined(USE_ZEPHYR)
178 // Zephyr has no getifaddrs()/ifaddrs.h; enumerate interfaces via the native net_if API.
179 // NET_IF_IPV6_NO_MLD is the direct analog of the LwIP branch's NETIF_FLAG_MLD6 check below.
180 struct EligibleIfContext {
181 F *callback;
182 bool found_any = false;
183 bool done = false;
184 };
185 EligibleIfContext ctx{&callback};
186 net_if_foreach(
187 [](struct net_if *iface, void *user_data) {
188 auto *ctx = static_cast<EligibleIfContext *>(user_data);
189 if (ctx->done) {
190 return;
191 }
192 if (!net_if_flag_is_set(iface, NET_IF_UP) || !net_if_flag_is_set(iface, NET_IF_IPV6) ||
193 net_if_flag_is_set(iface, NET_IF_IPV6_NO_MLD)) {
194 return;
195 }
196 int idx = net_if_get_by_iface(iface);
197 if (idx <= 0) {
198 return;
199 }
200 ctx->found_any = true;
201 if ((*ctx->callback)(static_cast<unsigned int>(idx))) {
202 ctx->done = true;
203 }
204 },
205 &ctx);
206 if (!ctx.found_any) {
207 errno = EADDRNOTAVAIL;
208 }
209 return ctx.found_any;
210#else
211 // The netif list needs the core lock, but the callback's setsockopt() takes that lock itself, so the
212 // indices are collected first. One that goes away before the callback runs is rejected by setsockopt().
213 // Devices have only a few interfaces, so the cap drops nothing in practice.
214 static constexpr size_t MAX_IFS = 8;
215 StaticVector<uint8_t, MAX_IFS> indices;
216 {
217 LwIPLock lock;
218 struct netif *netif;
219 NETIF_FOREACH(netif) {
220 if (indices.size() == MAX_IFS) {
221 break;
222 }
223 if (netif->name[0] == 'l' && netif->name[1] == 'o') {
224 continue;
225 }
226 if (!(netif->flags & NETIF_FLAG_UP) || !(netif->flags & NETIF_FLAG_MLD6)) {
227 continue;
228 }
229 indices.push_back(netif_get_index(netif));
230 }
231 }
232 for (uint8_t index : indices) {
233 if (callback(index)) {
234 break;
235 }
236 }
237 if (indices.empty()) {
238 errno = EADDRNOTAVAIL;
239 }
240 return !indices.empty();
241#endif
242}
243#endif // USE_SOCKET_IMPL_BSD_SOCKETS || USE_SOCKET_IMPL_LWIP_SOCKETS
244
245bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_index_out) {
246 if (strchr(ip_address, ':') == nullptr) {
247#if !USE_NETWORK_IPV4
248 errno = EAFNOSUPPORT;
249 return false;
250#else
251 struct in_addr ipv4mc{};
252#ifdef USE_ZEPHYR
253 if (zsock_inet_pton(AF_INET, ip_address, &ipv4mc) != 1) {
254#else
255 if (inet_aton(ip_address, &ipv4mc) == 0) {
256#endif
257 errno = EINVAL;
258 return false;
259 }
260#ifdef USE_ZEPHYR
261 // Zephyr's setsockopt(IP_ADD_MEMBERSHIP) requires exactly sizeof(struct ip_mreqn); it has
262 // no plain "struct ip_mreq" at all (unlike POSIX/lwIP).
263 struct ip_mreqn imreq{};
264 imreq.imr_address.s_addr = ESPHOME_INADDR_ANY;
265 imreq.imr_ifindex = 0;
266#else
267 struct ip_mreq imreq{};
268 imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY;
269#endif
270 imreq.imr_multiaddr = ipv4mc;
271 if (sock->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)) < 0) {
272 return false;
273 }
274 if (if_index_out != nullptr) {
275 *if_index_out = 0;
276 }
277 return true;
278#endif // USE_NETWORK_IPV4
279 }
280#ifdef USE_SOCKET_IPV6
281 struct ipv6_mreq imreq6{};
282#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
283#ifdef USE_ZEPHYR
284 if (zsock_inet_pton(AF_INET6, ip_address, &imreq6.ipv6mr_multiaddr) != 1) {
285#else
286 if (inet_pton(AF_INET6, ip_address, &imreq6.ipv6mr_multiaddr) != 1) {
287#endif
288 errno = EINVAL;
289 return false;
290 }
291#else
292 ip6_addr_t ip6;
293 if (!inet6_aton(ip_address, &ip6)) {
294 errno = EINVAL;
295 return false;
296 }
297 memcpy(imreq6.ipv6mr_multiaddr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
298#endif
299 // POSIX: interface=0 fails for link-local multicast (ff02::) and may select loopback for
300 // other scopes. LwIP: NETIF_NO_INDEX=0 is always rejected.
301 bool joined = false;
302 foreach_eligible_ipv6_if([&](unsigned int idx) {
303#ifdef USE_ZEPHYR
304 // This Zephyr fork's struct ipv6_mreq has no "ipv6mr_interface" (POSIX name) and no
305 // IPV6_JOIN_GROUP alias -- only the RFC 3678 legacy ipv6mr_ifindex/IPV6_ADD_MEMBERSHIP names.
306 imreq6.ipv6mr_ifindex = idx;
307 if (sock->setsockopt(IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &imreq6, sizeof(imreq6)) == 0) {
308#else
309 imreq6.ipv6mr_interface = idx;
310 if (sock->setsockopt(IPPROTO_IPV6, IPV6_JOIN_GROUP, &imreq6, sizeof(imreq6)) == 0) {
311#endif
312 joined = true;
313 return true;
314 }
315 return false;
316 });
317 if (!joined) {
318 return false;
319 }
320 if (if_index_out != nullptr) {
321#ifdef USE_ZEPHYR
322 *if_index_out = imreq6.ipv6mr_ifindex;
323#else
324 *if_index_out = imreq6.ipv6mr_interface;
325#endif
326 }
327 return true;
328#else
329 errno = EINVAL;
330 return false;
331#endif // USE_SOCKET_IPV6
332}
333
334#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
335#if USE_NETWORK_IPV6
336bool set_ipv6_multicast_if(Socket *sock, uint32_t if_index_in) {
337#if defined(USE_ZEPHYR) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
338 // Upstream lwIP (Zephyr's fork, LibreTiny) has no IPV6_MULTICAST_IF; outgoing
339 // multicast follows the default netif. Zephyr gains ZSOCK_IPV6_MULTICAST_IF in NCS 3.4.0.
340 (void) sock;
341 (void) if_index_in;
342 return true;
343#else
344 uint32_t ifindex = if_index_in;
345 if (ifindex == 0) {
346 foreach_eligible_ipv6_if([&](unsigned int idx) {
347 ifindex = idx;
348 return true;
349 });
350 if (ifindex == 0) {
351 errno = EADDRNOTAVAIL;
352 return false;
353 }
354 }
355 return sock->setsockopt(IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex)) == 0;
356#endif
357}
358#endif // USE_NETWORK_IPV6
359#endif // USE_SOCKET_IMPL_BSD_SOCKETS || USE_SOCKET_IMPL_LWIP_SOCKETS
360
361socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) {
362#if USE_NETWORK_IPV6
363 if (strchr(ip_address, ':') != nullptr) {
364 if (addrlen < sizeof(sockaddr_in6)) {
365 errno = EINVAL;
366 return 0;
367 }
368 auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
369 memset(server, 0, sizeof(sockaddr_in6));
370 server->sin6_family = AF_INET6;
371 server->sin6_port = htons(port);
372
373#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
374#if defined(USE_ZEPHYR)
375 // Zephyr BSD sockets: use native address conversion
376 if (zsock_inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) {
377 errno = EINVAL;
378 return 0;
379 }
380#else
381 // Use standard inet_pton for BSD sockets
382 if (inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) {
383 errno = EINVAL;
384 return 0;
385 }
386#endif
387#else
388 // Use LWIP-specific functions
389 ip6_addr_t ip6;
390 if (inet6_aton(ip_address, &ip6) != 1) {
391 errno = EINVAL;
392 return 0;
393 }
394 memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
395#endif
396 return sizeof(sockaddr_in6);
397 }
398#endif /* USE_NETWORK_IPV6 */
399#if USE_NETWORK_IPV4
400 if (addrlen < sizeof(sockaddr_in)) {
401 errno = EINVAL;
402 return 0;
403 }
404 auto *server = reinterpret_cast<sockaddr_in *>(addr);
405 memset(server, 0, sizeof(sockaddr_in));
406 server->sin_family = AF_INET;
407#if defined(USE_ZEPHYR)
408 // Zephyr BSD sockets: use native address conversion
409 if (zsock_inet_pton(AF_INET, ip_address, &server->sin_addr) != 1) {
410 errno = EINVAL;
411 return 0;
412 }
413#else
414 // inet_aton() reports failure separately from the address, so 255.255.255.255 needs no special case
415 if (inet_aton(ip_address, &server->sin_addr) == 0) {
416 errno = EINVAL;
417 return 0;
418 }
419#endif
420 server->sin_port = htons(port);
421 return sizeof(sockaddr_in);
422#else
423 memset(addr, 0, addrlen);
424 errno = EAFNOSUPPORT;
425 return 0;
426#endif /* USE_NETWORK_IPV4 */
427}
428
429#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
430ConnectPollResult poll_connect(Socket &sock, int &err_out) {
431 int fd = sock.get_fd();
432 if (fd < 0 || fd >= FD_SETSIZE) {
433 // FD_SET on either is undefined behavior
434 err_out = EBADF;
436 }
437 // Connect completion is a write event; the main loop only selects on reads
438 fd_set writefds;
439 FD_ZERO(&writefds);
440 FD_SET(fd, &writefds);
441 struct timeval tv = {0, 0};
442#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
443 // LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
444 int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
445#else
446 // Global-scope select: the entity namespace esphome::select shadows it here
447 int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
448#endif
449 if (ret < 0) {
450 err_out = errno;
452 }
453 if (ret == 0) {
455 }
456 int error = 0;
457 socklen_t len = sizeof(error);
458 if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
459 err_out = errno;
461 }
462 if (error != 0) {
463 err_out = error;
465 }
467}
468#endif
469
470socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
471#if USE_NETWORK_IPV6
472 if (addrlen < sizeof(sockaddr_in6)) {
473 errno = EINVAL;
474 return 0;
475 }
476 auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
477 memset(server, 0, sizeof(sockaddr_in6));
478 server->sin6_family = AF_INET6;
479 server->sin6_port = htons(port);
480 server->sin6_addr = IN6ADDR_ANY_INIT;
481 return sizeof(sockaddr_in6);
482#else
483 if (addrlen < sizeof(sockaddr_in)) {
484 errno = EINVAL;
485 return 0;
486 }
487 auto *server = reinterpret_cast<sockaddr_in *>(addr);
488 memset(server, 0, sizeof(sockaddr_in));
489 server->sin_family = AF_INET;
490 server->sin_addr.s_addr = ESPHOME_INADDR_ANY;
491 server->sin_port = htons(port);
492 return sizeof(sockaddr_in);
493#endif /* USE_NETWORK_IPV6 */
494}
495} // namespace esphome::socket
496#endif
uint8_t type
std::vector< uint8_t > & out
Definition gif.cpp:49
uint32_t socklen_t
Definition headers.h:109
struct in6_addr ip6_addr_t
Definition ip_address.h:24
int ret
std::vector< uint8_t > bytes
Definition sml_parser.h:12
ConnectPollResult poll_connect(Socket &sock, int &err_out)
Poll a connect() that returned EINPROGRESS.
Definition socket.h:223
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port)
Set a sockaddr to the specified address and port for the IP version used by socket_ip().
std::unique_ptr< Socket > socket_ip(int type, int protocol)
Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
bool set_ipv6_multicast_if(Socket *sock, uint32_t if_index_in=0)
Configure the outgoing IPv6 multicast interface on the given socket.
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)
ConnectPollResult
Outcome of polling a non-blocking connect(); see socket::poll_connect().
Definition headers.h:224
std::unique_ptr< Socket > socket(int domain, int type, int protocol)
Create a socket of the given domain, type and protocol.
bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_index_out=nullptr)
Join a multicast group on the given socket.
BSDSocketImpl UDPSocket
Definition socket.h:39
BSDSocketImpl Socket
Definition socket.h:36
constexpr int IP_DOMAIN
Definition socket.h:54
bool sockaddr_to_ipv4(const struct sockaddr *addr, uint32_t *out)
Network order IPv4 of an AF_INET or v4 mapped AF_INET6 peer; false for anything else.
bool socket_ready_fd(int fd, bool loop_monitored)
Shared ready() helper for fd-based socket implementations.
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd)
Definition wake_host.h:43
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
static void uint32_t
struct in_addr imr_interface
Definition headers.h:31
sa_family_t sa_family
Definition headers.h:97
SemaphoreHandle_t lock
Platform-specific main loop wake primitives.