Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WebRTC client & server #2135

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
be4d077
Experiment with a WebRTC client
tomaka Jan 21, 2022
9b00ba7
Tweaks
tomaka Jan 21, 2022
368fcbf
Add comment about worker
tomaka Jan 21, 2022
6028b53
WIP
tomaka Jan 21, 2022
938598a
WIP
tomaka Jan 23, 2022
009ed59
Missing await
tomaka Jan 23, 2022
05cab74
WIP
tomaka Jan 23, 2022
81f2166
Remove this whole certificate generation thing
tomaka Jan 23, 2022
e447789
WIP
tomaka Jan 23, 2022
47a5497
Tweaks to genRandomPayload
tomaka Jan 23, 2022
2ec5323
WIP
tomaka Jan 23, 2022
412ca74
bin/web-server: initial commit
melekes Mar 9, 2022
688a934
bin/webrtc-server: add handlers
melekes Mar 10, 2022
e80dde7
add CTRL-C handler
melekes Mar 10, 2022
02c8748
add webrtc-sdp crate for modifying sdp
melekes Mar 11, 2022
b57d1f3
remove sendrecv attr as it's default value
melekes Mar 11, 2022
ff4b88c
add doc for sctp-port and max-message-size
melekes Mar 11, 2022
4b4020b
lock files changes
melekes Mar 15, 2022
ccbeec1
remove certificates from js client dir
melekes Mar 15, 2022
56dd084
webrtc js: add a comment for candidate attr
melekes Mar 15, 2022
ca52f78
use settings engine to disable cert verification
melekes Mar 15, 2022
8fda941
use single UDP socket as a mux
melekes Mar 15, 2022
e83aa16
no need to parse SDP
melekes Mar 15, 2022
591c0b5
refactor JS client to use pc functions
melekes Mar 16, 2022
0f23739
log ice conn state changes in server
melekes Mar 16, 2022
79c5616
add docs for ice-lite
melekes Mar 18, 2022
da40d38
enable debugging and create valid key/cert
melekes Mar 18, 2022
c433903
write README with instructions
melekes Mar 18, 2022
206f282
set lite and DTLS role for server
melekes Mar 18, 2022
56eaa6b
regenerate key
melekes Mar 18, 2022
eb0e08a
update key
melekes Mar 18, 2022
f6560e4
working connection
melekes Mar 21, 2022
c347cda
add a=end-of-candidates attribute
melekes Mar 21, 2022
4bb5a1d
remove group & end-of-candidates attrs
melekes Mar 21, 2022
ecba040
minor logging changes
melekes Mar 21, 2022
50242e6
handle ipv6
melekes Mar 22, 2022
03972a9
update readme
melekes Mar 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1,471 changes: 1,415 additions & 56 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"bin/full-node",
"bin/light-base",
"bin/wasm-node/rust",
"bin/webrtc-server",
]

[features]
Expand Down
28 changes: 14 additions & 14 deletions bin/wasm-node/javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bin/wasm-node/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@

export { HealthChecker, SmoldotHealth, healthChecker } from './health.js';
export * from './client.js'; // TODO: precise exports

// TODO: remove this, just for prototyping
import connect from './webrtc/index.js';
connect('127.0.0.1', 'udp', 41000, '4');
232 changes: 232 additions & 0 deletions bin/wasm-node/javascript/src/webrtc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Smoldot
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

// # 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 is 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" (the local browser) 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.
//
// The code below only runs on one of the two agents, and simulates steps 2 and 3.
// 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 browser to execute step 4: try to connect to the remote's candidate.
//
// This process involves parsing the offer generated by the browser in order for the answer to
// match the browser's demands.
//
// ## TCP or UDP
//
// The SDP message generated by the offerer contains the list of so-called "media streams" that it
// wants to open. In our specific use-case, we configure the browser to always request one data
// stream.
//
// WebRTC by itself doesn't hardcode any specific protocol for these media streams. Instead, it is
// the SDP message of the offerer that specifies which protocol to use. In our use case, one data
// stream, we know that the browser will always request either TCP+DTLS+SCTP, or UDP+DTLS+SCTP.
//
// After the browser generates an SDP offer (by calling `createOffer`), we are allowed to tweak
// the actual SDP payload that we pass to `setLocalDescription` and that the browser will actually
// end up using for its local description. Thanks to this, we can force the browser to use TCP
// or to use UDP, no matter which one of the two it has requested in its offer.
//
// ## 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 as at this stage we have no way to know the certificate that the remote is going
// to use.
//
// To solve that problem, instead of each node generating their own random certificate, like you
// normally would, every libp2p node uses the same hardcoded publicly-known certificate.
// As such, the TLS layer won't offer any protection and another encryption layer will need to be
// negotiated on top of the DTLS+SCTP stream, like is the case for plain TCP connections.
//
// TODO: this is only one potential solution; see ongoing discussion in https://github.com/libp2p/specs/issues/220
// # About main thread vs worker
//
// You might wonder why this code is not executed within the WebWorker.
// The reason is that at the time of writing it is not allowed to create WebRTC connections within
// a WebWorker.
//
// See also https://github.com/w3c/webrtc-extensions/issues/64

