Enable ipv6 endpoints support (#262)

Resolves #250

Signed-off-by: Tao He <linzhu.ht@alibaba-inc.com>
This commit is contained in:
Tao He 2023-12-20 17:56:18 +08:00 committed by GitHub
parent b82efea7a9
commit 5ccaccec43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 22 deletions

View File

@ -16,7 +16,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, ubuntu-22.04, macos-11, macos-12] os: [ubuntu-20.04, ubuntu-22.04, macos-11, macos-12]
etcd: [v3.2.26, v3.3.11, v3.4.13, v3.5.7] etcd: [v3.2.32, v3.3.27, v3.4.27, v3.5.9]
exclude: exclude:
- os: ubuntu-20.04 - os: ubuntu-20.04
etcd: v3.2.26 etcd: v3.2.26
@ -243,6 +243,10 @@ jobs:
# tests without auth # tests without auth
echo "Run the etcd resolver test ........................."
# there's no ipv6 on github CI runner
# ./build/bin/EtcdResolverTest
echo "Run the etcd sync test ........................." echo "Run the etcd sync test ........................."
./build/bin/EtcdSyncTest ./build/bin/EtcdSyncTest

View File

@ -222,6 +222,14 @@ Connecting to multiple endpoints is supported:
etcd::Client etcd("http://a.com:2379;http://b.com:2379;http://c.com:2379"); etcd::Client etcd("http://a.com:2379;http://b.com:2379;http://c.com:2379");
``` ```
### IPv6
Connecting to IPv6 endpoints is supported:
```c++
etcd::Client etcd("http://::1:2379");
```
Behind the screen, gRPC's load balancer is used and the round-robin strategy will Behind the screen, gRPC's load balancer is used and the round-robin strategy will
be used by default. be used by default.

View File

