From 0801b3c6f8e9a6e5cfaf9ca6ec4add2a38364454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 26 Oct 2023 10:35:14 +0100 Subject: [PATCH] Feature/exit policy (#4030) * exit policy types * simple client for grabbing the policy * moved allowed_hosts to a submodule * started integrating exit policy into a NR * ability to construct ExitPolicyRequestFilter * fixed policy parsing to look for comment char from the left * conditionally setting up request filter * [wip] setting up correct url for exit policy upstream * clap flags for running with exit policy * fixed NR template * updated NR config template * making sure to perform request filtering in separate task * initial, placeholder, exit policy API endpoint * serving exit policy from an embedded NR * double slash sanitization * socks5 query for exit policy * adjusted address policy logging * cargo fmt * Updated exit policy url to point to the correct mainnet file * removed unecessary mutability in filter tests * fixed the code block showing example policy being interpreted as doc test --- Cargo.lock | 15 + Cargo.toml | 1 + common/config/src/lib.rs | 1 + common/config/src/serde_helpers.rs | 47 ++ common/exit-policy/Cargo.toml | 33 + common/exit-policy/src/client.rs | 10 + common/exit-policy/src/lib.rs | 233 ++++++ .../exit-policy/src/policy/address_policy.rs | 726 ++++++++++++++++++ common/exit-policy/src/policy/error.rs | 71 ++ common/exit-policy/src/policy/mod.rs | 14 + common/http-api-client/src/lib.rs | 4 + common/network-defaults/src/mainnet.rs | 6 + common/network-defaults/src/var_names.rs | 1 + common/socks5/requests/Cargo.toml | 1 + common/socks5/requests/src/request.rs | 2 + common/socks5/requests/src/response.rs | 52 ++ gateway/src/commands/helpers.rs | 6 + gateway/src/commands/init.rs | 8 + gateway/src/commands/run.rs | 7 + .../src/commands/setup_network_requester.rs | 25 +- gateway/src/http/mod.rs | 175 ++++- .../embedded_network_requester/mod.rs | 5 +- gateway/src/node/mod.rs | 54 +- nym-connect/desktop/Cargo.lock | 11 + nym-node/nym-node-requests/Cargo.toml | 3 +- .../v1/network_requester/exit_policy/mod.rs | 4 + .../network_requester/exit_policy/models.rs | 44 ++ .../src/api/v1/network_requester/mod.rs | 1 + .../src/api/v1/network_requester/models.rs | 13 - nym-node/nym-node-requests/src/lib.rs | 10 +- .../api/v1/network_requester/exit_policy.rs | 30 + .../router/api/v1/network_requester/mod.rs | 27 +- nym-node/src/http/router/api/v1/openapi.rs | 7 + nym-node/src/http/router/mod.rs | 7 + .../network-requester/Cargo.toml | 5 +- .../network-requester/src/cli/init.rs | 29 +- .../network-requester/src/cli/mod.rs | 5 + .../network-requester/src/cli/run.rs | 7 + .../network-requester/src/config/mod.rs | 29 +- .../src/config/persistence.rs | 2 + .../network-requester/src/config/template.rs | 8 + .../network-requester/src/core.rs | 125 +-- .../network-requester/src/error.rs | 33 +- .../network-requester/src/lib.rs | 3 +- .../network-requester/src/main.rs | 2 +- .../allowed_hosts/filter.rs | 67 +- .../allowed_hosts/group.rs | 2 +- .../allowed_hosts/host.rs | 0 .../allowed_hosts/hosts.rs | 2 +- .../{ => request_filter}/allowed_hosts/mod.rs | 0 .../allowed_hosts/standard_list.rs | 4 +- .../allowed_hosts/stored_allowed_hosts.rs | 2 +- .../src/request_filter/exit_policy/mod.rs | 82 ++ .../src/request_filter/mod.rs | 142 ++++ tools/nym-nr-query/Cargo.toml | 2 +- tools/nym-nr-query/src/main.rs | 75 +- 56 files changed, 2071 insertions(+), 209 deletions(-) create mode 100644 common/config/src/serde_helpers.rs create mode 100644 common/exit-policy/Cargo.toml create mode 100644 common/exit-policy/src/client.rs create mode 100644 common/exit-policy/src/lib.rs create mode 100644 common/exit-policy/src/policy/address_policy.rs create mode 100644 common/exit-policy/src/policy/error.rs create mode 100644 common/exit-policy/src/policy/mod.rs create mode 100644 nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/mod.rs create mode 100644 nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/models.rs create mode 100644 nym-node/src/http/router/api/v1/network_requester/exit_policy.rs rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/filter.rs (88%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/group.rs (96%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/host.rs (100%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/hosts.rs (99%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/mod.rs (100%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/standard_list.rs (96%) rename service-providers/network-requester/src/{ => request_filter}/allowed_hosts/stored_allowed_hosts.rs (98%) create mode 100644 service-providers/network-requester/src/request_filter/exit_policy/mod.rs create mode 100644 service-providers/network-requester/src/request_filter/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5c80148210..eb1e5cd900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6477,6 +6477,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "thiserror", + "tracing", + "utoipa", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" @@ -6803,6 +6815,7 @@ dependencies = [ "nym-config", "nym-credential-storage", "nym-crypto", + "nym-exit-policy", "nym-network-defaults", "nym-ordered-buffer", "nym-sdk", @@ -6887,6 +6900,7 @@ dependencies = [ "http-api-client", "nym-bin-common", "nym-crypto", + "nym-exit-policy", "nym-wireguard-types", "schemars", "serde", @@ -7178,6 +7192,7 @@ version = "0.1.0" dependencies = [ "bincode", "log", + "nym-exit-policy", "nym-service-providers-common", "nym-sphinx-addressing", "serde", diff --git a/Cargo.toml b/Cargo.toml index 93b62803db..e4d6645927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "common/crypto", "common/dkg", "common/execute", + "common/exit-policy", "common/http-api-client", "common/inclusion-probability", "common/ledger", diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index d3e4dbf6b5..3a9104eeb6 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -15,6 +15,7 @@ pub use toml::de::Error as TomlDeError; pub mod defaults; pub mod helpers; pub mod legacy_helpers; +pub mod serde_helpers; pub const NYM_DIR: &str = ".nym"; pub const DEFAULT_NYM_APIS_DIR: &str = "nym-api"; diff --git a/common/config/src/serde_helpers.rs b/common/config/src/serde_helpers.rs new file mode 100644 index 0000000000..27c11f21be --- /dev/null +++ b/common/config/src/serde_helpers.rs @@ -0,0 +1,47 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Deserializer}; +use std::fmt::Display; +use std::path::PathBuf; +use std::str::FromStr; + +pub fn de_maybe_stringified<'de, D, T, E>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + E: Display, +{ + let raw = String::deserialize(deserializer)?; + if raw.is_empty() { + Ok(None) + } else { + Ok(Some(raw.parse().map_err(serde::de::Error::custom)?)) + } +} + +pub fn de_maybe_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + de_maybe_stringified(deserializer) +} + +pub fn de_maybe_path<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + de_maybe_stringified(deserializer) +} + +pub fn de_maybe_port<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let port = u16::deserialize(deserializer)?; + if port == 0 { + Ok(None) + } else { + Ok(Some(port)) + } +} diff --git a/common/exit-policy/Cargo.toml b/common/exit-policy/Cargo.toml new file mode 100644 index 0000000000..a5e8f26974 --- /dev/null +++ b/common/exit-policy/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "nym-exit-policy" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tracing = { workspace = true } + +# feature-specific dependencies: + +## client feature +reqwest = { workspace = true, optional = true } + +## openapi feature +serde_json = { workspace = true, optional = true } +utoipa = { workspace = true, optional = true } + +[dev-dependencies] +serde_json = { workspace = true } + +[features] +default = [] +client = ["reqwest"] +openapi = ["utoipa", "serde_json"] diff --git a/common/exit-policy/src/client.rs b/common/exit-policy/src/client.rs new file mode 100644 index 0000000000..c6dfbe3f3b --- /dev/null +++ b/common/exit-policy/src/client.rs @@ -0,0 +1,10 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::policy::PolicyError; +use crate::ExitPolicy; +use reqwest::IntoUrl; + +pub async fn get_exit_policy(url: impl IntoUrl) -> Result { + ExitPolicy::parse_from_torrc(reqwest::get(url).await?.text().await?) +} diff --git a/common/exit-policy/src/lib.rs b/common/exit-policy/src/lib.rs new file mode 100644 index 0000000000..f1b41274bc --- /dev/null +++ b/common/exit-policy/src/lib.rs @@ -0,0 +1,233 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod policy; + +#[cfg(feature = "client")] +pub mod client; + +pub use crate::policy::{ + AddressPolicy, AddressPolicyAction, AddressPolicyRule, AddressPortPattern, PolicyError, + PortRange, +}; + +pub(crate) const EXIT_POLICY_FIELD_NAME: &str = "ExitPolicy"; +const COMMENT_CHAR: char = '#'; + +pub type ExitPolicy = AddressPolicy; + +pub fn parse_exit_policy>(exit_policy: S) -> Result { + let rules = exit_policy + .as_ref() + .lines() + .map(|maybe_rule| { + if let Some(comment_start) = maybe_rule.find(COMMENT_CHAR) { + &maybe_rule[..comment_start] + } else { + maybe_rule + } + .trim() + }) + .filter(|maybe_rule| !maybe_rule.is_empty()) + .map(parse_address_policy_rule) + .collect::, _>>()?; + + Ok(AddressPolicy { rules }) +} + +pub fn format_exit_policy(policy: &ExitPolicy) -> String { + policy + .rules + .iter() + .map(|rule| format!("{EXIT_POLICY_FIELD_NAME} {rule}")) + .fold(String::new(), |accumulator, rule| { + accumulator + &rule + "\n" + }) + .trim_end() + .to_string() +} + +fn parse_address_policy_rule(rule: &str) -> Result { + // each exit policy rule must begin with 'ExitPolicy' followed by the actual rule + rule.strip_prefix(EXIT_POLICY_FIELD_NAME) + .ok_or(PolicyError::NoExitPolicyPrefix { + entry: rule.to_string(), + })? + .trim() + .parse() +} + +// for each line, ignore everything after the comment + +#[cfg(test)] +mod tests { + use super::*; + use crate::policy::AddressPolicyAction::{Accept, Accept6, Reject, Reject6}; + use crate::policy::{AddressPortPattern, IpPattern, PortRange}; + + #[test] + fn parsing_policy() { + let sample = r#" +ExitPolicy reject 1.2.3.4/32:*#comment +ExitPolicy reject 1.2.3.5:* #comment +ExitPolicy reject 1.2.3.6/16:* +ExitPolicy reject 1.2.3.6/16:123-456 # comment + +ExitPolicy accept *:53 # DNS + +# random comment + +ExitPolicy accept6 *6:119 +ExitPolicy accept *4:120 +ExitPolicy reject6 [FC00::]/7:* + +#ExitPolicy accept *:8080 #and another comment here + +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8329:* +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8328:1234 +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8328/64:1235 + +#another comment +#ExitPolicy accept *:8080 + +ExitPolicy reject *:* + "#; + + let res = parse_exit_policy(sample).unwrap(); + + let mut expected = AddressPolicy::new(); + + // ExitPolicy reject 1.2.3.4/32:*#comment + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V4 { + addr_prefix: "1.2.3.4".parse().unwrap(), + mask: 32, + }, + ports: PortRange::new_all(), + }, + ); + + // ExitPolicy reject 1.2.3.5:* #comment + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V4 { + addr_prefix: "1.2.3.5".parse().unwrap(), + mask: 32, + }, + ports: PortRange::new_all(), + }, + ); + + // ExitPolicy reject 1.2.3.6/16:* + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V4 { + addr_prefix: "1.2.3.6".parse().unwrap(), + mask: 16, + }, + ports: PortRange::new_all(), + }, + ); + + // ExitPolicy reject 1.2.3.6/16:123-456 + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V4 { + addr_prefix: "1.2.3.6".parse().unwrap(), + mask: 16, + }, + ports: PortRange::new(123, 456).unwrap(), + }, + ); + + // ExitPolicy accept *:53 + expected.push( + Accept, + AddressPortPattern { + ip_pattern: IpPattern::Star, + ports: PortRange::new_singleton(53), + }, + ); + + // ExitPolicy accept6 *6:119 + expected.push( + Accept6, + AddressPortPattern { + ip_pattern: IpPattern::V6Star, + ports: PortRange::new_singleton(119), + }, + ); + + // ExitPolicy accept *4:120 + expected.push( + Accept, + AddressPortPattern { + ip_pattern: IpPattern::V4Star, + ports: PortRange::new_singleton(120), + }, + ); + + // ExitPolicy reject6 [FC00::]/7:* + expected.push( + Reject6, + AddressPortPattern { + ip_pattern: IpPattern::V6 { + addr_prefix: "FC00::".parse().unwrap(), + mask: 7, + }, + ports: PortRange::new_all(), + }, + ); + + // ExitPolicy FE80:0000:0000:0000:0202:B3FF:FE1E:8329:* + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V6 { + addr_prefix: "FE80:0000:0000:0000:0202:B3FF:FE1E:8329".parse().unwrap(), + mask: 128, + }, + ports: PortRange::new_all(), + }, + ); + + // ExitPolicy FE80:0000:0000:0000:0202:B3FF:FE1E:8328:1234 + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V6 { + addr_prefix: "FE80:0000:0000:0000:0202:B3FF:FE1E:8328".parse().unwrap(), + mask: 128, + }, + ports: PortRange::new_singleton(1234), + }, + ); + + // ExitPolicy FE80:0000:0000:0000:0202:B3FF:FE1E:8328/64:1235 + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::V6 { + addr_prefix: "FE80:0000:0000:0000:0202:B3FF:FE1E:8328".parse().unwrap(), + mask: 64, + }, + ports: PortRange::new_singleton(1235), + }, + ); + + expected.push( + Reject, + AddressPortPattern { + ip_pattern: IpPattern::Star, + ports: PortRange::new_all(), + }, + ); + + assert_eq!(res, expected) + } +} diff --git a/common/exit-policy/src/policy/address_policy.rs b/common/exit-policy/src/policy/address_policy.rs new file mode 100644 index 0000000000..09162a6e9b --- /dev/null +++ b/common/exit-policy/src/policy/address_policy.rs @@ -0,0 +1,726 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Implements address policies, based on a series of accept/reject +//! rules. + +use crate::policy::error::PolicyError; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use tracing::trace; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "lowercase")] +pub enum AddressPolicyAction { + /// A rule that accepts matching address:port combinations on IPv4 and IPv6. + Accept, + + /// A rule that rejects matching address:port combinations on IPv4 and IPv6. + Reject, + + /// A rule that accepts matching address:port combinations on IPv6 only. + Accept6, + + /// A rule that rejects matching address:port combinations on IPv6 only. + Reject6, +} + +impl AddressPolicyAction { + pub fn is_accept(&self) -> bool { + matches!( + self, + AddressPolicyAction::Accept | AddressPolicyAction::Accept6 + ) + } +} + +impl FromStr for AddressPolicyAction { + type Err = PolicyError; + + fn from_str(s: &str) -> Result { + match s { + "accept" => Ok(AddressPolicyAction::Accept), + "reject" => Ok(AddressPolicyAction::Reject), + "accept6" => Ok(AddressPolicyAction::Accept6), + "reject6" => Ok(AddressPolicyAction::Reject6), + other => Err(PolicyError::InvalidPolicyAction { + action: other.to_string(), + }), + } + } +} + +impl Display for AddressPolicyAction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AddressPolicyAction::Accept => write!(f, "accept"), + AddressPolicyAction::Reject => write!(f, "reject"), + AddressPolicyAction::Accept6 => write!(f, "accept6"), + AddressPolicyAction::Reject6 => write!(f, "reject6"), + } + } +} + +/// A sequence of rules that are applied to an address:port until one +/// matches. +/// +/// Each rule is of the form "accept(6) PATTERN" or "reject(6) PATTERN", +/// where every pattern describes a set of addresses and ports. +/// Address sets are given as a prefix of 0-128 bits that the address +/// must have; port sets are given as a low-bound and high-bound that +/// the target port might lie between. +/// +/// An example IPv4 policy might be: +/// +/// ```text +/// reject *:25 +/// reject 127.0.0.0/8:* +/// reject 192.168.0.0/16:* +/// accept *:80 +/// accept *:443 +/// accept *:9000-65535 +/// reject *:* +/// ``` +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", aliases(ExitPolicy))] +pub struct AddressPolicy { + /// A list of rules to apply to find out whether an address is + /// contained by this policy. + /// + /// The rules apply in order; the first one to match determines + /// whether the address is accepted or rejected. + pub(crate) rules: Vec, +} + +impl AddressPolicy { + /// Create a new AddressPolicy that matches nothing. + pub const fn new() -> Self { + AddressPolicy { rules: Vec::new() } + } + + /// Create a new AddressPolicy that matches everything. + pub fn new_open() -> Self { + AddressPolicy { + rules: vec![AddressPolicyRule::new( + AddressPolicyAction::Accept, + AddressPortPattern { + ip_pattern: IpPattern::Star, + ports: PortRange::new_all(), + }, + )], + } + } + + /// Check whether this AddressPolicy matches all patterns. + pub fn is_open(&self) -> bool { + if self.rules.len() != 1 { + return false; + } + + let rule = &self.rules[0]; + + rule.action == AddressPolicyAction::Accept + && rule.pattern.ip_pattern == IpPattern::Star + && rule.pattern.ports.is_all() + } + + /// Attempts to parse the AddressPolicy out of raw torrc representation. + pub fn parse_from_torrc>(raw: S) -> Result { + crate::parse_exit_policy(raw) + } + + /// Formats the AddressPolicy with torrc representation + pub fn format_as_torrc(&self) -> String { + crate::format_exit_policy(self) + } + + /// Apply this policy to an address:port combination + /// + /// We do this by applying each rule in sequence, until one + /// matches. + /// + /// Returns None if no rule matches. + pub fn allows(&self, addr: &IpAddr, port: u16) -> Option { + self.rules + .iter() + .find(|rule| rule.pattern.matches(addr, port)) + .map(|rule| { + trace!("'{addr}:{port}' is covered by rule '{rule}'"); + rule.action.is_accept() + }) + } + + /// As allows, but accept a SocketAddr. + pub fn allows_sockaddr(&self, addr: &SocketAddr) -> Option { + self.allows(&addr.ip(), addr.port()) + } + + /// Add a new rule to this policy. + /// + /// The newly added rule is applied _after_ all previous rules. + /// It matches all addresses and ports covered by AddressPortPattern. + /// + /// If accept is true, the rule is to accept addresses that match; + /// if accept is false, the rule rejects such addresses. + pub fn push(&mut self, action: AddressPolicyAction, pattern: AddressPortPattern) { + self.rules.push(AddressPolicyRule { action, pattern }) + } + + /// As push, but accepts a AddressPolicyRule. + pub fn push_rule(&mut self, rule: AddressPolicyRule) { + self.rules.push(rule) + } +} + +/// A single rule in an address policy. +/// +/// Contains a pattern, what to do with things that match it. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AddressPolicyRule { + /// What do we do with items that match the pattern? + action: AddressPolicyAction, + + /// What pattern are we trying to match? + pattern: AddressPortPattern, +} + +impl FromStr for AddressPolicyRule { + type Err = PolicyError; + + fn from_str(s: &str) -> Result { + // split on the first space, i.e. separation between the action and the pattern + let Some((action, pattern)) = s.split_once(' ') else { + return Err(PolicyError::MalformedAddressPolicy { raw: s.to_string() }); + }; + + Ok(AddressPolicyRule { + action: action.parse()?, + pattern: pattern.parse()?, + }) + } +} + +impl AddressPolicyRule { + pub fn new(action: AddressPolicyAction, pattern: AddressPortPattern) -> Self { + AddressPolicyRule { action, pattern } + } +} +impl Display for AddressPolicyRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.action, self.pattern) + } +} + +/// A pattern that may or may not match an address and port. +/// +/// Each AddrPortPattern has an IP pattern, which matches a set of +/// addresses by prefix, and a port pattern, which matches a range of +/// ports. +/// +/// # Example +/// +/// ``` +/// use nym_exit_policy::policy::AddressPortPattern; +/// use std::net::{IpAddr,Ipv4Addr}; +/// let localhost = IpAddr::V4(Ipv4Addr::new(127,3,4,5)); +/// let not_localhost = IpAddr::V4(Ipv4Addr::new(192,0,2,16)); +/// let pat: AddressPortPattern = "127.0.0.0/8:*".parse().unwrap(); +/// +/// assert!(pat.matches(&localhost, 22)); +/// assert!(!pat.matches(¬_localhost, 22)); +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AddressPortPattern { + /// A pattern to match somewhere between zero and all IP addresses. + #[serde(with = "stringified_ip_pattern")] + #[cfg_attr(feature = "openapi", schema(example = "1.2.3.6/16", value_type = String))] + pub(crate) ip_pattern: IpPattern, + + /// A pattern to match a range of ports. + pub(crate) ports: PortRange, +} + +mod stringified_ip_pattern { + use super::IpPattern; + use serde::{Deserialize, Deserializer, Serializer}; + use std::str::FromStr; + + pub fn serialize(pattern: &IpPattern, serializer: S) -> Result { + serializer.serialize_str(&pattern.to_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let s = ::deserialize(deserializer)?; + IpPattern::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl AddressPortPattern { + /// Return true iff this pattern matches a given address and port. + pub fn matches(&self, addr: &IpAddr, port: u16) -> bool { + self.ip_pattern.matches(addr) && self.ports.contains(port) + } + + /// As matches, but accept a SocketAddr. + pub fn matches_sockaddr(&self, addr: &SocketAddr) -> bool { + self.matches(&addr.ip(), addr.port()) + } +} + +impl Display for AddressPortPattern { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.ip_pattern, self.ports) + } +} + +impl FromStr for AddressPortPattern { + type Err = PolicyError; + + fn from_str(s: &str) -> Result { + let last_colon = s + .rfind(':') + .ok_or(PolicyError::MalformedAddressPortPattern { raw: s.to_string() })?; + + // doesn't have enough chars to cover the port, even if its a wildcard + if s.len() < last_colon + 2 { + return Err(PolicyError::MalformedAddressPortPattern { raw: s.to_string() }); + } + + let ip_pattern = s[..last_colon].parse()?; + let ports = s[last_colon + 1..].parse()?; + + Ok(AddressPortPattern { ip_pattern, ports }) + } +} + +/// A pattern that matches one or more IP addresses. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IpPattern { + /// Match all addresses. + Star, + + /// Match all IPv4 addresses. + V4Star, + + /// Match all IPv6 addresses. + V6Star, + + /// Match all IPv4 addresses beginning with a given prefix and mask. + V4 { addr_prefix: Ipv4Addr, mask: u8 }, + + /// Match all IPv6 addresses beginning with a given prefix and mask. + V6 { addr_prefix: Ipv6Addr, mask: u8 }, +} + +impl IpPattern { + /// Construct an IpPattern that matches the first `mask` bits of `addr`. + fn from_addr_and_mask(address: IpAddr, target_mask: u8) -> Result { + match (address, target_mask) { + (IpAddr::V4(_), 0) => Ok(IpPattern::V4Star), + (IpAddr::V6(_), 0) => Ok(IpPattern::V6Star), + (IpAddr::V4(addr_prefix), mask) if mask <= 32 => { + Ok(IpPattern::V4 { addr_prefix, mask }) + } + (IpAddr::V6(addr_prefix), mask) if mask <= 128 => { + Ok(IpPattern::V6 { addr_prefix, mask }) + } + (addr, mask) => { + if addr.is_ipv4() { + Err(PolicyError::InvalidIpV4Mask { mask }) + } else { + Err(PolicyError::InvalidIpV6Mask { mask }) + } + } + } + } + + /// Return true iff `addr` is matched by this pattern. + fn matches(&self, addr: &IpAddr) -> bool { + match (self, addr) { + (IpPattern::Star, _) => true, + (IpPattern::V4Star, IpAddr::V4(_)) => true, + (IpPattern::V6Star, IpAddr::V6(_)) => true, + (IpPattern::V4 { addr_prefix, mask }, IpAddr::V4(addr)) => { + let p1 = u32::from_be_bytes(addr_prefix.octets()); + let p2 = u32::from_be_bytes(addr.octets()); + + let shift = 32 - mask; + (p1 >> shift) == (p2 >> shift) + } + (IpPattern::V6 { addr_prefix, mask }, IpAddr::V6(addr)) => { + let p1 = u128::from_be_bytes(addr_prefix.octets()); + let p2 = u128::from_be_bytes(addr.octets()); + + let shift = 128 - mask; + (p1 >> shift) == (p2 >> shift) + } + (_, _) => false, + } + } +} + +impl Display for IpPattern { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IpPattern::Star => write!(f, "*"), + IpPattern::V4Star => write!(f, "*4"), + IpPattern::V6Star => write!(f, "*6"), + IpPattern::V4 { addr_prefix, mask } => { + write!(f, "{addr_prefix}/{mask}") + } + IpPattern::V6 { addr_prefix, mask } => { + write!(f, "{addr_prefix}/{mask}") + } + } + } +} + +/// Helper: try to parse a plain ipv4 address, or an IPv6 address +/// wrapped in brackets. +fn parse_addr(s: &str) -> Result { + if s.starts_with('[') && s.ends_with(']') { + Ipv6Addr::from_str(&s[1..s.len() - 1]).map(IpAddr::V6) + } else { + IpAddr::from_str(s) + } + .map_err(|source| PolicyError::MalformedIpAddress { + addr: s.to_string(), + source, + }) +} + +/// Helper: try to parse a port making sure it's non-zero +fn parse_port(s: &str) -> Result { + let port = s + .parse::() + .map_err(|_| PolicyError::InvalidPort { raw: s.to_string() })?; + + if port == 0 { + Err(PolicyError::InvalidPort { + raw: port.to_string(), + }) + } else { + Ok(port) + } +} + +impl FromStr for IpPattern { + type Err = PolicyError; + fn from_str(s: &str) -> Result { + let (ip_s, mask_s) = match s.find('/') { + Some(slash_idx) => (&s[..slash_idx], Some(&s[slash_idx + 1..])), + None => (s, None), + }; + + match (ip_s, mask_s) { + // '*' patterns + ("*", Some(m)) => Err(PolicyError::MaskWithStar { + mask: m.to_string(), + }), + ("*", None) => Ok(IpPattern::Star), + + // '*4' patterns + ("*4", Some(m)) => Err(PolicyError::MaskWithV4Star { + mask: m.to_string(), + }), + ("*4", None) => Ok(IpPattern::V4Star), + + // '*6' patterns + ("*6", Some(m)) => Err(PolicyError::MaskWithV6Star { + mask: m.to_string(), + }), + ("*6", None) => Ok(IpPattern::V6Star), + + (s, Some(m)) => { + let a: IpAddr = parse_addr(s)?; + let m: u8 = m.parse().map_err(|_| PolicyError::InvalidMask { + mask: m.to_string(), + })?; + IpPattern::from_addr_and_mask(a, m) + } + (s, None) => { + let a: IpAddr = parse_addr(s)?; + let m = if a.is_ipv4() { 32 } else { 128 }; + IpPattern::from_addr_and_mask(a, m) + } + } + } +} + +/// A PortRange is a set of consecutively numbered TCP or UDP ports. +/// +/// # Example +/// ``` +/// use nym_exit_policy::policy::PortRange; +/// +/// let r: PortRange = "22-8000".parse().unwrap(); +/// assert!(r.contains(128)); +/// assert!(r.contains(22)); +/// assert!(r.contains(8000)); +/// +/// assert!(! r.contains(21)); +/// assert!(! r.contains(8001)); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct PortRange { + /// The first port in this range. + #[cfg_attr(feature = "openapi", schema(example = 80))] + pub start: u16, + + /// The last port in this range. + #[cfg_attr(feature = "openapi", schema(example = 81))] + pub end: u16, +} + +impl PortRange { + /// Create a new port range spanning from start to end, asserting that + /// the correct invariants hold. + fn new_unchecked(start: u16, end: u16) -> Self { + assert_ne!(start, 0); + assert!(start <= end); + + PortRange { start, end } + } + + /// Create a port range containing all ports. + pub fn new_all() -> Self { + PortRange::new_unchecked(1, 65535) + } + + /// Create a new PortRange. + /// + /// The Portrange contains all ports between `start` and `end` inclusive. + /// + /// Returns None if lo is greater than end, or if either is zero. + pub const fn new(start: u16, end: u16) -> Option { + if start != 0 && start <= end { + Some(PortRange { start, end }) + } else { + None + } + } + + /// Create a new singleton PortRange. + pub const fn new_singleton(value: u16) -> Self { + PortRange { + start: value, + end: value, + } + } + + /// Return true if a port is in this range. + pub fn contains(&self, port: u16) -> bool { + self.start <= port && port <= self.end + } + + /// Return true if this range contains all ports. + pub fn is_all(&self) -> bool { + self.start == 1 && self.end == 65535 + } +} + +/// A PortRange is displayed as a number if it contains a single port, +/// and as a start point and end point separated by a dash if it contains +/// more than one port. +impl Display for PortRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_all() { + write!(f, "*") + } else if self.start == self.end { + write!(f, "{}", self.start) + } else { + write!(f, "{}-{}", self.start, self.end) + } + } +} + +impl FromStr for PortRange { + type Err = PolicyError; + fn from_str(s: &str) -> Result { + // check is if it's a star range + if s == "*" { + return Ok(PortRange::new_all()); + } + + if let Some(pos) = s.find('-') { + // This is a range; parse each part + let start = parse_port(&s[..pos])?; + let end = parse_port(&s[pos + 1..])?; + PortRange::new(start, end).ok_or(PolicyError::InvalidRange { start, end }) + } else { + // There was no hyphen, so try to parse this range as a singleton. + let value = parse_port(s)?; + Ok(PortRange::new_singleton(value)) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_bad_rules() { + fn check(s: &str) { + assert!(s.parse::().is_err()); + } + + check("marzipan:80"); + check("1.2.3.4:90-80"); + check("1.2.3.4/100:8888"); + check("[1.2.3.4]/16:80"); + check("[::1]/130:8888"); + } + + #[test] + fn test_rule_matches() { + fn check(address: &str, yes: &[&str], no: &[&str]) { + use std::net::SocketAddr; + let policy = address.parse::().unwrap(); + for s in yes { + let sa = s.parse::().unwrap(); + assert!(policy.matches_sockaddr(&sa)); + } + for s in no { + let sa = s.parse::().unwrap(); + assert!(!policy.matches_sockaddr(&sa)); + } + } + + check( + "1.2.3.4/16:80", + &["1.2.3.4:80", "1.2.44.55:80"], + &["9.9.9.9:80", "1.3.3.4:80", "1.2.3.4:81"], + ); + check( + "*:443-8000", + &["1.2.3.4:443", "[::1]:500"], + &["9.0.0.0:80", "[::1]:80"], + ); + check( + "[face::]/8:80", + &["[fab0::7]:80"], + &["[dd00::]:80", "[face::7]:443"], + ); + + check("0.0.0.0/0:*", &["127.0.0.1:80"], &["[f00b::]:80"]); + check("[::]/0:*", &["[f00b::]:80"], &["127.0.0.1:80"]); + } + + #[test] + fn test_policy_matches() -> Result<(), PolicyError> { + let mut policy = AddressPolicy::default(); + policy.push(AddressPolicyAction::Accept, "*:443".parse()?); + policy.push(AddressPolicyAction::Accept, "[::1]:80".parse()?); + policy.push(AddressPolicyAction::Reject, "*:80".parse()?); + + let policy = policy; // drop mut + assert!(policy + .allows_sockaddr(&"[::6]:443".parse().unwrap()) + .unwrap()); + assert!(policy + .allows_sockaddr(&"127.0.0.1:443".parse().unwrap()) + .unwrap()); + assert!(policy + .allows_sockaddr(&"[::1]:80".parse().unwrap()) + .unwrap()); + assert!(!policy + .allows_sockaddr(&"[::2]:80".parse().unwrap()) + .unwrap()); + assert!(!policy + .allows_sockaddr(&"127.0.0.1:80".parse().unwrap()) + .unwrap()); + assert!(policy + .allows_sockaddr(&"127.0.0.1:66".parse().unwrap()) + .is_none()); + Ok(()) + } + + #[test] + fn parse_portrange() { + assert_eq!( + "1-100".parse::().unwrap(), + PortRange::new(1, 100).unwrap() + ); + assert_eq!( + "01-100".parse::().unwrap(), + PortRange::new(1, 100).unwrap() + ); + assert_eq!( + "1-65535".parse::().unwrap(), + PortRange::new_all() + ); + assert_eq!( + "10-30".parse::().unwrap(), + PortRange::new(10, 30).unwrap() + ); + assert_eq!( + "9001".parse::().unwrap(), + PortRange::new(9001, 9001).unwrap() + ); + assert_eq!( + "9001-9001".parse::().unwrap(), + PortRange::new(9001, 9001).unwrap() + ); + assert_eq!("*".parse::().unwrap(), PortRange::new_all()); + + assert!("hello".parse::().is_err()); + assert!("0".parse::().is_err()); + assert!("65536".parse::().is_err()); + assert!("65537".parse::().is_err()); + assert!("1-2-3".parse::().is_err()); + assert!("10-5".parse::().is_err()); + assert!("1-".parse::().is_err()); + assert!("-2".parse::().is_err()); + assert!("-".parse::().is_err()); + } + + #[test] + fn test_portrange() { + assert!(PortRange::new_all().is_all()); + assert!(!PortRange::new(2, 65535).unwrap().is_all()); + + assert!(PortRange::new_all().contains(1)); + assert!(PortRange::new_all().contains(65535)); + assert!(PortRange::new_all().contains(7777)); + + assert!(PortRange::new(20, 30).unwrap().contains(20)); + assert!(PortRange::new(20, 30).unwrap().contains(25)); + assert!(PortRange::new(20, 30).unwrap().contains(30)); + assert!(!PortRange::new(20, 30).unwrap().contains(19)); + assert!(!PortRange::new(20, 30).unwrap().contains(31)); + } + + // this test exists due to manually implemented 'stringified_ip_pattern' on 'AddressPortPattern' + #[test] + fn policy_serde_json_roundtrip() { + let policy = AddressPolicy::parse_from_torrc( + r#" +ExitPolicy reject 1.2.3.4/32:* +ExitPolicy reject 1.2.3.5:* +ExitPolicy reject 1.2.3.6/16:* +ExitPolicy reject 1.2.3.6/16:123-456 +ExitPolicy accept *:53 +ExitPolicy accept6 *6:119 +ExitPolicy accept *4:120 +ExitPolicy reject6 [FC00::]/7:* +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8329:* +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8328:1234 +ExitPolicy reject FE80:0000:0000:0000:0202:B3FF:FE1E:8328/64:1235 +ExitPolicy reject *:*"#, + ) + .unwrap(); + + let json = serde_json::to_string(&policy).unwrap(); + let recovered: AddressPolicy = serde_json::from_str(&json).unwrap(); + + assert_eq!(recovered, policy); + } +} diff --git a/common/exit-policy/src/policy/error.rs b/common/exit-policy/src/policy/error.rs new file mode 100644 index 0000000000..5a39e096e5 --- /dev/null +++ b/common/exit-policy/src/policy/error.rs @@ -0,0 +1,71 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::EXIT_POLICY_FIELD_NAME; +use std::net::AddrParseError; +use thiserror::Error; + +/// Error from an unparsable or invalid policy. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum PolicyError { + #[cfg(feature = "client")] + #[error("failed to fetch the remote policy: {source}")] + ClientError { + #[from] + source: reqwest::Error, + }, + + #[error("/{mask} is not a valid mask for an IpV4 address")] + InvalidIpV4Mask { mask: u8 }, + + #[error("/{mask} is not a valid mask for an IpV6 address")] + InvalidIpV6Mask { mask: u8 }, + + #[error("'{action}' is not a valid policy action")] + InvalidPolicyAction { action: String }, + + #[error("'{addr}' is not a valid Ip address: {source}")] + MalformedIpAddress { + addr: String, + #[source] + source: AddrParseError, + }, + + /// Attempted to use a bitmask with the address "*". + #[error("attempted to use a bitmask ('/{mask}') with the address '*'")] + MaskWithStar { mask: String }, + + /// Attempted to use a bitmask with the address "*4". + #[error("attempted to use a bitmask ('/{mask}') with the address '*4'")] + MaskWithV4Star { mask: String }, + + /// Attempted to use a bitmask with the address "*6". + #[error("attempted to use a bitmask ('/{mask}') with the address '*6'")] + MaskWithV6Star { mask: String }, + + #[error("'/{mask}' is not a valid mask")] + InvalidMask { mask: String }, + + /// A port was not a number in the range 1..65535 + #[error( + "the provided port '{raw}' was either malformed or was not in the valid 1..65535 range" + )] + InvalidPort { raw: String }, + + /// A port range had its starting-point higher than its ending point. + #[error("the provided port range ({start}-{end}) was invalid. either the start was 0 or it was greater than the end.")] + InvalidRange { start: u16, end: u16 }, + + #[error("could not parse '{raw}' into a valid policy address:port pattern")] + MalformedAddressPortPattern { raw: String }, + + #[error("could not parse '{raw}' into a valid address policy")] + MalformedAddressPolicy { raw: String }, + + #[error( + "the provided exit policy entry does not start with the expected '{}' prefix: '{entry}'", + EXIT_POLICY_FIELD_NAME + )] + NoExitPolicyPrefix { entry: String }, +} diff --git a/common/exit-policy/src/policy/mod.rs b/common/exit-policy/src/policy/mod.rs new file mode 100644 index 0000000000..2100ad311d --- /dev/null +++ b/common/exit-policy/src/policy/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// adapted from: https://github.com/dgoulet-tor/arti/tree/781dc4bd64f515f0c13ae9907c473c2bad8fbf71 +// and https://github.com/torproject/tor/blob/3cb6a690be60fcdab60130402ff88dcfc0657596/contrib/or-tools/exitlist +// + https://github.com/torproject/tor/blob/3cb6a690be60fcdab60130402ff88dcfc0657596/src/feature/dirparse/policy_parse.c + +mod address_policy; +mod error; + +pub use address_policy::{ + AddressPolicy, AddressPolicyAction, AddressPolicyRule, AddressPortPattern, IpPattern, PortRange, +}; +pub use error::PolicyError; diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 2d816cc142..c3c4f98f09 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -386,12 +386,16 @@ pub fn sanitize_url, V: AsRef>( let mut path_segments = url .path_segments_mut() .expect("provided validator url does not have a base!"); + + path_segments.pop_if_empty(); + for segment in segments { let segment = segment.strip_prefix('/').unwrap_or(segment); let segment = segment.strip_suffix('/').unwrap_or(segment); path_segments.push(segment); } + // I don't understand why compiler couldn't figure out that it's no longer used // and can be dropped drop(path_segments); diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index fd002d4db7..236c9d9f1f 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -27,6 +27,10 @@ pub const NYXD_URL: &str = "https://rpc.nymtech.net"; pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; +// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging +pub const EXIT_POLICY_URL: &str = + "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; + pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new(NYXD_URL, Some(NYM_API))] } @@ -101,6 +105,7 @@ pub fn export_to_env() { set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); + set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } pub fn export_to_env_if_not_set() { @@ -148,4 +153,5 @@ pub fn export_to_env_if_not_set() { set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); + set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index d082045a03..c74117630e 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -27,6 +27,7 @@ pub const NAME_SERVICE_CONTRACT_ADDRESS: &str = "NAME_SERVICE_CONTRACT_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; pub const EXPLORER_API: &str = "EXPLORER_API"; +pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION"; diff --git a/common/socks5/requests/Cargo.toml b/common/socks5/requests/Cargo.toml index acb08b31a6..a5b60ee08f 100644 --- a/common/socks5/requests/Cargo.toml +++ b/common/socks5/requests/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] bincode = "1.3.3" log = { workspace = true } +nym-exit-policy = { path = "../../../common/exit-policy"} nym-service-providers-common = { path = "../../../service-providers/common" } nym-sphinx-addressing = { path = "../../../common/nymsphinx/addressing" } serde = { workspace = true, features = ["derive"] } diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index bca0bc25a5..72241f7ef2 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -103,9 +103,11 @@ pub struct SendRequest { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[non_exhaustive] pub enum QueryRequest { OpenProxy, Description, + ExitPolicy, } #[derive(Debug, Clone)] diff --git a/common/socks5/requests/src/response.rs b/common/socks5/requests/src/response.rs index e7ea358479..584f39df5b 100644 --- a/common/socks5/requests/src/response.rs +++ b/common/socks5/requests/src/response.rs @@ -5,6 +5,7 @@ use crate::{ make_bincode_serializer, ConnectionId, InsufficientSocketDataError, SocketData, Socks5ProtocolVersion, Socks5RequestError, }; +use nym_exit_policy::ExitPolicy; use nym_service_providers_common::interface::{Serializable, ServiceProviderResponse}; use serde::{Deserialize, Serialize}; use tap::TapFallible; @@ -155,6 +156,18 @@ impl Socks5Response { content: Socks5ResponseContent::Query(query_response), } } + + pub fn new_query_error>( + protocol_version: Socks5ProtocolVersion, + message: S, + ) -> Socks5Response { + Socks5Response { + protocol_version, + content: Socks5ResponseContent::Query(QueryResponse::Error { + message: message.into(), + }), + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -296,9 +309,18 @@ impl ConnectionError { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[non_exhaustive] pub enum QueryResponse { OpenProxy(bool), Description(String), + ExitPolicy { + enabled: bool, + upstream: String, + policy: Option, + }, + Error { + message: String, + }, } #[cfg(test)] @@ -364,11 +386,41 @@ mod tests { let bytes_description = description.clone().into_bytes(); assert_eq!(bytes_description, vec![3, 1, 3, 102, 111, 111]); + let error = Socks5ResponseContent::Query(QueryResponse::Error { + message: "this is an error".to_string(), + }); + let bytes_error = error.clone().into_bytes(); + assert_eq!( + bytes_error, + vec![ + 3, 3, 16, 116, 104, 105, 115, 32, 105, 115, 32, 97, 110, 32, 101, 114, 114, + 111, 114 + ] + ); + + let exit_policy = Socks5ResponseContent::Query(QueryResponse::ExitPolicy { + enabled: false, + upstream: "http://foo.bar".to_string(), + policy: Some(ExitPolicy::new_open()), + }); + let bytes_exit_policy = exit_policy.clone().into_bytes(); + assert_eq!( + bytes_exit_policy, + vec![ + 3, 2, 0, 14, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 1, + 1, 0, 1, 42, 1, 251, 255, 255 + ] + ); + let open_proxy2 = Socks5ResponseContent::try_from_bytes(&bytes_open_proxy).unwrap(); let description2 = Socks5ResponseContent::try_from_bytes(&bytes_description).unwrap(); + let error2 = Socks5ResponseContent::try_from_bytes(&bytes_error).unwrap(); + let exit_policy2 = Socks5ResponseContent::try_from_bytes(&bytes_exit_policy).unwrap(); assert_eq!(open_proxy, open_proxy2); assert_eq!(description, description2); + assert_eq!(error, error2); + assert_eq!(exit_policy, exit_policy2); } } } diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index c58020ca3c..9029c14a7a 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -95,6 +95,8 @@ pub(crate) struct OverrideNetworkRequesterConfig { pub(crate) medium_toggle: bool, pub(crate) open_proxy: Option, + pub(crate) enable_exit_policy: Option, + pub(crate) enable_statistics: Option, pub(crate) statistics_recipient: Option, } @@ -204,6 +206,10 @@ pub(crate) fn override_network_requester_config( nym_network_requester::Config::with_open_proxy, opts.open_proxy, ) + .with_optional( + nym_network_requester::Config::with_old_allow_list, + opts.enable_exit_policy.map(|e| !e), + ) .with_optional( nym_network_requester::Config::with_enabled_statistics, opts.enable_statistics, diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 645fbd7ab3..e5e14068a8 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -126,6 +126,12 @@ pub struct Init { )] medium_toggle: bool, + /// Specifies whether this network requester will run using the default ExitPolicy + /// as opposed to the allow list. + /// Note: this setting will become the default in the future releases. + #[clap(long)] + with_exit_policy: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -159,6 +165,7 @@ impl<'a> From<&'a Init> for OverrideNetworkRequesterConfig { no_cover: value.no_cover, medium_toggle: value.medium_toggle, open_proxy: value.open_proxy, + enable_exit_policy: value.with_exit_policy, enable_statistics: value.enable_statistics, statistics_recipient: value.statistics_recipient.clone(), } @@ -275,6 +282,7 @@ mod tests { fastmode: false, no_cover: false, medium_toggle: false, + with_exit_policy: None, }; std::env::set_var(BECH32_PREFIX, "n"); diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 95dd0c5707..185bf75a53 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -126,6 +126,12 @@ pub struct Run { #[clap(long, group = "network", hide = true)] custom_mixnet: Option, + /// Specifies whether this network requester will run using the default ExitPolicy + /// as opposed to the allow list. + /// Note: this setting will become the default in the future releases. + #[clap(long)] + with_exit_policy: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, @@ -162,6 +168,7 @@ impl<'a> From<&'a Run> for OverrideNetworkRequesterConfig { no_cover: value.no_cover, medium_toggle: value.medium_toggle, open_proxy: value.open_proxy, + enable_exit_policy: value.with_exit_policy, enable_statistics: value.enable_statistics, statistics_recipient: value.statistics_recipient.clone(), } diff --git a/gateway/src/commands/setup_network_requester.rs b/gateway/src/commands/setup_network_requester.rs index f59f37dbd5..6c45c05d19 100644 --- a/gateway/src/commands/setup_network_requester.rs +++ b/gateway/src/commands/setup_network_requester.rs @@ -12,46 +12,46 @@ use std::path::PathBuf; #[derive(Args, Clone)] pub struct CmdArgs { /// The id of the gateway you want to initialise local network requester for. - #[arg(long)] + #[clap(long)] id: String, /// Path to custom location for network requester's config. - #[arg(long)] + #[clap(long)] custom_config_path: Option, /// Specify whether the network requester should be enabled. // (you might want to create all the configs, generate keys, etc. but not actually run the NR just yet) - #[arg(long)] + #[clap(long)] enabled: Option, // note: those flags are set as bools as we want to explicitly override any settings values // so say `open_proxy` was set to true in the config.toml. youd have to explicitly state `open-proxy=false` // as an argument here to override it as opposed to not providing the value at all. /// Specifies whether this network requester should run in 'open-proxy' mode - #[arg(long)] + #[clap(long)] open_proxy: Option, /// Enable service anonymized statistics that get sent to a statistics aggregator server - #[arg(long)] + #[clap(long)] enable_statistics: Option, /// Mixnet client address where a statistics aggregator is running. The default value is a Nym /// aggregator client - #[arg(long)] + #[clap(long)] statistics_recipient: Option, /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init - #[arg(long, hide = true, conflicts_with = "medium_toggle")] + #[clap(long, hide = true, conflicts_with = "medium_toggle")] fastmode: bool, /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) - #[arg(long, hide = true, conflicts_with = "medium_toggle")] + #[clap(long, hide = true, conflicts_with = "medium_toggle")] no_cover: bool, /// Enable medium mixnet traffic, for experiments only. /// This includes things like disabling cover traffic, no per hop delays, etc. - #[arg( + #[clap( long, hide = true, conflicts_with = "no_cover", @@ -59,6 +59,12 @@ pub struct CmdArgs { )] medium_toggle: bool, + /// Specifies whether this network requester will run using the default ExitPolicy + /// as opposed to the allow list. + /// Note: this setting will become the default in the future releases. + #[clap(long)] + with_exit_policy: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -70,6 +76,7 @@ impl<'a> From<&'a CmdArgs> for OverrideNetworkRequesterConfig { no_cover: value.no_cover, medium_toggle: value.medium_toggle, open_proxy: value.open_proxy, + enable_exit_policy: value.with_exit_policy, enable_statistics: value.enable_statistics, statistics_recipient: value.statistics_recipient.clone(), } diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index eabb377456..a53a966584 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -4,10 +4,13 @@ use crate::config::Config; use crate::error::GatewayError; use crate::node::helpers::load_public_key; +use log::warn; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; +use nym_network_requester::RequestFilter; use nym_node::error::NymNodeError; use nym_node::http::api::api_requests; +use nym_node::http::api::api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node::http::api::api_requests::SignedHostInformation; use nym_node::http::router::WireguardAppState; use nym_node::wireguard::types::GatewayClientRegistry; @@ -98,44 +101,154 @@ fn load_network_requester_details( ) } -pub(crate) fn start_http_api( - gateway_config: &Config, - network_requester_config: Option<&nym_network_requester::Config>, - client_registry: Arc, - identity_keypair: &identity::KeyPair, +pub(crate) struct HttpApiBuilder<'a> { + gateway_config: &'a Config, + network_requester_config: Option<&'a nym_network_requester::Config>, + exit_policy: Option, + + identity_keypair: &'a identity::KeyPair, // TODO: this should be a wg specific key and not re-used sphinx sphinx_keypair: Arc, - task_client: TaskClient, -) -> Result<(), GatewayError> { - // is it suboptimal to load all the keys, etc for the second time after they've already been - // retrieved during startup of the rest of the components? - // yes, a bit. - // but in the grand scheme of things performance penalty is negligible since it's only happening on startup - // and makes the code a bit nicer to manage. on top of it, all of it will refactored anyway at some point - // (famous last words, eh? - 22.09.23) - let mut config = nym_node::http::Config::new( - bin_info_owned!(), - load_host_details( + client_registry: Option>, +} + +impl<'a> HttpApiBuilder<'a> { + pub(crate) fn new( + gateway_config: &'a Config, + identity_keypair: &'a identity::KeyPair, + sphinx_keypair: Arc, + ) -> Self { + HttpApiBuilder { gateway_config, - sphinx_keypair.public_key(), + network_requester_config: None, + exit_policy: None, identity_keypair, - )?, - ) - .with_gateway(load_gateway_details(gateway_config)?) - .with_landing_page_assets(gateway_config.http.landing_page_assets_path.as_ref()); + sphinx_keypair, + client_registry: None, + } + } + + #[must_use] + pub(crate) fn with_maybe_network_requester( + mut self, + network_requester_config: Option<&'a nym_network_requester::Config>, + ) -> Self { + self.network_requester_config = network_requester_config; + self + } + + #[must_use] + pub(crate) fn with_maybe_network_request_filter( + mut self, + request_filter: Option, + ) -> Self { + let Some(request_filter) = request_filter else { + warn!("no valid request filter has been passed. no changes will be made"); + return self; + }; + + // we can cheat here a bit since we're not refreshing the exit policy + // thus: + // - we can ignore the Arc pointer and clone the inner value + // - we can set the last refresh time to the current time + // + // once we start refreshing it, we'll have to change it, but at that point + // the allow list will be probably be completely removed and thus the pointer management + // will be much easier + let Some(exit_policy) = request_filter.current_exit_policy_filter() else { + warn!("this node does not use an exit policy. no changes will be made"); + return self; + }; - if let Some(nr_config) = network_requester_config { - config = config - .with_network_requester(load_network_requester_details(gateway_config, nr_config)?) + // if there's no upstream (i.e. open proxy), we couldn't have possibly updated it : ) + let last_updated = if exit_policy.upstream().is_some() { + #[allow(clippy::expect_used)] + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is set to before the unix epoch") + .as_secs() + } else { + 0 + }; + + self.exit_policy = Some(UsedExitPolicy { + enabled: true, + upstream_source: exit_policy + .upstream() + .map(|u| u.to_string()) + .unwrap_or_default(), + last_updated, + policy: Some(exit_policy.policy().clone()), + }); + + self } - let wg_state = WireguardAppState::new(sphinx_keypair, client_registry, Default::default()); - let router = nym_node::http::NymNodeRouter::new(config, Some(wg_state)); + #[must_use] + pub(crate) fn with_wireguard_client_registry( + mut self, + client_registry: Arc, + ) -> Self { + self.client_registry = Some(client_registry); + self + } + + pub(crate) fn start(self, task_client: TaskClient) -> Result<(), GatewayError> { + // is it suboptimal to load all the keys, etc for the second time after they've already been + // retrieved during startup of the rest of the components? + // yes, a bit. + // but in the grand scheme of things performance penalty is negligible since it's only happening on startup + // and makes the code a bit nicer to manage. on top of it, all of it will refactored anyway at some point + // (famous last words, eh? - 22.09.23) + let mut config = nym_node::http::Config::new( + bin_info_owned!(), + load_host_details( + self.gateway_config, + self.sphinx_keypair.public_key(), + self.identity_keypair, + )?, + ) + .with_gateway(load_gateway_details(self.gateway_config)?) + .with_landing_page_assets(self.gateway_config.http.landing_page_assets_path.as_ref()); + + if let Some(nr_config) = self.network_requester_config { + config = config.with_network_requester(load_network_requester_details( + self.gateway_config, + nr_config, + )?); - let server = router - .build_server(&gateway_config.http.bind_address)? - .with_task_client(task_client); - tokio::spawn(async move { server.run().await }); - Ok(()) + if let Some(exit_policy) = self.exit_policy { + config = config.with_used_exit_policy(exit_policy) + } + } + + let wg_state = self.client_registry.map(|client_registry| { + WireguardAppState::new(self.sphinx_keypair, client_registry, Default::default()) + }); + + let router = nym_node::http::NymNodeRouter::new(config, wg_state); + + let server = router + .build_server(&self.gateway_config.http.bind_address)? + .with_task_client(task_client); + tokio::spawn(async move { server.run().await }); + Ok(()) + } } + +// pub(crate) fn start_http_api( +// gateway_config: &Config, +// network_requester_config: Option<&nym_network_requester::Config>, +// client_registry: Arc, +// identity_keypair: &identity::KeyPair, +// // TODO: this should be a wg specific key and not re-used sphinx +// sphinx_keypair: Arc, +// +// task_client: TaskClient, +// ) -> Result<(), GatewayError> { +// HttpApiBuilder::new(gateway_config, identity_keypair, sphinx_keypair) +// .with_wireguard_client_registry(client_registry) +// .with_network_requester(network_requester_config) +// .start(task_client) +// } diff --git a/gateway/src/node/client_handling/embedded_network_requester/mod.rs b/gateway/src/node/client_handling/embedded_network_requester/mod.rs index 7d2d612aa0..e088535cd9 100644 --- a/gateway/src/node/client_handling/embedded_network_requester/mod.rs +++ b/gateway/src/node/client_handling/embedded_network_requester/mod.rs @@ -6,7 +6,6 @@ use crate::node::client_handling::websocket::message_receiver::{ }; use futures::StreamExt; use log::{debug, error}; -use nym_network_requester::core::OnStartData; use nym_network_requester::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::DestinationAddressBytes; @@ -22,9 +21,9 @@ pub(crate) struct LocalNetworkRequesterHandle { } impl LocalNetworkRequesterHandle { - pub(crate) fn new(start_data: OnStartData, mix_message_sender: MixMessageSender) -> Self { + pub(crate) fn new(address: Recipient, mix_message_sender: MixMessageSender) -> Self { Self { - address: start_data.address, + address, mix_message_sender, } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index f902bb66d7..a51180ba35 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -5,7 +5,7 @@ use self::storage::PersistentStorage; use crate::commands::helpers::{override_network_requester_config, OverrideNetworkRequesterConfig}; use crate::config::Config; use crate::error::GatewayError; -use crate::http::start_http_api; +use crate::http::HttpApiBuilder; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_network_requester::{ LocalNetworkRequesterHandle, MessageRouter, @@ -23,7 +23,7 @@ use log::*; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; -use nym_network_requester::{LocalGateway, NRServiceProviderBuilder}; +use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilter}; use nym_node::wireguard::types::GatewayClientRegistry; use nym_statistics_common::collector::StatisticsSender; use nym_task::{TaskClient, TaskManager}; @@ -41,6 +41,15 @@ pub(crate) mod mixnet_handling; pub(crate) mod statistics; pub(crate) mod storage; +// TODO: should this struct live here? +struct StartedNetworkRequester { + /// Request filter, either an exit policy or the allow list, used by the network requester. + used_request_filter: RequestFilter, + + /// Handle to interact with the local network requester + handle: LocalNetworkRequesterHandle, +} + /// Wire up and create Gateway instance pub(crate) async fn create_gateway( config: Config, @@ -215,7 +224,7 @@ impl Gateway { &self, forwarding_channel: MixForwardingSender, shutdown: TaskClient, - ) -> Result { + ) -> Result { info!("Starting network requester..."); // if network requester is enabled, configuration file must be provided! @@ -235,7 +244,6 @@ impl Gateway { router_tx, ); - // TODO: well, wire it up internally to gateway traffic, shutdowns, etc. let (on_start_tx, on_start_rx) = oneshot::channel(); let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone()) .with_shutdown(shutdown) @@ -266,12 +274,13 @@ impl Gateway { }; MessageRouter::new(nr_mix_receiver, packet_router).start_with_shutdown(router_shutdown); - info!( - "the local network requester is running on {}", - start_data.address - ); + let address = start_data.address; - Ok(LocalNetworkRequesterHandle::new(start_data, nr_mix_sender)) + info!("the local network requester is running on {address}",); + Ok(StartedNetworkRequester { + used_request_filter: start_data.request_filter, + handle: LocalNetworkRequesterHandle::new(address, nr_mix_sender), + }) } async fn wait_for_interrupt(shutdown: TaskManager) -> Result<(), Box> { @@ -340,15 +349,6 @@ impl Gateway { CoconutVerifier::new(nyxd_client) }; - start_http_api( - &self.config, - self.network_requester_opts.as_ref().map(|o| &o.config), - self.client_registry.clone(), - self.identity_keypair.as_ref(), - self.sphinx_keypair.clone(), - shutdown.subscribe().named("http-api"), - )?; - let mix_forwarding_channel = self.start_packet_forwarder(shutdown.subscribe().named("PacketForwarder")); @@ -372,7 +372,7 @@ impl Gateway { }); } - if self.config.network_requester.enabled { + let nr_request_filter = if self.config.network_requester.enabled { let embedded_nr = self .start_network_requester( mix_forwarding_channel.clone(), @@ -380,10 +380,22 @@ impl Gateway { ) .await?; // insert information about embedded NR to the active clients store - active_clients_store.insert_embedded(embedded_nr) + active_clients_store.insert_embedded(embedded_nr.handle); + Some(embedded_nr.used_request_filter) } else { info!("embedded network requester is disabled"); - } + None + }; + + HttpApiBuilder::new( + &self.config, + self.identity_keypair.as_ref(), + self.sphinx_keypair.clone(), + ) + .with_wireguard_client_registry(self.client_registry.clone()) + .with_maybe_network_requester(self.network_requester_opts.as_ref().map(|o| &o.config)) + .with_maybe_network_request_filter(nr_request_filter) + .start(shutdown.subscribe().named("http-api"))?; self.start_client_websocket_listener( mix_forwarding_channel, diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 83a2c97fe9..6dbf35cc44 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -4278,6 +4278,15 @@ dependencies = [ "nym-contracts-common", ] +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", + "tracing", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" @@ -4432,6 +4441,7 @@ dependencies = [ "base64 0.21.4", "nym-bin-common", "nym-crypto", + "nym-exit-policy", "nym-wireguard-types", "schemars", "serde", @@ -4558,6 +4568,7 @@ version = "0.1.0" dependencies = [ "bincode", "log", + "nym-exit-policy", "nym-service-providers-common", "nym-sphinx-addressing", "serde", diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index ff7cb13336..bea55fbffb 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -18,6 +18,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +nym-exit-policy = { path = "../../common/exit-policy" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } # feature-specific dependencies: @@ -36,4 +37,4 @@ tokio = { workspace = true, features = ["full"] } [features] default = ["client"] client = ["http-api-client", "async-trait"] -openapi = ["utoipa", "nym-bin-common/openapi", "nym-wireguard-types/openapi"] +openapi = ["utoipa", "nym-bin-common/openapi", "nym-wireguard-types/openapi", "nym-exit-policy/openapi"] diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/mod.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/models.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/models.rs new file mode 100644 index 0000000000..bd54d9ec3e --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/exit_policy/models.rs @@ -0,0 +1,44 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_exit_policy::{ + AddressPolicy, AddressPolicyAction, AddressPolicyRule, AddressPortPattern, ExitPolicy, + PortRange, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct UsedExitPolicy { + /// Flag indicating whether this node uses the below exit policy or + /// whether it still relies on the legacy allow lists. + pub enabled: bool, + + /// Source URL from which the exit policy was obtained + #[cfg_attr( + feature = "openapi", + schema(example = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt") + )] + pub upstream_source: String, + + /// Unix timestamp indicating when the exit policy was last updated from the upstream. + #[cfg_attr(feature = "openapi", schema(example = 1697731611))] + pub last_updated: u64, + + /// The actual policy used by this node. + // `ExitPolicy` is a type alias for `AddressPolicy`, + // but it seems utoipa is too stupid to realise it by itself + #[cfg_attr(feature = "openapi", schema(value_type = Option))] + pub policy: Option, +} + +impl Default for UsedExitPolicy { + fn default() -> Self { + UsedExitPolicy { + enabled: false, + upstream_source: "".to_string(), + last_updated: 0, + policy: None, + } + } +} diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs index c57081432b..6309de6974 100644 --- a/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs @@ -1,4 +1,5 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod exit_policy; pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs index b8aa29236b..08bb332356 100644 --- a/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs @@ -16,16 +16,3 @@ pub struct NetworkRequester { /// Nym address of this network requester. pub address: String, } - -// #[derive(Serialize, Debug, Clone, ToSchema)] -// pub struct ExitPolicy { -// // pub allowed_ports: -// // pub deny_list: DenyList, -// } -// -// #[derive(Serialize, Debug, Clone, ToSchema)] -// pub struct DenyListEntry { -// // pub ports: -// // pub ips: -// pub description: String, -// } diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index e1922f5d3b..cb7942f861 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -86,7 +86,15 @@ pub mod routes { } pub mod network_requester { - // use super::*; + use super::*; + + pub const EXIT_POLICY: &str = "/exit-policy"; + + absolute_route!( + exit_policy_absolute, + network_requester_absolute(), + EXIT_POLICY + ); } } } diff --git a/nym-node/src/http/router/api/v1/network_requester/exit_policy.rs b/nym-node/src/http/router/api/v1/network_requester/exit_policy.rs new file mode 100644 index 0000000000..a743f20514 --- /dev/null +++ b/nym-node/src/http/router/api/v1/network_requester/exit_policy.rs @@ -0,0 +1,30 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; + +/// Returns information about the exit policy used by this node. +#[utoipa::path( + get, + path = "/exit-policy", + context_path = "/api/v1/network-requester", + tag = "Network Requester", + responses( + (status = 200, content( + ("application/json" = UsedExitPolicy), + ("application/yaml" = UsedExitPolicy) + )) + ), + params(OutputParams) +)] +pub(crate) async fn node_exit_policy( + policy: UsedExitPolicy, + Query(output): Query, +) -> ExitPolicyResponse { + let output = output.output.unwrap_or_default(); + output.to_response(policy) +} + +pub type ExitPolicyResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/network_requester/mod.rs b/nym-node/src/http/router/api/v1/network_requester/mod.rs index aced86bad3..7b2ebabf34 100644 --- a/nym-node/src/http/router/api/v1/network_requester/mod.rs +++ b/nym-node/src/http/router/api/v1/network_requester/mod.rs @@ -1,23 +1,36 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::api::v1::network_requester::exit_policy::node_exit_policy; use axum::routing::get; use axum::Router; +use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node_requests::api::v1::network_requester::models; +use nym_node_requests::routes::api::v1::network_requester; +pub mod exit_policy; pub mod root; #[derive(Debug, Clone, Default)] pub struct Config { pub details: Option, + pub exit_policy: Option, } pub(crate) fn routes(config: Config) -> Router { - Router::new().route( - "/", - get({ - let network_requester_details = config.details; - move |query| root::root_network_requester(network_requester_details, query) - }), - ) + Router::new() + .route( + "/", + get({ + let network_requester_details = config.details; + move |query| root::root_network_requester(network_requester_details, query) + }), + ) + .route( + network_requester::EXIT_POLICY, + get({ + let policy = config.exit_policy.unwrap_or_default(); + move |query| node_exit_policy(policy, query) + }), + ) } diff --git a/nym-node/src/http/router/api/v1/openapi.rs b/nym-node/src/http/router/api/v1/openapi.rs index 8678c613d7..6302429f73 100644 --- a/nym-node/src/http/router/api/v1/openapi.rs +++ b/nym-node/src/http/router/api/v1/openapi.rs @@ -26,6 +26,7 @@ use utoipa_swagger_ui::SwaggerUi; api::v1::gateway::client_interfaces::wireguard::client_registry::get_client, api::v1::mixnode::root::root_mixnode, api::v1::network_requester::root::root_network_requester, + api::v1::network_requester::exit_policy::node_exit_policy, ), components( schemas( @@ -49,6 +50,12 @@ use utoipa_swagger_ui::SwaggerUi; api_requests::v1::gateway::client_interfaces::wireguard::models::ClientRegistrationResponse, api_requests::v1::mixnode::models::Mixnode, api_requests::v1::network_requester::models::NetworkRequester, + api_requests::v1::network_requester::exit_policy::models::AddressPolicy, + api_requests::v1::network_requester::exit_policy::models::AddressPolicyRule, + api_requests::v1::network_requester::exit_policy::models::AddressPolicyAction, + api_requests::v1::network_requester::exit_policy::models::AddressPortPattern, + api_requests::v1::network_requester::exit_policy::models::PortRange, + api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy, ), responses(RequestError) ) diff --git a/nym-node/src/http/router/mod.rs b/nym-node/src/http/router/mod.rs index a25c6427f8..3548266373 100644 --- a/nym-node/src/http/router/mod.rs +++ b/nym-node/src/http/router/mod.rs @@ -9,6 +9,7 @@ use crate::http::NymNodeHTTPServer; use axum::Router; use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard}; use nym_node_requests::api::v1::mixnode::models::Mixnode; +use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node_requests::api::v1::network_requester::models::NetworkRequester; use nym_node_requests::api::v1::node::models; use nym_node_requests::api::SignedHostInformation; @@ -87,6 +88,12 @@ impl Config { self.api.v1_config.network_requester.details = Some(network_requester); self } + + #[must_use] + pub fn with_used_exit_policy(mut self, exit_policy: UsedExitPolicy) -> Self { + self.api.v1_config.network_requester.exit_policy = Some(exit_policy); + self + } } pub struct NymNodeRouter { diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index c34975a3f3..42c54bc0b2 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -47,7 +47,7 @@ nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-crypto = { path = "../../common/crypto" } nym-network-defaults = { path = "../../common/network-defaults" } -nym-ordered-buffer = {path = "../../common/socks5/ordered-buffer"} +nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" } nym-sdk = { path = "../../sdk/rust/nym-sdk" } nym-service-providers-common = { path = "../common" } nym-socks5-proxy-helpers = { path = "../../common/socks5/proxy-helpers" } @@ -56,7 +56,8 @@ nym-sphinx = { path = "../../common/nymsphinx" } nym-statistics-common = { path = "../../common/statistics" } nym-task = { path = "../../common/task" } nym-types = { path = "../../common/types" } +nym-exit-policy = { path = "../../common/exit-policy", features = ["client"] } [dev-dependencies] tempfile = "3.5.0" -anyhow = "1.0.68" +anyhow = { workspace = true } diff --git a/service-providers/network-requester/src/cli/init.rs b/service-providers/network-requester/src/cli/init.rs index d4321846e7..d6c1960da6 100644 --- a/service-providers/network-requester/src/cli/init.rs +++ b/service-providers/network-requester/src/cli/init.rs @@ -29,42 +29,42 @@ use tap::TapFallible; #[derive(Args, Clone)] pub(crate) struct Init { /// Id of the nym-mixnet-client we want to create config for. - #[arg(long)] + #[clap(long)] id: String, /// Specifies whether this network requester should run in 'open-proxy' mode - #[arg(long)] + #[clap(long)] open_proxy: Option, /// Enable service anonymized statistics that get sent to a statistics aggregator server - #[arg(long)] + #[clap(long)] enable_statistics: Option, /// Mixnet client address where a statistics aggregator is running. The default value is a Nym /// aggregator client - #[arg(long)] + #[clap(long)] statistics_recipient: Option, /// Id of the gateway we are going to connect to. - #[arg(long)] + #[clap(long)] gateway: Option, /// Specifies whether the new gateway should be determined based by latency as opposed to being chosen /// uniformly. - #[arg(long, conflicts_with = "gateway")] + #[clap(long, conflicts_with = "gateway")] latency_based_selection: bool, /// Force register gateway. WARNING: this will overwrite any existing keys for the given id, /// potentially causing loss of access. - #[arg(long)] + #[clap(long)] force_register_gateway: bool, /// Comma separated list of rest endpoints of the nyxd validators - #[arg(long, alias = "nymd_validators", value_delimiter = ',')] + #[clap(long, alias = "nymd_validators", value_delimiter = ',')] nyxd_urls: Option>, /// Comma separated list of rest endpoints of the API validators - #[arg( + #[clap( long, alias = "api_validators", value_delimiter = ',', @@ -79,10 +79,16 @@ pub(crate) struct Init { /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. - #[arg(long)] + #[clap(long)] enabled_credentials_mode: Option, - #[arg(short, long, default_value_t = OutputFormat::default())] + /// Specifies whether this network requester will run using the default ExitPolicy + /// as opposed to the allow list. + /// Note: this setting will become the default in the future releases. + #[clap(long)] + with_exit_policy: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -95,6 +101,7 @@ impl From for OverrideConfig { medium_toggle: false, nyxd_urls: init_config.nyxd_urls, enabled_credentials_mode: init_config.enabled_credentials_mode, + enable_exit_policy: init_config.with_exit_policy, open_proxy: init_config.open_proxy, enable_statistics: init_config.enabled_credentials_mode, statistics_recipient: init_config.statistics_recipient, diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index 5566de0e93..c090912743 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -81,6 +81,7 @@ pub(crate) struct OverrideConfig { medium_toggle: bool, nyxd_urls: Option>, enabled_credentials_mode: Option, + enable_exit_policy: Option, open_proxy: Option, enable_statistics: Option, @@ -141,6 +142,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi args.enabled_credentials_mode.map(|b| !b), ) .with_optional(Config::with_open_proxy, args.open_proxy) + .with_optional( + Config::with_old_allow_list, + args.enable_exit_policy.map(|e| !e), + ) .with_optional(Config::with_enabled_statistics, args.enable_statistics) .with_optional(Config::with_statistics_recipient, args.statistics_recipient) } diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 0973e473b7..c55e37ca48 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -59,6 +59,12 @@ pub(crate) struct Run { conflicts_with = "fastmode" )] medium_toggle: bool, + + /// Specifies whether this network requester will run using the default ExitPolicy + /// as opposed to the allow list. + /// Note: this setting will become the default in the future releases. + #[clap(long)] + with_exit_policy: Option, } impl From for OverrideConfig { @@ -70,6 +76,7 @@ impl From for OverrideConfig { medium_toggle: run_config.medium_toggle, nyxd_urls: None, enabled_credentials_mode: run_config.enabled_credentials_mode, + enable_exit_policy: run_config.with_exit_policy, open_proxy: run_config.open_proxy, enable_statistics: run_config.enabled_credentials_mode, statistics_recipient: run_config.statistics_recipient, diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index b26ec1ff20..81ace42e36 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -5,8 +5,9 @@ use crate::config::persistence::NetworkRequesterPaths; use crate::config::template::CONFIG_TEMPLATE; use nym_bin_common::logging::LoggingSettings; use nym_config::{ - must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, - OptionalSet, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, + must_get_home, read_config_from_toml_file, save_formatted_config_to_file, + serde_helpers::de_maybe_stringified, NymConfigTemplate, OptionalSet, DEFAULT_CONFIG_DIR, + DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, }; use nym_service_providers_common::DEFAULT_SERVICE_PROVIDERS_DIR; use serde::{Deserialize, Serialize}; @@ -14,9 +15,11 @@ use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; +use url::Url; pub use nym_client_core::config::Config as BaseClientConfig; pub use nym_client_core::config::{DebugConfig, GatewayEndpointConfig}; +use nym_network_defaults::mainnet; use nym_sphinx::params::PacketSize; pub mod old_config_v1_1_13; @@ -141,6 +144,12 @@ impl Config { self } + #[must_use] + pub fn with_old_allow_list(mut self, use_old_allow_list: bool) -> Self { + self.network_requester.use_deprecated_allow_list = use_old_allow_list; + self + } + #[must_use] pub fn with_enabled_statistics(mut self, enabled_statistics: bool) -> Self { self.network_requester.enabled_statistics = enabled_statistics; @@ -216,6 +225,15 @@ pub struct NetworkRequester { /// Disable Poisson sending rate. /// This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true, pub disable_poisson_rate: bool, + + /// Specifies whether this network requester should be using the deprecated allow-list, + /// as opposed to the new ExitPolicy. + /// Note: this field will be removed in a near future. + pub use_deprecated_allow_list: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub upstream_exit_policy_url: Option, } impl Default for NetworkRequester { @@ -225,6 +243,12 @@ impl Default for NetworkRequester { enabled_statistics: false, statistics_recipient: None, disable_poisson_rate: true, + use_deprecated_allow_list: true, + upstream_exit_policy_url: Some( + mainnet::EXIT_POLICY_URL + .parse() + .expect("invalid default exit policy URL"), + ), } } } @@ -233,6 +257,7 @@ impl Default for NetworkRequester { #[serde(default, deny_unknown_fields)] pub struct Debug { /// Defines how often the standard allow list should get updated + /// Deprecated #[serde(with = "humantime_serde")] pub standard_list_update_interval: Duration, } diff --git a/service-providers/network-requester/src/config/persistence.rs b/service-providers/network-requester/src/config/persistence.rs index 563116933d..d62ffcea9a 100644 --- a/service-providers/network-requester/src/config/persistence.rs +++ b/service-providers/network-requester/src/config/persistence.rs @@ -14,9 +14,11 @@ pub struct NetworkRequesterPaths { #[serde(flatten)] pub common_paths: CommonClientPaths, + /// Deprecated /// Location of the file containing our allow.list pub allowed_list_location: PathBuf, + /// Deprecated /// Location of the file containing our unknown.list pub unknown_list_location: PathBuf, diff --git a/service-providers/network-requester/src/config/template.rs b/service-providers/network-requester/src/config/template.rs index a6aebee454..ae039f5da4 100644 --- a/service-providers/network-requester/src/config/template.rs +++ b/service-providers/network-requester/src/config/template.rs @@ -95,6 +95,14 @@ statistics_recipient = '{{ network_requester.statistics_recipient }}' # This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true, disable_poisson_rate = {{ network_requester.disable_poisson_rate }} +# Specifies whether this network requester should be using the deprecated allow-list, +# as opposed to the new ExitPolicy. +# Note: this field will be removed in a near future. +use_deprecated_allow_list = {{ network_requester.use_deprecated_allow_list }} + +# Specifies the url for an upstream source of the exit policy used by this node. +upstream_exit_policy_url = '{{ network_requester.upstream_exit_policy_url }}' + ##### logging configuration options ##### [logging] diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 39b35708fd..8751b2265c 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -1,14 +1,12 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::allowed_hosts::standard_list::StandardListUpdater; -use crate::allowed_hosts::stored_allowed_hosts::{start_allowed_list_reloader, StoredAllowedHosts}; -use crate::allowed_hosts::{OutboundRequestFilter, StandardList}; use crate::config::{BaseClientConfig, Config}; use crate::error::NetworkRequesterError; use crate::reply::MixnetMessage; +use crate::request_filter::RequestFilter; use crate::statistics::ServiceStatisticsCollector; -use crate::{allowed_hosts, reply, socks5}; +use crate::{reply, socks5}; use async_trait::async_trait; use futures::channel::{mpsc, oneshot}; use futures::stream::StreamExt; @@ -56,11 +54,16 @@ pub(crate) fn new_legacy_request_version() -> RequestVersion { pub struct OnStartData { // to add more fields as required pub address: Recipient, + + pub request_filter: RequestFilter, } impl OnStartData { - fn new(address: Recipient) -> Self { - Self { address } + fn new(address: Recipient, request_filter: RequestFilter) -> Self { + Self { + address, + request_filter, + } } } @@ -68,7 +71,6 @@ impl OnStartData { // ... something ... pub struct NRServiceProviderBuilder { config: Config, - outbound_request_filter: OutboundRequestFilter, wait_for_gateway: bool, custom_topology_provider: Option>, @@ -79,7 +81,7 @@ pub struct NRServiceProviderBuilder { pub struct NRServiceProvider { config: Config, - outbound_request_filter: OutboundRequestFilter, + request_filter: RequestFilter, mixnet_client: nym_sdk::mixnet::MixnetClient, controller_sender: ControllerSender, @@ -179,18 +181,8 @@ impl ServiceProvider for NRServiceProvider { impl NRServiceProviderBuilder { pub fn new(config: Config) -> NRServiceProviderBuilder { - let standard_list = StandardList::new(); - - let allowed_hosts = StoredAllowedHosts::new(&config.storage_paths.allowed_list_location); - let unknown_hosts = - allowed_hosts::HostsStore::new(&config.storage_paths.unknown_list_location); - - let outbound_request_filter = - OutboundRequestFilter::new(allowed_hosts, standard_list, unknown_hosts); - NRServiceProviderBuilder { config, - outbound_request_filter, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, @@ -334,26 +326,14 @@ impl NRServiceProviderBuilder { .await; }); - // start the standard list updater - StandardListUpdater::new( - self.config - .network_requester_debug - .standard_list_update_interval, - self.outbound_request_filter.standard_list(), - shutdown.get_handle().named("StandardListUpdater"), - ) - .start(); - - // start the allowed.list watcher and updater - start_allowed_list_reloader( - self.outbound_request_filter.allowed_hosts(), - shutdown.get_handle().named("stored_allowed_hosts_reloader"), - ) - .await; + let request_filter = RequestFilter::new(&self.config).await?; + request_filter + .start_update_tasks(&self.config.network_requester_debug, &shutdown) + .await; let mut service_provider = NRServiceProvider { config: self.config, - outbound_request_filter: self.outbound_request_filter, + request_filter: request_filter.clone(), mixnet_client, controller_sender, mix_input_sender, @@ -365,7 +345,10 @@ impl NRServiceProviderBuilder { log::info!("All systems go. Press CTRL-C to stop the server."); if let Some(on_start) = self.on_start { - if on_start.send(OnStartData::new(self_address)).is_err() { + if on_start + .send(OnStartData::new(self_address, request_filter)) + .is_err() + { // the parent has dropped the channel before receiving the response return Err(NetworkRequesterError::DisconnectedParent); } @@ -533,7 +516,7 @@ impl NRServiceProvider { } async fn handle_proxy_connect( - &mut self, + &self, remote_version: RequestVersion, sender_tag: Option, connect_req: Box, @@ -549,24 +532,6 @@ impl NRServiceProvider { let remote_addr = connect_req.remote_addr; let conn_id = connect_req.conn_id; - - let open_proxy = self.config.network_requester.open_proxy; - if !open_proxy && !self.outbound_request_filter.check(&remote_addr).await { - let log_msg = format!("Domain {remote_addr:?} failed filter check"); - log::info!("{log_msg}"); - let msg = MixnetMessage::new_connection_error( - return_address, - remote_version, - conn_id, - log_msg, - ); - self.mix_input_sender - .send(msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - return; - } - let traffic_config = self.config.base.debug.traffic; let packet_size = traffic_config .secondary_packet_size @@ -575,10 +540,34 @@ impl NRServiceProvider { let controller_sender_clone = self.controller_sender.clone(); let mix_input_sender_clone = self.mix_input_sender.clone(); let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths(); - let shutdown = self.shutdown.get_handle(); + let mut shutdown = self.shutdown.get_handle(); - // and start the proxy for this connection + // we're just cloning the underlying pointer, nothing expensive is happening here + let request_filter = self.request_filter.clone(); + + // at this point move it into the separate task + // because we might have to resolve the underlying address and it can take some time + // during which we don't want to block other incoming requests tokio::spawn(async move { + if !request_filter.check_address(&remote_addr).await { + let log_msg = format!("Domain {remote_addr:?} failed filter check"); + log::info!("{log_msg}"); + let error_msg = MixnetMessage::new_connection_error( + return_address, + remote_version, + conn_id, + log_msg, + ); + + mix_input_sender_clone + .send(error_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + shutdown.mark_as_success(); + return; + } + + // if all is good, start the proxy for this connection Self::start_proxy( remote_version, conn_id, @@ -615,6 +604,28 @@ impl NRServiceProvider { protocol_version, QueryResponse::Description("Description (placeholder)".to_string()), ), + QueryRequest::ExitPolicy => { + let response = match self.request_filter.current_exit_policy_filter() { + Some(exit_policy_filter) => QueryResponse::ExitPolicy { + enabled: true, + upstream: exit_policy_filter + .upstream() + .map(|u| u.to_string()) + .unwrap_or_default(), + policy: Some(exit_policy_filter.policy().clone()), + }, + None => QueryResponse::ExitPolicy { + enabled: false, + upstream: "".to_string(), + policy: None, + }, + }; + + Socks5Response::new_query(protocol_version, response) + } + _ => { + Socks5Response::new_query_error(protocol_version, "received unknown query variant") + } }; Ok(Some(response)) } diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index cb71cd715a..fb05dc976f 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -1,5 +1,7 @@ pub use nym_client_core::error::ClientCoreError; -use nym_socks5_requests::Socks5RequestError; +use nym_exit_policy::policy::PolicyError; +use nym_socks5_requests::{RemoteAddress, Socks5RequestError}; +use std::net::SocketAddr; #[derive(thiserror::Error, Debug)] pub enum NetworkRequesterError { @@ -33,4 +35,33 @@ pub enum NetworkRequesterError { #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, + + #[error("the provided socket address, '{addr}' is not covered by the exit policy!")] + AddressNotCoveredByExitPolicy { addr: SocketAddr }, + + #[error( + "could not resolve socket address for the provided remote address '{remote}': {source}" + )] + CouldNotResolveHost { + remote: RemoteAddress, + source: std::io::Error, + }, + + #[error("the provided address: '{remote}' was somehow resolved to an empty list of socket addresses")] + EmptyResolvedAddresses { remote: RemoteAddress }, + + #[error("failed to apply the exit policy: {source}")] + ExitPolicyFailure { + #[from] + source: PolicyError, + }, + + #[error("the url provided for the upstream exit policy source is malformed: {source}")] + MalformedExitPolicyUpstreamUrl { + #[source] + source: reqwest::Error, + }, + + #[error("can't setup an exit policy without any upstream urls")] + NoUpstreamExitPolicy, } diff --git a/service-providers/network-requester/src/lib.rs b/service-providers/network-requester/src/lib.rs index cc3915c481..1b730aacd1 100644 --- a/service-providers/network-requester/src/lib.rs +++ b/service-providers/network-requester/src/lib.rs @@ -1,11 +1,11 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod allowed_hosts; pub mod config; pub mod core; pub mod error; mod reply; +pub mod request_filter; mod socks5; mod statistics; @@ -25,3 +25,4 @@ pub use nym_client_core::{ }, }, }; +pub use request_filter::RequestFilter; diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 76e9349a7e..a8efdf0f76 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -6,12 +6,12 @@ use error::NetworkRequesterError; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; -mod allowed_hosts; mod cli; mod config; mod core; mod error; mod reply; +mod request_filter; mod socks5; mod statistics; diff --git a/service-providers/network-requester/src/allowed_hosts/filter.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/filter.rs similarity index 88% rename from service-providers/network-requester/src/allowed_hosts/filter.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/filter.rs index bb33bb931d..5b46f85c41 100644 --- a/service-providers/network-requester/src/allowed_hosts/filter.rs +++ b/service-providers/network-requester/src/request_filter/allowed_hosts/filter.rs @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use super::HostsStore; -use crate::allowed_hosts::group::HostsGroup; -use crate::allowed_hosts::standard_list::StandardList; -use crate::allowed_hosts::stored_allowed_hosts::StoredAllowedHosts; +use crate::request_filter::allowed_hosts::group::HostsGroup; +use crate::request_filter::allowed_hosts::standard_list::StandardList; +use crate::request_filter::allowed_hosts::stored_allowed_hosts::StoredAllowedHosts; use std::net::{IpAddr, SocketAddr}; +use tokio::sync::Mutex; #[derive(Debug)] enum RequestHost { @@ -31,7 +32,7 @@ pub(crate) struct OutboundRequestFilter { pub(super) allowed_hosts: StoredAllowedHosts, pub(super) standard_list: StandardList, root_domain_list: publicsuffix::List, - unknown_hosts: HostsStore, + unknown_hosts: Mutex, } impl OutboundRequestFilter { @@ -56,7 +57,7 @@ impl OutboundRequestFilter { allowed_hosts, standard_list, root_domain_list: domain_list, - unknown_hosts, + unknown_hosts: Mutex::new(unknown_hosts), } } @@ -86,11 +87,12 @@ impl OutboundRequestFilter { } } - fn add_to_unknown_hosts(&mut self, host: RequestHost) { + async fn add_to_unknown_hosts(&self, host: RequestHost) { + let mut guard = self.unknown_hosts.lock().await; match host { - RequestHost::IpAddr(ip_addr) => self.unknown_hosts.add_ip(ip_addr), - RequestHost::SocketAddr(socket_addr) => self.unknown_hosts.add_ip(socket_addr.ip()), - RequestHost::RootDomain(domain) => self.unknown_hosts.add_domain(&domain), + RequestHost::IpAddr(ip_addr) => guard.add_ip(ip_addr), + RequestHost::SocketAddr(socket_addr) => guard.add_ip(socket_addr.ip()), + RequestHost::RootDomain(domain) => guard.add_domain(&domain), } } @@ -112,7 +114,7 @@ impl OutboundRequestFilter { } } - async fn check_request_host(&mut self, request_host: &RequestHost) -> bool { + async fn check_request_host(&self, request_host: &RequestHost) -> bool { // first check our own allow list let local_allowed = self.check_allowed_hosts(request_host).await; @@ -128,12 +130,12 @@ impl OutboundRequestFilter { /// Returns `true` if a host's root domain is in the `allowed_hosts` list. /// /// If it's not in the list, return `false` and write it to the `unknown_hosts` storefile. - pub(crate) async fn check(&mut self, host: &str) -> bool { + pub(crate) async fn check(&self, host: &str) -> bool { let allowed = match self.parse_request_host(host) { Some(request_host) => { let res = self.check_request_host(&request_host).await; if !res { - self.add_to_unknown_hosts(request_host) + self.add_to_unknown_hosts(request_host).await } res } @@ -352,20 +354,32 @@ mod tests { #[tokio::test] async fn are_not_allowed() { let host = "unknown.com"; - let mut filter = setup_empty(); + let filter = setup_empty(); assert!(!filter.check(host).await); } #[tokio::test] async fn get_appended_once_to_the_unknown_hosts_list() { let host = "unknown.com"; - let mut filter = setup_empty(); + let filter = setup_empty(); filter.check(host).await; - assert_eq!(1, filter.unknown_hosts.data.domains.len()); - assert!(filter.unknown_hosts.data.domains.contains("unknown.com")); + assert_eq!(1, filter.unknown_hosts.lock().await.data.domains.len()); + assert!(filter + .unknown_hosts + .lock() + .await + .data + .domains + .contains("unknown.com")); filter.check(host).await; - assert_eq!(1, filter.unknown_hosts.data.domains.len()); - assert!(filter.unknown_hosts.data.domains.contains("unknown.com")); + assert_eq!(1, filter.unknown_hosts.lock().await.data.domains.len()); + assert!(filter + .unknown_hosts + .lock() + .await + .data + .domains + .contains("unknown.com")); } } @@ -377,7 +391,7 @@ mod tests { async fn are_allowed() { let host = "nymtech.net"; - let mut filter = setup_with_allowed(&["nymtech.net"]); + let filter = setup_with_allowed(&["nymtech.net"]); assert!(filter.check(host).await); } @@ -385,13 +399,13 @@ mod tests { async fn are_allowed_for_subdomains() { let host = "foomp.nymtech.net"; - let mut filter = setup_with_allowed(&["nymtech.net"]); + let filter = setup_with_allowed(&["nymtech.net"]); assert!(filter.check(host).await); } #[tokio::test] async fn are_not_appended_to_file() { - let mut filter = setup_with_allowed(&["nymtech.net"]); + let filter = setup_with_allowed(&["nymtech.net"]); // test initial state let lines = @@ -414,7 +428,7 @@ mod tests { let address_good_port = "1.1.1.1:1234"; let address_bad = "1.1.1.2"; - let mut filter = setup_with_allowed(&["1.1.1.1"]); + let filter = setup_with_allowed(&["1.1.1.1"]); assert!(filter.check(address_good).await); assert!(filter.check(address_good_port).await); assert!(!filter.check(address_bad).await); @@ -431,9 +445,8 @@ mod tests { let ip_v6_loopback_port = "[::1]:1234"; - let mut filter1 = setup_with_allowed(&[ip_v6_full, ip_v6_semi, "::1"]); - let mut filter2 = - setup_with_allowed(&[ip_v6_full_rendered, ip_v6_semi_rendered, "::1"]); + let filter1 = setup_with_allowed(&[ip_v6_full, ip_v6_semi, "::1"]); + let filter2 = setup_with_allowed(&[ip_v6_full_rendered, ip_v6_semi_rendered, "::1"]); assert!(filter1.check(ip_v6_full).await); assert!(filter1.check(ip_v6_full_rendered).await); @@ -460,7 +473,7 @@ mod tests { let outside_range2 = "1.2.2.4"; - let mut filter = setup_with_allowed(&[range1, range2]); + let filter = setup_with_allowed(&[range1, range2]); assert!(filter.check("127.0.0.1").await); assert!(filter.check("127.0.0.1:1234").await); assert!(filter.check(bottom_range2).await); @@ -478,7 +491,7 @@ mod tests { let top = "2620:0:ffff:ffff:ffff:ffff:ffff:ffff"; let mid = "2620:0:42::42"; - let mut filter = setup_with_allowed(&[range]); + let filter = setup_with_allowed(&[range]); assert!(filter.check(bottom1).await); assert!(filter.check(bottom2).await); assert!(filter.check(top).await); diff --git a/service-providers/network-requester/src/allowed_hosts/group.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/group.rs similarity index 96% rename from service-providers/network-requester/src/allowed_hosts/group.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/group.rs index 5b398e1ff0..5b5ba4c75a 100644 --- a/service-providers/network-requester/src/allowed_hosts/group.rs +++ b/service-providers/network-requester/src/request_filter/allowed_hosts/group.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::allowed_hosts::host::Host; +use crate::request_filter::allowed_hosts::host::Host; use ipnetwork::IpNetwork; use std::collections::HashSet; use std::net::IpAddr; diff --git a/service-providers/network-requester/src/allowed_hosts/host.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/host.rs similarity index 100% rename from service-providers/network-requester/src/allowed_hosts/host.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/host.rs diff --git a/service-providers/network-requester/src/allowed_hosts/hosts.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/hosts.rs similarity index 99% rename from service-providers/network-requester/src/allowed_hosts/hosts.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/hosts.rs index b9d534fe89..58ac73b669 100644 --- a/service-providers/network-requester/src/allowed_hosts/hosts.rs +++ b/service-providers/network-requester/src/request_filter/allowed_hosts/hosts.rs @@ -1,5 +1,5 @@ use super::host::Host; -use crate::allowed_hosts::group::HostsGroup; +use crate::request_filter::allowed_hosts::group::HostsGroup; use ipnetwork::IpNetwork; use std::{ fs::{self, File, OpenOptions}, diff --git a/service-providers/network-requester/src/allowed_hosts/mod.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/mod.rs similarity index 100% rename from service-providers/network-requester/src/allowed_hosts/mod.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/mod.rs diff --git a/service-providers/network-requester/src/allowed_hosts/standard_list.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/standard_list.rs similarity index 96% rename from service-providers/network-requester/src/allowed_hosts/standard_list.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/standard_list.rs index 3b873827e3..76cc3289a0 100644 --- a/service-providers/network-requester/src/allowed_hosts/standard_list.rs +++ b/service-providers/network-requester/src/request_filter/allowed_hosts/standard_list.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::allowed_hosts::group::HostsGroup; -use crate::allowed_hosts::host::Host; +use crate::request_filter::allowed_hosts::group::HostsGroup; +use crate::request_filter::allowed_hosts::host::Host; use nym_task::TaskClient; use regex::Regex; use std::sync::Arc; diff --git a/service-providers/network-requester/src/allowed_hosts/stored_allowed_hosts.rs b/service-providers/network-requester/src/request_filter/allowed_hosts/stored_allowed_hosts.rs similarity index 98% rename from service-providers/network-requester/src/allowed_hosts/stored_allowed_hosts.rs rename to service-providers/network-requester/src/request_filter/allowed_hosts/stored_allowed_hosts.rs index 20dc0499b9..a7d87d803e 100644 --- a/service-providers/network-requester/src/allowed_hosts/stored_allowed_hosts.rs +++ b/service-providers/network-requester/src/request_filter/allowed_hosts/stored_allowed_hosts.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::allowed_hosts::HostsStore; +use crate::request_filter::allowed_hosts::HostsStore; use async_file_watcher::{AsyncFileWatcher, FileWatcherEventReceiver}; use futures::channel::mpsc; use futures::StreamExt; diff --git a/service-providers/network-requester/src/request_filter/exit_policy/mod.rs b/service-providers/network-requester/src/request_filter/exit_policy/mod.rs new file mode 100644 index 0000000000..beab727a2b --- /dev/null +++ b/service-providers/network-requester/src/request_filter/exit_policy/mod.rs @@ -0,0 +1,82 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkRequesterError; +use log::trace; +use nym_exit_policy::client::get_exit_policy; +use nym_exit_policy::ExitPolicy; +use nym_socks5_requests::RemoteAddress; +use reqwest::IntoUrl; +use tokio::net::lookup_host; +use url::Url; + +pub struct ExitPolicyRequestFilter { + upstream: Option, + policy: ExitPolicy, +} + +impl ExitPolicyRequestFilter { + pub(crate) async fn new_upstream(url: impl IntoUrl) -> Result { + let url = url + .into_url() + .map_err(|source| NetworkRequesterError::MalformedExitPolicyUpstreamUrl { source })?; + + Ok(ExitPolicyRequestFilter { + upstream: Some(url.clone()), + policy: get_exit_policy(url).await?, + }) + } + + pub(crate) fn new(policy: ExitPolicy) -> Self { + ExitPolicyRequestFilter { + upstream: None, + policy, + } + } + + pub fn policy(&self) -> &ExitPolicy { + &self.policy + } + + pub fn upstream(&self) -> Option<&Url> { + self.upstream.as_ref() + } + + pub(crate) async fn check( + &self, + remote: &RemoteAddress, + ) -> Result { + // try to convert the remote to a proper socket address + let addrs = lookup_host(remote) + .await + .map_err(|source| NetworkRequesterError::CouldNotResolveHost { + remote: remote.to_string(), + source, + })? + .collect::>(); + + // I'm honestly not sure if it's possible to return an Ok with an empty iterator, + // but might as well guard against that + if addrs.is_empty() { + return Err(NetworkRequesterError::EmptyResolvedAddresses { + remote: remote.to_string(), + }); + } + + trace!("{remote} has been resolved to {addrs:?}"); + + // if the remote decided to give us an address that can resolve to multiple socket addresses, + // they'd better make sure all of them are allowed by the exit policy. + for addr in addrs { + if !self + .policy + .allows_sockaddr(&addr) + .ok_or(NetworkRequesterError::AddressNotCoveredByExitPolicy { addr })? + { + return Ok(false); + } + } + + Ok(true) + } +} diff --git a/service-providers/network-requester/src/request_filter/mod.rs b/service-providers/network-requester/src/request_filter/mod.rs new file mode 100644 index 0000000000..c3a3697ec0 --- /dev/null +++ b/service-providers/network-requester/src/request_filter/mod.rs @@ -0,0 +1,142 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{self, Config}; +use crate::error::NetworkRequesterError; +use crate::request_filter::allowed_hosts::standard_list::StandardListUpdater; +use crate::request_filter::allowed_hosts::stored_allowed_hosts::{ + start_allowed_list_reloader, StoredAllowedHosts, +}; +use crate::request_filter::allowed_hosts::{OutboundRequestFilter, StandardList}; +use crate::request_filter::exit_policy::ExitPolicyRequestFilter; +use log::{info, warn}; +use nym_exit_policy::ExitPolicy; +use nym_socks5_requests::RemoteAddress; +use nym_task::TaskHandle; +use std::sync::Arc; + +/// Old request filtering based on the allowed.list files. +pub mod allowed_hosts; +pub mod exit_policy; + +enum RequestFilterInner { + AllowList { + open_proxy: bool, + filter: OutboundRequestFilter, + }, + ExitPolicy { + policy_filter: ExitPolicyRequestFilter, + }, +} + +#[derive(Clone)] +pub struct RequestFilter { + inner: Arc, +} + +impl RequestFilter { + pub(crate) async fn new(config: &Config) -> Result { + if config.network_requester.use_deprecated_allow_list { + info!("setting up allow-list based 'OutboundRequestFilter'..."); + Ok(Self::new_allow_list_request_filter(config)) + } else { + info!("setting up ExitPolicy based request filter..."); + Self::new_exit_policy_filter(config).await + } + } + + pub fn current_exit_policy_filter(&self) -> Option<&ExitPolicyRequestFilter> { + match &*self.inner { + RequestFilterInner::AllowList { .. } => None, + RequestFilterInner::ExitPolicy { policy_filter } => Some(policy_filter), + } + } + + pub(crate) async fn start_update_tasks( + &self, + config: &config::Debug, + task_handle: &TaskHandle, + ) { + match &*self.inner { + RequestFilterInner::AllowList { open_proxy, filter } => { + // if we're running in open proxy, we don't have to spawn any refreshers, + // after all, we're going to be accepting all requests regardless + // of the local allow list or the standard list + if *open_proxy { + return; + } + + // start the standard list updater + StandardListUpdater::new( + config.standard_list_update_interval, + filter.standard_list(), + task_handle.get_handle().named("StandardListUpdater"), + ) + .start(); + + // start the allowed.list watcher and updater + start_allowed_list_reloader( + filter.allowed_hosts(), + task_handle + .get_handle() + .named("stored_allowed_hosts_reloader"), + ) + .await; + } + RequestFilterInner::ExitPolicy { .. } => { + // nothing to do for the exit policy (yet; we might add a refresher at some point) + } + } + } + + fn new_allow_list_request_filter(config: &Config) -> Self { + let standard_list = StandardList::new(); + let allowed_hosts = StoredAllowedHosts::new(&config.storage_paths.allowed_list_location); + let unknown_hosts = + allowed_hosts::HostsStore::new(&config.storage_paths.unknown_list_location); + + // TODO: technically if we're running open proxy, we don't have to be loading anything here + RequestFilter { + inner: Arc::new(RequestFilterInner::AllowList { + open_proxy: config.network_requester.open_proxy, + filter: OutboundRequestFilter::new(allowed_hosts, standard_list, unknown_hosts), + }), + } + } + + async fn new_exit_policy_filter(config: &Config) -> Result { + let policy_filter = if config.network_requester.open_proxy { + ExitPolicyRequestFilter::new(ExitPolicy::new_open()) + } else { + let upstream_url = config + .network_requester + .upstream_exit_policy_url + .as_ref() + .ok_or(NetworkRequesterError::NoUpstreamExitPolicy)?; + ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await? + }; + Ok(RequestFilter { + inner: Arc::new(RequestFilterInner::ExitPolicy { policy_filter }), + }) + } + + pub(crate) async fn check_address(&self, address: &RemoteAddress) -> bool { + match &*self.inner { + RequestFilterInner::AllowList { open_proxy, filter } => { + if *open_proxy { + return true; + } + filter.check(address).await + } + RequestFilterInner::ExitPolicy { policy_filter } => { + match policy_filter.check(address).await { + Err(err) => { + warn!("failed to validate '{address}' against the exit policy: {err}"); + false + } + Ok(res) => res, + } + } + } + } +} diff --git a/tools/nym-nr-query/Cargo.toml b/tools/nym-nr-query/Cargo.toml index 9e3dfbb727..9034b536db 100644 --- a/tools/nym-nr-query/Cargo.toml +++ b/tools/nym-nr-query/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.68" +anyhow = { workspace = true } clap = {version = "4.0", features = ["cargo", "derive"]} log = { workspace = true } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } diff --git a/tools/nym-nr-query/src/main.rs b/tools/nym-nr-query/src/main.rs index 362e1312ff..477d3937c9 100644 --- a/tools/nym-nr-query/src/main.rs +++ b/tools/nym-nr-query/src/main.rs @@ -1,5 +1,7 @@ +use anyhow::Result; use clap::{Parser, ValueEnum}; use nym_bin_common::output_format::OutputFormat; +use nym_network_defaults::NymNetworkDetails; use nym_sdk::mixnet::{self, IncludedSurbs, MixnetMessageSender}; use nym_service_providers_common::interface::{ ControlRequest, ControlResponse, ProviderInterfaceVersion, Request, Response, ResponseContent, @@ -51,6 +53,9 @@ enum Commands { /// Check if the network requester is acting a an open proxy OpenProxy, + /// Get the exit policy of this network requester + ExitPolicy, + /// Ping the network requester Ping, } @@ -103,17 +108,26 @@ async fn wait_for_socks5_response(client: &mut mixnet::MixnetClient) -> Socks5Re } } -async fn connect_to_mixnet(gateway: Option) -> mixnet::MixnetClient { - match gateway { - Some(gateway) => mixnet::MixnetClientBuilder::new_ephemeral() - .request_gateway(gateway.to_base58_string()) - .build() - .expect("Failed to create mixnet client") - .connect_to_mixnet() - .await - .expect("Failed to connect to the mixnet"), - None => mixnet::MixnetClient::connect_new().await.unwrap(), - } +async fn connect_to_mixnet(gateway: Option) -> Result { + let network = NymNetworkDetails::new_from_env(); + + Ok(match gateway { + Some(gateway) => { + mixnet::MixnetClientBuilder::new_ephemeral() + .network_details(network) + .request_gateway(gateway.to_base58_string()) + .build()? + .connect_to_mixnet() + .await? + } + None => { + mixnet::MixnetClientBuilder::new_ephemeral() + .network_details(network) + .build()? + .connect_to_mixnet() + .await? + } + }) } fn new_bin_info_request() -> Request { @@ -134,6 +148,14 @@ fn new_open_proxy_request() -> Request { Request::new_provider_data(ProviderInterfaceVersion::new_current(), request_open_proxy) } +fn new_exit_policy_request() -> Request { + let request_exit_policy = Socks5Request::new_query( + Socks5ProtocolVersion::new_current(), + QueryRequest::ExitPolicy, + ); + Request::new_provider_data(ProviderInterfaceVersion::new_current(), request_exit_policy) +} + fn new_ping_request() -> Request { let request_ping = ControlRequest::Health; Request::new_control(ProviderInterfaceVersion::new_current(), request_ping) @@ -145,9 +167,12 @@ struct QueryClient { } impl QueryClient { - async fn new(provider: mixnet::Recipient, gateway: Option) -> Self { - let client = connect_to_mixnet(gateway).await; - Self { client, provider } + async fn new( + provider: mixnet::Recipient, + gateway: Option, + ) -> Result { + let client = connect_to_mixnet(gateway).await?; + Ok(Self { client, provider }) } async fn query_bin_info(&mut self) -> ControlResponse { @@ -191,6 +216,24 @@ impl QueryClient { .clone() } + async fn query_exit_policy(&mut self) -> QueryResponse { + self.client + .send_message( + self.provider, + new_exit_policy_request().into_bytes(), + IncludedSurbs::Amount(10), + ) + .await + .unwrap(); + + let response = wait_for_socks5_response(&mut self.client).await; + response + .content + .as_query() + .expect("Unexpected response type!") + .clone() + } + async fn ping(&mut self) -> PingResponse { let now = std::time::Instant::now(); self.client @@ -275,7 +318,7 @@ async fn main() -> anyhow::Result<()> { nym_network_defaults::setup_env(args.config_env_file.as_ref()); text_println("Registering with gateway...", &args.output); - let mut client = QueryClient::new(args.provider, args.gateway).await; + let mut client = QueryClient::new(args.provider, args.gateway).await?; let our_gateway = client.client.nym_address().gateway(); text_println(&format!(" gateway: {our_gateway}"), &args.output); @@ -310,7 +353,9 @@ async fn main() -> anyhow::Result<()> { Commands::BinaryInfo => client.query_bin_info().await.into(), Commands::SupportedRequestVersions => client.query_supported_versions().await.into(), Commands::OpenProxy => client.query_open_proxy().await.into(), + Commands::ExitPolicy => client.query_exit_policy().await.into(), Commands::Ping => unreachable!(), + // _ => unimplemented!(), }; println!("{}", args.output.format(&resp)); }