mirror of
https://github.com/Rucknium/misc-research.git
synced 2025-04-28 07:54:46 +00:00
Add p2p network crawler
This commit is contained in:
parent
7e4a3c9625
commit
dbc1599f40
4 changed files with 1711 additions and 0 deletions
Monero-Peer-Subnet-Deduplication/code/Rust
1473
Monero-Peer-Subnet-Deduplication/code/Rust/Cargo.lock
generated
Normal file
1473
Monero-Peer-Subnet-Deduplication/code/Rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Monero-Peer-Subnet-Deduplication/code/Rust/Cargo.toml
Normal file
16
Monero-Peer-Subnet-Deduplication/code/Rust/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "monero-network-crawler"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] }
|
||||
tower = { git = "https://github.com/Cuprate/tower.git", rev = "6c7faf0", default-features = false } # <https://github.com/tower-rs/tower/pull/796>
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing = "0.1.40"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
cuprate-p2p-core = { git = "https://github.com/Cuprate/cuprate.git", branch = "expose-support-flags" }
|
||||
cuprate-wire = { git = "https://github.com/Cuprate/cuprate.git", branch = "expose-support-flags" }
|
||||
futures = "0.3.31"
|
||||
rand = "0.8"
|
21
Monero-Peer-Subnet-Deduplication/code/Rust/LICENSE.md
Normal file
21
Monero-Peer-Subnet-Deduplication/code/Rust/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Boog900
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
201
Monero-Peer-Subnet-Deduplication/code/Rust/src/main.rs
Normal file
201
Monero-Peer-Subnet-Deduplication/code/Rust/src/main.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
//! # Monero network crawler
|
||||
//!
|
||||
//! A simple tool to find all the reachable nodes on the Monero P2P network. It works by recursively connecting to
|
||||
//! every peer we are told about in a peer list message, starting by connecting to the seed nodes.
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::Infallible,
|
||||
fs::OpenOptions,
|
||||
io::Write,
|
||||
net::SocketAddr,
|
||||
sync::{LazyLock, OnceLock},
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dashmap::DashSet;
|
||||
use futures::{future::BoxFuture, stream, FutureExt};
|
||||
use tokio::{
|
||||
sync::{mpsc, Semaphore},
|
||||
time::timeout,
|
||||
};
|
||||
use tower::{make::Shared, util::MapErr, Service, ServiceExt};
|
||||
use tracing::error;
|
||||
use tracing_subscriber::{filter::LevelFilter, FmtSubscriber};
|
||||
|
||||
use cuprate_p2p_core::{
|
||||
client::{
|
||||
handshaker::builder::{DummyCoreSyncSvc, DummyProtocolRequestHandler},
|
||||
ConnectRequest, Connector, HandshakerBuilder, InternalPeerID,
|
||||
},
|
||||
services::{AddressBookRequest, AddressBookResponse},
|
||||
BroadcastMessage, ClearNet, NetZoneAddress, Network, NetworkZone,
|
||||
};
|
||||
use cuprate_wire::{common::PeerSupportFlags, BasicNodeData};
|
||||
|
||||
/// A set of all node's [`SocketAddr`] that we have successfully connected to.
|
||||
static SCANNED_NODES: LazyLock<DashSet<SocketAddr>> = LazyLock::new(DashSet::new);
|
||||
|
||||
/// The [`Connector`] service to make outbound connections to nodes.
|
||||
static CONNECTOR: OnceLock<
|
||||
Connector<
|
||||
ClearNet,
|
||||
AddressBookService,
|
||||
DummyCoreSyncSvc,
|
||||
MapErr<Shared<DummyProtocolRequestHandler>, fn(Infallible) -> tower::BoxError>,
|
||||
fn(InternalPeerID<<ClearNet as NetworkZone>::Addr>) -> stream::Pending<BroadcastMessage>,
|
||||
>,
|
||||
> = OnceLock::new();
|
||||
|
||||
/// The channel that is used to communicate a successful connection.
|
||||
static BAD_PEERS_CHANNEL: OnceLock<mpsc::Sender<(SocketAddr, bool)>> = OnceLock::new();
|
||||
|
||||
/// A [`Semaphore`] to limit the amount of concurrent connection attempts so we don't overrun ourself.
|
||||
static CONNECTION_SEMAPHORE: Semaphore = Semaphore::const_new(100);
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
FmtSubscriber::builder()
|
||||
.with_max_level(LevelFilter::DEBUG)
|
||||
.init();
|
||||
|
||||
let handshaker = HandshakerBuilder::<ClearNet>::new(BasicNodeData {
|
||||
my_port: 0,
|
||||
network_id: Network::Mainnet.network_id(),
|
||||
peer_id: rand::random(),
|
||||
support_flags: PeerSupportFlags::FLUFFY_BLOCKS,
|
||||
rpc_port: 0,
|
||||
rpc_credits_per_hash: 0,
|
||||
})
|
||||
.with_address_book(AddressBookService)
|
||||
.build();
|
||||
|
||||
// Create and set the CONNECTOR global.
|
||||
let connector = Connector::new(handshaker);
|
||||
let _ = CONNECTOR.set(connector.clone());
|
||||
|
||||
// Create and set the BAD_PEERS_CHANNEL global.
|
||||
let (bad_peers_tx, mut bad_peers_rx) = mpsc::channel(508);
|
||||
BAD_PEERS_CHANNEL.set(bad_peers_tx).unwrap();
|
||||
|
||||
// start by connecting to the seed nodes
|
||||
[
|
||||
"176.9.0.187:18080",
|
||||
"88.198.163.90:18080",
|
||||
"66.85.74.134:18080",
|
||||
"51.79.173.165:18080",
|
||||
"192.99.8.110:18080",
|
||||
"37.187.74.171:18080",
|
||||
"77.172.183.193:18080",
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|ip| {
|
||||
tokio::spawn(check_node(ip.parse().unwrap()));
|
||||
});
|
||||
|
||||
let mut bad_peers = HashSet::new();
|
||||
let mut bad_peers_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("bad_peers.txt")
|
||||
.unwrap();
|
||||
|
||||
let mut good_peers_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("good_peers.txt")
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let (peer, peer_bad) = bad_peers_rx.recv().await.unwrap();
|
||||
|
||||
if peer_bad {
|
||||
error!("Found bad peer: {peer:?}");
|
||||
if !bad_peers.insert(peer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bad_peers_file
|
||||
.write_fmt(format_args!("peer: {peer:?}, \n"))
|
||||
.unwrap();
|
||||
} else {
|
||||
good_peers_file
|
||||
.write_fmt(format_args!("peer: {peer:?}, \n"))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a node is reachable, sending the address down the [`BAD_PEERS_CHANNEL`] if it is.
|
||||
async fn check_node(addr: SocketAddr) -> Result<(), tower::BoxError> {
|
||||
// Acquire a semaphore permit.
|
||||
let _guard = CONNECTION_SEMAPHORE.acquire().await.unwrap();
|
||||
|
||||
// Grab the connector from the `CONNECTOR` global
|
||||
let mut connector = CONNECTOR.get().unwrap().clone();
|
||||
|
||||
// Attempt to connect and handshake, with a timeout.
|
||||
let mut client = timeout(
|
||||
Duration::from_secs(5),
|
||||
connector
|
||||
.ready()
|
||||
.await?
|
||||
.call(ConnectRequest { addr, permit: None }),
|
||||
)
|
||||
.await??;
|
||||
|
||||
// The proxy check has been removed - all peers reachable are good.
|
||||
let bad = false;
|
||||
|
||||
BAD_PEERS_CHANNEL.get().unwrap().send((addr, bad)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An address book service used in the [`CONNECTOR`] that just calls [`check_node`] on each peer in an
|
||||
/// incoming peer list and does not actually track peer addresses.
|
||||
#[derive(Clone)]
|
||||
pub struct AddressBookService;
|
||||
|
||||
impl Service<AddressBookRequest<ClearNet>> for AddressBookService {
|
||||
type Error = tower::BoxError;
|
||||
type Response = AddressBookResponse<ClearNet>;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: AddressBookRequest<ClearNet>) -> Self::Future {
|
||||
async {
|
||||
match req {
|
||||
AddressBookRequest::IncomingPeerList(peers) => {
|
||||
for mut peer in peers {
|
||||
peer.adr.make_canonical();
|
||||
if SCANNED_NODES.insert(peer.adr) {
|
||||
tokio::spawn(async move {
|
||||
if check_node(peer.adr).await.is_err() {
|
||||
SCANNED_NODES.remove(&peer.adr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AddressBookResponse::Ok)
|
||||
}
|
||||
AddressBookRequest::NewConnection { .. } => Ok(AddressBookResponse::Ok),
|
||||
AddressBookRequest::GetWhitePeers(_) => Ok(AddressBookResponse::Peers(vec![])),
|
||||
AddressBookRequest::TakeRandomWhitePeer { .. }
|
||||
| AddressBookRequest::TakeRandomGrayPeer { .. }
|
||||
| AddressBookRequest::TakeRandomPeer { .. }
|
||||
| AddressBookRequest::PeerlistSize
|
||||
| AddressBookRequest::ConnectionCount
|
||||
| AddressBookRequest::SetBan(_)
|
||||
| AddressBookRequest::GetBan(_)
|
||||
| AddressBookRequest::GetBans
|
||||
| AddressBookRequest::ConnectionInfo => Err("no peers".into()),
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue