mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 20:47:43 +08:00
[feat] add IPv6 support for direct IP connections, refs #129
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user