diff --git a/internal/platform/implementation/windows/BUILD b/internal/platform/implementation/windows/BUILD index 0eabb1db..ca43617e 100644 --- a/internal/platform/implementation/windows/BUILD +++ b/internal/platform/implementation/windows/BUILD @@ -300,6 +300,28 @@ cc_library( ], ) +cc_library( + name = "socket_address", + srcs = [ + "socket_address.cc", + ], + hdrs = [ + "socket_address.h", + ], + compatible_with = ["//buildenv/target:non_prod"], + defines = [ + "_WIN32_WINNT=_WIN32_WINNT_WIN10", + "WINVER=_WIN32_WINNT_WIN10", + ], + tags = ["windows"], + visibility = [ + "//:__subpackages__", + ], + deps = [ + "//internal/platform:logging", + ], +) + cc_library( name = "test_utils", srcs = [ @@ -427,3 +449,17 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "socket_address_test", + size = "small", + timeout = "short", + srcs = [ + "socket_address_test.cc", + ], + deps = [ + ":socket_address", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/internal/platform/implementation/windows/socket_address.cc b/internal/platform/implementation/windows/socket_address.cc new file mode 100644 index 00000000..fa949643 --- /dev/null +++ b/internal/platform/implementation/windows/socket_address.cc @@ -0,0 +1,177 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/platform/implementation/windows/socket_address.h" + +#include +#include + +#include +#include + +#include "internal/platform/logging.h" + +namespace nearby::windows { + +SocketAddress::SocketAddress(const sockaddr_in& address, bool dual_stack) + : dual_stack_(dual_stack) { + std::memcpy(&address_, &address, sizeof(sockaddr_in)); + address_.ss_family = AF_INET; + if (dual_stack_) { + ToMappedIPv6(); + } +} + +SocketAddress::SocketAddress(const sockaddr_in6& address) : dual_stack_(true) { + std::memcpy(&address_, &address, sizeof(sockaddr_in6)); + address_.ss_family = AF_INET6; +} + +SocketAddress::SocketAddress(const sockaddr_storage& address) + : dual_stack_(true) { + std::memcpy(&address_, &address, sizeof(sockaddr_storage)); + ToMappedIPv6(); +} + +void SocketAddress::ToMappedIPv6() { + if (address_.ss_family == AF_INET6) { + return; + } + if (address_.ss_family != AF_INET) { + LOG(ERROR) << "Unknown socket family: " << address_.ss_family; + return; + } + sockaddr_in* v4_address = reinterpret_cast(&address_); + in_addr orig_address; + std::memcpy(&orig_address, &v4_address->sin_addr, sizeof(in_addr)); + int orig_port = v4_address->sin_port; + address_.ss_family = AF_INET6; + sockaddr_in6* v6_address = reinterpret_cast(&address_); + v6_address->sin6_port = orig_port; + v6_address->sin6_flowinfo = 0; + v6_address->sin6_scope_id = 0; + v6_address->sin6_addr.u.Word[0] = 0; + v6_address->sin6_addr.u.Word[1] = 0; + v6_address->sin6_addr.u.Word[2] = 0; + v6_address->sin6_addr.u.Word[3] = 0; + v6_address->sin6_addr.u.Word[4] = 0; + v6_address->sin6_addr.u.Word[5] = 0xffff; + v6_address->sin6_addr.u.Word[6] = orig_address.S_un.S_un_w.s_w1; + v6_address->sin6_addr.u.Word[7] = orig_address.S_un.S_un_w.s_w2; +} + +bool SocketAddress::FromString(SocketAddress& address, + const std::string& address_string, int port) { + if (address_string.empty()) { + if (address.dual_stack_) { + address.address_.ss_family = AF_INET6; + sockaddr_in6* v6_address = + reinterpret_cast(&address.address_); + v6_address->sin6_port = 0; + v6_address->sin6_flowinfo = 0; + v6_address->sin6_scope_id = 0; + v6_address->sin6_addr = in6addr_any; + address.set_port(port); + return true; + } + address.address_.ss_family = AF_INET; + sockaddr_in* v4_address = reinterpret_cast(&address.address_); + v4_address->sin_port = 0; + v4_address->sin_addr.s_addr = INADDR_ANY; + address.set_port(port); + return true; + } + // Try v4 address first. + address.address_.ss_family = AF_INET; + int sock_address_size = sizeof(sockaddr_storage); + if (WSAStringToAddressA(const_cast(address_string.data()), AF_INET, + /*lpProtocolInfo=*/nullptr, + reinterpret_cast(&address.address_), + &sock_address_size) == 0) { + if (address.dual_stack_) { + address.ToMappedIPv6(); + } + address.set_port(port); + return true; + } + // Try v6 address if v4 address is not supported. + if (!address.dual_stack_) { + return false; + } + address.address_.ss_family = AF_INET6; + sock_address_size = sizeof(sockaddr_storage); + if (WSAStringToAddressA(const_cast(address_string.data()), AF_INET6, + /*lpProtocolInfo=*/nullptr, + reinterpret_cast(&address.address_), + &sock_address_size) == 0) { + address.set_port(port); + return true; + } + return false; +} + +int SocketAddress::port() const { + DCHECK(address_.ss_family == AF_INET || address_.ss_family == AF_INET6); + if (address_.ss_family == AF_INET) { + const sockaddr_in* v4_address = + reinterpret_cast(&address_); + return ntohs(v4_address->sin_port); + } + if (address_.ss_family == AF_INET6) { + const sockaddr_in6* v6_address = + reinterpret_cast(&address_); + return ntohs(v6_address->sin6_port); + } + LOG(ERROR) << "Unknown socket family: " << address_.ss_family; + return 0; +} + +bool SocketAddress::set_port(int port) { + DCHECK(address_.ss_family == AF_INET || address_.ss_family == AF_INET6); + if (port < 0 || port > 65535) { + LOG(ERROR) << "Invalid port: " << port; + return false; + } + if (address_.ss_family == AF_INET) { + sockaddr_in* v4_address = reinterpret_cast(&address_); + v4_address->sin_port = htons(port); + return true; + } + if (address_.ss_family == AF_INET6) { + sockaddr_in6* v6_address = reinterpret_cast(&address_); + v6_address->sin6_port = htons(port); + return true; + } + LOG(ERROR) << "Unknown socket family: " << address_.ss_family; + return false; +} + +std::string SocketAddress::ToString() const { + std::string address_string; + DWORD size = INET6_ADDRSTRLEN; // Max IP address length. + address_string.resize(size); + if (WSAAddressToStringA( + const_cast(reinterpret_cast(&address_)), + sizeof(sockaddr_storage), + /*lpProtocolInfo=*/nullptr, address_string.data(), &size) != 0) { + LOG(ERROR) << __func__ + << ": Cannot convert address to string: " << WSAGetLastError(); + return ""; + } + // size includes the null terminator. + address_string.resize(size - 1); + return address_string; +} + +} // namespace nearby::windows diff --git a/internal/platform/implementation/windows/socket_address.h b/internal/platform/implementation/windows/socket_address.h new file mode 100644 index 00000000..d40e0b41 --- /dev/null +++ b/internal/platform/implementation/windows/socket_address.h @@ -0,0 +1,80 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SOCKET_ADDRESS_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SOCKET_ADDRESS_H_ + +#include +#include + +#include +#include + +namespace nearby::windows { + +// A helper class that simplifies handling of both IPv4 and IPv6 addresses. +// IPv6 support needs to be enabled by setting `dual_stack` to true. +class SocketAddress { + public: + explicit SocketAddress(bool dual_stack = false) : dual_stack_(dual_stack) { + std::memset(&address_, 0, sizeof(address_)); + if (dual_stack_) { + address_.ss_family = AF_INET6; + } else { + address_.ss_family = AF_INET; + } + } + explicit SocketAddress(const sockaddr_in& address, bool dual_stack = false); + explicit SocketAddress(const sockaddr_in6& address); + explicit SocketAddress(const sockaddr_storage& address); + + ~SocketAddress() = default; + + // The `dual_stack` state of `address` determines whether the address is + // can be parsed as IPv6. + // If dual_stack is enabled, an IPv4 string will be returned as a mapped IPv6 + // address (e.g. [::ffff:192.0.2.1]). + // Use empty `address_string` to create and unspecified address ie. ADDR_ANY. + static bool FromString(SocketAddress& address, + const std::string& address_string, int port = 0); + + // `Returns port in host byte order. + int port() const; + // `port` is in host byte order. + bool set_port(int port); + + std::string ToString() const; + + const sockaddr_in* v4_address() const { + return reinterpret_cast(&address_); + } + const sockaddr_in6* v6_address() const { + return reinterpret_cast(&address_); + } + sockaddr* address() { + return reinterpret_cast(&address_); + } + + private: + // If `address_` is AF_INET, then rewrite into mapped ipv6 address, e.g. + // [::ffff:192.0.2.1]. + void ToMappedIPv6(); + + const bool dual_stack_ = false; + sockaddr_storage address_; +}; + +} // namespace nearby::windows + +#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_SOCKET_ADDRESS_H_ diff --git a/internal/platform/implementation/windows/socket_address_test.cc b/internal/platform/implementation/windows/socket_address_test.cc new file mode 100644 index 00000000..4c52f76f --- /dev/null +++ b/internal/platform/implementation/windows/socket_address_test.cc @@ -0,0 +1,150 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/platform/implementation/windows/socket_address.h" + +#include "gtest/gtest.h" + +namespace nearby::windows { +namespace { + +TEST(SocketAddressTest, CreateFromSockAddrIn) { + sockaddr_in address; + address.sin_family = AF_INET; + address.sin_port = htons(8080); + address.sin_addr.s_addr = inet_addr("192.168.1.1"); + SocketAddress socket_address(address, /*dual_stack=*/false); + EXPECT_EQ(socket_address.ToString(), "192.168.1.1:8080"); + EXPECT_EQ(socket_address.port(), 8080); +} + +TEST(SocketAddressTest, CreateFromSockAddrInDualStack) { + sockaddr_in address; + address.sin_family = AF_INET; + address.sin_port = htons(8080); + address.sin_addr.s_addr = inet_addr("192.168.1.1"); + SocketAddress socket_address(address, /*dual_stack=*/true); + EXPECT_EQ(socket_address.ToString(), "[::ffff:192.168.1.1]:8080"); + EXPECT_EQ(socket_address.port(), 8080); +} + +TEST(SocketAddressTest, CreateFromSockAddrIn6) { + sockaddr_in6 address; + address.sin6_family = AF_INET6; + address.sin6_port = htons(8080); + address.sin6_flowinfo = 0; + address.sin6_scope_id = 0; + address.sin6_addr.u.Word[0] = 0xFDDF; + address.sin6_addr.u.Word[1] = 0x253D; + address.sin6_addr.u.Word[2] = 0x0BA3; + address.sin6_addr.u.Word[3] = 0x51A2; + address.sin6_addr.u.Word[4] = 0x51ED; + address.sin6_addr.u.Word[5] = 0xEE8F; + address.sin6_addr.u.Word[6] = 0x8F1C; + address.sin6_addr.u.Word[7] = 0xB30C; + SocketAddress socket_address(address); + EXPECT_EQ(socket_address.ToString(), + "[dffd:3d25:a30b:a251:ed51:8fee:1c8f:cb3]:8080"); + EXPECT_EQ(socket_address.port(), 8080); +} + +TEST(SocketAddressTest, FromStringIPv4) { + SocketAddress address; + EXPECT_TRUE(SocketAddress::FromString(address, "192.168.1.1", 8080)); + EXPECT_EQ(address.ToString(), "192.168.1.1:8080"); + EXPECT_EQ(address.port(), 8080); +} + +TEST(SocketAddressTest, FromStringIPv4DualStack) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "192.168.1.1", 8080)); + EXPECT_EQ(address.ToString(), "[::ffff:192.168.1.1]:8080"); + EXPECT_EQ(address.port(), 8080); +} + +TEST(SocketAddressTest, FromStringIPv4NoPort) { + SocketAddress address; + EXPECT_TRUE(SocketAddress::FromString(address, "192.168.1.1")); + EXPECT_EQ(address.ToString(), "192.168.1.1"); + EXPECT_EQ(address.port(), 0); +} + +TEST(SocketAddressTest, FromStringIPv4AnyAddress) { + SocketAddress address; + EXPECT_TRUE(SocketAddress::FromString(address, "")); + EXPECT_EQ(address.ToString(), "0.0.0.0"); +} + +TEST(SocketAddressTest, FromStringIPv4NoPortDualStack) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "192.168.1.1")); + EXPECT_EQ(address.ToString(), "::ffff:192.168.1.1"); + EXPECT_EQ(address.port(), 0); +} + +TEST(SocketAddressTest, FromStringIPv6) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "2001:db8::1", 8080)); + EXPECT_EQ(address.ToString(), "[2001:db8::1]:8080"); + EXPECT_EQ(address.port(), 8080); +} + +TEST(SocketAddressTest, FromStringIPv6NoPort) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "2001:db8::1")); + EXPECT_EQ(address.ToString(), "2001:db8::1"); + EXPECT_EQ(address.port(), 0); +} + +TEST(SocketAddressTest, FromStringIPv6AnyAddress) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "")); + EXPECT_EQ(address.ToString(), "::"); +} + +TEST(SocketAddressTest, FromStringMappedIPv4) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_TRUE(SocketAddress::FromString(address, "::ffff:192.168.1.1", 8080)); + EXPECT_EQ(address.port(), 8080); + EXPECT_EQ(address.ToString(), "[::ffff:192.168.1.1]:8080"); +} + +TEST(SocketAddressTest, FromStringInvalid) { + SocketAddress address; + EXPECT_FALSE(SocketAddress::FromString(address, "invalid", 8080)); +} + +TEST(SocketAddressTest, FromStringInvalidDualStack) { + SocketAddress address(/*dual_stack=*/true); + EXPECT_FALSE(SocketAddress::FromString(address, "invalid", 8080)); +} + +TEST(SocketAddressTest, SetPort) { + SocketAddress address; + EXPECT_TRUE(SocketAddress::FromString(address, "127.0.0.1", 8080)); + EXPECT_EQ(address.port(), 8080); + EXPECT_TRUE(address.set_port(9090)); + EXPECT_EQ(address.port(), 9090); +} + +TEST(SocketAddressTest, SetInvalidPort) { + SocketAddress address; + EXPECT_TRUE(SocketAddress::FromString(address, "127.0.0.1", 8080)); + EXPECT_FALSE(address.set_port(-1)); + EXPECT_FALSE(address.set_port(65536)); + EXPECT_EQ(address.port(), 8080); +} + +} // namespace +} // namespace nearby::windows