mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 07:06:20 +00:00
[network] Fix IPAddress IPv6 support on HOST platform (#17067)
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@koston.org>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Clyde Stubbs
Copilot Autofix powered by AI
J. Nick Koston
parent
4268d91da3
commit
057143838d
@@ -19,10 +19,37 @@
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include <arpa/inet.h>
|
||||
#if USE_NETWORK_IPV6
|
||||
using ip4_addr_t = struct in_addr;
|
||||
using ip6_addr_t = struct in6_addr;
|
||||
struct ip_addr_t {
|
||||
union {
|
||||
struct in6_addr ip6;
|
||||
struct in_addr ip4;
|
||||
} u_addr;
|
||||
uint8_t type;
|
||||
};
|
||||
enum : uint8_t { IPADDR_TYPE_V4 = 0, IPADDR_TYPE_V6 = 6 };
|
||||
static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) {
|
||||
if (strchr(cp, ':') != nullptr) {
|
||||
if (inet_pton(AF_INET6, cp, &addr->u_addr.ip6) != 1) {
|
||||
return 0;
|
||||
}
|
||||
addr->type = IPADDR_TYPE_V6;
|
||||
return 1;
|
||||
}
|
||||
if (inet_aton(cp, &addr->u_addr.ip4) != 1) {
|
||||
return 0;
|
||||
}
|
||||
addr->type = IPADDR_TYPE_V4;
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
using ip_addr_t = in_addr;
|
||||
using ip4_addr_t = in_addr;
|
||||
#define ipaddr_aton(x, y) inet_aton((x), (y))
|
||||
#endif
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_HOST
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/net/net_ip.h>
|
||||
@@ -80,6 +107,81 @@ struct IPAddress {
|
||||
bool operator!=(const IPAddress &other) const { return !net_ipv6_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
|
||||
#elif defined(USE_HOST)
|
||||
#if USE_NETWORK_IPV6
|
||||
IPAddress() { memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); }
|
||||
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
|
||||
memset(&this->ip_addr_, 0, sizeof(this->ip_addr_));
|
||||
this->ip_addr_.u_addr.ip4.s_addr =
|
||||
htonl(((uint32_t) first << 24) | ((uint32_t) second << 16) | ((uint32_t) third << 8) | fourth);
|
||||
this->ip_addr_.type = IPADDR_TYPE_V4;
|
||||
}
|
||||
IPAddress(const char *in_address) {
|
||||
memset(&this->ip_addr_, 0, sizeof(this->ip_addr_));
|
||||
ipaddr_aton(in_address, &this->ip_addr_);
|
||||
}
|
||||
IPAddress(const std::string &in_address) : IPAddress(in_address.c_str()) {}
|
||||
IPAddress(const ip_addr_t *other_ip) { memcpy(&this->ip_addr_, other_ip, sizeof(ip_addr_t)); }
|
||||
IPAddress(ip4_addr_t *other_ip) {
|
||||
memset(&this->ip_addr_, 0, sizeof(this->ip_addr_));
|
||||
this->ip_addr_.u_addr.ip4 = *other_ip;
|
||||
this->ip_addr_.type = IPADDR_TYPE_V4;
|
||||
}
|
||||
IPAddress(ip6_addr_t *other_ip) {
|
||||
memset(&this->ip_addr_, 0, sizeof(this->ip_addr_));
|
||||
this->ip_addr_.u_addr.ip6 = *other_ip;
|
||||
this->ip_addr_.type = IPADDR_TYPE_V6;
|
||||
}
|
||||
operator ip_addr_t() const { return this->ip_addr_; }
|
||||
bool is_set() const {
|
||||
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
|
||||
static constexpr uint8_t zero[sizeof(struct in6_addr)] = {};
|
||||
return memcmp(this->ip_addr_.u_addr.ip6.s6_addr, zero, sizeof(zero)) != 0;
|
||||
}
|
||||
return this->ip_addr_.u_addr.ip4.s_addr != 0;
|
||||
}
|
||||
bool is_ip4() const { return this->ip_addr_.type == IPADDR_TYPE_V4; }
|
||||
bool is_ip6() const { return this->ip_addr_.type == IPADDR_TYPE_V6; }
|
||||
bool is_multicast() const {
|
||||
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
|
||||
return this->ip_addr_.u_addr.ip6.s6_addr[0] == 0xff;
|
||||
}
|
||||
return (ntohl(this->ip_addr_.u_addr.ip4.s_addr) & 0xF0000000UL) == 0xE0000000UL;
|
||||
}
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED(
|
||||
"str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0",
|
||||
"2026.2.0")
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
char *str_to(char *buf) const {
|
||||
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
|
||||
inet_ntop(AF_INET6, &this->ip_addr_.u_addr.ip6, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
} else {
|
||||
inet_ntop(AF_INET, &this->ip_addr_.u_addr.ip4, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
}
|
||||
lowercase_ip_str(buf);
|
||||
return buf;
|
||||
}
|
||||
bool operator==(const IPAddress &other) const {
|
||||
if (this->ip_addr_.type != other.ip_addr_.type) {
|
||||
return false;
|
||||
}
|
||||
if (this->ip_addr_.type == IPADDR_TYPE_V6) {
|
||||
return memcmp(&this->ip_addr_.u_addr.ip6, &other.ip_addr_.u_addr.ip6, sizeof(struct in6_addr)) == 0;
|
||||
}
|
||||
return this->ip_addr_.u_addr.ip4.s_addr == other.ip_addr_.u_addr.ip4.s_addr;
|
||||
}
|
||||
bool operator!=(const IPAddress &other) const { return !(*this == other); }
|
||||
IPAddress &operator+=(uint8_t increase) {
|
||||
if (this->ip_addr_.type == IPADDR_TYPE_V4) {
|
||||
(((uint8_t *) (&this->ip_addr_.u_addr.ip4))[3]) += increase;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
#else
|
||||
IPAddress() { ip_addr_.s_addr = 0; }
|
||||
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
|
||||
this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth);
|
||||
@@ -91,6 +193,7 @@ struct IPAddress {
|
||||
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
return buf; // IPv4 only, no hex letters to lowercase
|
||||
}
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#else
|
||||
IPAddress() { ip_addr_set_zero(&ip_addr_); }
|
||||
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
real_to_code = manifest.to_code
|
||||
|
||||
async def to_code_testing(config):
|
||||
await real_to_code(config)
|
||||
cg.add_define("USE_NETWORK_IPV6", True)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,208 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
#if USE_NETWORK_IPV6
|
||||
|
||||
namespace esphome::network::testing {
|
||||
|
||||
// =========================================================================
|
||||
// IPv4
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv4DefaultNotSet) {
|
||||
IPAddress addr;
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4DefaultIsIPv4) {
|
||||
IPAddress addr;
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4ParseAndSerialize) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromOctets) {
|
||||
IPAddress addr(192, 168, 1, 1);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4IsSet) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4IsIp4) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastDetected) {
|
||||
IPAddress addr("239.0.60.53");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastBoundaryLow) {
|
||||
IPAddress addr("224.0.0.0");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4MulticastBoundaryHigh) {
|
||||
IPAddress addr("239.255.255.255");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4UnicastNotMulticast) {
|
||||
IPAddress addr("192.168.1.1");
|
||||
EXPECT_FALSE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4EqualityMatch) {
|
||||
IPAddress a("192.168.1.1");
|
||||
IPAddress b("192.168.1.1");
|
||||
EXPECT_EQ(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4EqualityMismatch) {
|
||||
IPAddress a("192.168.1.1");
|
||||
IPAddress b("192.168.1.2");
|
||||
EXPECT_NE(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromOctetsMatchesParse) {
|
||||
IPAddress from_octets(192, 168, 1, 1);
|
||||
IPAddress from_string("192.168.1.1");
|
||||
EXPECT_EQ(from_octets, from_string);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv4FromIPAddrT) {
|
||||
ip_addr_t raw;
|
||||
memset(&raw, 0, sizeof(raw));
|
||||
raw.u_addr.ip4.s_addr = htonl((192u << 24) | (168u << 16) | (1u << 8) | 1u);
|
||||
raw.type = IPADDR_TYPE_V4;
|
||||
IPAddress addr(&raw);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "192.168.1.1");
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
EXPECT_FALSE(addr.is_ip6());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IPv6
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv6ParseAndSerialize) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "ff12::cafe");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6Loopback) {
|
||||
IPAddress addr("::1");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
EXPECT_STREQ(addr.str_to(buf), "::1");
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6IsIp6) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
EXPECT_TRUE(addr.is_ip6());
|
||||
EXPECT_FALSE(addr.is_ip4());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6AllZerosNotSet) {
|
||||
IPAddress addr("::");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6LoopbackIsSet) {
|
||||
IPAddress addr("::1");
|
||||
EXPECT_TRUE(addr.is_set());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6MulticastDetected) {
|
||||
IPAddress addr("ff12::cafe");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6MulticastLinkLocal) {
|
||||
IPAddress addr("ff02::1");
|
||||
EXPECT_TRUE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6UnicastNotMulticast) {
|
||||
IPAddress addr("::1");
|
||||
EXPECT_FALSE(addr.is_multicast());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6EqualityMatch) {
|
||||
IPAddress a("ff12::cafe");
|
||||
IPAddress b("ff12::cafe");
|
||||
EXPECT_EQ(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6EqualityMismatch) {
|
||||
IPAddress a("ff12::cafe");
|
||||
IPAddress b("ff02::1");
|
||||
EXPECT_NE(a, b);
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6OutputIsLowercase) {
|
||||
// inet_pton is case-insensitive; str_to must lowercase the output
|
||||
IPAddress addr("FF12::CAFE");
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
addr.str_to(buf);
|
||||
for (const char *p = buf; *p; ++p) {
|
||||
EXPECT_FALSE(*p >= 'A' && *p <= 'F') << "uppercase letter in: " << buf;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, IPv6FullAddressRoundTrip) {
|
||||
// A full 128-bit address with no compression opportunity
|
||||
const char *input = "fde0:983a:d0d3:a65e:725a:0fff:fe36:9916";
|
||||
IPAddress addr(input);
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
addr.str_to(buf);
|
||||
EXPECT_NE(buf[0], '\0');
|
||||
EXPECT_NE(std::string(buf).find("fde0"), std::string::npos);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Malformed input
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, MalformedIPv4YieldsEmptyAddress) {
|
||||
IPAddress addr("not-an-ip");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
}
|
||||
|
||||
TEST(IPAddressHost, MalformedIPv6YieldsEmptyAddress) {
|
||||
// "gg::1" looks like IPv6 (contains ':') but fails inet_pton; addr stays
|
||||
// zeroed (type=V4 from memset) so is_set() is false and is_ip4() is true.
|
||||
IPAddress addr("gg::1");
|
||||
EXPECT_FALSE(addr.is_set());
|
||||
EXPECT_TRUE(addr.is_ip4());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cross-family
|
||||
// =========================================================================
|
||||
|
||||
TEST(IPAddressHost, IPv4AndIPv6NotEqual) {
|
||||
IPAddress v4("192.168.1.1");
|
||||
IPAddress v6("::1");
|
||||
EXPECT_NE(v4, v6);
|
||||
}
|
||||
|
||||
} // namespace esphome::network::testing
|
||||
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_HOST
|
||||
Reference in New Issue
Block a user