export default function(targetIp: string, protocol: 'tcp' | 'udp', targetPort: number, ipVersion: '4' | '6') {
// Create a new peer connection.
const pc = new RTCPeerConnection();

// Create a new data channel. This will trigger a new negotiation (see
// `negotiationneeded` handler below).
const dataChannel = pc.createDataChannel("data");

// Log any connection state changes.
pc.onconnectionstatechange = (_event) => {
console.log("conn state: " + pc.connectionState);
};

// Log any ICE connection state changes.
pc.oniceconnectionstatechange = (_event) => {
console.log("ICE conn state: " + pc.iceConnectionState);
};

// When a new negotion is triggered, set both local and remote descriptions.
pc.onnegotiationneeded = async (_event) => {
// Create a new offer and set it as local description.
var sdpOffer = (await pc.createOffer()).sdp!;

// Replace ICE user and password with ones expected by the server.
sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:V6j+')
sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:OEKutPgoHVk/99FfqPOf444w');
await pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });

console.log("LOCAL OFFER: " + pc.localDescription!.sdp);

// Note that the trailing line feed is important, as otherwise Chrome
// fails to parse the payload.
const remoteSdp =
// Version of the SDP protocol. Always 0. (RFC8866)
"v=0" + "\n" +
// Identifies the creator of the SDP document. We are allowed to use dummy values
// (`-` and `0.0.0.0`) to remain anonymous, which we do. Note that "IN" means
// "Internet". (RFC8866)
"o=- " + (Date.now() / 1000).toFixed() + " 0 IN IP" + ipVersion + " " + targetIp + "\n" +
// Name for the session. We are allowed to pass a dummy `-`. (RFC8866)
"s=-" + "\n" +
// Start and end of the validity of the session. `0 0` means that the session never
// expires. (RFC8866)
"t=0 0" + "\n" +
// A lite implementation is only appropriate for devices that will
// *always* be connected to the public Internet and have a public
// IP address at which it can receive packets from any
// correspondent. ICE will not function when a lite implementation
// is placed behind a NAT (RFC8445).
"a=ice-lite" + "\n" +
// A `m=` line describes a request to establish a certain protocol.
// The protocol in this line (i.e. `TCP/DTLS/SCTP` or `UDP/DTLS/SCTP`) must always be
// the same as the one in the offer. We know that this is true because we tweak the
// offer to match the protocol.
// The `<fmt>` component must always be `pc-datachannel` for WebRTC.
// The rest of the SDP payload adds attributes to this specific media stream.
// RFCs: 8839, 8866, 8841
"m=application " + targetPort + " " + (protocol == 'tcp' ? "TCP" : "UDP") + "/DTLS/SCTP webrtc-datachannel" + "\n" +
// Indicates the IP address of the remote.
// Note that "IN" means "Internet".
"c=IN IP" + ipVersion + " " + targetIp + "\n" +
// Media ID - uniquely identifies this media stream (RFC9143).
"a=mid:0" + "\n" +
// Indicates that we are complying with RFC8839 (as oppposed to the legacy RFC5245).
"a=ice-options:ice2" + "\n" +
// ICE username and password, which are used for establishing and
// maintaining the ICE connection. (RFC8839)
// MUST match ones used by the answerer (server).
"a=ice-ufrag:aIGX" + "\n" +
"a=ice-pwd:ndajecaXt6vPIt6VYcUL8wpW" + "\n" +
// Fingerprint of the certificate that the server will use during the TLS
// handshake. (RFC8122)
// As explained at the top-level documentation, we use a hardcoded certificate.
// MUST be derived from the certificate used by the answerer (server).
// TODO: proper certificate and fingerprint
"a=fingerprint:sha-256 AC:D1:E5:33:EC:27:1F:CD:E0:27:59:47:F4:D6:2A:2B:23:31:FF:10:C9:DD:E0:29:8E:B7:B3:99:B4:BF:F6:0B" + "\n" +

// "TLS ID" uniquely identifies a TLS association.
// The ICE protocol uses a "TLS ID" system to indicate whether a fresh DTLS connection
// must be reopened in case of ICE renegotiation. Considering that ICE renegotiations
// never happen in our use case, we can simply put a random value and not care about
// it. Note however that the TLS ID in the answer must be present if and only if the
// offer contains one. (RFC8842)
// TODO: is it true that renegotiations never happen? what about a connection closing?
// TODO: right now browsers don't send it "a=tls-id:" + genRandomPayload(120) + "\n" +
// "tls-id" attribute MUST be present in the initial offer and respective answer (RFC8839).

