mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(gui-client): make all modules Linux-friendly (#3737)
Splits up platform-specific modules into `linux.rs` and `windows.rs` submodules, or `imp` modules within the parent module, so that we can compile all the platform-independent code (e.g. Tauri calls, `run_controller`) for Linux. This will show the Tauri GUI on Linux:  --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com>
This commit is contained in:
@@ -46,21 +46,24 @@ thiserror = { version = "1.0", default-features = false }
|
||||
tokio = { version = "1.36.0", features = ["time"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-log = "0.2"
|
||||
tracing-panic = "0.1.1"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
tracing-panic = "0.1.1"
|
||||
zip = { version = "0.6.6", features = ["deflate", "time"], default-features = false }
|
||||
|
||||
# These dependencies are locked behind `cfg(windows)` because they either can't compile at all on Linux, or they need native dependencies like glib that are difficult to get. Try not to add more here.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dirs = "5.0.1"
|
||||
# Used for infinite `pending` on not-yet-implemented functions
|
||||
futures = "0.3.30"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
tauri-winrt-notification = "0.1.3"
|
||||
windows-implement = "0.52.0"
|
||||
winreg = "0.52.0"
|
||||
wintun = "0.4.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.52.0"
|
||||
features = [
|
||||
# For implementing COM interfaces
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Args, Parser};
|
||||
use std::{os::windows::process::CommandExt, path::PathBuf, process::Command};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod about;
|
||||
mod auth;
|
||||
@@ -16,6 +16,8 @@ mod network_changes;
|
||||
mod resolvers;
|
||||
mod settings;
|
||||
mod updates;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wintun_install;
|
||||
|
||||
/// Output of `git describe` at compile time
|
||||
@@ -35,10 +37,6 @@ pub(crate) enum Error {
|
||||
Gui(#[from] gui::Error),
|
||||
}
|
||||
|
||||
// Hides Powershell's console on Windows
|
||||
// <https://stackoverflow.com/questions/59692146/is-it-possible-to-use-the-standard-library-to-spawn-a-process-without-showing-th#60958956>
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
/// The program's entry point, equivalent to `main`
|
||||
///
|
||||
/// When a user runs the Windows client normally without admin permissions, this will happen:
|
||||
@@ -69,22 +67,8 @@ pub(crate) fn run() -> Result<()> {
|
||||
// We're already elevated, just run the GUI
|
||||
run_gui(cli)
|
||||
} else {
|
||||
// We're not elevated, ask Powershell to re-launch us, then exit
|
||||
let current_exe = tauri_utils::platform::current_exe()?;
|
||||
if current_exe.display().to_string().contains('\"') {
|
||||
anyhow::bail!("The exe path must not contain double quotes, it makes it hard to elevate with Powershell");
|
||||
}
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("-Command")
|
||||
.arg("Start-Process")
|
||||
.arg("-FilePath")
|
||||
.arg(format!(r#""{}""#, current_exe.display()))
|
||||
.arg("-Verb")
|
||||
.arg("RunAs")
|
||||
.arg("-ArgumentList")
|
||||
.arg("elevated")
|
||||
.spawn()?;
|
||||
// We're not elevated, ask Powershell / sudo to re-launch us, then exit
|
||||
elevation::elevate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ fn start_server_and_connect() -> Result<(minidumper::Client, std::process::Child
|
||||
let socket_path = known_dirs::runtime()
|
||||
.context("`known_dirs::runtime` failed")?
|
||||
.join("crash_handler_pipe");
|
||||
std::fs::create_dir_all(
|
||||
socket_path
|
||||
.parent()
|
||||
.context("`known_dirs::runtime` should have a parent")?,
|
||||
)
|
||||
.context("Failed to create dir for crash_handler_pipe")?;
|
||||
|
||||
let mut server = None;
|
||||
|
||||
@@ -118,11 +124,13 @@ impl minidumper::ServerHandler for Handler {
|
||||
.expect("Should be able to find logs dir to put crash dump in")
|
||||
.join("last_crash.dmp");
|
||||
|
||||
if let Some(dir) = dump_path.parent() {
|
||||
if !dir.try_exists()? {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
}
|
||||
// `tracing` is unlikely to work inside the crash handler subprocess, so
|
||||
// just print to stderr and it may show up on the terminal. This helps in CI / local dev.
|
||||
eprintln!("Creating minidump at {}", dump_path.display());
|
||||
let Some(dir) = dump_path.parent() else {
|
||||
return Err(std::io::ErrorKind::NotFound.into());
|
||||
};
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let file = File::create(&dump_path)?;
|
||||
Ok((file, dump_path))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub enum Cmd {
|
||||
Crash,
|
||||
Hostname,
|
||||
NetworkChanges,
|
||||
Wintun,
|
||||
}
|
||||
|
||||
pub fn run(cmd: Cmd) -> Result<()> {
|
||||
@@ -19,7 +18,6 @@ pub fn run(cmd: Cmd) -> Result<()> {
|
||||
Cmd::Crash => crash(),
|
||||
Cmd::Hostname => hostname(),
|
||||
Cmd::NetworkChanges => client::network_changes::run_debug(),
|
||||
Cmd::Wintun => wintun(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,15 +46,3 @@ fn hostname() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to load wintun.dll and throw an error if it's not in the right place
|
||||
fn wintun() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let path = connlib_shared::windows::wintun_dll_path()?;
|
||||
// SAFETY: Loading a DLL from disk is unsafe. We're responsible for making sure
|
||||
// we're using the DLL's API correctly.
|
||||
unsafe { wintun::load_from_path(&path) }?;
|
||||
tracing::info!(?path, "Loaded wintun.dll");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
//! A module for registering, catching, and parsing deep links that are sent over to the app's already-running instance
|
||||
//! Based on reading some of the Windows code from <https://github.com/FabianLars/tauri-plugin-deep-link>, which is licensed "MIT OR Apache-2.0"
|
||||
|
||||
use crate::client::auth::Response as AuthResponse;
|
||||
use connlib_shared::{control::SecureUrl, BUNDLE_ID};
|
||||
use connlib_shared::control::SecureUrl;
|
||||
use secrecy::{ExposeSecret, Secret, SecretString};
|
||||
use std::{ffi::c_void, io, path::Path, str::FromStr};
|
||||
use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::windows::named_pipe};
|
||||
use windows::Win32::Security as WinSec;
|
||||
use std::io;
|
||||
|
||||
pub(crate) const FZ_SCHEME: &str = "firezone-fd0020211111";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "deep_link/linux.rs"]
|
||||
mod imp;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "deep_link/windows.rs"]
|
||||
mod imp;
|
||||
|
||||
// TODO: Replace this all for `anyhow`.
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("named pipe server couldn't start listening, we are probably the second instance")]
|
||||
@@ -26,6 +33,7 @@ pub enum Error {
|
||||
/// We got some data but it's not UTF-8
|
||||
#[error(transparent)]
|
||||
LinkNotUtf8(std::str::Utf8Error),
|
||||
#[cfg(target_os = "windows")]
|
||||
#[error("Couldn't set up security descriptor for deep link server")]
|
||||
SecurityDescriptor,
|
||||
/// Error from server's POV
|
||||
@@ -34,10 +42,13 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
/// Something went wrong setting up the registry
|
||||
#[cfg(target_os = "windows")]
|
||||
#[error(transparent)]
|
||||
WindowsRegistry(io::Error),
|
||||
}
|
||||
|
||||
pub(crate) use imp::{open, register, Server};
|
||||
|
||||
pub(crate) fn parse_auth_callback(url: &Secret<SecureUrl>) -> Option<AuthResponse> {
|
||||
let url = &url.expose_secret().inner;
|
||||
match url.host() {
|
||||
@@ -86,163 +97,6 @@ pub(crate) fn parse_auth_callback(url: &Secret<SecureUrl>) -> Option<AuthRespons
|
||||
})
|
||||
}
|
||||
|
||||
/// A server for a named pipe, so we can receive deep links from other instances
|
||||
/// of the client launched by web browsers
|
||||
pub struct Server {
|
||||
inner: named_pipe::NamedPipeServer,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Construct a server, but don't await client connections yet
|
||||
///
|
||||
/// Panics if there is no Tokio runtime
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
// This isn't air-tight - We recreate the whole server on each loop,
|
||||
// rather than binding 1 socket and accepting many streams like a normal socket API.
|
||||
// I can only assume Tokio is following Windows' underlying API.
|
||||
|
||||
// We could instead pick an ephemeral TCP port and write that to a file,
|
||||
// akin to how Unix processes will write their PID to a file to manage long-running instances
|
||||
// But this doesn't require us to listen on TCP.
|
||||
|
||||
let mut server_options = named_pipe::ServerOptions::new();
|
||||
server_options.first_pipe_instance(true);
|
||||
|
||||
// This will allow non-admin clients to connect to us even if we're running as admin
|
||||
let mut sd = WinSec::SECURITY_DESCRIPTOR::default();
|
||||
let psd = WinSec::PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut c_void);
|
||||
// SAFETY: Unsafe needed to call Win32 API. There shouldn't be any threading
|
||||
// or lifetime problems because we only pass pointers to our local vars to
|
||||
// Win32, and Win32 shouldn't save them anywhere.
|
||||
unsafe {
|
||||
// ChatGPT pointed me to these functions, it's better than the official MS docs
|
||||
WinSec::InitializeSecurityDescriptor(
|
||||
psd,
|
||||
windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION,
|
||||
)
|
||||
.map_err(|_| Error::SecurityDescriptor)?;
|
||||
WinSec::SetSecurityDescriptorDacl(psd, true, None, false)
|
||||
.map_err(|_| Error::SecurityDescriptor)?;
|
||||
}
|
||||
|
||||
let mut sa = WinSec::SECURITY_ATTRIBUTES {
|
||||
// TODO: Try `size_of_val` here instead
|
||||
nLength: std::mem::size_of::<WinSec::SECURITY_ATTRIBUTES>()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
|
||||
// TODO: On the IPC branch I found that this will cause prefix issues
|
||||
// with other named pipes. Change it.
|
||||
let path = named_pipe_path(BUNDLE_ID);
|
||||
let sa_ptr = &mut sa as *mut _ as *mut c_void;
|
||||
// SAFETY: Unsafe needed to call Win32 API. There shouldn't be any threading
|
||||
// or lifetime problems because we only pass pointers to our local vars to
|
||||
// Win32, and Win32 shouldn't save them anywhere.
|
||||
let server = unsafe { server_options.create_with_security_attributes_raw(path, sa_ptr) }
|
||||
.map_err(|_| Error::CantListen)?;
|
||||
|
||||
tracing::debug!("server is bound");
|
||||
Ok(Server { inner: server })
|
||||
}
|
||||
|
||||
/// Await one incoming deep link from a named pipe client
|
||||
/// Tokio's API is strange, so this consumes the server.
|
||||
/// I assume this is based on the underlying Windows API.
|
||||
/// I tried re-using the server and it acted strange. The official Tokio
|
||||
/// examples are not clear on this.
|
||||
pub async fn accept(mut self) -> Result<Secret<SecureUrl>, Error> {
|
||||
self.inner
|
||||
.connect()
|
||||
.await
|
||||
.map_err(Error::ServerCommunications)?;
|
||||
tracing::debug!("server got connection");
|
||||
|
||||
// TODO: Limit the read size here. Our typical callback is 350 bytes, so 4,096 bytes should be more than enough.
|
||||
// Also, I think `read_to_end` can do partial reads because this is a named pipe,
|
||||
// not a file. We might need a length-prefixed or newline-terminated format for IPC.
|
||||
let mut bytes = vec![];
|
||||
self.inner
|
||||
.read_to_end(&mut bytes)
|
||||
.await
|
||||
.map_err(Error::ServerCommunications)?;
|
||||
let bytes = Secret::new(bytes);
|
||||
|
||||
self.inner.disconnect().ok();
|
||||
|
||||
tracing::debug!("Server read");
|
||||
let s = SecretString::from_str(
|
||||
std::str::from_utf8(bytes.expose_secret()).map_err(Error::LinkNotUtf8)?,
|
||||
)
|
||||
.expect("Infallible");
|
||||
let url = Secret::new(SecureUrl::from_url(url::Url::parse(s.expose_secret())?));
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a deep link by sending it to the already-running instance of the app
|
||||
pub async fn open(url: &url::Url) -> Result<(), Error> {
|
||||
let path = named_pipe_path(BUNDLE_ID);
|
||||
let mut client = named_pipe::ClientOptions::new()
|
||||
.open(path)
|
||||
.map_err(Error::Connect)?;
|
||||
client
|
||||
.write_all(url.as_str().as_bytes())
|
||||
.await
|
||||
.map_err(Error::ClientCommunications)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Registers the current exe as the handler for our deep link scheme.
|
||||
///
|
||||
/// This is copied almost verbatim from tauri-plugin-deep-link's `register` fn, with an improvement
|
||||
/// that we send the deep link to a subcommand so the URL won't confuse `clap`
|
||||
///
|
||||
/// * `id` A unique ID for the app, e.g. "com.contoso.todo-list" or "dev.firezone.client"
|
||||
pub fn register() -> Result<(), Error> {
|
||||
let exe = tauri_utils::platform::current_exe()
|
||||
.map_err(Error::CurrentExe)?
|
||||
.display()
|
||||
.to_string()
|
||||
.replace("\\\\?\\", "");
|
||||
|
||||
set_registry_values(BUNDLE_ID, &exe).map_err(Error::WindowsRegistry)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set up the Windows registry to call the given exe when our deep link scheme is used
|
||||
///
|
||||
/// All errors from this function are registry-related
|
||||
fn set_registry_values(id: &str, exe: &str) -> Result<(), io::Error> {
|
||||
let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
|
||||
let base = Path::new("Software").join("Classes").join(FZ_SCHEME);
|
||||
|
||||
let (key, _) = hkcu.create_subkey(&base)?;
|
||||
key.set_value("", &format!("URL:{}", id))?;
|
||||
key.set_value("URL Protocol", &"")?;
|
||||
|
||||
let (icon, _) = hkcu.create_subkey(base.join("DefaultIcon"))?;
|
||||
icon.set_value("", &format!("{},0", &exe))?;
|
||||
|
||||
let (cmd, _) = hkcu.create_subkey(base.join("shell").join("open").join("command"))?;
|
||||
cmd.set_value("", &format!("{} open-deep-link \"%1\"", &exe))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a valid name for a Windows named pipe
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - BUNDLE_ID, e.g. `dev.firezone.client`
|
||||
fn named_pipe_path(id: &str) -> String {
|
||||
format!(r"\\.\pipe\{}", id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
30
rust/gui-client/src-tauri/src/client/deep_link/linux.rs
Normal file
30
rust/gui-client/src-tauri/src/client/deep_link/linux.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
//! TODO: Not implemented for Linux yet
|
||||
|
||||
use super::Error;
|
||||
use connlib_shared::control::SecureUrl;
|
||||
use secrecy::Secret;
|
||||
|
||||
pub(crate) struct Server {}
|
||||
|
||||
impl Server {
|
||||
pub(crate) fn new() -> Result<Self, Error> {
|
||||
tracing::warn!("Not implemented yet");
|
||||
tracing::trace!(scheme = super::FZ_SCHEME, "prevents dead code warning");
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
pub(crate) async fn accept(self) -> Result<Secret<SecureUrl>, Error> {
|
||||
tracing::warn!("Deep links not implemented yet on Linux");
|
||||
futures::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn open(_url: &url::Url) -> Result<(), Error> {
|
||||
tracing::warn!("Not implemented yet");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn register() -> Result<(), Error> {
|
||||
tracing::warn!("Not implemented yet");
|
||||
Ok(())
|
||||
}
|
||||
164
rust/gui-client/src-tauri/src/client/deep_link/windows.rs
Normal file
164
rust/gui-client/src-tauri/src/client/deep_link/windows.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
//! A module for registering, catching, and parsing deep links that are sent over to the app's already-running instance
|
||||
//! Based on reading some of the Windows code from <https://github.com/FabianLars/tauri-plugin-deep-link>, which is licensed "MIT OR Apache-2.0"
|
||||
|
||||
use super::{Error, FZ_SCHEME};
|
||||
use connlib_shared::{control::SecureUrl, BUNDLE_ID};
|
||||
use secrecy::{ExposeSecret, Secret, SecretString};
|
||||
use std::{ffi::c_void, io, path::Path, str::FromStr};
|
||||
use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::windows::named_pipe};
|
||||
use windows::Win32::Security as WinSec;
|
||||
|
||||
/// A server for a named pipe, so we can receive deep links from other instances
|
||||
/// of the client launched by web browsers
|
||||
pub(crate) struct Server {
|
||||
inner: named_pipe::NamedPipeServer,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Construct a server, but don't await client connections yet
|
||||
///
|
||||
/// Panics if there is no Tokio runtime
|
||||
pub(crate) fn new() -> Result<Self, Error> {
|
||||
// This isn't air-tight - We recreate the whole server on each loop,
|
||||
// rather than binding 1 socket and accepting many streams like a normal socket API.
|
||||
// I can only assume Tokio is following Windows' underlying API.
|
||||
|
||||
// We could instead pick an ephemeral TCP port and write that to a file,
|
||||
// akin to how Unix processes will write their PID to a file to manage long-running instances
|
||||
// But this doesn't require us to listen on TCP.
|
||||
|
||||
let mut server_options = named_pipe::ServerOptions::new();
|
||||
server_options.first_pipe_instance(true);
|
||||
|
||||
// This will allow non-admin clients to connect to us even if we're running as admin
|
||||
let mut sd = WinSec::SECURITY_DESCRIPTOR::default();
|
||||
let psd = WinSec::PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut c_void);
|
||||
// SAFETY: Unsafe needed to call Win32 API. There shouldn't be any threading
|
||||
// or lifetime problems because we only pass pointers to our local vars to
|
||||
// Win32, and Win32 shouldn't save them anywhere.
|
||||
unsafe {
|
||||
// ChatGPT pointed me to these functions, it's better than the official MS docs
|
||||
WinSec::InitializeSecurityDescriptor(
|
||||
psd,
|
||||
windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION,
|
||||
)
|
||||
.map_err(|_| Error::SecurityDescriptor)?;
|
||||
WinSec::SetSecurityDescriptorDacl(psd, true, None, false)
|
||||
.map_err(|_| Error::SecurityDescriptor)?;
|
||||
}
|
||||
|
||||
let mut sa = WinSec::SECURITY_ATTRIBUTES {
|
||||
// TODO: Try `size_of_val` here instead
|
||||
nLength: std::mem::size_of::<WinSec::SECURITY_ATTRIBUTES>()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
|
||||
// TODO: On the IPC branch I found that this will cause prefix issues
|
||||
// with other named pipes. Change it.
|
||||
let path = named_pipe_path(BUNDLE_ID);
|
||||
let sa_ptr = &mut sa as *mut _ as *mut c_void;
|
||||
// SAFETY: Unsafe needed to call Win32 API. There shouldn't be any threading
|
||||
// or lifetime problems because we only pass pointers to our local vars to
|
||||
// Win32, and Win32 shouldn't save them anywhere.
|
||||
let server = unsafe { server_options.create_with_security_attributes_raw(path, sa_ptr) }
|
||||
.map_err(|_| Error::CantListen)?;
|
||||
|
||||
tracing::debug!("server is bound");
|
||||
Ok(Server { inner: server })
|
||||
}
|
||||
|
||||
/// Await one incoming deep link from a named pipe client
|
||||
/// Tokio's API is strange, so this consumes the server.
|
||||
/// I assume this is based on the underlying Windows API.
|
||||
/// I tried re-using the server and it acted strange. The official Tokio
|
||||
/// examples are not clear on this.
|
||||
pub(crate) async fn accept(mut self) -> Result<Secret<SecureUrl>, Error> {
|
||||
self.inner
|
||||
.connect()
|
||||
.await
|
||||
.map_err(Error::ServerCommunications)?;
|
||||
tracing::debug!("server got connection");
|
||||
|
||||
// TODO: Limit the read size here. Our typical callback is 350 bytes, so 4,096 bytes should be more than enough.
|
||||
// Also, I think `read_to_end` can do partial reads because this is a named pipe,
|
||||
// not a file. We might need a length-prefixed or newline-terminated format for IPC.
|
||||
let mut bytes = vec![];
|
||||
self.inner
|
||||
.read_to_end(&mut bytes)
|
||||
.await
|
||||
.map_err(Error::ServerCommunications)?;
|
||||
let bytes = Secret::new(bytes);
|
||||
|
||||
self.inner.disconnect().ok();
|
||||
|
||||
tracing::debug!("Server read");
|
||||
let s = SecretString::from_str(
|
||||
std::str::from_utf8(bytes.expose_secret()).map_err(Error::LinkNotUtf8)?,
|
||||
)
|
||||
.expect("Infallible");
|
||||
let url = Secret::new(SecureUrl::from_url(url::Url::parse(s.expose_secret())?));
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a deep link by sending it to the already-running instance of the app
|
||||
pub async fn open(url: &url::Url) -> Result<(), Error> {
|
||||
let path = named_pipe_path(BUNDLE_ID);
|
||||
let mut client = named_pipe::ClientOptions::new()
|
||||
.open(path)
|
||||
.map_err(Error::Connect)?;
|
||||
client
|
||||
.write_all(url.as_str().as_bytes())
|
||||
.await
|
||||
.map_err(Error::ClientCommunications)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Registers the current exe as the handler for our deep link scheme.
|
||||
///
|
||||
/// This is copied almost verbatim from tauri-plugin-deep-link's `register` fn, with an improvement
|
||||
/// that we send the deep link to a subcommand so the URL won't confuse `clap`
|
||||
pub fn register() -> Result<(), Error> {
|
||||
let exe = tauri_utils::platform::current_exe()
|
||||
.map_err(Error::CurrentExe)?
|
||||
.display()
|
||||
.to_string()
|
||||
.replace("\\\\?\\", "");
|
||||
|
||||
set_registry_values(BUNDLE_ID, &exe).map_err(Error::WindowsRegistry)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set up the Windows registry to call the given exe when our deep link scheme is used
|
||||
///
|
||||
/// All errors from this function are registry-related
|
||||
fn set_registry_values(id: &str, exe: &str) -> Result<(), io::Error> {
|
||||
let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
|
||||
let base = Path::new("Software").join("Classes").join(FZ_SCHEME);
|
||||
|
||||
let (key, _) = hkcu.create_subkey(&base)?;
|
||||
key.set_value("", &format!("URL:{}", id))?;
|
||||
key.set_value("URL Protocol", &"")?;
|
||||
|
||||
let (icon, _) = hkcu.create_subkey(base.join("DefaultIcon"))?;
|
||||
icon.set_value("", &format!("{},0", &exe))?;
|
||||
|
||||
let (cmd, _) = hkcu.create_subkey(base.join("shell").join("open").join("command"))?;
|
||||
cmd.set_value("", &format!("{} open-deep-link \"%1\"", &exe))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a valid name for a Windows named pipe
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - BUNDLE_ID, e.g. `dev.firezone.client`
|
||||
fn named_pipe_path(id: &str) -> String {
|
||||
format!(r"\\.\pipe\{}", id)
|
||||
}
|
||||
@@ -1,39 +1,75 @@
|
||||
use crate::client::wintun_install;
|
||||
use std::str::FromStr;
|
||||
pub(crate) use imp::{check, elevate};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[error("couldn't install wintun.dll")]
|
||||
DllInstall(#[from] wintun_install::Error),
|
||||
#[error("couldn't load wintun.dll")]
|
||||
DllLoad,
|
||||
#[error("UUID parse error - This should be impossible since the UUID is hard-coded")]
|
||||
Uuid,
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
use anyhow::Result;
|
||||
|
||||
/// Creates a bogus wintun tunnel to check whether we have permissions to create wintun tunnels.
|
||||
/// Extracts wintun.dll if needed.
|
||||
///
|
||||
/// Returns true if already elevated, false if not elevated, error if we can't be sure
|
||||
pub(crate) fn check() -> Result<bool, Error> {
|
||||
// Almost the same as the code in tun_windows.rs in connlib
|
||||
const TUNNEL_UUID: &str = "72228ef4-cb84-4ca5-a4e6-3f8636e75757";
|
||||
const TUNNEL_NAME: &str = "Firezone Elevation Check";
|
||||
|
||||
let path = match wintun_install::ensure_dll() {
|
||||
Ok(x) => x,
|
||||
Err(wintun_install::Error::PermissionDenied) => return Ok(false),
|
||||
Err(e) => return Err(Error::DllInstall(e)),
|
||||
};
|
||||
|
||||
// SAFETY: Unsafe needed because we're loading a DLL from disk and it has arbitrary C code in it.
|
||||
// `wintun_install::ensure_dll` checks the hash before we get here. This protects against accidental corruption, but not against attacks. (Because of TOCTOU)
|
||||
let wintun = unsafe { wintun::load_from_path(path) }.map_err(|_| Error::DllLoad)?;
|
||||
let uuid = uuid::Uuid::from_str(TUNNEL_UUID).map_err(|_| Error::Uuid)?;
|
||||
|
||||
// Wintun hides the exact Windows error, so let's assume the only way Adapter::create can fail is if we're not elevated.
|
||||
if wintun::Adapter::create(&wintun, "Firezone", TUNNEL_NAME, Some(uuid.as_u128())).is_err() {
|
||||
return Ok(false);
|
||||
pub(crate) fn check() -> Result<bool> {
|
||||
// TODO
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn elevate() -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
use crate::client::wintun_install;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{os::windows::process::CommandExt, str::FromStr};
|
||||
|
||||
/// Check if we have elevated privileges, extract wintun.dll if needed.
|
||||
///
|
||||
/// Returns true if already elevated, false if not elevated, error if we can't be sure
|
||||
pub(crate) fn check() -> Result<bool> {
|
||||
// Almost the same as the code in tun_windows.rs in connlib
|
||||
const TUNNEL_UUID: &str = "72228ef4-cb84-4ca5-a4e6-3f8636e75757";
|
||||
const TUNNEL_NAME: &str = "Firezone Elevation Check";
|
||||
|
||||
let path = match wintun_install::ensure_dll() {
|
||||
Ok(x) => x,
|
||||
Err(wintun_install::Error::PermissionDenied) => return Ok(false),
|
||||
Err(e) => return Err(e).context("Failed to ensure wintun.dll is installed"),
|
||||
};
|
||||
|
||||
// SAFETY: Unsafe needed because we're loading a DLL from disk and it has arbitrary C code in it.
|
||||
// `wintun_install::ensure_dll` checks the hash before we get here. This protects against accidental corruption, but not against attacks. (Because of TOCTOU)
|
||||
let wintun =
|
||||
unsafe { wintun::load_from_path(path) }.context("Failed to load wintun.dll")?;
|
||||
let uuid =
|
||||
uuid::Uuid::from_str(TUNNEL_UUID).context("Impossible: Hard-coded UUID is invalid")?;
|
||||
|
||||
// Wintun hides the exact Windows error, so let's assume the only way Adapter::create can fail is if we're not elevated.
|
||||
if wintun::Adapter::create(&wintun, "Firezone", TUNNEL_NAME, Some(uuid.as_u128())).is_err()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn elevate() -> Result<()> {
|
||||
// Hides Powershell's console on Windows
|
||||
// <https://stackoverflow.com/questions/59692146/is-it-possible-to-use-the-standard-library-to-spawn-a-process-without-showing-th#60958956>
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
let current_exe = tauri_utils::platform::current_exe()?;
|
||||
if current_exe.display().to_string().contains('\"') {
|
||||
anyhow::bail!("The exe path must not contain double quotes, it makes it hard to elevate with Powershell");
|
||||
}
|
||||
std::process::Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("-Command")
|
||||
.arg("Start-Process")
|
||||
.arg("-FilePath")
|
||||
.arg(format!(r#""{}""#, current_exe.display()))
|
||||
.arg("-Verb")
|
||||
.arg("RunAs")
|
||||
.arg("-ArgumentList")
|
||||
.arg("elevated")
|
||||
.spawn()
|
||||
.context("Failed to elevate ourselves with `RunAs`")?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// TODO: `git grep` for unwraps before 1.0, especially this gui module <https://github.com/firezone/firezone/issues/3521>
|
||||
|
||||
use crate::client::{
|
||||
self, about, deep_link, known_dirs, logging, network_changes,
|
||||
self, about, deep_link, logging, network_changes,
|
||||
settings::{self, AdvancedSettings},
|
||||
Failure,
|
||||
};
|
||||
@@ -21,6 +21,14 @@ use ControllerRequest as Req;
|
||||
|
||||
mod system_tray_menu;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "gui/os_linux.rs"]
|
||||
mod os;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "gui/os_windows.rs"]
|
||||
mod os;
|
||||
|
||||
/// The Windows client doesn't use platform APIs to detect network connectivity changes,
|
||||
/// so we rely on connlib to do so. We have valid use cases for headless Windows clients
|
||||
/// (IoT devices, point-of-sale devices, etc), so try to reconnect for 30 days if there's
|
||||
@@ -53,6 +61,7 @@ impl Managed {
|
||||
}
|
||||
|
||||
// TODO: Replace with `anyhow` gradually per <https://github.com/firezone/firezone/pull/3546#discussion_r1477114789>
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error(r#"Couldn't show clickable notification titled "{0}""#)]
|
||||
@@ -303,9 +312,8 @@ async fn smoke_test(ctlr_tx: CtlrTx) -> Result<()> {
|
||||
settings::apply_advanced_settings_inner(&settings::AdvancedSettings::default()).await?;
|
||||
|
||||
// Test log exporting
|
||||
let path = known_dirs::session()
|
||||
.context("`known_dirs::session` failed during smoke test")?
|
||||
.join("smoke_test_log_export.zip");
|
||||
let path = PathBuf::from("smoke_test_log_export.zip");
|
||||
|
||||
let stem = "connlib-smoke-test".into();
|
||||
match tokio::fs::remove_file(&path).await {
|
||||
Ok(()) => {}
|
||||
@@ -585,7 +593,7 @@ impl Controller {
|
||||
Req::DisconnectedTokenExpired => {
|
||||
tracing::info!("Token expired");
|
||||
self.sign_out()?;
|
||||
show_notification(
|
||||
os::show_notification(
|
||||
"Firezone disconnected",
|
||||
"To access resources, sign in again.",
|
||||
)?;
|
||||
@@ -644,7 +652,7 @@ impl Controller {
|
||||
self.tunnel_ready = true;
|
||||
self.refresh_system_tray_menu()?;
|
||||
|
||||
show_notification(
|
||||
os::show_notification(
|
||||
"Firezone connected",
|
||||
"You are now signed in and able to access resources.",
|
||||
)?;
|
||||
@@ -655,7 +663,7 @@ impl Controller {
|
||||
// We don't need to route through the controller here either, we could
|
||||
// use the `open` crate directly instead of Tauri's wrapper
|
||||
// `tauri::api::shell::open`
|
||||
show_clickable_notification(
|
||||
os::show_clickable_notification(
|
||||
&title,
|
||||
"Click here to download the new version.",
|
||||
self.ctlr_tx.clone(),
|
||||
@@ -831,62 +839,3 @@ async fn run_controller(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show a notification in the bottom right of the screen
|
||||
///
|
||||
/// May say "Windows Powershell" and have the wrong icon in dev mode
|
||||
/// See <https://github.com/tauri-apps/tauri/issues/3700>
|
||||
fn show_notification(title: &str, body: &str) -> Result<(), Error> {
|
||||
tauri_winrt_notification::Toast::new(BUNDLE_ID)
|
||||
.title(title)
|
||||
.text1(body)
|
||||
.show()
|
||||
.map_err(|_| Error::Notification(title.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show a notification that signals `Controller` when clicked
|
||||
///
|
||||
/// May say "Windows Powershell" and have the wrong icon in dev mode
|
||||
/// See <https://github.com/tauri-apps/tauri/issues/3700>
|
||||
///
|
||||
/// Known issue: If the notification times out and goes into the notification center
|
||||
/// (the little thing that pops up when you click the bell icon), then we may not get the
|
||||
/// click signal.
|
||||
///
|
||||
/// I've seen this reported by people using Powershell, C#, etc., so I think it might
|
||||
/// be a Windows bug?
|
||||
/// - <https://superuser.com/questions/1488763/windows-10-notifications-not-activating-the-associated-app-when-clicking-on-it>
|
||||
/// - <https://stackoverflow.com/questions/65835196/windows-toast-notification-com-not-working>
|
||||
/// - <https://answers.microsoft.com/en-us/windows/forum/all/notifications-not-activating-the-associated-app/7a3b31b0-3a20-4426-9c88-c6e3f2ac62c6>
|
||||
///
|
||||
/// Firefox doesn't have this problem. Maybe they're using a different API.
|
||||
fn show_clickable_notification(
|
||||
title: &str,
|
||||
body: &str,
|
||||
tx: CtlrTx,
|
||||
req: ControllerRequest,
|
||||
) -> Result<(), Error> {
|
||||
// For some reason `on_activated` is FnMut
|
||||
let mut req = Some(req);
|
||||
|
||||
tauri_winrt_notification::Toast::new(BUNDLE_ID)
|
||||
.title(title)
|
||||
.text1(body)
|
||||
.scenario(tauri_winrt_notification::Scenario::Reminder)
|
||||
.on_activated(move || {
|
||||
if let Some(req) = req.take() {
|
||||
if let Err(error) = tx.blocking_send(req) {
|
||||
tracing::error!(
|
||||
?error,
|
||||
"User clicked on notification, but we couldn't tell `Controller`"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.show()
|
||||
.map_err(|_| Error::ClickableNotification(title.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
18
rust/gui-client/src-tauri/src/client/gui/os_linux.rs
Normal file
18
rust/gui-client/src-tauri/src/client/gui/os_linux.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::{ControllerRequest, CtlrTx, Error};
|
||||
|
||||
/// Show a notification in the bottom right of the screen
|
||||
pub(crate) fn show_notification(_title: &str, _body: &str) -> Result<(), Error> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show a notification that signals `Controller` when clicked
|
||||
pub(crate) fn show_clickable_notification(
|
||||
_title: &str,
|
||||
_body: &str,
|
||||
_tx: CtlrTx,
|
||||
_req: ControllerRequest,
|
||||
) -> Result<(), Error> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
61
rust/gui-client/src-tauri/src/client/gui/os_windows.rs
Normal file
61
rust/gui-client/src-tauri/src/client/gui/os_windows.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use super::{ControllerRequest, CtlrTx, Error};
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
|
||||
/// Show a notification in the bottom right of the screen
|
||||
///
|
||||
/// May say "Windows Powershell" and have the wrong icon in dev mode
|
||||
/// See <https://github.com/tauri-apps/tauri/issues/3700>
|
||||
pub(crate) fn show_notification(title: &str, body: &str) -> Result<(), Error> {
|
||||
tauri_winrt_notification::Toast::new(BUNDLE_ID)
|
||||
.title(title)
|
||||
.text1(body)
|
||||
.show()
|
||||
.map_err(|_| Error::Notification(title.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show a notification that signals `Controller` when clicked
|
||||
///
|
||||
/// May say "Windows Powershell" and have the wrong icon in dev mode
|
||||
/// See <https://github.com/tauri-apps/tauri/issues/3700>
|
||||
///
|
||||
/// Known issue: If the notification times out and goes into the notification center
|
||||
/// (the little thing that pops up when you click the bell icon), then we may not get the
|
||||
/// click signal.
|
||||
///
|
||||
/// I've seen this reported by people using Powershell, C#, etc., so I think it might
|
||||
/// be a Windows bug?
|
||||
/// - <https://superuser.com/questions/1488763/windows-10-notifications-not-activating-the-associated-app-when-clicking-on-it>
|
||||
/// - <https://stackoverflow.com/questions/65835196/windows-toast-notification-com-not-working>
|
||||
/// - <https://answers.microsoft.com/en-us/windows/forum/all/notifications-not-activating-the-associated-app/7a3b31b0-3a20-4426-9c88-c6e3f2ac62c6>
|
||||
///
|
||||
/// Firefox doesn't have this problem. Maybe they're using a different API.
|
||||
pub(crate) fn show_clickable_notification(
|
||||
title: &str,
|
||||
body: &str,
|
||||
tx: CtlrTx,
|
||||
req: ControllerRequest,
|
||||
) -> Result<(), Error> {
|
||||
// For some reason `on_activated` is FnMut
|
||||
let mut req = Some(req);
|
||||
|
||||
tauri_winrt_notification::Toast::new(BUNDLE_ID)
|
||||
.title(title)
|
||||
.text1(body)
|
||||
.scenario(tauri_winrt_notification::Scenario::Reminder)
|
||||
.on_activated(move || {
|
||||
if let Some(req) = req.take() {
|
||||
if let Err(error) = tx.blocking_send(req) {
|
||||
tracing::error!(
|
||||
?error,
|
||||
"User clicked on notification, but we couldn't tell `Controller`"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.show()
|
||||
.map_err(|_| Error::ClickableNotification(title.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,355 +1,9 @@
|
||||
//! A module for getting callbacks from Windows when we gain / lose Internet connectivity
|
||||
//!
|
||||
//! # Latency
|
||||
//!
|
||||
//! 2 or 3 seconds for the user clicking "Connect" or "Disconnect" on Wi-Fi,
|
||||
//! or for plugging or unplugging an Ethernet cable.
|
||||
//!
|
||||
//! Plugging in Ethernet may take longer since it waits on DHCP.
|
||||
//! Connecting to Wi-Fi usually notifies while Windows is showing the progress bar
|
||||
//! in the Wi-Fi menu.
|
||||
//!
|
||||
//! DNS server changes are (TODO <https://github.com/firezone/firezone/issues/3343>)
|
||||
//!
|
||||
//! # Worker thread
|
||||
//!
|
||||
//! `Listener` must live in a worker thread if used from Tauri.
|
||||
//! `
|
||||
//! This is because both `Listener` and some feature in Tauri (maybe drag-and-drop) depend on COM.
|
||||
//! `Listener` works fine if we initialize COM with COINIT_MULTITHREADED, but
|
||||
//! Tauri initializes COM some other way.
|
||||
//!
|
||||
//! In the debug command we don't need a worker thread because we're the only code
|
||||
//! in the process using COM.
|
||||
//!
|
||||
//! I tried disabling file drag-and-drop in tauri.conf.json, that didn't work:
|
||||
//! - <https://github.com/tauri-apps/tauri/commit/e0e49d87a200d3681d39ff2dd80bee5d408c943e>
|
||||
//! - <https://tauri.app/v1/api/js/window/#filedropenabled>
|
||||
//! - <https://tauri.app/v1/api/config/#windowconfig>
|
||||
//!
|
||||
//! There is some explanation of the COM threading stuff in MSDN here:
|
||||
//! - <https://learn.microsoft.com/en-us/windows/win32/api/objbase/ne-objbase-coinit>
|
||||
//! - <https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex>
|
||||
//!
|
||||
//! Raymond Chen also explains it on his blog: <https://devblogs.microsoft.com/oldnewthing/20191125-00/?p=103135>
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "network_changes/linux.rs"]
|
||||
mod imp;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::{runtime::Runtime, sync::Notify};
|
||||
use windows::{
|
||||
core::{ComInterface, Result as WinResult, GUID},
|
||||
Win32::{
|
||||
Networking::NetworkListManager::{
|
||||
INetworkEvents, INetworkEvents_Impl, INetworkListManager, NetworkListManager,
|
||||
NLM_CONNECTIVITY, NLM_NETWORK_PROPERTY_CHANGE,
|
||||
},
|
||||
System::Com,
|
||||
},
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "network_changes/windows.rs"]
|
||||
mod imp;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[error("Couldn't initialize COM: {0}")]
|
||||
ComInitialize(windows::core::Error),
|
||||
#[error("Couldn't stop worker thread")]
|
||||
CouldntStopWorkerThread,
|
||||
#[error("Couldn't creat NetworkListManager")]
|
||||
CreateNetworkListManager(windows::core::Error),
|
||||
#[error("Couldn't start listening to network events: {0}")]
|
||||
Listening(windows::core::Error),
|
||||
#[error("Couldn't stop listening to network events: {0}")]
|
||||
Unadvise(windows::core::Error),
|
||||
}
|
||||
|
||||
/// Debug subcommand to test network connectivity events
|
||||
pub(crate) fn run_debug() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Returns Err before COM is initialized
|
||||
assert!(get_apartment_type().is_err());
|
||||
|
||||
let com_worker = Worker::new()?;
|
||||
|
||||
// We have to initialize COM again for the main thread. This doesn't
|
||||
// seem to be a problem in the main app since Tauri initializes COM for itself.
|
||||
let _guard = ComGuard::new();
|
||||
|
||||
assert_eq!(
|
||||
get_apartment_type(),
|
||||
Ok((Com::APTTYPE_MTA, Com::APTTYPEQUALIFIER_NONE))
|
||||
);
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
tracing::info!("Listening for network events...");
|
||||
|
||||
rt.block_on(async move {
|
||||
loop {
|
||||
com_worker.notified().await;
|
||||
// Make sure whatever Tokio thread we're on is associated with COM
|
||||
// somehow.
|
||||
assert_eq!(
|
||||
get_apartment_type()?,
|
||||
(Com::APTTYPE_MTA, Com::APTTYPEQUALIFIER_NONE)
|
||||
);
|
||||
|
||||
tracing::info!(have_internet = %check_internet()?);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if Windows thinks we have Internet access per [IsConnectedToInternet](https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/nf-netlistmgr-inetworklistmanager-get_isconnectedtointernet)
|
||||
///
|
||||
/// Call this when `Listener` notifies you.
|
||||
pub fn check_internet() -> WinResult<bool> {
|
||||
// Retrieving the INetworkListManager takes less than half a millisecond, and this
|
||||
// makes the lifetimes and Send+Sync much simpler for callers, so just retrieve it
|
||||
// every single time.
|
||||
// SAFETY: No lifetime problems. TODO: Could threading be a problem?
|
||||
// I think in practice we'll never call this from two threads, but what if we did?
|
||||
// Maybe make it a method on a `!Send + !Sync` guard struct?
|
||||
let network_list_manager: INetworkListManager =
|
||||
unsafe { Com::CoCreateInstance(&NetworkListManager, None, Com::CLSCTX_ALL) }?;
|
||||
// SAFETY: `network_list_manager` isn't shared between threads, and the lifetime
|
||||
// should be good.
|
||||
let have_internet = unsafe { network_list_manager.IsConnectedToInternet() }?.as_bool();
|
||||
|
||||
Ok(have_internet)
|
||||
}
|
||||
|
||||
/// Worker thread that can be joined explicitly, and joins on Drop
|
||||
pub(crate) struct Worker {
|
||||
inner: Option<WorkerInner>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
/// Needed so that `Drop` can consume the oneshot Sender and the thread's JoinHandle
|
||||
struct WorkerInner {
|
||||
thread: std::thread::JoinHandle<Result<(), Error>>,
|
||||
stopper: tokio::sync::oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn new() -> std::io::Result<Self> {
|
||||
let notify = Arc::new(Notify::new());
|
||||
|
||||
let (stopper, rx) = tokio::sync::oneshot::channel();
|
||||
let thread = {
|
||||
let notify = Arc::clone(¬ify);
|
||||
std::thread::Builder::new()
|
||||
.name("Firezone COM worker".into())
|
||||
.spawn(move || {
|
||||
{
|
||||
let com = ComGuard::new()?;
|
||||
let _network_change_listener = Listener::new(&com, notify)?;
|
||||
rx.blocking_recv().ok();
|
||||
}
|
||||
tracing::debug!("COM worker thread shut down gracefully");
|
||||
Ok(())
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner: Some(WorkerInner { thread, stopper }),
|
||||
notify,
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as `drop`, but you can catch errors
|
||||
pub fn close(&mut self) -> Result<(), Error> {
|
||||
if let Some(inner) = self.inner.take() {
|
||||
inner
|
||||
.stopper
|
||||
.send(())
|
||||
.map_err(|_| Error::CouldntStopWorkerThread)?;
|
||||
match inner.thread.join() {
|
||||
Err(e) => std::panic::resume_unwind(e),
|
||||
Ok(x) => x?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn notified(&self) {
|
||||
self.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Worker {
|
||||
fn drop(&mut self) {
|
||||
self.close()
|
||||
.expect("should be able to close Worker cleanly");
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces the initialize-use-uninitialize order for `Listener` and COM
|
||||
///
|
||||
/// COM is meant to be initialized for a thread, and un-initialized for the same thread,
|
||||
/// so don't pass this between threads.
|
||||
struct ComGuard {
|
||||
dropped: bool,
|
||||
_unsend_unsync: PhantomUnsendUnsync,
|
||||
}
|
||||
|
||||
/// Marks a type as !Send and !Sync without nightly / unstable features
|
||||
///
|
||||
/// <https://stackoverflow.com/questions/62713667/how-to-implement-send-or-sync-for-a-type>
|
||||
type PhantomUnsendUnsync = std::marker::PhantomData<*const ()>;
|
||||
|
||||
impl ComGuard {
|
||||
/// Initialize a "Multi-threaded apartment" so that Windows COM stuff
|
||||
/// can be called, and COM callbacks can work.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
// SAFETY: Threading shouldn't be a problem since this is meant to initialize
|
||||
// COM per-thread anyway.
|
||||
unsafe { Com::CoInitializeEx(None, Com::COINIT_MULTITHREADED) }
|
||||
.map_err(Error::ComInitialize)?;
|
||||
Ok(Self {
|
||||
dropped: false,
|
||||
_unsend_unsync: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ComGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.dropped {
|
||||
self.dropped = true;
|
||||
// Required, per [CoInitializeEx docs](https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex#remarks)
|
||||
// Safety: Make sure all the COM objects are dropped before we call
|
||||
// CoUninitialize or the program might segfault.
|
||||
unsafe { Com::CoUninitialize() };
|
||||
tracing::debug!("Uninitialized COM");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listens to network connectivity change eents
|
||||
pub(crate) struct Listener<'a> {
|
||||
/// The cookies we get back from `Advise`. Can be None if the owner called `close`
|
||||
///
|
||||
/// This has to be mutable because we have to hook up the callbacks during
|
||||
/// Listener's constructor
|
||||
advise_cookie_net: Option<u32>,
|
||||
cxn_point_net: Com::IConnectionPoint,
|
||||
|
||||
inner: ListenerInner,
|
||||
|
||||
/// Hold a reference to a `ComGuard` to enforce the right init-use-uninit order
|
||||
_com: &'a ComGuard,
|
||||
}
|
||||
|
||||
/// This must be separate because we need to `Clone` that `Notify` and we can't
|
||||
/// `Clone` the COM objects in `Listener`
|
||||
// https://kennykerr.ca/rust-getting-started/how-to-implement-com-interface.html
|
||||
#[windows_implement::implement(INetworkEvents)]
|
||||
#[derive(Clone)]
|
||||
struct ListenerInner {
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl<'a> Drop for Listener<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.close().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Listener<'a> {
|
||||
/// Creates a new Listener
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `com` - Makes sure that CoInitializeEx was called. `com` have been created
|
||||
/// on the same thread as `new` is called on.
|
||||
/// * `notify` - A Tokio `Notify` that will be notified when Windows detects
|
||||
/// connectivity changes. Some notifications may be spurious.
|
||||
fn new(com: &'a ComGuard, notify: Arc<Notify>) -> Result<Self, Error> {
|
||||
// `windows-rs` automatically releases (de-refs) COM objects on Drop:
|
||||
// https://github.com/microsoft/windows-rs/issues/2123#issuecomment-1293194755
|
||||
// https://github.com/microsoft/windows-rs/blob/cefdabd15e4a7a7f71b7a2d8b12d5dc148c99adb/crates/samples/windows/wmi/src/main.rs#L22
|
||||
// SAFETY: TODO
|
||||
let network_list_manager: INetworkListManager =
|
||||
unsafe { Com::CoCreateInstance(&NetworkListManager, None, Com::CLSCTX_ALL) }
|
||||
.map_err(Error::CreateNetworkListManager)?;
|
||||
let cpc: Com::IConnectionPointContainer =
|
||||
network_list_manager.cast().map_err(Error::Listening)?;
|
||||
// SAFETY: TODO
|
||||
let cxn_point_net =
|
||||
unsafe { cpc.FindConnectionPoint(&INetworkEvents::IID) }.map_err(Error::Listening)?;
|
||||
|
||||
let mut this = Listener {
|
||||
advise_cookie_net: None,
|
||||
cxn_point_net,
|
||||
inner: ListenerInner { notify },
|
||||
_com: com,
|
||||
};
|
||||
|
||||
let callbacks: INetworkEvents = this.inner.clone().into();
|
||||
|
||||
// SAFETY: What happens if Windows sends us a network change event while
|
||||
// we're dropping Listener?
|
||||
// Is it safe to Advise on `this` and then immediately move it?
|
||||
this.advise_cookie_net =
|
||||
Some(unsafe { this.cxn_point_net.Advise(&callbacks) }.map_err(Error::Listening)?);
|
||||
|
||||
// After we call `Advise`, notify. This should avoid a problem if this happens:
|
||||
//
|
||||
// 1. Caller spawns a worker thread for Listener, but the worker thread isn't scheduled
|
||||
// 2. Caller continues setup, checks Internet is connected
|
||||
// 3. Internet gets disconnected but caller isn't notified
|
||||
// 4. Worker thread finally gets scheduled, but we never notify that the Internet was lost during setup. Caller is now out of sync with ground truth.
|
||||
this.inner.notify.notify_one();
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Like `drop` but you can catch errors
|
||||
///
|
||||
/// Unregisters the network change callbacks
|
||||
pub fn close(&mut self) -> Result<(), Error> {
|
||||
if let Some(cookie) = self.advise_cookie_net.take() {
|
||||
// SAFETY: I don't see any memory safety issues.
|
||||
unsafe { self.cxn_point_net.Unadvise(cookie) }.map_err(Error::Unadvise)?;
|
||||
tracing::debug!("Unadvised");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl INetworkEvents_Impl for ListenerInner {
|
||||
fn NetworkAdded(&self, _networkid: &GUID) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkDeleted(&self, _networkid: &GUID) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkConnectivityChanged(
|
||||
&self,
|
||||
_networkid: &GUID,
|
||||
_newconnectivity: NLM_CONNECTIVITY,
|
||||
) -> WinResult<()> {
|
||||
self.notify.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkPropertyChanged(
|
||||
&self,
|
||||
_networkid: &GUID,
|
||||
_flags: NLM_NETWORK_PROPERTY_CHANGE,
|
||||
) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks what COM apartment the current thread is in. For debugging only.
|
||||
fn get_apartment_type() -> WinResult<(Com::APTTYPE, Com::APTTYPEQUALIFIER)> {
|
||||
let mut apt_type = Com::APTTYPE_CURRENT;
|
||||
let mut apt_qualifier = Com::APTTYPEQUALIFIER_NONE;
|
||||
|
||||
// SAFETY: We just created the variables, and they're out parameters,
|
||||
// so Windows shouldn't store the pointers.
|
||||
unsafe { Com::CoGetApartmentType(&mut apt_type, &mut apt_qualifier) }?;
|
||||
Ok((apt_type, apt_qualifier))
|
||||
}
|
||||
pub(crate) use imp::{check_internet, run_debug, Worker};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Not implemented for Linux yet
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum Error {}
|
||||
|
||||
pub(crate) fn run_debug() -> Result<()> {
|
||||
tracing::warn!("network_changes not implemented yet on Linux");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TODO: Implement for Linux
|
||||
pub(crate) fn check_internet() -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) struct Worker {}
|
||||
|
||||
impl Worker {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
pub(crate) fn close(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn notified(&self) {
|
||||
futures::future::pending().await
|
||||
}
|
||||
}
|
||||
355
rust/gui-client/src-tauri/src/client/network_changes/windows.rs
Normal file
355
rust/gui-client/src-tauri/src/client/network_changes/windows.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
//! A module for getting callbacks from Windows when we gain / lose Internet connectivity
|
||||
//!
|
||||
//! # Latency
|
||||
//!
|
||||
//! 2 or 3 seconds for the user clicking "Connect" or "Disconnect" on Wi-Fi,
|
||||
//! or for plugging or unplugging an Ethernet cable.
|
||||
//!
|
||||
//! Plugging in Ethernet may take longer since it waits on DHCP.
|
||||
//! Connecting to Wi-Fi usually notifies while Windows is showing the progress bar
|
||||
//! in the Wi-Fi menu.
|
||||
//!
|
||||
//! DNS server changes are (TODO <https://github.com/firezone/firezone/issues/3343>)
|
||||
//!
|
||||
//! # Worker thread
|
||||
//!
|
||||
//! `Listener` must live in a worker thread if used from Tauri.
|
||||
//! `
|
||||
//! This is because both `Listener` and some feature in Tauri (maybe drag-and-drop) depend on COM.
|
||||
//! `Listener` works fine if we initialize COM with COINIT_MULTITHREADED, but
|
||||
//! Tauri initializes COM some other way.
|
||||
//!
|
||||
//! In the debug command we don't need a worker thread because we're the only code
|
||||
//! in the process using COM.
|
||||
//!
|
||||
//! I tried disabling file drag-and-drop in tauri.conf.json, that didn't work:
|
||||
//! - <https://github.com/tauri-apps/tauri/commit/e0e49d87a200d3681d39ff2dd80bee5d408c943e>
|
||||
//! - <https://tauri.app/v1/api/js/window/#filedropenabled>
|
||||
//! - <https://tauri.app/v1/api/config/#windowconfig>
|
||||
//!
|
||||
//! There is some explanation of the COM threading stuff in MSDN here:
|
||||
//! - <https://learn.microsoft.com/en-us/windows/win32/api/objbase/ne-objbase-coinit>
|
||||
//! - <https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex>
|
||||
//!
|
||||
//! Raymond Chen also explains it on his blog: <https://devblogs.microsoft.com/oldnewthing/20191125-00/?p=103135>
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::{runtime::Runtime, sync::Notify};
|
||||
use windows::{
|
||||
core::{ComInterface, Result as WinResult, GUID},
|
||||
Win32::{
|
||||
Networking::NetworkListManager::{
|
||||
INetworkEvents, INetworkEvents_Impl, INetworkListManager, NetworkListManager,
|
||||
NLM_CONNECTIVITY, NLM_NETWORK_PROPERTY_CHANGE,
|
||||
},
|
||||
System::Com,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[error("Couldn't initialize COM: {0}")]
|
||||
ComInitialize(windows::core::Error),
|
||||
#[error("Couldn't stop worker thread")]
|
||||
CouldntStopWorkerThread,
|
||||
#[error("Couldn't creat NetworkListManager")]
|
||||
CreateNetworkListManager(windows::core::Error),
|
||||
#[error("Couldn't start listening to network events: {0}")]
|
||||
Listening(windows::core::Error),
|
||||
#[error("Couldn't stop listening to network events: {0}")]
|
||||
Unadvise(windows::core::Error),
|
||||
}
|
||||
|
||||
/// Debug subcommand to test network connectivity events
|
||||
pub(crate) fn run_debug() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Returns Err before COM is initialized
|
||||
assert!(get_apartment_type().is_err());
|
||||
|
||||
let com_worker = Worker::new()?;
|
||||
|
||||
// We have to initialize COM again for the main thread. This doesn't
|
||||
// seem to be a problem in the main app since Tauri initializes COM for itself.
|
||||
let _guard = ComGuard::new();
|
||||
|
||||
assert_eq!(
|
||||
get_apartment_type(),
|
||||
Ok((Com::APTTYPE_MTA, Com::APTTYPEQUALIFIER_NONE))
|
||||
);
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
tracing::info!("Listening for network events...");
|
||||
|
||||
rt.block_on(async move {
|
||||
loop {
|
||||
com_worker.notified().await;
|
||||
// Make sure whatever Tokio thread we're on is associated with COM
|
||||
// somehow.
|
||||
assert_eq!(
|
||||
get_apartment_type()?,
|
||||
(Com::APTTYPE_MTA, Com::APTTYPEQUALIFIER_NONE)
|
||||
);
|
||||
|
||||
tracing::info!(have_internet = %check_internet()?);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if Windows thinks we have Internet access per [IsConnectedToInternet](https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/nf-netlistmgr-inetworklistmanager-get_isconnectedtointernet)
|
||||
///
|
||||
/// Call this when `Listener` notifies you.
|
||||
pub fn check_internet() -> Result<bool> {
|
||||
// Retrieving the INetworkListManager takes less than half a millisecond, and this
|
||||
// makes the lifetimes and Send+Sync much simpler for callers, so just retrieve it
|
||||
// every single time.
|
||||
// SAFETY: No lifetime problems. TODO: Could threading be a problem?
|
||||
// I think in practice we'll never call this from two threads, but what if we did?
|
||||
// Maybe make it a method on a `!Send + !Sync` guard struct?
|
||||
let network_list_manager: INetworkListManager =
|
||||
unsafe { Com::CoCreateInstance(&NetworkListManager, None, Com::CLSCTX_ALL) }?;
|
||||
// SAFETY: `network_list_manager` isn't shared between threads, and the lifetime
|
||||
// should be good.
|
||||
let have_internet = unsafe { network_list_manager.IsConnectedToInternet() }?.as_bool();
|
||||
|
||||
Ok(have_internet)
|
||||
}
|
||||
|
||||
/// Worker thread that can be joined explicitly, and joins on Drop
|
||||
pub(crate) struct Worker {
|
||||
inner: Option<WorkerInner>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
/// Needed so that `Drop` can consume the oneshot Sender and the thread's JoinHandle
|
||||
struct WorkerInner {
|
||||
thread: std::thread::JoinHandle<Result<(), Error>>,
|
||||
stopper: tokio::sync::oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let notify = Arc::new(Notify::new());
|
||||
|
||||
let (stopper, rx) = tokio::sync::oneshot::channel();
|
||||
let thread = {
|
||||
let notify = Arc::clone(¬ify);
|
||||
std::thread::Builder::new()
|
||||
.name("Firezone COM worker".into())
|
||||
.spawn(move || {
|
||||
{
|
||||
let com = ComGuard::new()?;
|
||||
let _network_change_listener = Listener::new(&com, notify)?;
|
||||
rx.blocking_recv().ok();
|
||||
}
|
||||
tracing::debug!("COM worker thread shut down gracefully");
|
||||
Ok(())
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner: Some(WorkerInner { thread, stopper }),
|
||||
notify,
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as `drop`, but you can catch errors
|
||||
pub(crate) fn close(&mut self) -> Result<()> {
|
||||
if let Some(inner) = self.inner.take() {
|
||||
inner
|
||||
.stopper
|
||||
.send(())
|
||||
.map_err(|_| Error::CouldntStopWorkerThread)?;
|
||||
match inner.thread.join() {
|
||||
Err(e) => std::panic::resume_unwind(e),
|
||||
Ok(x) => x?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn notified(&self) {
|
||||
self.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Worker {
|
||||
fn drop(&mut self) {
|
||||
self.close()
|
||||
.expect("should be able to close Worker cleanly");
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces the initialize-use-uninitialize order for `Listener` and COM
|
||||
///
|
||||
/// COM is meant to be initialized for a thread, and un-initialized for the same thread,
|
||||
/// so don't pass this between threads.
|
||||
struct ComGuard {
|
||||
dropped: bool,
|
||||
_unsend_unsync: PhantomUnsendUnsync,
|
||||
}
|
||||
|
||||
/// Marks a type as !Send and !Sync without nightly / unstable features
|
||||
///
|
||||
/// <https://stackoverflow.com/questions/62713667/how-to-implement-send-or-sync-for-a-type>
|
||||
type PhantomUnsendUnsync = std::marker::PhantomData<*const ()>;
|
||||
|
||||
impl ComGuard {
|
||||
/// Initialize a "Multi-threaded apartment" so that Windows COM stuff
|
||||
/// can be called, and COM callbacks can work.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
// SAFETY: Threading shouldn't be a problem since this is meant to initialize
|
||||
// COM per-thread anyway.
|
||||
unsafe { Com::CoInitializeEx(None, Com::COINIT_MULTITHREADED) }
|
||||
.map_err(Error::ComInitialize)?;
|
||||
Ok(Self {
|
||||
dropped: false,
|
||||
_unsend_unsync: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ComGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.dropped {
|
||||
self.dropped = true;
|
||||
// Required, per [CoInitializeEx docs](https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex#remarks)
|
||||
// Safety: Make sure all the COM objects are dropped before we call
|
||||
// CoUninitialize or the program might segfault.
|
||||
unsafe { Com::CoUninitialize() };
|
||||
tracing::debug!("Uninitialized COM");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listens to network connectivity change eents
|
||||
struct Listener<'a> {
|
||||
/// The cookies we get back from `Advise`. Can be None if the owner called `close`
|
||||
///
|
||||
/// This has to be mutable because we have to hook up the callbacks during
|
||||
/// Listener's constructor
|
||||
advise_cookie_net: Option<u32>,
|
||||
cxn_point_net: Com::IConnectionPoint,
|
||||
|
||||
inner: ListenerInner,
|
||||
|
||||
/// Hold a reference to a `ComGuard` to enforce the right init-use-uninit order
|
||||
_com: &'a ComGuard,
|
||||
}
|
||||
|
||||
/// This must be separate because we need to `Clone` that `Notify` and we can't
|
||||
/// `Clone` the COM objects in `Listener`
|
||||
// https://kennykerr.ca/rust-getting-started/how-to-implement-com-interface.html
|
||||
#[windows_implement::implement(INetworkEvents)]
|
||||
#[derive(Clone)]
|
||||
struct ListenerInner {
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl<'a> Drop for Listener<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.close().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Listener<'a> {
|
||||
/// Creates a new Listener
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `com` - Makes sure that CoInitializeEx was called. `com` have been created
|
||||
/// on the same thread as `new` is called on.
|
||||
/// * `notify` - A Tokio `Notify` that will be notified when Windows detects
|
||||
/// connectivity changes. Some notifications may be spurious.
|
||||
fn new(com: &'a ComGuard, notify: Arc<Notify>) -> Result<Self, Error> {
|
||||
// `windows-rs` automatically releases (de-refs) COM objects on Drop:
|
||||
// https://github.com/microsoft/windows-rs/issues/2123#issuecomment-1293194755
|
||||
// https://github.com/microsoft/windows-rs/blob/cefdabd15e4a7a7f71b7a2d8b12d5dc148c99adb/crates/samples/windows/wmi/src/main.rs#L22
|
||||
// SAFETY: TODO
|
||||
let network_list_manager: INetworkListManager =
|
||||
unsafe { Com::CoCreateInstance(&NetworkListManager, None, Com::CLSCTX_ALL) }
|
||||
.map_err(Error::CreateNetworkListManager)?;
|
||||
let cpc: Com::IConnectionPointContainer =
|
||||
network_list_manager.cast().map_err(Error::Listening)?;
|
||||
// SAFETY: TODO
|
||||
let cxn_point_net =
|
||||
unsafe { cpc.FindConnectionPoint(&INetworkEvents::IID) }.map_err(Error::Listening)?;
|
||||
|
||||
let mut this = Listener {
|
||||
advise_cookie_net: None,
|
||||
cxn_point_net,
|
||||
inner: ListenerInner { notify },
|
||||
_com: com,
|
||||
};
|
||||
|
||||
let callbacks: INetworkEvents = this.inner.clone().into();
|
||||
|
||||
// SAFETY: What happens if Windows sends us a network change event while
|
||||
// we're dropping Listener?
|
||||
// Is it safe to Advise on `this` and then immediately move it?
|
||||
this.advise_cookie_net =
|
||||
Some(unsafe { this.cxn_point_net.Advise(&callbacks) }.map_err(Error::Listening)?);
|
||||
|
||||
// After we call `Advise`, notify. This should avoid a problem if this happens:
|
||||
//
|
||||
// 1. Caller spawns a worker thread for Listener, but the worker thread isn't scheduled
|
||||
// 2. Caller continues setup, checks Internet is connected
|
||||
// 3. Internet gets disconnected but caller isn't notified
|
||||
// 4. Worker thread finally gets scheduled, but we never notify that the Internet was lost during setup. Caller is now out of sync with ground truth.
|
||||
this.inner.notify.notify_one();
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Like `drop` but you can catch errors
|
||||
///
|
||||
/// Unregisters the network change callbacks
|
||||
pub fn close(&mut self) -> Result<(), Error> {
|
||||
if let Some(cookie) = self.advise_cookie_net.take() {
|
||||
// SAFETY: I don't see any memory safety issues.
|
||||
unsafe { self.cxn_point_net.Unadvise(cookie) }.map_err(Error::Unadvise)?;
|
||||
tracing::debug!("Unadvised");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl INetworkEvents_Impl for ListenerInner {
|
||||
fn NetworkAdded(&self, _networkid: &GUID) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkDeleted(&self, _networkid: &GUID) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkConnectivityChanged(
|
||||
&self,
|
||||
_networkid: &GUID,
|
||||
_newconnectivity: NLM_CONNECTIVITY,
|
||||
) -> WinResult<()> {
|
||||
self.notify.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn NetworkPropertyChanged(
|
||||
&self,
|
||||
_networkid: &GUID,
|
||||
_flags: NLM_NETWORK_PROPERTY_CHANGE,
|
||||
) -> WinResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks what COM apartment the current thread is in. For debugging only.
|
||||
fn get_apartment_type() -> WinResult<(Com::APTTYPE, Com::APTTYPEQUALIFIER)> {
|
||||
let mut apt_type = Com::APTTYPE_CURRENT;
|
||||
let mut apt_qualifier = Com::APTTYPEQUALIFIER_NONE;
|
||||
|
||||
// SAFETY: We just created the variables, and they're out parameters,
|
||||
// so Windows shouldn't store the pointers.
|
||||
unsafe { Com::CoGetApartmentType(&mut apt_type, &mut apt_qualifier) }?;
|
||||
Ok((apt_type, apt_qualifier))
|
||||
}
|
||||
@@ -5,9 +5,16 @@ use std::net::IpAddr;
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("can't get system DNS resolvers: {0}")]
|
||||
#[cfg(target_os = "windows")]
|
||||
CantGetResolvers(#[from] ipconfig::error::Error),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn get() -> Result<Vec<IpAddr>, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get() -> Result<Vec<IpAddr>, Error> {
|
||||
Ok(ipconfig::get_adapters()?
|
||||
.iter()
|
||||
|
||||
@@ -7,16 +7,16 @@ fn main() -> anyhow::Result<()> {
|
||||
client::run()
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
#[cfg(target_os = "linux")]
|
||||
mod client;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod client {
|
||||
pub(crate) fn run() -> anyhow::Result<()> {
|
||||
println!("The Windows client does not compile on non-Windows platforms yet");
|
||||
println!("The GUI client does not compile on macOS yet");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything is hidden inside the `client` module so that we can exempt the
|
||||
/// Windows client from static analysis on other platforms where it would throw
|
||||
/// compile errors.
|
||||
#[cfg(target_os = "windows")]
|
||||
mod client;
|
||||
|
||||
Reference in New Issue
Block a user