diff --git a/ipa-core/src/bin/helper.rs b/ipa-core/src/bin/helper.rs index 734db1bc5..8641f3547 100644 --- a/ipa-core/src/bin/helper.rs +++ b/ipa-core/src/bin/helper.rs @@ -8,17 +8,23 @@ use std::{ }; use clap::{self, Parser, Subcommand}; +use futures::future::join; use hyper::http::uri::Scheme; use ipa_core::{ cli::{ client_config_setup, keygen, test_setup, ConfGenArgs, KeygenArgs, LoggingHandle, TestSetupArgs, Verbosity, }, - config::{hpke_registry, HpkeServerConfig, NetworkConfig, ServerConfig, TlsConfig}, + config::{ + hpke_registry, sharded_server_from_toml_str, HpkeServerConfig, ServerConfig, TlsConfig, + }, error::BoxError, executor::IpaRuntime, helpers::HelperIdentity, - net::{ClientIdentity, IpaHttpClient, MpcHttpTransport, ShardHttpTransport}, + net::{ + ClientIdentity, ConnectionFlavor, IpaHttpClient, MpcHttpTransport, Shard, + ShardHttpTransport, + }, sharding::ShardIndex, AppConfig, AppSetup, NonZeroU32PowerOfTwo, }; @@ -55,16 +61,32 @@ struct ServerArgs { #[arg(short, long, required = true)] identity: Option, + #[arg(default_value = "0")] + shard_index: Option, + + #[arg(default_value = "1")] + shard_count: Option, + /// Port to listen on #[arg(short, long, default_value = "3000")] port: Option, - /// Use the supplied prebound socket instead of binding a new socket + /// Port to use for shard-to-shard communication, if sharded MPC is used + #[arg(default_value = "6000")] + shard_port: Option, + + /// Use the supplied prebound socket instead of binding a new socket for mpc /// /// This is only intended for avoiding port conflicts in tests. #[arg(hide = true, long)] server_socket_fd: Option, + /// Use the supplied prebound socket instead of binding a new socket for shard server + /// + /// This is only intended for avoiding port conflicts in tests. + #[arg(hide = true, long)] + shard_server_socket_fd: Option, + /// Use insecure HTTP #[arg(short = 'k', long)] disable_https: bool, @@ -73,7 +95,7 @@ struct ServerArgs { #[arg(long, required = true)] network: Option, - /// TLS certificate for helper-to-helper communication + /// TLS certificate for helper-to-helper and shard-to-shard communication #[arg( long, visible_alias("cert"), @@ -82,7 +104,7 @@ struct ServerArgs { )] tls_cert: Option, - /// TLS key for helper-to-helper communication + /// TLS key for helper-to-helper and shard-to-shard communication #[arg(long, visible_alias("key"), requires = "tls_cert")] tls_key: Option, @@ -114,24 +136,59 @@ fn read_file(path: &Path) -> Result, BoxError> { .map_err(|e| format!("failed to open file {}: {e:?}", path.display()))?) } -async fn server(args: ServerArgs, logging_handle: LoggingHandle) -> Result<(), BoxError> { - let my_identity = HelperIdentity::try_from(args.identity.expect("enforced by clap")).unwrap(); - - let (identity, server_tls) = match (args.tls_cert, args.tls_key) { +/// Helper function that creates the client identity; either with certificates if they are provided +/// or just with headers otherwise. This works both for sharded and helper configs. +fn create_client_identity( + id: F::Identity, + tls_cert: Option, + tls_key: Option, +) -> Result<(ClientIdentity, Option), BoxError> { + match (tls_cert, tls_key) { (Some(cert_file), Some(key_file)) => { let mut key = read_file(&key_file)?; let mut certs = read_file(&cert_file)?; - ( - ClientIdentity::from_pkcs8(&mut certs, &mut key)?, + Ok(( + ClientIdentity::::from_pkcs8(&mut certs, &mut key)?, Some(TlsConfig::File { certificate_file: cert_file, private_key_file: key_file, }), - ) + )) } - (None, None) => (ClientIdentity::Header(my_identity), None), - _ => panic!("should have been rejected by clap"), - }; + (None, None) => Ok((ClientIdentity::Header(id), None)), + _ => Err("should have been rejected by clap".into()), + } +} + +/// Creates a [`TcpListener`] from an optional raw file descriptor. Safety notes: +/// 1. The `--server-socket-fd` option is only intended for use in tests, not in production. +/// 2. This must be the only call to from_raw_fd for this file descriptor, to ensure it has +/// only one owner. +fn create_listener(server_socket_fd: Option) -> Result, BoxError> { + server_socket_fd + .map(|fd| { + let listener = unsafe { TcpListener::from_raw_fd(fd) }; + if listener.local_addr().is_ok() { + info!("adopting fd {fd} as listening socket"); + Ok(listener) + } else { + Err(BoxError::from(format!("the server was asked to listen on fd {fd}, but it does not appear to be a valid socket"))) + } + }) + .transpose() +} + +async fn server(args: ServerArgs, logging_handle: LoggingHandle) -> Result<(), BoxError> { + let my_identity = HelperIdentity::try_from(args.identity.expect("enforced by clap")).unwrap(); + let shard_index = ShardIndex::from(args.shard_index.expect("enforced by clap")); + let shard_count = ShardIndex::from(args.shard_count.expect("enforced by clap")); + assert!(shard_index < shard_count); + assert_eq!(args.tls_cert.is_some(), !args.disable_https); + + let (identity, server_tls) = + create_client_identity(my_identity, args.tls_cert.clone(), args.tls_key.clone())?; + let (shard_identity, shard_server_tls) = + create_client_identity(shard_index, args.tls_cert, args.tls_key)?; let mk_encryption = args.mk_private_key.map(|sk_path| HpkeServerConfig::File { private_key_file: sk_path, @@ -149,6 +206,13 @@ async fn server(args: ServerArgs, logging_handle: LoggingHandle) -> Result<(), B port: args.port, disable_https: args.disable_https, tls: server_tls, + hpke_config: mk_encryption.clone(), + }; + + let shard_server_config = ServerConfig { + port: args.shard_port, + disable_https: args.disable_https, + tls: shard_server_tls, hpke_config: mk_encryption, }; @@ -157,60 +221,53 @@ async fn server(args: ServerArgs, logging_handle: LoggingHandle) -> Result<(), B } else { Scheme::HTTPS }; - let network_config_path = args.network.as_deref().unwrap(); - let network_config = NetworkConfig::from_toml_str(&fs::read_to_string(network_config_path)?)? - .override_scheme(&scheme); - // TODO: Following is just temporary until Shard Transport is actually used. - let shard_clients_config = network_config.client.clone(); - let shard_server_config = server_config.clone(); - // --- + let network_config_path = args.network.as_deref().unwrap(); + let network_config_string = &fs::read_to_string(network_config_path)?; + let (mut mpc_network, mut shard_network) = sharded_server_from_toml_str( + network_config_string, + my_identity, + shard_index, + shard_count, + args.shard_port, + )?; + mpc_network = mpc_network.override_scheme(&scheme); + shard_network = shard_network.override_scheme(&scheme); let http_runtime = new_http_runtime(&logging_handle); let clients = IpaHttpClient::from_conf( &IpaRuntime::from_tokio_runtime(&http_runtime), - &network_config, + &mpc_network, &identity, ); let (transport, server) = MpcHttpTransport::new( IpaRuntime::from_tokio_runtime(&http_runtime), my_identity, server_config, - network_config, + mpc_network, &clients, Some(handler), ); - // TODO: Following is just temporary until Shard Transport is actually used. - let shard_network_config = NetworkConfig::new_shards(vec![], shard_clients_config); - let (shard_transport, _shard_server) = ShardHttpTransport::new( + let shard_clients = IpaHttpClient::::shards_from_conf( + &IpaRuntime::from_tokio_runtime(&http_runtime), + &shard_network, + &shard_identity, + ); + let (shard_transport, shard_server) = ShardHttpTransport::new( IpaRuntime::from_tokio_runtime(&http_runtime), - ShardIndex::FIRST, - ShardIndex::from(1), + shard_index, + shard_count, shard_server_config, - shard_network_config, - vec![], + shard_network, + shard_clients, Some(shard_handler), ); - // --- let _app = setup.connect(transport.clone(), shard_transport.clone()); - let listener = args.server_socket_fd - .map(|fd| { - // SAFETY: - // 1. The `--server-socket-fd` option is only intended for use in tests, not in production. - // 2. This must be the only call to from_raw_fd for this file descriptor, to ensure it has - // only one owner. - let listener = unsafe { TcpListener::from_raw_fd(fd) }; - if listener.local_addr().is_ok() { - info!("adopting fd {fd} as listening socket"); - Ok(listener) - } else { - Err(BoxError::from(format!("the server was asked to listen on fd {fd}, but it does not appear to be a valid socket"))) - } - }) - .transpose()?; + let listener = create_listener(args.server_socket_fd)?; + let shard_listener = create_listener(args.shard_server_socket_fd)?; let (_addr, server_handle) = server .start_on( @@ -220,8 +277,17 @@ async fn server(args: ServerArgs, logging_handle: LoggingHandle) -> Result<(), B None as Option<()>, ) .await; + let (_saddr, shard_server_handle) = shard_server + .start_on( + &IpaRuntime::from_tokio_runtime(&http_runtime), + shard_listener, + // TODO, trace based on the content of the query. + None as Option<()>, + ) + .await; + + join(server_handle, shard_server_handle).await; - server_handle.await; [query_runtime, http_runtime].map(Runtime::shutdown_background); Ok(()) diff --git a/ipa-core/src/cli/clientconf.rs b/ipa-core/src/cli/clientconf.rs index 341a4253a..222b26420 100644 --- a/ipa-core/src/cli/clientconf.rs +++ b/ipa-core/src/cli/clientconf.rs @@ -17,6 +17,9 @@ pub struct ConfGenArgs { #[arg(short, long, num_args = 3, value_name = "PORT", default_values = vec!["3000", "3001", "3002"])] ports: Vec, + #[arg(short, long, num_args = 3, value_name = "SHARD_PORTS", default_values = vec!["6000", "6001", "6002"])] + shard_ports: Vec, + #[arg(long, num_args = 3, default_values = vec!["localhost", "localhost", "localhost"])] hosts: Vec, @@ -54,13 +57,14 @@ pub struct ConfGenArgs { /// [`ConfGenArgs`]: ConfGenArgs /// [`Paths`]: crate::cli::paths::PathExt pub fn setup(args: ConfGenArgs) -> Result<(), BoxError> { - let clients_conf: [_; 3] = zip(args.hosts.iter(), args.ports) + let clients_conf: [_; 3] = zip(args.hosts.iter(), zip(args.ports, args.shard_ports)) .enumerate() - .map(|(id, (host, port))| { + .map(|(id, (host, (port, shard_port)))| { let id: u8 = u8::try_from(id).unwrap() + 1; HelperClientConf { host, port, + shard_port, tls_cert_file: args.keys_dir.helper_tls_cert(id), mk_public_key_file: args.keys_dir.helper_mk_public_key(id), } @@ -96,6 +100,7 @@ pub fn setup(args: ConfGenArgs) -> Result<(), BoxError> { pub struct HelperClientConf<'a> { pub(crate) host: &'a str, pub(crate) port: u16, + pub(crate) shard_port: u16, pub(crate) tls_cert_file: PathBuf, pub(crate) mk_public_key_file: PathBuf, } @@ -133,6 +138,14 @@ pub fn gen_client_config<'a>( port = client_conf.port )), ); + peer.insert( + String::from("shard_url"), + Value::String(format!( + "{host}:{port}", + host = client_conf.host, + port = client_conf.shard_port + )), + ); peer.insert(String::from("certificate"), Value::String(certificate)); peer.insert( String::from("hpke"), diff --git a/ipa-core/src/cli/test_setup.rs b/ipa-core/src/cli/test_setup.rs index 538faf180..a3aa93cc4 100644 --- a/ipa-core/src/cli/test_setup.rs +++ b/ipa-core/src/cli/test_setup.rs @@ -36,6 +36,9 @@ pub struct TestSetupArgs { #[arg(short, long, num_args = 3, value_name = "PORT", default_values = vec!["3000", "3001", "3002"])] ports: Vec, + + #[arg(short, long, num_args = 3, value_name = "SHARD_PORT", default_values = vec!["6000", "6001", "6002"])] + shard_ports: Vec, } /// Prepare a test network of three helpers. @@ -56,8 +59,8 @@ pub fn test_setup(args: TestSetupArgs) -> Result<(), BoxError> { let localhost = String::from("localhost"); - let clients_config: [_; 3] = zip([1, 2, 3], args.ports) - .map(|(id, port)| { + let clients_config: [_; 3] = zip([1, 2, 3], zip(args.ports, args.shard_ports)) + .map(|(id, (port, shard_port))| { let keygen_args = KeygenArgs { name: localhost.clone(), tls_cert: args.output_dir.helper_tls_cert(id), @@ -72,6 +75,7 @@ pub fn test_setup(args: TestSetupArgs) -> Result<(), BoxError> { Ok(HelperClientConf { host: &localhost, port, + shard_port, tls_cert_file: keygen_args.tls_cert, mk_public_key_file: keygen_args.mk_public_key, }) diff --git a/ipa-core/src/config.rs b/ipa-core/src/config.rs index 49384b814..403f38b24 100644 --- a/ipa-core/src/config.rs +++ b/ipa-core/src/config.rs @@ -3,6 +3,7 @@ use std::{ fmt::{Debug, Formatter}, iter::zip, path::PathBuf, + str::FromStr, time::Duration, }; @@ -16,7 +17,7 @@ use tokio::fs; use crate::{ error::BoxError, - helpers::HelperIdentity, + helpers::{HelperIdentity, TransportIdentity}, hpke::{ Deserializable as _, IpaPrivateKey, IpaPublicKey, KeyRegistry, PrivateKeyOnly, PublicKeyOnly, Serializable as _, @@ -32,10 +33,14 @@ pub type OwnedPrivateKey = PrivateKeyDer<'static>; pub enum Error { #[error(transparent)] ParseError(#[from] config::ConfigError), - #[error("invalid uri: {0}")] + #[error("Invalid uri: {0}")] InvalidUri(#[from] hyper::http::uri::InvalidUri), + #[error("Invalid network size {0}")] + InvalidNetworkSize(usize), #[error(transparent)] IOError(#[from] std::io::Error), + #[error("Missing shard URLs for peers {0:?}")] + MissingShardUrls(Vec), } /// Configuration describing either 3 peers in a Ring or N shard peers. In a non-sharded case a @@ -184,6 +189,167 @@ impl NetworkConfig { } } +/// This struct is only used by [`parse_sharded_network_toml`] to parse the entire network. +/// Unlike [`NetworkConfig`], this one doesn't have identities. +#[derive(Clone, Debug, Deserialize)] +struct ShardedNetworkToml { + pub peers: Vec, + + /// HTTP client configuration. + #[serde(default)] + pub client: ClientConfig, +} + +impl ShardedNetworkToml { + fn missing_shard_urls(&self) -> Vec { + self.peers + .iter() + .enumerate() + .filter_map(|(i, peer)| { + if peer.shard_url.is_some() { + None + } else { + Some(i) + } + }) + .collect() + } +} + +/// This struct is only used by [`parse_sharded_network_toml`] to generate [`PeerConfig`]. It +/// contains an optional `shard_url`. +#[derive(Clone, Debug, Deserialize)] +struct ShardedPeerConfigToml { + #[serde(flatten)] + pub config: PeerConfig, + + #[serde(default, with = "crate::serde::option::uri")] + pub shard_url: Option, +} + +impl ShardedPeerConfigToml { + /// Clones the inner Peer. + fn to_mpc_peer(&self) -> PeerConfig { + self.config.clone() + } + + /// Create a new Peer but its url using [`ShardedPeerConfigToml::shard_url`]. + fn to_shard_peer(&self) -> PeerConfig { + let mut shard_peer = self.config.clone(); + shard_peer.url = self.shard_url.clone().expect("Shard URL should be set"); + shard_peer + } +} + +/// Parses a [`ShardedNetworkToml`] from a network.toml file. Validates that sharding urls are set +/// if necessary. The number of peers needs to be a multiple of 3. +fn parse_sharded_network_toml(input: &str) -> Result { + use config::{Config, File, FileFormat}; + + let parsed: ShardedNetworkToml = Config::builder() + .add_source(File::from_str(input, FileFormat::Toml)) + .build()? + .try_deserialize()?; + + if parsed.peers.len() % 3 != 0 { + return Err(Error::InvalidNetworkSize(parsed.peers.len())); + } + + // Validate sharding config is set + let any_shard_url_set = parsed.peers.iter().any(|peer| peer.shard_url.is_some()); + if any_shard_url_set || parsed.peers.len() > 3 { + let missing_urls = parsed.missing_shard_urls(); + if !missing_urls.is_empty() { + return Err(Error::MissingShardUrls(missing_urls)); + } + } + + Ok(parsed) +} + +/// Reads a the config for a specific, single, sharded server from string. Expects config to be +/// toml format. The server in the network is specified via `id`, `shard_index` and +/// `shard_count`. This function expects shard urls to be set for all peers. +/// +/// The first 3 peers corresponds to the leaders Ring. H1 shard 0, H2 shard 0, and H3 shard 0. +/// The next 3 correspond to the next ring with `shard_index` equals 1 and so on. +/// +/// Other methods to read the network.toml exist depending on the use, for example +/// [`NetworkConfig::from_toml_str`] reads a non-sharded config. +/// TODO: There will be one to read the information relevant for the RC (doesn't need shard +/// info) +/// +/// # Errors +/// if `input` is in an invalid format +/// +/// # Panics +/// If you somehow provide an invalid non-sharded network toml +pub fn sharded_server_from_toml_str( + input: &str, + id: HelperIdentity, + shard_index: ShardIndex, + shard_count: ShardIndex, + shard_port: Option, +) -> Result<(NetworkConfig, NetworkConfig), Error> { + let all_network = parse_sharded_network_toml(input)?; + + let ix: usize = shard_index.as_index(); + let ix_count: usize = shard_count.as_index(); + // assert ix < count + let mpc_id: usize = id.as_index(); + + let mpc_network = NetworkConfig { + peers: all_network + .peers + .iter() + .map(ShardedPeerConfigToml::to_mpc_peer) + .skip(ix * 3) + .take(3) + .collect(), + client: all_network.client.clone(), + identities: HelperIdentity::make_three().to_vec(), + }; + let missing_urls = all_network.missing_shard_urls(); + if missing_urls.is_empty() { + let shard_network = NetworkConfig { + peers: all_network + .peers + .iter() + .map(ShardedPeerConfigToml::to_shard_peer) + .skip(mpc_id) + .step_by(3) + .take(ix_count) + .collect(), + client: all_network.client, + identities: shard_count.iter().collect(), + }; + Ok((mpc_network, shard_network)) + } else if missing_urls == [0, 1, 2] && shard_count == ShardIndex(1) { + // This is the special case we're dealing with a non-sharded, single ring MPC. + // Since the shard network will be of size 1, it can't really communicate with anyone else. + // Hence we just create a config where I'm the only shard. We take the MPC configuration + // and modify the port. + let mut myself = ShardedPeerConfigToml::to_mpc_peer(all_network.peers.get(mpc_id).unwrap()); + let url = myself.url.to_string(); + let pos = url.rfind(':'); + let port = shard_port.expect("Shard port should be set"); + let new_url = if pos.is_some() { + format!("{}{port}", &url[..=pos.unwrap()]) + } else { + format!("{}:{port}", &url) + }; + myself.url = Uri::from_str(&new_url).expect("Problem creating uri with sharded port"); + let shard_network = NetworkConfig { + peers: vec![myself], + client: all_network.client, + identities: shard_count.iter().collect(), + }; + Ok((mpc_network, shard_network)) + } else { + return Err(Error::MissingShardUrls(missing_urls)); + } +} + #[derive(Clone, Debug, Deserialize)] pub struct PeerConfig { /// Peer URL @@ -527,12 +693,17 @@ mod tests { use hpke::{kem::X25519HkdfSha256, Kem}; use hyper::Uri; + use once_cell::sync::Lazy; use rand::rngs::StdRng; use rand_core::SeedableRng; - use super::{NetworkConfig, PeerConfig}; + use super::{ + parse_sharded_network_toml, sharded_server_from_toml_str, NetworkConfig, PeerConfig, + }; use crate::{ - config::{ClientConfig, HpkeClientConfig, Http2Configurator, HttpClientConfigurator}, + config::{ + ClientConfig, Error, HpkeClientConfig, Http2Configurator, HttpClientConfigurator, + }, helpers::HelperIdentity, net::test::TestConfigBuilder, sharding::ShardIndex, @@ -579,7 +750,10 @@ mod tests { let mut rng = StdRng::seed_from_u64(1); let (_, public_key) = X25519HkdfSha256::gen_keypair(&mut rng); let config = HpkeClientConfig { public_key }; - assert_eq!(format!("{config:?}"), "HpkeClientConfig { public_key: \"2bd9da78f01d8bc6948bbcbe44ec1e7163d05083e267d110cdb2e75d847e3b6f\" }"); + assert_eq!( + format!("{config:?}"), + r#"HpkeClientConfig { public_key: "2bd9da78f01d8bc6948bbcbe44ec1e7163d05083e267d110cdb2e75d847e3b6f" }"# + ); } #[test] @@ -631,4 +805,342 @@ mod tests { let conf = NetworkConfig::new_shards(vec![pc1.clone()], client); assert_eq!(conf.peers[ShardIndex(0)].url, pc1.url); } + + #[test] + fn parse_sharded_server_happy() { + // Asuming position of the second helper in the second shard (the middle server in the 3 x 3) + let (mpc, shard) = sharded_server_from_toml_str( + &SHARDED_OK_REPEAT, + HelperIdentity::TWO, + ShardIndex::from(1), + ShardIndex::from(3), + None, + ) + .unwrap(); + assert_eq!( + vec![ + "helper1.shard1.org:443", + "helper2.shard1.org:443", + "helper3.shard1.org:443" + ], + mpc.peers + .into_iter() + .map(|p| p.url.to_string()) + .collect::>() + ); + assert_eq!( + vec![ + "helper2.shard0.org:555", + "helper2.shard1.org:555", + "helper2.shard2.org:555" + ], + shard + .peers + .into_iter() + .map(|p| p.url.to_string()) + .collect::>() + ); + } + + /// Tests that the url of a shard gets updated with the shard url. + #[test] + fn transform_sharded_peers() { + let mut n = parse_sharded_network_toml(&SHARDED_OK_REPEAT).unwrap(); + assert_eq!( + "helper3.shard2.org:666", + n.peers.pop().unwrap().to_shard_peer().url + ); + assert_eq!( + "helper2.shard2.org:555", + n.peers.pop().unwrap().to_shard_peer().url + ); + } + + /// Expects an error if the number of peers isn't a multiple of 3 + #[test] + fn invalid_nr_of_peers() { + assert!(matches!( + parse_sharded_network_toml(&SHARDED_8), + Err(Error::InvalidNetworkSize(_)) + )); + } + + /// If any sharded url is set (indicating this is a sharding config), then ALL urls must be set. + #[test] + fn parse_network_toml_shard_urls_some_set() { + assert!(matches!( + parse_sharded_network_toml(&SHARDED_COMPAT_ONE_URL), + Err(Error::MissingShardUrls(_)) + )); + } + + /// If there are more than 3 peers configured (indicating this is a sharding config), then ALL urls must be set. + #[test] + fn parse_network_toml_shard_urls_set() { + assert!(matches!( + parse_sharded_network_toml(&SHARDED_MISSING_URLS_REPEAT), + Err(Error::MissingShardUrls(_)) + )); + } + + /// Check that [`sharded_server_from_toml_str`] can work in the previous format. + #[test] + fn parse_sharded_without_shard_urls() { + let (mpc, mut shard) = sharded_server_from_toml_str( + &NON_SHARDED_COMPAT, + HelperIdentity::ONE, + ShardIndex::FIRST, + ShardIndex::from(1), + Some(666), + ) + .unwrap(); + assert_eq!(1, shard.peers.len()); + assert_eq!(3, mpc.peers.len()); + assert_eq!( + "helper1.org:666", + shard.peers.pop().unwrap().url.to_string() + ); + } + + /// Check that [`sharded_server_from_toml_str`] can work in the previous format, even when the + /// given MPC URL doesn't have a port (NOTE: helper 2 doesn't specify it). + #[test] + fn parse_sharded_without_shard_urls_no_port() { + let (mpc, mut shard) = sharded_server_from_toml_str( + &NON_SHARDED_COMPAT, + HelperIdentity::TWO, + ShardIndex::FIRST, + ShardIndex::from(1), + Some(666), + ) + .unwrap(); + assert_eq!(1, shard.peers.len()); + assert_eq!(3, mpc.peers.len()); + assert_eq!( + "helper2.org:666", + shard.peers.pop().unwrap().url.to_string() + ); + } + + /// Testing happy case of a sharded network config + #[test] + fn happy_parse_sharded_network_toml() { + let r_entire_network = parse_sharded_network_toml(SHARDED_OK); + assert!(r_entire_network.is_ok()); + let entire_network = r_entire_network.unwrap(); + assert!(matches!( + entire_network.client.http_config, + HttpClientConfigurator::Http2(_) + )); + assert_eq!(3, entire_network.peers.len()); + assert_eq!("helper3.shard0.org:443", entire_network.peers[2].config.url); + assert_eq!( + "helper3.shard0.org:666", + entire_network.peers[2] + .shard_url + .as_ref() + .unwrap() + .to_string() + ); + } + + /// Testing happy case of a longer sharded network config + #[test] + fn happy_parse_larger_sharded_network_toml() { + let r_entire_network = parse_sharded_network_toml(&SHARDED_OK_REPEAT); + assert!(r_entire_network.is_ok()); + let entire_network = r_entire_network.unwrap(); + assert_eq!(9, entire_network.peers.len()); + assert_eq!( + "helper3.shard2.org:666", + entire_network.peers[8] + .shard_url + .as_ref() + .unwrap() + .to_string() + ); + } + + /// This test validates that the new logic that handles sharded configurations can also handle the previous version + #[test] + fn parse_non_sharded_network_toml() { + let r_entire_network = parse_sharded_network_toml(&NON_SHARDED_COMPAT); + assert!(r_entire_network.is_ok()); + let entire_network = r_entire_network.unwrap(); + assert!(matches!( + entire_network.client.http_config, + HttpClientConfigurator::Http2(_) + )); + assert_eq!(3, entire_network.peers.len()); + assert_eq!("helper3.org:443", entire_network.peers[2].config.url); + } + + // Following are some large &str const used for tests + + /// Valid: A non-sharded network toml, just how they used to be + static NON_SHARDED_COMPAT: Lazy = Lazy::new(|| format!("{CLIENT}{P1}{REST}")); + + /// Invalid: Same as [`NON_SHARDED_COMPAT`] but with a single `shard_port` set. + static SHARDED_COMPAT_ONE_URL: Lazy = + Lazy::new(|| format!("{CLIENT}{P1}\nshard_url = \"helper1.org:777\"\n{REST}")); + + /// Helper const used to create client configs + const CLIENT: &str = r#"[client.http_config] +ping_interval_secs = 90.0 +version = "http2" +"#; + + /// Helper const that has the first part of a Peer, just before were `shard_port` should be + /// specified. + const P1: &str = r#" +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIIMlnveFys5QUwCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMS5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMwM1oXDTI0 +MTIwNDAzMzMwM1owJjEkMCIGA1UEAwwbaGVscGVyMS5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmrrkaKM7HQ0Y3ZGJtHB7vfG +cT/hDCXCoob4pJ/fpPDMrqhiwTTck3bNOuzv9QIx+p5C2Qp8u67rYfK78w86NaNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMS5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAKVdDCQeXLRXDYXy4b1N1UxD/JPuD9H7zeRb8/nmIDTfAiBL +a6L0t1Ug8i2RcequSo21x319Tvs5nUbGwzMFSS5wKA== +-----END CERTIFICATE----- +""" +url = "helper1.org:443""#; + + /// The rest of a configuration + /// Note the second helper doesn't provide a port as part of its url + const REST: &str = r#" +[peers.hpke] +public_key = "f458d5e1989b2b8f5dacd4143276aa81eaacf7449744ab1251ff667c43550756" + +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIITOtoca16QckwCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMi5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMwOFoXDTI0 +MTIwNDAzMzMwOFowJjEkMCIGA1UEAwwbaGVscGVyMi5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETxOH4ATz6kBxLuRznKDFRugm +XKmH7mzRB9wn5vaVlVpDzf4nDHJ+TTzSS6Lb3YLsA7jrXDx+W7xPLGow1+9FNqNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMi5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAI4G5ICVm+v5KK5Y8WVetThtNCXGykUBAM1eE973FBOUAiAS +XXgJe9q9hAfHf0puZbv0j0tGY3BiqCkJJaLvK7ba+g== +-----END CERTIFICATE----- +""" +url = "helper2.org" + +[peers.hpke] +public_key = "62357179868e5594372b801ddf282c8523806a868a2bff2685f66aa05ffd6c22" + +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIIaf7eDCnXh2swCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMy5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMxMloXDTI0 +MTIwNDAzMzMxMlowJjEkMCIGA1UEAwwbaGVscGVyMy5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIMqxCCtu4joFr8YtOrEtq230 +NuTtUAaJHIHNtv4CvpUcbtlFMWFYUUum7d22A8YTfUeccG5PsjjCoQG/dhhSbKNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMy5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAOTSQWbN7kfIatNJEwWTBL4xOY88E3+SOnBNExCsTkQuAiBB +/cwOQQUEeE4llrDp+EnyGbzmVm5bINz8gePIxkKqog== +-----END CERTIFICATE----- +""" +url = "helper3.org:443" + +[peers.hpke] +public_key = "55f87a8794b4de9a60f8ede9ed000f5f10c028e22390922efc4fb63bc6be0a61" +"#; + + /// Valid: A sharded configuration + const SHARDED_OK: &str = r#" +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIIMlnveFys5QUwCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMS5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMwM1oXDTI0 +MTIwNDAzMzMwM1owJjEkMCIGA1UEAwwbaGVscGVyMS5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmrrkaKM7HQ0Y3ZGJtHB7vfG +cT/hDCXCoob4pJ/fpPDMrqhiwTTck3bNOuzv9QIx+p5C2Qp8u67rYfK78w86NaNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMS5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAKVdDCQeXLRXDYXy4b1N1UxD/JPuD9H7zeRb8/nmIDTfAiBL +a6L0t1Ug8i2RcequSo21x319Tvs5nUbGwzMFSS5wKA== +-----END CERTIFICATE----- +""" +url = "helper1.shard0.org:443" +shard_url = "helper1.shard0.org:444" + +[peers.hpke] +public_key = "f458d5e1989b2b8f5dacd4143276aa81eaacf7449744ab1251ff667c43550756" + +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIITOtoca16QckwCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMi5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMwOFoXDTI0 +MTIwNDAzMzMwOFowJjEkMCIGA1UEAwwbaGVscGVyMi5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETxOH4ATz6kBxLuRznKDFRugm +XKmH7mzRB9wn5vaVlVpDzf4nDHJ+TTzSS6Lb3YLsA7jrXDx+W7xPLGow1+9FNqNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMi5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAI4G5ICVm+v5KK5Y8WVetThtNCXGykUBAM1eE973FBOUAiAS +XXgJe9q9hAfHf0puZbv0j0tGY3BiqCkJJaLvK7ba+g== +-----END CERTIFICATE----- +""" +url = "helper2.shard0.org:443" +shard_url = "helper2.shard0.org:555" + +[peers.hpke] +public_key = "62357179868e5594372b801ddf282c8523806a868a2bff2685f66aa05ffd6c22" + +[[peers]] +certificate = """ +-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIIaf7eDCnXh2swCgYIKoZIzj0EAwIwJjEkMCIGA1UEAwwb +aGVscGVyMy5wcm9kLmlwYS1oZWxwZXIuZGV2MB4XDTI0MDkwNDAzMzMxMloXDTI0 +MTIwNDAzMzMxMlowJjEkMCIGA1UEAwwbaGVscGVyMy5wcm9kLmlwYS1oZWxwZXIu +ZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIMqxCCtu4joFr8YtOrEtq230 +NuTtUAaJHIHNtv4CvpUcbtlFMWFYUUum7d22A8YTfUeccG5PsjjCoQG/dhhSbKNZ +MFcwJgYDVR0RBB8wHYIbaGVscGVyMy5wcm9kLmlwYS1oZWxwZXIuZGV2MA4GA1Ud +DwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCgYIKoZI +zj0EAwIDSAAwRQIhAOTSQWbN7kfIatNJEwWTBL4xOY88E3+SOnBNExCsTkQuAiBB +/cwOQQUEeE4llrDp+EnyGbzmVm5bINz8gePIxkKqog== +-----END CERTIFICATE----- +""" +url = "helper3.shard0.org:443" +shard_url = "helper3.shard0.org:666" + +[peers.hpke] +public_key = "55f87a8794b4de9a60f8ede9ed000f5f10c028e22390922efc4fb63bc6be0a61" +"#; + + /// Valid: Three sharded configs together for 9 + static SHARDED_OK_REPEAT: Lazy = Lazy::new(|| { + format!( + "{}{}{}", + SHARDED_OK, + SHARDED_OK.replace("shard0", "shard1"), + SHARDED_OK.replace("shard0", "shard2") + ) + }); + + /// Invalid: A network toml with 8 entries + static SHARDED_8: Lazy = Lazy::new(|| { + let last_peers_index = SHARDED_OK_REPEAT.rfind("[[peers]]").unwrap(); + SHARDED_OK_REPEAT[..last_peers_index].to_string() + }); + + /// Invalid: Same as [`SHARDED_OK_REPEAT`] but without the expected ports + static SHARDED_MISSING_URLS_REPEAT: Lazy = Lazy::new(|| { + let lines: Vec<&str> = SHARDED_OK_REPEAT.lines().collect(); + let new_lines: Vec = lines + .iter() + .filter(|line| !line.starts_with("shard_url =")) + .map(std::string::ToString::to_string) + .collect(); + new_lines.join("\n") + }); } diff --git a/ipa-core/src/serde.rs b/ipa-core/src/serde.rs index 0acc2d925..ed65273d7 100644 --- a/ipa-core/src/serde.rs +++ b/ipa-core/src/serde.rs @@ -13,6 +13,27 @@ pub mod uri { } } +#[cfg(feature = "web-app")] +pub mod option { + pub mod uri { + use hyper::Uri; + use serde::{de::Error, Deserialize, Deserializer}; + + /// # Errors + /// if deserializing from string fails, or if string is not a [`Uri`] + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let opt_s: Option = Deserialize::deserialize(deserializer)?; + if let Some(s) = opt_s { + s.parse().map(Some).map_err(D::Error::custom) + } else { + Ok(None) + } + } + } +} + pub mod duration { use std::time::Duration; diff --git a/ipa-core/tests/common/mod.rs b/ipa-core/tests/common/mod.rs index ca1d5e08a..dae743987 100644 --- a/ipa-core/tests/common/mod.rs +++ b/ipa-core/tests/common/mod.rs @@ -109,9 +109,9 @@ impl CommandExt for Command { } } -fn test_setup(config_path: &Path) -> [TcpListener; 3] { - let sockets: [_; 3] = array::from_fn(|_| TcpListener::bind("127.0.0.1:0").unwrap()); - let ports: [u16; 3] = sockets +fn test_setup(config_path: &Path) -> [TcpListener; 6] { + let sockets: [_; 6] = array::from_fn(|_| TcpListener::bind("127.0.0.1:0").unwrap()); + let ports: [u16; 6] = sockets .each_ref() .map(|sock| sock.local_addr().unwrap().port()); @@ -121,7 +121,9 @@ fn test_setup(config_path: &Path) -> [TcpListener; 3] { .arg("test-setup") .args(["--output-dir".as_ref(), config_path.as_os_str()]) .arg("--ports") - .args(ports.map(|p| p.to_string())); + .args(ports.iter().take(3).map(|p| p.to_string())) + .arg("--shard-ports") + .args(ports.iter().skip(3).take(3).map(|p| p.to_string())); command.status().unwrap_status(); sockets @@ -129,45 +131,53 @@ fn test_setup(config_path: &Path) -> [TcpListener; 3] { pub fn spawn_helpers( config_path: &Path, - sockets: &[TcpListener; 3], + sockets: &[TcpListener; 6], https: bool, ) -> Vec { - zip([1, 2, 3], sockets) - .map(|(id, socket)| { - let mut command = Command::new(HELPER_BIN); + zip( + [1, 2, 3], + zip(sockets.iter().take(3), sockets.iter().skip(3).take(3)), + ) + .map(|(id, (socket, shard_socket))| { + let mut command = Command::new(HELPER_BIN); + command + .args(["-i", &id.to_string()]) + .args(["--network".into(), config_path.join("network.toml")]) + .silent(); + + if https { command - .args(["-i", &id.to_string()]) - .args(["--network".into(), config_path.join("network.toml")]) - .silent(); - - if https { - command - .args(["--tls-cert".into(), config_path.join(format!("h{id}.pem"))]) - .args(["--tls-key".into(), config_path.join(format!("h{id}.key"))]) - .args([ - "--mk-public-key".into(), - config_path.join(format!("h{id}_mk.pub")), - ]) - .args([ - "--mk-private-key".into(), - config_path.join(format!("h{id}_mk.key")), - ]); - } else { - command.arg("--disable-https"); - } - - command.preserved_fds(vec![socket.as_raw_fd()]); - command.args(["--server-socket-fd", &socket.as_raw_fd().to_string()]); - - // something went wrong if command is terminated at this point. - let mut child = command.spawn().unwrap(); - if let Ok(Some(status)) = child.try_wait() { - panic!("Helper binary terminated early with status = {status}"); - } - - child.terminate_on_drop() - }) - .collect::>() + .args(["--tls-cert".into(), config_path.join(format!("h{id}.pem"))]) + .args(["--tls-key".into(), config_path.join(format!("h{id}.key"))]) + .args([ + "--mk-public-key".into(), + config_path.join(format!("h{id}_mk.pub")), + ]) + .args([ + "--mk-private-key".into(), + config_path.join(format!("h{id}_mk.key")), + ]); + } else { + command.arg("--disable-https"); + } + + command.preserved_fds(vec![socket.as_raw_fd()]); + command.args(["--server-socket-fd", &socket.as_raw_fd().to_string()]); + command.preserved_fds(vec![shard_socket.as_raw_fd()]); + command.args([ + "--shard-server-socket-fd", + &shard_socket.as_raw_fd().to_string(), + ]); + + // something went wrong if command is terminated at this point. + let mut child = command.spawn().unwrap(); + if let Ok(Some(status)) = child.try_wait() { + panic!("Helper binary terminated early with status = {status}"); + } + + child.terminate_on_drop() + }) + .collect::>() } pub fn test_multiply(config_dir: &Path, https: bool) { diff --git a/ipa-core/tests/helper_networks.rs b/ipa-core/tests/helper_networks.rs index 7775ffba4..06adb56a7 100644 --- a/ipa-core/tests/helper_networks.rs +++ b/ipa-core/tests/helper_networks.rs @@ -71,8 +71,8 @@ fn keygen_confgen() { let dir = TempDir::new_delete_on_drop(); let path = dir.path(); - let sockets: [_; 3] = array::from_fn(|_| TcpListener::bind("127.0.0.1:0").unwrap()); - let ports: [u16; 3] = sockets + let sockets: [_; 6] = array::from_fn(|_| TcpListener::bind("127.0.0.1:0").unwrap()); + let ports: [u16; 6] = sockets .each_ref() .map(|sock| sock.local_addr().unwrap().port()); @@ -85,7 +85,9 @@ fn keygen_confgen() { .args(["--output-dir".as_ref(), path.as_os_str()]) .args(["--keys-dir".as_ref(), path.as_os_str()]) .arg("--ports") - .args(ports.map(|p| p.to_string())) + .args(ports.iter().take(3).map(|p| p.to_string())) + .arg("--shard-ports") + .args(ports.iter().skip(3).take(3).map(|p| p.to_string())) .arg("--hosts") .args(["localhost", "localhost", "localhost"]); if overwrite {