[feat] add IPv6 support for direct IP connections, refs #129

This commit is contained in:
dijunkun
2026-10-29 20:02:43 +08:00
parent f09a4c50d1
commit 7c6e14989f
18 changed files with 287 additions and 187 deletions
+84 -48
View File
@@ -13,6 +13,7 @@
#include <stdexcept>
#include <string_view>
#include "direct_address.h"
#include "lan_password.h"
#ifdef _WIN32
// Winsock must precede windows.h; the networking/ACL headers need its types.
@@ -373,53 +374,83 @@ unsigned ParsePort(const std::string& value) {
"Invalid LAN port");
return port;
}
bool IsIpv4(const std::string& value) {
unsigned octets[4]{};
size_t offset = 0;
for (unsigned i = 0; i < 4; ++i) {
size_t end = value.find('.', offset);
if (i == 3) end = value.size();
if (end == std::string::npos || end <= offset || end - offset > 3 ||
(end - offset > 1 && value[offset] == '0'))
return false;
for (size_t j = offset; j < end; ++j) {
if (value[j] < '0' || value[j] > '9') return false;
octets[i] = octets[i] * 10 + value[j] - '0';
}
if (octets[i] > 255) return false;
offset = end + 1;
}
return offset == value.size() + 1 && octets[0] > 0 && octets[0] != 127 &&
octets[0] < 224;
bool IsIpAddress(const std::string& value) {
return minirtc::ParseDirectAddress(value).has_value();
}
std::string NormalizeAddress(const std::string& value) {
const auto address = minirtc::ParseDirectAddress(value);
Require(address.has_value(), "Invalid LAN endpoint");
return minirtc::DirectAddressString(*address);
}
std::string FormatEndpoint(const std::string& address, unsigned port) {
return (address.find(':') == std::string::npos ? address
: "[" + address + "]") +
":" + std::to_string(port);
}
std::pair<std::string, unsigned> ParseEndpoint(const std::string& value,
unsigned default_port) {
std::string address = value;
const auto colon = address.find(':');
unsigned port = default_port;
if (colon != std::string::npos) {
port = ParsePort(address.substr(colon + 1));
if (!value.empty() && value.front() == '[') {
const auto close = value.find(']');
Require(close != std::string::npos, "Invalid LAN endpoint");
address = value.substr(1, close - 1);
Require(address.find(':') != std::string::npos, "Invalid LAN endpoint");
if (close + 1 < value.size()) {
Require(value[close + 1] == ':', "Invalid LAN endpoint");
port = ParsePort(value.substr(close + 2));
}
} else if (const auto colon = value.find(':');
colon != std::string::npos &&
value.find(':', colon + 1) == std::string::npos) {
port = ParsePort(value.substr(colon + 1));
address.resize(colon);
}
Require(IsIpv4(address) && port > 0 && port <= 65535,
"Enter a numeric IPv4 address or IPv4:port");
return {address, port};
Require(IsIpAddress(address) && port > 0 && port <= 65535,
"Invalid LAN endpoint");
return {NormalizeAddress(address), port};
}
std::vector<Interface> LocalIpv4Interfaces(bool physical_only) {
std::vector<Interface> LocalIpInterfaces(bool physical_only) {
std::vector<Interface> result;
const auto append = [&](const sockaddr* native, unsigned index) {
char text[INET6_ADDRSTRLEN]{};
std::string address;
if (native->sa_family == AF_INET) {
if (!inet_ntop(AF_INET,
&reinterpret_cast<const sockaddr_in*>(native)->sin_addr,
text, sizeof(text)))
return;
address = text;
} else if (native->sa_family == AF_INET6) {
auto v6 = *reinterpret_cast<const sockaddr_in6*>(native);
if (IN6_IS_ADDR_LINKLOCAL(&v6.sin6_addr)) {
if (!v6.sin6_scope_id) v6.sin6_scope_id = index;
#ifdef __APPLE__
// getifaddrs may expose the BSD embedded link scope as well.
v6.sin6_addr.s6_addr[2] = v6.sin6_addr.s6_addr[3] = 0;
#endif
} else {
v6.sin6_scope_id = 0;
}
if (!inet_ntop(AF_INET6, &v6.sin6_addr, text, sizeof(text))) return;
address = text;
if (v6.sin6_scope_id) address += "%" + std::to_string(v6.sin6_scope_id);
}
if (IsIpAddress(address)) result.push_back({NormalizeAddress(address)});
};
#ifdef _WIN32
ULONG bytes = 16384;
std::vector<unsigned char> buffer(bytes);
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
ULONG code =
GetAdaptersAddresses(AF_INET,
GetAdaptersAddresses(AF_UNSPEC,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST |
GAA_FLAG_SKIP_DNS_SERVER,
nullptr, adapters, &bytes);
if (code == ERROR_BUFFER_OVERFLOW) {
buffer.resize(bytes);
adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
code = GetAdaptersAddresses(AF_INET, 0, nullptr, adapters, &bytes);
code = GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, adapters, &bytes);
}
if (code != NO_ERROR) return result;
for (auto* a = adapters; a; a = a->Next) {
@@ -438,21 +469,19 @@ std::vector<Interface> LocalIpv4Interfaces(bool physical_only) {
continue;
}
for (auto* u = a->FirstUnicastAddress; u; u = u->Next) {
if (!u->Address.lpSockaddr || u->Address.lpSockaddr->sa_family != AF_INET)
if (!u->Address.lpSockaddr || (u->DadState != IpDadStatePreferred &&
u->DadState != IpDadStateDeprecated))
continue;
char address[INET_ADDRSTRLEN]{};
inet_ntop(
AF_INET,
&reinterpret_cast<sockaddr_in*>(u->Address.lpSockaddr)->sin_addr,
address, sizeof(address));
if (IsIpv4(address)) result.push_back({address});
append(u->Address.lpSockaddr, a->Ipv6IfIndex);
}
}
#else
ifaddrs* list = nullptr;
if (getifaddrs(&list) != 0) return result;
for (auto* a = list; a; a = a->ifa_next) {
if (!a->ifa_addr || a->ifa_addr->sa_family != AF_INET ||
if (!a->ifa_addr ||
(a->ifa_addr->sa_family != AF_INET &&
a->ifa_addr->sa_family != AF_INET6) ||
!(a->ifa_flags & IFF_UP) || (a->ifa_flags & IFF_LOOPBACK) ||
(physical_only && (a->ifa_flags & IFF_POINTOPOINT)))
continue;
@@ -470,15 +499,15 @@ std::vector<Interface> LocalIpv4Interfaces(bool physical_only) {
a->ifa_name / "device");
#endif
if (physical_only && !physical) continue;
char address[INET_ADDRSTRLEN]{};
inet_ntop(AF_INET, &reinterpret_cast<sockaddr_in*>(a->ifa_addr)->sin_addr,
address, sizeof(address));
if (IsIpv4(address)) result.push_back({address});
append(a->ifa_addr, if_nametoindex(a->ifa_name));
}
freeifaddrs(list);
#endif
std::sort(result.begin(), result.end(),
[](const auto& a, const auto& b) { return a.address < b.address; });
std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) {
const bool a_v6 = a.address.find(':') != std::string::npos;
const bool b_v6 = b.address.find(':') != std::string::npos;
return a_v6 != b_v6 ? !a_v6 : a.address < b.address;
});
result.erase(std::unique(result.begin(), result.end(),
[](const auto& a, const auto& b) {
return a.address == b.address;
@@ -549,7 +578,8 @@ void Identity::Load() {
!settings.contains("listen_all") || settings["listen_all"].is_boolean(),
"Invalid LAN listener mode");
const auto address = settings.value("address", std::string{});
Require(address.empty() || IsIpv4(address), "Invalid LAN listener address");
Require(address.empty() || IsIpAddress(address),
"Invalid LAN listener address");
if (!settings.contains("ports")) {
settings["ports"] = nlohmann::json::object();
if (!address.empty())
@@ -558,8 +588,14 @@ void Identity::Load() {
: nlohmann::json(52888u);
}
Require(settings["ports"].is_object(), "Invalid LAN port settings");
// Preserve the shared port when upgrading the IPv4 wildcard to dual stack.
if (settings["ports"].contains("0.0.0.0")) {
if (!settings["ports"].contains(kAllInterfaces))
settings["ports"][kAllInterfaces] = settings["ports"]["0.0.0.0"];
settings["ports"].erase("0.0.0.0");
}
for (const auto& [ip, port] : settings["ports"].items())
Require((IsIpv4(ip) || ip == kAllIpv4Interfaces) &&
Require((IsIpAddress(ip) || ip == kAllInterfaces) &&
port.is_number_integer() && port > 0 && port <= 65535,
"Invalid LAN port settings");
settings["address"] = address;
@@ -668,15 +704,15 @@ bool Identity::VerifyPassword(const nlohmann::json& verifier,
}
void Identity::SaveSettings(const std::string& address, unsigned port) {
Require((IsIpv4(address) || address == kAllIpv4Interfaces) && port > 0 &&
Require((IsIpAddress(address) || address == kAllInterfaces) && port > 0 &&
port <= 65535,
"Invalid LAN listener address");
auto next = state_;
const auto interfaces = LocalIpv4Interfaces();
const auto interfaces = LocalIpInterfaces();
const auto selected =
std::find_if(interfaces.begin(), interfaces.end(),
[&](const auto& i) { return i.address == address; });
Require(address == kAllIpv4Interfaces || selected != interfaces.end(),
Require(address == kAllInterfaces || selected != interfaces.end(),
"Selected LAN interface is unavailable");
next["settings"]["ports"][address] = port;
Save(next);
@@ -689,7 +725,7 @@ bool Identity::HasPortFor(const std::string& address) const {
return state_.at("settings").at("ports").contains(address);
}
void Identity::SelectAddress(const std::string& address) {
const auto interfaces = LocalIpv4Interfaces(!listen_all());
const auto interfaces = LocalIpInterfaces(!listen_all());
Require(std::any_of(interfaces.begin(), interfaces.end(),
[&](const auto& i) { return i.address == address; }),
"Selected LAN interface is unavailable");
@@ -704,7 +740,7 @@ void Identity::SaveListenerMode(bool listen_all,
auto next = state_;
next["settings"]["listen_all"] = listen_all;
for (const auto& [address, port] : ports) {
Require((IsIpv4(address) || address == kAllIpv4Interfaces) && port > 0 &&
Require((IsIpAddress(address) || address == kAllInterfaces) && port > 0 &&
port <= 65535,
"Invalid LAN listener address");
next["settings"]["ports"][address] = port;
+7 -4
View File
@@ -28,10 +28,13 @@ struct Target {
unsigned ParsePort(const std::string& value);
std::pair<std::string, unsigned> ParseEndpoint(const std::string& value,
unsigned default_port);
bool IsIpv4(const std::string& value);
inline constexpr char kAllIpv4Interfaces[] = "0.0.0.0";
// Non-loopback unicast IPv4 addresses, optionally restricted to physical NICs.
std::vector<Interface> LocalIpv4Interfaces(bool physical_only = false);
bool IsIpAddress(const std::string& value);
std::string NormalizeAddress(const std::string& value);
std::string FormatEndpoint(const std::string& address, unsigned port);
inline constexpr char kAllInterfaces[] = "::";
// Non-loopback unicast IPv4/IPv6 addresses, optionally restricted to physical
// NICs.
std::vector<Interface> LocalIpInterfaces(bool physical_only = false);
std::string RandomHex(size_t bytes);
bool ValidPassword(const std::string& password);
bool VerifyLanCertificate(X509* cert);
@@ -25,12 +25,14 @@ bool Id(const Json& value) {
});
}
Json Capabilities() {
return Json::array({"host-only-ipv4", "dtls-srtp-all", "password-auth-v1"});
return Json::array({"host-only-ipv4", "host-only-ipv6", "dtls-srtp-all",
"password-auth-v1"});
}
void Capable(const Json& j) {
void Capable(const Json& j, bool ipv6) {
const auto& caps = j.at("capabilities");
Check(caps.is_array() && caps.size() <= 16);
for (const auto& required : Capabilities())
for (const auto* required : {ipv6 ? "host-only-ipv6" : "host-only-ipv4",
"dtls-srtp-all", "password-auth-v1"})
Check(std::find(caps.begin(), caps.end(), required) != caps.end());
}
void Wipe(std::string& text) {
@@ -123,14 +125,14 @@ void SessionManager::Configure(const std::string& address, unsigned port) {
if (!port || port > 65535) throw std::runtime_error("Invalid LAN port");
if (!Identity::CurrentVerifier(identity_.verifier()))
throw std::runtime_error("LAN password is not ready");
ReplaceListener(identity_.listen_all() ? kAllIpv4Interfaces : address, port,
ReplaceListener(identity_.listen_all() ? kAllInterfaces : address, port,
true);
}
void SessionManager::SetListenAll(bool enabled) {
if (stopping_ || enabled == identity_.listen_all()) return;
if (!Identity::CurrentVerifier(identity_.verifier()))
throw std::runtime_error("LAN password is not ready");
const auto interfaces = LocalIpv4Interfaces(!enabled);
const auto interfaces = LocalIpInterfaces(!enabled);
const auto previous = listeners_;
// Wildcard and per-address listeners can overlap on the same port. Retire
// acceptors before rebinding; authenticated channels retain their sockets.
@@ -144,7 +146,7 @@ void SessionManager::SetListenAll(bool enabled) {
ports[address] = listeners_.at(address).port;
};
if (enabled)
add(kAllIpv4Interfaces);
add(kAllInterfaces);
else
for (const auto& interface : interfaces) add(interface.address);
// Publish the mode and all newly allocated ports in one durable write.
@@ -263,8 +265,8 @@ void SessionManager::SetListenerError(const std::string& address, unsigned port,
listener_warnings_[address] = std::move(issue);
}
std::string SessionManager::ListenerError(const std::string& address) const {
const auto it = listener_errors_.find(
identity_.listen_all() ? kAllIpv4Interfaces : address);
const auto it =
listener_errors_.find(identity_.listen_all() ? kAllInterfaces : address);
return it == listener_errors_.end() ? "" : it->second.error;
}
std::optional<SessionManager::ListenerIssue> SessionManager::TakeListenerWarning() {
@@ -277,7 +279,7 @@ std::optional<SessionManager::ListenerIssue> SessionManager::TakeListenerWarning
void SessionManager::RefreshListeners() {
if (stopping_ || !Identity::CurrentVerifier(identity_.verifier())) return;
const auto available = [&](const auto& address) {
if (identity_.listen_all()) return address == kAllIpv4Interfaces;
if (identity_.listen_all()) return address == kAllInterfaces;
return std::any_of(interfaces_.begin(), interfaces_.end(),
[&](const auto& i) { return i.address == address; });
};
@@ -298,7 +300,7 @@ void SessionManager::RefreshListeners() {
it = listener_errors_.erase(it);
}
const auto desired = identity_.listen_all()
? std::vector<Interface>{{kAllIpv4Interfaces}}
? std::vector<Interface>{{kAllInterfaces}}
: interfaces_;
for (const auto& interface : desired) {
const auto port = identity_.PortFor(interface.address);
@@ -354,7 +356,7 @@ bool SessionManager::Send(const SessionPtr& s, Json j, bool sequenced) {
int SessionManager::RetryAfter(const std::string& address,
unsigned port) const {
const auto [ip, endpoint_port] = ParseEndpoint(address, port);
const auto it = retry_at_.find(ip + ":" + std::to_string(endpoint_port));
const auto it = retry_at_.find(FormatEndpoint(ip, endpoint_port));
const auto now = Clock::now();
return it == retry_at_.end() || it->second <= now
? 0
@@ -446,7 +448,7 @@ void SessionManager::Message(const SessionPtr& s, const std::string& text) {
else
++it;
if (retry_at_.size() < 64)
retry_at_[s->target.address + ":" + std::to_string(s->target.port)] =
retry_at_[FormatEndpoint(s->target.address, s->target.port)] =
now + std::chrono::seconds(j["retry_after"].get<int>());
}
Close(s, error);
@@ -477,7 +479,7 @@ void SessionManager::Message(const SessionPtr& s, const std::string& text) {
Check(type == "auth" && !j.contains("session_id") && !j.contains("seq") &&
Id(j.at("request_id")) && j.at("password").is_string() &&
!j.contains("share") && !j.contains("proof"));
Capable(j);
Capable(j, s->channel->remote_address.find(':') != std::string::npos);
s->auth_request = j.at("request_id");
auto& submitted = j["password"].get_ref<std::string&>();
if (!ValidPassword(submitted)) {
@@ -814,9 +816,9 @@ void SessionManager::Tick() {
}
if (!stopping_ && now - interface_check_ >= std::chrono::seconds(2)) {
interface_check_ = now;
const auto local_interfaces = LocalIpv4Interfaces();
const auto local_interfaces = LocalIpInterfaces();
interfaces_ =
identity_.listen_all() ? local_interfaces : LocalIpv4Interfaces(true);
identity_.listen_all() ? local_interfaces : LocalIpInterfaces(true);
RefreshListeners();
for (auto& [_, s] : sessions_)
if (s->phase != Session::Connecting &&
@@ -81,8 +81,7 @@ class SessionManager {
const std::vector<Interface>& interfaces() const { return interfaces_; }
void SetListenAll(bool enabled);
unsigned PortFor(const std::string& address) const {
return identity_.PortFor(identity_.listen_all() ? kAllIpv4Interfaces
: address);
return identity_.PortFor(identity_.listen_all() ? kAllInterfaces : address);
}
void Configure(const std::string& address, unsigned port);
bool password_pending() const { return reset_job_.valid(); }
+61 -27
View File
@@ -13,6 +13,8 @@
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/server.hpp>
#include "direct_address.h"
namespace crossdesk::lan {
namespace {
using Clock = std::chrono::steady_clock;
@@ -25,15 +27,20 @@ constexpr const char* kPath = "/crossdesk/lan/v2";
constexpr const char* kProtocol = "crossdesk.lan.v2";
void ValidateListener(const std::string& address, unsigned port) {
if (port > 65535) throw std::runtime_error("Invalid LAN port");
if (address == kAllIpv4Interfaces) return;
const auto interfaces = LocalIpv4Interfaces();
if (!IsIpv4(address) ||
if (address == kAllInterfaces) return;
const auto interfaces = LocalIpInterfaces();
if (!IsIpAddress(address) ||
std::none_of(interfaces.begin(), interfaces.end(),
[&](const auto& i) { return i.address == address; }))
throw std::runtime_error("Selected LAN interface is unavailable");
}
Error ConfigureAcceptor(asio::ip::tcp::acceptor& acceptor, bool automatic_port) {
Error ConfigureAcceptor(asio::ip::tcp::acceptor& acceptor, bool automatic_port,
bool ipv6, bool dual_stack) {
Error error;
if (ipv6) {
acceptor.set_option(asio::ip::v6_only(!dual_stack), error);
if (error) return error;
}
#ifdef _WIN32
(void)automatic_port;
acceptor.set_option(asio::socket_base::reuse_address(false), error);
@@ -69,7 +76,7 @@ void CheckExistingEndpoint(const std::string& address, unsigned port) {
Error result;
bool timed_out = false;
socket.async_connect(
{asio::ip::make_address_v4(address), static_cast<unsigned short>(port)},
{asio::ip::make_address(address), static_cast<unsigned short>(port)},
[&](const Error& error) {
result = error;
deadline.cancel();
@@ -91,23 +98,27 @@ void CheckExistingEndpoint(const std::string& address, unsigned port) {
}
void CheckExistingListener(const std::string& address, unsigned port) {
#ifdef __APPLE__
if (address == kAllIpv4Interfaces) {
if (address == kAllInterfaces || address == "0.0.0.0") {
// BSD permits wildcard/specific overlap with SO_REUSEADDR. Probe binds on
// each exact address to detect specific listeners without sending traffic
// through VPN adapters which may accept connects to otherwise unused ports.
// The final wildcard bind detects an existing wildcard listener itself.
asio::io_context io;
auto addresses = LocalIpv4Interfaces();
auto addresses = LocalIpInterfaces();
addresses.push_back({"127.0.0.1"});
if (address == kAllInterfaces) addresses.push_back({"::1"});
for (const auto& interface : addresses) {
const auto ip = asio::ip::make_address(interface.address);
if (address == "0.0.0.0" && ip.is_v6()) continue;
asio::ip::tcp::acceptor probe(io);
Error error;
probe.open(asio::ip::tcp::v4(), error);
probe.open(ip.is_v6() ? asio::ip::tcp::v6() : asio::ip::tcp::v4(), error);
CheckListenError(error);
if (ip.is_v6()) probe.set_option(asio::ip::v6_only(true), error);
CheckListenError(error);
probe.set_option(asio::socket_base::reuse_address(true), error);
CheckListenError(error);
probe.bind({asio::ip::make_address_v4(interface.address),
static_cast<unsigned short>(port)}, error);
probe.bind({ip, static_cast<unsigned short>(port)}, error);
CheckListenError(error);
}
return;
@@ -115,6 +126,29 @@ void CheckExistingListener(const std::string& address, unsigned port) {
#endif
CheckExistingEndpoint(address, port);
}
// Fall back only when the host has no usable IPv6 stack. A busy requested
// port must still be reported, never silently degrade a dual-stack listener.
bool SupportsDualStack() {
asio::io_context io;
asio::ip::tcp::acceptor probe(io);
Error error;
probe.open(asio::ip::tcp::v6(), error);
if (!error) probe.set_option(asio::ip::v6_only(false), error);
if (!error) probe.bind({asio::ip::address_v6::any(), 0}, error);
if (error == asio::error::address_family_not_supported ||
error == std::errc::protocol_not_supported ||
error == asio::error::no_protocol_option ||
error == asio::error::operation_not_supported ||
error == std::errc::address_not_available)
return false;
CheckListenError(error);
return true;
}
std::string SocketAddress(asio::ip::address address) {
if (address.is_v6() && address.to_v6().is_v4_mapped())
address = asio::ip::make_address_v4(asio::ip::v4_mapped, address.to_v6());
return minirtc::DirectAddressString(address);
}
std::shared_ptr<Context> TlsContext() {
auto context = std::make_shared<Context>(Context::tls);
if (SSL_CTX_set_min_proto_version(context->native_handle(), TLS1_3_VERSION) !=
@@ -286,14 +320,10 @@ struct Transport::Impl {
Abort(c);
return;
}
if (!local.address().is_v4() || !remote.address().is_v4()) {
Abort(c);
return;
}
c->local_address = local.address().to_string();
c->remote_address = remote.address().to_string();
const auto interfaces = LocalIpv4Interfaces();
if (!IsIpv4(c->remote_address) ||
c->local_address = SocketAddress(local.address());
c->remote_address = SocketAddress(remote.address());
const auto interfaces = LocalIpInterfaces();
if (!minirtc::IsDirectAddressPair(c->local_address, c->remote_address) ||
std::none_of(
interfaces.begin(), interfaces.end(),
[&](const auto& i) { return i.address == c->local_address; })) {
@@ -389,7 +419,10 @@ Transport::Listener Transport::Listen(const Identity& identity,
auto future = promise->get_future();
asio::post(impl_->io, [this, address, port, context, promise] {
try {
if (port) CheckExistingListener(address, port);
const auto binding = address == kAllInterfaces && !SupportsDualStack()
? std::string("0.0.0.0")
: address;
if (port) CheckExistingListener(binding, port);
auto server = std::make_shared<Server>();
impl_->Configure(server.get());
const uint64_t id = impl_->next_id++;
@@ -427,15 +460,16 @@ Transport::Listener Transport::Listen(const Identity& identity,
});
auto bound_acceptor = std::make_shared<Server::acceptor_ptr>();
server->set_tcp_pre_bind_handler(
[port, bound_acceptor](Server::acceptor_ptr acceptor) {
[port, binding, bound_acceptor](Server::acceptor_ptr acceptor) {
*bound_acceptor = acceptor;
return ConfigureAcceptor(*acceptor, port == 0);
return ConfigureAcceptor(*acceptor, port == 0,
binding.find(':') != std::string::npos,
binding == kAllInterfaces);
});
Error listen_error;
server->listen(
asio::ip::tcp::endpoint(asio::ip::make_address_v4(address),
static_cast<unsigned short>(port)),
listen_error);
server->listen(asio::ip::tcp::endpoint(asio::ip::make_address(binding),
static_cast<unsigned short>(port)),
listen_error);
CheckListenError(listen_error);
const auto actual_port = server->get_local_endpoint(listen_error).port();
CheckListenError(listen_error);
@@ -474,7 +508,7 @@ void Transport::StopAccept(uint64_t listener) {
});
}
Transport::ChannelPtr Transport::Connect(const Target& target) {
if (!IsIpv4(target.address) || target.port == 0 || target.port > 65535)
if (!IsIpAddress(target.address) || target.port == 0 || target.port > 65535)
throw std::runtime_error("Invalid LAN endpoint");
auto c = std::make_shared<Channel>();
c->id = impl_->next_id++;
@@ -505,7 +539,7 @@ Transport::ChannelPtr Transport::Connect(const Target& target) {
client->set_tls_init_handler([context](Hdl) { return context; });
Error error;
auto connection = client->get_connection(
"wss://" + target.address + ":" + std::to_string(target.port) + kPath,
"wss://" + FormatEndpoint(target.address, target.port) + kPath,
error);
if (error) {
impl_->Finish(c);