From d8d543498fe08fc0c6385fa0bbbe5326f3069987 Mon Sep 17 00:00:00 2001 From: adalkiran <> Date: Thu, 11 Apr 2024 22:15:40 +0300 Subject: [PATCH] merge branch docs-mkdocs at once --- .github/FUNDING.yml | 3 + .github/workflows/documentation.yml | 45 ++++ .github/workflows/test.yml | 49 ++++ README.md | 44 ++-- docs/00-INFRASTRUCTURE.md | 15 +- docs/02-BACKEND-INITIALIZATION.md | 66 +++--- docs/03-FIRST-CLIENT-COMES-IN.md | 49 ++-- docs/04-STUN-BINDING-REQUEST-FROM-CLIENT.md | 30 +-- docs/05-DTLS-HANDSHAKE.md | 244 ++++++++------------ docs/06-SRTP-INITIALIZATION.md | 14 +- docs/07-SRTP-PACKETS-COME.md | 41 ++-- docs/08-VP8-PACKET-DECODE.md | 13 +- docs/09-CONCLUSION.md | 4 +- docs/images/icon.svg | 4 + docs/mkdocs/.gitignore | 12 + docs/mkdocs/assets/icon.png | Bin 0 -> 24913 bytes docs/mkdocs/assets/icon.svg | 4 + docs/mkdocs/execute-mkdocs.sh | 4 + docs/mkdocs/extra-content/index.md | 100 ++++++++ docs/mkdocs/javascripts/mathjax.js | 20 ++ docs/mkdocs/mkdocs.yml.template | 102 ++++++++ docs/mkdocs/prepare-mkdocs.sh | 66 ++++++ docs/mkdocs/stylesheets/custom.css | 3 + 23 files changed, 642 insertions(+), 290 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/documentation.yml create mode 100644 .github/workflows/test.yml create mode 100644 docs/images/icon.svg create mode 100644 docs/mkdocs/.gitignore create mode 100644 docs/mkdocs/assets/icon.png create mode 100644 docs/mkdocs/assets/icon.svg create mode 100755 docs/mkdocs/execute-mkdocs.sh create mode 100644 docs/mkdocs/extra-content/index.md create mode 100644 docs/mkdocs/javascripts/mathjax.js create mode 100644 docs/mkdocs/mkdocs.yml.template create mode 100755 docs/mkdocs/prepare-mkdocs.sh create mode 100644 docs/mkdocs/stylesheets/custom.css diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f024e4f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [adalkiran] diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..246500d --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,45 @@ +name: Release Documentation + +on: + workflow_dispatch: + push: + branches: + - 'docs-mkdocs' + - 'main' + paths: + - 'docs/**' + - '.github/workflows/**' + pull_request: + branches: + - 'main' + +permissions: + contents: write + +jobs: + deploy: + name: Deploy documentation + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@v3 + with: + sparse-checkout: | + .github + docs + + - name: Prepare files + run: chmod +x docs/mkdocs/prepare-mkdocs.sh && cd docs && ./mkdocs/prepare-mkdocs.sh + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12.2' + + - name: Install mkdocs + run: pip install mkdocs-material mkdocs-material[imaging] + + - name: Perform deployment + run: cd docs/mkdocs && mkdocs gh-deploy --force + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..99bbd81 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Build and Test + +# This workflow only runs linter, builds and runs unit tests for development branches. + +on: + workflow_dispatch: + push: + branches: + - '**' + - '!main' + paths-ignore: + - 'docs/**' + pull_request: + branches: + - '**' + - '!main' + +jobs: + + build: + name: Build and test project + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Preparing + run: sudo apt-get -y install libvpx-dev + + - name: Linting + run: | + cd backend + go fmt ./... + go vet ./... + + - name: Test + run: cd backend && go test -v ./... + + - name: Build + run: cd backend && go build -v ./... diff --git a/README.md b/README.md index f1e8ce3..c0a860d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# **WebRTC Nuts and Bolts** - +# **WebRTC Nuts and Bolts** [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/alper-dalkiran/) [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white&style=flat-square)](https://twitter.com/aalperdalkiran) @@ -14,9 +13,7 @@ You can track which steps taken during this journey by debugging or tracking the ![Backend initial output](docs/images/01-07-backend-initial-output.png) -
- -## **WHY THIS PROJECT?** +## :thought_balloon: **WHY THIS PROJECT?** This project was initially started to learn Go language and was made for experimental and educational purposes only, not for production use. @@ -24,21 +21,19 @@ After some progress on the development, I decided to pivot my experimental work But my style of learning leans on the deductive method instead of others, so instead of learning atomic pieces and concepts first, going linearly from beginning to the end, and learning an atomic piece on the time when learning this piece is required. -
- -## **DOCUMENTATION** +## :blue_book: **DOCUMENTATION** -The adventure of a WebRTC stream from start to finish can be found documented as step by step in [docs folder](docs/) +The adventure of a WebRTC stream from start to finish can be found documented as step by step at [WebRTC Nuts and Bolts - GitHub Pages](https://adalkiran.github.io/webrtc-nuts-and-bolts/) website with a visually better experience, or at [docs directory](./docs/). -
- -## **COVERAGE** +## :dart: **COVERAGE** Web front-end side: Pure TypeScript implementation: + * Communicate with signaling backend WebSocket, * Gathering webcam streaming track from browser and send this track to backend via UDP. Server back-end side: Pure Go language implementation: + * A simple signaling back-end WebSocket to transfer [SDP (Session Description Protocol)](https://en.wikipedia.org/wiki/Session_Description_Protocol) using [Gorilla WebSocket](https://github.com/gorilla/websocket) library. * Single port UDP listener, supports demultiplexing different data packet types (STUN, DTLS handshake, SRTP, SRTCP) coming from the same UDP connection. * Protocol implementations of (only required parts): @@ -50,10 +45,7 @@ Server back-end side: Pure Go language implementation: * [github.com/fatih/color](https://github.com/fatih/color) was used while printing colored output on console while logging. * Implementation of TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 [cipher suite](https://www.keyfactor.com/blog/cipher-suites-explained/) support using [Go Cryptography](https://pkg.go.dev/golang.org/x/crypto) library. - -
- -## **INSTALLATION and RUNNING** +## :package: **INSTALLATION and RUNNING** This project was designed to run in Docker Container. Docker Compose file creates two containers: webrtcnb-ui and webrtcnb-backend. @@ -97,16 +89,13 @@ Then, follow these steps: * After completion of all installations, press F5 to start server application. * Then, open a web browser and visit http://localhost:8080 (Tested on Chrome) -
- -## **ASSUMPTIONS** +## :bricks: **ASSUMPTIONS** Full-compliant WebRTC libraries should support a wide range of protocol details defined in RFC documents, client/server implementation differences, fallbacks for different protocol versions, a wide variety of cipher suites and media encoders/decoders. Also should be implemented as state machines, because WebRTC contains has some parts which managed as state machines, eg: [ICE (Interactive Connectivity Establishment)](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment), [DTLS (Datagram Transport Layer Security)](https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security) handshake, etc... - In **WebRTC Nuts and Bolts** scenario, some assumptions have been made to focus only on required set of details. -| Full-compliant WebRTC libraries | WebRTC Nuts and Bolts | +| Full-compliant WebRTC libraries | WebRTC Nuts and Bolts | |---|---| | WebRTC has no client or server concepts in its [peer-to-peer](https://tr.wikipedia.org/wiki/Peer-to-peer) nature, there are controlling or controlled peers. | This project aims to act as listener server and it only receives media, not sends. To make the code more simplistic and cleaner; the concepts "client" instead of "local peer" and "server" instead of "remote peer" has been used. | | Should support both controlling and controlled roles. | Go language side will act only as server (ICE controlling), SDP offer will come from this side, then SDP answer will be expected from the client. | @@ -115,9 +104,13 @@ In **WebRTC Nuts and Bolts** scenario, some assumptions have been made to focus | Should support multiple cipher suites for compatibility with different types of peers. More cipher suites can be found at [here](https://developers.cloudflare.com/ssl/ssl-tls/cipher-suites/). |  Only TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 is supported. | | Should implement packet reply detection, handling corrupted packets, handling unordered packet sequences and packet losses, byte array length checks, lots of security protections against cyberattacks, etc... | This project was developed to run in only ideal conditions. Incoming malicious packets were not considered. | -
+## :star: **CONTRIBUTING and SUPPORTING the PROJECT** + +You are welcome to [create issues](https://github.com/adalkiran/webrtc-nuts-and-bolts/issues/new) to report any bugs or problems you encounter. At present, I'm not sure whether this project should be expanded to cover more concepts or not. Only time will tell :blush:. -## **RESOURCES** +If you liked and found my project helpful and valuable, I would greatly appreciate it if you could give the repo a star :star: on GitHub. Your support and feedback not only help the project improve and grow but also contribute to reaching a wider audience within the community. Additionally, it motivates me to create even more innovative projects in the future. + +## :book: **RESOURCES** I want to thank to contributors of the awesome sources which were referred during development of this project and writing this documentation. You can find these sources below, also in between the lines in code and documentation. @@ -134,8 +127,7 @@ I want to thank to contributors of the awesome sources which were referred durin * [Tinydtls](https://github.com/eclipse/tinydtls): A library for DTLS processes, developed in C. * [Mozilla Web Docs: WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API): A documentation on WebRTC API at browser side. * Several RFC Documents: In code and documentation of this project, you can find several RFC document links cited. -
-## **LICENSE** +## :scroll: **LICENSE** -WebRTC Nuts and Bolts is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text. \ No newline at end of file +WebRTC Nuts and Bolts is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text. diff --git a/docs/00-INFRASTRUCTURE.md b/docs/00-INFRASTRUCTURE.md index eaa5890..3ee4ece 100644 --- a/docs/00-INFRASTRUCTURE.md +++ b/docs/00-INFRASTRUCTURE.md @@ -5,6 +5,7 @@ When you run the docker-compose.yml file individually (for production mode) or d ## **0.1. Container webrtcnb-ui (ui/Dockerfile) is booting up...** Related part of [docker-compose.yml](../docker-compose.yml): + ```yml ... ui: @@ -23,6 +24,7 @@ When you run the docker-compose.yml file individually (for production mode) or d ``` Related part of [ui/Dockerfile](../ui/Dockerfile): + ```dockerfile ARG VARIANT=18-bullseye FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} @@ -37,9 +39,11 @@ ENTRYPOINT yarn install && npm run start This file inherits from *mcr.microsoft.com/vscode/devcontainers/typescript-node:18-bullseye* image which come up with an environment includes NodeJS, Webpack, Webpack Dev Server, TypeScript, over Debian "Bullseye" Linux Distribution. We don't need to install related things manually. While building the custom image (once): + * Runs *apt-get update* to update installed OS dependencies While every time of the container starting up: + * Boots up *webrtcnb-ui* container * Maps */ui* directory (in host machine) to */workspace* (in container) * Exposes container's *8080* port to the host (so we can browse the website served in the container) @@ -50,6 +54,7 @@ While every time of the container starting up: ## **0.2. Container webrtcnb-backend (backend/Dockerfile) is booting up...** Related part of [docker-compose.yml](../docker-compose.yml): + ```yml ... backend: @@ -74,6 +79,7 @@ While every time of the container starting up: ``` Related part of [backend/Dockerfile](../backend/Dockerfile): + ```dockerfile ARG VARIANT=1.20.2-bullseye FROM golang:${VARIANT} @@ -92,12 +98,14 @@ ENTRYPOINT "/entrypoint.sh" This file inherits from *golang:1.20.2-bullseye* image which come up with an environment includes Go language support, libraries for processing VP8 (video) and OPUS (audio) encoding, on Debian "Bullseye" Linux Distribution. We don't need to install related things manually. While building the custom image (once): + * Embeds [entrypoint.sh](../backend/entrypoint.sh) and [entrypoint-dev.sh](../backend/entrypoint-dev.sh) files into the custom image * Runs *apt-get update* to update installed OS dependencies * Installs [libvpx](https://en.wikipedia.org/wiki/Libvpx) and other codec libraries. Currently we use the libvpx one only. * Allows the entrypoint shell script files to be executed. While every time of the container starting up: + * Boots up *webrtcnb-backend* container * Maps */backend* directory (in host machine) to */workspace* (in container) * Exposes container's *8081* port to the host (so our browser can access the websocket served in the container) @@ -106,6 +114,7 @@ While every time of the container starting up: * Executes *[entrypoint.sh](../backend/entrypoint.sh)* or *[entrypoint-dev.sh](../backend/entrypoint-dev.sh)* according to which mode (production or development) it runs. Related part of [backend/entrypoint.sh](../backend/entrypoint.sh): + ```sh echo "Downloading dependent Go modules..." go mod download -x @@ -115,6 +124,7 @@ go run . ``` Related part of [backend/entrypoint-dev.sh](../backend/entrypoint-dev.sh): + ```sh echo "Downloading dependent Go modules..." go mod download -x @@ -123,6 +133,7 @@ tail -f /dev/null ``` * Both entrypoint.sh and entrypoint-dev.sh files call *go mod download -x* to download and install related Go language dependencies defined in [go.mod](../backend/go.mod) (this step can take some time) + * If it is in production mode, it calls
*go run .*
to start our server immediately. @@ -137,6 +148,7 @@ If you followed up related instructions correctly, you can see outputs like thes * Checking the containers are running: Expected output (can vary) + ```console $ docker ps @@ -150,6 +162,7 @@ CONTAINER ID IMAGE COMMAND CREATED If you can see *「wdm」: Compiled successfully.* in latest output, it has started serving successfully. Expected output (can vary) (you can exit by pressing CTRL+C) + ```console $ docker logs -f webrtcnb-ui @@ -181,7 +194,7 @@ webpack 5.72.0 compiled successfully in 4333 ms If you can see *Running into Waiting loop...* in latest output, it has started successfully and waiting for you to start the server application manually. -```console +```sh $ docker logs -f webrtcnb-backend Container started diff --git a/docs/02-BACKEND-INITIALIZATION.md b/docs/02-BACKEND-INITIALIZATION.md index 199fee3..d9e2fbb 100644 --- a/docs/02-BACKEND-INITIALIZATION.md +++ b/docs/02-BACKEND-INITIALIZATION.md @@ -4,14 +4,12 @@ The entrypoint of backend application is the "main" function in [backend/src/mai This function will generate server DTLS certificate, discover local IPs and external IP by asking configured STUN Server, create the conference manager object, start UDP listener, HTTP server with WebSocket for Signaling, then wait for client requests. -
- ## **2.1. Waiting loop** -
It starts with creating a wait group, adds threads that needs to be added to the wait list. The *waitGroup.Wait()* method runs in a loop that waits until waitGroup item count becomes zero, so the process doesn't end. from [backend/src/main.go](../backend/src/main.go) + ```go func main() { waitGroup := new(sync.WaitGroup) @@ -20,40 +18,35 @@ func main() { } ``` - -
- ## **2.2. Loading configuration** -
Configuration file is loaded from [config.yaml](../backend/config.yml). -
-Sources: + +Sources: + * [A Medium article](https://medium.com/@bnprashanth256/reading-configuration-files-and-environment-variables-in-go-golang-c2607f912b63) * [Viper project (Github)](https://github.com/spf13/viper) from [backend/src/main.go](../backend/src/main.go) + ```go config.Load() ``` - -
- ## **2.3. DTLS initialization, generating self-signed certificate** -
- One piece of the process after a client's first request is DTLS Handshake. We will discuss further in chapter [05. DTLS HANDSHAKE](./05-DTLS-HANDSHAKE.md) During this handshake process, each peer send their digitally signed certificate each other to identify and prove themselves. This certifcate is a [X.509 certificate](https://en.wikipedia.org/wiki/X.509). In the DTLS handshake, you can use a pair of private key and public key, which: + * Previously generated and digitally signed by a known [Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority) (keys are stored in disk, database, configuration file, or somewhere in sort of formats) * Previously generated and digitally signed by yourself ([Self-signed Certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) by 3rd party software like OpenSSL) (keys are stored in disk or database, configuration file, or somewhere in sort of formats) * **(Our preference)** On-the-fly generated and digitally signed by yourself (same principles with Self-signed Certificate) but stored temporarily in RAM, it changes with every start of the application. from [backend/src/main.go](../backend/src/main.go) + ```go dtls.Init() ``` @@ -67,6 +60,7 @@ You can use different random generators (in cryptography, randomness is an impor Also we can use different methods to generate private and public keys, but in this project we preferred these options. from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go func generateServerCertificatePrivateKey() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -76,6 +70,7 @@ func generateServerCertificatePrivateKey() (*ecdsa.PrivateKey, error) { Now, we have randomly generated private key, and it's signed form (public key) in same object ([ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa#PrivateKey)). We create an [X.509 Certificate](https://pkg.go.dev/crypto/x509#Certificate) as byte array, then put them together into a [tls.Certificate](https://pkg.go.dev/crypto/tls#Certificate) object. from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go pubKey := &serverCertificatePrivateKey.PublicKey template := x509.Certificate{ @@ -115,6 +110,7 @@ Then, we should get fingerprint hash of this Server Certificate, to use further This function takes the byte array consists of certificate content, calculates [SHA256 Checksum](https://en.wikipedia.org/wiki/SHA-2) of it, result will be 32 bytes. Then, it converts this 32 bytes array to string which separated with ":", like "12:B9:B6:79:44:19:52:26:1D:01:63:2B:8B:3C:7D:19:CC:B2:5F:B5:9D:68:94:39:8D:01:D0:7B:40:6E:44:65". We store the result as global variable, "dtls.ServerCertificateFingerprint" (string) from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go func GetCertificateFingerprintFromBytes(certificate []byte) string { fingerprint := sha256.Sum256(certificate) @@ -131,6 +127,7 @@ func GetCertificateFingerprintFromBytes(certificate []byte) string { ``` Sources: + * [WebRTC for the Curious: Securing](https://webrtcforthecurious.com/docs/04-securing/#securing) (this source also contains details of DTLS Handshake which we will discuss further) * [Pion WebRTC DTLS project, GenerateSelfSigned function (Github)](https://github.com/pion/dtls/blob/bee42643f57a7f9c85ee3aa6a45a4fa9811ed122/pkg/crypto/selfsign/selfsign.go#L22) * [What Is an X.509 Certificate & How Does It Work? @@ -140,19 +137,18 @@ Sources: * SHA256 Sum on command line [source 1](https://www.baeldung.com/linux/sha-256-from-command-line), [source 2](https://techdocs.akamai.com/download-ctr/docs/verify-checksum) -
- ## **2.4. Gathering local and external IPs** -
We need to know which IPs (local or external) that our server is reachable on, to use further in SDP generation (candidates part). from [backend/src/main.go](../backend/src/main.go) + ```go discoveredServerIPs := discoverServerIPs() ``` * Discovery of local IP addresses of available and active [network interfaces](https://en.wikipedia.org/wiki/Network_interface): Made via "GetLocalIPs" function in [backend/src/common/networkutils.go](../backend/src/common/networkutils.go). Due to our application runs in a container, and we didn't configure Docker networking type of container as "host", we can gather only container's network interfaces, not the host machine. Expected output is one IP that in our Docker's subnet, usually starts with 172. +
Note: Anyone outside the Docker network (including the host machine itself) cannot reach using this IP address. The other side of Docker networking interface to the host machine has a different gateway IP. But we include it in our result anyway. @@ -161,22 +157,23 @@ Note: Anyone outside the Docker network (including the host machine itself) cann * Discovery of external (WAN) IP, even if behind NAT. A logical and applicable way to learn our WAN IP (our router's IP open to internet) is to ask someone else outside our network. As there are some globally available free STUN Servers, we can use one which is set up by ourselves; however, it is important that the STUN Server should be outside of our network, we should access it by WAN. We need a STUN (Session Traversal Utilities for NAT) client to speak in STUN protocol, so our project implements it with only required parts (not all STUN messages or attributes implemented). Sources: + * [WebRTC for the Curious: STUN](https://webrtcforthecurious.com/docs/03-connecting/#stun) * [Wikipedia: STUN](https://en.wikipedia.org/wiki/STUN) * [Some STUN Server addresses](https://gist.github.com/zziuni/3741933) * [STUN Protocol RFC - Session Traversal Utilities for NAT](https://datatracker.ietf.org/doc/html/rfc5389) -
### **2.4.1. Using our STUN Client** -
We create a STUN Client via "NewStunClient" function in [backend/src/stun/stunclient.go](../backend/src/stun/stunclient.go). This function takes some arguments: + * serverAddr: Configured STUN Server Address (can be accessed via config.Val.Server.StunServerAddr). Default is "stun.l.google.com:19302". * ufrag: "User fragment" is a string can be considered as "user name" for STUN Server. We generate a random ufrag via "GenerateICEUfrag" function in [backend/src/agent/generators.go](../backend/src/agent/generators.go). * pwd: Can be considered as "password" for STUN Server. We generate a random ufrag via "GenerateICEPwd" function in [backend/src/agent/generators.go](../backend/src/agent/generators.go). from [backend/src/stun/stunclient.go](../backend/src/stun/stunclient.go) + ```go func NewStunClient(serverAddr string, ufrag string, pwd string) *StunClient { return &StunClient{ @@ -190,18 +187,16 @@ func NewStunClient(serverAddr string, ufrag string, pwd string) *StunClient { We call our STUN Client's Discover() method, then add it to our candidate IP list. from [backend/src/main.go](../backend/src/main.go) + ```go mappedAddress, err := stunClient.Discover() ``` - -
- ### **2.4.2. Implementing STUN Protocol (as Client)** -
STUN packet structure -``` + +```console 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -220,6 +215,7 @@ We call our STUN Client's Discover() method, then add it to our candidate IP lis Our STUN message struct in [backend/src/stun/message.go](../backend/src/stun/message.go): from [backend/src/stun/message.go](../backend/src/stun/message.go) + ```go type Message struct { MessageType MessageType @@ -235,11 +231,11 @@ As a client, to ask our IP to the server, we should create a *STUN Binding Reque * Create a STUN message with "STUN Message Type" of MessageTypeBindingRequest (consists of Method: MessageMethodStunBinding (0x0001) and Class: MessageClassRequest (0x00)), generated Transaction ID and STUN Magic Cookie (constant value: 0x2112A442).
You can find: + * MessageType constants in [backend/src/stun/messagetype.go](../backend/src/stun/messagetype.go) * MessageClass constants in [backend/src/stun/messageclass.go](../backend/src/stun/messageclass.go) * MessageMethod constants in [backend/src/stun/messagemethod.go](../backend/src/stun/messagemethod.go) - * Now, our steps for sending binding request are: * We resolve IP address of ServerAddr (with port number) * Create binding request message object @@ -250,6 +246,7 @@ You can find: * Write encoded byte array to STUN Server UDP address via started listener. from [backend/src/stun/stunclient.go](../backend/src/stun/stunclient.go) + ```go serverUDPAddr, err := net.ResolveUDPAddr("udp", c.ServerAddr) if err != nil { @@ -276,6 +273,7 @@ After we have sent *STUN Binding Request* message successfully, we expect that t * If message integrity and authorization check succeeded, we decode the attribute that contains result IP address encoded with XOR, via DecodeAttrXorMappedAddress function, and return as result. from [backend/src/stun/stunclient.go](../backend/src/stun/stunclient.go) + ```go buf := make([]byte, 1024) @@ -308,18 +306,17 @@ After we have sent *STUN Binding Request* message successfully, we expect that t At the end, now we have gathered all available IP addresses. Sources: -* [go-stun project (Github)](https://github.com/ccding/go-stun) -
+* [go-stun project (Github)](https://github.com/ccding/go-stun) ## **2.5. Initialize Conference Manager** -
We create our ConferenceManager object that manages active conferences, server side ICE Agents per conference, and SDP Offer Answers, incoming via signaling WebSocket. The ConferenceManager has Run() method to listen ChanSdpOffer [Go Channel](https://go.dev/tour/concurrency/2) in infinite loop, we call this method as [Go Routine](https://medium.com/technofunnel/understanding-golang-and-goroutines-72ac3c9a014d), so it runs in parallel thread. We increase waitGroup's waiting list. from [backend/src/main.go](../backend/src/main.go) + ```go conferenceManager = conference.NewConferenceManager(discoveredServerIPs, config.Val.Server.UDP.SinglePort) waitGroup.Add(1) @@ -327,19 +324,18 @@ The ConferenceManager has Run() method to listen ChanSdpOffer [Go Channel](https ``` Sources: + * [Channel in Golang](https://www.geeksforgeeks.org/channel-in-golang/) * [Goroutines](https://golangbot.com/goroutines/) -
- ## **2.6. Starting UDP Listener** -
We create our UdpListener object that starts to listen specified UDP port and process incoming packets. The UdpListener has Run() method to listen UDP port in infinite loop, we call this method as [Go Routine](https://medium.com/technofunnel/understanding-golang-and-goroutines-72ac3c9a014d), so it runs in parallel thread. We increase waitGroup's waiting list. from [backend/src/main.go](../backend/src/main.go) + ```go var udpListener = udp.NewUdpListener("0.0.0.0", config.Val.Server.UDP.SinglePort, conferenceManager) waitGroup.Add(1) @@ -357,6 +353,7 @@ At the "Run" function in [backend/src/udp/udpListener.go](../backend/src/udp/udp * AddBuffer function acts as demultiplexer for different types of packets (different types of protocols) on same connection. from [backend/src/udp/udpListener.go](../backend/src/udp/udpListener.go) + ```go conn, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IP{0, 0, 0, 0}, @@ -389,16 +386,14 @@ At the "Run" function in [backend/src/udp/udpListener.go](../backend/src/udp/udp } ``` -
- ## **2.7. Starting Signaling HTTP Server** -
We create our signaling.HttpServer object that starts to listen specified signaling port and process incoming HTTP requests. The HttpServer has Run() method to listen signaling port in infinite loop, we call this method as [Go Routine](https://medium.com/technofunnel/understanding-golang-and-goroutines-72ac3c9a014d), so it runs in parallel thread. We increase waitGroup's waiting list. from [backend/src/main.go](../backend/src/main.go) + ```go httpServer, err := signaling.NewHttpServer(fmt.Sprintf(":%d", config.Val.Server.Signaling.WsPort), conferenceManager) ... @@ -413,6 +408,7 @@ At the "NewHttpServer" function in [backend/src/signaling/httpserver.go](../back "/ws" path for WebSocket requests. from [backend/src/signaling/httpserver.go](../backend/src/signaling/httpserver.go) + ```go func NewHttpServer(httpServerAddr string, conferenceManager *conference.ConferenceManager) (*HttpServer, error) { wsHub := newWsHub(conferenceManager) @@ -433,9 +429,11 @@ func NewHttpServer(httpServerAddr string, conferenceManager *conference.Conferen At the "Run" function in [backend/src/signaling/httpserver.go](../backend/src/signaling/httpserver.go), we call wsHub.run() and http.ListenAndServe to start signaling HTTP server. Sources: + * [Gorilla WebSocket: Chat Example (GitHub)](https://github.com/gorilla/websocket/tree/master/examples/chat) Now, our server application is waiting for client interactions on: + * For signaling requests on WebSocket port 8081 (default) * For incoming UDP packets (STUN, DTLS, RTP, RTCP, etc... packets) on port 15000 (default) diff --git a/docs/03-FIRST-CLIENT-COMES-IN.md b/docs/03-FIRST-CLIENT-COMES-IN.md index c2bf7c9..51fa7ba 100644 --- a/docs/03-FIRST-CLIENT-COMES-IN.md +++ b/docs/03-FIRST-CLIENT-COMES-IN.md @@ -10,10 +10,11 @@ Now, we can visit our web application by opening a browser window and writing ht When the web page was loaded, we run initialization code. -We defined *RTC* and *Signaling* classes in TypeScript in the same file [ui/src/app.ts](../ui/src/app.ts), we create an instance for each one, assign them as property of *window* object to access them globally, our first focus is not "clean code" in this project :) +We defined *RTC* and *Signaling* classes in TypeScript in the same file [ui/src/app.ts](../ui/src/app.ts), we create an instance for each one, assign them as property of *window* object to access them globally, our first focus is not "clean code" in this project :blush: We add onClick event handlers for our two buttons, *BtnCreatePC* and *BtnStopPC* to start and stop our PeerConnection. We also add onBeforeUnload event handler to *window* to close open connections gracefully, so we can inform the server application "hey, we are leaving the conference" on closing the browser tab/window. from [ui/src/app.ts](../ui/src/app.ts) + ```ts const rtc = new RTC(); (window).rtc = rtc; @@ -31,10 +32,7 @@ function initApp() { initApp(); ``` -
- ## **3.1. Initialization of RTC object** -
In the constructor of the *RTC* class, calls the "createLocalPeerConnection" function which creates a [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection), which represents a connection between the local device and a remote peer. This is a native browser API object. @@ -43,6 +41,7 @@ The [constructor of *RTCPeerConnection*](https://developer.mozilla.org/en-US/doc We assign some event handlers for some events, which we will discuss further. We assign handlers to some of them only to print to console, to be informed: "This event was fired". from [ui/src/app.ts](../ui/src/app.ts) + ```ts ... createLocalPeerConnection() { @@ -86,22 +85,18 @@ We assign some event handlers for some events, which we will discuss further. We ... ``` -
- ## **3.2. Let the show begin!** -
We discussed lots of boring stuff so far (unfortunately there is more)... Now, we click on the "Create PeerConnection" button to start the show! -
- ### **3.2.1 Creating streams, connecting to Signaling Server WebSocket** -
The button click will call "rtc.start()" function which: + * Calls "createLocalTracks" method, that says to the browser, "I want a video stream of default webcam, if it can be, should be in height 720p, also I want an audio stream of default microphone". The browser returns a [Promise<MediaStream>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), which we should wait for asking for permissions and (if available and permitted) initialization of camera and microphone devices. from [ui/src/app.ts](../ui/src/app.ts) + ```ts createLocalTracks(): Promise { return navigator.mediaDevices.getUserMedia({ @@ -116,6 +111,7 @@ The button click will call "rtc.start()" function which: * We called this.createLocalTracks, chained further operations by "then" to the returned Promise, and waiting for the browser asks for permissions and create streams for us. from [ui/src/app.ts](../ui/src/app.ts) + ```ts start() { return this.createLocalTracks() @@ -151,6 +147,7 @@ You can find detailed information about the signaling flow here: [Signaling tran We add related event handlers to our WebSocket client object. We will discuss on "ws.onmessage" futher. from [ui/src/app.ts](../ui/src/app.ts) + ```ts connect() { console.log("Start connect() http://localhost:8081/ws/"); @@ -175,15 +172,13 @@ We add related event handlers to our WebSocket client object. We will discuss on * Now we are waiting for "Welcome" message from the Signaling Server. -
- ### **3.2.2 The Signaling Server welcomes the client** -
When we make the first contact with the backend WebSocket with connecting, "serveWs" function in [backend/src/signaling/httpserver.go](../backend/src/signaling/httpserver.go) will be triggered. This function creates a WsClient object (defined in [backend/src/signaling/wsclient.go](../backend/src/signaling/wsclient.go)) and it forwards to the wsHub's "register" [channel](https://go.dev/tour/concurrency/2). This channel is listened by run() function of WsHub object, shown below: from [backend/src/signaling/httpserver.go](../backend/src/signaling/httpserver.go) + ```go func (s *HttpServer) serveWs(w http.ResponseWriter, r *http.Request) { ... @@ -196,6 +191,7 @@ func (s *HttpServer) serveWs(w http.ResponseWriter, r *http.Request) { This method in an infinite loop, when a message forwarded to "register" channel, it registers the new incoming client object itself, and sends a "ClientWelcomeMessage" with message type "Welcome". from [backend/src/signaling/wshub.go](../backend/src/signaling/wshub.go) + ```go func (h *WsHub) run() { for { @@ -216,10 +212,7 @@ func (h *WsHub) run() { } ``` -
- ### **3.2.3 The client receives the *Welcome* message of Signaling Server, then sends *JoinConference* request** -
When any data is received by the WebSocket client, the "ws.onmessage" event will be fired. @@ -228,6 +221,7 @@ When any data is received by the WebSocket client, the "ws.onmessage" event will The "Welcome message" coming from the server is processed by switch case for "Welcome" "data.type". The code below sends a JSON message that says "I want to join the conference named 'defaultConference'". Generated and sent JSON: + ```json { "type": "JoinConference", @@ -238,6 +232,7 @@ The "Welcome message" coming from the server is processed by switch case for "We ``` from [ui/src/app.ts](../ui/src/app.ts) + ```ts this.ws.onmessage = (message) => { const data = message.data ? JSON.parse(message.data) : null; @@ -257,14 +252,13 @@ The "Welcome message" coming from the server is processed by switch case for "We } ``` -
- ### **3.2.4 The client receives the *Welcome* message of Signaling Server, sends *JoinConference* request, then server sends SDP Offer** -
This method in an infinite loop, when a message forwarded to "messageReceived" channel, it [Unmarshalls](https://pkg.go.dev/encoding/json#Unmarshal) it, looks the type attribute of the JSON.
+ If it is "JoinConference": + * It calls "processJoinConference" function of WsHub object. * After, "EnsureConference" function in [backend/src/conference/conferencemanager.go](../backend/src/conference/conferencemanager.go) is called to create a conference object if not exists.
@@ -273,12 +267,13 @@ This function calls "NewConference", which creates a conference object and a new This agent will store signaled SDP data and UDP sockets related with the conference. Every ICE Agent has a unique Ufrag and Pwd string, which randomly generated while creating the instance. This Ufrag and Pwd data will be sent to the client inside SDP Offer.

-**Note:** By these Conference and ServerAgent objects per conference, we are able to manage multiple different conference rooms, but in this sample project we use only one conference name "defaultConference", and it doesn't count as a complete conference anyways :) +**Note:** By these Conference and ServerAgent objects per conference, we are able to manage multiple different conference rooms, but in this sample project we use only one conference name "defaultConference", and it doesn't count as a complete conference anyways :blush: * Then, "GenerateSdpOffer" function in [backend/src/sdp/sdp.go](../backend/src/sdp/sdp.go) is called to create an SDP Offer data, containing media types, ICE candidates' IP and port data, etc...
The generated SDP Offer data is sent to the client as JSON via Signaling WebSocket. from [backend/src/conference/conference.go](../backend/src/conference/conference.go) + ```go func NewConference(conferenceName string, candidateIPs []string, udpPort int) *Conference { result := &Conference{ @@ -290,6 +285,7 @@ func NewConference(conferenceName string, candidateIPs []string, udpPort int) *C ``` from [backend/src/signaling/wshub.go](../backend/src/signaling/wshub.go) + ```go func (h *WsHub) run() { for { @@ -313,10 +309,7 @@ During this process, our server console will be similar to this: ![Server: a new client connected](images/03-05-server-a-new-client-connected.png) -
- ### **3.2.5 The client receives the *SdpOffer* message of Signaling Server** -
When any data was received by the WebSocket client, the "ws.onmessage" event will be fired. @@ -327,6 +320,7 @@ The "SDP Offer message" coming from the server is processed by switch case for " * This message came as JSON, we need to convert it to [SDP format](https://en.wikipedia.org/wiki/Session_Description_Protocol) via [sdp-transform](https://www.npmjs.com/package/sdp-transform) library. from [ui/src/app.ts](../ui/src/app.ts) + ```ts this.ws.onmessage = (message) => { ... @@ -395,6 +389,7 @@ The "SDP Offer message" coming from the server is processed by switch case for " } } ``` + * The JSON message converted to the SDP format as: ![Browser Received SDP Offer](images/03-07-browser-received-sdpoffer.png) @@ -402,6 +397,7 @@ The "SDP Offer message" coming from the server is processed by switch case for " * We give the SDP Offer string to "acceptOffer" function of our *RTC* object. This function calls [setRemoteDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription) function of our RTCPeerConnection object. By this, we say our the localConnection (RTCPeerConnection) that "You fired the 'onnegotiationneeded' event and wanted us to SDP Offer/Answer negotiation, we did it and please take it then create an SDP Answer for us". from [ui/src/app.ts](../ui/src/app.ts) + ```ts acceptOffer(offerSdp: string) { return this.localConnection.setRemoteDescription({ @@ -434,22 +430,22 @@ The "SDP Offer message" coming from the server is processed by switch case for " * On onicecandidate event fired when our localConnection object's iceGatheringState value comes 'complete', it's time to parse *localConnection.localDescription.sdp* value (generated SDP Answer above), give it to "sendSdpToSignaling" function, converts this SDP Answer to JSON and sends to the client via Signaling. from [ui/src/app.ts](../ui/src/app.ts) + ```ts sendSdpToSignaling(parsedSdp: sdpTransform.SessionDescription) { console.log('sendSdpToSignaling', parsedSdp); signaling.ws.send(JSON.stringify({type: "SdpOfferAnswer", data: parsedSdp})); } ``` -![Browser SDP Answer via Signaling](images/03-11-browser-send-sdp-answer-signaling.png) -
+![Browser SDP Answer via Signaling](images/03-11-browser-send-sdp-answer-signaling.png) ### **3.2.6 The server receives the *SdpAnswer* message of Signaling Server** -
The client generated the SDP Answer corresponding to the SDP Offer the server sent. Then we forward it to ConferenceManager's ChanSdpOffer channel. from [backend/src/signaling/wshub.go](../backend/src/signaling/wshub.go) + ```go func (h *WsHub) run() { for { @@ -474,6 +470,7 @@ func (h *WsHub) run() { This method in an infinite loop, when a message forwarded to "ChanSdpOffer" channel, it loops over the incoming SDP Answer's media items, gives items to "EnsureSignalingMediaComponent" function of ServerAgent in [backend/src/agent/serveragent.go](../backend/src/agent/serveragent.go). This function creates SignalingMediaComponent objects per media item, then adds them to "SignalingMediaComponents" property of the ServerAgent. When UDP connection started, the UDP listener compares the ICE information coming from UDP and previously came "SignalingMediaComponents" via Signaling. from [backend/src/conference/conferencemanager.go](../backend/src/conference/conferencemanager.go) + ```go func (m *ConferenceManager) Run(waitGroup *sync.WaitGroup) { defer waitGroup.Done() diff --git a/docs/04-STUN-BINDING-REQUEST-FROM-CLIENT.md b/docs/04-STUN-BINDING-REQUEST-FROM-CLIENT.md index 6e86123..f7a2875 100644 --- a/docs/04-STUN-BINDING-REQUEST-FROM-CLIENT.md +++ b/docs/04-STUN-BINDING-REQUEST-FROM-CLIENT.md @@ -1,6 +1,7 @@ # **4. STUN BINDING REQUEST FROM CLIENT** In previous chapters: + * The client joined the conference room * Initialized webcam and microphone devices, created media stream, video and audio tracks * The client and the server exchanged SDP Offer and Answer via Signaling WebSocket @@ -8,10 +9,7 @@ In previous chapters: Note: For detailed information about STUN Protocol, you can find at "2.4.2. Implementing STUN Protocol (as Client)" chapter in [2. BACKEND INITIALIZATION](./02-BACKEND-INITIALIZATION.md) -
- ## **4.1. UDP Demultiplexing** -
First of all, this is our first "expected" packet incoming via UDP. " Demultiplexing" mechanism on sockets are implemented in a different (well architected) way, but in this project, we implemented the plain way. Let's explain our demultiplexing mechanism: @@ -21,6 +19,7 @@ First of all, this is our first "expected" packet incoming via UDP. " Demultiple * The "AddBuffer" function checks which protocol the packet can be related with: from [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) + ```go func (ms *UDPClientSocket) AddBuffer(buf []byte, offset int, arrayLen int) { logging.Descf(logging.ProtoUDP, "A packet received. The byte array (%d bytes) not parsed yet. Demultiplexing via if-else blocks.", arrayLen) @@ -44,22 +43,25 @@ func (ms *UDPClientSocket) AddBuffer(buf []byte, offset int, arrayLen int) { ``` In this context, we can check out stun.IsMessage function. This function checks: - * Data length (arrayLen) is greater than messageHeaderSize (20 bytes) -
and - * 4 bytes after 4. byte (between 4. and 8. indices) equals to STUN magicCookie (constant value, 0x2112A442). + +* Data length (arrayLen) is greater than messageHeaderSize (20 bytes) +
and +* 4 bytes after 4. byte (between 4. and 8. indices) equals to STUN magicCookie (constant value, 0x2112A442). If this buffer part complies with these conditions, we can say "this packet is a STUN protocol packet", then we can process it with STUN protocol's methods. from [backend/src/stun/message.go](../backend/src/stun/message.go) + ```go func IsMessage(buf []byte, offset int, arrayLen int) bool { return arrayLen >= messageHeaderSize && binary.BigEndian.Uint32(buf[offset+4:offset+8]) == magicCookie } ``` -
+ +
Click to expand Wireshark capture (Received): STUN Binding Request -``` +```console Frame 414: 140 bytes on wire (1120 bits), 140 bytes captured (1120 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -111,15 +113,15 @@ Session Traversal Utilities for NAT Attribute Length: 4 CRC-32: 0xaaff24d0 ``` -
-
+
Here is the console output when the server received an "expected" STUN Binding Request. ![Server Receive STUN Binding Request](images/04-01-server-received-stun-binding-request.png) After we determined that this packet is STUN packet, our steps will be: + * Decode the byte array as stun.Message object via "DecodeMessage" function [backend/src/stun/message.go](../backend/src/stun/message.go) * Validate the STUN packet's integrity via HMAC and fingerprint via CRC32, by "Validate" function [backend/src/stun/message.go](../backend/src/stun/message.go) * We only support "Binding Request" message type as incoming STUN packet, because of this, we have only one case. @@ -128,6 +130,7 @@ After we determined that this packet is STUN packet, our steps will be: * If everything is OK, we create a STUN Binding Response packet (by calling "createBindingResponse" function) and send it. This means "I accept your request, we can communicate by this channel, send me DTLS ClientHello message". from [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) + ```go switch stunMessage.MessageType { case stun.MessageTypeBindingRequest: @@ -159,10 +162,10 @@ After we determined that this packet is STUN packet, our steps will be: } ``` -
+
Click to expand Wireshark capture (Sent): STUN Binding Response -``` +```console Frame 451: 144 bytes on wire (1152 bits), 144 bytes captured (1152 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -217,9 +220,8 @@ Session Traversal Utilities for NAT Attribute Length: 4 CRC-32: 0x8387d149 ``` -
-
+
Now, the client will send a [ClientHello](https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.1.2) message and we can start the [DTLS Handshake](https://datatracker.ietf.org/doc/html/rfc4347#section-4.2) process. diff --git a/docs/05-DTLS-HANDSHAKE.md b/docs/05-DTLS-HANDSHAKE.md index 09dea68..70f4b62 100644 --- a/docs/05-DTLS-HANDSHAKE.md +++ b/docs/05-DTLS-HANDSHAKE.md @@ -6,7 +6,7 @@ In previous chapter, we received first "expected" UDP packet (STUN Binding Reque DTLS Handshake consists of some "flights" between client and server, schematized [here](https://tools.ietf.org/html/rfc4347#section-4.2.4) as: -``` +```console ------ ------ ClientHello --------> Flight 1 @@ -33,23 +33,23 @@ DTLS Handshake consists of some "flights" between client and server, schematized ``` Sources: -* [Breaking Down the TLS Handshake](https://www.youtube.com/watch?v=cuR05y_2Gxc&list=PLyqga7AXMtPMXgn1NwDqnSlgU05cnl4PA) (Before continuing, it's highly recommended to watch this Youtube playlist to understand TLS Handshake process, ciphers, curves, hashing, algorithms, etc... In videos, they tell about TLS v1.2 and v1.3, but we don't use v1.3). -
+* [Breaking Down the TLS Handshake](https://www.youtube.com/watch?v=cuR05y_2Gxc&list=PLyqga7AXMtPMXgn1NwDqnSlgU05cnl4PA) (Before continuing, it's highly recommended to watch this Youtube playlist to understand TLS Handshake process, ciphers, curves, hashing, algorithms, etc... In videos, they tell about TLS v1.2 and v1.3, but we don't use v1.3). ## **5.1. Client sends first ClientHello message (Flight 0)** -
When a new packet comes in, the "AddBuffer" function in [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) looks for which protocol standard this packet to rely on. In this context, we can check out dtls.IsDtlsPacket function. This function checks: - * Data length (arrayLen) is greater than zero -
and - * First byte of data (ordinally) is between 20 and 63. This byte represents the DTLS Record Header's ContentType value. + +* Data length (arrayLen) is greater than zero +
and +* First byte of data (ordinally) is between 20 and 63. This byte represents the DTLS Record Header's ContentType value. If this buffer part complies with these conditions, we can say "this packet is a DTLS protocol packet", then we can process it with DTLS protocol's methods. from [backend/src/dtls/dtlsmessage.go](../backend/src/dtls/dtlsmessage.go) + ```go func IsDtlsPacket(buf []byte, offset int, arrayLen int) bool { return arrayLen > 0 && buf[offset] >= 20 && buf[offset] <= 63 @@ -64,12 +64,10 @@ We determined that this packet is DTLS packet. A DTLS packet consists of a Recor The ClientHello packet we received is a "Handshake" message, so we should discuss the structure of a Handshake packet. -
- ## **5.1.1. DTLS Record Header** -
from [backend/src/dtls/recordheader.go](../backend/src/dtls/recordheader.go) + ```go type RecordHeader struct { ContentType ContentType @@ -81,6 +79,7 @@ type RecordHeader struct { ``` You can find: + * Detailed information about attributes of this record in [WebRTC for the Curious: Securing - DTLS](https://webrtcforthecurious.com/docs/04-securing/#dtls) * ContentType constants in [backend/src/stun/messagetype.go](../backend/src/dtls/recordheader.go) * DtlsVersion constants in [backend/src/stun/messagetype.go](../backend/src/dtls/recordheader.go) @@ -89,12 +88,10 @@ If Epoch is zero, the contents of the packet is in clear text, not encrypted. If Source: [Generic header structure of the DTLS record layer](https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L320) -
- ## **5.1.2. DTLS Record Header** -
from [backend/src/dtls/handshakeheader.go](../backend/src/dtls/handshakeheader.go) + ```go type HandshakeHeader struct { HandshakeType HandshakeType @@ -109,6 +106,7 @@ type HandshakeHeader struct { * "uint24" is defined in [backend/src/dtls/dtlsmessage.go](../backend/src/dtls/dtlsmessage.go), to represent 24 byte unsigned integer values in handshake data structure. from [backend/src/dtls/dtlsmessage.go](../backend/src/dtls/dtlsmessage.go) + ```go type uint24 [3]byte ``` @@ -120,15 +118,12 @@ DTLS fragmentation is not supported in this project, so we ignore these fragment Source: [Header structure for the DTLS handshake protocol](https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L344) - -
- ## **5.1.3. ClientHello Message Content (Flight 0)** -
The ClientHello message is the first message of the first flight. The RFC counts flights starting from 1, we count them starting from 0, because we assume the Flight 0 is "waiting for ClientHello" state, Flight 1 is after receiving the first ClientHello. So, you can track the flight numbers this way. from [backend/src/dtls/clienthello.go](../backend/src/dtls/clienthello.go) + ```go type ClientHello struct { Version DtlsVersion @@ -141,10 +136,10 @@ type ClientHello struct { } ``` -
+
Click to expand Wireshark capture (Received): DTLS ClientHello (first, without cookie) -``` +```console Frame 458: 189 bytes on wire (1512 bits), 189 bytes captured (1512 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -254,9 +249,8 @@ Datagram Transport Layer Security [JA3 Fullstring: 65277,49195-49199-52393-52392-49161-49171-49162-49172-156-47-53,23-65281-10-11-35-13-14,29-23-24,0] [JA3: c14667d7da3e6f7a7ab5519ef78c2452] ``` -
-
+
This message is bootstrapper of a new DTLS Handshake process. Client says to us, "I want to make a DTLS handshake with you, these are my security data to share". @@ -264,6 +258,7 @@ This message is bootstrapper of a new DTLS Handshake process. Client says to us, * Random: Each part (the client and the server) generates random values for themselves, we call them as "Client Random" and "Server Random". In DTLS v1.2, random byte array consists of 32 bytes. The first 4 bytes are for current system time, remaining 28 bytes are randomly generated. from [backend/src/dtls/random.go](../backend/src/dtls/random.go) + ```go type Random struct { GMTUnixTime time.Time @@ -278,6 +273,7 @@ Source: [Pion WebRTC: DTLS Random](https://github.com/pion/dtls/blob/b3e235f54b6 * CipherSuiteIDs: Each party has implemented and supported a set of [Cipher Suite](https://en.wikipedia.org/wiki/Cipher_suite)s, in DTLS and TLS each of them coded with uint16 constant value. With this information, the client informs us "I support these cipher suites, choose one of them which you support too, then we can continue using it."
A "Cipher Suite" is a set of algorithms, usually consists of a key exchange algorithm, a hash algorithm and signature/authentication algorithm. You can find cipher suites and values [here](https://www.rfc-editor.org/rfc/rfc8422.html#section-6), and our only one supported cipher suite constant at [backend/src/dtls/ciphersuites.go](../backend/src/dtls/ciphersuites.go). + * CompressionMethodIDs: We only support Uncompressed mode with value zero. * Extensions: Client can send a list of extension data with the ClientHello message. Some of them which we support: * UseExtendedMasterSecret: In encryption, we use a "master secret". Each party generates their own "master secret" and don't share it with others. There are some ways to generate it, a standard way and an extended way. We will discuss about this, but if ClientHello contains a UseExtendedMasterSecret extension, it means "the client uses extended master secret generation method, you should use it too". @@ -289,19 +285,18 @@ A "Cipher Suite" is a set of algorithms, usually consists of a key exchange algo

After receiving the first ClientHello (which doesn't contain a Cookie value), we: + * Set our DTLS Handshake Context's (HandshakeContext defined in [backend/src/dtls/handshakecontext.go](../backend/src/dtls/handshakecontext.go)) state as "Connecting" * Set the context's ProtocolVersion as incoming ClientHello's Version. We should check it, but in this project we assume two parties speak with DTLS v1.2. * Generate a 20 bytes DTLS Cookie by calling "generateDtlsCookie" function, and set the context's Cookie property. * Set the context's Flight to "Flight 2" -
- ## **5.2. Server sends HelloVerifyRequest message (Flight 2)** -
We generate a HelloVerifyRequest message by calling "createDtlsHelloVerifyRequest" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). We share the Cookie data which we generated previously (in context.Cookie) with the client. from [backend/src/dtls/helloverifyrequest.go](../backend/src/dtls/helloverifyrequest.go) + ```go type HelloVerifyRequest struct { Version DtlsVersion @@ -309,10 +304,10 @@ type HelloVerifyRequest struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS HelloVerifyRequest -``` +```console Frame 483: 80 bytes on wire (640 bits), 80 bytes captured (640 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -334,24 +329,19 @@ Datagram Transport Layer Security Cookie Length: 20 Cookie: b130316a0e21459cd54d85062f2fb018c4f78be0 ``` -
-
+
Now we are waiting for another (nearly the same) ClientHello message, but with a Cookie that has the same value as our HelloVerifyRequest. Sent HelloVerifyRequest - -
- ## **5.3. Client sends second ClientHello message (Flight 2)** -
-
+
Click to expand Wireshark capture (Received): DTLS ClientHello (second, with cookie) -``` +```console Frame 484: 209 bytes on wire (1672 bits), 209 bytes captured (1672 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -462,15 +452,15 @@ Datagram Transport Layer Security [JA3 Fullstring: 65277,49195-49199-52393-52392-49161-49171-49162-49172-156-47-53,23-65281-10-11-35-13-14,29-23-24,0] [JA3: c14667d7da3e6f7a7ab5519ef78c2452] ``` -
-
+
Received second ClientHello We received second ClientHello from the client, with nearl same content but with a Cookie. We: + * Know we are in Flight 2, we check if the incoming ClientHello's Cookie value and we sent via HelloVerifyRequest (stored in our context object). If it is empty, we should return to Flight 0 state and wait for a new ClientHello message. If not empty, we should compare two values. * Find a cipher suite ID which mutually supported by each peer, via calling "negotiateOnCipherSuiteIDs" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go), then set it to context.CipherSuite. In our example, negotiated on TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) cipher suite. * Loop through ClientHello message's extensions. We process the known and supported ones, and ignore unknown ones. Tracking the order at console output below: @@ -480,15 +470,12 @@ We: ![Processed second ClientHello](images/05-04-processed-second-clienthello.png) - -
- ### **5.3.1. Generating cryptographic keys** -
Now, server chose one from each cryptographic methods, algorithms etc... which client offered as alternatives. The server knows which methods they will use while encrypting, the client will learn further. The server should generate some secrets and keys, for it's side. We: + * Set incoming ClientHello.Random to context.ClientRandom * Generate a Server Random via calling "Generate" function in [backend/src/dtls/random.go](../backend/src/dtls/random.go) and set it to context.ServerRandom. * Generate a server private and server public key, by using [X25519 curve](https://pkg.go.dev/golang.org/x/crypto@v0.0.0-20220411220226-7b82a4e95df4/curve25519), via calling "GenerateCurveKeypair" function in [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go). These private and public keys are different from and not related with previously generated Server Certificate keys. @@ -512,6 +499,7 @@ We: * We set this calculated signature to context.ServerKeySignature from [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go) + ```go context.ServerKeySignature, err = GenerateKeySignature( clientRandomBytes, @@ -523,6 +511,7 @@ context.ServerKeySignature, err = GenerateKeySignature( ``` from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go func GenerateKeySignature(clientRandom []byte, serverRandom []byte, publicKey []byte, curve Curve, privateKey crypto.PrivateKey, hashAlgorithm HashAlgorithm) ([]byte, error) { msg := generateValueKeyMessage(clientRandom, serverRandom, publicKey, curve) @@ -543,16 +532,15 @@ func GenerateKeySignature(clientRandom []byte, serverRandom []byte, publicKey [] Now we are ready to send a group of messages: A ServerHello, a Certificate, a ServerKeyExchange, a CertificateRequest, and a ServerHelloDone message. These messages can be sent in same packet (by fragmenting) or one by one. We prefer to send them individually, one by one. Sources: -* [Pion WebRTC: DTLS, Generating Elliptic Keypair](https://github.com/pion/dtls/blob/bee42643f57a7f9c85ee3aa6a45a4fa9811ed122/pkg/crypto/elliptic/elliptic.go#L70) -
+* [Pion WebRTC: DTLS, Generating Elliptic Keypair](https://github.com/pion/dtls/blob/bee42643f57a7f9c85ee3aa6a45a4fa9811ed122/pkg/crypto/elliptic/elliptic.go#L70) ## **5.4. Server sends ServerHello message (Flight 4)** -
We generate a ServerHello message by calling "createDtlsServerHello" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/serverhello.go](../backend/src/dtls/serverhello.go) + ```go type ServerHello struct { Version DtlsVersion @@ -565,10 +553,10 @@ type ServerHello struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS ServerHello -``` +```console Frame 511: 121 bytes on wire (968 bits), 121 bytes captured (968 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -617,9 +605,8 @@ Datagram Transport Layer Security [JA3S Fullstring: 65277,49195,65281-14-11-23] [JA3S: eeb7a12006b679344c81e682d2ae5951] ``` -
-
+
* Set Version to context.ProtocolVersion * Set Random to context.ServerRandom @@ -633,24 +620,22 @@ Datagram Transport Layer Security ServerHello message was sent. -
- ## **5.5. Server sends Certificate message (Flight 4)** -
We generate a Certificate message by calling "createDtlsCertificate" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/certificate.go](../backend/src/dtls/certificate.go) + ```go type Certificate struct { Certificates [][]byte } ``` -
+
Click to expand Wireshark capture (Sent): DTLS Certificate -``` +```console Frame 513: 483 bytes on wire (3864 bits), 483 bytes captured (3864 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -736,9 +721,8 @@ Datagram Transport Layer Security Padding: 0 encrypted: 304502206b40cd413f21a016d5aa53325b2029e1a5ee3bddc2e49a9dadf97a5040718ff8… ``` -
-
+
* Set Certificates to ServerCertificate.Certificate @@ -748,14 +732,12 @@ We shared our X.509 Server Certificate data with the client. Certificate message was sent. -
- ## **5.6. Server sends ServerKeyExchange message (Flight 4)** -
We generate a ServerKeyExchange message by calling "createDtlsServerKeyExchange" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/serverkeyexchange.go](../backend/src/dtls/serverkeyexchange.go) + ```go type ServerKeyExchange struct { EllipticCurveType CurveType @@ -766,10 +748,10 @@ type ServerKeyExchange struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS ServerKeyExchange -``` +```console Frame 514: 169 bytes on wire (1352 bits), 169 bytes captured (1352 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -798,9 +780,8 @@ Datagram Transport Layer Security Signature Length: 72 Signature: 3046022100b8609b89a48945bee8ccad168c935466c2dcf46c8539bbf0f81ac6641e5db2… ``` -
-
+
* Set EllipticCurveType to context.CurveType (CurveTypeNamedCurve 0x03) * Set NamedCurve to context.Curve (CurveX25519 0x001d) @@ -818,14 +799,12 @@ Datagram Transport Layer Security ServerKeyExchange message was sent. -
- ## **5.7. Server sends CertificateRequest message (Flight 4)** -
We generate a CertificateRequest message by calling "createDtlsCertificateRequest" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/certificaterequest.go](../backend/src/dtls/certificaterequest.go) + ```go type CertificateRequest struct { CertificateTypes []CertificateType @@ -833,10 +812,10 @@ type CertificateRequest struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS CertificateRequest -``` +```console Frame 515: 65 bytes on wire (520 bits), 65 bytes captured (520 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -864,9 +843,8 @@ Datagram Transport Layer Security Signature Hash Algorithm Signature: ECDSA (3) Distinguished Names Length: 0 ``` -
-
+
* Set CertificateTypes to [CertificateTypeECDSASign (0x40)] * Set AlgoPairs to [AlgoPair{ @@ -883,23 +861,21 @@ We requested for a client certificate which ECDSASign type, generated using hash CertificateRequest message was sent. -
- ## **5.8. Server sends ServerHelloDone message (Flight 4)** -
We generate a ServerHelloDone message by calling "createDtlsServerHelloDone" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/serverhellodone.go](../backend/src/dtls/serverhellodone.go) + ```go type ServerHelloDone struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS ServerHelloDone -``` +```console Frame 516: 57 bytes on wire (456 bits), 57 bytes captured (456 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -918,9 +894,8 @@ Datagram Transport Layer Security Fragment Offset: 0 Fragment Length: 0 ``` -
-
+
The message doesn't carry any data. @@ -930,25 +905,22 @@ ServerHelloDone message was sent. Now we are waiting for a group of messages: A Certificate, a ClientKeyExchange, a CertificateVerify, a ChangeCipherSpec, a Finished message. For e.g., if our client is Chrome, it will send these messages in one packet. - -
- ## **5.9. Client sends Certificate message (Flight 4)** -
![Received Certificate](images/05-10-received-certificate.png) from [backend/src/dtls/certificate.go](../backend/src/dtls/certificate.go) + ```go type Certificate struct { Certificates [][]byte } ``` -
+
Click to expand Wireshark capture (Received): DTLS Certificate (came in combined packet) -``` +```console Frame 522: 579 bytes on wire (4632 bits), 579 bytes captured (4632 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1008,9 +980,8 @@ Datagram Transport Layer Security ... ``` -
-
+
The client shared their X.509 Server Certificate data with the server. @@ -1018,25 +989,22 @@ The client shared their X.509 Server Certificate data with the server. * Call "GetCertificateFingerprintFromBytes" function in [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) to generate the fingerprint hash from the certificate byte array * Compare the calculated fingerprint hash with context.ExpectedFingerprintHash which came with SDP data previously. So we can ensure that "the sender of SDP via Signaling" and "the sender of these handshake messages" are the same person/machine, not another man in the middle. - -
- ## **5.9. Client sends ClientKeyExchange message (Flight 4)** -
![Received ClientKeyExchange](images/05-11-received-clientkeyexchange.png) from [backend/src/dtls/clientkeyexchange.go](../backend/src/dtls/clientkeyexchange.go) + ```go type ClientKeyExchange struct { PublicKey []byte } ``` -
+
Click to expand Wireshark capture (Received): DTLS ClientKeyExchange (came in combined packet) -``` +```console Frame 522: 579 bytes on wire (4632 bits), 579 bytes captured (4632 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1063,30 +1031,22 @@ Datagram Transport Layer Security ... ``` -
-
+
* Set context.ClientKeyExchangePublic to message.PublicKey * If context.IsCipherSuiteInitialized is false (our cipher suite is not initialized yet), we are ready to initialize our cipher suite, call "initCipherSuite" function in from [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go) - -
- ### **5.9.1. Initialization of cipher suite** -
We need to generate a master secret. Then using this master secret, client random, and server random, we will initialize our keys. We will use these keys while encrypting/decrypting the SRTP packets further. - -
- #### **5.9.1.1. Generate a Pre-Master Secret** -
We call "GeneratePreMasterSecret" function in [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) from [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go) + ```go preMasterSecret, err := GeneratePreMasterSecret(context.ClientKeyExchangePublic, context.ServerPrivateKey, context.Curve) ``` @@ -1096,13 +1056,10 @@ We call "GeneratePreMasterSecret" function in [backend/src/dtls/crypto.go](../ba ***Attention Note:** Public key was the ClientKeyExchange message's Public Key, the ServerPrivateKey was generated via "GenerateCurveKeypair". Sources: -* [Differences between the terms "pre-master secret", "master secret", "private key", and "shared secret"? (Stackoverflow)](https://crypto.stackexchange.com/questions/27131/differences-between-the-terms-pre-master-secret-master-secret-private-key) - -
+* [Differences between the terms "pre-master secret", "master secret", "private key", and "shared secret"? (Stackoverflow)](https://crypto.stackexchange.com/questions/27131/differences-between-the-terms-pre-master-secret-master-secret-private-key) #### **5.9.1.2. Generate a Master Secret** -
We look to our context.UseExtendedMasterSecret boolean value. Remember that, if ClientHello message has UseExtendedMasterSecret extension, we should generate our master secret in extended way. @@ -1126,6 +1083,7 @@ Order should be like: * Call "GenerateExtendedMasterSecret" function in [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) with our *preMasterSecret*, *handshakeHash*, *context.CipherSuite.HashAlgorithm*. from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go func GenerateExtendedMasterSecret(preMasterSecret []byte, handshakeHash []byte, hashAlgorithm HashAlgorithm) ([]byte, error) { seed := append([]byte("extended master secret"), handshakeHash...) @@ -1148,7 +1106,6 @@ func GenerateExtendedMasterSecret(preMasterSecret []byte, handshakeHash []byte, ![Message concatenation result](images/05-12-message-concatenation-result.png) - **If context.UseExtendedMasterSecret is false:** **Note:** In current context, our code won't come this state, because (I think) Chrome always sends the UseExtendedMasterSecret extension with ClientHello message. But we can discuss on generating non-extended master secret: @@ -1162,20 +1119,15 @@ func GenerateExtendedMasterSecret(preMasterSecret []byte, handshakeHash []byte, * Set the result of "GenerateMasterSecret" function to context.ServerMasterSecret. - - Sources: + * [RFC 7627: Transport Layer Security (TLS) Session Hash and Extended Master Secret Extension - The Extended Master Secret](https://datatracker.ietf.org/doc/html/rfc7627#section-4) * [WebRTC for the Curious: Securing - Pseudorandom Function](https://webrtcforthecurious.com/docs/04-securing/#pseudorandom-function) * [Pseudorandom function family (Wikipedia)](https://en.wikipedia.org/wiki/Pseudorandom_function_family) * [RFC 4346: The Transport Layer Security (TLS) Protocol Version 1.1 - HMAC and the Pseudorandom Function](https://datatracker.ietf.org/doc/html/rfc4346#section-5) * [Pion WebRTC DTLS project, PHash function (Github)](https://github.com/pion/dtls/blob/a6397ff7282bc56dc37a68ea9211702edb4de1de/pkg/crypto/prf/prf.go#L155) - -
- #### **5.9.1.3. Initialize GCM** -
[GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) is abbreviation for "Galois/Counter Mode" and is a type of [block cipher mode of operation ](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation). In our project, we only implemented the TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite, and it uses [AES-GCM](https://www.cryptosys.net/pki/manpki/pki_aesgcmauthencryption.html) authenticated encryption. @@ -1187,6 +1139,7 @@ This struct was defined in "dtls" package specifically to process DTLS messages. You can find the original code on [Pion WebRTC DTLS project, GCM struct (Github)](https://github.com/pion/dtls/blob/b3e235f54b60ccc31aa10193807b5e8e394f17ff/pkg/crypto/ciphersuite/gcm.go#L20). from [backend/src/dtls/cryptogcm.go](../backend/src/dtls/cryptogcm.go) + ```go type GCM struct { localGCM, remoteGCM cipher.AEAD @@ -1200,7 +1153,7 @@ type GCM struct { * prfMacLen: 0 bytes * prfKeyLen: 16 bytes (our keys will be 16 bytes length) * prfIvLen: 4 bytes (our [Initialization Vectors](https://en.wikipedia.org/wiki/Initialization_vector) will be 4 bytes length) - +
**Note:** These constant length values can vary for different cipher suites. Due to our chosen suite's MAC length is zero, we didn't include things related with MAC in our code. @@ -1218,10 +1171,11 @@ Because we want: * A key for server (serverWriteKey) (16 bytes) * An initialization vector for client (clientWriteIV) (4 bytes) * An initialization vector for server (serverWriteIV) (4 bytes) -
+ So we need (16+16+4+4) = 40 bytes array which is generated by PHash with our masterSecret and seed. Then we extract our key and IV values from these 40 bytes sequentially. from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go func GenerateEncryptionKeys(masterSecret []byte, clientRandom []byte, serverRandom []byte, keyLen int, ivLen int, hashAlgorithm HashAlgorithm) (*EncryptionKeys, error) { logging.Descf(logging.ProtoCRYPTO, "Generating encryption keys with Key Length: %d, IV Length: %d via %s, using Master Secret, Server Random, Client Random...", keyLen, ivLen, hashAlgorithm) @@ -1264,11 +1218,13 @@ We are ready to create our GCM object, which contains our ciphers and IVs. We pa * While creating our ciphers, we create new instances by [aes.NewCipher](https://pkg.go.dev/crypto/aes#NewCipher) and [cipher.NewGCM](https://pkg.go.dev/crypto/cipher#NewGCM) from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go gcm, err := NewGCM(keys.ServerWriteKey, keys.ServerWriteIV, keys.ClientWriteKey, keys.ClientWriteIV) ``` from [backend/src/dtls/cryptogcm.go](../backend/src/dtls/cryptogcm.go) + ```go func NewGCM(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*GCM, error) { localBlock, err := aes.NewCipher(localKey) @@ -1287,27 +1243,24 @@ func NewGCM(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*GCM, erro remoteWriteIV: remoteWriteIV, }, nil } - ``` * We set returned GCM object to context.GCM and set context.IsCipherSuiteInitialized as true. Sources: + * [What is the main difference between a key, an IV and a nonce? (Stackoverflow)](https://crypto.stackexchange.com/questions/3965/what-is-the-main-difference-between-a-key-an-iv-and-a-nonce) * [Initialization vector](https://en.wikipedia.org/wiki/Initialization_vector) * [Keying material (NIST)](https://csrc.nist.gov/glossary/term/keying_material) * [Secret keying material (NIST)](https://csrc.nist.gov/glossary/term/secret_keying_material) * [Key material (Mozilla)](https://infosec.mozilla.org/guidelines/key_management#key-material) - -
- ## **5.10. Client sends CertificateVerify message (Flight 4)** -
![Received CertificateVerify](images/05-14-received-certificateverify.png) from [backend/src/dtls/certificateverify.go](../backend/src/dtls/certificateverify.go) + ```go type CertificateVerify struct { AlgoPair AlgoPair @@ -1315,10 +1268,10 @@ type CertificateVerify struct { } ``` -
+
Click to expand Wireshark capture (Received): DTLS CertificateVerify (came in combined packet) -``` +```console Frame 522: 579 bytes on wire (4632 bits), 579 bytes captured (4632 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1347,9 +1300,8 @@ Datagram Transport Layer Security ... ``` -
-
+
* Compare hash algorithm ID of incoming *message.AlgoPair.HashAlgorithm* and *context.CipherSuite.HashAlgorithm* * Compare signature algorithm ID of incoming *message.AlgoPair.SignatureAlgorithm* and *context.CipherSuite.SignatureAlgorithm* @@ -1370,27 +1322,26 @@ Order should be like: * Check the validity of the X.509 certificate which came with Certificate message from the client, by [ecdsa.Verify](https://pkg.go.dev/crypto/ecdsa#Verify). from [backend/src/dtls/crypto.go](../backend/src/dtls/crypto.go) + ```go ecdsa.Verify(clientCertificatePublicKey, hash, ecdsaSign.R, ecdsaSign.S) ``` -
- ## **5.11. Client sends ChangeCipherSpec message (Flight 4)** -
Received ChangeCipherSpec from [backend/src/dtls/changecipherspec.go](../backend/src/dtls/changecipherspec.go) + ```go type ChangeCipherSpec struct { } ``` -
+
Click to expand Wireshark capture (Received): DTLS ChangeCipherSpec (came in combined packet) -``` +```console Frame 522: 579 bytes on wire (4632 bits), 579 bytes captured (4632 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1409,36 +1360,31 @@ Datagram Transport Layer Security ... ``` -
-
+
This type of message contains only a byte with value 1. We don't do anything for this message. **Important note:** Epoch of Record Header were 0 and contents were in clear text (not encrypted) until (and including) this ChangeCipherSpec message. But the messages that will come after this, will have Epoch with 1 and will be encrypted. - -
- ## **5.12. Client sends Finished message (Flight 4)** -
![Received Finished](images/05-16-received-finished.png) from [backend/src/dtls/finished.go](../backend/src/dtls/finished.go) + ```go type Finished struct { VerifyData []byte } ``` -
+
Click to expand Wireshark capture (Received): DTLS Finished (came in combined packet) -
**Important note:** As you can see at the end of the block, latest handshake message (Finished) has Epoch: 1, and content was encrypted. Because of this, we can't see contents of it as clear text in Wireshark. -``` +```console Frame 522: 579 bytes on wire (4632 bits), 579 bytes captured (4632 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1456,9 +1402,8 @@ Datagram Transport Layer Security Handshake Protocol (Encrypted content: 0x0001000000000000e22c7b2609d96a7d3a98c7b80e32ea03868231eb419623ce8af27b5ab97b16df143e8743ab0cb6ba) ``` -
-
+
**Important note:** Epoch of Record Header is 1 and, this Finished message is the first message which was encrypted. If we can decode and decrypt contents of this message successfully, then verify the VerifyData successfully, it means that, the handshake process for our side succeeded! After that we will send two other messages, then it will be "completely" finished! @@ -1482,25 +1427,24 @@ Order should be like (attention to CertificateVerify and Finished were addded de * Set the context's Flight to "Flight 6" Sources: -* [RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2 - Finished](https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.9) -
+* [RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2 - Finished](https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.9) ## **5.13. Server sends ChangeCipherSpec message (Flight 6)** -
We generate a ChangeCipherSpec message by calling "createDtlsChangeCipherSpec" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/changecipherspec.go](../backend/src/dtls/changecipherspec.go) + ```go type ChangeCipherSpec struct { } ``` -
+
Click to expand Wireshark capture (Sent): DTLS ChangeCipherSpec -``` +```console Frame 549: 46 bytes on wire (368 bits), 46 bytes captured (368 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1514,9 +1458,8 @@ Datagram Transport Layer Security Length: 1 Change Cipher Spec Message ``` -
-
+
This type of message contains only a byte with value 1. @@ -1528,28 +1471,24 @@ This type of message contains only a byte with value 1. * Called "IncreaseServerEpoch" function in [backend/src/dtls/handshakecontext.go](../backend/src/dtls/handshakecontext.go) to increase context.ServerEpoch to 1, and set context.ServerSequenceNumber = 0. -
- ## **5.14. Server sends Finished message (Flight 6)** -
We generate a Finished message by calling "createDtlsFinished" function in [backend/src/dtls/handshakemanager.go](../backend/src/dtls/handshakemanager.go). from [backend/src/dtls/finished.go](../backend/src/dtls/finished.go) + ```go type Finished struct { VerifyData []byte } ``` -
+
Click to expand Wireshark capture (Sent): DTLS Finished -
- **Important note:** As you can see at the end of the block, the handshake message (Finished) has Epoch: 1, and content was encrypted. Because of this, we can't see contents of it as clear text in Wireshark. -``` +```console Frame 550: 93 bytes on wire (744 bits), 93 bytes captured (744 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -1564,9 +1503,8 @@ Datagram Transport Layer Security Handshake Protocol (Encrypted content: 0x61c244d15307ccaf237e2ef34fdd5bddff3d7d17cf3fcb837a80db65f777a2496c609b03d7a35b772754cc206bb16db2) ``` -
-
+
Sent Finished diff --git a/docs/06-SRTP-INITIALIZATION.md b/docs/06-SRTP-INITIALIZATION.md index 9c80fe6..e8ee555 100644 --- a/docs/06-SRTP-INITIALIZATION.md +++ b/docs/06-SRTP-INITIALIZATION.md @@ -38,6 +38,7 @@ Although our project was designed as a monolith and one package, in real-world D * The orchestrator gives the byte array to SRTP side for extracting keys and salts from given [keying material](https://csrc.nist.gov/glossary/term/keying_material) from [backend/src/dtls/finished.go](../backend/src/dtls/finished.go) + ```go func (ms *UDPClientSocket) OnDTLSStateChangeEvent(dtlsState dtls.DTLSState) { logging.Infof(logging.ProtoDTLS, "State Changed: %s [%v:%v].\n", dtlsState, ms.HandshakeContext.Addr.IP, ms.HandshakeContext.Addr.Port) @@ -62,6 +63,7 @@ func (ms *UDPClientSocket) OnDTLSStateChangeEvent(dtlsState dtls.DTLSState) { * It calls "extractEncryptionKeys" function in [backend/src/srtp/srtpmanager.go](../backend/src/srtp/srtpmanager.go) to extract key and salt values from this 56 bytes keying material sequentially. you can find further information [here](https://github.com/pion/srtp/blob/82008b58b1e7be7a0cb834270caafacc7ba53509/keying.go#L14) from [backend/src/srtp/srtpmanager.go](../backend/src/srtp/srtpmanager.go) + ```go func (m *SRTPManager) extractEncryptionKeys(protectionProfile ProtectionProfile, keyingMaterial []byte) (*EncryptionKeys, error) { keyLength, err := protectionProfile.KeyLength() @@ -92,12 +94,7 @@ func (m *SRTPManager) extractEncryptionKeys(protectionProfile ProtectionProfile, ![SRTP Initialization](images/06-01-srtp-initialization.png) - - -
- ## **6.1. Initialize GCM** -
[GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) is the abbreviation for "Galois/Counter Mode" and is a type of [Block cipher mode of operation ](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation). @@ -107,12 +104,13 @@ We discussed what is GCM and why we call our object as GCM, in initialization of So, we call the object that makes encryption/decryption over our SRTP and SCTP packets as GCM. from [backend/src/srtp/cryptogcm.go](../backend/src/srtp/cryptogcm.go) + ```go type GCM struct { srtpGCM, srtcpGCM cipher.AEAD srtpSalt, srtcpSalt []byte } -```` +``` * Now, we have an "EncryptionKeys" object defined in [backend/src/srtp/protectionprofiles.go](../backend/src/srtp/protectionprofiles.go).
@@ -126,11 +124,13 @@ We are ready to create our GCM object, which contains our ciphers and salt value * While creating our ciphers, we create new instances by [aes.NewCipher](https://pkg.go.dev/crypto/aes#NewCipher) and [cipher.AEAD](https://pkg.go.dev/crypto/cipher#AEAD) from [backend/src/srtp/srtpmanager.go](../backend/src/srtp/srtpmanager.go) + ```go gcm, err := InitGCM(keys.ClientMasterKey, keys.ClientMasterSalt) ``` from [backend/src/srtp/cryptogcm.go](../backend/src/srtp/cryptogcm.go) + ```go func NewGCM(masterKey, masterSalt []byte) (*GCM, error) { srtpSessionKey, err := aesCmKeyDerivation(labelSRTPEncryption, masterKey, masterSalt, 0, len(masterKey)) @@ -184,7 +184,7 @@ func NewGCM(masterKey, masterSalt []byte) (*GCM, error) { * We set returned GCM object to context.GCM and set context.IsCipherSuiteInitialized as true. -Now, we are ready to receive and decrypt incoming SRTP and SRTCP packets. We are not ready to encrypt, because we don't need and implemented this part :) +Now, we are ready to receive and decrypt incoming SRTP and SRTCP packets. We are not ready to encrypt, because we don't need and implemented this part :blush:
diff --git a/docs/07-SRTP-PACKETS-COME.md b/docs/07-SRTP-PACKETS-COME.md index f236c5e..0388ba2 100644 --- a/docs/07-SRTP-PACKETS-COME.md +++ b/docs/07-SRTP-PACKETS-COME.md @@ -5,7 +5,8 @@ In previous chapter, we completed the initialization of SRTP ciphers process suc When a new packet comes in, the "AddBuffer" function in [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) looks for which protocol standard this packet rely on. RTP packet structure -``` + +```console 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -25,6 +26,7 @@ When a new packet comes in, the "AddBuffer" function in [backend/src/agent/udpcl Our RTP packet struct in [backend/src/srtp/header.go](../backend/src/srtp/header.go) and [backend/src/srtp/packet.go](../backend/src/srtp/packet.go): from [backend/src/srtp/header.go](../backend/src/srtp/header.go) and [backend/src/srtp/packet.go](../backend/src/srtp/packet.go) + ```go type PayloadType byte @@ -57,13 +59,12 @@ type Packet struct { } ``` -
+
Click to expand Wireshark capture (Received): First SRTP Packet -
**Important note:** As you can see at the end of the block, Wireshark couldn't specify the protocol of the packet as RTP. It shows RTP/SRTP packets as raw UDP packets. -``` +```console Frame 568: 1068 bytes on wire (8544 bits), 1068 bytes captured (8544 bits) on interface lo0, id 0 Null/Loopback Internet Protocol Version 4, Src: 192.168.***.***, Dst: 192.168.***.*** @@ -72,20 +73,21 @@ Data (1036 bytes) Data: 806010d66b18660546dfb57f81a378739249bef2f2f5552907340c6229373297f3c52e7e… [Length: 1036] ``` -
-
+
**Important notice:** You can ask "The chapter is about SRTP but you show me RTP without the 'S'?". Because [SRTP (Secure Real-time Transport Protocol)](https://en.wikipedia.org/wiki/Secure_Real-time_Transport_Protocol), a secure version of [RTP (Real-time Transport Protocol)](https://en.wikipedia.org/wiki/Real-time_Transport_Protocol). Header is in clear text, Payload part is encrypted and authenticated we will discuss further. In this context, we can check out rtp.IsRtpPacket function. This function checks: - * Take the second byte (with index 1) which contains M (1 bit) (marker) and PT (7 bits) (PayloadType) - * Use "and" bitwise operator on this byte and 0b01111111 (it sets 8. bit as zero, takes other 7 bits) - * This result byte data (ordinally) is between 0 and 35, or between 96 and 127. This byte represents the SRTP Header's PayloadType value. + +* Take the second byte (with index 1) which contains M (1 bit) (marker) and PT (7 bits) (PayloadType) +* Use "and" bitwise operator on this byte and 0b01111111 (it sets 8. bit as zero, takes other 7 bits) +* This result byte data (ordinally) is between 0 and 35, or between 96 and 127. This byte represents the SRTP Header's PayloadType value. If this buffer part complies with these conditions, we can say "this packet is an SRTP protocol packet", then we can process it with SRTP protocol's methods. from [backend/src/rtp/rtpheader.go](../backend/src/rtp/rtpheader.go) + ```go func IsRtpPacket(buf []byte, offset int, arrayLen int) bool { // Initial segment of RTP header; 7 bit payload @@ -100,13 +102,10 @@ Here is the console output when the server received first "expected" SRTP packet ![Received RTP Packet](images/07-01-received-rtp-packet.png) Sources: -* [Multiplexing RTP and RTCP on a Single Port](https://csperkins.org/standards/ietf-67/2006-11-07-IETF67-AVT-rtp-rtcp-mux.pdf) - -
+* [Multiplexing RTP and RTCP on a Single Port](https://csperkins.org/standards/ietf-67/2006-11-07-IETF67-AVT-rtp-rtcp-mux.pdf) ## **7.1. Processing incoming SRTP package** -
We have determined that incoming packet is an RTP packet via rtp.IsRtpPacket function at "AddBuffer" function. @@ -114,6 +113,7 @@ We have determined that incoming packet is an RTP packet via rtp.IsRtpPacket fun * Forward the decoded RTP packet to the UDPClientSocket's "RtpDepacketizer" [channel](https://go.dev/tour/concurrency/2). This channel is listened by runRtpDepacketizer() function of UDPClientSocket object, shown below: from [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) + ```go func (ms *UDPClientSocket) AddBuffer(buf []byte, offset int, arrayLen int) { logging.Descf(logging.ProtoUDP, "A packet received. The byte array (%d bytes) not parsed yet. Demultiplexing via if-else blocks.", arrayLen) @@ -134,6 +134,7 @@ func (ms *UDPClientSocket) AddBuffer(buf []byte, offset int, arrayLen int) { * Sets the decrypted payload to rtpPacket.Payload property from [backend/src/agent/udpclientsocket.go](../backend/src/agent/udpclientsocket.go) + ```go func (ms *UDPClientSocket) runRtpDepacketizer() { defer close(ms.RtpDepacketizer) @@ -154,15 +155,12 @@ func (ms *UDPClientSocket) runRtpDepacketizer() { } ``` - -
- ## **7.2. Decoding the SRTP package** -
We use "DecryptRTPPacket" function in [backend/src/srtp/srtpcontext.go](../backend/src/srtp/srtpcontext.go) to decrypt the SRTP payload. You can find the original code [here](https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/srtp.go#L8). from [backend/src/srtp/srtpcontext.go](../backend/src/srtp/srtpcontext.go) + ```go func (c *SRTPContext) DecryptRTPPacket(packet *rtp.Packet) ([]byte, error) { s := c.getSRTPSSRCState(packet.Header.SSRC) @@ -191,6 +189,7 @@ func (c *SRTPContext) DecryptRTPPacket(packet *rtp.Packet) ([]byte, error) { * At this "srtpSSRCState" struct, we store the *ssrc*, *index*, and *rolloverHasProcessed* values: from [backend/src/srtp/srtpcontext.go](../backend/src/srtp/srtpcontext.go) + ```go type srtpSSRCState struct { ssrc uint32 @@ -217,6 +216,7 @@ type srtpSSRCState struct { * **Important notice:** It will decrypt the encrypted payload part, but with checking AEAD authentication with additional header data. This will give an "authentication error" if you give wrong or missing parameters. For example, on my first try, I didn't copy the ciphertext into dst variable, passed an allocated but empty byte array. But I realized that, the [AEAD.Open](https://pkg.go.dev/crypto/cipher#AEAD.Open) checks authentication in the dst parameter. from [backend/src/srtp/cryptogcm.go](../backend/src/srtp/cryptogcm.go) + ```go if _, err := g.srtpGCM.Open( dst[packet.HeaderSize:packet.HeaderSize], iv, ciphertext[packet.HeaderSize:], ciphertext[:packet.HeaderSize], @@ -225,8 +225,8 @@ type srtpSSRCState struct { } ``` - from [backend/src/srtp/cryptogcm.go](../backend/src/srtp/cryptogcm.go) + ```go func (g *GCM) Decrypt(packet *rtp.Packet, roc uint32) ([]byte, error) { ciphertext := packet.RawData @@ -253,14 +253,9 @@ func (g *GCM) Decrypt(packet *rtp.Packet, roc uint32) ([]byte, error) { ``` * Call the "updateROC" function which we get previously - * Our encrypted Payload is now decrypted and as clear text. - -
- ## **7.2. Decoding the VP8 Video Content** -
* If the packet's PayloadType is in PayloadTypeVP8 type, it forwards to the UDPClientSocket's "vp8Depacketizer" [channel](https://go.dev/tour/concurrency/2). This channel is listened by Run() function of VP8Decoder object in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go), discussed in chapter [8. VP8 PACKET DECODE](./08-VP8-PACKET-DECODE.md). diff --git a/docs/08-VP8-PACKET-DECODE.md b/docs/08-VP8-PACKET-DECODE.md index b2d92c9..fb1e6b2 100644 --- a/docs/08-VP8-PACKET-DECODE.md +++ b/docs/08-VP8-PACKET-DECODE.md @@ -7,7 +7,8 @@ In previous chapter, we decoded the SRTP packet and decrypted it successfully, a * Our VP8Decoder aims to process incoming VP8 RTP packets, catch and concatenate keyframe packets then convert the result to an image object and save it as a JPEG image file. VP8 Payload Descriptor -``` + +```console 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |X|R|N|S|PartID | (REQUIRED) @@ -25,6 +26,7 @@ T/K: |TID|Y| KEYIDX | (OPTIONAL) Our VP8 packet struct in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) + ```go type VP8Packet struct { // Required Header @@ -53,6 +55,7 @@ type VP8Packet struct { * We used [libvpx-go](https://github.com/xlab/libvpx-go) as codec, we didn't implement the VP8 codec. from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) + ```go func (d *VP8Decoder) Run() { @@ -116,24 +119,26 @@ func (d *VP8Decoder) Run() { ``` * If the function call succeeds, + ```go img := vpx.CodecGetFrame(d.context, &iter) ``` + * We call img.Deref() to convert decoded C structures as Go wrapper variables + ```go img.Deref() ``` + * If everything has gone OK, save the image as a JPEG file by [jpeg.Encode](https://pkg.go.dev/image/jpeg#Encode) * You can see your caught keyframes at /backend/output/ folder as shoot1.jpg, shoot2.jpg, etc... if multiple keyframes were caught. - * As you can see below, we received multiple RTP packets containing VP8 video data, in different packet lengths, then we caught a keyframe from these packets, concatenate them, then created and saved image. - * At the end of our journey, we saw the "[INFO] Image file saved: ../output/shoot1.jpg" log line! All of our these efforts are to achieve this.... ![Image saved](images/08-01-image-saved.png) - Sources: + * [How to convert VP8 interframe into image with Pion/Webrtc? (Stackoverflow)](https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc) * [RFC: RTP Payload Format for VP8 Video](https://tools.ietf.org/id/draft-ietf-payload-vp8-05.html) diff --git a/docs/09-CONCLUSION.md b/docs/09-CONCLUSION.md index f7cc712..57a38ff 100644 --- a/docs/09-CONCLUSION.md +++ b/docs/09-CONCLUSION.md @@ -1,8 +1,8 @@ # **9. CONCLUSION** -If you really read all of my journey in this walkthrough documentation, I want to congratulate you for your perseverance, patience, and durability :) Also, I want to thank you for your interest. +If you really read all of my journey in this walkthrough documentation, I want to congratulate you for your perseverance, patience, and durability :blush: Also, I want to thank you for your interest. -If you liked this repo, you can give a star on Github :) +If you liked this repo, you can give a star :star: on GitHub. Your support and feedback not only help the project improve and grow but also contribute to reaching a wider audience within the community. Additionally, it motivates me to create even more innovative projects in the future. Also thanks to contributors of the awesome sources which were referred during development of this project and writing this documentation. You can find these sources in [README](../README.md), also in between the lines. diff --git a/docs/images/icon.svg b/docs/images/icon.svg new file mode 100644 index 0000000..00d9327 --- /dev/null +++ b/docs/images/icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/mkdocs/.gitignore b/docs/mkdocs/.gitignore new file mode 100644 index 0000000..ec32b66 --- /dev/null +++ b/docs/mkdocs/.gitignore @@ -0,0 +1,12 @@ +* +!.gitignore +!mkdocs.yml.template +!*.sh +!stylesheets +!stylesheets/**/* +!javascripts +!javascripts/**/* +!assets +!assets/**/* +!extra-content +!extra-content/**/* diff --git a/docs/mkdocs/assets/icon.png b/docs/mkdocs/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..546b66b30cac403a0fdc4b809ee54a63ee955a68 GIT binary patch literal 24913 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 bw}}h^M^LaQZjl|F5K>XrP%6LoH1z)f(43tj literal 0 HcmV?d00001 diff --git a/docs/mkdocs/assets/icon.svg b/docs/mkdocs/assets/icon.svg new file mode 100644 index 0000000..00d9327 --- /dev/null +++ b/docs/mkdocs/assets/icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/mkdocs/execute-mkdocs.sh b/docs/mkdocs/execute-mkdocs.sh new file mode 100755 index 0000000..d50f4fd --- /dev/null +++ b/docs/mkdocs/execute-mkdocs.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +docker run --rm -it -v ${PWD}/docs:/docs --entrypoint /docs/mkdocs/prepare-mkdocs.sh squidfunk/mkdocs-material +docker run --rm -it -p 8000:8000 -v ${PWD}/docs/mkdocs:/docs squidfunk/mkdocs-material \ No newline at end of file diff --git a/docs/mkdocs/extra-content/index.md b/docs/mkdocs/extra-content/index.md new file mode 100644 index 0000000..585a161 --- /dev/null +++ b/docs/mkdocs/extra-content/index.md @@ -0,0 +1,100 @@ +--- +title: HOME +type: docs +menus: + - main +weight: 0 +--- +# **WebRTC Nuts and Bolts** + +[![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/alper-dalkiran/) +[![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white&style=flat-square)](https://twitter.com/aalperdalkiran) +![HitCount](https://hits.dwyl.com/adalkiran/webrtc-nuts-and-bolts.svg?style=flat-square) +![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +!!! info "Welcome!" + + This documentation website is a customized version of original documentation of the [:fontawesome-brands-github: WebRTC Nuts and Bolts repository](https://github.com/adalkiran/webrtc-nuts-and-bolts). You can find the running Go implementation of the project codes in this repository. + +A holistic way of understanding how WebRTC and its protocols run in practice, **with code and detailed documentation**. "The nuts and bolts" (practical side instead of theoretical facts, pure implementation details) of required protocols without using external dependencies or libraries. + +When you run the project and follow the instructions, web page initializes the webcam, does handshake with the backend application (executes several WebRTC processes), at the end, the backend catches keyframe images and saves them as JPEG image files. You can see your caught keyframes at /backend/output/ folder as shoot1.jpg, shoot2.jpg etc... if multiple keyframes were caught. + +You can track which steps taken during this journey by debugging or tracking the output at console. + +![Backend initial output](images/01-07-backend-initial-output.png) + +## :thought_balloon: **WHY THIS PROJECT?** + +This project was initially started to learn Go language and was made for experimental and educational purposes only, not for production use. + +After some progress on the development, I decided to pivot my experimental work to a walkthrough document. Because although there are lots of resources that exist already on the Internet, they cover small chunks of WebRTC concepts or protocols atomically. And they use the standard way of inductive method which teach in pieces then assemble them. + +But my style of learning leans on the deductive method instead of others, so instead of learning atomic pieces and concepts first, going linearly from beginning to the end, and learning an atomic piece on the time when learning this piece is required. + +## :dart: **COVERAGE** + +Web front-end side: Pure TypeScript implementation: + +* Communicate with signaling backend WebSocket, +* Gathering webcam streaming track from browser and send this track to backend via UDP. + +Server back-end side: Pure Go language implementation: + +* A simple signaling back-end WebSocket to transfer [SDP (Session Description Protocol)](https://en.wikipedia.org/wiki/Session_Description_Protocol) using [Gorilla WebSocket](https://github.com/gorilla/websocket) library. +* Single port UDP listener, supports demultiplexing different data packet types (STUN, DTLS handshake, SRTP, SRTCP) coming from the same UDP connection. +* Protocol implementations of (only required parts): + * [STUN (Session Traversal Utilities for NAT)](https://en.wikipedia.org/wiki/STUN) for discovering external IP behind NAT by a STUN server and replying to the client's STUN binding request came by UDP connection. + * [DTLS (Datagram Transport Layer Security)](https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security) for secure handshake, authenticating each oter, and crypto key exchange process. DTLS is similar to [TLS (Transport Layer Security)](https://tr.wikipedia.org/wiki/Transport_Layer_Security), DTLS runs over UDP instead of TCP. This project supports only DTLS v1.2. + * [RTP (Real-time Transport Protocol)](https://en.wikipedia.org/wiki/Real-time_Transport_Protocol) for transferring media packets in fragments. + * [SRTP (Secure Real-time Transport Protocol)](https://en.wikipedia.org/wiki/Secure_Real-time_Transport_Protocol), a secure version of RTP. +* Only header parsing for [VP8 video format](https://en.wikipedia.org/wiki/VP8) to depacketizing fragmented packets to construct a video frame. [libvpx-go](https://github.com/xlab/libvpx-go) was used for decoding VP8 keyframe as image. +* [github.com/fatih/color](https://github.com/fatih/color) was used while printing colored output on console while logging. +* Implementation of TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 [cipher suite](https://www.keyfactor.com/blog/cipher-suites-explained/) support using [Go Cryptography](https://pkg.go.dev/golang.org/x/crypto) library. + +## :package: **INSTALLATION and RUNNING** + +Installation and building instructions are described at [:fontawesome-brands-github: GitHub README](https://github.com/adalkiran/webrtc-nuts-and-bolts#package-installation-and-running). + +## :bricks: **ASSUMPTIONS** + +Full-compliant WebRTC libraries should support a wide range of protocol details defined in RFC documents, client/server implementation differences, fallbacks for different protocol versions, a wide variety of cipher suites and media encoders/decoders. Also should be implemented as state machines, because WebRTC contains has some parts which managed as state machines, eg: [ICE (Interactive Connectivity Establishment)](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment), [DTLS (Datagram Transport Layer Security)](https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security) handshake, etc... + +In **WebRTC Nuts and Bolts** scenario, some assumptions have been made to focus only on required set of details. + +| Full-compliant WebRTC libraries | WebRTC Nuts and Bolts | +|---|---| +| WebRTC has no client or server concepts in its [peer-to-peer](https://tr.wikipedia.org/wiki/Peer-to-peer) nature, there are controlling or controlled peers. | This project aims to act as listener server and it only receives media, not sends. To make the code more simplistic and cleaner; the concepts "client" instead of "local peer" and "server" instead of "remote peer" has been used. | +| Should support both controlling and controlled roles. | Go language side will act only as server (ICE controlling), SDP offer will come from this side, then SDP answer will be expected from the client. | +| For separation of concerns and to maintain architectural extensibility, all WebRTC libraries were implemented as separate packages/repos (STUN package, DTLS package, SRTP package, etc...) | To keep it simple, this project was designed as [monorepo](https://en.wikipedia.org/wiki/Monorepo) but separated into packages. This choice depends on architectural needs and technical maintenance needs. | +| Should support DTLS fragmentation. |  DTLS fragmentation is not supported. | +| Should support multiple cipher suites for compatibility with different types of peers. More cipher suites can be found at [here](https://developers.cloudflare.com/ssl/ssl-tls/cipher-suites/). |  Only TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 is supported. | +| Should implement packet reply detection, handling corrupted packets, handling unordered packet sequences and packet losses, byte array length checks, lots of security protections against cyberattacks, etc... | This project was developed to run in only ideal conditions. Incoming malicious packets were not considered. | + +## :star: **CONTRIBUTING and SUPPORTING the PROJECT** + +You are welcome to [create issues](https://github.com/adalkiran/webrtc-nuts-and-bolts/issues/new) to report any bugs or problems you encounter. At present, I'm not sure whether this project should be expanded to cover more concepts or not. Only time will tell :blush:. + +If you liked and found my project helpful and valuable, I would greatly appreciate it if you could give the repo a star :star: on [:fontawesome-brands-github: GitHub](https://github.com/adalkiran/webrtc-nuts-and-bolts). Your support and feedback not only help the project improve and grow but also contribute to reaching a wider audience within the community. Additionally, it motivates me to create even more innovative projects in the future. + +## :book: **RESOURCES** + +I want to thank to contributors of the awesome sources which were referred during development of this project and writing this documentation. You can find these sources below, also in between the lines in code and documentation. + +* [Wikipedia](https://en.wikipedia.org) +* [WebRTC For The Curious](https://webrtcforthecurious.com): Awesome resource on theoretical concepts of WebRTC. It is vendor agnostic. Created by creators of [Pion project](https://github.com/pion) +* [Pion project](https://github.com/pion) + * [Pion DTLS](https://github.com/pion/dtls): A library for DTLS protocol, developed in Go. Some parts about cryptography used with from this project, with modifications. + * [Pion SRTP](https://github.com/pion/srtp): A library for SRTP protocol, developed in Go. Some parts about cryptography used with from this project, with modifications. +* [Jitsi](https://github.com/jitsi) + * [Jitsi ice4j](https://github.com/jitsi/ice4j): A library for ICE processes including gathering ICE candidates, developed in Java and Kotlin. You can start to explore from [here](https://github.com/jitsi/ice4j/blob/d7c0e27a1cde7b877b34d8bb68dc39f18dc45f16/src/main/java/org/ice4j/ice/harvest/SinglePortUdpHarvester.java) and [here](https://github.com/jitsi/ice4j/blob/d7c0e27a1cde7b877b34d8bb68dc39f18dc45f16/src/main/java/org/ice4j/ice/harvest/AbstractUdpListener.java#L47) + * [Jitsi Media Transform](https://github.com/jitsi/jitsi-media-transform): A library for ICE processes including gathering ICE candidates, developed in Java and Kotlin. You can find different protocol implementations [here](https://github.com/jitsi/jitsi-media-transform/tree/master/src/main/kotlin/org/jitsi/nlj) + * [Jitsi Videobridge](https://github.com/jitsi/jitsi-videobridge): A server application that orchestrates these processes and serves API interfaces, developed in Java and Kotlin +* [The Bouncy Castle Crypto Package For Java](https://github.com/bcgit/bc-java): A library for TLS processes and cryptography, developed in Java. +* [Tinydtls](https://github.com/eclipse/tinydtls): A library for DTLS processes, developed in C. +* [Mozilla Web Docs: WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API): A documentation on WebRTC API at browser side. +* Several RFC Documents: In code and documentation of this project, you can find several RFC document links cited. + +## :scroll: **LICENSE** + +WebRTC Nuts and Bolts is licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/adalkiran/webrtc-nuts-and-bolts/blob/main/LICENSE) for the full license text. diff --git a/docs/mkdocs/javascripts/mathjax.js b/docs/mkdocs/javascripts/mathjax.js new file mode 100644 index 0000000..421c6a0 --- /dev/null +++ b/docs/mkdocs/javascripts/mathjax.js @@ -0,0 +1,20 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } + }; + + document$.subscribe(() => { + MathJax.startup.output.clearCache() + MathJax.typesetClear() + MathJax.texReset() + MathJax.typesetPromise() + }) + \ No newline at end of file diff --git a/docs/mkdocs/mkdocs.yml.template b/docs/mkdocs/mkdocs.yml.template new file mode 100644 index 0000000..7fa0970 --- /dev/null +++ b/docs/mkdocs/mkdocs.yml.template @@ -0,0 +1,102 @@ +site_name: WebRTC Nuts and Bolts +site_url: https://adalkiran.github.io/webrtc-nuts-and-bolts/ +site_author: Adil Alper DALKIRAN +site_description: A holistic way of understanding how WebRTC and its protocols run in practice, with code and detailed documentation. +copyright: Copyright © 2022 - present, Adil Alper DALKIRAN. All rights reserved. +repo_url: https://github.com/adalkiran/webrtc-nuts-and-bolts +repo_name: adalkiran/webrtc-nuts-and-bolts + +theme: + name: 'material' + logo: 'assets/icon.svg' + favicon: 'assets/icon.png' + features: + - toc.follow + - toc.integrate + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - navigation.footer + - content.code.copy + icon: + repo: fontawesome/brands/github + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-sunny + name: Switch to system preference + +extra_css: + - stylesheets/custom.css + +nav: + - LLaMA Nuts and Bolts: '../llama-nuts-and-bolts' + - 'WebRTC Nuts and Bolts': +{{navigation_placeholder}} + - 'Contact': 'https://www.linkedin.com/in/alper-dalkiran/' + +extra: + social: + - icon: fontawesome/brands/github + name: 'adalkiran' + link: https://github.com/adalkiran + - icon: fontawesome/brands/x-twitter + name: '@aadalkiran' + link: https://www.linkedin.com/in/alper-dalkiran/ + - icon: fontawesome/brands/linkedin + name: 'in/alper-dalkiran' + link: https://www.linkedin.com/in/alper-dalkiran/ + analytics: + provider: google + property: G-05VMCF3NF0 +# consent: +# title: Cookie consent +# description: >- +# We use cookies to recognize your repeated visits and preferences, as well +# as to measure the effectiveness of our documentation and whether users +# find what they're searching for. With your consent, you're helping us to +# make our documentation better. + +markdown_extensions: + - admonition + - toc: + permalink: true + - md_in_html + - pymdownx.arithmatex: + generic: true + inline_syntax: ['dollar'] + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + use_pygments: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +plugins: + - social + +extra_javascript: + - javascripts/mathjax.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js diff --git a/docs/mkdocs/prepare-mkdocs.sh b/docs/mkdocs/prepare-mkdocs.sh new file mode 100755 index 0000000..0821be0 --- /dev/null +++ b/docs/mkdocs/prepare-mkdocs.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +set -e + +cd mkdocs +rm -rf docs && mkdir -p docs +cp -r ../*.md ../images ./stylesheets ./javascripts ./assets ./docs + +rm ./docs/README.md +cp ./extra-content/*.md ./docs + +GITHUB_URL="https://github.com/adalkiran/webrtc-nuts-and-bolts" + +GITHUB_CONTENT_PREFIX="$GITHUB_URL/blob/main" + +nav_items="" + +for file in ./docs/*.md; do + + # Remove footer navigation + sed -i -e ':a;N;$!ba; s/\s*
\s*---*.*//g; ta' "$file" + + # Edit same level paths + sed -i -e 's/\](\.\//\](/g' "$file" + + # Edit upper level paths + sed -i -e "s,\(\[[^\[]*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file" + sed -i -e "s,\(\[.*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file" + sed -i -e "s,\(\[.*\]\)(\([^)]*\.ipynb\)),\1($GITHUB_CONTENT_PREFIX\/docs\/\2),g" "$file" + + # Edit img tag paths + sed -i -e 's/src="images\//src="..\/images\//g' "$file" + + # Edit external links + sed -i -e "/\!\[/!s,\[\([^\[]*\)\](http\([^)]*\)),\
\1\<\/a\>,g" "$file" + sed -i -e "/\!\[/!s,\[\(.*\)\](http\([^)]*\)),\\1\<\/a\>,g" "$file" + + file_name=$(basename "$file") + first_line=$(head -1 "$file") + title=$(echo "$first_line" | sed -e 's/^[^\.]*\. \(.*\)\*\*/\1/g') + chapter_num=$(echo "$first_line" | sed -e 's/^[^0-9]*\([0-9]*\)\..*/\1/g') + case $first_line in + "---"*) + nav_items="\n - '$file_name'${nav_items}" + ;; + *) + if [ ${#chapter_num} -lt 3 ]; then + chapter_num=$(expr $chapter_num + 1) + nav_items="${nav_items}\n - '$file_name'" + else + chapter_num=0 + nav_items="\n - '$file_name'${nav_items}" + fi + echo "--- +title: $title +type: docs +menus: + - main +weight: $chapter_num +--- +" | cat - "$file" > temp && mv temp "$file" + ;; + esac +done + +cat mkdocs.yml.template | sed -e "s/{{navigation_placeholder}}/${nav_items}/g" > mkdocs.yml diff --git a/docs/mkdocs/stylesheets/custom.css b/docs/mkdocs/stylesheets/custom.css new file mode 100644 index 0000000..bfe7afb --- /dev/null +++ b/docs/mkdocs/stylesheets/custom.css @@ -0,0 +1,3 @@ +mjx-container { + font-size: 16px!important; +} \ No newline at end of file