Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support client mode in kademlia #2184

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f8cf486
Adding config parameter to support client mode
Aug 7, 2021
1a042f8
Merge branch 'master' into kademlia-client-mode
Aug 7, 2021
3e759a0
example for showing peer interaction in kademlia
Aug 8, 2021
b8cbe62
finally able to discover other peers
Aug 8, 2021
fc218a0
Add `Mode` enum for kademlia client/server mode
Aug 10, 2021
e58e544
Remove confirmation code from `inject_fully_negotiated_inbound`
Aug 10, 2021
af1fcbd
Correct `client` value for network behaviour handler
Aug 10, 2021
c944ecb
Merge branch 'kademlia-client-mode' into kademlia-example
Aug 10, 2021
4be8fca
refactoring
Aug 11, 2021
e213350
Refactoring complete
Aug 12, 2021
e47148d
Add example for running kademlia in `client` mode
Aug 15, 2021
cdaa0a1
Merge commit with master
Aug 15, 2021
1a09907
removing .vscode
Aug 15, 2021
46167a1
Add better documentation and comments
Aug 21, 2021
8bf3ef4
replaced example with test in kad-behaviour
Aug 28, 2021
9ef4c4c
Changes to client mode test
Aug 29, 2021
c387f41
remove kad-example from cargo.toml
Aug 29, 2021
5b21774
Better checks for client mode test
Aug 29, 2021
5ea645e
Fix client mode test
Aug 29, 2021
aa6500f
Refactor kademlia client mode test
Aug 30, 2021
146405f
Rename to check variables
Aug 30, 2021
2cea417
Final fix for `client_mode` test.
Aug 30, 2021
54331e5
remove commented code
Aug 30, 2021
9d23741
Merge branch 'master' into kademlia-client-mode
Sep 3, 2021
b202c5b
Add some basic comments.
Sep 7, 2021
8234a17
Changes to test. Currently failing.
Sep 7, 2021
d5e6509
Changes to `client_mode` test. Currently failing.
Sep 7, 2021
06c1a30
Correct variable name.
Sep 10, 2021
d420fa6
Add checks to see if PUT and GET are working.
Sep 10, 2021
190c3dd
Add correct checks for PUT and GET operations.
Sep 10, 2021
ab97b10
Merge branch `master` into `kademlia-client-mode`
Sep 10, 2021
f3259b1
update prost dependencies
Sep 10, 2021
f308b0c
Replace `any` with `all` for server peer check.
Sep 11, 2021
8f33ba1
Merge branch `master` into `kademlia-client-mode`
Sep 14, 2021
520fb8e
Extra comments
Sep 14, 2021
d52c6af
Fix merge conflict with master
Sep 30, 2021
81eae5f
New test
Oct 3, 2021
f0c2fc3
Remove `allow_listening` logic.
Oct 3, 2021
f0bf7df
Fix gitignore
Oct 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,7 @@ members = [
[[example]]
name = "chat-tokio"
required-features = ["tcp-tokio", "mdns"]

[[example]]
name = "kademlia-example"
required-features = ["tcp-tokio", "mdns"]
157 changes: 157 additions & 0 deletions examples/kademlia-example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#![allow(dead_code, unused_variables)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of an example, could you add a test to kad/src/behaviour/tests.rs?

Copy link
Author

@whereistejas whereistejas Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing this test in the kademlia module will be kind of difficult, as I'm using a lot of other things as well, for building the swarm or transport. Is it okay if I put this is in a test directory?

I can further enhance kademlia-example to create four peers and cross-check if the peer in client mode is actually, absent from the other's routing tables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing this test in the kademlia module will be kind of difficult, as I'm using a lot of other things as well, for building the swarm or transport. Is it okay if I put this is in a test directory?

No need for most of it. You can take the manual_bucket_insert and bootstrap test as an example.

I can further enhance kademlia-example to create four peers and cross-check if the peer in client mode is actually, absent from the other's routing tables.

I don't think there is any need for an example. The "cross-check" sounds great, but should really be done in a test instead of an example.


// Use the following command to run:
// cargo run --example kademlia-example --features="tcp-tokio mdns"
// To run in client mode:
// cargo run --example kademlia-example --features="tcp-tokio mdns" --client

// Based on the following example code:
// https://github.com/zupzup/rust-peer-to-peer-example/blob/main/src/main.rs

use libp2p::{
core::upgrade,
identity,
kad::{
protocol::Mode, record::store::MemoryStore, Kademlia, KademliaConfig, KademliaEvent,
QueryResult,
},
mdns::{Mdns, MdnsConfig, MdnsEvent},
mplex,
noise::{Keypair, NoiseConfig, X25519Spec},
swarm::SwarmEvent,
tcp::TokioTcpConfig,
NetworkBehaviour, PeerId, Swarm, Transport,
};

use async_std::io;
use futures::{FutureExt, StreamExt};
use std::env;

#[derive(NetworkBehaviour)]
#[behaviour(event_process = false, out_event = "MyOutEvent")]
struct MyBehaviour {
kademlia: Kademlia<MemoryStore>,
// Using MDNS for peer discovery.
mdns: Mdns,
}

enum MyOutEvent {
Kademlia(KademliaEvent),
Mdns(MdnsEvent),
}

impl From<KademliaEvent> for MyOutEvent {
fn from(event: KademliaEvent) -> Self {
MyOutEvent::Kademlia(event)
}
}

impl From<MdnsEvent> for MyOutEvent {
fn from(event: MdnsEvent) -> Self {
MyOutEvent::Mdns(event)
}
}

#[tokio::main]
async fn main() {
let client_mode: bool = if let Some(kad_mode) = env::args().nth(1) {
kad_mode.starts_with("--client")
} else {
false
};

create_peer(client_mode).await
}

async fn create_peer(client_mode: bool) {
let key = identity::Keypair::generate_ed25519();
let peer_id = PeerId::from_public_key(&key.public());

// Print the current peer ID.
println!("{:?}", peer_id.clone());

let auth_keys = Keypair::<X25519Spec>::new() // create new auth keys
.into_authentic(&key) // sign the keys
.unwrap();

let transport = TokioTcpConfig::new()
.upgrade(upgrade::Version::V1) // upgrade will only show up if you import `Transport`
.authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
.multiplex(mplex::MplexConfig::new())
.boxed();

let mut config = KademliaConfig::default();

let kad_config = if client_mode {
config.set_mode(Mode::Client);
println!("Setting to client mode.");
config
} else {
config
};

let behaviour = MyBehaviour {
kademlia: Kademlia::with_config(
peer_id.clone(),
MemoryStore::new(peer_id.clone()),
kad_config,
),
mdns: Mdns::new(MdnsConfig::default()).await.unwrap(),
};

let mut swarm = Swarm::new(transport, behaviour, peer_id.clone());

swarm
.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap())
.unwrap();

let stdin = io::stdin();
let mut buffer = String::new();

loop {
futures::select! {
line = stdin.read_line(&mut buffer).fuse() => handle_command(buffer.clone(), &mut swarm),
event = swarm.next() => match event.unwrap() {
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on: {}", address),
SwarmEvent::Behaviour(event) => handle_event(event, &mut swarm),
_ => {}
}
}
}
}

