mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[socket] Add join_multicast_group() and set_ipv6_multicast_if() helpers (#17045)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Clyde Stubbs
Copilot Autofix powered by AI
J. Nick Koston
parent
b73f151619
commit
f562d7a6a4
@@ -11,6 +11,16 @@
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/wake.h"
|
||||
#endif
|
||||
#if defined(USE_HOST)
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#elif defined(USE_ZEPHYR)
|
||||
#ifndef CLANG_TIDY // NCS 2.9.2 net_if.h has a BUILD_ASSERT clang rejects; gone in NCS 3.4
|
||||
#include <zephyr/net/net_if.h>
|
||||
#endif
|
||||
#else
|
||||
#include "lwip/netif.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::socket {
|
||||
|
||||
@@ -139,6 +149,203 @@ std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
// Callback must be a temporary lambda
|
||||
template<typename F> static bool foreach_eligible_ipv6_if(F &&callback) {
|
||||
#if defined(USE_HOST)
|
||||
struct ifaddrs *ifaddr;
|
||||
if (getifaddrs(&ifaddr) != 0) {
|
||||
return false;
|
||||
}
|
||||
bool found_any = false;
|
||||
for (struct ifaddrs *ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET6) {
|
||||
continue;
|
||||
}
|
||||
if ((ifa->ifa_flags & IFF_LOOPBACK) || !(ifa->ifa_flags & IFF_UP) || !(ifa->ifa_flags & IFF_MULTICAST)) {
|
||||
continue;
|
||||
}
|
||||
unsigned int idx = if_nametoindex(ifa->ifa_name);
|
||||
if (idx == 0) {
|
||||
continue;
|
||||
}
|
||||
found_any = true;
|
||||
if (callback(idx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr);
|
||||
if (!found_any) {
|
||||
errno = EADDRNOTAVAIL;
|
||||
}
|
||||
return found_any;
|
||||
#elif defined(USE_ZEPHYR) && defined(CLANG_TIDY)
|
||||
(void) callback;
|
||||
return false;
|
||||
#elif defined(USE_ZEPHYR)
|
||||
// Zephyr has no getifaddrs()/ifaddrs.h; enumerate interfaces via the native net_if API.
|
||||
// NET_IF_IPV6_NO_MLD is the direct analog of the LwIP branch's NETIF_FLAG_MLD6 check below.
|
||||
struct EligibleIfContext {
|
||||
F *callback;
|
||||
bool found_any = false;
|
||||
bool done = false;
|
||||
};
|
||||
EligibleIfContext ctx{&callback};
|
||||
net_if_foreach(
|
||||
[](struct net_if *iface, void *user_data) {
|
||||
auto *ctx = static_cast<EligibleIfContext *>(user_data);
|
||||
if (ctx->done) {
|
||||
return;
|
||||
}
|
||||
if (!net_if_flag_is_set(iface, NET_IF_UP) || !net_if_flag_is_set(iface, NET_IF_IPV6) ||
|
||||
net_if_flag_is_set(iface, NET_IF_IPV6_NO_MLD)) {
|
||||
return;
|
||||
}
|
||||
int idx = net_if_get_by_iface(iface);
|
||||
if (idx <= 0) {
|
||||
return;
|
||||
}
|
||||
ctx->found_any = true;
|
||||
if ((*ctx->callback)(static_cast<unsigned int>(idx))) {
|
||||
ctx->done = true;
|
||||
}
|
||||
},
|
||||
&ctx);
|
||||
if (!ctx.found_any) {
|
||||
errno = EADDRNOTAVAIL;
|
||||
}
|
||||
return ctx.found_any;
|
||||
#else
|
||||
bool found_any = false;
|
||||
struct netif *netif;
|
||||
NETIF_FOREACH(netif) {
|
||||
if (netif->name[0] == 'l' && netif->name[1] == 'o') {
|
||||
continue;
|
||||
}
|
||||
if (!(netif->flags & NETIF_FLAG_UP) || !(netif->flags & NETIF_FLAG_MLD6)) {
|
||||
continue;
|
||||
}
|
||||
found_any = true;
|
||||
if (callback(netif_get_index(netif))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_any) {
|
||||
errno = EADDRNOTAVAIL;
|
||||
}
|
||||
return found_any;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_index_out) {
|
||||
if (strchr(ip_address, ':') == nullptr) {
|
||||
struct in_addr ipv4mc {};
|
||||
#ifdef USE_ZEPHYR
|
||||
if (zsock_inet_pton(AF_INET, ip_address, &ipv4mc) != 1) {
|
||||
#else
|
||||
if (inet_aton(ip_address, &ipv4mc) == 0) {
|
||||
#endif
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
#ifdef USE_ZEPHYR
|
||||
// Zephyr's setsockopt(IP_ADD_MEMBERSHIP) requires exactly sizeof(struct ip_mreqn); it has
|
||||
// no plain "struct ip_mreq" at all (unlike POSIX/lwIP).
|
||||
struct ip_mreqn imreq {};
|
||||
imreq.imr_address.s_addr = ESPHOME_INADDR_ANY;
|
||||
imreq.imr_ifindex = 0;
|
||||
#else
|
||||
struct ip_mreq imreq {};
|
||||
imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY;
|
||||
#endif
|
||||
imreq.imr_multiaddr = ipv4mc;
|
||||
if (sock->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)) < 0) {
|
||||
return false;
|
||||
}
|
||||
if (if_index_out != nullptr) {
|
||||
*if_index_out = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
struct ipv6_mreq imreq6 {};
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
#ifdef USE_ZEPHYR
|
||||
if (zsock_inet_pton(AF_INET6, ip_address, &imreq6.ipv6mr_multiaddr) != 1) {
|
||||
#else
|
||||
if (inet_pton(AF_INET6, ip_address, &imreq6.ipv6mr_multiaddr) != 1) {
|
||||
#endif
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
ip6_addr_t ip6;
|
||||
if (!inet6_aton(ip_address, &ip6)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
memcpy(imreq6.ipv6mr_multiaddr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
#endif
|
||||
// POSIX: interface=0 fails for link-local multicast (ff02::) and may select loopback for
|
||||
// other scopes. LwIP: NETIF_NO_INDEX=0 is always rejected.
|
||||
bool joined = false;
|
||||
foreach_eligible_ipv6_if([&](unsigned int idx) {
|
||||
#ifdef USE_ZEPHYR
|
||||
// This Zephyr fork's struct ipv6_mreq has no "ipv6mr_interface" (POSIX name) and no
|
||||
// IPV6_JOIN_GROUP alias -- only the RFC 3678 legacy ipv6mr_ifindex/IPV6_ADD_MEMBERSHIP names.
|
||||
imreq6.ipv6mr_ifindex = idx;
|
||||
if (sock->setsockopt(IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &imreq6, sizeof(imreq6)) == 0) {
|
||||
#else
|
||||
imreq6.ipv6mr_interface = idx;
|
||||
if (sock->setsockopt(IPPROTO_IPV6, IPV6_JOIN_GROUP, &imreq6, sizeof(imreq6)) == 0) {
|
||||
#endif
|
||||
joined = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!joined) {
|
||||
return false;
|
||||
}
|
||||
if (if_index_out != nullptr) {
|
||||
#ifdef USE_ZEPHYR
|
||||
*if_index_out = imreq6.ipv6mr_ifindex;
|
||||
#else
|
||||
*if_index_out = imreq6.ipv6mr_interface;
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
#endif // USE_NETWORK_IPV6
|
||||
}
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
bool set_ipv6_multicast_if(Socket *sock, uint32_t if_index_in) {
|
||||
#if defined(USE_ZEPHYR) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
// Upstream lwIP (Zephyr's fork, LibreTiny) has no IPV6_MULTICAST_IF; outgoing
|
||||
// multicast follows the default netif. Zephyr gains ZSOCK_IPV6_MULTICAST_IF in NCS 3.4.0.
|
||||
(void) sock;
|
||||
(void) if_index_in;
|
||||
return true;
|
||||
#else
|
||||
uint32_t ifindex = if_index_in;
|
||||
if (ifindex == 0) {
|
||||
foreach_eligible_ipv6_if([&](unsigned int idx) {
|
||||
ifindex = idx;
|
||||
return true;
|
||||
});
|
||||
if (ifindex == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return sock->setsockopt(IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex)) == 0;
|
||||
#endif
|
||||
}
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_SOCKET_IMPL_BSD_SOCKETS || USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) {
|
||||
#if USE_NETWORK_IPV6
|
||||
if (strchr(ip_address, ':') != nullptr) {
|
||||
|
||||
@@ -94,6 +94,7 @@ inline bool Socket::ready() const {
|
||||
/// Create a socket of the given domain, type and protocol.
|
||||
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
|
||||
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
|
||||
/// Not registered with the main loop's select() - use socket_ip_loop_monitored() if you need that.
|
||||
std::unique_ptr<Socket> socket_ip(int type, int protocol);
|
||||
|
||||
/// Create a socket and monitor it for data in the main loop.
|
||||
@@ -149,6 +150,27 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
/// Join a multicast group on the given socket.
|
||||
/// @param sock The socket to join on
|
||||
/// @param ip_address Null-terminated multicast address string (IPv4 or IPv6)
|
||||
/// @param if_index_out If non-null, receives the interface index used (IPv4 always writes 0). For
|
||||
/// link-local IPv6 groups, the caller must also copy this into sin6_scope_id
|
||||
/// before bind() -- POSIX requires it, LwIP ignores it.
|
||||
/// @return true on success, false on failure (errno set)
|
||||
bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_index_out = nullptr);
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
/// Configure the outgoing IPv6 multicast interface on the given socket.
|
||||
/// @param sock The socket to configure
|
||||
/// @param if_index_in Non-zero: use this interface index directly. Zero (default): probe for the first eligible
|
||||
/// interface via foreach_eligible_ipv6_if(). On single-interface devices (typical for ESP32/ESP8266) this is always
|
||||
/// correct. On multi-homed hosts the "first eligible" interface is arbitrary; pass the desired index explicitly.
|
||||
/// @return true on success, false on failure (errno set). A no-op returning true on LWIP_SOCKETS and Zephyr.
|
||||
bool set_ipv6_multicast_if(Socket *sock, uint32_t if_index_in = 0);
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_SOCKET_IMPL_BSD_SOCKETS || USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
/// Poll a connect() that returned EINPROGRESS. On error, err_out is SO_ERROR (or
|
||||
/// errno) on fd implementations and the failure the callbacks recorded on raw lwip.
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
packages:
|
||||
socket: !include common.yaml
|
||||
@@ -0,0 +1,96 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
namespace esphome::socket::testing {
|
||||
|
||||
// =========================================================================
|
||||
// join_multicast_group — EINVAL on malformed input
|
||||
// =========================================================================
|
||||
|
||||
TEST(JoinMulticastGroup, MalformedIPv4YieldsEINVAL) {
|
||||
auto sock = esphome::socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
errno = 0;
|
||||
EXPECT_FALSE(join_multicast_group(sock.get(), "not-an-ip"));
|
||||
EXPECT_EQ(errno, EINVAL);
|
||||
}
|
||||
|
||||
TEST(JoinMulticastGroup, MalformedIPv6YieldsEINVAL) {
|
||||
// ':' present but parse fails — EINVAL regardless of USE_NETWORK_IPV6
|
||||
auto sock = esphome::socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
errno = 0;
|
||||
EXPECT_FALSE(join_multicast_group(sock.get(), "gg::not-ipv6"));
|
||||
EXPECT_EQ(errno, EINVAL);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// join_multicast_group — IPv4 IP_ADD_MEMBERSHIP
|
||||
// =========================================================================
|
||||
|
||||
TEST(JoinMulticastGroup, IPv4JoinSetsIfIndexToZero) {
|
||||
auto sock = esphome::socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
uint32_t if_index = 99;
|
||||
// A host with no multicast route (e.g. an isolated container) cannot join
|
||||
if (join_multicast_group(sock.get(), "239.0.60.53", &if_index)) {
|
||||
EXPECT_EQ(if_index, 0u);
|
||||
} else {
|
||||
EXPECT_EQ(if_index, 99u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JoinMulticastGroup, IPv4JoinNullIfIndexOut) {
|
||||
auto sock = esphome::socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
errno = 0;
|
||||
EXPECT_TRUE(join_multicast_group(sock.get(), "239.0.60.53", nullptr) || errno != 0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// join_multicast_group — IPv6 and set_ipv6_multicast_if
|
||||
// =========================================================================
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
|
||||
TEST(JoinMulticastGroup, IPv6JoinSucceedsOrNoInterface) {
|
||||
auto sock = esphome::socket::socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
uint32_t if_index = 0;
|
||||
bool ok = join_multicast_group(sock.get(), "ff12::cafe", &if_index);
|
||||
if (ok) {
|
||||
EXPECT_GT(if_index, 0u);
|
||||
} else {
|
||||
EXPECT_EQ(errno, EADDRNOTAVAIL);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SetIPv6MulticastIf, ProbeWithZeroIndexSucceedsOrNoInterface) {
|
||||
auto sock = esphome::socket::socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock, nullptr);
|
||||
errno = 0;
|
||||
EXPECT_TRUE(set_ipv6_multicast_if(sock.get(), 0) || errno == EADDRNOTAVAIL);
|
||||
}
|
||||
|
||||
TEST(SetIPv6MulticastIf, ExplicitIndexFromJoinSucceeds) {
|
||||
auto sock_join = esphome::socket::socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||
auto sock_send = esphome::socket::socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||
ASSERT_NE(sock_join, nullptr);
|
||||
ASSERT_NE(sock_send, nullptr);
|
||||
uint32_t if_index = 0;
|
||||
bool joined = join_multicast_group(sock_join.get(), "ff12::cafe", &if_index);
|
||||
if (joined && if_index != 0) {
|
||||
EXPECT_TRUE(set_ipv6_multicast_if(sock_send.get(), if_index));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_NETWORK_IPV6
|
||||
|
||||
} // namespace esphome::socket::testing
|
||||
|
||||
#endif // USE_HOST
|
||||
Reference in New Issue
Block a user