Compare commits

...

2 Commits

Author SHA1 Message Date
Simon Wicky 1e74de2d67 domain fronting on the run part 2024-07-18 14:37:12 +02:00
Simon Wicky a886715948 ugly first try at domain fronting 2024-07-16 15:11:48 +02:00
17 changed files with 304 additions and 18 deletions
+1
View File
@@ -73,6 +73,7 @@ impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
nym_apis: init_config.common_args.nym_apis,
fronting_domains: init_config.common_args.fronting_domains,
disable_socket: init_config.disable_socket,
port: init_config.port,
host: init_config.host,
+5
View File
@@ -97,6 +97,7 @@ pub(crate) enum Commands {
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
nym_apis: Option<Vec<url::Url>>,
fronting_domains: Option<Vec<url::Url>>,
disable_socket: Option<bool>,
port: Option<u16>,
host: Option<IpAddr>,
@@ -133,6 +134,10 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
.with_base(BaseClientConfig::with_disabled_cover_traffic, args.no_cover)
.with_optional(Config::with_port, args.port)
.with_optional(Config::with_host, args.host)
.with_optional_ext(
BaseClientConfig::with_fronting_domains,
args.fronting_domains,
)
.with_optional_custom_env_ext(
BaseClientConfig::with_custom_nym_apis,
args.nym_apis,
+1
View File
@@ -36,6 +36,7 @@ impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
nym_apis: run_config.common_args.nym_apis,
fronting_domains: run_config.common_args.fronting_domains,
disable_socket: run_config.disable_socket,
port: run_config.port,
host: run_config.host,
@@ -117,6 +117,11 @@ impl Config {
self
}
pub fn with_fronting_domains(mut self, fronting_domains: Vec<Url>) -> Self {
self.client.fronting_domains = Some(fronting_domains);
self
}
pub fn set_custom_nym_apis(&mut self, nym_api_urls: Vec<Url>) {
self.client.nym_api_urls = nym_api_urls;
}
@@ -289,6 +294,10 @@ impl Config {
pub fn get_nym_api_endpoints(&self) -> Vec<Url> {
self.client.nym_api_urls.clone()
}
pub fn get_fronting_domains(&self) -> Option<Vec<Url>> {
self.client.fronting_domains.clone()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
@@ -314,6 +323,9 @@ pub struct Client {
/// Addresses to APIs running on validator from which the client gets the view of the network.
#[serde(alias = "validator_api_urls")]
pub nym_api_urls: Vec<Url>,
/// Domain to use for domain fronting censorship circumvention
pub fronting_domains: Option<Vec<Url>>,
}
impl Client {
@@ -340,6 +352,7 @@ impl Client {
disabled_credentials_mode: true,
nyxd_urls,
nym_api_urls,
fronting_domains: None,
}
}
@@ -349,6 +362,7 @@ impl Client {
disabled_credentials_mode: bool,
nyxd_urls: Vec<Url>,
nym_api_urls: Vec<Url>,
fronting_domains: Option<Vec<Url>>,
) -> Self {
Client {
version: version.into(),
@@ -356,6 +370,7 @@ impl Client {
disabled_credentials_mode,
nyxd_urls,
nym_api_urls,
fronting_domains,
}
}
}
@@ -95,6 +95,7 @@ impl From<ConfigV5> for Config {
id: value.client.id,
disabled_credentials_mode: value.client.disabled_credentials_mode,
nyxd_urls: value.client.nyxd_urls,
fronting_domains: None, //SW need proper migrations if it gets applied
nym_api_urls: value.client.nym_api_urls,
},
debug: DebugConfig {
@@ -111,7 +111,7 @@ where
hardcoded_topology.get_gateways()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::current_gateways(&mut rng, &core.client.nym_api_urls).await?
crate::init::helpers::current_gateways(&mut rng, &core.client.nym_api_urls, None).await?
};
// since we're registering with a brand new gateway,
@@ -70,6 +70,13 @@ pub struct CommonClientInitArgs {
)]
pub nym_apis: Option<Vec<url::Url>>,
///Comma separated list of urls to use for domain fronting
#[cfg_attr(
feature = "cli",
clap(long, value_delimiter = ',', requires = "nym_apis", hide = true)
)]
pub fronting_domains: Option<Vec<url::Url>>,
/// Path to .json file containing custom network specification.
#[cfg_attr(feature = "cli", clap(long, group = "network", hide = true))]
pub custom_mixnet: Option<PathBuf>,
@@ -144,6 +151,16 @@ where
.collect::<Vec<&str>>()
.join(",")
);
if let Some(fronting_domains) = &core.client.fronting_domains {
log::info!(
"fronted by : {}",
fronting_domains
.iter()
.map(|url| url.host_str().unwrap_or_default())
.collect::<Vec<&str>>()
.join(",")
);
}
let key_store = OnDiskKeys::new(paths.keys.clone());
let details_store = setup_fs_gateways_storage(&paths.gateway_registrations).await?;
@@ -163,7 +180,12 @@ where
hardcoded_topology.get_gateways()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::current_gateways(&mut rng, &core.client.nym_api_urls).await?
crate::init::helpers::current_gateways(
&mut rng,
&core.client.nym_api_urls,
core.client.fronting_domains.as_ref(),
)
.await?
};
let gateway_setup = GatewaySetup::New {
@@ -35,6 +35,13 @@ pub struct CommonClientRunArgs {
)]
pub nym_apis: Option<Vec<url::Url>>,
///Comma separated list of urls to use for domain fronting
#[cfg_attr(
feature = "cli",
clap(long, value_delimiter = ',', requires = "nym_apis", hide = true)
)]
pub fronting_domains: Option<Vec<url::Url>>,
/// Path to .json file containing custom network specification.
#[cfg_attr(feature = "cli", clap(long, group = "network", hide = true))]
pub custom_mixnet: Option<PathBuf>,
@@ -3,6 +3,7 @@
use super::packet_statistics_control::PacketStatisticsReporter;
use super::received_buffer::ReceivedBufferMessage;
use super::topology_control::fronted_api_provider::FrontedApiTopologyProvider;
use super::topology_control::geo_aware_provider::GeoAwareTopologyProvider;
use crate::client::base_client::storage::helpers::store_client_keys;
use crate::client::base_client::storage::MixnetClientStorage;
@@ -467,17 +468,29 @@ where
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
config_topology: config::Topology,
nym_api_urls: Vec<Url>,
fronting_domains: Option<Vec<Url>>,
) -> Box<dyn TopologyProvider + Send + Sync> {
// if no custom provider was ... provided ..., create one using nym-api
custom_provider.unwrap_or_else(|| match config_topology.topology_structure {
config::TopologyStructure::NymApi => Box::new(NymApiTopologyProvider::new(
nym_api_provider::Config {
min_mixnode_performance: config_topology.minimum_mixnode_performance,
min_gateway_performance: config_topology.minimum_gateway_performance,
},
nym_api_urls,
env!("CARGO_PKG_VERSION").to_string(),
)),
config::TopologyStructure::NymApi => match fronting_domains {
Some(domains) => Box::new(FrontedApiTopologyProvider::new(
nym_api_provider::Config {
min_mixnode_performance: config_topology.minimum_mixnode_performance,
min_gateway_performance: config_topology.minimum_gateway_performance,
},
nym_api_urls,
domains,
env!("CARGO_PKG_VERSION").to_string(),
)),
None => Box::new(NymApiTopologyProvider::new(
nym_api_provider::Config {
min_mixnode_performance: config_topology.minimum_mixnode_performance,
min_gateway_performance: config_topology.minimum_gateway_performance,
},
nym_api_urls,
env!("CARGO_PKG_VERSION").to_string(),
)),
},
config::TopologyStructure::GeoAware(group_by) => {
Box::new(GeoAwareTopologyProvider::new(
nym_api_urls,
@@ -689,6 +702,7 @@ where
self.custom_topology_provider.take(),
self.config.debug.topology,
self.config.get_nym_api_endpoints(),
self.config.get_fronting_domains(),
);
// needs to be started as the first thing to block if required waiting for the gateway
@@ -0,0 +1,147 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use log::{debug, error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::{NymTopology, NymTopologyError};
use rand::prelude::SliceRandom;
use rand::thread_rng;
use url::Url;
use super::nym_api_provider::Config;
pub(crate) struct FrontedApiTopologyProvider {
config: Config,
validator_client: nym_validator_client::client::NymApiClient,
nym_api_urls: Vec<Url>,
fronting_domains: Vec<Url>,
shuffling: Vec<usize>,
client_version: String,
currently_used_api: usize,
}
impl FrontedApiTopologyProvider {
pub(crate) fn new(
config: Config,
nym_api_urls: Vec<Url>,
fronting_domains: Vec<Url>,
client_version: String,
) -> Self {
//SW for the PoC, we assume same lenght between fronting domains and api_urls
let mut shuffling = (0..nym_api_urls.len()).collect::<Vec<_>>();
shuffling.shuffle(&mut thread_rng());
FrontedApiTopologyProvider {
config,
validator_client: nym_validator_client::client::NymApiClient::new_fronted(
nym_api_urls[shuffling[0]].clone(),
fronting_domains[shuffling[0]].clone(),
),
nym_api_urls,
fronting_domains,
shuffling,
client_version,
currently_used_api: 0,
}
}
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
return;
}
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
self.validator_client.change_nym_api_with_fronting(
self.nym_api_urls[self.shuffling[self.currently_used_api]].clone(),
self.fronting_domains[self.shuffling[self.currently_used_api]].clone(),
);
}
/// Verifies whether nodes a reasonably distributed among all mix layers.
///
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
/// However, this is a rather unrealistic expectation, instead we check whether there exists
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
///
/// # Arguments
///
/// * `topology`: active topology constructed from validator api data
fn check_layer_distribution(
&self,
active_topology: &NymTopology,
) -> Result<(), NymTopologyError> {
let lower_threshold = 0.15;
let upper_threshold = 0.66;
active_topology.ensure_even_layer_distribution(lower_threshold, upper_threshold)
}
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_basic_mixnodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
error!("failed to get network mixnodes - {err}");
return None;
}
Ok(mixes) => mixes,
};
let gateways = match self
.validator_client
.get_basic_gateways(Some(self.client_version.clone()))
.await
{
Err(err) => {
error!("failed to get network gateways - {err}");
return None;
}
Ok(gateways) => gateways,
};
debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(),
gateways.len()
);
let topology = NymTopology::from_unordered(
mixnodes.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_mixnode_performance
}),
gateways.iter().filter(|g| {
g.performance.round_to_integer() >= self.config.min_gateway_performance
}),
);
if let Err(err) = self.check_layer_distribution(&topology) {
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}");
self.use_next_nym_api();
None
} else {
Some(topology)
}
}
}
// hehe, wasm
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl TopologyProvider for FrontedApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl TopologyProvider for FrontedApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
@@ -17,6 +17,7 @@ use tokio::time::sleep;
use wasmtimer::tokio::sleep;
mod accessor;
pub(crate) mod fronted_api_provider;
pub mod geo_aware_provider;
pub(crate) mod nym_api_provider;
+22 -5
View File
@@ -9,6 +9,7 @@ use nym_crypto::asymmetric::identity;
use nym_gateway_client::GatewayClient;
use nym_topology::{filter::VersionFilterable, gateway, mix};
use nym_validator_client::client::IdentityKeyRef;
use rand::seq::IteratorRandom;
use rand::{seq::SliceRandom, Rng};
use std::{sync::Arc, time::Duration};
use tungstenite::Message;
@@ -59,13 +60,29 @@ impl<'a> GatewayWithLatency<'a> {
pub async fn current_gateways<R: Rng>(
rng: &mut R,
nym_apis: &[Url],
fronting_domains: Option<&Vec<Url>>,
) -> Result<Vec<gateway::Node>, ClientCoreError> {
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
let client = nym_validator_client::client::NymApiClient::new(nym_api.clone());
let client = match fronting_domains {
Some(domains) => {
let (api_url, fronting_url) = nym_apis
.iter()
.zip(domains)
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
log::debug!("Fetching list of gateways from: {nym_api}");
nym_validator_client::client::NymApiClient::new_fronted(
api_url.clone(),
fronting_url.clone(),
)
}
None => {
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
nym_validator_client::client::NymApiClient::new(nym_api.clone())
}
};
log::debug!("Fetching list of gateways from: {}", client.api_url());
let gateways = client.get_cached_described_gateways().await?;
log::debug!("Found {} gateways", gateways.len());
@@ -258,6 +258,12 @@ impl NymApiClient {
NymApiClient { nym_api }
}
pub fn new_fronted(api_url: Url, fronting_url: Url) -> Self {
let nym_api = nym_api::Client::new_fronted(api_url, fronting_url, None);
NymApiClient { nym_api }
}
pub fn api_url(&self) -> &Url {
self.nym_api.current_url()
}
@@ -266,6 +272,15 @@ impl NymApiClient {
self.nym_api.change_base_url(new_endpoint);
}
pub fn change_nym_api_with_fronting(
&mut self,
new_api_endpoint: Url,
new_fronting_domain: Url,
) {
self.nym_api
.change_fronted_url(new_api_endpoint, new_fronting_domain);
}
pub async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
+40
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use http::header;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response, StatusCode};
use serde::de::DeserializeOwned;
@@ -96,6 +97,13 @@ impl ClientBuilder {
self
}
pub fn with_host_header(mut self, host: &str) -> Self {
let mut headers = header::HeaderMap::new();
headers.insert(header::HOST, HeaderValue::from_str(host).unwrap()); //SW Handle this unwrap later
self.reqwest_client_builder = self.reqwest_client_builder.default_headers(headers);
self
}
pub fn with_user_agent<V>(mut self, value: V) -> Self
where
V: TryInto<HeaderValue>,
@@ -155,6 +163,23 @@ impl Client {
)
}
pub fn new_fronted(base_url: Url, fronting_url: Url, timeout: Option<Duration>) -> Self {
let host = base_url.host_str().unwrap();
let mut fronted_url = base_url.clone();
fronted_url.set_host(fronting_url.host_str()).unwrap();
let builder = ClientBuilder::new::<_, String>(fronted_url)
.expect(
"we provided valid url and we were unwrapping previous construction errors anyway",
)
.with_host_header(host);
//SW polish that later if needed
match timeout {
Some(timeout) => builder.with_timeout(timeout).build::<String>().unwrap(),
None => builder.build::<String>().unwrap(),
}
}
pub fn new_url<U, E>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
@@ -179,6 +204,21 @@ impl Client {
self.base_url = new_url
}
pub fn change_fronted_url(&mut self, new_api_url: Url, new_fronting_url: Url) {
let host = new_api_url.host_str().unwrap();
let mut new_fronted_url = new_api_url.clone();
new_fronted_url
.set_host(new_fronting_url.host_str())
.unwrap();
let mut headers = header::HeaderMap::new();
headers.insert(header::HOST, HeaderValue::from_str(host).unwrap()); //SW Handle this unwrap later
self.reqwest_client = reqwest::ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap();
self.base_url = new_fronted_url
}
pub fn current_url(&self) -> &Url {
&self.base_url
}
+1 -1
View File
@@ -122,7 +122,7 @@ pub async fn setup_gateway_from_api(
nym_apis: &[Url],
) -> Result<InitialisationResult, WasmCoreError> {
let mut rng = thread_rng();
let gateways = current_gateways(&mut rng, nym_apis).await?;
let gateways = current_gateways(&mut rng, nym_apis, None).await?;
setup_gateway_wasm(client_store, force_tls, chosen_gateway, &gateways).await
}
+1 -1
View File
@@ -274,7 +274,7 @@ where
specification: GatewaySelectionSpecification::UniformRemote {
must_use_tls: false,
},
available_gateways: current_gateways(&mut rng, &nym_apis).await?,
available_gateways: current_gateways(&mut rng, &nym_apis, None).await?,
wg_tun_address: None,
});
+1 -1
View File
@@ -459,7 +459,7 @@ where
);
let mut rng = OsRng;
let available_gateways = current_gateways(&mut rng, &nym_api_endpoints).await?;
let available_gateways = current_gateways(&mut rng, &nym_api_endpoints, None).await?;
Ok(GatewaySetup::New {
specification: selection_spec,