mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 12:32:53 +08:00
[feat] add configurable all-interface IPv4 listening, refs #129
This commit is contained in:
@@ -405,7 +405,7 @@ std::pair<std::string, unsigned> ParseEndpoint(const std::string& value,
|
||||
"Enter a numeric IPv4 address or IPv4:port");
|
||||
return {address, port};
|
||||
}
|
||||
std::vector<Interface> PhysicalInterfaces() {
|
||||
std::vector<Interface> LocalIpv4Interfaces(bool physical_only) {
|
||||
std::vector<Interface> result;
|
||||
#ifdef _WIN32
|
||||
ULONG bytes = 16384;
|
||||
@@ -424,17 +424,22 @@ std::vector<Interface> PhysicalInterfaces() {
|
||||
if (code != NO_ERROR) return result;
|
||||
for (auto* a = adapters; a; a = a->Next) {
|
||||
if (a->OperStatus != IfOperStatusUp ||
|
||||
(a->IfType != IF_TYPE_ETHERNET_CSMACD &&
|
||||
a->IfType != IF_TYPE_IEEE80211) ||
|
||||
a->PhysicalAddressLength == 0)
|
||||
continue;
|
||||
// HardwareInterface rejects software adapters that imitate Ethernet.
|
||||
MIB_IF_ROW2 row{};
|
||||
row.InterfaceLuid = a->Luid;
|
||||
if (GetIfEntry2(&row) != NO_ERROR ||
|
||||
!row.InterfaceAndOperStatusFlags.HardwareInterface)
|
||||
a->IfType == IF_TYPE_SOFTWARE_LOOPBACK)
|
||||
continue;
|
||||
if (physical_only) {
|
||||
if ((a->IfType != IF_TYPE_ETHERNET_CSMACD &&
|
||||
a->IfType != IF_TYPE_IEEE80211) ||
|
||||
a->PhysicalAddressLength == 0)
|
||||
continue;
|
||||
MIB_IF_ROW2 row{};
|
||||
row.InterfaceLuid = a->Luid;
|
||||
if (GetIfEntry2(&row) != NO_ERROR ||
|
||||
!row.InterfaceAndOperStatusFlags.HardwareInterface)
|
||||
continue;
|
||||
}
|
||||
for (auto* u = a->FirstUnicastAddress; u; u = u->Next) {
|
||||
if (!u->Address.lpSockaddr || u->Address.lpSockaddr->sa_family != AF_INET)
|
||||
continue;
|
||||
char address[INET_ADDRSTRLEN]{};
|
||||
inet_ntop(
|
||||
AF_INET,
|
||||
@@ -448,8 +453,8 @@ std::vector<Interface> PhysicalInterfaces() {
|
||||
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 ||
|
||||
!(a->ifa_flags & IFF_UP) ||
|
||||
(a->ifa_flags & (IFF_LOOPBACK | IFF_POINTOPOINT)))
|
||||
!(a->ifa_flags & IFF_UP) || (a->ifa_flags & IFF_LOOPBACK) ||
|
||||
(physical_only && (a->ifa_flags & IFF_POINTOPOINT)))
|
||||
continue;
|
||||
bool physical = false;
|
||||
#ifdef __APPLE__
|
||||
@@ -464,7 +469,7 @@ std::vector<Interface> PhysicalInterfaces() {
|
||||
physical = std::filesystem::exists(std::filesystem::path("/sys/class/net") /
|
||||
a->ifa_name / "device");
|
||||
#endif
|
||||
if (!physical) continue;
|
||||
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));
|
||||
@@ -472,6 +477,13 @@ std::vector<Interface> PhysicalInterfaces() {
|
||||
}
|
||||
freeifaddrs(list);
|
||||
#endif
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const auto& a, const auto& b) { return a.address < b.address; });
|
||||
result.erase(std::unique(result.begin(), result.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.address == b.address;
|
||||
}),
|
||||
result.end());
|
||||
return result;
|
||||
}
|
||||
std::string RandomHex(size_t bytes) {
|
||||
@@ -533,6 +545,9 @@ void Identity::Load() {
|
||||
auto& settings = state_["settings"];
|
||||
if (settings.is_null()) settings = nlohmann::json::object();
|
||||
Require(settings.is_object(), "Invalid LAN settings");
|
||||
Require(
|
||||
!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");
|
||||
if (!settings.contains("ports")) {
|
||||
@@ -544,7 +559,8 @@ void Identity::Load() {
|
||||
}
|
||||
Require(settings["ports"].is_object(), "Invalid LAN port settings");
|
||||
for (const auto& [ip, port] : settings["ports"].items())
|
||||
Require(IsIpv4(ip) && port.is_number_integer() && port > 0 && port <= 65535,
|
||||
Require((IsIpv4(ip) || ip == kAllIpv4Interfaces) &&
|
||||
port.is_number_integer() && port > 0 && port <= 65535,
|
||||
"Invalid LAN port settings");
|
||||
settings["address"] = address;
|
||||
settings["version"] = 2;
|
||||
@@ -652,14 +668,16 @@ bool Identity::VerifyPassword(const nlohmann::json& verifier,
|
||||
}
|
||||
|
||||
void Identity::SaveSettings(const std::string& address, unsigned port) {
|
||||
Require(IsIpv4(address) && port > 0 && port <= 65535,
|
||||
Require((IsIpv4(address) || address == kAllIpv4Interfaces) && port > 0 &&
|
||||
port <= 65535,
|
||||
"Invalid LAN listener address");
|
||||
auto next = state_;
|
||||
const auto interfaces = PhysicalInterfaces();
|
||||
const auto selected = std::find_if(
|
||||
interfaces.begin(), interfaces.end(),
|
||||
[&](const auto& i) { return i.address == address; });
|
||||
Require(selected != interfaces.end(), "Selected LAN interface is unavailable");
|
||||
const auto interfaces = LocalIpv4Interfaces();
|
||||
const auto selected =
|
||||
std::find_if(interfaces.begin(), interfaces.end(),
|
||||
[&](const auto& i) { return i.address == address; });
|
||||
Require(address == kAllIpv4Interfaces || selected != interfaces.end(),
|
||||
"Selected LAN interface is unavailable");
|
||||
next["settings"]["ports"][address] = port;
|
||||
Save(next);
|
||||
state_ = std::move(next);
|
||||
@@ -671,7 +689,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 = PhysicalInterfaces();
|
||||
const auto interfaces = LocalIpv4Interfaces(!listen_all());
|
||||
Require(std::any_of(interfaces.begin(), interfaces.end(),
|
||||
[&](const auto& i) { return i.address == address; }),
|
||||
"Selected LAN interface is unavailable");
|
||||
@@ -681,4 +699,17 @@ void Identity::SelectAddress(const std::string& address) {
|
||||
Save(next);
|
||||
state_ = std::move(next);
|
||||
}
|
||||
void Identity::SaveListenerMode(bool listen_all,
|
||||
const std::map<std::string, unsigned>& ports) {
|
||||
auto next = state_;
|
||||
next["settings"]["listen_all"] = listen_all;
|
||||
for (const auto& [address, port] : ports) {
|
||||
Require((IsIpv4(address) || address == kAllIpv4Interfaces) && port > 0 &&
|
||||
port <= 65535,
|
||||
"Invalid LAN listener address");
|
||||
next["settings"]["ports"][address] = port;
|
||||
}
|
||||
Save(next);
|
||||
state_ = std::move(next);
|
||||
}
|
||||
} // namespace crossdesk::lan
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -28,7 +29,9 @@ 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);
|
||||
std::vector<Interface> PhysicalInterfaces();
|
||||
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);
|
||||
std::string RandomHex(size_t bytes);
|
||||
bool ValidPassword(const std::string& password);
|
||||
bool VerifyLanCertificate(X509* cert);
|
||||
@@ -51,6 +54,9 @@ class Identity {
|
||||
// their saved port. Selection only controls the address shown in the UI.
|
||||
unsigned PortFor(const std::string& address) const;
|
||||
bool HasPortFor(const std::string& address) const;
|
||||
bool listen_all() const { return settings().value("listen_all", true); }
|
||||
void SaveListenerMode(bool listen_all,
|
||||
const std::map<std::string, unsigned>& ports);
|
||||
void SelectAddress(const std::string& address);
|
||||
const std::string& id() const { return id_; }
|
||||
std::filesystem::path certificate() const {
|
||||
|
||||
@@ -100,7 +100,8 @@ SessionManager::SessionManager(std::filesystem::path path, Hooks hooks)
|
||||
// Migrate the saved password, retaining its value, identity and settings.
|
||||
SetPassword(access_password_);
|
||||
} else if (identity_.verifier().is_null()) {
|
||||
// Generate the initial password before automatically accepting connections.
|
||||
// Generate the initial password before automatically accepting
|
||||
// connections.
|
||||
ResetPassword();
|
||||
} else {
|
||||
hooks_.error("LAN password is not ready");
|
||||
@@ -122,7 +123,49 @@ 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(address, port, true);
|
||||
ReplaceListener(identity_.listen_all() ? kAllIpv4Interfaces : 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 previous = listeners_;
|
||||
// Wildcard and per-address listeners can overlap on the same port. Retire
|
||||
// acceptors before rebinding; authenticated channels retain their sockets.
|
||||
for (const auto& [_, listener] : previous) RetireListener(listener.id);
|
||||
listeners_.clear();
|
||||
std::map<std::string, unsigned> ports;
|
||||
try {
|
||||
const auto add = [&](const std::string& address) {
|
||||
ReplaceListener(address, identity_.PortFor(address), false,
|
||||
!identity_.HasPortFor(address));
|
||||
ports[address] = listeners_.at(address).port;
|
||||
};
|
||||
if (enabled)
|
||||
add(kAllIpv4Interfaces);
|
||||
else
|
||||
for (const auto& interface : interfaces) add(interface.address);
|
||||
// Publish the mode and all newly allocated ports in one durable write.
|
||||
identity_.SaveListenerMode(enabled, ports);
|
||||
} catch (...) {
|
||||
for (const auto& [_, listener] : listeners_) RetireListener(listener.id);
|
||||
listeners_.clear();
|
||||
// Keep the original configuration and restore its exact ports. A failed
|
||||
// restore remains visible and is retried by ordinary listener maintenance.
|
||||
for (const auto& [address, listener] : previous) {
|
||||
try {
|
||||
ReplaceListener(address, listener.port, false);
|
||||
} catch (const std::exception& error) {
|
||||
SetListenerError(address, listener.port, error.what());
|
||||
}
|
||||
}
|
||||
throw;
|
||||
}
|
||||
interfaces_ = interfaces;
|
||||
listener_errors_.clear();
|
||||
listener_warnings_.clear();
|
||||
}
|
||||
void SessionManager::ReplaceListener(const std::string& address, unsigned port,
|
||||
bool save, bool allow_fallback) {
|
||||
@@ -220,7 +263,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(address);
|
||||
const auto it = listener_errors_.find(
|
||||
identity_.listen_all() ? kAllIpv4Interfaces : address);
|
||||
return it == listener_errors_.end() ? "" : it->second.error;
|
||||
}
|
||||
std::optional<SessionManager::ListenerIssue> SessionManager::TakeListenerWarning() {
|
||||
@@ -233,6 +277,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;
|
||||
return std::any_of(interfaces_.begin(), interfaces_.end(),
|
||||
[&](const auto& i) { return i.address == address; });
|
||||
};
|
||||
@@ -252,7 +297,10 @@ void SessionManager::RefreshListeners() {
|
||||
listener_warnings_.erase(it->first);
|
||||
it = listener_errors_.erase(it);
|
||||
}
|
||||
for (const auto& interface : interfaces_) {
|
||||
const auto desired = identity_.listen_all()
|
||||
? std::vector<Interface>{{kAllIpv4Interfaces}}
|
||||
: interfaces_;
|
||||
for (const auto& interface : desired) {
|
||||
const auto port = identity_.PortFor(interface.address);
|
||||
const bool initialize = !identity_.HasPortFor(interface.address);
|
||||
try {
|
||||
@@ -672,7 +720,10 @@ void SessionManager::Tick() {
|
||||
for (auto& event : transport_.Poll()) {
|
||||
auto it = sessions_.find(event.channel->id);
|
||||
if (it == sessions_.end()) {
|
||||
const auto listener = listeners_.find(event.channel->local_address);
|
||||
const auto listener = std::find_if(
|
||||
listeners_.begin(), listeners_.end(), [&](const auto& entry) {
|
||||
return entry.second.id == event.channel->listener;
|
||||
});
|
||||
if (event.kind != Transport::Event::Open || !event.channel->incoming ||
|
||||
stopping_ || listener == listeners_.end() ||
|
||||
event.channel->listener != listener->second.id) {
|
||||
@@ -763,12 +814,14 @@ void SessionManager::Tick() {
|
||||
}
|
||||
if (!stopping_ && now - interface_check_ >= std::chrono::seconds(2)) {
|
||||
interface_check_ = now;
|
||||
interfaces_ = PhysicalInterfaces();
|
||||
const auto local_interfaces = LocalIpv4Interfaces();
|
||||
interfaces_ =
|
||||
identity_.listen_all() ? local_interfaces : LocalIpv4Interfaces(true);
|
||||
RefreshListeners();
|
||||
for (auto& [_, s] : sessions_)
|
||||
if (s->phase != Session::Connecting &&
|
||||
!s->channel->local_address.empty() &&
|
||||
std::none_of(interfaces_.begin(), interfaces_.end(),
|
||||
std::none_of(local_interfaces.begin(), local_interfaces.end(),
|
||||
[&](const auto& i) {
|
||||
return i.address == s->channel->local_address;
|
||||
}))
|
||||
|
||||
@@ -79,6 +79,11 @@ class SessionManager {
|
||||
Identity& identity() { return identity_; }
|
||||
// Refreshed by Tick; UI and listener maintenance share the same snapshot.
|
||||
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);
|
||||
}
|
||||
void Configure(const std::string& address, unsigned port);
|
||||
bool password_pending() const { return reset_job_.valid(); }
|
||||
void ResetPassword();
|
||||
|
||||
@@ -25,7 +25,8 @@ 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");
|
||||
const auto interfaces = PhysicalInterfaces();
|
||||
if (address == kAllIpv4Interfaces) return;
|
||||
const auto interfaces = LocalIpv4Interfaces();
|
||||
if (!IsIpv4(address) ||
|
||||
std::none_of(interfaces.begin(), interfaces.end(),
|
||||
[&](const auto& i) { return i.address == address; }))
|
||||
@@ -57,7 +58,7 @@ void CheckListenError(const Error& error) {
|
||||
throw std::runtime_error("LanPortPermissionDenied");
|
||||
throw std::runtime_error("Cannot listen on LAN port: " + error.message());
|
||||
}
|
||||
void CheckExistingListener(const std::string& address, unsigned port) {
|
||||
void CheckExistingEndpoint(const std::string& address, unsigned port) {
|
||||
#ifdef __APPLE__
|
||||
// BSD permits a specific-address listener alongside a wildcard listener with
|
||||
// SO_REUSEADDR. Detect that live listener without treating TIME_WAIT or our
|
||||
@@ -88,6 +89,32 @@ void CheckExistingListener(const std::string& address, unsigned port) {
|
||||
(void)port;
|
||||
#endif
|
||||
}
|
||||
void CheckExistingListener(const std::string& address, unsigned port) {
|
||||
#ifdef __APPLE__
|
||||
if (address == kAllIpv4Interfaces) {
|
||||
// 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();
|
||||
addresses.push_back({"127.0.0.1"});
|
||||
for (const auto& interface : addresses) {
|
||||
asio::ip::tcp::acceptor probe(io);
|
||||
Error error;
|
||||
probe.open(asio::ip::tcp::v4(), 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);
|
||||
CheckListenError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
CheckExistingEndpoint(address, port);
|
||||
}
|
||||
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) !=
|
||||
@@ -265,7 +292,7 @@ struct Transport::Impl {
|
||||
}
|
||||
c->local_address = local.address().to_string();
|
||||
c->remote_address = remote.address().to_string();
|
||||
const auto interfaces = PhysicalInterfaces();
|
||||
const auto interfaces = LocalIpv4Interfaces();
|
||||
if (!IsIpv4(c->remote_address) ||
|
||||
std::none_of(
|
||||
interfaces.begin(), interfaces.end(),
|
||||
|
||||
Reference in New Issue
Block a user