diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 6184432ed..021e82fde 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -1955,6 +1955,7 @@ dependencies = [
"git-version",
"humantime",
"ipconfig",
+ "known-folders",
"nix 0.28.0",
"resolv-conf",
"sd-notify",
@@ -1964,7 +1965,9 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
+ "tracing-subscriber",
"url",
+ "windows-service",
]
[[package]]
@@ -7535,6 +7538,17 @@ dependencies = [
"windows-targets 0.52.5",
]
+[[package]]
+name = "windows-service"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
+dependencies = [
+ "bitflags 2.5.0",
+ "widestring",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "windows-sys"
version = "0.36.1"
diff --git a/rust/gui-client/docs/intended_behavior.md b/rust/gui-client/docs/intended_behavior.md
index 9884634a7..5ec0c4868 100644
--- a/rust/gui-client/docs/intended_behavior.md
+++ b/rust/gui-client/docs/intended_behavior.md
@@ -39,7 +39,7 @@ Best performed on a clean VM
1. Export the logs
1. Expect the zip file to start with "firezone_logs_"
1. Expect `zipinfo` to show a single directory in the root of the zip, to prevent zip bombing
-1. Expect two subdirectories in the zip, "connlib", and "app", each with 3 files, totalling 6 files
+1. Expect two subdirectories in the zip, "connlib", and "app", with 3 and 2 files respectively, totalling 5 files
## Settings tab
diff --git a/rust/gui-client/src-tauri/tauri.conf.json b/rust/gui-client/src-tauri/tauri.conf.json
index 7b8c997d6..41b0d711c 100644
--- a/rust/gui-client/src-tauri/tauri.conf.json
+++ b/rust/gui-client/src-tauri/tauri.conf.json
@@ -35,7 +35,14 @@
"icons/icon.png"
],
"publisher": "Firezone",
- "shortDescription": "Firezone"
+ "shortDescription": "Firezone",
+ "windows": {
+ "wix": {
+ "componentRefs": ["FirezoneClientIpcService"],
+ "fragmentPaths": ["./win_files/service.wxs"],
+ "template": "./win_files/main.wxs"
+ }
+ }
},
"security": {
"csp": null
diff --git a/rust/gui-client/src-tauri/win_files/main.wxs b/rust/gui-client/src-tauri/win_files/main.wxs
new file mode 100644
index 000000000..79b28789e
--- /dev/null
+++ b/rust/gui-client/src-tauri/win_files/main.wxs
@@ -0,0 +1,315 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#if allow_downgrades}}
+
+ {{else}}
+
+ {{/if}}
+
+
+ Installed AND NOT UPGRADINGPRODUCTCODE
+
+
+
+
+ {{#if banner_path}}
+
+ {{/if}}
+ {{#if dialog_image_path}}
+
+ {{/if}}
+ {{#if license}}
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed
+
+
+
+ {{#unless license}}
+
+ 1
+ 1
+ {{/unless}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#each binaries as |bin| ~}}
+
+ {{/each~}}
+ {{#if enable_elevated_update_task}}
+
+
+
+
+
+
+
+
+
+ {{/if}}
+ {{resources}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{#each merge_modules as |msm| ~}}
+
+
+
+
+
+
+
+ {{/each~}}
+
+
+
+
+
+ {{#each resource_file_ids as |resource_file_id| ~}}
+
+ {{/each~}}
+
+ {{#if enable_elevated_update_task}}
+
+
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+ {{#each binaries as |bin| ~}}
+
+ {{/each~}}
+
+
+
+
+ {{#each component_group_refs as |id| ~}}
+
+ {{/each~}}
+ {{#each component_refs as |id| ~}}
+
+ {{/each~}}
+ {{#each feature_group_refs as |id| ~}}
+
+ {{/each~}}
+ {{#each feature_refs as |id| ~}}
+
+ {{/each~}}
+ {{#each merge_refs as |id| ~}}
+
+ {{/each~}}
+
+
+ {{#if install_webview}}
+
+
+
+
+
+
+ {{#if download_bootstrapper}}
+
+
+
+
+
+
+ {{/if}}
+
+
+ {{#if webview2_bootstrapper_path}}
+
+
+
+
+
+
+
+ {{/if}}
+
+
+ {{#if webview2_installer_path}}
+
+
+
+
+
+
+
+ {{/if}}
+
+ {{/if}}
+
+ {{#if enable_elevated_update_task}}
+
+
+
+
+ NOT(REMOVE)
+
+
+
+
+
+
+ (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
+
+
+ {{/if}}
+
+
+
+
diff --git a/rust/gui-client/src-tauri/win_files/service.wxs b/rust/gui-client/src-tauri/win_files/service.wxs
new file mode 100644
index 000000000..087412bdd
--- /dev/null
+++ b/rust/gui-client/src-tauri/win_files/service.wxs
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/headless-client/Cargo.toml b/rust/headless-client/Cargo.toml
index 62a226b93..c3ca3a7c9 100644
--- a/rust/headless-client/Cargo.toml
+++ b/rust/headless-client/Cargo.toml
@@ -37,6 +37,9 @@ dirs = "5.0.1"
[target.'cfg(target_os = "windows")'.dependencies]
ipconfig = "0.3.2"
+known-folders = "1.1.0"
+tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
+windows-service = "0.7.0"
[lints]
workspace = true
diff --git a/rust/headless-client/src/imp_linux.rs b/rust/headless-client/src/imp_linux.rs
index 37502e25c..cfbe2846a 100644
--- a/rust/headless-client/src/imp_linux.rs
+++ b/rust/headless-client/src/imp_linux.rs
@@ -63,8 +63,12 @@ pub fn default_token_path() -> PathBuf {
.join("token")
}
+/// Only called from the GUI Client's build of the IPC service
+///
+/// On Linux this is the same as running with `ipc-service`
pub fn run_only_ipc_service() -> Result<()> {
let cli = Cli::parse();
+ // systemd supplies this but maybe we should hard-code a better default
let (layer, _handle) = cli.log_dir.as_deref().map(file_logger::layer).unzip();
setup_global_subscriber(layer);
tracing::info!(git_version = crate::GIT_VERSION);
@@ -72,7 +76,9 @@ pub fn run_only_ipc_service() -> Result<()> {
if !nix::unistd::getuid().is_root() {
anyhow::bail!("This is the IPC service binary, it's not meant to run interactively.");
}
- run_ipc_service(cli)
+ let rt = tokio::runtime::Runtime::new()?;
+ let (_shutdown_tx, shutdown_rx) = mpsc::channel(1);
+ run_ipc_service(cli, rt, shutdown_rx)
}
pub(crate) fn check_token_permissions(path: &Path) -> Result<()> {
@@ -178,9 +184,12 @@ pub fn sock_path() -> PathBuf {
.join("ipc.sock")
}
-pub(crate) fn run_ipc_service(cli: Cli) -> Result<()> {
- let rt = tokio::runtime::Runtime::new()?;
- tracing::info!("run_daemon");
+pub(crate) fn run_ipc_service(
+ cli: Cli,
+ rt: tokio::runtime::Runtime,
+ _shutdown_rx: mpsc::Receiver<()>,
+) -> Result<()> {
+ tracing::info!("run_ipc_service");
rt.block_on(async { ipc_listen(cli).await })
}
diff --git a/rust/headless-client/src/imp_windows.rs b/rust/headless-client/src/imp_windows.rs
index 10cf8cf39..55fdc78e3 100644
--- a/rust/headless-client/src/imp_windows.rs
+++ b/rust/headless-client/src/imp_windows.rs
@@ -1,13 +1,28 @@
use crate::Cli;
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use clap::Parser;
use connlib_client_shared::file_logger;
-use firezone_cli_utils::setup_global_subscriber;
use std::{
+ ffi::OsString,
net::IpAddr,
path::{Path, PathBuf},
+ str::FromStr,
task::{Context, Poll},
+ time::Duration,
};
+use tokio::sync::mpsc;
+use tracing::subscriber::set_global_default;
+use tracing_subscriber::{layer::SubscriberExt as _, EnvFilter, Layer, Registry};
+use windows_service::{
+ service::{
+ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
+ ServiceType,
+ },
+ service_control_handler::{self, ServiceControlHandlerResult},
+};
+
+const SERVICE_NAME: &str = "firezone_client_ipc";
+const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
pub(crate) struct Signals {
sigint: tokio::signal::windows::CtrlC,
@@ -39,18 +54,119 @@ pub(crate) fn default_token_path() -> std::path::PathBuf {
PathBuf::from("token.txt")
}
+/// Only called from the GUI Client's build of the IPC service
+///
+/// On Windows, this is wrapped specially so that Windows' service controller
+/// can launch it.
pub fn run_only_ipc_service() -> Result<()> {
- let cli = Cli::parse();
- let (layer, _handle) = cli.log_dir.as_deref().map(file_logger::layer).unzip();
- setup_global_subscriber(layer);
- tracing::info!(git_version = crate::GIT_VERSION);
-
- run_ipc_service(cli)
+ windows_service::service_dispatcher::start(SERVICE_NAME, ffi_service_run)?;
+ Ok(())
}
-pub(crate) fn run_ipc_service(_cli: Cli) -> Result<()> {
- // TODO: Process split on Windows
- todo!()
+// Generates `ffi_service_run` from `service_run`
+windows_service::define_windows_service!(ffi_service_run, windows_service_run);
+
+fn windows_service_run(_arguments: Vec) {
+ if let Err(_e) = fallible_windows_service_run() {
+ todo!();
+ }
+}
+
+#[cfg(debug_assertions)]
+const SERVICE_RUST_LOG: &str = "debug";
+
+#[cfg(not(debug_assertions))]
+const SERVICE_RUST_LOG: &str = "info";
+
+// Most of the Windows-specific service stuff should go here
+fn fallible_windows_service_run() -> Result<()> {
+ let cli = Cli::parse();
+ let log_path =
+ crate::known_dirs::imp::ipc_service_logs().context("Can't compute IPC service logs dir")?;
+ std::fs::create_dir_all(&log_path)?;
+ let (layer, _handle) = file_logger::layer(&log_path);
+ let filter = EnvFilter::from_str(SERVICE_RUST_LOG)?;
+ let subscriber = Registry::default().with(layer.with_filter(filter));
+ set_global_default(subscriber)?;
+ tracing::info!(git_version = crate::GIT_VERSION);
+
+ let rt = tokio::runtime::Runtime::new()?;
+ let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
+
+ let event_handler = move |control_event| -> ServiceControlHandlerResult {
+ tracing::debug!(?control_event);
+ match control_event {
+ // TODO
+ ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
+ ServiceControl::Stop => {
+ tracing::info!("Got stop signal from service controller");
+ shutdown_tx.blocking_send(()).unwrap();
+ ServiceControlHandlerResult::NoError
+ }
+ ServiceControl::UserEvent(_) => ServiceControlHandlerResult::NoError,
+ ServiceControl::Continue
+ | ServiceControl::NetBindAdd
+ | ServiceControl::NetBindDisable
+ | ServiceControl::NetBindEnable
+ | ServiceControl::NetBindRemove
+ | ServiceControl::ParamChange
+ | ServiceControl::Pause
+ | ServiceControl::Preshutdown
+ | ServiceControl::Shutdown
+ | ServiceControl::HardwareProfileChange(_)
+ | ServiceControl::PowerEvent(_)
+ | ServiceControl::SessionChange(_)
+ | ServiceControl::TimeChange
+ | ServiceControl::TriggerEvent => ServiceControlHandlerResult::NotImplemented,
+ _ => ServiceControlHandlerResult::NotImplemented,
+ }
+ };
+
+ // Tell Windows that we're running (equivalent to sd_notify in systemd)
+ 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 | ServiceControlAccept::SHUTDOWN,
+ exit_code: ServiceExitCode::Win32(0),
+ checkpoint: 0,
+ wait_hint: Duration::default(),
+ process_id: None,
+ })?;
+
+ run_ipc_service(cli, rt, shutdown_rx)?;
+
+ // Tell Windows that we're stopping
+ 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(())
+}
+
+/// Common entry point for both the Windows-wrapped IPC service and the debug IPC service
+///
+/// Running as a Windows service is complicated, so to make debugging easier
+/// we'll have a dev-only mode that runs all the IPC code as a normal process
+/// in an admin console.
+pub(crate) fn run_ipc_service(
+ cli: Cli,
+ rt: tokio::runtime::Runtime,
+ shutdown_rx: mpsc::Receiver<()>,
+) -> Result<()> {
+ tracing::info!("run_ipc_service");
+ rt.block_on(async { ipc_listen(cli, shutdown_rx).await })
+}
+
+async fn ipc_listen(_cli: Cli, mut shutdown_rx: mpsc::Receiver<()>) -> Result<()> {
+ shutdown_rx.recv().await;
+
+ Ok(())
}
pub fn system_resolvers() -> Result> {
diff --git a/rust/headless-client/src/known_dirs.rs b/rust/headless-client/src/known_dirs.rs
index b1975e67c..17f96afe1 100644
--- a/rust/headless-client/src/known_dirs.rs
+++ b/rust/headless-client/src/known_dirs.rs
@@ -10,7 +10,7 @@
pub use imp::{logs, runtime, session, settings};
#[cfg(any(target_os = "linux", target_os = "macos"))]
-mod imp {
+pub mod imp {
use connlib_shared::BUNDLE_ID;
use std::path::PathBuf;
@@ -47,9 +47,20 @@ mod imp {
}
#[cfg(target_os = "windows")]
-mod imp {
+pub mod imp {
+ use connlib_shared::BUNDLE_ID;
+ use known_folders::{get_known_folder_path, KnownFolder};
use std::path::PathBuf;
+ pub fn ipc_service_logs() -> Option {
+ Some(
+ get_known_folder_path(KnownFolder::ProgramData)?
+ .join(BUNDLE_ID)
+ .join("data")
+ .join("logs"),
+ )
+ }
+
/// e.g. `C:\Users\Alice\AppData\Local\dev.firezone.client\data\logs`
///
/// See connlib docs for details
diff --git a/rust/headless-client/src/lib.rs b/rust/headless-client/src/lib.rs
index 9148b9e65..b57d77338 100644
--- a/rust/headless-client/src/lib.rs
+++ b/rust/headless-client/src/lib.rs
@@ -165,15 +165,20 @@ pub fn run() -> Result<()> {
tracing::info!(git_version = crate::GIT_VERSION);
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+ let (_shutdown_tx, shutdown_rx) = mpsc::channel(1);
+
match cli.command() {
Cmd::Auto => {
if let Some(token) = get_token(token_env_var, &cli)? {
- run_standalone(cli, &token)
+ run_standalone(cli, rt, &token)
} else {
- imp::run_ipc_service(cli)
+ imp::run_ipc_service(cli, rt, shutdown_rx)
}
}
- Cmd::IpcService => imp::run_ipc_service(cli),
+ Cmd::IpcService => imp::run_ipc_service(cli, rt, shutdown_rx),
Cmd::Standalone => {
let token = get_token(token_env_var, &cli)?.with_context(|| {
format!(
@@ -181,7 +186,7 @@ pub fn run() -> Result<()> {
cli.token_path
)
})?;
- run_standalone(cli, &token)
+ run_standalone(cli, rt, &token)
}
}
}
@@ -193,11 +198,8 @@ enum SignalKind {
Interrupt,
}
-fn run_standalone(cli: Cli, token: &SecretString) -> Result<()> {
+fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -> Result<()> {
tracing::info!("Running in standalone mode");
- let rt = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()?;
let _guard = rt.enter();
// TODO: Should this default to 30 days?
let max_partition_time = cli.max_partition_time.map(|d| d.into());
diff --git a/scripts/build/tauri-rename-windows.sh b/scripts/build/tauri-rename-windows.sh
index e410ee751..1098c3bb2 100755
--- a/scripts/build/tauri-rename-windows.sh
+++ b/scripts/build/tauri-rename-windows.sh
@@ -17,3 +17,9 @@ function make_hash() {
make_hash "$BINARY_DEST_PATH.exe"
make_hash "$BINARY_DEST_PATH.msi"
make_hash "$BINARY_DEST_PATH.pdb"
+
+# Test-install the MSI package, since it already exists here
+msiexec //i "$BINARY_DEST_PATH.msi" //log install.log //qn
+# For debugging
+cat install.log
+sc query FirezoneClientIpcService | grep RUNNING