[feat] add configurable all-interface IPv4 listening, refs #129

This commit is contained in:
dijunkun
2026-10-29 18:23:34 +08:00
parent dd774de4f8
commit f09a4c50d1
15 changed files with 266 additions and 50 deletions
+52 -21
View File
@@ -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
+7 -1
View File
@@ -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();
+30 -3
View File
@@ -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(),