diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b10a29b84c..dd2a70291b 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -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", diff --git a/nym-node/src/node/mixnet/packet_forwarding/global.rs b/common/bin-common/src/ip_check/mod.rs similarity index 92% rename from nym-node/src/node/mixnet/packet_forwarding/global.rs rename to common/bin-common/src/ip_check/mod.rs index 6f27444a57..26a10e9005 100644 --- a/nym-node/src/node/mixnet/packet_forwarding/global.rs +++ b/common/bin-common/src/ip_check/mod.rs @@ -1,29 +1,12 @@ -// Copyright 2025 - Nym Technologies SA -// 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) } diff --git a/common/bin-common/src/lib.rs b/common/bin-common/src/lib.rs index 9353302b2f..663936085d 100644 --- a/common/bin-common/src/lib.rs +++ b/common/bin-common/src/lib.rs @@ -9,3 +9,6 @@ pub mod completions; #[cfg(feature = "output_format")] pub mod output_format; + +#[cfg(feature = "ip_check")] +pub mod ip_check; diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 26eb3d6f12..1cd66c1de3 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -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", diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 450a88a761..d436bc441e 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -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}; diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 147a5a0129..85e0df1b7e 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -512,6 +512,26 @@ pub(crate) struct ExitGatewayArgs { env = NYMNODE_OPEN_PROXY_ARG, )] pub(crate) open_proxy: Option, + + /// 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, + + /// 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, } 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 } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 88f225fc93..8d43a10f3d 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -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 diff --git a/nym-node/src/config/old_configs/old_config_v12.rs b/nym-node/src/config/old_configs/old_config_v12.rs index 9e71ab6be4..ce0b381e3f 100644 --- a/nym-node/src/config/old_configs/old_config_v12.rs +++ b/nym-node/src/config/old_configs/old_config_v12.rs @@ -938,6 +938,7 @@ pub async fn try_upgrade_config_v12>( 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>( }, }, 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 diff --git a/nym-node/src/config/service_providers.rs b/nym-node/src/config/service_providers.rs index b7d2f2cb77..863218b1f8 100644 --- a/nym-node/src/config/service_providers.rs +++ b/nym-node/src/config/service_providers.rs @@ -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(), } } diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 01f8a7113d..196c5dcc2f 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -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"; diff --git a/nym-node/src/node/mixnet/packet_forwarding/mod.rs b/nym-node/src/node/mixnet/packet_forwarding/mod.rs index 6fa21d31c5..dc8d4af7d4 100644 --- a/nym-node/src/node/mixnet/packet_forwarding/mod.rs +++ b/nym-node/src/node/mixnet/packet_forwarding/mod.rs @@ -15,8 +15,6 @@ use std::io; use tokio::time::Instant; use tracing::{debug, error, trace, warn}; -pub(crate) mod global; - pub struct PacketForwarder { delay_queue: NonExhaustiveDelayQueue, mixnet_client: C, diff --git a/nym-node/src/node/routing_filter/network_filter.rs b/nym-node/src/node/routing_filter/network_filter.rs index eae67a66f7..274015f784 100644 --- a/nym-node/src/node/routing_filter/network_filter.rs +++ b/nym-node/src/node/routing_filter/network_filter.rs @@ -1,9 +1,9 @@ // Copyright 2025 - Nym Technologies SA // 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; diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index fdb12ae514..956205f8b5 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -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 } diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 3b39fa9706..2df5152045 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -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( diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs index 1352812a24..a2c5c9c2ff 100644 --- a/service-providers/ip-packet-router/src/config/old_config_v1.rs +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -95,6 +95,7 @@ impl From 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, } diff --git a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs index 133df560c7..544768cdd8 100644 --- a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs @@ -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, policy: ExitPolicy, + allow_local_ips: bool, } impl ExitPolicyRequestFilter { - pub(crate) async fn new_upstream(url: impl IntoUrl) -> Result { + pub(crate) async fn new_upstream( + url: impl IntoUrl, + allow_local_ips: bool, + ) -> Result { 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 { + // 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 }) diff --git a/service-providers/ip-packet-router/src/request_filter/mod.rs b/service-providers/ip-packet-router/src/request_filter/mod.rs index 2c724af9eb..4d928e366a 100644 --- a/service-providers/ip-packet-router/src/request_filter/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/mod.rs @@ -43,15 +43,23 @@ impl RequestFilter { } async fn new_exit_policy_filter(config: &Config) -> Result { + 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 { diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index fcb1d3a0d6..426c6a6b22 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -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 } diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index 635e7f16b0..3d0e7f2fd1 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -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 diff --git a/service-providers/network-requester/src/config/old/v5.rs b/service-providers/network-requester/src/config/old/v5.rs index 6e6d5de055..6fe5042007 100644 --- a/service-providers/network-requester/src/config/old/v5.rs +++ b/service-providers/network-requester/src/config/old/v5.rs @@ -113,6 +113,7 @@ impl From 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, } diff --git a/service-providers/network-requester/src/config/template.rs b/service-providers/network-requester/src/config/template.rs index ac1f52f37b..a9886c16d3 100644 --- a/service-providers/network-requester/src/config/template.rs +++ b/service-providers/network-requester/src/config/template.rs @@ -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 }} 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 index 140091625e..61e15ca438 100644 --- a/service-providers/network-requester/src/request_filter/exit_policy/mod.rs +++ b/service-providers/network-requester/src/request_filter/exit_policy/mod.rs @@ -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, policy: ExitPolicy, + allow_local_ips: bool, } impl From 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 { + pub(crate) async fn new_upstream( + url: impl IntoUrl, + allow_local_ips: bool, + ) -> Result { 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 { + 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)