diff --git a/Cargo.lock b/Cargo.lock index ccf2297a..0195612a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6489,7 +6489,7 @@ dependencies = [ [[package]] name = "wallguard" -version = "0.1.18" +version = "0.1.19" dependencies = [ "async-channel", "captis", diff --git a/proto/models.proto b/proto/models.proto index a5451ad1..a7adb2e1 100644 --- a/proto/models.proto +++ b/proto/models.proto @@ -83,7 +83,8 @@ message IpAddress { message NetworkInterface { string name = 1; string device = 2; - repeated IpAddress addresses = 3; + string description = 3; + repeated IpAddress addresses = 4; } message SSHConfig { diff --git a/proto/service.proto b/proto/service.proto index be4a310e..eb33905c 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -85,6 +85,7 @@ enum ServiceProtocol { HTTPS = 1; SSH = 2; TTY = 3; + RD = 4; } message ServiceInfo { diff --git a/wallguard-common/src/protobuf/wallguard_models.rs b/wallguard-common/src/protobuf/wallguard_models.rs index 7a8cf468..9b1000ee 100644 --- a/wallguard-common/src/protobuf/wallguard_models.rs +++ b/wallguard-common/src/protobuf/wallguard_models.rs @@ -120,7 +120,9 @@ pub struct NetworkInterface { pub name: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub device: ::prost::alloc::string::String, - #[prost(message, repeated, tag = "3")] + #[prost(string, tag = "3")] + pub description: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] pub addresses: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/wallguard-common/src/protobuf/wallguard_service.rs b/wallguard-common/src/protobuf/wallguard_service.rs index 463bf86a..822d610f 100644 --- a/wallguard-common/src/protobuf/wallguard_service.rs +++ b/wallguard-common/src/protobuf/wallguard_service.rs @@ -134,6 +134,7 @@ pub enum ServiceProtocol { Https = 1, Ssh = 2, Tty = 3, + Rd = 4, } impl ServiceProtocol { /// String value of the enum field names used in the ProtoBuf definition. @@ -146,6 +147,7 @@ impl ServiceProtocol { Self::Https => "HTTPS", Self::Ssh => "SSH", Self::Tty => "TTY", + Self::Rd => "RD", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -155,6 +157,7 @@ impl ServiceProtocol { "HTTPS" => Some(Self::Https), "SSH" => Some(Self::Ssh), "TTY" => Some(Self::Tty), + "RD" => Some(Self::Rd), _ => None, } } diff --git a/wallguard-server/src/datastore/models/mod.rs b/wallguard-server/src/datastore/models/mod.rs index 80879400..477a181b 100644 --- a/wallguard-server/src/datastore/models/mod.rs +++ b/wallguard-server/src/datastore/models/mod.rs @@ -4,7 +4,6 @@ mod device_configuration; mod device_instance; mod heartbeat; mod installation_code; -mod remote_access_session; mod service; mod tunnel; @@ -14,6 +13,5 @@ pub use device_configuration::*; pub use device_instance::*; pub use heartbeat::*; pub use installation_code::*; -pub use remote_access_session::*; pub use service::*; pub use tunnel::*; diff --git a/wallguard-server/src/datastore/models/remote_access_session.rs b/wallguard-server/src/datastore/models/remote_access_session.rs deleted file mode 100644 index 991c2978..00000000 --- a/wallguard-server/src/datastore/models/remote_access_session.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::datastore::db_tables::DBTable; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy, Default)] -#[serde(rename_all = "lowercase")] -pub enum RemoteAccessType { - #[default] - Ssh, - Tty, - Ui, - RemoteDesktop, - Mcp, -} - -impl TryFrom<&str> for RemoteAccessType { - type Error = String; - - fn try_from(value: &str) -> Result { - let lc_value = value.to_lowercase(); - match lc_value.as_str() { - "ui" => Ok(RemoteAccessType::Ui), - "ssh" => Ok(RemoteAccessType::Ssh), - "tty" => Ok(RemoteAccessType::Tty), - "mcp" => Ok(RemoteAccessType::Mcp), - "remote_desktop" => Ok(RemoteAccessType::RemoteDesktop), - _ => Err(format!("Remote access of type {lc_value} is not suppored")), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct RemoteAccessSession { - pub id: String, - pub device_id: String, - pub instance_id: String, - #[serde(rename = "remote_access_session")] - pub token: String, - #[serde(rename = "remote_access_type")] - pub r#type: RemoteAccessType, - #[serde(rename = "remote_access_local_addr")] - pub local_addr: Option, - #[serde(rename = "remote_access_local_port")] - pub local_port: Option, - #[serde(rename = "remote_access_local_protocol")] - pub protocol: Option, -} - -impl RemoteAccessSession { - pub fn pluck() -> Vec { - vec![ - "id".into(), - "device_id".into(), - "remote_access_session".into(), - "remote_access_type".into(), - "instance_id".into(), - "remote_access_local_addr".into(), - "remote_access_local_port".into(), - "remote_access_local_protocol".into(), - ] - } - - pub fn table() -> DBTable { - DBTable::RemoteAccessSessions - } -} diff --git a/wallguard-server/src/datastore/models/tunnel.rs b/wallguard-server/src/datastore/models/tunnel.rs index e4f7b5fa..f9a55dfd 100644 --- a/wallguard-server/src/datastore/models/tunnel.rs +++ b/wallguard-server/src/datastore/models/tunnel.rs @@ -13,6 +13,7 @@ pub enum TunnelType { Ssh, Http, Https, + Rd, } impl TryFrom<&str> for TunnelType { @@ -24,6 +25,7 @@ impl TryFrom<&str> for TunnelType { "http" => Ok(TunnelType::Http), "https" => Ok(TunnelType::Https), "tty" => Ok(TunnelType::Tty), + "rd" => Ok(TunnelType::Rd), other => { Err(format!("Tunnel of type {other} is not supported")).handle_err(location!()) } @@ -38,6 +40,7 @@ impl Display for TunnelType { TunnelType::Ssh => "ssh", TunnelType::Http => "http", TunnelType::Https => "https", + TunnelType::Rd => "rd", }; f.write_str(value) diff --git a/wallguard-server/src/datastore/operations/mod.rs b/wallguard-server/src/datastore/operations/mod.rs index 68879fde..6ddda866 100644 --- a/wallguard-server/src/datastore/operations/mod.rs +++ b/wallguard-server/src/datastore/operations/mod.rs @@ -21,7 +21,6 @@ mod obtain_device; mod obtain_installation_code; mod obtain_service; mod obtain_services; -mod obtain_session; mod obtain_tunnel; mod redeem_installation_code; mod register_device; diff --git a/wallguard-server/src/datastore/operations/obtain_session.rs b/wallguard-server/src/datastore/operations/obtain_session.rs deleted file mode 100644 index 0483100a..00000000 --- a/wallguard-server/src/datastore/operations/obtain_session.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::datastore::{Datastore, RemoteAccessSession}; -use crate::utilities::json; -use nullnet_libdatastore::{AdvanceFilterBuilder, GetByFilterRequestBuilder}; -use nullnet_liberror::{Error, ErrorHandler, Location, location}; - -impl Datastore { - pub async fn obtain_session( - &self, - token: &str, - session_token: &str, - ) -> Result, Error> { - let filter = AdvanceFilterBuilder::new() - .field("remote_access_session") - .values(format!("[\"{session_token}\"]")) - .r#type("criteria") - .operator("equal") - .entity(RemoteAccessSession::table()) - .build(); - - let request = GetByFilterRequestBuilder::new() - .table(RemoteAccessSession::table()) - .plucks(RemoteAccessSession::pluck()) - .limit(1) - .advance_filter(filter) - .order_by("timestamp") - .order_direction("desc") - .case_sensitive_sorting(true) - .build(); - - let response = self.inner.clone().get_by_filter(request, token).await?; - if response.count == 0 { - return Ok(None); - } - - let json_data = json::parse_string(&response.data)?; - let data = json::first_element_from_array(&json_data)?; - - let session = - serde_json::from_value::(data).handle_err(location!())?; - Ok(Some(session)) - } -} diff --git a/wallguard-server/src/http_api/mod.rs b/wallguard-server/src/http_api/mod.rs index 924b0e00..74215559 100644 --- a/wallguard-server/src/http_api/mod.rs +++ b/wallguard-server/src/http_api/mod.rs @@ -16,7 +16,7 @@ use config::HttpApiConfig; mod api; mod config; -// mod rd_gateway; +mod rd_gateway_v2; pub mod ssh_gateway_v2; pub mod tty_gateway_v2; pub mod utilities; diff --git a/wallguard-server/src/http_api/rd_gateway/mod.rs b/wallguard-server/src/http_api/rd_gateway/mod.rs deleted file mode 100644 index c3dc2e5d..00000000 --- a/wallguard-server/src/http_api/rd_gateway/mod.rs +++ /dev/null @@ -1,89 +0,0 @@ -use super::utilities::error_json::ErrorJson; -use super::utilities::request_handling; -use super::utilities::tunneling; -use crate::app_context::AppContext; -use crate::datastore::RemoteAccessType; -use actix_web::HttpRequest; -use actix_web::HttpResponse; -use actix_web::Responder; -use actix_web::rt; -use actix_web::web::{Data, Payload}; -use handle_connection::handle_connection; - -mod handle_connection; -mod signal_message; - -pub(super) async fn open_remote_desktop_session( - request: HttpRequest, - context: Data, - body: Payload, -) -> impl Responder { - let session_token = match request_handling::extract_session_token(&request) { - Ok(token) => token, - Err(resp) => return resp, - }; - - let token = match request_handling::fetch_token(&context).await { - Ok(t) => t, - Err(resp) => return resp, - }; - - let session = match request_handling::fetch_session(&context, &token.jwt, &session_token).await - { - Ok(sess) => sess, - Err(resp) => return resp, - }; - - if let Err(resp) = - request_handling::ensure_session_type(&session, RemoteAccessType::RemoteDesktop) - { - return resp; - } - - let Ok(device) = context - .datastore - .obtain_device_by_id(&token.jwt, &session.device_id, false) - .await - else { - return HttpResponse::InternalServerError() - .json(ErrorJson::from("Unable to retrieve device from datastore")); - }; - - if device.is_none() { - return HttpResponse::NotFound().json(ErrorJson::from("Associated device not found")); - } - - let device = device.unwrap(); - - if !device.authorized { - return HttpResponse::NotFound().json(ErrorJson::from("Device is unauthorized")); - } - - let Ok(tunnel) = - tunneling::establish_tunneled_rd(&context, &device.id, &session.instance_id).await - else { - return HttpResponse::InternalServerError() - .json(ErrorJson::from("Failed to establish a tunnel")); - }; - - if !tunnel.is_authenticated() { - return HttpResponse::InternalServerError() - .json(ErrorJson::from("Tunnel is not authenticated")); - } - - let (response, ws_session, ws_stream) = - match request_handling::upgrade_to_websocket(request, body) { - Ok(r) => r, - Err(resp) => return resp, - }; - - rt::spawn(handle_connection( - ws_stream, - ws_session, - tunnel, - session, - context.get_ref().clone(), - )); - - response -} diff --git a/wallguard-server/src/http_api/rd_gateway/handle_connection.rs b/wallguard-server/src/http_api/rd_gateway_v2/handle_connection.rs similarity index 100% rename from wallguard-server/src/http_api/rd_gateway/handle_connection.rs rename to wallguard-server/src/http_api/rd_gateway_v2/handle_connection.rs diff --git a/wallguard-server/src/http_api/rd_gateway_v2/mod.rs b/wallguard-server/src/http_api/rd_gateway_v2/mod.rs new file mode 100644 index 00000000..5748504b --- /dev/null +++ b/wallguard-server/src/http_api/rd_gateway_v2/mod.rs @@ -0,0 +1,76 @@ +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use super::utilities::error_json::ErrorJson; +use super::utilities::request_handling; +use crate::app_context::AppContext; +use crate::datastore::TunnelStatus; +use crate::tunneling::tunnel_common::WallguardTunnel; +use actix_web::HttpRequest; +use actix_web::HttpResponse; +use actix_web::Responder; +use actix_web::rt; +use actix_web::web::{Data, Payload}; + +mod websocket_relay; + +pub(super) async fn open_remote_desktop_session( + request: HttpRequest, + context: Data, + body: Payload, +) -> impl Responder { + let tunnel_id = match request_handling::extract_session_token(&request) { + Ok(tunnel_id) => tunnel_id.to_ascii_uppercase(), + Err(response) => return response, + }; + + let Some(WallguardTunnel::RemoteDesktop(rd_tunnel)) = context.tunnels_manager.get(&tunnel_id).await + else { + return HttpResponse::NotFound().json(ErrorJson::from("Tunnel not found")); + }; + + { + let mut lock = rd_tunnel.lock().await; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let (date, time) = crate::utilities::time::timestamp_to_datetime(timestamp.cast_signed()); + lock.data.tunnel_data.last_access_date = Some(date); + lock.data.tunnel_data.last_access_time = Some(time); + + if let Ok(token) = context.sysdev_token_provider.get().await { + let _ = context + .datastore + .update_tunnel_accessed(&token.jwt, &lock.data.tunnel_data.id, false, timestamp) + .await; + + let _ = context + .datastore + .update_tunnel_status( + &token.jwt, + &lock.data.tunnel_data.id, + TunnelStatus::Active, + token.account.is_root_account, + ) + .await; + } + } + + let (response, ws_session, stream) = match request_handling::upgrade_to_websocket(request, body) + { + Ok(r) => r, + Err(resp) => return resp, + }; + + rt::spawn(websocket_relay::websocket_relay( + stream, + ws_session, + ssh_tunnel, + context.into_inner(), + )); + + response +} diff --git a/wallguard-server/src/http_api/rd_gateway_v2/websocket_relay.rs b/wallguard-server/src/http_api/rd_gateway_v2/websocket_relay.rs new file mode 100644 index 00000000..f6894bba --- /dev/null +++ b/wallguard-server/src/http_api/rd_gateway_v2/websocket_relay.rs @@ -0,0 +1,106 @@ +use std::sync::Arc; + +use crate::{app_context::AppContext, datastore::TunnelStatus, tunneling::{rd::RemoteDesktopTunnel, ssh::SshTunnel}}; +use actix_ws::{AggregatedMessage, AggregatedMessageStream, MessageStream, Session as WSSession}; +use futures_util::StreamExt as _; +use prost::bytes::Bytes; +use tokio::sync::Mutex; +use wallguard_common::timestamped_packet::TimestampedPacket; +use webrtc::{media::Sample, peer_connection::RTCPeerConnection, srtp::session::Session}; + +pub async fn websocket_relay( + stream: MessageStream, + ws_session: WSSession, + rd_tunnel: Arc>, + context: Arc, +) { + +} + + +async fn handle_signaling( + mut stream: MessageStream, + mut session: Session, + connection: Arc, +) { + while let Some(result) = stream.next().await { + match result { + Ok(msg) => { + if let Message::Text(text) = msg + && let Ok(signal) = serde_json::from_str::(text.as_ref()) + { + match signal { + SignalMessage::Offer { sdp } => { + let offer = RTCSessionDescription::offer(sdp).unwrap(); + connection.set_remote_description(offer).await.unwrap(); + + let answer = connection.create_answer(None).await.unwrap(); + connection + .set_local_description(answer.clone()) + .await + .unwrap(); + + let msg = SignalMessage::Answer { sdp: answer.sdp }; + session + .text(serde_json::to_string(&msg).unwrap()) + .await + .unwrap(); + } + SignalMessage::Ice { candidate } => { + let candidate_init: RTCIceCandidateInit = + serde_json::from_str(&candidate).unwrap(); + connection.add_ice_candidate(candidate_init).await.unwrap(); + } + _ => {} + } + } + } + Err(e) => { + log::error!("WebSocket error: {e}"); + break; + } + } + } +} + +async fn handle_messages_from_remote_desktop( + tunnel: TunnelInstance, + track: Arc, +) { + loop { + let Ok(message) = tunnel.read().await else { + log::error!("RD → WebRTC: Failed to read from RD tunnel"); + break; + }; + + let Some(message) = message.message else { + log::info!("RD → WebRTC: Reached EOF (client disconnected)."); + break; + }; + + let ClientMessage::Data(frame) = message else { + log::error!("RD → WebRTC: Unexpected message."); + break; + }; + + let Ok(packet) = TimestampedPacket::from_bytes(&frame.data) else { + log::error!("Failed to deserialize RD tunnel packet"); + continue; + }; + + let sample = Sample { + data: packet.data.into(), + duration: packet.duration, + ..Default::default() + }; + + if let Err(err) = track.write_sample(&sample).await { + log::error!("RD → WebRTC: Failed to send sample: {err}"); + } else { + let len = sample.data.len(); + log::debug!("RD → WebRTC: Sent sample ({len} bytes)"); + } + } + + log::info!("RD → WS: RD reader loop exited."); +} diff --git a/wallguard-server/src/tunneling/command.rs b/wallguard-server/src/tunneling/command.rs index 710a8b06..59a435db 100644 --- a/wallguard-server/src/tunneling/command.rs +++ b/wallguard-server/src/tunneling/command.rs @@ -57,7 +57,7 @@ pub async fn establish_tunneled_tty( establish_tunneled_channel(context, device_id, &instance_id, TunnelType::Tty).await } -pub async fn _establish_tunneled_rd( +pub async fn establish_tunneled_rd( context: &AppContext, device_id: &str, ) -> Result { diff --git a/wallguard-server/src/tunneling/mod.rs b/wallguard-server/src/tunneling/mod.rs index cedfe4f7..2314f91b 100644 --- a/wallguard-server/src/tunneling/mod.rs +++ b/wallguard-server/src/tunneling/mod.rs @@ -2,6 +2,7 @@ use crate::{ app_context::AppContext, tunneling::{ http::HttpTunnel, + rd::RemoteDesktopTunnel, ssh::SshTunnel, timeout_controller::TimeoutController, tty::TtyTunnel, @@ -11,13 +12,15 @@ use crate::{ use std::{collections::HashMap, sync::Arc}; use tokio::sync::Mutex; -mod command; pub mod http; +pub mod rd; pub mod ssh; -mod timeout_controller; pub mod tty; pub mod tunnel_common; +mod command; +mod timeout_controller; + #[derive(Debug, Clone)] pub struct TunnelsManager { tunnels: Arc>>, @@ -73,6 +76,10 @@ impl TunnelsManager { let tunnel = TtyTunnel::new(context, data).await?; Ok(WallguardTunnel::Tty(Arc::new(Mutex::new(tunnel)))) } + TunnelType::Rd => { + let tunnel = RemoteDesktopTunnel::new(context, data); + Ok(WallguardTunnel::RemoteDesktop(Arc::new(Mutex::new(tunnel)))) + } } } diff --git a/wallguard-server/src/tunneling/rd/mod.rs b/wallguard-server/src/tunneling/rd/mod.rs new file mode 100644 index 00000000..fcda9917 --- /dev/null +++ b/wallguard-server/src/tunneling/rd/mod.rs @@ -0,0 +1,46 @@ +use crate::{ + app_context::AppContext, datastore::TunnelStatus, tunneling::tunnel_common::TunnelCommonData, +}; +use actix_ws::Session; +use nullnet_liberror::{Error, ErrorHandler, Location, location}; +use std::sync::Arc; + +pub mod peer; +mod signal; + +#[derive(Debug, Clone)] +pub struct RemoteDesktopTunnel { + pub data: TunnelCommonData, + context: Arc, +} + +impl RemoteDesktopTunnel { + pub fn new(context: Arc, data: TunnelCommonData) -> Self { + Self { context, data } + } + + pub async fn request_peer(&self, session: Session) -> Result { + let tunnel = + super::command::establish_tunneled_rd(&self.context, &self.data.tunnel_data.device_id) + .await?; + + peer::RdPeerConnection::new(session, tunnel) + .await + .ok_or("Failed to establish peer connection") + .handle_err(location!()) + } + + pub async fn terminate(&self) -> Result<(), Error> { + let token = self.context.sysdev_token_provider.get().await?; + + self.context + .datastore + .update_tunnel_status( + &token.jwt, + &self.data.tunnel_data.id, + TunnelStatus::Terminated, + false, + ) + .await + } +} diff --git a/wallguard-server/src/tunneling/rd/peer.rs b/wallguard-server/src/tunneling/rd/peer.rs new file mode 100644 index 00000000..4ad4eb65 --- /dev/null +++ b/wallguard-server/src/tunneling/rd/peer.rs @@ -0,0 +1,112 @@ +use crate::{reverse_tunnel::TunnelInstance, tunneling::rd::signal::SignalMessage}; +use actix_ws::Session; +use std::sync::Arc; +use tokio::{io::AsyncWriteExt, sync::Mutex}; +use webrtc::{ + api::{ + APIBuilder, + interceptor_registry::register_default_interceptors, + media_engine::{MIME_TYPE_H264, MediaEngine}, + }, + ice_transport::ice_server::RTCIceServer, + interceptor, + peer_connection::{RTCPeerConnection, configuration::RTCConfiguration}, + rtp_transceiver::rtp_codec::RTCRtpCodecCapability, + track::track_local::track_local_static_sample::TrackLocalStaticSample, +}; + +pub struct RdPeerConnection { + inner: Arc, + video_track: Arc, +} + +impl RdPeerConnection { + pub async fn new(session: Session, tunnel: TunnelInstance) -> Option { + let mut media_engine = MediaEngine::default(); + let _ = media_engine.register_default_codecs(); + + let registry = interceptor::registry::Registry::new(); + let registry = register_default_interceptors(registry, &mut media_engine).unwrap(); + + let api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build(); + + let config = RTCConfiguration { + ice_servers: vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_owned()], + ..Default::default() + }], + ..Default::default() + }; + + let inner = Arc::new(api.new_peer_connection(config).await.unwrap()); + + Self::register_callbacks(&inner, session, tunnel); + + let video_track = Arc::new(TrackLocalStaticSample::new( + RTCRtpCodecCapability { + mime_type: MIME_TYPE_H264.to_owned(), + clock_rate: 90000, + channels: 0, + ..Default::default() + }, + "video".to_string(), + "main".to_string(), + )); + + if inner.add_track(video_track.clone()).await.is_err() { + log::error!("Failed to add video track to the peer connection"); + return None; + } + + Some(Self { inner, video_track }) + } + + fn register_callbacks( + inner: &Arc, + session: Session, + tunnel: TunnelInstance, + ) { + let sesh = session.clone(); + inner.on_ice_candidate(Box::new(move |candidate| { + let mut sesh = sesh.clone(); + Box::pin(async move { + if let Some(candidate) = candidate { + let candidate_json = candidate.to_json().unwrap(); + let msg = SignalMessage::Ice { + candidate: serde_json::to_string(&candidate_json).unwrap(), + }; + let _ = sesh.text(serde_json::to_string(&msg).unwrap()).await; + } + }) + })); + + inner.on_peer_connection_state_change(Box::new(move |_| Box::pin(async {}))); + inner.on_ice_connection_state_change(Box::new(move |_| Box::pin(async {}))); + + let a_tunnel = Arc::new(Mutex::new(tunnel)); + inner.on_data_channel(Box::new(move |data_channel| { + data_channel.on_open(Box::new(move || Box::pin(async {}))); + + let a_tunnel = a_tunnel.clone(); + data_channel.on_message(Box::new(move |msg| { + let a_tunnel = a_tunnel.clone(); + Box::pin(async move { + let _ = a_tunnel + .lock() + .await + .write_all(msg.data.iter().as_slice()) + .await; + }) + })); + + Box::pin(async {}) + })); + } + + async fn close(&self) { + let _ = self.inner.close().await; + } +} diff --git a/wallguard-server/src/http_api/rd_gateway/signal_message.rs b/wallguard-server/src/tunneling/rd/signal.rs similarity index 100% rename from wallguard-server/src/http_api/rd_gateway/signal_message.rs rename to wallguard-server/src/tunneling/rd/signal.rs diff --git a/wallguard-server/src/tunneling/timeout_controller.rs b/wallguard-server/src/tunneling/timeout_controller.rs index 68681507..6843d729 100644 --- a/wallguard-server/src/tunneling/timeout_controller.rs +++ b/wallguard-server/src/tunneling/timeout_controller.rs @@ -138,6 +138,35 @@ impl TimeoutController { expired_ids.push(tun.data.tunnel_data.id.clone()); } } + WallguardTunnel::RemoteDesktop(rd_tunnel) => { + let tun = rd_tunnel.lock().await; + + let last_accessed_timestamp = + crate::utilities::time::datetime_to_timestamp( + &tun.data + .tunnel_data + .last_access_date + .clone() + .unwrap_or_default(), + &tun.data + .tunnel_data + .last_access_time + .clone() + .unwrap_or_default(), + ) + .unwrap_or_default() + .cast_unsigned(); + + let timestamp = if last_accessed_timestamp != 0 { + last_accessed_timestamp + } else { + tun.data.created_at + }; + + if timestamp < Self::cutoff_timestamp(self.idle_timeout_duration()) { + expired_ids.push(tun.data.tunnel_data.id.clone()); + } + } }; } diff --git a/wallguard-server/src/tunneling/tunnel_common.rs b/wallguard-server/src/tunneling/tunnel_common.rs index d45a378a..e463c1a8 100644 --- a/wallguard-server/src/tunneling/tunnel_common.rs +++ b/wallguard-server/src/tunneling/tunnel_common.rs @@ -4,6 +4,7 @@ use tokio::sync::Mutex; use crate::app_context::AppContext; use crate::datastore::{ServiceInfo, TunnelModel, TunnelStatus, TunnelType}; use crate::tunneling::http::HttpTunnel; +use crate::tunneling::rd::RemoteDesktopTunnel; use crate::tunneling::ssh::SshTunnel; use crate::tunneling::tty::TtyTunnel; use std::fmt::{Display, Formatter}; @@ -138,6 +139,7 @@ pub enum WallguardTunnel { Http(Arc>), Ssh(Arc>), Tty(Arc>), + RemoteDesktop(Arc>), } impl WallguardTunnel { @@ -146,6 +148,7 @@ impl WallguardTunnel { WallguardTunnel::Http(http_tunnel) => http_tunnel.lock().await.terminate().await, WallguardTunnel::Ssh(ssh_tunnel) => ssh_tunnel.lock().await.terminate().await, WallguardTunnel::Tty(tty_tunnel) => tty_tunnel.lock().await.terminate().await, + WallguardTunnel::RemoteDesktop(rd_tunnel) => rd_tunnel.lock().await.terminate().await, } } @@ -154,6 +157,7 @@ impl WallguardTunnel { WallguardTunnel::Http(tun) => tun.lock().await.data.service_data.id.clone(), WallguardTunnel::Ssh(tun) => tun.lock().await.data.service_data.id.clone(), WallguardTunnel::Tty(tun) => tun.lock().await.data.service_data.id.clone(), + WallguardTunnel::RemoteDesktop(tun) => tun.lock().await.data.service_data.id.clone(), } } @@ -162,6 +166,7 @@ impl WallguardTunnel { WallguardTunnel::Http(tun) => tun.lock().await.data.tunnel_data.id.clone(), WallguardTunnel::Ssh(tun) => tun.lock().await.data.tunnel_data.id.clone(), WallguardTunnel::Tty(tun) => tun.lock().await.data.tunnel_data.id.clone(), + WallguardTunnel::RemoteDesktop(tun) => tun.lock().await.data.tunnel_data.id.clone(), } } } diff --git a/wallguard/Cargo.toml b/wallguard/Cargo.toml index f6560edd..34eb6e38 100644 --- a/wallguard/Cargo.toml +++ b/wallguard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wallguard" -version = "0.1.18" +version = "0.1.19" edition = "2024" license = "AGPL-3.0-only" diff --git a/wallguard/src/control_channel/commands/open_remote_desktop_session_command.rs b/wallguard/src/control_channel/commands/open_remote_desktop_session_command.rs index beff2b08..39103a21 100644 --- a/wallguard/src/control_channel/commands/open_remote_desktop_session_command.rs +++ b/wallguard/src/control_channel/commands/open_remote_desktop_session_command.rs @@ -67,6 +67,10 @@ async fn stream_to_system( let message = buffer[..bytes].to_vec(); + if message.is_empty() { + break; + } + if let Err(err) = remote_desktop_manager .on_client_message(client_id, message) .await @@ -77,6 +81,8 @@ async fn stream_to_system( ); } } + + Ok(()) } async fn system_to_stream( diff --git a/wallguard/src/fireparse/opnsense/interfaces_parser.rs b/wallguard/src/fireparse/opnsense/interfaces_parser.rs index 5eb68502..3bd17bf2 100644 --- a/wallguard/src/fireparse/opnsense/interfaces_parser.rs +++ b/wallguard/src/fireparse/opnsense/interfaces_parser.rs @@ -16,6 +16,12 @@ impl OpnSenseInterfacesParser { if let XMLNode::Element(interface_element) = interface { let name = interface_element.name.clone(); + let description = interface_element + .get_child("descr") + .and_then(|c| c.get_text()) + .unwrap_or_default() + .to_string(); + let device = interface_element .get_child("if") .and_then(|e| e.get_text()) @@ -59,6 +65,7 @@ impl OpnSenseInterfacesParser { interfaces.push(NetworkInterface { name, device, + description, addresses, }); } diff --git a/wallguard/src/fireparse/pfsense/interfaces_parser.rs b/wallguard/src/fireparse/pfsense/interfaces_parser.rs index 1b6f97ca..980a026f 100644 --- a/wallguard/src/fireparse/pfsense/interfaces_parser.rs +++ b/wallguard/src/fireparse/pfsense/interfaces_parser.rs @@ -19,6 +19,12 @@ impl PfSenseInterfacesParser { { let name = interface.name.clone(); + let description = interface + .get_child("descr") + .and_then(|c| c.get_text()) + .unwrap_or_default() + .to_string(); + let device = interface .get_child("if") .and_then(|c| c.get_text()) @@ -57,6 +63,7 @@ impl PfSenseInterfacesParser { interfaces.push(NetworkInterface { name, device, + description, addresses, }); } @@ -82,10 +89,12 @@ mod tests { igb0 + 192.168.1.1 igb1 + 192.168.1.2 @@ -97,12 +106,14 @@ mod tests { assert_eq!(interfaces.len(), 2); assert_eq!(interfaces[0].name, "wan"); assert_eq!(interfaces[0].device, "igb0"); + assert_eq!(interfaces[0].description, "description1"); assert_eq!(interfaces[0].addresses.len(), 1); assert_eq!(interfaces[0].addresses[0].address, "192.168.1.1"); assert_eq!(interfaces[0].addresses[0].version, 4); assert_eq!(interfaces[1].name, "lan"); assert_eq!(interfaces[1].device, "igb1"); + assert_eq!(interfaces[1].description, "description2"); assert_eq!(interfaces[1].addresses.len(), 1); assert_eq!(interfaces[1].addresses[0].address, "192.168.1.2"); assert_eq!(interfaces[1].addresses[0].version, 4); diff --git a/wallguard/src/netinfo/service/mod.rs b/wallguard/src/netinfo/service/mod.rs index 39f83a2a..f11caa80 100644 --- a/wallguard/src/netinfo/service/mod.rs +++ b/wallguard/src/netinfo/service/mod.rs @@ -14,6 +14,7 @@ pub enum Protocol { Https, Ssh, Tty, + Rd, } #[derive(Debug)] @@ -31,6 +32,7 @@ impl From for ServiceInfoGrpc { Protocol::Https => ProtocolGrpc::Https.into(), Protocol::Ssh => ProtocolGrpc::Ssh.into(), Protocol::Tty => ProtocolGrpc::Tty.into(), + Protocol::Rd => ProtocolGrpc::Rd.into(), }, program: val.program, address: val.addr.ip().to_string(), @@ -44,7 +46,7 @@ pub async fn gather_info(mut sockets: Vec) -> Vec { retval.extend(http::filter(&mut sockets).await); retval.extend(ssh::filter(&mut sockets).await); - retval.extend(pseudo::filter(&mut sockets)); + retval.extend(pseudo::filter(&mut sockets).await); retval } diff --git a/wallguard/src/netinfo/service/pseudo.rs b/wallguard/src/netinfo/service/pseudo.rs index c3d00950..5bac09e2 100644 --- a/wallguard/src/netinfo/service/pseudo.rs +++ b/wallguard/src/netinfo/service/pseudo.rs @@ -1,12 +1,40 @@ +use crate::netinfo::service::ServiceInfo; use crate::netinfo::sock::SocketInfo; +use std::env; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use tokio::process::Command; -use crate::netinfo::service::ServiceInfo; +async fn has_desktop() -> bool { + if cfg!(target_os = "freebsd") { + return false; + } + + if env::var("DISPLAY").is_ok() || env::var("WAYLAND_DISPLAY").is_ok() { + return true; + } + + let output = Command::new("pgrep") + .args(&["-x", "Xorg|Xwayland|sway|weston|mutter|kwin_wayland"]) + .output() + .await; -pub fn filter(_: &mut Vec) -> Vec { - vec![ServiceInfo { + matches!(output, Ok(o) if o.status.success()) +} + +pub async fn filter(_: &mut Vec) -> Vec { + let mut retval = vec![ServiceInfo { addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), protocol: super::Protocol::Tty, program: String::from("/wallguard-tty"), - }] + }]; + + if has_desktop().await { + retval.push(ServiceInfo { + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), + protocol: super::Protocol::Rd, + program: String::from("/wallguard-remote-desktop"), + }); + } + + retval }