diff --git a/Cargo.toml b/Cargo.toml index e3d17d8029d..673b5eea2e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ default = [ "websocket", "yamux", ] +dcutr = ["libp2p-dcutr"] deflate = ["libp2p-deflate"] dns-async-std = ["libp2p-dns", "libp2p-dns/async-std"] dns-tokio = ["libp2p-dns", "libp2p-dns/tokio"] @@ -48,7 +49,7 @@ noise = ["libp2p-noise"] ping = ["libp2p-ping", "libp2p-metrics/ping"] plaintext = ["libp2p-plaintext"] pnet = ["libp2p-pnet"] -relay = ["libp2p-relay"] +relay = ["libp2p-relay", "libp2p-metrics/relay"] request-response = ["libp2p-request-response"] rendezvous = ["libp2p-rendezvous"] tcp-async-io = ["libp2p-tcp", "libp2p-tcp/async-io"] @@ -72,6 +73,7 @@ futures-timer = "3.0.2" # Explicit dependency to be used in `wasm-bindgen` featu getrandom = "0.2.3" # Explicit dependency to be used in `wasm-bindgen` feature instant = "0.1.11" # Explicit dependency to be used in `wasm-bindgen` feature lazy_static = "1.2" +libp2p-dcutr = { version = "0.1.0", path = "protocols/dcutr", optional = true } libp2p-core = { version = "0.31.0", path = "core", default-features = false } libp2p-floodsub = { version = "0.33.0", path = "protocols/floodsub", optional = true } libp2p-gossipsub = { version = "0.35.0", path = "./protocols/gossipsub", optional = true } @@ -119,6 +121,7 @@ members = [ "misc/peer-id-generator", "muxers/mplex", "muxers/yamux", + "protocols/dcutr", "protocols/floodsub", "protocols/gossipsub", "protocols/rendezvous", diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 67c5ba39f29..1c15de5eae2 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -95,7 +95,43 @@ Introduce `upgrade::read_length_prefixed` and `upgrade::write_length_prefixed`. See [PR 2111](https://github.com/libp2p/rust-libp2p/pull/2111). +- Add support for multistream-select [simultaneous open extension] to assign _initiator_ and + _responder_ role during authentication protocol negotiation on simultaneously opened connection. + + This is one important component of the greater effort to support hole punching in rust-libp2p. + + - `Transport::upgrade` no longer takes a multistream-select `Version`. Instead the + multistream-select `Version`s `V1`, `V1Lazy` and `V1SimultaneousOpen` can be selected when + setting the authentication upgrade via `Builder::authenticate_with_version` and the + multistream-select `Version`s `V1` and `V1Lazy` can be selected when setting the multiplexing + upgrade via `Builder::multiplex_with_version`. + + Users merely wanting to maintain the status quo should use the following call chain depending + on which `Version` they previously used: + + - `Version::V1` + + ```rust + my_transport.upgrade() + .authenticate(my_authentication) + .multiplex(my_multiplexer) + ``` + - `Version::V1Lazy` + + ```rust + my_transport.upgrade() + .authenticate_with_version(my_authentication, Version::V1Lazy) + .multiplex_with_version(my_multiplexer, Version::V1Lazy) + ``` + + - `Builder::multiplex_ext` is removed in favor of the new simultaneous open workflow. Please reach + out in case you depend on `Builder::multiplex_ext`. + + See [PR 2066]. + [PR 2090]: https://github.com/libp2p/rust-libp2p/pull/2090 +[simultaneous open extension]: https://github.com/libp2p/specs/blob/master/connections/simopen.md +[PR 2066]: https://github.com/libp2p/rust-libp2p/pull/2066 # 0.28.3 [2021-04-26] diff --git a/core/src/connection/listeners.rs b/core/src/connection/listeners.rs index 4c394aeb75d..b15bd2573e0 100644 --- a/core/src/connection/listeners.rs +++ b/core/src/connection/listeners.rs @@ -412,8 +412,6 @@ where #[cfg(test)] mod tests { - use futures::{future::BoxFuture, stream::BoxStream}; - use super::*; use crate::transport; @@ -463,12 +461,18 @@ mod tests { impl transport::Transport for DummyTrans { type Output = (); type Error = std::io::Error; - type Listener = BoxStream< - 'static, - Result, std::io::Error>, + type Listener = Pin< + Box< + dyn Stream< + Item = Result< + ListenerEvent, + std::io::Error, + >, + >, + >, >; - type ListenerUpgrade = BoxFuture<'static, Result>; - type Dial = BoxFuture<'static, Result>; + type ListenerUpgrade = Pin>>>; + type Dial = Pin>>>; fn listen_on( self, @@ -519,12 +523,18 @@ mod tests { impl transport::Transport for DummyTrans { type Output = (); type Error = std::io::Error; - type Listener = BoxStream< - 'static, - Result, std::io::Error>, + type Listener = Pin< + Box< + dyn Stream< + Item = Result< + ListenerEvent, + std::io::Error, + >, + >, + >, >; - type ListenerUpgrade = BoxFuture<'static, Result>; - type Dial = BoxFuture<'static, Result>; + type ListenerUpgrade = Pin>>>; + type Dial = Pin>>>; fn listen_on( self, diff --git a/core/src/transport.rs b/core/src/transport.rs index 7006c15a810..ade8daf6a6c 100644 --- a/core/src/transport.rs +++ b/core/src/transport.rs @@ -46,7 +46,6 @@ pub use self::boxed::Boxed; pub use self::choice::OrTransport; pub use self::memory::MemoryTransport; pub use self::optional::OptionalTransport; -pub use self::upgrade::Upgrade; /// A transport provides connection-oriented communication between two peers /// through ordered streams of data (i.e. connections). @@ -198,12 +197,12 @@ pub trait Transport { /// Begins a series of protocol upgrades via an /// [`upgrade::Builder`](upgrade::Builder). - fn upgrade(self, version: upgrade::Version) -> upgrade::Builder + fn upgrade(self) -> upgrade::Builder where Self: Sized, Self::Error: 'static, { - upgrade::Builder::new(self, version) + upgrade::Builder::new(self) } } diff --git a/core/src/transport/upgrade.rs b/core/src/transport/upgrade.rs index 7777be9256e..4a0d50392ff 100644 --- a/core/src/transport/upgrade.rs +++ b/core/src/transport/upgrade.rs @@ -20,25 +20,21 @@ //! Configuration of transport protocol upgrades. -pub use crate::upgrade::Version; - use crate::{ muxing::{StreamMuxer, StreamMuxerBox}, transport::{ - and_then::AndThen, boxed::boxed, timeout::TransportTimeout, ListenerEvent, Transport, - TransportError, + and_then::AndThen, boxed::boxed, timeout::TransportTimeout, Transport, TransportError, }, upgrade::{ - self, apply_inbound, apply_outbound, InboundUpgrade, InboundUpgradeApply, OutboundUpgrade, - OutboundUpgradeApply, UpgradeError, + self, AuthenticationUpgradeApply, AuthenticationVersion, InboundUpgrade, + InboundUpgradeApply, OutboundUpgrade, OutboundUpgradeApply, Role, UpgradeError, Version, }, ConnectedPoint, Negotiated, PeerId, }; -use futures::{prelude::*, ready}; +use futures::{future::Either, prelude::*, ready}; use multiaddr::Multiaddr; use std::{ error::Error, - fmt, pin::Pin, task::{Context, Poll}, time::Duration, @@ -68,7 +64,6 @@ use std::{ #[derive(Clone)] pub struct Builder { inner: T, - version: upgrade::Version, } impl Builder @@ -77,8 +72,8 @@ where T::Error: 'static, { /// Creates a `Builder` over the given (base) `Transport`. - pub fn new(inner: T, version: upgrade::Version) -> Builder { - Builder { inner, version } + pub fn new(inner: T) -> Builder { + Builder { inner } } /// Upgrades the transport to perform authentication of the remote. @@ -96,7 +91,9 @@ where pub fn authenticate( self, upgrade: U, - ) -> Authenticated Authenticate + Clone>> + ) -> Authenticated< + AndThen AuthenticationUpgradeApply + Clone>, + > where T: Transport, C: AsyncRead + AsyncWrite + Unpin, @@ -105,82 +102,29 @@ where U: OutboundUpgrade, Output = (PeerId, D), Error = E> + Clone, E: Error + 'static, { - let version = self.version; - Authenticated(Builder::new( - self.inner.and_then(move |conn, endpoint| Authenticate { - inner: upgrade::apply(conn, upgrade, endpoint, version), - }), - version, - )) - } -} - -/// An upgrade that authenticates the remote peer, typically -/// in the context of negotiating a secure channel. -/// -/// Configured through [`Builder::authenticate`]. -#[pin_project::pin_project] -pub struct Authenticate -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade> + OutboundUpgrade>, -{ - #[pin] - inner: EitherUpgrade, -} - -impl Future for Authenticate -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade> - + OutboundUpgrade< - Negotiated, - Output = >>::Output, - Error = >>::Error, - >, -{ - type Output = as Future>::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - Future::poll(this.inner, cx) + self.authenticate_with_version(upgrade, AuthenticationVersion::default()) } -} -/// An upgrade that negotiates a (sub)stream multiplexer on -/// top of an authenticated transport. -/// -/// Configured through [`Authenticated::multiplex`]. -#[pin_project::pin_project] -pub struct Multiplex -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade> + OutboundUpgrade>, -{ - peer_id: Option, - #[pin] - upgrade: EitherUpgrade, -} - -impl Future for Multiplex -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade, Output = M, Error = E>, - U: OutboundUpgrade, Output = M, Error = E>, -{ - type Output = Result<(PeerId, M), UpgradeError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - let m = match ready!(Future::poll(this.upgrade, cx)) { - Ok(m) => m, - Err(err) => return Poll::Ready(Err(err)), - }; - let i = this - .peer_id - .take() - .expect("Multiplex future polled after completion."); - Poll::Ready(Ok((i, m))) + /// Same as [`Builder::authenticate`] with the option to choose the + /// [`AuthenticationVersion`] used to upgrade the connection. + pub fn authenticate_with_version( + self, + upgrade: U, + version: AuthenticationVersion, + ) -> Authenticated< + AndThen AuthenticationUpgradeApply + Clone>, + > + where + T: Transport, + C: AsyncRead + AsyncWrite + Unpin, + D: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade, Output = (PeerId, D), Error = E>, + U: OutboundUpgrade, Output = (PeerId, D), Error = E> + Clone, + E: Error + 'static, + { + Authenticated(Builder::new(self.inner.and_then(move |conn, endpoint| { + upgrade::apply_authentication(conn, upgrade, endpoint, version) + }))) } } @@ -203,19 +147,66 @@ where /// /// * I/O upgrade: `C -> D`. /// * Transport output: `(PeerId, C) -> (PeerId, D)`. - pub fn apply(self, upgrade: U) -> Authenticated> + pub fn apply( + self, + upgrade: U, + ) -> Authenticated< + AndThen< + T, + impl FnOnce( + ((PeerId, Role), C), + ConnectedPoint, + ) -> UpgradeAuthenticated + + Clone, + >, + > + where + T: Transport, + C: AsyncRead + AsyncWrite + Unpin, + D: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade, Output = D, Error = E>, + U: OutboundUpgrade, Output = D, Error = E> + Clone, + E: Error + 'static, + { + self.apply_with_version(upgrade, Version::default()) + } + + /// Same as [`Authenticated::apply`] with the option to choose the + /// [`Version`] used to upgrade the connection. + pub fn apply_with_version( + self, + upgrade: U, + version: Version, + ) -> Authenticated< + AndThen< + T, + impl FnOnce( + ((PeerId, Role), C), + ConnectedPoint, + ) -> UpgradeAuthenticated + + Clone, + >, + > where - T: Transport, + T: Transport, C: AsyncRead + AsyncWrite + Unpin, D: AsyncRead + AsyncWrite + Unpin, U: InboundUpgrade, Output = D, Error = E>, U: OutboundUpgrade, Output = D, Error = E> + Clone, E: Error + 'static, { - Authenticated(Builder::new( - Upgrade::new(self.0.inner, upgrade), - self.0.version, - )) + Authenticated(Builder::new(self.0.inner.and_then( + move |((i, r), c), _endpoint| { + let upgrade = match r { + Role::Initiator => Either::Left(upgrade::apply_outbound(c, upgrade, version)), + Role::Responder => Either::Right(upgrade::apply_inbound(c, upgrade)), + }; + UpgradeAuthenticated { + user_data: Some((i, r)), + upgrade, + } + }, + ))) } /// Upgrades the transport with a (sub)stream multiplexer. @@ -231,60 +222,95 @@ where pub fn multiplex( self, upgrade: U, - ) -> Multiplexed Multiplex + Clone>> + ) -> Multiplexed< + AndThen< + T, + impl FnOnce(((PeerId, Role), C), ConnectedPoint) -> UpgradeAuthenticated + + Clone, + >, + > where - T: Transport, + T: Transport, C: AsyncRead + AsyncWrite + Unpin, M: StreamMuxer, U: InboundUpgrade, Output = M, Error = E>, U: OutboundUpgrade, Output = M, Error = E> + Clone, E: Error + 'static, { - let version = self.0.version; - Multiplexed(self.0.inner.and_then(move |(i, c), endpoint| { - let upgrade = upgrade::apply(c, upgrade, endpoint, version); - Multiplex { - peer_id: Some(i), - upgrade, - } - })) + self.multiplex_with_version(upgrade, Version::default()) } - /// Like [`Authenticated::multiplex`] but accepts a function which returns the upgrade. - /// - /// The supplied function is applied to [`PeerId`] and [`ConnectedPoint`] - /// and returns an upgrade which receives the I/O resource `C` and must - /// produce a [`StreamMuxer`] `M`. The transport must already be authenticated. - /// This ends the (regular) transport upgrade process. - /// - /// ## Transitions - /// - /// * I/O upgrade: `C -> M`. - /// * Transport output: `(PeerId, C) -> (PeerId, M)`. - pub fn multiplex_ext( + /// Same as [`Authenticated::multiplex`] with the option to choose the + /// [`Version`] used to upgrade the connection. + pub fn multiplex_with_version( self, - up: F, - ) -> Multiplexed Multiplex + Clone>> + upgrade: U, + version: Version, + ) -> Multiplexed< + AndThen< + T, + impl FnOnce(((PeerId, Role), C), ConnectedPoint) -> UpgradeAuthenticated + + Clone, + >, + > where - T: Transport, + T: Transport, C: AsyncRead + AsyncWrite + Unpin, M: StreamMuxer, U: InboundUpgrade, Output = M, Error = E>, U: OutboundUpgrade, Output = M, Error = E> + Clone, E: Error + 'static, - F: for<'a> FnOnce(&'a PeerId, &'a ConnectedPoint) -> U + Clone, { - let version = self.0.version; - Multiplexed(self.0.inner.and_then(move |(peer_id, c), endpoint| { - let upgrade = upgrade::apply(c, up(&peer_id, &endpoint), endpoint, version); - Multiplex { - peer_id: Some(peer_id), + Multiplexed(self.0.inner.and_then(move |((i, r), c), _endpoint| { + let upgrade = match r { + Role::Initiator => Either::Left(upgrade::apply_outbound(c, upgrade, version)), + Role::Responder => Either::Right(upgrade::apply_inbound(c, upgrade)), + }; + UpgradeAuthenticated { + user_data: Some(i), upgrade, } })) } } +/// An upgrade that negotiates a (sub)stream multiplexer on +/// top of an authenticated transport. +/// +/// Configured through [`Authenticated::multiplex`]. +#[pin_project::pin_project] +pub struct UpgradeAuthenticated +where + C: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade> + OutboundUpgrade>, +{ + user_data: Option, + #[pin] + upgrade: EitherUpgrade, +} + +impl Future for UpgradeAuthenticated +where + C: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade, Output = M, Error = E>, + U: OutboundUpgrade, Output = M, Error = E>, +{ + type Output = Result<(D, M), UpgradeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let m = match ready!(Future::poll(this.upgrade, cx)) { + Ok(m) => m, + Err(err) => return Poll::Ready(Err(err)), + }; + let user_data = this + .user_data + .take() + .expect("UpgradeAuthenticated future polled after completion."); + Poll::Ready(Ok((user_data, m))) + } +} + /// A authenticated and multiplexed transport, obtained from /// [`Authenticated::multiplex`]. #[derive(Clone)] @@ -350,257 +376,4 @@ where } /// An inbound or outbound upgrade. -type EitherUpgrade = future::Either, OutboundUpgradeApply>; - -/// A custom upgrade on an [`Authenticated`] transport. -/// -/// See [`Transport::upgrade`] -#[derive(Debug, Copy, Clone)] -pub struct Upgrade { - inner: T, - upgrade: U, -} - -impl Upgrade { - pub fn new(inner: T, upgrade: U) -> Self { - Upgrade { inner, upgrade } - } -} - -impl Transport for Upgrade -where - T: Transport, - T::Error: 'static, - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade, Output = D, Error = E>, - U: OutboundUpgrade, Output = D, Error = E> + Clone, - E: Error + 'static, -{ - type Output = (PeerId, D); - type Error = TransportUpgradeError; - type Listener = ListenerStream; - type ListenerUpgrade = ListenerUpgradeFuture; - type Dial = DialUpgradeFuture; - - fn dial(self, addr: Multiaddr) -> Result> { - let future = self - .inner - .dial(addr) - .map_err(|err| err.map(TransportUpgradeError::Transport))?; - Ok(DialUpgradeFuture { - future: Box::pin(future), - upgrade: future::Either::Left(Some(self.upgrade)), - }) - } - - fn listen_on(self, addr: Multiaddr) -> Result> { - let stream = self - .inner - .listen_on(addr) - .map_err(|err| err.map(TransportUpgradeError::Transport))?; - Ok(ListenerStream { - stream: Box::pin(stream), - upgrade: self.upgrade, - }) - } - - fn address_translation(&self, server: &Multiaddr, observed: &Multiaddr) -> Option { - self.inner.address_translation(server, observed) - } -} - -/// Errors produced by a transport upgrade. -#[derive(Debug)] -pub enum TransportUpgradeError { - /// Error in the transport. - Transport(T), - /// Error while upgrading to a protocol. - Upgrade(UpgradeError), -} - -impl fmt::Display for TransportUpgradeError -where - T: fmt::Display, - U: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TransportUpgradeError::Transport(e) => write!(f, "Transport error: {}", e), - TransportUpgradeError::Upgrade(e) => write!(f, "Upgrade error: {}", e), - } - } -} - -impl Error for TransportUpgradeError -where - T: Error + 'static, - U: Error + 'static, -{ - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - TransportUpgradeError::Transport(e) => Some(e), - TransportUpgradeError::Upgrade(e) => Some(e), - } - } -} - -/// The [`Transport::Dial`] future of an [`Upgrade`]d transport. -pub struct DialUpgradeFuture -where - U: OutboundUpgrade>, - C: AsyncRead + AsyncWrite + Unpin, -{ - future: Pin>, - upgrade: future::Either, (Option, OutboundUpgradeApply)>, -} - -impl Future for DialUpgradeFuture -where - F: TryFuture, - C: AsyncRead + AsyncWrite + Unpin, - U: OutboundUpgrade, Output = D>, - U::Error: Error, -{ - type Output = Result<(PeerId, D), TransportUpgradeError>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // We use a `this` variable because the compiler can't mutably borrow multiple times - // accross a `Deref`. - let this = &mut *self; - - loop { - this.upgrade = match this.upgrade { - future::Either::Left(ref mut up) => { - let (i, c) = match ready!(TryFuture::try_poll(this.future.as_mut(), cx) - .map_err(TransportUpgradeError::Transport)) - { - Ok(v) => v, - Err(err) => return Poll::Ready(Err(err)), - }; - let u = up - .take() - .expect("DialUpgradeFuture is constructed with Either::Left(Some)."); - future::Either::Right((Some(i), apply_outbound(c, u, upgrade::Version::V1))) - } - future::Either::Right((ref mut i, ref mut up)) => { - let d = match ready!( - Future::poll(Pin::new(up), cx).map_err(TransportUpgradeError::Upgrade) - ) { - Ok(d) => d, - Err(err) => return Poll::Ready(Err(err)), - }; - let i = i - .take() - .expect("DialUpgradeFuture polled after completion."); - return Poll::Ready(Ok((i, d))); - } - } - } - } -} - -impl Unpin for DialUpgradeFuture -where - U: OutboundUpgrade>, - C: AsyncRead + AsyncWrite + Unpin, -{ -} - -/// The [`Transport::Listener`] stream of an [`Upgrade`]d transport. -pub struct ListenerStream { - stream: Pin>, - upgrade: U, -} - -impl Stream for ListenerStream -where - S: TryStream, Error = E>, - F: TryFuture, - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade, Output = D> + Clone, -{ - type Item = Result< - ListenerEvent, TransportUpgradeError>, - TransportUpgradeError, - >; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match ready!(TryStream::try_poll_next(self.stream.as_mut(), cx)) { - Some(Ok(event)) => { - let event = event - .map(move |future| ListenerUpgradeFuture { - future: Box::pin(future), - upgrade: future::Either::Left(Some(self.upgrade.clone())), - }) - .map_err(TransportUpgradeError::Transport); - Poll::Ready(Some(Ok(event))) - } - Some(Err(err)) => Poll::Ready(Some(Err(TransportUpgradeError::Transport(err)))), - None => Poll::Ready(None), - } - } -} - -impl Unpin for ListenerStream {} - -/// The [`Transport::ListenerUpgrade`] future of an [`Upgrade`]d transport. -pub struct ListenerUpgradeFuture -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade>, -{ - future: Pin>, - upgrade: future::Either, (Option, InboundUpgradeApply)>, -} - -impl Future for ListenerUpgradeFuture -where - F: TryFuture, - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade, Output = D>, - U::Error: Error, -{ - type Output = Result<(PeerId, D), TransportUpgradeError>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // We use a `this` variable because the compiler can't mutably borrow multiple times - // accross a `Deref`. - let this = &mut *self; - - loop { - this.upgrade = match this.upgrade { - future::Either::Left(ref mut up) => { - let (i, c) = match ready!(TryFuture::try_poll(this.future.as_mut(), cx) - .map_err(TransportUpgradeError::Transport)) - { - Ok(v) => v, - Err(err) => return Poll::Ready(Err(err)), - }; - let u = up - .take() - .expect("ListenerUpgradeFuture is constructed with Either::Left(Some)."); - future::Either::Right((Some(i), apply_inbound(c, u))) - } - future::Either::Right((ref mut i, ref mut up)) => { - let d = match ready!(TryFuture::try_poll(Pin::new(up), cx) - .map_err(TransportUpgradeError::Upgrade)) - { - Ok(v) => v, - Err(err) => return Poll::Ready(Err(err)), - }; - let i = i - .take() - .expect("ListenerUpgradeFuture polled after completion."); - return Poll::Ready(Ok((i, d))); - } - } - } - } -} - -impl Unpin for ListenerUpgradeFuture -where - C: AsyncRead + AsyncWrite + Unpin, - U: InboundUpgrade>, -{ -} +type EitherUpgrade = future::Either, InboundUpgradeApply>; diff --git a/core/src/upgrade.rs b/core/src/upgrade.rs index d9edd6492bc..2dad9419418 100644 --- a/core/src/upgrade.rs +++ b/core/src/upgrade.rs @@ -70,7 +70,10 @@ mod transfer; use futures::future::Future; pub use self::{ - apply::{apply, apply_inbound, apply_outbound, InboundUpgradeApply, OutboundUpgradeApply}, + apply::{ + apply, apply_authentication, apply_inbound, apply_outbound, AuthenticationUpgradeApply, + AuthenticationVersion, InboundUpgradeApply, OutboundUpgradeApply, Version, + }, denied::DeniedUpgrade, either::EitherUpgrade, error::UpgradeError, @@ -81,7 +84,7 @@ pub use self::{ transfer::{read_length_prefixed, read_varint, write_length_prefixed, write_varint}, }; pub use crate::Negotiated; -pub use multistream_select::{NegotiatedComplete, NegotiationError, ProtocolError, Version}; +pub use multistream_select::{NegotiatedComplete, NegotiationError, ProtocolError, Role}; /// Types serving as protocol names. /// diff --git a/core/src/upgrade/apply.rs b/core/src/upgrade/apply.rs index 3b4763d2303..b63450468ae 100644 --- a/core/src/upgrade/apply.rs +++ b/core/src/upgrade/apply.rs @@ -19,15 +19,53 @@ // DEALINGS IN THE SOFTWARE. use crate::upgrade::{InboundUpgrade, OutboundUpgrade, ProtocolName, UpgradeError}; -use crate::{ConnectedPoint, Negotiated}; -use futures::{future::Either, prelude::*}; +use crate::{ConnectedPoint, Negotiated, PeerId}; +use futures::{ + future::{Either, MapOk, TryFutureExt}, + prelude::*, +}; use log::debug; use multistream_select::{self, DialerSelectFuture, ListenerSelectFuture}; use std::{iter, mem, pin::Pin, task::Context, task::Poll}; -pub use multistream_select::Version; +pub use multistream_select::{NegotiationError, Role}; + +/// Wrapper around multistream-select `Version`. +/// +/// See [`multistream_select::Version`] for details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Version { + /// See [`multistream_select::Version::V1`]. + V1, + /// See [`multistream_select::Version::V1Lazy`]. + V1Lazy, +} + +impl From for multistream_select::Version { + fn from(v: Version) -> Self { + match v { + Version::V1 => multistream_select::Version::V1, + Version::V1Lazy => multistream_select::Version::V1Lazy, + } + } +} + +impl Default for Version { + fn default() -> Self { + match multistream_select::Version::default() { + multistream_select::Version::V1 => Version::V1, + multistream_select::Version::V1Lazy => Version::V1Lazy, + multistream_select::Version::V1SimultaneousOpen => { + unreachable!("see `v1_sim_open_is_not_default`") + } + } + } +} /// Applies an upgrade to the inbound and outbound direction of a connection or substream. +/// +/// Note: Use [`apply_authentication`] when negotiating an authentication protocol on top of a +/// transport allowing simultaneously opened connections. pub fn apply( conn: C, up: U, @@ -74,7 +112,7 @@ where .protocol_info() .into_iter() .map(NameWrap as fn(_) -> NameWrap<_>); - let future = multistream_select::dialer_select_proto(conn, iter, v); + let future = multistream_select::dialer_select_proto(conn, iter, v.into()); OutboundUpgradeApply { inner: OutboundUpgradeApplyState::Init { future, @@ -208,13 +246,18 @@ where mut future, upgrade, } => { - let (info, connection) = match Future::poll(Pin::new(&mut future), cx)? { + let (info, connection, role) = match Future::poll(Pin::new(&mut future), cx)? { Poll::Ready(x) => x, Poll::Pending => { self.inner = OutboundUpgradeApplyState::Init { future, upgrade }; return Poll::Pending; } }; + assert_eq!( + role, Role::Initiator, + "Expect negotiation not using `Version::V1SimultaneousOpen` to either return \ + as `Initiator` or fail.", + ); self.inner = OutboundUpgradeApplyState::Upgrade { future: Box::pin(upgrade.upgrade_outbound(connection, info.0)), }; @@ -243,14 +286,214 @@ where } } -type NameWrapIter = iter::Map::Item) -> NameWrap<::Item>>; +/// Wrapper around multistream-select `Version`. +/// +/// See [`multistream_select::Version`] for details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AuthenticationVersion { + /// See [`multistream_select::Version::V1`]. + V1, + /// See [`multistream_select::Version::V1Lazy`]. + V1Lazy, + /// See [`multistream_select::Version::V1SimultaneousOpen`]. + V1SimultaneousOpen, +} + +impl Default for AuthenticationVersion { + fn default() -> Self { + match multistream_select::Version::default() { + multistream_select::Version::V1 => AuthenticationVersion::V1, + multistream_select::Version::V1Lazy => AuthenticationVersion::V1Lazy, + multistream_select::Version::V1SimultaneousOpen => { + AuthenticationVersion::V1SimultaneousOpen + } + } + } +} + +impl From for multistream_select::Version { + fn from(v: AuthenticationVersion) -> Self { + match v { + AuthenticationVersion::V1 => multistream_select::Version::V1, + AuthenticationVersion::V1Lazy => multistream_select::Version::V1Lazy, + AuthenticationVersion::V1SimultaneousOpen => { + multistream_select::Version::V1SimultaneousOpen + } + } + } +} + +/// Applies an authentication upgrade to the inbound or outbound direction of a connection. +/// +/// Note: This is like [`apply`] with additional support for transports allowing simultaneously +/// opened connections. Unless run on such transport and used to negotiate the authentication +/// protocol you likely want to use [`apply`] instead of [`apply_authentication`]. +pub fn apply_authentication( + conn: C, + up: U, + cp: ConnectedPoint, + v: AuthenticationVersion, +) -> AuthenticationUpgradeApply +where + C: AsyncRead + AsyncWrite + Unpin, + D: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade, Output = (PeerId, D)>, + U: OutboundUpgrade< + Negotiated, + Output = (PeerId, D), + Error = >>::Error, + > + Clone, +{ + fn add_responder(input: (P, C)) -> (P, C, Role) { + (input.0, input.1, Role::Responder) + } + + let iter = up + .protocol_info() + .into_iter() + .map(NameWrap as fn(_) -> NameWrap<_>); + + AuthenticationUpgradeApply { + inner: AuthenticationUpgradeApplyState::Init { + future: match cp { + ConnectedPoint::Dialer { .. } => Either::Left( + multistream_select::dialer_select_proto(conn, iter, v.into()), + ), + ConnectedPoint::Listener { .. } => Either::Right( + multistream_select::listener_select_proto(conn, iter) + .map_ok(add_responder as fn(_) -> _), + ), + }, + upgrade: up, + }, + } +} + +pub struct AuthenticationUpgradeApply +where + U: InboundUpgrade> + OutboundUpgrade>, +{ + inner: AuthenticationUpgradeApplyState, +} + +impl Unpin for AuthenticationUpgradeApply +where + C: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade> + OutboundUpgrade>, +{ +} + +enum AuthenticationUpgradeApplyState +where + U: InboundUpgrade> + OutboundUpgrade>, +{ + Init { + future: Either< + multistream_select::DialerSelectFuture< + C, + NameWrapIter<::IntoIter>, + >, + MapOk< + ListenerSelectFuture>, + fn((NameWrap, Negotiated)) -> (NameWrap, Negotiated, Role), + >, + >, + upgrade: U, + }, + Upgrade { + role: Role, + future: Either< + Pin>>::Future>>, + Pin>>::Future>>, + >, + }, + Undefined, +} + +impl Future for AuthenticationUpgradeApply +where + C: AsyncRead + AsyncWrite + Unpin, + D: AsyncRead + AsyncWrite + Unpin, + U: InboundUpgrade, Output = (PeerId, D)>, + U: OutboundUpgrade< + Negotiated, + Output = (PeerId, D), + Error = >>::Error, + > + Clone, +{ + type Output = + Result<((PeerId, Role), D), UpgradeError<>>::Error>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + match mem::replace(&mut self.inner, AuthenticationUpgradeApplyState::Undefined) { + AuthenticationUpgradeApplyState::Init { + mut future, + upgrade, + } => { + let (info, io, role) = match Future::poll(Pin::new(&mut future), cx)? { + Poll::Ready(x) => x, + Poll::Pending => { + self.inner = AuthenticationUpgradeApplyState::Init { future, upgrade }; + return Poll::Pending; + } + }; + let fut = match role { + Role::Initiator => { + Either::Left(Box::pin(upgrade.upgrade_outbound(io, info.0))) + } + Role::Responder => { + Either::Right(Box::pin(upgrade.upgrade_inbound(io, info.0))) + } + }; + self.inner = AuthenticationUpgradeApplyState::Upgrade { future: fut, role }; + } + AuthenticationUpgradeApplyState::Upgrade { mut future, role } => { + match Future::poll(Pin::new(&mut future), cx) { + Poll::Pending => { + self.inner = AuthenticationUpgradeApplyState::Upgrade { future, role }; + return Poll::Pending; + } + Poll::Ready(Ok((peer_id, d))) => { + debug!("Successfully applied negotiated protocol"); + return Poll::Ready(Ok(((peer_id, role), d))); + } + Poll::Ready(Err(e)) => { + debug!("Failed to apply negotiated protocol"); + return Poll::Ready(Err(UpgradeError::Apply(e))); + } + } + } + AuthenticationUpgradeApplyState::Undefined => { + panic!("AuthenticationUpgradeApplyState::poll called after completion") + } + } + } + } +} + +pub type NameWrapIter = + iter::Map::Item) -> NameWrap<::Item>>; /// Wrapper type to expose an `AsRef<[u8]>` impl for all types implementing `ProtocolName`. #[derive(Clone)] -struct NameWrap(N); +pub struct NameWrap(N); impl AsRef<[u8]> for NameWrap { fn as_ref(&self) -> &[u8] { self.0.protocol_name() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v1_sim_open_is_not_default() { + assert_ne!( + multistream_select::Version::default(), + multistream_select::Version::V1SimultaneousOpen, + ); + } +} diff --git a/core/tests/transport_upgrade.rs b/core/tests/transport_upgrade.rs index f02fb2f3bd7..42712463e89 100644 --- a/core/tests/transport_upgrade.rs +++ b/core/tests/transport_upgrade.rs @@ -23,7 +23,7 @@ mod util; use futures::prelude::*; use libp2p_core::identity; use libp2p_core::transport::{MemoryTransport, Transport}; -use libp2p_core::upgrade::{self, InboundUpgrade, OutboundUpgrade, UpgradeInfo}; +use libp2p_core::upgrade::{InboundUpgrade, OutboundUpgrade, UpgradeInfo}; use libp2p_mplex::MplexConfig; use libp2p_noise as noise; use multiaddr::{Multiaddr, Protocol}; @@ -85,7 +85,7 @@ fn upgrade_pipeline() { .into_authentic(&listener_keys) .unwrap(); let listener_transport = MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(listener_noise_keys).into_authenticated()) .apply(HelloUpgrade {}) .apply(HelloUpgrade {}) @@ -103,7 +103,7 @@ fn upgrade_pipeline() { .into_authentic(&dialer_keys) .unwrap(); let dialer_transport = MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(dialer_noise_keys).into_authenticated()) .apply(HelloUpgrade {}) .apply(HelloUpgrade {}) diff --git a/core/tests/util.rs b/core/tests/util.rs index 9592daca9eb..b9e6c52578b 100644 --- a/core/tests/util.rs +++ b/core/tests/util.rs @@ -7,7 +7,7 @@ use libp2p_core::{ muxing::{StreamMuxer, StreamMuxerBox}, network::{Network, NetworkConfig}, transport::{self, memory::MemoryTransport}, - upgrade, Multiaddr, PeerId, Transport, + Multiaddr, PeerId, Transport, }; use libp2p_mplex as mplex; use libp2p_noise as noise; @@ -24,7 +24,7 @@ pub fn test_network(cfg: NetworkConfig) -> TestNetwork { .into_authentic(&local_key) .unwrap(); let transport: TestTransport = MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(mplex::MplexConfig::new()) .boxed(); diff --git a/examples/chat-tokio.rs b/examples/chat-tokio.rs index 47dbb4341ba..a04cebd4f92 100644 --- a/examples/chat-tokio.rs +++ b/examples/chat-tokio.rs @@ -38,7 +38,6 @@ use futures::StreamExt; use libp2p::{ - core::upgrade, floodsub::{self, Floodsub, FloodsubEvent}, identity, mdns::{Mdns, MdnsEvent}, @@ -74,7 +73,7 @@ async fn main() -> Result<(), Box> { // encryption and Mplex for multiplexing of substreams on a TCP stream. let transport = TokioTcpConfig::new() .nodelay(true) - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(mplex::MplexConfig::new()) .boxed(); diff --git a/examples/ipfs-private.rs b/examples/ipfs-private.rs index 4b44ad3f40a..24f4a371554 100644 --- a/examples/ipfs-private.rs +++ b/examples/ipfs-private.rs @@ -34,9 +34,7 @@ use async_std::{io, task}; use futures::{future, prelude::*}; use libp2p::{ - core::{ - either::EitherTransport, muxing::StreamMuxerBox, transport, transport::upgrade::Version, - }, + core::{either::EitherTransport, muxing::StreamMuxerBox, transport}, gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent, MessageAuthenticity}, identify::{Identify, IdentifyConfig, IdentifyEvent}, identity, @@ -77,7 +75,7 @@ pub fn build_transport( None => EitherTransport::Right(base_transport), }; maybe_encrypted - .upgrade(Version::V1) + .upgrade() .authenticate(noise_config) .multiplex(yamux_config) .timeout(Duration::from_secs(20)) diff --git a/misc/metrics/Cargo.toml b/misc/metrics/Cargo.toml index 334a3f76343..939c3396943 100644 --- a/misc/metrics/Cargo.toml +++ b/misc/metrics/Cargo.toml @@ -15,12 +15,14 @@ gossipsub = ["libp2p-gossipsub"] identify = ["libp2p-identify"] kad = ["libp2p-kad"] ping = ["libp2p-ping"] +relay = ["libp2p-relay"] [dependencies] libp2p-core = { version = "0.31.0", path = "../../core" } libp2p-gossipsub = { version = "0.35.0", path = "../../protocols/gossipsub", optional = true } libp2p-identify = { version = "0.33.0", path = "../../protocols/identify", optional = true } libp2p-kad = { version = "0.34.0", path = "../../protocols/kad", optional = true } +libp2p-relay = { version = "0.5.0", path = "../../protocols/relay", optional = true } libp2p-ping = { version = "0.33.0", path = "../../protocols/ping", optional = true } libp2p-swarm = { version = "0.33.0", path = "../../swarm" } open-metrics-client = "0.13.0" diff --git a/misc/metrics/src/lib.rs b/misc/metrics/src/lib.rs index c3aa376b241..0a507d7ae62 100644 --- a/misc/metrics/src/lib.rs +++ b/misc/metrics/src/lib.rs @@ -33,6 +33,8 @@ mod identify; mod kad; #[cfg(feature = "ping")] mod ping; +#[cfg(feature = "relay")] +mod relay; mod swarm; use open_metrics_client::registry::Registry; @@ -47,6 +49,8 @@ pub struct Metrics { kad: kad::Metrics, #[cfg(feature = "ping")] ping: ping::Metrics, + #[cfg(feature = "relay")] + relay: relay::Metrics, swarm: swarm::Metrics, } @@ -70,6 +74,8 @@ impl Metrics { kad: kad::Metrics::new(sub_registry), #[cfg(feature = "ping")] ping: ping::Metrics::new(sub_registry), + #[cfg(feature = "relay")] + relay: relay::Metrics::new(sub_registry), swarm: swarm::Metrics::new(sub_registry), } } diff --git a/misc/metrics/src/relay.rs b/misc/metrics/src/relay.rs new file mode 100644 index 00000000000..9bf85c46c56 --- /dev/null +++ b/misc/metrics/src/relay.rs @@ -0,0 +1,106 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use open_metrics_client::encoding::text::Encode; +use open_metrics_client::metrics::counter::Counter; +use open_metrics_client::metrics::family::Family; +use open_metrics_client::registry::Registry; + +pub struct Metrics { + events: Family, +} + +impl Metrics { + pub fn new(registry: &mut Registry) -> Self { + let sub_registry = registry.sub_registry_with_prefix("relay"); + + let events = Family::default(); + sub_registry.register( + "events", + "Events emitted by the relay NetworkBehaviour", + Box::new(events.clone()), + ); + + Self { events } + } +} + +#[derive(Clone, Hash, PartialEq, Eq, Encode)] +struct EventLabels { + event: EventType, +} + +#[derive(Clone, Hash, PartialEq, Eq, Encode)] +enum EventType { + ReservationReqAccepted, + ReservationReqAcceptFailed, + ReservationReqDenied, + ReservationReqDenyFailed, + ReservationTimedOut, + CircuitReqDenied, + CircuitReqDenyFailed, + CircuitReqAccepted, + CircuitReqAcceptFailed, + CircuitClosed, +} + +impl From<&libp2p_relay::v2::relay::Event> for EventType { + fn from(event: &libp2p_relay::v2::relay::Event) -> Self { + match event { + libp2p_relay::v2::relay::Event::ReservationReqAccepted { .. } => { + EventType::ReservationReqAccepted + } + libp2p_relay::v2::relay::Event::ReservationReqAcceptFailed { .. } => { + EventType::ReservationReqAcceptFailed + } + libp2p_relay::v2::relay::Event::ReservationReqDenied { .. } => { + EventType::ReservationReqDenied + } + libp2p_relay::v2::relay::Event::ReservationReqDenyFailed { .. } => { + EventType::ReservationReqDenyFailed + } + libp2p_relay::v2::relay::Event::ReservationTimedOut { .. } => { + EventType::ReservationTimedOut + } + libp2p_relay::v2::relay::Event::CircuitReqDenied { .. } => EventType::CircuitReqDenied, + libp2p_relay::v2::relay::Event::CircuitReqDenyFailed { .. } => { + EventType::CircuitReqDenyFailed + } + libp2p_relay::v2::relay::Event::CircuitReqAccepted { .. } => { + EventType::CircuitReqAccepted + } + libp2p_relay::v2::relay::Event::CircuitReqAcceptFailed { .. } => { + EventType::CircuitReqAcceptFailed + } + libp2p_relay::v2::relay::Event::CircuitClosed { .. } => EventType::CircuitClosed, + } + } +} + +impl super::Recorder for super::Metrics { + fn record(&self, event: &libp2p_relay::v2::relay::Event) { + self.relay + .events + .get_or_create(&EventLabels { + event: event.into(), + }) + .inc(); + } +} diff --git a/misc/multistream-select/CHANGELOG.md b/misc/multistream-select/CHANGELOG.md index 81710ea4e4a..02efe4491b8 100644 --- a/misc/multistream-select/CHANGELOG.md +++ b/misc/multistream-select/CHANGELOG.md @@ -9,6 +9,9 @@ - Implement `From for ProtocolError` instead of `Into`. [PR 2169](https://github.com/libp2p/rust-libp2p/pull/2169) +[simultaneous open extension]: https://github.com/libp2p/specs/blob/master/connections/simopen.md +[PR 2066]: https://github.com/libp2p/rust-libp2p/pull/2066 + # 0.10.3 [2021-03-17] - Update dependencies. diff --git a/misc/multistream-select/Cargo.toml b/misc/multistream-select/Cargo.toml index fb4e41fcb35..ad98873ff04 100644 --- a/misc/multistream-select/Cargo.toml +++ b/misc/multistream-select/Cargo.toml @@ -15,6 +15,7 @@ bytes = "1" futures = "0.3" log = "0.4" pin-project = "1.0.0" +rand = "0.7" smallvec = "1.6.1" unsigned-varint = "0.7" diff --git a/misc/multistream-select/src/dialer_select.rs b/misc/multistream-select/src/dialer_select.rs index 7a8c75daa6f..decd266348d 100644 --- a/misc/multistream-select/src/dialer_select.rs +++ b/misc/multistream-select/src/dialer_select.rs @@ -20,11 +20,12 @@ //! Protocol negotiation strategies for the peer acting as the dialer. -use crate::protocol::{HeaderLine, Message, MessageIO, Protocol, ProtocolError}; +use crate::protocol::{HeaderLine, Message, MessageIO, Protocol, ProtocolError, SIM_OPEN_ID}; use crate::{Negotiated, NegotiationError, Version}; use futures::{future::Either, prelude::*}; use std::{ + cmp::Ordering, convert::TryFrom as _, iter, mem, pin::Pin, @@ -36,8 +37,9 @@ use std::{ /// /// This function is given an I/O stream and a list of protocols and returns a /// computation that performs the protocol negotiation with the remote. The -/// returned `Future` resolves with the name of the negotiated protocol and -/// a [`Negotiated`] I/O stream. +/// returned `Future` resolves with the name of the negotiated protocol, a +/// [`Negotiated`] I/O stream and the [`Role`] of the peer on the connection +/// going forward. /// /// The chosen message flow for protocol negotiation depends on the numbers of /// supported protocols given. That is, this function delegates to serial or @@ -61,15 +63,22 @@ where I::Item: AsRef<[u8]>, { let iter = protocols.into_iter(); - // We choose between the "serial" and "parallel" strategies based on the number of protocols. - if iter.size_hint().1.map(|n| n <= 3).unwrap_or(false) { - Either::Left(dialer_select_proto_serial(inner, iter, version)) - } else { - Either::Right(dialer_select_proto_parallel(inner, iter, version)) + match version { + Version::V1 | Version::V1Lazy => { + // We choose between the "serial" and "parallel" strategies based on the number of protocols. + if iter.size_hint().1.map(|n| n <= 3).unwrap_or(false) { + Either::Left(dialer_select_proto_serial(inner, iter, version)) + } else { + Either::Right(dialer_select_proto_parallel(inner, iter, version)) + } + } + Version::V1SimultaneousOpen => { + Either::Left(dialer_select_proto_serial(inner, iter, version)) + } } } -/// Future, returned by `dialer_select_proto`, which selects a protocol and dialer +/// Future, returned by `dialer_select_proto`, which selects a protocol and /// either trying protocols in-order, or by requesting all protocols supported /// by the remote upfront, from which the first protocol found in the dialer's /// list of protocols is selected. @@ -141,10 +150,44 @@ pub struct DialerSelectSeq { } enum SeqState { - SendHeader { io: MessageIO }, - SendProtocol { io: MessageIO, protocol: N }, - FlushProtocol { io: MessageIO, protocol: N }, - AwaitProtocol { io: MessageIO, protocol: N }, + SendHeader { + io: MessageIO, + }, + + // Simultaneous open protocol extension + SendSimOpen { + io: MessageIO, + protocol: Option, + }, + FlushSimOpen { + io: MessageIO, + protocol: N, + }, + AwaitSimOpen { + io: MessageIO, + protocol: N, + }, + SimOpenPhase { + selection: SimOpenPhase, + protocol: N, + }, + Responder { + responder: crate::ListenerSelectFuture, + }, + + // Standard multistream-select protocol + SendProtocol { + io: MessageIO, + protocol: N, + }, + FlushProtocol { + io: MessageIO, + protocol: N, + }, + AwaitProtocol { + io: MessageIO, + protocol: N, + }, Done, } @@ -154,9 +197,9 @@ where // It also makes the implementation considerably easier to write. R: AsyncRead + AsyncWrite + Unpin, I: Iterator, - I::Item: AsRef<[u8]>, + I::Item: AsRef<[u8]> + Clone, { - type Output = Result<(I::Item, Negotiated), NegotiationError>; + type Output = Result<(I::Item, Negotiated, Role), NegotiationError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -177,13 +220,141 @@ where return Poll::Ready(Err(From::from(err))); } - let protocol = this.protocols.next().ok_or(NegotiationError::Failed)?; + match this.version { + Version::V1 | Version::V1Lazy => { + let protocol = this.protocols.next().ok_or(NegotiationError::Failed)?; + + // The dialer always sends the header and the first protocol + // proposal in one go for efficiency. + *this.state = SeqState::SendProtocol { io, protocol }; + } + Version::V1SimultaneousOpen => { + *this.state = SeqState::SendSimOpen { io, protocol: None }; + } + } + } + + SeqState::SendSimOpen { mut io, protocol } => { + match Pin::new(&mut io).poll_ready(cx)? { + Poll::Ready(()) => {} + Poll::Pending => { + *this.state = SeqState::SendSimOpen { io, protocol }; + return Poll::Pending; + } + } + + match protocol { + None => { + let msg = Message::Protocol(SIM_OPEN_ID); + if let Err(err) = Pin::new(&mut io).start_send(msg) { + return Poll::Ready(Err(From::from(err))); + } + + let protocol = this.protocols.next().ok_or(NegotiationError::Failed)?; + *this.state = SeqState::SendSimOpen { + io, + protocol: Some(protocol), + }; + } + Some(protocol) => { + let p = Protocol::try_from(protocol.as_ref())?; + if let Err(err) = + Pin::new(&mut io).start_send(Message::Protocol(p.clone())) + { + return Poll::Ready(Err(From::from(err))); + } + log::debug!("Dialer: Proposed protocol: {}", p); - // The dialer always sends the header and the first protocol - // proposal in one go for efficiency. - *this.state = SeqState::SendProtocol { io, protocol }; + *this.state = SeqState::FlushSimOpen { io, protocol } + } + } } + SeqState::FlushSimOpen { mut io, protocol } => { + match Pin::new(&mut io).poll_flush(cx)? { + Poll::Ready(()) => *this.state = SeqState::AwaitSimOpen { io, protocol }, + Poll::Pending => { + *this.state = SeqState::FlushSimOpen { io, protocol }; + return Poll::Pending; + } + } + } + + SeqState::AwaitSimOpen { mut io, protocol } => { + let msg = match Pin::new(&mut io).poll_next(cx)? { + Poll::Ready(Some(msg)) => msg, + Poll::Pending => { + *this.state = SeqState::AwaitSimOpen { io, protocol }; + return Poll::Pending; + } + // Treat EOF error as [`NegotiationError::Failed`], not as + // [`NegotiationError::ProtocolError`], allowing dropping or closing an I/O + // stream as a permissible way to "gracefully" fail a negotiation. + Poll::Ready(None) => return Poll::Ready(Err(NegotiationError::Failed)), + }; + + match msg { + Message::Header(v) if v == HeaderLine::from(*this.version) => { + *this.state = SeqState::AwaitSimOpen { io, protocol }; + } + Message::Protocol(p) if p == SIM_OPEN_ID => { + let selection = SimOpenPhase { + state: SimOpenState::SendNonce { io }, + }; + *this.state = SeqState::SimOpenPhase { + selection, + protocol, + }; + } + Message::NotAvailable => { + *this.state = SeqState::AwaitProtocol { io, protocol } + } + _ => return Poll::Ready(Err(ProtocolError::InvalidMessage.into())), + } + } + + SeqState::SimOpenPhase { + mut selection, + protocol, + } => { + let (io, selection_res) = match Pin::new(&mut selection).poll(cx)? { + Poll::Ready((io, res)) => (io, res), + Poll::Pending => { + *this.state = SeqState::SimOpenPhase { + selection, + protocol, + }; + return Poll::Pending; + } + }; + + match selection_res { + Role::Initiator => { + *this.state = SeqState::SendProtocol { io, protocol }; + } + Role::Responder => { + #[allow(clippy::needless_collect)] + let protocols: Vec<_> = this.protocols.collect(); + *this.state = SeqState::Responder { + responder: crate::listener_select::listener_select_proto_no_header( + io, + std::iter::once(protocol).chain(protocols.into_iter()), + ), + } + } + } + } + + SeqState::Responder { mut responder } => match Pin::new(&mut responder).poll(cx) { + Poll::Ready(res) => { + return Poll::Ready(res.map(|(p, io)| (p, io, Role::Responder))) + } + Poll::Pending => { + *this.state = SeqState::Responder { responder }; + return Poll::Pending; + } + }, + SeqState::SendProtocol { mut io, protocol } => { match Pin::new(&mut io).poll_ready(cx)? { Poll::Ready(()) => {} @@ -203,7 +374,9 @@ where *this.state = SeqState::FlushProtocol { io, protocol } } else { match this.version { - Version::V1 => *this.state = SeqState::FlushProtocol { io, protocol }, + Version::V1 | Version::V1SimultaneousOpen => { + *this.state = SeqState::FlushProtocol { io, protocol } + } // This is the only effect that `V1Lazy` has compared to `V1`: // Optimistically settling on the only protocol that // the dialer supports for this negotiation. Notably, @@ -212,7 +385,7 @@ where log::debug!("Dialer: Expecting proposed protocol: {}", p); let hl = HeaderLine::from(Version::V1Lazy); let io = Negotiated::expecting(io.into_reader(), p, Some(hl)); - return Poll::Ready(Ok((protocol, io))); + return Poll::Ready(Ok((protocol, io, Role::Initiator))); } } } @@ -245,10 +418,19 @@ where Message::Header(v) if v == HeaderLine::from(*this.version) => { *this.state = SeqState::AwaitProtocol { io, protocol }; } + Message::Protocol(p) if p == SIM_OPEN_ID => { + let selection = SimOpenPhase { + state: SimOpenState::SendNonce { io }, + }; + *this.state = SeqState::SimOpenPhase { + selection, + protocol, + }; + } Message::Protocol(ref p) if p.as_ref() == protocol.as_ref() => { log::debug!("Dialer: Received confirmation for protocol: {}", p); let io = Negotiated::completed(io.into_inner()); - return Poll::Ready(Ok((protocol, io))); + return Poll::Ready(Ok((protocol, io, Role::Initiator))); } Message::NotAvailable => { log::debug!( @@ -268,6 +450,182 @@ where } } +struct SimOpenPhase { + state: SimOpenState, +} + +enum SimOpenState { + SendNonce { io: MessageIO }, + FlushNonce { io: MessageIO, local_nonce: u64 }, + ReadNonce { io: MessageIO, local_nonce: u64 }, + SendRole { io: MessageIO, local_role: Role }, + FlushRole { io: MessageIO, local_role: Role }, + ReadRole { io: MessageIO, local_role: Role }, + Done, +} + +/// Role of the local node after protocol negotiation. +/// +/// Always equals [`Role::Initiator`] unless [`Version::V1SimultaneousOpen`] is +/// used in which case node may end up in either role after negotiation. +/// +/// See [`Version::V1SimultaneousOpen`] for details. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Role { + Initiator, + Responder, +} + +impl Future for SimOpenPhase +where + // The Unpin bound here is required because we produce a `Negotiated` as the output. + // It also makes the implementation considerably easier to write. + R: AsyncRead + AsyncWrite + Unpin, +{ + type Output = Result<(MessageIO, Role), NegotiationError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + match mem::replace(&mut self.state, SimOpenState::Done) { + SimOpenState::SendNonce { mut io } => { + match Pin::new(&mut io).poll_ready(cx)? { + Poll::Ready(()) => {} + Poll::Pending => { + self.state = SimOpenState::SendNonce { io }; + return Poll::Pending; + } + } + + let local_nonce = rand::random(); + let msg = Message::Select(local_nonce); + if let Err(err) = Pin::new(&mut io).start_send(msg) { + return Poll::Ready(Err(From::from(err))); + } + + self.state = SimOpenState::FlushNonce { io, local_nonce }; + } + SimOpenState::FlushNonce { + mut io, + local_nonce, + } => match Pin::new(&mut io).poll_flush(cx)? { + Poll::Ready(()) => self.state = SimOpenState::ReadNonce { io, local_nonce }, + Poll::Pending => { + self.state = SimOpenState::FlushNonce { io, local_nonce }; + return Poll::Pending; + } + }, + SimOpenState::ReadNonce { + mut io, + local_nonce, + } => { + let msg = match Pin::new(&mut io).poll_next(cx)? { + Poll::Ready(Some(msg)) => msg, + Poll::Pending => { + self.state = SimOpenState::ReadNonce { io, local_nonce }; + return Poll::Pending; + } + // Treat EOF error as [`NegotiationError::Failed`], not as + // [`NegotiationError::ProtocolError`], allowing dropping or closing an I/O + // stream as a permissible way to "gracefully" fail a negotiation. + Poll::Ready(None) => return Poll::Ready(Err(NegotiationError::Failed)), + }; + + match msg { + // As an optimization, the simultaneous open + // multistream-select variant sends both the + // simultaneous open ID (`/libp2p/simultaneous-connect`) + // and a protocol before flushing. In the case where the + // remote acts as a listener already, it can accept or + // decline the attached protocol within the same + // round-trip. + // + // In this particular situation, the remote acts as a + // dialer and uses the simultaneous open variant. Given + // that nonces need to be exchanged first, the attached + // protocol by the remote needs to be ignored. + Message::Protocol(_) => { + self.state = SimOpenState::ReadNonce { io, local_nonce }; + } + Message::Select(remote_nonce) => { + match local_nonce.cmp(&remote_nonce) { + Ordering::Equal => { + // Start over. + self.state = SimOpenState::SendNonce { io }; + } + Ordering::Greater => { + self.state = SimOpenState::SendRole { + io, + local_role: Role::Initiator, + }; + } + Ordering::Less => { + self.state = SimOpenState::SendRole { + io, + local_role: Role::Responder, + }; + } + } + } + _ => return Poll::Ready(Err(ProtocolError::InvalidMessage.into())), + } + } + SimOpenState::SendRole { mut io, local_role } => { + match Pin::new(&mut io).poll_ready(cx)? { + Poll::Ready(()) => {} + Poll::Pending => { + self.state = SimOpenState::SendRole { io, local_role }; + return Poll::Pending; + } + } + + let msg = match local_role { + Role::Initiator => Message::Initiator, + Role::Responder => Message::Responder, + }; + + if let Err(err) = Pin::new(&mut io).start_send(msg) { + return Poll::Ready(Err(From::from(err))); + } + + self.state = SimOpenState::FlushRole { io, local_role }; + } + SimOpenState::FlushRole { mut io, local_role } => { + match Pin::new(&mut io).poll_flush(cx)? { + Poll::Ready(()) => self.state = SimOpenState::ReadRole { io, local_role }, + Poll::Pending => { + self.state = SimOpenState::FlushRole { io, local_role }; + return Poll::Pending; + } + } + } + SimOpenState::ReadRole { mut io, local_role } => { + let remote_msg = match Pin::new(&mut io).poll_next(cx)? { + Poll::Ready(Some(msg)) => msg, + Poll::Pending => { + self.state = SimOpenState::ReadRole { io, local_role }; + return Poll::Pending; + } + // Treat EOF error as [`NegotiationError::Failed`], not as + // [`NegotiationError::ProtocolError`], allowing dropping or closing an I/O + // stream as a permissible way to "gracefully" fail a negotiation. + Poll::Ready(None) => return Poll::Ready(Err(NegotiationError::Failed)), + }; + + let result = match local_role { + Role::Initiator if remote_msg == Message::Responder => Ok((io, local_role)), + Role::Responder if remote_msg == Message::Initiator => Ok((io, local_role)), + + _ => Err(ProtocolError::InvalidMessage.into()), + }; + + return Poll::Ready(result); + } + SimOpenState::Done => panic!("SimOpenPhase::poll called after completion"), + } + } + } +} + /// A `Future` returned by [`dialer_select_proto_parallel`] which negotiates /// a protocol selectively by considering all supported protocols of the remote /// "in parallel". @@ -295,7 +653,7 @@ where I: Iterator, I::Item: AsRef<[u8]>, { - type Output = Result<(I::Item, Negotiated), NegotiationError>; + type Output = Result<(I::Item, Negotiated, Role), NegotiationError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -394,7 +752,7 @@ where log::debug!("Dialer: Expecting proposed protocol: {}", p); let io = Negotiated::expecting(io.into_reader(), p, None); - return Poll::Ready(Ok((protocol, io))); + return Poll::Ready(Ok((protocol, io, Role::Initiator))); } ParState::Done => panic!("ParState::poll called after completion"), diff --git a/misc/multistream-select/src/lib.rs b/misc/multistream-select/src/lib.rs index 00291f4ece8..1239b016939 100644 --- a/misc/multistream-select/src/lib.rs +++ b/misc/multistream-select/src/lib.rs @@ -79,7 +79,7 @@ //! let socket = TcpStream::connect("127.0.0.1:10333").await.unwrap(); //! //! let protos = vec![b"/echo/1.0.0", b"/echo/2.5.0"]; -//! let (protocol, _io) = dialer_select_proto(socket, protos, Version::V1).await.unwrap(); +//! let (protocol, _io, _role) = dialer_select_proto(socket, protos, Version::V1).await.unwrap(); //! //! println!("Negotiated protocol: {:?}", protocol); //! // You can now use `_io` to communicate with the remote. @@ -94,7 +94,7 @@ mod negotiated; mod protocol; mod tests; -pub use self::dialer_select::{dialer_select_proto, DialerSelectFuture}; +pub use self::dialer_select::{dialer_select_proto, DialerSelectFuture, Role}; pub use self::listener_select::{listener_select_proto, ListenerSelectFuture}; pub use self::negotiated::{Negotiated, NegotiatedComplete, NegotiationError}; pub use self::protocol::ProtocolError; @@ -137,6 +137,18 @@ pub enum Version { /// [1]: https://github.com/multiformats/go-multistream/issues/20 /// [2]: https://github.com/libp2p/rust-libp2p/pull/1212 V1Lazy, + /// A variant of version 1 that selects a single initiator when both peers are acting as such, + /// in other words when both peers simultaneously open a connection. + /// + /// This multistream-select variant is specified in [1]. + /// + /// Note: [`Version::V1SimultaneousOpen`] should only be used (a) on transports that allow + /// simultaneously opened connections, e.g. TCP with socket reuse and (2) during the first + /// negotiation on the connection, most likely the secure channel protocol negotiation. In all + /// other cases one should use [`Version::V1`] or [`Version::V1Lazy`]. + /// + /// [1]: https://github.com/libp2p/specs/blob/master/connections/simopen.md + V1SimultaneousOpen, // Draft: https://github.com/libp2p/specs/pull/95 // V2, } diff --git a/misc/multistream-select/src/listener_select.rs b/misc/multistream-select/src/listener_select.rs index aa433e40c4d..15c06fb323a 100644 --- a/misc/multistream-select/src/listener_select.rs +++ b/misc/multistream-select/src/listener_select.rs @@ -42,6 +42,37 @@ use std::{ /// returned `Future` resolves with the name of the negotiated protocol and /// a [`Negotiated`] I/O stream. pub fn listener_select_proto(inner: R, protocols: I) -> ListenerSelectFuture +where + R: AsyncRead + AsyncWrite, + I: IntoIterator, + I::Item: AsRef<[u8]>, +{ + listener_select_proto_with_state( + State::RecvHeader { + io: MessageIO::new(inner), + }, + protocols, + ) +} + +/// Used when selected as a [`crate::Role::Responder`] during [`crate::dialer_select_proto`] +/// negotiation with [`crate::Version::V1SimultaneousOpen`] +pub(crate) fn listener_select_proto_no_header( + io: MessageIO, + protocols: I, +) -> ListenerSelectFuture +where + R: AsyncRead + AsyncWrite, + I: IntoIterator, + I::Item: AsRef<[u8]>, +{ + listener_select_proto_with_state(State::RecvMessage { io }, protocols) +} + +fn listener_select_proto_with_state( + state: State, + protocols: I, +) -> ListenerSelectFuture where R: AsyncRead + AsyncWrite, I: IntoIterator, @@ -62,9 +93,7 @@ where }); ListenerSelectFuture { protocols: SmallVec::from_iter(protocols), - state: State::RecvHeader { - io: MessageIO::new(inner), - }, + state, last_sent_na: false, } } diff --git a/misc/multistream-select/src/protocol.rs b/misc/multistream-select/src/protocol.rs index 1cfdcc4b588..b7f1611c27f 100644 --- a/misc/multistream-select/src/protocol.rs +++ b/misc/multistream-select/src/protocol.rs @@ -35,6 +35,7 @@ use std::{ error::Error, fmt, io, pin::Pin, + str::FromStr, task::{Context, Poll}, }; use unsigned_varint as uvi; @@ -48,6 +49,20 @@ const MSG_MULTISTREAM_1_0: &[u8] = b"/multistream/1.0.0\n"; const MSG_PROTOCOL_NA: &[u8] = b"na\n"; /// The encoded form of a multistream-select 'ls' message. const MSG_LS: &[u8] = b"ls\n"; +/// The encoded form of a 'select:' message of the multistream-select +/// simultaneous open protocol extension. +const MSG_SELECT: &[u8] = b"select:"; +/// The encoded form of a 'initiator' message of the multistream-select +/// simultaneous open protocol extension. +const MSG_INITIATOR: &[u8] = b"initiator\n"; +/// The encoded form of a 'responder' message of the multistream-select +/// simultaneous open protocol extension. +const MSG_RESPONDER: &[u8] = b"responder\n"; + +/// The identifier of the multistream-select simultaneous open protocol +/// extension. +pub(crate) const SIM_OPEN_ID: Protocol = + Protocol(Bytes::from_static(b"/libp2p/simultaneous-connect")); /// The multistream-select header lines preceeding negotiation. /// @@ -61,7 +76,7 @@ pub enum HeaderLine { impl From for HeaderLine { fn from(v: Version) -> HeaderLine { match v { - Version::V1 | Version::V1Lazy => HeaderLine::V1, + Version::V1 | Version::V1Lazy | Version::V1SimultaneousOpen => HeaderLine::V1, } } } @@ -119,6 +134,9 @@ pub enum Message { Protocols(Vec), /// A message signaling that a requested protocol is not available. NotAvailable, + Select(u64), + Initiator, + Responder, } impl Message { @@ -160,6 +178,22 @@ impl Message { dest.put(MSG_PROTOCOL_NA); Ok(()) } + Message::Select(nonce) => { + dest.put(MSG_SELECT); + dest.put(nonce.to_string().as_ref()); + dest.put_u8(b'\n'); + Ok(()) + } + Message::Initiator => { + dest.reserve(MSG_INITIATOR.len()); + dest.put(MSG_INITIATOR); + Ok(()) + } + Message::Responder => { + dest.reserve(MSG_RESPONDER.len()); + dest.put(MSG_RESPONDER); + Ok(()) + } } } @@ -177,6 +211,26 @@ impl Message { return Ok(Message::ListProtocols); } + if msg.len() > MSG_SELECT.len() + 1 /* \n */ + && msg[.. MSG_SELECT.len()] == *MSG_SELECT + && msg.last() == Some(&b'\n') + { + if let Some(nonce) = std::str::from_utf8(&msg[MSG_SELECT.len()..msg.len() - 1]) + .ok() + .and_then(|s| u64::from_str(s).ok()) + { + return Ok(Message::Select(nonce)); + } + } + + if msg == MSG_INITIATOR { + return Ok(Message::Initiator); + } + + if msg == MSG_RESPONDER { + return Ok(Message::Responder); + } + // If it starts with a `/`, ends with a line feed without any // other line feeds in-between, it must be a protocol name. if msg.get(0) == Some(&b'/') diff --git a/misc/multistream-select/src/tests.rs b/misc/multistream-select/src/tests.rs index ca627d24fcf..763301943d7 100644 --- a/misc/multistream-select/src/tests.rs +++ b/misc/multistream-select/src/tests.rs @@ -35,25 +35,24 @@ fn select_proto_basic() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener_addr = listener.local_addr().unwrap(); - let server = async_std::task::spawn(async move { + let server = async move { let connec = listener.accept().await.unwrap().0; let protos = vec![b"/proto1", b"/proto2"]; let (proto, mut io) = listener_select_proto(connec, protos).await.unwrap(); assert_eq!(proto, b"/proto2"); - let mut out = vec![0; 32]; - let n = io.read(&mut out).await.unwrap(); - out.truncate(n); + let mut out = vec![0; 4]; + io.read_exact(&mut out).await.unwrap(); assert_eq!(out, b"ping"); io.write_all(b"pong").await.unwrap(); io.flush().await.unwrap(); - }); + }; - let client = async_std::task::spawn(async move { + let client = async move { let connec = TcpStream::connect(&listener_addr).await.unwrap(); let protos = vec![b"/proto3", b"/proto2"]; - let (proto, mut io) = dialer_select_proto(connec, protos.into_iter(), version) + let (proto, mut io, _) = dialer_select_proto(connec, protos.into_iter(), version) .await .unwrap(); assert_eq!(proto, b"/proto2"); @@ -61,18 +60,17 @@ fn select_proto_basic() { io.write_all(b"ping").await.unwrap(); io.flush().await.unwrap(); - let mut out = vec![0; 32]; - let n = io.read(&mut out).await.unwrap(); - out.truncate(n); + let mut out = vec![0; 4]; + io.read_exact(&mut out).await.unwrap(); assert_eq!(out, b"pong"); - }); + }; - server.await; - client.await; + futures::future::join(server, client).await; } async_std::task::block_on(run(Version::V1)); async_std::task::block_on(run(Version::V1Lazy)); + async_std::task::block_on(run(Version::V1SimultaneousOpen)); } /// Tests the expected behaviour of failed negotiations. @@ -110,7 +108,7 @@ fn negotiation_failed() { let connec = TcpStream::connect(&listener_addr).await.unwrap(); let mut io = match dialer_select_proto(connec, dial_protos.into_iter(), version).await { Err(NegotiationError::Failed) => return, - Ok((_, io)) => io, + Ok((_, io, _)) => io, Err(_) => panic!(), }; // The dialer may write a payload that is even sent before it @@ -170,7 +168,7 @@ fn negotiation_failed() { for (listen_protos, dial_protos) in protos { for dial_payload in payloads.clone() { - for &version in &[Version::V1, Version::V1Lazy] { + for &version in &[Version::V1, Version::V1Lazy, Version::V1SimultaneousOpen] { async_std::task::block_on(run(Test { version, listen_protos: listen_protos.clone(), @@ -199,7 +197,7 @@ fn select_proto_parallel() { let client = async_std::task::spawn(async move { let connec = TcpStream::connect(&listener_addr).await.unwrap(); let protos = vec![b"/proto3", b"/proto2"]; - let (proto, io) = dialer_select_proto_parallel(connec, protos.into_iter(), version) + let (proto, io, _) = dialer_select_proto_parallel(connec, protos.into_iter(), version) .await .unwrap(); assert_eq!(proto, b"/proto2"); @@ -231,7 +229,7 @@ fn select_proto_serial() { let client = async_std::task::spawn(async move { let connec = TcpStream::connect(&listener_addr).await.unwrap(); let protos = vec![b"/proto3", b"/proto2"]; - let (proto, io) = dialer_select_proto_serial(connec, protos.into_iter(), version) + let (proto, io, _) = dialer_select_proto_serial(connec, protos.into_iter(), version) .await .unwrap(); assert_eq!(proto, b"/proto2"); @@ -244,4 +242,37 @@ fn select_proto_serial() { async_std::task::block_on(run(Version::V1)); async_std::task::block_on(run(Version::V1Lazy)); + async_std::task::block_on(run(Version::V1SimultaneousOpen)); +} + +#[test] +fn simultaneous_open() { + async fn run(version: Version) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let listener_addr = listener.local_addr().unwrap(); + + let server = async move { + let connec = listener.accept().await.unwrap().0; + let protos = vec![b"/proto1", b"/proto2"]; + let (proto, io, _) = dialer_select_proto_serial(connec, protos, version) + .await + .unwrap(); + assert_eq!(proto, b"/proto2"); + io.complete().await.unwrap(); + }; + + let client = async move { + let connec = TcpStream::connect(&listener_addr).await.unwrap(); + let protos = vec![b"/proto3", b"/proto2"]; + let (proto, io, _) = dialer_select_proto_serial(connec, protos.into_iter(), version) + .await + .unwrap(); + assert_eq!(proto, b"/proto2"); + io.complete().await.unwrap(); + }; + + futures::future::join(server, client).await; + } + + futures::executor::block_on(run(Version::V1SimultaneousOpen)); } diff --git a/misc/multistream-select/tests/transport.rs b/misc/multistream-select/tests/transport.rs index 1c48af37715..53f079a7ac1 100644 --- a/misc/multistream-select/tests/transport.rs +++ b/misc/multistream-select/tests/transport.rs @@ -39,13 +39,14 @@ use std::{ type TestTransport = transport::Boxed<(PeerId, StreamMuxerBox)>; type TestNetwork = Network; -fn mk_transport(up: upgrade::Version) -> (PeerId, TestTransport) { +// TODO: Fix _up +fn mk_transport(_up: upgrade::Version) -> (PeerId, TestTransport) { let keys = identity::Keypair::generate_ed25519(); let id = keys.public().to_peer_id(); ( id, MemoryTransport::default() - .upgrade(up) + .upgrade() .authenticate(PlainText2Config { local_public_key: keys.public(), }) diff --git a/muxers/mplex/benches/split_send_size.rs b/muxers/mplex/benches/split_send_size.rs index 5380f21cc6b..66592c4febf 100644 --- a/muxers/mplex/benches/split_send_size.rs +++ b/muxers/mplex/benches/split_send_size.rs @@ -155,7 +155,7 @@ fn tcp_transport(split_send_size: usize) -> BenchTransport { libp2p_tcp::TcpConfig::new() .nodelay(true) - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(PlainText2Config { local_public_key }) .multiplex(mplex) .timeout(Duration::from_secs(5)) @@ -170,7 +170,7 @@ fn mem_transport(split_send_size: usize) -> BenchTransport { mplex.set_split_send_size(split_send_size); transport::MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(PlainText2Config { local_public_key }) .multiplex(mplex) .timeout(Duration::from_secs(5)) diff --git a/protocols/dcutr/Cargo.toml b/protocols/dcutr/Cargo.toml new file mode 100644 index 00000000000..10e63ad31c6 --- /dev/null +++ b/protocols/dcutr/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "libp2p-dcutr" +edition = "2018" +description = "Direct connection upgrade through relay" +version = "0.1.0" +authors = ["Max Inden "] +license = "MIT" +repository = "https://github.com/libp2p/rust-libp2p" +keywords = ["peer-to-peer", "libp2p", "networking"] +categories = ["network-programming", "asynchronous"] + +[dependencies] +asynchronous-codec = "0.6" +bytes = "1" +either = "1.6.0" +futures = "0.3.1" +futures-timer = "3.0" +instant = "0.1.11" +libp2p-core = { version = "0.30", path = "../../core" } +libp2p-swarm = { version = "0.32", path = "../../swarm" } +log = "0.4" +prost = "0.7" +thiserror = "1.0" +unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] } +void = "1" + +[build-dependencies] +prost-build = "0.7" + +[dev-dependencies] +env_logger = "0.8.3" +libp2p = { path = "../..", features = ["dcutr"] } +libp2p-identify = { path = "../identify" } +libp2p-ping = { path = "../ping" } +libp2p-plaintext = { path = "../../transports/plaintext" } +libp2p-relay = { version = "0.5", path = "../relay" } +libp2p-yamux = { path = "../../muxers/yamux" } +rand = "0.7" +structopt = "0.3.21" \ No newline at end of file diff --git a/protocols/dcutr/build.rs b/protocols/dcutr/build.rs new file mode 100644 index 00000000000..b159bb4c817 --- /dev/null +++ b/protocols/dcutr/build.rs @@ -0,0 +1,23 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +fn main() { + prost_build::compile_protos(&["src/message.proto"], &["src"]).unwrap(); +} diff --git a/protocols/dcutr/examples/client.rs b/protocols/dcutr/examples/client.rs new file mode 100644 index 00000000000..735671e0dac --- /dev/null +++ b/protocols/dcutr/examples/client.rs @@ -0,0 +1,277 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use futures::executor::block_on; +use futures::future::FutureExt; +use futures::stream::StreamExt; +use libp2p::core::multiaddr::{Multiaddr, Protocol}; +use libp2p::core::upgrade; +use libp2p::dcutr; +use libp2p::dns::DnsConfig; +use libp2p::identify::{Identify, IdentifyConfig, IdentifyEvent, IdentifyInfo}; +use libp2p::noise; +use libp2p::ping::{Ping, PingConfig, PingEvent}; +use libp2p::relay::v2::client::{self, Client}; +use libp2p::swarm::{SwarmBuilder, SwarmEvent}; +use libp2p::tcp::TcpConfig; +use libp2p::Transport; +use libp2p::{identity, NetworkBehaviour, PeerId}; +use log::info; +use std::convert::TryInto; +use std::error::Error; +use std::net::Ipv4Addr; +use std::str::FromStr; +use std::task::{Context, Poll}; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "libp2p DCUtR client")] +struct Opts { + /// The mode (relay, client-listen, client-dial) + #[structopt(long)] + mode: Mode, + + /// Fixed value to generate deterministic peer id + #[structopt(long)] + secret_key_seed: u8, + + /// The listening address + #[structopt(long)] + relay_address: Multiaddr, + + /// Peer ID of the remote peer to hole punch to. + #[structopt(long)] + remote_peer_id: Option, +} + +#[derive(Debug, StructOpt)] +enum Mode { + Dial, + Listen, +} + +impl FromStr for Mode { + type Err = String; + fn from_str(mode: &str) -> Result { + match mode { + "dial" => Ok(Mode::Dial), + "listen" => Ok(Mode::Listen), + _ => Err("Expected either 'dial' or 'listen'".to_string()), + } + } +} + +fn main() -> Result<(), Box> { + env_logger::init(); + + let opts = Opts::from_args(); + + let local_key = generate_ed25519(opts.secret_key_seed); + let local_peer_id = PeerId::from(local_key.public()); + info!("Local peer id: {:?}", local_peer_id); + + let (transport, client) = Client::new_transport_and_behaviour( + local_peer_id, + block_on(DnsConfig::system(TcpConfig::new().port_reuse(true))).unwrap(), + ); + + let noise_keys = noise::Keypair::::new() + .into_authentic(&local_key) + .expect("Signing libp2p-noise static DH keypair failed."); + + let transport = transport + .upgrade() + .authenticate_with_version( + noise::NoiseConfig::xx(noise_keys).into_authenticated(), + upgrade::AuthenticationVersion::V1SimultaneousOpen, + ) + .multiplex(libp2p_yamux::YamuxConfig::default()) + .boxed(); + + #[derive(NetworkBehaviour)] + #[behaviour(out_event = "Event", event_process = false)] + struct Behaviour { + relay_client: Client, + ping: Ping, + identify: Identify, + dcutr: dcutr::behaviour::Behaviour, + } + + #[derive(Debug)] + enum Event { + Ping(PingEvent), + Identify(IdentifyEvent), + Relay(client::Event), + Dcutr(dcutr::behaviour::Event), + } + + impl From for Event { + fn from(e: PingEvent) -> Self { + Event::Ping(e) + } + } + + impl From for Event { + fn from(e: IdentifyEvent) -> Self { + Event::Identify(e) + } + } + + impl From for Event { + fn from(e: client::Event) -> Self { + Event::Relay(e) + } + } + + impl From for Event { + fn from(e: dcutr::behaviour::Event) -> Self { + Event::Dcutr(e) + } + } + + let behaviour = Behaviour { + relay_client: client, + ping: Ping::new(PingConfig::new()), + identify: Identify::new(IdentifyConfig::new( + "/TODO/0.0.1".to_string(), + local_key.public(), + )), + dcutr: dcutr::behaviour::Behaviour::new(), + }; + + let mut swarm = SwarmBuilder::new(transport, behaviour, local_peer_id) + .dial_concurrency_factor(10_u8.try_into().unwrap()) + .build(); + + swarm + .listen_on( + Multiaddr::empty() + .with("0.0.0.0".parse::().unwrap().into()) + .with(Protocol::Tcp(0)), + ) + .unwrap(); + + // Wait to listen on localhost. + block_on(async { + let mut delay = futures_timer::Delay::new(std::time::Duration::from_secs(1)).fuse(); + loop { + futures::select! { + event = swarm.next() => { + match event.unwrap() { + SwarmEvent::NewListenAddr { address, .. } => { + info!("Listening on {:?}", address); + } + event => panic!("{:?}", event), + } + } + _ = delay => { + break; + } + } + } + }); + + match opts.mode { + Mode::Dial => { + swarm.dial(opts.relay_address.clone()).unwrap(); + } + Mode::Listen => { + swarm + .listen_on(opts.relay_address.clone().with(Protocol::P2pCircuit)) + .unwrap(); + } + } + + // Wait till connected to relay to learn external address. + block_on(async { + loop { + match swarm.next().await.unwrap() { + SwarmEvent::NewListenAddr { .. } => {} + SwarmEvent::Dialing { .. } => {} + SwarmEvent::ConnectionEstablished { .. } => {} + SwarmEvent::Behaviour(Event::Ping(_)) => {} + SwarmEvent::Behaviour(Event::Relay(_)) => {} + SwarmEvent::Behaviour(Event::Identify(IdentifyEvent::Sent { .. })) => {} + SwarmEvent::Behaviour(Event::Identify(IdentifyEvent::Received { + info: IdentifyInfo { observed_addr, .. }, + .. + })) => { + info!("Observed address: {:?}", observed_addr); + break; + } + event => panic!("{:?}", event), + } + } + }); + + if matches!(opts.mode, Mode::Dial) { + swarm + .dial( + opts.relay_address + .clone() + .with(Protocol::P2pCircuit) + .with(Protocol::P2p(opts.remote_peer_id.unwrap().into())), + ) + .unwrap(); + } + + block_on(futures::future::poll_fn(move |cx: &mut Context<'_>| { + loop { + match swarm.poll_next_unpin(cx) { + Poll::Ready(Some(SwarmEvent::NewListenAddr { address, .. })) => { + info!("Listening on {:?}", address); + } + Poll::Ready(Some(SwarmEvent::Behaviour(Event::Relay(event)))) => { + info!("{:?}", event) + } + Poll::Ready(Some(SwarmEvent::Behaviour(Event::Dcutr(event)))) => { + info!("{:?}", event) + } + Poll::Ready(Some(SwarmEvent::Behaviour(Event::Identify(event)))) => { + info!("{:?}", event) + } + Poll::Ready(Some(SwarmEvent::Behaviour(Event::Ping(_)))) => {} + Poll::Ready(Some(SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + })) => { + info!("Established connection to {:?} via {:?}", peer_id, endpoint); + } + Poll::Ready(Some(SwarmEvent::OutgoingConnectionError { peer_id, error })) => { + info!("Outgoing connection error to {:?}: {:?}", peer_id, error); + } + Poll::Ready(Some(_)) => {} + Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => { + break; + } + } + } + Poll::Pending + })) +} + +fn generate_ed25519(secret_key_seed: u8) -> identity::Keypair { + let mut bytes = [0u8; 32]; + bytes[0] = secret_key_seed; + + let secret_key = identity::ed25519::SecretKey::from_bytes(&mut bytes) + .expect("this returns `Err` only if the length is wrong; the length is correct; qed"); + identity::Keypair::Ed25519(secret_key.into()) +} diff --git a/protocols/dcutr/src/behaviour.rs b/protocols/dcutr/src/behaviour.rs new file mode 100644 index 00000000000..b39f33b53f9 --- /dev/null +++ b/protocols/dcutr/src/behaviour.rs @@ -0,0 +1,342 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! [`NetworkBehaviour`] to act as a direct connection upgrade through relay node. + +use crate::handler; +use either::Either; +use libp2p_core::connection::{ConnectedPoint, ConnectionId}; +use libp2p_core::multiaddr::Protocol; +use libp2p_core::{Multiaddr, PeerId}; +use libp2p_swarm::dial_opts::{self, DialOpts}; +use libp2p_swarm::{ + DialError, IntoProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, + PollParameters, ProtocolsHandler, +}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::task::{Context, Poll}; + +/// The events produced by the [`Behaviour`]. +#[derive(Debug, PartialEq, Eq)] +pub enum Event { + InitiateDirectConnectionUpgrade { + remote_peer_id: PeerId, + local_relayed_addr: Multiaddr, + }, + RemoteInitiatedDirectConnectionUpgrade { + remote_peer_id: PeerId, + remote_relayed_addr: Multiaddr, + }, + DirectConnectionUpgradeSucceeded { + remote_peer_id: PeerId, + }, + DirectConnectionUpgradeFailed { + remote_peer_id: PeerId, + }, +} + +pub struct Behaviour { + /// Queue of actions to return when polled. + queued_actions: VecDeque< + NetworkBehaviourAction< + ::OutEvent, + ::ProtocolsHandler, + >, + >, + + /// All direct (non-relayed) connections. + direct_connections: HashMap>, +} + +impl Behaviour { + pub fn new() -> Self { + Behaviour { + queued_actions: Default::default(), + direct_connections: Default::default(), + } + } +} + +impl NetworkBehaviour for Behaviour { + type ProtocolsHandler = handler::Prototype; + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ProtocolsHandler { + handler::Prototype::UnknownConnection + } + + fn addresses_of_peer(&mut self, _peer_id: &PeerId) -> Vec { + vec![] + } + + fn inject_connected(&mut self, _peer_id: &PeerId) {} + + fn inject_connection_established( + &mut self, + peer_id: &PeerId, + connection_id: &ConnectionId, + connected_point: &ConnectedPoint, + _failed_addresses: Option<&Vec>, + ) { + if crate::is_relayed_connection(connected_point) { + if connected_point.is_listener() && !self.direct_connections.contains_key(peer_id) { + // TODO: Try dialing the remote peer directly. Specification: + // + // > The protocol starts with the completion of a relay connection from A to B. Upon + // observing the new connection, the inbound peer (here B) checks the addresses + // advertised by A via identify. If that set includes public addresses, then A may + // be reachable by a direct connection, in which case B attempts a unilateral + // connection upgrade by initiating a direct connection to A. + // + // https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#the-protocol + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: *peer_id, + handler: NotifyHandler::One(*connection_id), + event: Either::Left(handler::relayed::Command::Connect { + obs_addrs: vec![], + attempt: 1, + }), + }); + let local_addr = match connected_point { + ConnectedPoint::Listener { local_addr, .. } => local_addr, + ConnectedPoint::Dialer { .. } => unreachable!("Due to outer if."), + }; + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::InitiateDirectConnectionUpgrade { + remote_peer_id: *peer_id, + local_relayed_addr: local_addr.clone(), + }, + )); + } + } else { + self.direct_connections + .entry(*peer_id) + .or_default() + .insert(*connection_id); + } + } + + fn inject_dial_failure( + &mut self, + peer_id: Option, + handler: Self::ProtocolsHandler, + _error: &DialError, + ) { + match handler { + handler::Prototype::DirectConnection { + relayed_connection_id, + role: handler::Role::Initiator { attempt }, + } => { + let peer_id = + peer_id.expect("Prototype::DirectConnection is always for known peer."); + if attempt < 3 { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer_id, + handler: NotifyHandler::One(relayed_connection_id), + event: Either::Left(handler::relayed::Command::Connect { + obs_addrs: vec![], + attempt: attempt + 1, + }), + }); + } else { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer_id, + handler: NotifyHandler::One(relayed_connection_id), + event: Either::Left( + handler::relayed::Command::UpgradeFinishedDontKeepAlive, + ), + }); + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::DirectConnectionUpgradeFailed { + remote_peer_id: peer_id, + }, + )); + } + } + _ => {} + } + } + + fn inject_disconnected(&mut self, peer_id: &PeerId) { + assert!(!self.direct_connections.contains_key(peer_id)); + } + + fn inject_connection_closed( + &mut self, + peer_id: &PeerId, + connection_id: &ConnectionId, + connected_point: &ConnectedPoint, + _handler: <::ProtocolsHandler as IntoProtocolsHandler>::Handler, + ) { + if !crate::is_relayed_connection(connected_point) { + let connections = self + .direct_connections + .get_mut(peer_id) + .expect("Peer of direct connection to be tracked."); + connections + .remove(connection_id) + .then(|| ()) + .expect("Direct connection to be tracked."); + if connections.is_empty() { + self.direct_connections.remove(peer_id); + } + } + } + + fn inject_event( + &mut self, + event_source: PeerId, + connection: ConnectionId, + handler_event: <::Handler as ProtocolsHandler>::OutEvent, + ) { + match handler_event { + Either::Left(handler::relayed::Event::InboundConnectRequest { + inbound_connect, + remote_addr, + }) => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: event_source, + handler: NotifyHandler::One(connection), + event: Either::Left(handler::relayed::Command::AcceptInboundConnect { + inbound_connect, + obs_addrs: vec![], + }), + }); + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::RemoteInitiatedDirectConnectionUpgrade { + remote_peer_id: event_source, + remote_relayed_addr: remote_addr, + }, + )); + } + Either::Left(handler::relayed::Event::InboundNegotiationFailed) => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::DirectConnectionUpgradeFailed { + remote_peer_id: event_source, + }, + )); + } + Either::Left(handler::relayed::Event::InboundConnectNegotiated(remote_addrs)) => { + self.queued_actions.push_back(NetworkBehaviourAction::Dial { + opts: DialOpts::peer_id(event_source) + .addresses(remote_addrs) + .condition(dial_opts::PeerCondition::Always) + .build(), + handler: handler::Prototype::DirectConnection { + relayed_connection_id: connection, + role: handler::Role::Listener, + }, + }); + } + Either::Left(handler::relayed::Event::OutboundNegotiationFailed) => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::DirectConnectionUpgradeFailed { + remote_peer_id: event_source, + }, + )); + } + Either::Left(handler::relayed::Event::OutboundConnectNegotiated { + remote_addrs, + attempt, + }) => { + self.queued_actions.push_back(NetworkBehaviourAction::Dial { + opts: DialOpts::peer_id(event_source) + .condition(dial_opts::PeerCondition::Always) + .addresses(remote_addrs) + .build(), + handler: handler::Prototype::DirectConnection { + relayed_connection_id: connection, + role: handler::Role::Initiator { attempt: attempt }, + }, + }); + } + Either::Right(Either::Left( + handler::direct::Event::DirectConnectionUpgradeSucceeded { + relayed_connection_id, + }, + )) => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: event_source, + handler: NotifyHandler::One(relayed_connection_id), + event: Either::Left( + handler::relayed::Command::UpgradeFinishedDontKeepAlive, + ), + }); + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::DirectConnectionUpgradeSucceeded { + remote_peer_id: event_source, + }, + )); + return; + } + Either::Right(Either::Right(event)) => void::unreachable(event), + }; + } + + fn poll( + &mut self, + _cx: &mut Context<'_>, + poll_parameters: &mut impl PollParameters, + ) -> Poll> { + if let Some(mut event) = self.queued_actions.pop_front() { + // Set obs addresses. + if let NetworkBehaviourAction::NotifyHandler { + event: + Either::Left(handler::relayed::Command::Connect { + ref mut obs_addrs, .. + }), + .. + } + | NetworkBehaviourAction::NotifyHandler { + event: + Either::Left(handler::relayed::Command::AcceptInboundConnect { + ref mut obs_addrs, + .. + }), + .. + } = &mut event + { + *obs_addrs = poll_parameters + .external_addresses() + .filter(|a| !a.addr.iter().any(|p| p == Protocol::P2pCircuit)) + .map(|a| { + a.addr + .with(Protocol::P2p((*poll_parameters.local_peer_id()).into())) + }) + .collect(); + } + + return Poll::Ready(event); + } + + Poll::Pending + } +} diff --git a/protocols/dcutr/src/handler.rs b/protocols/dcutr/src/handler.rs new file mode 100644 index 00000000000..5554c5aff6f --- /dev/null +++ b/protocols/dcutr/src/handler.rs @@ -0,0 +1,83 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::protocol; +use either::Either; +use libp2p_core::connection::ConnectionId; +use libp2p_core::upgrade::{self, DeniedUpgrade}; +use libp2p_core::{ConnectedPoint, PeerId}; +use libp2p_swarm::protocols_handler::DummyProtocolsHandler; +use libp2p_swarm::protocols_handler::SendWrapper; +use libp2p_swarm::{IntoProtocolsHandler, ProtocolsHandler}; + +pub mod direct; +pub mod relayed; + +pub enum Prototype { + DirectConnection { + role: Role, + relayed_connection_id: ConnectionId, + }, + UnknownConnection, +} + +pub enum Role { + Initiator { attempt: u8 }, + Listener, +} + +impl IntoProtocolsHandler for Prototype { + type Handler = Either>; + + fn into_handler(self, _remote_peer_id: &PeerId, endpoint: &ConnectedPoint) -> Self::Handler { + let is_relayed_connection = crate::is_relayed_connection(endpoint); + + match self { + Self::UnknownConnection => { + if is_relayed_connection { + Either::Left(relayed::Handler::new(endpoint.clone())) + } else { + Either::Right(Either::Right(DummyProtocolsHandler::default())) + } + } + Self::DirectConnection { + relayed_connection_id, + .. + } => { + assert!( + !is_relayed_connection, + "`Prototype::DirectConnection` is never created for relayed connection." + ); + Either::Right(Either::Left(direct::Handler::new(relayed_connection_id))) + } + } + } + + fn inbound_protocol(&self) -> ::InboundProtocol { + match self { + Prototype::UnknownConnection => upgrade::EitherUpgrade::A(SendWrapper( + upgrade::EitherUpgrade::A(protocol::inbound::Upgrade {}), + )), + Prototype::DirectConnection { .. } => { + upgrade::EitherUpgrade::A(SendWrapper(upgrade::EitherUpgrade::B(DeniedUpgrade))) + } + } + } +} diff --git a/protocols/dcutr/src/handler/direct.rs b/protocols/dcutr/src/handler/direct.rs new file mode 100644 index 00000000000..980e6e7f462 --- /dev/null +++ b/protocols/dcutr/src/handler/direct.rs @@ -0,0 +1,114 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! [`ProtocolsHandler`] handling direct connection upgraded through a relayed connection. + +use libp2p_core::connection::ConnectionId; +use libp2p_core::upgrade::{DeniedUpgrade, InboundUpgrade, OutboundUpgrade}; +use libp2p_swarm::{ + KeepAlive, NegotiatedSubstream, ProtocolsHandler, ProtocolsHandlerEvent, + ProtocolsHandlerUpgrErr, SubstreamProtocol, +}; +use std::task::{Context, Poll}; +use void::Void; + +#[derive(Debug)] +pub enum Event { + DirectConnectionUpgradeSucceeded { relayed_connection_id: ConnectionId }, +} + +pub struct Handler { + relayed_connection_id: ConnectionId, + reported: bool, +} + +impl Handler { + pub(crate) fn new(relayed_connection_id: ConnectionId) -> Self { + Self { + reported: false, + relayed_connection_id, + } + } +} + +impl ProtocolsHandler for Handler { + type InEvent = void::Void; + type OutEvent = Event; + type Error = ProtocolsHandlerUpgrErr; + type InboundProtocol = DeniedUpgrade; + type OutboundProtocol = DeniedUpgrade; + type OutboundOpenInfo = Void; + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + SubstreamProtocol::new(DeniedUpgrade, ()) + } + + fn inject_fully_negotiated_inbound( + &mut self, + _: >::Output, + _: Self::InboundOpenInfo, + ) { + } + + fn inject_fully_negotiated_outbound( + &mut self, + _: >::Output, + _: Self::OutboundOpenInfo, + ) { + } + + fn inject_event(&mut self, _: Self::InEvent) {} + + fn inject_dial_upgrade_error( + &mut self, + _: Self::OutboundOpenInfo, + _: ProtocolsHandlerUpgrErr< + >::Error, + >, + ) { + } + + fn connection_keep_alive(&self) -> KeepAlive { + KeepAlive::No + } + + fn poll( + &mut self, + _: &mut Context<'_>, + ) -> Poll< + ProtocolsHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + if !self.reported { + self.reported = true; + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::DirectConnectionUpgradeSucceeded { + relayed_connection_id: self.relayed_connection_id, + }, + )); + } + Poll::Pending + } +} diff --git a/protocols/dcutr/src/handler/relayed.rs b/protocols/dcutr/src/handler/relayed.rs new file mode 100644 index 00000000000..d7ca27bcfb6 --- /dev/null +++ b/protocols/dcutr/src/handler/relayed.rs @@ -0,0 +1,308 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! [`ProtocolsHandler`] handling relayed connection potentially upgraded to a direct connection. + +use crate::protocol; +use futures::future::{BoxFuture, FutureExt}; +use futures::stream::{FuturesUnordered, StreamExt}; +use instant::Instant; +use libp2p_core::either::EitherOutput; +use libp2p_core::multiaddr::Multiaddr; +use libp2p_core::upgrade::UpgradeError; +use libp2p_core::upgrade::{self, DeniedUpgrade}; +use libp2p_core::ConnectedPoint; +use libp2p_swarm::protocols_handler::{InboundUpgradeSend, OutboundUpgradeSend}; +use libp2p_swarm::{ + KeepAlive, NegotiatedSubstream, ProtocolsHandler, ProtocolsHandlerEvent, + ProtocolsHandlerUpgrErr, SubstreamProtocol, +}; +use log::debug; +use std::collections::VecDeque; +use std::fmt; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub enum Command { + Connect { + obs_addrs: Vec, + attempt: u8, + }, + AcceptInboundConnect { + obs_addrs: Vec, + inbound_connect: protocol::inbound::PendingConnect, + }, + /// Upgrading the relayed connection to a direct connection either failed for good or succeeded. + /// There is no need to keep the relayed connection alive for the sake of upgrading to a direct + /// connection. + UpgradeFinishedDontKeepAlive, +} + +impl fmt::Debug for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Command::Connect { obs_addrs, attempt } => f + .debug_struct("Command::Connect") + .field("obs_addrs", obs_addrs) + .field("attempt", attempt) + .finish(), + Command::AcceptInboundConnect { + obs_addrs, + inbound_connect: _, + } => f + .debug_struct("Command::AcceptInboundConnect") + .field("obs_addrs", obs_addrs) + .finish(), + Command::UpgradeFinishedDontKeepAlive => f + .debug_struct("Command::UpgradeFinishedDontKeepAlive") + .finish(), + } + } +} + +pub enum Event { + InboundConnectRequest { + inbound_connect: protocol::inbound::PendingConnect, + remote_addr: Multiaddr, + }, + InboundNegotiationFailed, + InboundConnectNegotiated(Vec), + OutboundNegotiationFailed, + OutboundConnectNegotiated { + remote_addrs: Vec, + attempt: u8, + }, +} + +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Event::InboundConnectRequest { + inbound_connect: _, + remote_addr, + } => f + .debug_struct("Event::InboundConnectRequest") + .field("remote_addrs", remote_addr) + .finish(), + Event::InboundNegotiationFailed => { + f.debug_tuple("Event::InboundNegotiationFailed").finish() + } + Event::InboundConnectNegotiated(addrs) => f + .debug_tuple("Event::InboundConnectNegotiated") + .field(addrs) + .finish(), + Event::OutboundNegotiationFailed => { + f.debug_tuple("Event::OutboundNegotiationFailed").finish() + } + Event::OutboundConnectNegotiated { + remote_addrs, + attempt, + } => f + .debug_struct("Event::OutboundConnectNegotiated") + .field("remote_addrs", remote_addrs) + .field("attempt", attempt) + .finish(), + } + } +} + +pub struct Handler { + endpoint: ConnectedPoint, + /// Queue of events to return when polled. + queued_events: VecDeque< + ProtocolsHandlerEvent< + ::OutboundProtocol, + ::OutboundOpenInfo, + ::OutEvent, + ::Error, + >, + >, + /// Inbound connects, accepted by the behaviour, pending completion. + inbound_connects: FuturesUnordered< + BoxFuture<'static, Result, protocol::inbound::UpgradeError>>, + >, + keep_alive: KeepAlive, +} + +impl Handler { + pub fn new(endpoint: ConnectedPoint) -> Self { + Self { + endpoint, + queued_events: Default::default(), + inbound_connects: Default::default(), + keep_alive: KeepAlive::Until(Instant::now() + Duration::from_secs(30)), + } + } +} + +impl ProtocolsHandler for Handler { + type InEvent = Command; + type OutEvent = Event; + type Error = ProtocolsHandlerUpgrErr; + type InboundProtocol = upgrade::EitherUpgrade; + type OutboundProtocol = protocol::outbound::Upgrade; + type OutboundOpenInfo = u8; // Number of upgrade attempts. + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + match self.endpoint { + ConnectedPoint::Dialer { .. } => { + SubstreamProtocol::new(upgrade::EitherUpgrade::A(protocol::inbound::Upgrade {}), ()) + } + ConnectedPoint::Listener { .. } => { + SubstreamProtocol::new(upgrade::EitherUpgrade::B(DeniedUpgrade), ()) + } + } + } + + fn inject_fully_negotiated_inbound( + &mut self, + output: >::Output, + _: Self::InboundOpenInfo, + ) { + match output { + EitherOutput::First(inbound_connect) => { + let remote_addr = match & self.endpoint { + ConnectedPoint::Dialer { address } => address.clone(), + ConnectedPoint::Listener { ..} => unreachable!("`::listen_protocol` denies all incoming substreams as a listener."), + }; + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::InboundConnectRequest { + inbound_connect, + remote_addr: remote_addr, + }, + )); + } + // A connection listener denies all incoming substreams, thus none can ever be fully negotiated. + EitherOutput::Second(output) => void::unreachable(output), + } + } + + fn inject_fully_negotiated_outbound( + &mut self, + protocol::outbound::Connect { obs_addrs }: >::Output, + attempt: Self::OutboundOpenInfo, + ) { + assert!( + self.endpoint.is_listener(), + "A connection dialer never initiates a connection upgrade." + ); + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::OutboundConnectNegotiated { + remote_addrs: obs_addrs, + attempt, + }, + )); + } + + fn inject_event(&mut self, event: Self::InEvent) { + match event { + Command::Connect { obs_addrs, attempt } => { + self.queued_events + .push_back(ProtocolsHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + protocol::outbound::Upgrade::new(obs_addrs), + attempt, + ), + }); + } + Command::AcceptInboundConnect { + inbound_connect, + obs_addrs, + } => { + self.inbound_connects + .push(inbound_connect.accept(obs_addrs).boxed()); + } + Command::UpgradeFinishedDontKeepAlive => { + self.keep_alive = KeepAlive::No; + } + } + } + + fn inject_listen_upgrade_error( + &mut self, + _: Self::InboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + if let ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) = error { + debug!("Inbound negotiation failed: {:?}", e); + self.keep_alive = KeepAlive::No; + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::InboundNegotiationFailed, + )); + } + } + + fn inject_dial_upgrade_error( + &mut self, + _open_info: Self::OutboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + if let ProtocolsHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) = error { + debug!("Outbound negotiation failed: {:?}", e); + self.keep_alive = KeepAlive::No; + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::OutboundNegotiationFailed, + )); + } + } + + fn connection_keep_alive(&self) -> KeepAlive { + self.keep_alive + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ProtocolsHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + // Return queued events. + if let Some(event) = self.queued_events.pop_front() { + return Poll::Ready(event); + } + + while let Poll::Ready(Some(result)) = self.inbound_connects.poll_next_unpin(cx) { + match result { + Ok(addresses) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::InboundConnectNegotiated(addresses), + )); + } + Err(e) => { + debug!("Inbound negotiation failed: {:?}", e); + self.keep_alive = KeepAlive::No; + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::InboundNegotiationFailed, + )); + } + } + } + + Poll::Pending + } +} diff --git a/protocols/dcutr/src/lib.rs b/protocols/dcutr/src/lib.rs new file mode 100644 index 00000000000..0c883b2720e --- /dev/null +++ b/protocols/dcutr/src/lib.rs @@ -0,0 +1,42 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! Implementation of the [libp2p direct connection upgrade through relay +//! specification](https://github.com/libp2p/specs/pull/173). + +use libp2p_core::multiaddr::Protocol; +use libp2p_core::ConnectedPoint; + +pub mod behaviour; +pub mod handler; +mod protocol; + +mod message_proto { + include!(concat!(env!("OUT_DIR"), "/holepunch.pb.rs")); +} + +fn is_relayed_connection(connected_point: &ConnectedPoint) -> bool { + match connected_point { + ConnectedPoint::Dialer { address } => address, + ConnectedPoint::Listener { local_addr, .. } => local_addr, + } + .iter() + .any(|p| p == Protocol::P2pCircuit) +} diff --git a/protocols/dcutr/src/message.proto b/protocols/dcutr/src/message.proto new file mode 100644 index 00000000000..ab5b220f2ea --- /dev/null +++ b/protocols/dcutr/src/message.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; + +package holepunch.pb; + +message HolePunch { + enum Type { + CONNECT = 100; + SYNC = 300; + } + + required Type type=1; + + // For hole punching, we'll send some additional observed addresses to the remote peer + // that could have been filtered by the Host address factory (for example: AutoRelay removes all public addresses if peer has private reachability). + // This is a hack! + // We plan to have a better address discovery and advertisement mechanism in the future. + // See https://github.com/libp2p/go-libp2p-autonat/pull/98 + repeated bytes ObsAddrs = 2; +} diff --git a/protocols/dcutr/src/protocol.rs b/protocols/dcutr/src/protocol.rs new file mode 100644 index 00000000000..d2b8b39a6d0 --- /dev/null +++ b/protocols/dcutr/src/protocol.rs @@ -0,0 +1,26 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +pub mod inbound; +pub mod outbound; + +const PROTOCOL_NAME: &[u8; 13] = b"/libp2p/dcutr"; + +const MAX_MESSAGE_SIZE_BYTES: usize = 4096; diff --git a/protocols/dcutr/src/protocol/inbound.rs b/protocols/dcutr/src/protocol/inbound.rs new file mode 100644 index 00000000000..5b03f01491d --- /dev/null +++ b/protocols/dcutr/src/protocol/inbound.rs @@ -0,0 +1,151 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::message_proto::{hole_punch, HolePunch}; +use asynchronous_codec::Framed; +use bytes::BytesMut; +use futures::{future::BoxFuture, prelude::*}; +use libp2p_core::{upgrade, Multiaddr}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::convert::TryFrom; +use std::io::Cursor; +use std::iter; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub struct Upgrade {} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(super::PROTOCOL_NAME) + } +} + +impl upgrade::InboundUpgrade for Upgrade { + type Output = PendingConnect; + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let mut codec = UviBytes::default(); + codec.set_max_len(super::MAX_MESSAGE_SIZE_BYTES); + let mut substream = Framed::new(substream, codec); + + async move { + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let HolePunch { r#type, obs_addrs } = HolePunch::decode(Cursor::new(msg))?; + + let obs_addrs = if obs_addrs.is_empty() { + return Err(UpgradeError::NoAddresses); + } else { + obs_addrs + .into_iter() + .map(TryFrom::try_from) + .collect::, _>>() + .map_err(|_| UpgradeError::InvalidAddrs)? + }; + + let r#type = hole_punch::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + + match r#type { + hole_punch::Type::Connect => {} + hole_punch::Type::Sync => return Err(UpgradeError::UnexpectedTypeSync), + } + + Ok(PendingConnect { + substream, + remote_obs_addrs: obs_addrs, + }) + } + .boxed() + } +} + +pub struct PendingConnect { + substream: Framed, + remote_obs_addrs: Vec, +} + +impl PendingConnect { + pub async fn accept( + mut self, + local_obs_addrs: Vec, + ) -> Result, UpgradeError> { + let msg = HolePunch { + r#type: hole_punch::Type::Connect.into(), + obs_addrs: local_obs_addrs.into_iter().map(|a| a.to_vec()).collect(), + }; + + let mut encoded_msg = BytesMut::new(); + msg.encode(&mut encoded_msg) + .expect("BytesMut to have sufficient capacity."); + + self.substream.send(encoded_msg.freeze()).await?; + let msg: bytes::BytesMut = self + .substream + .next() + .await + .ok_or(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let HolePunch { r#type, .. } = HolePunch::decode(Cursor::new(msg))?; + + let r#type = hole_punch::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + hole_punch::Type::Connect => return Err(UpgradeError::UnexpectedTypeConnect), + hole_punch::Type::Sync => {} + } + + Ok(self.remote_obs_addrs) + } +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode response: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error("Io error {0}")] + Io( + #[from] + #[source] + std::io::Error, + ), + #[error("Expected at least one address in reservation.")] + NoAddresses, + #[error("Invalid addresses.")] + InvalidAddrs, + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Unexpected message type 'connect'")] + UnexpectedTypeConnect, + #[error("Unexpected message type 'sync'")] + UnexpectedTypeSync, +} diff --git a/protocols/dcutr/src/protocol/outbound.rs b/protocols/dcutr/src/protocol/outbound.rs new file mode 100644 index 00000000000..5a9f96ca28b --- /dev/null +++ b/protocols/dcutr/src/protocol/outbound.rs @@ -0,0 +1,159 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::message_proto::{hole_punch, HolePunch}; +use asynchronous_codec::Framed; +use bytes::BytesMut; +use futures::{future::BoxFuture, prelude::*}; +use futures_timer::Delay; +use libp2p_core::{upgrade, Multiaddr}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::convert::TryFrom; +use std::io::Cursor; +use std::iter; +use std::time::Instant; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub struct Upgrade { + obs_addrs: Vec, +} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(super::PROTOCOL_NAME) + } +} + +impl Upgrade { + pub fn new(obs_addrs: Vec) -> Self { + Self { obs_addrs } + } +} + +impl upgrade::OutboundUpgrade for Upgrade { + type Output = Connect; + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let msg = HolePunch { + r#type: hole_punch::Type::Connect.into(), + obs_addrs: self.obs_addrs.into_iter().map(|a| a.to_vec()).collect(), + }; + + let mut encoded_msg = BytesMut::new(); + msg.encode(&mut encoded_msg) + .expect("BytesMut to have sufficient capacity."); + + let mut codec = UviBytes::default(); + codec.set_max_len(super::MAX_MESSAGE_SIZE_BYTES); + let mut substream = Framed::new(substream, codec); + + async move { + substream.send(encoded_msg.freeze()).await?; + + let sent_time = Instant::now(); + + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let rtt = sent_time.elapsed(); + + let HolePunch { r#type, obs_addrs } = HolePunch::decode(Cursor::new(msg))?; + + let r#type = hole_punch::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + hole_punch::Type::Connect => {} + hole_punch::Type::Sync => return Err(UpgradeError::UnexpectedTypeSync), + } + + let obs_addrs = if obs_addrs.is_empty() { + return Err(UpgradeError::NoAddresses); + } else { + obs_addrs + .into_iter() + .map(TryFrom::try_from) + .collect::, _>>() + .map_err(|_| UpgradeError::InvalidAddrs)? + }; + + let msg = HolePunch { + r#type: hole_punch::Type::Sync.into(), + obs_addrs: vec![], + }; + + let mut encoded_msg = BytesMut::new(); + msg.encode(&mut encoded_msg) + .expect("BytesMut to have sufficient capacity."); + + substream.send(encoded_msg.freeze()).await?; + + Delay::new(rtt / 2).await; + + Ok(Connect { obs_addrs }) + } + .boxed() + } +} + +pub struct Connect { + pub obs_addrs: Vec, +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode response: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error("Io error {0}")] + Io( + #[from] + #[source] + std::io::Error, + ), + #[error("Expected 'status' field to be set.")] + MissingStatusField, + #[error("Expected 'reservation' field to be set.")] + MissingReservationField, + #[error("Expected at least one address in reservation.")] + NoAddresses, + #[error("Invalid expiration timestamp in reservation.")] + InvalidReservationExpiration, + #[error("Invalid addresses in reservation.")] + InvalidAddrs, + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Unexpected message type 'connect'")] + UnexpectedTypeConnect, + #[error("Unexpected message type 'sync'")] + UnexpectedTypeSync, + #[error("Failed to parse response type field.")] + ParseStatusField, +} diff --git a/protocols/dcutr/tests/lib.rs b/protocols/dcutr/tests/lib.rs new file mode 100644 index 00000000000..df207b507d3 --- /dev/null +++ b/protocols/dcutr/tests/lib.rs @@ -0,0 +1,278 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use futures::executor::LocalPool; +use futures::future::FutureExt; +use futures::io::{AsyncRead, AsyncWrite}; +use futures::stream::StreamExt; +use futures::task::Spawn; +use libp2p::core::multiaddr::{Multiaddr, Protocol}; +use libp2p::core::muxing::StreamMuxerBox; +use libp2p::core::transport::{Boxed, MemoryTransport, Transport}; +use libp2p::core::PublicKey; +use libp2p::core::{identity, PeerId}; +use libp2p::dcutr; +use libp2p::ping::{Ping, PingConfig, PingEvent}; +use libp2p::plaintext::PlainText2Config; +use libp2p::relay::v2::client; +use libp2p::relay::v2::relay; +use libp2p::NetworkBehaviour; +use libp2p_swarm::{AddressScore, NetworkBehaviour, Swarm, SwarmEvent}; +use std::time::Duration; + +#[test] +fn connect() { + let _ = env_logger::try_init(); + let mut pool = LocalPool::new(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let mut relay = build_relay(); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone(), AddressScore::Infinite); + spawn_swarm_on_pool(&pool, relay); + + let mut dst = build_client(); + let dst_peer_id = *dst.local_peer_id(); + let dst_relayed_addr = relay_addr + .clone() + .with(Protocol::P2p(relay_peer_id.into())) + .with(Protocol::P2pCircuit) + .with(Protocol::P2p(dst_peer_id.into())); + let dst_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + + dst.listen_on(dst_relayed_addr.clone()).unwrap(); + dst.listen_on(dst_addr.clone()).unwrap(); + dst.add_external_address(dst_addr.clone(), AddressScore::Infinite); + + pool.run_until(wait_for_reservation( + &mut dst, + dst_relayed_addr.clone(), + relay_peer_id, + false, // No renewal. + )); + spawn_swarm_on_pool(&pool, dst); + + let mut src = build_client(); + let src_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + src.listen_on(src_addr.clone()).unwrap(); + pool.run_until(wait_for_new_listen_addr(&mut src, &src_addr)); + src.add_external_address(src_addr.clone(), AddressScore::Infinite); + + src.dial(dst_relayed_addr.clone()).unwrap(); + + pool.run_until(wait_for_connection_established(&mut src, &dst_relayed_addr)); + assert_eq!( + dcutr::behaviour::Event::RemoteInitiatedDirectConnectionUpgrade { + remote_peer_id: dst_peer_id, + remote_relayed_addr: dst_relayed_addr + }, + pool.run_until(wait_for_dcutr_event(&mut src)) + ); + pool.run_until(wait_for_connection_established( + &mut src, + &dst_addr.with(Protocol::P2p(dst_peer_id.into())), + )); +} + +fn build_relay() -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_public_key = local_key.public(); + let local_peer_id = local_public_key.clone().to_peer_id(); + + let transport = build_transport(MemoryTransport::default().boxed(), local_public_key); + + Swarm::new( + transport, + Relay { + ping: Ping::new(PingConfig::new()), + relay: relay::Relay::new( + local_peer_id, + relay::Config { + reservation_duration: Duration::from_secs(2), + ..Default::default() + }, + ), + }, + local_peer_id, + ) +} + +fn build_client() -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_public_key = local_key.public(); + let local_peer_id = local_public_key.clone().to_peer_id(); + + let (transport, behaviour) = + client::Client::new_transport_and_behaviour(local_peer_id, MemoryTransport::default()); + let transport = build_transport(transport.boxed(), local_public_key); + + Swarm::new( + transport, + Client { + ping: Ping::new(PingConfig::new()), + relay: behaviour, + dcutr: dcutr::behaviour::Behaviour::new(), + }, + local_peer_id, + ) +} + +fn build_transport( + transport: Boxed, + local_public_key: PublicKey, +) -> Boxed<(PeerId, StreamMuxerBox)> +where + StreamSink: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + let transport = transport + .upgrade() + .authenticate(PlainText2Config { local_public_key }) + .multiplex(libp2p_yamux::YamuxConfig::default()) + .boxed(); + + transport +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "RelayEvent", event_process = false)] +struct Relay { + relay: relay::Relay, + ping: Ping, +} + +#[derive(Debug)] +enum RelayEvent { + Relay(relay::Event), + Ping(PingEvent), +} + +impl From for RelayEvent { + fn from(event: relay::Event) -> Self { + RelayEvent::Relay(event) + } +} + +impl From for RelayEvent { + fn from(event: PingEvent) -> Self { + RelayEvent::Ping(event) + } +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "ClientEvent", event_process = false)] +struct Client { + relay: client::Client, + ping: Ping, + dcutr: dcutr::behaviour::Behaviour, +} + +#[derive(Debug)] +enum ClientEvent { + Relay(client::Event), + Ping(PingEvent), + Dcutr(dcutr::behaviour::Event), +} + +impl From for ClientEvent { + fn from(event: client::Event) -> Self { + ClientEvent::Relay(event) + } +} + +impl From for ClientEvent { + fn from(event: PingEvent) -> Self { + ClientEvent::Ping(event) + } +} + +impl From for ClientEvent { + fn from(event: dcutr::behaviour::Event) -> Self { + ClientEvent::Dcutr(event) + } +} + +fn spawn_swarm_on_pool(pool: &LocalPool, swarm: Swarm) { + pool.spawner() + .spawn_obj(swarm.collect::>().map(|_| ()).boxed().into()) + .unwrap(); +} + +async fn wait_for_reservation( + client: &mut Swarm, + client_addr: Multiaddr, + relay_peer_id: PeerId, + is_renewal: bool, +) { + loop { + match client.select_next_some().await { + SwarmEvent::NewListenAddr { address, .. } if address != client_addr => {} + SwarmEvent::Behaviour(ClientEvent::Relay(client::Event::ReservationReqAccepted { + relay_peer_id: peer_id, + renewal, + })) if relay_peer_id == peer_id && renewal == is_renewal => break, + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + SwarmEvent::Dialing(peer_id) if peer_id == relay_peer_id => {} + SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == relay_peer_id => {} + e => panic!("{:?}", e), + } + } + + // Wait for `NewListenAddr` event. + match client.select_next_some().await { + SwarmEvent::NewListenAddr { address, .. } if address == client_addr => {} + e => panic!("{:?}", e), + } +} + +async fn wait_for_connection_established(client: &mut Swarm, addr: &Multiaddr) { + loop { + match client.select_next_some().await { + SwarmEvent::IncomingConnection { .. } => {} + SwarmEvent::ConnectionEstablished { endpoint, .. } + if endpoint.get_remote_address() == addr => + { + break + } + SwarmEvent::Dialing(_) => {} + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + SwarmEvent::ConnectionEstablished { .. } => {} + e => panic!("{:?}", e), + } + } +} + +async fn wait_for_new_listen_addr(client: &mut Swarm, new_addr: &Multiaddr) { + match client.select_next_some().await { + SwarmEvent::NewListenAddr { address, .. } if address == *new_addr => {} + e => panic!("{:?}", e), + } +} + +async fn wait_for_dcutr_event(client: &mut Swarm) -> dcutr::behaviour::Event { + loop { + match client.select_next_some().await { + SwarmEvent::Behaviour(ClientEvent::Dcutr(e)) => return e, + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + e => panic!("{:?}", e), + } + } +} diff --git a/protocols/gossipsub/src/lib.rs b/protocols/gossipsub/src/lib.rs index a05c81806ee..774b2c04ccb 100644 --- a/protocols/gossipsub/src/lib.rs +++ b/protocols/gossipsub/src/lib.rs @@ -87,7 +87,7 @@ //! // This is test transport (memory). //! let noise_keys = libp2p_noise::Keypair::::new().into_authentic(&local_key).unwrap(); //! let transport = MemoryTransport::default() -//! .upgrade(libp2p_core::upgrade::Version::V1) +//! .upgrade() //! .authenticate(libp2p_noise::NoiseConfig::xx(noise_keys).into_authenticated()) //! .multiplex(libp2p_mplex::MplexConfig::new()) //! .boxed(); diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index f4345ed3160..fdbb3acd295 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -30,7 +30,7 @@ use std::{ use futures::StreamExt; use libp2p_core::{ - identity, multiaddr::Protocol, transport::MemoryTransport, upgrade, Multiaddr, Transport, + identity, multiaddr::Protocol, transport::MemoryTransport, Multiaddr, Transport, }; use libp2p_gossipsub::{ Gossipsub, GossipsubConfigBuilder, GossipsubEvent, IdentTopic as Topic, MessageAuthenticity, @@ -148,7 +148,7 @@ fn build_node() -> (Multiaddr, Swarm) { let public_key = key.public(); let transport = MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(PlainText2Config { local_public_key: public_key.clone(), }) diff --git a/protocols/identify/src/identify.rs b/protocols/identify/src/identify.rs index e3600e0d818..2e1baf8bc1e 100644 --- a/protocols/identify/src/identify.rs +++ b/protocols/identify/src/identify.rs @@ -518,7 +518,7 @@ fn multiaddr_matches_peer_id(addr: &Multiaddr, peer_id: &PeerId) -> bool { mod tests { use super::*; use futures::pin_mut; - use libp2p_core::{identity, muxing::StreamMuxerBox, transport, upgrade, PeerId, Transport}; + use libp2p_core::{identity, muxing::StreamMuxerBox, transport, PeerId, Transport}; use libp2p_mplex::MplexConfig; use libp2p_noise as noise; use libp2p_swarm::{Swarm, SwarmEvent}; @@ -535,7 +535,7 @@ mod tests { let pubkey = id_keys.public(); let transport = TcpConfig::new() .nodelay(true) - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(MplexConfig::new()) .boxed(); diff --git a/protocols/kad/src/behaviour/test.rs b/protocols/kad/src/behaviour/test.rs index b5d52f273fb..b9828df3fd2 100644 --- a/protocols/kad/src/behaviour/test.rs +++ b/protocols/kad/src/behaviour/test.rs @@ -33,7 +33,7 @@ use libp2p_core::{ multiaddr::{multiaddr, Multiaddr, Protocol}, multihash::{Code, Multihash, MultihashDigest}, transport::MemoryTransport, - upgrade, PeerId, Transport, + PeerId, Transport, }; use libp2p_noise as noise; use libp2p_swarm::{Swarm, SwarmEvent}; @@ -60,7 +60,7 @@ fn build_node_with_config(cfg: KademliaConfig) -> (Multiaddr, TestSwarm) { .into_authentic(&local_key) .unwrap(); let transport = MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(yamux::YamuxConfig::default()) .boxed(); diff --git a/protocols/ping/tests/ping.rs b/protocols/ping/tests/ping.rs index 3a7acd72fb1..cd4c2cb4d1d 100644 --- a/protocols/ping/tests/ping.rs +++ b/protocols/ping/tests/ping.rs @@ -250,7 +250,7 @@ fn mk_transport(muxer: MuxerChoice) -> (PeerId, transport::Boxed<(PeerId, Stream peer_id, TcpConfig::new() .nodelay(true) - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(match muxer { MuxerChoice::Yamux => upgrade::EitherUpgrade::A(yamux::YamuxConfig::default()), diff --git a/protocols/relay/Cargo.toml b/protocols/relay/Cargo.toml index 87391c985e1..867ebfa6453 100644 --- a/protocols/relay/Cargo.toml +++ b/protocols/relay/Cargo.toml @@ -21,8 +21,10 @@ libp2p-swarm = { version = "0.33.0", path = "../../swarm" } log = "0.4" pin-project = "1" prost = "0.9" -rand = "0.7" +rand = "0.8.4" smallvec = "1.6.1" +static_assertions = "1" +thiserror = "1.0" unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] } void = "1" @@ -31,10 +33,11 @@ prost-build = "0.9" [dev-dependencies] env_logger = "0.9.0" -structopt = "0.3.21" libp2p = { path = "../.." } +libp2p-identify = { path = "../identify" } libp2p-kad = { path = "../kad" } libp2p-ping = { path = "../ping" } -libp2p-identify = { path = "../identify" } libp2p-plaintext = { path = "../../transports/plaintext" } libp2p-yamux = { path = "../../muxers/yamux" } +quickcheck = "1" +structopt = "0.3.21" diff --git a/protocols/relay/build.rs b/protocols/relay/build.rs index c3a7d4bd823..a6776c9fab8 100644 --- a/protocols/relay/build.rs +++ b/protocols/relay/build.rs @@ -19,5 +19,6 @@ // DEALINGS IN THE SOFTWARE. fn main() { - prost_build::compile_protos(&["src/message.proto"], &["src"]).unwrap(); + prost_build::compile_protos(&["src/v1/message.proto"], &["src/v1"]).unwrap(); + prost_build::compile_protos(&["src/v2/message.proto"], &["src/v2"]).unwrap(); } diff --git a/protocols/relay/examples/relay.rs b/protocols/relay/examples/relay_v1.rs similarity index 97% rename from protocols/relay/examples/relay.rs rename to protocols/relay/examples/relay_v1.rs index 0cd421959f8..bd319d490fe 100644 --- a/protocols/relay/examples/relay.rs +++ b/protocols/relay/examples/relay_v1.rs @@ -61,12 +61,12 @@ use futures::executor::block_on; use futures::stream::StreamExt; use libp2p::dns::DnsConfig; use libp2p::plaintext; -use libp2p::relay::{Relay, RelayConfig}; +use libp2p::relay::v1::{Relay, RelayConfig}; use libp2p::swarm::SwarmEvent; use libp2p::tcp::TcpConfig; use libp2p::Transport; -use libp2p::{core::upgrade, identity::ed25519, ping, Multiaddr}; -use libp2p::{identity, NetworkBehaviour, PeerId, Swarm}; +use libp2p::{identity, Multiaddr, NetworkBehaviour, PeerId, Swarm}; +use libp2p::{identity::ed25519, ping}; use std::error::Error; use std::task::{Context, Poll}; use std::time::Duration; @@ -94,7 +94,7 @@ fn main() -> Result<(), Box> { ..Default::default() }; let (relay_wrapped_transport, relay_behaviour) = - libp2p_relay::new_transport_and_behaviour(relay_config, transport); + libp2p_relay::v1::new_transport_and_behaviour(relay_config, transport); let behaviour = Behaviour { relay: relay_behaviour, @@ -110,7 +110,7 @@ fn main() -> Result<(), Box> { }; let transport = relay_wrapped_transport - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(plaintext) .multiplex(libp2p_yamux::YamuxConfig::default()) .boxed(); diff --git a/protocols/relay/examples/relay_v2.rs b/protocols/relay/examples/relay_v2.rs new file mode 100644 index 00000000000..cd230b6bf67 --- /dev/null +++ b/protocols/relay/examples/relay_v2.rs @@ -0,0 +1,114 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// Copyright 2021 Protocol Labs. +// +// 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. + +use futures::executor::block_on; +use futures::stream::StreamExt; +use libp2p::core::upgrade; +use libp2p::identify::{Identify, IdentifyConfig, IdentifyEvent}; +use libp2p::noise; +use libp2p::ping::{Ping, PingConfig, PingEvent}; +use libp2p::relay::v2::relay::{self, Relay}; +use libp2p::swarm::{Swarm, SwarmEvent}; +use libp2p::tcp::TcpConfig; +use libp2p::Transport; +use libp2p::{identity, NetworkBehaviour, PeerId}; +use std::error::Error; + +fn main() -> Result<(), Box> { + env_logger::init(); + + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + println!("Local peer id: {:?}", local_peer_id); + + let tcp_transport = TcpConfig::new(); + + let noise_keys = noise::Keypair::::new() + .into_authentic(&local_key) + .expect("Signing libp2p-noise static DH keypair failed."); + + let transport = tcp_transport + .upgrade() + .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) + .multiplex(libp2p_yamux::YamuxConfig::default()) + .boxed(); + + #[derive(NetworkBehaviour)] + #[behaviour(out_event = "Event", event_process = false)] + struct Behaviour { + relay: Relay, + ping: Ping, + identify: Identify, + } + + #[derive(Debug)] + enum Event { + Ping(PingEvent), + Identify(IdentifyEvent), + Relay(relay::Event), + } + + impl From for Event { + fn from(e: PingEvent) -> Self { + Event::Ping(e) + } + } + + impl From for Event { + fn from(e: IdentifyEvent) -> Self { + Event::Identify(e) + } + } + + impl From for Event { + fn from(e: relay::Event) -> Self { + Event::Relay(e) + } + } + + let behaviour = Behaviour { + relay: Relay::new(local_peer_id, Default::default()), + ping: Ping::new(PingConfig::new()), + identify: Identify::new(IdentifyConfig::new( + "/TODO/0.0.1".to_string(), + local_key.public(), + )), + }; + + let mut swarm = Swarm::new(transport, behaviour, local_peer_id); + + // Listen on all interfaces and whatever port the OS assigns + swarm.listen_on("/ip4/0.0.0.0/tcp/4001".parse()?)?; + + block_on(async { + loop { + match swarm.next().await.expect("Infinite Stream.") { + SwarmEvent::Behaviour(Event::Relay(event)) => { + println!("{:?}", event) + } + SwarmEvent::NewListenAddr { address, .. } => { + println!("Listening on {:?}", address); + } + _ => {} + } + } + }) +} diff --git a/protocols/relay/src/lib.rs b/protocols/relay/src/lib.rs index 1b40b52f4fd..02cfefbe639 100644 --- a/protocols/relay/src/lib.rs +++ b/protocols/relay/src/lib.rs @@ -1,4 +1,5 @@ // Copyright 2019 Parity Technologies (UK) Ltd. +// Copyright 2021 Protocol Labs. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), @@ -18,107 +19,12 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -//! Implementation of the [libp2p circuit relay -//! specification](https://github.com/libp2p/specs/tree/master/relay). -//! -//! ## Example -//! -//! ```rust -//! # use libp2p_core::transport::memory::MemoryTransport; -//! # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; -//! # use libp2p_swarm::{Swarm, dial_opts::DialOpts}; -//! # use libp2p_core::{identity, Multiaddr, multiaddr::Protocol, PeerId, upgrade, Transport}; -//! # use libp2p_yamux::YamuxConfig; -//! # use libp2p_plaintext::PlainText2Config; -//! # use std::convert::TryInto; -//! # use std::str::FromStr; -//! # -//! # let local_key = identity::Keypair::generate_ed25519(); -//! # let local_public_key = local_key.public(); -//! # let local_peer_id = local_public_key.to_peer_id(); -//! # let plaintext = PlainText2Config { -//! # local_public_key: local_public_key.clone(), -//! # }; -//! # -//! let (relay_transport, relay_behaviour) = new_transport_and_behaviour( -//! RelayConfig::default(), -//! MemoryTransport::default(), -//! ); -//! -//! let transport = relay_transport -//! .upgrade(upgrade::Version::V1) -//! .authenticate(plaintext) -//! .multiplex(YamuxConfig::default()) -//! .boxed(); -//! -//! let mut swarm = Swarm::new(transport, relay_behaviour, local_peer_id); -//! -//! let relay_addr = Multiaddr::from_str("/memory/1234").unwrap() -//! .with(Protocol::P2p(PeerId::random().into())) -//! .with(Protocol::P2pCircuit); -//! let dst_addr = relay_addr.clone().with(Protocol::Memory(5678)); -//! -//! // Listen for incoming connections via relay node (1234). -//! swarm.listen_on(relay_addr).unwrap(); -//! -//! // Dial node (5678) via relay node (1234). -//! swarm.dial(dst_addr).unwrap(); -//! ``` -//! -//! ## Terminology -//! -//! ### Entities -//! -//! - **Source**: The node initiating a connection via a *relay* to a *destination*. -//! -//! - **Relay**: The node being asked by a *source* to relay to a *destination*. -//! -//! - **Destination**: The node contacted by the *source* via the *relay*. -//! -//! ### Messages -//! -//! - **Outgoing relay request**: The request sent by a *source* to a *relay*. -//! -//! - **Incoming relay request**: The request received by a *relay* from a *source*. -//! -//! - **Outgoing destination request**: The request sent by a *relay* to a *destination*. -//! -//! - **Incoming destination request**: The request received by a *destination* from a *relay*. +//! libp2p circuit relay implementations -mod behaviour; +pub mod v1; +pub mod v2; -mod message_proto { - include!(concat!(env!("OUT_DIR"), "/message.pb.rs")); -} - -mod handler; -mod protocol; -mod transport; - -pub use behaviour::{Relay, RelayConfig}; -pub use transport::{RelayError, RelayTransport}; - -use libp2p_core::Transport; - -/// Create both a [`RelayTransport`] wrapping the provided [`Transport`] -/// as well as a [`Relay`] [`NetworkBehaviour`](libp2p_swarm::NetworkBehaviour). -/// -/// Interconnects the returned [`RelayTransport`] and [`Relay`]. -pub fn new_transport_and_behaviour( - config: RelayConfig, - transport: T, -) -> (RelayTransport, Relay) { - let (transport, from_transport) = RelayTransport::new(transport); - let behaviour = Relay::new(config, from_transport); - (transport, behaviour) -} - -/// The ID of an outgoing / incoming, relay / destination request. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct RequestId(u64); - -impl RequestId { - fn new() -> RequestId { - RequestId(rand::random()) - } +// Check that we can safely cast a `usize` to a `u64`. +static_assertions::const_assert! { + std::mem::size_of::() <= std::mem::size_of::() } diff --git a/protocols/relay/src/v1.rs b/protocols/relay/src/v1.rs new file mode 100644 index 00000000000..be86d7cad47 --- /dev/null +++ b/protocols/relay/src/v1.rs @@ -0,0 +1,126 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// 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. + +//! Implementation of the [libp2p circuit relay v1 +//! specification](https://github.com/libp2p/specs/tree/master/relay). +//! +//! ## Example +//! +//! ```rust +//! # use libp2p_core::transport::memory::MemoryTransport; +//! # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; +//! # use libp2p_swarm::{Swarm, dial_opts::DialOpts}; +//! # use libp2p_core::{identity, Multiaddr, multiaddr::Protocol, PeerId, Transport}; +//! # use libp2p_yamux::YamuxConfig; +//! # use libp2p_plaintext::PlainText2Config; +//! # use std::convert::TryInto; +//! # use std::str::FromStr; +//! # +//! # let local_key = identity::Keypair::generate_ed25519(); +//! # let local_public_key = local_key.public(); +//! # let local_peer_id = local_public_key.to_peer_id(); +//! # let plaintext = PlainText2Config { +//! # local_public_key: local_public_key.clone(), +//! # }; +//! # +//! let (relay_transport, relay_behaviour) = new_transport_and_behaviour( +//! RelayConfig::default(), +//! MemoryTransport::default(), +//! ); +//! +//! let transport = relay_transport +//! .upgrade() +//! .authenticate(plaintext) +//! .multiplex(YamuxConfig::default()) +//! .boxed(); +//! +//! let mut swarm = Swarm::new(transport, relay_behaviour, local_peer_id); +//! +//! let relay_addr = Multiaddr::from_str("/memory/1234").unwrap() +//! .with(Protocol::P2p(PeerId::random().into())) +//! .with(Protocol::P2pCircuit); +//! let dst_addr = relay_addr.clone().with(Protocol::Memory(5678)); +//! +//! // Listen for incoming connections via relay node (1234). +//! swarm.listen_on(relay_addr).unwrap(); +//! +//! // Dial node (5678) via relay node (1234). +//! swarm.dial(dst_addr).unwrap(); +//! ``` +//! +//! ## Terminology +//! +//! ### Entities +//! +//! - **Source**: The node initiating a connection via a *relay* to a *destination*. +//! +//! - **Relay**: The node being asked by a *source* to relay to a *destination*. +//! +//! - **Destination**: The node contacted by the *source* via the *relay*. +//! +//! ### Messages +//! +//! - **Outgoing relay request**: The request sent by a *source* to a *relay*. +//! +//! - **Incoming relay request**: The request received by a *relay* from a *source*. +//! +//! - **Outgoing destination request**: The request sent by a *relay* to a *destination*. +//! +//! - **Incoming destination request**: The request received by a *destination* from a *relay*. + +mod behaviour; +mod connection; +mod copy_future; +mod handler; +mod protocol; +mod transport; + +pub use behaviour::{Relay, RelayConfig}; +pub use connection::Connection; +pub use transport::{RelayError, RelayListener, RelayTransport}; + +use libp2p_core::Transport; + +mod message_proto { + include!(concat!(env!("OUT_DIR"), "/message_v1.pb.rs")); +} + +/// Create both a [`RelayTransport`] wrapping the provided [`Transport`] +/// as well as a [`Relay`] [`NetworkBehaviour`](libp2p_swarm::NetworkBehaviour). +/// +/// Interconnects the returned [`RelayTransport`] and [`Relay`]. +pub fn new_transport_and_behaviour( + config: RelayConfig, + transport: T, +) -> (RelayTransport, Relay) { + let (transport, from_transport) = RelayTransport::new(transport); + let behaviour = Relay::new(config, from_transport); + (transport, behaviour) +} + +/// The ID of an outgoing / incoming, relay / destination request. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct RequestId(u64); + +impl RequestId { + fn new() -> RequestId { + RequestId(rand::random()) + } +} diff --git a/protocols/relay/src/behaviour.rs b/protocols/relay/src/v1/behaviour.rs similarity index 97% rename from protocols/relay/src/behaviour.rs rename to protocols/relay/src/v1/behaviour.rs index 2ad2023fbcf..953be32d6bd 100644 --- a/protocols/relay/src/behaviour.rs +++ b/protocols/relay/src/v1/behaviour.rs @@ -18,11 +18,12 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::handler::{RelayHandlerConfig, RelayHandlerEvent, RelayHandlerIn, RelayHandlerProto}; -use crate::message_proto::circuit_relay; -use crate::protocol; -use crate::transport::TransportToBehaviourMsg; -use crate::RequestId; +use crate::v1::handler::{ + RelayHandlerConfig, RelayHandlerEvent, RelayHandlerIn, RelayHandlerProto, +}; +use crate::v1::message_proto::circuit_relay; +use crate::v1::transport::{OutgoingRelayReqError, TransportToBehaviourMsg}; +use crate::v1::{protocol, Connection, RequestId}; use futures::channel::{mpsc, oneshot}; use futures::prelude::*; use libp2p_core::connection::{ConnectedPoint, ConnectionId, ListenerId}; @@ -40,10 +41,10 @@ use std::time::Duration; /// Network behaviour allowing the local node to act as a source, a relay and a destination. pub struct Relay { config: RelayConfig, - /// Channel receiver from [`crate::RelayTransport`]. + /// Channel receiver from [`crate::v1::RelayTransport`]. from_transport: mpsc::Receiver, - /// Events that need to be send to a [`RelayListener`](crate::transport::RelayListener) via + /// Events that need to be send to a [`RelayListener`](crate::v1::RelayListener) via /// [`Self::listeners`] or [`Self::listener_any_relay`]. outbox_to_listeners: VecDeque<(PeerId, BehaviourToListenerMsg)>, /// Events that need to be yielded to the outside when polling. @@ -84,11 +85,11 @@ struct OutgoingDialingRelayReq { relay_addr: Multiaddr, dst_addr: Option, dst_peer_id: PeerId, - send_back: oneshot::Sender>, + send_back: oneshot::Sender>, } struct OutgoingUpgradingRelayReq { - send_back: oneshot::Sender>, + send_back: oneshot::Sender>, } enum IncomingRelayReq { @@ -772,18 +773,6 @@ impl NetworkBehaviour for Relay { } } -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum BehaviourToListenerMsg { - ConnectionToRelayEstablished, - IncomingRelayedConnection { - stream: protocol::Connection, - src_peer_id: PeerId, - relay_peer_id: PeerId, - relay_addr: Multiaddr, - }, -} - enum RelayListener { Connecting { relay_addr: Multiaddr, @@ -797,7 +786,7 @@ enum RelayListener { impl RelayListener { /// Returns whether the channel to the - /// [`RelayListener`](crate::transport::RelayListener) is closed. + /// [`RelayListener`](crate::v1::RelayListener) is closed. fn is_closed(&self) -> bool { match self { RelayListener::Connecting { to_listener, .. } @@ -806,7 +795,14 @@ impl RelayListener { } } -#[derive(Debug, Eq, PartialEq)] -pub enum OutgoingRelayReqError { - DialingRelay, +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum BehaviourToListenerMsg { + ConnectionToRelayEstablished, + IncomingRelayedConnection { + stream: Connection, + src_peer_id: PeerId, + relay_peer_id: PeerId, + relay_addr: Multiaddr, + }, } diff --git a/protocols/relay/src/v1/connection.rs b/protocols/relay/src/v1/connection.rs new file mode 100644 index 00000000000..99b12d04a42 --- /dev/null +++ b/protocols/relay/src/v1/connection.rs @@ -0,0 +1,95 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// 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. + +use bytes::Bytes; +use futures::channel::oneshot; +use futures::io::{AsyncRead, AsyncWrite}; +use libp2p_swarm::NegotiatedSubstream; +use std::io::{Error, IoSlice}; +use std::pin::Pin; +use std::task::{Context, Poll}; + +/// A [`NegotiatedSubstream`] acting as a relayed [`Connection`]. +#[derive(Debug)] +pub struct Connection { + /// [`Connection`] might at first return data, that was already read during relay negotiation. + initial_data: Bytes, + stream: NegotiatedSubstream, + /// Notifies the other side of the channel of this [`Connection`] being dropped. + _notifier: oneshot::Sender<()>, +} + +impl Unpin for Connection {} + +impl Connection { + pub fn new( + initial_data: Bytes, + stream: NegotiatedSubstream, + notifier: oneshot::Sender<()>, + ) -> Self { + Connection { + initial_data, + stream, + + _notifier: notifier, + } + } +} + +impl AsyncWrite for Connection { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.stream).poll_write(cx, buf) + } + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Pin::new(&mut self.stream).poll_flush(cx) + } + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Pin::new(&mut self.stream).poll_close(cx) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context, + bufs: &[IoSlice], + ) -> Poll> { + Pin::new(&mut self.stream).poll_write_vectored(cx, bufs) + } +} + +impl AsyncRead for Connection { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + if !self.initial_data.is_empty() { + let n = std::cmp::min(self.initial_data.len(), buf.len()); + let data = self.initial_data.split_to(n); + buf[0..n].copy_from_slice(&data[..]); + return Poll::Ready(Ok(n)); + } + + Pin::new(&mut self.stream).poll_read(cx, buf) + } +} diff --git a/protocols/relay/src/protocol/copy_future.rs b/protocols/relay/src/v1/copy_future.rs similarity index 100% rename from protocols/relay/src/protocol/copy_future.rs rename to protocols/relay/src/v1/copy_future.rs diff --git a/protocols/relay/src/handler.rs b/protocols/relay/src/v1/handler.rs similarity index 98% rename from protocols/relay/src/handler.rs rename to protocols/relay/src/v1/handler.rs index 792ffd50534..4da3ed93f14 100644 --- a/protocols/relay/src/handler.rs +++ b/protocols/relay/src/v1/handler.rs @@ -18,9 +18,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::circuit_relay; -use crate::protocol; -use crate::RequestId; +use crate::v1::message_proto::circuit_relay; +use crate::v1::{protocol, Connection, RequestId}; use futures::channel::oneshot::{self, Canceled}; use futures::future::BoxFuture; use futures::prelude::*; @@ -93,10 +92,7 @@ pub struct RelayHandler { accept_dst_futures: FuturesUnordered< BoxFuture< 'static, - Result< - (PeerId, protocol::Connection, oneshot::Receiver<()>), - protocol::IncomingDstReqError, - >, + Result<(PeerId, Connection, oneshot::Receiver<()>), protocol::IncomingDstReqError>, >, >, /// Futures that copy from a source to a destination. @@ -108,8 +104,8 @@ pub struct RelayHandler { /// Queue of events to return when polled. queued_events: Vec, /// Tracks substreams lend out to other [`RelayHandler`]s or as - /// [`Connection`](protocol::Connection) to the - /// [`RelayTransport`](crate::RelayTransport). + /// [`Connection`](Connection) to the + /// [`RelayTransport`](crate::v1::RelayTransport). /// /// For each substream to the peer of this handler, there is a future in here that resolves once /// the given substream is dropped. @@ -169,7 +165,7 @@ pub enum RelayHandlerEvent { /// > **Note**: There is no proof that we are actually communicating with the destination. An /// > encryption handshake has to be performed on top of this substream in order to /// > avoid MITM attacks. - OutgoingRelayReqSuccess(PeerId, RequestId, protocol::Connection), + OutgoingRelayReqSuccess(PeerId, RequestId, Connection), /// The local node has accepted an incoming destination request. Contains a substream that /// communicates with the source. @@ -178,7 +174,7 @@ pub enum RelayHandlerEvent { /// > encryption handshake has to be performed on top of this substream in order to /// > avoid MITM attacks. IncomingDstReqSuccess { - stream: protocol::Connection, + stream: Connection, src_peer_id: PeerId, relay_peer_id: PeerId, relay_addr: Multiaddr, @@ -429,6 +425,7 @@ impl ProtocolsHandler for RelayHandler { "Can not successfully dial a destination when actually dialing a relay." ), }; + // TODO: Should this not be driven by the src handler? self.copy_futures .push(incoming_relay_req.fulfill(to_dest_substream, from_dst_read_buffer)); } diff --git a/protocols/relay/src/message.proto b/protocols/relay/src/v1/message.proto similarity index 98% rename from protocols/relay/src/message.proto rename to protocols/relay/src/v1/message.proto index dfaf0411eea..0366c9ef9ca 100644 --- a/protocols/relay/src/message.proto +++ b/protocols/relay/src/v1/message.proto @@ -1,5 +1,5 @@ syntax = "proto2"; -package message.pb; +package message_v1.pb; message CircuitRelay { diff --git a/protocols/relay/src/protocol.rs b/protocols/relay/src/v1/protocol.rs similarity index 65% rename from protocols/relay/src/protocol.rs rename to protocols/relay/src/v1/protocol.rs index 900d16a6d74..274ad39d3ac 100644 --- a/protocols/relay/src/protocol.rs +++ b/protocols/relay/src/v1/protocol.rs @@ -18,28 +18,12 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::circuit_relay; +use crate::v1::message_proto::circuit_relay; -use bytes::Bytes; -use futures::channel::oneshot; -use futures::io::{AsyncRead, AsyncWrite}; use libp2p_core::{multiaddr::Error as MultiaddrError, Multiaddr, PeerId}; -use libp2p_swarm::NegotiatedSubstream; use smallvec::SmallVec; -use std::io::{Error, IoSlice}; -use std::pin::Pin; -use std::task::{Context, Poll}; use std::{convert::TryFrom, error, fmt}; -/// Any message received on the wire whose length exceeds this value is refused. -// -// The circuit relay specification sets a maximum of 1024 bytes per multiaddr. A single message can -// contain multiple addresses for both the source and destination node. Setting the maximum message -// length to 10 times that limit is an unproven estimate. Feel free to refine this in the future. -const MAX_ACCEPTED_MESSAGE_LEN: usize = 10 * 1024; - -const PROTOCOL_NAME: &[u8; 27] = b"/libp2p/circuit/relay/0.1.0"; - // Source -> Relay mod incoming_relay_req; mod outgoing_relay_req; @@ -55,7 +39,14 @@ pub use self::outgoing_dst_req::{OutgoingDstReq, OutgoingDstReqError}; mod listen; pub use self::listen::{RelayListen, RelayListenError, RelayRemoteReq}; -pub mod copy_future; +/// Any message received on the wire whose length exceeds this value is refused. +// +// The circuit relay specification sets a maximum of 1024 bytes per multiaddr. A single message can +// contain multiple addresses for both the source and destination node. Setting the maximum message +// length to 10 times that limit is an unproven estimate. Feel free to refine this in the future. +const MAX_ACCEPTED_MESSAGE_LEN: usize = 10 * 1024; + +const PROTOCOL_NAME: &[u8; 27] = b"/libp2p/circuit/relay/0.1.0"; /// Representation of a `CircuitRelay_Peer` protobuf message with refined field types. /// @@ -112,71 +103,3 @@ impl error::Error for PeerParseError { } } } - -/// A [`NegotiatedSubstream`] acting as a relayed [`Connection`]. -#[derive(Debug)] -pub struct Connection { - /// [`Connection`] might at first return data, that was already read during relay negotiation. - initial_data: Bytes, - stream: NegotiatedSubstream, - /// Notifies the other side of the channel of this [`Connection`] being dropped. - _notifier: oneshot::Sender<()>, -} - -impl Unpin for Connection {} - -impl Connection { - fn new( - initial_data: Bytes, - stream: NegotiatedSubstream, - notifier: oneshot::Sender<()>, - ) -> Self { - Connection { - initial_data, - stream, - - _notifier: notifier, - } - } -} - -impl AsyncWrite for Connection { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.stream).poll_write(cx, buf) - } - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - Pin::new(&mut self.stream).poll_flush(cx) - } - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - Pin::new(&mut self.stream).poll_close(cx) - } - - fn poll_write_vectored( - mut self: Pin<&mut Self>, - cx: &mut Context, - bufs: &[IoSlice], - ) -> Poll> { - Pin::new(&mut self.stream).poll_write_vectored(cx, bufs) - } -} - -impl AsyncRead for Connection { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - if !self.initial_data.is_empty() { - let n = std::cmp::min(self.initial_data.len(), buf.len()); - let data = self.initial_data.split_to(n); - buf[0..n].copy_from_slice(&data[..]); - return Poll::Ready(Ok(n)); - } - - Pin::new(&mut self.stream).poll_read(cx, buf) - } -} diff --git a/protocols/relay/src/protocol/incoming_dst_req.rs b/protocols/relay/src/v1/protocol/incoming_dst_req.rs similarity index 94% rename from protocols/relay/src/protocol/incoming_dst_req.rs rename to protocols/relay/src/v1/protocol/incoming_dst_req.rs index 589cbc3d709..21ed1cb83a8 100644 --- a/protocols/relay/src/protocol/incoming_dst_req.rs +++ b/protocols/relay/src/v1/protocol/incoming_dst_req.rs @@ -18,8 +18,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::{circuit_relay, CircuitRelay}; -use crate::protocol::Peer; +use crate::v1::message_proto::{circuit_relay, CircuitRelay}; +use crate::v1::protocol::Peer; +use crate::v1::Connection; use asynchronous_codec::{Framed, FramedParts}; use bytes::BytesMut; @@ -69,10 +70,8 @@ impl IncomingDstReq { /// stream then points to the source (as retreived with `src_id()` and `src_addrs()`). pub fn accept( self, - ) -> BoxFuture< - 'static, - Result<(PeerId, super::Connection, oneshot::Receiver<()>), IncomingDstReqError>, - > { + ) -> BoxFuture<'static, Result<(PeerId, Connection, oneshot::Receiver<()>), IncomingDstReqError>> + { let IncomingDstReq { mut stream, src } = self; let msg = CircuitRelay { r#type: Some(circuit_relay::Type::Status.into()), @@ -102,7 +101,7 @@ impl IncomingDstReq { Ok(( src.peer_id, - super::Connection::new(read_buffer.freeze(), io, tx), + Connection::new(read_buffer.freeze(), io, tx), rx, )) } diff --git a/protocols/relay/src/protocol/incoming_relay_req.rs b/protocols/relay/src/v1/protocol/incoming_relay_req.rs similarity index 97% rename from protocols/relay/src/protocol/incoming_relay_req.rs rename to protocols/relay/src/v1/protocol/incoming_relay_req.rs index 948a2281f5b..97f1b96ce3f 100644 --- a/protocols/relay/src/protocol/incoming_relay_req.rs +++ b/protocols/relay/src/v1/protocol/incoming_relay_req.rs @@ -18,9 +18,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use super::copy_future::CopyFuture; -use crate::message_proto::{circuit_relay, circuit_relay::Status, CircuitRelay}; -use crate::protocol::Peer; +use crate::v1::copy_future::CopyFuture; +use crate::v1::message_proto::{circuit_relay, circuit_relay::Status, CircuitRelay}; +use crate::v1::protocol::Peer; use asynchronous_codec::{Framed, FramedParts}; use bytes::{Bytes, BytesMut}; diff --git a/protocols/relay/src/protocol/listen.rs b/protocols/relay/src/v1/protocol/listen.rs similarity index 95% rename from protocols/relay/src/protocol/listen.rs rename to protocols/relay/src/v1/protocol/listen.rs index 168861a2715..87d77458050 100644 --- a/protocols/relay/src/protocol/listen.rs +++ b/protocols/relay/src/v1/protocol/listen.rs @@ -18,10 +18,11 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::{circuit_relay, CircuitRelay}; -use crate::protocol::incoming_dst_req::IncomingDstReq; -use crate::protocol::incoming_relay_req::IncomingRelayReq; -use crate::protocol::{Peer, PeerParseError, MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; +use crate::v1::message_proto::{circuit_relay, CircuitRelay}; +use crate::v1::protocol::incoming_dst_req::IncomingDstReq; +use crate::v1::protocol::incoming_relay_req::IncomingRelayReq; +use crate::v1::protocol::{Peer, PeerParseError}; +use crate::v1::protocol::{MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; use asynchronous_codec::Framed; use futures::channel::oneshot; use futures::{future::BoxFuture, prelude::*}; diff --git a/protocols/relay/src/protocol/outgoing_dst_req.rs b/protocols/relay/src/v1/protocol/outgoing_dst_req.rs similarity index 98% rename from protocols/relay/src/protocol/outgoing_dst_req.rs rename to protocols/relay/src/v1/protocol/outgoing_dst_req.rs index 181e31ef4a8..60871b17a93 100644 --- a/protocols/relay/src/protocol/outgoing_dst_req.rs +++ b/protocols/relay/src/v1/protocol/outgoing_dst_req.rs @@ -18,8 +18,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::{circuit_relay, CircuitRelay}; -use crate::protocol::{Peer, MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; +use crate::v1::message_proto::{circuit_relay, CircuitRelay}; +use crate::v1::protocol::Peer; +use crate::v1::protocol::{MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; use asynchronous_codec::{Framed, FramedParts}; use bytes::Bytes; use futures::future::BoxFuture; diff --git a/protocols/relay/src/protocol/outgoing_relay_req.rs b/protocols/relay/src/v1/protocol/outgoing_relay_req.rs similarity index 96% rename from protocols/relay/src/protocol/outgoing_relay_req.rs rename to protocols/relay/src/v1/protocol/outgoing_relay_req.rs index a9d450b04d7..b6e062d304e 100644 --- a/protocols/relay/src/protocol/outgoing_relay_req.rs +++ b/protocols/relay/src/v1/protocol/outgoing_relay_req.rs @@ -18,8 +18,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::message_proto::{circuit_relay, CircuitRelay}; -use crate::protocol::{MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; +use crate::v1::message_proto::{circuit_relay, CircuitRelay}; +use crate::v1::protocol::{MAX_ACCEPTED_MESSAGE_LEN, PROTOCOL_NAME}; +use crate::v1::Connection; use asynchronous_codec::{Framed, FramedParts}; use futures::channel::oneshot; use futures::future::BoxFuture; @@ -68,7 +69,7 @@ impl upgrade::UpgradeInfo for OutgoingRelayReq { } impl upgrade::OutboundUpgrade for OutgoingRelayReq { - type Output = (super::Connection, oneshot::Receiver<()>); + type Output = (Connection, oneshot::Receiver<()>); type Error = OutgoingRelayReqError; type Future = BoxFuture<'static, Result>; @@ -156,7 +157,7 @@ impl upgrade::OutboundUpgrade for OutgoingRelayReq { let (tx, rx) = oneshot::channel(); - Ok((super::Connection::new(read_buffer.freeze(), io, tx), rx)) + Ok((Connection::new(read_buffer.freeze(), io, tx), rx)) } .boxed() } diff --git a/protocols/relay/src/transport.rs b/protocols/relay/src/v1/transport.rs similarity index 96% rename from protocols/relay/src/transport.rs rename to protocols/relay/src/v1/transport.rs index b410faf0af4..811541793e8 100644 --- a/protocols/relay/src/transport.rs +++ b/protocols/relay/src/v1/transport.rs @@ -18,9 +18,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::behaviour::{BehaviourToListenerMsg, OutgoingRelayReqError}; -use crate::protocol; -use crate::RequestId; +use crate::v1::behaviour::BehaviourToListenerMsg; +use crate::v1::{Connection, RequestId}; use futures::channel::mpsc; use futures::channel::oneshot; use futures::future::{BoxFuture, Future, FutureExt}; @@ -43,7 +42,7 @@ use std::task::{Context, Poll}; /// ``` /// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, Transport}; /// # use libp2p_core::transport::memory::MemoryTransport; -/// # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; +/// # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; /// # let inner_transport = MemoryTransport::default(); /// # let (relay_transport, relay_behaviour) = new_transport_and_behaviour( /// # RelayConfig::default(), @@ -57,7 +56,7 @@ use std::task::{Context, Poll}; /// ``` /// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, PeerId, Transport}; /// # use libp2p_core::transport::memory::MemoryTransport; -/// # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; +/// # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; /// # let inner_transport = MemoryTransport::default(); /// # let (relay_transport, relay_behaviour) = new_transport_and_behaviour( /// # RelayConfig::default(), @@ -77,7 +76,7 @@ use std::task::{Context, Poll}; /// ``` /// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, PeerId, Transport}; /// # use libp2p_core::transport::memory::MemoryTransport; -/// # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; +/// # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; /// # let inner_transport = MemoryTransport::default(); /// # let (relay_transport, relay_behaviour) = new_transport_and_behaviour( /// # RelayConfig::default(), @@ -98,7 +97,7 @@ use std::task::{Context, Poll}; /// ``` /// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, PeerId, Transport}; /// # use libp2p_core::transport::memory::MemoryTransport; -/// # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; +/// # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; /// # let inner_transport = MemoryTransport::default(); /// # let (relay_transport, relay_behaviour) = new_transport_and_behaviour( /// # RelayConfig::default(), @@ -121,7 +120,7 @@ impl RelayTransport { /// ///``` /// # use libp2p_core::transport::dummy::DummyTransport; - /// # use libp2p_relay::{RelayConfig, new_transport_and_behaviour}; + /// # use libp2p_relay::v1::{RelayConfig, new_transport_and_behaviour}; /// /// let inner_transport = DummyTransport::<()>::new(); /// let (relay_transport, relay_behaviour) = new_transport_and_behaviour( @@ -143,7 +142,7 @@ impl RelayTransport { } impl Transport for RelayTransport { - type Output = EitherOutput<::Output, protocol::Connection>; + type Output = EitherOutput<::Output, Connection>; type Error = EitherError<::Error, RelayError>; type Listener = RelayListener; type ListenerUpgrade = RelayedListenerUpgrade; @@ -427,17 +426,17 @@ impl Stream for RelayListener { } } -pub type RelayedDial = BoxFuture<'static, Result>; +pub type RelayedDial = BoxFuture<'static, Result>; #[pin_project(project = RelayedListenerUpgradeProj)] pub enum RelayedListenerUpgrade { Inner(#[pin] ::ListenerUpgrade), - Relayed(Option), + Relayed(Option), } impl Future for RelayedListenerUpgrade { type Output = Result< - EitherOutput<::Output, protocol::Connection>, + EitherOutput<::Output, Connection>, EitherError<::Error, RelayError>, >; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -534,7 +533,7 @@ impl std::fmt::Display for RelayError { impl std::error::Error for RelayError {} -/// Message from the [`RelayTransport`] to the [`Relay`](crate::Relay) +/// Message from the [`RelayTransport`] to the [`Relay`](crate::v1::Relay) /// [`NetworkBehaviour`](libp2p_swarm::NetworkBehaviour). pub enum TransportToBehaviourMsg { /// Dial destination node via relay node. @@ -544,7 +543,7 @@ pub enum TransportToBehaviourMsg { relay_peer_id: PeerId, dst_addr: Option, dst_peer_id: PeerId, - send_back: oneshot::Sender>, + send_back: oneshot::Sender>, }, /// Listen for incoming relayed connections via relay node. ListenReq { @@ -555,3 +554,8 @@ pub enum TransportToBehaviourMsg { to_listener: mpsc::Sender, }, } + +#[derive(Debug, Eq, PartialEq)] +pub enum OutgoingRelayReqError { + DialingRelay, +} diff --git a/protocols/relay/src/v2.rs b/protocols/relay/src/v2.rs new file mode 100644 index 00000000000..59c34e68561 --- /dev/null +++ b/protocols/relay/src/v2.rs @@ -0,0 +1,41 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! Implementation of the [libp2p circuit relay v2 +//! specification](https://github.com/libp2p/specs/issues/314). + +mod message_proto { + include!(concat!(env!("OUT_DIR"), "/message_v2.pb.rs")); +} + +pub mod client; +mod copy_future; +pub mod protocol; +pub mod relay; + +/// The ID of an outgoing / incoming, relay / destination request. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct RequestId(u64); + +impl RequestId { + fn new() -> RequestId { + RequestId(rand::random()) + } +} diff --git a/protocols/relay/src/v2/client.rs b/protocols/relay/src/v2/client.rs new file mode 100644 index 00000000000..ce6ae00c33e --- /dev/null +++ b/protocols/relay/src/v2/client.rs @@ -0,0 +1,561 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! [`NetworkBehaviour`] to act as a circuit relay v2 **client**. + +mod handler; +mod transport; + +use crate::v2::protocol::inbound_stop; +use bytes::Bytes; +use futures::channel::mpsc::{Receiver, Sender}; +use futures::channel::oneshot; +use futures::future::{BoxFuture, FutureExt}; +use futures::io::{AsyncRead, AsyncWrite}; +use futures::ready; +use futures::stream::StreamExt; +use libp2p_core::connection::{ConnectedPoint, ConnectionId}; +use libp2p_core::{Multiaddr, PeerId, Transport}; +use libp2p_swarm::dial_opts::DialOpts; +use libp2p_swarm::{ + DialError, NegotiatedSubstream, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, + PollParameters, +}; +use std::collections::{HashMap, VecDeque}; +use std::io::{Error, IoSlice}; +use std::pin::Pin; +use std::task::{Context, Poll}; + +/// The events produced by the [`Client`] behaviour. +#[derive(Debug)] +pub enum Event { + /// An outbound reservation has been accepted. + ReservationReqAccepted { + relay_peer_id: PeerId, + /// Indicates whether the request replaces an existing reservation. + renewal: bool, + }, + ReservationReqFailed { + relay_peer_id: PeerId, + /// Indicates whether the request replaces an existing reservation. + renewal: bool, + }, + OutboundCircuitReqFailed { + relay_peer_id: PeerId, + }, + /// An inbound circuit request has been denied. + InboundCircuitReqDenied { + src_peer_id: PeerId, + }, + /// Denying an inbound circuit request failed. + InboundCircuitReqDenyFailed { + src_peer_id: PeerId, + error: std::io::Error, + }, +} + +pub struct Client { + local_peer_id: PeerId, + + from_transport: Receiver, + connected_peers: HashMap>, + rqsts_pending_connection: HashMap>, + + /// Queue of actions to return when polled. + queued_actions: VecDeque>, +} + +impl Client { + pub fn new_transport_and_behaviour( + local_peer_id: PeerId, + transport: T, + ) -> (transport::ClientTransport, Self) { + let (transport, from_transport) = transport::ClientTransport::new(transport); + let behaviour = Client { + local_peer_id, + from_transport, + connected_peers: Default::default(), + rqsts_pending_connection: Default::default(), + queued_actions: Default::default(), + }; + (transport, behaviour) + } +} + +impl NetworkBehaviour for Client { + type ProtocolsHandler = handler::Prototype; + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ProtocolsHandler { + handler::Prototype::new(self.local_peer_id) + } + + fn addresses_of_peer(&mut self, _: &PeerId) -> Vec { + vec![] + } + + fn inject_connected(&mut self, _peer_id: &PeerId) {} + + fn inject_connection_established( + &mut self, + peer_id: &PeerId, + connection_id: &ConnectionId, + _: &ConnectedPoint, + _failed_addresses: Option<&Vec>, + ) { + self.connected_peers + .entry(*peer_id) + .or_default() + .push(*connection_id); + + for rqst in self + .rqsts_pending_connection + .remove(peer_id) + .map(|rqsts| rqsts.into_iter()) + .into_iter() + .flatten() + { + match rqst { + RqstPendingConnection::Reservation { to_listener, .. } => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: *peer_id, + handler: NotifyHandler::One(*connection_id), + event: handler::In::Reserve { to_listener }, + }); + } + RqstPendingConnection::Circuit { + send_back, + dst_peer_id, + .. + } => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: *peer_id, + handler: NotifyHandler::One(*connection_id), + event: handler::In::EstablishCircuit { + send_back, + dst_peer_id, + }, + }); + } + } + } + } + + fn inject_dial_failure( + &mut self, + peer_id: Option, + _handler: handler::Prototype, + _error: &DialError, + ) { + if let Some(peer_id) = peer_id { + self.rqsts_pending_connection.remove(&peer_id); + } + } + + fn inject_disconnected(&mut self, _peer: &PeerId) {} + + fn inject_connection_closed( + &mut self, + peer_id: &PeerId, + connection_id: &ConnectionId, + _: &ConnectedPoint, + _handler: handler::Handler, + ) { + self.connected_peers.get_mut(peer_id).map(|cs| { + cs.remove( + cs.iter() + .position(|c| c == connection_id) + .expect("Connection to be known."), + ) + }); + } + + fn inject_event( + &mut self, + event_source: PeerId, + _connection: ConnectionId, + handler_event: handler::Event, + ) { + match handler_event { + handler::Event::ReservationReqAccepted { renewal } => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqAccepted { + relay_peer_id: event_source, + renewal, + }, + )) + } + handler::Event::ReservationReqFailed { renewal } => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqFailed { + relay_peer_id: event_source, + renewal, + }, + )) + } + handler::Event::OutboundCircuitReqFailed {} => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::OutboundCircuitReqFailed { + relay_peer_id: event_source, + }, + )) + } + handler::Event::InboundCircuitReqDenied { src_peer_id } => self + .queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::InboundCircuitReqDenied { src_peer_id }, + )), + handler::Event::InboundCircuitReqDenyFailed { src_peer_id, error } => self + .queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::InboundCircuitReqDenyFailed { src_peer_id, error }, + )), + } + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + _poll_parameters: &mut impl PollParameters, + ) -> Poll> { + if let Some(event) = self.queued_actions.pop_front() { + return Poll::Ready(event); + } + + loop { + match self.from_transport.poll_next_unpin(cx) { + Poll::Ready(Some(transport::TransportToBehaviourMsg::ListenReq { + relay_peer_id, + relay_addr, + to_listener, + })) => { + match self + .connected_peers + .get(&relay_peer_id) + .and_then(|cs| cs.get(0)) + { + Some(connection_id) => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: relay_peer_id, + handler: NotifyHandler::One(*connection_id), + event: handler::In::Reserve { to_listener }, + }); + } + None => { + self.rqsts_pending_connection + .entry(relay_peer_id) + .or_default() + .push(RqstPendingConnection::Reservation { to_listener }); + let handler = self.new_handler(); + return Poll::Ready(NetworkBehaviourAction::Dial { + opts: DialOpts::peer_id(relay_peer_id) + .addresses(vec![relay_addr]) + .extend_addresses_through_behaviour() + .build(), + handler, + }); + } + } + } + Poll::Ready(Some(transport::TransportToBehaviourMsg::DialReq { + relay_addr, + relay_peer_id, + dst_peer_id, + send_back, + .. + })) => { + match self + .connected_peers + .get(&relay_peer_id) + .and_then(|cs| cs.get(0)) + { + Some(connection_id) => { + return Poll::Ready(NetworkBehaviourAction::NotifyHandler { + peer_id: relay_peer_id, + handler: NotifyHandler::One(*connection_id), + event: handler::In::EstablishCircuit { + send_back, + dst_peer_id, + }, + }); + } + None => { + self.rqsts_pending_connection + .entry(relay_peer_id) + .or_default() + .push(RqstPendingConnection::Circuit { + dst_peer_id, + send_back, + }); + let handler = self.new_handler(); + return Poll::Ready(NetworkBehaviourAction::Dial { + opts: DialOpts::peer_id(relay_peer_id) + .addresses(vec![relay_addr]) + .extend_addresses_through_behaviour() + .build(), + handler, + }); + } + } + } + Poll::Ready(None) => unreachable!( + "`Relay` `NetworkBehaviour` polled after channel from \ + `RelayTransport` has been closed.", + ), + Poll::Pending => break, + } + } + + Poll::Pending + } +} + +/// A [`NegotiatedSubstream`] acting as a [`RelayedConnection`]. +pub enum RelayedConnection { + InboundAccepting { + accept: BoxFuture<'static, Result<(NegotiatedSubstream, Bytes), std::io::Error>>, + drop_notifier: oneshot::Sender<()>, + }, + Operational { + read_buffer: Bytes, + substream: NegotiatedSubstream, + drop_notifier: oneshot::Sender<()>, + }, + Poisoned, +} + +impl Unpin for RelayedConnection {} + +impl RelayedConnection { + pub(crate) fn new_inbound( + circuit: inbound_stop::Circuit, + drop_notifier: oneshot::Sender<()>, + ) -> Self { + RelayedConnection::InboundAccepting { + accept: circuit.accept().boxed(), + drop_notifier, + } + } + + pub(crate) fn new_outbound( + substream: NegotiatedSubstream, + read_buffer: Bytes, + drop_notifier: oneshot::Sender<()>, + ) -> Self { + RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + } + } + + fn accept_inbound( + self: &mut Pin<&mut Self>, + cx: &mut Context, + mut accept: BoxFuture<'static, Result<(NegotiatedSubstream, Bytes), std::io::Error>>, + drop_notifier: oneshot::Sender<()>, + ) -> Poll> { + match accept.poll_unpin(cx) { + Poll::Ready(Ok((substream, read_buffer))) => { + **self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + Poll::Ready(Ok(())) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => { + **self = RelayedConnection::InboundAccepting { + accept, + drop_notifier, + }; + Poll::Pending + } + } + } +} + +impl AsyncWrite for RelayedConnection { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + loop { + match std::mem::replace(&mut *self, RelayedConnection::Poisoned) { + RelayedConnection::InboundAccepting { + accept, + drop_notifier, + } => ready!(self.accept_inbound(cx, accept, drop_notifier))?, + RelayedConnection::Operational { + mut substream, + read_buffer, + drop_notifier, + } => { + let result = Pin::new(&mut substream).poll_write(cx, buf); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return result; + } + RelayedConnection::Poisoned => unreachable!("RelayedConnection is poisoned."), + } + } + } + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + loop { + match std::mem::replace(&mut *self, RelayedConnection::Poisoned) { + RelayedConnection::InboundAccepting { + accept, + drop_notifier, + } => ready!(self.accept_inbound(cx, accept, drop_notifier))?, + RelayedConnection::Operational { + mut substream, + read_buffer, + drop_notifier, + } => { + let result = Pin::new(&mut substream).poll_flush(cx); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return result; + } + RelayedConnection::Poisoned => unreachable!("RelayedConnection is poisoned."), + } + } + } + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + loop { + match std::mem::replace(&mut *self, RelayedConnection::Poisoned) { + RelayedConnection::InboundAccepting { + accept, + drop_notifier, + } => ready!(self.accept_inbound(cx, accept, drop_notifier))?, + RelayedConnection::Operational { + mut substream, + read_buffer, + drop_notifier, + } => { + let result = Pin::new(&mut substream).poll_close(cx); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return result; + } + RelayedConnection::Poisoned => unreachable!("RelayedConnection is poisoned."), + } + } + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context, + bufs: &[IoSlice], + ) -> Poll> { + loop { + match std::mem::replace(&mut *self, RelayedConnection::Poisoned) { + RelayedConnection::InboundAccepting { + accept, + drop_notifier, + } => ready!(self.accept_inbound(cx, accept, drop_notifier))?, + RelayedConnection::Operational { + mut substream, + read_buffer, + drop_notifier, + } => { + let result = Pin::new(&mut substream).poll_write_vectored(cx, bufs); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return result; + } + RelayedConnection::Poisoned => unreachable!("RelayedConnection is poisoned."), + } + } + } +} + +impl AsyncRead for RelayedConnection { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + loop { + match std::mem::replace(&mut *self, RelayedConnection::Poisoned) { + RelayedConnection::InboundAccepting { + accept, + drop_notifier, + } => ready!(self.accept_inbound(cx, accept, drop_notifier))?, + RelayedConnection::Operational { + mut substream, + mut read_buffer, + drop_notifier, + } => { + if !read_buffer.is_empty() { + let n = std::cmp::min(read_buffer.len(), buf.len()); + let data = read_buffer.split_to(n); + buf[0..n].copy_from_slice(&data[..]); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return Poll::Ready(Ok(n)); + } + + let result = Pin::new(&mut substream).poll_read(cx, buf); + *self = RelayedConnection::Operational { + substream, + read_buffer, + drop_notifier, + }; + return result; + } + RelayedConnection::Poisoned => unreachable!("RelayedConnection is poisoned."), + } + } + } +} + +enum RqstPendingConnection { + Reservation { + to_listener: Sender, + }, + Circuit { + dst_peer_id: PeerId, + send_back: oneshot::Sender>, + }, +} diff --git a/protocols/relay/src/v2/client/handler.rs b/protocols/relay/src/v2/client/handler.rs new file mode 100644 index 00000000000..013e813d2e5 --- /dev/null +++ b/protocols/relay/src/v2/client/handler.rs @@ -0,0 +1,610 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::client::transport; +use crate::v2::message_proto::Status; +use crate::v2::protocol::{inbound_stop, outbound_hop}; +use futures::channel::mpsc::Sender; +use futures::channel::oneshot; +use futures::future::{BoxFuture, FutureExt}; +use futures::sink::SinkExt; +use futures::stream::{FuturesUnordered, StreamExt}; +use futures_timer::Delay; +use instant::Instant; +use libp2p_core::either::EitherError; +use libp2p_core::multiaddr::Protocol; +use libp2p_core::{upgrade, ConnectedPoint, Multiaddr, PeerId}; +use libp2p_swarm::protocols_handler::{InboundUpgradeSend, OutboundUpgradeSend}; +use libp2p_swarm::{ + IntoProtocolsHandler, KeepAlive, NegotiatedSubstream, ProtocolsHandler, ProtocolsHandlerEvent, + ProtocolsHandlerUpgrErr, SubstreamProtocol, +}; +use log::debug; +use std::collections::VecDeque; +use std::fmt; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub enum In { + Reserve { + to_listener: Sender, + }, + EstablishCircuit { + dst_peer_id: PeerId, + send_back: oneshot::Sender>, + }, +} + +impl fmt::Debug for In { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + In::Reserve { to_listener: _ } => f.debug_struct("In::Reserve").finish(), + In::EstablishCircuit { + dst_peer_id, + send_back: _, + } => f + .debug_struct("In::EstablishCircuit") + .field("dst_peer_id", dst_peer_id) + .finish(), + } + } +} + +#[derive(Debug)] +pub enum Event { + ReservationReqAccepted { + /// Indicates whether the request replaces an existing reservation. + renewal: bool, + }, + ReservationReqFailed { + /// Indicates whether the request replaces an existing reservation. + renewal: bool, + }, + OutboundCircuitReqFailed {}, + /// An inbound circuit request has been denied. + InboundCircuitReqDenied { + src_peer_id: PeerId, + }, + /// Denying an inbound circuit request failed. + InboundCircuitReqDenyFailed { + src_peer_id: PeerId, + error: std::io::Error, + }, +} + +pub struct Prototype { + local_peer_id: PeerId, +} + +impl Prototype { + pub(crate) fn new(local_peer_id: PeerId) -> Self { + Self { local_peer_id } + } +} + +impl IntoProtocolsHandler for Prototype { + type Handler = Handler; + + fn into_handler(self, remote_peer_id: &PeerId, endpoint: &ConnectedPoint) -> Self::Handler { + Handler { + remote_peer_id: *remote_peer_id, + remote_addr: endpoint.get_remote_address().clone(), + local_peer_id: self.local_peer_id, + queued_events: Default::default(), + pending_error: Default::default(), + reservation: None, + alive_lend_out_substreams: Default::default(), + circuit_deny_futs: Default::default(), + send_error_futs: Default::default(), + keep_alive: KeepAlive::Yes, + } + } + + fn inbound_protocol(&self) -> ::InboundProtocol { + inbound_stop::Upgrade {} + } +} + +pub struct Handler { + local_peer_id: PeerId, + remote_peer_id: PeerId, + remote_addr: Multiaddr, + pending_error: Option< + ProtocolsHandlerUpgrErr< + EitherError, + >, + >, + /// Until when to keep the connection alive. + keep_alive: KeepAlive, + + /// Queue of events to return when polled. + queued_events: VecDeque< + ProtocolsHandlerEvent< + ::OutboundProtocol, + ::OutboundOpenInfo, + ::OutEvent, + ::Error, + >, + >, + + reservation: Option, + + /// Tracks substreams lend out to the transport. + /// + /// Contains a [`futures::future::Future`] for each lend out substream that + /// resolves once the substream is dropped. + /// + /// Once all substreams are dropped and this handler has no other work, + /// [`KeepAlive::Until`] can be set, allowing the connection to be closed + /// eventually. + alive_lend_out_substreams: FuturesUnordered>, + + circuit_deny_futs: FuturesUnordered)>>, + + send_error_futs: FuturesUnordered>, +} + +impl ProtocolsHandler for Handler { + type InEvent = In; + type OutEvent = Event; + type Error = ProtocolsHandlerUpgrErr< + EitherError, + >; + type InboundProtocol = inbound_stop::Upgrade; + type OutboundProtocol = outbound_hop::Upgrade; + type OutboundOpenInfo = OutboundOpenInfo; + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + SubstreamProtocol::new(inbound_stop::Upgrade {}, ()) + } + + fn inject_fully_negotiated_inbound( + &mut self, + inbound_circuit: >::Output, + _: Self::InboundOpenInfo, + ) { + match &mut self.reservation { + Some(Reservation::Accepted { pending_msgs, .. }) + | Some(Reservation::Renewal { pending_msgs, .. }) => { + let src_peer_id = inbound_circuit.src_peer_id(); + let (tx, rx) = oneshot::channel(); + self.alive_lend_out_substreams.push(rx); + let connection = super::RelayedConnection::new_inbound(inbound_circuit, tx); + pending_msgs.push_back(transport::ToListenerMsg::IncomingRelayedConnection { + stream: connection, + src_peer_id, + relay_peer_id: self.remote_peer_id, + relay_addr: self.remote_addr.clone(), + }); + } + _ => { + let src_peer_id = inbound_circuit.src_peer_id(); + self.circuit_deny_futs.push( + inbound_circuit + .deny(Status::NoReservation) + .map(move |result| (src_peer_id, result)) + .boxed(), + ) + } + } + } + + fn inject_fully_negotiated_outbound( + &mut self, + output: >::Output, + info: Self::OutboundOpenInfo, + ) { + match (output, info) { + ( + outbound_hop::Output::Reservation { + renewal_timeout, + addrs, + }, + OutboundOpenInfo::Reserve { to_listener }, + ) => { + let (renewal, mut pending_msgs) = match self.reservation.take() { + Some(Reservation::Accepted { pending_msgs, .. }) + | Some(Reservation::Renewal { pending_msgs, .. }) => (true, pending_msgs), + None => (false, VecDeque::new()), + }; + + pending_msgs.push_back(transport::ToListenerMsg::Reservation(Ok( + transport::Reservation { + addrs: addrs + .into_iter() + .map(|a| { + a.with(Protocol::P2pCircuit) + .with(Protocol::P2p(self.local_peer_id.into())) + }) + .collect(), + }, + ))); + self.reservation = Some(Reservation::Accepted { + renewal_timeout, + pending_msgs, + to_listener, + }); + + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::ReservationReqAccepted { renewal }, + )); + } + ( + outbound_hop::Output::Circuit { + substream, + read_buffer, + }, + OutboundOpenInfo::Connect { send_back }, + ) => { + let (tx, rx) = oneshot::channel(); + self.alive_lend_out_substreams.push(rx); + let _ = send_back.send(Ok(super::RelayedConnection::new_outbound( + substream, + read_buffer, + tx, + ))); + } + _ => unreachable!(), + } + } + + fn inject_event(&mut self, event: Self::InEvent) { + match event { + In::Reserve { to_listener } => { + self.queued_events + .push_back(ProtocolsHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + outbound_hop::Upgrade::Reserve, + OutboundOpenInfo::Reserve { to_listener }, + ), + }); + } + In::EstablishCircuit { + send_back, + dst_peer_id, + } => { + self.queued_events + .push_back(ProtocolsHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + outbound_hop::Upgrade::Connect { dst_peer_id }, + OutboundOpenInfo::Connect { send_back }, + ), + }); + } + } + } + + fn inject_listen_upgrade_error( + &mut self, + _: Self::InboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + match error { + ProtocolsHandlerUpgrErr::Timeout | ProtocolsHandlerUpgrErr::Timer => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::Failed, + )) => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + )) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select(upgrade::NegotiationError::ProtocolError(e)), + )); + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Apply(error)) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::A(error)), + )) + } + } + } + + fn inject_dial_upgrade_error( + &mut self, + open_info: Self::OutboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + match open_info { + OutboundOpenInfo::Reserve { mut to_listener } => { + let renewal = self.reservation.take().is_some(); + + match error { + ProtocolsHandlerUpgrErr::Timeout | ProtocolsHandlerUpgrErr::Timer => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::Failed, + )) => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + )) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + ), + )); + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Apply(error)) => { + match error { + outbound_hop::UpgradeError::Decode(_) + | outbound_hop::UpgradeError::Io(_) + | outbound_hop::UpgradeError::ParseTypeField + | outbound_hop::UpgradeError::ParseStatusField + | outbound_hop::UpgradeError::MissingStatusField + | outbound_hop::UpgradeError::MissingReservationField + | outbound_hop::UpgradeError::NoAddressesinReservation + | outbound_hop::UpgradeError::InvalidReservationExpiration + | outbound_hop::UpgradeError::InvalidReservationAddrs + | outbound_hop::UpgradeError::UnexpectedTypeConnect + | outbound_hop::UpgradeError::UnexpectedTypeReserve => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + } + outbound_hop::UpgradeError::UnexpectedStatus(status) => { + match status { + Status::Ok => { + unreachable!( + "Status success was explicitly expected earlier." + ) + } + // With either status below there is either no reason to stay + // connected or it is a protocol violation. + // Thus terminate the connection. + Status::ConnectionFailed + | Status::NoReservation + | Status::PermissionDenied + | Status::UnexpectedMessage + | Status::MalformedMessage => { + self.pending_error = + Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + } + // The connection to the relay might still proof helpful CONNECT + // requests. Thus do not terminate the connection. + Status::ReservationRefused | Status::ResourceLimitExceeded => {} + } + } + } + } + } + + self.send_error_futs.push( + async move { + let _ = to_listener.send(transport::ToListenerMsg::Reservation(Err(()))); + } + .boxed(), + ); + + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::ReservationReqFailed { renewal }, + )); + } + OutboundOpenInfo::Connect { send_back } => { + match error { + ProtocolsHandlerUpgrErr::Timeout | ProtocolsHandlerUpgrErr::Timer => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::Failed, + )) => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + )) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + ), + )); + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Apply(error)) => { + match error { + outbound_hop::UpgradeError::Decode(_) + | outbound_hop::UpgradeError::Io(_) + | outbound_hop::UpgradeError::ParseTypeField + | outbound_hop::UpgradeError::ParseStatusField + | outbound_hop::UpgradeError::MissingStatusField + | outbound_hop::UpgradeError::MissingReservationField + | outbound_hop::UpgradeError::NoAddressesinReservation + | outbound_hop::UpgradeError::InvalidReservationExpiration + | outbound_hop::UpgradeError::InvalidReservationAddrs + | outbound_hop::UpgradeError::UnexpectedTypeConnect + | outbound_hop::UpgradeError::UnexpectedTypeReserve => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + } + outbound_hop::UpgradeError::UnexpectedStatus(status) => { + match status { + Status::Ok => { + unreachable!( + "Status success was explicitly expected earlier." + ) + } + // With either status below there is either no reason to stay + // connected or it is a protocol violation. + // Thus terminate the connection. + Status::ReservationRefused + | Status::UnexpectedMessage + | Status::MalformedMessage => { + self.pending_error = + Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + } + // While useless for reaching this particular destination, the + // connection to the relay might still proof helpful for other + // destinations. Thus do not terminate the connection. + Status::ResourceLimitExceeded + | Status::ConnectionFailed + | Status::NoReservation + | Status::PermissionDenied => {} + } + } + } + } + }; + + let _ = send_back.send(Err(())); + + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::OutboundCircuitReqFailed {}, + )); + } + } + } + + fn connection_keep_alive(&self) -> KeepAlive { + self.keep_alive + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ProtocolsHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + // Check for a pending (fatal) error. + if let Some(err) = self.pending_error.take() { + // The handler will not be polled again by the `Swarm`. + return Poll::Ready(ProtocolsHandlerEvent::Close(err)); + } + + // Return queued events. + if let Some(event) = self.queued_events.pop_front() { + return Poll::Ready(event); + } + + // Check if reservation needs renewal. + match self.reservation.take() { + Some(Reservation::Accepted { + mut renewal_timeout, + pending_msgs, + to_listener, + }) => match renewal_timeout.as_mut().map(|t| t.poll_unpin(cx)) { + Some(Poll::Ready(())) => { + self.reservation = Some(Reservation::Renewal { pending_msgs }); + return Poll::Ready(ProtocolsHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + outbound_hop::Upgrade::Reserve, + OutboundOpenInfo::Reserve { to_listener }, + ), + }); + } + Some(Poll::Pending) | None => { + self.reservation = Some(Reservation::Accepted { + renewal_timeout, + pending_msgs, + to_listener, + }); + } + }, + r => self.reservation = r, + } + + // Forward messages to transport listener. + if let Some(Reservation::Accepted { + pending_msgs, + to_listener, + .. + }) = &mut self.reservation + { + if !pending_msgs.is_empty() { + match to_listener.poll_ready(cx) { + Poll::Ready(Ok(())) => { + if let Err(e) = to_listener + .start_send(pending_msgs.pop_front().expect("Called !is_empty().")) + { + debug!("Failed to sent pending message to listener: {:?}", e); + self.reservation.take(); + } + } + Poll::Ready(Err(e)) => { + debug!("Channel to listener failed: {:?}", e); + self.reservation.take(); + } + Poll::Pending => {} + } + } + } + + // Deny incoming circuit requests. + if let Poll::Ready(Some((src_peer_id, result))) = self.circuit_deny_futs.poll_next_unpin(cx) + { + match result { + Ok(()) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::InboundCircuitReqDenied { src_peer_id }, + )) + } + Err(error) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::InboundCircuitReqDenyFailed { src_peer_id, error }, + )) + } + } + } + + // Send errors to transport. + while let Poll::Ready(Some(())) = self.send_error_futs.poll_next_unpin(cx) {} + + // Update keep-alive handling. + if self.reservation.is_none() + && self.alive_lend_out_substreams.is_empty() + && self.circuit_deny_futs.is_empty() + { + match self.keep_alive { + KeepAlive::Yes => { + self.keep_alive = KeepAlive::Until(Instant::now() + Duration::from_secs(10)); + } + KeepAlive::Until(_) => {} + KeepAlive::No => panic!("Handler never sets KeepAlive::No."), + } + } else { + self.keep_alive = KeepAlive::Yes; + } + + Poll::Pending + } +} + +enum Reservation { + Accepted { + /// [`None`] if reservation does not expire. + renewal_timeout: Option, + pending_msgs: VecDeque, + to_listener: Sender, + }, + Renewal { + pending_msgs: VecDeque, + }, +} + +pub enum OutboundOpenInfo { + Reserve { + to_listener: Sender, + }, + Connect { + send_back: oneshot::Sender>, + }, +} diff --git a/protocols/relay/src/v2/client/transport.rs b/protocols/relay/src/v2/client/transport.rs new file mode 100644 index 00000000000..8cc6ce1dbfb --- /dev/null +++ b/protocols/relay/src/v2/client/transport.rs @@ -0,0 +1,509 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::client::RelayedConnection; +use crate::v2::RequestId; +use futures::channel::mpsc; +use futures::channel::oneshot; +use futures::future::{BoxFuture, Future, FutureExt}; +use futures::sink::SinkExt; +use futures::stream::{Stream, StreamExt}; +use libp2p_core::either::{EitherError, EitherFuture, EitherOutput}; +use libp2p_core::multiaddr::{Multiaddr, Protocol}; +use libp2p_core::transport::{ListenerEvent, TransportError}; +use libp2p_core::{PeerId, Transport}; +use pin_project::pin_project; +use std::collections::VecDeque; +use std::pin::Pin; +use std::task::{Context, Poll}; +use thiserror::Error; + +/// A [`Transport`] wrapping another [`Transport`] enabling client relay capabilities. +/// +/// Allows the local node to: +/// +/// 1. Use inner wrapped transport as before. +/// +/// ``` +/// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, Transport, PeerId}; +/// # use libp2p_core::transport::memory::MemoryTransport; +/// # use libp2p_relay::v2::client; +/// # let inner_transport = MemoryTransport::default(); +/// # let (transport, behaviour) = client::Client::new_transport_and_behaviour( +/// # PeerId::random(), +/// # inner_transport, +/// # ); +/// transport.dial(Multiaddr::empty().with(Protocol::Memory(42))); +/// ``` +/// +/// 2. Establish relayed connections by dialing `/p2p-circuit` addresses. +/// +/// ``` +/// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, Transport, PeerId}; +/// # use libp2p_core::transport::memory::MemoryTransport; +/// # use libp2p_relay::v2::client; +/// # let inner_transport = MemoryTransport::default(); +/// # let (transport, behaviour) = client::Client::new_transport_and_behaviour( +/// # PeerId::random(), +/// # inner_transport, +/// # ); +/// let dst_addr_via_relay = Multiaddr::empty() +/// .with(Protocol::Memory(40)) // Relay address. +/// .with(Protocol::P2p(PeerId::random().into())) // Relay peer id. +/// .with(Protocol::P2pCircuit) // Signal to connect via relay and not directly. +/// .with(Protocol::Memory(42)) // Destination address. +/// .with(Protocol::P2p(PeerId::random().into())); // Destination peer id. +/// transport.dial(dst_addr_via_relay).unwrap(); +/// ``` +/// +/// 3. Listen for incoming relayed connections via specific relay. +/// +/// ``` +/// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, Transport, PeerId}; +/// # use libp2p_core::transport::memory::MemoryTransport; +/// # use libp2p_relay::v2::client; +/// # let inner_transport = MemoryTransport::default(); +/// # let (transport, behaviour) = client::Client::new_transport_and_behaviour( +/// # PeerId::random(), +/// # inner_transport, +/// # ); +/// let relay_addr = Multiaddr::empty() +/// .with(Protocol::Memory(40)) // Relay address. +/// .with(Protocol::P2p(PeerId::random().into())) // Relay peer id. +/// .with(Protocol::P2pCircuit); // Signal to listen via remote relay node. +/// transport.listen_on(relay_addr).unwrap(); +/// ``` +#[derive(Clone)] +pub struct ClientTransport { + to_behaviour: mpsc::Sender, + + inner_transport: T, +} + +impl ClientTransport { + /// Create a new [`ClientTransport`] by wrapping an existing [`Transport`] in a + /// [`ClientTransport`]. + /// + ///``` + /// # use libp2p_core::{Multiaddr, multiaddr::{Protocol}, Transport, PeerId}; + /// # use libp2p_core::transport::memory::MemoryTransport; + /// # use libp2p_relay::v2::client; + /// let inner_transport = MemoryTransport::default(); + /// let (transport, behaviour) = client::Client::new_transport_and_behaviour( + /// PeerId::random(), + /// inner_transport, + /// ); + ///``` + pub(crate) fn new(t: T) -> (Self, mpsc::Receiver) { + let (to_behaviour, from_transport) = mpsc::channel(0); + + let transport = ClientTransport { + to_behaviour, + + inner_transport: t, + }; + + (transport, from_transport) + } +} + +impl Transport for ClientTransport { + type Output = EitherOutput<::Output, RelayedConnection>; + type Error = EitherError<::Error, RelayError>; + type Listener = RelayListener; + type ListenerUpgrade = RelayedListenerUpgrade; + type Dial = EitherFuture<::Dial, RelayedDial>; + + fn listen_on(self, addr: Multiaddr) -> Result> { + match parse_relayed_multiaddr(addr)? { + // Address does not contain circuit relay protocol. Use inner transport. + Err(addr) => { + let inner_listener = match self.inner_transport.listen_on(addr) { + Ok(listener) => listener, + Err(TransportError::MultiaddrNotSupported(addr)) => { + return Err(TransportError::MultiaddrNotSupported(addr)) + } + Err(TransportError::Other(err)) => { + return Err(TransportError::Other(EitherError::A(err))) + } + }; + Ok(RelayListener::Inner(inner_listener)) + } + // Address does contain circuit relay protocol. Use relayed listener. + Ok(relayed_addr) => { + let (relay_peer_id, relay_addr) = match relayed_addr { + RelayedMultiaddr { + relay_peer_id: None, + relay_addr: _, + .. + } => return Err(RelayError::MissingDstPeerId.into()), + RelayedMultiaddr { + relay_peer_id: _, + relay_addr: None, + .. + } => return Err(RelayError::MissingRelayAddr.into()), + RelayedMultiaddr { + relay_peer_id: Some(peer_id), + relay_addr: Some(addr), + .. + } => (peer_id, addr), + }; + + let (to_listener, from_behaviour) = mpsc::channel(0); + let mut to_behaviour = self.to_behaviour; + let msg_to_behaviour = Some( + async move { + to_behaviour + .send(TransportToBehaviourMsg::ListenReq { + relay_peer_id, + relay_addr, + to_listener, + }) + .await + } + .boxed(), + ); + + Ok(RelayListener::Relayed { + queued_new_addresses: Default::default(), + from_behaviour, + msg_to_behaviour, + }) + } + } + } + + fn dial(self, addr: Multiaddr) -> Result> { + match parse_relayed_multiaddr(addr)? { + // Address does not contain circuit relay protocol. Use inner transport. + Err(addr) => match self.inner_transport.dial(addr) { + Ok(dialer) => Ok(EitherFuture::First(dialer)), + Err(TransportError::MultiaddrNotSupported(addr)) => { + Err(TransportError::MultiaddrNotSupported(addr)) + } + Err(TransportError::Other(err)) => Err(TransportError::Other(EitherError::A(err))), + }, + // Address does contain circuit relay protocol. Dial destination via relay. + Ok(RelayedMultiaddr { + relay_peer_id, + relay_addr, + dst_peer_id, + dst_addr, + }) => { + // TODO: In the future we might want to support dialing a relay by its address only. + let relay_peer_id = relay_peer_id.ok_or(RelayError::MissingRelayPeerId)?; + let relay_addr = relay_addr.ok_or(RelayError::MissingRelayAddr)?; + let dst_peer_id = dst_peer_id.ok_or(RelayError::MissingDstPeerId)?; + + let mut to_behaviour = self.to_behaviour; + Ok(EitherFuture::Second( + async move { + let (tx, rx) = oneshot::channel(); + to_behaviour + .send(TransportToBehaviourMsg::DialReq { + request_id: RequestId::new(), + relay_addr, + relay_peer_id, + dst_addr, + dst_peer_id, + send_back: tx, + }) + .await?; + let stream = rx.await?.map_err(|()| RelayError::Connect)?; + Ok(stream) + } + .boxed(), + )) + } + } + } + + fn address_translation(&self, server: &Multiaddr, observed: &Multiaddr) -> Option { + self.inner_transport.address_translation(server, observed) + } +} + +#[derive(Default)] +struct RelayedMultiaddr { + relay_peer_id: Option, + relay_addr: Option, + dst_peer_id: Option, + dst_addr: Option, +} + +/// Parse a [`Multiaddr`] containing a [`Protocol::P2pCircuit`]. +/// +/// Returns `Ok(Err(provided_addr))` when passed address contains no [`Protocol::P2pCircuit`]. +/// +/// Returns `Err(_)` when address is malformed. +fn parse_relayed_multiaddr( + addr: Multiaddr, +) -> Result, RelayError> { + if !addr.iter().any(|p| matches!(p, Protocol::P2pCircuit)) { + return Ok(Err(addr)); + } + + let mut relayed_multiaddr = RelayedMultiaddr::default(); + + let mut before_circuit = true; + for protocol in addr.into_iter() { + match protocol { + Protocol::P2pCircuit => { + if before_circuit { + before_circuit = false; + } else { + return Err(RelayError::MultipleCircuitRelayProtocolsUnsupported); + } + } + Protocol::P2p(hash) => { + let peer_id = PeerId::from_multihash(hash).map_err(|_| RelayError::InvalidHash)?; + + if before_circuit { + if relayed_multiaddr.relay_peer_id.is_some() { + return Err(RelayError::MalformedMultiaddr); + } + relayed_multiaddr.relay_peer_id = Some(peer_id) + } else { + if relayed_multiaddr.dst_peer_id.is_some() { + return Err(RelayError::MalformedMultiaddr); + } + relayed_multiaddr.dst_peer_id = Some(peer_id) + } + } + p => { + if before_circuit { + relayed_multiaddr + .relay_addr + .get_or_insert(Multiaddr::empty()) + .push(p); + } else { + relayed_multiaddr + .dst_addr + .get_or_insert(Multiaddr::empty()) + .push(p); + } + } + } + } + + Ok(Ok(relayed_multiaddr)) +} + +#[pin_project(project = RelayListenerProj)] +pub enum RelayListener { + Inner(#[pin] ::Listener), + Relayed { + queued_new_addresses: VecDeque, + from_behaviour: mpsc::Receiver, + msg_to_behaviour: Option>>, + }, +} + +impl Stream for RelayListener { + type Item = Result< + ListenerEvent, EitherError<::Error, RelayError>>, + EitherError<::Error, RelayError>, + >; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + + match this { + RelayListenerProj::Inner(listener) => match listener.poll_next(cx) { + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(EitherError::A(e)))), + Poll::Ready(Some(Ok(ListenerEvent::Upgrade { + upgrade, + local_addr, + remote_addr, + }))) => { + return Poll::Ready(Some(Ok(ListenerEvent::Upgrade { + upgrade: RelayedListenerUpgrade::Inner(upgrade), + local_addr, + remote_addr, + }))) + } + Poll::Ready(Some(Ok(ListenerEvent::NewAddress(addr)))) => { + return Poll::Ready(Some(Ok(ListenerEvent::NewAddress(addr)))) + } + Poll::Ready(Some(Ok(ListenerEvent::AddressExpired(addr)))) => { + return Poll::Ready(Some(Ok(ListenerEvent::AddressExpired(addr)))) + } + Poll::Ready(Some(Ok(ListenerEvent::Error(err)))) => { + return Poll::Ready(Some(Ok(ListenerEvent::Error(EitherError::A(err))))) + } + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => {} + }, + RelayListenerProj::Relayed { + queued_new_addresses, + from_behaviour, + msg_to_behaviour, + } => { + if let Some(msg) = msg_to_behaviour { + match Future::poll(msg.as_mut(), cx) { + Poll::Ready(Ok(())) => *msg_to_behaviour = None, + Poll::Ready(Err(e)) => { + return Poll::Ready(Some(Err(EitherError::B(e.into())))) + } + Poll::Pending => {} + } + } + + if let Some(addr) = queued_new_addresses.pop_front() { + return Poll::Ready(Some(Ok(ListenerEvent::NewAddress(addr)))); + } + + match from_behaviour.poll_next_unpin(cx) { + Poll::Ready(Some(ToListenerMsg::IncomingRelayedConnection { + stream, + src_peer_id, + relay_addr, + relay_peer_id: _, + })) => { + return Poll::Ready(Some(Ok(ListenerEvent::Upgrade { + upgrade: RelayedListenerUpgrade::Relayed(Some(stream)), + local_addr: relay_addr.with(Protocol::P2pCircuit), + remote_addr: Protocol::P2p(src_peer_id.into()).into(), + }))); + } + Poll::Ready(Some(ToListenerMsg::Reservation(Ok(Reservation { addrs })))) => { + let mut iter = addrs.into_iter(); + let first = iter.next(); + queued_new_addresses.extend(iter); + if let Some(addr) = first { + return Poll::Ready(Some(Ok(ListenerEvent::NewAddress(addr)))); + } + } + Poll::Ready(Some(ToListenerMsg::Reservation(Err(())))) => { + return Poll::Ready(Some(Err(EitherError::B(RelayError::Reservation)))); + } + Poll::Ready(None) => { + panic!( + "Expect sender of `from_behaviour` not to be dropped before listener." + ); + } + Poll::Pending => {} + } + } + } + + Poll::Pending + } +} + +pub type RelayedDial = BoxFuture<'static, Result>; + +#[pin_project(project = RelayedListenerUpgradeProj)] +pub enum RelayedListenerUpgrade { + Inner(#[pin] ::ListenerUpgrade), + Relayed(Option), +} + +impl Future for RelayedListenerUpgrade { + type Output = Result< + EitherOutput<::Output, RelayedConnection>, + EitherError<::Error, RelayError>, + >; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.project() { + RelayedListenerUpgradeProj::Inner(upgrade) => match upgrade.poll(cx) { + Poll::Ready(Ok(out)) => return Poll::Ready(Ok(EitherOutput::First(out))), + Poll::Ready(Err(err)) => return Poll::Ready(Err(EitherError::A(err))), + Poll::Pending => {} + }, + RelayedListenerUpgradeProj::Relayed(substream) => { + return Poll::Ready(Ok(EitherOutput::Second( + substream.take().expect("Future polled after completion."), + ))) + } + } + + Poll::Pending + } +} + +/// Error that occurred during relay connection setup. +#[derive(Debug, Error)] +pub enum RelayError { + #[error("Missing relay peer id.")] + MissingRelayPeerId, + #[error("Missing relay address.")] + MissingRelayAddr, + #[error("Missing destination peer id.")] + MissingDstPeerId, + #[error("Invalid peer id hash.")] + InvalidHash, + #[error("Failed to send message to relay behaviour: {0:?}")] + SendingMessageToBehaviour(#[from] mpsc::SendError), + #[error("Response from behaviour was canceled")] + ResponseFromBehaviourCanceled(#[from] oneshot::Canceled), + #[error( + "Address contains multiple circuit relay protocols (`p2p-circuit`) which is not supported." + )] + MultipleCircuitRelayProtocolsUnsupported, + #[error("One of the provided multiaddresses is malformed.")] + MalformedMultiaddr, + #[error("Failed to get Reservation.")] + Reservation, + #[error("Failed to connect to destination.")] + Connect, +} + +impl From for TransportError> { + fn from(error: RelayError) -> Self { + TransportError::Other(EitherError::B(error)) + } +} + +/// Message from the [`ClientTransport`] to the [`Relay`](crate::v2::relay::Relay) +/// [`NetworkBehaviour`](libp2p_swarm::NetworkBehaviour). +pub enum TransportToBehaviourMsg { + /// Dial destination node via relay node. + DialReq { + request_id: RequestId, + relay_addr: Multiaddr, + relay_peer_id: PeerId, + dst_addr: Option, + dst_peer_id: PeerId, + send_back: oneshot::Sender>, + }, + /// Listen for incoming relayed connections via relay node. + ListenReq { + relay_peer_id: PeerId, + relay_addr: Multiaddr, + to_listener: mpsc::Sender, + }, +} + +#[allow(clippy::large_enum_variant)] +pub enum ToListenerMsg { + Reservation(Result), + IncomingRelayedConnection { + stream: RelayedConnection, + src_peer_id: PeerId, + relay_peer_id: PeerId, + relay_addr: Multiaddr, + }, +} + +pub struct Reservation { + pub(crate) addrs: Vec, +} diff --git a/protocols/relay/src/v2/copy_future.rs b/protocols/relay/src/v2/copy_future.rs new file mode 100644 index 00000000000..eec0ab01f6a --- /dev/null +++ b/protocols/relay/src/v2/copy_future.rs @@ -0,0 +1,149 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// Copyright 2021 Protocol Labs. +// +// 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. + +//! Helper to interconnect two substreams, connecting the receiver side of A with the sender side of +//! B and vice versa. +//! +//! Inspired by [`futures::io::Copy`]. + +use futures::future::Future; +use futures::future::FutureExt; +use futures::io::{AsyncBufRead, BufReader}; +use futures::io::{AsyncRead, AsyncWrite}; +use futures::ready; +use futures_timer::Delay; +use std::convert::TryInto; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub struct CopyFuture { + src: BufReader, + dst: BufReader, + + max_circuit_duration: Delay, + max_circuit_bytes: u64, + bytes_sent: u64, +} + +impl CopyFuture { + pub fn new(src: S, dst: D, max_circuit_duration: Duration, max_circuit_bytes: u64) -> Self { + CopyFuture { + src: BufReader::new(src), + dst: BufReader::new(dst), + max_circuit_duration: Delay::new(max_circuit_duration), + max_circuit_bytes, + bytes_sent: Default::default(), + } + } +} + +impl Future for CopyFuture +where + S: AsyncRead + AsyncWrite + Unpin, + D: AsyncRead + AsyncWrite + Unpin, +{ + type Output = io::Result<()>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = &mut *self; + + loop { + if this.bytes_sent > this.max_circuit_bytes { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::Other, + "Max circuit bytes reached.", + ))); + } + + enum Status { + Pending, + Done, + Progressed, + } + + let src_status = match forward_data(&mut this.src, &mut this.dst, cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Ready(Ok(0)) => Status::Done, + Poll::Ready(Ok(i)) => { + this.bytes_sent += i; + Status::Progressed + } + Poll::Pending => Status::Pending, + }; + + let dst_status = match forward_data(&mut this.dst, &mut this.src, cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Ready(Ok(0)) => Status::Done, + Poll::Ready(Ok(i)) => { + this.bytes_sent += i; + Status::Progressed + } + Poll::Pending => Status::Pending, + }; + + match (src_status, dst_status) { + // Both source and destination are done sending data. + (Status::Done, Status::Done) => return Poll::Ready(Ok(())), + // Either source or destination made progress. + (Status::Progressed, _) | (_, Status::Progressed) => {} + // Both are pending. Check if max circuit duration timer fired, otherwise return + // Poll::Pending. + (Status::Pending, Status::Pending) => break, + // One is done sending data, the other is pending. Check if timer fired, otherwise + // return Poll::Pending. + (Status::Pending, Status::Done) | (Status::Done, Status::Pending) => break, + } + } + + if let Poll::Ready(()) = this.max_circuit_duration.poll_unpin(cx) { + return Poll::Ready(Err(io::ErrorKind::TimedOut.into())); + } + + Poll::Pending + } +} + +/// Forwards data from `source` to `destination`. +/// +/// Returns `0` when done, i.e. `source` having reached EOF, returns number of bytes sent otherwise, +/// thus indicating progress. +fn forward_data( + mut src: &mut S, + mut dst: &mut D, + cx: &mut Context<'_>, +) -> Poll> { + let buffer = ready!(Pin::new(&mut src).poll_fill_buf(cx))?; + if buffer.is_empty() { + ready!(Pin::new(&mut dst).poll_flush(cx))?; + ready!(Pin::new(&mut dst).poll_close(cx))?; + return Poll::Ready(Ok(0)); + } + + let i = ready!(Pin::new(dst).poll_write(cx, buffer))?; + if i == 0 { + return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); + } + Pin::new(src).consume(i); + + Poll::Ready(Ok(i.try_into().expect("usize to fit into u64."))) +} diff --git a/protocols/relay/src/v2/message.proto b/protocols/relay/src/v2/message.proto new file mode 100644 index 00000000000..b3b16511269 --- /dev/null +++ b/protocols/relay/src/v2/message.proto @@ -0,0 +1,60 @@ +syntax = "proto2"; + +package message_v2.pb; + +message HopMessage { + enum Type { + RESERVE = 0; + CONNECT = 1; + STATUS = 2; + } + + required Type type = 1; + + optional Peer peer = 2; + optional Reservation reservation = 3; + optional Limit limit = 4; + + optional Status status = 5; +} + +message StopMessage { + enum Type { + CONNECT = 0; + STATUS = 1; + } + + required Type type = 1; + + optional Peer peer = 2; + optional Limit limit = 3; + + optional Status status = 4; +} + +message Peer { + required bytes id = 1; + repeated bytes addrs = 2; +} + +message Reservation { + optional uint64 expire = 1; // Unix expiration time (UTC) + repeated bytes addrs = 2; // relay addrs for reserving peer + optional bytes voucher = 3; // reservation voucher +} + +message Limit { + optional uint32 duration = 1; // seconds + optional uint64 data = 2; // bytes +} + +enum Status { + OK = 100; + RESERVATION_REFUSED = 200; + RESOURCE_LIMIT_EXCEEDED = 201; + PERMISSION_DENIED = 202; + CONNECTION_FAILED = 203; + NO_RESERVATION = 204; + MALFORMED_MESSAGE = 400; + UNEXPECTED_MESSAGE = 401; +} \ No newline at end of file diff --git a/protocols/relay/src/v2/protocol.rs b/protocols/relay/src/v2/protocol.rs new file mode 100644 index 00000000000..b76ef6379d4 --- /dev/null +++ b/protocols/relay/src/v2/protocol.rs @@ -0,0 +1,29 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +pub mod inbound_hop; +pub mod inbound_stop; +pub mod outbound_hop; +pub mod outbound_stop; + +const HOP_PROTOCOL_NAME: &[u8; 31] = b"/libp2p/circuit/relay/0.2.0/hop"; +const STOP_PROTOCOL_NAME: &[u8; 32] = b"/libp2p/circuit/relay/0.2.0/stop"; + +const MAX_MESSAGE_SIZE: usize = 4096; diff --git a/protocols/relay/src/v2/protocol/inbound_hop.rs b/protocols/relay/src/v2/protocol/inbound_hop.rs new file mode 100644 index 00000000000..0da45c603ab --- /dev/null +++ b/protocols/relay/src/v2/protocol/inbound_hop.rs @@ -0,0 +1,237 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::message_proto::{hop_message, HopMessage, Limit, Reservation, Status}; +use crate::v2::protocol::{HOP_PROTOCOL_NAME, MAX_MESSAGE_SIZE}; +use asynchronous_codec::{Framed, FramedParts}; +use bytes::{Bytes, BytesMut}; +use futures::{future::BoxFuture, prelude::*}; +use libp2p_core::{upgrade, Multiaddr, PeerId}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::convert::TryInto; +use std::io::Cursor; +use std::iter; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub struct Upgrade { + pub reservation_duration: Duration, + pub max_circuit_duration: Duration, + pub max_circuit_bytes: u64, +} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(HOP_PROTOCOL_NAME) + } +} + +impl upgrade::InboundUpgrade for Upgrade { + type Output = Req; + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let mut codec = UviBytes::::default(); + codec.set_max_len(MAX_MESSAGE_SIZE); + let mut substream = Framed::new(substream, codec); + + async move { + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let HopMessage { + r#type, + peer, + reservation: _, + limit: _, + status: _, + } = HopMessage::decode(Cursor::new(msg))?; + + let r#type = hop_message::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + hop_message::Type::Reserve => Ok(Req::Reserve(ReservationReq { + substream, + reservation_duration: self.reservation_duration, + max_circuit_duration: self.max_circuit_duration, + max_circuit_bytes: self.max_circuit_bytes, + })), + hop_message::Type::Connect => { + let dst = PeerId::from_bytes(&peer.ok_or(UpgradeError::MissingPeer)?.id) + .map_err(|_| UpgradeError::ParsePeerId)?; + Ok(Req::Connect(CircuitReq { dst, substream })) + } + hop_message::Type::Status => Err(UpgradeError::UnexpectedTypeStatus), + } + } + .boxed() + } +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode message: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Failed to parse peer id.")] + ParsePeerId, + #[error("Expected 'peer' field to be set.")] + MissingPeer, + #[error("Unexpected message type 'status'")] + UnexpectedTypeStatus, +} + +pub enum Req { + Reserve(ReservationReq), + Connect(CircuitReq), +} + +pub struct ReservationReq { + substream: Framed, + reservation_duration: Duration, + max_circuit_duration: Duration, + max_circuit_bytes: u64, +} + +impl ReservationReq { + pub async fn accept(self, addrs: Vec) -> Result<(), std::io::Error> { + let msg = HopMessage { + r#type: hop_message::Type::Status.into(), + peer: None, + reservation: Some(Reservation { + addrs: addrs.into_iter().map(|a| a.to_vec()).collect(), + expire: Some( + (SystemTime::now() + self.reservation_duration) + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ), + voucher: None, + }), + limit: Some(Limit { + duration: Some( + self.max_circuit_duration + .as_secs() + .try_into() + .expect("`max_circuit_duration` not to exceed `u32::MAX`."), + ), + data: Some(self.max_circuit_bytes), + }), + status: Some(Status::Ok.into()), + }; + + self.send(msg).await + } + + pub async fn deny(self, status: Status) -> Result<(), std::io::Error> { + let msg = HopMessage { + r#type: hop_message::Type::Status.into(), + peer: None, + reservation: None, + limit: None, + status: Some(status.into()), + }; + + self.send(msg).await + } + + async fn send(mut self, msg: HopMessage) -> Result<(), std::io::Error> { + let mut msg_bytes = BytesMut::new(); + msg.encode(&mut msg_bytes) + .expect("BytesMut to have sufficient capacity."); + self.substream.send(msg_bytes.freeze()).await?; + self.substream.flush().await?; + self.substream.close().await?; + + Ok(()) + } +} + +pub struct CircuitReq { + dst: PeerId, + substream: Framed, +} + +impl CircuitReq { + pub fn dst(&self) -> PeerId { + self.dst + } + + pub async fn accept(mut self) -> Result<(NegotiatedSubstream, Bytes), std::io::Error> { + let msg = HopMessage { + r#type: hop_message::Type::Status.into(), + peer: None, + reservation: None, + limit: None, + status: Some(Status::Ok.into()), + }; + + self.send(msg).await?; + + let FramedParts { + io, + read_buffer, + write_buffer, + .. + } = self.substream.into_parts(); + assert!( + write_buffer.is_empty(), + "Expect a flushed Framed to have an empty write buffer." + ); + + Ok((io, read_buffer.freeze())) + } + + pub async fn deny(mut self, status: Status) -> Result<(), std::io::Error> { + let msg = HopMessage { + r#type: hop_message::Type::Status.into(), + peer: None, + reservation: None, + limit: None, + status: Some(status.into()), + }; + self.send(msg).await?; + self.substream.close().await + } + + async fn send(&mut self, msg: HopMessage) -> Result<(), std::io::Error> { + let mut msg_bytes = BytesMut::new(); + msg.encode(&mut msg_bytes) + .expect("BytesMut to have sufficient capacity."); + self.substream.send(msg_bytes.freeze()).await?; + self.substream.flush().await?; + + Ok(()) + } +} diff --git a/protocols/relay/src/v2/protocol/inbound_stop.rs b/protocols/relay/src/v2/protocol/inbound_stop.rs new file mode 100644 index 00000000000..4911c60f536 --- /dev/null +++ b/protocols/relay/src/v2/protocol/inbound_stop.rs @@ -0,0 +1,161 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::message_proto::{stop_message, Status, StopMessage}; +use crate::v2::protocol::{MAX_MESSAGE_SIZE, STOP_PROTOCOL_NAME}; +use asynchronous_codec::{Framed, FramedParts}; +use bytes::{Bytes, BytesMut}; +use futures::{future::BoxFuture, prelude::*}; +use libp2p_core::{upgrade, PeerId}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::io::Cursor; +use std::iter; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub struct Upgrade {} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(STOP_PROTOCOL_NAME) + } +} + +impl upgrade::InboundUpgrade for Upgrade { + type Output = Circuit; + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let mut codec = UviBytes::::default(); + codec.set_max_len(MAX_MESSAGE_SIZE); + let mut substream = Framed::new(substream, codec); + + async move { + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let StopMessage { + r#type, + peer, + limit: _, + status: _, + } = StopMessage::decode(Cursor::new(msg))?; + + let r#type = + stop_message::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + stop_message::Type::Connect => { + let src_peer_id = + PeerId::from_bytes(&peer.ok_or(UpgradeError::MissingPeer)?.id) + .map_err(|_| UpgradeError::ParsePeerId)?; + Ok(Circuit { + substream, + src_peer_id, + }) + } + stop_message::Type::Status => Err(UpgradeError::UnexpectedTypeStatus), + } + } + .boxed() + } +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode message: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Failed to parse peer id.")] + ParsePeerId, + #[error("Expected 'peer' field to be set.")] + MissingPeer, + #[error("Unexpected message type 'status'")] + UnexpectedTypeStatus, +} + +pub struct Circuit { + substream: Framed, + src_peer_id: PeerId, +} + +impl Circuit { + pub(crate) fn src_peer_id(&self) -> PeerId { + self.src_peer_id + } + + pub async fn accept(mut self) -> Result<(NegotiatedSubstream, Bytes), std::io::Error> { + let msg = StopMessage { + r#type: stop_message::Type::Status.into(), + peer: None, + limit: None, + status: Some(Status::Ok.into()), + }; + + self.send(msg).await?; + + let FramedParts { + io, + read_buffer, + write_buffer, + .. + } = self.substream.into_parts(); + assert!( + write_buffer.is_empty(), + "Expect a flushed Framed to have an empty write buffer." + ); + + Ok((io, read_buffer.freeze())) + } + + pub async fn deny(mut self, status: Status) -> Result<(), std::io::Error> { + let msg = StopMessage { + r#type: stop_message::Type::Status.into(), + peer: None, + limit: None, + status: Some(status.into()), + }; + + self.send(msg).await + } + + async fn send(&mut self, msg: StopMessage) -> Result<(), std::io::Error> { + let mut msg_bytes = BytesMut::new(); + msg.encode(&mut msg_bytes) + .expect("BytesMut to have sufficient capacity."); + self.substream.send(msg_bytes.freeze()).await?; + self.substream.flush().await?; + + Ok(()) + } +} diff --git a/protocols/relay/src/v2/protocol/outbound_hop.rs b/protocols/relay/src/v2/protocol/outbound_hop.rs new file mode 100644 index 00000000000..892a998b707 --- /dev/null +++ b/protocols/relay/src/v2/protocol/outbound_hop.rs @@ -0,0 +1,225 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::message_proto::{hop_message, HopMessage, Peer, Status}; +use crate::v2::protocol::{HOP_PROTOCOL_NAME, MAX_MESSAGE_SIZE}; +use asynchronous_codec::{Framed, FramedParts}; +use bytes::Bytes; +use futures::{future::BoxFuture, prelude::*}; +use futures_timer::Delay; +use instant::Instant; +use libp2p_core::{upgrade, Multiaddr, PeerId}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::convert::TryFrom; +use std::io::Cursor; +use std::iter; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub enum Upgrade { + Reserve, + Connect { dst_peer_id: PeerId }, +} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(HOP_PROTOCOL_NAME) + } +} + +impl upgrade::OutboundUpgrade for Upgrade { + type Output = Output; + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let msg = match self { + Upgrade::Reserve => HopMessage { + r#type: hop_message::Type::Reserve.into(), + peer: None, + reservation: None, + limit: None, + status: None, + }, + Upgrade::Connect { dst_peer_id } => HopMessage { + r#type: hop_message::Type::Connect.into(), + peer: Some(Peer { + id: dst_peer_id.to_bytes(), + addrs: vec![], + }), + reservation: None, + limit: None, + status: None, + }, + }; + + let mut encoded_msg = Vec::new(); + msg.encode(&mut encoded_msg) + .expect("Vec to have sufficient capacity."); + + let mut codec = UviBytes::default(); + codec.set_max_len(MAX_MESSAGE_SIZE); + let mut substream = Framed::new(substream, codec); + + async move { + substream.send(std::io::Cursor::new(encoded_msg)).await?; + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let HopMessage { + r#type, + peer: _, + reservation, + limit: _, + status, + } = HopMessage::decode(Cursor::new(msg))?; + + let r#type = hop_message::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + hop_message::Type::Connect => return Err(UpgradeError::UnexpectedTypeConnect), + hop_message::Type::Reserve => return Err(UpgradeError::UnexpectedTypeReserve), + hop_message::Type::Status => {} + } + + let status = Status::from_i32(status.ok_or(UpgradeError::MissingStatusField)?) + .ok_or(UpgradeError::ParseStatusField)?; + match status { + Status::Ok => {} + s => return Err(UpgradeError::UnexpectedStatus(s)), + } + + let output = match self { + Upgrade::Reserve => { + let reservation = reservation.ok_or(UpgradeError::MissingReservationField)?; + + let addrs = if reservation.addrs.is_empty() { + return Err(UpgradeError::NoAddressesinReservation); + } else { + reservation + .addrs + .into_iter() + .map(TryFrom::try_from) + .collect::, _>>() + .map_err(|_| UpgradeError::InvalidReservationAddrs)? + }; + + let renewal_timeout = if let Some(expires) = reservation.expire { + Some( + unix_timestamp_to_instant(expires) + .and_then(|instant| instant.checked_duration_since(Instant::now())) + .map(Delay::new) + .ok_or(UpgradeError::InvalidReservationExpiration)?, + ) + } else { + None + }; + + substream.close().await?; + + Output::Reservation { + renewal_timeout, + addrs, + } + } + Upgrade::Connect { .. } => { + let FramedParts { + io, + read_buffer, + write_buffer, + .. + } = substream.into_parts(); + assert!( + write_buffer.is_empty(), + "Expect a flushed Framed to have empty write buffer." + ); + + Output::Circuit { + substream: io, + read_buffer: read_buffer.freeze(), + } + } + }; + + Ok(output) + } + .boxed() + } +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode message: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Expected 'status' field to be set.")] + MissingStatusField, + #[error("Expected 'reservation' field to be set.")] + MissingReservationField, + #[error("Expected at least one address in reservation.")] + NoAddressesinReservation, + #[error("Invalid expiration timestamp in reservation.")] + InvalidReservationExpiration, + #[error("Invalid addresses in reservation.")] + InvalidReservationAddrs, + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Unexpected message type 'connect'")] + UnexpectedTypeConnect, + #[error("Unexpected message type 'reserve'")] + UnexpectedTypeReserve, + #[error("Failed to parse response type field.")] + ParseStatusField, + #[error("Unexpected message status '{0:?}'")] + UnexpectedStatus(Status), +} + +fn unix_timestamp_to_instant(secs: u64) -> Option { + Instant::now().checked_add(Duration::from_secs( + secs.checked_sub( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + )?, + )) +} + +pub enum Output { + Reservation { + renewal_timeout: Option, + addrs: Vec, + }, + Circuit { + substream: NegotiatedSubstream, + read_buffer: Bytes, + }, +} diff --git a/protocols/relay/src/v2/protocol/outbound_stop.rs b/protocols/relay/src/v2/protocol/outbound_stop.rs new file mode 100644 index 00000000000..b3de1e98383 --- /dev/null +++ b/protocols/relay/src/v2/protocol/outbound_stop.rs @@ -0,0 +1,148 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::message_proto::{stop_message, Limit, Peer, Status, StopMessage}; +use crate::v2::protocol::{MAX_MESSAGE_SIZE, STOP_PROTOCOL_NAME}; +use asynchronous_codec::{Framed, FramedParts}; +use bytes::Bytes; +use futures::{future::BoxFuture, prelude::*}; +use libp2p_core::{upgrade, PeerId}; +use libp2p_swarm::NegotiatedSubstream; +use prost::Message; +use std::convert::TryInto; +use std::io::Cursor; +use std::iter; +use std::time::Duration; +use thiserror::Error; +use unsigned_varint::codec::UviBytes; + +pub struct Upgrade { + pub relay_peer_id: PeerId, + pub max_circuit_duration: Duration, + pub max_circuit_bytes: u64, +} + +impl upgrade::UpgradeInfo for Upgrade { + type Info = &'static [u8]; + type InfoIter = iter::Once; + + fn protocol_info(&self) -> Self::InfoIter { + iter::once(STOP_PROTOCOL_NAME) + } +} + +impl upgrade::OutboundUpgrade for Upgrade { + type Output = (NegotiatedSubstream, Bytes); + type Error = UpgradeError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, substream: NegotiatedSubstream, _: Self::Info) -> Self::Future { + let msg = StopMessage { + r#type: stop_message::Type::Connect.into(), + peer: Some(Peer { + id: self.relay_peer_id.to_bytes(), + addrs: vec![], + }), + limit: Some(Limit { + duration: Some( + self.max_circuit_duration + .as_secs() + .try_into() + .expect("`max_circuit_duration` not to exceed `u32::MAX`."), + ), + data: Some(self.max_circuit_bytes), + }), + status: None, + }; + + let mut encoded_msg = Vec::new(); + msg.encode(&mut encoded_msg) + .expect("Vec to have sufficient capacity."); + + let mut codec = UviBytes::default(); + codec.set_max_len(MAX_MESSAGE_SIZE); + let mut substream = Framed::new(substream, codec); + + async move { + substream.send(std::io::Cursor::new(encoded_msg)).await?; + let msg: bytes::BytesMut = substream + .next() + .await + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""))??; + + let StopMessage { + r#type, + peer: _, + limit: _, + status, + } = StopMessage::decode(Cursor::new(msg))?; + + let r#type = + stop_message::Type::from_i32(r#type).ok_or(UpgradeError::ParseTypeField)?; + match r#type { + stop_message::Type::Connect => return Err(UpgradeError::UnexpectedTypeConnect), + stop_message::Type::Status => {} + } + + let status = Status::from_i32(status.ok_or(UpgradeError::MissingStatusField)?) + .ok_or(UpgradeError::ParseStatusField)?; + match status { + Status::Ok => {} + s => return Err(UpgradeError::UnexpectedStatus(s)), + } + + let FramedParts { + io, + read_buffer, + write_buffer, + .. + } = substream.into_parts(); + assert!( + write_buffer.is_empty(), + "Expect a flushed Framed to have an empty write buffer." + ); + + Ok((io, read_buffer.freeze())) + } + .boxed() + } +} + +#[derive(Debug, Error)] +pub enum UpgradeError { + #[error("Failed to decode message: {0}.")] + Decode( + #[from] + #[source] + prost::DecodeError, + ), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Expected 'status' field to be set.")] + MissingStatusField, + #[error("Failed to parse response type field.")] + ParseTypeField, + #[error("Unexpected message type 'connect'")] + UnexpectedTypeConnect, + #[error("Failed to parse response type field.")] + ParseStatusField, + #[error("Unexpected message status '{0:?}'")] + UnexpectedStatus(Status), +} diff --git a/protocols/relay/src/v2/relay.rs b/protocols/relay/src/v2/relay.rs new file mode 100644 index 00000000000..378c5cce1a4 --- /dev/null +++ b/protocols/relay/src/v2/relay.rs @@ -0,0 +1,642 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +//! [`NetworkBehaviour`] to act as a circuit relay v2 **relay**. + +mod handler; +pub mod rate_limiter; + +use crate::v2::message_proto; +use instant::Instant; +use libp2p_core::connection::{ConnectedPoint, ConnectionId}; +use libp2p_core::multiaddr::Protocol; +use libp2p_core::{Multiaddr, PeerId}; +use libp2p_swarm::{NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::num::NonZeroU32; +use std::ops::Add; +use std::task::{Context, Poll}; +use std::time::Duration; + +/// Configuration for the [`Relay`] [`NetworkBehaviour`]. +/// +/// # Panics +/// +/// [`Config::max_circuit_duration`] may not exceed [`u32::MAX`]. +pub struct Config { + pub max_reservations: usize, + pub reservation_duration: Duration, + pub reservation_rate_limiters: Vec>, + + pub max_circuits: usize, + pub max_circuit_duration: Duration, + pub max_circuit_bytes: u64, + pub circuit_src_rate_limiters: Vec>, +} + +impl Default for Config { + fn default() -> Self { + let reservation_rate_limiters = vec![ + // For each peer ID one reservation every 2 minutes with up to 30 reservations per hour. + rate_limiter::new_per_peer(rate_limiter::GenericRateLimiterConfig { + limit: NonZeroU32::new(30).expect("30 > 0"), + interval: Duration::from_secs(60 * 2), + }), + // For each IP address one reservation every minute with up to 60 reservations per hour. + rate_limiter::new_per_ip(rate_limiter::GenericRateLimiterConfig { + limit: NonZeroU32::new(60).expect("60 > 0"), + interval: Duration::from_secs(60), + }), + ]; + + let circuit_src_rate_limiters = vec![ + // For each source peer ID one circuit every 2 minute with up to 30 circuits per hour. + rate_limiter::new_per_peer(rate_limiter::GenericRateLimiterConfig { + limit: NonZeroU32::new(30).expect("30 > 0"), + interval: Duration::from_secs(60 * 2), + }), + // For each source IP address one circuit every minute with up to 60 circuits per hour. + rate_limiter::new_per_ip(rate_limiter::GenericRateLimiterConfig { + limit: NonZeroU32::new(60).expect("60 > 0"), + interval: Duration::from_secs(60), + }), + ]; + + Config { + max_reservations: 128, + reservation_duration: Duration::from_secs(60 * 60), + reservation_rate_limiters, + + max_circuits: 16, + max_circuit_duration: Duration::from_secs(2 * 60), + max_circuit_bytes: 1 << 17, // 128 kibibyte + circuit_src_rate_limiters, + } + } +} + +/// The events produced by the [`Relay`] behaviour. +#[derive(Debug)] +pub enum Event { + /// An inbound reservation request has been accepted. + ReservationReqAccepted { + src_peer_id: PeerId, + /// Indicates whether the request replaces an existing reservation. + renewed: bool, + }, + /// Accepting an inbound reservation request failed. + ReservationReqAcceptFailed { + src_peer_id: PeerId, + error: std::io::Error, + }, + /// An inbound reservation request has been denied. + ReservationReqDenied { src_peer_id: PeerId }, + /// Denying an inbound reservation request has failed. + ReservationReqDenyFailed { + src_peer_id: PeerId, + error: std::io::Error, + }, + /// An inbound reservation has timed out. + ReservationTimedOut { src_peer_id: PeerId }, + /// An inbound circuit request has been denied. + CircuitReqDenied { + src_peer_id: PeerId, + dst_peer_id: PeerId, + }, + /// Denying an inbound circuit request failed. + CircuitReqDenyFailed { + src_peer_id: PeerId, + dst_peer_id: PeerId, + error: std::io::Error, + }, + /// An inbound cirucit request has been accepted. + CircuitReqAccepted { + src_peer_id: PeerId, + dst_peer_id: PeerId, + }, + /// Accepting an inbound circuit request failed. + CircuitReqAcceptFailed { + src_peer_id: PeerId, + dst_peer_id: PeerId, + error: std::io::Error, + }, + /// An inbound circuit has closed. + CircuitClosed { + src_peer_id: PeerId, + dst_peer_id: PeerId, + error: Option, + }, +} + +/// [`Relay`] is a [`NetworkBehaviour`] that implements the relay server +/// functionality of the circuit relay v2 protocol. +pub struct Relay { + config: Config, + + local_peer_id: PeerId, + + reservations: HashMap>, + circuits: CircuitsTracker, + + /// Queue of actions to return when polled. + queued_actions: VecDeque>, +} + +impl Relay { + pub fn new(local_peer_id: PeerId, config: Config) -> Self { + Self { + config, + local_peer_id, + reservations: Default::default(), + circuits: Default::default(), + queued_actions: Default::default(), + } + } +} + +impl NetworkBehaviour for Relay { + type ProtocolsHandler = handler::Prototype; + type OutEvent = Event; + + fn new_handler(&mut self) -> Self::ProtocolsHandler { + handler::Prototype { + config: handler::Config { + reservation_duration: self.config.reservation_duration, + max_circuit_duration: self.config.max_circuit_duration, + max_circuit_bytes: self.config.max_circuit_bytes, + }, + } + } + + fn addresses_of_peer(&mut self, _remote_peer_id: &PeerId) -> Vec { + vec![] + } + + fn inject_connected(&mut self, _peer_id: &PeerId) {} + + fn inject_disconnected(&mut self, _peer: &PeerId) {} + + fn inject_connection_closed( + &mut self, + peer: &PeerId, + connection: &ConnectionId, + _: &ConnectedPoint, + _handler: handler::Handler, + ) { + self.reservations + .get_mut(peer) + .map(|cs| cs.remove(&connection)) + .unwrap_or(false); + + for circuit in self + .circuits + .remove_by_connection(*peer, *connection) + .iter() + // Only emit [`CircuitClosed`] for accepted requests. + .filter(|c| matches!(c.status, CircuitStatus::Accepted)) + { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitClosed { + src_peer_id: circuit.src_peer_id, + dst_peer_id: circuit.dst_peer_id, + error: Some(std::io::ErrorKind::ConnectionAborted.into()), + }, + )); + } + } + + fn inject_event( + &mut self, + event_source: PeerId, + connection: ConnectionId, + event: handler::Event, + ) { + match event { + handler::Event::ReservationReqReceived { + inbound_reservation_req, + remote_addr, + } => { + let now = Instant::now(); + + let handler_event = if remote_addr.iter().any(|p| p == Protocol::P2pCircuit) { + // Deny reservation requests over relayed circuits. + handler::In::DenyReservationReq { + inbound_reservation_req, + status: message_proto::Status::PermissionDenied, + } + } else if self + .reservations + .iter() + .map(|(_, cs)| cs.len()) + .sum::() + >= self.config.max_reservations + || !self + .config + .reservation_rate_limiters + .iter_mut() + .all(|limiter| limiter.try_next(event_source, &remote_addr, now)) + { + // Deny reservation exceeding limits. + handler::In::DenyReservationReq { + inbound_reservation_req, + status: message_proto::Status::ResourceLimitExceeded, + } + } else { + // Accept reservation. + self.reservations + .entry(event_source) + .or_default() + .insert(connection); + handler::In::AcceptReservationReq { + inbound_reservation_req, + addrs: vec![], + } + }; + + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(connection), + peer_id: event_source, + event: handler_event, + }); + } + handler::Event::ReservationReqAccepted { renewed } => { + // Ensure local eventual consistent reservation state matches handler (source of + // truth). + self.reservations + .entry(event_source) + .or_default() + .insert(connection); + + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqAccepted { + src_peer_id: event_source, + renewed, + }, + )); + } + handler::Event::ReservationReqAcceptFailed { error } => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqAcceptFailed { + src_peer_id: event_source, + error, + }, + )); + } + handler::Event::ReservationReqDenied {} => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqDenied { + src_peer_id: event_source, + }, + )); + } + handler::Event::ReservationReqDenyFailed { error } => { + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationReqDenyFailed { + src_peer_id: event_source, + error, + }, + )); + } + handler::Event::ReservationTimedOut {} => { + self.reservations + .get_mut(&event_source) + .map(|cs| cs.remove(&connection)); + + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::ReservationTimedOut { + src_peer_id: event_source, + }, + )); + } + handler::Event::CircuitReqReceived { + inbound_circuit_req, + remote_addr, + } => { + let now = Instant::now(); + + let action = if remote_addr.iter().any(|p| p == Protocol::P2pCircuit) { + // Deny circuit requests over relayed circuit. + // + // An attacker could otherwise build recursive or cyclic circuits. + NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(connection), + peer_id: event_source, + event: handler::In::DenyCircuitReq { + circuit_id: None, + inbound_circuit_req, + status: message_proto::Status::PermissionDenied, + }, + } + } else if self.circuits.len() >= self.config.max_circuits + || !self + .config + .circuit_src_rate_limiters + .iter_mut() + .all(|limiter| limiter.try_next(event_source, &remote_addr, now)) + { + // Deny circuit exceeding limits. + NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(connection), + peer_id: event_source, + event: handler::In::DenyCircuitReq { + circuit_id: None, + inbound_circuit_req, + status: message_proto::Status::ResourceLimitExceeded, + }, + } + } else if let Some(dst_conn) = self + .reservations + .get(&inbound_circuit_req.dst()) + .map(|cs| cs.iter().next()) + .flatten() + { + // Accept circuit request if reservation present. + let circuit_id = self.circuits.insert(Circuit { + status: CircuitStatus::Accepting, + src_peer_id: event_source, + src_connection_id: connection, + dst_peer_id: inbound_circuit_req.dst(), + dst_connection_id: *dst_conn, + }); + + NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(*dst_conn), + peer_id: event_source, + event: handler::In::NegotiateOutboundConnect { + circuit_id, + inbound_circuit_req, + relay_peer_id: self.local_peer_id, + src_peer_id: event_source, + src_connection_id: connection, + }, + } + } else { + // Deny circuit request if no reservation present. + NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(connection), + peer_id: event_source, + event: handler::In::DenyCircuitReq { + circuit_id: None, + inbound_circuit_req, + status: message_proto::Status::NoReservation, + }, + } + }; + self.queued_actions.push_back(action); + } + handler::Event::CircuitReqDenied { + circuit_id, + dst_peer_id, + } => { + if let Some(circuit_id) = circuit_id { + self.circuits.remove(circuit_id); + } + + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitReqDenied { + src_peer_id: event_source, + dst_peer_id, + }, + )); + } + handler::Event::CircuitReqDenyFailed { + circuit_id, + dst_peer_id, + error, + } => { + if let Some(circuit_id) = circuit_id { + self.circuits.remove(circuit_id); + } + + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitReqDenyFailed { + src_peer_id: event_source, + dst_peer_id, + error, + }, + )); + } + handler::Event::OutboundConnectNegotiated { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req, + dst_handler_notifier, + dst_stream, + dst_pending_data, + } => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(src_connection_id), + peer_id: src_peer_id, + event: handler::In::AcceptAndDriveCircuit { + circuit_id, + dst_peer_id: event_source, + inbound_circuit_req, + dst_handler_notifier, + dst_stream, + dst_pending_data, + }, + }); + } + handler::Event::OutboundConnectNegotiationFailed { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req, + status, + } => { + self.queued_actions + .push_back(NetworkBehaviourAction::NotifyHandler { + handler: NotifyHandler::One(src_connection_id), + peer_id: src_peer_id, + event: handler::In::DenyCircuitReq { + circuit_id: Some(circuit_id), + inbound_circuit_req, + status, + }, + }); + } + handler::Event::CircuitReqAccepted { + dst_peer_id, + circuit_id, + } => { + self.circuits.accepted(circuit_id); + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitReqAccepted { + src_peer_id: event_source, + dst_peer_id, + }, + )); + } + handler::Event::CircuitReqAcceptFailed { + dst_peer_id, + circuit_id, + error, + } => { + self.circuits.remove(circuit_id); + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitReqAcceptFailed { + src_peer_id: event_source, + dst_peer_id, + error, + }, + )); + } + handler::Event::CircuitClosed { + dst_peer_id, + circuit_id, + error, + } => { + self.circuits.remove(circuit_id); + + self.queued_actions + .push_back(NetworkBehaviourAction::GenerateEvent( + Event::CircuitClosed { + src_peer_id: event_source, + dst_peer_id, + error, + }, + )); + } + } + } + + fn poll( + &mut self, + _cx: &mut Context<'_>, + poll_parameters: &mut impl PollParameters, + ) -> Poll> { + if let Some(mut event) = self.queued_actions.pop_front() { + // Set external addresses in [`AcceptReservationReq`]. + if let NetworkBehaviourAction::NotifyHandler { + event: handler::In::AcceptReservationReq { ref mut addrs, .. }, + .. + } = &mut event + { + *addrs = poll_parameters + .external_addresses() + .map(|a| { + a.addr + .with(Protocol::P2p((*poll_parameters.local_peer_id()).into())) + }) + .collect(); + } + + return Poll::Ready(event); + } + + Poll::Pending + } +} + +#[derive(Default)] +struct CircuitsTracker { + next_id: CircuitId, + circuits: HashMap, +} + +impl CircuitsTracker { + fn len(&self) -> usize { + self.circuits.len() + } + + fn insert(&mut self, circuit: Circuit) -> CircuitId { + let id = self.next_id; + self.next_id = self.next_id + 1; + + self.circuits.insert(id, circuit); + + id + } + + fn accepted(&mut self, circuit_id: CircuitId) { + if let Some(c) = self.circuits.get_mut(&circuit_id) { + c.status = CircuitStatus::Accepted; + }; + } + + fn remove(&mut self, circuit_id: CircuitId) -> Option { + self.circuits.remove(&circuit_id) + } + + fn remove_by_connection( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + ) -> Vec { + let mut removed = vec![]; + + self.circuits.retain(|_circuit_id, circuit| { + let is_src = + circuit.src_peer_id == peer_id && circuit.src_connection_id == connection_id; + let is_dst = + circuit.dst_peer_id == peer_id && circuit.dst_connection_id == connection_id; + + if is_src || is_dst { + removed.push(circuit.clone()); + // Remove circuit from HashMap. + false + } else { + // Retain circuit in HashMap. + true + } + }); + + removed + } +} + +#[derive(Clone)] +struct Circuit { + src_peer_id: PeerId, + src_connection_id: ConnectionId, + dst_peer_id: PeerId, + dst_connection_id: ConnectionId, + status: CircuitStatus, +} + +#[derive(Clone)] +enum CircuitStatus { + Accepting, + Accepted, +} + +#[derive(Default, Clone, Copy, Debug, Hash, Eq, PartialEq)] +pub struct CircuitId(u64); + +impl Add for CircuitId { + type Output = CircuitId; + + fn add(self, rhs: u64) -> Self { + CircuitId(self.0 + rhs) + } +} diff --git a/protocols/relay/src/v2/relay/handler.rs b/protocols/relay/src/v2/relay/handler.rs new file mode 100644 index 00000000000..353df82b1f8 --- /dev/null +++ b/protocols/relay/src/v2/relay/handler.rs @@ -0,0 +1,892 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use crate::v2::copy_future::CopyFuture; +use crate::v2::message_proto::Status; +use crate::v2::protocol::{inbound_hop, outbound_stop}; +use crate::v2::relay::CircuitId; +use bytes::Bytes; +use futures::channel::oneshot::{self, Canceled}; +use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use futures::io::AsyncWriteExt; +use futures::stream::{FuturesUnordered, StreamExt}; +use futures_timer::Delay; +use instant::Instant; +use libp2p_core::connection::ConnectionId; +use libp2p_core::either::EitherError; +use libp2p_core::{upgrade, ConnectedPoint, Multiaddr, PeerId}; +use libp2p_swarm::protocols_handler::{InboundUpgradeSend, OutboundUpgradeSend}; +use libp2p_swarm::{ + IntoProtocolsHandler, KeepAlive, NegotiatedSubstream, ProtocolsHandler, ProtocolsHandlerEvent, + ProtocolsHandlerUpgrErr, SubstreamProtocol, +}; +use std::collections::VecDeque; +use std::fmt; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub struct Config { + pub reservation_duration: Duration, + pub max_circuit_duration: Duration, + pub max_circuit_bytes: u64, +} + +pub enum In { + AcceptReservationReq { + inbound_reservation_req: inbound_hop::ReservationReq, + addrs: Vec, + }, + DenyReservationReq { + inbound_reservation_req: inbound_hop::ReservationReq, + status: Status, + }, + DenyCircuitReq { + circuit_id: Option, + inbound_circuit_req: inbound_hop::CircuitReq, + status: Status, + }, + NegotiateOutboundConnect { + circuit_id: CircuitId, + inbound_circuit_req: inbound_hop::CircuitReq, + relay_peer_id: PeerId, + src_peer_id: PeerId, + src_connection_id: ConnectionId, + }, + AcceptAndDriveCircuit { + circuit_id: CircuitId, + dst_peer_id: PeerId, + inbound_circuit_req: inbound_hop::CircuitReq, + dst_handler_notifier: oneshot::Sender<()>, + dst_stream: NegotiatedSubstream, + dst_pending_data: Bytes, + }, +} + +impl fmt::Debug for In { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + In::AcceptReservationReq { + inbound_reservation_req: _, + addrs, + } => f + .debug_struct("In::AcceptReservationReq") + .field("addrs", addrs) + .finish(), + In::DenyReservationReq { + inbound_reservation_req: _, + status, + } => f + .debug_struct("In::DenyReservationReq") + .field("status", status) + .finish(), + In::DenyCircuitReq { + circuit_id, + inbound_circuit_req: _, + status, + } => f + .debug_struct("In::DenyCircuitReq") + .field("circuit_id", circuit_id) + .field("status", status) + .finish(), + In::NegotiateOutboundConnect { + circuit_id, + inbound_circuit_req: _, + relay_peer_id, + src_peer_id, + src_connection_id, + } => f + .debug_struct("In::NegotiateOutboundConnect") + .field("circuit_id", circuit_id) + .field("relay_peer_id", relay_peer_id) + .field("src_peer_id", src_peer_id) + .field("src_connection_id", src_connection_id) + .finish(), + In::AcceptAndDriveCircuit { + circuit_id, + inbound_circuit_req: _, + dst_peer_id, + dst_handler_notifier: _, + dst_stream: _, + dst_pending_data: _, + } => f + .debug_struct("In::AcceptAndDriveCircuit") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .finish(), + } + } +} + +/// The events produced by the [`Handler`]. +#[allow(clippy::large_enum_variant)] +pub enum Event { + /// An inbound reservation request has been received. + ReservationReqReceived { + inbound_reservation_req: inbound_hop::ReservationReq, + remote_addr: Multiaddr, + }, + /// An inbound reservation request has been accepted. + ReservationReqAccepted { + /// Indicates whether the request replaces an existing reservation. + renewed: bool, + }, + /// Accepting an inbound reservation request failed. + ReservationReqAcceptFailed { error: std::io::Error }, + /// An inbound reservation request has been denied. + ReservationReqDenied {}, + /// Denying an inbound reservation request has failed. + ReservationReqDenyFailed { error: std::io::Error }, + /// An inbound reservation has timed out. + ReservationTimedOut {}, + /// An inbound circuit request has been received. + CircuitReqReceived { + inbound_circuit_req: inbound_hop::CircuitReq, + remote_addr: Multiaddr, + }, + /// An inbound circuit request has been denied. + CircuitReqDenied { + circuit_id: Option, + dst_peer_id: PeerId, + }, + /// Denying an inbound circuit request failed. + CircuitReqDenyFailed { + circuit_id: Option, + dst_peer_id: PeerId, + error: std::io::Error, + }, + /// An inbound cirucit request has been accepted. + CircuitReqAccepted { + circuit_id: CircuitId, + dst_peer_id: PeerId, + }, + /// Accepting an inbound circuit request failed. + CircuitReqAcceptFailed { + circuit_id: CircuitId, + dst_peer_id: PeerId, + error: std::io::Error, + }, + /// An outbound substream for an inbound circuit request has been + /// negotiated. + OutboundConnectNegotiated { + circuit_id: CircuitId, + src_peer_id: PeerId, + src_connection_id: ConnectionId, + inbound_circuit_req: inbound_hop::CircuitReq, + dst_handler_notifier: oneshot::Sender<()>, + dst_stream: NegotiatedSubstream, + dst_pending_data: Bytes, + }, + /// Negotiating an outbound substream for an inbound circuit request failed. + OutboundConnectNegotiationFailed { + circuit_id: CircuitId, + src_peer_id: PeerId, + src_connection_id: ConnectionId, + inbound_circuit_req: inbound_hop::CircuitReq, + status: Status, + }, + /// An inbound circuit has closed. + CircuitClosed { + circuit_id: CircuitId, + dst_peer_id: PeerId, + error: Option, + }, +} + +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Event::ReservationReqReceived { + inbound_reservation_req: _, + remote_addr, + } => f + .debug_struct("Event::ReservationReqReceived") + .field("remote_addr", remote_addr) + .finish(), + Event::ReservationReqAccepted { renewed } => f + .debug_struct("Event::ReservationReqAccepted") + .field("renewed", renewed) + .finish(), + Event::ReservationReqAcceptFailed { error } => f + .debug_struct("Event::ReservationReqAcceptFailed") + .field("error", error) + .finish(), + Event::ReservationReqDenied {} => { + f.debug_struct("Event::ReservationReqDenied").finish() + } + Event::ReservationReqDenyFailed { error } => f + .debug_struct("Event::ReservationReqDenyFailed") + .field("error", error) + .finish(), + Event::ReservationTimedOut {} => f.debug_struct("Event::ReservationTimedOut").finish(), + Event::CircuitReqReceived { + remote_addr, + inbound_circuit_req: _, + } => f + .debug_struct("Event::CircuitReqReceived") + .field("remote_addr", remote_addr) + .finish(), + Event::CircuitReqDenied { + circuit_id, + dst_peer_id, + } => f + .debug_struct("Event::CircuitReqDenied") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .finish(), + Event::CircuitReqDenyFailed { + circuit_id, + dst_peer_id, + error, + } => f + .debug_struct("Event::CircuitReqDenyFailed") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .field("error", error) + .finish(), + Event::CircuitReqAccepted { + circuit_id, + dst_peer_id, + } => f + .debug_struct("Event::CircuitReqAccepted") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .finish(), + Event::CircuitReqAcceptFailed { + circuit_id, + dst_peer_id, + error, + } => f + .debug_struct("Event::CircuitReqAcceptFailed") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .field("error", error) + .finish(), + Event::OutboundConnectNegotiated { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req: _, + dst_handler_notifier: _, + dst_stream: _, + dst_pending_data: _, + } => f + .debug_struct("Event::OutboundConnectNegotiated") + .field("circuit_id", circuit_id) + .field("src_peer_id", src_peer_id) + .field("src_connection_id", src_connection_id) + .finish(), + Event::OutboundConnectNegotiationFailed { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req: _, + status, + } => f + .debug_struct("Event::OutboundConnectNegotiationFailed") + .field("circuit_id", circuit_id) + .field("src_peer_id", src_peer_id) + .field("src_connection_id", src_connection_id) + .field("status", status) + .finish(), + Event::CircuitClosed { + circuit_id, + dst_peer_id, + error, + } => f + .debug_struct("Event::CircuitClosed") + .field("circuit_id", circuit_id) + .field("dst_peer_id", dst_peer_id) + .field("error", error) + .finish(), + } + } +} + +pub struct Prototype { + pub config: Config, +} + +impl IntoProtocolsHandler for Prototype { + type Handler = Handler; + + fn into_handler(self, _remote_peer_id: &PeerId, endpoint: &ConnectedPoint) -> Self::Handler { + Handler { + remote_addr: endpoint.get_remote_address().clone(), + config: self.config, + queued_events: Default::default(), + pending_error: Default::default(), + reservation_accept_futures: Default::default(), + reservation_deny_futures: Default::default(), + circuit_accept_futures: Default::default(), + circuit_deny_futures: Default::default(), + alive_lend_out_substreams: Default::default(), + circuits: Default::default(), + active_reservation: Default::default(), + keep_alive: KeepAlive::Yes, + } + } + + fn inbound_protocol(&self) -> ::InboundProtocol { + inbound_hop::Upgrade { + reservation_duration: self.config.reservation_duration, + max_circuit_duration: self.config.max_circuit_duration, + max_circuit_bytes: self.config.max_circuit_bytes, + } + } +} + +/// [`ProtocolsHandler`] that manages substreams for a relay on a single +/// connection with a peer. +pub struct Handler { + remote_addr: Multiaddr, + + /// Static [`Handler`] [`Config`]. + config: Config, + + /// Queue of events to return when polled. + queued_events: VecDeque< + ProtocolsHandlerEvent< + ::OutboundProtocol, + ::OutboundOpenInfo, + ::OutEvent, + ::Error, + >, + >, + + /// A pending fatal error that results in the connection being closed. + pending_error: Option< + ProtocolsHandlerUpgrErr< + EitherError, + >, + >, + + /// Until when to keep the connection alive. + keep_alive: KeepAlive, + + /// Futures accepting an inbound reservation request. + reservation_accept_futures: Futures>, + /// Futures denying an inbound reservation request. + reservation_deny_futures: Futures>, + /// Timeout for the currently active reservation. + active_reservation: Option, + + /// Futures accepting an inbound circuit request. + circuit_accept_futures: Futures>, + /// Futures deying an inbound circuit request. + circuit_deny_futures: Futures<(Option, PeerId, Result<(), std::io::Error>)>, + /// Tracks substreams lend out to other [`Handler`]s. + /// + /// Contains a [`futures::future::Future`] for each lend out substream that + /// resolves once the substream is dropped. + /// + /// Once all substreams are dropped and this handler has no other work, + /// [`KeepAlive::Until`] can be set, allowing the connection to be closed + /// eventually. + alive_lend_out_substreams: FuturesUnordered>, + /// Futures relaying data for circuit between two peers. + circuits: Futures<(CircuitId, PeerId, Result<(), std::io::Error>)>, +} + +type Futures = FuturesUnordered>; + +impl ProtocolsHandler for Handler { + type InEvent = In; + type OutEvent = Event; + type Error = ProtocolsHandlerUpgrErr< + EitherError, + >; + type InboundProtocol = inbound_hop::Upgrade; + type OutboundProtocol = outbound_stop::Upgrade; + type OutboundOpenInfo = OutboundOpenInfo; + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + SubstreamProtocol::new( + inbound_hop::Upgrade { + reservation_duration: self.config.reservation_duration, + max_circuit_duration: self.config.max_circuit_duration, + max_circuit_bytes: self.config.max_circuit_bytes, + }, + (), + ) + } + + fn inject_fully_negotiated_inbound( + &mut self, + request: >::Output, + _request_id: Self::InboundOpenInfo, + ) { + match request { + inbound_hop::Req::Reserve(inbound_reservation_req) => { + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::ReservationReqReceived { + inbound_reservation_req, + remote_addr: self.remote_addr.clone(), + }, + )); + } + inbound_hop::Req::Connect(inbound_circuit_req) => { + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::CircuitReqReceived { + inbound_circuit_req, + remote_addr: self.remote_addr.clone(), + }, + )); + } + } + } + + fn inject_fully_negotiated_outbound( + &mut self, + (dst_stream, dst_pending_data): >::Output, + outbound_open_info: Self::OutboundOpenInfo, + ) { + let OutboundOpenInfo { + circuit_id, + inbound_circuit_req, + src_peer_id, + src_connection_id, + } = outbound_open_info; + let (tx, rx) = oneshot::channel(); + self.alive_lend_out_substreams.push(rx); + + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::OutboundConnectNegotiated { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req, + dst_handler_notifier: tx, + dst_stream, + dst_pending_data, + }, + )); + } + + fn inject_event(&mut self, event: Self::InEvent) { + match event { + In::AcceptReservationReq { + inbound_reservation_req, + addrs, + } => { + self.reservation_accept_futures + .push(inbound_reservation_req.accept(addrs).boxed()); + } + In::DenyReservationReq { + inbound_reservation_req, + status, + } => { + self.reservation_deny_futures + .push(inbound_reservation_req.deny(status).boxed()); + } + In::NegotiateOutboundConnect { + circuit_id, + inbound_circuit_req, + relay_peer_id, + src_peer_id, + src_connection_id, + } => { + self.queued_events + .push_back(ProtocolsHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + outbound_stop::Upgrade { + relay_peer_id, + max_circuit_duration: self.config.max_circuit_duration, + max_circuit_bytes: self.config.max_circuit_bytes, + }, + OutboundOpenInfo { + circuit_id, + inbound_circuit_req, + src_peer_id, + src_connection_id, + }, + ), + }); + } + In::DenyCircuitReq { + circuit_id, + inbound_circuit_req, + status, + } => { + let dst_peer_id = inbound_circuit_req.dst(); + self.circuit_deny_futures.push( + inbound_circuit_req + .deny(status) + .map(move |result| (circuit_id, dst_peer_id, result)) + .boxed(), + ); + } + In::AcceptAndDriveCircuit { + circuit_id, + dst_peer_id, + inbound_circuit_req, + dst_handler_notifier, + dst_stream, + dst_pending_data, + } => { + self.circuit_accept_futures.push( + inbound_circuit_req + .accept() + .map_ok(move |(src_stream, src_pending_data)| CircuitParts { + circuit_id, + src_stream, + src_pending_data, + dst_peer_id, + dst_handler_notifier, + dst_stream, + dst_pending_data, + }) + .map_err(move |e| (circuit_id, dst_peer_id, e)) + .boxed(), + ); + } + } + } + + fn inject_listen_upgrade_error( + &mut self, + _: Self::InboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + match error { + ProtocolsHandlerUpgrErr::Timeout | ProtocolsHandlerUpgrErr::Timer => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::Failed, + )) => {} + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + )) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select(upgrade::NegotiationError::ProtocolError(e)), + )); + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Apply(error)) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::A(error)), + )) + } + } + } + + fn inject_dial_upgrade_error( + &mut self, + open_info: Self::OutboundOpenInfo, + error: ProtocolsHandlerUpgrErr<::Error>, + ) { + let status = match error { + ProtocolsHandlerUpgrErr::Timeout | ProtocolsHandlerUpgrErr::Timer => { + Status::ConnectionFailed + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::Failed, + )) => { + // The remote has previously done a reservation. Doing a reservation but not + // supporting the stop protocol is pointless, thus disconnecting. + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select(upgrade::NegotiationError::Failed), + )); + Status::ConnectionFailed + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Select( + upgrade::NegotiationError::ProtocolError(e), + )) => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Select(upgrade::NegotiationError::ProtocolError(e)), + )); + Status::ConnectionFailed + } + ProtocolsHandlerUpgrErr::Upgrade(upgrade::UpgradeError::Apply(error)) => { + match error { + outbound_stop::UpgradeError::Decode(_) + | outbound_stop::UpgradeError::Io(_) + | outbound_stop::UpgradeError::ParseTypeField + | outbound_stop::UpgradeError::MissingStatusField + | outbound_stop::UpgradeError::ParseStatusField + | outbound_stop::UpgradeError::UnexpectedTypeConnect => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + Status::ConnectionFailed + } + outbound_stop::UpgradeError::UnexpectedStatus(status) => { + match status { + Status::Ok => { + unreachable!("Status success is explicitly exempt.") + } + // A destination node returning nonsensical status is a protocol + // violation. Thus terminate the connection. + Status::ReservationRefused + | Status::NoReservation + | Status::ConnectionFailed => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )); + } + // With either status below there is no reason to stay connected. + // Thus terminate the connection. + Status::MalformedMessage | Status::UnexpectedMessage => { + self.pending_error = Some(ProtocolsHandlerUpgrErr::Upgrade( + upgrade::UpgradeError::Apply(EitherError::B(error)), + )) + } + // While useless for reaching this particular destination, the + // connection to the relay might still proof helpful for other + // destinations. Thus do not terminate the connection. + Status::ResourceLimitExceeded | Status::PermissionDenied => {} + } + status + } + } + } + }; + + let OutboundOpenInfo { + circuit_id, + inbound_circuit_req, + src_peer_id, + src_connection_id, + } = open_info; + + self.queued_events.push_back(ProtocolsHandlerEvent::Custom( + Event::OutboundConnectNegotiationFailed { + circuit_id, + src_peer_id, + src_connection_id, + inbound_circuit_req, + status, + }, + )); + } + + fn connection_keep_alive(&self) -> KeepAlive { + self.keep_alive + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ProtocolsHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + // Check for a pending (fatal) error. + if let Some(err) = self.pending_error.take() { + // The handler will not be polled again by the `Swarm`. + return Poll::Ready(ProtocolsHandlerEvent::Close(err)); + } + + // Return queued events. + if let Some(event) = self.queued_events.pop_front() { + return Poll::Ready(event); + } + + if let Poll::Ready(Some((circuit_id, dst_peer_id, result))) = + self.circuits.poll_next_unpin(cx) + { + match result { + Ok(()) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom(Event::CircuitClosed { + circuit_id, + dst_peer_id, + error: None, + })) + } + Err(e) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom(Event::CircuitClosed { + circuit_id, + dst_peer_id, + error: Some(e), + })) + } + } + } + + if let Poll::Ready(Some(result)) = self.reservation_accept_futures.poll_next_unpin(cx) { + match result { + Ok(()) => { + let renewed = self + .active_reservation + .replace(Delay::new(self.config.reservation_duration)) + .is_some(); + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::ReservationReqAccepted { renewed }, + )); + } + Err(error) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::ReservationReqAcceptFailed { error }, + )); + } + } + } + + if let Poll::Ready(Some(result)) = self.reservation_deny_futures.poll_next_unpin(cx) { + match result { + Ok(()) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::ReservationReqDenied {}, + )) + } + Err(error) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::ReservationReqDenyFailed { error }, + )); + } + } + } + + if let Poll::Ready(Some(result)) = self.circuit_accept_futures.poll_next_unpin(cx) { + match result { + Ok(parts) => { + let CircuitParts { + circuit_id, + mut src_stream, + src_pending_data, + dst_peer_id, + dst_handler_notifier, + mut dst_stream, + dst_pending_data, + } = parts; + let max_circuit_duration = self.config.max_circuit_duration; + let max_circuit_bytes = self.config.max_circuit_bytes; + + let circuit = async move { + let (result_1, result_2) = futures::future::join( + src_stream.write_all(&dst_pending_data), + dst_stream.write_all(&src_pending_data), + ) + .await; + result_1?; + result_2?; + + CopyFuture::new( + src_stream, + dst_stream, + max_circuit_duration, + max_circuit_bytes, + ) + .await?; + + // Inform destination handler that the stream to the destination is dropped. + drop(dst_handler_notifier); + Ok(()) + } + .map(move |r| (circuit_id, dst_peer_id, r)) + .boxed(); + + self.circuits.push(circuit); + + return Poll::Ready(ProtocolsHandlerEvent::Custom(Event::CircuitReqAccepted { + circuit_id, + dst_peer_id, + })); + } + Err((circuit_id, dst_peer_id, error)) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::CircuitReqAcceptFailed { + circuit_id, + dst_peer_id, + error, + }, + )); + } + } + } + + if let Poll::Ready(Some((circuit_id, dst_peer_id, result))) = + self.circuit_deny_futures.poll_next_unpin(cx) + { + match result { + Ok(()) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom(Event::CircuitReqDenied { + circuit_id, + dst_peer_id, + })); + } + Err(error) => { + return Poll::Ready(ProtocolsHandlerEvent::Custom( + Event::CircuitReqDenyFailed { + circuit_id, + dst_peer_id, + error, + }, + )); + } + } + } + + while let Poll::Ready(Some(Err(Canceled))) = + self.alive_lend_out_substreams.poll_next_unpin(cx) + {} + + if let Some(Poll::Ready(())) = self + .active_reservation + .as_mut() + .map(|fut| fut.poll_unpin(cx)) + { + self.active_reservation = None; + return Poll::Ready(ProtocolsHandlerEvent::Custom(Event::ReservationTimedOut {})); + } + + if self.reservation_accept_futures.is_empty() + && self.reservation_deny_futures.is_empty() + && self.circuit_accept_futures.is_empty() + && self.circuit_deny_futures.is_empty() + && self.alive_lend_out_substreams.is_empty() + && self.circuits.is_empty() + && self.active_reservation.is_none() + { + match self.keep_alive { + KeepAlive::Yes => { + self.keep_alive = KeepAlive::Until(Instant::now() + Duration::from_secs(10)); + } + KeepAlive::Until(_) => {} + KeepAlive::No => panic!("Handler never sets KeepAlive::No."), + } + } else { + self.keep_alive = KeepAlive::Yes; + } + + Poll::Pending + } +} + +pub struct OutboundOpenInfo { + circuit_id: CircuitId, + inbound_circuit_req: inbound_hop::CircuitReq, + src_peer_id: PeerId, + src_connection_id: ConnectionId, +} + +pub struct CircuitParts { + circuit_id: CircuitId, + src_stream: NegotiatedSubstream, + src_pending_data: Bytes, + dst_peer_id: PeerId, + dst_handler_notifier: oneshot::Sender<()>, + dst_stream: NegotiatedSubstream, + dst_pending_data: Bytes, +} diff --git a/protocols/relay/src/v2/relay/rate_limiter.rs b/protocols/relay/src/v2/relay/rate_limiter.rs new file mode 100644 index 00000000000..9148e039d4f --- /dev/null +++ b/protocols/relay/src/v2/relay/rate_limiter.rs @@ -0,0 +1,315 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +pub use generic::{ + RateLimiter as GenericRateLimiter, RateLimiterConfig as GenericRateLimiterConfig, +}; +use instant::Instant; +use libp2p_core::multiaddr::{Multiaddr, Protocol}; +use libp2p_core::PeerId; +use std::net::IpAddr; + +/// Allows rate limiting access to some resource based on the [`PeerId`] and +/// [`Multiaddr`] of a remote peer. +/// +/// See [`new_per_peer`] and [`new_per_ip`] for precast implementations. Use +/// [`GenericRateLimiter`] to build your own, e.g. based on the atonomous system +/// number of a peers IP address. +pub trait RateLimiter: Send { + fn try_next(&mut self, peer: PeerId, addr: &Multiaddr, now: Instant) -> bool; +} + +pub fn new_per_peer(config: GenericRateLimiterConfig) -> Box { + let mut limiter = GenericRateLimiter::new(config); + Box::new(move |peer_id, _addr: &Multiaddr, now| limiter.try_next(peer_id, now)) +} + +pub fn new_per_ip(config: GenericRateLimiterConfig) -> Box { + let mut limiter = GenericRateLimiter::new(config); + Box::new(move |_peer_id, addr: &Multiaddr, now| { + multiaddr_to_ip(addr) + .map(|a| limiter.try_next(a, now)) + .unwrap_or(true) + }) +} + +impl bool + Send> RateLimiter for T { + fn try_next(&mut self, peer: PeerId, addr: &Multiaddr, now: Instant) -> bool { + self(peer, addr, now) + } +} + +fn multiaddr_to_ip(addr: &Multiaddr) -> Option { + addr.iter().find_map(|p| match p { + Protocol::Ip4(addr) => Some(addr.into()), + Protocol::Ip6(addr) => Some(addr.into()), + _ => None, + }) +} + +mod generic { + use instant::Instant; + use std::collections::{HashMap, VecDeque}; + use std::convert::TryInto; + use std::hash::Hash; + use std::num::NonZeroU32; + use std::time::Duration; + + /// Rate limiter using the [Token Bucket] algorithm. + /// + /// [Token Bucket]: https://en.wikipedia.org/wiki/Token_bucket + pub struct RateLimiter { + limit: u32, + interval: Duration, + + refill_schedule: VecDeque<(Instant, Id)>, + buckets: HashMap, + } + + /// Configuration for a [`RateLimiter`]. + #[derive(Clone, Copy)] + pub struct RateLimiterConfig { + /// The maximum number of tokens in the bucket at any point in time. + pub limit: NonZeroU32, + /// The interval at which a single token is added to the bucket. + pub interval: Duration, + } + + impl RateLimiter { + pub(crate) fn new(config: RateLimiterConfig) -> Self { + assert!(!config.interval.is_zero()); + + Self { + limit: config.limit.into(), + interval: config.interval, + refill_schedule: Default::default(), + buckets: Default::default(), + } + } + + pub(crate) fn try_next(&mut self, id: Id, now: Instant) -> bool { + self.refill(now); + + match self.buckets.get_mut(&id) { + // If the bucket exists, try to take a token. + Some(balance) => match balance.checked_sub(1) { + Some(a) => { + *balance = a; + true + } + None => false, + }, + // If the bucket is missing, act like the bucket has `limit` number of tokens. Take one + // token and track the new bucket balance. + None => { + self.buckets.insert(id.clone(), self.limit - 1); + self.refill_schedule.push_back((now, id)); + true + } + } + } + + fn refill(&mut self, now: Instant) { + // Note when used with a high number of buckets: This loop refills all the to-be-refilled + // buckets at once, thus potentially delaying the parent call to `try_next`. + loop { + match self.refill_schedule.get(0) { + // Only continue if (a) there is a bucket and (b) the bucket has not already been + // refilled recently. + Some((last_refill, _)) if now.duration_since(*last_refill) >= self.interval => { + } + // Otherwise stop refilling. Items in `refill_schedule` are sorted, thus, if the + // first ain't ready, none of them are. + _ => return, + }; + + let (last_refill, id) = self + .refill_schedule + .pop_front() + .expect("Queue not to be empty."); + + // Get the current balance of the bucket. + let balance = self + .buckets + .get(&id) + .expect("Entry can only be removed via refill."); + + // Calculate the new balance. + let duration_since = now.duration_since(last_refill); + let new_tokens = duration_since + .as_micros() + // Note that the use of `as_micros` limits the number of tokens to 10^6 per second. + .checked_div(self.interval.as_micros()) + .and_then(|i| i.try_into().ok()) + .unwrap_or(u32::MAX); + let new_balance = balance.checked_add(new_tokens).unwrap_or(u32::MAX); + + // If the new balance is below the limit, update the bucket. + if new_balance < self.limit { + self.buckets + .insert(id.clone(), new_balance) + .expect("To override value."); + self.refill_schedule.push_back((now, id)); + } else { + // If the balance is above the limit, the bucket can be removed, given that a + // non-existing bucket is equivalent to a bucket with `limit` tokens. + self.buckets.remove(&id); + } + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + use quickcheck::{QuickCheck, TestResult}; + use std::num::NonZeroU32; + + #[test] + fn first() { + let id = 1; + let mut l = RateLimiter::new(RateLimiterConfig { + limit: NonZeroU32::new(10).unwrap(), + interval: Duration::from_secs(1), + }); + assert!(l.try_next(id, Instant::now())); + } + + #[test] + fn limits() { + let id = 1; + let now = Instant::now(); + let mut l = RateLimiter::new(RateLimiterConfig { + limit: NonZeroU32::new(10).unwrap(), + interval: Duration::from_secs(1), + }); + for _ in 0..10 { + assert!(l.try_next(id, now)); + } + + assert!(!l.try_next(id, now)); + } + + #[test] + fn refills() { + let id = 1; + let now = Instant::now(); + let mut l = RateLimiter::new(RateLimiterConfig { + limit: NonZeroU32::new(10).unwrap(), + interval: Duration::from_secs(1), + }); + + for _ in 0..10 { + assert!(l.try_next(id, now)); + } + assert!(!l.try_next(id, now)); + + let now = now + Duration::from_secs(1); + assert!(l.try_next(id, now)); + assert!(!l.try_next(id, now)); + + let now = now + Duration::from_secs(10); + for _ in 0..10 { + assert!(l.try_next(id, now)); + } + } + + #[test] + fn move_at_half_interval_steps() { + let id = 1; + let now = Instant::now(); + let mut l = RateLimiter::new(RateLimiterConfig { + limit: NonZeroU32::new(1).unwrap(), + interval: Duration::from_secs(2), + }); + + assert!(l.try_next(id, now)); + assert!(!l.try_next(id, now)); + + let now = now + Duration::from_secs(1); + assert!(!l.try_next(id, now)); + + let now = now + Duration::from_secs(1); + assert!(l.try_next(id, now)); + } + + #[test] + fn garbage_collects() { + let now = Instant::now(); + let mut l = RateLimiter::new(RateLimiterConfig { + limit: NonZeroU32::new(1).unwrap(), + interval: Duration::from_secs(1), + }); + + assert!(l.try_next(1, now)); + + let now = now + Duration::from_secs(1); + assert!(l.try_next(2, now)); + + assert_eq!(l.buckets.len(), 1); + assert_eq!(l.refill_schedule.len(), 1); + } + + #[test] + fn quick_check() { + fn prop( + limit: NonZeroU32, + interval: Duration, + events: Vec<(u32, Duration)>, + ) -> TestResult { + if interval.is_zero() { + return TestResult::discard(); + } + + let mut now = Instant::now(); + let mut l = RateLimiter::new(RateLimiterConfig { + limit: limit.try_into().unwrap(), + interval, + }); + + for (id, d) in events { + now = if let Some(now) = now.checked_add(d) { + now + } else { + return TestResult::discard(); + }; + l.try_next(id, now); + } + + now = if let Some(now) = interval + .checked_mul(limit.into()) + .and_then(|full_interval| now.checked_add(full_interval)) + { + now + } else { + return TestResult::discard(); + }; + assert!(l.try_next(1, now)); + + assert_eq!(l.buckets.len(), 1); + assert_eq!(l.refill_schedule.len(), 1); + + TestResult::passed() + } + + QuickCheck::new().quickcheck(prop as fn(_, _, _) -> _) + } + } +} diff --git a/protocols/relay/tests/lib.rs b/protocols/relay/tests/v1.rs similarity index 99% rename from protocols/relay/tests/lib.rs rename to protocols/relay/tests/v1.rs index c316f06a8fc..d77e8241118 100644 --- a/protocols/relay/tests/lib.rs +++ b/protocols/relay/tests/v1.rs @@ -28,15 +28,15 @@ use libp2p_core::connection::ConnectedPoint; use libp2p_core::either::EitherTransport; use libp2p_core::multiaddr::{Multiaddr, Protocol}; use libp2p_core::transport::{MemoryTransport, Transport, TransportError}; -use libp2p_core::{identity, upgrade, PeerId}; +use libp2p_core::{identity, PeerId}; use libp2p_identify::{Identify, IdentifyConfig, IdentifyEvent, IdentifyInfo}; use libp2p_kad::{GetClosestPeersOk, Kademlia, KademliaEvent, QueryResult}; use libp2p_ping as ping; use libp2p_plaintext::PlainText2Config; -use libp2p_relay::{Relay, RelayConfig}; +use libp2p_relay::v1::{new_transport_and_behaviour, Relay, RelayConfig}; use libp2p_swarm::protocols_handler::KeepAlive; use libp2p_swarm::{ - dial_opts::DialOpts, DialError, DummyBehaviour, NetworkBehaviour, NetworkBehaviourAction, + DialError, DummyBehaviour, NetworkBehaviour, NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters, Swarm, SwarmEvent, }; use std::task::{Context, Poll}; @@ -1284,7 +1284,7 @@ fn build_swarm(reachability: Reachability, relay_mode: RelayMode) -> Swarm EitherTransport::Right(transport), }; - let (transport, relay_behaviour) = libp2p_relay::new_transport_and_behaviour( + let (transport, relay_behaviour) = new_transport_and_behaviour( RelayConfig { actively_connect_to_dst_nodes: relay_mode.into(), ..Default::default() @@ -1293,7 +1293,7 @@ fn build_swarm(reachability: Reachability, relay_mode: RelayMode) -> Swarm Swarm { let transport = MemoryTransport::default(); let (transport, relay_behaviour) = - libp2p_relay::new_transport_and_behaviour(RelayConfig::default(), transport); + new_transport_and_behaviour(RelayConfig::default(), transport); let transport = transport - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(plaintext) .multiplex(libp2p_yamux::YamuxConfig::default()) .boxed(); @@ -1353,7 +1353,7 @@ fn build_keep_alive_only_swarm() -> Swarm { let transport = MemoryTransport::default(); let transport = transport - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(plaintext) .multiplex(libp2p_yamux::YamuxConfig::default()) .boxed(); diff --git a/protocols/relay/tests/v2.rs b/protocols/relay/tests/v2.rs new file mode 100644 index 00000000000..1dd09029915 --- /dev/null +++ b/protocols/relay/tests/v2.rs @@ -0,0 +1,351 @@ +// Copyright 2021 Protocol Labs. +// +// 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. + +use futures::executor::LocalPool; +use futures::future::FutureExt; +use futures::io::{AsyncRead, AsyncWrite}; +use futures::stream::StreamExt; +use futures::task::Spawn; +use libp2p::core::multiaddr::{Multiaddr, Protocol}; +use libp2p::core::muxing::StreamMuxerBox; +use libp2p::core::transport::{Boxed, MemoryTransport, Transport}; +use libp2p::core::PublicKey; +use libp2p::core::{identity, PeerId}; +use libp2p::ping::{Ping, PingConfig, PingEvent}; +use libp2p::plaintext::PlainText2Config; +use libp2p::relay::v2::client; +use libp2p::relay::v2::relay; +use libp2p::NetworkBehaviour; +use libp2p_swarm::{AddressScore, NetworkBehaviour, Swarm, SwarmEvent}; +use std::time::Duration; + +#[test] +fn reservation() { + let _ = env_logger::try_init(); + let mut pool = LocalPool::new(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let mut relay = build_relay(); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone(), AddressScore::Infinite); + spawn_swarm_on_pool(&pool, relay); + + let client_addr = relay_addr + .clone() + .with(Protocol::P2p(relay_peer_id.into())) + .with(Protocol::P2pCircuit); + let mut client = build_client(); + let client_peer_id = *client.local_peer_id(); + + client.listen_on(client_addr.clone()).unwrap(); + + // Wait for initial reservation. + pool.run_until(wait_for_reservation( + &mut client, + client_addr + .clone() + .with(Protocol::P2p(client_peer_id.into())), + relay_peer_id, + false, // No renewal. + )); + + // Wait for renewal. + pool.run_until(wait_for_reservation( + &mut client, + client_addr.with(Protocol::P2p(client_peer_id.into())), + relay_peer_id, + true, // Renewal. + )); +} + +#[test] +fn connect() { + let _ = env_logger::try_init(); + let mut pool = LocalPool::new(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let mut relay = build_relay(); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone(), AddressScore::Infinite); + spawn_swarm_on_pool(&pool, relay); + + let mut dst = build_client(); + let dst_peer_id = *dst.local_peer_id(); + let dst_addr = relay_addr + .clone() + .with(Protocol::P2p(relay_peer_id.into())) + .with(Protocol::P2pCircuit) + .with(Protocol::P2p(dst_peer_id.into())); + + dst.listen_on(dst_addr.clone()).unwrap(); + + pool.run_until(wait_for_reservation( + &mut dst, + dst_addr.clone(), + relay_peer_id, + false, // No renewal. + )); + spawn_swarm_on_pool(&pool, dst); + + let mut src = build_client(); + + src.dial(dst_addr).unwrap(); + + pool.run_until(async { + loop { + match src.select_next_some().await { + SwarmEvent::Dialing(peer_id) if peer_id == relay_peer_id => {} + SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == relay_peer_id => {} + SwarmEvent::Behaviour(ClientEvent::Ping(PingEvent { peer, .. })) + if peer == dst_peer_id => + { + break + } + SwarmEvent::Behaviour(ClientEvent::Ping(PingEvent { peer, .. })) + if peer == relay_peer_id => {} + SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == dst_peer_id => { + break + } + e => panic!("{:?}", e), + } + } + }) +} + +#[test] +fn handle_dial_failure() { + let _ = env_logger::try_init(); + let mut pool = LocalPool::new(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer_id = PeerId::random(); + + let mut client = build_client(); + let client_peer_id = *client.local_peer_id(); + let client_addr = relay_addr + .clone() + .with(Protocol::P2p(relay_peer_id.into())) + .with(Protocol::P2pCircuit) + .with(Protocol::P2p(client_peer_id.into())); + + client.listen_on(client_addr.clone()).unwrap(); + assert!(!pool.run_until(wait_for_dial(&mut client, relay_peer_id))); +} + +#[test] +fn reuse_connection() { + let _ = env_logger::try_init(); + let mut pool = LocalPool::new(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let mut relay = build_relay(); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone(), AddressScore::Infinite); + spawn_swarm_on_pool(&pool, relay); + + let client_addr = relay_addr + .clone() + .with(Protocol::P2p(relay_peer_id.into())) + .with(Protocol::P2pCircuit); + let mut client = build_client(); + let client_peer_id = *client.local_peer_id(); + + client.dial(relay_addr).unwrap(); + assert!(pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + + client.listen_on(client_addr.clone()).unwrap(); + pool.run_until(wait_for_reservation( + &mut client, + client_addr.with(Protocol::P2p(client_peer_id.into())), + relay_peer_id, + false, // No renewal. + )); +} + +fn build_relay() -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_public_key = local_key.public(); + let local_peer_id = local_public_key.clone().to_peer_id(); + + let transport = build_transport(MemoryTransport::default().boxed(), local_public_key); + + Swarm::new( + transport, + Relay { + ping: Ping::new(PingConfig::new()), + relay: relay::Relay::new( + local_peer_id, + relay::Config { + reservation_duration: Duration::from_secs(2), + ..Default::default() + }, + ), + }, + local_peer_id, + ) +} + +fn build_client() -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_public_key = local_key.public(); + let local_peer_id = local_public_key.clone().to_peer_id(); + + let (transport, behaviour) = + client::Client::new_transport_and_behaviour(local_peer_id, MemoryTransport::default()); + let transport = build_transport(transport.boxed(), local_public_key); + + Swarm::new( + transport, + Client { + ping: Ping::new(PingConfig::new()), + relay: behaviour, + }, + local_peer_id, + ) +} + +fn build_transport( + transport: Boxed, + local_public_key: PublicKey, +) -> Boxed<(PeerId, StreamMuxerBox)> +where + StreamSink: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + let transport = transport + .upgrade() + .authenticate(PlainText2Config { local_public_key }) + .multiplex(libp2p_yamux::YamuxConfig::default()) + .boxed(); + + transport +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "RelayEvent", event_process = false)] +struct Relay { + relay: relay::Relay, + ping: Ping, +} + +#[derive(Debug)] +enum RelayEvent { + Relay(relay::Event), + Ping(PingEvent), +} + +impl From for RelayEvent { + fn from(event: relay::Event) -> Self { + RelayEvent::Relay(event) + } +} + +impl From for RelayEvent { + fn from(event: PingEvent) -> Self { + RelayEvent::Ping(event) + } +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "ClientEvent", event_process = false)] +struct Client { + relay: client::Client, + ping: Ping, +} + +#[derive(Debug)] +enum ClientEvent { + Relay(client::Event), + Ping(PingEvent), +} + +impl From for ClientEvent { + fn from(event: client::Event) -> Self { + ClientEvent::Relay(event) + } +} + +impl From for ClientEvent { + fn from(event: PingEvent) -> Self { + ClientEvent::Ping(event) + } +} + +fn spawn_swarm_on_pool(pool: &LocalPool, swarm: Swarm) { + pool.spawner() + .spawn_obj(swarm.collect::>().map(|_| ()).boxed().into()) + .unwrap(); +} + +async fn wait_for_reservation( + client: &mut Swarm, + client_addr: Multiaddr, + relay_peer_id: PeerId, + is_renewal: bool, +) { + let mut new_listen_addr = false; + let mut reservation_req_accepted = false; + + loop { + match client.select_next_some().await { + SwarmEvent::Behaviour(ClientEvent::Relay(client::Event::ReservationReqAccepted { + relay_peer_id: peer_id, + renewal, + })) if relay_peer_id == peer_id && renewal == is_renewal => { + reservation_req_accepted = true; + if new_listen_addr { + break; + } + } + SwarmEvent::NewListenAddr { address, .. } if address == client_addr => { + new_listen_addr = true; + if reservation_req_accepted { + break; + } + } + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + SwarmEvent::Dialing(peer_id) if peer_id == relay_peer_id => {} + SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == relay_peer_id => {} + e => panic!("{:?}", e), + } + } +} + +async fn wait_for_dial(client: &mut Swarm, relay_peer_id: PeerId) -> bool { + loop { + match client.select_next_some().await { + SwarmEvent::Dialing(peer_id) if peer_id == relay_peer_id => {} + SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == relay_peer_id => { + return true + } + SwarmEvent::OutgoingConnectionError { peer_id, .. } + if peer_id == Some(relay_peer_id) => + { + return false + } + e => panic!("{:?}", e), + } + } +} diff --git a/protocols/rendezvous/tests/harness.rs b/protocols/rendezvous/tests/harness.rs index 6b5a202b476..f9d993284c6 100644 --- a/protocols/rendezvous/tests/harness.rs +++ b/protocols/rendezvous/tests/harness.rs @@ -23,7 +23,6 @@ use futures::stream::FusedStream; use futures::StreamExt; use futures::{future, Stream}; use libp2p::core::muxing::StreamMuxerBox; -use libp2p::core::transport::upgrade::Version; use libp2p::core::transport::MemoryTransport; use libp2p::core::upgrade::SelectUpgrade; use libp2p::core::{identity, Multiaddr, PeerId, Transport}; @@ -52,7 +51,7 @@ where let noise = NoiseConfig::xx(dh_keys).into_authenticated(); let transport = MemoryTransport::default() - .upgrade(Version::V1) + .upgrade() .authenticate(noise) .multiplex(SelectUpgrade::new( YamuxConfig::default(), diff --git a/protocols/request-response/tests/ping.rs b/protocols/request-response/tests/ping.rs index 6cd6a732d4e..5ad332e85e4 100644 --- a/protocols/request-response/tests/ping.rs +++ b/protocols/request-response/tests/ping.rs @@ -26,7 +26,7 @@ use libp2p_core::{ identity, muxing::StreamMuxerBox, transport::{self, Transport}, - upgrade::{self, read_length_prefixed, write_length_prefixed}, + upgrade::{read_length_prefixed, write_length_prefixed}, Multiaddr, PeerId, }; use libp2p_noise::{Keypair, NoiseConfig, X25519Spec}; @@ -302,7 +302,7 @@ fn mk_transport() -> (PeerId, transport::Boxed<(PeerId, StreamMuxerBox)>) { peer_id, TcpConfig::new() .nodelay(true) - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(libp2p_yamux::YamuxConfig::default()) .boxed(), diff --git a/src/lib.rs b/src/lib.rs index b8728005ea3..c803e23cd2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,10 @@ pub use multiaddr; #[doc(inline)] pub use libp2p_core as core; +#[cfg(feature = "dcutr")] +#[cfg_attr(docsrs, doc(cfg(feature = "dcutr")))] +#[doc(inline)] +pub use libp2p_dcutr as dcutr; #[cfg(feature = "deflate")] #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))] #[cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")))] @@ -204,7 +208,7 @@ pub async fn development_transport( .expect("Signing libp2p-noise static DH keypair failed."); Ok(transport - .upgrade(core::upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(core::upgrade::SelectUpgrade::new( yamux::YamuxConfig::default(), @@ -261,7 +265,7 @@ pub fn tokio_development_transport( .expect("Signing libp2p-noise static DH keypair failed."); Ok(transport - .upgrade(core::upgrade::Version::V1) + .upgrade() .authenticate(noise::NoiseConfig::xx(noise_keys).into_authenticated()) .multiplex(core::upgrade::SelectUpgrade::new( yamux::YamuxConfig::default(), diff --git a/swarm/src/behaviour.rs b/swarm/src/behaviour.rs index f50b0250f6f..54fc9f93e3d 100644 --- a/swarm/src/behaviour.rs +++ b/swarm/src/behaviour.rs @@ -285,7 +285,7 @@ pub enum NetworkBehaviourAction< /// # use libp2p::core::connection::ConnectionId; /// # use libp2p::core::identity; /// # use libp2p::core::transport::{MemoryTransport, Transport}; - /// # use libp2p::core::upgrade::{self, DeniedUpgrade, InboundUpgrade, OutboundUpgrade}; + /// # use libp2p::core::upgrade::{DeniedUpgrade, InboundUpgrade, OutboundUpgrade}; /// # use libp2p::core::PeerId; /// # use libp2p::plaintext::PlainText2Config; /// # use libp2p::swarm::{ @@ -304,7 +304,7 @@ pub enum NetworkBehaviourAction< /// # let local_peer_id = PeerId::from(local_public_key.clone()); /// # /// # let transport = MemoryTransport::default() - /// # .upgrade(upgrade::Version::V1) + /// # .upgrade() /// # .authenticate(PlainText2Config { local_public_key }) /// # .multiplex(yamux::YamuxConfig::default()) /// # .boxed(); diff --git a/swarm/src/lib.rs b/swarm/src/lib.rs index c28bcee9dc4..9f6ea698ce5 100644 --- a/swarm/src/lib.rs +++ b/swarm/src/lib.rs @@ -1455,7 +1455,7 @@ mod tests { use crate::protocols_handler::DummyProtocolsHandler; use crate::test::{CallTraceBehaviour, MockBehaviour}; use futures::{executor, future}; - use libp2p::core::{identity, multiaddr, transport, upgrade}; + use libp2p::core::{identity, multiaddr, transport}; use libp2p::plaintext; use libp2p::yamux; @@ -1475,7 +1475,7 @@ mod tests { let id_keys = identity::Keypair::generate_ed25519(); let local_public_key = id_keys.public(); let transport = transport::MemoryTransport::default() - .upgrade(upgrade::Version::V1) + .upgrade() .authenticate(plaintext::PlainText2Config { local_public_key: local_public_key.clone(), }) diff --git a/transports/noise/src/lib.rs b/transports/noise/src/lib.rs index d6141483a40..28261e85dd1 100644 --- a/transports/noise/src/lib.rs +++ b/transports/noise/src/lib.rs @@ -47,7 +47,7 @@ //! let id_keys = identity::Keypair::generate_ed25519(); //! let dh_keys = Keypair::::new().into_authentic(&id_keys).unwrap(); //! let noise = NoiseConfig::xx(dh_keys).into_authenticated(); -//! let builder = TcpConfig::new().upgrade(upgrade::Version::V1).authenticate(noise); +//! let builder = TcpConfig::new().upgrade().authenticate(noise); //! // let transport = builder.multiplex(...); //! # } //! ``` @@ -349,8 +349,9 @@ where /// See [`NoiseConfig::into_authenticated`]. /// /// On success, the upgrade yields the [`PeerId`] obtained from the -/// `RemoteIdentity`. The output of this upgrade is thus directly suitable -/// for creating an [`authenticated`](libp2p_core::transport::upgrade::Authenticate) +/// `RemoteIdentity`. The output of this upgrade is thus directly suitable for +/// creating an +/// [`authenticated`](libp2p_core::transport::upgrade::Builder::authenticate) /// transport for use with a [`Network`](libp2p_core::Network). #[derive(Clone)] pub struct NoiseAuthenticated { diff --git a/transports/noise/tests/smoke.rs b/transports/noise/tests/smoke.rs index e1e1e1c0c04..dc5a386dbf8 100644 --- a/transports/noise/tests/smoke.rs +++ b/transports/noise/tests/smoke.rs @@ -41,9 +41,7 @@ fn core_upgrade_compat() { let id_keys = identity::Keypair::generate_ed25519(); let dh_keys = Keypair::::new().into_authentic(&id_keys).unwrap(); let noise = NoiseConfig::xx(dh_keys).into_authenticated(); - let _ = TcpConfig::new() - .upgrade(upgrade::Version::V1) - .authenticate(noise); + let _ = TcpConfig::new().upgrade().authenticate(noise); } #[test] diff --git a/transports/plaintext/src/lib.rs b/transports/plaintext/src/lib.rs index 1e9cfecf66f..99fbf2fd3d6 100644 --- a/transports/plaintext/src/lib.rs +++ b/transports/plaintext/src/lib.rs @@ -56,7 +56,7 @@ mod structs_proto { /// io, /// PlainText1Config{}, /// endpoint, -/// libp2p_core::transport::upgrade::Version::V1, +/// libp2p_core::upgrade::Version::V1, /// ) /// }) /// .map(|plaintext, _endpoint| {