[ping] Refactor the ping protocol for conformity. (#1692)

* Refactor the ping protocol.

Such that pings are sent over a single substream, as it is
done in other libp2p implementations. Note that, since each
peer sends its pings over a single, dedicated substream,
every peer that participates in the protocol has effectively
two open substreams.

* Cleanup

* Update ping changelog.

* Update protocols/ping/src/protocol.rs

Co-authored-by: Max Inden <mail@max-inden.de>

Co-authored-by: Max Inden <mail@max-inden.de>
This commit is contained in:
Roman Borschel
2020-08-10 12:54:55 +02:00
committed by GitHub
parent 5cd981b875
commit b595972961
5 changed files with 353 additions and 221 deletions

View File

@ -18,23 +18,27 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use futures::{future::BoxFuture, prelude::*};
use futures::prelude::*;
use libp2p_core::{InboundUpgrade, OutboundUpgrade, UpgradeInfo};
use log::debug;
use libp2p_swarm::NegotiatedSubstream;
use rand::{distributions, prelude::*};
use std::{io, iter, time::Duration};
use void::Void;
use wasm_timer::Instant;
/// Represents a prototype for an upgrade to handle the ping protocol.
/// The `Ping` protocol upgrade.
///
/// The protocol works the following way:
/// The ping protocol sends 32 bytes of random data in configurable
/// intervals over a single outbound substream, expecting to receive
/// the same bytes as a response. At the same time, incoming pings
/// on inbound substreams are answered by sending back the received bytes.
///
/// - Dialer sends 32 bytes of random data.
/// - Listener receives the data and sends it back.
/// - Dialer receives the data and verifies that it matches what it sent.
/// At most a single inbound and outbound substream is kept open at
/// any time. In case of a ping timeout or another error on a substream, the
/// substream is dropped. If a configurable number of consecutive
/// outbound pings fail, the connection is closed.
///
/// The dialer produces a `Duration`, which corresponds to the round-trip time
/// of the payload.
/// Successful pings report the round-trip time.
///
/// > **Note**: The round-trip time of a ping may be subject to delays induced
/// > by the underlying transport, e.g. in the case of TCP there is
@ -55,59 +59,60 @@ impl UpgradeInfo for Ping {
}
}
impl<TSocket> InboundUpgrade<TSocket> for Ping
where
TSocket: AsyncRead + AsyncWrite + Send + Unpin + 'static,
{
type Output = ();
type Error = io::Error;
type Future = BoxFuture<'static, Result<(), io::Error>>;
impl InboundUpgrade<NegotiatedSubstream> for Ping {
type Output = NegotiatedSubstream;
type Error = Void;
type Future = future::Ready<Result<Self::Output, Self::Error>>;
fn upgrade_inbound(self, mut socket: TSocket, _: Self::Info) -> Self::Future {
async move {
let mut payload = [0u8; PING_SIZE];
while let Ok(_) = socket.read_exact(&mut payload).await {
socket.write_all(&payload).await?;
}
socket.close().await?;
Ok(())
}.boxed()
fn upgrade_inbound(self, stream: NegotiatedSubstream, _: Self::Info) -> Self::Future {
future::ok(stream)
}
}
impl<TSocket> OutboundUpgrade<TSocket> for Ping
where
TSocket: AsyncRead + AsyncWrite + Send + Unpin + 'static,
{
type Output = Duration;
type Error = io::Error;
type Future = BoxFuture<'static, Result<Duration, io::Error>>;
impl OutboundUpgrade<NegotiatedSubstream> for Ping {
type Output = NegotiatedSubstream;
type Error = Void;
type Future = future::Ready<Result<Self::Output, Self::Error>>;
fn upgrade_outbound(self, mut socket: TSocket, _: Self::Info) -> Self::Future {
let payload: [u8; PING_SIZE] = thread_rng().sample(distributions::Standard);
debug!("Preparing ping payload {:?}", payload);
async move {
socket.write_all(&payload).await?;
socket.close().await?;
let started = Instant::now();
let mut recv_payload = [0u8; 32];
socket.read_exact(&mut recv_payload).await?;
if recv_payload == payload {
Ok(started.elapsed())
} else {
Err(io::Error::new(io::ErrorKind::InvalidData, "Ping payload mismatch"))
}
}.boxed()
fn upgrade_outbound(self, stream: NegotiatedSubstream, _: Self::Info) -> Self::Future {
future::ok(stream)
}
}
/// Sends a ping and waits for the pong.
pub async fn send_ping<S>(mut stream: S) -> io::Result<(S, Duration)>
where
S: AsyncRead + AsyncWrite + Unpin
{
let payload: [u8; PING_SIZE] = thread_rng().sample(distributions::Standard);
log::debug!("Preparing ping payload {:?}", payload);
stream.write_all(&payload).await?;
let started = Instant::now();
let mut recv_payload = [0u8; PING_SIZE];
stream.read_exact(&mut recv_payload).await?;
if recv_payload == payload {
Ok((stream, started.elapsed()))
} else {
Err(io::Error::new(io::ErrorKind::InvalidData, "Ping payload mismatch"))
}
}
/// Waits for a ping and sends a pong.
pub async fn recv_ping<S>(mut stream: S) -> io::Result<S>
where
S: AsyncRead + AsyncWrite + Unpin
{
let mut payload = [0u8; PING_SIZE];
stream.read_exact(&mut payload).await?;
stream.write_all(&payload).await?;
stream.flush().await?;
Ok(stream)
}
#[cfg(test)]
mod tests {
use super::Ping;
use futures::prelude::*;
use super::*;
use libp2p_core::{
upgrade,
multiaddr::multiaddr,
transport::{
Transport,
@ -134,12 +139,12 @@ mod tests {
let listener_event = listener.next().await.unwrap();
let (listener_upgrade, _) = listener_event.unwrap().into_upgrade().unwrap();
let conn = listener_upgrade.await.unwrap();
upgrade::apply_inbound(conn, Ping::default()).await.unwrap();
recv_ping(conn).await.unwrap();
});
async_std::task::block_on(async move {
let c = MemoryTransport.dial(listener_addr).unwrap().await.unwrap();
let rtt = upgrade::apply_outbound(c, Ping::default(), upgrade::Version::V1).await.unwrap();
let (_, rtt) = send_ping(c).await.unwrap();
assert!(rtt > Duration::from_secs(0));
});
}