Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use atomicwrites to back up /etc/resolv.conf more robustly #3828

Merged
merged 7 commits into from
Mar 4, 2024
13 changes: 4 additions & 9 deletions rust/connlib/shared/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,10 @@ pub enum ConnlibError {
#[error(transparent)]
JoinError(#[from] JoinError),

// Error variants for `/etc/resolv.conf` DNS control
#[error("Failed to read `resolv.conf`: {0}")]
ReadResolvConf(std::io::Error),
#[error("Failed to parse `resolv.conf`")]
ParseResolvConf,
#[error("Failed to backup `resolv.conf`: {0}")]
WriteResolvConfBackup(std::io::Error),
#[error("Failed to rewrite `resolv.conf`: {0}")]
RewriteResolvConf(std::io::Error),
#[cfg(target_os = "linux")]
#[error("Error while rewriting `/etc/resolv.conf`: {0}")]
ResolvConf(#[from] crate::linux::etc_resolv_conf::Error),

#[error(transparent)]
Snownet(#[from] snownet::Error),
#[error("Detected non-allowed packet in channel")]
Expand Down
56 changes: 34 additions & 22 deletions rust/connlib/shared/src/linux/etc_resolv_conf.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
use crate::{Error, Result};
use std::{net::IpAddr, path::Path};
use tokio::io::AsyncWriteExt;
use std::{io::Write, net::IpAddr, path::Path};

pub const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
pub const ETC_RESOLV_CONF_BACKUP: &str = "/etc/resolv.conf.before-firezone";

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to read `resolv.conf`: {0}")]
Read(std::io::Error),
#[error("Failed to parse `resolv.conf`")]
Parse,
#[error("Failed to rewrite `resolv.conf`: {0}")]
Rewrite(std::io::Error),
#[error("Failed to sync `resolv.conf` backup: {0}")]
SyncBackup(std::io::Error),
#[error("Failed to backup `resolv.conf`: {0}")]
WriteBackup(std::io::Error),
}

/// Back up `/etc/resolv.conf`(sic) and then modify it in-place
///
/// This is async because it's called in a Tokio context and it's nice to use their
/// `fs` module
pub async fn configure_dns(dns_config: &[IpAddr]) -> Result<()> {
pub async fn configure_dns(dns_config: &[IpAddr]) -> Result<(), Error> {
configure_dns_at_paths(
dns_config,
Path::new(ETC_RESOLV_CONF),
Expand All @@ -22,7 +34,7 @@ pub async fn configure_dns(dns_config: &[IpAddr]) -> Result<()> {
///
/// This is sync because it's called from the Linux CLI client where we don't have our own
/// Tokio context.
pub fn unconfigure_dns() -> Result<()> {
pub fn unconfigure_dns() -> Result<(), Error> {
tracing::debug!("Unconfiguring `/etc/resolv.conf` not implemented yet");
Ok(())
}
Expand All @@ -31,35 +43,35 @@ async fn configure_dns_at_paths(
dns_config: &[IpAddr],
resolv_path: &Path,
backup_path: &Path,
) -> Result<()> {
) -> Result<(), Error> {
if dns_config.is_empty() {
tracing::info!("`dns_config` is empty, leaving `/etc/resolv.conf` unchanged");
return Ok(());
}

let text = tokio::fs::read_to_string(resolv_path)
.await
.map_err(Error::ReadResolvConf)?;
let parsed = resolv_conf::Config::parse(&text).map_err(|_| Error::ParseResolvConf)?;
.map_err(Error::Read)?;
let parsed = resolv_conf::Config::parse(&text).map_err(|_| Error::Parse)?;

// Back up the original resolv.conf. If there's already a backup, don't modify it
match tokio::fs::File::options()
.write(true)
.create_new(true)
.open(backup_path)
.await
{
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
// `atomicwrites` handles the fsync and rename-into-place tricks to resist file corruption
// if we lose power during the write.
let backup_file = atomicwrites::AtomicFile::new(
backup_path,
atomicwrites::OverwriteBehavior::DisallowOverwrite,
);
match backup_file.write(|f| f.write_all(text.as_bytes())) {
Err(atomicwrites::Error::Internal(error))
if error.kind() == std::io::ErrorKind::AlreadyExists =>
{
tracing::info!(?backup_path, "Backup path already exists, won't overwrite");
}
Err(error) => return Err(Error::WriteResolvConfBackup(error)),
// TODO: Would do a rename-into-place here if the contents of the file mattered more
Ok(mut f) => f.write_all(text.as_bytes()).await?,
Err(atomicwrites::Error::Internal(error)) => return Err(Error::SyncBackup(error)),
Err(atomicwrites::Error::User(error)) => return Err(Error::WriteBackup(error)),
Ok(()) => {}
}

ReactorScram marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Would do an fsync here if resolv.conf was important and not
// auto-generated by Docker on every run.

let mut new_resolv_conf = parsed.clone();
new_resolv_conf.nameservers = dns_config.iter().map(|addr| (*addr).into()).collect();

Expand All @@ -82,7 +94,7 @@ async fn configure_dns_at_paths(

tokio::fs::write(resolv_path, new_text)
.await
.map_err(Error::RewriteResolvConf)?;
.map_err(Error::Rewrite)?;

Ok(())
}
Expand Down
Loading