diff --git a/assets/translations/en.json b/assets/translations/en.json index 113f3fc70..ab85f4c66 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -237,6 +237,7 @@ "wifi": "Wi-Fi", "password-prompt": "Enter password", "password-prompt-for": "Enter password for {ssid}", + "vpns": "VPNs", "unavailable-title": "NetworkManager unavailable", "unavailable-detail": "Install and enable NetworkManager to manage networks from here.", "no-networks": "No networks in range" diff --git a/src/dbus/network/network_service.cpp b/src/dbus/network/network_service.cpp index 60cb67cca..a5b3a3927 100644 --- a/src/dbus/network/network_service.cpp +++ b/src/dbus/network/network_service.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace { @@ -197,18 +198,21 @@ void NetworkService::setChangeCallback(ChangeCallback callback) { m_changeCallba void NetworkService::refresh() { const auto previousAps = m_accessPoints; + const auto previousVpns = m_vpnConnections; const auto previousSaved = m_savedSsids; refreshAccessPoints(); + refreshVpnConnections(); refreshSavedConnections(); NetworkState next = readState(); const bool apsChanged = previousAps != m_accessPoints; + const bool vpnsChanged = previousVpns != m_vpnConnections; const bool savedChanged = previousSaved != m_savedSsids; const bool stateChanged = next != m_state; const bool wirelessEnabledChanged = next.wirelessEnabled != m_state.wirelessEnabled; const NetworkChangeOrigin origin = wirelessEnabledChanged ? consumeWirelessEnabledChangeOrigin(next.wirelessEnabled) : NetworkChangeOrigin::External; m_state = std::move(next); - if ((stateChanged || apsChanged || savedChanged) && m_changeCallback) { + if ((stateChanged || apsChanged || vpnsChanged || savedChanged) && m_changeCallback) { m_changeCallback(m_state, origin); } } @@ -301,6 +305,54 @@ bool NetworkService::activateAccessPoint(const AccessPointInfo& ap) { } } +bool NetworkService::activateVpnConnection(const VpnConnectionInfo& vpn) { + if (vpn.path.empty()) { + return false; + } + try { + sdbus::ObjectPath activePath; + m_nm->callMethod("ActivateConnection") + .onInterface(k_nmInterface) + .withArguments(sdbus::ObjectPath{vpn.path}, sdbus::ObjectPath{"/"}, sdbus::ObjectPath{"/"}) + .storeResultsTo(activePath); + kLog.info("activating vpn name={} active={}", vpn.name, std::string(activePath)); + return true; + } catch (const sdbus::Error& e) { + kLog.warn("ActivateConnection(vpn) failed name={} path={} err={}", vpn.name, vpn.path, e.what()); + return false; + } +} + +bool NetworkService::deactivateVpnConnection(const VpnConnectionInfo& vpn) { + if (vpn.path.empty()) { + return false; + } + try { + std::vector activeConnections; + const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface); + activeConnections = activeVar.get>(); + for (const auto& activePath : activeConnections) { + try { + auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath); + const auto profilePath = + getPropertyOr(*active, k_nmActiveConnectionInterface, "Connection", sdbus::ObjectPath{}); + const auto activeState = getPropertyOr(*active, k_nmActiveConnectionInterface, "State", 0U); + if (profilePath != vpn.path || activeState != k_nmActiveConnectionStateActivated) { + continue; + } + m_nm->callMethod("DeactivateConnection").onInterface(k_nmInterface).withArguments(activePath); + kLog.info("deactivated vpn name={} active={}", vpn.name, std::string(activePath)); + return true; + } catch (const sdbus::Error&) { + } + } + } catch (const sdbus::Error& e) { + kLog.warn("DeactivateConnection(vpn) failed name={} path={} err={}", vpn.name, vpn.path, e.what()); + return false; + } + return false; +} + void NetworkService::setWirelessEnabled(bool enabled) { if (enabled != m_state.wirelessEnabled) { m_pendingLocalWirelessEnabled = enabled; @@ -517,6 +569,90 @@ void NetworkService::refreshSavedConnections() { m_savedSsids = std::move(next); } +void NetworkService::refreshVpnConnections() { + std::set activeVpnPaths; + try { + std::vector activeConnections; + const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface); + activeConnections = activeVar.get>(); + for (const auto& activePath : activeConnections) { + try { + auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath); + const auto type = getPropertyOr(*active, k_nmActiveConnectionInterface, "Type", std::string{}); + const auto state = getPropertyOr(*active, k_nmActiveConnectionInterface, "State", 0U); + if (type != "vpn" || state != k_nmActiveConnectionStateActivated) { + continue; + } + const auto profilePath = + getPropertyOr(*active, k_nmActiveConnectionInterface, "Connection", sdbus::ObjectPath{}); + if (!profilePath.empty() && profilePath != "/") { + activeVpnPaths.insert(std::string(profilePath)); + } + } catch (const sdbus::Error&) { + } + } + } catch (const sdbus::Error& e) { + kLog.debug("refreshVpnConnections active list: {}", e.what()); + } + + std::vector next; + try { + auto settings = sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath); + std::vector connectionPaths; + settings->callMethod("ListConnections").onInterface(k_nmSettingsInterface).storeResultsTo(connectionPaths); + for (const auto& connectionPath : connectionPaths) { + try { + auto connection = sdbus::createProxy(m_bus.connection(), k_nmBusName, connectionPath); + std::map> cfg; + connection->callMethod("GetSettings").onInterface(k_nmSettingsConnectionInterface).storeResultsTo(cfg); + auto connIt = cfg.find("connection"); + if (connIt == cfg.end()) { + continue; + } + auto typeIt = connIt->second.find("type"); + if (typeIt == connIt->second.end()) { + continue; + } + std::string type; + try { + type = typeIt->second.get(); + } catch (const sdbus::Error&) { + continue; + } + if (type != "vpn") { + continue; + } + + VpnConnectionInfo info; + info.path = std::string(connectionPath); + auto idIt = connIt->second.find("id"); + if (idIt != connIt->second.end()) { + try { + info.name = idIt->second.get(); + } catch (const sdbus::Error&) { + } + } + if (info.name.empty()) { + info.name = info.path; + } + info.active = activeVpnPaths.contains(info.path); + next.push_back(std::move(info)); + } catch (const sdbus::Error&) { + } + } + } catch (const sdbus::Error& e) { + kLog.debug("refreshVpnConnections: {}", e.what()); + } + + std::ranges::sort(next, [](const VpnConnectionInfo& a, const VpnConnectionInfo& b) { + if (a.active != b.active) { + return a.active; + } + return a.name < b.name; + }); + m_vpnConnections = std::move(next); +} + void NetworkService::ensureWifiDeviceSubscribed(const std::string& devicePath) { if (m_wifiDevices.contains(devicePath)) { return; diff --git a/src/dbus/network/network_service.h b/src/dbus/network/network_service.h index d2632d808..f7fac2cc0 100644 --- a/src/dbus/network/network_service.h +++ b/src/dbus/network/network_service.h @@ -25,6 +25,14 @@ struct AccessPointInfo { bool operator==(const AccessPointInfo&) const = default; }; +struct VpnConnectionInfo { + std::string path; // NM Settings.Connection object path + std::string name; + bool active = false; + + bool operator==(const VpnConnectionInfo&) const = default; +}; + enum class NetworkConnectivity { Unknown = 0, None = 1, @@ -65,6 +73,7 @@ public: [[nodiscard]] const NetworkState& state() const noexcept { return m_state; } [[nodiscard]] const std::vector& accessPoints() const noexcept { return m_accessPoints; } + [[nodiscard]] const std::vector& vpnConnections() const noexcept { return m_vpnConnections; } [[nodiscard]] static const char* glyphForState(const NetworkState& state) noexcept; [[nodiscard]] static const char* wifiGlyphForState(const NetworkState& state) noexcept; [[nodiscard]] static const char* wifiGlyphForSignal(std::uint8_t signal) noexcept; @@ -79,6 +88,10 @@ public: // handles that via a SecretAgent). bool activateAccessPoint(const AccessPointInfo& ap); + // Activate / deactivate a saved VPN connection profile. + bool activateVpnConnection(const VpnConnectionInfo& vpn); + bool deactivateVpnConnection(const VpnConnectionInfo& vpn); + // Enable / disable the Wi-Fi radio. void setWirelessEnabled(bool enabled); @@ -94,6 +107,7 @@ public: private: void refreshAccessPoints(); void refreshSavedConnections(); + void refreshVpnConnections(); void rebindActiveConnection(); void rebindActiveDevice(const std::string& devicePath); void rebindActiveAccessPoint(const std::string& apPath); @@ -113,6 +127,7 @@ private: std::string m_activeApPath; NetworkState m_state; std::vector m_accessPoints; + std::vector m_vpnConnections; std::vector m_savedSsids; bool m_scanning = false; std::int64_t m_scanBaselineLastScan = 0; diff --git a/src/shell/control_center/network_tab.cpp b/src/shell/control_center/network_tab.cpp index 43bfcef99..912587f7f 100644 --- a/src/shell/control_center/network_tab.cpp +++ b/src/shell/control_center/network_tab.cpp @@ -187,6 +187,120 @@ namespace { Signal<>::ScopedConnection m_paletteConn; }; + class VpnConnectionRow : public Flex { + public: + VpnConnectionRow(float scale, VpnConnectionInfo vpn, std::function onActivate, + std::function onDeactivate) + : m_vpn(std::move(vpn)), m_onActivate(std::move(onActivate)), m_onDeactivate(std::move(onDeactivate)) { + setDirection(FlexDirection::Horizontal); + setAlign(FlexAlign::Center); + setGap(Style::spaceSm * scale); + setPadding(Style::spaceSm * scale, Style::spaceMd * scale); + setMinHeight(kRowMinHeight * scale); + setRadius(Style::radiusMd * scale); + setFill(colorSpecFromRole(ColorRole::Surface)); + clearBorder(); + + auto kind = std::make_unique