@ -99,19 +99,19 @@ static std::string string_join(std::vector<std::string> const& srcs,
} }
static bool dns_resolve(std::string const& target, static bool dns_resolve(std::string const& target,
std::vector<std::string>& endpoints) { std::vector<std::string>& endpoints, bool ipv4 = true) {
struct addrinfo hints = {}, *addrs;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
std::vector<std::string> target_parts; std::vector<std::string> target_parts;
string_split(target_parts, target, ":"); {
if (target_parts.size() != 2) { size_t rindex = target.rfind(':');
if (rindex == target.npos) {
#ifndef NDEBUG #ifndef NDEBUG
std::cerr << "[warn] invalid URL: " << target << std::endl; std::cerr << "[warn] invalid URL: " << target << ", expects 'host:port'"
<< std::endl;
#endif #endif
return false; return false;
}
target_parts.push_back(target.substr(0, rindex));
target_parts.push_back(target.substr(rindex + 1));
} }
#if defined(_WIN32) #if defined(_WIN32)
@ -132,22 +132,42 @@ static bool dns_resolve(std::string const& target,
} }
#endif #endif
struct addrinfo hints = {}, *addrs;
hints.ai_family = ipv4 ? AF_INET : AF_INET6;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
int r = getaddrinfo(target_parts[0].c_str(), target_parts[1].c_str(), &hints, int r = getaddrinfo(target_parts[0].c_str(), target_parts[1].c_str(), &hints,
&addrs); &addrs);
if (r != 0) { if (r != 0) {
#ifndef NDEBUG #ifndef NDEBUG
std::cerr << "[warn] getaddrinfo() failed for endpoint " << target std::cerr << "[warn] getaddrinfo() as " << (ipv4 ? "ipv4" : "ipv6")
<< " with error: " << r << std::endl; << " failed for endpoint " << target << " with error: " << r
<< ", " << strerror(errno) << std::endl;
#endif #endif
return false; return false;
} }
char host[16] = {'\0'}; char host[16] = {'\0'};
for (struct addrinfo* addr = addrs; addr != nullptr; addr = addr->ai_next) { for (struct addrinfo* addr = addrs; addr != nullptr; addr = addr->ai_next) {
if (addr->ai_family != AF_INET && addr->ai_family != AF_INET6) {
continue;
}
memset(host, '\0', sizeof(host)); memset(host, '\0', sizeof(host));
getnameinfo(addr->ai_addr, addr->ai_addrlen, host, sizeof(host), NULL, 0, int r = getnameinfo(addr->ai_addr, addr->ai_addrlen, host, sizeof(host),
NI_NUMERICHOST); NULL, 0, NI_NUMERICHOST);
endpoints.emplace_back(std::string(host) + ":" + target_parts[1]); if (r != 0) {
#ifndef NDEBUG
std::cerr << "[warn] getnameinfo() failed for endpoint " << target
<< " with error: " << r << ", " << strerror(errno) << std::endl;
#endif
continue;
}
std::string host_string = host;
if (addr->ai_family == AF_INET6) {
host_string = "[" + host_string + "]";
}
endpoints.emplace_back(host_string + ":" + target_parts[1]);
} }
freeaddrinfo(addrs); freeaddrinfo(addrs);
return true; return true;
@ -156,19 +176,28 @@ static bool dns_resolve(std::string const& target,
const std::string strip_and_resolve_addresses(std::string const& address) { const std::string strip_and_resolve_addresses(std::string const& address) {
std::vector<std::string> addresses; std::vector<std::string> addresses;
string_split(addresses, address, ",;"); string_split(addresses, address, ",;");
std::string stripped_address; std::string stripped_v4_address, stripped_v6_address;
{ {
std::vector<std::string> stripped_addresses; std::vector<std::string> stripped_v4_addresses, stripped_v6_addresses;
std::string substr("://"); std::string substr("://");
for (auto const& addr : addresses) { for (auto const& addr : addresses) {
std::string::size_type idx = addr.find(substr); std::string::size_type idx = addr.find(substr);
std::string target = std::string target =
idx == std::string::npos ? addr : addr.substr(idx + substr.length()); idx == std::string::npos ? addr : addr.substr(idx + substr.length());
etcd::detail::dns_resolve(target, stripped_addresses); etcd::detail::dns_resolve(target, stripped_v4_addresses, true);
etcd::detail::dns_resolve(target, stripped_v6_addresses, false);
} }
stripped_address = string_join(stripped_addresses, ","); stripped_v4_address = string_join(stripped_v4_addresses, ",");
stripped_v6_address = string_join(stripped_v6_addresses, ",");
} }
return "ipv4:///" + stripped_address; // prefer resolved ipv4 addresses
if (!stripped_v4_address.empty()) {
return "ipv4:///" + stripped_v4_address;
}
if (!stripped_v6_address.empty()) {
return "ipv6:///" + stripped_v6_address;
}
return std::string{};
} }
bool authenticate(std::shared_ptr<grpc::Channel> const& channel, bool authenticate(std::shared_ptr<grpc::Channel> const& channel,
@ -227,7 +256,10 @@ static std::shared_ptr<grpc::Channel> create_grpc_channel(
const grpc::ChannelArguments& grpc_args) { const grpc::ChannelArguments& grpc_args) {
const std::string addresses = const std::string addresses =
etcd::detail::strip_and_resolve_addresses(address); etcd::detail::strip_and_resolve_addresses(address);
if (addresses.empty() || addresses == "ipv4:///") { #ifndef NDEBUG
std::cerr << "[debug] resolved addresses: " << addresses << std::endl;
#endif
if (addresses.empty() || addresses == "ipv4:///" || addresses == "ipv6:///") {
// bypass grpc initialization to avoid noisy logs from grpc // bypass grpc initialization to avoid noisy logs from grpc
return grpc::CreateChannelInternal( return grpc::CreateChannelInternal(
"", "",

23
tst/EtcdResolverTest.cpp Normal file
View File

@ -0,0 +1,23 @@
#define CATCH_CONFIG_MAIN
#include <catch.hpp>
#include <iostream>
#include "etcd/Client.hpp"
static const std::string etcd_v4_url =
etcdv3::detail::resolve_etcd_endpoints("http://127.0.0.1:2379");
static const std::string etcd_v6_url =
etcdv3::detail::resolve_etcd_endpoints("http://::1:2379");
TEST_CASE("test ipv4 connection") {
std::cout << "ipv4 endpoints: " << etcd_v4_url << std::endl;
etcd::Client etcd(etcd_v4_url);
REQUIRE(etcd.head().get().is_ok());
}
TEST_CASE("test ipv6 connection") {
std::cout << "ipv6 endpoints: " << etcd_v6_url << std::endl;
etcd::Client etcd(etcd_v6_url);
REQUIRE(etcd.head().get().is_ok());
}