diff --git a/Cargo.lock b/Cargo.lock index 308c6e4fa..81b85fa31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1944,6 +1944,7 @@ dependencies = [ "diesel_migrations", "docopt", "env_logger", + "exit_trust_root", "futures 0.3.31", "ipnetwork", "lazy_static", @@ -3229,6 +3230,7 @@ dependencies = [ "clarity", "crypto_box", "deep_space", + "exit_trust_root", "handlebars", "ipnetwork", "lazy_static", diff --git a/althea_types/src/exits/server_list_signatures.rs b/althea_types/src/exits/server_list_signatures.rs index cf1a00006..9bb25c41f 100644 --- a/althea_types/src/exits/server_list_signatures.rs +++ b/althea_types/src/exits/server_list_signatures.rs @@ -63,10 +63,15 @@ impl ExitServerList { if sig.is_valid() { let hash = get_ethereum_msg_hash(&self.encode_to_eth_abi()); match sig.recover(&hash) { - Ok(addr) => addr == key, - Err(_) => false, + Ok(addr) => { + println!("Recovered address is {:?}", addr); + addr == key}, + Err(_) => { + println!("Failed to recover address from signature"); + false}, } } else { + println!("Signature is invalid"); false } } diff --git a/exit_trust_root/Cargo.toml b/exit_trust_root/Cargo.toml index 483c33c02..5ffe86737 100644 --- a/exit_trust_root/Cargo.toml +++ b/exit_trust_root/Cargo.toml @@ -5,8 +5,12 @@ edition = "2021" license = "Apache-2.0" description = "Server to provide a root of exit trust for Althea routers" -[[bin]] +[lib] name = "exit_trust_root" +path = "src/lib.rs" + +[[bin]] +name = "exit_trust_root_server" path = "src/bin.rs" [dependencies] diff --git a/exit_trust_root/src/bin.rs b/exit_trust_root/src/bin.rs index a3a989b47..ed4832808 100644 --- a/exit_trust_root/src/bin.rs +++ b/exit_trust_root/src/bin.rs @@ -1,129 +1,8 @@ -use actix_web::rt::System; -use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; -use althea_types::{ExitServerList, SignedExitServerList}; -use clarity::Address; -use config::{load_config, CONFIG}; use env_logger::Env; -use log::info; -use rita_client_registration::client_db::get_exits_list; -use rustls::ServerConfig; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use std::thread; -use std::time::Duration; -use tls::{load_certs, load_rustls_private_key}; -use web30::client::Web3; -use web30::jsonrpc::error::Web3Error; - -pub mod config; -pub mod tls; - -pub const DEVELOPMENT: bool = cfg!(feature = "development"); -const SSL: bool = !DEVELOPMENT; -pub const DOMAIN: &str = if cfg!(test) || DEVELOPMENT { - "localhost" -} else { - "exitroot.althea.net" -}; -/// The backend RPC port for the info server fucntions implemented in this repo -const SERVER_PORT: u16 = 9000; - -/// This endpoint retrieves and signs the data from any specified exit contract, -/// allowing this server to serve as a root of trust for several different exit contracts. -#[get("/{exit_contract}")] -async fn return_exit_contract_data( - exit_contract: web::Path
, - cache: web::Data>>>, -) -> impl Responder { - let contract = exit_contract.into_inner(); - let cached_list = { - let cache_read = cache.read().unwrap(); - cache_read.get(&contract).cloned() - }; - - match cached_list { - Some(list) => { - // return a signed exit server list based on the given key - HttpResponse::Ok().json(list) - } - None => match retrieve_exit_server_list(contract, cache.get_ref().clone()).await { - Ok(list) => { - HttpResponse::Ok().json(list) - } - Err(e) => { - info!("Failed to get exit list from contract {:?}", e); - HttpResponse::InternalServerError().json("Failed to get exit list from contract") - } - }, - } -} - -async fn retrieve_exit_server_list( - exit_contract: Address, - cache: Arc>>, -) -> Result { - const WEB3_TIMEOUT: Duration = Duration::from_secs(10); - let exits = get_exits_list( - &Web3::new("https://dai.althea.net", WEB3_TIMEOUT), - CONFIG.clarity_private_key.to_address(), - exit_contract, - ) - .await; - match exits { - Ok(exits) => { - info!("Got exit list from contract"); - let exit_list = ExitServerList { - contract: exit_contract, - exit_list: exits, - created: std::time::SystemTime::now(), - }; - let cache_value = exit_list.sign(CONFIG.clarity_private_key); - - // add this new exit list to the cache - cache - .write() - .unwrap() - .insert(exit_contract, cache_value.clone()); - Ok(cache_value) - } - Err(e) => { - info!("Failed to get exit list from contract {:?}", e); - Err(e) - } - } -} - -// five minutes -const SIGNATURE_UPDATE_SLEEP: Duration = Duration::from_secs(300); - -/// In order to improve scalability this loop grabs and signs an updated list of exits from each exit contract -/// that has previously been requested from this server every 5 minutes. This allows the server to return instantly -/// on the next request from the client without having to perform rpc query 1-1 with requests. -fn signature_update_loop(cache: Arc>>) { - thread::spawn(move || loop { - let runner = System::new(); - runner.block_on(async { - let cache_iter = cache.read().unwrap().clone(); - for (exit_contract, _value) in cache_iter.iter() { - // get the latest exit list from the contract - match retrieve_exit_server_list(*exit_contract, cache.clone()).await { - // grab the cache here so we don't lock it while awaiting for every single contract - Ok(cache_value) => { - // update the cache - cache.write().unwrap().insert(*exit_contract, cache_value); - } - Err(e) => { - info!("Failed to get exit list from contract {:?}", e); - } - } - } - }); - thread::sleep(SIGNATURE_UPDATE_SLEEP); - }); -} +use exit_trust_root::{config::load_config, start_exit_trust_root_server}; #[actix_web::main] -async fn main() -> std::io::Result<()> { +async fn main() { openssl_probe::init_ssl_cert_env_vars(); env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); @@ -131,31 +10,5 @@ async fn main() -> std::io::Result<()> { // lazy static variable after this load_config(); - let exit_contract_data_cache: Arc>> = - Arc::new(RwLock::new(HashMap::new())); - signature_update_loop(exit_contract_data_cache.clone()); - let web_data = web::Data::new(exit_contract_data_cache.clone()); - - let server = HttpServer::new(move || { - App::new() - .service(return_exit_contract_data) - .app_data(web_data.clone()) - }); - let server = if SSL { - let cert_chain = load_certs(&format!("/etc/letsencrypt/live/{}/fullchain.pem", DOMAIN)); - let keys = - load_rustls_private_key(&format!("/etc/letsencrypt/live/{}/privkey.pem", DOMAIN)); - let config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(cert_chain, keys) - .unwrap(); - - info!("Binding to SSL"); - server.bind_rustls(format!("{}:{}", DOMAIN, SERVER_PORT), config.clone())? - } else { - server.bind(format!("{}:{}", DOMAIN, SERVER_PORT))? - }; - - server.run().await + start_exit_trust_root_server(); } diff --git a/exit_trust_root/src/config.rs b/exit_trust_root/src/config.rs index c236a4883..dc558330d 100644 --- a/exit_trust_root/src/config.rs +++ b/exit_trust_root/src/config.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::DEVELOPMENT; + ///Struct containing settings for Exit root server #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ConfigStruct { @@ -39,7 +40,10 @@ impl ConfigStruct { pub fn load_config() -> ConfigStruct { // change the config name based on our development status let file_name = if DEVELOPMENT || cfg!(test) { - "config.toml" + return ConfigStruct { + clarity_private_key: PrivateKey::from_bytes([1u8; 32]).unwrap(), + wg_private_key: WgKey::from([2; 32]), + } } else { "/etc/exit_root_server.toml" }; diff --git a/exit_trust_root/src/lib.rs b/exit_trust_root/src/lib.rs new file mode 100644 index 000000000..739c386d0 --- /dev/null +++ b/exit_trust_root/src/lib.rs @@ -0,0 +1,186 @@ +use actix_web::rt::System; +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use althea_types::{ExitServerList, SignedExitServerList}; +use clarity::Address; +use config::CONFIG; +use log::info; +use rita_client_registration::client_db::get_exits_list; +use rustls::ServerConfig; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; +use tls::{load_certs, load_rustls_private_key}; +use web30::client::Web3; +use web30::jsonrpc::error::Web3Error; + +pub mod config; +pub mod tls; + +const RPC_SERVER: &str = "https://dai.althea.net"; +const WEB3_TIMEOUT: Duration = Duration::from_secs(10); + +// five minutes +const SIGNATURE_UPDATE_SLEEP: Duration = Duration::from_secs(300); + +pub const DEVELOPMENT: bool = cfg!(feature = "development"); +const SSL: bool = !DEVELOPMENT; +// todo this is copied over from exit root server, deduplicate +pub const EXIT_ROOT_DOMAIN: &str = if cfg!(test) || cfg!(feature = "development") { + "http://10.0.0.1:4050" +} else { + "https://exitroot.althea.net" +}; +/// The backend RPC port for the info server fucntions implemented in this repo +pub const SERVER_PORT: u16 = 4050; + +/// This endpoint retrieves and signs the data from any specified exit contract, +/// allowing this server to serve as a root of trust for several different exit contracts. +#[get("/{exit_contract}")] +async fn return_exit_contract_data( + exit_contract: web::Path
, + cache: web::Data>>>, +) -> impl Responder { + let contract: Address = exit_contract.into_inner(); + let cached_list = { + let cache_read = cache.read().unwrap(); + cache_read.get(&contract).cloned() + }; + + match cached_list { + Some(list) => { + // return a signed exit server list based on the given key + HttpResponse::Ok().json(list) + } + None => match retrieve_exit_server_list(contract, cache.get_ref().clone()).await { + Ok(list) => HttpResponse::Ok().json(list), + Err(e) => { + info!("Failed to get exit list from contract {:?}", e); + HttpResponse::InternalServerError().json("Failed to get exit list from contract") + } + }, + } +} + +async fn retrieve_exit_server_list( + exit_contract: Address, + cache: Arc>>, +) -> Result { + let exits = match DEVELOPMENT || cfg!(test) { + true => { + let node_ip = IpAddr::V4(Ipv4Addr::new(7, 7, 7, 1)); + let web3_url = format!("http://{}:8545", node_ip); + info!("Our address is {:?}", CONFIG.clarity_private_key.to_address()); + get_exits_list( + &Web3::new(&web3_url, WEB3_TIMEOUT), + CONFIG.clarity_private_key.to_address(), + exit_contract, + ) + .await + } + false => { + get_exits_list( + &Web3::new(RPC_SERVER, WEB3_TIMEOUT), + CONFIG.clarity_private_key.to_address(), + exit_contract, + ) + .await + } + }; + + match exits { + Ok(exits) => { + info!("Got exit list from contract"); + let exit_list = ExitServerList { + contract: exit_contract, + exit_list: exits, + created: std::time::SystemTime::now(), + }; + println!("Signing exit list with PUBKEY: {:?}", CONFIG.clarity_private_key.to_address()); + let cache_value = exit_list.sign(CONFIG.clarity_private_key); + + // add this new exit list to the cache + cache + .write() + .unwrap() + .insert(exit_contract, cache_value.clone()); + Ok(cache_value) + } + Err(e) => { + info!("Failed to get exit list from contract {:?}", e); + Err(e) + } + } +} + +pub fn start_exit_trust_root_server() { + let exit_contract_data_cache: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + signature_update_loop(exit_contract_data_cache.clone()); + let web_data = web::Data::new(exit_contract_data_cache.clone()); + thread::spawn(move || { + let runner = System::new(); + runner.block_on(async move { + let server = HttpServer::new(move || { + App::new() + .service(return_exit_contract_data) + .app_data(web_data.clone()) + }); + info!("Starting exit trust root server on {:?}", EXIT_ROOT_DOMAIN); + let server = if SSL { + let cert_chain = + load_certs(&format!("/etc/letsencrypt/live/{}/fullchain.pem", EXIT_ROOT_DOMAIN)); + let keys = load_rustls_private_key(&format!( + "/etc/letsencrypt/live/{}/privkey.pem", + EXIT_ROOT_DOMAIN + )); + let config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, keys) + .unwrap(); + + info!("Binding to SSL"); + server + .bind_rustls(format!("{}:{}", EXIT_ROOT_DOMAIN, SERVER_PORT), config.clone()) + .unwrap() + } else { + info!("Binding to {}:{}", EXIT_ROOT_DOMAIN, SERVER_PORT); + server.bind(format!("{}:{}", EXIT_ROOT_DOMAIN, SERVER_PORT)).unwrap() + }; + + let _ = server.run().await; + }); + }); +} + +/// In order to improve scalability this loop grabs and signs an updated list of exits from each exit contract +/// that has previously been requested from this server every 5 minutes. This allows the server to return instantly +/// on the next request from the client without having to perform rpc query 1-1 with requests. +pub fn signature_update_loop(cache: Arc>>) { + thread::spawn(move || loop { + let runner = System::new(); + runner.block_on(async { + let cache_iter = cache.read().unwrap().clone(); + for (exit_contract, _value) in cache_iter.iter() { + // get the latest exit list from the contract + match retrieve_exit_server_list(*exit_contract, cache.clone()).await { + // grab the cache here so we don't lock it while awaiting for every single contract + Ok(cache_value) => { + // update the cache + cache.write().unwrap().insert(*exit_contract, cache_value); + } + Err(e) => { + info!("Failed to get exit list from contract {:?}", e); + } + } + } + }); + if DEVELOPMENT || cfg!(test) { + thread::sleep(Duration::from_secs(10)); + } else { + thread::sleep(SIGNATURE_UPDATE_SLEEP); + } + }); +} diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 032da4756..4e0d3caac 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -21,6 +21,7 @@ settings = { path = "../settings" } rita_client = { path = "../rita_client", features = ["dev_env"] } rita_common = { path = "../rita_common", features = ["integration_test"] } rita_exit = { path = "../rita_exit", features = ["dev_env"] } +exit_trust_root = { path = "../exit_trust_root", features = ["development"] } rita_client_registration = { path = "../rita_client_registration" } rita_db_migration = { path = "../rita_db_migration" } ctrlc = { version = "3.4.5", features = ["termination"] } diff --git a/integration_tests/src/debts.rs b/integration_tests/src/debts.rs index d7c68a6c6..36ab89f5b 100644 --- a/integration_tests/src/debts.rs +++ b/integration_tests/src/debts.rs @@ -5,7 +5,7 @@ use std::time::Duration; use crate::five_nodes::five_node_config; use crate::registration_server::start_registration_server; use crate::setup_utils::namespaces::*; -use crate::setup_utils::rita::thread_spawner; +use crate::setup_utils::rita::{spawn_exit_root, thread_spawner}; use crate::utils::{ add_exits_contract_exit_list, deploy_contracts, generate_traffic, get_default_settings, get_ip_from_namespace, populate_routers_eth, query_debts, register_all_namespaces_to_exit, @@ -49,7 +49,7 @@ pub async fn run_debts_test() { info!("Starting registration server"); start_registration_server(db_addr).await; - let (client_settings, exit_settings) = get_default_settings(namespaces.clone()); + let (client_settings, exit_settings, exit_root_addr) = get_default_settings(namespaces.clone()); // The exit price is set to ns.cost during thread_spawner let exit_price = namespaces.get_namespace(4).unwrap().cost; @@ -59,6 +59,9 @@ pub async fn run_debts_test() { let res = setup_ns(namespaces.clone()); info!("Namespaces setup: {res:?}"); + info!("Starting root server!"); + spawn_exit_root(); + let rita_identities = thread_spawner(namespaces.clone(), client_settings, exit_settings, db_addr) .expect("Could not spawn Rita threads"); @@ -67,7 +70,7 @@ pub async fn run_debts_test() { // Add exits to the contract exit list so clients get the propers exits they can migrate to add_exits_contract_exit_list(db_addr, rita_identities.clone()).await; - populate_routers_eth(rita_identities).await; + populate_routers_eth(rita_identities, exit_root_addr).await; // Test for network convergence test_reach_all(namespaces.clone()); diff --git a/integration_tests/src/five_nodes.rs b/integration_tests/src/five_nodes.rs index 64694e090..206803bb7 100644 --- a/integration_tests/src/five_nodes.rs +++ b/integration_tests/src/five_nodes.rs @@ -1,6 +1,6 @@ use crate::registration_server::start_registration_server; use crate::setup_utils::namespaces::*; -use crate::setup_utils::rita::thread_spawner; +use crate::setup_utils::rita::{spawn_exit_root, thread_spawner}; use crate::utils::{ add_exits_contract_exit_list, deploy_contracts, get_default_settings, populate_routers_eth, register_all_namespaces_to_exit, test_all_internet_connectivity, test_reach_all, test_routes, @@ -21,13 +21,16 @@ pub async fn run_five_node_test_scenario() { info!("Starting registration server"); start_registration_server(db_addr).await; - let (client_settings, exit_settings) = get_default_settings(namespaces.clone()); + let (client_settings, exit_settings, exit_root_addr) = get_default_settings(namespaces.clone()); namespaces.validate(); let res = setup_ns(namespaces.clone()); info!("Namespaces setup: {res:?}"); + info!("Starting root server!"); + spawn_exit_root(); + let rita_identities = thread_spawner(namespaces.clone(), client_settings, exit_settings, db_addr) .expect("Could not spawn Rita threads"); @@ -40,7 +43,7 @@ pub async fn run_five_node_test_scenario() { //thread::sleep(SETUP_WAIT * 500); info!("About to populate routers with eth"); - populate_routers_eth(rita_identities).await; + populate_routers_eth(rita_identities, exit_root_addr).await; test_reach_all(namespaces.clone()); @@ -65,7 +68,7 @@ pub fn five_node_config() -> (NamespaceInfo, HashMap) { | | | | | | - 3 4---------7 + 3 4(Exit)-------7 |\ | | \ | | \ | diff --git a/integration_tests/src/mutli_exit.rs b/integration_tests/src/mutli_exit.rs index afd6812b0..0d8ec958c 100644 --- a/integration_tests/src/mutli_exit.rs +++ b/integration_tests/src/mutli_exit.rs @@ -6,7 +6,7 @@ use crate::{ registration_server::start_registration_server, setup_utils::{ namespaces::{setup_ns, Namespace, NamespaceInfo, NodeType, PriceId, RouteHop}, - rita::thread_spawner, + rita::{spawn_exit_root, thread_spawner}, }, utils::{ add_exits_contract_exit_list, deploy_contracts, get_default_settings, get_node_id_from_ip, @@ -44,12 +44,16 @@ pub async fn run_multi_exit_test() { info!("Starting registration server"); start_registration_server(db_addr).await; - let (rita_client_settings, rita_exit_settings) = get_default_settings(namespaces.clone()); + let (rita_client_settings, rita_exit_settings, exit_root_addr) = + get_default_settings(namespaces.clone()); namespaces.validate(); let res = setup_ns(namespaces.clone()); + info!("Starting root server!"); + spawn_exit_root(); + let rita_identities = thread_spawner( namespaces.clone(), rita_client_settings, @@ -62,7 +66,7 @@ pub async fn run_multi_exit_test() { // Add exits to the contract exit list so clients get the propers exits they can migrate to add_exits_contract_exit_list(db_addr, rita_identities.clone()).await; - populate_routers_eth(rita_identities).await; + populate_routers_eth(rita_identities, exit_root_addr).await; // Test for network convergence test_reach_all(namespaces.clone()); diff --git a/integration_tests/src/payments_althea.rs b/integration_tests/src/payments_althea.rs index 65627b65a..bceaa005f 100644 --- a/integration_tests/src/payments_althea.rs +++ b/integration_tests/src/payments_althea.rs @@ -1,7 +1,7 @@ use crate::five_nodes::five_node_config; use crate::registration_server::start_registration_server; use crate::setup_utils::namespaces::*; -use crate::setup_utils::rita::thread_spawner; +use crate::setup_utils::rita::{spawn_exit_root, thread_spawner}; use crate::utils::{ add_exits_contract_exit_list, deploy_contracts, generate_traffic, get_althea_grpc, get_default_settings, populate_routers_eth, print_althea_balances, @@ -51,13 +51,17 @@ pub async fn run_althea_payments_test_scenario() { info!("Starting registration server"); start_registration_server(db_addr).await; - let (mut client_settings, mut exit_settings) = get_default_settings(namespaces.clone()); + let (mut client_settings, mut exit_settings, exit_root_addr) = + get_default_settings(namespaces.clone()); namespaces.validate(); let res = setup_ns(namespaces.clone()); info!("Namespaces setup: {res:?}"); + info!("Starting root server!"); + spawn_exit_root(); + // Modify configs to use Althea chain let (client_settings, exit_settings) = althea_payments_map(&mut client_settings, &mut exit_settings); @@ -70,7 +74,7 @@ pub async fn run_althea_payments_test_scenario() { // Add exits to the contract exit list so clients get the propers exits they can migrate to add_exits_contract_exit_list(db_addr, rita_identities.clone()).await; - populate_routers_eth(rita_identities.clone()).await; + populate_routers_eth(rita_identities.clone(), exit_root_addr).await; // Test for network convergence test_reach_all(namespaces.clone()); diff --git a/integration_tests/src/payments_eth.rs b/integration_tests/src/payments_eth.rs index 4201aa763..5c6790c65 100644 --- a/integration_tests/src/payments_eth.rs +++ b/integration_tests/src/payments_eth.rs @@ -2,6 +2,7 @@ use crate::five_nodes::five_node_config; use crate::registration_server::start_registration_server; use crate::setup_utils::namespaces::setup_ns; use crate::setup_utils::namespaces::Namespace; +use crate::setup_utils::rita::spawn_exit_root; use crate::setup_utils::rita::thread_spawner; use crate::utils::add_exits_contract_exit_list; use crate::utils::deploy_contracts; @@ -50,7 +51,8 @@ pub async fn run_eth_payments_test_scenario() { info!("Starting registration server"); start_registration_server(db_addr).await; - let (mut client_settings, mut exit_settings) = get_default_settings(namespaces.clone()); + let (mut client_settings, mut exit_settings, exit_root_addr) = + get_default_settings(namespaces.clone()); // Set payment thresholds low enough so that they get triggered after an iperf let (client_settings, exit_settings) = @@ -61,6 +63,9 @@ pub async fn run_eth_payments_test_scenario() { let res = setup_ns(namespaces.clone()); info!("Namespaces setup: {res:?}"); + info!("Starting root server!"); + spawn_exit_root(); + let rita_identities = thread_spawner(namespaces.clone(), client_settings, exit_settings, db_addr) .expect("Could not spawn Rita threads"); @@ -69,7 +74,7 @@ pub async fn run_eth_payments_test_scenario() { // Add exits to the contract exit list so clients get the propers exits they can migrate to add_exits_contract_exit_list(db_addr, rita_identities.clone()).await; - populate_routers_eth(rita_identities).await; + populate_routers_eth(rita_identities, exit_root_addr).await; test_reach_all(namespaces.clone()); test_routes(namespaces.clone(), expected_routes); diff --git a/integration_tests/src/setup_utils/namespaces.rs b/integration_tests/src/setup_utils/namespaces.rs index 39c4106cf..d8aeb0c05 100644 --- a/integration_tests/src/setup_utils/namespaces.rs +++ b/integration_tests/src/setup_utils/namespaces.rs @@ -3,6 +3,7 @@ use nix::{ fcntl::{open, OFlag}, sys::stat::Mode, }; +use settings::exit::EXIT_LIST_IP; use std::collections::{HashMap, HashSet}; /// This struct holds the format for a namespace info @@ -309,6 +310,21 @@ pub fn setup_ns(spaces: NamespaceInfo) -> Result<(), KernelInterfaceError> { &veth_exit_to_native, ], )?; + // add ip address for the exit list endpoint + run_command( + "ip", + &[ + "netns", + "exec", + &name.get_name(), + "ip", + "addr", + "add", + EXIT_LIST_IP, + "dev", + &veth_exit_to_native, + ], + )?; // set default route run_command( "ip", diff --git a/integration_tests/src/setup_utils/rita.rs b/integration_tests/src/setup_utils/rita.rs index da6df1aae..7dfdbd0d4 100644 --- a/integration_tests/src/setup_utils/rita.rs +++ b/integration_tests/src/setup_utils/rita.rs @@ -4,9 +4,17 @@ use super::babel::spawn_babel; use super::namespaces::get_nsfd; use super::namespaces::NamespaceInfo; use super::namespaces::NodeType; +use actix_web::rt::System; +use actix_web::web; +use actix_web::App; +use actix_web::HttpServer; use althea_kernel_interface::KernelInterfaceError; use althea_types::Identity; +use althea_types::SignedExitServerList; use clarity::Address; +use exit_trust_root; +use exit_trust_root::return_exit_contract_data; +use exit_trust_root::signature_update_loop; use ipnetwork::IpNetwork; use ipnetwork::Ipv6Network; use log::info; @@ -24,6 +32,9 @@ use rita_exit::{ }; use settings::set_flag_config; use settings::{client::RitaClientSettings, exit::RitaExitSettingsStruct}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +use std::time::Instant; use std::{ collections::{HashMap, HashSet}, convert::TryInto, @@ -275,3 +286,42 @@ pub fn spawn_rita_exit( let val = router_identity_ref_local.read().unwrap().unwrap(); val } + +/// Spawns the exit root server and waits for it to finish starting, panics if it does not finish starting +pub fn spawn_exit_root() { + let successful_start: Arc = Arc::new(AtomicBool::new(false)); + let start_move = successful_start.clone(); + // the exit root server does not get its own namespace- instead it runs in the native namespace/host + let exit_contract_data_cache: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + signature_update_loop(exit_contract_data_cache.clone()); + let web_data = web::Data::new(exit_contract_data_cache.clone()); + thread::spawn(move || { + let successful_start = start_move.clone(); + let runner = System::new(); + runner.block_on(async move { + let server = HttpServer::new(move || { + App::new() + .service(return_exit_contract_data) + .app_data(web_data.clone()) + }); + info!("Starting exit trust root server on 10.0.0.1:4050"); + let server = server + .bind("10.0.0.1:4050") + .unwrap(); + successful_start.store(true, Relaxed); + let _ = server.run().await; + }); + }); + + const FAILURE_TIME: Duration = Duration::from_secs(10); + let start = Instant::now(); + while start.elapsed() < FAILURE_TIME { + if successful_start.clone().load(Relaxed) { + break; + } + } + if !successful_start.load(Relaxed) { + panic!("Exit root server failed to start!"); + } +} diff --git a/integration_tests/src/utils.rs b/integration_tests/src/utils.rs index 95c30959c..7e7f37686 100644 --- a/integration_tests/src/utils.rs +++ b/integration_tests/src/utils.rs @@ -21,7 +21,7 @@ use althea_types::{ }; use awc::http::StatusCode; use babel_monitor::{open_babel_stream, parse_routes, structs::Route}; -use clarity::{Address, Transaction, Uint256}; +use clarity::{Address, PrivateKey as ClarityPrivkey, Transaction, Uint256}; use deep_space::{Address as AltheaAddress, Coin, Contact, CosmosPrivateKey, PrivateKey}; use futures::future::join_all; use ipnetwork::IpNetwork; @@ -379,12 +379,17 @@ pub const EXIT_ROOT_IP: IpAddr = IpAddr::V6(Ipv6Addr::new(0xfd00, 200, 199, 198, 197, 196, 195, 194)); // this masks public ipv6 ips in the test env and is being used to test assignment pub const EXIT_SUBNET: Ipv6Addr = Ipv6Addr::new(0xfbad, 200, 0, 0, 0, 0, 0, 0); +pub const EXIT_ROOT_SERVER_URL: &str = "http://10.0.0.1:4050"; /// Gets the default client and exit settings pub fn get_default_settings( namespaces: NamespaceInfo, -) -> (RitaClientSettings, RitaExitSettingsStruct) { +) -> (RitaClientSettings, RitaExitSettingsStruct, Address) { let mut exit_servers = HashMap::new(); + // generate keys for the exit root server + let exit_root_privkey = ClarityPrivkey::from_bytes([1u8; 32]).unwrap(); + let exit_root_addr = exit_root_privkey.to_address(); + info!("Exit root address is {:?}", exit_root_addr); let exit = RitaExitSettingsStruct { client_registration_url: "https://7.7.7.1:40400/register_router".to_string(), workers: 2, @@ -397,6 +402,8 @@ pub fn get_default_settings( allowed_countries: HashSet::new(), log: LoggingSettings::default(), operator: ExitOperatorSettings::default(), + allowed_exit_list_signatures: vec![exit_root_addr], + exit_root_url: EXIT_ROOT_SERVER_URL.to_owned(), }; let client = RitaClientSettings::default(); @@ -435,17 +442,20 @@ pub fn get_default_settings( } .into(), ); + // todo bootstrapping exits should be removed to test the exit root server? client .exit_client .bootstrapping_exits .clone_from(&exit_servers); + client.exit_client.allowed_exit_list_signatures = vec![exit_root_addr]; + // first node is passed through to the host machine for testing second node is used // for testnet queries exit.payment.althea_grpc_list = vec![get_althea_grpc()]; exit.payment.eth_node_list = vec![get_eth_node()]; client.payment.althea_grpc_list = vec![get_althea_grpc()]; client.payment.eth_node_list = vec![get_eth_node()]; - (client, exit) + (client, exit, exit_root_addr) } pub fn althea_system_chain_client(settings: RitaClientSettings) -> RitaClientSettings { @@ -1130,7 +1140,7 @@ pub async fn register_all_namespaces_to_exit(namespaces: NamespaceInfo) { } } -pub async fn populate_routers_eth(rita_identities: InstanceData) { +pub async fn populate_routers_eth(rita_identities: InstanceData, exit_root_addr: Address) { // Exits need to have funds to request a registered client list, which is needed for proper setup info!("Topup exits with funds"); let web3 = Web3::new(&get_eth_node(), WEB3_TIMEOUT); @@ -1141,8 +1151,9 @@ pub async fn populate_routers_eth(rita_identities: InstanceData) { for e in rita_identities.exit_identities { to_top_up.push(e.eth_address) } + to_top_up.push(exit_root_addr); - info!("Sending 50 eth to all routers"); + info!("Sending 50 eth to all routers and exit root server"); send_eth_bulk((ONE_ETH * 50).into(), &to_top_up, &web3).await; } diff --git a/rita_client/src/exit_manager/exit_loop.rs b/rita_client/src/exit_manager/exit_loop.rs index a5ee89819..792f1ed96 100644 --- a/rita_client/src/exit_manager/exit_loop.rs +++ b/rita_client/src/exit_manager/exit_loop.rs @@ -14,12 +14,12 @@ use crate::traffic_watcher::{query_exit_debts, QueryExitDebts}; use actix_async::System as AsyncSystem; use althea_kernel_interface::ip_addr::setup_ipv6_slaac as setup_ipv6_slaac_ki; use althea_kernel_interface::ip_route::get_default_route; +use althea_kernel_interface::netns::get_namespace; use althea_kernel_interface::run_command; use althea_types::ExitDetails; use althea_types::ExitServerList; use althea_types::{ExitIdentity, ExitState}; use rita_common::blockchain_oracle::low_balance; -use std::net::IpAddr; use std::thread; use std::time::{Duration, Instant}; @@ -99,7 +99,7 @@ async fn exit_manager_loop(em_state: &mut ExitManager, babel_port: u16) { info!("We have details for the selected exit!"); // TODO setup exit using old selected exit the first run, of the loop, right now we force a wait // for this request to complete before we get things setup, we can store the ExitIdentity somewhere - handle_exit_switching(em_state, current_exit_ip, babel_port).await; + handle_exit_switching(em_state, babel_port).await; setup_exit_tunnel( em_state.exit_switcher_state.currently_selected.clone(), @@ -129,16 +129,13 @@ async fn exit_manager_loop(em_state: &mut ExitManager, babel_port: u16) { } /// This function handles deciding if we need to switch exits, the new selected exit is returned. If no exit is selected, the current exit is returned. -async fn handle_exit_switching( - em_state: &mut ExitManager, - current_exit_id: IpAddr, - babel_port: u16, -) { +async fn handle_exit_switching(em_state: &mut ExitManager, babel_port: u16) { // Get cluster exit list. This is saved locally and updated every tick depending on what exit we connect to. // When it is empty, it means an exit we connected to went down, and we use the list from memory to connect to a new instance - let exit_list = match get_exit_list(current_exit_id).await { + let exit_list = match get_exit_list().await { Ok(a) => { - info!("Received an exit list: {:?}", a); + let ns: String = get_namespace().unwrap(); + error!("ns {:?} Received an exit list: {:?}", ns, a); a.data } Err(e) => { diff --git a/rita_client/src/exit_manager/requests.rs b/rita_client/src/exit_manager/requests.rs index 92d39b61b..7321e4041 100644 --- a/rita_client/src/exit_manager/requests.rs +++ b/rita_client/src/exit_manager/requests.rs @@ -10,8 +10,12 @@ use althea_types::WgKey; use althea_types::{ExitClientIdentity, ExitRegistrationDetails, ExitState}; use settings::exit::EXIT_LIST_IP; use settings::exit::EXIT_LIST_PORT; +use settings::get_rita_client; use settings::set_rita_client; use std::net::{IpAddr, SocketAddr}; +use std::time::Duration; + +const EXIT_LIST_TIMEOUT: Duration = Duration::from_secs(20); async fn send_exit_setup_request( exit_pubkey: WgKey, @@ -208,54 +212,15 @@ pub async fn exit_status_request(exit: IpAddr) -> Result<(), RitaClientError> { Ok(()) } -/// Hits the exit_list endpoint for a given exit. -pub async fn get_exit_list(exit: IpAddr) -> Result { - let current_exit = match settings::get_rita_client() - .exit_client - .bootstrapping_exits - .get(&exit) - { - Some(current_exit) => current_exit.clone(), - None => { - return Err(RitaClientError::NoExitError(exit.to_string())); - } - }; - - let exit_pubkey = current_exit.wg_key; - let reg_details = match settings::get_rita_client().payment.contact_info { - Some(val) => val.into(), - None => { - return Err(RitaClientError::MiscStringError( - "No valid details".to_string(), - )) - } - }; - let ident = ExitClientIdentity { - global: match settings::get_rita_client().get_identity() { - Some(id) => id, - None => { - return Err(RitaClientError::MiscStringError( - "Identity has no mesh IP ready yet".to_string(), - )); - } - }, - wg_port: DEFAULT_WG_LISTEN_PORT, - reg_details, - }; - - // todo formatting for the exit list endpoint +/// Hits the exit_list endpoint +pub async fn get_exit_list() -> Result { let endpoint = format!("http://{}:{}/exit_list", EXIT_LIST_IP, EXIT_LIST_PORT); - let settings = settings::get_rita_client(); - let our_pubkey = settings.network.wg_public_key.unwrap(); - let our_privkey = settings.network.wg_private_key.unwrap(); - - let ident = encrypt_exit_client_id(our_pubkey, &our_privkey.into(), &exit_pubkey.into(), ident); let client = awc::Client::default(); let response = client .post(&endpoint) - .timeout(CLIENT_LOOP_TIMEOUT) - .send_json(&ident) + .timeout(EXIT_LIST_TIMEOUT) + .send() .await; let response = match response { Ok(mut response) => response.json().await, @@ -278,13 +243,15 @@ pub async fn get_exit_list(exit: IpAddr) -> Result Ok(list), - false => Err(RitaClientError::MiscStringError( - "Failed to verify exit list signature!".to_owned(), - )), + let config = get_rita_client(); + let allowed_signers = config.exit_client.allowed_exit_list_signatures; + for key in allowed_signers { + error!("Verifying exit list with key {:?}", key); + if list.verify(key) { + return Ok(list); + } } + Err(RitaClientError::MiscStringError( + "Failed to verify exit list signature!".to_owned(), + )) } diff --git a/rita_exit/Cargo.toml b/rita_exit/Cargo.toml index 79704e069..a5093c393 100644 --- a/rita_exit/Cargo.toml +++ b/rita_exit/Cargo.toml @@ -7,6 +7,7 @@ license = "Apache-2.0" [dependencies] num256 = "0.5" +exit_trust_root = { path = "../exit_trust_root" } rita_common = { path = "../rita_common" } rita_client_registration = { path = "../rita_client_registration" } althea_kernel_interface = { path = "../althea_kernel_interface" } diff --git a/rita_exit/src/network_endpoints/mod.rs b/rita_exit/src/network_endpoints/mod.rs index 545e0ba15..d2c73daa2 100644 --- a/rita_exit/src/network_endpoints/mod.rs +++ b/rita_exit/src/network_endpoints/mod.rs @@ -19,7 +19,6 @@ use reqwest::ClientBuilder; use rita_common::blockchain_oracle::potential_payment_issues_detected; use rita_common::debt_keeper::get_debts_list; use rita_common::rita_loop::get_web3_server; -use settings::exit::EXIT_LIST_IP; use settings::get_rita_exit; use std::net::SocketAddr; use std::time::Duration; @@ -195,33 +194,44 @@ pub async fn get_exit_list() -> HttpResponse { let contract_addr = rita_exit.exit_network.registered_users_contract_addr; // we are receiving a SignedExitServerList from the root server. - let signed_list: SignedExitServerList = - match get_exit_list_from_root(contract_addr).await { - Some(a) => a, - None => { - return HttpResponse::InternalServerError() - .json("Failed to get exit list from root server"); - } - }; + let signed_list: SignedExitServerList = match get_exit_list_from_root(contract_addr).await { + Some(a) => a, + None => { + return HttpResponse::InternalServerError() + .json("Failed to get exit list from root server!"); + } + }; HttpResponse::Ok().json(signed_list) } -async fn get_exit_list_from_root( - contract_addr: Address, -) -> Option { - let request_url = format!("https://{}/{}", EXIT_LIST_IP, contract_addr); +async fn get_exit_list_from_root(contract_addr: Address) -> Option { + let rita_exit = get_rita_exit(); + let request_url = rita_exit.exit_root_url; + let allowed_signers = rita_exit.allowed_exit_list_signatures; let timeout = Duration::new(15, 0); let client = ClientBuilder::new().timeout(timeout).build().unwrap(); + let request_url = format!("{}/{}", request_url, contract_addr); + info!("Requesting exit list from {}", request_url); let response = client - .head(request_url) + .get(request_url) .send() .await .expect("Could not receive data from exit root server"); if response.status().is_success() { info!("Received an exit list"); - match response.json().await { - Ok(a) => Some(a), + match response.json::().await { + Ok(a) => { + // verify the signature of the exit list + for signer in allowed_signers { + if a.verify(signer) { + info!("Verified exit list signature"); + return Some(a); + } + } + error!("Failed to verify exit list signature"); + None + } Err(e) => { error!("Failed to parse exit list from root server {:?}", e); None diff --git a/settings/src/client.rs b/settings/src/client.rs index 05d0202b9..8ba273933 100644 --- a/settings/src/client.rs +++ b/settings/src/client.rs @@ -5,6 +5,7 @@ use crate::payment::PaymentSettings; use crate::{json_merge, set_rita_client, SettingsError}; use althea_types::regions::Regions; use althea_types::{ExitIdentity, ExitState, Identity}; +use clarity::Address; use std::collections::{HashMap, HashSet}; use std::net::IpAddr; use std::path::{Path, PathBuf}; @@ -61,6 +62,8 @@ pub struct ExitClientSettings { /// For example if we have a None value, we will connect to other exits with no specified region, but not ones that specify a region lock. /// If we have some value we will connect to exits that have that region specified as well as exits with no region specified. pub our_region: Option, + /// This is the Address/Pubkey of the exit root of trust server which clients use to verify signed exit lists + pub allowed_exit_list_signatures: Vec
, } impl Default for ExitClientSettings { @@ -71,6 +74,7 @@ impl Default for ExitClientSettings { exit_db_smart_contract_on_xdai: exit_db_smart_contract_on_xdai(), lan_nics: HashSet::new(), our_region: None, + allowed_exit_list_signatures: Vec::new(), } } } diff --git a/settings/src/exit.rs b/settings/src/exit.rs index 8bc5a2a35..72e195a2e 100644 --- a/settings/src/exit.rs +++ b/settings/src/exit.rs @@ -13,8 +13,8 @@ use std::path::{Path, PathBuf}; pub const APP_NAME: &str = "rita_exit"; -// IP serving exit lists from the root server -pub const EXIT_LIST_IP: &str = "10.10.10.10"; +// IP serving exit lists from the root server back to clients +pub const EXIT_LIST_IP: &str = "10.11.12.13"; /// This is the port which exit lists are served over pub const EXIT_LIST_PORT: u16 = 5566; /// This is the network settings specific to rita_exit @@ -118,6 +118,10 @@ pub struct RitaExitSettingsStruct { /// (ISO country code) #[serde(skip_serializing_if = "HashSet::is_empty", default)] pub allowed_countries: HashSet, + /// This is the Address/Pubkey of the exit root of trust server which clients use to verify signed exit lists + pub allowed_exit_list_signatures: Vec
, + /// url to the exit root of trust server to query exit lists + pub exit_root_url: String, } impl RitaExitSettingsStruct { @@ -141,6 +145,8 @@ impl RitaExitSettingsStruct { exit_network: ExitNetworkSettings::test_default(), allowed_countries: HashSet::new(), log: LoggingSettings::default(), + allowed_exit_list_signatures: Vec::new(), + exit_root_url: "".to_string(), } } diff --git a/test_runner/src/main.rs b/test_runner/src/main.rs index 331a1891a..3ba0d49d4 100644 --- a/test_runner/src/main.rs +++ b/test_runner/src/main.rs @@ -28,6 +28,8 @@ async fn main() { env_logger::Builder::default() .filter(None, log::LevelFilter::Error) .filter(Some("integration_tests"), log::LevelFilter::Info) + .filter(Some("rita_exit"), log::LevelFilter::Info) + .filter(Some("exit_trust_root"), log::LevelFilter::Info) .init(); set_sigterm();