From b30a629fb2c7d504335f9f14792e39c2e4093f44 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Wed, 23 Apr 2025 14:23:57 +0200 Subject: [PATCH 01/12] feat(certificates): add self-signed certs on Ubuntu --- local-server/src/certificates/mod.rs | 100 +++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index df8d34ba..0dc0ecd1 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -15,9 +15,10 @@ use std::{ pub use wildcard_sni_resolver::WildcardSniResolver; const LINKUP_CA_COMMON_NAME: &str = "Linkup Local CA"; +const LINKUP_CA_PEM_NAME: &str = "linkup_ca.cert.pem"; fn ca_cert_pem_path(certs_dir: &Path) -> PathBuf { - certs_dir.join("linkup_ca.cert.pem") + certs_dir.join(LINKUP_CA_PEM_NAME) } fn ca_key_pem_path(certs_dir: &Path) -> PathBuf { @@ -177,6 +178,7 @@ fn upsert_ca_cert(certs_dir: &Path) { fs::write(ca_key_pem_path(certs_dir), key_pair.serialize_pem()).unwrap(); } +#[cfg(target_os = "macos")] fn ca_exists_in_keychain() -> bool { process::Command::new("sudo") .arg("security") @@ -191,6 +193,21 @@ fn ca_exists_in_keychain() -> bool { .success() } +#[cfg(target_os = "linux")] +fn ca_exists_in_keychain() -> bool { + process::Command::new("find") + .arg("/etc/ssl/certs") + .arg("-iname") + .arg(LINKUP_CA_PEM_NAME) + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + .expect("Failed to find linkup CA") + .success() +} + +#[cfg(target_os = "macos")] fn add_ca_to_keychain(certs_dir: &Path) { process::Command::new("sudo") .arg("security") @@ -208,6 +225,28 @@ fn add_ca_to_keychain(certs_dir: &Path) { .expect("Failed to add CA to keychain"); } +#[cfg(target_os = "linux")] +fn add_ca_to_keychain(certs_dir: &Path) { + process::Command::new("sudo") + .arg("cp") + .arg(ca_cert_pem_path(certs_dir)) + .arg("/usr/local/share/ca-certificates") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + .expect("Failed to copy CA to /usr/local/share/ca-certificates"); + + process::Command::new("sudo") + .arg("update-ca-certificates") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + .expect("Failed to update CA certificates"); +} + +#[cfg(target_os = "macos")] fn remove_ca_from_keychain() -> Result<(), UninstallError> { let status = process::Command::new("sudo") .arg("security") @@ -230,11 +269,57 @@ fn remove_ca_from_keychain() -> Result<(), UninstallError> { Ok(()) } +#[cfg(target_os = "linux")] +fn remove_ca_from_keychain() -> Result<(), UninstallError> { + let status = process::Command::new("sudo") + .arg("rm") + .arg(format!( + "/usr/local/share/ca-certificates/{}", + LINKUP_CA_PEM_NAME + )) + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + .map_err(|error| UninstallError::DeleteCaCertificate(error.to_string()))?; + + if !status.success() { + return Err(UninstallError::DeleteCaCertificate( + "rm command returned unsuccessful exit status".to_string(), + )); + } + + process::Command::new("sudo") + .arg("update-ca-certificates") + .arg("--fresh") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .status() + .expect("Failed to update CA certificates"); + + Ok(()) +} + fn firefox_profiles_cert_storages() -> Vec { - let home = env::var("HOME").expect("Failed to get HOME env var"); + #[cfg(target_os = "macos")] + let profile_dirs = ["Library/Application Support/Firefox/Profiles"]; + + #[cfg(target_os = "linux")] + let profile_dirs = [ + ".mozilla/firefox", + "snap/firefox/common/.mozilla/firefox", + ".var/app/org.mozilla.firefox/.mozilla/firefox", + ]; - match fs::read_dir(PathBuf::from(home).join("Library/Application Support/Firefox/Profiles")) { - Ok(dir) => dir + let home = env::var("HOME").expect("Failed to get HOME env var"); + match profile_dirs + .iter() + .map(|dir| PathBuf::from(home.clone()).join(dir)) + .find(|path| path.exists()) + .map(fs::read_dir) + { + Some(Ok(dir)) => dir .filter_map(|entry| { let entry = entry.expect("Failed to read Firefox profile dir entry entry"); let path = entry.path(); @@ -251,11 +336,16 @@ fn firefox_profiles_cert_storages() -> Vec { } }) .collect::>(), - Err(error) => { + Some(Err(error)) => { if !matches!(error.kind(), std::io::ErrorKind::NotFound) { eprintln!("Failed to load Firefox profiles: {}", error); } + Vec::new() + } + _ => { + eprintln!("Failed to load Firefox profiles"); + Vec::new() } } From 3c23a7e3b6391b89d5f1b4aa14347df0cb0faa02 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Wed, 23 Apr 2025 15:42:46 +0200 Subject: [PATCH 02/12] Rewrite the ff_cert fn to be more readable --- local-server/src/certificates/mod.rs | 48 ++++++++++++---------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 0dc0ecd1..50cbb0e0 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -313,42 +313,36 @@ fn firefox_profiles_cert_storages() -> Vec { ]; let home = env::var("HOME").expect("Failed to get HOME env var"); - match profile_dirs + let mut storages: Vec = Vec::new(); + + for dir in profile_dirs .iter() - .map(|dir| PathBuf::from(home.clone()).join(dir)) - .find(|path| path.exists()) + .map(|dir| PathBuf::from(&home).join(dir)) .map(fs::read_dir) { - Some(Ok(dir)) => dir - .filter_map(|entry| { - let entry = entry.expect("Failed to read Firefox profile dir entry entry"); - let path = entry.path(); - if path.is_dir() { - if path.join("cert9.db").exists() { - Some(format!("{}{}", "sql:", path.to_str().unwrap())) - } else if path.join("cert8.db").exists() { - Some(format!("{}{}", "dmb:", path.to_str().unwrap())) - } else { - None + match dir { + Ok(dir) => { + for entry in dir.filter_map(|entry| entry.ok()) { + let path = entry.path(); + if path.is_dir() { + if path.join("cert9.db").exists() { + storages.push(format!("{}{}", "sql:", path.to_str().unwrap())); + } else if path.join("cert8.db").exists() { + storages.push(format!("{}{}", "dmb:", path.to_str().unwrap())); + } } - } else { - None } - }) - .collect::>(), - Some(Err(error)) => { - if !matches!(error.kind(), std::io::ErrorKind::NotFound) { - eprintln!("Failed to load Firefox profiles: {}", error); } - Vec::new() - } - _ => { - eprintln!("Failed to load Firefox profiles"); - - Vec::new() + Err(error) => { + if !matches!(error.kind(), std::io::ErrorKind::NotFound) { + eprintln!("Failed to load Firefox profiles: {}", error); + } + } } } + + storages } fn add_ca_to_nss(certs_dir: &Path, cert_storages: &[String]) { From 86080b6e6101ad51dbbe57f7463360fb314c3426 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Thu, 24 Apr 2025 09:38:37 +0200 Subject: [PATCH 03/12] Show localDNS for linux --- linkup-cli/src/commands/health.rs | 66 ++++++++++------------------ linkup-cli/src/commands/mod.rs | 2 - linkup-cli/src/commands/server.rs | 1 - linkup-cli/src/commands/start.rs | 19 +++----- linkup-cli/src/commands/stop.rs | 1 - linkup-cli/src/commands/uninstall.rs | 24 +++++----- linkup-cli/src/local_config.rs | 2 - linkup-cli/src/main.rs | 2 - linkup-cli/src/services/mod.rs | 2 - 9 files changed, 40 insertions(+), 79 deletions(-) diff --git a/linkup-cli/src/commands/health.rs b/linkup-cli/src/commands/health.rs index 77e9b585..dc5ae012 100644 --- a/linkup-cli/src/commands/health.rs +++ b/linkup-cli/src/commands/health.rs @@ -15,7 +15,6 @@ use crate::{ Result, }; -#[cfg(target_os = "macos")] use super::local_dns; #[derive(clap::Args)] @@ -91,13 +90,11 @@ struct OrphanProcess { struct BackgroudServices { linkup_server: BackgroundServiceHealth, cloudflared: BackgroundServiceHealth, - #[cfg(target_os = "macos")] dns_server: BackgroundServiceHealth, possible_orphan_processes: Vec, } #[derive(Debug, Serialize)] -#[cfg_attr(not(target_os = "macos"), allow(dead_code))] enum BackgroundServiceHealth { Unknown, NotInstalled, @@ -106,7 +103,6 @@ enum BackgroundServiceHealth { } impl BackgroudServices { - #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] fn load(state: &Option) -> Self { let mut managed_pids: Vec = Vec::with_capacity(4); @@ -132,7 +128,6 @@ impl BackgroudServices { BackgroundServiceHealth::NotInstalled }; - #[cfg(target_os = "macos")] let dns_server = match find_service_pid(services::LocalDnsServer::ID) { Some(pid) => { managed_pids.push(pid); @@ -159,7 +154,6 @@ impl BackgroudServices { Self { linkup_server, cloudflared, - #[cfg(target_os = "macos")] dns_server, possible_orphan_processes: find_potential_orphan_processes(managed_pids), } @@ -267,14 +261,12 @@ impl Linkup { } } -#[cfg(target_os = "macos")] #[derive(Debug, Serialize)] struct LocalDNS { is_installed: Option, resolvers: Vec, } -#[cfg(target_os = "macos")] impl LocalDNS { fn load(state: &Option) -> Result { // If there is no state, we cannot know if local-dns is installed since we depend on @@ -297,7 +289,6 @@ struct Health { session: Session, background_services: BackgroudServices, linkup: Linkup, - #[cfg(target_os = "macos")] local_dns: LocalDNS, } @@ -312,7 +303,6 @@ impl Health { session, background_services: BackgroudServices::load(&state), linkup: Linkup::load()?, - #[cfg(target_os = "macos")] local_dns: LocalDNS::load(&state)?, }) } @@ -358,19 +348,12 @@ impl Display for Health { BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, } - #[cfg(target_os = "macos")] - { - write!(f, " - DNS Server ")?; - match &self.background_services.dns_server { - BackgroundServiceHealth::NotInstalled => { - writeln!(f, "{}", "NOT INSTALLED".yellow())? - } - BackgroundServiceHealth::Stopped => writeln!(f, "{}", "NOT RUNNING".yellow())?, - BackgroundServiceHealth::Running(pid) => { - writeln!(f, "{} ({})", "RUNNING".blue(), pid)? - } - BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, - } + write!(f, " - DNS Server ")?; + match &self.background_services.dns_server { + BackgroundServiceHealth::NotInstalled => writeln!(f, "{}", "NOT INSTALLED".yellow())?, + BackgroundServiceHealth::Stopped => writeln!(f, "{}", "NOT RUNNING".yellow())?, + BackgroundServiceHealth::Running(pid) => writeln!(f, "{} ({})", "RUNNING".blue(), pid)?, + BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, } write!(f, " - Cloudflared ")?; @@ -408,30 +391,27 @@ impl Display for Health { } } - #[cfg(target_os = "macos")] - { - write!(f, "{}", "Local DNS: ".bold().italic())?; - match self.local_dns.is_installed { - Some(installed) => { - write!(f, "\n Installed: ",)?; - if installed { - writeln!(f, "{}", "YES".green())?; - } else { - writeln!(f, "{}", "NO".yellow())? - } + write!(f, "{}", "Local DNS: ".bold().italic())?; + match self.local_dns.is_installed { + Some(installed) => { + write!(f, "\n Installed: ",)?; + if installed { + writeln!(f, "{}", "YES".green())?; + } else { + writeln!(f, "{}", "NO".yellow())? + } - write!(f, " Resolvers:")?; - if self.local_dns.resolvers.is_empty() { - writeln!(f, " {}", "EMPTY".yellow())?; - } else { - writeln!(f)?; - for file in &self.local_dns.resolvers { - writeln!(f, " - {}", file)?; - } + write!(f, " Resolvers:")?; + if self.local_dns.resolvers.is_empty() { + writeln!(f, " {}", "EMPTY".yellow())?; + } else { + writeln!(f)?; + for file in &self.local_dns.resolvers { + writeln!(f, " - {}", file)?; } } - None => writeln!(f, "{}", "UNKNOWN".yellow())?, } + None => writeln!(f, "{}", "UNKNOWN".yellow())?, } write!(f, "{}", "Possible orphan processes:".bold().italic())?; diff --git a/linkup-cli/src/commands/mod.rs b/linkup-cli/src/commands/mod.rs index 09de8067..808ab18f 100644 --- a/linkup-cli/src/commands/mod.rs +++ b/linkup-cli/src/commands/mod.rs @@ -2,7 +2,6 @@ pub mod completion; pub mod deploy; pub mod health; pub mod local; -#[cfg(target_os = "macos")] pub mod local_dns; pub mod preview; pub mod remote; @@ -19,7 +18,6 @@ pub use {deploy::deploy, deploy::DeployArgs}; pub use {deploy::destroy, deploy::DestroyArgs}; pub use {health::health, health::Args as HealthArgs}; pub use {local::local, local::Args as LocalArgs}; -#[cfg(target_os = "macos")] pub use {local_dns::local_dns, local_dns::Args as LocalDnsArgs}; pub use {preview::preview, preview::Args as PreviewArgs}; pub use {remote::remote, remote::Args as RemoteArgs}; diff --git a/linkup-cli/src/commands/server.rs b/linkup-cli/src/commands/server.rs index 260eadc5..d3ff972b 100644 --- a/linkup-cli/src/commands/server.rs +++ b/linkup-cli/src/commands/server.rs @@ -36,7 +36,6 @@ pub async fn server(args: &Args) -> Result<()> { .unwrap(); }); - #[cfg(target_os = "macos")] let handler_https = { use std::path::PathBuf; diff --git a/linkup-cli/src/commands/start.rs b/linkup-cli/src/commands/start.rs index 55296de5..9c33e38f 100644 --- a/linkup-cli/src/commands/start.rs +++ b/linkup-cli/src/commands/start.rs @@ -46,7 +46,6 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) let local_server = services::LocalServer::new(); let cloudflare_tunnel = services::CloudflareTunnel::new(); - #[cfg(target_os = "macos")] let local_dns_server = services::LocalDnsServer::new(); let mut display_thread: Option> = None; @@ -60,7 +59,6 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) &[ services::LocalServer::NAME, services::CloudflareTunnel::NAME, - #[cfg(target_os = "macos")] services::LocalDnsServer::NAME, ], status_update_channel.1, @@ -91,16 +89,13 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) } } - #[cfg(target_os = "macos")] - { - if exit_error.is_none() { - match local_dns_server - .run_with_progress(&mut state, status_update_channel.0.clone()) - .await - { - Ok(_) => (), - Err(err) => exit_error = Some(err), - } + if exit_error.is_none() { + match local_dns_server + .run_with_progress(&mut state, status_update_channel.0.clone()) + .await + { + Ok(_) => (), + Err(err) => exit_error = Some(err), } } diff --git a/linkup-cli/src/commands/stop.rs b/linkup-cli/src/commands/stop.rs index 87520b97..2bbbb06a 100644 --- a/linkup-cli/src/commands/stop.rs +++ b/linkup-cli/src/commands/stop.rs @@ -34,7 +34,6 @@ pub fn stop(_args: &Args, clear_env: bool) -> Result<()> { stop_service(services::LocalServer::ID); stop_service(services::CloudflareTunnel::ID); - #[cfg(target_os = "macos")] stop_service(services::LocalDnsServer::ID); println!("Stopped linkup"); diff --git a/linkup-cli/src/commands/uninstall.rs b/linkup-cli/src/commands/uninstall.rs index 60152704..a8eca1b9 100644 --- a/linkup-cli/src/commands/uninstall.rs +++ b/linkup-cli/src/commands/uninstall.rs @@ -1,6 +1,10 @@ use std::{fs, process}; -use crate::{commands, linkup_dir_path, linkup_exe_path, prompt, InstallationMethod, Result}; +use crate::commands::local_dns; +use crate::local_config::LocalState; +use crate::{ + commands, linkup_dir_path, linkup_exe_path, local_config, prompt, InstallationMethod, Result, +}; #[derive(clap::Args)] pub struct Args {} @@ -19,19 +23,11 @@ pub async fn uninstall(_args: &Args, config_arg: &Option) -> Result<()> commands::stop(&commands::StopArgs {}, true)?; - #[cfg(target_os = "macos")] - { - use crate::{ - commands::local_dns, - local_config::{self, LocalState}, - }; - - if local_dns::is_installed(&local_config::managed_domains( - LocalState::load().ok().as_ref(), - config_arg, - )) { - local_dns::uninstall(config_arg).await?; - } + if local_dns::is_installed(&local_config::managed_domains( + LocalState::load().ok().as_ref(), + config_arg, + )) { + local_dns::uninstall(config_arg).await?; } let exe_path = linkup_exe_path()?; diff --git a/linkup-cli/src/local_config.rs b/linkup-cli/src/local_config.rs index da0c278b..9d5310ee 100644 --- a/linkup-cli/src/local_config.rs +++ b/linkup-cli/src/local_config.rs @@ -365,7 +365,6 @@ impl From<&LocalState> for ServerConfig { } } -#[cfg(target_os = "macos")] pub fn managed_domains(state: Option<&LocalState>, cfg_path: &Option) -> Vec { let config_domains = match config_path(cfg_path).ok() { Some(cfg_path) => match get_config(&cfg_path) { @@ -396,7 +395,6 @@ pub fn managed_domains(state: Option<&LocalState>, cfg_path: &Option) -> domain_set.into_iter().collect() } -#[cfg(target_os = "macos")] pub fn top_level_domains(domains: &[String]) -> Vec { domains .iter() diff --git a/linkup-cli/src/main.rs b/linkup-cli/src/main.rs index f67b1ee9..68f7aaf0 100644 --- a/linkup-cli/src/main.rs +++ b/linkup-cli/src/main.rs @@ -226,7 +226,6 @@ enum Commands { #[clap(about = "View linkup component and service status")] Status(commands::StatusArgs), - #[cfg(target_os = "macos")] #[clap(about = "Speed up your local environment by routing traffic locally when possible")] LocalDNS(commands::LocalDnsArgs), @@ -272,7 +271,6 @@ async fn main() -> anyhow::Result<()> { Commands::Local(args) => commands::local(args).await, Commands::Remote(args) => commands::remote(args).await, Commands::Status(args) => commands::status(args), - #[cfg(target_os = "macos")] Commands::LocalDNS(args) => commands::local_dns(args, &cli.config).await, Commands::Completion(args) => commands::completion(args), Commands::Preview(args) => commands::preview(args, &cli.config).await, diff --git a/linkup-cli/src/services/mod.rs b/linkup-cli/src/services/mod.rs index fab8f2b7..3559ef4d 100644 --- a/linkup-cli/src/services/mod.rs +++ b/linkup-cli/src/services/mod.rs @@ -4,11 +4,9 @@ use sysinfo::{ProcessRefreshKind, RefreshKind, System}; use thiserror::Error; mod cloudflare_tunnel; -#[cfg(target_os = "macos")] mod local_dns_server; mod local_server; -#[cfg(target_os = "macos")] pub use local_dns_server::LocalDnsServer; pub use local_server::LocalServer; pub use sysinfo::{Pid, Signal}; From 94c66c73dca52c67a2bf5e68adc4154e5e10d382 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Thu, 24 Apr 2025 09:41:57 +0200 Subject: [PATCH 04/12] Add sudo commands to the general build --- linkup-cli/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/linkup-cli/src/main.rs b/linkup-cli/src/main.rs index 68f7aaf0..dadedf97 100644 --- a/linkup-cli/src/main.rs +++ b/linkup-cli/src/main.rs @@ -96,7 +96,6 @@ fn current_version() -> Version { .expect("current version on CARGO_PKG_VERSION should be a valid version") } -#[cfg(target_os = "macos")] fn is_sudo() -> bool { let sudo_check = std::process::Command::new("sudo") .arg("-n") @@ -112,7 +111,6 @@ fn is_sudo() -> bool { false } -#[cfg(target_os = "macos")] fn sudo_su() -> Result<()> { let status = std::process::Command::new("sudo") .arg("su") From b5931611d335a206da9929de8e466e20dd5fb9d7 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Thu, 24 Apr 2025 16:25:53 +0200 Subject: [PATCH 05/12] Don't flush dns for linux as it's not needed --- linkup-cli/src/commands/local_dns.rs | 16 ++++++++++++---- local-server/src/certificates/mod.rs | 7 ++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index bf549dca..2e671c32 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -133,8 +133,11 @@ fn install_resolvers(resolve_domains: &[String]) -> Result<()> { } } - flush_dns_cache()?; - kill_dns_responder()?; + #[cfg(target_os = "macos")] + { + flush_dns_cache()?; + kill_dns_responder()?; + } Ok(()) } @@ -150,8 +153,11 @@ fn uninstall_resolvers(resolve_domains: &[String]) -> Result<()> { .with_context(|| format!("Failed to delete /etc/resolver/{domain}",))?; } - flush_dns_cache()?; - kill_dns_responder()?; + #[cfg(target_os = "macos")] + { + flush_dns_cache()?; + kill_dns_responder()?; + } Ok(()) } @@ -173,6 +179,7 @@ pub fn list_resolvers() -> std::result::Result, std::io::Error> { Ok(resolvers) } +#[cfg(target_os = "macos")] fn flush_dns_cache() -> Result<()> { let status_flush = Command::new("dscacheutil") .args(["-flushcache"]) @@ -186,6 +193,7 @@ fn flush_dns_cache() -> Result<()> { Ok(()) } +#[cfg(target_os = "macos")] fn kill_dns_responder() -> Result<()> { let status_kill_responder = Command::new("sudo") .args(["killall", "-HUP", "mDNSResponder"]) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 50cbb0e0..1ff4dc3c 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -92,7 +92,12 @@ pub fn setup_self_signed_certificates( println!( "For self-signed certificates to work with Firefox, you need to have nss installed." ); - println!("You can find it on https://formulae.brew.sh/formula/nss."); + let nss_url = if cfg!(target_os = "macos") { + "https://formulae.brew.sh/formula/nss" + } else { + "sudo apt install libnss3-tools" + }; + println!("You can install it with {}.", nss_url); println!("Please install it and then try to install local-dns again."); return Err(SetupError::MissingNSS); From f1a4b9a70ded4708f5d9ea15251412fb2ee65603 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 09:35:27 +0200 Subject: [PATCH 06/12] Update local-server/src/certificates/mod.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- local-server/src/certificates/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 1ff4dc3c..cbc93bcc 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -301,9 +301,12 @@ fn remove_ca_from_keychain() -> Result<(), UninstallError> { .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .status() - .expect("Failed to update CA certificates"); - - Ok(()) + .map_err(|error| UninstallError::UpdateCaCertificates(error.to_string()))? + .success() + .then_some(()) + .ok_or_else(|| UninstallError::UpdateCaCertificates( + "update-ca-certificates command returned unsuccessful exit status".to_string(), + ))?; } fn firefox_profiles_cert_storages() -> Vec { From 41887c7ef80970b78fc91395ea47de65026edd61 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 09:35:51 +0200 Subject: [PATCH 07/12] Review fixes --- local-server/src/certificates/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index cbc93bcc..6640deab 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -95,7 +95,7 @@ pub fn setup_self_signed_certificates( let nss_url = if cfg!(target_os = "macos") { "https://formulae.brew.sh/formula/nss" } else { - "sudo apt install libnss3-tools" + "`sudo apt install libnss3-tools`" }; println!("You can install it with {}.", nss_url); println!("Please install it and then try to install local-dns again."); From e4b4e7f0ee676ddd6d06ef925e96c74b997771c7 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 09:41:51 +0200 Subject: [PATCH 08/12] Flush DNS on Linux --- linkup-cli/src/commands/local_dns.rs | 35 ++++++++++++++++++++++++---- local-server/src/certificates/mod.rs | 8 ++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index 2e671c32..9f986fce 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -133,11 +133,8 @@ fn install_resolvers(resolve_domains: &[String]) -> Result<()> { } } - #[cfg(target_os = "macos")] - { - flush_dns_cache()?; - kill_dns_responder()?; - } + flush_dns_cache()?; + kill_dns_responder()?; Ok(()) } @@ -193,6 +190,20 @@ fn flush_dns_cache() -> Result<()> { Ok(()) } +#[cfg(target_os = "linux")] +fn flush_dns_cache() -> Result<()> { + let status_flush = Command::new("sudo") + .args(["resolvectl", "flush-caches"]) + .status() + .context("Failed to flush DNS cache")?; + + if !status_flush.success() { + return Err(anyhow!("Flushing DNS cache was unsuccessful")); + } + + Ok(()) +} + #[cfg(target_os = "macos")] fn kill_dns_responder() -> Result<()> { let status_kill_responder = Command::new("sudo") @@ -206,3 +217,17 @@ fn kill_dns_responder() -> Result<()> { Ok(()) } + +#[cfg(target_os = "linux")] +fn kill_dns_responder() -> Result<()> { + let status_kill_responder = Command::new("sudo") + .args(["killall", "-USR2", "systemd-resolved"]) + .status() + .context("Failed to kill DNS responder")?; + + if !status_kill_responder.success() { + return Err(anyhow!("Killing DNS responder was unsuccessful")); + } + + Ok(()) +} diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 6640deab..d625e1a2 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -304,9 +304,11 @@ fn remove_ca_from_keychain() -> Result<(), UninstallError> { .map_err(|error| UninstallError::UpdateCaCertificates(error.to_string()))? .success() .then_some(()) - .ok_or_else(|| UninstallError::UpdateCaCertificates( - "update-ca-certificates command returned unsuccessful exit status".to_string(), - ))?; + .ok_or_else(|| { + UninstallError::UpdateCaCertificates( + "update-ca-certificates command returned unsuccessful exit status".to_string(), + ) + })?; } fn firefox_profiles_cert_storages() -> Vec { From 2f59f5e58c5ccaa13fc35ca48844e6a0b2ee9586 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 09:46:11 +0200 Subject: [PATCH 09/12] Fix typo --- local-server/src/certificates/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index d625e1a2..63b29e99 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -301,14 +301,14 @@ fn remove_ca_from_keychain() -> Result<(), UninstallError> { .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .status() - .map_err(|error| UninstallError::UpdateCaCertificates(error.to_string()))? + .map_err(|error| UninstallError::DeleteCaCertificate(error.to_string()))? .success() .then_some(()) .ok_or_else(|| { - UninstallError::UpdateCaCertificates( + UninstallError::DeleteCaCertificate( "update-ca-certificates command returned unsuccessful exit status".to_string(), ) - })?; + }) } fn firefox_profiles_cert_storages() -> Vec { From db22d53343db1ae8f949436c694385b46507c6e7 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 09:55:09 +0200 Subject: [PATCH 10/12] Clearer error for updating cert registry --- local-server/src/certificates/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 63b29e99..42ae532b 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -119,6 +119,8 @@ pub enum UninstallError { RemoveCertsFolder(String), #[error("Failed to remove CA certificate from keychain: {0}")] DeleteCaCertificate(String), + #[error("Failed to update CA certificate registry from keychain: {0}")] + RefreshCaCertificateRegistry(String), } pub fn uninstall_self_signed_certificates(certs_dir: &Path) -> Result<(), UninstallError> { @@ -301,11 +303,11 @@ fn remove_ca_from_keychain() -> Result<(), UninstallError> { .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .status() - .map_err(|error| UninstallError::DeleteCaCertificate(error.to_string()))? + .map_err(|error| UninstallError::RefreshCaCertificateRegistry(error.to_string()))? .success() .then_some(()) .ok_or_else(|| { - UninstallError::DeleteCaCertificate( + UninstallError::RefreshCaCertificateRegistry( "update-ca-certificates command returned unsuccessful exit status".to_string(), ) }) From bec1fb43b5e03b53c0bfe0b787f2385426606e54 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Fri, 25 Apr 2025 14:03:28 +0200 Subject: [PATCH 11/12] Don't throw error on dns flushing for linux; log it instead --- linkup-cli/src/commands/local_dns.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index 9f986fce..a619faec 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -198,7 +198,7 @@ fn flush_dns_cache() -> Result<()> { .context("Failed to flush DNS cache")?; if !status_flush.success() { - return Err(anyhow!("Flushing DNS cache was unsuccessful")); + log::warn!("Flushing DNS cache was unsuccessful"); } Ok(()) @@ -226,7 +226,7 @@ fn kill_dns_responder() -> Result<()> { .context("Failed to kill DNS responder")?; if !status_kill_responder.success() { - return Err(anyhow!("Killing DNS responder was unsuccessful")); + log::warn!("Killing DNS responder was unsuccessful"); } Ok(()) From ed2855846b65e8d134207f0de4e3e3241eee8c04 Mon Sep 17 00:00:00 2001 From: Roman Prudnikov Date: Mon, 28 Apr 2025 10:15:47 +0200 Subject: [PATCH 12/12] Clean DNS on uninstall on Linux; misc fixes --- linkup-cli/src/commands/local_dns.rs | 7 ++----- linkup-cli/src/commands/uninstall.rs | 7 +++---- local-server/src/certificates/mod.rs | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index a619faec..0a1560aa 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -150,11 +150,8 @@ fn uninstall_resolvers(resolve_domains: &[String]) -> Result<()> { .with_context(|| format!("Failed to delete /etc/resolver/{domain}",))?; } - #[cfg(target_os = "macos")] - { - flush_dns_cache()?; - kill_dns_responder()?; - } + flush_dns_cache()?; + kill_dns_responder()?; Ok(()) } diff --git a/linkup-cli/src/commands/uninstall.rs b/linkup-cli/src/commands/uninstall.rs index a8eca1b9..8a126818 100644 --- a/linkup-cli/src/commands/uninstall.rs +++ b/linkup-cli/src/commands/uninstall.rs @@ -1,9 +1,8 @@ use std::{fs, process}; -use crate::commands::local_dns; -use crate::local_config::LocalState; use crate::{ - commands, linkup_dir_path, linkup_exe_path, local_config, prompt, InstallationMethod, Result, + commands, commands::local_dns, linkup_dir_path, linkup_exe_path, local_config::managed_domains, + local_config::LocalState, prompt, InstallationMethod, Result, }; #[derive(clap::Args)] @@ -23,7 +22,7 @@ pub async fn uninstall(_args: &Args, config_arg: &Option) -> Result<()> commands::stop(&commands::StopArgs {}, true)?; - if local_dns::is_installed(&local_config::managed_domains( + if local_dns::is_installed(&managed_domains( LocalState::load().ok().as_ref(), config_arg, )) { diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 42ae532b..7bbde1f8 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -93,7 +93,7 @@ pub fn setup_self_signed_certificates( "For self-signed certificates to work with Firefox, you need to have nss installed." ); let nss_url = if cfg!(target_os = "macos") { - "https://formulae.brew.sh/formula/nss" + "`brew install nss`" } else { "`sudo apt install libnss3-tools`" }; @@ -119,7 +119,7 @@ pub enum UninstallError { RemoveCertsFolder(String), #[error("Failed to remove CA certificate from keychain: {0}")] DeleteCaCertificate(String), - #[error("Failed to update CA certificate registry from keychain: {0}")] + #[error("Failed to refresh CA certificate registry: {0}")] RefreshCaCertificateRegistry(String), }