mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
feat(windows-client): run the GUI and tunnel in separate processes (#4978)
Ready for review. Closes #3712. Supersedes #4940. Refs #4963. I haven't figured out if it needs any new automated tests (unit, integration, etc.) but the code itself is ready for review. There is more refactoring that could be done, or could be left for later. ```[tasklist] - [x] Move wintun setup from GUI to IPC service / headless client - [x] Make sure the device ID is in a sensible place - [x] Export IPC service logs in the zips - [x] Test GUI + SC IPC service on Windows (f4db808919a passed) - [x] Make sure IPC service does not busy-loop - [x] Test un-install checklist for Windows - [x] Test upgrade checklist for Windows - [x] Test GUI + systemd IPC service on Linux (c4ab7e7 passed) - [x] Test upgrade checklist for Linux - [x] Test un-install checklist for Linux - [x] Make sure the IPC service logs out and deactivates DNS control if the GUI crashes - [x] Test network changing - [x] (it's intended behavior) ~~Look into spurious `on_update_resources` (fad86babd7)~~ - [x] ~~Test max partition time on offline laptop~~ (I ended up just setting a 30-day default in the code) - [x] Make sure headless Client does not busy-loop - [x] Test standalone headless on Linux - [ ] Add unit / integration tests - [ ] Think about security a bit #3971 ``` --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com> Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
4
rust/Cargo.lock
generated
4
rust/Cargo.lock
generated
@@ -1944,7 +1944,6 @@ dependencies = [
|
||||
"output_vt100",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"sadness-generator",
|
||||
"secrecy",
|
||||
"semver",
|
||||
@@ -1990,15 +1989,18 @@ dependencies = [
|
||||
"known-folders",
|
||||
"nix 0.28.0",
|
||||
"resolv-conf",
|
||||
"ring",
|
||||
"sd-notify",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"windows 0.56.0",
|
||||
"windows-service",
|
||||
]
|
||||
|
||||
|
||||
@@ -193,7 +193,8 @@ mod tests {
|
||||
async fn device_windows() {
|
||||
// Install wintun so the test can run
|
||||
// CI only needs x86_64 for now
|
||||
let wintun_bytes = include_bytes!("../../../../gui-client/wintun/bin/amd64/wintun.dll");
|
||||
let wintun_bytes =
|
||||
include_bytes!("../../../../headless-client/src/windows/wintun/bin/amd64/wintun.dll");
|
||||
let wintun_path = connlib_shared::windows::wintun_dll_path().unwrap();
|
||||
tokio::fs::create_dir_all(wintun_path.parent().unwrap())
|
||||
.await
|
||||
|
||||
@@ -32,7 +32,6 @@ native-dialog = "0.7.0"
|
||||
output_vt100 = "0.1"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false, features = ["stream", "rustls-tls"] }
|
||||
ring = "0.17"
|
||||
sadness-generator = "0.5.0"
|
||||
secrecy = { workspace = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -44,6 +43,7 @@ tauri-runtime = "0.14.2"
|
||||
tauri-utils = "1.5.3"
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
tokio = { version = "1.36.0", features = ["signal", "time", "macros", "rt", "rt-multi-thread"] }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-log = "0.2"
|
||||
tracing-panic = "0.1.2"
|
||||
@@ -55,7 +55,6 @@ zip = { version = "1.2.3", features = ["deflate", "time"], default-features = fa
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dirs = "5.0.1"
|
||||
nix = { version = "0.28.0", features = ["user"] }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -74,15 +73,11 @@ features = [
|
||||
"Win32_Foundation",
|
||||
# For listening for network change events
|
||||
"Win32_Networking_NetworkListManager",
|
||||
# For deep_link module
|
||||
"Win32_Security",
|
||||
# COM is needed to listen for network change events
|
||||
"Win32_System_Com",
|
||||
# Needed to listen for system DNS changes
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_Threading",
|
||||
# For deep_link module
|
||||
"Win32_System_SystemServices",
|
||||
]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() -> anyhow::Result<()> {
|
||||
firezone_headless_client::imp::run_only_ipc_service()
|
||||
firezone_headless_client::run_only_ipc_service()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Args, Parser};
|
||||
use firezone_headless_client::FIREZONE_GROUP;
|
||||
use std::path::PathBuf;
|
||||
@@ -10,6 +10,7 @@ mod debug_commands;
|
||||
mod deep_link;
|
||||
mod elevation;
|
||||
mod gui;
|
||||
mod ipc;
|
||||
mod logging;
|
||||
mod network_changes;
|
||||
mod resolvers;
|
||||
@@ -18,9 +19,6 @@ mod updates;
|
||||
mod uptime;
|
||||
mod welcome;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wintun_install;
|
||||
|
||||
/// Output of `git describe` at compile time
|
||||
/// e.g. `1.0.0-pre.4-20-ged5437c88-modified` where:
|
||||
///
|
||||
@@ -66,14 +64,10 @@ pub(crate) fn run() -> Result<()> {
|
||||
|
||||
match cli.command {
|
||||
None => {
|
||||
match elevation::check() {
|
||||
// We're already elevated, just run the GUI
|
||||
match elevation::is_normal_user() {
|
||||
// Our elevation is correct (not elevated), just run the GUI
|
||||
Ok(true) => run_gui(cli),
|
||||
Ok(false) => {
|
||||
// We're not elevated, ask Powershell to re-launch us, then exit. On Linux this is completely different.
|
||||
elevation::elevate()?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(false) => bail!("The GUI should run as a normal user, not elevated"),
|
||||
Err(error) => {
|
||||
show_error_dialog(&error)?;
|
||||
Err(error.into())
|
||||
@@ -92,8 +86,7 @@ pub(crate) fn run() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Some(Cmd::SmokeTest) => {
|
||||
// Check for elevation. This also ensures wintun.dll is installed.
|
||||
if !elevation::check()? {
|
||||
if !elevation::is_normal_user()? {
|
||||
anyhow::bail!("`smoke-test` failed its elevation check");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn run(cmd: Cmd) -> Result<()> {
|
||||
}
|
||||
|
||||
fn check_for_updates() -> Result<()> {
|
||||
client::logging::debug_command_setup()?;
|
||||
firezone_headless_client::debug_command_setup()?;
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let version = rt.block_on(client::updates::check())?;
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Server {
|
||||
}
|
||||
|
||||
pub(crate) async fn open(url: &url::Url) -> Result<()> {
|
||||
crate::client::logging::debug_command_setup()?;
|
||||
firezone_headless_client::debug_command_setup()?;
|
||||
|
||||
let path = sock_path()?;
|
||||
let mut stream = UnixStream::connect(&path).await?;
|
||||
|
||||
@@ -5,9 +5,8 @@ use super::FZ_SCHEME;
|
||||
use anyhow::{Context, Result};
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
use secrecy::Secret;
|
||||
use std::{ffi::c_void, io, path::Path};
|
||||
use std::{io, path::Path};
|
||||
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
|
||||
@@ -32,40 +31,8 @@ impl Server {
|
||||
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,
|
||||
)
|
||||
.context("InitializeSecurityDescriptor failed")?;
|
||||
WinSec::SetSecurityDescriptorDacl(psd, true, None, false)
|
||||
.context("SetSecurityDescriptorDacl failed")?;
|
||||
}
|
||||
|
||||
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) }
|
||||
let server = server_options
|
||||
.create(pipe_path())
|
||||
.map_err(|_| super::Error::CantListen)?;
|
||||
|
||||
tracing::debug!("server is bound");
|
||||
@@ -101,9 +68,8 @@ impl Server {
|
||||
|
||||
/// Open a deep link by sending it to the already-running instance of the app
|
||||
pub async fn open(url: &url::Url) -> Result<()> {
|
||||
let path = named_pipe_path(BUNDLE_ID);
|
||||
let mut client = named_pipe::ClientOptions::new()
|
||||
.open(path)
|
||||
.open(pipe_path())
|
||||
.context("Couldn't connect to named pipe server")?;
|
||||
client
|
||||
.write_all(url.as_str().as_bytes())
|
||||
@@ -112,6 +78,10 @@ pub async fn open(url: &url::Url) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pipe_path() -> String {
|
||||
firezone_headless_client::platform::named_pipe_path(&format!("{BUNDLE_ID}.deep_link"))
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -147,23 +117,3 @@ fn set_registry_values(id: &str, exe: &str) -> Result<(), io::Error> {
|
||||
|
||||
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 {
|
||||
#[test]
|
||||
fn named_pipe_path() {
|
||||
assert_eq!(
|
||||
super::named_pipe_path("dev.firezone.client"),
|
||||
r"\\.\pipe\dev.firezone.client"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
pub(crate) use imp::{check, elevate};
|
||||
pub(crate) use imp::is_normal_user;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
use crate::client::gui::Error;
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Context;
|
||||
|
||||
/// Returns true if we're running without root privileges
|
||||
///
|
||||
/// On Linux we've already switched to IPC, so the process must NOT be elevated
|
||||
#[allow(clippy::print_stderr)]
|
||||
pub(crate) fn check() -> Result<bool, Error> {
|
||||
pub(crate) fn is_normal_user() -> anyhow::Result<bool, Error> {
|
||||
// Must use `eprintln` here because `tracing` won't be initialized yet.
|
||||
let user = std::env::var("USER").context("USER env var should be set")?;
|
||||
if user == "root" {
|
||||
@@ -14,7 +17,7 @@ mod imp {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let fz_gid = firezone_headless_client::imp::firezone_group()?.gid;
|
||||
let fz_gid = firezone_headless_client::platform::firezone_group()?.gid;
|
||||
let groups = nix::unistd::getgroups().context("`nix::unistd::getgroups`")?;
|
||||
if !groups.contains(&fz_gid) {
|
||||
return Err(Error::UserNotInFirezoneGroup);
|
||||
@@ -22,87 +25,29 @@ mod imp {
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn elevate() -> Result<()> {
|
||||
anyhow::bail!("Firezone must not elevate on Linux.");
|
||||
}
|
||||
}
|
||||
|
||||
// Stub only
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use anyhow::Result;
|
||||
|
||||
pub(crate) fn check() -> Result<bool, crate::client::gui::Error> {
|
||||
/// Placeholder for cargo check on macOS
|
||||
pub(crate) fn is_normal_user() -> anyhow::Result<bool, crate::client::gui::Error> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn elevate() -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
use crate::client::{gui::Error, wintun_install};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{os::windows::process::CommandExt, str::FromStr};
|
||||
use crate::client::gui::Error;
|
||||
|
||||
/// Check if we have elevated privileges, extract wintun.dll if needed.
|
||||
// Returns true on Windows
|
||||
///
|
||||
/// 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(e)
|
||||
.context("Failed to ensure wintun.dll is installed")
|
||||
.map_err(Error::Other)
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
/// On Windows we are switching to IPC, and checking for elevation is complicated,
|
||||
/// so it just always returns true. The Windows GUI does work correctly even if
|
||||
/// elevated, so we should warn users that it doesn't need elevation, but it's
|
||||
/// not a show-stopper if they accidentally "Run as admin".
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn is_normal_user() -> anyhow::Result<bool, Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
//! The real macOS Client is in `swift/apple`
|
||||
|
||||
use crate::client::{
|
||||
self, about, deep_link, logging, network_changes,
|
||||
self, about, deep_link,
|
||||
ipc::{self, CallbackHandler},
|
||||
logging, network_changes,
|
||||
settings::{self, AdvancedSettings},
|
||||
Failure,
|
||||
};
|
||||
@@ -16,7 +18,6 @@ use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration};
|
||||
use system_tray_menu::Event as TrayMenuEvent;
|
||||
use tauri::{Manager, SystemTray, SystemTrayEvent};
|
||||
use tokio::sync::{mpsc, oneshot, Notify};
|
||||
use tunnel_wrapper::CallbackHandler;
|
||||
use url::Url;
|
||||
|
||||
use ControllerRequest as Req;
|
||||
@@ -39,21 +40,6 @@ mod os;
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
mod os;
|
||||
|
||||
// This syntax is odd, but it helps `cargo-mutants` understand the platform-specific modules
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "tunnel-wrapper/in_proc.rs"]
|
||||
mod tunnel_wrapper_in_proc;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "tunnel-wrapper/ipc.rs"]
|
||||
mod tunnel_wrapper_ipc;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use tunnel_wrapper_in_proc as tunnel_wrapper;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use tunnel_wrapper_ipc as tunnel_wrapper;
|
||||
|
||||
pub(crate) type CtlrTx = mpsc::Sender<ControllerRequest>;
|
||||
|
||||
/// All managed state that we might need to access from odd places like Tauri commands.
|
||||
@@ -495,7 +481,7 @@ struct Controller {
|
||||
/// Everything related to a signed-in user session
|
||||
struct Session {
|
||||
callback_handler: CallbackHandler,
|
||||
connlib: tunnel_wrapper::TunnelWrapper,
|
||||
connlib: ipc::Client,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
@@ -518,7 +504,7 @@ impl Controller {
|
||||
"Calling connlib Session::connect"
|
||||
);
|
||||
|
||||
let mut connlib = tunnel_wrapper::connect(
|
||||
let mut connlib = ipc::Client::connect(
|
||||
api_url.as_str(),
|
||||
token,
|
||||
callback_handler.clone(),
|
||||
|
||||
76
rust/gui-client/src-tauri/src/client/ipc.rs
Normal file
76
rust/gui-client/src-tauri/src/client/ipc.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::client::gui::{ControllerRequest, CtlrTx};
|
||||
use anyhow::{Context as _, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use connlib_client_shared::callbacks::ResourceDescription;
|
||||
use firezone_headless_client::IpcClientMsg;
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
pub(crate) use platform::Client;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "ipc/linux.rs"]
|
||||
mod platform;
|
||||
|
||||
// Stub only
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "ipc/macos.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "ipc/windows.rs"]
|
||||
mod platform;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CallbackHandler {
|
||||
pub notify_controller: Arc<Notify>,
|
||||
pub ctlr_tx: CtlrTx,
|
||||
pub resources: Arc<ArcSwap<Vec<ResourceDescription>>>,
|
||||
}
|
||||
|
||||
// Callbacks must all be non-blocking
|
||||
impl connlib_client_shared::Callbacks for CallbackHandler {
|
||||
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
|
||||
// The errors don't implement `Serialize`, so we don't get a machine-readable
|
||||
// error here, but we should consider it an error anyway. `on_disconnect`
|
||||
// is always an error
|
||||
tracing::error!("on_disconnect {error:?}");
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::Disconnected)
|
||||
.expect("controller channel failed");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(&self, _: Ipv4Addr, _: Ipv6Addr, _: Vec<IpAddr>) -> Option<i32> {
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::TunnelReady)
|
||||
.expect("controller channel failed");
|
||||
None
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<ResourceDescription>) {
|
||||
tracing::debug!("on_update_resources");
|
||||
self.resources.store(resources.into());
|
||||
self.notify_controller.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub(crate) async fn reconnect(&mut self) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::Reconnect)
|
||||
.await
|
||||
.context("Couldn't send Reconnect")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tell connlib about the system's default resolvers
|
||||
pub(crate) async fn set_dns(&mut self, dns: Vec<IpAddr>) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::SetDns(dns))
|
||||
.await
|
||||
.context("Couldn't send SetDns")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
82
rust/gui-client/src-tauri/src/client/ipc/linux.rs
Normal file
82
rust/gui-client/src-tauri/src/client/ipc/linux.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use connlib_client_shared::Callbacks;
|
||||
use firezone_headless_client::{platform::sock_path, IpcClientMsg, IpcServerMsg};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use tokio::net::{unix::OwnedWriteHalf, UnixStream};
|
||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||
|
||||
/// Forwards events to and from connlib
|
||||
pub(crate) struct Client {
|
||||
recv_task: tokio::task::JoinHandle<Result<()>>,
|
||||
tx: FramedWrite<OwnedWriteHalf, LengthDelimitedCodec>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub(crate) async fn disconnect(mut self) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::Disconnect)
|
||||
.await
|
||||
.context("Couldn't send Disconnect")?;
|
||||
self.tx.close().await?;
|
||||
self.recv_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn send_msg(&mut self, msg: &IpcClientMsg) -> Result<()> {
|
||||
self.tx
|
||||
.send(
|
||||
serde_json::to_string(msg)
|
||||
.context("Couldn't encode IPC message as JSON")?
|
||||
.into(),
|
||||
)
|
||||
.await
|
||||
.context("Couldn't send IPC message")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn connect(
|
||||
api_url: &str,
|
||||
token: SecretString,
|
||||
callback_handler: super::CallbackHandler,
|
||||
tokio_handle: tokio::runtime::Handle,
|
||||
) -> Result<Self> {
|
||||
tracing::info!(pid = std::process::id(), "Connecting to IPC service...");
|
||||
let stream = UnixStream::connect(sock_path())
|
||||
.await
|
||||
.context("Couldn't connect to UDS")?;
|
||||
let (rx, tx) = stream.into_split();
|
||||
// Receives messages from the IPC service
|
||||
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
|
||||
let tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
|
||||
// TODO: Make sure this joins / drops somewhere
|
||||
let recv_task = tokio_handle.spawn(async move {
|
||||
while let Some(msg) = rx.next().await.transpose()? {
|
||||
let msg: IpcServerMsg = serde_json::from_slice(&msg)?;
|
||||
match msg {
|
||||
IpcServerMsg::Ok => {}
|
||||
IpcServerMsg::OnDisconnect => callback_handler.on_disconnect(
|
||||
&connlib_client_shared::Error::Other("errors can't be serialized"),
|
||||
),
|
||||
IpcServerMsg::OnUpdateResources(v) => callback_handler.on_update_resources(v),
|
||||
IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
callback_handler.on_set_interface_config(ipv4, ipv6, dns);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut client = Self { recv_task, tx };
|
||||
let token = token.expose_secret().clone();
|
||||
client
|
||||
.send_msg(&IpcClientMsg::Connect {
|
||||
api_url: api_url.to_string(),
|
||||
token,
|
||||
})
|
||||
.await
|
||||
.context("Couldn't send Connect message")?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
26
rust/gui-client/src-tauri/src/client/ipc/macos.rs
Normal file
26
rust/gui-client/src-tauri/src/client/ipc/macos.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use firezone_headless_client::IpcClientMsg;
|
||||
use secrecy::SecretString;
|
||||
|
||||
pub(crate) struct Client {}
|
||||
|
||||
impl Client {
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn disconnect(self) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn send_msg(&mut self, _msg: &IpcClientMsg) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) async fn connect(
|
||||
_api_url: &str,
|
||||
_token: SecretString,
|
||||
_callback_handler: super::CallbackHandler,
|
||||
_tokio_handle: tokio::runtime::Handle,
|
||||
) -> Result<Self> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
117
rust/gui-client/src-tauri/src/client/ipc/windows.rs
Normal file
117
rust/gui-client/src-tauri/src/client/ipc/windows.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use connlib_client_shared::Callbacks;
|
||||
use firezone_headless_client::{IpcClientMsg, IpcServerMsg};
|
||||
use futures::{SinkExt, Stream};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use std::{pin::pin, task::Poll};
|
||||
use tokio::{net::windows::named_pipe, sync::mpsc};
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
|
||||
pub(crate) struct Client {
|
||||
task: tokio::task::JoinHandle<Result<()>>,
|
||||
// Needed temporarily to avoid a big refactor. We can remove this in the future.
|
||||
tx: mpsc::Sender<String>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub(crate) async fn disconnect(mut self) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::Disconnect)
|
||||
.await
|
||||
.context("Couldn't send Disconnect")?;
|
||||
self.task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn send_msg(&mut self, msg: &IpcClientMsg) -> Result<()> {
|
||||
self.tx
|
||||
.send(serde_json::to_string(msg).context("Couldn't encode IPC message as JSON")?)
|
||||
.await
|
||||
.context("Couldn't send IPC message")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn connect(
|
||||
api_url: &str,
|
||||
token: SecretString,
|
||||
callback_handler: super::CallbackHandler,
|
||||
tokio_handle: tokio::runtime::Handle,
|
||||
) -> Result<Self> {
|
||||
tracing::info!(pid = std::process::id(), "Connecting to IPC service...");
|
||||
let ipc = named_pipe::ClientOptions::new()
|
||||
.open(firezone_headless_client::windows::pipe_path())
|
||||
.context("Couldn't connect to named pipe server")?;
|
||||
let ipc = Framed::new(ipc, LengthDelimitedCodec::new());
|
||||
// This channel allows us to communicate with the GUI even though NamedPipeClient
|
||||
// doesn't have `into_split`.
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio_handle.spawn(async move {
|
||||
let mut ipc = pin!(ipc);
|
||||
loop {
|
||||
let ev = std::future::poll_fn(|cx| {
|
||||
match rx.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => return Poll::Ready(Ok(IpcEvent::Gui(msg))),
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(Err(anyhow!("MPSC channel from GUI closed")))
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match ipc.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(msg)) => {
|
||||
let msg = serde_json::from_slice(&msg?)?;
|
||||
return Poll::Ready(Ok(IpcEvent::Connlib(msg)));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(Err(anyhow!("IPC service disconnected from us")))
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
})
|
||||
.await;
|
||||
|
||||
match ev {
|
||||
Ok(IpcEvent::Gui(msg)) => ipc.send(msg.into()).await?,
|
||||
Ok(IpcEvent::Connlib(msg)) => match msg {
|
||||
IpcServerMsg::Ok => {}
|
||||
IpcServerMsg::OnDisconnect => callback_handler.on_disconnect(
|
||||
&connlib_client_shared::Error::Other("errors can't be serialized"),
|
||||
),
|
||||
IpcServerMsg::OnUpdateResources(v) => {
|
||||
callback_handler.on_update_resources(v)
|
||||
}
|
||||
IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
callback_handler.on_set_interface_config(ipv4, ipv6, dns);
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "Error while waiting for IPC tx/rx");
|
||||
// TODO: Catch that error when the task is joined
|
||||
Err(error)?
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut client = Self { task, tx };
|
||||
let token = token.expose_secret().clone();
|
||||
client
|
||||
.send_msg(&IpcClientMsg::Connect {
|
||||
api_url: api_url.to_string(),
|
||||
token,
|
||||
})
|
||||
.await
|
||||
.context("Couldn't send Connect message")?;
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
enum IpcEvent {
|
||||
/// The GUI wants to send a message to the service
|
||||
Gui(String),
|
||||
/// The connlib instance in the service wants to send a message to the GUI
|
||||
Connlib(IpcServerMsg),
|
||||
}
|
||||
@@ -49,7 +49,7 @@ pub(crate) enum Error {
|
||||
|
||||
/// Set up logs after the process has started
|
||||
pub(crate) fn setup(log_filter: &str) -> Result<Handles> {
|
||||
let log_path = app_log_path()?.src;
|
||||
let log_path = known_dirs::logs().context("Can't compute app log dir")?;
|
||||
|
||||
std::fs::create_dir_all(&log_path).map_err(Error::CreateDirAll)?;
|
||||
let (layer, logger) = file_logger::layer(&log_path);
|
||||
@@ -73,17 +73,6 @@ pub(crate) fn setup(log_filter: &str) -> Result<Handles> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets up logging for stderr only, with INFO level by default
|
||||
pub(crate) fn debug_command_setup() -> Result<(), Error> {
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
let layer = fmt::layer().with_filter(filter);
|
||||
let subscriber = Registry::default().with(layer);
|
||||
set_global_default(subscriber)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn clear_logs() -> StdResult<(), String> {
|
||||
clear_logs_inner().await.map_err(|e| e.to_string())
|
||||
@@ -221,34 +210,16 @@ async fn count_one_dir(path: &Path) -> Result<FileCount> {
|
||||
Ok(file_count)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn log_paths() -> Result<Vec<LogPath>> {
|
||||
Ok(vec![
|
||||
LogPath {
|
||||
// TODO: This is magic, it must match the systemd file
|
||||
src: PathBuf::from("/var/log").join(connlib_shared::BUNDLE_ID),
|
||||
src: firezone_headless_client::known_dirs::ipc_service_logs()
|
||||
.context("Can't compute IPC service logs dir")?,
|
||||
dst: PathBuf::from("connlib"),
|
||||
},
|
||||
app_log_path()?,
|
||||
LogPath {
|
||||
src: known_dirs::logs().context("Can't compute app log dir")?,
|
||||
dst: PathBuf::from("app"),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/// Windows doesn't have separate connlib logs until #3712 merges
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn log_paths() -> Result<Vec<LogPath>> {
|
||||
Ok(vec![app_log_path()?])
|
||||
}
|
||||
|
||||
/// Log dir for just the GUI app
|
||||
///
|
||||
/// e.g. `$HOME/.cache/dev.firezone.client/data/logs`
|
||||
/// or `%LOCALAPPDATA%/dev.firezone.client/data/logs`
|
||||
///
|
||||
/// On Windows this also happens to contain the connlib logs,
|
||||
/// until #3712 merges
|
||||
fn app_log_path() -> Result<LogPath> {
|
||||
Ok(LogPath {
|
||||
src: known_dirs::logs().context("Can't compute app log dir")?,
|
||||
dst: PathBuf::from("app"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ mod imp {
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn get() -> Result<Vec<IpAddr>> {
|
||||
firezone_headless_client::imp::get_system_default_resolvers_systemd_resolved()
|
||||
firezone_headless_client::platform::get_system_default_resolvers_systemd_resolved()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@ mod imp {
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn get() -> Result<Vec<IpAddr>> {
|
||||
firezone_headless_client::imp::system_resolvers()
|
||||
firezone_headless_client::platform::system_resolvers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Default for AdvancedSettings {
|
||||
Self {
|
||||
auth_base_url: Url::parse("https://app.firez.one").unwrap(),
|
||||
api_url: Url::parse("wss://api.firez.one").unwrap(),
|
||||
log_filter: "firezone_gui_client=debug,firezone_tunnel=trace,phoenix_channel=debug,connlib_shared=debug,connlib_client_shared=debug,boringtun=debug,snownet=debug,str0m=info,info".to_string(),
|
||||
log_filter: "firezone_gui_client=debug,info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ impl Default for AdvancedSettings {
|
||||
Self {
|
||||
auth_base_url: Url::parse("https://app.firezone.dev").unwrap(),
|
||||
api_url: Url::parse("wss://api.firezone.dev").unwrap(),
|
||||
log_filter: "str0m=warn,info".to_string(),
|
||||
log_filter: "info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
//! In-process wrapper for connlib
|
||||
//!
|
||||
//! This is used so that Windows can keep connlib in the GUI process a little longer,
|
||||
//! until the Linux process splitting is settled. Once both platforms are split,
|
||||
//! this should be deleted.
|
||||
//!
|
||||
//! With this module, the main GUI module should have no direct dependence on connlib.
|
||||
//! And some things in here will live in the tunnel process after the IPC split.
|
||||
//! (It's okay to depend on trivial things like `BUNDLE_ID`)
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use connlib_client_shared::Sockets;
|
||||
use connlib_shared::{callbacks::ResourceDescription, keypair, LoginUrl};
|
||||
use secrecy::SecretString;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use super::ControllerRequest;
|
||||
use super::CtlrTx;
|
||||
|
||||
/// 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
|
||||
/// been a partition.
|
||||
const MAX_PARTITION_TIME: Duration = Duration::from_secs(60 * 60 * 24 * 30);
|
||||
|
||||
// This will stay in the GUI process
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CallbackHandler {
|
||||
pub notify_controller: Arc<Notify>,
|
||||
pub ctlr_tx: CtlrTx,
|
||||
pub resources: Arc<ArcSwap<Vec<ResourceDescription>>>,
|
||||
}
|
||||
|
||||
/// Forwards events to and from connlib
|
||||
///
|
||||
/// In the `in_proc` module this is just a stub. The real purpose is to abstract
|
||||
/// over both in-proc connlib instances and connlib instances living in the tunnel
|
||||
/// process, across an IPC boundary.
|
||||
pub(crate) struct TunnelWrapper {
|
||||
session: connlib_client_shared::Session,
|
||||
}
|
||||
|
||||
impl TunnelWrapper {
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn disconnect(self) -> Result<()> {
|
||||
self.session.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn reconnect(&mut self) -> Result<()> {
|
||||
self.session.reconnect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn set_dns(&mut self, dns: Vec<IpAddr>) -> Result<()> {
|
||||
self.session.set_dns(dns);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts connlib in-process
|
||||
///
|
||||
/// This is `async` because the IPC version is async
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn connect(
|
||||
api_url: &str,
|
||||
token: SecretString,
|
||||
callback_handler: CallbackHandler,
|
||||
tokio_handle: tokio::runtime::Handle,
|
||||
) -> Result<TunnelWrapper> {
|
||||
// Device ID should be in the tunnel process
|
||||
let device_id =
|
||||
connlib_shared::device_id::get().context("Failed to read / create device ID")?;
|
||||
|
||||
// Private keys should be generated in the tunnel process
|
||||
let (private_key, public_key) = keypair();
|
||||
|
||||
let login = LoginUrl::client(api_url, &token, device_id.id, None, public_key.to_bytes())?;
|
||||
|
||||
// Deactivate DNS control since that can prevent us from bootstrapping a connection
|
||||
// to the portal. Maybe we could bring up a sentinel resolver before
|
||||
// connecting to the portal, but right now the portal seems to need system DNS
|
||||
// for the first connection.
|
||||
connlib_shared::deactivate_dns_control()?;
|
||||
|
||||
// All direct calls into connlib must be in the tunnel process
|
||||
let session = connlib_client_shared::Session::connect(
|
||||
login,
|
||||
Sockets::new(),
|
||||
private_key,
|
||||
None,
|
||||
callback_handler,
|
||||
Some(MAX_PARTITION_TIME),
|
||||
tokio_handle,
|
||||
);
|
||||
Ok(TunnelWrapper { session })
|
||||
}
|
||||
|
||||
// Callbacks must all be non-blocking
|
||||
impl connlib_client_shared::Callbacks for CallbackHandler {
|
||||
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
|
||||
tracing::debug!("on_disconnect {error:?}");
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::Disconnected)
|
||||
.expect("controller channel failed");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(&self, _: Ipv4Addr, _: Ipv6Addr, _: Vec<IpAddr>) -> Option<i32> {
|
||||
tracing::info!("on_set_interface_config");
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::TunnelReady)
|
||||
.expect("controller channel failed");
|
||||
None
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<ResourceDescription>) {
|
||||
tracing::debug!("on_update_resources");
|
||||
self.resources.store(resources.into());
|
||||
self.notify_controller.notify_one();
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use connlib_client_shared::Callbacks;
|
||||
use connlib_shared::callbacks::ResourceDescription;
|
||||
use firezone_headless_client::{imp::sock_path, IpcClientMsg, IpcServerMsg};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
net::{unix::OwnedWriteHalf, UnixStream},
|
||||
sync::Notify,
|
||||
};
|
||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||
|
||||
use super::ControllerRequest;
|
||||
use super::CtlrTx;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CallbackHandler {
|
||||
pub notify_controller: Arc<Notify>,
|
||||
pub ctlr_tx: CtlrTx,
|
||||
pub resources: Arc<ArcSwap<Vec<ResourceDescription>>>,
|
||||
}
|
||||
|
||||
/// Forwards events to and from connlib
|
||||
pub(crate) struct TunnelWrapper {
|
||||
recv_task: tokio::task::JoinHandle<Result<()>>,
|
||||
tx: FramedWrite<OwnedWriteHalf, LengthDelimitedCodec>,
|
||||
}
|
||||
|
||||
impl TunnelWrapper {
|
||||
pub(crate) async fn disconnect(mut self) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::Disconnect)
|
||||
.await
|
||||
.context("Couldn't send Disconnect")?;
|
||||
self.tx.close().await?;
|
||||
self.recv_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn reconnect(&mut self) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::Reconnect)
|
||||
.await
|
||||
.context("Couldn't send Reconnect")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tell connlib about the system's default resolvers
|
||||
///
|
||||
/// `dns` is passed as value because the in-proc impl needs that
|
||||
pub(crate) async fn set_dns(&mut self, dns: Vec<IpAddr>) -> Result<()> {
|
||||
self.send_msg(&IpcClientMsg::SetDns(dns))
|
||||
.await
|
||||
.context("Couldn't send SetDns")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_msg(&mut self, msg: &IpcClientMsg) -> Result<()> {
|
||||
self.tx
|
||||
.send(
|
||||
serde_json::to_string(msg)
|
||||
.context("Couldn't encode IPC message as JSON")?
|
||||
.into(),
|
||||
)
|
||||
.await
|
||||
.context("Couldn't send IPC message")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
api_url: &str,
|
||||
token: SecretString,
|
||||
callback_handler: CallbackHandler,
|
||||
tokio_handle: tokio::runtime::Handle,
|
||||
) -> Result<TunnelWrapper> {
|
||||
tracing::info!(pid = std::process::id(), "Connecting to IPC service...");
|
||||
let stream = UnixStream::connect(sock_path())
|
||||
.await
|
||||
.context("Couldn't connect to UDS")?;
|
||||
let (rx, tx) = stream.into_split();
|
||||
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
|
||||
let tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
|
||||
// TODO: Make sure this joins / drops somewhere
|
||||
let recv_task = tokio_handle.spawn(async move {
|
||||
while let Some(msg) = rx.next().await {
|
||||
let msg = msg?;
|
||||
let msg: IpcServerMsg = serde_json::from_slice(&msg)?;
|
||||
match msg {
|
||||
IpcServerMsg::Ok => {}
|
||||
IpcServerMsg::OnDisconnect => callback_handler.on_disconnect(
|
||||
&connlib_client_shared::Error::Other("errors can't be serialized"),
|
||||
),
|
||||
IpcServerMsg::OnUpdateResources(v) => callback_handler.on_update_resources(v),
|
||||
IpcServerMsg::TunnelReady => callback_handler.on_tunnel_ready(),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut client = TunnelWrapper { recv_task, tx };
|
||||
let token = token.expose_secret().clone();
|
||||
client
|
||||
.send_msg(&IpcClientMsg::Connect {
|
||||
api_url: api_url.to_string(),
|
||||
token,
|
||||
})
|
||||
.await
|
||||
.context("Couldn't send Connect message")?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
// Callbacks must all be non-blocking
|
||||
// TODO: DRY
|
||||
impl connlib_client_shared::Callbacks for CallbackHandler {
|
||||
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
|
||||
// The connlib error type cannot be serialized, but `on_disconnect` is
|
||||
// always an error, so at least log it as such on the GUI side.
|
||||
tracing::error!("on_disconnect {error:?}");
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::Disconnected)
|
||||
.expect("controller channel failed");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(&self, _: Ipv4Addr, _: Ipv6Addr, _: Vec<IpAddr>) -> Option<i32> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<ResourceDescription>) {
|
||||
tracing::debug!("on_update_resources");
|
||||
self.resources.store(resources.into());
|
||||
self.notify_controller.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl CallbackHandler {
|
||||
fn on_tunnel_ready(&self) {
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::TunnelReady)
|
||||
.expect("controller channel failed");
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
||||
connlib-client-shared = { workspace = true }
|
||||
connlib-shared = { workspace = true }
|
||||
firezone-cli-utils = { workspace = true }
|
||||
futures = "0.3.30"
|
||||
git-version = "0.3.9"
|
||||
humantime = "2.1"
|
||||
secrecy = { workspace = true }
|
||||
@@ -21,16 +22,16 @@ serde_json = "1.0.115"
|
||||
# This actually relies on many other features in Tokio, so this will probably
|
||||
# fail to build outside the workspace. <https://github.com/firezone/firezone/pull/4328#discussion_r1540342142>
|
||||
tokio = { version = "1.36.0", features = ["macros", "signal"] }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
url = { version = "2.3.1", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dirs = "5.0.1"
|
||||
futures = "0.3.30"
|
||||
nix = { version = "0.28.0", features = ["fs", "user"] }
|
||||
resolv-conf = "0.7.0"
|
||||
sd-notify = "0.4.1" # This is a pure Rust re-implementation, so it isn't vulnerable to CVE-2024-3094
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
dirs = "5.0.1"
|
||||
@@ -38,8 +39,19 @@ dirs = "5.0.1"
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
ipconfig = "0.3.2"
|
||||
known-folders = "1.1.0"
|
||||
ring = "0.17"
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
windows-service = "0.7.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.56.0"
|
||||
features = [
|
||||
# For named pipe IPC
|
||||
"Win32_Security",
|
||||
# For named pipe IPC
|
||||
"Win32_System_SystemServices",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
use crate::Cli;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use connlib_client_shared::file_logger;
|
||||
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,
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let sigint = tokio::signal::windows::ctrl_c()?;
|
||||
Ok(Self { sigint })
|
||||
}
|
||||
|
||||
pub(crate) fn poll(&mut self, cx: &mut Context) -> Poll<super::SignalKind> {
|
||||
if self.sigint.poll_recv(cx).is_ready() {
|
||||
return Poll::Ready(super::SignalKind::Interrupt);
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
// The return value is useful on Linux
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn check_token_permissions(_path: &Path) -> Result<()> {
|
||||
// TODO: Make sure the token is only readable by admin / our service user on Windows
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn default_token_path() -> std::path::PathBuf {
|
||||
// TODO: System-wide default token path for Windows
|
||||
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<()> {
|
||||
windows_service::service_dispatcher::start(SERVICE_NAME, ffi_service_run)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Generates `ffi_service_run` from `service_run`
|
||||
windows_service::define_windows_service!(ffi_service_run, windows_service_run);
|
||||
|
||||
fn windows_service_run(_arguments: Vec<OsString>) {
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
// Fixes <https://github.com/firezone/firezone/issues/4899>,
|
||||
// DNS rules persisting after reboot
|
||||
connlib_shared::deactivate_dns_control().ok();
|
||||
|
||||
// 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,
|
||||
})?;
|
||||
|
||||
if let Err(error) = run_ipc_service(cli, rt, shutdown_rx) {
|
||||
tracing::error!(?error, "error from run_ipc_service");
|
||||
}
|
||||
|
||||
// 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<Vec<IpAddr>> {
|
||||
let resolvers = ipconfig::get_adapters()?
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.dns_servers())
|
||||
.filter(|ip| match ip {
|
||||
IpAddr::V4(_) => true,
|
||||
// Filter out bogus DNS resolvers on my dev laptop that start with fec0:
|
||||
IpAddr::V6(ip) => !ip.octets().starts_with(&[0xfe, 0xc0]),
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
// This is private, so keep it at `debug` or `trace`
|
||||
tracing::debug!(?resolvers);
|
||||
Ok(resolvers)
|
||||
}
|
||||
@@ -7,13 +7,19 @@
|
||||
//!
|
||||
//! I wanted the ProgramData folder on Windows, which `dirs` alone doesn't provide.
|
||||
|
||||
pub use imp::{logs, runtime, session, settings};
|
||||
pub use platform::{ipc_service_logs, logs, runtime, session, settings};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub mod imp {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod platform {
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn ipc_service_logs() -> Option<PathBuf> {
|
||||
// TODO: This is magic, it must match the systemd file
|
||||
Some(PathBuf::from("/var/log").join(connlib_shared::BUNDLE_ID))
|
||||
}
|
||||
|
||||
/// e.g. `/home/alice/.cache/dev.firezone.client/data/logs`
|
||||
///
|
||||
/// Logs are considered cache because they're not configs and it's technically okay
|
||||
@@ -46,8 +52,31 @@ pub mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod platform {
|
||||
pub fn ipc_service_logs() -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn logs() -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn runtime() -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn session() -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn settings() -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod imp {
|
||||
pub mod platform {
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
use known_folders::{get_known_folder_path, KnownFolder};
|
||||
use std::path::PathBuf;
|
||||
@@ -113,7 +142,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn smoke() {
|
||||
for dir in [logs(), runtime(), session(), settings()] {
|
||||
for dir in [ipc_service_logs(), logs(), runtime(), session(), settings()] {
|
||||
let dir = dir.expect("should have gotten Some(path)");
|
||||
assert!(dir
|
||||
.components()
|
||||
|
||||
@@ -14,22 +14,29 @@ use connlib_client_shared::{file_logger, keypair, Callbacks, LoginUrl, Session,
|
||||
use connlib_shared::callbacks;
|
||||
use firezone_cli_utils::setup_global_subscriber;
|
||||
use secrecy::SecretString;
|
||||
use std::{future, net::IpAddr, path::PathBuf, task::Poll};
|
||||
use std::{
|
||||
future,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
path::PathBuf,
|
||||
task::Poll,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::subscriber::set_global_default;
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter, Layer as _, Registry};
|
||||
|
||||
use imp::default_token_path;
|
||||
use platform::default_token_path;
|
||||
|
||||
pub mod known_dirs;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod imp_linux;
|
||||
pub mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use imp_linux as imp;
|
||||
pub use linux as platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod imp_windows;
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use imp_windows as imp;
|
||||
pub use windows as platform;
|
||||
|
||||
/// Only used on Linux
|
||||
pub const FIREZONE_GROUP: &str = "firezone-client";
|
||||
@@ -124,8 +131,12 @@ pub enum IpcClientMsg {
|
||||
pub enum IpcServerMsg {
|
||||
Ok,
|
||||
OnDisconnect,
|
||||
OnSetInterfaceConfig {
|
||||
ipv4: Ipv4Addr,
|
||||
ipv6: Ipv6Addr,
|
||||
dns: Vec<IpAddr>,
|
||||
},
|
||||
OnUpdateResources(Vec<callbacks::ResourceDescription>),
|
||||
TunnelReady,
|
||||
}
|
||||
|
||||
pub fn run_only_headless_client() -> Result<()> {
|
||||
@@ -141,7 +152,8 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
|
||||
// Docs indicate that `remove_var` should actually be marked unsafe
|
||||
// SAFETY: We haven't spawned any other threads, this code should be the first
|
||||
// thing to run after entering `main`. So nobody else is reading the environment.
|
||||
// thing to run after entering `main` and parsing CLI args.
|
||||
// So nobody else is reading the environment.
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
// This removes the token from the environment per <https://security.stackexchange.com/a/271285>. We run as root so it may not do anything besides defense-in-depth.
|
||||
@@ -164,18 +176,7 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
cli.token_path
|
||||
)
|
||||
})?;
|
||||
run_standalone(cli, rt, &token)
|
||||
}
|
||||
|
||||
// Allow dead code because Windows doesn't have an obvious SIGHUP equivalent
|
||||
#[allow(dead_code)]
|
||||
enum SignalKind {
|
||||
Hangup,
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -> Result<()> {
|
||||
tracing::info!("Running in standalone mode");
|
||||
tracing::info!("Running in headless / standalone mode");
|
||||
let _guard = rt.enter();
|
||||
// TODO: Should this default to 30 days?
|
||||
let max_partition_time = cli.max_partition_time.map(|d| d.into());
|
||||
@@ -187,7 +188,13 @@ fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -
|
||||
};
|
||||
|
||||
let (private_key, public_key) = keypair();
|
||||
let login = LoginUrl::client(cli.api_url, token, firezone_id, None, public_key.to_bytes())?;
|
||||
let login = LoginUrl::client(
|
||||
cli.api_url,
|
||||
&token,
|
||||
firezone_id,
|
||||
None,
|
||||
public_key.to_bytes(),
|
||||
)?;
|
||||
|
||||
if cli.check {
|
||||
tracing::info!("Check passed");
|
||||
@@ -197,6 +204,7 @@ fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -
|
||||
let (on_disconnect_tx, mut on_disconnect_rx) = mpsc::channel(1);
|
||||
let callback_handler = CallbackHandler { on_disconnect_tx };
|
||||
|
||||
platform::setup_before_connlib()?;
|
||||
let session = Session::connect(
|
||||
login,
|
||||
Sockets::new(),
|
||||
@@ -207,9 +215,9 @@ fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -
|
||||
rt.handle().clone(),
|
||||
);
|
||||
// TODO: this should be added dynamically
|
||||
session.set_dns(imp::system_resolvers().unwrap_or_default());
|
||||
session.set_dns(platform::system_resolvers().unwrap_or_default());
|
||||
|
||||
let mut signals = imp::Signals::new()?;
|
||||
let mut signals = platform::Signals::new()?;
|
||||
|
||||
let result = rt.block_on(async {
|
||||
future::poll_fn(|cx| loop {
|
||||
@@ -240,6 +248,27 @@ fn run_standalone(cli: Cli, rt: tokio::runtime::Runtime, token: &SecretString) -
|
||||
result
|
||||
}
|
||||
|
||||
pub fn run_only_ipc_service() -> Result<()> {
|
||||
// Docs indicate that `remove_var` should actually be marked unsafe
|
||||
// SAFETY: We haven't spawned any other threads, this code should be the first
|
||||
// thing to run after entering `main` and parsing CLI args.
|
||||
// So nobody else is reading the environment.
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
// This removes the token from the environment per <https://security.stackexchange.com/a/271285>. We run as root so it may not do anything besides defense-in-depth.
|
||||
std::env::remove_var(TOKEN_ENV_KEY);
|
||||
}
|
||||
assert!(std::env::var(TOKEN_ENV_KEY).is_err());
|
||||
platform::run_only_ipc_service()
|
||||
}
|
||||
|
||||
// Allow dead code because Windows doesn't have an obvious SIGHUP equivalent
|
||||
#[allow(dead_code)]
|
||||
enum SignalKind {
|
||||
Hangup,
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CallbackHandler {
|
||||
/// Channel for an error message if connlib disconnects due to an error
|
||||
@@ -298,7 +327,7 @@ fn read_token_file(cli: &Cli) -> Result<Option<SecretString>> {
|
||||
if std::fs::metadata(&path).is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
imp::check_token_permissions(&path)?;
|
||||
platform::check_token_permissions(&path)?;
|
||||
|
||||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
// We got the metadata a second ago, but can't read the file itself.
|
||||
@@ -312,3 +341,14 @@ fn read_token_file(cli: &Cli) -> Result<Option<SecretString>> {
|
||||
tracing::info!(?path, "Loaded token from disk");
|
||||
Ok(Some(token))
|
||||
}
|
||||
|
||||
/// Sets up logging for stderr only, with INFO level by default
|
||||
pub fn debug_command_setup() -> Result<()> {
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
let layer = fmt::layer().with_filter(filter);
|
||||
let subscriber = Registry::default().with(layer);
|
||||
set_global_default(subscriber)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn default_token_path() -> PathBuf {
|
||||
/// 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<()> {
|
||||
pub(crate) 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();
|
||||
@@ -210,6 +210,7 @@ async fn ipc_listen(cli: Cli) -> Result<()> {
|
||||
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?;
|
||||
|
||||
loop {
|
||||
connlib_shared::deactivate_dns_control()?;
|
||||
tracing::info!("Listening for GUI to connect over IPC...");
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let cred = stream.peer_cred()?;
|
||||
@@ -242,10 +243,15 @@ impl Callbacks for CallbackHandlerIpc {
|
||||
.expect("should be able to send OnDisconnect");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(&self, _: Ipv4Addr, _: Ipv6Addr, _: Vec<IpAddr>) -> Option<i32> {
|
||||
fn on_set_interface_config(
|
||||
&self,
|
||||
ipv4: Ipv4Addr,
|
||||
ipv6: Ipv6Addr,
|
||||
dns: Vec<IpAddr>,
|
||||
) -> Option<i32> {
|
||||
tracing::info!("TunnelReady (on_set_interface_config)");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::TunnelReady)
|
||||
.try_send(IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns })
|
||||
.expect("Should be able to send TunnelReady");
|
||||
None
|
||||
}
|
||||
@@ -259,7 +265,6 @@ impl Callbacks for CallbackHandlerIpc {
|
||||
}
|
||||
|
||||
async fn handle_ipc_client(cli: &Cli, stream: UnixStream) -> Result<()> {
|
||||
connlib_shared::deactivate_dns_control()?;
|
||||
let (rx, tx) = stream.into_split();
|
||||
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
|
||||
let mut tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
@@ -300,7 +305,9 @@ async fn handle_ipc_client(cli: &Cli, stream: UnixStream) -> Result<()> {
|
||||
private_key,
|
||||
None,
|
||||
callback_handler.clone(),
|
||||
cli.max_partition_time.map(|t| t.into()),
|
||||
cli.max_partition_time
|
||||
.map(|t| t.into())
|
||||
.or(Some(std::time::Duration::from_secs(60 * 60 * 24 * 30))),
|
||||
tokio::runtime::Handle::try_current()?,
|
||||
));
|
||||
}
|
||||
@@ -319,6 +326,14 @@ async fn handle_ipc_client(cli: &Cli, stream: UnixStream) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Platform-specific setup needed for connlib
|
||||
///
|
||||
/// On Linux this does nothing
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn setup_before_connlib() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::IpAddr;
|
||||
485
rust/headless-client/src/windows.rs
Normal file
485
rust/headless-client/src/windows.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
//! Implementation of headless Client and IPC service for Windows
|
||||
//!
|
||||
//! Try not to panic in the IPC service. Windows doesn't consider the
|
||||
//! service to be stopped even if its only process ends, for some reason.
|
||||
//! We must tell Windows explicitly when our service is stopping.
|
||||
|
||||
use crate::{IpcClientMsg, IpcServerMsg, SignalKind};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use clap::Parser;
|
||||
use connlib_client_shared::{callbacks, file_logger, keypair, Callbacks, LoginUrl, Sockets};
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
use futures::{SinkExt, Stream};
|
||||
use std::{
|
||||
ffi::{c_void, OsString},
|
||||
future::{poll_fn, Future},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
path::{Path, PathBuf},
|
||||
pin::pin,
|
||||
str::FromStr,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{net::windows::named_pipe, sync::mpsc};
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
use tracing::subscriber::set_global_default;
|
||||
use tracing_subscriber::{layer::SubscriberExt as _, EnvFilter, Layer, Registry};
|
||||
use url::Url;
|
||||
use windows::Win32::Security as WinSec;
|
||||
use windows_service::{
|
||||
service::{
|
||||
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
|
||||
ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
};
|
||||
|
||||
mod wintun_install;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const SERVICE_RUST_LOG: &str = "firezone_headless_client=debug,firezone_tunnel=trace,phoenix_channel=debug,connlib_shared=debug,connlib_client_shared=debug,boringtun=debug,snownet=debug,str0m=info,info";
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const SERVICE_RUST_LOG: &str = "str0m=warn,info";
|
||||
|
||||
const SERVICE_NAME: &str = "firezone_client_ipc";
|
||||
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
|
||||
|
||||
pub(crate) struct Signals {
|
||||
sigint: tokio::signal::windows::CtrlC,
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let sigint = tokio::signal::windows::ctrl_c()?;
|
||||
Ok(Self { sigint })
|
||||
}
|
||||
|
||||
pub(crate) fn poll(&mut self, cx: &mut Context) -> Poll<SignalKind> {
|
||||
if self.sigint.poll_recv(cx).is_ready() {
|
||||
return Poll::Ready(SignalKind::Interrupt);
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Default)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct CliIpcService {
|
||||
#[command(subcommand)]
|
||||
command: CmdIpc,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum CmdIpc {
|
||||
#[command(hide = true)]
|
||||
DebugIpcService,
|
||||
IpcService,
|
||||
}
|
||||
|
||||
impl Default for CmdIpc {
|
||||
fn default() -> Self {
|
||||
Self::IpcService
|
||||
}
|
||||
}
|
||||
|
||||
// The return value is useful on Linux
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn check_token_permissions(_path: &Path) -> Result<()> {
|
||||
// TODO: For Headless Client, make sure the token is only readable by admin / our service user on Windows
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn default_token_path() -> std::path::PathBuf {
|
||||
// TODO: For Headless Client, system-wide default token path for Windows
|
||||
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(crate) fn run_only_ipc_service() -> Result<()> {
|
||||
let cli = CliIpcService::parse();
|
||||
match cli.command {
|
||||
CmdIpc::DebugIpcService => run_debug_ipc_service(cli),
|
||||
CmdIpc::IpcService => windows_service::service_dispatcher::start(SERVICE_NAME, ffi_service_run).context("windows_service::service_dispatcher failed. This isn't running in an interactive terminal, right?"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_debug_ipc_service(cli: CliIpcService) -> Result<()> {
|
||||
crate::debug_command_setup()?;
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let mut ipc_service = pin!(ipc_listen(cli));
|
||||
let mut signals = Signals::new()?;
|
||||
rt.block_on(async {
|
||||
std::future::poll_fn(|cx| {
|
||||
match signals.poll(cx) {
|
||||
Poll::Ready(SignalKind::Hangup) => {
|
||||
return Poll::Ready(Err(anyhow::anyhow!(
|
||||
"Impossible, we don't catch Hangup on Windows"
|
||||
)));
|
||||
}
|
||||
Poll::Ready(SignalKind::Interrupt) => {
|
||||
tracing::info!("Caught Interrupt signal");
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match ipc_service.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
return Poll::Ready(Err(anyhow::anyhow!(
|
||||
"Impossible, ipc_listen can't return Ok"
|
||||
)));
|
||||
}
|
||||
Poll::Ready(Err(error)) => {
|
||||
return Poll::Ready(Err(error).context("ipc_listen failed"));
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
// Generates `ffi_service_run` from `service_run`
|
||||
windows_service::define_windows_service!(ffi_service_run, windows_service_run);
|
||||
|
||||
fn windows_service_run(_arguments: Vec<OsString>) {
|
||||
if let Err(error) = fallible_windows_service_run() {
|
||||
tracing::error!(?error, "fallible_windows_service_run returned an error");
|
||||
}
|
||||
}
|
||||
|
||||
// Most of the Windows-specific service stuff should go here
|
||||
fn fallible_windows_service_run() -> Result<()> {
|
||||
let log_path =
|
||||
crate::known_dirs::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, mut 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,
|
||||
}
|
||||
};
|
||||
|
||||
// Fixes <https://github.com/firezone/firezone/issues/4899>,
|
||||
// DNS rules persisting after reboot
|
||||
connlib_shared::deactivate_dns_control().ok();
|
||||
|
||||
// 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,
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})?;
|
||||
|
||||
let mut ipc_service = pin!(ipc_listen(CliIpcService::default()));
|
||||
let result = rt.block_on(async {
|
||||
std::future::poll_fn(|cx| {
|
||||
match shutdown_rx.poll_recv(cx) {
|
||||
Poll::Ready(Some(())) => {
|
||||
tracing::info!("Got shutdown signal");
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(Err(anyhow!(
|
||||
"shutdown channel unexpectedly dropped, shutting down"
|
||||
)))
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match ipc_service.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
return Poll::Ready(Err(anyhow!("Impossible, ipc_listen can't return Ok")))
|
||||
}
|
||||
Poll::Ready(Err(error)) => {
|
||||
return Poll::Ready(Err(error.context("ipc_listen failed")))
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
// 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(if result.is_ok() { 0 } else { 1 }),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})?;
|
||||
result
|
||||
}
|
||||
|
||||
async fn ipc_listen(_cli: CliIpcService) -> Result<()> {
|
||||
setup_before_connlib()?;
|
||||
loop {
|
||||
// This is redundant on the first loop. After that it clears the rules
|
||||
// between GUI instances.
|
||||
connlib_shared::deactivate_dns_control()?;
|
||||
let server = create_pipe_server()?;
|
||||
tracing::info!("Listening for GUI to connect over IPC...");
|
||||
server
|
||||
.connect()
|
||||
.await
|
||||
.context("Couldn't accept IPC connection from GUI")?;
|
||||
if let Err(error) = handle_ipc_client(server).await {
|
||||
tracing::error!(?error, "Error while handling IPC client");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pipe_server() -> Result<named_pipe::NamedPipeServer> {
|
||||
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 though we're running with privilege
|
||||
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 sae them anywhere.
|
||||
unsafe {
|
||||
// ChatGPT pointed me to these functions
|
||||
WinSec::InitializeSecurityDescriptor(
|
||||
psd,
|
||||
windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION,
|
||||
)
|
||||
.context("InitializeSecurityDescriptor failed")?;
|
||||
WinSec::SetSecurityDescriptorDacl(psd, true, None, false)
|
||||
.context("SetSecurityDescriptorDacl failed")?;
|
||||
}
|
||||
|
||||
let mut sa = WinSec::SECURITY_ATTRIBUTES {
|
||||
nLength: 0,
|
||||
lpSecurityDescriptor: psd.0,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
sa.nLength = std::mem::size_of_val(&sa)
|
||||
.try_into()
|
||||
.context("Size of SECURITY_ATTRIBUTES struct is not right")?;
|
||||
|
||||
let sa_ptr = &mut sa as *mut _ as *mut c_void;
|
||||
// SAFETY: Unsafe needed to call Win32 API. We only pass pointers to local vars, and Win32 shouldn't store them, so there shouldn't be any threading of lifetime problems.
|
||||
let server = unsafe { server_options.create_with_security_attributes_raw(pipe_path(), sa_ptr) }
|
||||
.context("Failed to listen on named pipe")?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
/// Named pipe for IPC between GUI client and IPC service
|
||||
pub fn pipe_path() -> String {
|
||||
named_pipe_path(&format!("{BUNDLE_ID}.ipc_service"))
|
||||
}
|
||||
|
||||
enum IpcEvent {
|
||||
/// A message that the client sent us
|
||||
Client(IpcClientMsg),
|
||||
/// A message that connlib wants to send
|
||||
Connlib(IpcServerMsg),
|
||||
/// The IPC client disconnected
|
||||
IpcDisconnect,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CallbackHandlerIpc {
|
||||
cb_tx: mpsc::Sender<IpcServerMsg>,
|
||||
}
|
||||
|
||||
impl Callbacks for CallbackHandlerIpc {
|
||||
fn on_disconnect(&self, _error: &connlib_client_shared::Error) {
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnDisconnect)
|
||||
.expect("should be able to send OnDisconnect");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(
|
||||
&self,
|
||||
ipv4: Ipv4Addr,
|
||||
ipv6: Ipv6Addr,
|
||||
dns: Vec<IpAddr>,
|
||||
) -> Option<i32> {
|
||||
tracing::info!("TunnelReady");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns })
|
||||
.expect("Should be able to send TunnelReady");
|
||||
None
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<callbacks::ResourceDescription>) {
|
||||
tracing::info!(len = resources.len(), "New resource list");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnUpdateResources(resources))
|
||||
.expect("Should be able to send OnUpdateResources");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ipc_client(server: named_pipe::NamedPipeServer) -> Result<()> {
|
||||
let framed = Framed::new(server, LengthDelimitedCodec::new());
|
||||
let mut framed = pin!(framed);
|
||||
let (cb_tx, mut cb_rx) = mpsc::channel(100);
|
||||
|
||||
let mut connlib = None;
|
||||
let callback_handler = CallbackHandlerIpc { cb_tx };
|
||||
loop {
|
||||
let ev = poll_fn(|cx| {
|
||||
match cb_rx.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => return Poll::Ready(Ok(IpcEvent::Connlib(msg))),
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(Err(anyhow!(
|
||||
"Impossible - MPSC channel from connlib closed"
|
||||
)))
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match framed.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(msg)) => {
|
||||
let msg = serde_json::from_slice(&msg?)?;
|
||||
return Poll::Ready(Ok(IpcEvent::Client(msg)));
|
||||
}
|
||||
Poll::Ready(None) => return Poll::Ready(Ok(IpcEvent::IpcDisconnect)),
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
})
|
||||
.await;
|
||||
|
||||
match ev {
|
||||
Ok(IpcEvent::Client(msg)) => match msg {
|
||||
IpcClientMsg::Connect { api_url, token } => {
|
||||
let token = secrecy::SecretString::from(token);
|
||||
assert!(connlib.is_none());
|
||||
let device_id = connlib_shared::device_id::get()
|
||||
.context("Failed to read / create device ID")?;
|
||||
let (private_key, public_key) = keypair();
|
||||
|
||||
let login = LoginUrl::client(
|
||||
Url::parse(&api_url)?,
|
||||
&token,
|
||||
device_id.id,
|
||||
None,
|
||||
public_key.to_bytes(),
|
||||
)?;
|
||||
|
||||
connlib = Some(connlib_client_shared::Session::connect(
|
||||
login,
|
||||
Sockets::new(),
|
||||
private_key,
|
||||
None,
|
||||
callback_handler.clone(),
|
||||
Some(std::time::Duration::from_secs(60 * 60 * 24 * 30)),
|
||||
tokio::runtime::Handle::try_current()?,
|
||||
));
|
||||
}
|
||||
IpcClientMsg::Disconnect => {
|
||||
if let Some(connlib) = connlib.take() {
|
||||
connlib.disconnect();
|
||||
}
|
||||
}
|
||||
IpcClientMsg::Reconnect => {
|
||||
connlib.as_mut().context("No connlib session")?.reconnect()
|
||||
}
|
||||
IpcClientMsg::SetDns(v) => {
|
||||
connlib.as_mut().context("No connlib session")?.set_dns(v)
|
||||
}
|
||||
},
|
||||
Ok(IpcEvent::Connlib(msg)) => framed.send(serde_json::to_string(&msg)?.into()).await?,
|
||||
Ok(IpcEvent::IpcDisconnect) => {
|
||||
tracing::info!("IPC client disconnected");
|
||||
break;
|
||||
}
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn system_resolvers() -> Result<Vec<IpAddr>> {
|
||||
let resolvers = ipconfig::get_adapters()?
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.dns_servers())
|
||||
.filter(|ip| match ip {
|
||||
IpAddr::V4(_) => true,
|
||||
// Filter out bogus DNS resolvers on my dev laptop that start with fec0:
|
||||
IpAddr::V6(ip) => !ip.octets().starts_with(&[0xfe, 0xc0]),
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
// This is private, so keep it at `debug` or `trace`
|
||||
tracing::debug!(?resolvers);
|
||||
Ok(resolvers)
|
||||
}
|
||||
|
||||
/// Returns a valid name for a Windows named pipe
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - BUNDLE_ID, e.g. `dev.firezone.client`
|
||||
pub fn named_pipe_path(id: &str) -> String {
|
||||
format!(r"\\.\pipe\{}", id)
|
||||
}
|
||||
|
||||
/// Platform-specific setup needed for connlib
|
||||
///
|
||||
/// On Windows this installs wintun.dll
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn setup_before_connlib() -> Result<()> {
|
||||
wintun_install::ensure_dll()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn named_pipe_path() {
|
||||
assert_eq!(
|
||||
super::named_pipe_path("dev.firezone.client"),
|
||||
r"\\.\pipe\dev.firezone.client"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ fn dll_already_exists(path: &Path, dll_bytes: &DllBytes) -> bool {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn get_dll_bytes() -> DllBytes {
|
||||
DllBytes {
|
||||
bytes: include_bytes!("../../../wintun/bin/amd64/wintun.dll"),
|
||||
bytes: include_bytes!("wintun/bin/amd64/wintun.dll"),
|
||||
expected_sha256: "e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce",
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ fn get_dll_bytes() -> DllBytes {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
fn get_dll_bytes() -> DllBytes {
|
||||
DllBytes {
|
||||
bytes: include_bytes!("../../../wintun/bin/arm64/wintun.dll"),
|
||||
bytes: include_bytes!("wintun/bin/arm64/wintun.dll"),
|
||||
expected_sha256: "f7ba89005544be9d85231a9e0d5f23b2d15b3311667e2dad0debd344918a3f80",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user