Skip to content

Commit

Permalink
Feature/exit policy (#4030)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jstuczyn authored Oct 26, 2023
1 parent a5fa5dc commit 0801b3c
Show file tree
Hide file tree
Showing 56 changed files with 2,071 additions and 209 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ members = [
"common/crypto",
"common/dkg",
"common/execute",
"common/exit-policy",
"common/http-api-client",
"common/inclusion-probability",
"common/ledger",
Expand Down
1 change: 1 addition & 0 deletions common/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
47 changes: 47 additions & 0 deletions common/config/src/serde_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2023 - Nym Technologies SA <[email protected]>
// 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<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr<Err = E>,
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<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
de_maybe_stringified(deserializer)
}

pub fn de_maybe_path<'de, D>(deserializer: D) -> Result<Option<PathBuf>, D::Error>
where
D: Deserializer<'de>,
{
de_maybe_stringified(deserializer)
}

pub fn de_maybe_port<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: Deserializer<'de>,
{
let port = u16::deserialize(deserializer)?;
if port == 0 {
Ok(None)
} else {
Ok(Some(port))
}
}
33 changes: 33 additions & 0 deletions common/exit-policy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
10 changes: 10 additions & 0 deletions common/exit-policy/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2023 - Nym Technologies SA <[email protected]>
// 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, PolicyError> {
ExitPolicy::parse_from_torrc(reqwest::get(url).await?.text().await?)
}
233 changes: 233 additions & 0 deletions common/exit-policy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Copyright 2023 - Nym Technologies SA <[email protected]>
// 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<S: AsRef<str>>(exit_policy: S) -> Result<ExitPolicy, PolicyError> {
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::<Result<Vec<_>, _>>()?;

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<AddressPolicyRule, PolicyError> {
// 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)
}
}
Loading

0 comments on commit 0801b3c

Please sign in to comment.