Skip to content

Commit

Permalink
feat: Add WebRTC transport (#2622)
Browse files Browse the repository at this point in the history
Hey 👋 This is a WebRTC transport implemented in accordance w/ the [spec](libp2p/specs#412). It's based on the [webrtc-rs](https://github.com/webrtc-rs/webrtc) library.

Resolves: #1066.
  • Loading branch information
melekes authored Nov 17, 2022
1 parent 43fdfe2 commit a714864
Show file tree
Hide file tree
Showing 26 changed files with 4,220 additions and 3 deletions.
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ full = [
"wasm-bindgen",
"wasm-ext",
"wasm-ext-websocket",
"webrtc",
"websocket",
"yamux",
]
Expand Down Expand Up @@ -75,11 +76,12 @@ rsa = ["libp2p-core/rsa"]
secp256k1 = ["libp2p-core/secp256k1"]
serde = ["libp2p-core/serde", "libp2p-kad?/serde", "libp2p-gossipsub?/serde"]
tcp = ["dep:libp2p-tcp"]
tokio = ["libp2p-swarm/tokio", "libp2p-mdns?/tokio", "libp2p-tcp?/tokio", "libp2p-dns?/tokio", "libp2p-quic?/tokio"]
tokio = ["libp2p-swarm/tokio", "libp2p-mdns?/tokio", "libp2p-tcp?/tokio", "libp2p-dns?/tokio", "libp2p-quic?/tokio", "libp2p-webrtc?/tokio"]
uds = ["dep:libp2p-uds"]
wasm-bindgen = ["futures-timer/wasm-bindgen", "instant/wasm-bindgen", "getrandom/js"]
wasm-ext = ["dep:libp2p-wasm-ext"]
wasm-ext-websocket = ["wasm-ext", "libp2p-wasm-ext?/websocket"]
webrtc = ["dep:libp2p-webrtc", "libp2p-webrtc?/pem"]
websocket = ["dep:libp2p-websocket"]
yamux = ["dep:libp2p-yamux"]

Expand Down Expand Up @@ -108,6 +110,7 @@ libp2p-request-response = { version = "0.23.0", path = "protocols/request-respon
libp2p-swarm = { version = "0.41.0", path = "swarm" }
libp2p-uds = { version = "0.37.0", path = "transports/uds", optional = true }
libp2p-wasm-ext = { version = "0.38.0", path = "transports/wasm-ext", optional = true }
libp2p-webrtc = { version = "0.1.0-alpha", path = "transports/webrtc", optional = true }
libp2p-yamux = { version = "0.42.0", path = "muxers/yamux", optional = true }
multiaddr = { version = "0.16.0" }
parking_lot = "0.12.0"
Expand Down Expand Up @@ -168,7 +171,8 @@ members = [
"transports/tcp",
"transports/uds",
"transports/websocket",
"transports/wasm-ext"
"transports/wasm-ext",
"transports/webrtc"
]

[[example]]
Expand Down
3 changes: 3 additions & 0 deletions misc/prost-codec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@

# 0.3.0 [unreleased]

- Implement `From` trait for `std::io::Error`. See [PR 2622].
- Don't leak `prost` dependency in `Error` type. See [PR 3058].

[PR 2622]: https://github.com/libp2p/rust-libp2p/pull/2622/
[PR 3058]: https://github.com/libp2p/rust-libp2p/pull/3058/

# 0.2.0
Expand Down
2 changes: 1 addition & 1 deletion misc/prost-codec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] }
[dev-dependencies]
prost-build = "0.11"

# Passing arguments to the docsrs builder in order to properly document cfg's.
# Passing arguments to the docsrs builder in order to properly document cfg's.
# More information: https://docs.rs/about/builds#cross-compiling
[package.metadata.docs.rs]
all-features = true
Expand Down
6 changes: 6 additions & 0 deletions misc/prost-codec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ impl<In, Out: Message + Default> Decoder for Codec<In, Out> {
#[derive(thiserror::Error, Debug)]
#[error("Failed to encode/decode message")]
pub struct Error(#[from] std::io::Error);

impl From<Error> for std::io::Error {
fn from(e: Error) -> Self {
e.0
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ pub use libp2p_uds as uds;
#[cfg(feature = "wasm-ext")]
#[doc(inline)]
pub use libp2p_wasm_ext as wasm_ext;
#[cfg(feature = "webrtc")]
#[cfg_attr(docsrs, doc(cfg(feature = "webrtc")))]
#[doc(inline)]
pub use libp2p_webrtc as webrtc;
#[cfg(feature = "websocket")]
#[cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")))]
#[doc(inline)]
Expand Down
58 changes: 58 additions & 0 deletions transports/webrtc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[package]
name = "libp2p-webrtc"
version = "0.1.0-alpha"
authors = ["Parity Technologies <admin@parity.io>"]
description = "WebRTC transport for libp2p"
repository = "https://github.com/libp2p/rust-libp2p"
license = "MIT"
edition = "2021"
keywords = ["peer-to-peer", "libp2p", "networking"]
categories = ["network-programming", "asynchronous"]

[dependencies]
async-trait = "0.1"
asynchronous-codec = "0.6.1"
bytes = "1"
futures = "0.3"
futures-timer = "3"
hex = "0.4"
if-watch = "2.0"
libp2p-core = { version = "0.38.0", path = "../../core" }
libp2p-noise = { version = "0.41.0", path = "../../transports/noise" }
log = "0.4"
multihash = { version = "0.16", default-features = false, features = ["sha2"] }
prost = "0.11"
prost-codec = { version = "0.3.0", path = "../../misc/prost-codec" }
rand = "0.8"
rcgen = "0.9.3"
serde = { version = "1.0", features = ["derive"] }
stun = "0.4"
thiserror = "1"
tinytemplate = "1.2"
tokio = { version = "1.19", features = ["net"], optional = true}
tokio-util = { version = "0.7", features = ["compat"], optional = true }
webrtc = { version = "0.6.0", optional = true }

[features]
tokio = ["dep:tokio", "dep:tokio-util", "dep:webrtc"]
pem = ["webrtc?/pem"]

[build-dependencies]
prost-build = "0.11"

[dev-dependencies]
anyhow = "1.0"
env_logger = "0.9"
hex-literal = "0.3"
libp2p = { path = "../..", features = ["full"] }
tokio = { version = "1.19", features = ["full"] }
unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] }
void = "1"

[[test]]
name = "smoke"
required-features = ["tokio"]

[[example]]
name = "listen_ping"
required-features = ["tokio"]
23 changes: 23 additions & 0 deletions transports/webrtc/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 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();
}
66 changes: 66 additions & 0 deletions transports/webrtc/examples/listen_ping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use anyhow::Result;
use futures::StreamExt;
use libp2p::swarm::{keep_alive, NetworkBehaviour};
use libp2p::Transport;
use libp2p::{ping, Swarm};
use libp2p_core::identity;
use libp2p_core::muxing::StreamMuxerBox;
use rand::thread_rng;
use void::Void;

/// An example WebRTC server that will accept connections and run the ping protocol on them.
#[tokio::main]
async fn main() -> Result<()> {
let mut swarm = create_swarm()?;

swarm.listen_on("/ip4/127.0.0.1/udp/0/webrtc".parse()?)?;

loop {
let event = swarm.next().await.unwrap();
eprintln!("New event: {event:?}")
}
}

fn create_swarm() -> Result<Swarm<Behaviour>> {
let id_keys = identity::Keypair::generate_ed25519();
let peer_id = id_keys.public().to_peer_id();
let transport = libp2p_webrtc::tokio::Transport::new(
id_keys,
libp2p_webrtc::tokio::Certificate::generate(&mut thread_rng())?,
);

let transport = transport
.map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn)))
.boxed();

