mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(headless-client): use systemd-resolved DNS control by default (#6163)
Closes #5063, supersedes #5850 Other refactors and changes made as part of this: - Adds the ability to disable DNS control on Windows - Removes the spooky-action-at-a-distance `from_env` functions that used to be buried in `tunnel` - `FIREZONE_DNS_CONTROL` is now a regular `clap` argument again --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com>
This commit is contained in:
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@@ -1782,6 +1782,7 @@ name = "firezone-bin-shared"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"connlib-shared",
|
||||
"futures",
|
||||
"ip-packet",
|
||||
|
||||
@@ -7,6 +7,7 @@ description = "Firezone-specific modules shared between binaries."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
connlib-shared = { workspace = true }
|
||||
futures = "0.3"
|
||||
ip-packet = { workspace = true }
|
||||
|
||||
@@ -4,10 +4,13 @@ use crate::FIREZONE_MARK;
|
||||
use nix::sys::socket::{setsockopt, sockopt};
|
||||
use socket_factory::{TcpSocket, UdpSocket};
|
||||
|
||||
const FIREZONE_DNS_CONTROL: &str = "FIREZONE_DNS_CONTROL";
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
|
||||
pub enum DnsControlMethod {
|
||||
/// Explicitly disable DNS control.
|
||||
///
|
||||
/// We don't use an `Option<Method>` because leaving out the CLI arg should
|
||||
/// use Systemd, not disable DNS control.
|
||||
Disabled,
|
||||
/// Back up `/etc/resolv.conf` and replace it with our own
|
||||
///
|
||||
/// Only suitable for the Alpine CI containers and maybe something like an
|
||||
@@ -16,23 +19,12 @@ pub enum DnsControlMethod {
|
||||
/// Cooperate with `systemd-resolved`
|
||||
///
|
||||
/// Suitable for most Ubuntu systems, probably
|
||||
Systemd,
|
||||
SystemdResolved,
|
||||
}
|
||||
|
||||
impl Default for DnsControlMethod {
|
||||
fn default() -> Self {
|
||||
Self::Systemd
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsControlMethod {
|
||||
/// Reads FIREZONE_DNS_CONTROL. Returns None if invalid or not set
|
||||
pub fn from_env() -> Option<DnsControlMethod> {
|
||||
match std::env::var(FIREZONE_DNS_CONTROL).as_deref() {
|
||||
Ok("etc-resolv-conf") => Some(DnsControlMethod::EtcResolvConf),
|
||||
Ok("systemd-resolved") => Some(DnsControlMethod::Systemd),
|
||||
_ => None,
|
||||
}
|
||||
Self::SystemdResolved
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,13 @@ struct SignalParams {
|
||||
/// Should be equivalent to `dbus-monitor --system "type='signal',interface='org.freedesktop.DBus.Properties',path='/org/freedesktop/resolve1',member='PropertiesChanged'"`
|
||||
pub async fn new_dns_notifier(
|
||||
_tokio_handle: tokio::runtime::Handle,
|
||||
method: Option<DnsControlMethod>,
|
||||
method: DnsControlMethod,
|
||||
) -> Result<Worker> {
|
||||
match method {
|
||||
Some(DnsControlMethod::EtcResolvConf) | None => Ok(Worker::new_dns_poller()),
|
||||
Some(DnsControlMethod::Systemd) => {
|
||||
DnsControlMethod::Disabled | DnsControlMethod::EtcResolvConf => {
|
||||
Ok(Worker::new_dns_poller())
|
||||
}
|
||||
DnsControlMethod::SystemdResolved => {
|
||||
Worker::new_dbus(SignalParams {
|
||||
dest: "org.freedesktop.resolve1",
|
||||
path: "/org/freedesktop/resolve1",
|
||||
@@ -51,11 +53,11 @@ pub async fn new_dns_notifier(
|
||||
/// Should be similar to `dbus-monitor --system "type='signal',interface='org.freedesktop.NetworkManager',member='StateChanged'"`
|
||||
pub async fn new_network_notifier(
|
||||
_tokio_handle: tokio::runtime::Handle,
|
||||
method: Option<DnsControlMethod>,
|
||||
method: DnsControlMethod,
|
||||
) -> Result<Worker> {
|
||||
match method {
|
||||
Some(DnsControlMethod::EtcResolvConf) | None => Ok(Worker::Null),
|
||||
Some(DnsControlMethod::Systemd) => {
|
||||
DnsControlMethod::Disabled | DnsControlMethod::EtcResolvConf => Ok(Worker::Null),
|
||||
DnsControlMethod::SystemdResolved => {
|
||||
Worker::new_dbus(SignalParams {
|
||||
dest: "org.freedesktop.NetworkManager",
|
||||
path: "/org/freedesktop/NetworkManager",
|
||||
|
||||
@@ -82,21 +82,15 @@ use windows::{
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn new_dns_notifier(
|
||||
_tokio_handle: tokio::runtime::Handle,
|
||||
method: Option<DnsControlMethod>,
|
||||
_method: DnsControlMethod,
|
||||
) -> Result<async_dns::DnsNotifier> {
|
||||
match method {
|
||||
Some(DnsControlMethod::Nrpt) | None => {}
|
||||
}
|
||||
async_dns::DnsNotifier::new()
|
||||
}
|
||||
|
||||
pub async fn new_network_notifier(
|
||||
_tokio_handle: tokio::runtime::Handle,
|
||||
method: Option<DnsControlMethod>,
|
||||
_method: DnsControlMethod,
|
||||
) -> Result<NetworkNotifier> {
|
||||
match method {
|
||||
Some(DnsControlMethod::Nrpt) | None => {}
|
||||
}
|
||||
NetworkNotifier::new().await
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@ use std::path::PathBuf;
|
||||
/// Also used for self-elevation
|
||||
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
|
||||
pub enum DnsControlMethod {
|
||||
/// Explicitly disable DNS control.
|
||||
///
|
||||
/// We don't use an `Option<Method>` because leaving out the CLI arg should
|
||||
/// use NRPT, not disable DNS control.
|
||||
Disabled,
|
||||
/// NRPT, the only DNS control method we use on Windows.
|
||||
Nrpt,
|
||||
}
|
||||
@@ -20,14 +25,6 @@ impl Default for DnsControlMethod {
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsControlMethod {
|
||||
/// Needed to match Linux
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub fn from_env() -> Option<DnsControlMethod> {
|
||||
Some(DnsControlMethod::Nrpt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client
|
||||
///
|
||||
/// This is where we can save config, logs, crash dumps, etc.
|
||||
|
||||
@@ -37,7 +37,6 @@ SystemCallArchitectures=native
|
||||
SystemCallFilter=@aio @basic-io @file-system @io-event @ipc @network-io @signal @system-service
|
||||
UMask=077
|
||||
|
||||
Environment="FIREZONE_DNS_CONTROL=systemd-resolved"
|
||||
Environment="LOG_DIR=/var/log/dev.firezone.client"
|
||||
EnvironmentFile="/etc/default/firezone-client-ipc"
|
||||
|
||||
|
||||
@@ -800,7 +800,7 @@ async fn run_controller(
|
||||
}
|
||||
|
||||
let tokio_handle = tokio::runtime::Handle::current();
|
||||
let dns_control_method = Some(Default::default());
|
||||
let dns_control_method = Default::default();
|
||||
|
||||
let mut dns_notifier = new_dns_notifier(tokio_handle.clone(), dns_control_method).await?;
|
||||
let mut network_notifier =
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
//! Platform-specific code to control the system's DNS resolution
|
||||
//!
|
||||
//! On Linux, we use `systemd-resolved` by default. We can also control
|
||||
//! `/etc/resolv.conf` or explicitly not control DNS.
|
||||
//!
|
||||
//! On Windows, we use NRPT by default. We can also explicitly not control DNS.
|
||||
|
||||
use anyhow::Result;
|
||||
use firezone_bin_shared::DnsControlMethod;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -8,8 +19,33 @@ mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows as platform;
|
||||
|
||||
pub(crate) use platform::{system_resolvers, DnsController};
|
||||
use platform::system_resolvers;
|
||||
|
||||
/// Controls system-wide DNS.
|
||||
///
|
||||
/// Always call `deactivate` when Firezone starts.
|
||||
///
|
||||
/// Only one of these should exist on the entire system at a time.
|
||||
pub(crate) struct DnsController {
|
||||
pub dns_control_method: DnsControlMethod,
|
||||
}
|
||||
|
||||
impl Drop for DnsController {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.deactivate() {
|
||||
tracing::error!(?error, "Failed to deactivate DNS control");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsController {
|
||||
pub(crate) fn system_resolvers(&self) -> Vec<IpAddr> {
|
||||
system_resolvers(self.dns_control_method).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {
|
||||
system_resolvers(DnsControlMethod::default())
|
||||
}
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
use super::DnsController;
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use firezone_bin_shared::{DnsControlMethod, TunDeviceManager};
|
||||
use std::{net::IpAddr, process::Command, str::FromStr};
|
||||
|
||||
mod etc_resolv_conf;
|
||||
|
||||
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {
|
||||
get_system_default_resolvers_systemd_resolved()
|
||||
}
|
||||
|
||||
/// Controls system-wide DNS.
|
||||
///
|
||||
/// Always call `deactivate` when Firezone starts.
|
||||
///
|
||||
/// Only one of these should exist on the entire system at a time.
|
||||
pub(crate) struct DnsController {
|
||||
dns_control_method: Option<DnsControlMethod>,
|
||||
}
|
||||
|
||||
impl Default for DnsController {
|
||||
fn default() -> Self {
|
||||
// We'll remove `get_from_env` in #5068
|
||||
let dns_control_method = DnsControlMethod::from_env();
|
||||
tracing::info!(?dns_control_method);
|
||||
Self { dns_control_method }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DnsController {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.deactivate() {
|
||||
tracing::error!(?error, "Failed to deactivate DNS control");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsController {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn deactivate(&mut self) -> Result<()> {
|
||||
tracing::debug!("Deactivating DNS control...");
|
||||
if let Some(DnsControlMethod::EtcResolvConf) = self.dns_control_method {
|
||||
if let DnsControlMethod::EtcResolvConf = self.dns_control_method {
|
||||
// TODO: Check that nobody else modified the file while we were running.
|
||||
etc_resolv_conf::revert()?;
|
||||
}
|
||||
@@ -51,11 +22,15 @@ impl DnsController {
|
||||
/// it would be bad if this was called from 2 threads at once.
|
||||
///
|
||||
/// Cancel safety: Try not to cancel this.
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: &[IpAddr]) -> Result<()> {
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: Vec<IpAddr>) -> Result<()> {
|
||||
match self.dns_control_method {
|
||||
None => Ok(()),
|
||||
Some(DnsControlMethod::EtcResolvConf) => etc_resolv_conf::configure(dns_config).await,
|
||||
Some(DnsControlMethod::Systemd) => configure_systemd_resolved(dns_config).await,
|
||||
DnsControlMethod::Disabled => Ok(()),
|
||||
DnsControlMethod::EtcResolvConf => {
|
||||
tokio::task::spawn_blocking(move || etc_resolv_conf::configure(&dns_config))
|
||||
.await
|
||||
.context("Failed to `spawn_blocking` DNS control task")?
|
||||
}
|
||||
DnsControlMethod::SystemdResolved => configure_systemd_resolved(&dns_config).await,
|
||||
}
|
||||
.context("Failed to control DNS")
|
||||
}
|
||||
@@ -65,7 +40,7 @@ impl DnsController {
|
||||
/// Does nothing if we're using other DNS control methods or none at all
|
||||
pub(crate) fn flush(&self) -> Result<()> {
|
||||
// Flushing is only implemented for systemd-resolved
|
||||
if matches!(self.dns_control_method, Some(DnsControlMethod::Systemd)) {
|
||||
if matches!(self.dns_control_method, DnsControlMethod::SystemdResolved) {
|
||||
tracing::debug!("Flushing systemd-resolved DNS cache...");
|
||||
Command::new("resolvectl").arg("flush-caches").status()?;
|
||||
tracing::debug!("Flushed DNS.");
|
||||
@@ -106,11 +81,12 @@ async fn configure_systemd_resolved(dns_config: &[IpAddr]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn system_resolvers() -> Result<Vec<IpAddr>> {
|
||||
match DnsControlMethod::from_env() {
|
||||
None => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::EtcResolvConf) => get_system_default_resolvers_resolv_conf(),
|
||||
Some(DnsControlMethod::Systemd) => get_system_default_resolvers_systemd_resolved(),
|
||||
pub(crate) fn system_resolvers(dns_control_method: DnsControlMethod) -> Result<Vec<IpAddr>> {
|
||||
match dns_control_method {
|
||||
DnsControlMethod::Disabled | DnsControlMethod::EtcResolvConf => {
|
||||
get_system_default_resolvers_resolv_conf()
|
||||
}
|
||||
DnsControlMethod::SystemdResolved => get_system_default_resolvers_systemd_resolved(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
net::IpAddr,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub(crate) const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
|
||||
@@ -34,8 +35,8 @@ 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(crate) async fn configure(dns_config: &[IpAddr]) -> Result<()> {
|
||||
configure_at_paths(dns_config, &ResolvPaths::default()).await
|
||||
pub(crate) fn configure(dns_config: &[IpAddr]) -> Result<()> {
|
||||
configure_at_paths(dns_config, &ResolvPaths::default())
|
||||
}
|
||||
|
||||
/// Revert changes Firezone made to `/etc/resolv.conf`
|
||||
@@ -46,25 +47,22 @@ pub(crate) fn revert() -> Result<()> {
|
||||
revert_at_paths(&ResolvPaths::default())
|
||||
}
|
||||
|
||||
async fn configure_at_paths(dns_config: &[IpAddr], paths: &ResolvPaths) -> Result<()> {
|
||||
fn configure_at_paths(dns_config: &[IpAddr], paths: &ResolvPaths) -> Result<()> {
|
||||
if dns_config.is_empty() {
|
||||
tracing::warn!("`dns_config` is empty, leaving `/etc/resolv.conf` unchanged");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = tokio::fs::read_to_string(&paths.resolv)
|
||||
.await
|
||||
.context("Failed to read `resolv.conf`")?;
|
||||
// There is a TOCTOU here, if the user somehow enables `systemd-resolved` while Firezone is booting up.
|
||||
ensure_regular_file(&paths.resolv)?;
|
||||
|
||||
let text = fs::read_to_string(&paths.resolv).context("Failed to read `resolv.conf`")?;
|
||||
let text = if text.starts_with(MAGIC_HEADER) {
|
||||
tracing::info!("The last run of Firezone crashed before reverting `/etc/resolv.conf`. Reverting it now before re-writing it.");
|
||||
let resolv_path = &paths.resolv;
|
||||
let paths = paths.clone();
|
||||
tokio::task::spawn_blocking(move || revert_at_paths(&paths))
|
||||
.await
|
||||
.context("`spawn_blocking` failed while trying to run `revert_at_paths`")?
|
||||
.context("Failed to revert `'resolv.conf`")?;
|
||||
tokio::fs::read_to_string(resolv_path)
|
||||
.await
|
||||
revert_at_paths(&paths).context("Failed to revert `'resolv.conf`")?;
|
||||
fs::read_to_string(resolv_path)
|
||||
.context("Failed to re-read `resolv.conf` after reverting it")?
|
||||
} else {
|
||||
// The last run of Firezone reverted resolv.conf successfully,
|
||||
@@ -86,15 +84,11 @@ async fn configure_at_paths(dns_config: &[IpAddr], paths: &ResolvPaths) -> Resul
|
||||
&paths.backup,
|
||||
atomicwrites::OverwriteBehavior::AllowOverwrite,
|
||||
);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
backup_file
|
||||
.write(|f| f.write_all(text.as_bytes()))
|
||||
.context("Failed to back up `resolv.conf`")
|
||||
})
|
||||
.await
|
||||
.context("Failed to run sync file operation in a blocking task")??;
|
||||
backup_file
|
||||
.write(|f| f.write_all(text.as_bytes()))
|
||||
.context("Failed to back up `resolv.conf`")?;
|
||||
|
||||
let mut new_resolv_conf = parsed.clone();
|
||||
let mut new_resolv_conf = parsed;
|
||||
|
||||
new_resolv_conf.nameservers = dns_config.iter().map(|addr| (*addr).into()).collect();
|
||||
|
||||
@@ -119,15 +113,15 @@ async fn configure_at_paths(dns_config: &[IpAddr], paths: &ResolvPaths) -> Resul
|
||||
new_resolv_conf,
|
||||
);
|
||||
|
||||
tokio::fs::write(&paths.resolv, new_text)
|
||||
.await
|
||||
.context("Failed to rewrite `resolv.conf`")?;
|
||||
fs::write(&paths.resolv, new_text).context("Failed to rewrite `resolv.conf`")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Must be sync so we can call it from `Drop` impls
|
||||
fn revert_at_paths(paths: &ResolvPaths) -> Result<()> {
|
||||
match std::fs::copy(&paths.backup, &paths.resolv) {
|
||||
ensure_regular_file(&paths.resolv)?;
|
||||
match fs::copy(&paths.backup, &paths.resolv) {
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
tracing::debug!("Didn't revert `/etc/resolv.conf`, no backup file found");
|
||||
return Ok(());
|
||||
@@ -142,6 +136,14 @@ fn revert_at_paths(paths: &ResolvPaths) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_regular_file(path: &Path) -> Result<()> {
|
||||
let file_type = fs::symlink_metadata(path)?.file_type();
|
||||
if !file_type.is_file() {
|
||||
bail!("File `{path:?}` is not a regular file, cannot use it to control DNS");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{configure_at_paths, revert_at_paths, ResolvPaths};
|
||||
@@ -302,7 +304,7 @@ nameserver 100.100.111.2
|
||||
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 1])])?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])?;
|
||||
@@ -324,7 +326,7 @@ nameserver 100.100.111.2
|
||||
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
configure_at_paths(&[], &paths).await?;
|
||||
configure_at_paths(&[], &paths)?;
|
||||
|
||||
check_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
// No backup since we didn't touch the original file
|
||||
@@ -339,11 +341,11 @@ nameserver 100.100.111.2
|
||||
let (_temp_dir, paths) = create_temp_paths();
|
||||
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
revert_at_paths(&paths)?;
|
||||
|
||||
write_resolv_conf(&paths.resolv, &[CLOUDFLARE_DNS.into()])?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 2])])?;
|
||||
check_resolv_conf(&paths.backup, &[CLOUDFLARE_DNS.into()])?;
|
||||
revert_at_paths(&paths)?;
|
||||
@@ -366,7 +368,7 @@ nameserver 100.100.111.2
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
// First run
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 1])])
|
||||
.context("First run, resolv.conf should have sentinel")?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])
|
||||
@@ -375,7 +377,7 @@ nameserver 100.100.111.2
|
||||
// Crash happens
|
||||
|
||||
// Second run
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 2])])
|
||||
.context("Second run, resolv.conf should have new sentinel")?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])
|
||||
@@ -399,7 +401,7 @@ nameserver 100.100.111.2
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
// First run
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 1])])
|
||||
.context("First run, resolv.conf should have sentinel")?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])
|
||||
@@ -410,7 +412,7 @@ nameserver 100.100.111.2
|
||||
write_resolv_conf(&paths.resolv, &[CLOUDFLARE_DNS.into()])?;
|
||||
|
||||
// Second run
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 2])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 2])])
|
||||
.context("Second run, resolv.conf should have new sentinel")?;
|
||||
check_resolv_conf(&paths.backup, &[CLOUDFLARE_DNS.into()])
|
||||
@@ -435,11 +437,11 @@ nameserver 100.100.111.2
|
||||
write_resolv_conf(&paths.resolv, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
// Configure twice
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 1])])?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths).await?;
|
||||
configure_at_paths(&[IpAddr::from([100, 100, 111, 1])], &paths)?;
|
||||
check_resolv_conf(&paths.resolv, &[IpAddr::from([100, 100, 111, 1])])?;
|
||||
check_resolv_conf(&paths.backup, &[GOOGLE_DNS.into()])?;
|
||||
|
||||
|
||||
@@ -13,34 +13,15 @@
|
||||
//!
|
||||
//! <https://superuser.com/a/1752670>
|
||||
|
||||
use super::DnsController;
|
||||
use anyhow::{Context as _, Result};
|
||||
use firezone_bin_shared::platform::CREATE_NO_WINDOW;
|
||||
use firezone_bin_shared::{platform::CREATE_NO_WINDOW, DnsControlMethod};
|
||||
use std::{net::IpAddr, os::windows::process::CommandExt, path::Path, process::Command};
|
||||
|
||||
pub fn system_resolvers_for_gui() -> Result<Vec<IpAddr>> {
|
||||
system_resolvers()
|
||||
}
|
||||
|
||||
/// Controls system-wide DNS.
|
||||
///
|
||||
/// Always call `deactivate` when Firezone starts.
|
||||
///
|
||||
/// Only one of these should exist on the entire system at a time.
|
||||
#[derive(Default)]
|
||||
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) = self.deactivate() {
|
||||
tracing::error!(?error, "Failed to deactivate DNS control");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsController {
|
||||
/// Deactivate any control Firezone has over the computer's DNS
|
||||
///
|
||||
@@ -64,10 +45,15 @@ impl DnsController {
|
||||
/// 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
|
||||
/// Must be async and an owned `Vec` to match the Linux signature
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: &[IpAddr]) -> Result<()> {
|
||||
activate(dns_config).context("Failed to activate DNS control")?;
|
||||
pub(crate) async fn set_dns(&mut self, dns_config: Vec<IpAddr>) -> Result<()> {
|
||||
match self.dns_control_method {
|
||||
DnsControlMethod::Disabled => {}
|
||||
DnsControlMethod::Nrpt => {
|
||||
activate(&dns_config).context("Failed to activate DNS control")?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -85,7 +71,7 @@ impl DnsController {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system_resolvers() -> Result<Vec<IpAddr>> {
|
||||
pub(crate) fn system_resolvers(_method: DnsControlMethod) -> Result<Vec<IpAddr>> {
|
||||
let resolvers = ipconfig::get_adapters()?
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.dns_servers())
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::{
|
||||
device_id,
|
||||
dns_control::{self, DnsController},
|
||||
known_dirs, signals, CallbackHandler, CliCommon, InternalServerMsg, IpcServerMsg,
|
||||
TOKEN_ENV_KEY,
|
||||
device_id, dns_control::DnsController, known_dirs, signals, CallbackHandler, CliCommon,
|
||||
InternalServerMsg, IpcServerMsg, TOKEN_ENV_KEY,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use connlib_client_shared::{file_logger, keypair, ConnectArgs, LoginUrl, Session};
|
||||
use firezone_bin_shared::{DnsControlMethod, TunDeviceManager};
|
||||
use futures::{
|
||||
future::poll_fn,
|
||||
task::{Context, Poll},
|
||||
@@ -21,7 +20,6 @@ use url::Url;
|
||||
pub mod ipc;
|
||||
use backoff::ExponentialBackoffBuilder;
|
||||
use connlib_shared::get_user_agent;
|
||||
use firezone_bin_shared::TunDeviceManager;
|
||||
use ipc::{Server as IpcServer, ServiceId};
|
||||
use phoenix_channel::PhoenixChannel;
|
||||
use secrecy::Secret;
|
||||
@@ -92,12 +90,12 @@ pub fn run_only_ipc_service() -> Result<()> {
|
||||
match cli.command {
|
||||
Cmd::Install => platform::install_ipc_service(),
|
||||
Cmd::Run => platform::run_ipc_service(cli.common),
|
||||
Cmd::RunDebug => run_debug_ipc_service(),
|
||||
Cmd::RunDebug => run_debug_ipc_service(cli),
|
||||
Cmd::RunSmokeTest => run_smoke_test(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_debug_ipc_service() -> Result<()> {
|
||||
fn run_debug_ipc_service(cli: Cli) -> Result<()> {
|
||||
crate::setup_stdout_logging()?;
|
||||
tracing::info!(
|
||||
arch = std::env::consts::ARCH,
|
||||
@@ -108,7 +106,7 @@ fn run_debug_ipc_service() -> Result<()> {
|
||||
let _guard = rt.enter();
|
||||
let mut signals = signals::Terminate::new()?;
|
||||
|
||||
rt.block_on(ipc_listen(&mut signals))
|
||||
rt.block_on(ipc_listen(cli.common.dns_control, &mut signals))
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -124,7 +122,9 @@ fn run_smoke_test() -> Result<()> {
|
||||
crate::setup_stdout_logging()?;
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let _guard = rt.enter();
|
||||
let mut dns_controller = DnsController::default();
|
||||
let mut dns_controller = DnsController {
|
||||
dns_control_method: Default::default(),
|
||||
};
|
||||
// Deactivate Firezone DNS control in case the system or IPC service crashed
|
||||
// and we need to recover. <https://github.com/firezone/firezone/issues/4899>
|
||||
dns_controller.deactivate()?;
|
||||
@@ -146,12 +146,15 @@ fn run_smoke_test() -> Result<()> {
|
||||
///
|
||||
/// If an IPC client is connected when we catch a terminate signal, we send the
|
||||
/// client a hint about that before we exit.
|
||||
async fn ipc_listen(signals: &mut signals::Terminate) -> Result<()> {
|
||||
async fn ipc_listen(
|
||||
dns_control_method: DnsControlMethod,
|
||||
signals: &mut signals::Terminate,
|
||||
) -> Result<()> {
|
||||
// Create the device ID and IPC service config dir if needed
|
||||
// This also gives the GUI a safe place to put the log filter config
|
||||
device_id::get_or_create().context("Failed to read / create device ID")?;
|
||||
let mut server = IpcServer::new(ServiceId::Prod).await?;
|
||||
let mut dns_controller = DnsController::default();
|
||||
let mut dns_controller = DnsController { dns_control_method };
|
||||
loop {
|
||||
let mut handler_fut = pin!(Handler::new(&mut server, &mut dns_controller));
|
||||
let Some(handler) = poll_fn(|cx| {
|
||||
@@ -320,7 +323,7 @@ impl<'a> Handler<'a> {
|
||||
}
|
||||
InternalServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
self.tun_device.set_ips(ipv4, ipv6).await?;
|
||||
self.dns_controller.set_dns(&dns).await?;
|
||||
self.dns_controller.set_dns(dns).await?;
|
||||
}
|
||||
InternalServerMsg::OnUpdateRoutes { ipv4, ipv6 } => {
|
||||
self.tun_device.set_routes(ipv4, ipv6).await?
|
||||
@@ -371,7 +374,7 @@ impl<'a> Handler<'a> {
|
||||
Session::connect(args, portal, tokio::runtime::Handle::try_current()?);
|
||||
let tun = self.tun_device.make_tun()?;
|
||||
new_session.set_tun(Box::new(tun));
|
||||
new_session.set_dns(dns_control::system_resolvers().unwrap_or_default());
|
||||
new_session.set_dns(self.dns_controller.system_resolvers());
|
||||
self.connlib = Some(new_session);
|
||||
}
|
||||
ClientMsg::Disconnect => {
|
||||
@@ -453,17 +456,18 @@ mod tests {
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const EXE_NAME: &str = "firezone-client-ipc";
|
||||
|
||||
// Can't remember how Clap works sometimes
|
||||
// Also these are examples
|
||||
#[test]
|
||||
fn cli() {
|
||||
let exe_name = "firezone-client-ipc";
|
||||
|
||||
let actual = Cli::parse_from([exe_name, "--log-dir", "bogus_log_dir", "run-debug"]);
|
||||
let actual =
|
||||
Cli::try_parse_from([EXE_NAME, "--log-dir", "bogus_log_dir", "run-debug"]).unwrap();
|
||||
assert!(matches!(actual.command, Cmd::RunDebug));
|
||||
assert_eq!(actual.common.log_dir, Some(PathBuf::from("bogus_log_dir")));
|
||||
|
||||
let actual = Cli::parse_from([exe_name, "run"]);
|
||||
let actual = Cli::try_parse_from([EXE_NAME, "run"]).unwrap();
|
||||
assert!(matches!(actual.command, Cmd::Run));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub(crate) fn run_ipc_service(cli: CliCommon) -> Result<()> {
|
||||
let _guard = rt.enter();
|
||||
let mut signals = signals::Terminate::new()?;
|
||||
|
||||
rt.block_on(super::ipc_listen(&mut signals))
|
||||
rt.block_on(super::ipc_listen(cli.dns_control, &mut signals))
|
||||
}
|
||||
|
||||
pub(crate) fn install_ipc_service() -> Result<()> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::CliCommon;
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use connlib_client_shared::file_logger;
|
||||
use firezone_bin_shared::DnsControlMethod;
|
||||
use futures::future::{self, Either};
|
||||
use std::{ffi::OsString, pin::pin, time::Duration};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -176,7 +177,7 @@ async fn service_run_async(mut shutdown_rx: mpsc::Receiver<()>) -> Result<()> {
|
||||
// Useless - Windows will never send us Ctrl+C when running as a service
|
||||
// This just keeps the signatures simpler
|
||||
let mut signals = crate::signals::Terminate::new()?;
|
||||
let listen_fut = pin!(super::ipc_listen(&mut signals));
|
||||
let listen_fut = pin!(super::ipc_listen(DnsControlMethod::Nrpt, &mut signals));
|
||||
match future::select(listen_fut, pin!(shutdown_rx.recv())).await {
|
||||
Either::Left((Err(error), _)) => Err(error).context("`ipc_listen` threw an error"),
|
||||
Either::Left((Ok(()), _)) => {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use connlib_client_shared::{Callbacks, Error as ConnlibError};
|
||||
use connlib_shared::callbacks;
|
||||
use firezone_bin_shared::DnsControlMethod;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
path::PathBuf,
|
||||
@@ -66,8 +67,16 @@ pub(crate) const GIT_VERSION: &str = git_version::git_version!(
|
||||
const TOKEN_ENV_KEY: &str = "FIREZONE_TOKEN";
|
||||
|
||||
/// CLI args common to both the IPC service and the headless Client
|
||||
#[derive(clap::Args)]
|
||||
#[derive(clap::Parser)]
|
||||
struct CliCommon {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[arg(long, env = "FIREZONE_DNS_CONTROL", default_value = "systemd-resolved")]
|
||||
dns_control: DnsControlMethod,
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[arg(long, env = "FIREZONE_DNS_CONTROL", default_value = "nrpt")]
|
||||
dns_control: DnsControlMethod,
|
||||
|
||||
/// File logging directory. Should be a path that's writeable by the current user.
|
||||
#[arg(short, long, env = "LOG_DIR")]
|
||||
log_dir: Option<PathBuf>,
|
||||
@@ -163,10 +172,55 @@ pub fn setup_stdout_logging() -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
const EXE_NAME: &str = "firezone-client-ipc";
|
||||
|
||||
// Make sure it's okay to store a bunch of these to mitigate #5880
|
||||
#[test]
|
||||
fn callback_msg_size() {
|
||||
assert_eq!(std::mem::size_of::<InternalServerMsg>(), 56)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn dns_control() {
|
||||
let actual = CliCommon::parse_from([EXE_NAME]);
|
||||
assert!(matches!(
|
||||
actual.dns_control,
|
||||
DnsControlMethod::SystemdResolved
|
||||
));
|
||||
|
||||
let actual = CliCommon::parse_from([EXE_NAME, "--dns-control", "disabled"]);
|
||||
assert!(matches!(actual.dns_control, DnsControlMethod::Disabled));
|
||||
|
||||
let actual = CliCommon::parse_from([EXE_NAME, "--dns-control", "etc-resolv-conf"]);
|
||||
assert!(matches!(
|
||||
actual.dns_control,
|
||||
DnsControlMethod::EtcResolvConf
|
||||
));
|
||||
|
||||
let actual = CliCommon::parse_from([EXE_NAME, "--dns-control", "systemd-resolved"]);
|
||||
assert!(matches!(
|
||||
actual.dns_control,
|
||||
DnsControlMethod::SystemdResolved
|
||||
));
|
||||
|
||||
assert!(CliCommon::try_parse_from([EXE_NAME, "--dns-control", "invalid"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn dns_control() {
|
||||
let actual = CliCommon::parse_from([EXE_NAME]);
|
||||
assert!(matches!(actual.dns_control, DnsControlMethod::Nrpt));
|
||||
|
||||
let actual = CliCommon::parse_from([EXE_NAME, "--dns-control", "disabled"]);
|
||||
assert!(matches!(actual.dns_control, DnsControlMethod::Disabled));
|
||||
|
||||
let actual = CliCommon::parse_from([EXE_NAME, "--dns-control", "nrpt"]);
|
||||
assert!(matches!(actual.dns_control, DnsControlMethod::Nrpt));
|
||||
|
||||
assert!(CliCommon::try_parse_from([EXE_NAME, "--dns-control", "invalid"]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! AKA "Headless"
|
||||
|
||||
use crate::{
|
||||
default_token_path, device_id, dns_control, platform, signals, CallbackHandler, CliCommon,
|
||||
DnsController, InternalServerMsg, IpcServerMsg, TOKEN_ENV_KEY,
|
||||
default_token_path, device_id, platform, signals, CallbackHandler, CliCommon, DnsController,
|
||||
InternalServerMsg, IpcServerMsg, TOKEN_ENV_KEY,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use backoff::ExponentialBackoffBuilder;
|
||||
@@ -10,8 +10,7 @@ use clap::Parser;
|
||||
use connlib_client_shared::{file_logger, keypair, ConnectArgs, LoginUrl, Session};
|
||||
use connlib_shared::get_user_agent;
|
||||
use firezone_bin_shared::{
|
||||
new_dns_notifier, new_network_notifier, setup_global_subscriber, DnsControlMethod,
|
||||
TunDeviceManager,
|
||||
new_dns_notifier, new_network_notifier, setup_global_subscriber, TunDeviceManager,
|
||||
};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use phoenix_channel::PhoenixChannel;
|
||||
@@ -191,7 +190,8 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
let mut hangup = signals::Hangup::new()?;
|
||||
let mut terminate = pin!(terminate.recv().fuse());
|
||||
let mut hangup = pin!(hangup.recv().fuse());
|
||||
let mut dns_controller = DnsController::default();
|
||||
let dns_control_method = cli.common.dns_control;
|
||||
let mut dns_controller = DnsController { dns_control_method };
|
||||
// Deactivate Firezone DNS control in case the system or IPC service crashed
|
||||
// and we need to recover. <https://github.com/firezone/firezone/issues/4899>
|
||||
dns_controller.deactivate()?;
|
||||
@@ -199,7 +199,6 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
let mut cb_rx = ReceiverStream::new(cb_rx).fuse();
|
||||
|
||||
let tokio_handle = tokio::runtime::Handle::current();
|
||||
let dns_control_method = DnsControlMethod::from_env();
|
||||
|
||||
let mut dns_notifier = new_dns_notifier(tokio_handle.clone(), dns_control_method).await?;
|
||||
|
||||
@@ -209,7 +208,7 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
|
||||
let tun = tun_device.make_tun()?;
|
||||
session.set_tun(Box::new(tun));
|
||||
session.set_dns(dns_control::system_resolvers().unwrap_or_default());
|
||||
session.set_dns(dns_controller.system_resolvers());
|
||||
|
||||
let result = loop {
|
||||
let mut dns_changed = pin!(dns_notifier.notified().fuse());
|
||||
@@ -230,7 +229,7 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
// If the DNS control method is not `systemd-resolved`
|
||||
// then we'll use polling here, so no point logging every 5 seconds that we're checking the DNS
|
||||
tracing::trace!("DNS change, notifying Session");
|
||||
session.set_dns(dns_control::system_resolvers()?);
|
||||
session.set_dns(dns_controller.system_resolvers());
|
||||
continue;
|
||||
},
|
||||
result = network_changed => {
|
||||
@@ -257,7 +256,7 @@ pub fn run_only_headless_client() -> Result<()> {
|
||||
),
|
||||
InternalServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
|
||||
tun_device.set_ips(ipv4, ipv6).await?;
|
||||
dns_controller.set_dns(&dns).await?;
|
||||
dns_controller.set_dns(dns).await?;
|
||||
// `on_set_interface_config` is guaranteed to be called when the tunnel is completely ready
|
||||
// <https://github.com/firezone/firezone/pull/6026#discussion_r1692297438>
|
||||
if let Some(instant) = last_connlib_start_instant.take() {
|
||||
@@ -353,14 +352,15 @@ mod tests {
|
||||
fn cli() {
|
||||
let exe_name = "firezone-headless-client";
|
||||
|
||||
let actual = Cli::parse_from([exe_name, "--api-url", "wss://api.firez.one"]);
|
||||
let actual = Cli::try_parse_from([exe_name, "--api-url", "wss://api.firez.one"]).unwrap();
|
||||
assert_eq!(
|
||||
actual.api_url,
|
||||
Url::parse("wss://api.firez.one").expect("Hard-coded URL should always be parsable")
|
||||
);
|
||||
assert!(!actual.check);
|
||||
|
||||
let actual = Cli::parse_from([exe_name, "--check", "--log-dir", "bogus_log_dir"]);
|
||||
let actual =
|
||||
Cli::try_parse_from([exe_name, "--check", "--log-dir", "bogus_log_dir"]).unwrap();
|
||||
assert!(actual.check);
|
||||
assert_eq!(actual.common.log_dir, Some(PathBuf::from("bogus_log_dir")));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If we set the DNS control to `systemd-resolved` but that's not available,
|
||||
# we should still boot up and allow IP / CIDR resources to work
|
||||
# the Client should bail out with an error message and non-zero exit code.
|
||||
|
||||
set -euox pipefail
|
||||
|
||||
source "./scripts/tests/lib.sh"
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ SystemCallFilter=@aio @basic-io @file-system @io-event @network-io @signal @syst
|
||||
UMask=077
|
||||
|
||||
Environment="FIREZONE_API_URL=ws://localhost:8081"
|
||||
# TODO: Remove after #6163 gets into a release
|
||||
Environment="FIREZONE_DNS_CONTROL=systemd-resolved"
|
||||
Environment="RUST_LOG=info"
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ write_files:
|
||||
UMask=077
|
||||
|
||||
Environment="FIREZONE_API_URL=${firezone_api_url}"
|
||||
Environment="FIREZONE_DNS_CONTROL=systemd-resolved"
|
||||
Environment="FIREZONE_ID=${firezone_client_id}"
|
||||
Environment="RUST_LOG=${firezone_client_log_level}"
|
||||
Environment="LOG_DIR=/var/log/firezone"
|
||||
|
||||
@@ -59,7 +59,6 @@ Set some environment variables to configure it:
|
||||
export FIREZONE_NAME="Development API test client"
|
||||
export FIREZONE_ID=$(uuidgen)
|
||||
export FIREZONE_TOKEN=<TOKEN>
|
||||
export FIREZONE_DNS_CONTROL="systemd-resolved" # or "etc-resolv-conf"
|
||||
export LOG_DIR="./"
|
||||
sudo -E ./firezone-client-headless-linux_1.0.0_x86_64
|
||||
```
|
||||
@@ -74,11 +73,14 @@ automatically use it to securely connect to Resources.
|
||||
|
||||
### Split DNS
|
||||
|
||||
By default, Split DNS is **disabled** for the Linux headless Client. This means
|
||||
that access to DNS-based Resources won't be routed through Firezone.
|
||||
By default, Split DNS is **enabled** for the Linux headless Client as of version
|
||||
1.1.5.
|
||||
|
||||
To enable Split DNS for the Linux Client, set the `FIREZONE_DNS_CONTROL`
|
||||
environment variable to `systemd-resolved` or `etc-resolv-conf`.
|
||||
To disable Split DNS for the Linux Client, set the `FIREZONE_DNS_CONTROL`
|
||||
environment variable to `disabled`.
|
||||
|
||||
To control `/etc/resolv.conf` directly, set `FIREZONE_DNS_CONTROL` to
|
||||
`etc-resolv-conf`.
|
||||
|
||||
Read more below to figure out which DNS control method is appropriate for your
|
||||
system.
|
||||
@@ -86,15 +88,15 @@ system.
|
||||
#### systemd-resolved
|
||||
|
||||
On most modern Linux distributions, DNS resolution is handled by
|
||||
`systemd-resolved`. If this is the case for you, set `FIREZONE_DNS_CONTROL` to
|
||||
`systemd-resolved`. If you're not sure, you can check by running the following
|
||||
command:
|
||||
`systemd-resolved`. If this is the case for you, do not set
|
||||
`FIREZONE_DNS_CONTROL`. If you're not sure whether you use `systemd-resolved`,
|
||||
you can check by running the following command:
|
||||
|
||||
```bash
|
||||
systemctl status systemd-resolved
|
||||
```
|
||||
|
||||
You'll need to ensure that `/etc/resolv.conf` is a symlink to
|
||||
Ensure that `/etc/resolv.conf` is a symlink to
|
||||
`/run/systemd/resolve/stub-resolv.conf`:
|
||||
|
||||
```bash
|
||||
@@ -108,9 +110,9 @@ sudo mv /etc/resolve.conf.new /etc/resolv.conf
|
||||
|
||||
#### NetworkManager
|
||||
|
||||
In most cases, if you're using NetworkManager your system is likely already
|
||||
using `systemd-resolved`, and you should set `FIREZONE_DNS_CONTROL` to
|
||||
`systemd-resolved`.
|
||||
In most cases, if you're using NetworkManager your system already uses
|
||||
`systemd-resolved`, and you should leave `FIREZONE_DNS_CONTROL` unset, which
|
||||
will use the default `systemd-resolved` DNS control method.
|
||||
|
||||
When NetworkManager detects that symlink exists, it will automatically use
|
||||
`systemd-resolved` for DNS resolution and no other configuration is necessary.
|
||||
@@ -134,14 +136,14 @@ sudo mv /etc/resolv.conf.before-firezone /etc/resolv.conf
|
||||
|
||||
### Environment variable reference
|
||||
|
||||
| Variable Name | Default Value | Description |
|
||||
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `FIREZONE_TOKEN` | | Service account token generated by the portal to authenticate this Client. |
|
||||
| `FIREZONE_NAME` | `<system hostname>` | Friendly name for this client to display in the UI. |
|
||||
| `FIREZONE_ID` | | Identifier used by the portal to identify this client for metadata and display purposes. |
|
||||
| `FIREZONE_DNS_CONTROL` | (blank) | The DNS control method to use. Set this to `systemd-resolved` to use systemd-resolved for Split DNS, or `etc-resolv-conf` to use the `/etc/resolv.conf` file. If left blank, Split DNS will be **disabled**. |
|
||||
| `LOG_DIR` | | File logging directory. Should be a path that's writeable by the current user. If unset, logs will be written to `stdout` only. |
|
||||
| `RUST_LOG` | `error` | Log level for the client. Set to `debug` for verbose logging. Read more about configuring Rust log levels [here](https://docs.rs/env_logger/latest/env_logger/). |
|
||||
| Variable Name | Default Value | Description |
|
||||
| ---------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FIREZONE_TOKEN` | | Service account token generated by the portal to authenticate this Client. |
|
||||
| `FIREZONE_NAME` | `<system hostname>` | Friendly name for this client to display in the UI. |
|
||||
| `FIREZONE_ID` | | Identifier used by the portal to identify this client for metadata and display purposes. |
|
||||
| `FIREZONE_DNS_CONTROL` | (blank) | The DNS control method to use. The default is `systemd-resolved`. Set this to `disabled` to disable DNS control, or `etc-resolv-conf` to use the `/etc/resolv.conf` file. Do not use `etc-resolv-conf` if `/etc/resolv.conf` is not a regular file, e.g. if it's a symlink to `/run/systemd/resolve/stub-resolv.conf` |
|
||||
| `LOG_DIR` | | File logging directory. Should be a path that's writeable by the current user. If unset, logs will be written to `stdout` only. |
|
||||
| `RUST_LOG` | `error` | Log level for the client. Set to `debug` for verbose logging. Read more about configuring Rust log levels [here](https://docs.rs/env_logger/latest/env_logger/). |
|
||||
|
||||
### Help output
|
||||
|
||||
@@ -239,7 +241,7 @@ nameserver 192.168.1.1
|
||||
### Check if Firezone is being used
|
||||
|
||||
Check if `curl` reports a remote IP in Firezone's range when you connect to a
|
||||
resource:
|
||||
Resource:
|
||||
|
||||
```text
|
||||
> curl --silent --output /dev/null --write-out %{remote_ip}\\n https://example.com
|
||||
|
||||
@@ -160,11 +160,30 @@ Global:
|
||||
Link 2 (enp0s6): 10.0.2.3 fec0::3
|
||||
```
|
||||
|
||||
```bash
|
||||
cat /etc/resolv.conf
|
||||
```
|
||||
|
||||
Normal `resolv.conf` if `systemd-resolved` is installed, whether or not Firezone
|
||||
is running:
|
||||
|
||||
```text
|
||||
# This file is managed by man:systemd-resolved(8). Do not edit.
|
||||
...
|
||||
```
|
||||
|
||||
Firezone `resolv.conf` if you set `FIREZONE_DNS_CONTROL=etc-resolv-conf`:
|
||||
|
||||
```text
|
||||
# BEGIN Firezone DNS configuration
|
||||
...
|
||||
```
|
||||
|
||||
### Revert Firezone DNS control
|
||||
|
||||
The Firezone GUI Client for Linux uses `systemd-resolved` to control DNS, which
|
||||
will automatically revert DNS to the system defaults when you quit the Firezone
|
||||
GUI, which destroys the `tun-firezone` virtual network interface.
|
||||
By default, the Firezone GUI Client for Linux controls DNS using
|
||||
`systemd-resolved`, which will automatically revert DNS to the system defaults
|
||||
when Firezone is disconnected.
|
||||
|
||||
If the network interface stays up and DNS does not revert, you can try
|
||||
restarting the tunnel service. Quit the Firezone GUI, then run:
|
||||
|
||||
@@ -15,6 +15,12 @@ export default function GUI({ title }: { title: string }) {
|
||||
{/*
|
||||
<Entry version="1.1.10" date={new Date("Invalid date")}>
|
||||
<ul className="list-disc space-y-2 pl-4 mb-4">
|
||||
<ChangeItem enable={title === "Linux GUI"} pull="6163">
|
||||
Supports using `etc-resolv-conf` DNS control method, or disabling DNS control
|
||||
</ChangeItem>
|
||||
<ChangeItem enable={title === "Windows"} pull="6163">
|
||||
Supports disabling DNS control
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6184">
|
||||
Mitigates a bug where the IPC service can panic if an internal channel fills up
|
||||
</ChangeItem>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import ChangeItem from "./ChangeItem";
|
||||
import Entry from "./Entry";
|
||||
import Entries from "./Entries";
|
||||
import ChangeItem from "./ChangeItem";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Headless() {
|
||||
const href = "/dl/firezone-client-headless-linux/:version/:arch";
|
||||
@@ -12,6 +12,9 @@ export default function Headless() {
|
||||
{/*
|
||||
<Entry version="1.1.5" date={new Date("Invalid date")}>
|
||||
<ul className="list-disc space-y-2 pl-4 mb-4">
|
||||
<ChangeItem pull="6163">
|
||||
Uses `systemd-resolved` DNS control by default on Linux
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6184">
|
||||
Mitigates a bug where the Client can panic if an internal channel fills up
|
||||
</ChangeItem>
|
||||
|
||||
Reference in New Issue
Block a user