diff --git a/relay/sources/relayd/Cargo.lock b/relay/sources/relayd/Cargo.lock index 67b36ac1454..a52cccd043a 100644 --- a/relay/sources/relayd/Cargo.lock +++ b/relay/sources/relayd/Cargo.lock @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -788,9 +788,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" dependencies = [ "wasm-bindgen", ] @@ -1609,15 +1609,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] @@ -1634,9 +1634,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -1645,9 +1645,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c5e91e4240b46c4c19219d6cc84784444326131a4210f496f948d5cc827a29" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -1694,9 +1694,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +checksum = "740223c51853f3145fe7c90360d2d4232f2b62e3449489c207eccde818979982" dependencies = [ "lazy_static", ] @@ -2153,9 +2153,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if", "serde", @@ -2165,9 +2165,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", @@ -2180,9 +2180,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c" dependencies = [ "cfg-if", "js-sys", @@ -2192,9 +2192,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2202,9 +2202,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", @@ -2215,15 +2215,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/relay/sources/relayd/src/api/remote_run.rs b/relay/sources/relayd/src/api/remote_run.rs index 32d36a20f55..d5d582c566e 100644 --- a/relay/sources/relayd/src/api/remote_run.rs +++ b/relay/sources/relayd/src/api/remote_run.rs @@ -279,7 +279,7 @@ impl RemoteRun { } let client = match job_config.downstream_clients.get(&id) { - Some(c) => c.clone(), + Some(c) => c.read().await.clone(), None => { error!("unknown sub-relay '{}'", id); return Box::new(futures::stream::empty()); @@ -287,6 +287,7 @@ impl RemoteRun { }; let response = client + .client() .post(&format!( "https://{}:{}/rudder/relay-api/remote-run/{}", hostname, diff --git a/relay/sources/relayd/src/api/shared_files.rs b/relay/sources/relayd/src/api/shared_files.rs index 1e74b9187ac..66bee35ad6b 100644 --- a/relay/sources/relayd/src/api/shared_files.rs +++ b/relay/sources/relayd/src/api/shared_files.rs @@ -160,9 +160,8 @@ async fn put_forward( job_config: Arc, body: Bytes, ) -> Result { - job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + client .put(&format!( "{}/{}/{}", job_config.cfg.upstream_url(), @@ -299,9 +298,9 @@ async fn head_forward( params: SharedFilesHeadParams, job_config: Arc, ) -> Result { - job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + + client .head(&format!( "{}/{}/{}", job_config.cfg.upstream_url(), diff --git a/relay/sources/relayd/src/configuration/main.rs b/relay/sources/relayd/src/configuration/main.rs index 8cd139f15f2..8abab324d6f 100644 --- a/relay/sources/relayd/src/configuration/main.rs +++ b/relay/sources/relayd/src/configuration/main.rs @@ -121,6 +121,16 @@ impl Configuration { }) } + pub fn peer_authentication(&self) -> PeerAuthentication { + // compute actual model + if !self.output.upstream.verify_certificates { + warn!("output.upstream.verify_certificates parameter is deprecated, use general.peer_authentication instead"); + PeerAuthentication::DangerousNone + } else { + self.general.peer_authentication + } + } + /// Gives current url of the upstream relay API /// Can be removed once upstream.url is removed pub fn upstream_url(&self) -> String { @@ -175,7 +185,7 @@ pub struct GeneralConfig { pub https_port: u16, /// Which certificate validation model to use #[serde(default = "GeneralConfig::default_peer_authentication")] - pub peer_authentication: PeerAuthentication, + peer_authentication: PeerAuthentication, } impl GeneralConfig { diff --git a/relay/sources/relayd/src/lib.rs b/relay/sources/relayd/src/lib.rs index 0882e8ab1d5..75612a61704 100644 --- a/relay/sources/relayd/src/lib.rs +++ b/relay/sources/relayd/src/lib.rs @@ -51,6 +51,7 @@ use tracing_subscriber::{ }; lazy_static! { + /// User-Agent used in our HTTP requests static ref USER_AGENT: String = format!("rudder-relayd/{}", crate_version!()); } @@ -210,39 +211,102 @@ fn signal_handlers(job_config: Arc) { }); } -// Graceful reload/restart -// -// If we reload parts of the config we need to reload everything. -// That means restarting all tokio tasks. -// -// We could use a channel to tell all tasks to finish what they are doing and stop. -// -// Potentially remote-run could take minutes to run, we need to decide what to do in this case. +// WARNING we do not reload content of main.conf, only data files it points at +// FIXME add to doc + +// Le client REQWEST SE CLONE + +// AJOUTER DES LOGS POUR POUVOIR TESTER + +#[derive(Clone, Debug)] +pub enum HttpClient { + /// Keep the associated certificates to be able to compare afterwards + /// + /// We can't currently compare reqwest::Certificate or openssl::X509Ref, + /// so we'll compare pem exports. + Pinned(Client, Vec), + System(Client), + NoVerify(Client), +} + +type PemCertificate = Vec; + +// With Rudder cert model we currently need one client for each host we talk to. +// Fortunately we only talk with other policy servers, which are only a few. // -// Tasks could start making a copy if the config they use to allow correct reload. +// Not efficient in "System" case, but it's deprecated anyway. // +// A future improvement could be to implement public key pinning in the reqwest-hyper-tokio stack. +impl HttpClient { + /// Common parameters + fn new_client_builder() -> reqwest::ClientBuilder { + Client::builder() + // enforce HTTPS to prevent misconfigurations + .https_only(true) + .user_agent(USER_AGENT.clone()) + } + + // Not very efficient as we parse a cert just dumped by openssl + fn new_pinned(certs: Vec) -> Result { + let mut client = Self::new_client_builder() + .danger_accept_invalid_hostnames(true) + .tls_built_in_root_certs(false); + for cert in &certs { + client = client.add_root_certificate(Certificate::from_pem(cert)?); + } + Ok(Self::Pinned(client.build()?, certs)) + } + + fn new_system() -> Result { + Ok(Self::System(Self::new_client_builder().build()?)) + } + + fn new_no_verify() -> Result { + Ok(Self::System( + Self::new_client_builder() + .danger_accept_invalid_certs(true) + .build()?, + )) + } + + // FIXME rename or remove + pub fn client(&self) -> &Client { + match *self { + Self::Pinned(ref c, _) => c, + Self::System(ref c) => c, + Self::NoVerify(ref c) => c, + } + } + + /// If order is not good, reload + /// We have only one certificate anyway + pub fn needs_reload(&self, certs: Option<&[PemCertificate]>) -> bool { + match (self, certs) { + (&Self::Pinned(_, ref current), Some(certs)) => current.as_slice() != certs, + (&Self::Pinned(_, _), None) => { + unreachable!("Client uses cert-pinning, checking requires a cert list") + } + (_, _) => false, + } + } +} pub struct JobConfig { /// Does not reload, by definition pub cli_cfg: CliConfiguration, - pub cfg: Configuration, pub nodes: RwLock, pub pool: Option, /// Parent policy server - pub upstream_client: Client, + pub upstream_client: RwLock, /// Sub relays // TODO could be lazily created - pub downstream_clients: HashMap, + pub downstream_clients: HashMap>, handle: LogHandle, } impl JobConfig { - pub fn new( - cli_cfg: CliConfiguration, - cfg: Configuration, - handle: LogHandle, - ) -> Result, Error> { + fn create_dirs(cfg: &Configuration) -> Result<(), Error> { // Create needed directories if cfg.processing.inventory.output != InventoryOutputSelect::Disabled { create_dir_all(cfg.processing.inventory.directory.join("incoming"))?; @@ -259,6 +323,18 @@ impl JobConfig { create_dir_all(cfg.processing.reporting.directory.join("failed"))?; } + Ok(()) + } + + // FIXME split client creation into a reentrable function for reloading + + pub fn new( + cli_cfg: CliConfiguration, + cfg: Configuration, + handle: LogHandle, + ) -> Result, Error> { + Self::create_dirs(&cfg)?; + let pool = if cfg.processing.reporting.output == ReportingOutputSelect::Database { Some(pg_pool(&cfg.output.database)?) } else { @@ -268,33 +344,20 @@ impl JobConfig { // HTTP client // - // compute actual model - let model = if !cfg.output.upstream.verify_certificates { - warn!("output.upstream.verify_certificates parameter is deprecated, use general.certificate_verification_model instead"); - PeerAuthentication::DangerousNone - } else { - cfg.general.peer_authentication - }; + // actual model + let model = cfg.peer_authentication(); let upstream_client = match model { PeerAuthentication::CertPinning => { - let cert = Certificate::from_pem(&fs::read( - &cfg.output.upstream.server_certificate_file, - )?)?; - Self::new_http_client(vec![cert])? + let cert = fs::read(&cfg.output.upstream.server_certificate_file)?; + HttpClient::new_pinned(vec![cert]) } - PeerAuthentication::SystemRootCerts => Client::builder() - .user_agent(USER_AGENT.clone()) - .https_only(true) - .build()?, + PeerAuthentication::SystemRootCerts => HttpClient::new_system(), PeerAuthentication::DangerousNone => { warn!("Certificate verification is disabled, it should not be done in production"); - Client::builder() - .user_agent(USER_AGENT.clone()) - .danger_accept_invalid_certs(true) - .build()? + HttpClient::new_no_verify() } - }; + }?; let nodes = NodesList::new( cfg.node_id()?, @@ -312,27 +375,16 @@ impl JobConfig { Some(stack) => stack .into_iter() // certificate has already be parsed by openssl, assume it's correct - .map(|c| Certificate::from_pem(&c.to_pem().unwrap()).unwrap()) + .map(|c| c.to_pem().unwrap()) .collect(), None => vec![], }; - Self::new_http_client(certs)? + HttpClient::new_pinned(certs) } - PeerAuthentication::SystemRootCerts => Client::builder() - .user_agent(USER_AGENT.clone()) - .https_only(true) - .build()?, - PeerAuthentication::DangerousNone => { - warn!( - "Certificate verification is disabled, it should not be done in production" - ); - Client::builder() - .user_agent(USER_AGENT.clone()) - .danger_accept_invalid_certs(true) - .build()? - } - }; - downstream_clients.insert(id, client); + PeerAuthentication::SystemRootCerts => HttpClient::new_system(), + PeerAuthentication::DangerousNone => HttpClient::new_no_verify(), + }?; + downstream_clients.insert(id, RwLock::new(client)); } } @@ -344,7 +396,7 @@ impl JobConfig { nodes, pool, handle, - upstream_client, + upstream_client: RwLock::new(upstream_client), downstream_clients, })) } @@ -375,29 +427,30 @@ impl JobConfig { SUB_NODES.set(nodes.sub_nodes() as i64); } + async fn reload_http_clients(&self) -> Result<(), Error> { + // upstream client + let cert = fs::read(&self.cfg.output.upstream.server_certificate_file)?; + let certs = vec![cert]; + let needs_reload = self.upstream_client.read().await.needs_reload(Some(&certs)); + if needs_reload { + let mut upstream_client = self.upstream_client.write().await; + *upstream_client = HttpClient::new_pinned(certs)?; + } + + // sub-relays + + + Ok(()) + } + pub async fn reload(&self) -> Result<(), Error> { info!("Configuration reload requested"); self.reload_logging()?; self.reload_nodeslist().await?; + // We need up-to-date certs + // so run after nodes list refresh + self.reload_http_clients().await?; self.reload_metrics().await; Ok(()) } - - // With Rudder cert model we currently need one client for each host we talk to - // - // Not efficient in "System" case, but it's deprecated anyway - fn new_http_client(certs: Vec) -> Result { - let mut client = Client::builder().user_agent(USER_AGENT.clone()); - - client = client - // Let's enforce https to prevent misconfigurations - .https_only(true) - .danger_accept_invalid_hostnames(true) - .tls_built_in_root_certs(false); - for cert in certs { - client = client.add_root_certificate(cert); - } - - Ok(client.build()?) - } } diff --git a/relay/sources/relayd/src/output/upstream.rs b/relay/sources/relayd/src/output/upstream.rs index 5e1fcdc1ae0..fe2ca5109bd 100644 --- a/relay/sources/relayd/src/output/upstream.rs +++ b/relay/sources/relayd/src/output/upstream.rs @@ -48,9 +48,9 @@ async fn forward_file( ) -> Result<(), Error> { let content = tokio::fs::read(path.clone()).await?; - let result = job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + + let result = client .put(&format!( "{}/{}/{}", job_config.cfg.upstream_url(),