Ok(Swarm::with_tokio_executor(
transport,
Behaviour::default(),
peer_id,
))
}

#[derive(NetworkBehaviour, Default)]
#[behaviour(out_event = "Event", event_process = false)]
struct Behaviour {
ping: ping::Behaviour,
keep_alive: keep_alive::Behaviour,
}

#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum Event {
Ping(ping::Event),
}

impl From<ping::Event> for Event {
fn from(e: ping::Event) -> Self {
Event::Ping(e)
}
}

impl From<Void> for Event {
fn from(event: Void) -> Self {
void::unreachable(event)
}
}
90 changes: 90 additions & 0 deletions transports/webrtc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2022 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_core::Transport`] trait for WebRTC protocol without a signaling
//! server.
//!
//! # Overview
//!
//! ## ICE
//!
//! RFCs: 8839, 8445 See also:
//! <https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html#rfc.section.5.2.3>
//!
//! The WebRTC protocol uses ICE in order to establish a connection.
//!
//! In a typical ICE setup, there are two endpoints, called agents, that want to communicate. One
//! of these two agents can be the local browser, while the other agent is the target of the
//! connection.
//!
//! Even though in this specific context all we want is a simple client-server communication, it is
//! helpful to keep in mind that ICE was designed to solve the problem of NAT traversal.
//!
//! The ICE workflow works as follows:
//!
//! - An "offerer" determines ways in which it could be accessible (either an
//! IP address or through a relay using a TURN server), which are called "candidates". It then
//! generates a small text payload in a format called SDP, that describes the request for a
//! connection.
//! - The offerer sends this SDP-encoded message to the answerer. The medium through which this
//! exchange is done is out of scope of the ICE protocol.
//! - The answerer then finds its own candidates, and generates an answer, again in the SDP format.
//! This answer is sent back to the offerer.
//! - Each agent then tries to connect to the remote's candidates.
//!
//! We pretend to send the offer to the remote agent (the target of the connection), then pretend
//! that it has found a valid IP address for itself (i.e. a candidate), then pretend that the SDP
//! answer containing this candidate has been sent back. This will cause the offerer to execute
//! step 4: try to connect to the remote's candidate.
//!
//! ## TCP or UDP
//!
//! WebRTC by itself doesn't hardcode any specific protocol for media streams. Instead, it is the
//! SDP message of the offerer that specifies which protocol to use. In our use case (one or more
//! data channels), we know that the offerer will always request either TCP+DTLS+SCTP, or
//! UDP+DTLS+SCTP.
//!
//! The implementation only supports UDP at the moment, so if the offerer requests TCP+DTLS+SCTP, it
//! will not respond. Support for TCP may be added in the future (see
//! <https://github.com/webrtc-rs/webrtc/issues/132>).
//!
//! ## DTLS+SCTP
//!
//! RFCs: 8841, 8832
//!
//! In both cases (TCP or UDP), the next layer is DTLS. DTLS is similar to the well-known TLS
//! protocol, except that it doesn't guarantee ordering of delivery (as this is instead provided by
//! the SCTP layer on top of DTLS). In other words, once the TCP or UDP connection is established,
//! the browser will try to perform a DTLS handshake.
//!
//! During the ICE negotiation, each agent must include in its SDP packet a hash of the self-signed
//! certificate that it will use during the DTLS handshake. In our use-case, where we try to
//! hand-crate the SDP answer generated by the remote, this is problematic. A way to solve this
//! is to make the hash a part of the remote's multiaddr. On the server side, we turn
//! certificate verification off.
mod message_proto {
#![allow(clippy::derive_partial_eq_without_eq)]

include!(concat!(env!("OUT_DIR"), "/webrtc.pb.rs"));
}

#[cfg(feature = "tokio")]
pub mod tokio;
20 changes: 20 additions & 0 deletions transports/webrtc/src/message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
syntax = "proto2";

package webrtc.pb;

message Message {
enum Flag {
// The sender will no longer send messages on the stream.
FIN = 0;
// The sender will no longer read messages on the stream. Incoming data is
// being discarded on receipt.
STOP_SENDING = 1;
// The sender abruptly terminates the sending part of the stream. The
// receiver can discard any data that it already received on that stream.
RESET = 2;
}

optional Flag flag=1;

optional bytes message = 2;
}
Loading

0 comments on commit a714864

Please sign in to comment.