Rust Peer to Peer node
Let's build the foundation of a peer-to-peer network in Rust. We will take an iterative, step-by-step approach, starting with a single peer and gradually expanding its capabilities to form a network, just as the document recommends.
We will follow the primary recommendation of using Rust and the libp2p library, as it is the most flexible and comprehensive choice mentioned in Section 3 of your design document.
Prerequisites
Before we begin, ensure you have the Rust toolchain installed. If not, you can get it from rust-lang.org.
Step 1: The First Peer – Identity and Basic Structure
The design document emphasizes in Section 4 that every node must have a unique identity. In libp2p, this is achieved through a cryptographic keypair, from which a unique PeerId is derived. Let's create our first peer.
1. Create a New Rust Project
Open your terminal and run:
cargo new rust-p2p-node
cd rust-p2p-node2. Add libp2p Dependencies
As recommended, we'll use libp2p. We also need tokio for asynchronous runtime and futures for stream handling. Open your Cargo.toml file and add the following dependencies:
[dependencies]
libp2p = { version = "0.53.2", features = ["tokio", "gossipsub", "mdns", "kad", "noise", "tcp", "yamux", "identify"] }
tokio = { version = "1", features = ["full"] }
futures = "0.3.30"Note: We are enabling several
libp2pfeatures upfront (gossipsub,kad, etc.) that we will use in later steps, aligning with the architectural design for chat and discovery.
3. Create the Peer's Identity and Core Network Component (Swarm)
Now, let's write the initial code. This program will generate a new identity for our peer, print its unique PeerId, and set up the main networking component called a Swarm. The Swarm is responsible for managing connections and driving network events.
Replace the content of src/main.rs with the following:
use futures::StreamExt;
use libp2p::{
identity,
swarm::{SwarmBuilder, SwarmEvent},
PeerId, Swarm,
};
// Define a custom network behavior. For now, it will be empty.
// We'll add capabilities like discovery and chat in later steps.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "MyBehaviourEvent")]
struct MyBehaviour {
// We will add fields here later, e.g., for Kademlia and Gossipsub.
}
// Define a placeholder for our custom event enum.
enum MyBehaviourEvent {
// We will define event types here later.
}
#[tokio::main]
async fn main() {
// 1. Create a new identity for the peer.
// As per Section 4, this is a fundamental requirement.
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Our local peer ID is: {}", local_peer_id);
// 2. Build the transport.
// This is the foundation for how peers will communicate.
// We'll use TCP, as it's a standard and reliable choice.
// The design doc also mentions QUIC and WebRTC, which libp2p supports.
let transport = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default())
.upgrade(libp2p::core::upgrade::Version::V1Lazy)
.authenticate(libp2p::noise::Config::new(&local_key).unwrap())
.multiplex(libp2p::yamux::Config::default())
.boxed();
// 3. Create an empty network behaviour.
let behaviour = MyBehaviour {};
// 4. Create the Swarm.
// The Swarm manages all connections and network events for the peer.
let mut swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id).build();
// 5. Tell the Swarm to listen on a random TCP port.
// The "/ip4/0.0.0.0/tcp/0" address means we'll listen on all network interfaces
// on a port assigned by the OS.
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()).unwrap();
// 6. The main event loop.
// The Swarm will emit events that we need to handle.
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => {
println!("Listening on local address: {}", address);
}
// We'll add more event handlers here in the next steps.
event => {
println!("Unhandled swarm event: {:?}", event);
}
}
}
}4. Run Your First Peer
Execute the code in your terminal:
cargo runYou will see output similar to this, showing your peer's unique ID and the address it's listening on:
Our local peer ID is: 12D3KooW...
Listening on local address: /ip4/127.0.0.1/tcp/51234
Listening on local address: /ip4/192.168.1.10/tcp/51234You have now successfully created a single, isolated peer. It has an identity and is listening for incoming connections, but it doesn't know about any other peers yet.
Step 2: Two Peers Talking – Manual Discovery and Connection
To have a network, we need at least two peers. Let's make our application able to connect to another peer if we provide its address. This demonstrates a manual connection before we automate discovery.
1. Modify main.rs to Accept a Peer Address
We'll use command-line arguments to optionally pass the address of a peer to connect to.
Update src/main.rs:
// ... (keep the use statements and struct definitions from before)
#[tokio::main]
async fn main() {
// ... (keep the identity and transport creation code) ...
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Our local peer ID is: {}", local_peer_id);
let transport = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default())
.upgrade(libp2p::core::upgrade::Version::V1Lazy)
.authenticate(libp2p::noise::Config::new(&local_key).unwrap())
.multiplex(libp2p::yamux::Config::default())
.boxed();
let behaviour = MyBehaviour {};
let mut swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id).build();
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()).unwrap();
// New code: Check for a peer address to dial from command line arguments.
if let Some(addr_to_dial) = std::env::args().nth(1) {
let addr: Multiaddr = addr_to_dial.parse().expect("Failed to parse address.");
match swarm.dial(addr.clone()) {
Ok(_) => println!("Dialed peer at {}", addr),
Err(e) => println!("Failed to dial peer at {}: {:?}", addr, e),
}
}
// The main event loop.
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => {
println!("Listening on local address: {}", address);
}
// Add a handler for connection events.
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
println!("Connected to peer: {}", peer_id);
println!("Endpoint: {:?}", endpoint.get_remote_address());
}
SwarmEvent::ConnectionClosed { peer_id, cause, .. } => {
println!("Connection lost with peer: {}. Cause: {:?}", peer_id, cause);
}
// ... (keep the other event handler)
event => {
// println!("Unhandled swarm event: {:?}", event);
}
}
}
}Note: We also need to add
use libp2p::Multiaddr;to theusestatements at the top of the file.
2. Run Two Peers
Now, we'll run two instances of our application.
Terminal 1 (The "Server" Peer): Run the application without any arguments. It will start up and print its listening address.
cargo runNote the listening address it prints, for example:
/ip4/127.0.0.1/tcp/51234.Terminal 2 (The "Client" Peer): Run the application again, but this time, provide the listening address of the first peer as a command-line argument.
# Replace the address with the one from Terminal 1 cargo run -- /ip4/127.0.0.1/tcp/51234
You will see log messages in both terminals indicating that a connection has been established! This is the most basic form of a P2P network.
Step 3: Automated Discovery with Kademlia DHT
Manually providing addresses is not scalable. As your design document outlines in Section 2 and 4, a Distributed Hash Table (DHT) is essential for automated peer discovery. We'll implement Kademlia, which is a strong recommendation.
1. Update the Network Behaviour
First, we need to add the Kademlia protocol to our MyBehaviour struct.
Modify src/main.rs:
use libp2p::{
kad::{Kademlia, KademliaEvent, store::MemoryStore},
// ... other use statements
};
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "MyBehaviourEvent")]
struct MyBehaviour {
// Add the Kademlia behaviour.
kad: Kademlia<MemoryStore>,
// We'll also add Identify to help with NAT traversal later.
identify: libp2p::identify::Behaviour,
}
// Update the event enum to include Kademlia events.
#[derive(Debug)]
enum MyBehaviourEvent {
Kad(KademliaEvent),
Identify(libp2p::identify::Event),
}
// Implement the conversion from the specific event to our umbrella enum.
impl From<KademliaEvent> for MyBehaviourEvent {
fn from(event: KademliaEvent) -> Self {
MyBehaviourEvent::Kad(event)
}
}
impl From<libp2p::identify::Event> for MyBehaviourEvent {
fn from(event: libp2p::identify::Event) -> Self {
MyBehaviourEvent::Identify(event)
}
}
// ... (main function follows)2. Integrate Kademlia into the Swarm
Now, we instantiate Kademlia and add it to the Swarm. We will also implement the "bootstrapping" process described in Section 4. If our peer is given the address of another peer, it will connect and add it to its Kademlia routing table. This "bootstrap node" will then help our peer discover the rest of the network.
Update the main function in src/main.rs:
// ... (keep use statements and struct/enum definitions)
#[tokio::main]
async fn main() {
// ... (keep identity and transport creation code) ...
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("Our local peer ID is: {}", local_peer_id);
let transport = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default())
.upgrade(libp2p::core::upgrade::Version::V1Lazy)
.authenticate(libp2p::noise::Config::new(&local_key).unwrap())
.multiplex(libp2p::yamux::Config::default())
.boxed();
// Create the Kademlia behaviour.
let store = MemoryStore::new(local_peer_id);
let kad_behaviour = Kademlia::new(local_peer_id, store);
// Create the Identify behaviour
let identify_behaviour = libp2p::identify::Behaviour::new(
libp2p::identify::Config::new("p2p-chat/1.0.0".to_string(), local_key.public())
);
// Create our combined behaviour.
let mut behaviour = MyBehaviour {
kad: kad_behaviour,
identify: identify_behaviour,
};
let mut swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id).build();
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()).unwrap();
// Dial the bootstrap node if one is provided.
if let Some(addr_str) = std::env::args().nth(1) {
let addr: Multiaddr = addr_str.parse().expect("Failed to parse address.");
let peer_id_str = std::env::args().nth(2).expect("Please provide a peer ID.");
let peer_id: PeerId = peer_id_str.parse().expect("Failed to parse PeerId.");
swarm.behaviour_mut().kad.add_address(&peer_id, addr.clone());
println!("Added bootstrap peer: {} at {}", peer_id, addr);
}
// Start the discovery process.
swarm.behaviour_mut().kad.bootstrap().ok();
// The main event loop.
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => {
println!("Listening on: {} with PeerId {}", address, local_peer_id);
}
SwarmEvent::Behaviour(MyBehaviourEvent::Kad(event)) => {
println!("Kademlia event: {:?}", event);
}
SwarmEvent::Behaviour(MyBehaviourEvent::Identify(event)) => {
println!("Identify event: {:?}", event);
// When we identify a new peer, add them to Kademlia's routing table.
if let libp2p::identify::Event::Received { peer_id, info } = event {
for addr in info.listen_addrs {
swarm.behaviour_mut().kad.add_address(&peer_id, addr);
}
}
}
// ... (other handlers)
_ => {}
}
}
}3. Run the Discovery Network
Terminal 1 (Bootstrap Node): Start the first peer. Note both its listening address AND its
PeerId.cargo run # Output: Listening on: /ip4/127.0.0.1/tcp/55555 with PeerId 12D3KooW...Terminal 2 (New Peer): Start the second peer, giving it the address and PeerId of the first peer.
# Use the address and PeerId from Terminal 1 cargo run -- /ip4/127.0.0.1/tcp/55555 12D3KooW...
You will now see KademliaEvent logs in both terminals. The new peer has connected to the bootstrap node and is using the Kademlia DHT to learn about the network topology. You have successfully implemented the core of the decentralized discovery system!
Next Steps
Following this foundation, the next logical steps based on your design document would be:
Implement Chat with Gossipsub: Add the
libp2p-gossipsubbehaviour to yourMyBehaviourstruct to enable scalable, topic-based messaging, as recommended for the chat module.Handle NAT Traversal: Integrate
libp2p-relayto allow peers behind restrictive NATs to communicate, implementing the concepts from Section 6 of your document.Build the File-Sharing Module: Design a protocol for advertising and transferring files, potentially using concepts from BitTorrent or IPFS as suggested.
Containerize for Deployment: Create a
Dockerfileto package the application, simplifying deployment on Unix/Linux servers as outlined in Section 8.
This step-by-step guide has established the fundamental components of your P2P network, creating a solid, expandable base upon which you can build the full file, chat, and video sharing application.
Last updated