diff --git a/Cargo.lock b/Cargo.lock index e2460de..2c9c0d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1762,7 +1762,7 @@ dependencies = [ [[package]] name = "juicebox_marshalling" -version = "0.1.1" +version = "0.2.0" dependencies = [ "ciborium", "curve25519-dalek", @@ -1771,7 +1771,7 @@ dependencies = [ [[package]] name = "juicebox_networking" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-trait", "http", @@ -1788,7 +1788,7 @@ dependencies = [ [[package]] name = "juicebox_noise" -version = "0.1.1" +version = "0.2.0" dependencies = [ "blake2", "chacha20poly1305", @@ -1804,7 +1804,7 @@ dependencies = [ [[package]] name = "juicebox_oprf" -version = "0.1.1" +version = "0.2.0" dependencies = [ "curve25519-dalek", "digest", @@ -1819,7 +1819,7 @@ dependencies = [ [[package]] name = "juicebox_process_group" -version = "0.1.1" +version = "0.2.0" dependencies = [ "libc", "nix", @@ -1828,7 +1828,7 @@ dependencies = [ [[package]] name = "juicebox_realm_api" -version = "0.1.1" +version = "0.2.0" dependencies = [ "blake2", "curve25519-dalek", @@ -1848,7 +1848,7 @@ dependencies = [ [[package]] name = "juicebox_realm_auth" -version = "0.1.1" +version = "0.2.0" dependencies = [ "hex", "jsonwebtoken", @@ -1859,7 +1859,7 @@ dependencies = [ [[package]] name = "juicebox_sdk" -version = "0.1.1" +version = "0.2.0" dependencies = [ "argon2", "async-trait", @@ -1890,7 +1890,7 @@ dependencies = [ [[package]] name = "juicebox_secret_sharing" -version = "0.1.1" +version = "0.2.0" dependencies = [ "curve25519-dalek", "itertools 0.11.0", @@ -1943,6 +1943,7 @@ dependencies = [ "rustls 0.20.8", "rustls-pemfile", "secret_manager", + "semver", "serde", "service_core", "signal-hook", diff --git a/Cargo.toml b/Cargo.toml index a87aac1..8cf4f20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,6 +141,7 @@ rustls = "0.20.8" rustls-pemfile = "1.0" secrecy = { version = "0.8.0", features = ["serde"] } secret_manager = { path = "secret_manager" } +semver = "1.0.18" serde = { version = "1.0.152", default-features = false, features = [ "alloc", "derive", diff --git a/load_balancer/Cargo.toml b/load_balancer/Cargo.toml index edf910b..b624656 100644 --- a/load_balancer/Cargo.toml +++ b/load_balancer/Cargo.toml @@ -29,6 +29,7 @@ opentelemetry-http = { workspace = true } rustls = { workspace = true } rustls-pemfile = { workspace = true } secret_manager = { workspace = true } +semver = { workspace = true } serde = { workspace = true } service_core = { workspace = true } signal-hook = { workspace = true } diff --git a/load_balancer/src/load_balancer.rs b/load_balancer/src/load_balancer.rs index 902d358..2770fcf 100644 --- a/load_balancer/src/load_balancer.rs +++ b/load_balancer/src/load_balancer.rs @@ -7,9 +7,11 @@ use futures::Future; use http_body_util::{BodyExt, Full, LengthLimitError, Limited}; use hyper::server::conn::http1; use hyper::service::Service; +use hyper::StatusCode; use hyper::{body::Incoming as IncomingBody, Request, Response}; use opentelemetry_http::HeaderExtractor; use rustls::server::ResolvesServerCert; +use semver::Version; use serde::Serialize; use std::collections::HashMap; use std::error::Error; @@ -32,7 +34,7 @@ use juicebox_marshalling as marshalling; use juicebox_networking::reqwest::{Client, ClientOptions}; use juicebox_networking::rpc; use juicebox_realm_api::requests::{ClientRequest, ClientResponse, BODY_SIZE_LIMIT}; -use juicebox_realm_api::types::RealmId; +use juicebox_realm_api::types::{RealmId, JUICEBOX_VERSION_HEADER}; use juicebox_realm_auth::validation::Validator as AuthTokenValidator; use observability::logging::{Spew, TracingSource}; use observability::metrics::{self, Tag}; @@ -50,6 +52,7 @@ struct State { agent_client: Client, realms: Mutex>>>, metrics: metrics::Client, + semver: Version, } static TCP_ACCEPT_SPEW: Spew = Spew::new(); @@ -70,6 +73,7 @@ impl LoadBalancer { agent_client: Client::new(ClientOptions::default()), realms: Mutex::new(Arc::new(HashMap::new())), metrics, + semver: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(), })) } @@ -234,7 +238,35 @@ impl Service> for LoadBalancer { if request.uri().path() == "/livez" { return Ok(Response::builder() .status(200) - .body(Full::from(Bytes::from("Juicebox load balancer: OK\n"))) + .body(Full::from(Bytes::from(format!( + "Juicebox load balancer: {}\n", + state.semver + )))) + .unwrap()); + } + + let has_valid_version = request + .headers() + .get(JUICEBOX_VERSION_HEADER) + .and_then(|version| version.to_str().ok()) + .and_then(|str| Version::parse(str).ok()) + .is_some_and(|semver| { + // verify that major.minor is >= to our major.minor + // patch and bugfix versions can be out-of-sync + // to allow the SDK and realm software to make + // changes that don't break protocol compatability + semver.major > state.semver.major + || (semver.major == state.semver.major + && semver.minor >= state.semver.minor) + }); + + if !has_valid_version { + return Ok(Response::builder() + .status(StatusCode::UPGRADE_REQUIRED) + .body(Full::from(Bytes::from(format!( + "SDK upgrade required to version >={}.{}", + state.semver.major, state.semver.minor + )))) .unwrap()); } diff --git a/sdk b/sdk index 129de9f..aedf534 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 129de9f8a3d470ea95a6fb0f3df924abf24f5410 +Subproject commit aedf5342ca69bc5f655e82d0fa5b3e9ad6f5e828 diff --git a/testing/tests/loadbalancer.rs b/testing/tests/loadbalancer.rs index c8ecbae..3948401 100644 --- a/testing/tests/loadbalancer.rs +++ b/testing/tests/loadbalancer.rs @@ -1,5 +1,7 @@ -use http::StatusCode; +use http::{HeaderMap, HeaderValue, StatusCode}; +use juicebox_sdk::{JUICEBOX_VERSION_HEADER, VERSION}; use once_cell::sync::Lazy; + use std::path::PathBuf; use std::time::Duration; @@ -42,6 +44,13 @@ async fn request_bodysize_check() { .use_rustls_tls(); b = b.add_root_certificate(cluster.lb_cert()); + let mut headers = HeaderMap::new(); + headers.append( + JUICEBOX_VERSION_HEADER, + HeaderValue::from_str(VERSION).unwrap(), + ); + b = b.default_headers(headers); + let http = b.build().unwrap(); let req = vec![1; BODY_SIZE_LIMIT + 1]; let res = http