fn handle_command(command: String, swarm: &mut Swarm<MyBehaviour>) {
if command.contains("list peer") {
list_peers(swarm);
}
}

fn handle_event(event: MyOutEvent, swarm: &mut Swarm<MyBehaviour>) {
match event {
MyOutEvent::Kademlia(kad_event) => match kad_event {
KademliaEvent::OutboundQueryCompleted { id, result, stats } => todo!(),
_ => {}
},
MyOutEvent::Mdns(mdns_event) => match mdns_event {
MdnsEvent::Discovered(nodes) => {
for (peer_id, multiaddr) in nodes {
swarm
.behaviour_mut()
.kademlia
.add_address(&peer_id, multiaddr);
}
}
_ => {}
},
}
}

fn list_peers(swarm: &mut Swarm<MyBehaviour>) {
for bucket in swarm.behaviour_mut().kademlia.kbuckets() {
if bucket.num_entries() > 0 {
for item in bucket.iter() {
println!("Peer ID: {:?}", item.node.key.preimage());
}
}
}
}
20 changes: 19 additions & 1 deletion protocols/kad/src/behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::handler::{
};
use crate::jobs::*;
use crate::kbucket::{self, Distance, KBucketsTable, NodeStatus};
use crate::protocol::{KadConnectionType, KadPeer, KademliaProtocolConfig};
use crate::protocol::{KadConnectionType, KadPeer, KademliaProtocolConfig, Mode};
use crate::query::{Query, QueryConfig, QueryId, QueryPool, QueryPoolState};
use crate::record::{
self,
Expand Down Expand Up @@ -147,6 +147,8 @@ pub struct KademliaConfig {
connection_idle_timeout: Duration,
kbucket_inserts: KademliaBucketInserts,
caching: KademliaCaching,
// Set the peer mode.
client: Mode,
whereistejas marked this conversation as resolved.
Show resolved Hide resolved
}

/// The configuration for Kademlia "write-back" caching after successful
Expand Down Expand Up @@ -180,6 +182,8 @@ impl Default for KademliaConfig {
connection_idle_timeout: Duration::from_secs(10),
kbucket_inserts: KademliaBucketInserts::OnConnected,
caching: KademliaCaching::Enabled { max_peers: 1 },
// By default, a peer will not be in client mode.
client: Mode::default(),
whereistejas marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down Expand Up @@ -352,6 +356,19 @@ impl KademliaConfig {
self.caching = c;
self
}

/// Enable or disable the `client` mode of a node.
///
/// Client mode is disabled by default.
///
/// In `client` mode, the peer would not be added to any other peers
/// routing table. The client peer can send queries over the Kademlia
/// network, but will not receive any queries from the other peers in
/// the network.
whereistejas marked this conversation as resolved.
Show resolved Hide resolved
pub fn set_mode(&mut self, mode: Mode) -> &mut Self {
self.client = mode;
self
}
}

impl<TStore> Kademlia<TStore>
Expand Down Expand Up @@ -1645,6 +1662,7 @@ where
protocol_config: self.protocol_config.clone(),
allow_listening: true,
idle_timeout: self.connection_idle_timeout,
client: Mode::default(),
})
}

