Merge pull request #2649 from Mathew-D/v5

Added VPN's to the Network tab
This commit is contained in:
Lemmy
2026-05-09 19:42:42 -04:00
committed by GitHub
6 changed files with 405 additions and 8 deletions
+3
View File
@@ -237,6 +237,9 @@
"wifi": "Wi-Fi",
"password-prompt": "Enter password",
"password-prompt-for": "Enter password for {ssid}",
"vpn": "VPN",
"vpns": "VPNs",
"wireless": "Wireless",
"unavailable-title": "NetworkManager unavailable",
"unavailable-detail": "Install and enable NetworkManager to manage networks from here.",
"no-networks": "No networks in range"
+195 -6
View File
@@ -9,6 +9,7 @@
#include <map>
#include <sdbus-c++/IProxy.h>
#include <sdbus-c++/Types.h>
#include <set>
#include <vector>
namespace {
@@ -98,6 +99,9 @@ namespace {
} // namespace
const char* NetworkService::glyphForState(const NetworkState& state) noexcept {
if (state.vpnActive) {
return "shield-check";
}
if (state.kind == NetworkConnectivity::Wired) {
return state.connected ? "ethernet" : "ethernet-off";
}
@@ -197,18 +201,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 +308,76 @@ bool NetworkService::activateAccessPoint(const AccessPointInfo& ap) {
}
}
bool NetworkService::activateVpnConnection(const VpnConnectionInfo& vpn) {
if (vpn.path.empty()) {
return false;
}
try {
// Async: ActivateConnection can involve polkit/agent interactions, and a
// synchronous call can stall the main loop while authorization is pending.
const std::string vpnName = vpn.name;
const std::string vpnPath = vpn.path;
m_nm->callMethodAsync("ActivateConnection")
.onInterface(k_nmInterface)
.withArguments(sdbus::ObjectPath{vpnPath}, sdbus::ObjectPath{"/"}, sdbus::ObjectPath{"/"})
.uponReplyInvoke([vpnName, vpnPath](std::optional<sdbus::Error> err, sdbus::ObjectPath activePath) {
if (err.has_value()) {
kLog.warn("ActivateConnection(vpn) failed name={} path={}: {}", vpnName, vpnPath, err->what());
} else {
kLog.info("activating vpn name={} active={}", vpnName, 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;
}
// Async: DeactivateConnection on a system-owned profile is gated by polkit,
// and a sync call would freeze the main loop while the polkit agent prompts
// (or while polkit waits for an agent to register). Fire-and-forget here.
const std::string activePathStr = std::string(activePath);
const std::string vpnName = vpn.name;
m_nm->callMethodAsync("DeactivateConnection")
.onInterface(k_nmInterface)
.withArguments(sdbus::ObjectPath{activePathStr})
.uponReplyInvoke([activePathStr, vpnName](std::optional<sdbus::Error> err) {
if (err.has_value()) {
kLog.warn("DeactivateConnection(vpn) failed name={} active={}: {}", vpnName, activePathStr,
err->what());
} else {
kLog.info("deactivated vpn name={} active={}", vpnName, activePathStr);
}
});
return true;
} catch (const sdbus::Error&) {
}
}
} catch (const sdbus::Error& e) {
kLog.warn("DeactivateConnection(vpn) lookup failed path={}: {}", vpn.path, e.what());
return false;
}
return false;
}
void NetworkService::setWirelessEnabled(bool enabled) {
if (enabled != m_state.wirelessEnabled) {
m_pendingLocalWirelessEnabled = enabled;
@@ -517,6 +594,102 @@ void NetworkService::refreshSavedConnections() {
m_savedSsids = std::move(next);
}
void NetworkService::refreshVpnConnections() {
std::vector<VpnConnectionInfo> next;
std::set<std::string> vpnProfilePaths;
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;
}
const bool hasVpnSection = cfg.contains("vpn");
const bool vpnLikeType = type == "vpn" || type == "wireguard";
if (!vpnLikeType && !hasVpnSection) {
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 = false;
vpnProfilePaths.insert(info.path);
next.push_back(std::move(info));
} catch (const sdbus::Error&) {
}
}
} catch (const sdbus::Error& e) {
kLog.debug("refreshVpnConnections: {}", e.what());
}
if (!next.empty()) {
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 state = getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
if (state != k_nmActiveConnectionStateActivated) {
continue;
}
const auto profilePath = getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface,
"Connection", sdbus::ObjectPath{});
const std::string profilePathStr = std::string(profilePath);
if (!vpnProfilePaths.contains(profilePathStr)) {
continue;
}
for (auto& vpn : next) {
if (vpn.path == profilePathStr) {
vpn.active = true;
break;
}
}
} catch (const sdbus::Error&) {
}
}
} catch (const sdbus::Error& e) {
kLog.debug("refreshVpnConnections active list: {}", 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;
@@ -776,6 +949,27 @@ NetworkState NetworkService::readState() {
next.wirelessEnabled = getPropertyOr<bool>(*m_nm, k_nmInterface, "WirelessEnabled", false);
next.scanning = m_scanning;
next.vpnActive = false;
// Check primary connection: detect VPN type and connection state
if (m_activeConnection != nullptr) {
const auto type =
getPropertyOr<std::string>(*m_activeConnection, k_nmActiveConnectionInterface, "Type", std::string{});
next.vpnActive = (type == "vpn" || type == "wireguard");
const auto state = getPropertyOr<std::uint32_t>(*m_activeConnection, k_nmActiveConnectionInterface, "State", 0U);
next.connected = state == k_nmActiveConnectionStateActivated;
}
// Also check if any VPN profile is active (in case it's not the primary connection)
if (!next.vpnActive) {
for (const auto& vpn : m_vpnConnections) {
if (vpn.active) {
next.vpnActive = true;
next.connected = true;
break;
}
}
}
if (m_activeDevice == nullptr) {
return next;
@@ -788,11 +982,6 @@ NetworkState NetworkService::readState() {
getPropertyOr<sdbus::ObjectPath>(*m_activeDevice, k_nmDeviceInterface, "Ip4Config", sdbus::ObjectPath{});
next.ipv4 = firstIpv4FromConfig(m_bus.connection(), ip4ConfigPath);
if (m_activeConnection != nullptr) {
const auto state = getPropertyOr<std::uint32_t>(*m_activeConnection, k_nmActiveConnectionInterface, "State", 0U);
next.connected = state == k_nmActiveConnectionStateActivated;
}
if (deviceType == k_nmDeviceTypeWifi) {
next.kind = NetworkConnectivity::Wireless;
if (m_activeAp != nullptr) {
+16
View File
@@ -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,
@@ -37,6 +45,7 @@ struct NetworkState {
bool connected = false;
bool wirelessEnabled = false;
bool scanning = false;
bool vpnActive = false; // true if a VPN is the active connection
std::string ssid; // Wi-Fi only
std::string ipv4; // dotted-quad of first address; empty if none
std::string interfaceName; // e.g. "wlan0", "eth0"
@@ -65,6 +74,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 +89,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 +108,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 +128,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;
+1
View File
@@ -191,6 +191,7 @@ const std::unordered_map<std::string, char32_t> kIcons = {
{"ethernet-off", 0xECCD},
{"ethernet-exclamation", 0xECCE},
{"ethernet-question", 0xECCF},
{"shield-check", 0xEB22},
// Bluetooth devices
{"bluetooth", 0xEA37},
+187 -2
View File
@@ -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(i18n::tr("control-center.network.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) {
@@ -403,6 +517,23 @@ std::unique_ptr<Flex> NetworkTab::createHeaderActions() {
});
m_rescanButton = rescan.get();
row->addChild(std::move(rescan));
auto vpnLabel = std::make_unique<Label>();
vpnLabel->setText(i18n::tr("control-center.network.vpns"));
vpnLabel->setFontSize(Style::fontSizeCaption * scale);
vpnLabel->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
row->addChild(std::move(vpnLabel));
auto vpnToggle = std::make_unique<Toggle>();
vpnToggle->setToggleSize(ToggleSize::Small);
vpnToggle->setScale(scale);
vpnToggle->setChecked(m_vpnVisible);
vpnToggle->setOnChange([this](bool checked) {
m_vpnVisible = checked;
PanelManager::instance().refresh();
});
m_vpnToggle = vpnToggle.get();
row->addChild(std::move(vpnToggle));
return row;
}
@@ -439,6 +570,7 @@ void NetworkTab::onClose() {
m_list = nullptr;
m_rescanButton = nullptr;
m_wifiToggle = nullptr;
m_vpnToggle = nullptr;
m_disconnectRow = nullptr;
m_disconnectButton = nullptr;
m_scanSpinner = nullptr;
@@ -532,6 +664,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 +688,11 @@ 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) + "\nvis:" + (m_vpnVisible ? '1' : '0'));
if (listWidth == m_lastListWidth && nextKey == m_lastListKey) {
return;
}
@@ -554,7 +703,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"));
@@ -563,6 +712,42 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
empty->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
m_list->addChild(std::move(empty));
} else {
if (m_vpnVisible && !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));
}
}
if (!aps.empty()) {
auto section = std::make_unique<Label>();
section->setText(i18n::tr("control-center.network.wireless"));
section->setCaptionStyle();
section->setFontSize(Style::fontSizeCaption * contentScale());
section->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
m_list->addChild(std::move(section));
}
for (const auto& ap : aps) {
const bool saved = m_network != nullptr && m_network->hasSavedConnection(ap.ssid);
auto row = std::make_unique<AccessPointRow>(
+3
View File
@@ -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;
@@ -53,9 +54,11 @@ private:
Button* m_rescanButton = nullptr;
Toggle* m_wifiToggle = nullptr;
Toggle* m_vpnToggle = nullptr;
Flex* m_disconnectRow = nullptr;
Button* m_disconnectButton = nullptr;
Spinner* m_scanSpinner = nullptr;
bool m_vpnVisible = true;
std::string m_lastListKey;
float m_lastListWidth = -1.0f;