diff --git a/Cargo.lock b/Cargo.lock index 90619b2..e03a895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,9 +119,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -314,9 +314,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "block-buffer" @@ -357,9 +357,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "jobserver", "libc", @@ -567,6 +567,7 @@ dependencies = [ "axum-extra", "base64", "clap", + "defguard_version", "dotenvy", "futures", "futures-util", @@ -576,7 +577,7 @@ dependencies = [ "rust-embed", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.15", "time", "tokio", "tokio-stream", @@ -584,6 +585,7 @@ dependencies = [ "tonic", "tonic-prost", "tonic-prost-build", + "tower", "tower-http", "tower_governor", "tracing", @@ -592,6 +594,21 @@ dependencies = [ "vergen-git2", ] +[[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 = "deranged" version = "0.4.0" @@ -1508,6 +1525,18 @@ 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" @@ -1591,6 +1620,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 = "polyval" version = "0.6.2" @@ -1645,9 +1687,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -1741,6 +1783,15 @@ dependencies = [ "winapi", ] +[[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" @@ -1924,6 +1975,7 @@ checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2021,6 +2073,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -2178,9 +2236,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -2228,11 +2286,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.15", ] [[package]] @@ -2248,9 +2306,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bdab9a6..0c1ffeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://github.com/DefGuard/proxy" repository = "https://github.com/DefGuard/proxy" [dependencies] +defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "f61ce40927a4d21095ea53a691219d5ae46e3e4e" } # base `axum` deps axum = { version = "0.7", features = ["macros", "tracing", "ws"] } axum-client-ip = "0.6" @@ -48,6 +49,7 @@ tower_governor = "0.4" rust-embed = { version = "8.5", features = ["include-exclude"] } mime_guess = "2.0" base64 = "0.22.1" +tower = "0.5.2" futures = "0.3.31" futures-util = "0.3.31" diff --git a/deny.toml b/deny.toml index 955fa45..4c66fb4 100644 --- a/deny.toml +++ b/deny.toml @@ -108,9 +108,9 @@ 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 = [ - # Each entry is the crate and version constraint, and its specific allow - # list - #{ allow = ["Zlib"], crate = "adler32" }, + { allow = [ + "AGPL-3.0-only", + ], crate = "defguard_version" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/src/grpc.rs b/src/grpc.rs index 4f13478..40450e1 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -6,10 +6,12 @@ use std::{ Arc, Mutex, }, }; - use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{Request, Response, Status, Streaming}; +use tracing::Instrument; + +use defguard_version::{version_info_from_metadata, DefguardComponent}; use crate::{ error::ApiError, @@ -89,7 +91,7 @@ impl proxy_server::Proxy for ProxyServer { type BidiStream = UnboundedReceiverStream>; /// Handle bidirectional communication with Defguard core. - #[instrument(name = "bidirectional_communication", level = "debug", skip(self))] + #[instrument(name = "bidirectional_communication", level = "info", skip(self))] async fn bidi( &self, request: Request>, @@ -98,6 +100,9 @@ impl proxy_server::Proxy for ProxyServer { error!("Failed to determine client address for request: {request:?}"); return Err(Status::internal("Failed to determine client address")); }; + let (version, info) = version_info_from_metadata(request.metadata()); + let span = tracing::info_span!("core_bidi_stream", component = %DefguardComponent::Core, version, info); + let _guard = span.enter(); info!("Defguard Core gRPC client connected from: {address}"); let (tx, rx) = mpsc::unbounded_channel(); @@ -108,37 +113,40 @@ impl proxy_server::Proxy for ProxyServer { let results = Arc::clone(&self.results); let connected = Arc::clone(&self.connected); let mut stream = request.into_inner(); - tokio::spawn(async move { - loop { - match stream.message().await { - Ok(Some(response)) => { - debug!("Received message from Defguard core: {response:?}"); - connected.store(true, Ordering::Relaxed); - // Discard empty payloads. - if let Some(payload) = response.payload { - if let Some(rx) = results.lock().unwrap().remove(&response.id) { - if let Err(err) = rx.send(payload) { - error!("Failed to send message to rx: {err:?}"); + tokio::spawn( + async move { + loop { + match stream.message().await { + Ok(Some(response)) => { + debug!("Received message from Defguard core: {response:?}"); + connected.store(true, Ordering::Relaxed); + // Discard empty payloads. + if let Some(payload) = response.payload { + if let Some(rx) = results.lock().unwrap().remove(&response.id) { + if let Err(err) = rx.send(payload) { + error!("Failed to send message to rx: {err:?}"); + } + } else { + error!("Missing receiver for response #{}", response.id); } - } else { - error!("Missing receiver for response #{}", response.id); } } - } - Ok(None) => { - info!("gRPC stream has been closed"); - break; - } - Err(err) => { - error!("gRPC client error: {err}"); - break; + Ok(None) => { + info!("gRPC stream has been closed"); + break; + } + Err(err) => { + error!("gRPC client error: {err}"); + break; + } } } + info!("Defguard core client disconnected: {address}"); + connected.store(false, Ordering::Relaxed); + clients.lock().unwrap().remove(&address); } - info!("Defguard core client disconnected: {address}"); - connected.store(false, Ordering::Relaxed); - clients.lock().unwrap().remove(&address); - }); + .instrument(tracing::Span::current()), + ); Ok(Response::new(UnboundedReceiverStream::new(rx))) } diff --git a/src/http.rs b/src/http.rs index 5662c08..87d30fe 100644 --- a/src/http.rs +++ b/src/http.rs @@ -16,9 +16,11 @@ use axum::{ }; use axum_extra::extract::cookie::Key; use clap::crate_version; +use defguard_version::{server::DefguardVersionLayer, Version}; use serde::Serialize; use tokio::{net::TcpListener, sync::oneshot, task::JoinSet}; use tonic::transport::{Identity, Server, ServerTlsConfig}; +use tower::ServiceBuilder; use tower_governor::{ governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer, }; @@ -34,6 +36,7 @@ use crate::{ grpc::ProxyServer, handlers::{desktop_client_mfa, enrollment, password_reset, polling}, proto::proxy_server, + VERSION, }; pub(crate) static ENROLLMENT_COOKIE_NAME: &str = "defguard_proxy"; @@ -166,8 +169,11 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { } else { Server::builder() }; + let versioned_service = ServiceBuilder::new() + .layer(DefguardVersionLayer::new(Version::parse(VERSION)?)) + .service(proxy_server::ProxyServer::new(grpc_server)); builder - .add_service(proxy_server::ProxyServer::new(grpc_server)) + .add_service(versioned_service) .serve(addr) .await .context("Error running gRPC server") diff --git a/src/logging.rs b/src/logging.rs index 983b455..9b97f93 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,5 +1,12 @@ +use defguard_version::{ + tracing::{ + build_version_suffix, extract_version_info_from_context, VersionFieldLayer, + VersionFilteredFields, VersionSuffixWriter, + }, + ComponentInfo, DefguardVersionError, Version, +}; use log::LevelFilter; -use tracing::{Event, Subscriber}; +use tracing::{Event, Level, Subscriber}; use tracing_subscriber::{ fmt::{ self, @@ -13,39 +20,48 @@ use tracing_subscriber::{ EnvFilter, }; -// Initializes tracing with the specified log level. +// Initializes tracing with the specified log level and version information. // Allows fine-grained filtering with `EnvFilter` directives. // The directives are read from `DEFGUARD_PROXY_LOG_FILTER` env variable. // For more info read: -pub fn init_tracing(level: &LevelFilter) { +pub fn init_tracing(own_version: Version, level: &LevelFilter) -> Result<(), DefguardVersionError> { tracing_subscriber::registry() .with( EnvFilter::try_from_env("DEFGUARD_PROXY_LOG_FILTER") .unwrap_or_else(|_| level.to_string().into()), ) - .with(fmt::layer().event_format(HttpFormatter::default())) + .with(VersionFieldLayer) + .with( + fmt::layer() + .event_format(HttpVersionFormatter::new(own_version)?) + .fmt_fields(VersionFilteredFields), + ) .init(); + info!("Tracing initialized"); + Ok(()) } -/// Implements fail2ban-friendly log format using `tracing_subscriber::fmt::format::FormatEvent` trait. +/// Implements fail2ban-friendly log format with version suffixes. /// HTTP info (if available) is extracted from the specified tracing span. The format is as follows: -/// TIMESTAMP LEVEL CLIENT_ADDR METHOD URI LOG_MESSAGE || TRACING_DATA -pub(crate) struct HttpFormatter<'a> { +/// TIMESTAMP LEVEL CLIENT_ADDR METHOD URI LOG_MESSAGE [VERSION_SUFFIXES] || TRACING_DATA +pub(crate) struct HttpVersionFormatter<'a> { span: &'a str, timer: SystemTime, + component_info: ComponentInfo, } -impl Default for HttpFormatter<'_> { - fn default() -> Self { - Self { +impl<'a> HttpVersionFormatter<'a> { + pub fn new(own_version: Version) -> Result { + Ok(Self { span: "http_request", timer: SystemTime, - } + component_info: ComponentInfo::new(own_version), + }) } } -impl HttpFormatter<'_> { +impl HttpVersionFormatter<'_> { fn format_timestamp(&self, writer: &mut Writer<'_>) -> std::fmt::Result { if self.timer.format_time(writer).is_err() { writer.write_str("")?; @@ -54,7 +70,7 @@ impl HttpFormatter<'_> { } } -impl FormatEvent for HttpFormatter<'_> +impl FormatEvent for HttpVersionFormatter<'_> where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'a> FormatFields<'a> + 'static, @@ -62,15 +78,20 @@ where fn format_event( &self, ctx: &FmtContext<'_, S, N>, - mut writer: format::Writer<'_>, + writer: format::Writer<'_>, event: &Event<'_>, ) -> std::fmt::Result { - let meta = event.metadata(); + // Extract version information + let extracted = extract_version_info_from_context(ctx); - // timestamp, level & target - self.format_timestamp(&mut writer)?; - write!(writer, "{} ", meta.level())?; - write!(writer, "{}: ", meta.target(),)?; + // Build version suffix + let is_error = *event.metadata().level() == Level::ERROR; + let version_suffix = build_version_suffix( + &extracted, + &self.component_info.version, + &self.component_info.system, + is_error, + ); // iterate and accumulate spans storing our special span in separate variable if encountered let mut context_logs = String::new(); @@ -82,7 +103,8 @@ where context_logs.push_str(&format!(" {span_name}")); seen = true; - if let Some(fields) = span.extensions().get::>() { + let extensions = span.extensions(); + if let Some(fields) = extensions.get::>() { if !fields.is_empty() { match span_name { x if x == self.span => http_log = Some(format!("{fields}")), @@ -97,6 +119,16 @@ where } } + // Create a wrapper writer that will append version info before newlines + let mut wrapper = VersionSuffixWriter::new(writer, version_suffix); + let mut versioned_writer = Writer::new(&mut wrapper); + + // timestamp, level & target + self.format_timestamp(&mut versioned_writer)?; + let meta = event.metadata(); + write!(versioned_writer, "{} ", meta.level())?; + write!(versioned_writer, "{}: ", meta.target(),)?; + // write http context log (ip, method, path) if let Some(log) = http_log { let split: Vec<&str> = log.split(['=', ' ']).collect(); @@ -107,16 +139,16 @@ where let ip = addr .and_then(|s| s.split(':').next().map(ToString::to_string)) .unwrap_or("unknown".to_string()); - write!(writer, "{ip} {method} {path} ")?; + write!(versioned_writer, "{ip} {method} {path} ")?; } // write actual log message - ctx.format_fields(writer.by_ref(), event)?; + ctx.format_fields(versioned_writer.by_ref(), event)?; // write span context if !context_logs.is_empty() { - write!(writer, " || Tracing data: {context_logs}")?; + write!(versioned_writer, " || Tracing data: {context_logs}")?; } - writeln!(writer) + writeln!(versioned_writer) } } diff --git a/src/main.rs b/src/main.rs index 1e22f07..0c3ef87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use defguard_proxy::{config::get_config, http::run_server, logging::init_tracing, VERSION}; +use defguard_version::Version; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -7,9 +8,9 @@ async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); } - // read config from env let config = get_config()?; - init_tracing(&config.log_level); + init_tracing(Version::parse(VERSION)?, &config.log_level)?; + // read config from env tracing::info!("Starting ... version v{}", VERSION); // run API web server