diff --git a/Cargo.lock b/Cargo.lock index 21053341..a7c3ca72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,7 @@ dependencies = [ "axum", "base64", "clap", + "defguard_version", "defguard_wireguard_rs", "env_logger", "gethostname", @@ -400,10 +401,26 @@ dependencies = [ "tonic", "tonic-prost", "tonic-prost-build", + "tracing", "vergen-git2", "x25519-dalek", ] +[[package]] +name = "defguard_version" +version = "0.0.0" +source = "git+https://github.com/DefGuard/defguard.git?rev=f61ce40927a4d21095ea53a691219d5ae46e3e4e#f61ce40927a4d21095ea53a691219d5ae46e3e4e" +dependencies = [ + "http", + "os_info", + "semver", + "thiserror 2.0.15", + "tonic", + "tower", + "tracing", + "tracing-subscriber", +] + [[package]] name = "defguard_wireguard_rs" version = "0.7.5" @@ -993,6 +1010,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.175" @@ -1041,6 +1064,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1223,6 +1255,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1265,6 +1307,24 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "os_info" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +dependencies = [ + "log", + "plist", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "paste" version = "1.0.15" @@ -1325,6 +1385,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1448,6 +1521,15 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-xml" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -1480,8 +1562,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1492,9 +1583,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1703,6 +1800,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1851,6 +1957,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -2121,6 +2236,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2170,6 +2315,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2239,6 +2390,28 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 213fc8b8..96915c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "1.5.0" edition = "2021" [dependencies] +defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "f61ce40927a4d21095ea53a691219d5ae46e3e4e" } axum = { version = "0.8", features = ["macros"] } base64 = "0.22" clap = { version = "4.5", features = ["derive", "env"] } @@ -29,6 +30,7 @@ tonic = { version = "0.14", default-features = false, features = [ "tls-native-roots", "tls-ring", ] } +tracing = "0.1" tonic-prost = "0.14" [target.'cfg(target_os = "linux")'.dependencies] diff --git a/deny.toml b/deny.toml index 2282829b..4c2d088a 100644 --- a/deny.toml +++ b/deny.toml @@ -106,7 +106,10 @@ allow = [ confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list -exceptions = [{ allow = ["AGPL-3.0-only"], crate = "defguard-gateway" }] +exceptions = [ + { allow = ["AGPL-3.0-only"], crate = "defguard-gateway" }, + { allow = ["AGPL-3.0-only"], crate = "defguard_version" } +] # Some crates don't have (easily) machine readable licensing information, # adding a clarification entry for it allows you to manually specify the diff --git a/src/config.rs b/src/config.rs index 8235fe46..87d42a5a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,9 @@ use crate::error::GatewayError; #[clap(about = "Defguard VPN gateway service")] #[command(version)] pub struct Config { + #[arg(long, short = 'l', env = "DEFGUARD_LOG_LEVEL", default_value = "info")] + pub log_level: String, + /// Token received from Defguard after completing the network wizard #[arg( long, @@ -113,6 +116,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + log_level: "info".to_string(), token: "TOKEN".into(), name: None, grpc_url: "http://localhost:50051".into(), diff --git a/src/error.rs b/src/error.rs index d05e5d45..aceb3c4f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use defguard_version::{DefguardVersionError, SemverError}; use defguard_wireguard_rs::error::WireguardInterfaceError; use thiserror::Error; @@ -43,4 +44,10 @@ pub enum GatewayError { #[error("Firewall error: {0}")] FirewallError(#[from] FirewallError), + + #[error(transparent)] + DefguardVersionError(#[from] DefguardVersionError), + + #[error(transparent)] + SemverError(#[from] SemverError), } diff --git a/src/gateway.rs b/src/gateway.rs index 335e49ab..e5155881 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1,3 +1,8 @@ +use defguard_version::{ + client::version_interceptor, parse_metadata, ComponentInfo, DefguardComponent, Version, +}; +use defguard_wireguard_rs::{net::IpAddrMask, WireguardInterfaceApi}; +use gethostname::gethostname; use std::{ collections::HashMap, fs::read_to_string, @@ -8,9 +13,6 @@ use std::{ }, time::{Duration, SystemTime}, }; - -use defguard_wireguard_rs::{net::IpAddrMask, WireguardInterfaceApi}; -use gethostname::gethostname; use tokio::{ select, sync::mpsc, @@ -25,6 +27,7 @@ use tonic::{ transport::{Certificate, Channel, ClientTlsConfig, Endpoint}, Request, Status, Streaming, }; +use tracing::{instrument, Instrument}; use crate::{ config::Config, @@ -69,14 +72,29 @@ impl From for InterfaceConfiguration { } } -#[derive(Clone)] -struct AuthInterceptor { +type InterceptorFn = Box) -> Result, Status> + Send + Sync>; + +/// Intercepts all grpc requests adding authentication and version metadata +struct RequestInterceptor { hostname: MetadataValue, token: MetadataValue, + version: defguard_version::Version, + version_interceptor_fn: InterceptorFn, } -impl AuthInterceptor { - fn new(token: &str) -> Result { +impl Clone for RequestInterceptor { + fn clone(&self) -> Self { + Self { + hostname: self.hostname.clone(), + token: self.token.clone(), + version: self.version.clone(), + version_interceptor_fn: Box::new(version_interceptor(self.version.clone())), + } + } +} + +impl RequestInterceptor { + fn new(token: &str, version: Version) -> Result { let token = MetadataValue::try_from(token)?; let hostname = MetadataValue::try_from( gethostname() @@ -84,12 +102,21 @@ impl AuthInterceptor { .expect("Unable to get current hostname during gRPC connection setup."), )?; - Ok(Self { hostname, token }) + Ok(Self { + hostname, + token, + version: version.clone(), + version_interceptor_fn: Box::new(version_interceptor(version)), + }) } } -impl Interceptor for AuthInterceptor { - fn call(&mut self, mut request: Request<()>) -> Result, Status> { +impl Interceptor for RequestInterceptor { + fn call(&mut self, request: Request<()>) -> Result, Status> { + // Apply version interceptor - adds version headers + let mut request = (self.version_interceptor_fn)(request)?; + + // Add auth headers let metadata = request.metadata_mut(); metadata.insert("authorization", self.token.clone()); metadata.insert("hostname", self.hostname.clone()); @@ -110,7 +137,8 @@ pub struct Gateway { #[cfg_attr(not(target_os = "linux"), allow(unused))] firewall_config: Option, pub connected: Arc, - client: GatewayServiceClient>, + client: GatewayServiceClient>, + core_info: Option, stats_thread: Option>, } @@ -131,6 +159,7 @@ impl Gateway { stats_thread: None, firewall_api, firewall_config: None, + core_info: None, }) } @@ -182,6 +211,7 @@ impl Gateway { } /// Starts tokio thread collecting stats and sending them to backend service via gRPC. + #[instrument(skip_all)] fn spawn_stats_thread(&mut self) -> UnboundedReceiverStream { if let Some(handle) = self.stats_thread.take() { debug!("Aborting previous stats thread before starting a new one"); @@ -192,63 +222,67 @@ impl Gateway { let wgapi = Arc::clone(&self.wgapi); let (tx, rx) = mpsc::unbounded_channel(); debug!("Spawning stats thread"); - let handle = spawn(async move { - // helper map to track if peer data is actually changing - // and avoid sending duplicate stats - let mut peer_map = HashMap::new(); - let mut interval = interval(period); - let mut id = 1; - 'outer: loop { - // wait until next iteration - interval.tick().await; - debug!("Sending active peer stats updates."); - let interface_data = wgapi.lock().unwrap().read_interface_data(); - match interface_data { - Ok(host) => { - let peers = host.peers; - debug!( - "Found {} peers configured on WireGuard interface", - peers.len() - ); - for peer in peers.into_values().filter(|p| { - p.last_handshake - .is_some_and(|lhs| lhs != SystemTime::UNIX_EPOCH) - }) { - let has_changed = peer_map - .get(&peer.public_key) - .is_none_or(|last_peer| *last_peer != peer); - if has_changed { - peer_map.insert(peer.public_key.clone(), peer.clone()); - id += 1; - if tx - .send(StatsUpdate { - id, - payload: Some(Payload::PeerStats((&peer).into())), - }) - .is_err() - { - debug!("Stats stream disappeared"); - break 'outer; + let handle = spawn( + async move { + // helper map to track if peer data is actually changing + // and avoid sending duplicate stats + let mut peer_map = HashMap::new(); + let mut interval = interval(period); + let mut id = 1; + 'outer: loop { + // wait until next iteration + interval.tick().await; + debug!("Sending active peer stats updates."); + let interface_data = wgapi.lock().unwrap().read_interface_data(); + match interface_data { + Ok(host) => { + let peers = host.peers; + debug!( + "Found {} peers configured on WireGuard interface", + peers.len() + ); + for peer in peers.into_values().filter(|p| { + p.last_handshake + .is_some_and(|lhs| lhs != SystemTime::UNIX_EPOCH) + }) { + let has_changed = peer_map + .get(&peer.public_key) + .is_none_or(|last_peer| *last_peer != peer); + if has_changed { + peer_map.insert(peer.public_key.clone(), peer.clone()); + id += 1; + if tx + .send(StatsUpdate { + id, + payload: Some(Payload::PeerStats((&peer).into())), + }) + .is_err() + { + debug!("Stats stream disappeared"); + break 'outer; + } + } else { + debug!( + "Stats for peer {} have not changed. Skipping.", + peer.public_key + ); } - } else { - debug!( - "Stats for peer {} have not changed. Skipping.", - peer.public_key - ); } } + Err(err) => error!("Failed to retrieve WireGuard interface stats: {err}"), } - Err(err) => error!("Failed to retrieve WireGuard interface stats: {err}"), + debug!("Sent peer stats updates for all peers."); } - debug!("Sent peer stats updates for all peers."); } - }); + .instrument(tracing::Span::current()), + ); self.stats_thread = Some(handle); UnboundedReceiverStream::new(rx) } + #[instrument(skip_all)] async fn handle_stats_thread( - mut client: GatewayServiceClient>, + mut client: GatewayServiceClient>, rx: UnboundedReceiverStream, ) { let status = client.stats(rx).await; @@ -429,6 +463,19 @@ impl Gateway { Ok(()) } + fn get_tracing_variables(&self) -> (String, String) { + let version = self + .core_info + .as_ref() + .map_or(String::from("?"), |info| info.version.to_string()); + let info = self + .core_info + .as_ref() + .map_or(String::from("?"), |info| info.system.to_string()); + + (version, info) + } + /// Continuously tries to connect to gRPC endpoint. Once the connection is established /// configures the interface, starts the stats thread, connects and returns the updates stream. async fn connect(&mut self) -> Streaming { @@ -451,6 +498,16 @@ impl Gateway { }; match (response, stream) { (Ok(response), Ok(stream)) => { + self.core_info = parse_metadata(response.metadata()); + let (version, info) = self.get_tracing_variables(); + let span = tracing::info_span!( + "core_configuration", + component = %DefguardComponent::Core, + version, + info + ); + let _guard = span.enter(); + if let Err(err) = self.configure(response.into_inner()) { error!("Interface configuration failed: {err}"); continue; @@ -477,7 +534,7 @@ impl Gateway { fn setup_client( config: &Config, - ) -> Result>, GatewayError> + ) -> Result>, GatewayError> { debug!("Preparing gRPC client configuration"); let tls = ClientTlsConfig::new(); @@ -497,14 +554,15 @@ impl Gateway { .keep_alive_while_idle(true) .tls_config(tls)?; let channel = endpoint.connect_lazy(); - - let auth_interceptor = AuthInterceptor::new(&config.token)?; - let client = GatewayServiceClient::with_interceptor(channel, auth_interceptor); + let version = Version::parse(VERSION)?; + let request_interceptor = RequestInterceptor::new(&config.token, version)?; + let client = GatewayServiceClient::with_interceptor(channel, request_interceptor); debug!("gRPC client configuration done"); Ok(client) } + #[instrument(skip_all)] async fn handle_updates(&mut self, updates_stream: &mut Streaming) { loop { match updates_stream.message().await { @@ -642,6 +700,14 @@ impl Gateway { debug!("Executing specified POST_UP command: {post_up}"); execute_command(post_up)?; } + let (version, info) = self.get_tracing_variables(); + let span = tracing::info_span!( + "core_grpc", + component = %DefguardComponent::Core, + version, + info, + ); + let _guard = span.enter(); let stats_stream = self.spawn_stats_thread(); let client = self.client.clone(); select! { @@ -717,6 +783,7 @@ mod tests { stats_thread: None, firewall_api, firewall_config: None, + core_info: None, }; // new config is the same @@ -908,6 +975,7 @@ mod tests { stats_thread: None, firewall_api: FirewallApi::new("test_interface").unwrap(), firewall_config: None, + core_info: None, }; // Gateway has no firewall config, new rules are empty @@ -980,6 +1048,7 @@ mod tests { stats_thread: None, firewall_api: FirewallApi::new("test_interface").unwrap(), firewall_config: None, + core_info: None, }; // Gateway has no config gateway.firewall_config = None; diff --git a/src/main.rs b/src/main.rs index ce5b2317..def54a2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,12 @@ use std::{fs::File, io::Write, process, sync::Arc}; use defguard_gateway::{ config::get_config, enterprise::firewall::api::FirewallApi, error::GatewayError, - execute_command, gateway::Gateway, init_syslog, server::run_server, + execute_command, gateway::Gateway, init_syslog, server::run_server, VERSION, }; +use defguard_version::Version; #[cfg(not(any(target_os = "macos", target_os = "netbsd")))] use defguard_wireguard_rs::Kernel; use defguard_wireguard_rs::{Userspace, WGApi}; -use env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}; use tokio::task::JoinSet; #[tokio::main] @@ -30,8 +30,9 @@ async fn main() -> Result<(), GatewayError> { return Err(error); } } else { - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - } + let version = Version::parse(VERSION)?; + defguard_version::tracing::init(version, &config.log_level.to_string())? + }; if let Some(pre_up) = &config.pre_up { log::info!("Executing specified PRE_UP command: {pre_up}");