mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(firezone-tunnel): move routes and DNS control out of connlib and up to the Client (#5111)
Refs #3636 (This pays down some of the technical debt from Linux DNS) Refs #4473 (This partially fulfills it) Refs #5068 (This is needed to make `FIREZONE_DNS_CONTROL` mandatory) As of dd6421: - On both Linux and Windows, DNS control and IP setting (i.e. `on_set_interface_config`) both move to the Client - On Windows, route setting stays in `tun_windows.rs`. Route setting in Windows requires us to know the interface index, which we don't know in the Client code. If we could pass opaque platform-specific data between the tunnel and the Client it would be easy. - On Linux, route setting moves to the Client and Gateway, which completely removes the `worker` task in `tun_linux.rs` - Notifying systemd that we're ready moves up to the headless Client / IPC service ```[tasklist] ### Before merging / notes - [x] Does DNS roaming work on Linux on `main`? I don't see where it hooks up. I think I only set up DNS in `Tun::new` (Yes, the `Tun` gets recreated every time we reconfigure the device) - [x] Fix Windows Clients - [x] Fix Gateway - [x] Make sure connlib doesn't get the DNS control method from the env var (will be fixed in #5068) - [x] De-dupe consts - [ ] ~~Add DNS control test~~ (failed) - [ ] Smoke test Linux - [ ] Smoke test Windows ```
This commit is contained in:
13
rust/Cargo.lock
generated
13
rust/Cargo.lock
generated
@@ -1173,7 +1173,6 @@ name = "connlib-shared"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atomicwrites",
|
||||
"base64 0.22.1",
|
||||
"boringtun",
|
||||
"chrono",
|
||||
@@ -1186,20 +1185,19 @@ dependencies = [
|
||||
"known-folders",
|
||||
"libc",
|
||||
"log",
|
||||
"mutants",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route",
|
||||
"os_info",
|
||||
"phoenix-channel",
|
||||
"proptest",
|
||||
"rand 0.8.5",
|
||||
"rand_core 0.6.4",
|
||||
"resolv-conf",
|
||||
"ring",
|
||||
"rtnetlink",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"swift-bridge",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -1985,6 +1983,7 @@ name = "firezone-headless-client"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atomicwrites",
|
||||
"clap",
|
||||
"connlib-client-shared",
|
||||
"connlib-shared",
|
||||
@@ -1993,21 +1992,26 @@ dependencies = [
|
||||
"futures",
|
||||
"git-version",
|
||||
"humantime",
|
||||
"ip_network",
|
||||
"ipconfig",
|
||||
"known-folders",
|
||||
"mutants",
|
||||
"nix 0.28.0",
|
||||
"resolv-conf",
|
||||
"ring",
|
||||
"rtnetlink",
|
||||
"sd-notify",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows 0.56.0",
|
||||
"windows-service",
|
||||
]
|
||||
@@ -2087,7 +2091,6 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"rangemap",
|
||||
"rtnetlink",
|
||||
"sd-notify",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -21,7 +21,6 @@ ip_network = { version = "0.4", default-features = false, features = ["serde"] }
|
||||
os_info = { version = "3", default-features = false }
|
||||
rand = { version = "0.8", default-features = false, features = ["std"] }
|
||||
rand_core = { version = "0.6.4", default-features = false, features = ["std"] }
|
||||
resolv-conf = "0.7.0"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||
serde_json = { version = "1.0", default-features = false, features = ["std"] }
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
@@ -41,9 +40,7 @@ hickory-proto = { workspace = true, optional = true }
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
itertools = "0.12"
|
||||
mutants = "0.0.3" # Needed to mark functions as exempt from `cargo-mutants` testing
|
||||
tokio = { version = "1.36", features = ["macros", "rt"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
@@ -52,11 +49,9 @@ swift-bridge = { workspace = true }
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
tracing-android = "0.2"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
# Needed to safely backup `/etc/resolv.conf` and write the device ID on behalf of `gui-client`
|
||||
atomicwrites = "0.4.3"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
netlink-packet-route = { version = "0.19", default-features = false }
|
||||
netlink-packet-core = { version = "0.7", default-features = false }
|
||||
rtnetlink = { workspace = true }
|
||||
|
||||
# Windows tunnel dependencies
|
||||
|
||||
@@ -42,6 +42,22 @@ impl From<Ipv6Network> for Cidrv6 {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cidrv4> for IpNetwork {
|
||||
fn from(x: Cidrv4) -> Self {
|
||||
Ipv4Network::new(x.address, x.prefix)
|
||||
.expect("A Cidrv4 should always translate to a valid Ipv4Network")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cidrv6> for IpNetwork {
|
||||
fn from(x: Cidrv6) -> Self {
|
||||
Ipv6Network::new(x.address, x.prefix)
|
||||
.expect("A Cidrv6 should always translate to a valid Ipv6Network")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Status {
|
||||
Unknown,
|
||||
|
||||
@@ -6,15 +6,7 @@
|
||||
pub mod callbacks;
|
||||
pub mod error;
|
||||
pub mod messages;
|
||||
|
||||
/// Module to generate and store a persistent device ID on disk
|
||||
///
|
||||
/// Only properly implemented on Linux and Windows (platforms with Tauri and headless client)
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub mod device_id;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
pub mod tun_device_manager;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
@@ -48,22 +40,11 @@ pub type DomainName = domain::base::Name<Vec<u8>>;
|
||||
/// <https://learn.microsoft.com/en-us/windows/configuration/find-the-application-user-model-id-of-an-installed-app>
|
||||
pub const BUNDLE_ID: &str = "dev.firezone.client";
|
||||
|
||||
pub const DEFAULT_MTU: u32 = 1280;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const LIB_NAME: &str = "connlib";
|
||||
|
||||
/// Deactivates DNS control on Windows
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn deactivate_dns_control() -> anyhow::Result<()> {
|
||||
windows::dns::deactivate()
|
||||
}
|
||||
|
||||
/// Deactivates DNS control on other platforms (does nothing)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn deactivate_dns_control() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn keypair() -> (StaticSecret, PublicKey) {
|
||||
let private_key = StaticSecret::random_from_rng(OsRng);
|
||||
let public_key = PublicKey::from(&private_key);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Linux-specific things like DNS control methods
|
||||
|
||||
const FIREZONE_DNS_CONTROL: &str = "FIREZONE_DNS_CONTROL";
|
||||
|
||||
pub mod etc_resolv_conf;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DnsControlMethod {
|
||||
/// Back up `/etc/resolv.conf` and replace it with our own
|
||||
///
|
||||
/// Only suitable for the Alpine CI containers and maybe something like an
|
||||
/// embedded system
|
||||
EtcResolvConf,
|
||||
/// Cooperate with NetworkManager (TODO)
|
||||
NetworkManager,
|
||||
/// Cooperate with `systemd-resolved`
|
||||
///
|
||||
/// Suitable for most Ubuntu systems, probably
|
||||
Systemd,
|
||||
}
|
||||
|
||||
/// Reads FIREZONE_DNS_CONTROL. Returns None if invalid or not set
|
||||
pub fn get_dns_control_from_env() -> Option<DnsControlMethod> {
|
||||
match std::env::var(FIREZONE_DNS_CONTROL).as_deref() {
|
||||
Ok("etc-resolv-conf") => Some(DnsControlMethod::EtcResolvConf),
|
||||
Ok("network-manager") => Some(DnsControlMethod::NetworkManager),
|
||||
Ok("systemd-resolved") => Some(DnsControlMethod::Systemd),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
14
rust/connlib/shared/src/tun_device_manager.rs
Normal file
14
rust/connlib/shared/src/tun_device_manager.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! DNS and route control for the virtual network interface in `firezone-tunnel`
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux as platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows as platform;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub use platform::TunDeviceManager;
|
||||
230
rust/connlib/shared/src/tun_device_manager/linux.rs
Normal file
230
rust/connlib/shared/src/tun_device_manager/linux.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Virtual network interface
|
||||
|
||||
use crate::{
|
||||
callbacks::{Cidrv4, Cidrv6},
|
||||
DEFAULT_MTU,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use futures::TryStreamExt;
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use netlink_packet_route::route::{RouteProtocol, RouteScope};
|
||||
use netlink_packet_route::rule::RuleAction;
|
||||
use rtnetlink::{new_connection, Error::NetlinkError, Handle, RouteAddRequest, RuleAddRequest};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
pub const FIREZONE_MARK: u32 = 0xfd002021;
|
||||
pub const IFACE_NAME: &str = "tun-firezone";
|
||||
const FILE_ALREADY_EXISTS: i32 = -17;
|
||||
const FIREZONE_TABLE: u32 = 0x2021_fd00;
|
||||
|
||||
/// For lack of a better name
|
||||
pub struct TunDeviceManager {
|
||||
connection: Connection,
|
||||
routes: HashSet<IpNetwork>,
|
||||
}
|
||||
|
||||
struct Connection {
|
||||
handle: Handle,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for TunDeviceManager {
|
||||
fn drop(&mut self) {
|
||||
self.connection.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl TunDeviceManager {
|
||||
/// Creates a new managed tunnel device.
|
||||
///
|
||||
/// Panics if called without a Tokio runtime.
|
||||
pub fn new() -> Result<Self> {
|
||||
let (cxn, handle, _) = new_connection()?;
|
||||
let task = tokio::spawn(cxn);
|
||||
let connection = Connection { handle, task };
|
||||
|
||||
Ok(Self {
|
||||
connection,
|
||||
routes: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn set_ips(&mut self, ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Result<()> {
|
||||
let handle = &self.connection.handle;
|
||||
let index = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(IFACE_NAME.to_string())
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("No interface"))?
|
||||
.header
|
||||
.index;
|
||||
|
||||
let ips = handle
|
||||
.address()
|
||||
.get()
|
||||
.set_link_index_filter(index)
|
||||
.execute();
|
||||
|
||||
ips.try_for_each(|ip| handle.address().del(ip).execute())
|
||||
.await?;
|
||||
|
||||
handle.link().set(index).mtu(DEFAULT_MTU).execute().await?;
|
||||
|
||||
let res_v4 = handle.address().add(index, ipv4.into(), 32).execute().await;
|
||||
let res_v6 = handle
|
||||
.address()
|
||||
.add(index, ipv6.into(), 128)
|
||||
.execute()
|
||||
.await;
|
||||
|
||||
handle.link().set(index).up().execute().await?;
|
||||
|
||||
if res_v4.is_ok() {
|
||||
if let Err(e) = make_rule(handle).v4().execute().await {
|
||||
if !matches!(&e, NetlinkError(err) if err.raw_code() == FILE_ALREADY_EXISTS) {
|
||||
tracing::warn!(
|
||||
"Couldn't add ip rule for ipv4: {e:?}, ipv4 packets won't be routed"
|
||||
);
|
||||
}
|
||||
// TODO: Be smarter about this
|
||||
} else {
|
||||
tracing::debug!("Successfully created ip rule for ipv4");
|
||||
}
|
||||
}
|
||||
|
||||
if res_v6.is_ok() {
|
||||
if let Err(e) = make_rule(handle).v6().execute().await {
|
||||
if !matches!(&e, NetlinkError(err) if err.raw_code() == FILE_ALREADY_EXISTS) {
|
||||
tracing::warn!(
|
||||
"Couldn't add ip rule for ipv6: {e:?}, ipv6 packets won't be routed"
|
||||
);
|
||||
}
|
||||
// TODO: Be smarter about this
|
||||
} else {
|
||||
tracing::debug!("Successfully created ip rule for ipv6");
|
||||
}
|
||||
}
|
||||
|
||||
res_v4.or(res_v6)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn set_routes(&mut self, ipv4: Vec<Cidrv4>, ipv6: Vec<Cidrv6>) -> Result<()> {
|
||||
let new_routes: HashSet<IpNetwork> = ipv4
|
||||
.into_iter()
|
||||
.map(IpNetwork::from)
|
||||
.chain(ipv6.into_iter().map(IpNetwork::from))
|
||||
.collect();
|
||||
if new_routes == self.routes {
|
||||
return Ok(());
|
||||
}
|
||||
tracing::info!(?new_routes, "set_routes");
|
||||
let handle = &self.connection.handle;
|
||||
|
||||
let index = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(IFACE_NAME.to_string())
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.context("No interface")?
|
||||
.header
|
||||
.index;
|
||||
|
||||
for route in new_routes.difference(&self.routes) {
|
||||
add_route(route, index, handle).await?;
|
||||
}
|
||||
|
||||
for route in self.routes.difference(&new_routes) {
|
||||
delete_route(route, index, handle).await?;
|
||||
}
|
||||
|
||||
self.routes = new_routes;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_rule(handle: &Handle) -> RuleAddRequest {
|
||||
let mut rule = handle
|
||||
.rule()
|
||||
.add()
|
||||
.fw_mark(FIREZONE_MARK)
|
||||
.table_id(FIREZONE_TABLE)
|
||||
.action(RuleAction::ToTable);
|
||||
|
||||
rule.message_mut()
|
||||
.header
|
||||
.flags
|
||||
.push(netlink_packet_route::rule::RuleFlag::Invert);
|
||||
|
||||
rule.message_mut()
|
||||
.attributes
|
||||
.push(netlink_packet_route::rule::RuleAttribute::Protocol(
|
||||
RouteProtocol::Kernel,
|
||||
));
|
||||
|
||||
rule
|
||||
}
|
||||
|
||||
fn make_route(idx: u32, handle: &Handle) -> RouteAddRequest {
|
||||
handle
|
||||
.route()
|
||||
.add()
|
||||
.output_interface(idx)
|
||||
.protocol(RouteProtocol::Static)
|
||||
.scope(RouteScope::Universe)
|
||||
.table_id(FIREZONE_TABLE)
|
||||
}
|
||||
|
||||
fn make_route_v4(idx: u32, handle: &Handle, route: Ipv4Network) -> RouteAddRequest<Ipv4Addr> {
|
||||
make_route(idx, handle)
|
||||
.v4()
|
||||
.destination_prefix(route.network_address(), route.netmask())
|
||||
}
|
||||
|
||||
fn make_route_v6(idx: u32, handle: &Handle, route: Ipv6Network) -> RouteAddRequest<Ipv6Addr> {
|
||||
make_route(idx, handle)
|
||||
.v6()
|
||||
.destination_prefix(route.network_address(), route.netmask())
|
||||
}
|
||||
|
||||
async fn add_route(route: &IpNetwork, idx: u32, handle: &Handle) -> Result<()> {
|
||||
let res = match route {
|
||||
IpNetwork::V4(ipnet) => make_route_v4(idx, handle, *ipnet).execute().await,
|
||||
IpNetwork::V6(ipnet) => make_route_v6(idx, handle, *ipnet).execute().await,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(NetlinkError(err)) if err.raw_code() == FILE_ALREADY_EXISTS => {}
|
||||
// TODO: we should be able to surface this error and handle it depending on
|
||||
// if any of the added routes succeeded.
|
||||
Err(err) => Err(err).context("Failed to add route")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_route(route: &IpNetwork, idx: u32, handle: &Handle) -> Result<()> {
|
||||
let message = match route {
|
||||
IpNetwork::V4(ipnet) => make_route_v4(idx, handle, *ipnet).message_mut().clone(),
|
||||
IpNetwork::V6(ipnet) => make_route_v6(idx, handle, *ipnet).message_mut().clone(),
|
||||
};
|
||||
|
||||
handle
|
||||
.route()
|
||||
.del(message)
|
||||
.execute()
|
||||
.await
|
||||
.context("Failed to delete route")?;
|
||||
Ok(())
|
||||
}
|
||||
63
rust/connlib/shared/src/tun_device_manager/windows.rs
Normal file
63
rust/connlib/shared/src/tun_device_manager/windows.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use crate::{
|
||||
windows::{CREATE_NO_WINDOW, TUNNEL_NAME},
|
||||
Cidrv4, Cidrv6,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
os::windows::process::CommandExt,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
pub struct TunDeviceManager {}
|
||||
|
||||
impl TunDeviceManager {
|
||||
// Fallible on Linux
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn set_ips(&mut self, ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Result<()> {
|
||||
tracing::debug!("Setting our IPv4 = {}", ipv4);
|
||||
tracing::debug!("Setting our IPv6 = {}", ipv6);
|
||||
|
||||
// TODO: See if there's a good Win32 API for this
|
||||
// Using netsh directly instead of wintun's `set_network_addresses_tuple` because their code doesn't work for IPv6
|
||||
Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("interface")
|
||||
.arg("ipv4")
|
||||
.arg("set")
|
||||
.arg("address")
|
||||
.arg(format!("name=\"{TUNNEL_NAME}\""))
|
||||
.arg("source=static")
|
||||
.arg(format!("address={}", ipv4))
|
||||
.arg("mask=255.255.255.255")
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("interface")
|
||||
.arg("ipv6")
|
||||
.arg("set")
|
||||
.arg("address")
|
||||
.arg(format!("interface=\"{TUNNEL_NAME}\""))
|
||||
.arg(format!("address={}", ipv6))
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn set_routes(&mut self, _: Vec<Cidrv4>, _: Vec<Cidrv6>) -> Result<()> {
|
||||
// TODO: Windows still does route updates in `tun_windows.rs`. I can move it up
|
||||
// here, but since the Client and Gateway don't know the index of the WinTun
|
||||
// interface, I'd have to use the Windows API
|
||||
// <https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/NetworkManagement/IpHelper/fn.GetAdaptersAddresses.html>
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,15 @@ use crate::Error;
|
||||
use known_folders::{get_known_folder_path, KnownFolder};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 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>
|
||||
/// Also used for self-elevation
|
||||
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// wintun automatically append " Tunnel" to this
|
||||
pub const TUNNEL_NAME: &str = "Firezone";
|
||||
|
||||
/// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client
|
||||
///
|
||||
/// This is where we can save config, logs, crash dumps, etc.
|
||||
@@ -16,113 +25,6 @@ pub fn app_local_data_dir() -> Result<PathBuf, Error> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub mod dns {
|
||||
//! Gives Firezone DNS privilege over other DNS resolvers on the system
|
||||
//!
|
||||
//! This uses NRPT and claims all domains, similar to the `systemd-resolved` control method
|
||||
//! on Linux.
|
||||
//! This allows us to "shadow" DNS resolvers that are configured by the user or DHCP on
|
||||
//! physical interfaces, as long as they don't have any NRPT rules that outrank us.
|
||||
//!
|
||||
//! If Firezone crashes, restarting Firezone and closing it gracefully will resume
|
||||
//! normal DNS operation. The Powershell command to remove the NRPT rule can also be run
|
||||
//! by hand.
|
||||
//!
|
||||
//! The system default resolvers don't need to be reverted because they're never deleted.
|
||||
//!
|
||||
//! <https://superuser.com/a/1752670>
|
||||
|
||||
use anyhow::Result;
|
||||
use std::{net::IpAddr, os::windows::process::CommandExt, process::Command};
|
||||
|
||||
/// 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>
|
||||
/// Also used for self-elevation
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// Unique magic number that we can use to delete our well-known NRPT rule.
|
||||
// Copied from the deep link schema
|
||||
const FZ_MAGIC: &str = "firezone-fd0020211111";
|
||||
|
||||
/// Tells Windows to send all DNS queries to our sentinels
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `dns_config_string`: Comma-separated IP addresses of DNS servers, e.g. "1.1.1.1,8.8.8.8"
|
||||
pub fn activate(dns_config: &[IpAddr], iface_idx: u32) -> Result<()> {
|
||||
let dns_config_string = dns_config
|
||||
.iter()
|
||||
.map(|ip| format!("\"{ip}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
// Set our DNS IP as the DNS server for our interface
|
||||
// TODO: Known issue where web browsers will keep a connection open to a site,
|
||||
// using QUIC, HTTP/2, or even HTTP/1.1, and so they won't resolve the DNS
|
||||
// again unless you let that connection time out:
|
||||
// <https://github.com/firezone/firezone/issues/3113#issuecomment-1882096111>
|
||||
// TODO: If we have a Windows gateway, it shouldn't configure DNS, right?
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("-Command")
|
||||
.arg(format!("Set-DnsClientServerAddress -InterfaceIndex {iface_idx} -ServerAddresses({dns_config_string})"))
|
||||
.status()?;
|
||||
|
||||
tracing::info!("Activating DNS control");
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args([
|
||||
"-Command",
|
||||
"Add-DnsClientNrptRule",
|
||||
"-Namespace",
|
||||
".",
|
||||
"-Comment",
|
||||
FZ_MAGIC,
|
||||
"-NameServers",
|
||||
&dns_config_string,
|
||||
])
|
||||
.status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tells Windows to send all DNS queries to this new set of sentinels
|
||||
///
|
||||
/// Currently implemented as just removing the rule and re-adding it, which
|
||||
/// creates a gap but doesn't require us to parse Powershell output to figure
|
||||
/// out the rule's UUID.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `dns_config_string` - Passed verbatim to [`activate`]
|
||||
pub fn change(dns_config: &[IpAddr], iface_idx: u32) -> Result<()> {
|
||||
deactivate()?;
|
||||
activate(dns_config, iface_idx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deactivate() -> Result<()> {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-Command", "Get-DnsClientNrptRule", "|"])
|
||||
.args(["where", "Comment", "-eq", FZ_MAGIC, "|"])
|
||||
.args(["foreach", "{"])
|
||||
.args(["Remove-DnsClientNrptRule", "-Name", "$_.Name", "-Force"])
|
||||
.args(["}"])
|
||||
.status()?;
|
||||
tracing::info!("Deactivated DNS control");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush Windows' system-wide DNS cache
|
||||
pub fn flush() -> Result<()> {
|
||||
tracing::info!("Flushing Windows DNS cache");
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-Command", "Clear-DnsClientCache"])
|
||||
.status()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the absolute path for installing and loading `wintun.dll`
|
||||
///
|
||||
/// e.g. `C:\Users\User\AppData\Local\dev.firezone.client\data\wintun.dll`
|
||||
|
||||
@@ -55,7 +55,6 @@ proptest = ["dep:proptest", "connlib-shared/proptest"]
|
||||
netlink-packet-route = { version = "0.19", default-features = false }
|
||||
netlink-packet-core = { version = "0.7", default-features = false }
|
||||
rtnetlink = { workspace = true }
|
||||
sd-notify = "0.4.1"
|
||||
|
||||
# Android tunnel dependencies
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
#![cfg_attr(target_family = "windows", allow(dead_code))] // TODO: Remove when windows is fully implemented.
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
mod tun_darwin;
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
@@ -87,9 +84,9 @@ impl Device {
|
||||
dns_config: Vec<IpAddr>,
|
||||
callbacks: &impl Callbacks,
|
||||
) -> Result<(), ConnlibError> {
|
||||
// On Android / Linux we recreate the tunnel every time we re-configure it
|
||||
self.tun = Some(Tun::new(config, dns_config.clone(), callbacks)?);
|
||||
|
||||
// The actual values are ignored, this is just used as a `TunnelReady` signal
|
||||
callbacks.on_set_interface_config(config.ipv4, config.ipv6, dns_config);
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
@@ -99,7 +96,7 @@ impl Device {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "ios", target_os = "macos", target_os = "windows"))]
|
||||
pub(crate) fn set_config(
|
||||
&mut self,
|
||||
config: &Interface,
|
||||
@@ -107,7 +104,7 @@ impl Device {
|
||||
callbacks: &impl Callbacks,
|
||||
) -> Result<(), ConnlibError> {
|
||||
// For macos the filedescriptor is the same throughout its lifetime.
|
||||
// If we reinitialzie tun, we might drop the old tun after the new one is created
|
||||
// If we reinitialize tun, we might drop the old tun after the new one is created
|
||||
// this unregisters the file descriptor with the reactor so we never wake up
|
||||
// in case an event is triggered.
|
||||
if self.tun.is_none() {
|
||||
@@ -123,28 +120,6 @@ impl Device {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
pub(crate) fn set_config(
|
||||
&mut self,
|
||||
config: &Interface,
|
||||
dns_config: Vec<IpAddr>,
|
||||
callbacks: &impl Callbacks,
|
||||
) -> Result<(), ConnlibError> {
|
||||
if self.tun.is_none() {
|
||||
self.tun = Some(Tun::new()?);
|
||||
}
|
||||
|
||||
self.tun.as_ref().unwrap().set_config(config, &dns_config)?;
|
||||
|
||||
callbacks.on_set_interface_config(config.ipv4, config.ipv6, dns_config);
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub(crate) fn poll_read<'b>(
|
||||
&mut self,
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
use super::utils;
|
||||
use crate::device_channel::ioctl;
|
||||
use crate::FIREZONE_MARK;
|
||||
use connlib_shared::{
|
||||
linux::{etc_resolv_conf, DnsControlMethod},
|
||||
messages::Interface as InterfaceConfig,
|
||||
Callbacks, Error, Result,
|
||||
messages::Interface as InterfaceConfig, tun_device_manager::platform::IFACE_NAME, Callbacks,
|
||||
Error, Result,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use ip_network::IpNetwork;
|
||||
use libc::{
|
||||
close, fcntl, makedev, mknod, open, F_GETFL, F_SETFL, IFF_NO_PI, IFF_TUN, O_NONBLOCK, O_RDWR,
|
||||
S_IFCHR,
|
||||
};
|
||||
use netlink_packet_route::route::{RouteProtocol, RouteScope};
|
||||
use netlink_packet_route::rule::RuleAction;
|
||||
use rtnetlink::{new_connection, Error::NetlinkError, Handle};
|
||||
use rtnetlink::{RouteAddRequest, RuleAddRequest};
|
||||
use std::collections::HashSet;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::net::IpAddr;
|
||||
use std::path::Path;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
fmt, fs, io,
|
||||
fs, io,
|
||||
os::{
|
||||
fd::{AsRawFd, RawFd},
|
||||
unix::fs::PermissionsExt,
|
||||
@@ -32,46 +23,21 @@ use std::{
|
||||
};
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
const IFACE_NAME: &str = "tun-firezone";
|
||||
const TUNSETIFF: libc::c_ulong = 0x4004_54ca;
|
||||
const TUN_DEV_MAJOR: u32 = 10;
|
||||
const TUN_DEV_MINOR: u32 = 200;
|
||||
const DEFAULT_MTU: u32 = 1280;
|
||||
const FILE_ALREADY_EXISTS: i32 = -17;
|
||||
const FIREZONE_TABLE: u32 = 0x2021_fd00;
|
||||
|
||||
// Safety: We know that this is a valid C string.
|
||||
const TUN_FILE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"/dev/net/tun\0") };
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tun {
|
||||
handle: Handle,
|
||||
connection: tokio::task::JoinHandle<()>,
|
||||
dns_control_method: Option<DnsControlMethod>,
|
||||
fd: AsyncFd<RawFd>,
|
||||
|
||||
worker: Option<BoxFuture<'static, Result<()>>>,
|
||||
routes: HashSet<IpNetwork>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Tun {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Tun")
|
||||
.field("handle", &self.handle)
|
||||
.field("connection", &self.connection)
|
||||
.field("fd", &self.fd)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Tun {
|
||||
fn drop(&mut self) {
|
||||
unsafe { close(self.fd.as_raw_fd()) };
|
||||
self.connection.abort();
|
||||
tracing::debug!("Reverting DNS control...");
|
||||
if let Some(DnsControlMethod::EtcResolvConf) = self.dns_control_method {
|
||||
// TODO: Check that nobody else modified the file while we were running.
|
||||
etc_resolv_conf::revert().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,35 +51,16 @@ impl Tun {
|
||||
}
|
||||
|
||||
pub fn poll_read(&mut self, buf: &mut [u8], cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
|
||||
if let Some(worker) = self.worker.as_mut() {
|
||||
match worker.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
self.worker = None;
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
self.worker = None;
|
||||
return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e)));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
utils::poll_raw_fd(&self.fd, |fd| read(fd, buf), cx)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
config: &InterfaceConfig,
|
||||
dns_config: Vec<IpAddr>,
|
||||
_: &impl Callbacks,
|
||||
callbacks: &impl Callbacks,
|
||||
) -> Result<Self> {
|
||||
tracing::debug!(?dns_config);
|
||||
|
||||
// TODO: Tech debt: <https://github.com/firezone/firezone/issues/3636>
|
||||
// TODO: Gateways shouldn't set up DNS, right? Only clients?
|
||||
// TODO: Move this configuration up to the client
|
||||
let dns_control_method = connlib_shared::linux::get_dns_control_from_env();
|
||||
tracing::info!(?dns_control_method);
|
||||
|
||||
create_tun_device()?;
|
||||
|
||||
let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } {
|
||||
@@ -132,68 +79,23 @@ impl Tun {
|
||||
|
||||
set_non_blocking(fd)?;
|
||||
|
||||
let (connection, handle, _) = new_connection()?;
|
||||
let join_handle = tokio::spawn(connection);
|
||||
callbacks.on_set_interface_config(config.ipv4, config.ipv6, dns_config);
|
||||
|
||||
Ok(Self {
|
||||
handle: handle.clone(),
|
||||
connection: join_handle,
|
||||
dns_control_method: dns_control_method.clone(),
|
||||
fd: AsyncFd::new(fd)?,
|
||||
worker: Some(
|
||||
set_iface_config(config.clone(), dns_config, handle, dns_control_method).boxed(),
|
||||
),
|
||||
routes: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)] // fn signature needs to align with other platforms.
|
||||
pub fn set_routes(&mut self, new_routes: HashSet<IpNetwork>, _: &impl Callbacks) -> Result<()> {
|
||||
if new_routes == self.routes {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let handle = self.handle.clone();
|
||||
let current_routes = self.routes.clone();
|
||||
self.routes.clone_from(&new_routes);
|
||||
|
||||
let set_routes_worker = async move {
|
||||
let index = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(IFACE_NAME.to_string())
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(Error::NoIface)?
|
||||
.header
|
||||
.index;
|
||||
|
||||
for route in new_routes.difference(¤t_routes) {
|
||||
add_route(route, index, &handle).await;
|
||||
}
|
||||
|
||||
for route in current_routes.difference(&new_routes) {
|
||||
delete_route(route, index, &handle).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match self.worker.take() {
|
||||
None => self.worker = Some(set_routes_worker.boxed()),
|
||||
Some(current_worker) => {
|
||||
self.worker = Some(
|
||||
async move {
|
||||
current_worker.await?;
|
||||
set_routes_worker.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
}
|
||||
pub fn set_routes(
|
||||
&mut self,
|
||||
new_routes: HashSet<IpNetwork>,
|
||||
callbacks: &impl Callbacks,
|
||||
) -> Result<()> {
|
||||
callbacks.on_update_routes(
|
||||
new_routes.iter().copied().filter_map(super::ipv4).collect(),
|
||||
new_routes.iter().copied().filter_map(super::ipv6).collect(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -203,147 +105,6 @@ impl Tun {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(handle))]
|
||||
async fn set_iface_config(
|
||||
config: InterfaceConfig,
|
||||
dns_config: Vec<IpAddr>,
|
||||
handle: Handle,
|
||||
dns_control_method: Option<DnsControlMethod>,
|
||||
) -> Result<()> {
|
||||
let index = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(IFACE_NAME.to_string())
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(Error::NoIface)?
|
||||
.header
|
||||
.index;
|
||||
|
||||
let ips = handle
|
||||
.address()
|
||||
.get()
|
||||
.set_link_index_filter(index)
|
||||
.execute();
|
||||
|
||||
ips.try_for_each(|ip| handle.address().del(ip).execute())
|
||||
.await?;
|
||||
|
||||
handle.link().set(index).mtu(DEFAULT_MTU).execute().await?;
|
||||
|
||||
let res_v4 = handle
|
||||
.address()
|
||||
.add(index, config.ipv4.into(), 32)
|
||||
.execute()
|
||||
.await;
|
||||
let res_v6 = handle
|
||||
.address()
|
||||
.add(index, config.ipv6.into(), 128)
|
||||
.execute()
|
||||
.await;
|
||||
|
||||
handle.link().set(index).up().execute().await?;
|
||||
|
||||
if res_v4.is_ok() {
|
||||
if let Err(e) = make_rule(&handle).v4().execute().await {
|
||||
if !matches!(&e, NetlinkError(err) if err.raw_code() == FILE_ALREADY_EXISTS) {
|
||||
tracing::warn!(
|
||||
"Couldn't add ip rule for ipv4: {e:?}, ipv4 packets won't be routed"
|
||||
);
|
||||
}
|
||||
// TODO: Be smarter about this
|
||||
} else {
|
||||
tracing::debug!("Successfully created ip rule for ipv4");
|
||||
}
|
||||
}
|
||||
|
||||
if res_v6.is_ok() {
|
||||
if let Err(e) = make_rule(&handle).v6().execute().await {
|
||||
if !matches!(&e, NetlinkError(err) if err.raw_code() == FILE_ALREADY_EXISTS) {
|
||||
tracing::warn!(
|
||||
"Couldn't add ip rule for ipv6: {e:?}, ipv6 packets won't be routed"
|
||||
);
|
||||
}
|
||||
// TODO: Be smarter about this
|
||||
} else {
|
||||
tracing::debug!("Successfully created ip rule for ipv6");
|
||||
}
|
||||
}
|
||||
|
||||
res_v4.or(res_v6)?;
|
||||
|
||||
if let Err(error) = match dns_control_method {
|
||||
None => Ok(()),
|
||||
Some(DnsControlMethod::EtcResolvConf) => etc_resolv_conf::configure(&dns_config)
|
||||
.await
|
||||
.map_err(Error::ResolvConf),
|
||||
Some(DnsControlMethod::NetworkManager) => configure_network_manager(&dns_config),
|
||||
Some(DnsControlMethod::Systemd) => configure_systemd_resolved(&dns_config).await,
|
||||
} {
|
||||
tracing::error!("Failed to control DNS: {error}");
|
||||
panic!("Failed to control DNS: {error}");
|
||||
}
|
||||
|
||||
// TODO: Having this inside the library is definitely wrong. I think `set_iface_config`
|
||||
// needs to return before `new` returns, so that the `on_tunnel_ready` callback
|
||||
// happens after the IP address and DNS are set up. Then we can call `sd_notify`
|
||||
// inside `on_tunnel_ready` in the client.
|
||||
//
|
||||
// `sd_notify::notify` is always safe to call, it silently returns `Ok(())`
|
||||
// if we aren't running as a systemd service.
|
||||
if let Err(error) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) {
|
||||
// Nothing we can do about it
|
||||
tracing::warn!(?error, "Failed to notify systemd that we're ready");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_rule(handle: &Handle) -> RuleAddRequest {
|
||||
let mut rule = handle
|
||||
.rule()
|
||||
.add()
|
||||
.fw_mark(FIREZONE_MARK)
|
||||
.table_id(FIREZONE_TABLE)
|
||||
.action(RuleAction::ToTable);
|
||||
|
||||
rule.message_mut()
|
||||
.header
|
||||
.flags
|
||||
.push(netlink_packet_route::rule::RuleFlag::Invert);
|
||||
|
||||
rule.message_mut()
|
||||
.attributes
|
||||
.push(netlink_packet_route::rule::RuleAttribute::Protocol(
|
||||
RouteProtocol::Kernel,
|
||||
));
|
||||
|
||||
rule
|
||||
}
|
||||
|
||||
fn make_route(idx: u32, handle: &Handle) -> RouteAddRequest {
|
||||
handle
|
||||
.route()
|
||||
.add()
|
||||
.output_interface(idx)
|
||||
.protocol(RouteProtocol::Static)
|
||||
.scope(RouteScope::Universe)
|
||||
.table_id(FIREZONE_TABLE)
|
||||
}
|
||||
|
||||
fn make_route_v4(idx: u32, handle: &Handle, route: Ipv4Network) -> RouteAddRequest<Ipv4Addr> {
|
||||
make_route(idx, handle)
|
||||
.v4()
|
||||
.destination_prefix(route.network_address(), route.netmask())
|
||||
}
|
||||
|
||||
fn make_route_v6(idx: u32, handle: &Handle, route: Ipv6Network) -> RouteAddRequest<Ipv6Addr> {
|
||||
make_route(idx, handle)
|
||||
.v6()
|
||||
.destination_prefix(route.network_address(), route.netmask())
|
||||
}
|
||||
|
||||
fn get_last_error() -> Error {
|
||||
Error::Io(io::Error::last_os_error())
|
||||
}
|
||||
@@ -401,34 +162,6 @@ fn write(fd: RawFd, buf: &[u8]) -> io::Result<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_route(route: &IpNetwork, idx: u32, handle: &Handle) {
|
||||
let res = match route {
|
||||
IpNetwork::V4(ipnet) => make_route_v4(idx, handle, *ipnet).execute().await,
|
||||
IpNetwork::V6(ipnet) => make_route_v6(idx, handle, *ipnet).execute().await,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(NetlinkError(err)) if err.raw_code() == FILE_ALREADY_EXISTS => {}
|
||||
// TODO: we should be able to surface this error and handle it depending on
|
||||
// if any of the added routes succeeded.
|
||||
Err(err) => {
|
||||
tracing::error!(%route, "failed to add route: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_route(route: &IpNetwork, idx: u32, handle: &Handle) {
|
||||
let message = match route {
|
||||
IpNetwork::V4(ipnet) => make_route_v4(idx, handle, *ipnet).message_mut().clone(),
|
||||
IpNetwork::V6(ipnet) => make_route_v6(idx, handle, *ipnet).message_mut().clone(),
|
||||
};
|
||||
|
||||
if let Err(err) = handle.route().del(message).execute().await {
|
||||
tracing::error!(%route, "failed to add route: {err:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
impl ioctl::Request<SetTunFlagsPayload> {
|
||||
fn new() -> Self {
|
||||
let name_as_bytes = IFACE_NAME.as_bytes();
|
||||
@@ -446,42 +179,6 @@ impl ioctl::Request<SetTunFlagsPayload> {
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_network_manager(_dns_config: &[IpAddr]) -> Result<()> {
|
||||
Err(Error::Other(
|
||||
"DNS control with NetworkManager is not implemented yet",
|
||||
))
|
||||
}
|
||||
|
||||
async fn configure_systemd_resolved(dns_config: &[IpAddr]) -> Result<()> {
|
||||
let status = tokio::process::Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.arg(IFACE_NAME)
|
||||
.args(dns_config.iter().map(ToString::to_string))
|
||||
.status()
|
||||
.await
|
||||
.map_err(|_| Error::ResolvectlFailed)?;
|
||||
if !status.success() {
|
||||
tracing::error!("`resolvectl dns` failed");
|
||||
return Err(Error::ResolvectlFailed);
|
||||
}
|
||||
|
||||
let status = tokio::process::Command::new("resolvectl")
|
||||
.arg("domain")
|
||||
.arg(IFACE_NAME)
|
||||
.arg("~.")
|
||||
.status()
|
||||
.await
|
||||
.map_err(|_| Error::ResolvectlFailed)?;
|
||||
if !status.success() {
|
||||
tracing::error!("`resolvectl domain` failed");
|
||||
return Err(Error::ResolvectlFailed);
|
||||
}
|
||||
|
||||
tracing::info!(?dns_config, "Configured DNS sentinels with `resolvectl`");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct SetTunFlagsPayload {
|
||||
flags: std::ffi::c_short,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use connlib_shared::{messages::Interface as InterfaceConfig, Callbacks, Result};
|
||||
use connlib_shared::{
|
||||
windows::{CREATE_NO_WINDOW, TUNNEL_NAME},
|
||||
Callbacks, Result, DEFAULT_MTU,
|
||||
};
|
||||
use ip_network::IpNetwork;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io,
|
||||
net::{IpAddr, SocketAddrV4, SocketAddrV6},
|
||||
net::{SocketAddrV4, SocketAddrV6},
|
||||
os::windows::process::CommandExt,
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
@@ -22,12 +25,8 @@ use windows::Win32::{
|
||||
Networking::WinSock::{AF_INET, AF_INET6},
|
||||
};
|
||||
|
||||
// wintun automatically appends " Tunnel" to this
|
||||
const TUNNEL_NAME: &str = "Firezone";
|
||||
|
||||
// TODO: Double-check that all these get dropped gracefully on disconnect
|
||||
pub struct Tun {
|
||||
adapter: Arc<wintun::Adapter>,
|
||||
/// The index of our network adapter, we can use this when asking Windows to add / remove routes / DNS rules
|
||||
/// It's stable across app restarts and I'm assuming across system reboots too.
|
||||
iface_idx: u32,
|
||||
@@ -39,21 +38,12 @@ pub struct Tun {
|
||||
|
||||
impl Drop for Tun {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = connlib_shared::windows::dns::deactivate() {
|
||||
tracing::error!(?error, "Failed to deactivate DNS control");
|
||||
}
|
||||
if let Err(e) = self.session.shutdown() {
|
||||
tracing::error!("wintun::Session::shutdown: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Copied from tun_linux.rs
|
||||
const DEFAULT_MTU: u32 = 1280;
|
||||
|
||||
impl Tun {
|
||||
pub fn new() -> Result<Self> {
|
||||
const TUNNEL_UUID: &str = "e9245bc1-b8c1-44ca-ab1d-c6aad4f13b9c";
|
||||
@@ -62,7 +52,8 @@ impl Tun {
|
||||
// The Windows client, in `wintun_install` hashes the DLL at startup, before calling connlib, so it's unlikely for the DLL to be accidentally corrupted by the time we get here.
|
||||
let path = connlib_shared::windows::wintun_dll_path()?;
|
||||
let wintun = unsafe { wintun::load_from_path(path) }?;
|
||||
let uuid = uuid::Uuid::from_str(TUNNEL_UUID).expect("static UUID to parse correctly");
|
||||
let uuid =
|
||||
uuid::Uuid::from_str(TUNNEL_UUID).expect("static UUID should always parse correctly");
|
||||
let adapter =
|
||||
match wintun::Adapter::create(&wintun, "Firezone", TUNNEL_NAME, Some(uuid.as_u128())) {
|
||||
Ok(x) => x,
|
||||
@@ -97,7 +88,6 @@ impl Tun {
|
||||
let recv_thread = start_recv_thread(packet_tx, Arc::clone(&session))?;
|
||||
|
||||
Ok(Self {
|
||||
adapter,
|
||||
iface_idx,
|
||||
_recv_thread: recv_thread,
|
||||
packet_rx,
|
||||
@@ -106,52 +96,16 @@ impl Tun {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: &InterfaceConfig, dns_config: &[IpAddr]) -> Result<()> {
|
||||
tracing::debug!("Setting our IPv4 = {}", config.ipv4);
|
||||
tracing::debug!("Setting our IPv6 = {}", config.ipv6);
|
||||
|
||||
// TODO: See if there's a good Win32 API for this
|
||||
// Using netsh directly instead of wintun's `set_network_addresses_tuple` because their code doesn't work for IPv6
|
||||
Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("interface")
|
||||
.arg("ipv4")
|
||||
.arg("set")
|
||||
.arg("address")
|
||||
.arg(format!("name=\"{TUNNEL_NAME}\""))
|
||||
.arg("source=static")
|
||||
.arg(format!("address={}", config.ipv4))
|
||||
.arg("mask=255.255.255.255")
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("interface")
|
||||
.arg("ipv6")
|
||||
.arg("set")
|
||||
.arg("address")
|
||||
.arg(format!("interface=\"{TUNNEL_NAME}\""))
|
||||
.arg(format!("address={}", config.ipv6))
|
||||
.stdout(Stdio::null())
|
||||
.status()?;
|
||||
|
||||
tracing::debug!("Our IPs are {:?}", self.adapter.get_addresses()?);
|
||||
|
||||
let iface_idx = self.adapter.get_adapter_index()?;
|
||||
|
||||
connlib_shared::windows::dns::change(dns_config, iface_idx)
|
||||
.expect("Should be able to control DNS");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// It's okay if this blocks until the route is added in the OS.
|
||||
pub fn set_routes(
|
||||
&mut self,
|
||||
new_routes: HashSet<IpNetwork>,
|
||||
_callbacks: &impl Callbacks,
|
||||
) -> Result<()> {
|
||||
if new_routes == self.routes {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for new_route in new_routes.difference(&self.routes) {
|
||||
self.add_route(*new_route)?;
|
||||
}
|
||||
@@ -160,36 +114,9 @@ impl Tun {
|
||||
self.remove_route(*old_route)?;
|
||||
}
|
||||
|
||||
self.routes = new_routes;
|
||||
|
||||
// TODO: Might be calling this more often than it needs
|
||||
connlib_shared::windows::dns::flush().expect("Should be able to flush Windows' DNS cache");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// It's okay if this blocks until the route is added in the OS.
|
||||
fn add_route(&self, route: IpNetwork) -> Result<()> {
|
||||
const DUPLICATE_ERR: u32 = 0x80071392;
|
||||
let entry = self.forward_entry(route);
|
||||
|
||||
// SAFETY: Windows shouldn't store the reference anywhere, it's just a way to pass lots of arguments at once. And no other thread sees this variable.
|
||||
match unsafe { CreateIpForwardEntry2(&entry) }.ok() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.code().0 as u32 == DUPLICATE_ERR => {
|
||||
tracing::debug!(%route, "Failed to add duplicate route, ignoring");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
// It's okay if this blocks until the route is removed in the OS.
|
||||
fn remove_route(&self, route: IpNetwork) -> Result<()> {
|
||||
let entry = self.forward_entry(route);
|
||||
|
||||
// SAFETY: Windows shouldn't store the reference anywhere, it's just a way to pass lots of arguments at once. And no other thread sees this variable.
|
||||
unsafe { DeleteIpForwardEntry2(&entry) }.ok()?;
|
||||
flush_dns().expect("Should be able to flush Windows' DNS cache");
|
||||
self.routes = new_routes;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -247,6 +174,31 @@ impl Tun {
|
||||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
// It's okay if this blocks until the route is added in the OS.
|
||||
fn add_route(&self, route: IpNetwork) -> Result<()> {
|
||||
const DUPLICATE_ERR: u32 = 0x80071392;
|
||||
let entry = self.forward_entry(route);
|
||||
|
||||
// SAFETY: Windows shouldn't store the reference anywhere, it's just a way to pass lots of arguments at once. And no other thread sees this variable.
|
||||
match unsafe { CreateIpForwardEntry2(&entry) }.ok() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.code().0 as u32 == DUPLICATE_ERR => {
|
||||
tracing::debug!(%route, "Failed to add duplicate route, ignoring");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
// It's okay if this blocks until the route is removed in the OS.
|
||||
fn remove_route(&self, route: IpNetwork) -> Result<()> {
|
||||
let entry = self.forward_entry(route);
|
||||
|
||||
// SAFETY: Windows shouldn't store the reference anywhere, it's just a way to pass lots of arguments at once. And no other thread sees this variable.
|
||||
unsafe { DeleteIpForwardEntry2(&entry) }.ok()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn forward_entry(&self, route: IpNetwork) -> MIB_IPFORWARD_ROW2 {
|
||||
let mut row = MIB_IPFORWARD_ROW2::default();
|
||||
// SAFETY: Windows shouldn't store the reference anywhere, it's just setting defaults
|
||||
@@ -271,6 +223,16 @@ impl Tun {
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush Windows' system-wide DNS cache
|
||||
pub(crate) fn flush_dns() -> Result<()> {
|
||||
tracing::info!("Flushing Windows DNS cache");
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-Command", "Clear-DnsClientCache"])
|
||||
.status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_recv_thread(
|
||||
packet_tx: mpsc::Sender<wintun::Packet>,
|
||||
session: Arc<wintun::Session>,
|
||||
|
||||
@@ -17,9 +17,6 @@ use std::collections::{HashSet, VecDeque};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const PEERS_IPV4: &str = "100.64.0.0/11";
|
||||
const PEERS_IPV6: &str = "fd00:2021:1111::/107";
|
||||
|
||||
const EXPIRE_RESOURCES_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
impl<CB> GatewayTunnel<CB>
|
||||
@@ -33,10 +30,6 @@ where
|
||||
self.io
|
||||
.device_mut()
|
||||
.set_config(config, vec![], &callbacks)?;
|
||||
self.io.device_mut().set_routes(
|
||||
HashSet::from([PEERS_IPV4.parse().unwrap(), PEERS_IPV6.parse().unwrap()]),
|
||||
&callbacks,
|
||||
)?;
|
||||
|
||||
let name = self.io.device_mut().name().to_owned();
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ const MTU: usize = 1280;
|
||||
|
||||
const REALM: &str = "firezone";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const FIREZONE_MARK: u32 = 0xfd002021;
|
||||
|
||||
pub type GatewayTunnel<CB> = Tunnel<CB, GatewayState>;
|
||||
pub type ClientTunnel<CB> = Tunnel<CB, ClientState>;
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ fn make_socket(addr: impl Into<SocketAddr>) -> Result<std::net::UdpSocket> {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
socket.set_mark(crate::FIREZONE_MARK)?;
|
||||
socket.set_mark(connlib_shared::tun_device_manager::platform::FIREZONE_MARK)?;
|
||||
}
|
||||
|
||||
// Note: for AF_INET sockets IPV6_V6ONLY is not a valid flag
|
||||
|
||||
@@ -3,10 +3,11 @@ use crate::messages::InitGateway;
|
||||
use anyhow::{Context, Result};
|
||||
use backoff::ExponentialBackoffBuilder;
|
||||
use clap::Parser;
|
||||
use connlib_shared::{get_user_agent, keypair, Callbacks, LoginUrl, StaticSecret};
|
||||
use connlib_shared::{get_user_agent, keypair, Callbacks, Cidrv4, Cidrv6, LoginUrl, StaticSecret};
|
||||
use firezone_cli_utils::{setup_global_subscriber, CommonArgs};
|
||||
use firezone_tunnel::{GatewayTunnel, Sockets};
|
||||
use futures::{future, TryFutureExt};
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use secrecy::{Secret, SecretString};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::Infallible;
|
||||
@@ -21,6 +22,8 @@ mod eventloop;
|
||||
mod messages;
|
||||
|
||||
const ID_PATH: &str = "/var/lib/firezone/gateway_id";
|
||||
const PEERS_IPV4: &str = "100.64.0.0/11";
|
||||
const PEERS_IPV6: &str = "fd00:2021:1111::/107";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -111,6 +114,16 @@ async fn run(login: LoginUrl, private_key: StaticSecret) -> Result<Infallible> {
|
||||
tunnel
|
||||
.set_interface(&init.interface)
|
||||
.context("Failed to set interface")?;
|
||||
let mut tun_device = connlib_shared::tun_device_manager::TunDeviceManager::new()?;
|
||||
tun_device
|
||||
.set_ips(init.interface.ipv4, init.interface.ipv6)
|
||||
.await?;
|
||||
tun_device
|
||||
.set_routes(
|
||||
vec![Cidrv4::from(PEERS_IPV4.parse::<Ipv4Network>().unwrap())],
|
||||
vec![Cidrv6::from(PEERS_IPV6.parse::<Ipv6Network>().unwrap())],
|
||||
)
|
||||
.await?;
|
||||
tunnel.update_relays(HashSet::default(), init.relays);
|
||||
|
||||
let mut eventloop = Eventloop::new(tunnel, portal);
|
||||
|
||||
@@ -13,7 +13,6 @@ mod gui;
|
||||
mod ipc;
|
||||
mod logging;
|
||||
mod network_changes;
|
||||
mod resolvers;
|
||||
mod settings;
|
||||
mod updates;
|
||||
mod uptime;
|
||||
|
||||
@@ -79,7 +79,7 @@ pub async fn open(url: &url::Url) -> Result<()> {
|
||||
}
|
||||
|
||||
fn pipe_path() -> String {
|
||||
firezone_headless_client::platform::named_pipe_path(&format!("{BUNDLE_ID}.deep_link"))
|
||||
firezone_headless_client::windows::named_pipe_path(&format!("{BUNDLE_ID}.deep_link"))
|
||||
}
|
||||
|
||||
/// Registers the current exe as the handler for our deep link scheme.
|
||||
|
||||
@@ -191,10 +191,6 @@ pub(crate) fn run(cli: client::Cli) -> Result<(), Error> {
|
||||
|
||||
if let Some(client::Cmd::SmokeTest) = &cli.command {
|
||||
let ctlr_tx = ctlr_tx.clone();
|
||||
// Generate the device ID so that the device ID code is covered
|
||||
// by the smoke test.
|
||||
// This is redundant since we will also lazily generate it when we boot connlib.
|
||||
connlib_shared::device_id::get().ok();
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = smoke_test(ctlr_tx).await {
|
||||
tracing::error!(?error, "Error during smoke test");
|
||||
@@ -495,7 +491,7 @@ impl Controller {
|
||||
let api_url = self.advanced_settings.api_url.clone();
|
||||
tracing::info!(api_url = api_url.to_string(), "Starting connlib...");
|
||||
|
||||
let mut connlib = ipc::Client::connect(
|
||||
let connlib = ipc::Client::connect(
|
||||
api_url.as_str(),
|
||||
token,
|
||||
callback_handler.clone(),
|
||||
@@ -503,10 +499,6 @@ impl Controller {
|
||||
)
|
||||
.await?;
|
||||
|
||||
connlib
|
||||
.set_dns(client::resolvers::get().unwrap_or_default())
|
||||
.await?;
|
||||
|
||||
self.session = Some(Session {
|
||||
callback_handler,
|
||||
connlib,
|
||||
|
||||
@@ -41,7 +41,7 @@ impl CallbackHandler {
|
||||
.expect("controller channel failed");
|
||||
}
|
||||
|
||||
fn on_set_interface_config(&self) {
|
||||
fn on_tunnel_ready(&self) {
|
||||
self.ctlr_tx
|
||||
.try_send(ControllerRequest::TunnelReady)
|
||||
.expect("controller channel failed");
|
||||
@@ -103,14 +103,8 @@ impl Client {
|
||||
error_msg,
|
||||
is_authentication_error,
|
||||
} => callback_handler.on_disconnect(error_msg, is_authentication_error),
|
||||
IpcServerMsg::OnTunnelReady => callback_handler.on_tunnel_ready(),
|
||||
IpcServerMsg::OnUpdateResources(v) => callback_handler.on_update_resources(v),
|
||||
IpcServerMsg::OnSetInterfaceConfig {
|
||||
ipv4: _,
|
||||
ipv6: _,
|
||||
dns: _,
|
||||
} => {
|
||||
callback_handler.on_set_interface_config();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Not implemented for Linux yet
|
||||
|
||||
use anyhow::Result;
|
||||
use firezone_headless_client::dns_control::system_resolvers_for_gui;
|
||||
use std::net::IpAddr;
|
||||
use tokio::time::Interval;
|
||||
|
||||
@@ -47,7 +48,7 @@ impl DnsListener {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
interval: create_interval(),
|
||||
last_seen: crate::client::resolvers::get().unwrap_or_default(),
|
||||
last_seen: system_resolvers_for_gui().unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ impl DnsListener {
|
||||
loop {
|
||||
self.interval.tick().await;
|
||||
tracing::trace!("Checking for DNS changes");
|
||||
let new = crate::client::resolvers::get().unwrap_or_default();
|
||||
let new = system_resolvers_for_gui().unwrap_or_default();
|
||||
if new != self.last_seen {
|
||||
self.last_seen.clone_from(&new);
|
||||
return Ok(new);
|
||||
|
||||
@@ -486,7 +486,10 @@ mod async_dns {
|
||||
r = self.listener_4.notified() => r?,
|
||||
r = self.listener_6.notified() => r?,
|
||||
}
|
||||
Ok(crate::client::resolvers::get().unwrap_or_default())
|
||||
Ok(
|
||||
firezone_headless_client::dns_control::system_resolvers_for_gui()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//! Module to handle Windows system-wide DNS resolvers
|
||||
|
||||
pub(crate) use imp::get;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
use anyhow::Result;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn get() -> Result<Vec<IpAddr>> {
|
||||
firezone_headless_client::platform::get_system_default_resolvers_systemd_resolved()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use anyhow::Result;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn get() -> Result<Vec<IpAddr>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
use anyhow::Result;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn get() -> Result<Vec<IpAddr>> {
|
||||
firezone_headless_client::platform::system_resolvers()
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ authors = ["Firezone, Inc."]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0" }
|
||||
atomicwrites = "0.4.3" # Needed to safely backup `/etc/resolv.conf` and write the device ID on behalf of `gui-client`
|
||||
clap = { version = "4.5", features = ["derive", "env", "string"] }
|
||||
connlib-client-shared = { workspace = true }
|
||||
connlib-shared = { workspace = true }
|
||||
@@ -16,6 +17,7 @@ firezone-cli-utils = { workspace = true }
|
||||
futures = "0.3.30"
|
||||
git-version = "0.3.9"
|
||||
humantime = "2.1"
|
||||
ip_network = { version = "0.4", default-features = false }
|
||||
secrecy = { workspace = true }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
@@ -26,11 +28,19 @@ 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 }
|
||||
uuid = { version = "1.7", default-features = false, features = ["std", "v4", "serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
mutants = "0.0.3" # Needed to mark functions as exempt from `cargo-mutants` testing
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dirs = "5.0.1"
|
||||
nix = { version = "0.28.0", features = ["fs", "user"] }
|
||||
resolv-conf = "0.7.0"
|
||||
rtnetlink = { workspace = true }
|
||||
sd-notify = "0.4.1" # This is a pure Rust re-implementation, so it isn't vulnerable to CVE-2024-3094
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
@@ -47,9 +57,14 @@ windows-service = "0.7.0"
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.56.0"
|
||||
features = [
|
||||
# For DNS control and route control
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
|
||||
# For named pipe IPC
|
||||
"Win32_Security",
|
||||
# For named pipe IPC
|
||||
"Win32_System_SystemServices",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
pub struct DeviceId {
|
||||
pub id: String,
|
||||
pub(crate) struct DeviceId {
|
||||
pub(crate) id: String,
|
||||
}
|
||||
|
||||
/// Returns the device ID, generating it and saving it to disk if needed.
|
||||
@@ -14,8 +14,8 @@ pub struct DeviceId {
|
||||
/// Returns: The UUID as a String, suitable for sending verbatim to `connlib_client_shared::Session::connect`.
|
||||
///
|
||||
/// Errors: If the disk is unwritable when initially generating the ID, or unwritable when re-generating an invalid ID.
|
||||
pub fn get() -> Result<DeviceId> {
|
||||
let dir = imp::path().context("Failed to compute path for firezone-id file")?;
|
||||
pub(crate) fn get() -> Result<DeviceId> {
|
||||
let dir = platform::path().context("Failed to compute path for firezone-id file")?;
|
||||
let path = dir.join("firezone-id.json");
|
||||
|
||||
// Try to read it from the disk
|
||||
@@ -60,14 +60,14 @@ impl DeviceIdJson {
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
mod imp {
|
||||
mod platform {
|
||||
pub(crate) fn path() -> Option<std::path::PathBuf> {
|
||||
panic!("This function is only implemented on Linux and Windows since those have pure-Rust clients")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
mod platform {
|
||||
use std::path::PathBuf;
|
||||
/// `/var/lib/$BUNDLE_ID/config/firezone-id`
|
||||
///
|
||||
@@ -82,14 +82,14 @@ mod imp {
|
||||
pub(crate) fn path() -> Option<PathBuf> {
|
||||
Some(
|
||||
PathBuf::from("/var/lib")
|
||||
.join(crate::BUNDLE_ID)
|
||||
.join(connlib_shared::BUNDLE_ID)
|
||||
.join("config"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
mod platform {
|
||||
use known_folders::{get_known_folder_path, KnownFolder};
|
||||
|
||||
/// e.g. `C:\ProgramData\dev.firezone.client\config`
|
||||
@@ -98,7 +98,7 @@ mod imp {
|
||||
pub(crate) fn path() -> Option<std::path::PathBuf> {
|
||||
Some(
|
||||
get_known_folder_path(KnownFolder::ProgramData)?
|
||||
.join(crate::BUNDLE_ID)
|
||||
.join(connlib_shared::BUNDLE_ID)
|
||||
.join("config"),
|
||||
)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ mod imp {
|
||||
mod tests {
|
||||
#[test]
|
||||
fn smoke() {
|
||||
let dir = super::imp::path().expect("should have gotten Some(path)");
|
||||
let dir = super::platform::path().expect("should have gotten Some(path)");
|
||||
assert!(dir
|
||||
.components()
|
||||
.any(|x| x == std::path::Component::Normal("dev.firezone.client".as_ref())));
|
||||
15
rust/headless-client/src/dns_control.rs
Normal file
15
rust/headless-client/src/dns_control.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
use linux as platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows as platform;
|
||||
|
||||
pub(crate) use platform::{deactivate, system_resolvers, DnsController};
|
||||
|
||||
// TODO: Move DNS and network change listening to the IPC service, so this won't
|
||||
// need to be public.
|
||||
pub use platform::system_resolvers_for_gui;
|
||||
204
rust/headless-client/src/dns_control/linux.rs
Normal file
204
rust/headless-client/src/dns_control/linux.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use connlib_shared::tun_device_manager::linux::IFACE_NAME;
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
|
||||
mod etc_resolv_conf;
|
||||
|
||||
const FIREZONE_DNS_CONTROL: &str = "FIREZONE_DNS_CONTROL";
|
||||
|
||||
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {
|
||||
get_system_default_resolvers_systemd_resolved()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum DnsControlMethod {
|
||||
/// Back up `/etc/resolv.conf` and replace it with our own
|
||||
///
|
||||
/// Only suitable for the Alpine CI containers and maybe something like an
|
||||
/// embedded system
|
||||
EtcResolvConf,
|
||||
/// Cooperate with NetworkManager (TODO)
|
||||
NetworkManager,
|
||||
/// Cooperate with `systemd-resolved`
|
||||
///
|
||||
/// Suitable for most Ubuntu systems, probably
|
||||
Systemd,
|
||||
}
|
||||
|
||||
pub(crate) struct DnsController {
|
||||
dns_control_method: Option<DnsControlMethod>,
|
||||
}
|
||||
|
||||
impl Drop for DnsController {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Reverting DNS control...");
|
||||
if let Some(DnsControlMethod::EtcResolvConf) = self.dns_control_method {
|
||||
// TODO: Check that nobody else modified the file while we were running.
|
||||
etc_resolv_conf::revert().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsController {
|
||||
pub(crate) fn new() -> Self {
|
||||
// We'll remove `get_dns_control_from_env` in #5068
|
||||
let dns_control_method = get_dns_control_from_env();
|
||||
tracing::info!(?dns_control_method);
|
||||
|
||||
Self { dns_control_method }
|
||||
}
|
||||
|
||||
/// Set the computer's system-wide DNS servers
|
||||
///
|
||||
/// The `mut` in `&mut self` is not needed by Rust's rules, but
|
||||
/// it would be bad if this was called from 2 threads at once.
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: &[IpAddr]) -> Result<()> {
|
||||
match self.dns_control_method {
|
||||
None => Ok(()),
|
||||
Some(DnsControlMethod::EtcResolvConf) => etc_resolv_conf::configure(dns_config).await,
|
||||
Some(DnsControlMethod::NetworkManager) => configure_network_manager(dns_config),
|
||||
Some(DnsControlMethod::Systemd) => configure_systemd_resolved(dns_config).await,
|
||||
}
|
||||
.context("Failed to control DNS")
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads FIREZONE_DNS_CONTROL. Returns None if invalid or not set
|
||||
fn get_dns_control_from_env() -> Option<DnsControlMethod> {
|
||||
match std::env::var(FIREZONE_DNS_CONTROL).as_deref() {
|
||||
Ok("etc-resolv-conf") => Some(DnsControlMethod::EtcResolvConf),
|
||||
Ok("network-manager") => Some(DnsControlMethod::NetworkManager),
|
||||
Ok("systemd-resolved") => Some(DnsControlMethod::Systemd),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_network_manager(_dns_config: &[IpAddr]) -> Result<()> {
|
||||
anyhow::bail!("DNS control with NetworkManager is not implemented yet",)
|
||||
}
|
||||
|
||||
async fn configure_systemd_resolved(dns_config: &[IpAddr]) -> Result<()> {
|
||||
let status = tokio::process::Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.arg(IFACE_NAME)
|
||||
.args(dns_config.iter().map(ToString::to_string))
|
||||
.status()
|
||||
.await
|
||||
.context("`resolvectl dns` didn't run")?;
|
||||
if !status.success() {
|
||||
bail!("`resolvectl dns` returned non-zero");
|
||||
}
|
||||
|
||||
let status = tokio::process::Command::new("resolvectl")
|
||||
.arg("domain")
|
||||
.arg(IFACE_NAME)
|
||||
.arg("~.")
|
||||
.status()
|
||||
.await
|
||||
.context("`resolvectl domain` didn't run")?;
|
||||
if !status.success() {
|
||||
bail!("`resolvectl domain` returned non-zero");
|
||||
}
|
||||
|
||||
tracing::info!(?dns_config, "Configured DNS sentinels with `resolvectl`");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn system_resolvers() -> Result<Vec<IpAddr>> {
|
||||
match crate::dns_control::platform::get_dns_control_from_env() {
|
||||
None => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::EtcResolvConf) => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::NetworkManager) => get_system_default_resolvers_network_manager(),
|
||||
Some(DnsControlMethod::Systemd) => get_system_default_resolvers_systemd_resolved(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_system_default_resolvers_resolv_conf() -> Result<Vec<IpAddr>> {
|
||||
// Assume that `configure_resolv_conf` has run in `tun_linux.rs`
|
||||
|
||||
let s = std::fs::read_to_string(etc_resolv_conf::ETC_RESOLV_CONF_BACKUP)
|
||||
.or_else(|_| std::fs::read_to_string(etc_resolv_conf::ETC_RESOLV_CONF))
|
||||
.context("`resolv.conf` should be readable")?;
|
||||
let parsed = resolv_conf::Config::parse(s).context("`resolv.conf` should be parsable")?;
|
||||
|
||||
// Drop the scoping info for IPv6 since connlib doesn't take it
|
||||
let nameservers = parsed
|
||||
.nameservers
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect();
|
||||
Ok(nameservers)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn get_system_default_resolvers_network_manager() -> Result<Vec<IpAddr>> {
|
||||
tracing::error!("get_system_default_resolvers_network_manager not implemented yet");
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Returns the DNS servers listed in `resolvectl dns`
|
||||
fn get_system_default_resolvers_systemd_resolved() -> Result<Vec<IpAddr>> {
|
||||
// Unfortunately systemd-resolved does not have a machine-readable
|
||||
// text output for this command: <https://github.com/systemd/systemd/issues/29755>
|
||||
//
|
||||
// The officially supported way is probably to use D-Bus.
|
||||
let output = std::process::Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.output()
|
||||
.context("Failed to run `resolvectl dns` and read output")?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("`resolvectl dns` returned non-zero exit code");
|
||||
}
|
||||
let output = String::from_utf8(output.stdout).context("`resolvectl` output was not UTF-8")?;
|
||||
Ok(parse_resolvectl_output(&output))
|
||||
}
|
||||
|
||||
/// Parses the text output of `resolvectl dns`
|
||||
///
|
||||
/// Cannot fail. If the parsing code is wrong, the IP address vec will just be incomplete.
|
||||
fn parse_resolvectl_output(s: &str) -> Vec<IpAddr> {
|
||||
s.lines()
|
||||
.flat_map(|line| line.split(' '))
|
||||
.filter_map(|word| IpAddr::from_str(word).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Does nothing on Linux, needed to match the Windows interface
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn deactivate() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[test]
|
||||
fn parse_resolvectl_output() {
|
||||
let cases = [
|
||||
// WSL
|
||||
(
|
||||
r"Global: 172.24.80.1
|
||||
Link 2 (eth0):
|
||||
Link 3 (docker0):
|
||||
Link 24 (br-fc0b71997a3c):
|
||||
Link 25 (br-0c129dafb204):
|
||||
Link 26 (br-e67e83b19dce):
|
||||
",
|
||||
[IpAddr::from([172, 24, 80, 1])],
|
||||
),
|
||||
// Ubuntu 20.04
|
||||
(
|
||||
r"Global:
|
||||
Link 2 (enp0s3): 192.168.1.1",
|
||||
[IpAddr::from([192, 168, 1, 1])],
|
||||
),
|
||||
];
|
||||
|
||||
for (i, (input, expected)) in cases.iter().enumerate() {
|
||||
let actual = super::parse_resolvectl_output(input);
|
||||
assert_eq!(actual, expected, "Case {i} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::{io::Write, net::IpAddr, path::PathBuf};
|
||||
|
||||
pub const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
|
||||
pub const ETC_RESOLV_CONF_BACKUP: &str = "/etc/resolv.conf.before-firezone";
|
||||
pub(crate) const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
|
||||
pub(crate) const ETC_RESOLV_CONF_BACKUP: &str = "/etc/resolv.conf.before-firezone";
|
||||
/// Used to figure out whether we crashed on our last run or not.
|
||||
///
|
||||
/// If we did crash, we need to restore the system-wide DNS from the backup file.
|
||||
@@ -11,7 +11,7 @@ const MAGIC_HEADER: &str = "# BEGIN Firezone DNS configuration";
|
||||
|
||||
// Wanted these args to have names so they don't get mixed up
|
||||
#[derive(Clone)]
|
||||
pub struct ResolvPaths {
|
||||
pub(crate) struct ResolvPaths {
|
||||
resolv: PathBuf,
|
||||
backup: PathBuf,
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl Default for ResolvPaths {
|
||||
/// This is async because it's called in a Tokio context and it's nice to use their
|
||||
/// `fs` module
|
||||
#[cfg_attr(test, mutants::skip)] // Would modify system-wide `/etc/resolv.conf`
|
||||
pub async fn configure(dns_config: &[IpAddr]) -> Result<()> {
|
||||
pub(crate) async fn configure(dns_config: &[IpAddr]) -> Result<()> {
|
||||
configure_at_paths(dns_config, &ResolvPaths::default()).await
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub async fn configure(dns_config: &[IpAddr]) -> Result<()> {
|
||||
///
|
||||
/// Must be sync because it's called in `Tun::drop`
|
||||
#[cfg_attr(test, mutants::skip)] // Would modify system-wide `/etc/resolv.conf`
|
||||
pub fn revert() -> Result<()> {
|
||||
pub(crate) fn revert() -> Result<()> {
|
||||
revert_at_paths(&ResolvPaths::default())
|
||||
}
|
||||
|
||||
128
rust/headless-client/src/dns_control/windows.rs
Normal file
128
rust/headless-client/src/dns_control/windows.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
//! Gives Firezone DNS privilege over other DNS resolvers on the system
|
||||
//!
|
||||
//! This uses NRPT and claims all domains, similar to the `systemd-resolved` control method
|
||||
//! on Linux.
|
||||
//! This allows us to "shadow" DNS resolvers that are configured by the user or DHCP on
|
||||
//! physical interfaces, as long as they don't have any NRPT rules that outrank us.
|
||||
//!
|
||||
//! If Firezone crashes, restarting Firezone and closing it gracefully will resume
|
||||
//! normal DNS operation. The Powershell command to remove the NRPT rule can also be run
|
||||
//! by hand.
|
||||
//!
|
||||
//! The system default resolvers don't need to be reverted because they're never deleted.
|
||||
//!
|
||||
//! <https://superuser.com/a/1752670>
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use connlib_shared::windows::{CREATE_NO_WINDOW, TUNNEL_NAME};
|
||||
use std::{net::IpAddr, os::windows::process::CommandExt, process::Command};
|
||||
|
||||
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {
|
||||
system_resolvers()
|
||||
}
|
||||
|
||||
pub(crate) struct DnsController {}
|
||||
|
||||
// Unique magic number that we can use to delete our well-known NRPT rule.
|
||||
// Copied from the deep link schema
|
||||
const FZ_MAGIC: &str = "firezone-fd0020211111";
|
||||
|
||||
impl Drop for DnsController {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = deactivate() {
|
||||
tracing::error!(?error, "Failed to deactivate DNS control");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsController {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// Set the computer's system-wide DNS servers
|
||||
///
|
||||
/// There's a gap in this because on Windows we deactivate and re-activate control.
|
||||
///
|
||||
/// The `mut` in `&mut self` is not needed by Rust's rules, but
|
||||
/// it would be bad if this was called from 2 threads at once.
|
||||
///
|
||||
/// Must be async to match the Linux signature
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: &[IpAddr]) -> Result<()> {
|
||||
deactivate().context("Failed to deactivate DNS control")?;
|
||||
activate(dns_config).context("Failed to activate DNS control")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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)
|
||||
}
|
||||
|
||||
/// Tells Windows to send all DNS queries to our sentinels
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `dns_config_string`: Comma-separated IP addresses of DNS servers, e.g. "1.1.1.1,8.8.8.8"
|
||||
pub(crate) fn activate(dns_config: &[IpAddr]) -> Result<()> {
|
||||
let dns_config_string = dns_config
|
||||
.iter()
|
||||
.map(|ip| format!("\"{ip}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
// Set our DNS IP as the DNS server for our interface
|
||||
// TODO: Known issue where web browsers will keep a connection open to a site,
|
||||
// using QUIC, HTTP/2, or even HTTP/1.1, and so they won't resolve the DNS
|
||||
// again unless you let that connection time out:
|
||||
// <https://github.com/firezone/firezone/issues/3113#issuecomment-1882096111>
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("-Command")
|
||||
.arg(format!(
|
||||
"Set-DnsClientServerAddress {TUNNEL_NAME} -ServerAddresses({dns_config_string})"
|
||||
))
|
||||
.status()?;
|
||||
|
||||
tracing::info!("Activating DNS control");
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args([
|
||||
"-Command",
|
||||
"Add-DnsClientNrptRule",
|
||||
"-Namespace",
|
||||
".",
|
||||
"-Comment",
|
||||
FZ_MAGIC,
|
||||
"-NameServers",
|
||||
&dns_config_string,
|
||||
])
|
||||
.status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Must be `sync` so we can call it from `Drop`
|
||||
pub(crate) fn deactivate() -> Result<()> {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-Command", "Get-DnsClientNrptRule", "|"])
|
||||
.args(["where", "Comment", "-eq", FZ_MAGIC, "|"])
|
||||
.args(["foreach", "{"])
|
||||
.args(["Remove-DnsClientNrptRule", "-Name", "$_.Name", "-Force"])
|
||||
.args(["}"])
|
||||
.status()?;
|
||||
tracing::info!("Deactivated DNS control");
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use clap::Parser;
|
||||
use connlib_client_shared::{
|
||||
file_logger, keypair, Callbacks, Error as ConnlibError, LoginUrl, Session, Sockets,
|
||||
};
|
||||
use connlib_shared::callbacks;
|
||||
use connlib_shared::{callbacks, tun_device_manager, Cidrv4, Cidrv6};
|
||||
use firezone_cli_utils::setup_global_subscriber;
|
||||
use futures::{future, SinkExt, StreamExt};
|
||||
use secrecy::SecretString;
|
||||
@@ -34,6 +34,9 @@ use platform::default_token_path;
|
||||
/// Must be constructed inside a Tokio runtime context.
|
||||
use platform::Signals;
|
||||
|
||||
/// Generate a persistent device ID, stores it to disk, and reads it back.
|
||||
pub(crate) mod device_id;
|
||||
pub mod dns_control;
|
||||
pub mod known_dirs;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -44,7 +47,7 @@ pub use linux as platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows as platform;
|
||||
pub(crate) use windows as platform;
|
||||
|
||||
/// Only used on Linux
|
||||
pub const FIREZONE_GROUP: &str = "firezone-client";
|
||||
@@ -57,7 +60,7 @@ pub const FIREZONE_GROUP: &str = "firezone-client";
|
||||
/// * `g` doesn't mean anything
|
||||
/// * `ed5437c88` is the Git commit hash
|
||||
/// * `-modified` is present if the working dir has any changes from that commit number
|
||||
pub const GIT_VERSION: &str = git_version::git_version!(
|
||||
pub(crate) const GIT_VERSION: &str = git_version::git_version!(
|
||||
args = ["--always", "--dirty=-modified", "--tags"],
|
||||
fallback = "unknown"
|
||||
);
|
||||
@@ -83,7 +86,7 @@ struct Cli {
|
||||
env = "FIREZONE_API_URL",
|
||||
default_value = "wss://api.firezone.dev"
|
||||
)]
|
||||
pub api_url: url::Url,
|
||||
api_url: url::Url,
|
||||
|
||||
/// Check the configuration and return 0 before connecting to the API
|
||||
///
|
||||
@@ -101,7 +104,7 @@ struct Cli {
|
||||
// AKA `device_id` in the Windows and Linux GUI clients
|
||||
// Generated automatically if not provided
|
||||
#[arg(short = 'i', long, env = "FIREZONE_ID")]
|
||||
pub firezone_id: Option<String>,
|
||||
firezone_id: Option<String>,
|
||||
|
||||
/// Token generated by the portal to authorize websocket connection.
|
||||
// systemd recommends against passing secrets through env vars:
|
||||
@@ -170,6 +173,19 @@ pub enum IpcClientMsg {
|
||||
SetDns(Vec<IpAddr>),
|
||||
}
|
||||
|
||||
enum InternalServerMsg {
|
||||
Ipc(IpcServerMsg),
|
||||
OnSetInterfaceConfig {
|
||||
ipv4: Ipv4Addr,
|
||||
ipv6: Ipv6Addr,
|
||||
dns: Vec<IpAddr>,
|
||||
},
|
||||
OnUpdateRoutes {
|
||||
ipv4: Vec<Cidrv4>,
|
||||
ipv6: Vec<Cidrv6>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub enum IpcServerMsg {
|
||||
Ok,
|
||||
@@ -177,11 +193,7 @@ pub enum IpcServerMsg {
|
||||
error_msg: String,
|
||||
is_authentication_error: bool,
|
||||
},
|
||||
OnSetInterfaceConfig {
|
||||
ipv4: Ipv4Addr,
|
||||
ipv6: Ipv6Addr,
|
||||
dns: Vec<IpAddr>,
|
||||
},
|
||||
OnTunnelReady,
|
||||
OnUpdateResources(Vec<callbacks::ResourceDescription>),
|
||||
}
|
||||
|
||||
@@ -235,7 +247,7 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
// AKA "Device ID", not the Firezone slug
|
||||
let firezone_id = match cli.firezone_id {
|
||||
Some(id) => id,
|
||||
None => connlib_shared::device_id::get().context("Could not get `firezone_id` from CLI, could not read it from disk, could not generate it and save it to disk")?.id,
|
||||
None => device_id::get().context("Could not get `firezone_id` from CLI, could not read it from disk, could not generate it and save it to disk")?.id,
|
||||
};
|
||||
|
||||
let (private_key, public_key) = keypair();
|
||||
@@ -252,10 +264,8 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO: Can theoretically be removed the same way the Windows IPC service works,
|
||||
// using an abort handle
|
||||
let (on_disconnect_tx, mut on_disconnect_rx) = mpsc::channel(1);
|
||||
let callback_handler = CallbackHandler { on_disconnect_tx };
|
||||
let (cb_tx, mut cb_rx) = mpsc::channel(10);
|
||||
let callback_handler = CallbackHandler { cb_tx };
|
||||
|
||||
platform::setup_before_connlib()?;
|
||||
let session = Session::connect(
|
||||
@@ -268,13 +278,16 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
rt.handle().clone(),
|
||||
);
|
||||
// TODO: this should be added dynamically
|
||||
session.set_dns(platform::system_resolvers().unwrap_or_default());
|
||||
session.set_dns(dns_control::system_resolvers().unwrap_or_default());
|
||||
platform::notify_service_controller()?;
|
||||
|
||||
let result = rt.block_on(async {
|
||||
let mut dns_controller = dns_control::DnsController::new();
|
||||
let mut tun_device = tun_device_manager::TunDeviceManager::new()?;
|
||||
let mut signals = Signals::new()?;
|
||||
|
||||
loop {
|
||||
match future::select(pin!(signals.recv()), pin!(on_disconnect_rx.recv())).await {
|
||||
match future::select(pin!(signals.recv()), pin!(cb_rx.recv())).await {
|
||||
future::Either::Left((SignalKind::Hangup, _)) => {
|
||||
tracing::info!("Caught Hangup signal");
|
||||
session.reconnect();
|
||||
@@ -284,11 +297,24 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
future::Either::Right((None, _)) => {
|
||||
return Err(anyhow::anyhow!("on_disconnect_rx unexpectedly ran empty"));
|
||||
}
|
||||
future::Either::Right((Some(error), _)) => {
|
||||
return Err(anyhow!(error).context("Firezone disconnected"))
|
||||
return Err(anyhow::anyhow!("cb_rx unexpectedly ran empty"));
|
||||
}
|
||||
future::Either::Right((Some(msg), _)) => match msg {
|
||||
InternalServerMsg::Ipc(IpcServerMsg::OnDisconnect {
|
||||
error_msg,
|
||||
is_authentication_error: _,
|
||||
}) => return Err(anyhow!(error_msg).context("Firezone disconnected")),
|
||||
InternalServerMsg::Ipc(IpcServerMsg::Ok)
|
||||
| InternalServerMsg::Ipc(IpcServerMsg::OnTunnelReady)
|
||||
| InternalServerMsg::Ipc(IpcServerMsg::OnUpdateResources(_)) => {}
|
||||
InternalServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
tun_device.set_ips(ipv4, ipv6).await?;
|
||||
dns_controller.set_dns(&dns).await?;
|
||||
}
|
||||
InternalServerMsg::OnUpdateRoutes { ipv4, ipv6 } => {
|
||||
tun_device.set_routes(ipv4, ipv6).await?
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -342,23 +368,23 @@ pub(crate) fn run_debug_ipc_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CallbackHandlerIpc {
|
||||
cb_tx: mpsc::Sender<IpcServerMsg>,
|
||||
struct CallbackHandler {
|
||||
cb_tx: mpsc::Sender<InternalServerMsg>,
|
||||
}
|
||||
|
||||
impl Callbacks for CallbackHandlerIpc {
|
||||
fn on_disconnect(&self, error: &ConnlibError) {
|
||||
impl Callbacks for CallbackHandler {
|
||||
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
|
||||
tracing::error!(?error, "Got `on_disconnect` from connlib");
|
||||
let is_authentication_error = if let ConnlibError::PortalConnectionFailed(error) = error {
|
||||
error.is_authentication_error()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnDisconnect {
|
||||
.try_send(InternalServerMsg::Ipc(IpcServerMsg::OnDisconnect {
|
||||
error_msg: error.to_string(),
|
||||
is_authentication_error: if let ConnlibError::PortalConnectionFailed(error) = error
|
||||
{
|
||||
error.is_authentication_error()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
})
|
||||
is_authentication_error,
|
||||
}))
|
||||
.expect("should be able to send OnDisconnect");
|
||||
}
|
||||
|
||||
@@ -370,7 +396,7 @@ impl Callbacks for CallbackHandlerIpc {
|
||||
) -> Option<i32> {
|
||||
tracing::info!("TunnelReady (on_set_interface_config)");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns })
|
||||
.try_send(InternalServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns })
|
||||
.expect("Should be able to send TunnelReady");
|
||||
None
|
||||
}
|
||||
@@ -378,15 +404,24 @@ impl Callbacks for CallbackHandlerIpc {
|
||||
fn on_update_resources(&self, resources: Vec<callbacks::ResourceDescription>) {
|
||||
tracing::debug!(len = resources.len(), "New resource list");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnUpdateResources(resources))
|
||||
.try_send(InternalServerMsg::Ipc(IpcServerMsg::OnUpdateResources(
|
||||
resources,
|
||||
)))
|
||||
.expect("Should be able to send OnUpdateResources");
|
||||
}
|
||||
|
||||
fn on_update_routes(&self, ipv4: Vec<Cidrv4>, ipv6: Vec<Cidrv6>) -> Option<i32> {
|
||||
self.cb_tx
|
||||
.try_send(InternalServerMsg::OnUpdateRoutes { ipv4, ipv6 })
|
||||
.expect("Should be able to send messages");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn ipc_listen() -> Result<std::convert::Infallible> {
|
||||
let mut server = platform::IpcServer::new().await?;
|
||||
loop {
|
||||
connlib_shared::deactivate_dns_control()?;
|
||||
dns_control::deactivate()?;
|
||||
let stream = server.next_client().await?;
|
||||
if let Err(error) = handle_ipc_client(stream).await {
|
||||
tracing::error!(?error, "Error while handling IPC client");
|
||||
@@ -398,17 +433,31 @@ async fn handle_ipc_client(stream: platform::IpcStream) -> Result<()> {
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
|
||||
let mut tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
let (cb_tx, mut cb_rx) = mpsc::channel(100);
|
||||
let (cb_tx, mut cb_rx) = mpsc::channel(10);
|
||||
|
||||
let send_task = tokio::spawn(async move {
|
||||
let mut dns_controller = dns_control::DnsController::new();
|
||||
let mut tun_device = tun_device_manager::TunDeviceManager::new()?;
|
||||
|
||||
while let Some(msg) = cb_rx.recv().await {
|
||||
tx.send(serde_json::to_string(&msg)?.into()).await?;
|
||||
match msg {
|
||||
InternalServerMsg::Ipc(msg) => tx.send(serde_json::to_string(&msg)?.into()).await?,
|
||||
InternalServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
tun_device.set_ips(ipv4, ipv6).await?;
|
||||
dns_controller.set_dns(&dns).await?;
|
||||
tx.send(serde_json::to_string(&IpcServerMsg::OnTunnelReady)?.into())
|
||||
.await?;
|
||||
}
|
||||
InternalServerMsg::OnUpdateRoutes { ipv4, ipv6 } => {
|
||||
tun_device.set_routes(ipv4, ipv6).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
let mut connlib = None;
|
||||
let callback_handler = CallbackHandlerIpc { cb_tx };
|
||||
let callback_handler = CallbackHandler { cb_tx };
|
||||
while let Some(msg) = rx.next().await {
|
||||
let msg = msg?;
|
||||
let msg: IpcClientMsg = serde_json::from_slice(&msg)?;
|
||||
@@ -417,8 +466,7 @@ async fn handle_ipc_client(stream: platform::IpcStream) -> Result<()> {
|
||||
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 device_id = device_id::get().context("Failed to read / create device ID")?;
|
||||
let (private_key, public_key) = keypair();
|
||||
|
||||
let login = LoginUrl::client(
|
||||
@@ -429,7 +477,7 @@ async fn handle_ipc_client(stream: platform::IpcStream) -> Result<()> {
|
||||
public_key.to_bytes(),
|
||||
)?;
|
||||
|
||||
connlib = Some(connlib_client_shared::Session::connect(
|
||||
let new_session = connlib_client_shared::Session::connect(
|
||||
login,
|
||||
Sockets::new(),
|
||||
private_key,
|
||||
@@ -437,7 +485,9 @@ async fn handle_ipc_client(stream: platform::IpcStream) -> Result<()> {
|
||||
callback_handler.clone(),
|
||||
Some(std::time::Duration::from_secs(60 * 60 * 24 * 30)),
|
||||
tokio::runtime::Handle::try_current()?,
|
||||
));
|
||||
);
|
||||
new_session.set_dns(dns_control::system_resolvers().unwrap_or_default());
|
||||
connlib = Some(new_session);
|
||||
}
|
||||
IpcClientMsg::Disconnect => {
|
||||
if let Some(connlib) = connlib.take() {
|
||||
@@ -464,28 +514,6 @@ enum SignalKind {
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CallbackHandler {
|
||||
/// Channel for an error message if connlib disconnects due to an error
|
||||
on_disconnect_tx: mpsc::Sender<String>,
|
||||
}
|
||||
|
||||
impl Callbacks for CallbackHandler {
|
||||
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
|
||||
// Convert the error to a String since we can't clone it
|
||||
self.on_disconnect_tx
|
||||
.try_send(error.to_string())
|
||||
.expect("should be able to tell the main thread that we disconnected");
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<callbacks::ResourceDescription>) {
|
||||
// See easily with `export RUST_LOG=firezone_headless_client=debug`
|
||||
for resource in &resources {
|
||||
tracing::debug!(?resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the token from disk if it was not in the environment
|
||||
///
|
||||
/// # Returns
|
||||
@@ -562,10 +590,6 @@ mod tests {
|
||||
fn cli() -> anyhow::Result<()> {
|
||||
let exe_name = "firezone-headless-client";
|
||||
|
||||
let actual = Cli::parse_from([exe_name]);
|
||||
assert_eq!(actual.api_url, Url::parse("wss://api.firezone.dev")?);
|
||||
assert!(!actual.check);
|
||||
|
||||
let actual = Cli::parse_from([exe_name, "--api-url", "wss://api.firez.one"]);
|
||||
assert_eq!(actual.api_url, Url::parse("wss://api.firez.one")?);
|
||||
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
use super::{CliCommon, SignalKind, FIREZONE_GROUP, TOKEN_ENV_KEY};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use connlib_client_shared::file_logger;
|
||||
use connlib_shared::linux::{etc_resolv_conf, get_dns_control_from_env, DnsControlMethod};
|
||||
use firezone_cli_utils::setup_global_subscriber;
|
||||
use futures::future::{select, Either};
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
pin::pin,
|
||||
str::FromStr,
|
||||
};
|
||||
use tokio::{
|
||||
net::{UnixListener, UnixStream},
|
||||
@@ -44,7 +41,7 @@ impl Signals {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_token_path() -> PathBuf {
|
||||
pub(crate) fn default_token_path() -> PathBuf {
|
||||
PathBuf::from("/etc")
|
||||
.join(connlib_shared::BUNDLE_ID)
|
||||
.join("token")
|
||||
@@ -81,65 +78,6 @@ pub(crate) fn check_token_permissions(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn system_resolvers() -> Result<Vec<IpAddr>> {
|
||||
match get_dns_control_from_env() {
|
||||
None => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::EtcResolvConf) => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::NetworkManager) => get_system_default_resolvers_network_manager(),
|
||||
Some(DnsControlMethod::Systemd) => get_system_default_resolvers_systemd_resolved(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_system_default_resolvers_resolv_conf() -> Result<Vec<IpAddr>> {
|
||||
// Assume that `configure_resolv_conf` has run in `tun_linux.rs`
|
||||
|
||||
let s = std::fs::read_to_string(etc_resolv_conf::ETC_RESOLV_CONF_BACKUP)
|
||||
.or_else(|_| std::fs::read_to_string(etc_resolv_conf::ETC_RESOLV_CONF))
|
||||
.context("`resolv.conf` should be readable")?;
|
||||
let parsed = resolv_conf::Config::parse(s).context("`resolv.conf` should be parsable")?;
|
||||
|
||||
// Drop the scoping info for IPv6 since connlib doesn't take it
|
||||
let nameservers = parsed
|
||||
.nameservers
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect();
|
||||
Ok(nameservers)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn get_system_default_resolvers_network_manager() -> Result<Vec<IpAddr>> {
|
||||
tracing::error!("get_system_default_resolvers_network_manager not implemented yet");
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Returns the DNS servers listed in `resolvectl dns`
|
||||
pub fn get_system_default_resolvers_systemd_resolved() -> Result<Vec<IpAddr>> {
|
||||
// Unfortunately systemd-resolved does not have a machine-readable
|
||||
// text output for this command: <https://github.com/systemd/systemd/issues/29755>
|
||||
//
|
||||
// The officially supported way is probably to use D-Bus.
|
||||
let output = std::process::Command::new("resolvectl")
|
||||
.arg("dns")
|
||||
.output()
|
||||
.context("Failed to run `resolvectl dns` and read output")?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("`resolvectl dns` returned non-zero exit code");
|
||||
}
|
||||
let output = String::from_utf8(output.stdout).context("`resolvectl` output was not UTF-8")?;
|
||||
Ok(parse_resolvectl_output(&output))
|
||||
}
|
||||
|
||||
/// Parses the text output of `resolvectl dns`
|
||||
///
|
||||
/// Cannot fail. If the parsing code is wrong, the IP address vec will just be incomplete.
|
||||
fn parse_resolvectl_output(s: &str) -> Vec<IpAddr> {
|
||||
s.lines()
|
||||
.flat_map(|line| line.split(' '))
|
||||
.filter_map(|word| IpAddr::from_str(word).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The path for our Unix Domain Socket
|
||||
///
|
||||
/// Docker keeps theirs in `/run` and also appears to use filesystem permissions
|
||||
@@ -215,6 +153,10 @@ impl IpcServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_service_controller() -> Result<()> {
|
||||
Ok(sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?)
|
||||
}
|
||||
|
||||
/// Platform-specific setup needed for connlib
|
||||
///
|
||||
/// On Linux this does nothing
|
||||
@@ -222,36 +164,3 @@ impl IpcServer {
|
||||
pub(crate) fn setup_before_connlib() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[test]
|
||||
fn parse_resolvectl_output() {
|
||||
let cases = [
|
||||
// WSL
|
||||
(
|
||||
r"Global: 172.24.80.1
|
||||
Link 2 (eth0):
|
||||
Link 3 (docker0):
|
||||
Link 24 (br-fc0b71997a3c):
|
||||
Link 25 (br-0c129dafb204):
|
||||
Link 26 (br-e67e83b19dce):
|
||||
",
|
||||
[IpAddr::from([172, 24, 80, 1])],
|
||||
),
|
||||
// Ubuntu 20.04
|
||||
(
|
||||
r"Global:
|
||||
Link 2 (enp0s3): 192.168.1.1",
|
||||
[IpAddr::from([192, 168, 1, 1])],
|
||||
),
|
||||
];
|
||||
|
||||
for (i, (input, expected)) in cases.iter().enumerate() {
|
||||
let actual = super::parse_resolvectl_output(input);
|
||||
assert_eq!(actual, expected, "Case {i} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use connlib_client_shared::file_logger;
|
||||
use connlib_shared::BUNDLE_ID;
|
||||
use std::{
|
||||
ffi::{c_void, OsString},
|
||||
net::IpAddr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
@@ -102,7 +101,7 @@ fn fallible_windows_service_run(arguments: Vec<OsString>) -> Result<()> {
|
||||
|
||||
// Fixes <https://github.com/firezone/firezone/issues/4899>,
|
||||
// DNS rules persisting after reboot
|
||||
connlib_shared::deactivate_dns_control().ok();
|
||||
crate::dns_control::deactivate().ok();
|
||||
|
||||
let ipc_task = rt.spawn(super::ipc_listen());
|
||||
let ipc_task_ah = ipc_task.abort_handle();
|
||||
@@ -242,22 +241,6 @@ pub fn pipe_path() -> String {
|
||||
named_pipe_path(&format!("{BUNDLE_ID}.ipc_service"))
|
||||
}
|
||||
|
||||
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
|
||||
@@ -267,6 +250,14 @@ pub fn named_pipe_path(id: &str) -> String {
|
||||
format!(r"\\.\pipe\{}", id)
|
||||
}
|
||||
|
||||
// Does nothing on Windows. On Linux this notifies systemd that we're ready.
|
||||
// When we eventually have a system service for the Windows Headless Client,
|
||||
// this could notify the Windows service controller too.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn notify_service_controller() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn setup_before_connlib() -> Result<()> {
|
||||
wintun_install::ensure_dll()?;
|
||||
Ok(())
|
||||
|
||||
@@ -41,7 +41,6 @@ pub(crate) fn ensure_dll() -> Result<PathBuf, Error> {
|
||||
let dir = path.parent().ok_or(Error::DllPathInvalid)?;
|
||||
std::fs::create_dir_all(dir).map_err(|_| Error::CreateDirAll)?;
|
||||
|
||||
// TODO: This log never shows up because `tracing` isn't started when we install wintun.dll
|
||||
tracing::info!(?path, "wintun.dll path");
|
||||
|
||||
// This hash check is not meant to protect against attacks. It only lets us skip redundant disk writes, and it updates the DLL if needed.
|
||||
|
||||
Reference in New Issue
Block a user