Expand Down
54 changes: 38 additions & 16 deletions protocols/kad/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

use crate::protocol::{
KadInStreamSink, KadOutStreamSink, KadPeer, KadRequestMsg, KadResponseMsg,
KademliaProtocolConfig,
KademliaProtocolConfig, Mode,
};
use crate::record::{self, Record};
use futures::prelude::*;
Expand Down Expand Up @@ -62,10 +62,15 @@ impl<T: Clone + fmt::Debug + Send + 'static> IntoProtocolsHandler for KademliaHa
}

fn inbound_protocol(&self) -> <Self::Handler as ProtocolsHandler>::InboundProtocol {
if self.config.allow_listening {
upgrade::EitherUpgrade::A(self.config.protocol_config.clone())
} else {
upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade)
match self.config.client {
Mode::Client => upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade),
Mode::Server => {
if self.config.allow_listening {
whereistejas marked this conversation as resolved.
Show resolved Hide resolved
upgrade::EitherUpgrade::A(self.config.protocol_config.clone())
} else {
upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade)
}
}
}
}
}
Expand Down Expand Up @@ -123,6 +128,9 @@ pub struct KademliaHandlerConfig {

/// Time after which we close an idle connection.
pub idle_timeout: Duration,

// If Mode::Client, node will act in `client` mode in the Kademlia network.
pub client: Mode,
whereistejas marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mode logic you are introducing in this pull request replaces the KademliaHandlerConfig::allow_listening logic. The latter can thus be replaced in favor of the former 🎉

}

/// State of an active substream, opened either by us or by the remote.
Expand Down Expand Up @@ -480,11 +488,20 @@ where
type InboundOpenInfo = ();

fn listen_protocol(&self) -> SubstreamProtocol<Self::InboundProtocol, Self::InboundOpenInfo> {
if self.config.allow_listening {
SubstreamProtocol::new(self.config.protocol_config.clone(), ())
.map_upgrade(upgrade::EitherUpgrade::A)
} else {
SubstreamProtocol::new(upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade), ())
match self.config.client {
Mode::Server => {
if self.config.allow_listening {
SubstreamProtocol::new(self.config.protocol_config.clone(), ())
.map_upgrade(upgrade::EitherUpgrade::A)
} else {
SubstreamProtocol::new(upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade), ())
}
}
// If we are in client mode, I don't want to advertise Kademlia so that other peers will
// not send me Kademlia requests.
Mode::Client => {
SubstreamProtocol::new(upgrade::EitherUpgrade::B(upgrade::DeniedUpgrade), ())
}
}
}

Expand Down Expand Up @@ -520,12 +537,16 @@ where
self.next_connec_unique_id.0 += 1;
self.substreams
.push(SubstreamState::InWaitingMessage(connec_unique_id, protocol));
if let ProtocolStatus::Unconfirmed = self.protocol_status {
// Upon the first successfully negotiated substream, we know that the
// remote is configured with the same protocol name and we want
// the behaviour to add this peer to the routing table, if possible.
self.protocol_status = ProtocolStatus::Confirmed;
}
// TODO:
// Just because another peer is sending us Kademlia requests, doesn't necessarily
// mean that it will answer the Kademlia requests that we send to it.
// Thus, Commenting this code out for now.
// if let ProtocolStatus::Unconfirmed = self.protocol_status {
// // Upon the first successfully negotiated substream, we know that the
// // remote is configured with the same protocol name and we want
// // the behaviour to add this peer to the routing table, if possible.
// self.protocol_status = ProtocolStatus::Confirmed;
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I am in favor of documenting this, though obviously both the TODO and the commented out code needs to be removed.

}

fn inject_event(&mut self, message: KademliaHandlerIn<TUserData>) {
Expand Down Expand Up @@ -763,6 +784,7 @@ impl Default for KademliaHandlerConfig {
protocol_config: Default::default(),
allow_listening: true,
idle_timeout: Duration::from_secs(10),
client: Mode::default(),
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions protocols/kad/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ pub const DEFAULT_PROTO_NAME: &[u8] = b"/ipfs/kad/1.0.0";
/// The default maximum size for a varint length-delimited packet.
pub const DEFAULT_MAX_PACKET_SIZE: usize = 16 * 1024;

#[derive(Debug, Clone)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[derive(Debug, Clone)]
/// See [`crate::KademliaConfig::set_mode`].
#[derive(Debug, Clone)]

pub enum Mode {
Client,
Server,
}

impl Default for Mode {
fn default() -> Self {
Mode::Server
}
}

/// Status of our connection to a node reported by the Kademlia protocol.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub enum KadConnectionType {
Expand Down