feat(network): add VPN connection management
This commit is contained in:
parent
2d196081b7
commit
6a7fa04067
4 changed files with 275 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ connect = Connect
|
||||||
cancel = Cancel
|
cancel = Cancel
|
||||||
settings = Network settings...
|
settings = Network settings...
|
||||||
visible-wireless-networks = Visible wireless networks
|
visible-wireless-networks = Visible wireless networks
|
||||||
|
vpn-connections = VPN connections
|
||||||
enter-password = Enter the password or encryption key
|
enter-password = Enter the password or encryption key
|
||||||
router-wps-button = You can also connect by pressing the "WPS" button on the router
|
router-wps-button = You can also connect by pressing the "WPS" button on the router
|
||||||
unable-to-connect = Unable to connect to network
|
unable-to-connect = Unable to connect to network
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ struct CosmicNetworkApplet {
|
||||||
// UI state
|
// UI state
|
||||||
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
|
nm_sender: Option<UnboundedSender<NetworkManagerRequest>>,
|
||||||
show_visible_networks: bool,
|
show_visible_networks: bool,
|
||||||
|
show_available_vpns: bool,
|
||||||
new_connection: Option<NewConnectionState>,
|
new_connection: Option<NewConnectionState>,
|
||||||
conn: Option<Connection>,
|
conn: Option<Connection>,
|
||||||
timeline: Timeline,
|
timeline: Timeline,
|
||||||
|
|
@ -122,6 +123,82 @@ fn wifi_icon(strength: u8) -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn vpn_section<'a>(
|
||||||
|
nm_state: &'a NetworkManagerState,
|
||||||
|
show_available_vpns: bool,
|
||||||
|
space_xxs: u16,
|
||||||
|
space_s: u16,
|
||||||
|
) -> Column<'a, Message> {
|
||||||
|
let mut vpn_col = column![];
|
||||||
|
|
||||||
|
if !nm_state.available_vpns.is_empty() {
|
||||||
|
let dropdown_icon = if show_available_vpns {
|
||||||
|
"go-up-symbolic"
|
||||||
|
} else {
|
||||||
|
"go-down-symbolic"
|
||||||
|
};
|
||||||
|
|
||||||
|
vpn_col = vpn_col.push(
|
||||||
|
padded_control(divider::horizontal::default()).padding([space_xxs, space_s])
|
||||||
|
);
|
||||||
|
|
||||||
|
let vpn_toggle_btn = menu_button(row![
|
||||||
|
text::body(fl!("vpn-connections"))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(24.0))
|
||||||
|
.align_y(Alignment::Center),
|
||||||
|
container(icon::from_name(dropdown_icon).size(16).symbolic(true))
|
||||||
|
.center(Length::Fixed(24.0))
|
||||||
|
])
|
||||||
|
.on_press(Message::ToggleVpnList);
|
||||||
|
|
||||||
|
vpn_col = vpn_col.push(vpn_toggle_btn);
|
||||||
|
|
||||||
|
if show_available_vpns {
|
||||||
|
for vpn in &nm_state.available_vpns {
|
||||||
|
// Check if this VPN is currently active
|
||||||
|
let is_active = nm_state.active_conns.iter().any(|conn| {
|
||||||
|
matches!(conn, ActiveConnectionInfo::Vpn { name, .. } if name == &vpn.name)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut btn_content = vec![
|
||||||
|
icon::from_name("network-vpn-symbolic")
|
||||||
|
.size(24)
|
||||||
|
.symbolic(true)
|
||||||
|
.into(),
|
||||||
|
text::body(&vpn.name)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if is_active {
|
||||||
|
btn_content.push(
|
||||||
|
text::body(fl!("connected"))
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut btn = menu_button(
|
||||||
|
Row::with_children(btn_content)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.spacing(8),
|
||||||
|
);
|
||||||
|
|
||||||
|
btn = if is_active {
|
||||||
|
btn.on_press(Message::DeactivateVpn(vpn.name.clone()))
|
||||||
|
} else {
|
||||||
|
btn.on_press(Message::ActivateVpn(vpn.uuid.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
vpn_col = vpn_col.push(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vpn_col
|
||||||
|
}
|
||||||
|
|
||||||
impl CosmicNetworkApplet {
|
impl CosmicNetworkApplet {
|
||||||
fn update_nm_state(&mut self, mut new_state: NetworkManagerState) {
|
fn update_nm_state(&mut self, mut new_state: NetworkManagerState) {
|
||||||
self.update_togglers(&new_state);
|
self.update_togglers(&new_state);
|
||||||
|
|
@ -245,7 +322,10 @@ pub(crate) enum Message {
|
||||||
ResetFailedKnownSsid(String, HwAddress),
|
ResetFailedKnownSsid(String, HwAddress),
|
||||||
OpenHwDevice(Option<HwAddress>),
|
OpenHwDevice(Option<HwAddress>),
|
||||||
TogglePasswordVisibility,
|
TogglePasswordVisibility,
|
||||||
Surface(surface::Action), // Errored(String),
|
Surface(surface::Action),
|
||||||
|
ActivateVpn(String), // UUID of VPN to activate
|
||||||
|
DeactivateVpn(String), // Name of VPN to deactivate
|
||||||
|
ToggleVpnList, // Show/hide available VPNs
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cosmic::Application for CosmicNetworkApplet {
|
impl cosmic::Application for CosmicNetworkApplet {
|
||||||
|
|
@ -601,6 +681,19 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
*identity = new_identity;
|
*identity = new_identity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::ActivateVpn(uuid) => {
|
||||||
|
if let Some(tx) = self.nm_sender.as_ref() {
|
||||||
|
let _ = tx.unbounded_send(NetworkManagerRequest::ActivateVpn(uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::DeactivateVpn(name) => {
|
||||||
|
if let Some(tx) = self.nm_sender.as_ref() {
|
||||||
|
let _ = tx.unbounded_send(NetworkManagerRequest::DeactivateVpn(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ToggleVpnList => {
|
||||||
|
self.show_available_vpns = !self.show_available_vpns;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -992,6 +1085,9 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
content = content.push(available_connections_btn);
|
content = content.push(available_connections_btn);
|
||||||
|
|
||||||
if !self.show_visible_networks {
|
if !self.show_visible_networks {
|
||||||
|
if !self.nm_state.available_vpns.is_empty() {
|
||||||
|
content = content.push(vpn_section(&self.nm_state, self.show_available_vpns, space_xxs, space_s));
|
||||||
|
}
|
||||||
return self.view_window_return(content);
|
return self.view_window_return(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1136,6 +1232,11 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
.push(scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)));
|
.push(scrollable(Column::with_children(list_col)).height(Length::Fixed(300.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add VPN connections section after wireless networks when they are expanded
|
||||||
|
if !self.nm_state.available_vpns.is_empty() {
|
||||||
|
content = content.push(vpn_section(&self.nm_state, self.show_available_vpns, space_xxs, space_s));
|
||||||
|
}
|
||||||
|
|
||||||
self.view_window_return(content)
|
self.view_window_return(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
cosmic-applet-network/src/network_manager/available_vpns.rs
Normal file
45
cosmic-applet-network/src/network_manager/available_vpns.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings};
|
||||||
|
use zbus::Connection;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VpnConnection {
|
||||||
|
pub name: String,
|
||||||
|
pub uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all available VPN connections from NetworkManager settings
|
||||||
|
pub async fn load_vpn_connections(conn: &Connection) -> anyhow::Result<Vec<VpnConnection>> {
|
||||||
|
let nm_settings = NetworkManagerSettings::new(conn).await?;
|
||||||
|
let connections = nm_settings.list_connections().await?;
|
||||||
|
|
||||||
|
let mut vpn_connections = Vec::new();
|
||||||
|
|
||||||
|
for connection in connections {
|
||||||
|
let settings_map = match connection.get_settings().await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = Settings::new(settings_map);
|
||||||
|
|
||||||
|
// Check if this is a VPN connection
|
||||||
|
if let Some(connection_settings) = &settings.connection {
|
||||||
|
if let Some(conn_type) = &connection_settings.type_ {
|
||||||
|
// VPN connections have type "vpn" or "wireguard"
|
||||||
|
if conn_type == "vpn" || conn_type == "wireguard" {
|
||||||
|
let name = connection_settings.id.clone().unwrap_or_else(|| "Unknown VPN".to_string());
|
||||||
|
let uuid = connection_settings.uuid.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
vpn_connections.push(VpnConnection { name, uuid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by name for consistent UI
|
||||||
|
vpn_connections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
Ok(vpn_connections)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod active_conns;
|
pub mod active_conns;
|
||||||
|
pub mod available_vpns;
|
||||||
pub mod available_wifi;
|
pub mod available_wifi;
|
||||||
pub mod current_networks;
|
pub mod current_networks;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
|
|
@ -34,6 +35,7 @@ use zbus::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
|
available_vpns::{VpnConnection, load_vpn_connections},
|
||||||
available_wifi::{AccessPoint, handle_wireless_device},
|
available_wifi::{AccessPoint, handle_wireless_device},
|
||||||
current_networks::{ActiveConnectionInfo, active_connections},
|
current_networks::{ActiveConnectionInfo, active_connections},
|
||||||
};
|
};
|
||||||
|
|
@ -282,6 +284,123 @@ async fn start_listening(
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
Some(NetworkManagerRequest::ActivateVpn(uuid)) => {
|
||||||
|
tracing::info!("Activating VPN with UUID: {}", uuid);
|
||||||
|
let network_manager = match NetworkManager::new(&conn).await {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
||||||
|
_ = output
|
||||||
|
.send(NetworkManagerEvent::RequestResponse {
|
||||||
|
req: NetworkManagerRequest::ActivateVpn(uuid),
|
||||||
|
success: false,
|
||||||
|
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
return State::Waiting(conn, rx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut success = false;
|
||||||
|
|
||||||
|
// Find the connection by UUID
|
||||||
|
if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await {
|
||||||
|
if let Ok(connections) = nm_settings.list_connections().await {
|
||||||
|
for connection in connections {
|
||||||
|
if let Ok(settings) = connection.get_settings().await {
|
||||||
|
let settings = Settings::new(settings);
|
||||||
|
if let Some(conn_settings) = &settings.connection {
|
||||||
|
if conn_settings.uuid.as_ref() == Some(&uuid) {
|
||||||
|
// Activate the VPN connection without a specific device
|
||||||
|
// Call the D-Bus method directly since VPNs don't need a device
|
||||||
|
use zbus::zvariant::ObjectPath;
|
||||||
|
let empty_device = ObjectPath::try_from("/").unwrap();
|
||||||
|
|
||||||
|
match network_manager.inner()
|
||||||
|
.call_method("ActivateConnection", &(connection.inner().path(), empty_device.clone(), empty_device))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("Successfully activated VPN: {}", uuid);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to activate VPN {}: {:?}", uuid, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
tracing::warn!("VPN connection with UUID {} not found or failed to activate", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||||
|
_ = output
|
||||||
|
.send(NetworkManagerEvent::RequestResponse {
|
||||||
|
req: NetworkManagerRequest::ActivateVpn(uuid),
|
||||||
|
success,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Some(NetworkManagerRequest::DeactivateVpn(name)) => {
|
||||||
|
tracing::info!("Deactivating VPN: {}", name);
|
||||||
|
let network_manager = match NetworkManager::new(&conn).await {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
||||||
|
_ = output
|
||||||
|
.send(NetworkManagerEvent::RequestResponse {
|
||||||
|
req: NetworkManagerRequest::DeactivateVpn(name),
|
||||||
|
success: false,
|
||||||
|
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
return State::Waiting(conn, rx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut success = false;
|
||||||
|
|
||||||
|
// Find and deactivate the active VPN connection by name
|
||||||
|
if let Ok(active_connections) = network_manager.active_connections().await {
|
||||||
|
for active_conn in active_connections {
|
||||||
|
if let Ok(conn_id) = active_conn.id().await {
|
||||||
|
if conn_id == name && active_conn.vpn().await.unwrap_or(false) {
|
||||||
|
match network_manager.deactivate_connection(&active_conn).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("Successfully deactivated VPN: {}", name);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to deactivate VPN {}: {:?}", name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
tracing::warn!("Active VPN connection '{}' not found or failed to deactivate", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
||||||
|
_ = output
|
||||||
|
.send(NetworkManagerEvent::RequestResponse {
|
||||||
|
req: NetworkManagerRequest::DeactivateVpn(name),
|
||||||
|
success,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return State::Finished;
|
return State::Finished;
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +479,8 @@ pub enum NetworkManagerRequest {
|
||||||
},
|
},
|
||||||
Forget(String, HwAddress),
|
Forget(String, HwAddress),
|
||||||
Reload,
|
Reload,
|
||||||
|
ActivateVpn(String), // UUID of VPN connection to activate
|
||||||
|
DeactivateVpn(String), // Name of active VPN connection to deactivate
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -384,6 +505,7 @@ pub struct NetworkManagerState {
|
||||||
pub wireless_access_points: Vec<AccessPoint>,
|
pub wireless_access_points: Vec<AccessPoint>,
|
||||||
pub active_conns: Vec<ActiveConnectionInfo>,
|
pub active_conns: Vec<ActiveConnectionInfo>,
|
||||||
pub known_access_points: Vec<AccessPoint>,
|
pub known_access_points: Vec<AccessPoint>,
|
||||||
|
pub available_vpns: Vec<VpnConnection>,
|
||||||
pub wifi_enabled: bool,
|
pub wifi_enabled: bool,
|
||||||
pub airplane_mode: bool,
|
pub airplane_mode: bool,
|
||||||
pub connectivity: NmConnectivityState,
|
pub connectivity: NmConnectivityState,
|
||||||
|
|
@ -395,6 +517,7 @@ impl Default for NetworkManagerState {
|
||||||
wireless_access_points: Vec::new(),
|
wireless_access_points: Vec::new(),
|
||||||
active_conns: Vec::new(),
|
active_conns: Vec::new(),
|
||||||
known_access_points: Vec::new(),
|
known_access_points: Vec::new(),
|
||||||
|
available_vpns: Vec::new(),
|
||||||
wifi_enabled: false,
|
wifi_enabled: false,
|
||||||
airplane_mode: false,
|
airplane_mode: false,
|
||||||
connectivity: NmConnectivityState::Unknown,
|
connectivity: NmConnectivityState::Unknown,
|
||||||
|
|
@ -490,6 +613,9 @@ impl NetworkManagerState {
|
||||||
self_.known_access_points = known_access_points;
|
self_.known_access_points = known_access_points;
|
||||||
self_.connectivity = network_manager.connectivity().await?;
|
self_.connectivity = network_manager.connectivity().await?;
|
||||||
|
|
||||||
|
// Load available VPN connections
|
||||||
|
self_.available_vpns = load_vpn_connections(conn).await.unwrap_or_default();
|
||||||
|
|
||||||
Ok(self_)
|
Ok(self_)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,6 +624,7 @@ impl NetworkManagerState {
|
||||||
self.active_conns = Vec::new();
|
self.active_conns = Vec::new();
|
||||||
self.known_access_points = Vec::new();
|
self.known_access_points = Vec::new();
|
||||||
self.wireless_access_points = Vec::new();
|
self.wireless_access_points = Vec::new();
|
||||||
|
self.available_vpns = Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_wifi(
|
async fn connect_wifi(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue