rules so only ipv4 can mine

This commit is contained in:
viraladmin 2026-07-25 08:50:39 -06:00
parent 9381914a2c
commit d06b4c83f0
11 changed files with 272 additions and 96 deletions

1
Cargo.lock generated
View File

@ -350,6 +350,7 @@ dependencies = [
"shellexpand", "shellexpand",
"skein", "skein",
"sled", "sled",
"socket2",
"tokio", "tokio",
"tokio-postgres", "tokio-postgres",
"windows-service", "windows-service",

View File

@ -36,6 +36,7 @@ hex = "0.4"
falcon-rs = "0.2.4" falcon-rs = "0.2.4"
fn-dsa = "0.3.0" fn-dsa = "0.3.0"
tokio = { version = "1.45.1", features = ["full", "test-util"] } tokio = { version = "1.45.1", features = ["full", "test-util"] }
socket2 = "0.5.7"
tokio-postgres = "0.7" tokio-postgres = "0.7"
rand = "0.8" rand = "0.8"
sled = "0.34" sled = "0.34"

View File

@ -43,9 +43,16 @@ pub fn ip_to_binary(ip_str: &str) -> Vec<u8> {
bytes bytes
} }
Ok(IpAddr::V6(ipv6_addr)) => { Ok(IpAddr::V6(ipv6_addr)) => {
// IPv6 already fits the fixed 16-byte network layout. 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() ipv6_addr.octets().to_vec()
} }
}
Err(_) => { Err(_) => {
// Invalid IP strings are rejected by returning no bytes. // Invalid IP strings are rejected by returning no bytes.
Vec::new() Vec::new()
@ -123,7 +130,15 @@ pub fn ip_port_to_binary(ip_port: &str) -> Result<Vec<u8>, String> {
ipv6_bytes[12..].copy_from_slice(&ipv4.octets()); ipv6_bytes[12..].copy_from_slice(&ipv4.octets());
ipv6_bytes.to_vec() 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 // 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<Vec<u8>, String> {
result.extend_from_slice(&port.to_le_bytes()); result.extend_from_slice(&port.to_le_bytes());
Ok(result) 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);
}
}

View File

@ -1,44 +1,75 @@
use crate::Settings; use crate::Settings;
use ipnetwork::IpNetwork; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::net::{IpAddr, SocketAddr};
use tokio::net::lookup_host; use tokio::net::lookup_host;
pub fn is_public_network_address(ip_or_endpoint: &str) -> bool { fn endpoint_ip(ip_or_endpoint: &str) -> Option<IpAddr> {
// These ranges cannot be used as advertised network addresses if let Ok(ip) = ip_or_endpoint.parse::<IpAddr>() {
// because remote peers cannot route to them over the public network. return Some(ip);
let private_ranges: Vec<IpNetwork> = 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(),
];
// 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::<IpAddr>() {
Ok(ip) => ip,
Err(_) => {
let host = ip_or_endpoint let host = ip_or_endpoint
.strip_prefix('[') .strip_prefix('[')
.and_then(|value| value.split_once(']').map(|(ip, _)| ip)) .and_then(|value| value.split_once(']').map(|(ip, _)| ip))
.or_else(|| ip_or_endpoint.rsplit_once(':').map(|(ip, _)| ip)) .or_else(|| ip_or_endpoint.rsplit_once(':').map(|(ip, _)| ip))
.unwrap_or(ip_or_endpoint); .unwrap_or(ip_or_endpoint);
let Ok(ip) = host.parse::<IpAddr>() else { host.parse::<IpAddr>().ok()
return false;
};
ip
} }
};
!private_ranges pub fn canonical_ipv4(ip: IpAddr) -> Option<Ipv4Addr> {
.iter() match ip {
.any(|range: &IpNetwork| range.contains(parsed_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 {
match endpoint_ip(ip_or_endpoint) {
Some(IpAddr::V4(ip)) => is_public_ipv4(ip),
_ => false,
}
}
pub fn canonical_miner_endpoint(endpoint: &str) -> Option<String> {
let port = endpoint.rsplit_once(':')?.1.parse::<u16>().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()
}
}
} }
pub async fn get_connections() -> Vec<String> { pub async fn get_connections() -> Vec<String> {
get_connections_for_transport(false).await
}
pub async fn get_node_connections() -> Vec<String> {
get_connections_for_transport(true).await
}
async fn get_connections_for_transport(ipv4_only: bool) -> Vec<String> {
// If settings cannot be loaded, startup simply has no piggyback peers. // If settings cannot be loaded, startup simply has no piggyback peers.
let settings = match Settings::load() { let settings = match Settings::load() {
Ok(settings) => settings, Ok(settings) => settings,
@ -50,7 +81,7 @@ pub async fn get_connections() -> Vec<String> {
let mut resolved_connections = Vec::new(); let mut resolved_connections = Vec::new();
for server in settings.piggybacks { 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); 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)) Some((server.to_string(), default_port))
} }
async fn resolve_bootstrap_peer(server: &str, default_port: &str) -> Option<String> { async fn resolve_bootstrap_peer(
server: &str,
default_port: &str,
ipv4_only: bool,
) -> Option<String> {
let (host, port) = split_bootstrap_peer(server, default_port)?; let (host, port) = split_bootstrap_peer(server, default_port)?;
if let Ok(ip) = host.parse::<IpAddr>() { if let Ok(ip) = host.parse::<IpAddr>() {
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 Some(SocketAddr::new(ip, port).to_string());
} }
return None; return None;
@ -111,7 +146,8 @@ async fn resolve_bootstrap_peer(server: &str, default_port: &str) -> Option<Stri
// The resolved endpoint handed to the rest of the protocol is still an IP. // The resolved endpoint handed to the rest of the protocol is still an IP.
let resolved = lookup_host((host.as_str(), port)).await.ok()?; let resolved = lookup_host((host.as_str(), port)).await.ok()?;
for socket in resolved { for socket in resolved {
if is_public_network_address(&socket.ip().to_string()) { if (!ipv4_only || canonical_ipv4(socket.ip()).is_some()) && is_routable_address(socket.ip())
{
return Some(socket.to_string()); return Some(socket.to_string());
} }
} }
@ -163,3 +199,33 @@ pub async fn get_listen_ip() -> String {
// selection to the operating system and NAT/router. // selection to the operating system and NAT/router.
settings.listen_ip 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"));
}
}

View File

@ -1,4 +1,5 @@
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::common::network_startup::canonical_miner_endpoint;
use crate::log::{error, warn}; use crate::log::{error, warn};
use crate::miner::flag::{begin_chain_sync, ChainOperationGuard}; use crate::miner::flag::{begin_chain_sync, ChainOperationGuard};
use crate::orphans::structs::OrphanCheckup2; 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, mark_peer_network_map_synced, mark_peer_operational, mark_peer_wallet_registry_synced,
spawn_peer_setup_retry, spawn_peer_setup_retry,
}; };
use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::generate_uid; use crate::records::memory::response_channels::generate_uid;
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::records::memory::structs::Connection; use crate::records::memory::structs::Connection;
@ -414,6 +416,24 @@ pub async fn handle_handshake(
// aced is used instead of ping and pong // aced is used instead of ping and pong
// as its HEX and compressed better // as its HEX and compressed better
if received_message == "aced" { 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 //validate handshake tests
if !perform_handshake_tests(HandshakeTestParams { if !perform_handshake_tests(HandshakeTestParams {
map: map.clone(), map: map.clone(),
@ -426,6 +446,7 @@ pub async fn handle_handshake(
received_signed_message: &received_signed_message, received_signed_message: &received_signed_message,
received_address: &received_public_key, received_address: &received_public_key,
received_ip: &received_ip, received_ip: &received_ip,
client_type,
}) })
.await .await
{ {
@ -435,25 +456,33 @@ pub async fn handle_handshake(
return; return;
} }
// Port 0 is the explicit client marker. A node advertising a let (connection_type, connections_endpoint) = if client_type == ClientType::Client {
// nonzero miner port must actually be reachable before it can be let observed_endpoint = {
// stored in connection memory or the network map. let stream_guard = stream.lock().await;
let Some(advertised_port) = endpoint_port(&received_ip) else { stream_guard
let hashmap_key = generate_uid(); .peer_addr()
let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; .ok()
let uid = u32::from_le_bytes(padded_bytes); .map(|address| address.to_string())
let response_bytes = };
RpcResponse::Binary("error: Invalid advertised endpoint.".as_bytes().to_vec()); let Some(observed_endpoint) = observed_endpoint else {
response_bytes.send(&stream, None, uid).await;
drop_failed_handshake(&stream).await; drop_failed_handshake(&stream).await;
return; return;
}; };
("client", observed_endpoint)
let connection_type = if advertised_port == 0 {
"client"
} else if is_port_open(&received_ip).await.unwrap_or(false) {
"miner"
} else { } else {
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 hashmap_key = generate_uid();
let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0]; let padded_bytes = [hashmap_key[0], hashmap_key[1], hashmap_key[2], 0];
let uid = u32::from_le_bytes(padded_bytes); let uid = u32::from_le_bytes(padded_bytes);
@ -465,6 +494,8 @@ pub async fn handle_handshake(
response_bytes.send(&stream, None, uid).await; response_bytes.send(&stream, None, uid).await;
drop_failed_handshake(&stream).await; drop_failed_handshake(&stream).await;
return; return;
}
("miner", canonical_endpoint)
}; };
if connection_type == "miner" { if connection_type == "miner" {
@ -508,7 +539,7 @@ pub async fn handle_handshake(
// write to memory // write to memory
let connections_key = write_to_memory( let connections_key = write_to_memory(
&received_ip, &connections_endpoint,
stream.clone(), stream.clone(),
connection_type, connection_type,
received_short_address, received_short_address,

View File

@ -1,4 +1,5 @@
use crate::records::memory::connections::CONNECTIONS; use crate::records::memory::connections::CONNECTIONS;
use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::{generate_uid, Command}; use crate::records::memory::response_channels::{generate_uid, Command};
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::rpc::server::structs::HandshakeTestParams; use crate::rpc::server::structs::HandshakeTestParams;
@ -160,7 +161,9 @@ pub async fn perform_handshake_tests(params: HandshakeTestParams<'_>) -> bool {
{ {
return false; return false;
} }
// verify IP // 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( if !verify_ip_address(
params.map.clone(), params.map.clone(),
params.stream.clone(), params.stream.clone(),
@ -170,6 +173,7 @@ pub async fn perform_handshake_tests(params: HandshakeTestParams<'_>) -> bool {
{ {
return false; return false;
} }
}
// verify handshake // verify handshake
if !verify_handshake( if !verify_handshake(
params.stream.clone(), params.stream.clone(),

View File

@ -1,4 +1,4 @@
use crate::log::error; use crate::log::{error, warn};
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::rpc::server::handshake::handle_handshake; use crate::rpc::server::handshake::handle_handshake;
use crate::sled::Db; use crate::sled::Db;
@ -7,6 +7,7 @@ use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::SocketAddr; use crate::SocketAddr;
use crate::TcpListener; use crate::TcpListener;
use socket2::{Domain, Protocol, Socket, Type};
// wait incomming connections // wait incomming connections
pub async fn start_rpc( pub async fn start_rpc(
@ -45,6 +46,44 @@ async fn rpc_server(
return; 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<TcpListener> {
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<Wallet>,
map: Arc<Mutex<Command>>,
) {
loop { loop {
match listener.accept().await { match listener.accept().await {
Ok((stream, _)) => { Ok((stream, _)) => {

View File

@ -40,6 +40,7 @@ pub struct HandshakeTestParams<'a> {
pub received_signed_message: &'a str, pub received_signed_message: &'a str,
pub received_address: &'a str, pub received_address: &'a str,
pub received_ip: &'a str, pub received_ip: &'a str,
pub client_type: ClientType,
} }
// RpcFloodState is stored as a compact sled value so flood counters can // RpcFloodState is stored as a compact sled value so flood counters can

View File

@ -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::Arc;
use crate::Mutex; use crate::Mutex;
use crate::SocketAddr; use crate::SocketAddr;
@ -14,22 +14,20 @@ pub fn endpoint_port(ip_port: &str) -> Option<u16> {
pub async fn ip_test(ip_port: &str, stream: &Arc<Mutex<TcpStream>>) -> bool { pub async fn ip_test(ip_port: &str, stream: &Arc<Mutex<TcpStream>>) -> bool {
// Private, loopback, malformed, and otherwise non-public addresses // Private, loopback, malformed, and otherwise non-public addresses
// are rejected before comparing them with the connected socket. // 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; return false;
} };
let stream_guard = stream.lock().await; let stream_guard = stream.lock().await;
let remote_addr = stream_guard.peer_addr(); let remote_addr = stream_guard.peer_addr();
// Strip the port from either IPv4 host:port or bracketed IPv6 // Strip the port from either IPv4 host:port or bracketed IPv6
// [host]:port forms before comparing against the socket address. // [host]:port forms before comparing against the socket address.
let submitted_ip = ip_port let submitted_ip = canonical_endpoint
.strip_prefix('[') .rsplit_once(':')
.and_then(|value| value.split_once(']').map(|(ip, _)| ip)) .and_then(|(ip, _)| ip.parse().ok());
.or_else(|| ip_port.rsplit_once(':').map(|(ip, _)| ip))
.unwrap_or(ip_port);
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 // check if port is open

View File

@ -1,5 +1,4 @@
use crate::common::binary_conversions::ip_port_to_binary; 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::common::skein::skein_256_hash_data;
use crate::io; use crate::io;
use crate::records::memory::response_channels::Byte3; use crate::records::memory::response_channels::Byte3;
@ -70,11 +69,9 @@ pub async fn connect_and_handshake(
} }
async fn get_client_ip() -> String { async fn get_client_ip() -> String {
// The standalone client announces its local IP with port 0 because it is not opening a server port. // Port 0 identifies an RPC client. The server derives the client's
let (_port, _ip, real_ip) = get_ip_and_port().await; // actual IPv4 or IPv6 address from the accepted socket.
let parts: Vec<&str> = real_ip.split(':').collect(); "0.0.0.0:0".to_string()
let ip = parts.first().copied().unwrap_or("127.0.0.1");
format!("{ip}:0")
} }
async fn perform_handshake( async fn perform_handshake(

View File

@ -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::log::{error, info, warn};
use crate::miner::flag::{ use crate::miner::flag::{
clear_mining_stop_request, is_normal_mode, set_mining_state, set_node_mode, MiningState, clear_mining_stop_request, is_normal_mode, set_mining_state, set_node_mode, MiningState,
@ -55,7 +55,7 @@ async fn attempt_bootstrap_connections(
) -> Result<bool, String> { ) -> Result<bool, String> {
// Try the configured bootstrap peers one by one until a // Try the configured bootstrap peers one by one until a
// handshake succeeds or the list is exhausted. // 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<String> = None; let mut last_error: Option<String> = None;
for server in filtered_servers { for server in filtered_servers {