diff --git a/Cargo.lock b/Cargo.lock index bc0117e1..5c81a21c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -241,7 +241,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", ] [[package]] @@ -250,6 +250,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -596,9 +605,9 @@ checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "der" @@ -1638,6 +1647,7 @@ dependencies = [ name = "krilltest" version = "0.1.0" dependencies = [ + "bytes", "clap", "hex", "indoc", @@ -1856,9 +1866,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2422,9 +2432,9 @@ checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", @@ -4278,10 +4288,11 @@ dependencies = [ [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec 0.9.1", "time", ] diff --git a/krilltest/Cargo.toml b/krilltest/Cargo.toml index 379ac943..b2693c20 100644 --- a/krilltest/Cargo.toml +++ b/krilltest/Cargo.toml @@ -8,14 +8,15 @@ rust-version.workspace = true license.workspace = true [dependencies] -krill = { path = ".." } +bytes = "1" clap = { version = "*", features = ["cargo", "derive"]} +hex = "0.4.3" +indoc = "2.0.7" +krill = { path = ".." } nix = { version = "*", features = ["signal"] } +rand = "0.10.2" rcgen = "0.14" reqwest = { version = "*", features = ["blocking"] } -tempfile = "*" -indoc = "2.0.7" -hex = "0.4.3" -rand = "0.10.2" rpki = "0.19.3" +tempfile = "*" diff --git a/krilltest/src/environment/core.rs b/krilltest/src/environment/core.rs index be06673f..3baca937 100644 --- a/krilltest/src/environment/core.rs +++ b/krilltest/src/environment/core.rs @@ -1,13 +1,11 @@ //! The test environment. -#![allow(unused)] - -use std::collections::HashMap; -use std::net::IpAddr; -use std::path::PathBuf; use super::krill::KrillServer; use super::nginx::NginxServer; use super::routinator::Routinator; +use std::collections::HashMap; +use std::net::IpAddr; +use std::path::PathBuf; //------------ Environment --------------------------------------------------- @@ -71,8 +69,7 @@ impl Environment { krill_bin, self.base_dir.join("krill"), listen, - "https://testbed.example.com/rrdp/".into(), - "rsync://testbed.example.com/repo/".into(), + self.nginx.base_url(), true, ); let name = name.to_string(); diff --git a/krilltest/src/environment/krill.rs b/krilltest/src/environment/krill.rs index ead21614..2eb0b6fd 100644 --- a/krilltest/src/environment/krill.rs +++ b/krilltest/src/environment/krill.rs @@ -26,11 +26,16 @@ pub struct KrillServer { /// The listen address for the server. listen: (IpAddr, u16), - /// The URI for the RRDP server. - rrdp_uri: Https, + /// The listen address for the nginx proxy in front of Krill. + /// + /// When Krill advertizes itself to the outside world, for example in a + /// TAL file, it needs to mention this address, not its own address. + public_base_url: String, - /// The URI for the rsync server. - rsync_uri: Rsync, + /// Whether or not this Krill instance should act as a testbed. + /// + /// A testbed is a combined RPKI trust anchor and publication server. + is_testbed: bool, /// The Krill process if it is running. process: Option, @@ -42,22 +47,19 @@ impl KrillServer { krill_bin: String, server_dir: PathBuf, listen: (IpAddr, u16), - rrdp_uri: String, - rsync_uri: String, - enable_ta: bool, + public_base_url: String, + is_testbed: bool, ) -> Self { - let rrdp_uri = Https::from_string(rrdp_uri).unwrap(); - let rsync_uri = Rsync::from_string(rsync_uri).unwrap(); let mut res = Self { krill: krill_bin, server_dir, listen, - rrdp_uri, - rsync_uri, + public_base_url, + is_testbed, process: None, }; fs::create_dir_all(&res.server_dir).unwrap(); - res.make_conf(enable_ta); + res.make_conf(); res.start(); res } @@ -84,15 +86,29 @@ impl KrillServer { fn pid_file(&self) -> PathBuf { self.server_dir.join("krill.pid") } + + /// Returns the base URL at which Krill can be contacted by clients. + /// + /// If Krill is fronted by a proxy like nginx this will point to the + /// proxy rather than to Krill itself. + fn public_base_url(&self) -> &str { + &self.public_base_url + } + + /// Returns the public URL at which the Trust Anchor Locator can be found. + pub fn tal_url(&self) -> String { + format!("{}ta/ta.tal", self.public_base_url()) + } } /// # Setup impl KrillServer { /// Creates the Krill config. - fn make_conf(&self, enable_ta: bool) { + fn make_conf(&self) { let mut conf = File::create(self.config_path()).unwrap(); // Create string representations of configuration values. + let service_uri = &self.public_base_url; let storage_uri = format!("memory://{}", hex::encode(rand::random::<[u8; 8]>())); // tls_keys_dir, repo_dir and pid_file must be set because we are @@ -122,16 +138,23 @@ impl KrillServer { bgp_riswhois_enabled = false post_protocol_msg_timeout_seconds = 10 unix_socket = "{unix_socket}" + service_uri = "{service_uri}" "# ); - if enable_ta { - let rrdp_base_uri = &self.rrdp_uri; - let rsync_jail = &self.rsync_uri; - let ta_aia = - format!("rsync://{}/ta/ta.cer", rsync_jail.authority()); - let ta_uri = - format!("https://{}/ta/ta.cer", rrdp_base_uri.authority()); + if self.is_testbed { + // A note about rsync: we have to configure Krill with an rsync + // URI, but Krill itself is not capable of acting as an rsync + // server. In the rsync URIs below no port number is specified, + // thus multiple Krill instances created with is_testbed = true + // would refer to the same rsync server. However at the time of + // writing there is no rsync server in our test setup and RPs are + // expected to use RRDP rather than rsync, i.e. these URIs have to + // be specified but will not be used. + let rsync_jail = format!("rsync://{}/repo/", self.listen.0); + let rrdp_base_uri = format!("{}rrdp/", self.public_base_url()); + let ta_aia = format!("rsync://{}/ta/ta.cer", self.listen.0); + let ta_uri = format!("{}ta/ta.cer", self.public_base_url()); writedoc!( conf, diff --git a/krilltest/src/environment/nginx.rs b/krilltest/src/environment/nginx.rs index 28c33165..b6cdfb4e 100644 --- a/krilltest/src/environment/nginx.rs +++ b/krilltest/src/environment/nginx.rs @@ -1,10 +1,10 @@ -//! Controlling an Nginx server. +//! Controlling an Nginx server. +use crate::utils::fmt::WriteOrPanic; -use std::{fs, process}; use std::fs::File; use std::net::IpAddr; use std::path::PathBuf; -use crate::utils::fmt::WriteOrPanic; +use std::{fs, process}; use indoc::writedoc; @@ -21,9 +21,6 @@ pub struct NginxServer { /// The listen address for the server. listen: (IpAddr, u16), - /// A map between path prefixes and directories. - routes: Vec<(String, PathBuf)>, - /// The Nginx process if it is running. process: Option, } @@ -39,22 +36,14 @@ impl NginxServer { nginx: nginx_bin, server_dir, listen, - routes: Default::default(), process: None, }; fs::create_dir_all(res.tls_path()).unwrap(); res.make_tls(); - - fs::create_dir_all(res.root_path()).unwrap(); - fs::write( - res.root_path().join("test.txt"), - "test" - ).unwrap(); - res.make_conf(); res.start(); - + res } } @@ -75,7 +64,6 @@ impl Drop for NginxServer { } } - /// # Paths to things impl NginxServer { /// Returns the directory for the TLS configuration. @@ -105,11 +93,14 @@ impl NginxServer { /// Returns the server root path. fn root_path(&self) -> PathBuf { - self.server_dir.join("http") + // TODO: Don't hard-code the location of a specific Krill instance + // RRDP data directory here, as we may want to serve data published by + // multiple Krill instances. + self.server_dir.join("../krill/data/repo/") } /// Returns the base URL of the server. - pub fn url(&self) -> String { + pub fn base_url(&self) -> String { match self.listen.0 { IpAddr::V4(addr) => { format!("https://{}:{}/", addr, self.listen.1) @@ -119,11 +110,6 @@ impl NginxServer { } } } - - /// Returns the URL of the test file. - pub fn test_url(&self) -> String { - format!("{}test.txt", self.url()) - } } /// # Setup @@ -154,19 +140,6 @@ impl NginxServer { let ssl_certificate_key = self.tls_key_path().display().to_string(); let tmp = self.tmp_path().display().to_string(); - let mut locations = String::new(); - for (location, alias) in &self.routes { - let alias = alias.display().to_string(); - writedoc!( - locations, - r#" - location {location} {{ - alias {alias}; - }} - "# - ); - } - // Write the NGINX config file using the strings we just created. writedoc!( conf, @@ -174,6 +147,12 @@ impl NginxServer { events {{}} daemon off; pid {tmp}/pid; + # error_log set here occurs too late to prevent a warning + # during nginx startup about not being able to write to + # /var/log/nginx/error.log, to solve that we pass -e when + # launching nginx. We do however need to specify error_log + # here if we want to control the level at which nginx logs. + # error_log /dev/stdout debug; http {{ proxy_temp_path {tmp}; fastcgi_temp_path {tmp}; @@ -187,7 +166,56 @@ impl NginxServer { ssl_certificate {ssl_certificate}; ssl_certificate_key {ssl_certificate_key}; client_body_temp_path {tmp}; - {locations} + + # From Krill docs: + client_max_body_size 128m; + + # TODO: Make the location blocks below dynamically + # generated rather than hard-coded, so that we can + # support multiple Krill instances behind the nginx. + + # Proxy RFC 8181 publication server requests to Krill. + location /rfc8181 {{ + proxy_pass https://127.0.0.1:3001/rfc8181; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # krill does not use a valid certificate/tls is handled by nginx + proxy_ssl_verify off; + }} + + # Proxy Krill API requests to Krill. + location /api {{ + proxy_pass https://127.0.0.1:3001/api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # allow IPv4 and IPv6 documentation ranges + # allow 192.0.2.0/24; + # allow 2001:0db8::/32; + # deny all; + + # krill does not use a valid certificate/tls is handled by nginx + proxy_ssl_verify off; + }} + + # Serve RRDP files generated by Krill + # This is handled by the 'root' directive above. + + location /ta {{ + proxy_pass https://127.0.0.1:3001/ta; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # krill does not use a valid certificate/tls is handled by nginx + proxy_ssl_verify off; + }} }} }} "# diff --git a/krilltest/src/environment/routinator.rs b/krilltest/src/environment/routinator.rs index 5ec95c5e..505d5df1 100644 --- a/krilltest/src/environment/routinator.rs +++ b/krilltest/src/environment/routinator.rs @@ -1,9 +1,15 @@ //! Controlling a Routinator. #![allow(unused)] -use std::fs; -use std::path::PathBuf; +use crate::utils::fmt::WriteOrPanic; +use std::fs::File; +use std::net::IpAddr; +use std::os::unix::process::CommandExt; +use std::path::PathBuf; +use std::{fs, process}; + +use indoc::writedoc; //------------ Routinator ---------------------------------------------------- @@ -15,8 +21,8 @@ pub struct Routinator { /// The directory where Routinator will keep its stuff. base_dir: PathBuf, - /// Path to the TLS certificate used by Nginx server. - tls_cert_path: PathBuf, + /// The path to the TLS certificate used by the RRDP server. + rrdp_tls_cert_path: PathBuf, } impl Routinator { @@ -26,16 +32,99 @@ impl Routinator { pub fn new( routinator_bin: PathBuf, base_dir: PathBuf, - tls_cert_path: PathBuf, + rrdp_tls_cert_path: PathBuf, ) -> Self { - let res = Self { + let mut res = Self { routinator: routinator_bin, base_dir, - tls_cert_path, + rrdp_tls_cert_path, }; fs::create_dir(&res.base_dir).unwrap(); + fs::create_dir(&res.extra_tals_dir()).unwrap(); + res.make_conf(); res } } +/// # Paths and URLs +impl Routinator { + /// Returns the path to the Routinator config file. + fn config_path(&self) -> PathBuf { + self.base_dir.join("routinator.conf") + } + /// Returns the Routinator repository directory + fn repo_dir(&self) -> PathBuf { + self.base_dir.join("rpki-cache") + } + + fn extra_tals_dir(&self) -> PathBuf { + self.base_dir.join("extra-tals") + } +} + +/// # Setup +impl Routinator { + fn make_conf(&self) { + let mut conf = File::create(self.config_path()).unwrap(); + + // Create string representations of configuration values. + let repository_dir = self.repo_dir().display().to_string(); + let rrdp_root_cert_path = + self.rrdp_tls_cert_path.display().to_string(); + + // Write the Routinator config file using the strings we just created. + writedoc!( + conf, + r#" + repository-dir = "{repository_dir}" + log = "stderr" + log-level = "info" + rrdp-root-certs = ["{rrdp_root_cert_path}"] + allow-dubious-hosts = true + #disable-rsync = true + "# + ); + } + + /// Store the given TAL bytes as a file in the correct location so that + /// Routinator will use it. + pub fn install_tal(&self, name: &str, bytes: &[u8]) { + let mut f = File::create_new( + self.extra_tals_dir().join(name).with_added_extension("tal"), + ) + .unwrap(); + std::io::Write::write_all(&mut f, bytes).unwrap(); + } + + /// Update the local repository, validate the ROAs and return the VRPs. + /// + /// TODO: Deserialize the JSON into Rust data types. + pub fn vrps(&self) -> Vec { + let output = process::Command::new(&self.routinator) + .args([ + // Tell Routinator where to find its config file. + "-c", + &self.config_path().display().to_string(), + // Don't use any of the default Trust Anchor Locators. + "--no-rir-tals", + // Use only the TAL of our Krill instance. + "--extra-tals-dir", + &self.extra_tals_dir().display().to_string(), + // Do a one time validation run. + "vrps", + // Output in JSON format. + "--format", + "json", + // Output to standard output. + "--output", + "-", + ]) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + + output.stdout + } +} diff --git a/krilltest/src/main.rs b/krilltest/src/main.rs index 4f1dad0f..db7c07f0 100644 --- a/krilltest/src/main.rs +++ b/krilltest/src/main.rs @@ -4,12 +4,18 @@ //! scenarios involve setting up one or more Krill instances, issue commands //! to them, and then verifying the results by checking the data set output //! by a Routinator validation run. +use krilltest::environment::Environment; + +use std::io::Read; +use std::io::Write; +use std::net::IpAddr; +use std::path::Path; +use std::path::PathBuf; +use std::thread::sleep; +use std::time::Duration; use clap::Parser; use clap::crate_version; -use krilltest::environment::Environment; -use std::net::IpAddr; -use std::path::PathBuf; use tempfile::TempDir; //------------ main ---------------------------------------------------------- @@ -31,15 +37,73 @@ fn main() { args.routinator, ); - let some_free_port = args.rrdp_port + 1; - let krill_listen = (args.listen_addr, some_free_port); - let _krill = environment.add_krill("first", args.krill, krill_listen); + // Add a Krill test bed to the test environment and get the location of + // Trust Anchor Locator so that we can install it for Routinator to use. + let tal_url = { + // TODO: Allocate a port that isn't already in use, don't just do +1. + let some_free_port1 = args.rrdp_port + 1; + let krill_listen = (args.listen_addr, some_free_port1); + let krill = environment.add_krill("first", args.krill, krill_listen); + krill.tal_url() + }; - eprintln!("Hit enter to quit."); + // Load the TLS certificate that can be used to verify that a TLS + // connection to Krill can be trusted. The alternative would be to use the + // reqwest `danger_accept_invalid_certs(true)` functionality but this is + // more correct, but also more verbose. + let tls_cert = load_tls_cert(&environment.nginx().tls_cert_path()); - let mut buffer = String::new(); - let stdin = std::io::stdin(); - stdin.read_line(&mut buffer).unwrap(); + // Fetch the Krill TAL and install it in the Routinator extra tals + // directory. + let tal_bytes = fetch_url(tal_url, tls_cert, 5); + + // Configure Routinator to use the Krill TAL. + environment.routinator().install_tal("Krill", &tal_bytes); + + // Do a Routinator validation run and fetch the available VRPs. + std::io::stdout() + .write_all(&environment.routinator().vrps()) + .unwrap(); + + // TODO: Actually add ROAs to Krill and verify that the fetched VRPs are + // correct. +} + +fn load_tls_cert(tls_cert_path: &Path) -> reqwest::Certificate { + let mut f = std::fs::File::open(tls_cert_path).unwrap(); + let mut tls_ca_cert_pem_bytes = vec![]; + f.read_to_end(&mut tls_ca_cert_pem_bytes).unwrap(); + drop(f); + let tls_ca_cert = + reqwest::Certificate::from_pem(&tls_ca_cert_pem_bytes).unwrap(); + tls_ca_cert +} + +fn fetch_url( + url: String, + tls_cert: reqwest::Certificate, + max_tries: u8, +) -> bytes::Bytes { + let trusting_client = reqwest::blocking::Client::builder() + .tls_certs_only([tls_cert]) + .build() + .unwrap(); + let mut tries_left = max_tries; + while tries_left > 0 { + match trusting_client.get(&url).send() { + Ok(bytes) => { + return bytes.bytes().unwrap(); + } + Err(err) => { + eprintln!( + "{url} not yet available, will retry in 1 second: {err}" + ); + sleep(Duration::from_secs(1)); + } + } + tries_left -= 1; + } + unreachable!(); } //------------ Args ----------------------------------------------------------