// Indicates that the remote DTLS server will only listen for incoming
// connections. (RFC5763)
// The answerer (server) MUST not be located behind a NAT (RFC6135).
"a=setup:passive" + "\n" +
// The SCTP port (RFC8841)
// Note it's different from the "m=" line port value, which
// indicates the port of the underlying transport-layer protocol
// (UDP or TCP)
"a=sctp-port:5000" + "\n" +
// The maximum SCTP user message size (in bytes) (RFC8841)
"a=max-message-size:100000" + "\n" +
// A transport address for a candidate that can be used for connectivity checks (RFC8839).
"a=candidate:1 1 " + (protocol == 'tcp' ? "TCP" : "UDP") + " 2113667327 " + targetIp + " " + targetPort + " typ host" + "\n";

await pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });

console.log("REMOTE ANSWER: " + pc.remoteDescription!.sdp);
};

dataChannel.onopen = () => {
console.log(`'${dataChannel.label}' opened`);
};

dataChannel.onerror = (error) => {
console.log(`'${dataChannel.label}' errored: ${error}`);
};

dataChannel.onclose = () => {
console.log(`'${dataChannel.label}' closed`);
};

dataChannel.onmessage = (m) => {
console.log(`new message on '${dataChannel.label}': '${m.data}'`);
}
}

/**
* Generates a random payload whose grammar is: ALPHA / DIGIT / "+" / "/"
*/
// function genRandomPayload(entryopyBits: number): string {
// // Note that the grammar is letter, digits, +, and /. In other words, this is base64 except
// // without the potential trailing `=`. This trailing `=` is annoying to handle so we just use
// // hexadecimal.
// let data = new Uint8Array(Math.ceil(entryopyBits / 8));
// window.crypto.getRandomValues(data);
// return [...data].map(x => x.toString(16).padStart(2, '0')).join('');
// }
26 changes: 26 additions & 0 deletions bin/webrtc-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "webrtc-server"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>", "Pierre Krieger <pierre.krieger1708@gmail.com>"]
description = "webRTC server"
repository = "https://github.com/paritytech/smoldot"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
edition = "2021"
publish = false

[dependencies]
anyhow = "1.0.56"
async-std = { version = "1.10.0", features = ["attributes", "tokio1"] }
clap = { version = "3.1.6", features = ["derive"] }
futures = "0.3.17"
rcgen = "0.8.14"
webrtc = { version = "0.4.0", git = "https://github.com/melekes/webrtc", branch = "anton/168-allow-persistent-certificates" }
webrtc-ice = "0.6.6"
webrtc-dtls = "0.5.2"
ctrlc = "3.2.1"
tokio = { version = "1.17.0", default-features = false, features = ["net"] }
rustls = "0.19.0"
rustls-pemfile = "0.3.0"
env_logger = "0.9.0"
log = "0.4.14"
chrono = "0.4.19"
51 changes: 51 additions & 0 deletions bin/webrtc-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# WebRTC server

## Install

The certificate in `./static` directory **MUST** be marked as trusted (Chrome:
'Security and privacy' -> 'Security' -> 'Manage certificates').
Comment on lines +5 to +6

Choose a reason for hiding this comment

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

Why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can discard that. It turned out to be another issue with the library I'm using.


You are welcome to use the certificate in `./static`. There's nothing more to
do in that case!

The certificate and key were generated using the following commands:

```sh
openssl ecparam -name prime256v1 -genkey -noout -out smoldot.pem
openssl req -key smoldot.pem -new -subj '/O=Parity/OU=Smoldot' -out smoldot.csr
openssl x509 -req -in smoldot.csr -days 3650 -extfile extfile.conf -signkey smoldot.pem -out smoldot.crt
openssl pkcs8 -topk8 -nocrypt -in smoldot.pem -out smoldot.private.pem
mv smoldot.private.pem smoldot.key

# Cleanup.
rm smoldot.csr smoldot.pem

# Calculate sha256 fingerprint of the certificate
# Don't forget to update one used in the client
openssl x509 -noout -fingerprint -sha256 -inform pem -in smoldot.crt
```

Alternatively, you can use [certstrap](https://github.com/square/certstrap) to
generate a CA and a certificate:

```sh
certstrap init --common-name CertAuth --curve P-256
certstrap request-cert --common-name smoldot -ip 127.0.0.1,0:0:0:0:0:0:0:1 -domain localhost -curve P-256
certstrap sign smoldot --CA CertAuth

# Calculate sha256 fingerprint of the certificate
# Don't forget to update one used in the client
openssl x509 -noout -fingerprint -sha256 -inform pem -in smoldot.crt
```

You will need to mark the CA as trusted ('System' in Keychain on Mac).

## Run

```sh
# ipv4
./webrtc-server -l 127.0.0.1:41000 --debug

# ipv6
./webrtc-server -l ::1:41000 --debug
```