-
Notifications
You must be signed in to change notification settings - Fork 74
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
Closed
WebRTC client & server #2135
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 9b00ba7
Tweaks
tomaka 368fcbf
Add comment about worker
tomaka 6028b53
WIP
tomaka 938598a
WIP
tomaka 009ed59
Missing await
tomaka 05cab74
WIP
tomaka 81f2166
Remove this whole certificate generation thing
tomaka e447789
WIP
tomaka 47a5497
Tweaks to genRandomPayload
tomaka 2ec5323
WIP
tomaka 412ca74
bin/web-server: initial commit
melekes 688a934
bin/webrtc-server: add handlers
melekes e80dde7
add CTRL-C handler
melekes 02c8748
add webrtc-sdp crate for modifying sdp
melekes b57d1f3
remove sendrecv attr as it's default value
melekes ff4b88c
add doc for sctp-port and max-message-size
melekes 4b4020b
lock files changes
melekes ccbeec1
remove certificates from js client dir
melekes 56dd084
webrtc js: add a comment for candidate attr
melekes ca52f78
use settings engine to disable cert verification
melekes 8fda941
use single UDP socket as a mux
melekes e83aa16
no need to parse SDP
melekes 591c0b5
refactor JS client to use pc functions
melekes 0f23739
log ice conn state changes in server
melekes 79c5616
add docs for ice-lite
melekes da40d38
enable debugging and create valid key/cert
melekes c433903
write README with instructions
melekes 206f282
set lite and DTLS role for server
melekes 56eaa6b
regenerate key
melekes eb0e08a
update key
melekes f6560e4
working connection
melekes c347cda
add a=end-of-candidates attribute
melekes 4bb5a1d
remove group & end-of-candidates attrs
melekes ecba040
minor logging changes
melekes 50242e6
handle ipv6
melekes 03972a9
update readme
melekes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(''); | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <[email protected]>", "Pierre Krieger <[email protected]>"] | ||
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'). | ||
|
||
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 | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this necessary?
There was a problem hiding this comment.
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.