diff --git a/Cargo.lock b/Cargo.lock index fac2d46..b29963d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2589,6 +2589,8 @@ dependencies = [ "tokio", "toml", "tower-http", + "windows 0.61.1", + "windows-service", "x11rb", ] @@ -2997,6 +2999,12 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -3174,6 +3182,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-service" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.59.0", +] + [[package]] name = "windows-strings" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2640b6c..33939de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,20 @@ x11rb = "0.13.1" [target.'cfg(target_os = "windows")'.dependencies] str0m = { version = "0.8.0", default-features = false, features = ["sha1", "wincrypto"] } +windows-service = "0.8.0" +windows = { version = "0.61.1", features = [ + "Win32_Foundation", + "Win32_System_RemoteDesktop", + "Win32_Security", + "Win32_System_Threading", + "Win32_System_Services", + "Win32_System_SystemServices", + "Win32_System_Environment", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Environment", + "Win32_System_StationsAndDesktops", + "Win32_System_SystemServices" +] } [target.'cfg(not(target_os = "windows"))'.dependencies] str0m = { version = "0.8.0" } @@ -50,3 +64,6 @@ bindgen = "0.69.4" [profile.dist] inherits = "release" lto = "thin" + +[profile.release] +debug = true diff --git a/README.md b/README.md index 194b044..21e68e0 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,23 @@ Tenebra uses GStreamer to record the screen in a cross-platform way, and to enco [GStreamer Installs](https://gstreamer.freedesktop.org/download/) -To use a Github release, you only need the runtime package. To build Tenebra, you need to install both the development and the runtime packages. +To use a Github release, you only need the runtime package. To build Tenebra, you need to install both the development and the runtime packages. On Windows, GStreamer's bin folder must be added to the PATH. -After the server is built with `cargo build --release`, you may run it: +After the server is built with `cargo build --release`, you may run it. On macOS and Windows, this is as easy as: ``` ./target/release/tenebra ``` +But on Windows, Tenebra must run as a service in order to have the necessary integrity level to interact with all parts of the desktop. First, a service must be registered: +``` +sc create Tenebra binPath= "C:\path\to\tenebra\exe" +``` + +Then, starting Tenebra is as easy as: +``` +sc start Tenebra +``` + However, Tenebra reads from a config file which must be populated before running Tenebra. If it is not populated, Tenebra will fail before copying the default config file to the config file directory. * On **Linux** the config file is at `$XDG_CONFIG_HOME`/tenebra/config.toml or `$HOME`/.config/tenebra/config.toml (e.g. /home/alice/.config/tenebra/config.toml) @@ -56,8 +66,8 @@ On macOS, [VideoToolbox](https://developer.apple.com/documentation/videotoolbox) On Windows, [Media Foundation](https://learn.microsoft.com/en-us/windows/win32/medfound/microsoft-media-foundation-sdk) can be used to perform hardware accelerated H.264 encoding. This can be enabled by setting the `hwencode` property in the config.toml to `true`. The `mfh264enc` GStreamer element must be installed and USABLE. Enable `hwencode` will also automatically enable the use of D3D11 for video format conversion. -## Touch input +## Touch input & pen input -On Linux and Windows, Tenebra has support for receiving and emulating touch events (e.g. from an iPad client). +On Linux and Windows, Tenebra has support for receiving and emulating touch and pen events (e.g. from an iPad client). On Linux, this requires permission to access uinput. Reference your distribution's documentation for details. diff --git a/src/input.rs b/src/input.rs index 88b361e..b103b46 100644 --- a/src/input.rs +++ b/src/input.rs @@ -278,6 +278,9 @@ pub fn do_input( let mut held: HashSet = HashSet::new(); while let Some(msg) = rx.blocking_recv() { + #[cfg(target_os = "windows")] + let _ = crate::windows_service::sync_thread_desktop(); + match msg { InputCommand { r#type, diff --git a/src/main.rs b/src/main.rs index 897379f..b188c80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,6 +101,10 @@ pub mod keys; mod rtc; mod stun; +// This module contains all code related to Windows service functionality +#[cfg(target_os = "windows")] +pub mod windows_service; + pub struct AppError(anyhow::Error); impl IntoResponse for AppError { @@ -447,12 +451,37 @@ fn default_vbv_buf_capacity() -> u32 { 120 } + +#[cfg(target_os = "windows")] +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + let option: Option<&str> = args.get(1).map(|s| s.as_str()); + if let Some("--console") = option { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(crate::entrypoint())?; + } else { + windows_service::run()?; + } + Ok(()) +} + +#[cfg(not(target_os = "windows"))] #[tokio::main] async fn main() -> Result<()> { + entrypoint().await +} + +async fn entrypoint() -> Result<()> { // WinCrypto simplifies build significantly on Windows #[cfg(target_os = "windows")] str0m::config::CryptoProvider::WinCrypto.install_process_default(); + #[cfg(target_os = "windows")] + let _ = windows_service::sync_thread_desktop(); + // check if we're behind symmetric NAT if stun::is_symmetric_nat() .await @@ -467,11 +496,19 @@ async fn main() -> Result<()> { gstreamer::init().unwrap(); // get the config path + #[cfg(not(target_os = "windows"))] let mut config_path = dirs::config_dir() .context("Failed to find config directory")? .join("tenebra"); - std::fs::create_dir_all(&config_path).context("Failed to create config directory")?; + #[cfg(not(target_os = "windows"))] config_path.push("config.toml"); + + #[cfg(target_os = "windows")] + let mut config_path = std::path::Path::new("C:\\tenebra.toml"); + + #[cfg(not(target_os = "windows"))] + std::fs::create_dir_all(&config_path).context("Failed to create config directory")?; + if !config_path.exists() { std::fs::write(&config_path, include_bytes!("default.toml")) .context("Failed to write default config")?; diff --git a/src/windows_service.rs b/src/windows_service.rs new file mode 100644 index 0000000..9d8ec66 --- /dev/null +++ b/src/windows_service.rs @@ -0,0 +1,233 @@ +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher +}; +use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, HDESK, OpenInputDesktop, CloseDesktop, SetThreadDesktop, DF_ALLOWOTHERACCOUNTHOOK}; +use windows::Win32::Foundation::*; +use windows::Win32::System::RemoteDesktop::WTSGetActiveConsoleSessionId; +use windows::Win32::System::Threading::{ + CreateProcessAsUserW, GetCurrentProcess, OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, + CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, +}; +use windows::Win32::Security::*; +use windows::Win32::System::Environment::{DestroyEnvironmentBlock, CreateEnvironmentBlock}; +use windows::core::{PCWSTR, PWSTR}; + +use anyhow::Result; + +use core::ffi::c_void; +use std::cell::Cell; +use std::time::Duration; +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::OsStrExt; + +const SERVICE_NAME: &str = "Tenebra"; +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +struct RAIIGuard { + cleanup: Option, +} + +impl RAIIGuard { + fn new(cleanup: F) -> Self { + RAIIGuard { + cleanup: Some(cleanup), + } + } +} + +impl Drop for RAIIGuard { + fn drop(&mut self) { + if let Some(f) = self.cleanup.take() { + f(); + } + } +} + +pub fn run() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) +} + +define_windows_service!(ffi_service_main, service_main); + +pub fn service_main(_arguments: Vec) { + use std::io::Write; + let mut file = std::fs::File::create("C:\\tenebra_log.txt").unwrap(); + unsafe { std::env::set_var("RUST_BACKTRACE", "1"); } + if let Err(e) = run_service() { + writeln!(&mut file, "Error: {:?}", e).unwrap(); + } +} + +pub fn run_service() -> Result<()> { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + // There's no need to handle the stop event because the service exists immediately + ServiceControl::Stop => ServiceControlHandlerResult::NoError, + ServiceControl::UserEvent(_code) => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + // Launch the process + unsafe { + let mut token_handle = HANDLE::default(); + OpenProcessToken( + GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_DUPLICATE | TOKEN_QUERY, + &mut token_handle, + )?; + let _token_guard = RAIIGuard::new(|| { let _ = CloseHandle(token_handle); }); + + // Give ourselves SeTcbPrivilege + let mut luid = LUID::default(); + LookupPrivilegeValueW(None, SE_TCB_NAME, &mut luid)?; + let mut new_privs = TOKEN_PRIVILEGES { + PrivilegeCount: 1, + ..Default::default() + }; + new_privs.Privileges[0].Luid = luid; + new_privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges( + token_handle, + false, // Do not disable all other privileges + Some(&new_privs), + 0, // Buffer length for previous state (not needed) + None, // Pointer to previous state (not needed) + None, // Return length (not needed) + )?; + + let mut new_token_handle = HANDLE::default(); + DuplicateTokenEx( + token_handle, + TOKEN_ACCESS_MASK(windows::Win32::System::SystemServices::MAXIMUM_ALLOWED), + None, + SecurityImpersonation, + TokenPrimary, + &mut new_token_handle, + )?; + let _new_token_guard = RAIIGuard::new(|| { let _ = CloseHandle(new_token_handle); }); + + // Get the session ID of the active user's session + let sid = WTSGetActiveConsoleSessionId(); + if sid == 0xFFFFFFFF { + // error, abort + anyhow::bail!("Bad session ID from WTSGetActiveConsoleSessionId"); + } + SetTokenInformation( + new_token_handle, + TokenSessionId, + &sid as *const u32 as *const c_void, + std::mem::size_of::() as u32, + )?; + + let mut env_block: *mut c_void = std::ptr::null_mut(); + CreateEnvironmentBlock(&mut env_block, Some(new_token_handle), false)?; // Use default env + let _env_block_guard = RAIIGuard::new(|| { let _ = DestroyEnvironmentBlock(env_block); }); + + let mut si: STARTUPINFOW = std::mem::zeroed(); + si.cb = std::mem::size_of::() as u32; + let mut desktop_name: Vec = OsStr::new("winsta0\\default") + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + si.lpDesktop = PWSTR(desktop_name.as_mut_ptr()); + + // Relying on the output of current_exe is NOT a security risk, because an attacker + // cannot swap this executable out for a new executable while the service is running. + // Windows prevents users from deleting the executable of a running service. + let command = format!("{} --console", std::env::current_exe()?.display()); + let mut command_wide: Vec = OsStr::new(&command) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); + CreateProcessAsUserW( + Some(new_token_handle), + None, + Some(PWSTR(command_wide.as_mut_ptr())), + None, + None, + false, + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, + Some(env_block), + None, + &si, + &mut pi, + )?; + + let _ = CloseHandle(pi.hProcess); + let _ = CloseHandle(pi.hThread); + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + + +thread_local! { + static CURRENT_DESKTOP: Cell> = const { Cell::new(None) }; +} + +pub fn sync_thread_desktop() -> Result<()> { + unsafe { + let new_desktop = OpenInputDesktop( + DF_ALLOWOTHERACCOUNTHOOK, + false, + DESKTOP_ACCESS_FLAGS(0x10000000), + )?; + + CURRENT_DESKTOP.with(|cell| { + let current = cell.get(); + + let should_switch = match current { + Some(current) if current == new_desktop => { + CloseDesktop(new_desktop).ok(); // Already using it; discard duplicate handle + return Ok(()); + } + Some(old) => { + SetThreadDesktop(new_desktop)?; // Switch first + CloseDesktop(old).ok(); // Then safely close old + cell.set(Some(new_desktop)); + Ok(()) + } + None => { + SetThreadDesktop(new_desktop)?; + cell.set(Some(new_desktop)); + Ok(()) + } + }; + + should_switch + }) + } +}