mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Adding VPN support to the network control center tab
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <map>
|
||||
#include <sdbus-c++/IProxy.h>
|
||||
#include <sdbus-c++/Types.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
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<sdbus::ObjectPath> activeConnections;
|
||||
const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
||||
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
||||
for (const auto& activePath : activeConnections) {
|
||||
try {
|
||||
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
||||
const auto profilePath =
|
||||
getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface, "Connection", sdbus::ObjectPath{});
|
||||
const auto activeState = getPropertyOr<std::uint32_t>(*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<std::string> activeVpnPaths;
|
||||
try {
|
||||
std::vector<sdbus::ObjectPath> activeConnections;
|
||||
const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
||||
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
||||
for (const auto& activePath : activeConnections) {
|
||||
try {
|
||||
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
||||
const auto type = getPropertyOr<std::string>(*active, k_nmActiveConnectionInterface, "Type", std::string{});
|
||||
const auto state = getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
|
||||
if (type != "vpn" || state != k_nmActiveConnectionStateActivated) {
|
||||
continue;
|
||||
}
|
||||
const auto profilePath =
|
||||
getPropertyOr<sdbus::ObjectPath>(*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<VpnConnectionInfo> next;
|
||||
try {
|
||||
auto settings = sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath);
|
||||
std::vector<sdbus::ObjectPath> 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<std::string, std::map<std::string, sdbus::Variant>> 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<std::string>();
|
||||
} 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<std::string>();
|
||||
} 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;
|
||||
|
||||
@@ -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<AccessPointInfo>& accessPoints() const noexcept { return m_accessPoints; }
|
||||
[[nodiscard]] const std::vector<VpnConnectionInfo>& 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<AccessPointInfo> m_accessPoints;
|
||||
std::vector<VpnConnectionInfo> m_vpnConnections;
|
||||
std::vector<std::string> m_savedSsids;
|
||||
bool m_scanning = false;
|
||||
std::int64_t m_scanBaselineLastScan = 0;
|
||||
|
||||
@@ -187,6 +187,120 @@ namespace {
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
class VpnConnectionRow : public Flex {
|
||||
public:
|
||||
VpnConnectionRow(float scale, VpnConnectionInfo vpn, std::function<void(const VpnConnectionInfo&)> onActivate,
|
||||
std::function<void(const VpnConnectionInfo&)> 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<Label>();
|
||||
kind->setText("VPN");
|
||||
kind->setCaptionStyle();
|
||||
kind->setFontSize(Style::fontSizeCaption * scale);
|
||||
kind->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(kind));
|
||||
|
||||
auto name = std::make_unique<Label>();
|
||||
name->setText(m_vpn.name);
|
||||
name->setBold(m_vpn.active);
|
||||
name->setFontSize(Style::fontSizeBody * scale);
|
||||
name->setColor(colorSpecFromRole(ColorRole::OnSurface));
|
||||
name->setFlexGrow(1.0f);
|
||||
m_title = name.get();
|
||||
addChild(std::move(name));
|
||||
|
||||
auto action = std::make_unique<Button>();
|
||||
action->setVariant(m_vpn.active ? ButtonVariant::Destructive : ButtonVariant::Default);
|
||||
action->setText(i18n::tr(m_vpn.active ? "control-center.network.disconnect" : "control-center.network.connect"));
|
||||
action->setOnClick([this]() { triggerAction(); });
|
||||
m_actionButton = static_cast<Button*>(addChild(std::move(action)));
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) { triggerAction(); });
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_inputArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
if (m_actionButton != nullptr) {
|
||||
const float areaWidth = std::max(0.0f, m_actionButton->x() - gap());
|
||||
m_inputArea->setSize(areaWidth, height());
|
||||
}
|
||||
applyState();
|
||||
}
|
||||
|
||||
LayoutSize doMeasure(Renderer& renderer, const LayoutConstraints& constraints) override {
|
||||
return measureByLayout(renderer, constraints);
|
||||
}
|
||||
|
||||
void doArrange(Renderer& renderer, const LayoutRect& rect) override { arrangeByLayout(renderer, rect); }
|
||||
|
||||
private:
|
||||
void triggerAction() {
|
||||
if (m_vpn.active) {
|
||||
if (m_onDeactivate) {
|
||||
m_onDeactivate(m_vpn);
|
||||
}
|
||||
} else {
|
||||
if (m_onActivate) {
|
||||
m_onActivate(m_vpn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void applyState() {
|
||||
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
|
||||
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
|
||||
if (pressed) {
|
||||
setFill(colorSpecFromRole(ColorRole::Primary));
|
||||
setBorder(colorSpecFromRole(ColorRole::Primary), Style::borderWidth);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(colorSpecFromRole(ColorRole::OnPrimary));
|
||||
}
|
||||
return;
|
||||
}
|
||||
setFill(colorSpecFromRole(m_vpn.active ? ColorRole::SurfaceVariant : ColorRole::Surface));
|
||||
if (hov) {
|
||||
setBorder(colorSpecFromRole(ColorRole::Primary), Style::borderWidth);
|
||||
} else {
|
||||
clearBorder();
|
||||
}
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(colorSpecFromRole(ColorRole::OnSurface));
|
||||
}
|
||||
}
|
||||
|
||||
VpnConnectionInfo m_vpn;
|
||||
std::function<void(const VpnConnectionInfo&)> m_onActivate;
|
||||
std::function<void(const VpnConnectionInfo&)> m_onDeactivate;
|
||||
Label* m_title = nullptr;
|
||||
Button* m_actionButton = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkTab::NetworkTab(NetworkService* network, NetworkSecretAgent* secrets) : m_network(network), m_secrets(secrets) {
|
||||
@@ -532,6 +646,19 @@ std::string NetworkTab::apListKey(const std::vector<AccessPointInfo>& aps) const
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string NetworkTab::vpnListKey(const std::vector<VpnConnectionInfo>& vpns) const {
|
||||
std::string key;
|
||||
for (const auto& vpn : vpns) {
|
||||
key += vpn.path;
|
||||
key.push_back(':');
|
||||
key += vpn.name;
|
||||
key.push_back(':');
|
||||
key += vpn.active ? '1' : '0';
|
||||
key.push_back('\n');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
uiAssertNotRendering("NetworkTab::rebuildApList");
|
||||
if (m_list == nullptr || m_listScroll == nullptr) {
|
||||
@@ -543,7 +670,9 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
}
|
||||
|
||||
const auto& aps = m_network != nullptr ? m_network->accessPoints() : std::vector<AccessPointInfo>{};
|
||||
const std::string nextKey = aps.empty() ? std::string("empty") : apListKey(aps);
|
||||
const auto& vpns = m_network != nullptr ? m_network->vpnConnections() : std::vector<VpnConnectionInfo>{};
|
||||
const std::string nextKey =
|
||||
aps.empty() && vpns.empty() ? std::string("empty") : (apListKey(aps) + "\n---\n" + vpnListKey(vpns));
|
||||
if (listWidth == m_lastListWidth && nextKey == m_lastListKey) {
|
||||
return;
|
||||
}
|
||||
@@ -554,7 +683,7 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
m_list->removeChild(m_list->children().front().get());
|
||||
}
|
||||
|
||||
if (aps.empty()) {
|
||||
if (aps.empty() && vpns.empty()) {
|
||||
auto empty = std::make_unique<Label>();
|
||||
empty->setText(m_network != nullptr ? i18n::tr("control-center.network.no-networks")
|
||||
: i18n::tr("control-center.network.unavailable-title"));
|
||||
@@ -580,6 +709,32 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
});
|
||||
m_list->addChild(std::move(row));
|
||||
}
|
||||
if (!vpns.empty()) {
|
||||
auto section = std::make_unique<Label>();
|
||||
section->setText(i18n::tr("control-center.network.vpns"));
|
||||
section->setCaptionStyle();
|
||||
section->setFontSize(Style::fontSizeCaption * contentScale());
|
||||
section->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
m_list->addChild(std::move(section));
|
||||
|
||||
for (const auto& vpn : vpns) {
|
||||
auto row = std::make_unique<VpnConnectionRow>(
|
||||
contentScale(), vpn,
|
||||
[this](const VpnConnectionInfo& clicked) {
|
||||
if (m_network != nullptr) {
|
||||
m_network->activateVpnConnection(clicked);
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
},
|
||||
[this](const VpnConnectionInfo& clicked) {
|
||||
if (m_network != nullptr) {
|
||||
m_network->deactivateVpnConnection(clicked);
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_list->addChild(std::move(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
m_list->layout(renderer);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ private:
|
||||
void showPasswordPrompt(const NetworkSecretAgent::SecretRequest& request);
|
||||
void clearPasswordPrompt();
|
||||
[[nodiscard]] std::string apListKey(const std::vector<AccessPointInfo>& aps) const;
|
||||
[[nodiscard]] std::string vpnListKey(const std::vector<VpnConnectionInfo>& vpns) const;
|
||||
|
||||
NetworkService* m_network = nullptr;
|
||||
NetworkSecretAgent* m_secrets = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user