From d06b4c83f01e371029fe282a633ac8ec8a5cd94f Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Sat, 25 Jul 2026 08:50:39 -0600 Subject: [PATCH] rules so only ipv4 can mine --- Cargo.lock | 1 + Cargo.toml | 1 + src/common/binary_conversions.rs | 44 +++++- src/common/network_startup.rs | 138 +++++++++++++----- src/rpc/server/handshake.rs | 91 ++++++++---- src/rpc/server/handshake_verifications.rs | 22 +-- src/rpc/server/start_rpc.rs | 41 +++++- src/rpc/server/structs.rs | 1 + src/rpc/server/tests.rs | 16 +- src/standalone_tools/connections/handshake.rs | 9 +- src/startup/connections.rs | 4 +- 11 files changed, 272 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aca46ce..f1207a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,7 @@ dependencies = [ "shellexpand", "skein", "sled", + "socket2", "tokio", "tokio-postgres", "windows-service", diff --git a/Cargo.toml b/Cargo.toml index ec964fa..d4c9259 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ hex = "0.4" falcon-rs = "0.2.4" fn-dsa = "0.3.0" tokio = { version = "1.45.1", features = ["full", "test-util"] } +socket2 = "0.5.7" tokio-postgres = "0.7" rand = "0.8" sled = "0.34" diff --git a/src/common/binary_conversions.rs b/src/common/binary_conversions.rs index 53ab7d4..df5054a 100644 --- a/src/common/binary_conversions.rs +++ b/src/common/binary_conversions.rs @@ -43,8 +43,15 @@ pub fn ip_to_binary(ip_str: &str) -> Vec { bytes } Ok(IpAddr::V6(ipv6_addr)) => { - // IPv6 already fits the fixed 16-byte network layout. - ipv6_addr.octets().to_vec() + if let Some(ipv4_addr) = ipv6_addr.to_ipv4_mapped() { + // IPv4-mapped IPv6 is the same identity as native IPv4. + let mut bytes = vec![0u8; 12]; + bytes.extend_from_slice(&ipv4_addr.octets()); + bytes + } else { + // Native IPv6 already fits the fixed 16-byte layout. + ipv6_addr.octets().to_vec() + } } Err(_) => { // Invalid IP strings are rejected by returning no bytes. @@ -123,7 +130,15 @@ pub fn ip_port_to_binary(ip_port: &str) -> Result, String> { ipv6_bytes[12..].copy_from_slice(&ipv4.octets()); ipv6_bytes.to_vec() } - IpAddr::V6(ipv6) => ipv6.octets().to_vec(), + IpAddr::V6(ipv6) => { + if let Some(ipv4) = ipv6.to_ipv4_mapped() { + let mut bytes = [0u8; 16]; + bytes[12..].copy_from_slice(&ipv4.octets()); + bytes.to_vec() + } else { + ipv6.octets().to_vec() + } + } }; // Ports are little-endian because the rest of the binary protocol @@ -132,3 +147,26 @@ pub fn ip_port_to_binary(ip_port: &str) -> Result, String> { result.extend_from_slice(&port.to_le_bytes()); Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipv4_mapped_address_uses_native_ipv4_wire_identity() { + assert_eq!( + ip_to_binary("::ffff:203.0.113.25"), + ip_to_binary("203.0.113.25") + ); + assert_eq!( + ip_port_to_binary("::ffff:203.0.113.25:50050").unwrap(), + ip_port_to_binary("203.0.113.25:50050").unwrap() + ); + } + + #[test] + fn native_ipv6_wire_format_remains_available_for_rpc_transport() { + assert_eq!(ip_to_binary("2001:db8::1").len(), 16); + assert_eq!(ip_port_to_binary("[2001:db8::1]:50055").unwrap().len(), 18); + } +} diff --git a/src/common/network_startup.rs b/src/common/network_startup.rs index 7f8c980..9896689 100644 --- a/src/common/network_startup.rs +++ b/src/common/network_startup.rs @@ -1,44 +1,75 @@ use crate::Settings; -use ipnetwork::IpNetwork; -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tokio::net::lookup_host; +fn endpoint_ip(ip_or_endpoint: &str) -> Option { + if let Ok(ip) = ip_or_endpoint.parse::() { + return Some(ip); + } + + let host = ip_or_endpoint + .strip_prefix('[') + .and_then(|value| value.split_once(']').map(|(ip, _)| ip)) + .or_else(|| ip_or_endpoint.rsplit_once(':').map(|(ip, _)| ip)) + .unwrap_or(ip_or_endpoint); + host.parse::().ok() +} + +pub fn canonical_ipv4(ip: IpAddr) -> Option { + match ip { + IpAddr::V4(ip) => Some(ip), + IpAddr::V6(ip) => ip.to_ipv4_mapped(), + } +} + +fn is_public_ipv4(ip: Ipv4Addr) -> bool { + !ip.is_unspecified() + && !ip.is_loopback() + && !ip.is_private() + && !ip.is_link_local() + && !ip.is_multicast() + && !ip.is_broadcast() + && !(ip.octets()[0] == 100 && (64..=127).contains(&ip.octets()[1])) +} + pub fn is_public_network_address(ip_or_endpoint: &str) -> bool { - // These ranges cannot be used as advertised network addresses - // because remote peers cannot route to them over the public network. - let private_ranges: Vec = vec![ - "127.0.0.0/8".parse().unwrap(), - "10.0.0.0/8".parse().unwrap(), - "172.16.0.0/12".parse().unwrap(), - "192.168.0.0/16".parse().unwrap(), - "100.64.0.0/10".parse().unwrap(), - "fd00::/8".parse().unwrap(), - "fe80::/10".parse().unwrap(), - ]; + match endpoint_ip(ip_or_endpoint) { + Some(IpAddr::V4(ip)) => is_public_ipv4(ip), + _ => false, + } +} - // Accept either a bare IP or an ip:port endpoint. Bare IPv6 addresses - // must be tested before splitting on the final port separator. - let parsed_ip = match ip_or_endpoint.parse::() { - Ok(ip) => ip, - Err(_) => { - let host = ip_or_endpoint - .strip_prefix('[') - .and_then(|value| value.split_once(']').map(|(ip, _)| ip)) - .or_else(|| ip_or_endpoint.rsplit_once(':').map(|(ip, _)| ip)) - .unwrap_or(ip_or_endpoint); - let Ok(ip) = host.parse::() else { - return false; - }; - ip +pub fn canonical_miner_endpoint(endpoint: &str) -> Option { + let port = endpoint.rsplit_once(':')?.1.parse::().ok()?; + if port == 0 { + return None; + } + let ip = canonical_ipv4(endpoint_ip(endpoint)?)?; + is_public_ipv4(ip).then(|| SocketAddr::new(IpAddr::V4(ip), port).to_string()) +} + +fn is_routable_address(ip: IpAddr) -> bool { + match ip { + IpAddr::V4(ip) => is_public_ipv4(ip), + IpAddr::V6(ip) => { + !ip.is_unspecified() + && !ip.is_loopback() + && !ip.is_unique_local() + && !ip.is_unicast_link_local() + && !ip.is_multicast() } - }; - - !private_ranges - .iter() - .any(|range: &IpNetwork| range.contains(parsed_ip)) + } } pub async fn get_connections() -> Vec { + get_connections_for_transport(false).await +} + +pub async fn get_node_connections() -> Vec { + get_connections_for_transport(true).await +} + +async fn get_connections_for_transport(ipv4_only: bool) -> Vec { // If settings cannot be loaded, startup simply has no piggyback peers. let settings = match Settings::load() { Ok(settings) => settings, @@ -50,7 +81,7 @@ pub async fn get_connections() -> Vec { let mut resolved_connections = Vec::new(); for server in settings.piggybacks { - if let Some(endpoint) = resolve_bootstrap_peer(&server, &default_port).await { + if let Some(endpoint) = resolve_bootstrap_peer(&server, &default_port, ipv4_only).await { resolved_connections.push(endpoint); } @@ -97,11 +128,15 @@ fn split_bootstrap_peer(server: &str, default_port: &str) -> Option<(String, u16 Some((server.to_string(), default_port)) } -async fn resolve_bootstrap_peer(server: &str, default_port: &str) -> Option { +async fn resolve_bootstrap_peer( + server: &str, + default_port: &str, + ipv4_only: bool, +) -> Option { let (host, port) = split_bootstrap_peer(server, default_port)?; if let Ok(ip) = host.parse::() { - if is_public_network_address(&ip.to_string()) { + if (!ipv4_only || canonical_ipv4(ip).is_some()) && is_routable_address(ip) { return Some(SocketAddr::new(ip, port).to_string()); } return None; @@ -111,7 +146,8 @@ async fn resolve_bootstrap_peer(server: &str, default_port: &str) -> Option String { // selection to the operating system and NAT/router. settings.listen_ip } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn miner_endpoint_normalizes_ipv4_mapped_ipv6() { + assert_eq!( + canonical_miner_endpoint("::ffff:8.8.8.8:50050"), + Some("8.8.8.8:50050".to_string()) + ); + } + + #[test] + fn miner_endpoint_rejects_native_ipv6_and_client_port() { + assert_eq!( + canonical_miner_endpoint("[2001:4860:4860::8888]:50050"), + None + ); + assert_eq!(canonical_miner_endpoint("8.8.8.8:0"), None); + } + + #[test] + fn network_map_requires_canonical_public_ipv4() { + assert!(is_public_network_address("8.8.8.8")); + assert!(!is_public_network_address("::ffff:8.8.8.8")); + assert!(!is_public_network_address("2001:4860:4860::8888")); + assert!(!is_public_network_address("192.168.1.10")); + } +} diff --git a/src/rpc/server/handshake.rs b/src/rpc/server/handshake.rs index dfaacc6..ff371ca 100644 --- a/src/rpc/server/handshake.rs +++ b/src/rpc/server/handshake.rs @@ -1,4 +1,5 @@ use crate::common::check_genesis::genesis_checkup; +use crate::common::network_startup::canonical_miner_endpoint; use crate::log::{error, warn}; use crate::miner::flag::{begin_chain_sync, ChainOperationGuard}; use crate::orphans::structs::OrphanCheckup2; @@ -9,6 +10,7 @@ use crate::records::memory::connections::{ mark_peer_network_map_synced, mark_peer_operational, mark_peer_wallet_registry_synced, spawn_peer_setup_retry, }; +use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::generate_uid; use crate::records::memory::response_channels::Command; use crate::records::memory::structs::Connection; @@ -414,6 +416,24 @@ pub async fn handle_handshake( // aced is used instead of ping and pong // as its HEX and compressed better if received_message == "aced" { + // Port 0 is the explicit RPC-client marker. Classify the socket + // before running miner-only public-address checks. + let Some(advertised_port) = endpoint_port(&received_ip) else { + let hashmap_key = generate_uid(); + let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; + let uid = u32::from_le_bytes(padded_bytes); + RpcResponse::Binary(b"error: Invalid advertised endpoint.".to_vec()) + .send(&stream, None, uid) + .await; + drop_failed_handshake(&stream).await; + return; + }; + let client_type = if advertised_port == 0 { + ClientType::Client + } else { + ClientType::Miner + }; + //validate handshake tests if !perform_handshake_tests(HandshakeTestParams { map: map.clone(), @@ -426,6 +446,7 @@ pub async fn handle_handshake( received_signed_message: &received_signed_message, received_address: &received_public_key, received_ip: &received_ip, + client_type, }) .await { @@ -435,36 +456,46 @@ pub async fn handle_handshake( return; } - // Port 0 is the explicit client marker. A node advertising a - // nonzero miner port must actually be reachable before it can be - // stored in connection memory or the network map. - let Some(advertised_port) = endpoint_port(&received_ip) else { - let hashmap_key = generate_uid(); - let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; - let uid = u32::from_le_bytes(padded_bytes); - let response_bytes = - RpcResponse::Binary("error: Invalid advertised endpoint.".as_bytes().to_vec()); - response_bytes.send(&stream, None, uid).await; - drop_failed_handshake(&stream).await; - return; - }; - - let connection_type = if advertised_port == 0 { - "client" - } else if is_port_open(&received_ip).await.unwrap_or(false) { - "miner" + let (connection_type, connections_endpoint) = if client_type == ClientType::Client { + let observed_endpoint = { + let stream_guard = stream.lock().await; + stream_guard + .peer_addr() + .ok() + .map(|address| address.to_string()) + }; + let Some(observed_endpoint) = observed_endpoint else { + drop_failed_handshake(&stream).await; + return; + }; + ("client", observed_endpoint) } else { - let hashmap_key = generate_uid(); - let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; - let uid = u32::from_le_bytes(padded_bytes); - let response_bytes = RpcResponse::Binary( - "error: Handshake failed: advertised miner port is not reachable." - .as_bytes() - .to_vec(), - ); - response_bytes.send(&stream, None, uid).await; - drop_failed_handshake(&stream).await; - return; + let Some(canonical_endpoint) = canonical_miner_endpoint(&received_ip) else { + let hashmap_key = generate_uid(); + let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; + let uid = u32::from_le_bytes(padded_bytes); + RpcResponse::Binary( + b"error: Miner nodes must advertise a public IPv4 address.".to_vec(), + ) + .send(&stream, None, uid) + .await; + drop_failed_handshake(&stream).await; + return; + }; + if !is_port_open(&canonical_endpoint).await.unwrap_or(false) { + let hashmap_key = generate_uid(); + let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; + let uid = u32::from_le_bytes(padded_bytes); + let response_bytes = RpcResponse::Binary( + "error: Handshake failed: advertised miner port is not reachable." + .as_bytes() + .to_vec(), + ); + response_bytes.send(&stream, None, uid).await; + drop_failed_handshake(&stream).await; + return; + } + ("miner", canonical_endpoint) }; if connection_type == "miner" { @@ -508,7 +539,7 @@ pub async fn handle_handshake( // write to memory let connections_key = write_to_memory( - &received_ip, + &connections_endpoint, stream.clone(), connection_type, received_short_address, diff --git a/src/rpc/server/handshake_verifications.rs b/src/rpc/server/handshake_verifications.rs index c435bc5..55816ca 100644 --- a/src/rpc/server/handshake_verifications.rs +++ b/src/rpc/server/handshake_verifications.rs @@ -1,4 +1,5 @@ use crate::records::memory::connections::CONNECTIONS; +use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::{generate_uid, Command}; use crate::rpc::responses::RpcResponse; use crate::rpc::server::structs::HandshakeTestParams; @@ -160,15 +161,18 @@ pub async fn perform_handshake_tests(params: HandshakeTestParams<'_>) -> bool { { return false; } - // verify IP - if !verify_ip_address( - params.map.clone(), - params.stream.clone(), - params.received_ip, - ) - .await - { - return false; + // RPC clients are identified by the accepted socket. Only miners + // advertise a public endpoint that must match the connected peer. + if params.client_type == ClientType::Miner { + if !verify_ip_address( + params.map.clone(), + params.stream.clone(), + params.received_ip, + ) + .await + { + return false; + } } // verify handshake if !verify_handshake( diff --git a/src/rpc/server/start_rpc.rs b/src/rpc/server/start_rpc.rs index c767937..9efcfa2 100644 --- a/src/rpc/server/start_rpc.rs +++ b/src/rpc/server/start_rpc.rs @@ -1,4 +1,4 @@ -use crate::log::error; +use crate::log::{error, warn}; use crate::records::memory::response_channels::Command; use crate::rpc::server::handshake::handle_handshake; use crate::sled::Db; @@ -7,6 +7,7 @@ use crate::Arc; use crate::Mutex; use crate::SocketAddr; use crate::TcpListener; +use socket2::{Domain, Protocol, Socket, Type}; // wait incomming connections pub async fn start_rpc( @@ -45,6 +46,44 @@ async fn rpc_server( return; } }; + // IPv4 remains the node transport and advertised identity. A second + // IPv6-only listener accepts RPC clients on the same configured port; + // the handshake rejects any IPv6 socket that advertises a miner port. + if server_socket.is_ipv4() { + let ipv6_listener = (|| -> std::io::Result { + let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?; + socket.set_only_v6(true)?; + let address = SocketAddr::new("::".parse().unwrap(), server_socket.port()); + socket.bind(&address.into())?; + socket.listen(1024)?; + let listener: std::net::TcpListener = socket.into(); + listener.set_nonblocking(true)?; + TcpListener::from_std(listener) + })(); + match ipv6_listener { + Ok(ipv6_listener) => { + let ipv6_db = db.clone(); + let ipv6_wallet = wallet.clone(); + let ipv6_map = map.clone(); + tokio::spawn(async move { + accept_connections(ipv6_listener, ipv6_db, ipv6_wallet, ipv6_map).await; + }); + } + Err(err) => { + warn!("IPv6 RPC client listener unavailable: {err}"); + } + } + } + + accept_connections(listener, db.clone(), wallet, map).await; +} + +async fn accept_connections( + listener: TcpListener, + db: Db, + wallet: Arc, + map: Arc>, +) { loop { match listener.accept().await { Ok((stream, _)) => { diff --git a/src/rpc/server/structs.rs b/src/rpc/server/structs.rs index 4831e3f..8fbbca7 100644 --- a/src/rpc/server/structs.rs +++ b/src/rpc/server/structs.rs @@ -40,6 +40,7 @@ pub struct HandshakeTestParams<'a> { pub received_signed_message: &'a str, pub received_address: &'a str, pub received_ip: &'a str, + pub client_type: ClientType, } // RpcFloodState is stored as a compact sled value so flood counters can diff --git a/src/rpc/server/tests.rs b/src/rpc/server/tests.rs index 7e578f3..32e11f0 100644 --- a/src/rpc/server/tests.rs +++ b/src/rpc/server/tests.rs @@ -1,4 +1,4 @@ -use crate::common::network_startup::is_public_network_address; +use crate::common::network_startup::{canonical_ipv4, canonical_miner_endpoint}; use crate::Arc; use crate::Mutex; use crate::SocketAddr; @@ -14,22 +14,20 @@ pub fn endpoint_port(ip_port: &str) -> Option { pub async fn ip_test(ip_port: &str, stream: &Arc>) -> bool { // Private, loopback, malformed, and otherwise non-public addresses // are rejected before comparing them with the connected socket. - if !is_public_network_address(ip_port) { + let Some(canonical_endpoint) = canonical_miner_endpoint(ip_port) else { return false; - } + }; let stream_guard = stream.lock().await; let remote_addr = stream_guard.peer_addr(); // Strip the port from either IPv4 host:port or bracketed IPv6 // [host]:port forms before comparing against the socket address. - let submitted_ip = ip_port - .strip_prefix('[') - .and_then(|value| value.split_once(']').map(|(ip, _)| ip)) - .or_else(|| ip_port.rsplit_once(':').map(|(ip, _)| ip)) - .unwrap_or(ip_port); + let submitted_ip = canonical_endpoint + .rsplit_once(':') + .and_then(|(ip, _)| ip.parse().ok()); - remote_addr.is_ok_and(|addr| addr.ip().to_string() == submitted_ip) + remote_addr.is_ok_and(|addr| canonical_ipv4(addr.ip()) == submitted_ip) } // check if port is open diff --git a/src/standalone_tools/connections/handshake.rs b/src/standalone_tools/connections/handshake.rs index ba705c5..931d7f3 100644 --- a/src/standalone_tools/connections/handshake.rs +++ b/src/standalone_tools/connections/handshake.rs @@ -1,5 +1,4 @@ use crate::common::binary_conversions::ip_port_to_binary; -use crate::common::network_startup::get_ip_and_port; use crate::common::skein::skein_256_hash_data; use crate::io; use crate::records::memory::response_channels::Byte3; @@ -70,11 +69,9 @@ pub async fn connect_and_handshake( } async fn get_client_ip() -> String { - // The standalone client announces its local IP with port 0 because it is not opening a server port. - let (_port, _ip, real_ip) = get_ip_and_port().await; - let parts: Vec<&str> = real_ip.split(':').collect(); - let ip = parts.first().copied().unwrap_or("127.0.0.1"); - format!("{ip}:0") + // Port 0 identifies an RPC client. The server derives the client's + // actual IPv4 or IPv6 address from the accepted socket. + "0.0.0.0:0".to_string() } async fn perform_handshake( diff --git a/src/startup/connections.rs b/src/startup/connections.rs index 63135d0..8813e9f 100644 --- a/src/startup/connections.rs +++ b/src/startup/connections.rs @@ -1,4 +1,4 @@ -use crate::common::network_startup::get_connections; +use crate::common::network_startup::get_node_connections; use crate::log::{error, info, warn}; use crate::miner::flag::{ clear_mining_stop_request, is_normal_mode, set_mining_state, set_node_mode, MiningState, @@ -55,7 +55,7 @@ async fn attempt_bootstrap_connections( ) -> Result { // Try the configured bootstrap peers one by one until a // handshake succeeds or the list is exhausted. - let filtered_servers = get_connections().await; + let filtered_servers = get_node_connections().await; let mut last_error: Option = None; for server in filtered_servers {