Block non-public IPR/NR checks (#6670)

* Block non-public IPR/NR checks

* Add CLI override flag
This commit is contained in:
dynco-nym
2026-04-15 15:59:38 +02:00
committed by GitHub
parent 7ceaf9a40e
commit ad56645fc5
22 changed files with 142 additions and 35 deletions
+1
View File
@@ -38,6 +38,7 @@ default = []
openapi = ["utoipa"]
output_format = ["serde_json", "dep:clap"]
bin_info_schema = ["schemars"]
ip_check = []
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
otel-otlp = [
"basic_tracing",
@@ -1,29 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
// use `ip` feature without nightly
// issue: https://github.com/rust-lang/rust/issues/27709
pub(crate) const fn is_global_ip(ip: &IpAddr) -> bool {
pub const fn is_global_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(addr) => is_global_ipv4(addr),
IpAddr::V6(addr) => is_global_ipv6(addr),
}
}
const fn is_shared_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)
}
const fn is_benchmarking_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18
}
const fn is_reserved_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] & 240 == 240 && !ip.is_broadcast()
}
const fn is_global_ipv4(ip: &Ipv4Addr) -> bool {
!(ip.octets()[0] == 0 // "This network"
|| ip.is_private()
@@ -42,6 +25,18 @@ const fn is_global_ipv4(ip: &Ipv4Addr) -> bool {
|| ip.is_broadcast())
}
const fn is_shared_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)
}
const fn is_benchmarking_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18
}
const fn is_reserved_ipv4(ip: &Ipv4Addr) -> bool {
ip.octets()[0] & 240 == 240 && !ip.is_broadcast()
}
const fn is_documentation_ipv6(ip: &Ipv6Addr) -> bool {
(ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)
}
+3
View File
@@ -9,3 +9,6 @@ pub mod completions;
#[cfg(feature = "output_format")]
pub mod output_format;
#[cfg(feature = "ip_check")]
pub mod ip_check;
+1
View File
@@ -58,6 +58,7 @@ sysinfo = { workspace = true }
nym-bin-common = { workspace = true, features = [
"basic_tracing",
"output_format",
"ip_check"
] }
nym-client-core-config-types = { workspace = true, features = [
"disk-persistence",
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::config::upgrade_helpers::try_load_current_config;
use crate::error::NymNodeError;
use crate::node::NymNode;
use crate::node::bonding_information::BondingInformation;
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
use nym_bin_common::ip_check::is_global_ip;
use std::fs;
use std::net::IpAddr;
use tracing::{debug, info, trace, warn};
+26
View File
@@ -512,6 +512,26 @@ pub(crate) struct ExitGatewayArgs {
env = NYMNODE_OPEN_PROXY_ARG,
)]
pub(crate) open_proxy: Option<bool>,
/// Allow the network requester to forward traffic to non-globally-routable
/// addresses. Intended for local development, private-network deployments,
/// and testnet scenarios.
/// Not recommended on production exit gateway unless you know what you're doing.
#[clap(
long,
env = NYMNODE_NR_ALLOW_LOCAL_IPS_ARG,
)]
pub(crate) nr_allow_local_ips: Option<bool>,
/// Allow the IP packet router to forward traffic to non-globally-routable
/// addresses. Intended for local development, private-network deployments,
/// and testnet scenarios.
/// Not recommended on production exit gateway unless you know what you're doing.
#[clap(
long,
env = NYMNODE_IPR_ALLOW_LOCAL_IPS_ARG,
)]
pub(crate) ipr_allow_local_ips: Option<bool>,
}
impl ExitGatewayArgs {
@@ -533,6 +553,12 @@ impl ExitGatewayArgs {
if let Some(open_proxy) = self.open_proxy {
section.open_proxy = open_proxy
}
if let Some(allow_local_ips) = self.nr_allow_local_ips {
section.network_requester.allow_local_ips = allow_local_ips
}
if let Some(allow_local_ips) = self.ipr_allow_local_ips {
section.ip_packet_router.allow_local_ips = allow_local_ips
}
section
}
+2
View File
@@ -107,6 +107,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig {
},
network_requester: nym_network_requester::config::NetworkRequester {
open_proxy: config.service_providers.open_proxy,
allow_local_ips: config.service_providers.network_requester.allow_local_ips,
disable_poisson_rate: config
.service_providers
.network_requester
@@ -150,6 +151,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig {
},
ip_packet_router: nym_ip_packet_router::config::IpPacketRouter {
open_proxy: config.service_providers.open_proxy,
allow_local_ips: config.service_providers.ip_packet_router.allow_local_ips,
disable_poisson_rate: config
.service_providers
.ip_packet_router
@@ -938,6 +938,7 @@ pub async fn try_upgrade_config_v12<P: AsRef<Path>>(
open_proxy: old_cfg.service_providers.open_proxy,
upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url,
network_requester: NetworkRequester {
allow_local_ips: false,
debug: NetworkRequesterDebug {
enabled: old_cfg.service_providers.network_requester.debug.enabled,
disable_poisson_rate: old_cfg
@@ -953,6 +954,7 @@ pub async fn try_upgrade_config_v12<P: AsRef<Path>>(
},
},
ip_packet_router: IpPacketRouter {
allow_local_ips: false,
debug: IpPacketRouterDebug {
enabled: old_cfg.service_providers.ip_packet_router.debug.enabled,
disable_poisson_rate: old_cfg
+12
View File
@@ -75,6 +75,11 @@ impl ServiceProvidersConfig {
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
pub struct NetworkRequester {
/// Allow the network requester to forward traffic to non-globally-routable addresses.
/// Intended for development & testing.
#[serde(default)]
pub allow_local_ips: bool,
#[serde(default)]
pub debug: NetworkRequesterDebug,
}
@@ -83,6 +88,7 @@ pub struct NetworkRequester {
impl Default for NetworkRequester {
fn default() -> Self {
NetworkRequester {
allow_local_ips: false,
debug: Default::default(),
}
}
@@ -117,6 +123,11 @@ impl Default for NetworkRequesterDebug {
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct IpPacketRouter {
/// Allow the IP packet router to forward traffic to non-globally-routable addresses.
/// Intended for development & testing.
#[serde(default)]
pub allow_local_ips: bool,
#[serde(default)]
pub debug: IpPacketRouterDebug,
}
@@ -125,6 +136,7 @@ pub struct IpPacketRouter {
impl Default for IpPacketRouter {
fn default() -> Self {
IpPacketRouter {
allow_local_ips: false,
debug: Default::default(),
}
}
+2
View File
@@ -70,6 +70,8 @@ pub mod vars {
// exit gateway:
pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY";
pub const NYMNODE_OPEN_PROXY_ARG: &str = "NYMNODE_OPEN_PROXY";
pub const NYMNODE_NR_ALLOW_LOCAL_IPS_ARG: &str = "NYMNODE_NR_ALLOW_LOCAL_IPS";
pub const NYMNODE_IPR_ALLOW_LOCAL_IPS_ARG: &str = "NYMNODE_IPR_ALLOW_LOCAL_IPS";
// LP:
pub const NYMNODE_LP_CONTROL_BIND_ADDRESS_ARG: &str = "NYMNODE_LP_CONTROL_BIND_ADDRESS";
@@ -15,8 +15,6 @@ use std::io;
use tokio::time::Instant;
use tracing::{debug, error, trace, warn};
pub(crate) mod global;
pub struct PacketForwarder<C, F> {
delay_queue: NonExhaustiveDelayQueue<MixPacket>,
mixnet_client: C,
@@ -1,9 +1,9 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
use crate::node::routing_filter::RoutingFilter;
use arc_swap::ArcSwap;
use nym_bin_common::ip_check::is_global_ip;
use std::collections::HashSet;
use std::net::IpAddr;
use std::sync::Arc;
@@ -19,7 +19,7 @@ etherparse = { workspace = true }
futures = { workspace = true }
log = { workspace = true }
nym-bin-common = { workspace = true, features = ["clap", "basic_tracing"] }
nym-bin-common = { workspace = true, features = ["clap", "basic_tracing", "ip_check"] }
nym-client-core = { workspace = true }
nym-config = { workspace = true }
nym-crypto = { workspace = true }
@@ -192,6 +192,11 @@ pub struct IpPacketRouter {
/// and thus would attempt to resolve **ANY** request it receives.
pub open_proxy: bool,
/// Allow the IP packet router to forward traffic to non-globally-routable addresses.
/// Intended for development & testing.
#[serde(default)]
pub allow_local_ips: bool,
/// Disable Poisson sending rate.
pub disable_poisson_rate: bool,
@@ -204,6 +209,7 @@ impl Default for IpPacketRouter {
fn default() -> Self {
IpPacketRouter {
open_proxy: false,
allow_local_ips: false,
disable_poisson_rate: true,
#[allow(clippy::expect_used)]
upstream_exit_policy_url: Some(
@@ -95,6 +95,7 @@ impl From<IpPacketRouterV1> for IpPacketRouter {
fn from(value: IpPacketRouterV1) -> Self {
IpPacketRouter {
open_proxy: false,
allow_local_ips: false,
disable_poisson_rate: value.disable_poisson_rate,
upstream_exit_policy_url: value.upstream_exit_policy_url,
}
@@ -4,6 +4,8 @@
use std::net::SocketAddr;
use crate::error::IpPacketRouterError;
use log::warn;
use nym_bin_common::ip_check::is_global_ip;
use nym_exit_policy::ExitPolicy;
use nym_exit_policy::client::get_exit_policy;
use reqwest::IntoUrl;
@@ -13,10 +15,14 @@ pub struct ExitPolicyRequestFilter {
#[allow(unused)]
upstream: Option<Url>,
policy: ExitPolicy,
allow_local_ips: bool,
}
impl ExitPolicyRequestFilter {
pub(crate) async fn new_upstream(url: impl IntoUrl) -> Result<Self, IpPacketRouterError> {
pub(crate) async fn new_upstream(
url: impl IntoUrl,
allow_local_ips: bool,
) -> Result<Self, IpPacketRouterError> {
let url = url
.into_url()
.map_err(|source| IpPacketRouterError::MalformedExitPolicyUpstreamUrl { source })?;
@@ -24,21 +30,24 @@ impl ExitPolicyRequestFilter {
Ok(ExitPolicyRequestFilter {
upstream: Some(url.clone()),
policy: get_exit_policy(url).await?,
allow_local_ips,
})
}
#[allow(unused)]
pub(crate) fn new(policy: ExitPolicy) -> Self {
pub(crate) fn new(policy: ExitPolicy, allow_local_ips: bool) -> Self {
ExitPolicyRequestFilter {
upstream: None,
policy,
allow_local_ips,
}
}
pub fn new_from_policy(policy: ExitPolicy) -> Self {
pub fn new_from_policy(policy: ExitPolicy, allow_local_ips: bool) -> Self {
ExitPolicyRequestFilter {
upstream: None,
policy,
allow_local_ips,
}
}
@@ -53,6 +62,13 @@ impl ExitPolicyRequestFilter {
}
pub(crate) async fn check(&self, addr: &SocketAddr) -> Result<bool, IpPacketRouterError> {
// private ranges are disallowed regardless of policy: end user has
// no business with internal/private IP ranges of IPR
if !self.allow_local_ips && !is_global_ip(&addr.ip()) {
warn!("Rejecting non-global address {addr}");
return Ok(false);
}
self.policy
.allows_sockaddr(addr)
.ok_or(IpPacketRouterError::AddressNotCoveredByExitPolicy { addr: *addr })
@@ -43,15 +43,23 @@ impl RequestFilter {
}
async fn new_exit_policy_filter(config: &Config) -> Result<Self, IpPacketRouterError> {
let allow_local_ips = config.ip_packet_router.allow_local_ips;
if allow_local_ips {
warn!(
"Requests to non-global destinations are allowed by the policy guard. \
This is intended for local development and NOT recommended in production \
unless you know what you're doing."
);
}
let policy_filter = if config.ip_packet_router.open_proxy {
ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open())
ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open(), allow_local_ips)
} else {
let upstream_url = config
.ip_packet_router
.upstream_exit_policy_url
.as_ref()
.ok_or(IpPacketRouterError::NoUpstreamExitPolicy)?;
ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await?
ExitPolicyRequestFilter::new_upstream(upstream_url.clone(), allow_local_ips).await?
};
Ok(RequestFilter {
@@ -44,7 +44,7 @@ zeroize = { workspace = true }
# internal
nym-async-file-watcher = { workspace = true }
nym-bin-common = { workspace = true, features = ["output_format", "clap", "basic_tracing"] }
nym-bin-common = { workspace = true, features = ["output_format", "clap", "basic_tracing", "ip_check"] }
nym-client-core = { workspace = true, features = ["cli", "fs-gateways-storage", "fs-surb-storage"] }
nym-client-websocket-requests = { workspace = true }
nym-config = { workspace = true }
@@ -210,6 +210,11 @@ pub struct NetworkRequester {
/// and thus would attempt to resolve **ANY** request it receives.
pub open_proxy: bool,
/// Allow the network requester to forward traffic to non-globally-routable addresses.
/// Intended for development & testing.
#[serde(default)]
pub allow_local_ips: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true,
pub disable_poisson_rate: bool,
@@ -223,6 +228,7 @@ impl Default for NetworkRequester {
fn default() -> Self {
NetworkRequester {
open_proxy: false,
allow_local_ips: false,
disable_poisson_rate: true,
upstream_exit_policy_url: Some(
mainnet::EXIT_POLICY_URL
@@ -113,6 +113,7 @@ impl From<NetworkRequesterV5> for NetworkRequester {
fn from(value: NetworkRequesterV5) -> Self {
NetworkRequester {
open_proxy: value.open_proxy,
allow_local_ips: false,
disable_poisson_rate: value.disable_poisson_rate,
upstream_exit_policy_url: value.upstream_exit_policy_url,
}
@@ -81,6 +81,11 @@ nr_description = '{{ storage_paths.nr_description }}'
# and thus would attempt to resolve **ANY** request it receives.
open_proxy = {{ network_requester.open_proxy }}
# Allow forwarding traffic to non-globally-routable addresses.
# Intended for local development for security reasons.
# Only enable if you know what you're doing.
allow_local_ips = {{ network_requester.allow_local_ips }}
# Disable Poisson sending rate
# This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true,
disable_poisson_rate = {{ network_requester.disable_poisson_rate }}
@@ -3,7 +3,8 @@
use crate::config::Config;
use crate::error::NetworkRequesterError;
use log::trace;
use log::{trace, warn};
use nym_bin_common::ip_check::is_global_ip;
use nym_exit_policy::ExitPolicy;
use nym_exit_policy::client::get_exit_policy;
use nym_socks5_requests::RemoteAddress;
@@ -14,16 +15,20 @@ use url::Url;
pub struct ExitPolicyRequestFilter {
upstream: Option<Url>,
policy: ExitPolicy,
allow_local_ips: bool,
}
impl From<ExitPolicy> for ExitPolicyRequestFilter {
fn from(value: ExitPolicy) -> Self {
ExitPolicyRequestFilter::new_from_policy(value)
ExitPolicyRequestFilter::new_from_policy(value, false)
}
}
impl ExitPolicyRequestFilter {
pub(crate) async fn new_upstream(url: impl IntoUrl) -> Result<Self, NetworkRequesterError> {
pub(crate) async fn new_upstream(
url: impl IntoUrl,
allow_local_ips: bool,
) -> Result<Self, NetworkRequesterError> {
let url = url
.into_url()
.map_err(|source| NetworkRequesterError::MalformedExitPolicyUpstreamUrl { source })?;
@@ -31,27 +36,37 @@ impl ExitPolicyRequestFilter {
Ok(ExitPolicyRequestFilter {
upstream: Some(url.clone()),
policy: get_exit_policy(url).await?,
allow_local_ips,
})
}
pub(crate) async fn new(config: &Config) -> Result<Self, NetworkRequesterError> {
let allow_local_ips = config.network_requester.allow_local_ips;
if allow_local_ips {
warn!(
"Requests to non-global destinations are allowed by the policy guard. \
This is intended for local development and NOT recommended in production \
unless you know what you're doing."
);
}
let policy_filter = if config.network_requester.open_proxy {
ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open())
ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open(), allow_local_ips)
} else {
let upstream_url = config
.network_requester
.upstream_exit_policy_url
.as_ref()
.ok_or(NetworkRequesterError::NoUpstreamExitPolicy)?;
ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await?
ExitPolicyRequestFilter::new_upstream(upstream_url.clone(), allow_local_ips).await?
};
Ok(policy_filter)
}
pub fn new_from_policy(policy: ExitPolicy) -> Self {
pub fn new_from_policy(policy: ExitPolicy, allow_local_ips: bool) -> Self {
ExitPolicyRequestFilter {
upstream: None,
policy,
allow_local_ips,
}
}
@@ -89,6 +104,13 @@ impl ExitPolicyRequestFilter {
// 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 {
// private ranges are disallowed regardless of policy: end user has
// no business with internal/private IP ranges of network requester
if !self.allow_local_ips && !is_global_ip(&addr.ip()) {
warn!("Rejecting non-global address {addr} for '{remote}'");
return Ok(false);
}
// exit policy determines which PUBLIC facing addresses are allowed
if !self
.policy
.allows_sockaddr(&addr)