mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-02 03:52:43 +08:00
[feat] add configurable all-interface IPv4 listening, refs #129
This commit is contained in:
@@ -1529,6 +1529,24 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
|
||||
main->set_codec_index(video_encode_format_button_value_);
|
||||
main->set_ip_direct_enabled_draft(config_center_->IsIpDirectEnabled());
|
||||
main->set_settings_save_error("");
|
||||
try {
|
||||
bool listen_all = true;
|
||||
if (lan_) {
|
||||
listen_all = lan_->identity().listen_all();
|
||||
} else {
|
||||
const auto directory = path_manager_->GetConfigPath() / "lan";
|
||||
if (std::filesystem::exists(directory / "identity.json")) {
|
||||
lan::Identity identity(directory);
|
||||
identity.Load();
|
||||
listen_all = identity.listen_all();
|
||||
}
|
||||
}
|
||||
main->set_lan_listen_all(listen_all);
|
||||
main->set_lan_listen_all_draft(listen_all);
|
||||
} catch (const std::exception& error) {
|
||||
main->set_settings_save_error(
|
||||
main->global<ui::LanStrings>().invoke_error_message(UiText(error.what())));
|
||||
}
|
||||
main->set_force_relay_enabled(config_center_->IsForceRelay());
|
||||
main->set_self_hosted_enabled(enable_self_hosted_);
|
||||
main->set_autostart_enabled(enable_autostart_);
|
||||
@@ -1767,7 +1785,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
||||
try {
|
||||
lan_->identity().SelectAddress(std::string(address));
|
||||
const auto port =
|
||||
UiText(std::to_string(lan_->identity().PortFor(std::string(address))));
|
||||
UiText(std::to_string(lan_->PortFor(std::string(address))));
|
||||
main->set_lan_listener_ip(address);
|
||||
main->set_lan_listener_port(port);
|
||||
main->set_lan_local_port(port);
|
||||
@@ -2452,6 +2470,7 @@ void GuiApplication::Tick() {
|
||||
TickLan();
|
||||
if (LanAvailable()) {
|
||||
const auto address = lan_->identity().settings().value("address", "");
|
||||
ui_->main->set_lan_listen_all(lan_->identity().listen_all());
|
||||
ui_->main->set_lan_password_pending(lan_->password_pending());
|
||||
ui_->main->set_lan_listening(lan_->listening());
|
||||
if (lan_connecting_ && lan_connecting_->authorized) {
|
||||
@@ -2504,7 +2523,7 @@ void GuiApplication::Tick() {
|
||||
? available_interfaces.front().address : "";
|
||||
}
|
||||
const auto port =
|
||||
UiText(std::to_string(lan_->identity().PortFor(display_address)));
|
||||
UiText(std::to_string(lan_->PortFor(display_address)));
|
||||
if (previous_address != display_address) {
|
||||
// A disappearing address must not transfer its unfinished port edit to
|
||||
// another interface. Explicit selection commits the old edit first.
|
||||
@@ -2523,7 +2542,8 @@ void GuiApplication::Tick() {
|
||||
if (!ui_->main->get_lan_port_editing() && !ui_->main->get_lan_port_dirty())
|
||||
ui_->main->set_lan_local_port(port);
|
||||
ui_->main->set_lan_listener_error(UiText(
|
||||
display_address.empty() ? "Selected LAN interface is unavailable"
|
||||
display_address.empty() && !lan_->identity().listen_all()
|
||||
? "Selected LAN interface is unavailable"
|
||||
: lan_->ListenerError(display_address)));
|
||||
// Show failures one address at a time, without interrupting edits or another
|
||||
// dialog. A working listener must not clear another address's error.
|
||||
@@ -4570,6 +4590,8 @@ bool GuiApplication::ValidateSelfHostedSettingsFromUi(bool gallery_window) {
|
||||
|
||||
bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
||||
const auto previous_server = GetSignalServerSettings(*config_center_);
|
||||
const bool previous_ip_enabled = config_center_->IsIpDirectEnabled();
|
||||
const auto previous_connection_mode = config_center_->GetConnectionMode();
|
||||
auto& main = ui_->Window(gallery_window);
|
||||
main->set_settings_save_error("");
|
||||
const bool has_endpoint = main->get_self_hosted_enabled() ||
|
||||
@@ -4630,6 +4652,32 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
||||
localization::Literal("无法保存隐私屏解锁设置,请重试。", localization_language_index_)));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const bool listen_all = main->get_lan_listen_all_draft();
|
||||
if (LanAvailable() && main->get_ip_direct_enabled_draft()) {
|
||||
lan_->SetListenAll(listen_all);
|
||||
} else if (lan_) {
|
||||
// Persist the preference without rebinding a disabled/stopping listener.
|
||||
if (listen_all != lan_->identity().listen_all())
|
||||
lan_->identity().SaveListenerMode(listen_all, {});
|
||||
} else if (listen_all != main->get_lan_listen_all()) {
|
||||
// Do not open/create an inactive identity when this preference is unchanged.
|
||||
lan::Identity identity(path_manager_->GetConfigPath() / "lan");
|
||||
identity.Load();
|
||||
if (listen_all != identity.listen_all())
|
||||
identity.SaveListenerMode(listen_all, {});
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
if (config_center_->SetIpDirectEnabled(previous_ip_enabled) != 0 ||
|
||||
config_center_->SetConnectionMode(previous_connection_mode) != 0)
|
||||
LOG_ERROR("Failed to restore IP availability after listener settings failure");
|
||||
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
|
||||
LOG_ERROR("Failed to restore DeskHub settings after listener settings failure");
|
||||
main->set_settings_tab(0);
|
||||
main->set_settings_save_error(
|
||||
main->global<ui::LanStrings>().invoke_error_message(UiText(error.what())));
|
||||
return false;
|
||||
}
|
||||
language_button_value_ =
|
||||
std::clamp(main->get_language_index(), 0, localization::kSystemLanguage);
|
||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||
|
||||
@@ -492,6 +492,8 @@ inline constexpr TranslationRow kTranslationRows[] = {
|
||||
{"高", {"高", "High", "Высокое", "高", "高", "높음", "Hoch", "Élevée", "Alta", "Alta"}},
|
||||
{"(仅在交互终端显示)", {"(仅在交互终端显示)", "(shown only in an interactive terminal)", "(виден только в интерактивном терминале)", "(僅在互動終端顯示)", "(対話型端末でのみ表示)", "(대화형 터미널에만 표시됨)", "(wird nur in einem interaktiven Terminal angezeigt)", "(affiché uniquement dans un terminal interactif)", "(se muestra solo en una terminal interactiva)", "(mostrado apenas em um terminal interativo)"}},
|
||||
{":同步键鼠到所有已连接设备", {":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах", ":同步鍵鼠到所有已連線裝置", ": 接続されているすべてのデバイスへのミラー入力", ": 연결된 모든 장치에 대한 미러 입력", ": Eingang auf alle angeschlossenen Geräte spiegeln", ": entrée miroir sur tous les appareils connectés", ": entrada espejo para todos los dispositivos conectados", ": entrada espelhada para todos os dispositivos conectados"}},
|
||||
{"监听所有 IPv4 接口", {"监听所有 IPv4 接口", "Listen on all IPv4 interfaces", "Все интерфейсы IPv4", "監聽所有 IPv4 介面", "すべての IPv4 で待ち受け", "모든 IPv4 인터페이스 수신", "Alle IPv4-Schnittstellen", "Toutes les interfaces IPv4", "Todas las interfaces IPv4", "Todas as interfaces IPv4"}},
|
||||
{"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", {"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", "Use one port on all interfaces, including VPNs. Select an actual IP to share. Turn off to restore separate ports per IP.", "Один порт для всех интерфейсов, включая VPN. Для подключения выберите реальный IP. Отключение вернёт отдельные порты для каждого IP.", "所有介面共用一個連接埠,包括 VPN。分享時請選擇實際 IP;關閉後恢復各 IP 的獨立連接埠。", "VPN を含む全インターフェースで同じポートを使います。共有には実際の IP を選択してください。オフにすると IP ごとのポートに戻ります。", "VPN을 포함한 모든 인터페이스가 하나의 포트를 사용합니다. 공유할 실제 IP를 선택하세요. 끄면 IP별 포트가 복원됩니다.", "Ein Port für alle Schnittstellen, einschließlich VPN. Zum Teilen eine tatsächliche IP wählen. Ausschalten stellt die Ports pro IP wieder her.", "Un port commun à toutes les interfaces, VPN inclus. Choisissez une IP réelle à partager. Désactivez pour rétablir les ports propres à chaque IP.", "Un puerto para todas las interfaces, incluidas las VPN. Seleccione una IP real para compartir. Desactive para restaurar los puertos de cada IP.", "Uma porta para todas as interfaces, incluindo VPNs. Selecione um IP real para compartilhar. Desative para restaurar as portas de cada IP."}},
|
||||
};
|
||||
|
||||
struct NamedTranslation { const char* name; const char* literal; };
|
||||
@@ -986,6 +988,8 @@ inline constexpr const char* kSlintLiterals[] = {
|
||||
"未连接",
|
||||
"确认断开",
|
||||
"确认执行",
|
||||
"监听所有 IPv4 接口",
|
||||
"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。",
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
|
||||
@@ -9,5 +9,6 @@ export { FileTransferNotification } from "file_transfer_notification.slint";
|
||||
|
||||
export { GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
|
||||
export { GalleryStrings } from "device_strings.slint";
|
||||
export { LanStrings } from "lan_strings.slint";
|
||||
export { GalleryNetworkDevice } from "gallery_network_dialog.slint";
|
||||
export { DeviceGroupOption } from "device_group_dialog.slint";
|
||||
|
||||
@@ -465,6 +465,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
in-out property <bool> lan-listening;
|
||||
in-out property <bool> lan-listen-all: true;
|
||||
in-out property <string> lan-listener-ip;
|
||||
in-out property <string> lan-listener-port: "52888";
|
||||
out property <string> lan-listener-address: root.lan-listener-ip == "" ? "" : root.lan-listener-ip + ":" + root.lan-listener-port;
|
||||
@@ -634,6 +635,7 @@ export component MainWindow inherits Window {
|
||||
in-out property <int> capture-method-index: 0;
|
||||
in-out property <int> codec-index: 0;
|
||||
in-out property <bool> ip-direct-enabled-draft: true;
|
||||
in-out property <bool> lan-listen-all-draft: true;
|
||||
in property <string> settings-save-error;
|
||||
in-out property <bool> force-relay-enabled: false;
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
@@ -1282,7 +1284,7 @@ export component MainWindow inherits Window {
|
||||
width: lan-edit-port-metrics.preferred-width + 20px; height: 22px;
|
||||
quiet: true; text: lan-edit-port-metrics.text;
|
||||
text-size: 12px; text-color: WorkbenchStyle.accent;
|
||||
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||
enabled: root.lan-mode && !root.lan-password-pending && (root.lan-listen-all || root.lan-interfaces.length > 0);
|
||||
clicked => {
|
||||
// Moving from the editor to this action is still editing.
|
||||
root.lan-apply-pending = false;
|
||||
@@ -2720,11 +2722,16 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
SettingsGroup {
|
||||
title: GalleryStrings.texts[116]; rows: 3;
|
||||
title: GalleryStrings.texts[116]; rows: 4;
|
||||
SettingsToggleRow {
|
||||
label: GalleryStrings.texts[117];
|
||||
checked <=> root.ip-direct-enabled-draft;
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: GalleryStrings.texts[206];
|
||||
checked <=> root.lan-listen-all-draft;
|
||||
enabled: root.ip-direct-enabled-draft;
|
||||
}
|
||||
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: !root.settings-session-active; }
|
||||
SettingsToggleRow {
|
||||
label: UiStrings.self-hosted;
|
||||
@@ -2744,7 +2751,7 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: GalleryStrings.texts[119];
|
||||
text: GalleryStrings.texts[119] + "\n" + GalleryStrings.texts[207];
|
||||
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||
}
|
||||
SettingsGroup {
|
||||
|
||||
@@ -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