Squashed 'third_party/rawrtc/rawrtc/' content from commit aa3ae4b24

Change-Id: I38a655a4259b62f591334e90a1315bd4e7e4d8ec
git-subtree-dir: third_party/rawrtc/rawrtc
git-subtree-split: aa3ae4b247275cc6e69c30613b3a4ba7fdc82d1b
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..2be4f2f
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,209 @@
+version: 2
+
+shared:
+  host: &shared-host
+    working_directory: ~/rawrtc
+    steps:
+      - checkout
+
+      # Set dynamic library path and binary path (since we prefix)
+      - run:
+          name: Setup environment variables
+          command: |
+            echo 'export PREFIX=/tmp/prefix' >> ${BASH_ENV}
+            echo 'export LD_LIBRARY_PATH=${PREFIX}/lib:${PREFIX}/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}' >> ${BASH_ENV}
+            echo 'export PATH=${PREFIX}/bin:${PATH}' >> ${BASH_ENV}
+
+      # Configure library
+      - run:
+          name: Configure
+          command: |
+            mkdir build
+            meson build --prefix ${PREFIX}
+
+      # Build library
+      - run:
+          name: Build
+          command: |
+            cd build
+            ninja install
+
+      # Run ICE gatherer
+      - run:
+          name: Run ICE gatherer
+          command: |
+            ulimit -c unlimited -S
+            ice-gatherer
+
+      # Store core dumps on failure
+      - run:
+          command: |
+            mkdir -p /tmp/core_dumps
+            cp core.* /tmp/core_dumps
+          when: on_fail
+
+      - store_artifacts:
+          path: /tmp/core_dumps
+
+  cross: &shared-cross
+    working_directory: ~/rawrtc
+    steps:
+      - checkout
+
+      # Configure library
+      - run:
+          name: Configure
+          command: |
+            mkdir build
+            meson build --prefix /tmp/prefix --cross-file ${CROSS_FILE_NAME}
+
+      # Build library
+      - run:
+          name: Build
+          command: |
+            cd build
+            ninja install
+
+
+jobs:
+  # Host: Ubuntu 14.04 LTS
+  trusty-gcc:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:trusty
+    environment:
+      CC: gcc
+  trusty-clang:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:trusty
+    environment:
+      CC: clang
+
+  # Host: Ubuntu 16.04 LTS
+  xenial-gcc:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:xenial
+    environment:
+      CC: gcc
+  xenial-clang:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:xenial
+    environment:
+      CC: clang
+
+  # Host: Ubuntu 18.04 LTS
+  bionic-gcc:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:bionic
+    environment:
+      CC: gcc
+  bionic-clang:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:bionic
+    environment:
+      CC: clang
+
+  # Host: Arch Linux
+  archlinux-gcc:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:archlinux
+    environment:
+      CC: gcc
+  archlinux-clang:
+    <<: *shared-host
+    docker:
+      - image: rawrtc/ci-image:archlinux
+    environment:
+      CC: clang
+
+  # Cross: Linux ARMv6
+  linux-armv6:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:linux-armv6
+
+  # Cross: Linux ARMv7
+  linux-armv7:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:linux-armv7
+
+  # Cross: Android API 16 ARM
+  android-16-arm:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-16-arm
+
+  # Cross: Android API 16 x86
+  android-16-x86:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-16-x86
+
+  # Cross: Android API 28 ARM
+  android-28-arm:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-28-arm
+
+  # Cross: Android API 28 ARM64
+  android-28-arm64:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-28-arm64
+
+  # Cross: Android API 28 x86
+  android-28-x86:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-28-x86
+
+  # Cross: Android API 28 x86_64
+  android-28-x86_64:
+    <<: *shared-cross
+    docker:
+      - image: rawrtc/cross-build:android-28-x86_64
+
+  # Cross: Windows x86
+  #windows-x86:
+  #  <<: *shared-cross
+  #  docker:
+  #    - image: rawrtc/cross-build:windows-x86
+
+  # Cross: Windows x64
+  #windows-x64:
+  #  <<: *shared-cross
+  #  docker:
+  #    - image: rawrtc/cross-build:windows-x64
+
+
+workflows:
+  version: 2
+
+  # Build all
+  build:
+    jobs:
+      - trusty-gcc
+      - trusty-clang
+      - xenial-gcc
+      - xenial-clang
+      - bionic-gcc
+      - bionic-clang
+      - archlinux-gcc
+      - archlinux-clang
+      - linux-armv6
+      - linux-armv7
+      - android-16-arm
+      - android-16-x86
+      - android-28-arm
+      - android-28-arm64
+      - android-28-x86
+      - android-28-x86_64
+      #- windows-x86
+      #- windows-x64
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..32e283f
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,76 @@
+---
+BasedOnStyle: LLVM
+Language: Cpp
+AlignAfterOpenBracket: AlwaysBreak
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: DontAlign
+AlignOperands: true
+AlignTrailingComments: false
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Empty
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+BinPackArguments: true
+BinPackParameters: false
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Attach
+BreakBeforeTernaryOperators: true
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+ColumnLimit: 100
+CommentPragmas: '^ IWYU pragma:'
+ContinuationIndentWidth: 4
+DerivePointerAlignment: false
+DisableFormat: false
+ForEachMacros:
+  - LIST_FOREACH
+IncludeBlocks: Preserve
+IncludeCategories:
+  - Regex: '^"[^\.]+'
+    Priority: 1
+  - Regex: '^"\.\./'
+    Priority: 2
+  - Regex: '^<rawrtc(\.h|/)'
+    Priority: 3
+  - Regex: '^<(re\.h|rew\.h|rawrtcc|rawrtcdc|usrsctp)'
+    Priority: 4
+  - Regex: '^<openssl'
+    Priority: 5
+  - Regex: '.*'
+    Priority: 6
+IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: true
+IndentPPDirectives: AfterHash
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 6000
+PointerAlignment: Left
+ReflowComments: true
+SortIncludes: true
+SpaceAfterCStyleCast: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 2
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+TabWidth: 4
+UseTab: Never
+...
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b885487
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+
+# Var
+/var
+
+# Build folder
+/build*
+
+# Subprojects
+/subprojects/*/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed46f5f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,46 @@
+# Build matrix
+language: c
+matrix:
+  include:
+    # Mac OS 10.13 (default)
+    - os: osx
+      osx_image: xcode9.4
+      compiler: clang
+
+    # Mac OS 10.13 (latest)
+    - os: osx
+      osx_image: xcode10.1
+      compiler: clang
+
+# Dependencies
+addons:
+  homebrew:
+    packages:
+      - meson
+      - ninja
+      - openssl
+    update: true
+
+# Setup prefix & enable core dumps
+before_script:
+  - export PREFIX=/tmp/prefix
+  - export LD_LIBRARY_PATH=${PREFIX}/lib:${LD_LIBRARY_PATH}
+  - export PATH=${PREFIX}/bin:${PATH}
+  - export PKG_CONFIG_PATH=/usr/local/opt/openssl/lib/pkgconfig:${PKG_CONFIG_PATH}
+  - ulimit -c unlimited -S
+
+# Build library and run ICE gatherer
+script:
+  - |
+    mkdir build
+    meson build --prefix ${PREFIX}
+    cd build
+    ninja install
+  - ice-gatherer
+
+# Find core dump and print traceback on failure
+after_failure:
+  - |
+    for f in $(find /cores -maxdepth 1 -name 'core.*' -print); do
+      lldb --core $f --batch --one-line "bt"
+    done;
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2fbd6ec
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,55 @@
+# Changelog
+
+## [0.5.1] (2019-08-15)
+
+* Re-enable peer reflexive candidates (#141)
+
+## [0.5.0] (2019-08-15)
+
+* Fix BoringSSL compatibility (#139)
+* Calculate ICE candidate priority (#140)
+* Use [upstream usrsctp](https://github.com/sctplab/usrsctp/)
+* Expose more transport parameters (#146)
+  - Add getter/setter for send/receiver buffer length
+  - Add getter/setter for congestion control algorithm
+  - Add getter/setter for MTU
+
+## [0.4.0] (2019-03-19)
+
+* Replace CMake with Meson build system
+  - This finally allowed us to get rid of the dependencies script :tada:
+* Internal restructuring of source files
+
+## [0.3.0] (2019-03-02)
+
+* Split the stack into three separate entities (major)
+  - RAWRTC (this repository) contains the WebRTC/ORTC API, the ICE/DTLS stack
+    and bindings to the data channel implementation.
+  - [RAWRTCDC](https://github.com/rawrtc/rawrtc-data-channel) contains the data
+    channel implementation
+  - [RAWRTCC](https://github.com/rawrtc/rawrtc-common) contains common
+    functionality required by both RAWRTC and RAWRTCDC
+* Added a gathering timeout
+* Fixed various issues with the ICE transport states
+
+## [0.2.2] (2018-04-14)
+
+* Fixed parsing the DTLS role in the peer connection API
+
+## [0.2.1] (2018-02-26)
+
+* Fixed missing cast in examples leading to issues on specific platforms (such
+  as ARM)
+
+## [0.2.0] (2018-02-12)
+
+* Initial release of RAWRTC
+
+
+
+[0.5.0]: https://github.com/rawrtc/rawrtc/compare/v0.4.0...v0.5.0
+[0.4.0]: https://github.com/rawrtc/rawrtc/compare/v0.3.0...v0.4.0
+[0.3.0]: https://github.com/rawrtc/rawrtc/compare/v0.2.2...v0.3.0
+[0.2.2]: https://github.com/rawrtc/rawrtc/compare/v0.2.1...v0.2.2
+[0.2.1]: https://github.com/rawrtc/rawrtc/compare/v0.2.0...v0.2.1
+[0.2.0]: https://github.com/rawrtc/rawrtc/compare/bd9d1ef15d008fdc24b4d5e3158e775a03ffec16...v0.2.0
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ef5447b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2019, Lennart Grahl
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb6d1bb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,446 @@
+# RAWRTC
+
+[![CircleCI build status][circleci-badge]][circleci-url]
+[![Travis CI build status][travis-ci-badge]][travis-ci-url]
+[![Join our chat on Gitter][gitter-icon]][gitter]
+
+A WebRTC and ORTC library with a small footprint.
+
+## Features
+
+The following list represents all features that are planned for RAWRTC.
+Features with a check mark are already implemented.
+
+* ICE [[draft-ietf-ice-rfc-5245bis-08]][ice]
+  - [x] Trickle ICE [[draft-ietf-ice-trickle-07]][trickle-ice]
+  - [x] IPv4
+  - [x] IPv6
+  - [x] UDP
+  - [ ] TCP
+* STUN [[RFC 5389]][stun]
+  - [x] UDP
+  - [ ] TCP
+  - [ ] TLS over TCP
+  - [ ] DTLS over UDP [[RFC 7350]][stun-turn-dtls]
+* TURN [[RFC 5766]][turn]
+  - [ ] UDP
+  - [ ] TCP
+  - [ ] TLS over TCP
+  - [ ] DTLS over UDP [[RFC 7350]][stun-turn-dtls]
+* Data Channel
+  - [x] DCEP [[draft-ietf-rtcweb-data-protocol-09]][dcep]
+  - [x] SCTP-based [[draft-ietf-rtcweb-data-channel-13]][sctp-dc]
+* API
+  - [x] WebRTC C-API based on the [W3C WebRTC API][w3c-webrtc] and
+    [[draft-ietf-rtcweb-jsep-24]][jsep]
+  - [x] ORTC C-API based on the [W3C CG ORTC API][w3c-ortc]
+
+## FAQ
+
+1. *Does this use multiple threads?*
+
+   No, this library is single-threaded and uses an event loop underneath.
+
+2. *Can I use it in a threaded environment?*
+
+   Yes. Just make sure you're always calling it from the same thread the event
+   loop is running on, or either [lock/unlock the event loop thread][re-lock]
+   or [use the message queues provided by re][re-mqueue] to exchange data with
+   the event loop thread. However, it is important that you only run one *re*
+   event loop in one thread.
+
+3. *I only want a data channel implementation for my SFU!*
+
+   Check out [RAWRTCDC][rawrtcdc].
+
+## Prerequisites
+
+The following tools are required:
+
+* [git][git]
+* [ninja][ninja] >= 1.5
+* [meson][meson] >= 0.48.0
+* pkg-config (`pkgconf` for newer FreeBSD versions)
+* SSL development libraries (`libssl-dev` on Debian, `openssl` on OSX and
+  FreeBSD)
+
+## Build
+
+```bash
+cd <path-to-rawrtc>
+mkdir build
+meson build
+cd build
+ninja
+```
+
+## Run
+
+RAWRTC provides a lot of tools that can be used for quick testing purposes and
+to get started. Let's go through them one by one. If you just want to check out
+data channels and browser interoperation, skip to the
+[`peer-connection` tool section](#peer-connection) which uses the WebRTC API or
+to the [`data-channel-sctp` tool section](#data-channel-sctp) which uses the
+ORTC API.
+
+Most of the tools have required or optional arguments which are shared among
+tools. Below is a description for the various shared arguments:
+
+#### offering
+
+Whether the peer is going to create an offer. Provide `1` to create an offer
+immediately or `0` to create an answer once the remote offer has been
+processed.
+
+Only used by WebRTC API tools.
+
+#### ice-role
+
+Determines the ICE role to be used by the ICE transport, where `0` means
+*controlled* and `1` means *controlling*.
+
+Only used by ORTC API tools.
+
+#### sctp-port
+
+The port number the internal SCTP stack is supposed to use. Defaults to `5000`.
+
+Note: It doesn't matter which port you choose unless you want to be able to
+debug SCTP messages. In this case, it's easier to distinguish the peers by
+their port numbers.
+
+Only used by ORTC API tools.
+
+#### ice-candidate-type
+
+If supplied, one or more specific ICE candidate types will be enabled and all
+other ICE candidate types will be disabled. Can be one of the following
+strings:
+
+* *host*
+* *srflx*
+* *prflx*
+* *relay*
+
+Note that this has no effect on the gathering policy. The candidates will be
+gathered but they will simply be ignored by the tool.
+
+If not supplied, all ICE candidate types are enabled.
+
+### ice-gatherer
+
+API: ORTC
+
+The ICE gatherer tool gathers and prints ICE candidates. Once gathering is
+complete, the tool exits.
+
+Usage:
+
+    ice-gatherer
+
+### ice-transport-loopback
+
+API: ORTC
+
+The ICE transport loopback tool starts two ICE transport instances which
+establish an ICE connection. Once you see the following line for both clients
+*A* and *B*, the ICE connection has been established:
+
+    (<client>) ICE transport state: connected
+
+Usage:
+
+    ice-transport-loopback [<ice-candidate-type> ...]
+
+### dtls-transport-loopback
+
+API: ORTC
+
+The DTLS transport loopback tool starts two DTLS transport instances which
+work on top of an established ICE transport connection.
+
+To verify that the DTLS connection establishes, wait for the following line for
+both clients *A* and *B*:
+
+    (<client>) DTLS transport state change: connected
+
+Usage:
+
+    dtls-transport-loopback [<ice-candidate-type> ...]
+
+### sctp-redirect-transport
+
+API: ORTC
+
+The SCTP redirect transport tool starts an SCTP redirect transport on top of an
+established DTLS transport to relay SCTP messages from and to a third party.
+This tool has been developed to be able to test data channel implementations
+without having to write the required DTLS and ICE stacks. An example of such a
+testing tool is [dctt][dctt] which uses the kernel SCTP stack of FreeBSD.
+
+Building:
+
+This tool is not built by default. You can enable building it in the following
+way:
+
+```bash
+cd <path-to-rawrtc>/build
+meson configure -Dsctp_redirect_transport=true
+ninja
+```
+
+Usage:
+
+    sctp-redirect-transport <0|1 (ice-role)> <redirect-ip> <redirect-port>
+                            [<sctp-port>] [<maximum-message-size>]
+                            [<ice-candidate-type> ...]
+
+Special arguments:
+
+* `redirect-ip`: The IP address on which the external SCTP stack is listening.
+* `redirect-port` The port on which the external SCTP stack is listening.
+* `maximum-message-size`: The maximum message size of a data channel message
+  the external SCTP stack is able to handle. `0` indicates that messages of
+  arbitrary size can be handled. Defaults to `0`.
+
+### data-channel-sctp-loopback
+
+API: ORTC
+
+The data channel SCTP loopback tool creates several data channels on top of an
+abstracted SCTP data transport. As soon as a data channel is open, a message
+will be sent to the other peer. Furthermore, another message will be sent on a
+specific channel after a brief timeout.
+
+To verify that a data channels opens, wait for the following line:
+
+    (<client>) Data channel open: <channel-label>
+    
+The tool will send some large (16 MiB) test data to the other peer depending on
+the ICE role. We are able to do this because RAWRTC handles data channel
+messages correctly and does not have a maximum message size limitation compared
+to most other implementations (check out
+[this article][demystifying-webrtc-dc-size-limit] for a detailed explanation).
+
+Usage:
+
+    data-channel-sctp-loopback [<ice-candidate-type> ...]
+
+### data-channel-sctp
+
+API: ORTC
+
+The data channel SCTP tool creates several data channels on top of an
+abstracted SCTP data transport:
+
+* A pre-negotiated data channel with the label `cat-noises` and the id `0`
+  that is reliable and ordered. In the WebRTC JS API, the channel would be
+  created by invoking:
+   
+   ```js
+   peerConnection.createDataChannel('cat-noises', {
+       ordered: true,
+       id: 0
+   });
+   ```
+
+* A data channel with the label `bear-noises` that is reliable but unordered.
+  In the WebRTC JS API, the channel would be created by invoking:
+   
+   ```js
+   peerConnection.createDataChannel('bear-noises', {
+       ordered: false,
+       maxRetransmits: 0
+   });
+   ```
+
+To establish a connection with another peer, the following procecure must be
+followed:
+
+1. The JSON blob after `Local Parameters:` must be pasted into the other peer
+   you want to establish a connection with. This can be either a browser
+   instance that uses the 
+   [WebRTC-ORTC browser example tool][webrtc-ortc-example] or another instance
+   of this tool.
+
+2. The other peer's local parameters in form of a JSON blob must be pasted into
+   this tool's instance.
+
+3. Once you've pasted the local parameters into each other's instance, the peer
+   connection can be established by pressing *Enter* in both instances (press
+   the *Start* button in the browser).
+
+The tool will send some test data to the other peer depending on the ICE role.
+However, the browser tool behaves a bit differently. Check the log output of
+the tool instances (console output in the browser) to see what data has been
+sent and whether it has been received successfully.
+
+In the browser, you can use the created data channels by accessing
+`peer.dc['<channel-name>']`, for example:
+
+```js
+peer.dc['example-channel'].send('RAWR!')
+```
+
+Usage:
+
+    data-channel-sctp <0|1 (ice-role)> [<sctp-port>] [<ice-candidate-type> ...]
+
+### data-channel-sctp-streamed
+
+API: ORTC
+
+The data channel SCTP streamed tool is the counterpart to the *normal* data
+channel SCTP tool but uses the streaming mode. **Be aware this tool and the
+streaming mode is currently experimental and incomplete.**
+
+The necessary peer connection establishment steps are identical to the ones
+described for the [data-channel-sctp](#data-channel-sctp) tool.
+
+Usage:
+
+    data-channel-sctp-streamed <0|1 (ice-role)> [<sctp-port>]
+                               [<ice-candidate-type> ...]
+
+### data-channel-sctp-echo
+
+API: ORTC
+
+The data channel SCTP echo tool behaves just like any other echo server: It
+echoes received data on any data channel back to the sender.
+
+The necessary peer connection establishment steps are identical to the ones
+described for the [data-channel-sctp](#data-channel-sctp) tool.
+
+Usage:
+
+    data-channel-sctp-echo <0|1 (ice-role)> [<sctp-port>]
+                           [<ice-candidate-type> ...]
+    
+### data-channel-sctp-throughput
+
+API: ORTC
+
+The data channel SCTP throughput tool allows you to test throughput by sending
+one or more message. It will report the amount of seconds elapsed and the
+throughput in Mbit/s.
+
+The necessary peer connection establishment steps are identical to the ones
+described for the [data-channel-sctp](#data-channel-sctp) tool. However,
+be aware that this tool has no browser counterpart at the moment, so it only
+makes sense to use two instances of this tool for throughput testing.
+
+Usage:
+
+    data-channel-sctp-throughput <0|1 (ice-role)> <message-size> [<n-times>]
+                                 [<sctp-port>] [<ice-candidate-type> ...]
+
+Special arguments:
+
+* `message-size`: Is the message size in bytes used for throughput testing. The
+  controlling peer will determine the message size for both peers, so this
+  argument is being ignored for the controlled peer.
+* `n-times`: Is the amount of times the message will be sent. Again, this
+  value is being ignored for the controlled peer.
+
+### peer-connection
+
+API: WebRTC
+
+The peer connection tool creates a peer connection instance and several data
+channels:
+
+* A pre-negotiated data channel with the label `cat-noises` and the id `0`
+  that is reliable and ordered. In the JS API, the channel would be created
+  by invoking:
+   
+   ```js
+   peerConnection.createDataChannel('cat-noises', {
+       ordered: true,
+       id: 0
+   });
+   ```
+
+* A data channel with the label `bear-noises` that is reliable but unordered.
+  In the WebRTC JS API, the channel would be created by invoking:
+   
+   ```js
+   peerConnection.createDataChannel('bear-noises', {
+       ordered: false,
+       maxRetransmits: 0
+   });
+   ```
+
+To establish a connection with another peer, the following procecure must be
+followed:
+
+1. If the peer is taking the *offering* role, the generated JSON blob that
+   contains the *offer SDP* must be pasted into the other peer you want to
+   establish a connection with. This can be either a browser instance that uses
+   the [WebRTC browser example tool][webrtc-example] or another instance of
+   this tool. In case it is a browser instance, press the *Start* button and
+   paste the data directly into the text area below
+   `Paste remote description:`. In case it is another instance of this tool,
+   paste the data into the other peer's console and press *Enter*.
+
+2. The peer who takes the *answering* role now generates a JSON blob as well
+   that contains the *answer SDP*. It must be pasted into the other browser
+   instance or tool instance as described in the previous step.
+
+3. The peer connection should be established automatically once *offer* and
+   *answer* have been exchanged and applied.
+
+The tool will send some test data to the other peer depending on whether or not
+it took the *offering* role. However, the browser tool behaves a bit
+differently. Check the log output of the tool instances (in the browser, either
+open the console log or check out the live log on the right side) to see what
+data has been sent and whether it has been received successfully.
+
+In the browser, you can use the created data channels by accessing
+`pc.dcs['<channel-name>']` in the console log, for example:
+
+```js
+pc.dcs['cat-noises'].send('RAWR!')
+```
+
+Usage:
+
+    peer-connection <0|1 (offering)> [<ice-candidate-type> ...]
+
+## Contributing
+
+When creating a pull request, it is recommended to run `format-all.sh` to
+apply a consistent code style.
+
+
+
+[circleci-badge]: https://circleci.com/gh/rawrtc/rawrtc.svg?style=shield
+[circleci-url]: https://circleci.com/gh/rawrtc/rawrtc
+[travis-ci-badge]: https://travis-ci.org/rawrtc/rawrtc.svg?branch=master
+[travis-ci-url]: https://travis-ci.org/rawrtc/rawrtc
+[gitter]: https://gitter.im/rawrtc/Lobby
+[gitter-icon]: https://badges.gitter.im/rawrtc/Lobby.svg
+
+[ice]: https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-08
+[trickle-ice]: https://tools.ietf.org/html/draft-ietf-ice-trickle-07
+[stun]: https://tools.ietf.org/html/rfc5389
+[turn]: https://tools.ietf.org/html/rfc5766
+[stun-turn-dtls]: https://tools.ietf.org/html/rfc7350
+[dcep]: https://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-09
+[sctp-dc]: https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13
+[jsep]: https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-19
+[w3c-webrtc]: https://www.w3.org/TR/webrtc/
+[w3c-ortc]: https://draft.ortc.org
+
+[re-lock]: http://www.creytiv.com/doxygen/re-dox/html/re__main_8h.html#ad335fcaa56e36b39cb1192af1a6b9904
+[re-mqueue]: http://www.creytiv.com/doxygen/re-dox/html/re__mqueue_8h.html
+[rawrtcdc]: https://github.com/rawrtc/rawrtc-data-channel
+
+[git]: (https://git-scm.com)
+[meson]: https://mesonbuild.com
+[ninja]: https://ninja-build.org
+
+[webrtc-ortc-example]: https://rawgit.com/rawrtc/rawrtc/master/htdocs/ortc/index.html
+[webrtc-example]: https://rawgit.com/rawrtc/rawrtc/master/htdocs/webrtc/index.html
+[dctt]: https://github.com/nplab/dctt
+[demystifying-webrtc-dc-size-limit]: https://lgrahl.de/articles/demystifying-webrtc-dc-size-limit.html
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..30100ec
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,39 @@
+# Release Process
+
+Signing key: https://lgrahl.de/pub/pgp-key.txt
+
+1. Set variables:
+
+   ```bash
+   export VERSION=<version>
+   export GPG_KEY=3FDB14868A2B36D638F3C495F98FBED10482ABA6
+   ```
+
+2. Update version number in `meson.build` and `CHANGELOG.md`. Also, update the
+   URL with the corresponding tags.
+
+3. Do a signed commit and signed tag of the release:
+
+   ```bash
+   git add meson.build CHANGELOG.md
+   git commit -S${GPG_KEY} -m "Release v${VERSION}"
+   git tag -u ${GPG_KEY} -m "Release v${VERSION}" v${VERSION}
+   ```
+
+4. Push.
+
+   ```bash
+   git push && git push --tags
+   ```
+
+5. Create a new release on GitHub.
+
+6. Prepare CHANGELOG.md for upcoming changes:
+
+   ```md
+    ## [Unreleased] (YYYY-MM-DD)
+
+    * ...
+   ```
+
+7. Pat yourself on the back and celebrate!
diff --git a/assets/rawrtc-icon-256.png b/assets/rawrtc-icon-256.png
new file mode 100644
index 0000000..394a7b3
--- /dev/null
+++ b/assets/rawrtc-icon-256.png
Binary files differ
diff --git a/assets/rawrtc-icon.svg b/assets/rawrtc-icon.svg
new file mode 100644
index 0000000..226480d
--- /dev/null
+++ b/assets/rawrtc-icon.svg
@@ -0,0 +1,431 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="102mm"
+   height="102mm"
+   viewBox="0 0 361.41733 361.41731"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r"
+   sodipodi:docname="rawrtc-icon.svg">
+  <title
+     id="title4219">Rawr Dinosaur</title>
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.4"
+     inkscape:cx="281.11546"
+     inkscape:cy="44.246759"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:snap-global="false"
+     inkscape:window-width="2511"
+     inkscape:window-height="1056"
+     inkscape:window-x="49"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     fit-margin-top="1"
+     fit-margin-left="1"
+     fit-margin-right="1"
+     fit-margin-bottom="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Rawr Dinosaur</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Tavin</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source>tavinsorigami.com</dc:source>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>dinsosaur</rdf:li>
+            <rdf:li>green</rdf:li>
+            <rdf:li>comic</rdf:li>
+            <rdf:li>thick outline</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <cc:license
+           rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Layer 2"
+     transform="translate(-11.432672,-95.738476)">
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,211.59186,368.44051)"
+       id="g376">
+      <path
+         inkscape:connector-curvature="0"
+         id="path378"
+         style="fill:#ff6600;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 0,-7.534 -6.107,-13.642 -13.641,-13.642 -7.535,0 -13.642,6.108 -13.642,13.642 0,7.534 6.107,13.642 13.642,13.642 C -6.107,13.642 0,7.534 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,370.05178,256.27199)"
+       id="g404">
+      <path
+         inkscape:connector-curvature="0"
+         id="path406"
+         style="fill:#ffcc00;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 0,-7.533 -6.107,-13.642 -13.641,-13.642 -7.535,0 -13.642,6.109 -13.642,13.642 0,7.534 6.107,13.643 13.642,13.643 C -6.107,13.643 0,7.534 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,174.20235,254.49419)"
+       id="g408">
+      <path
+         inkscape:connector-curvature="0"
+         id="path410"
+         style="fill:#0089cc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 0,-7.533 -6.107,-13.642 -13.642,-13.642 -7.533,0 -13.641,6.109 -13.641,13.642 0,7.534 6.108,13.643 13.641,13.643 C -6.107,13.643 0,7.534 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,334.44592,368.44051)"
+       id="g412">
+      <path
+         inkscape:connector-curvature="0"
+         id="path414"
+         style="fill:#009939;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 0,-7.534 -6.107,-13.642 -13.643,-13.642 -7.533,0 -13.641,6.108 -13.641,13.642 0,7.534 6.108,13.642 13.641,13.642 C -6.107,13.642 0,7.534 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,272.1297,183.27078)"
+       id="g416">
+      <path
+         inkscape:connector-curvature="0"
+         id="path418"
+         style="fill:#bf0000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 0,-7.534 -6.108,-13.642 -13.642,-13.642 -7.534,0 -13.642,6.108 -13.642,13.642 0,7.534 6.108,13.642 13.642,13.642 C -6.108,13.642 0,7.534 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,209.81407,256.27199)"
+       id="g420">
+      <path
+         inkscape:connector-curvature="0"
+         id="path422"
+         style="fill:#fc0007;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 C 0,-0.287 0.025,-0.568 0.043,-0.851 6.094,0.545 10.61,5.955 10.61,12.43 c 0,0.287 -0.025,0.569 -0.043,0.852 C 4.516,11.885 0,6.475 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,220.38449,295.94437)"
+       id="g424">
+      <path
+         inkscape:connector-curvature="0"
+         id="path426"
+         style="fill:#1cd306;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 2.351,-4.11 6.769,-6.887 11.843,-6.887 2.068,0 4.021,0.474 5.778,1.298 -2.35,4.11 -6.768,6.887 -11.843,6.887 C 3.71,1.298 1.757,0.824 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,174.20235,368.44051)"
+       id="g428">
+      <path
+         inkscape:connector-curvature="0"
+         id="path430"
+         style="fill:#0f7504;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="M 0,0 C 0,-3.333 1.198,-6.383 3.184,-8.752 5.168,-6.383 6.366,-3.333 6.366,0 6.366,3.333 5.168,6.383 3.184,8.752 1.198,6.383 0,3.333 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,62.360391,328.05453)"
+       id="g432">
+      <path
+         inkscape:connector-curvature="0"
+         id="path434"
+         style="fill:#0c5e87;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c 1.657,-0.716 3.481,-1.117 5.401,-1.117 5.024,0 9.401,2.723 11.769,6.766 C 15.513,6.365 13.688,6.765 11.769,6.765 6.745,6.765 2.367,4.042 0,0" />
+    </g>
+    <g
+       transform="matrix(5.8731269,0,0,-5.8731269,112.2309,176.51141)"
+       id="g436">
+      <path
+         inkscape:connector-curvature="0"
+         id="path438"
+         style="fill:#6b0001;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         d="m 0,0 c -0.032,-0.38 -0.059,-0.762 -0.059,-1.151 0,-6.47 4.509,-11.875 10.553,-13.277 0.031,0.38 0.058,0.762 0.058,1.15 C 10.552,-6.808 6.044,-1.401 0,0" />
+    </g>
+  </g>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-33.665753,-116.45693)">
+    <flowRoot
+       xml:space="preserve"
+       id="flowRoot4707"
+       style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"><flowRegion
+         id="flowRegion4709"><rect
+           id="rect4711"
+           width="3014.2952"
+           height="1814.234"
+           x="-3260.7725"
+           y="-963.90228" /></flowRegion><flowPara
+         id="flowPara4713" /></flowRoot>    <g
+       id="g4177"
+       transform="matrix(0.40940013,0,0,0.40940013,99.753722,120.75976)">
+      <g
+         id="g4715">
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:11.14011383;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 132.02124,739.32303 C 128.1691,704.58699 143.35476,613.5443 106.75681,628.67576 84.765796,644.49638 78.233315,683.43461 86.071666,714.4766 c -32.995139,3.64832 -45.805629,36.59695 -45.925603,31.61771 15.82005,0.20091 71.260287,-2.09149 91.875177,-6.77128 z"
+           id="path4180-6-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.62448359;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 391.78889,198.16624 30.43517,57.14277 c 40.72674,-1.40802 32.23752,-74.3271 -30.43517,-57.14277 z"
+           id="path4188"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.62448359;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 424.3909,264.84828 13.83137,48.50878 c 34.78364,2.80381 41.86818,-61.54402 -13.83137,-48.50878 z"
+           id="path4188-9"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.67187166;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 442.33419,326.79193 9.18221,39.87478 c 34.78363,2.80381 46.51734,-52.91002 -9.18221,-39.87478 z"
+           id="path4188-9-2"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.99820495;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 353.9906,148.6214 34.42008,31.90493 c 42.25192,5.4435 -5.71985,-82.770504 -34.42008,-31.90493 z"
+           id="path4188-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.90342808;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 264.219,123.89948 69.62028,12.64447 c 13.22922,1.97567 -7.32192,-30.24486 -18.02456,-32.83014 -6.56682,-1.58627 -28.65541,5.66403 -51.59572,20.18567 z"
+           id="path4188-8-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.74026108;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 197.35303,134.24499 49.69564,-9.27261 c 13.22922,1.97567 -9.02414,-16.46214 -19.68493,-19.21499 -10.54727,-2.72354 -15.17626,11.65 -30.01071,28.4876 z"
+           id="path4188-8-3-2"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.67187166;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 471.64801,553.00542 -3.49769,38.46589 c 31.83129,5.55938 44.94038,-38.19475 3.49769,-38.46589 z"
+           id="path4188-9-2-7"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.92802;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 476.9725,622.27186 48.63091,19.21116 c 15.92011,0.98001 -8.81242,-20.76224 -13.25005,-26.71122 -6.71959,-9.00794 -30.03628,-3.33806 -35.38086,7.50006 z"
+           id="path4188-9-2-7-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 540.92527,639.6125 43.46505,16.86302 c -5.26954,-6.00609 -13.81231,-30.07881 -18.67515,-29.95069 -5.28963,3.49552 -19.54639,8.25513 -24.7899,13.08767 z"
+           id="path4188-9-2-7-3-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 587.58135,657.10143 46.75249,10.28823 c -5.26954,-6.00609 -26.02264,-31.01806 -30.88557,-30.88994 -5.28953,3.49552 -10.62342,15.76917 -15.86692,20.60171 z"
+           id="path4188-9-2-7-3-8-4"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 653.77804,675.32257 43.46505,7.47047 c -8.96277,-22.73981 -23.56254,-32.48943 -43.46505,-7.47047 z"
+           id="path4188-9-2-7-3-8-4-7"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 329.58835,459.81036 c -18.6718,-22.31702 -40.3629,6.24288 -56.45298,-12.61891 2.26243,22.52667 -16.95676,21.63525 -24.57371,6.30946 l -14.61139,28.89068 c 41.76943,2.36295 64.94414,-13.86874 95.63808,-22.58123 z"
+           id="path4540"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:13.76126289;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 80.068798,327.32334 c -5.927441,-46.67073 8.5719,-85.35427 31.817502,-116.09287 23.14552,-26.58373 51.22378,-54.15291 80.44266,-69.39686 72.07498,-37.0945 162.8758,-9.25794 213.10069,60.22991 39.9959,112.0935 76.4321,240.56475 16.05837,289.15983 28.49744,-9.36974 29.64817,-77.44024 81.24566,-98.62178 25.79121,31.05051 14.61009,83.17466 -50.71983,148.4023 14.30055,10.87666 26.47518,26.61243 23.48136,77.01892 l -12.21033,9.39254 30.99548,2.81777 216.96788,64.8086 c -88.36925,21.65592 -270.91929,77.01272 -282.71576,12.21032 l 4.69631,17.37621 5.63552,14.08883 -111.30175,0.46962 c 8.85472,-17.24029 4.7866,-33.76906 25.82952,-34.75243 2.77392,-100.14184 37.35653,-74.50986 48.37168,-90.16848 0,0 -29.86576,-2.45886 -46.49317,48.84126 l -6.57482,34.75243 c -9.6273,9.07968 -21.03576,7.47252 -27.23837,37.10058 l -209.92356,4.69627 c 0.94402,-57.74401 15.5303,-70.68108 5.13877,-180.68196 -3.23988,-34.29604 -2.65724,-66.07331 -5.91298,-95.68865 -5.62411,-51.15887 -16.179775,-95.86646 -30.690832,-135.96236 z"
+           id="path4180"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccccccccccccccccccccssc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 134.98378,349.72369 c 17.09386,42.68254 30.10215,85.35777 25.56996,134.82319 65.7462,7.3403 146.05953,2.0632 216.51413,-35.53223 8.50279,7.30449 12.55362,3.32124 15.49388,-3.89595 1.97418,-4.84573 3.81243,-11.69645 4.79599,-15.51899 13.17641,-51.73883 -7.63174,-121.06163 -27.59561,-145.62723 -207.88982,25.00086 -188.58231,37.25058 -233.45002,66.08328 z"
+           id="path4178"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccscccc" />
+        <path
+           style="fill:#cb1d3b;fill-opacity:1;fill-rule:evenodd;stroke:#cb1d3b;stroke-width:12.92442894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 383.71688,357.19864 -0.55017,63.53084 c -27.64293,41.74431 -77.25926,49.05797 -126.4171,54.94405 4.64573,-44.12673 -25.87247,-108.02862 126.96727,-118.47489 z"
+           id="path4186"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:2.66113067;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 147.19676,342.58479 c 41.21647,-19.255 73.61039,-41.90896 160.71556,-46.90128 -6.43423,13.05863 -2.28707,26.59302 -8.63864,34.55313 -4.48598,-3.5731 -7.5501,-7.85713 -10.16827,-12.36416 -2.81539,-4.37309 -9.09796,-6.23507 -10.7044,-3.35571 -2.22384,3.98621 0.40224,14.31797 -3.29703,15.05761 -25.92353,-30.26311 -32.2088,-0.71232 -33.00751,16.5074 -15.86599,-7.85438 -18.16124,-14.66486 -16.88488,-21.20059 0,0 -18.69133,-3.53741 -15.95879,21.11506 -6.08136,0.70284 -12.06844,1.07615 -15.54576,-7.33486 -12.70444,-16.21117 -29.49718,22.30319 -24.76851,38.3128 -21.4009,0.20162 -20.71926,-22.24053 -21.74177,-34.3894 z"
+           id="path4158"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccsccccccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1.30174112;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 116.50711,310.18439 c -2.75029,-19.4266 18.36276,-76.50875 54.82755,-25.19801 17.64966,22.81934 -22.30951,52.92111 -54.82755,25.19801 z"
+           id="path4162"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.48770416;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 141.95743,293.6027 c 31.65686,0.82328 7.35029,-24.20581 0.81806,-13.45562 -1.15065,2.15415 -0.81806,6.38953 -0.81806,13.45562 z"
+           id="path4166"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 331.36188,192.93619 c -11.4145,-15.34356 -29.89439,-21.97542 -48.5343,-15.5308 -8.40162,2.98772 -15.09629,9.19459 -20.79736,17.53275 -6.47644,9.69656 -9.01846,21.40699 -7.81854,32.76357 3.4525,24.75377 22.1203,40.06591 45.64602,40.62054 17.0198,-1.41619 33.51565,-14.49055 37.48531,-40.29377 1.15148,-13.08162 -1.26976,-26.56679 -5.98113,-35.09229 z"
+           id="path4170"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.85963011;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 295.37692,210.74953 c 16.5309,2.21586 19.33681,-18.93227 14.09711,-24.5487 -4.8752,-3.16208 -13.49133,-1.65733 -18.25013,3.3076 -6.33771,5.97379 -5.08981,12.96092 -1.82429,16.59204 5.60669,-1.55809 7.53131,-0.1172 5.97731,4.64906 z"
+           id="path4172"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3.25435281;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 337.81377,178.45141 c -20.31543,-31.11618 -60.11235,-29.6121 -72.32269,-14.08882"
+           id="path4182"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.0131464;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 360.70749,284.76228 c 3.95627,34.33139 -2.51933,73.26533 -20.63203,83.74169 -12.80067,-28.94639 -20.49015,-57.60423 -49.45612,-70.39156 22.15648,-14.0999 46.06143,-14.21215 70.08815,-13.35013 z"
+           id="path4160"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 127.01397,356.36522 C 193.54683,300.31366 276.90782,278.97 371.75463,281.31587 l -2.65667,4.64907 c -81.77035,3.92009 -162.07551,23.31304 -242.08399,70.40028 z"
+           id="path4176"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 370.68766,283.64798 c 32.14269,95.25493 30.69311,123.82116 18.59751,161.33089 -0.053,-5.60251 -5.02007,1.5235 -5.03801,-4.01595 -0.17304,-53.23777 -5.3522,-104.67303 -22.01282,-152.61867 z"
+           id="path4418"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccscc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.0202322;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 194.12276,486.55587 c -2.82218,-12.73688 -12.82364,-23.58605 -21.20648,-33.14541 1.4931,21.96111 -10.31825,20.53747 -13.6933,31.59453 z"
+           id="path4160-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 234.22531,482.78543 c -7.00839,-3.71346 -11.55732,-7.48678 -10.50022,-10.59799 0.83888,-2.46899 -0.44817,-4.67431 -2.31607,-6.8539 -8.56602,-14.41103 -14.37494,13.79235 -14.46895,21.55375 z"
+           id="path4160-8-0"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="csccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 265.85725,477.12877 c -6.49225,-5.3572 -10.55182,-12.29534 -15.47296,-18.11605 -15.37682,8.29177 -9.67929,15.74045 -9.48774,22.54999 z"
+           id="path4160-8-0-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 159.5367,484.20622 c 83.63157,3.37122 139.74117,7.67085 220.71969,-34.87145 l -2.02476,-7.24842 c -71.96518,39.02277 -125.86637,40.92864 -218.69493,42.11987 z"
+           id="path4176-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.17535067;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 375.55543,451.78624 c -11.31473,-63.1826 -33.34447,-53.10988 -33.05548,7.68795 6.21711,-4.56673 26.83837,-7.3169 33.05548,-7.68795 z"
+           id="path4160-8-0-3-5-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 351.17336,463.46322 c -29.98291,-17.4642 -35.09792,-1.96781 -72.39271,-17.93216 l -9.29815,38.52091 z"
+           id="path4557"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 294.18276,473.95711 c 1.14562,-8.3459 -8.22728,-11.29911 -13.14833,-17.11982 -15.37691,8.29176 -13.66428,12.75175 -13.47274,19.56129 6.21711,-4.56673 20.40405,-2.07042 26.62107,-2.44147 z"
+           id="path4160-8-0-3-5"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#009143;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.20933723;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 320.20847,726.2179 -9.85892,0.56337 -115.39135,6.59379 c -3.84777,-43.38376 -20.51238,-73.81344 -15.46934,-102.96828 12.591,-72.79123 106.49023,-89.26336 138.2158,-20.6996 10.96252,23.69162 12.90295,57.492 13.18339,95.04436 0.0437,5.87229 -16.69958,11.17763 -13.88233,20.40985 z"
+           id="path4559"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccssscc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 220.40686,605.81246 70.44409,-14.08882"
+           id="path4561"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 258.44671,698.32909 328.8908,684.24026"
+           id="path4561-9"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5.16791201;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 190.71554,645.49304 313.02825,631.87976"
+           id="path4561-6"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 252.32304,672.61597 71.42021,-7.74682"
+           id="path4561-9-0"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 216.1802,722.74973 286.62429,708.6609"
+           id="path4561-9-4"
+           inkscape:connector-curvature="0" />
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:11.14011383;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 114.73974,526.85256 -2.42384,-12.90112 c -0.67869,-48.533 -6.56042,28.28038 -1.3129,-60.6534 2.49435,-42.27363 -36.951872,-68.84416 -82.825898,-62.04257 -7.445318,64.41212 97.349028,98.71837 86.562638,135.59709 z"
+           id="path4180-6"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccscc" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/assets/rawrtc-logo.svg b/assets/rawrtc-logo.svg
new file mode 100644
index 0000000..c793c95
--- /dev/null
+++ b/assets/rawrtc-logo.svg
@@ -0,0 +1,457 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="238.26369mm"
+   height="184.63713mm"
+   viewBox="0 0 844.24141 654.22605"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r"
+   sodipodi:docname="rawrtc-logo.svg">
+  <title
+     id="title4219">Rawr Dinosaur</title>
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="223.90945"
+     inkscape:cy="413.45413"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer2"
+     showgrid="false"
+     inkscape:snap-global="false"
+     inkscape:window-width="2511"
+     inkscape:window-height="1056"
+     inkscape:window-x="49"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     fit-margin-top="1"
+     fit-margin-left="1"
+     fit-margin-right="1"
+     fit-margin-bottom="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Rawr Dinosaur</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Tavin</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source>tavinsorigami.com</dc:source>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>dinsosaur</rdf:li>
+            <rdf:li>green</rdf:li>
+            <rdf:li>comic</rdf:li>
+            <rdf:li>thick outline</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <cc:license
+           rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-50.808614,-145.37548)">
+    <flowRoot
+       xml:space="preserve"
+       id="flowRoot4707"
+       style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"><flowRegion
+         id="flowRegion4709"><rect
+           id="rect4711"
+           width="3014.2952"
+           height="1814.234"
+           x="-3260.7725"
+           y="-963.90228" /></flowRegion><flowPara
+         id="flowPara4713" /></flowRoot>    <g
+       id="g4177"
+       transform="matrix(0.7687595,0,0,0.7687595,339.43981,217.80097)">
+      <g
+         id="g4715">
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:11.14011383;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 132.02124,739.32303 C 128.1691,704.58699 143.35476,613.5443 106.75681,628.67576 84.765796,644.49638 78.233315,683.43461 86.071666,714.4766 c -32.995139,3.64832 -45.805629,36.59695 -45.925603,31.61771 15.82005,0.20091 71.260287,-2.09149 91.875177,-6.77128 z"
+           id="path4180-6-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.62448359;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 391.78889,198.16624 30.43517,57.14277 c 40.72674,-1.40802 32.23752,-74.3271 -30.43517,-57.14277 z"
+           id="path4188"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.62448359;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 424.3909,264.84828 13.83137,48.50878 c 34.78364,2.80381 41.86818,-61.54402 -13.83137,-48.50878 z"
+           id="path4188-9"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.67187166;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 442.33419,326.79193 9.18221,39.87478 c 34.78363,2.80381 46.51734,-52.91002 -9.18221,-39.87478 z"
+           id="path4188-9-2"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.99820495;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 353.9906,148.6214 34.42008,31.90493 c 42.25192,5.4435 -5.71985,-82.770504 -34.42008,-31.90493 z"
+           id="path4188-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.90342808;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 264.219,123.89948 69.62028,12.64447 c 13.22922,1.97567 -7.32192,-30.24486 -18.02456,-32.83014 -6.56682,-1.58627 -28.65541,5.66403 -51.59572,20.18567 z"
+           id="path4188-8-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.74026108;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 197.35303,134.24499 49.69564,-9.27261 c 13.22922,1.97567 -9.02414,-16.46214 -19.68493,-19.21499 -10.54727,-2.72354 -15.17626,11.65 -30.01071,28.4876 z"
+           id="path4188-8-3-2"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.67187166;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 471.64801,553.00542 -3.49769,38.46589 c 31.83129,5.55938 44.94038,-38.19475 3.49769,-38.46589 z"
+           id="path4188-9-2-7"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.92802;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 476.9725,622.27186 48.63091,19.21116 c 15.92011,0.98001 -8.81242,-20.76224 -13.25005,-26.71122 -6.71959,-9.00794 -30.03628,-3.33806 -35.38086,7.50006 z"
+           id="path4188-9-2-7-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccsc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 540.92527,639.6125 43.46505,16.86302 c -5.26954,-6.00609 -13.81231,-30.07881 -18.67515,-29.95069 -5.28963,3.49552 -19.54639,8.25513 -24.7899,13.08767 z"
+           id="path4188-9-2-7-3-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 587.58135,657.10143 46.75249,10.28823 c -5.26954,-6.00609 -26.02264,-31.01806 -30.88557,-30.88994 -5.28953,3.49552 -10.62342,15.76917 -15.86692,20.60171 z"
+           id="path4188-9-2-7-3-8-4"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#1c8449;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:4.09118652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 653.77804,675.32257 43.46505,7.47047 c -8.96277,-22.73981 -23.56254,-32.48943 -43.46505,-7.47047 z"
+           id="path4188-9-2-7-3-8-4-7"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="opacity:1;fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 329.58835,459.81036 c -18.6718,-22.31702 -40.3629,6.24288 -56.45298,-12.61891 2.26243,22.52667 -16.95676,21.63525 -24.57371,6.30946 l -14.61139,28.89068 c 41.76943,2.36295 64.94414,-13.86874 95.63808,-22.58123 z"
+           id="path4540"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:13.76126289;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 80.068798,327.32334 c -5.927441,-46.67073 8.5719,-85.35427 31.817502,-116.09287 23.14552,-26.58373 51.22378,-54.15291 80.44266,-69.39686 72.07498,-37.0945 162.8758,-9.25794 213.10069,60.22991 39.9959,112.0935 76.4321,240.56475 16.05837,289.15983 28.49744,-9.36974 29.64817,-77.44024 81.24566,-98.62178 25.79121,31.05051 14.61009,83.17466 -50.71983,148.4023 14.30055,10.87666 26.47518,26.61243 23.48136,77.01892 l -12.21033,9.39254 30.99548,2.81777 216.96788,64.8086 c -88.36925,21.65592 -270.91929,77.01272 -282.71576,12.21032 l 4.69631,17.37621 5.63552,14.08883 -111.30175,0.46962 c 8.85472,-17.24029 4.7866,-33.76906 25.82952,-34.75243 2.77392,-100.14184 37.35653,-74.50986 48.37168,-90.16848 0,0 -29.86576,-2.45886 -46.49317,48.84126 l -6.57482,34.75243 c -9.6273,9.07968 -21.03576,7.47252 -27.23837,37.10058 l -209.92356,4.69627 c 0.94402,-57.74401 15.5303,-70.68108 5.13877,-180.68196 -3.23988,-34.29604 -2.65724,-66.07331 -5.91298,-95.68865 -5.62411,-51.15887 -16.179775,-95.86646 -30.690832,-135.96236 z"
+           id="path4180"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccccccccccccccccccccssc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 134.98378,349.72369 c 17.09386,42.68254 30.10215,85.35777 25.56996,134.82319 65.7462,7.3403 146.05953,2.0632 216.51413,-35.53223 8.50279,7.30449 12.55362,3.32124 15.49388,-3.89595 1.97418,-4.84573 3.81243,-11.69645 4.79599,-15.51899 13.17641,-51.73883 -7.63174,-121.06163 -27.59561,-145.62723 -207.88982,25.00086 -188.58231,37.25058 -233.45002,66.08328 z"
+           id="path4178"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccscccc" />
+        <path
+           style="fill:#cb1d3b;fill-opacity:1;fill-rule:evenodd;stroke:#cb1d3b;stroke-width:12.92442894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 383.71688,357.19864 -0.55017,63.53084 c -27.64293,41.74431 -77.25926,49.05797 -126.4171,54.94405 4.64573,-44.12673 -25.87247,-108.02862 126.96727,-118.47489 z"
+           id="path4186"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:2.66113067;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 147.19676,342.58479 c 41.21647,-19.255 73.61039,-41.90896 160.71556,-46.90128 -6.43423,13.05863 -2.28707,26.59302 -8.63864,34.55313 -4.48598,-3.5731 -7.5501,-7.85713 -10.16827,-12.36416 -2.81539,-4.37309 -9.09796,-6.23507 -10.7044,-3.35571 -2.22384,3.98621 0.40224,14.31797 -3.29703,15.05761 -25.92353,-30.26311 -32.2088,-0.71232 -33.00751,16.5074 -15.86599,-7.85438 -18.16124,-14.66486 -16.88488,-21.20059 0,0 -18.69133,-3.53741 -15.95879,21.11506 -6.08136,0.70284 -12.06844,1.07615 -15.54576,-7.33486 -12.70444,-16.21117 -29.49718,22.30319 -24.76851,38.3128 -21.4009,0.20162 -20.71926,-22.24053 -21.74177,-34.3894 z"
+           id="path4158"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccsccccccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1.30174112;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 116.50711,310.18439 c -2.75029,-19.4266 18.36276,-76.50875 54.82755,-25.19801 17.64966,22.81934 -22.30951,52.92111 -54.82755,25.19801 z"
+           id="path4162"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.48770416;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 141.95743,293.6027 c 31.65686,0.82328 7.35029,-24.20581 0.81806,-13.45562 -1.15065,2.15415 -0.81806,6.38953 -0.81806,13.45562 z"
+           id="path4166"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 331.36188,192.93619 c -11.4145,-15.34356 -29.89439,-21.97542 -48.5343,-15.5308 -8.40162,2.98772 -15.09629,9.19459 -20.79736,17.53275 -6.47644,9.69656 -9.01846,21.40699 -7.81854,32.76357 3.4525,24.75377 22.1203,40.06591 45.64602,40.62054 17.0198,-1.41619 33.51565,-14.49055 37.48531,-40.29377 1.15148,-13.08162 -1.26976,-26.56679 -5.98113,-35.09229 z"
+           id="path4170"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccccc" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.85963011;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 295.37692,210.74953 c 16.5309,2.21586 19.33681,-18.93227 14.09711,-24.5487 -4.8752,-3.16208 -13.49133,-1.65733 -18.25013,3.3076 -6.33771,5.97379 -5.08981,12.96092 -1.82429,16.59204 5.60669,-1.55809 7.53131,-0.1172 5.97731,4.64906 z"
+           id="path4172"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3.25435281;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 337.81377,178.45141 c -20.31543,-31.11618 -60.11235,-29.6121 -72.32269,-14.08882"
+           id="path4182"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.0131464;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 360.70749,284.76228 c 3.95627,34.33139 -2.51933,73.26533 -20.63203,83.74169 -12.80067,-28.94639 -20.49015,-57.60423 -49.45612,-70.39156 22.15648,-14.0999 46.06143,-14.21215 70.08815,-13.35013 z"
+           id="path4160"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 127.01397,356.36522 C 193.54683,300.31366 276.90782,278.97 371.75463,281.31587 l -2.65667,4.64907 c -81.77035,3.92009 -162.07551,23.31304 -242.08399,70.40028 z"
+           id="path4176"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 370.68766,283.64798 c 32.14269,95.25493 30.69311,123.82116 18.59751,161.33089 -0.053,-5.60251 -5.02007,1.5235 -5.03801,-4.01595 -0.17304,-53.23777 -5.3522,-104.67303 -22.01282,-152.61867 z"
+           id="path4418"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccscc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.0202322;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 194.12276,486.55587 c -2.82218,-12.73688 -12.82364,-23.58605 -21.20648,-33.14541 1.4931,21.96111 -10.31825,20.53747 -13.6933,31.59453 z"
+           id="path4160-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 234.22531,482.78543 c -7.00839,-3.71346 -11.55732,-7.48678 -10.50022,-10.59799 0.83888,-2.46899 -0.44817,-4.67431 -2.31607,-6.8539 -8.56602,-14.41103 -14.37494,13.79235 -14.46895,21.55375 z"
+           id="path4160-8-0"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="csccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 265.85725,477.12877 c -6.49225,-5.3572 -10.55182,-12.29534 -15.47296,-18.11605 -15.37682,8.29177 -9.67929,15.74045 -9.48774,22.54999 z"
+           id="path4160-8-0-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 159.5367,484.20622 c 83.63157,3.37122 139.74117,7.67085 220.71969,-34.87145 l -2.02476,-7.24842 c -71.96518,39.02277 -125.86637,40.92864 -218.69493,42.11987 z"
+           id="path4176-8"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5.17535067;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 375.55543,451.78624 c -11.31473,-63.1826 -33.34447,-53.10988 -33.05548,7.68795 6.21711,-4.56673 26.83837,-7.3169 33.05548,-7.68795 z"
+           id="path4160-8-0-3-5-3"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccc" />
+        <path
+           style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.92981505px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 351.17336,463.46322 c -29.98291,-17.4642 -35.09792,-1.96781 -72.39271,-17.93216 l -9.29815,38.52091 z"
+           id="path4557"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.38544869;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 294.18276,473.95711 c 1.14562,-8.3459 -8.22728,-11.29911 -13.14833,-17.11982 -15.37691,8.29176 -13.66428,12.75175 -13.47274,19.56129 6.21711,-4.56673 20.40405,-2.07042 26.62107,-2.44147 z"
+           id="path4160-8-0-3-5"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccc" />
+        <path
+           style="fill:#009143;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8.20933723;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 320.20847,726.2179 -9.85892,0.56337 -115.39135,6.59379 c -3.84777,-43.38376 -20.51238,-73.81344 -15.46934,-102.96828 12.591,-72.79123 106.49023,-89.26336 138.2158,-20.6996 10.96252,23.69162 12.90295,57.492 13.18339,95.04436 0.0437,5.87229 -16.69958,11.17763 -13.88233,20.40985 z"
+           id="path4559"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cccssscc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 220.40686,605.81246 70.44409,-14.08882"
+           id="path4561"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 258.44671,698.32909 328.8908,684.24026"
+           id="path4561-9"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5.16791201;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 190.71554,645.49304 313.02825,631.87976"
+           id="path4561-6"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 252.32304,672.61597 71.42021,-7.74682"
+           id="path4561-9-0"
+           inkscape:connector-curvature="0" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4.2771492;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 216.1802,722.74973 286.62429,708.6609"
+           id="path4561-9-4"
+           inkscape:connector-curvature="0" />
+        <path
+           style="opacity:1;fill:#8fc34b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:11.14011383;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 114.73974,526.85256 -2.42384,-12.90112 c -0.67869,-48.533 -6.56042,28.28038 -1.3129,-60.6534 2.49435,-42.27363 -36.951872,-68.84416 -82.825898,-62.04257 -7.445318,64.41212 97.349028,98.71837 86.562638,135.59709 z"
+           id="path4180-6"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccscc" />
+      </g>
+    </g>
+  </g>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Layer 2"
+     transform="translate(-28.575533,-124.65703)">
+    <g
+       id="g3648"
+       transform="matrix(0.90490866,0,0,0.90490866,37.135618,164.59176)">
+      <g
+         id="g376"
+         transform="matrix(6.490298,0,0,-6.490298,212.84938,252.95316)">
+        <path
+           d="m 0,0 c 0,-7.534 -6.107,-13.642 -13.641,-13.642 -7.535,0 -13.642,6.108 -13.642,13.642 0,7.534 6.107,13.642 13.642,13.642 C -6.107,13.642 0,7.534 0,0"
+           style="fill:#ff6600;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path378"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g404"
+         transform="matrix(6.490298,0,0,-6.490298,387.96089,128.99754)">
+        <path
+           d="m 0,0 c 0,-7.533 -6.107,-13.642 -13.641,-13.642 -7.535,0 -13.642,6.109 -13.642,13.642 0,7.534 6.107,13.643 13.642,13.643 C -6.107,13.643 0,7.534 0,0"
+           style="fill:#ffcc00;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path406"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g408"
+         transform="matrix(6.490298,0,0,-6.490298,171.53084,127.03292)">
+        <path
+           d="m 0,0 c 0,-7.533 -6.107,-13.642 -13.642,-13.642 -7.533,0 -13.641,6.109 -13.641,13.642 0,7.534 6.108,13.643 13.641,13.643 C -6.107,13.643 0,7.534 0,0"
+           style="fill:#0089cc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path410"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g412"
+         transform="matrix(6.490298,0,0,-6.490298,348.61343,252.95316)">
+        <path
+           d="m 0,0 c 0,-7.534 -6.107,-13.642 -13.643,-13.642 -7.533,0 -13.641,6.108 -13.641,13.642 0,7.534 6.108,13.642 13.641,13.642 C -6.107,13.642 0,7.534 0,0"
+           style="fill:#009939;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path414"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g416"
+         transform="matrix(6.490298,0,0,-6.490298,279.74878,48.325072)">
+        <path
+           d="m 0,0 c 0,-7.534 -6.108,-13.642 -13.642,-13.642 -7.534,0 -13.642,6.108 -13.642,13.642 0,7.534 6.108,13.642 13.642,13.642 C -6.108,13.642 0,7.534 0,0"
+           style="fill:#bf0000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path418"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g420"
+         transform="matrix(6.490298,0,0,-6.490298,210.88478,128.99754)">
+        <path
+           d="M 0,0 C 0,-0.287 0.025,-0.568 0.043,-0.851 6.094,0.545 10.61,5.955 10.61,12.43 c 0,0.287 -0.025,0.569 -0.043,0.852 C 4.516,11.885 0,6.475 0,0"
+           style="fill:#fc0007;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path422"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g424"
+         transform="matrix(6.490298,0,0,-6.490298,222.56598,172.83885)">
+        <path
+           d="m 0,0 c 2.351,-4.11 6.769,-6.887 11.843,-6.887 2.068,0 4.021,0.474 5.778,1.298 -2.35,4.11 -6.768,6.887 -11.843,6.887 C 3.71,1.298 1.757,0.824 0,0"
+           style="fill:#1cd306;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path426"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g428"
+         transform="matrix(6.490298,0,0,-6.490298,171.53084,252.95316)">
+        <path
+           d="M 0,0 C 0,-3.333 1.198,-6.383 3.184,-8.752 5.168,-6.383 6.366,-3.333 6.366,0 6.366,3.333 5.168,6.383 3.184,8.752 1.198,6.383 0,3.333 0,0"
+           style="fill:#0f7504;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path430"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g432"
+         transform="matrix(6.490298,0,0,-6.490298,47.93609,208.32327)">
+        <path
+           d="m 0,0 c 1.657,-0.716 3.481,-1.117 5.401,-1.117 5.024,0 9.401,2.723 11.769,6.766 C 15.513,6.365 13.688,6.765 11.769,6.765 6.745,6.765 2.367,4.042 0,0"
+           style="fill:#0c5e87;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path434"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g436"
+         transform="matrix(6.490298,0,0,-6.490298,103.04718,40.855409)">
+        <path
+           d="m 0,0 c -0.032,-0.38 -0.059,-0.762 -0.059,-1.151 0,-6.47 4.509,-11.875 10.553,-13.277 0.031,0.38 0.058,0.762 0.058,1.15 C 10.552,-6.808 6.044,-1.401 0,0"
+           style="fill:#6b0001;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path438"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g482"
+         transform="matrix(-6.490298,0,0,-6.490298,257.27873,241.99749)">
+        <path
+           d="M 0,0 -2.134,0 C -4.018,0 -5.55,1.527 -5.55,3.406 l 0,19.316 c 0,1.879 1.532,3.407 3.416,3.407 l 26.759,0 c 1.884,0 3.415,-1.528 3.415,-3.407 l 0,-19.316 C 28.04,1.527 26.509,0 24.625,0 L 15.509,0 -2.781,-8.966 0,0 Z"
+           style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+           id="path484"
+           inkscape:connector-curvature="0" />
+        <text
+           transform="scale(-1.0002415,-0.99975854)"
+           sodipodi:linespacing="125%"
+           id="text3636"
+           y="-8.319458"
+           x="-11.542574"
+           style="font-style:normal;font-weight:normal;font-size:6.06505919px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           xml:space="preserve"><tspan
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:15.16264725px;line-height:125%;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono, Bold';text-align:center;writing-mode:lr-tb;text-anchor:middle"
+             y="-8.319458"
+             x="-11.542574"
+             id="tspan3638"
+             sodipodi:role="line">RAWR</tspan></text>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/format-all.sh b/format-all.sh
new file mode 100755
index 0000000..6a31ecb
--- /dev/null
+++ b/format-all.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+for dir in "include" "src" "tools"; do
+    pushd "${dir}" &>/dev/null
+    find . \
+        -type f \
+        \( -name "*.c" -o -name "*.h" \) \
+        -exec clang-format -i '{}' \;
+    popd &>/dev/null
+done
diff --git a/htdocs/ortc/index.html b/htdocs/ortc/index.html
new file mode 100644
index 0000000..f5e46c6
--- /dev/null
+++ b/htdocs/ortc/index.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>ORTC/WebRTC Interop Data Channel Example</title>
+</head>
+<style>
+textarea {
+}
+</style>
+<body>
+    <textarea cols="120" rows="20" id="local-parameters" onclick="this.select();" readonly></textarea>
+    <textarea cols="120" rows="20" id="remote-parameters"></textarea><br />
+    <button type="button" onclick="start(peer);" id="start" disabled>Start</button>
+
+    <script type="text/javascript" src="sdp.js"></script>
+    <script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
+    <script type="text/javascript" src="webrtc-rawrtc.js"></script>
+    <script type="text/javascript">
+        'use strict';
+        
+        let localParameters = document.getElementById('local-parameters');
+        let remoteParameters = document.getElementById('remote-parameters');
+        let startButton = document.getElementById('start');
+        
+        function getURLParameter(name) {
+            return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
+        }
+
+        function setRemoteParameters(peer) {
+            // Parse and apply the remote parameters
+            let parameters = JSON.parse(remoteParameters.value);
+            console.log('Remote parameters:', parameters);
+            peer.setRemoteParameters(parameters)
+            .then((parameters) => {
+                // Generate local parameters if controlled
+                if (peer instanceof ControlledPeer) {
+                    getLocalParameters(peer);
+                }
+            })
+            .catch((error) => {
+                console.error(error);
+            });
+        };
+
+        function getLocalParameters(peer) {
+            // Generate and show the local parameters
+            peer.getLocalParameters()
+            .then((parameters) => {
+                console.log('Local parameters:', parameters);
+                localParameters.value = JSON.stringify(parameters);
+            });
+        }
+
+        function createPeer(controllingParameter) {
+            let controlling = controllingParameter == 'true' || controllingParameter == '1';
+            console.log('Role: ICE-Controll' + (controlling ? 'ing' : 'ed'));
+            
+            // Create peer depending on the role
+            let peer = controlling ? new ControllingPeer() : new ControlledPeer();
+            peer.createPeerConnection();
+            let cat = peer.createDataChannel(peer.pc.createDataChannel('cat-noises', {
+                ordered: true,
+                negotiated: true,
+                id: 0
+            }));
+            peer.createDataChannel();
+            
+            // Create local parameters if we are the controlling peer.
+            // Keep in mind this still uses offer/answer in the background, thus this
+            // limitation which does not exist for ORTC but does for WebRTC.
+            if (controlling) {
+                getLocalParameters(peer);
+            }
+            
+            return peer;
+        }
+        
+        function start(peer) {
+            // Apply remote parameters
+            // For the controlled peer, this will automatically generate local parameters
+            setRemoteParameters(peer);
+            startButton.disabled = true;
+        }
+        
+        // Create peer
+        // Determine role from GET parameter (?controlling=true|false)
+        let peer = createPeer(getURLParameter('controlling'));
+        startButton.disabled = false;
+    </script>
+</body>
+</html>
diff --git a/htdocs/ortc/sdp.js b/htdocs/ortc/sdp.js
new file mode 100644
index 0000000..0dd2472
--- /dev/null
+++ b/htdocs/ortc/sdp.js
@@ -0,0 +1,549 @@
+/*
+ * Copyright Philipp Hancke
+ * License: MIT
+ * Source: https://github.com/fippo/sdp
+ *
+ * Extended by Lennart Grahl
+ */
+
+/* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parts[1],
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      default: // Unknown extensions are silently ignored.
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+          (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts SCTP capabilities from SDP media section or sessionpart.
+SDPUtils.getSctpCapabilities = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var maxMessageSize = lines.filter(function(line) {
+    return line.indexOf('a=max-message-size:') === 0;
+  });
+  // TODO: Use 65536 once Firefox has disabled PPID-based fragmentation
+  //       see: https://lgrahl.de/articles/demystifying-webrtc-dc-size-limit.html
+  maxMessageSize = maxMessageSize.length ? parseInt(maxMessageSize[0].substr(19)) : 16384;
+  return {
+    maxMessageSize: maxMessageSize
+  };
+};
+
+// Serializes SCTP capabilities to SDP.
+SDPUtils.writeSctpCapabilities = function(capabilities) {
+  return 'a=max-message-size:' + capabilities.maxMessageSize + '\r\n'; // (03)
+};
+
+// Extracts SCTP port from SDP media section or sessionpart.
+SDPUtils.getSctpPort = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var port = lines.filter(function(line) {
+    return line.indexOf('a=sctp-port:') === 0;
+  });
+  port = port.length ? parseInt(port[0].substr(12), 10) : 5000;
+  return port;
+};
+
+// Serializes SCTP port to SDP.
+SDPUtils.writeSctpPort = function(port) {
+  // TODO: Enable (chromium can't cope with it)
+  // return 'a=sctp-port:' + (port ? port : 5000) + '\r\n'; // (03)
+  return '';
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var fpLine = lines.filter(function(line) {
+    return line.indexOf('a=fingerprint:') === 0;
+  })[0].substr(14);
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  var dtlsParameters = {
+    role: 'auto',
+    fingerprints: [{
+      algorithm: fpLine.split(' ')[0],
+      value: fpLine.split(' ')[1]
+    }]
+  };
+  return dtlsParameters;
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+          .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+        if (codec.preferredPayloadType !== undefined) {
+          return codec.preferredPayloadType;
+        }
+        return codec.payloadType;
+      }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(parts) {
+        return parts.attribute === 'cname';
+      });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+      .map(function(line) {
+        var parts = line.split(' ');
+        parts.shift();
+        return parts.map(function(part) {
+          return parseInt(part, 10);
+        });
+      });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          payloadType: codec.payloadType,
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(5), 10);
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+SDPUtils.writeSessionBoilerplate = function() {
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet.
+  if (transceiver.rtpSender) {
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
diff --git a/htdocs/ortc/webrtc-rawrtc.js b/htdocs/ortc/webrtc-rawrtc.js
new file mode 100644
index 0000000..bad70e0
--- /dev/null
+++ b/htdocs/ortc/webrtc-rawrtc.js
@@ -0,0 +1,396 @@
+'use strict';
+
+class Peer {
+    constructor() {
+        this.pc = null;
+        this.localMid = null;
+        this.localCandidates = [];
+        this.localParameters = null;
+        this.localDescription = null;
+        this.remoteParameters = null;
+        this.remoteDescription = null;
+        var _waitGatheringComplete = {};
+        _waitGatheringComplete.promise = new Promise((resolve, reject) => {
+            _waitGatheringComplete.resolve = resolve;
+            _waitGatheringComplete.reject = reject;
+        });
+        this._waitGatheringComplete = _waitGatheringComplete;
+        this.dc = {}
+    }
+
+    createPeerConnection() {
+        if (this.pc) {
+            console.warn('RTCPeerConnection already created');
+            return this.pc;
+        }
+
+        var self = this;
+
+        // Create peer connection
+        var pc = new RTCPeerConnection({
+            iceServers: [{
+                urls: 'stun:stun.l.google.com:19302'
+            }]
+        });
+
+        // Bind peer connection events
+        pc.onnegotiationneeded = function(event) {
+            console.log('Negotiation needed')
+        };
+        pc.onicecandidate = function(event) {
+            if (event.candidate) {
+                console.log('Gathered candidate:', event.candidate);
+                self.localCandidates.push(event.candidate);
+            } else {
+                console.log('Gathering complete');
+                self._waitGatheringComplete.resolve();
+            }
+        };
+        pc.onicecandidateerror = function(event) {
+            console.error('ICE candidate error:', event.errorText);
+        };
+        pc.onsignalingstatechange = function(event) {
+            console.log('Signaling state changed to:', pc.signalingState);
+        };
+        pc.oniceconnectionstatechange = function(event) {
+            console.log('ICE connection state changed to:', pc.iceConnectionState);
+        };
+        pc.onicegatheringstatechange = function(event) {
+            console.log('ICE gathering state changed to:', pc.iceGatheringState);
+        };
+        pc.onconnectionstatechange = function(event) {
+            console.log('Connection state changed to:', pc.connectionState);
+        };
+        pc.ondatachannel = function(event) {
+            self.createDataChannel(event.channel);
+        };
+
+        this.pc = pc;
+        return pc;
+    }
+
+    createDataChannel(dc) {
+        // Create data channel
+        dc = (typeof dc !== 'undefined') ? dc : this.pc.createDataChannel('example-channel', {
+            ordered: true
+        });
+
+        // Bind data channel events
+        dc.onopen = function(event) {
+            console.log('Data channel', dc.label, '(', dc.id, ')', 'open');
+            // Send 'hello'
+            dc.send('Hello from WebRTC on', navigator.userAgent);
+        };
+        dc.onbufferedamountlow = function(event) {
+            console.log('Data channel', dc.label, '(', dc.id, ')', 'buffered amount low');
+        };
+        dc.onerror = function(event) {
+            console.error('Data channel', dc.label, '(', dc.id, ')', 'error:', event);
+        };
+        dc.onclose = function(event) {
+            console.log('Data channel', dc.label, '(', dc.id, ')', 'closed');
+        };
+        dc.onmessage = function(event) {
+            var length = event.data.size || event.data.byteLength || event.data.length;
+            console.info('Data channel', dc.label, '(', dc.id, ')', 'message size:', length);
+        };
+
+        // Store channel
+        this.dc[dc.label] = dc;
+
+        return dc;
+    }
+
+    getLocalParameters() {
+        return new Promise((resolve, reject) => {
+            var error;
+            var self = this;
+
+            if (!this.localDescription) {
+                error = 'Must create offer/answer';
+                console.error(error);
+                reject(error);
+                return;
+            }
+
+            // Initialise parameters
+            var parameters = {
+                iceParameters: null,
+                iceCandidates: [],
+                dtlsParameters: null,
+                sctpParameters: null,
+            };
+
+            // Split sections
+            var sections = SDPUtils.splitSections(this.localDescription.sdp);
+            var session = sections.shift();
+
+            // Go through media sections
+            sections.forEach(function(mediaSection, sdpMLineIndex) {
+                // TODO: Ignore anything else but data transports
+
+                // Get mid
+                // TODO: This breaks with multiple transceivers
+                if (!self.localMid) {
+                    var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:');
+                    if (mid.length > 0) {
+                        self.localMid = mid[0].substr(6);
+                    }
+                }
+
+                // Get ICE parameters
+                if (!parameters.iceParameters) {
+                    parameters.iceParameters = SDPUtils.getIceParameters(mediaSection, session);
+                }
+
+                // Get DTLS parameters
+                if (!parameters.dtlsParameters) {
+                    parameters.dtlsParameters = SDPUtils.getDtlsParameters(mediaSection, session);
+                }
+
+                // Get SCTP parameters
+                if (!parameters.sctpParameters) {
+                    parameters.sctpParameters = SDPUtils.getSctpCapabilities(mediaSection, session);
+                    parameters.sctpParameters.port = SDPUtils.getSctpPort(mediaSection, session);
+                }
+            });
+
+            // ICE lite parameter
+            if (!parameters.iceParameters
+                    || !parameters.dtlsParameters
+                    || !parameters.sctpParameters) {
+                error = 'Could not retrieve required parameters from local description';
+                console.error(error);
+                reject(error);
+                return;
+            }
+            parameters.iceParameters.iceLite =
+                SDPUtils.matchPrefix(session, 'a=ice-lite').length > 0;
+
+            // Get ICE candidates
+            this._waitGatheringComplete.promise.then(() => {
+                // Add ICE candidates
+                for (var sdpCandidate of self.localCandidates) {
+                    var candidate = SDPUtils.parseCandidate(sdpCandidate.candidate);
+                    parameters.iceCandidates.push(candidate);
+                }
+
+                // Add ICE candidate complete sentinel
+                // parameters.iceCandidates.push({complete: true}); // TODO
+
+                // Done
+                resolve(parameters);
+            });
+        });
+    }
+
+    setRemoteParameters(parameters, type, localMid = null) {
+        return new Promise((resolve, reject) => {
+            if (this.remoteDescription) {
+                resolve(this.remoteDescription);
+                return;
+            }
+
+            if (!this.pc) {
+                console.error('Must create RTCPeerConnection instance');
+                return;
+            }
+
+            if (!localMid) {
+                localMid = this.localMid;
+            }
+            this.remoteParameters = parameters;
+
+            // Translate DTLS role
+            // TODO: This somehow didn't make it into SDPUtils
+            var setupType;
+            switch (parameters.dtlsParameters.role) {
+                case 'client':
+                    setupType = 'active';
+                    break;
+                case 'server':
+                    setupType = 'passive';
+                    break;
+                default:
+                    // We map 'offer' to 'controlling' and 'answer' to 'controlled',
+                    // so rawrtc will take 'server' if offering and 'client' if answering
+                    // as specified by the ORTC spec
+                    switch (type) {
+                        case 'offer':
+                            // WebRTC requires actpass in offer
+                            setupType = 'actpass';
+                            break;
+                        case 'answer':
+                            setupType = 'active';
+                            break;
+                    }
+                    break;
+            }
+
+            // Write session section
+            var sdp = SDPUtils.writeSessionBoilerplate();
+            sdp += 'a=group:BUNDLE ' + localMid + '\r\n';
+            sdp += 'a=ice-options:trickle\r\n';
+            if (parameters.iceParameters.iceLite) {
+                sdp += 'a=ice-lite\r\n';
+            }
+
+            // Write media section
+            // TODO: Replace
+            // sdp += 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n'; // (03)
+            sdp += 'm=application 9 DTLS/SCTP ' + parameters.sctpParameters.port + '\r\n'; // (01)
+            sdp += 'c=IN IP4 0.0.0.0\r\n';
+            sdp += 'a=mid:' + localMid + '\r\n';
+            sdp += 'a=sendrecv\r\n';
+
+            // SCTP part
+            sdp += SDPUtils.writeSctpCapabilities(parameters.sctpParameters);
+            sdp += SDPUtils.writeSctpPort(parameters.sctpParameters.port);
+            sdp += 'a=sctpmap:' + parameters.sctpParameters.port + ' webrtc-datachannel 65535\r\n'; // (01)
+
+            // DTLS part
+            sdp += SDPUtils.writeDtlsParameters(parameters.dtlsParameters, setupType);
+
+            // ICE part
+            sdp += 'a=connection:new\r\n'; // (03)
+            sdp += SDPUtils.writeIceParameters(parameters.iceParameters);
+
+            // Done
+            console.log('Remote description:\n' + sdp);
+
+            // Set remote description
+            this.pc.setRemoteDescription({type: type, sdp: sdp})
+            .then(() => {
+                console.log('Remote description:\n' + this.pc.remoteDescription.sdp);
+                this.remoteDescription = this.pc.remoteDescription;
+
+                // Add ICE candidates
+                for (var iceCandidate of parameters.iceCandidates) {
+                    // Add component which ORTC doesn't have
+                    // Note: We choose RTP as it doesn't actually matter for us
+                    iceCandidate.component = 1; // RTP
+
+                    // Create
+                    var candidate = new RTCIceCandidate({
+                        candidate: SDPUtils.writeCandidate(iceCandidate),
+                        sdpMLineIndex: 0, // TODO: Fix
+                        sdpMid: localMid // TODO: Fix
+                    });
+
+                    // Add
+                    console.log(candidate.candidate);
+                    this.pc.addIceCandidate(candidate)
+                    .then(() => {
+                        console.log('Added remote candidate', candidate);
+                    });
+                }
+
+                // It's trickle ICE, no need to wait for candidates to be added
+                resolve();
+            })
+            .catch((error) => {
+                reject(error);
+            });
+        });
+    }
+
+    start() {}
+}
+
+class ControllingPeer extends Peer {
+    getLocalParameters() {
+        return new Promise((resolve, reject) => {
+            if (!this.pc) {
+                var error = 'Must create RTCPeerConnection instance';
+                console.error(error);
+                reject(error);
+                return;
+            }
+
+            var getLocalParameters = () => {
+                // Return parameters
+                super.getLocalParameters()
+                .then((parameters) => {
+                    this.localParameters = parameters;
+                    resolve(parameters);
+                })
+                .catch((error) => {
+                    reject(error);
+                });
+            };
+
+            // Create offer
+            if (!this.localDescription) {
+                this.pc.createOffer()
+                .then((description) => {
+                    return this.pc.setLocalDescription(description);
+                })
+                .then(() => {
+                    console.log('Local description:\n' + this.pc.localDescription.sdp);
+                    this.localDescription = this.pc.localDescription;
+                    getLocalParameters();
+                })
+                .catch((error) => {
+                    reject(error);
+                });
+            } else {
+                getLocalParameters();
+            }
+        });
+    }
+
+    setRemoteParameters(parameters, localMid = null) {
+        return super.setRemoteParameters(parameters, 'answer', localMid);
+    }
+}
+
+class ControlledPeer extends Peer {
+    getLocalParameters() {
+        return new Promise((resolve, reject) => {
+            var error;
+
+            if (!this.pc) {
+                error = 'Must create RTCPeerConnection instance';
+                console.error(error);
+                reject(error);
+                return;
+            }
+            if (!this.remoteDescription) {
+                error = 'Must have remote description';
+                console.error(error);
+                reject(error);
+                return;
+            }
+
+            var getLocalParameters = () => {
+                // Return parameters
+                super.getLocalParameters()
+                .then((parameters) => {
+                    resolve(parameters);
+                })
+                .catch((error) => {
+                    reject(error);
+                });
+            };
+
+            // Create answer
+            if (!this.localDescription) {
+                this.pc.createAnswer()
+                .then((description) => {
+                    return this.pc.setLocalDescription(description);
+                })
+                .then(() => {
+                    console.log('Local description:\n' + this.pc.localDescription.sdp);
+                    this.localDescription = this.pc.localDescription;
+                    getLocalParameters();
+                });
+            } else {
+                getLocalParameters();
+            }
+        });
+    }
+
+    setRemoteParameters(parameters, localMid = null) {
+        return super.setRemoteParameters(parameters, 'offer', localMid);
+    }
+}
diff --git a/htdocs/webrtc/index.html b/htdocs/webrtc/index.html
new file mode 100644
index 0000000..f8b27b9
--- /dev/null
+++ b/htdocs/webrtc/index.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<html>
+<head lang="en">
+    <meta charset="UTF-8">
+    <title>WebRTC Data Channel Example</title>
+    <style>
+        @keyframes new-fade {
+            0% {
+                background-color: #fffb85;
+            }
+            100% {
+                background-color: transparent;
+            }
+        }
+
+        html, body {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+        }
+        body {
+            height: 100vh;
+            display: flex;
+        }
+
+        main {
+            width: 100%;
+            display: flex;
+            flex-grow: 1;
+            flex-wrap: nowrap;
+        }
+
+        section, aside {
+            padding: 5px;
+            flex-grow: 1;
+            flex-basis: 0;
+        }
+        aside {
+            overflow-y: auto;
+        }
+
+        section textarea {
+            width: 100%;
+        }
+
+        #log {
+            margin: 0;
+            padding: 0;
+            flex-grow: 1;
+            flex-basis: 0;
+        }
+        #log > * {
+            margin: 2px 0;
+            padding: 0 6px;
+            animation: new-fade 1.5s ease-out 1;
+        }
+        #log .debug, #log .log {
+            border-left: 2px solid lightgrey;
+        }
+        #log .error {
+            border-left: 2px solid red;
+        }
+        #log .info {
+            border-left: 2px solid cornflowerblue;
+        }
+        #log .warn {
+            border-left: 2px solid orange;
+        }
+    </style>
+</head>
+<body>
+
+<main>
+    <section>
+        <form autocomplete="off">
+            <!-- Role -->
+            <label for="role">Role: Offering</label>
+            <input type="checkbox" id="role" checked>
+            <br>
+
+            <!-- Message size to be used -->
+            <label for="message-size">Message size (bytes):</label>
+            <input id="message-size" type="number" name="message-size" value="16384" step="16384">
+            <br>
+
+            <!-- WebSocket URL & Start button -->
+            <input type="text" id="ws-url" placeholder="Optional Server URL">
+            <button type="button" id="start" disabled>Start</button>
+            <br><hr>
+
+            <!-- Copy local description from this textarea -->
+            Copy local description:<br>
+            <textarea rows="15" id="local-description" disabled readonly></textarea>
+
+            <!-- Paste remote description into this textarea -->
+            Paste remote description:<br>
+            <textarea rows="15" id="remote-description" disabled></textarea>
+            <br>
+        </form>
+    </section>
+    <aside>
+        <pre id="log"></pre>
+    </aside>
+</main>
+
+<!-- Import dependencies -->
+<script src="https://webrtc.github.io/adapter/adapter-6.1.1.js"></script>
+<script src="signaling.js"></script>
+<script src="peerconnection.js"></script>
+
+<!-- UI code -->
+<script>
+'use strict';
+
+// Get elements
+const roleCheckbox = document.getElementById('role');
+const messageSizeInput = document.getElementById('message-size');
+const localDescriptionTextarea = document.getElementById('local-description');
+const remoteDescriptionTextarea = document.getElementById('remote-description');
+const wsUrlInput = document.getElementById('ws-url');
+const startButton = document.getElementById('start');
+const logPre = document.getElementById('log');
+
+// Load & store persistent values
+if (typeof(Storage) !== 'undefined') {
+    const persistentElements = [
+        ['role', 'checked', (value) => value === 'true'],
+        ['message-size', 'value'],
+        ['ws-url', 'value']
+    ];
+    for (const [id, property, transform] of persistentElements) {
+        let value = localStorage.getItem(id);
+        const element = document.getElementById(id);
+        if (transform !== undefined) {
+            value = transform(value);
+        }
+        if (value !== null) {
+            element[property] = value;
+        }
+        element.addEventListener('change', () => {
+            localStorage.setItem(id, element[property]);
+        });
+    }
+}
+
+// Display console logs in the browser as well
+for (const name of ['debug', 'error', 'info', 'log', 'warn']) {
+    const method = window.console[name];
+    window.console[name] = function() {
+        method.apply(null, arguments);
+        const entry = document.createElement('div');
+        entry.classList.add(name);
+        for (let i = 0; i < arguments.length; ++i) {
+            let item = arguments[i];
+            if (typeof arguments[i] === 'object') {
+                entry.innerHTML += JSON.stringify(item, null, 2) + ' ';
+            } else {
+                entry.innerHTML += item + ' ';
+            }
+        }
+        logPre.prepend(entry);
+    };
+}
+
+// Auto-select all text when clicking local description
+localDescriptionTextarea.addEventListener('click', function() {
+    this.select();
+});
+
+// Bind start button & enable
+startButton.addEventListener('click', () => {
+    roleCheckbox.disabled = true;
+    messageSizeInput.disabled = true;
+    wsUrlInput.disabled = true;
+    startButton.disabled = true;
+    localDescriptionTextarea.disabled = false;
+    start(roleCheckbox.checked);
+});
+startButton.disabled = false;
+
+const start = (offering) => {
+    console.info('Starting with role:', offering ? 'Offering' : 'Answering');
+
+    // Create signaling instance
+    const wsUrl = wsUrlInput.value;
+    let signaling;
+    if (wsUrl === '') {
+        signaling = new CopyPasteSignaling();
+    } else {
+        signaling = new WebSocketSignaling(wsUrl + (offering ? '/1' : '/0'));
+    }
+    signaling.onLocalDescriptionUpdate = (description) => {
+        localDescriptionTextarea.value = JSON.stringify(description);
+
+        // Enable remote description once local description has been set
+        remoteDescriptionTextarea.disabled = false;
+    };
+    signaling.onRemoteDescriptionUpdate = (description) => {
+        remoteDescriptionTextarea.value = JSON.stringify(description);
+    };
+
+    // Create peer connection instance
+    const pc = new WebRTCPeerConnection(signaling, offering);
+    window.pc = pc;
+
+    // Apply remote description when pasting
+    const onRemoteDescriptionTextareaChange = () => {
+        // Remove event listener
+        remoteDescriptionTextarea.oninput = null;
+        remoteDescriptionTextarea.onchange = null;
+
+        // Apply remote description once (needs to include candidates)
+        const description = JSON.parse(remoteDescriptionTextarea.value);
+        signaling.handleRemoteDescription(description, true)
+            .catch((error) => console.error(error));
+
+        // Make read-only
+        remoteDescriptionTextarea.readOnly = true;
+    };
+    remoteDescriptionTextarea.oninput = onRemoteDescriptionTextareaChange;
+    remoteDescriptionTextarea.onchange = onRemoteDescriptionTextareaChange;
+
+    // Enable remote description early (if not offering)
+    if (!offering) {
+        remoteDescriptionTextarea.disabled = false;
+    }
+
+    // Get message size
+    const messageSize = parseInt(messageSizeInput.value);
+    if (isNaN(messageSize)) {
+        throw 'Invalid message size value';
+    }
+
+    // Create data channels
+    const createDataChannelWithName = (
+        name, options = null, createOnOpenWithName = null, createOnOpenOptions = null
+    ) => {
+        const dc = pc.createDataChannel(name, options);
+        const defaultOnOpenHandler = dc.onopen;
+        dc.onopen = (event) => {
+            defaultOnOpenHandler(event);
+            if (createOnOpenWithName !== null) {
+                window.setTimeout(() => {
+                    createDataChannelWithName(createOnOpenWithName, createOnOpenOptions);
+                }, 1000);
+            }
+            if (messageSize > pc.pc.sctp.maxMessageSize) {
+                console.warn(dc._name, 'message size (' + messageSize + ') > maximum message size' +
+                    ' (' + pc.pc.sctp.maxMessageSize + ')');
+            }
+            let data = new Uint8Array(messageSize);
+            console.log(dc._name, 'outgoing message (' + data.byteLength + ' bytes)');
+            try {
+                dc.send(data);
+            } catch (error) {
+                if (error.name === 'TypeError') {
+                    console.error(dc._name, 'message too large to send');
+                } else {
+                    console.error(dc._name, 'Unknown error:', error.name);
+                }
+            }
+        };
+    };
+    createDataChannelWithName('cat-noises', {
+        negotiated: true,
+        id: 0,
+    }, 'dinosaur-noises');
+};
+
+// Introduction
+console.info("Hello! Press 'Start' when you're ready.");
+</script>
+
+</body>
+</html>
diff --git a/htdocs/webrtc/peerconnection.js b/htdocs/webrtc/peerconnection.js
new file mode 100644
index 0000000..9161966
--- /dev/null
+++ b/htdocs/webrtc/peerconnection.js
@@ -0,0 +1,95 @@
+'use strict';
+
+/**
+ * A WebRTC peer connection helper. Tightly coupled with the signaling
+ * class.
+ */
+class WebRTCPeerConnection {
+    constructor(signaling, offering, configuration = null) {
+        // Set default configuration (if none provided)
+        if (configuration === null) {
+            configuration = {
+                iceServers: [{
+                    urls: 'stun:stun.services.mozilla.com',
+                }],
+            };
+        }
+
+        // Create peer connection and bind events
+        const pc = new RTCPeerConnection(configuration);
+        pc._offering = offering; // Meh!
+        signaling.pc = pc;
+        pc.onnegotiationneeded = async () => {
+            console.log('Negotiation needed');
+
+            // Create offer (if required)
+            if (offering) {
+                console.log('Creating offer');
+                const description = await pc.createOffer();
+                await pc.setLocalDescription(description);
+                signaling.handleLocalDescription(description);
+            }
+        };
+        pc.signalingstatechange = () => {
+            console.log('Signaling state:', pc.signalingState);
+        };
+        pc.oniceconnectionstatechange = () => {
+            console.log('ICE connection state:', pc.iceConnectionState);
+        };
+        pc.onicegatheringstatechange = () => {
+            console.log('ICE gathering state:', pc.iceGatheringState);
+        };
+        pc.onconnectionstatechange = () => {
+            console.log('Connection state:', pc.connectionState);
+        };
+        pc.onicecandidate = (event) => {
+            signaling.handleLocalCandidate(event.candidate);
+        };
+        pc.onicecandidateerror = (event) => {
+            console.error('ICE candidate error:', event);
+        };
+        pc.ondatachannel = (event) => {
+            const dc = event.channel;
+            console.log('Incoming data channel:', dc.label);
+
+            // Bind events
+            this.bindDataChannelEvents(dc);
+        };
+
+        // Store configuration & signalling instance
+        this.pc = pc;
+        this.dcs = {};
+    }
+
+    createDataChannel(name, options = null) {
+        const pc = this.pc;
+
+        // Create data channel and bind events
+        const dc = pc.createDataChannel(name, options);
+        this.bindDataChannelEvents(dc);
+
+        // Store data channel and return
+        this.dcs[name] = dc;
+        return dc;
+    }
+
+    bindDataChannelEvents(dc) {
+        dc._name = dc.label; // Meh!
+        dc.onopen = () => {
+            console.log(dc._name, 'open');
+        };
+        dc.onclose = () => {
+            console.log(dc._name, 'closed');
+        };
+        dc.onerror = (event) => {
+            console.log(dc._name, 'error:', event);
+        };
+        dc.onbufferedamountlow = () => {
+            console.log(dc._name, 'buffered amount low:', dc.bufferedAmount);
+        };
+        dc.onmessage = (event) => {
+            const size = event.data.byteLength || event.data.size;
+            console.log(dc._name, 'incoming message (' + size + ' bytes)');
+        };
+    }
+}
diff --git a/htdocs/webrtc/signaling.js b/htdocs/webrtc/signaling.js
new file mode 100644
index 0000000..ff3e3b0
--- /dev/null
+++ b/htdocs/webrtc/signaling.js
@@ -0,0 +1,211 @@
+'use strict';
+
+/**
+ * A copy & paste signalling implementation.
+ *
+ * Tightly coupled with the WebRTC peer connection class.
+ */
+class CopyPasteSignaling {
+    constructor(pc = null) {
+        this._pc = pc;
+        this.pending_inbound_messages = [];
+        this.localIceCandidatesSent = false;
+        this.remoteIceCandidatesReceived = false;
+        this._onLocalDescriptionUpdate = null;
+        this._onRemoteDescriptionUpdate = null;
+    }
+
+    set pc(pc) {
+        this._pc = pc;
+
+        // Process all pending inbound messages
+        for (const message of this.pending_inbound_messages) {
+            this.receiveMessage(message.type, message.value);
+        }
+    }
+
+    set onLocalDescriptionUpdate(callback) {
+        this._onLocalDescriptionUpdate = callback;
+    }
+
+    set onRemoteDescriptionUpdate(callback) {
+        this._onRemoteDescriptionUpdate = callback;
+    }
+
+    handleLocalDescription(description, complete = false) {
+        console.log('Local description:', description);
+
+        // Send local description
+        this.sendMessage('description', description);
+        if (complete) {
+            this.localIceCandidatesSent = true;
+            this.maybeClose();
+            console.info('Local description complete');
+        }
+
+        // Call 'update'
+        if (this._onLocalDescriptionUpdate !== null) {
+            this._onLocalDescriptionUpdate(this._pc.localDescription);
+        }
+    }
+
+    async handleRemoteDescription(description, complete = false) {
+        // Set remote description
+        console.log('Setting remote description');
+        await this._pc.setRemoteDescription(description);
+        console.log('Remote description:', this._pc.remoteDescription);
+        if (complete) {
+            this.remoteIceCandidatesReceived = true;
+            this.maybeClose();
+            console.info('Remote description complete');
+        }
+
+        // Call 'update' (remote description)
+        if (this._onRemoteDescriptionUpdate !== null) {
+            this._onRemoteDescriptionUpdate(this._pc.remoteDescription);
+        }
+
+        // Create answer (if required)
+        if (!this._pc._offering) {
+            console.log(name, 'Creating answer');
+            description = await this._pc.createAnswer();
+
+            // Apply local description
+            await this._pc.setLocalDescription(description);
+            this.handleLocalDescription(description);
+        }
+    }
+
+    handleLocalCandidate(candidate) {
+        console.log('Local ICE candidate:', candidate);
+
+        // Send local candidate
+        this.sendMessage('candidate', candidate);
+
+        // Special handling for last candidate
+        if (candidate === null) {
+            this.localIceCandidatesSent = true;
+            this.maybeClose();
+            console.info('Local description complete');
+        }
+
+        // Call 'update' (local description)
+        if (this._onLocalDescriptionUpdate !== null) {
+            this._onLocalDescriptionUpdate(this._pc.localDescription);
+        }
+    }
+
+    async handleRemoteCandidate(candidate) {
+        console.log('Remote ICE candidate:', candidate);
+        if (candidate !== null) {
+            // Add remote candidate (if any)
+            await this._pc.addIceCandidate(candidate);
+        } else {
+            // Special handling for last candidate
+            this.remoteIceCandidatesReceived = true;
+            this.maybeClose();
+            console.info('Remote description complete');
+        }
+
+        // Call 'update' (remote description)
+        if (this._onRemoteDescriptionUpdate !== null) {
+            this._onRemoteDescriptionUpdate(this._pc.remoteDescription);
+        }
+    }
+
+    sendMessage(type, value) {
+        // Does nothing by default
+    }
+
+    receiveMessage(type, value) {
+        // Hold back messages until peer connection is set
+        if (this._pc === null) {
+            this.pending_inbound_messages.push({type: type, value: value});
+        }
+
+        // Handle message
+        switch (type) {
+            case 'description':
+                this.handleRemoteDescription(value).catch((error) => console.error(error));
+                break;
+            case 'candidate':
+                this.handleRemoteCandidate(value).catch((error) => console.error(error));
+                break;
+            default:
+                console.warn('Unknown message type:', type);
+                break;
+        }
+    }
+
+    maybeClose() {
+        // Close once all messages have been exchanged
+        if (this.localIceCandidatesSent && this.remoteIceCandidatesReceived) {
+            console.log('Closing signalling channel');
+            this.close();
+        }
+    }
+
+    close() {
+        // Does nothing by default
+    }
+}
+
+/**
+ * A signalling implementation intended for this signalling server:
+ * https://github.com/rawrtc/rawrtc-terminal-demo/tree/master/signaling
+ *
+ * Tightly coupled with the WebRTC peer connection class.
+ *
+ * Example: `ws://localhost/meow/0` when offering, and
+ *          `ws://localhost/meow/1` when answering.
+ */
+class WebSocketSignaling extends CopyPasteSignaling {
+    constructor(wsUrl, pc = null) {
+        super(pc);
+        this.pending_outbound_messages = [];
+
+        const ws = new WebSocket(wsUrl);
+        ws.onopen = () => {
+            console.log('WS open');
+            for (const message of this.pending_outbound_messages) {
+                this.sendMessage(message.type, message.value);
+            }
+        };
+        ws.onclose = () => {
+            console.log('WS closed');
+        };
+        ws.onerror = (event) => {
+            console.error('WS error:', event);
+        };
+        ws.onmessage = (event) => {
+            const message = JSON.parse(event.data);
+            if (!('type' in message)) {
+                console.warn("Invalid message, did not contain a 'type' field");
+                return;
+            }
+            this.receiveMessage(message.type, message.value || null);
+        };
+
+        // Store web socket instance
+        this.ws = ws;
+    }
+
+    sendMessage(type, value) {
+        // Cache if not open, yet.
+        if (this.ws.readyState !== 1) {
+            this.pending_outbound_messages.push({type: type, value: value});
+            return;
+        }
+
+        // Send
+        this.ws.send(JSON.stringify({
+            type: type,
+            value: value
+        }));
+    }
+
+    close() {
+        super.close();
+        this.ws.close();
+    }
+}
diff --git a/include/meson.build b/include/meson.build
new file mode 100644
index 0000000..6b63c7f
--- /dev/null
+++ b/include/meson.build
@@ -0,0 +1,3 @@
+# Install headers
+install_headers(files('rawrtc.h'))
+subdir('rawrtc')
diff --git a/include/rawrtc.h b/include/rawrtc.h
new file mode 100644
index 0000000..7801c6f
--- /dev/null
+++ b/include/rawrtc.h
@@ -0,0 +1,24 @@
+#pragma once
+#include "rawrtc/config.h"
+
+#include "rawrtc/certificate.h"
+#include "rawrtc/dtls_fingerprint.h"
+#include "rawrtc/dtls_parameters.h"
+#include "rawrtc/dtls_transport.h"
+#include "rawrtc/ice_candidate.h"
+#include "rawrtc/ice_gather_options.h"
+#include "rawrtc/ice_gatherer.h"
+#include "rawrtc/ice_parameters.h"
+#include "rawrtc/ice_server.h"
+#include "rawrtc/ice_transport.h"
+#include "rawrtc/main.h"
+#include "rawrtc/peer_connection.h"
+#include "rawrtc/peer_connection_configuration.h"
+#include "rawrtc/peer_connection_description.h"
+#include "rawrtc/peer_connection_ice_candidate.h"
+#include "rawrtc/peer_connection_state.h"
+#if RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
+#    include "rawrtc/sctp_redirect_transport.h"
+#endif
+#include "rawrtc/sctp_transport.h"
+#include "rawrtc/utils.h"
diff --git a/include/rawrtc/certificate.h b/include/rawrtc/certificate.h
new file mode 100644
index 0000000..b81d461
--- /dev/null
+++ b/include/rawrtc/certificate.h
@@ -0,0 +1,121 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Certificate private key types.
+ */
+enum rawrtc_certificate_key_type {
+    // An RSA private key.
+    RAWRTC_CERTIFICATE_KEY_TYPE_RSA = TLS_KEYTYPE_RSA,
+    // An elliptic curve private key.
+    RAWRTC_CERTIFICATE_KEY_TYPE_EC = TLS_KEYTYPE_EC,
+};
+
+/*
+ * Certificate signing hash algorithms.
+ */
+enum rawrtc_certificate_sign_algorithm {
+    // Sign algorithm not set.
+    // Note: When passing this as an argument, a sensible default signing
+    //       algorithm shall be used.
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE = 0,
+    // SHA-256 sign algorithm.
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256 = TLS_FINGERPRINT_SHA256,
+    // SHA-384 sign algorithm.
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384,
+    // SHA-512 sign algorithm.
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512,
+};
+
+/*
+ * Certificate encoding.
+ */
+enum rawrtc_certificate_encode {
+    // Only encode the certificate.
+    RAWRTC_CERTIFICATE_ENCODE_CERTIFICATE,
+    // Only encode the private key.
+    RAWRTC_CERTIFICATE_ENCODE_PRIVATE_KEY,
+    // Encode both the certificate and the private key.
+    RAWRTC_CERTIFICATE_ENCODE_BOTH,
+};
+
+/*
+ * Certificate options.
+ */
+struct rawrtc_certificate_options;
+
+/*
+ * Certificate.
+ */
+struct rawrtc_certificate;
+
+/*
+ * Certificates.
+ * Note: Inherits `struct rawrtc_array_container`.
+ */
+struct rawrtc_certificates {
+    size_t n_certificates;
+    struct rawrtc_certificate* certificates[];
+};
+
+/*
+ * Create certificate options.
+ *
+ * All arguments but `key_type` are optional. Sane and safe default
+ * values will be applied, don't worry!
+ *
+ * `*optionsp` must be unreferenced.
+ *
+ * If `common_name` is `NULL` the default common name will be applied.
+ * If `valid_until` is `0` the default certificate lifetime will be
+ * applied.
+ * If the key type is `ECC` and `named_curve` is `NULL`, the default
+ * named curve will be used.
+ * If the key type is `RSA` and `modulus_length` is `0`, the default
+ * amount of bits will be used. The same applies to the
+ * `sign_algorithm` if it has been set to `NONE`.
+ */
+enum rawrtc_code rawrtc_certificate_options_create(
+    struct rawrtc_certificate_options** const optionsp,  // de-referenced
+    enum rawrtc_certificate_key_type const key_type,
+    char* common_name,  // nullable, copied
+    uint_fast32_t valid_until,
+    enum rawrtc_certificate_sign_algorithm sign_algorithm,
+    char* named_curve,  // nullable, copied, ignored for RSA
+    uint_fast32_t modulus_length  // ignored for ECC
+);
+
+/*
+ * Create and generate a self-signed certificate.
+ *
+ * Sane and safe default options will be applied if `options` is
+ * `NULL`.
+ *
+ * `*certificatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_certificate_generate(
+    struct rawrtc_certificate** const certificatep,
+    struct rawrtc_certificate_options* options  // nullable
+);
+
+/*
+ * TODO http://draft.ortc.org/#dom-rtccertificate
+ * rawrtc_certificate_from_bytes
+ * rawrtc_certificate_get_expires
+ * rawrtc_certificate_get_fingerprint
+ * rawrtc_certificate_get_algorithm
+ */
+
+/*
+ * Translate a certificate sign algorithm to str.
+ */
+char const* rawrtc_certificate_sign_algorithm_to_str(
+    enum rawrtc_certificate_sign_algorithm const algorithm);
+
+/*
+ * Translate a str to a certificate sign algorithm (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/config.h.in b/include/rawrtc/config.h.in
new file mode 100644
index 0000000..cfb50f6
--- /dev/null
+++ b/include/rawrtc/config.h.in
@@ -0,0 +1,15 @@
+#pragma once
+
+/// Current version of the library.
+///
+/// Follows [Semantic Versioning 2.0.0](https://semver.org)
+#mesondefine RAWRTC_VERSION
+#mesondefine RAWRTC_VERSION_MAJOR
+#mesondefine RAWRTC_VERSION_MINOR
+#mesondefine RAWRTC_VERSION_PATCH
+
+/// Debug level
+#mesondefine RAWRTC_DEBUG_LEVEL
+
+/// Whether the SCTP redirect transport has been compiled
+#mesondefine RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
diff --git a/include/rawrtc/dtls_fingerprint.h b/include/rawrtc/dtls_fingerprint.h
new file mode 100644
index 0000000..437bcbe
--- /dev/null
+++ b/include/rawrtc/dtls_fingerprint.h
@@ -0,0 +1,43 @@
+#pragma once
+#include "certificate.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * DTLS fingerprint.
+ */
+struct rawrtc_dtls_fingerprint;
+
+/*
+ * DTLS fingerprints.
+ * Note: Inherits `struct rawrtc_array_container`.
+ */
+struct rawrtc_dtls_fingerprints {
+    size_t n_fingerprints;
+    struct rawrtc_dtls_fingerprint* fingerprints[];
+};
+
+/*
+ * Create a new DTLS fingerprint instance.
+ * `*fingerprintp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_create(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm,
+    char* const value  // copied
+);
+
+/*
+ * Get the DTLS certificate fingerprint's sign algorithm.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const sign_algorithmp,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint);
+
+/*
+ * Get the DTLS certificate's fingerprint value.
+ * `*valuep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_value(
+    char** const valuep,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint);
diff --git a/include/rawrtc/dtls_parameters.h b/include/rawrtc/dtls_parameters.h
new file mode 100644
index 0000000..ba8df3f
--- /dev/null
+++ b/include/rawrtc/dtls_parameters.h
@@ -0,0 +1,38 @@
+#pragma once
+#include "dtls_transport.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_dtls_fingerprint;
+struct rawrtc_dtls_fingerprints;
+
+/*
+ * DTLS parameters.
+ */
+struct rawrtc_dtls_parameters;
+
+/*
+ * Create a new DTLS parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_create(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct rawrtc_dtls_fingerprint* const fingerprints[],  // referenced (each item)
+    size_t const n_fingerprints);
+
+/*
+ * Get the DTLS parameter's role value.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_role(
+    enum rawrtc_dtls_role* rolep,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters);
+
+/*
+ * Get the DTLS parameter's fingerprint array.
+ * `*fingerprintsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_fingerprints(
+    struct rawrtc_dtls_fingerprints** const fingerprintsp,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters);
diff --git a/include/rawrtc/dtls_transport.h b/include/rawrtc/dtls_transport.h
new file mode 100644
index 0000000..c89fcba
--- /dev/null
+++ b/include/rawrtc/dtls_transport.h
@@ -0,0 +1,119 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_certificate;
+struct rawrtc_dtls_parameters;
+struct rawrtc_ice_transport;
+
+/*
+ * DTLS role.
+ */
+enum rawrtc_dtls_role {
+    RAWRTC_DTLS_ROLE_AUTO,
+    RAWRTC_DTLS_ROLE_CLIENT,
+    RAWRTC_DTLS_ROLE_SERVER,
+};
+
+/*
+ * DTLS transport state.
+ */
+enum rawrtc_dtls_transport_state {
+    RAWRTC_DTLS_TRANSPORT_STATE_NEW,
+    RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING,
+    RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED,
+    RAWRTC_DTLS_TRANSPORT_STATE_CLOSED,
+    RAWRTC_DTLS_TRANSPORT_STATE_FAILED,
+};
+
+/*
+ * DTLS transport.
+ */
+struct rawrtc_dtls_transport;
+
+/*
+ * DTLS transport state change handler.
+ */
+typedef void (*rawrtc_dtls_transport_state_change_handler)(
+    enum rawrtc_dtls_transport_state const state, void* const arg);
+
+/*
+ * DTLS transport error handler.
+ */
+typedef void (*rawrtc_dtls_transport_error_handler)(
+    // TODO: error.message (probably from OpenSSL)
+    void* const arg);
+
+/*
+ * Create a new DTLS transport.
+ * `*transport` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_transport_create(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates,
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+);
+
+/*
+ * Start the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_start(
+    struct rawrtc_dtls_transport* const transport,
+    struct rawrtc_dtls_parameters* const remote_parameters  // copied
+);
+
+/*
+ * Stop and close the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_stop(struct rawrtc_dtls_transport* const transport);
+
+/*
+ * TODO (from RTCIceTransport interface)
+ * rawrtc_certificate_list_*
+ * rawrtc_dtls_transport_get_certificates
+ */
+
+/*
+ * Get the current state of the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_state(
+    enum rawrtc_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+/*
+ * Get local DTLS parameters of a transport.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_local_parameters(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+/*
+ * TODO (from RTCIceTransport interface)
+ * rawrtc_dtls_transport_get_remote_parameters
+ * rawrtc_dtls_transport_get_remote_certificates
+ * rawrtc_dtls_transport_set_state_change_handler
+ * rawrtc_dtls_transport_set_error_handler
+ */
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_dtls_transport_state_to_name(enum rawrtc_dtls_transport_state const state);
+
+/*
+ * Translate a DTLS role to str.
+ */
+char const* rawrtc_dtls_role_to_str(enum rawrtc_dtls_role const role);
+
+/*
+ * Translate a str to a DTLS role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_dtls_role(
+    enum rawrtc_dtls_role* const rolep,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/ice_candidate.h b/include/rawrtc/ice_candidate.h
new file mode 100644
index 0000000..3d048aa
--- /dev/null
+++ b/include/rawrtc/ice_candidate.h
@@ -0,0 +1,206 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <netinet/in.h>  // IPPROTO_UDP, IPPROTO_TCP
+
+/*
+ * ICE protocol.
+ */
+enum rawrtc_ice_protocol {
+    RAWRTC_ICE_PROTOCOL_UDP = IPPROTO_UDP,
+    RAWRTC_ICE_PROTOCOL_TCP = IPPROTO_TCP,
+};
+
+/*
+ * ICE candidate type.
+ */
+enum rawrtc_ice_candidate_type {
+    RAWRTC_ICE_CANDIDATE_TYPE_HOST = ICE_CAND_TYPE_HOST,
+    RAWRTC_ICE_CANDIDATE_TYPE_SRFLX = ICE_CAND_TYPE_SRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_PRFLX = ICE_CAND_TYPE_PRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_RELAY = ICE_CAND_TYPE_RELAY,
+};
+
+/*
+ * ICE TCP candidate type.
+ */
+enum rawrtc_ice_tcp_candidate_type {
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE = ICE_TCP_ACTIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE = ICE_TCP_PASSIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_SO = ICE_TCP_SO,
+};
+
+/*
+ * ICE candidate.
+ */
+struct rawrtc_ice_candidate;
+
+/*
+ * ICE candidates.
+ * Note: Inherits `struct rawrtc_array_container`.
+ */
+struct rawrtc_ice_candidates {
+    size_t n_candidates;
+    struct rawrtc_ice_candidate* candidates[];
+};
+
+/*
+ * Create an ICE candidate.
+ * `*candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    char* const foundation,  // copied
+    uint32_t const priority,
+    char* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    char* const related_address,  // copied, nullable
+    uint16_t const related_port);
+
+/*
+ * Get the ICE candidate's foundation.
+ * `*foundationp` will be set to a copy of the IP address that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_foundation(
+    char** const foundationp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's priority.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_priority(
+    uint32_t* const priorityp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's IP address.
+ * `*ipp` will be set to a copy of the IP address that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_ip(
+    char** const ipp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's protocol.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's port.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's type.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_type(
+    enum rawrtc_ice_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's TCP type.
+ * Return `RAWRTC_CODE_NO_VALUE` in case the protocol is not TCP.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_tcp_type(
+    enum rawrtc_ice_tcp_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's related IP address.
+ * `*related_address` will be set to a copy of the related address that
+ * must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related address exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_address(
+    char** const related_addressp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Get the ICE candidate's related IP address' port.
+ * `*related_portp` will be set to a copy of the related address'
+ * port.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related port exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_port(
+    uint16_t* const related_portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate);
+
+/*
+ * Translate a protocol to the corresponding IPPROTO_*.
+ */
+int rawrtc_ice_protocol_to_ipproto(enum rawrtc_ice_protocol const protocol);
+
+/*
+ * Translate a IPPROTO_* to the corresponding protocol.
+ */
+enum rawrtc_code rawrtc_ipproto_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    int const ipproto);
+
+/*
+ * Translate an ICE protocol to str.
+ */
+char const* rawrtc_ice_protocol_to_str(enum rawrtc_ice_protocol const protocol);
+
+/*
+ * Translate a pl to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct pl const* const pl);
+
+/*
+ * Translate a str to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    char const* const str);
+
+/*
+ * Translate an ICE candidate type to str.
+ */
+char const* rawrtc_ice_candidate_type_to_str(enum rawrtc_ice_candidate_type const type);
+
+/*
+ * Translate a pl to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl);
+
+/*
+ * Translate a str to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    char const* const str);
+
+/*
+ * Translate an ICE TCP candidate type to str.
+ */
+char const* rawrtc_ice_tcp_candidate_type_to_str(enum rawrtc_ice_tcp_candidate_type const type);
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl);
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/ice_gather_options.h b/include/rawrtc/ice_gather_options.h
new file mode 100644
index 0000000..d850eb5
--- /dev/null
+++ b/include/rawrtc/ice_gather_options.h
@@ -0,0 +1,57 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE gather policy.
+ */
+enum rawrtc_ice_gather_policy {
+    RAWRTC_ICE_GATHER_POLICY_ALL,
+    RAWRTC_ICE_GATHER_POLICY_NOHOST,
+    RAWRTC_ICE_GATHER_POLICY_RELAY,
+};
+
+/*
+ * ICE credential type
+ */
+enum rawrtc_ice_credential_type {
+    RAWRTC_ICE_CREDENTIAL_TYPE_NONE,
+    RAWRTC_ICE_CREDENTIAL_TYPE_PASSWORD,
+    RAWRTC_ICE_CREDENTIAL_TYPE_TOKEN,
+};
+
+/*
+ * ICE gather options.
+ */
+struct rawrtc_ice_gather_options;
+
+/*
+ * Create a new ICE gather options instance.
+ * `*optionsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_create(
+    struct rawrtc_ice_gather_options** const optionsp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy);
+
+/*
+ * Add an ICE server to the gather options.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_add_server(
+    struct rawrtc_ice_gather_options* const options,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type);
+
+/*
+ * Translate an ICE gather policy to str.
+ */
+char const* rawrtc_ice_gather_policy_to_str(enum rawrtc_ice_gather_policy const policy);
+
+/*
+ * Translate a str to an ICE gather policy (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_gather_policy(
+    enum rawrtc_ice_gather_policy* const policyp,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/ice_gatherer.h b/include/rawrtc/ice_gatherer.h
new file mode 100644
index 0000000..2cb7462
--- /dev/null
+++ b/include/rawrtc/ice_gatherer.h
@@ -0,0 +1,118 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_ice_candidate;
+struct rawrtc_ice_candidates;
+struct rawrtc_ice_gather_options;
+struct rawrtc_ice_parameters;
+
+/*
+ * ICE gatherer state.
+ */
+enum rawrtc_ice_gatherer_state {
+    RAWRTC_ICE_GATHERER_STATE_NEW,
+    RAWRTC_ICE_GATHERER_STATE_GATHERING,
+    RAWRTC_ICE_GATHERER_STATE_COMPLETE,
+    RAWRTC_ICE_GATHERER_STATE_CLOSED,
+};
+
+/*
+ * ICE gatherer.
+ */
+struct rawrtc_ice_gatherer;
+
+/*
+ * ICE gatherer state change handler.
+ */
+typedef void (*rawrtc_ice_gatherer_state_change_handler)(
+    enum rawrtc_ice_gatherer_state const state,  // read-only
+    void* const arg);
+
+/*
+ * ICE gatherer error handler.
+ */
+typedef void (*rawrtc_ice_gatherer_error_handler)(
+    struct rawrtc_ice_candidate* const candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg);
+
+/*
+ * ICE gatherer local candidate handler.
+ * Note: 'candidate' and 'url' will be NULL in case gathering is complete.
+ * 'url' will be NULL in case a host candidate has been gathered.
+ */
+typedef void (*rawrtc_ice_gatherer_local_candidate_handler)(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg);
+
+/*
+ * Create a new ICE gatherer.
+ * `*gathererp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_create(
+    struct rawrtc_ice_gatherer** const gathererp,  // de-referenced
+    struct rawrtc_ice_gather_options* const options,  // referenced
+    rawrtc_ice_gatherer_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_gatherer_error_handler const error_handler,  // nullable
+    rawrtc_ice_gatherer_local_candidate_handler const local_candidate_handler,  // nullable
+    void* const arg  // nullable
+);
+
+/*
+ * Close the ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_close(struct rawrtc_ice_gatherer* const gatherer);
+
+/*
+ * Start gathering using an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_gather(
+    struct rawrtc_ice_gatherer* const gatherer,
+    struct rawrtc_ice_gather_options* const options  // referenced, nullable
+);
+
+/*
+ * TODO (from RTCIceGatherer interface)
+ * rawrtc_ice_gatherer_get_component
+ */
+
+/*
+ * Get the current state of an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer);
+
+/*
+ * Get local ICE parameters of an ICE gatherer.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_parameters(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer);
+
+/*
+ * Get local ICE candidates of an ICE gatherer.
+ * `*candidatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_candidates(
+    struct rawrtc_ice_candidates** const candidatesp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer);
+
+/*
+ * TODO (from RTCIceGatherer interface)
+ * rawrtc_ice_gatherer_create_associated_gatherer (unsupported)
+ * rawrtc_ice_gatherer_set_state_change_handler
+ * rawrtc_ice_gatherer_set_error_handler
+ * rawrtc_ice_gatherer_set_local_candidate_handler
+ */
+
+/*
+ * Get the corresponding name for an ICE gatherer state.
+ */
+char const* rawrtc_ice_gatherer_state_to_name(enum rawrtc_ice_gatherer_state const state);
diff --git a/include/rawrtc/ice_parameters.h b/include/rawrtc/ice_parameters.h
new file mode 100644
index 0000000..c8bd5a0
--- /dev/null
+++ b/include/rawrtc/ice_parameters.h
@@ -0,0 +1,41 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE parameters.
+ */
+struct rawrtc_ice_parameters;
+
+/*
+ * Create a new ICE parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_create(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    char* const username_fragment,  // copied
+    char* const password,  // copied
+    bool const ice_lite);
+
+/*
+ * Get the ICE parameter's username fragment value.
+ * `*username_fragmentp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters);
+
+/*
+ * Get the ICE parameter's password value.
+ * `*passwordp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_password(
+    char** const passwordp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters);
+
+/*
+ * Get the ICE parameter's ICE lite value.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_ice_lite(
+    bool* const ice_litep,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters);
diff --git a/include/rawrtc/ice_server.h b/include/rawrtc/ice_server.h
new file mode 100644
index 0000000..c245732
--- /dev/null
+++ b/include/rawrtc/ice_server.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server transport protocol.
+ */
+enum rawrtc_ice_server_transport {
+    RAWRTC_ICE_SERVER_TRANSPORT_UDP,
+    RAWRTC_ICE_SERVER_TRANSPORT_TCP,
+    RAWRTC_ICE_SERVER_TRANSPORT_DTLS,
+    RAWRTC_ICE_SERVER_TRANSPORT_TLS,
+};
+
+/*
+ * ICE server.
+ */
+struct rawrtc_ice_server;
+
+/*
+ * ICE servers.
+ * Note: Inherits `struct rawrtc_array_container`.
+ */
+struct rawrtc_ice_servers {
+    size_t n_servers;
+    struct rawrtc_ice_server* servers[];
+};
diff --git a/include/rawrtc/ice_transport.h b/include/rawrtc/ice_transport.h
new file mode 100644
index 0000000..eb08276
--- /dev/null
+++ b/include/rawrtc/ice_transport.h
@@ -0,0 +1,148 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_ice_candidate;
+struct rawrtc_ice_gatherer;
+struct rawrtc_ice_parameters;
+
+/*
+ * ICE role.
+ */
+enum rawrtc_ice_role {
+    RAWRTC_ICE_ROLE_UNKNOWN = ICE_ROLE_UNKNOWN,
+    RAWRTC_ICE_ROLE_CONTROLLING = ICE_ROLE_CONTROLLING,
+    RAWRTC_ICE_ROLE_CONTROLLED = ICE_ROLE_CONTROLLED,
+};
+
+/*
+ * ICE transport state.
+ */
+enum rawrtc_ice_transport_state {
+    RAWRTC_ICE_TRANSPORT_STATE_NEW,
+    RAWRTC_ICE_TRANSPORT_STATE_CHECKING,
+    RAWRTC_ICE_TRANSPORT_STATE_CONNECTED,
+    RAWRTC_ICE_TRANSPORT_STATE_COMPLETED,
+    RAWRTC_ICE_TRANSPORT_STATE_DISCONNECTED,
+    RAWRTC_ICE_TRANSPORT_STATE_FAILED,
+    RAWRTC_ICE_TRANSPORT_STATE_CLOSED,
+};
+
+/*
+ * ICE transport.
+ */
+struct rawrtc_ice_transport;
+
+/*
+ * ICE transport state change handler.
+ */
+typedef void (*rawrtc_ice_transport_state_change_handler)(
+    enum rawrtc_ice_transport_state const state, void* const arg);
+
+/*
+ * ICE transport pair change handler.
+ */
+typedef void (*rawrtc_ice_transport_candidate_pair_change_handler)(
+    struct rawrtc_ice_candidate* const local,  // read-only
+    struct rawrtc_ice_candidate* const remote,  // read-only
+    void* const arg);
+
+/*
+ * Create a new ICE transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_transport_create(
+    struct rawrtc_ice_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced, nullable
+    rawrtc_ice_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_transport_candidate_pair_change_handler const
+        candidate_pair_change_handler,  // nullable
+    void* const arg  // nullable
+);
+
+/*
+ * Start the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_start(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced
+    struct rawrtc_ice_parameters* const remote_parameters,  // referenced
+    enum rawrtc_ice_role const role);
+
+/*
+ * Stop and close the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_stop(struct rawrtc_ice_transport* const transport);
+
+/*
+ * TODO (from RTCIceTransport interface)
+ * rawrtc_ice_transport_get_ice_gatherer
+ */
+
+/*
+ * Get the current ICE role of the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    struct rawrtc_ice_transport* const transport);
+
+/*
+ * TODO
+ * rawrtc_ice_transport_get_component
+ */
+
+/*
+ * Get the current state of the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_ice_transport* const transport);
+
+/*
+ * rawrtc_ice_transport_get_remote_candidates
+ * rawrtc_ice_transport_get_selected_candidate_pair
+ * rawrtc_ice_transport_get_remote_parameters
+ * rawrtc_ice_transport_create_associated_transport (unsupported)
+ */
+
+/*
+ * Add a remote candidate ot the ICE transport.
+ * Note: 'candidate' must be NULL to inform the transport that the
+ * remote site finished gathering.
+ */
+enum rawrtc_code rawrtc_ice_transport_add_remote_candidate(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* candidate  // nullable
+);
+
+/*
+ * Set the remote candidates on the ICE transport overwriting all
+ * existing remote candidates.
+ */
+enum rawrtc_code rawrtc_ice_transport_set_remote_candidates(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* const candidates[],  // referenced (each item)
+    size_t const n_candidates);
+
+/* TODO (from RTCIceTransport interface)
+ * rawrtc_ice_transport_set_state_change_handler
+ * rawrtc_ice_transport_set_candidate_pair_change_handler
+ */
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_ice_transport_state_to_name(enum rawrtc_ice_transport_state const state);
+
+/*
+ * Translate an ICE role to str.
+ */
+char const* rawrtc_ice_role_to_str(enum rawrtc_ice_role const role);
+
+/*
+ * Translate a str to an ICE role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/main.h b/include/rawrtc/main.h
new file mode 100644
index 0000000..a692961
--- /dev/null
+++ b/include/rawrtc/main.h
@@ -0,0 +1,35 @@
+#pragma once
+#include <rawrtcc/code.h>
+
+/*
+ * Transport layers.
+ */
+enum {
+    RAWRTC_LAYER_SCTP = 20,
+    RAWRTC_LAYER_DTLS_SRTP_STUN = 10,  // TODO: Pretty sure we are able to detect STUN earlier
+    RAWRTC_LAYER_ICE = 0,
+    RAWRTC_LAYER_STUN = -10,
+    RAWRTC_LAYER_TURN = -10,
+};
+
+/*
+ * Configuration.
+ */
+struct rawrtc_config;
+
+/*
+ * Initialise rawrtc. Must be called before making a call to any other
+ * function.
+ *
+ * Note: In case `init_re` is not set to `true`, you MUST initialise
+ *       re yourselves before calling this function.
+ */
+enum rawrtc_code rawrtc_init(bool const init_re);
+
+/*
+ * Close rawrtc and free up all resources.
+ *
+ * Note: In case `close_re` is not set to `true`, you MUST close
+ *       re yourselves.
+ */
+enum rawrtc_code rawrtc_close(bool const close_re);
diff --git a/include/rawrtc/meson.build b/include/rawrtc/meson.build
new file mode 100644
index 0000000..a35c19c
--- /dev/null
+++ b/include/rawrtc/meson.build
@@ -0,0 +1,32 @@
+# Generate configuration header
+configure_file(
+    input: 'config.h.in',
+    output: 'config.h',
+    configuration: configuration,
+    install_dir: '/'.join([get_option('includedir'), 'rawrtc']))
+
+# Install headers
+includes = files([
+    'certificate.h',
+    'dtls_fingerprint.h',
+    'dtls_parameters.h',
+    'dtls_transport.h',
+    'ice_candidate.h',
+    'ice_gather_options.h',
+    'ice_gatherer.h',
+    'ice_parameters.h',
+    'ice_server.h',
+    'ice_transport.h',
+    'main.h',
+    'peer_connection.h',
+    'peer_connection_configuration.h',
+    'peer_connection_description.h',
+    'peer_connection_ice_candidate.h',
+    'peer_connection_state.h',
+    'sctp_transport.h',
+    'utils.h',
+])
+if get_option('sctp_redirect_transport')
+    includes += files('sctp_redirect_transport.h')
+endif
+install_headers(includes, subdir: 'rawrtc')
diff --git a/include/rawrtc/peer_connection.h b/include/rawrtc/peer_connection.h
new file mode 100644
index 0000000..9c962c0
--- /dev/null
+++ b/include/rawrtc/peer_connection.h
@@ -0,0 +1,340 @@
+#pragma once
+#include "ice_gatherer.h"
+#include "ice_transport.h"
+#include "peer_connection_state.h"
+#include <rawrtc/peer_connection_configuration.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_peer_connection_description;
+struct rawrtc_peer_connection_ice_candidate;
+
+/*
+ * Peer connection.
+ */
+struct rawrtc_peer_connection;
+
+/*
+ * Peer connection state change handler.
+ */
+typedef void (*rawrtc_peer_connection_state_change_handler)(
+    enum rawrtc_peer_connection_state const state,  // read-only
+    void* const arg);
+
+/*
+ * Negotiation needed handler.
+ */
+typedef void (*rawrtc_negotiation_needed_handler)(void* const arg);
+
+/*
+ * Peer connection ICE local candidate handler.
+ * Note: 'candidate' and 'url' will be NULL in case gathering is complete.
+ * 'url' will be NULL in case a host candidate has been gathered.
+ */
+typedef void (*rawrtc_peer_connection_local_candidate_handler)(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg);
+
+/*
+ * Peer connection ICE local candidate error handler.
+ * Note: 'candidate' and 'url' will be NULL in case gathering is complete.
+ * 'url' will be NULL in case a host candidate has been gathered.
+ */
+typedef void (*rawrtc_peer_connection_local_candidate_error_handler)(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg);
+
+/*
+ * Signaling state handler.
+ */
+typedef void (*rawrtc_signaling_state_change_handler)(
+    enum rawrtc_signaling_state const state,  // read-only
+    void* const arg);
+
+/*
+ * Create a new peer connection.
+ * `*connectionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create(
+    struct rawrtc_peer_connection** const connectionp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* configuration,  // referenced
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler,  // nullable
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler,  // nullable
+    rawrtc_ice_transport_state_change_handler const
+        ice_connection_state_change_handler,  // nullable
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler,  // nullable
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler,  // nullable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    void* const arg  // nullable
+);
+
+/*
+ * Close the peer connection. This will stop all underlying transports
+ * and results in a final 'closed' state.
+ */
+enum rawrtc_code rawrtc_peer_connection_close(struct rawrtc_peer_connection* const connection);
+
+/*
+ * Create an offer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_offer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    bool const ice_restart);
+
+/*
+ * Create an answer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_answer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set and apply the local description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+);
+
+/*
+ * Get local description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no local description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set and apply the remote description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_remote_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+);
+
+/*
+ * Get remote description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_remote_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Add an ICE candidate to the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_add_ice_candidate(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+/*
+ * Get the current signalling state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state(
+    enum rawrtc_signaling_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Get the current ICE gathering state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Get the current ICE connection state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Get the current (peer) connection state of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state(
+    enum rawrtc_peer_connection_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Get indication whether the remote peer accepts trickled ICE
+ * candidates.
+ *
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_can_trickle_ice_candidates(
+    bool* const can_trickle_ice_candidatesp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Create a data channel on a peer connection.
+ * `*channelp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_data_channel(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_data_channel_parameters* const parameters,  // referenced
+    rawrtc_data_channel_open_handler const open_handler,  // nullable
+    rawrtc_data_channel_buffered_amount_low_handler const buffered_amount_low_handler,  // nullable
+    rawrtc_data_channel_error_handler const error_handler,  // nullable
+    rawrtc_data_channel_close_handler const close_handler,  // nullable
+    rawrtc_data_channel_message_handler const message_handler,  // nullable
+    void* const arg  // nullable
+);
+
+/*
+ * Unset the handler argument and all handlers of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_unset_handlers(
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's negotiation needed handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_negotiation_needed_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler  // nullable
+);
+
+/*
+ * Get the peer connection's negotiation needed handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_negotiation_needed_handler(
+    rawrtc_negotiation_needed_handler* const negotiation_needed_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's ICE local candidate handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler  // nullable
+);
+
+/*
+ * Get the peer connection's ICE local candidate handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_handler(
+    rawrtc_peer_connection_local_candidate_handler* const
+        local_candidate_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's ICE local candidate error handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_error_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler  // nullable
+);
+
+/*
+ * Get the peer connection's ICE local candidate error handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_error_handler(
+    rawrtc_peer_connection_local_candidate_error_handler* const
+        local_candidate_error_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's signaling state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_signaling_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler  // nullable
+);
+
+/*
+ * Get the peer connection's signaling state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state_change_handler(
+    rawrtc_signaling_state_change_handler* const signaling_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's ice connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_transport_state_change_handler const ice_connection_state_change_handler  // nullable
+);
+
+/*
+ * Get the peer connection's ice connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state_change_handler(
+    rawrtc_ice_transport_state_change_handler* const
+        ice_connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's ice gathering state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_gathering_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler  // nullable
+);
+
+/*
+ * Get the peer connection's ice gathering state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state_change_handler(
+    rawrtc_ice_gatherer_state_change_handler* const
+        ice_gathering_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's (peer) connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler  // nullable
+);
+
+/*
+ * Get the peer connection's (peer) connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state_change_handler(
+    rawrtc_peer_connection_state_change_handler* const
+        connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
+
+/*
+ * Set the peer connection's data channel handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_data_channel_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_data_channel_handler const data_channel_handler  // nullable
+);
+
+/*
+ * Get the peer connection's data channel handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_data_channel_handler(
+    rawrtc_data_channel_handler* const data_channel_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection);
diff --git a/include/rawrtc/peer_connection_configuration.h b/include/rawrtc/peer_connection_configuration.h
new file mode 100644
index 0000000..d32a02b
--- /dev/null
+++ b/include/rawrtc/peer_connection_configuration.h
@@ -0,0 +1,96 @@
+#pragma once
+#include "ice_gather_options.h"
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_certificate;
+struct rawrtc_certificates;
+struct rawrtc_ice_servers;
+
+/*
+ * Peer connection configuration.
+ */
+struct rawrtc_peer_connection_configuration;
+
+/*
+ * Create a new peer connection configuration.
+ * `*configurationp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_create(
+    struct rawrtc_peer_connection_configuration** const configurationp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy);
+
+/*
+ * Add an ICE server to the peer connection configuration.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type);
+
+/*
+ * Get ICE servers from the peer connection configuration.
+ * `*serversp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_ice_servers(
+    struct rawrtc_ice_servers** const serversp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration);
+
+/*
+ * Add a certificate to the peer connection configuration to be used
+ * instead of an ephemerally generated one.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_certificate(
+    struct rawrtc_peer_connection_configuration* configuration,
+    struct rawrtc_certificate* const certificate  // copied
+);
+
+/*
+ * Get certificates from the peer connection configuration.
+ * `*certificatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_certificates(
+    struct rawrtc_certificates** const certificatesp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration);
+
+/*
+ * Set whether to use legacy SDP for data channel parameter encoding.
+ * Note: Legacy SDP for data channels is on by default due to parsing problems in Chrome.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_sdp_05(
+    struct rawrtc_peer_connection_configuration* configuration, bool on);
+
+/*
+ * Set the SCTP transport's send and receive buffer length in bytes.
+ * If both values are zero, the default buffer length will be used. Otherwise,
+ * zero is invalid.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_buffer_length(
+    struct rawrtc_peer_connection_configuration* configuration,
+    uint32_t send_buffer_length,
+    uint32_t receive_buffer_length);
+
+/*
+ * Set the SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_congestion_ctrl_algorithm(
+    struct rawrtc_peer_connection_configuration* configuration,
+    enum rawrtc_sctp_transport_congestion_ctrl algorithm);
+
+/*
+ * Set the SCTP transport's maximum transmission unit (MTU).
+ * A value of zero indicates that the default MTU should be used.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu(
+    struct rawrtc_peer_connection_configuration* configuration, uint32_t mtu);
+
+/*
+ * Enable or disable MTU discovery on the SCTP transport.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu_discovery(
+    struct rawrtc_peer_connection_configuration* configuration, bool on);
diff --git a/include/rawrtc/peer_connection_description.h b/include/rawrtc/peer_connection_description.h
new file mode 100644
index 0000000..8befa52
--- /dev/null
+++ b/include/rawrtc/peer_connection_description.h
@@ -0,0 +1,54 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * SDP type.
+ */
+enum rawrtc_sdp_type {
+    RAWRTC_SDP_TYPE_OFFER,
+    RAWRTC_SDP_TYPE_PROVISIONAL_ANSWER,
+    RAWRTC_SDP_TYPE_ANSWER,
+    RAWRTC_SDP_TYPE_ROLLBACK,
+};
+
+/*
+ * Peer connection description.
+ */
+struct rawrtc_peer_connection_description;
+
+/*
+ * Create a description by parsing it from SDP.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_create(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    enum rawrtc_sdp_type const type,
+    char const* const sdp);
+
+/*
+ * Get the SDP type of the description.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    struct rawrtc_peer_connection_description* const description);
+
+/*
+ * Get the SDP of the description.
+ * `*sdpp` will be set to a copy of the SDP that must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_description* const description);
+
+/*
+ * Translate an SDP type to str.
+ */
+char const* rawrtc_sdp_type_to_str(enum rawrtc_sdp_type const type);
+
+/*
+ * Translate a str to an SDP type.
+ */
+enum rawrtc_code rawrtc_str_to_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    char const* const str);
diff --git a/include/rawrtc/peer_connection_ice_candidate.h b/include/rawrtc/peer_connection_ice_candidate.h
new file mode 100644
index 0000000..180a6f5
--- /dev/null
+++ b/include/rawrtc/peer_connection_ice_candidate.h
@@ -0,0 +1,83 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_ice_candidate;
+
+/*
+ * Peer connection ICE candidate.
+ */
+struct rawrtc_peer_connection_ice_candidate;
+
+/*
+ * Create a new ICE candidate from SDP.
+ * `*candidatesp` must be unreferenced.
+ *
+ * Note: This is equivalent to creating an `RTCIceCandidate` from an
+ *       `RTCIceCandidateInit` instance in the W3C WebRTC
+ *       specification.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    char* const sdp,
+    char* const mid,  // nullable, copied
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, copied
+);
+
+/*
+ * Encode the ICE candidate into SDP.
+ * `*sdpp` will be set to a copy of the SDP attribute that must be
+ * unreferenced.
+ *
+ * Note: This is equivalent to the `candidate` attribute of the W3C
+ *       WebRTC specification's `RTCIceCandidateInit`.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+/*
+ * Get the media stream identification tag the ICE candidate is
+ * associated to.
+ * `*midp` will be set to a copy of the candidate's mid and must be
+ * unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no 'mid' has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*midp* must
+ * be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_mid(
+    char** const midp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+/*
+ * Get the media stream line index the ICE candidate is associated to.
+ * Return `RAWRTC_CODE_NO_VALUE` in case no media line index has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_media_line_index(
+    uint8_t* const media_line_index,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+/*
+ * Get the username fragment the ICE candidate is associated to.
+ * `*username_fragmentp` will be set to a copy of the candidate's
+ * username fragment and must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no username fragment has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*username_fragmentp* must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+/*
+ * Get the underlying ORTC ICE candidate from the ICE candidate.
+ * `*ortc_candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_ortc_candidate(
+    struct rawrtc_ice_candidate** const ortc_candidatep,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
diff --git a/include/rawrtc/peer_connection_state.h b/include/rawrtc/peer_connection_state.h
new file mode 100644
index 0000000..6649734
--- /dev/null
+++ b/include/rawrtc/peer_connection_state.h
@@ -0,0 +1,35 @@
+#pragma once
+
+/*
+ * Signalling state.
+ */
+enum rawrtc_signaling_state {
+    RAWRTC_SIGNALING_STATE_STABLE,
+    RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER,
+    RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER,
+    RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER,
+    RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER,
+    RAWRTC_SIGNALING_STATE_CLOSED,
+};
+
+/*
+ * Peer connection state.
+ */
+enum rawrtc_peer_connection_state {
+    RAWRTC_PEER_CONNECTION_STATE_NEW,
+    RAWRTC_PEER_CONNECTION_STATE_CONNECTING,
+    RAWRTC_PEER_CONNECTION_STATE_CONNECTED,
+    RAWRTC_PEER_CONNECTION_STATE_DISCONNECTED,
+    RAWRTC_PEER_CONNECTION_STATE_FAILED,
+    RAWRTC_PEER_CONNECTION_STATE_CLOSED,
+};
+
+/*
+ * Get the corresponding name for a signaling state.
+ */
+char const* rawrtc_signaling_state_to_name(enum rawrtc_signaling_state const state);
+
+/*
+ * Get the corresponding name for a peer connection state.
+ */
+char const* rawrtc_peer_connection_state_to_name(enum rawrtc_peer_connection_state const state);
diff --git a/include/rawrtc/sctp_redirect_transport.h b/include/rawrtc/sctp_redirect_transport.h
new file mode 100644
index 0000000..80a9d8c
--- /dev/null
+++ b/include/rawrtc/sctp_redirect_transport.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_dtls_transport;
+
+/*
+ * Create an SCTP redirect transport.
+ * `*transportp` must be unreferenced.
+ *
+ * `port` defaults to `5000` if set to `0`.
+ * `redirect_ip` is the target IP SCTP packets will be redirected to
+ *  and must be a IPv4 address.
+ * `redirect_port` is the target SCTP port packets will be redirected
+ *  to.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_create(
+    struct rawrtc_sctp_redirect_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    char* const redirect_ip,  // copied
+    uint16_t const redirect_port,
+    rawrtc_sctp_redirect_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+);
diff --git a/include/rawrtc/sctp_transport.h b/include/rawrtc/sctp_transport.h
new file mode 100644
index 0000000..59c365f
--- /dev/null
+++ b/include/rawrtc/sctp_transport.h
@@ -0,0 +1,21 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_dtls_transport;
+
+/*
+ * Create an SCTP transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_create(
+    struct rawrtc_sctp_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    rawrtc_sctp_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+);
diff --git a/include/rawrtc/utils.h b/include/rawrtc/utils.h
new file mode 100644
index 0000000..94f63b9
--- /dev/null
+++ b/include/rawrtc/utils.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <re.h>
+
+/*
+ * Array container.
+ */
+struct rawrtc_array_container {
+    size_t n_items;
+    void* items[];
+};
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..92af070
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,114 @@
+# Project definition
+project('rawrtc', 'c',
+    version: '0.5.1',
+    default_options: ['c_std=c99'],
+    meson_version: '>=0.48.0')
+
+# Set compiler warning flags
+compiler = meson.get_compiler('c')
+compiler_args = compiler.get_supported_arguments([
+    '-Wall',
+    '-Wmissing-declarations',
+    '-Wmissing-prototypes',
+    '-Wstrict-prototypes',
+    '-Wbad-function-cast',
+    '-Wsign-compare',
+    '-Wnested-externs',
+    '-Wshadow',
+    '-Waggregate-return',
+    '-Wcast-align',
+    '-Wextra',
+    '-Wold-style-definition',
+    '-Wdeclaration-after-statement',
+    '-Wuninitialized',
+    '-Wshorten-64-to-32',
+    '-pedantic',
+])
+add_project_arguments(compiler_args, language: 'c')
+
+# Configuration
+configuration = configuration_data()
+
+# Dependency: OpenSSL
+openssl_dep = dependency('openssl',
+    version: '>=1.0.2',
+    required: true)
+
+# Dependency: re
+# Note: We need to force using our own fork until re has accepted all our patches
+re_dep = dependency('librawrre',
+    version: '>=0.6.0',
+    fallback: ['re', 're_dep'],
+    required: true)
+
+# Dependency: rew
+rew_dep = dependency('librawrrew',
+    version: '>=0.5.0',
+    fallback: ['rew', 'rew_dep'],
+    required: true)
+
+# Dependency: rawrtcc
+rawrtcc_dep = dependency('rawrtcc',
+    version: '>=0.1.2',
+    fallback: ['rawrtcc', 'rawrtcc_dep'],
+    required: true)
+
+# Dependency: rawrtcdc
+rawrtcdc_dep = dependency('rawrtcdc',
+    version: '>=0.1.3',
+    fallback: ['rawrtcdc', 'rawrtcdc_dep'],
+    required: true)
+
+# Dependencies list
+dependencies = [
+    openssl_dep,
+    re_dep,
+    rew_dep,
+    rawrtcc_dep,
+    rawrtcdc_dep,
+]
+
+# Options
+configuration.set10('RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT', get_option('sctp_redirect_transport'))
+
+# Version
+version = meson.project_version()
+version_array = version.split('.')
+configuration.set_quoted('RAWRTC_VERSION', version)
+configuration.set('RAWRTC_VERSION_MAJOR', version_array[0])
+configuration.set('RAWRTC_VERSION_MINOR', version_array[1])
+configuration.set('RAWRTC_VERSION_PATCH', version_array[2])
+
+# Set debug level
+configuration.set('RAWRTC_DEBUG_LEVEL', get_option('debug_level'))
+
+# Includes
+include_dir = include_directories('include')
+subdir('include')
+
+# Sources
+subdir('src')
+
+# Build library
+rawrtc = library(meson.project_name(), sources,
+    dependencies: dependencies,
+    include_directories: include_dir,
+    install: true,
+    version: version)
+
+# Generate pkg-config file
+pkg = import('pkgconfig')
+pkg.generate(rawrtc,
+    name: meson.project_name(),
+    description: 'A WebRTC and ORTC library with a small footprint.',
+    url: 'https://github.com/rawrtc/rawrtc')
+
+# Declare dependency
+rawrtc_dep = declare_dependency(
+    include_directories: include_dir,
+    link_with: rawrtc)
+
+# Tools (optional)
+if get_option('tools')
+    subdir('tools')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..5f93c92
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,6 @@
+option('debug_level', type: 'integer', min: 0, max: 7, value: 5,
+    description: 'Global debug level')
+option('tools', type: 'boolean', value: true,
+    description: 'Build RAWRTC tools')
+option('sctp_redirect_transport', type: 'boolean', value: false,
+    description: 'Build SCTP redirect transport')
diff --git a/src/certificate/certificate.c b/src/certificate/certificate.c
new file mode 100644
index 0000000..9f68b8e
--- /dev/null
+++ b/src/certificate/certificate.c
@@ -0,0 +1,899 @@
+#include "certificate.h"
+#include "../utils/utils.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/bio.h>
+#include <openssl/bn.h>
+#include <openssl/crypto.h>
+#include <openssl/ec.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/objects.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <openssl/x509.h>
+#include <limits.h>  // INT_MAX, LONG_MAX
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "certificate"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Default certificate options.
+ */
+struct rawrtc_certificate_options rawrtc_default_certificate_options = {
+    .key_type = RAWRTC_CERTIFICATE_KEY_TYPE_EC,
+    .common_name = "anonymous@rawrtc.org",
+    .valid_until = 3600 * 24 * 30,  // 30 days
+    .sign_algorithm = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    .named_curve = "prime256v1",
+    .modulus_length = 3072,
+};
+
+/*
+ * Print and flush the OpenSSL error queue.
+ */
+static int print_openssl_error(char const* message, size_t length, void* arg) {
+    (void) message;
+    (void) length;
+    (void) arg;
+    DEBUG_WARNING("%b", message, length);
+
+    // 1 to continue outputting the error queue
+    return 1;
+}
+
+/*
+ * Generates an n-bit RSA key pair.
+ * Caller must call `EVP_PKEY_free(*keyp)` when done.
+ */
+static enum rawrtc_code generate_key_rsa(
+    EVP_PKEY** const keyp,  // de-referenced
+    uint_fast32_t const modulus_length) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    EVP_PKEY* key = NULL;
+    RSA* rsa = NULL;
+    BIGNUM* bn = NULL;
+
+    // Check arguments
+    if (!keyp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > INT_MAX)
+    if (modulus_length > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Create an empty EVP_PKEY structure
+    key = EVP_PKEY_new();
+    if (!key) {
+        DEBUG_WARNING("Could not create EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Initialise RSA structure
+    rsa = RSA_new();
+    if (!rsa) {
+        DEBUG_WARNING("Could not initialise RSA structure\n");
+        goto out;
+    }
+
+    // Allocate BIGNUM
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(OPENSSL_IS_BORINGSSL)
+    bn = BN_secure_new();
+#else
+    bn = BN_new();
+#endif
+    if (!bn) {
+        DEBUG_WARNING("Could not allocate BIGNUM\n");
+        goto out;
+    }
+
+    // Generate RSA key pair and store it in the RSA structure
+    BN_set_word(bn, RSA_F4);
+    if (!RSA_generate_key_ex(rsa, (int) modulus_length, bn, NULL)) {
+        DEBUG_WARNING("Could not generate RSA key pair\n");
+        goto out;
+    }
+
+    // Store the generated RSA key pair in the EVP_PKEY structure
+    if (!EVP_PKEY_set1_RSA(key, rsa)) {
+        DEBUG_WARNING("Could not assign RSA key pair to EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (rsa) {
+        RSA_free(rsa);
+    }
+    if (bn) {
+        BN_free(bn);
+    }
+    if (error) {
+        if (key) {
+            EVP_PKEY_free(key);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *keyp = key;
+    }
+    return error;
+}
+
+/*
+ * Generates an ECC key pair.
+ * Caller must call `EVP_PKEY_free(*keyp)` when done.
+ */
+static enum rawrtc_code generate_key_ecc(
+    EVP_PKEY** const keyp,  // de-referenced
+    char* const named_curve) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    EVP_PKEY* key = NULL;
+    int curve_group_nid;
+    EC_KEY* ecc = NULL;
+
+    // Check arguments
+    if (!keyp || !named_curve) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create an empty EVP_PKEY structure
+    key = EVP_PKEY_new();
+    if (!key) {
+        DEBUG_WARNING("Could not create EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Get NID of named curve
+    curve_group_nid = OBJ_txt2nid(named_curve);
+    if (curve_group_nid == NID_undef) {
+        DEBUG_WARNING("Could not determine group NID of named curve: %s\n", named_curve);
+        goto out;
+    }
+
+    // Initialise EC structure for named curve
+    ecc = EC_KEY_new_by_curve_name(curve_group_nid);
+    if (!ecc) {
+        DEBUG_WARNING("Could not initialise EC structure for named curve\n");
+        goto out;
+    }
+
+    // This is needed to correctly sign the certificate
+    EC_KEY_set_asn1_flag(ecc, OPENSSL_EC_NAMED_CURVE);
+
+    // Generate the ECC key pair and store it in the EC structure
+    if (!EC_KEY_generate_key(ecc)) {
+        DEBUG_WARNING("Could not generate ECC key pair\n");
+        goto out;
+    }
+
+    // Store the generated ECC key pair in the EVP_PKEY structure
+    if (!EVP_PKEY_assign_EC_KEY(key, ecc)) {
+        DEBUG_WARNING("Could not assign ECC key pair to EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        if (ecc) {
+            EC_KEY_free(ecc);
+        }
+        if (key) {
+            EVP_PKEY_free(key);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *keyp = key;
+    }
+    return error;
+}
+
+/*
+ * Generates a self-signed certificate.
+ * Caller must call `X509_free(*certificatep)` when done.
+ */
+static enum rawrtc_code generate_self_signed_certificate(
+    X509** const certificatep,  // de-referenced
+    EVP_PKEY* const key,
+    char* const common_name,
+    uint_fast32_t const valid_until,
+    enum rawrtc_certificate_sign_algorithm const sign_algorithm) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    X509* certificate = NULL;
+    X509_NAME* name = NULL;
+    EVP_MD const* sign_function;
+
+    // Check arguments
+    if (!certificatep || !key || !common_name) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > LONG_MAX)
+    if (valid_until > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Get sign function
+    sign_function = rawrtc_get_sign_function(sign_algorithm);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate and initialise x509 structure
+    certificate = X509_new();
+    if (!certificate) {
+        DEBUG_WARNING("Could not initialise x509 structure\n");
+        goto out;
+    }
+
+    // Set x509 version
+    // Note: '2' maps to version 3
+    if (!X509_set_version(certificate, 2)) {
+        DEBUG_WARNING("Could not set x509 version\n");
+        goto out;
+    }
+
+    // Set the serial number randomly (doesn't need to be unique as we are self-signing)
+    if (!ASN1_INTEGER_set(X509_get_serialNumber(certificate), rand_u32())) {
+        DEBUG_WARNING("Could not set x509 serial number\n");
+        goto out;
+    }
+
+    // Create an empty X509_NAME structure
+    name = X509_NAME_new();
+    if (!name) {
+        DEBUG_WARNING("Could not create x509_NAME structure\n");
+        goto out;
+    }
+
+    // Set common name field on X509_NAME structure
+    if (!X509_NAME_add_entry_by_txt(
+            name, "CN", MBSTRING_ASC, (uint8_t*) common_name, (int) strlen(common_name), -1, 0)) {
+        DEBUG_WARNING("Could not apply common name (%s) on certificate\n", common_name);
+        goto out;
+    }
+
+    // Set issuer and subject name
+    if (!X509_set_issuer_name(certificate, name) || !X509_set_subject_name(certificate, name)) {
+        DEBUG_WARNING("Could not set issuer name on certificate\n");
+        goto out;
+    }
+
+    // Certificate is valid from now (-1 day) until whatever has been provided in parameters
+    if (!X509_gmtime_adj(X509_get_notBefore(certificate), -3600 * 24) ||
+        !X509_gmtime_adj(X509_get_notAfter(certificate), (long) valid_until)) {
+        DEBUG_WARNING("Could not apply lifetime range to certificate\n");
+        goto out;
+    }
+
+    // Set public key of certificate
+    if (!X509_set_pubkey(certificate, key)) {
+        DEBUG_WARNING("Could not set public key to certificate\n");
+        goto out;
+    }
+
+    // Sign the certificate
+    if (!X509_sign(certificate, key, sign_function)) {
+        DEBUG_WARNING("Could not sign the certificate\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (name) {
+        X509_NAME_free(name);
+    }
+    if (error) {
+        if (certificate) {
+            X509_free(certificate);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+/*
+ * Destructor for existing certificate options.
+ */
+static void rawrtc_certificate_options_destroy(void* arg) {
+    struct rawrtc_certificate_options* const options = arg;
+
+    // Un-reference
+    mem_deref(options->named_curve);
+    mem_deref(options->common_name);
+}
+
+/*
+ * Create certificate options.
+ *
+ * All arguments but `key_type` are optional. Sane and safe default
+ * values will be applied, don't worry!
+ *
+ * `*optionsp` must be unreferenced.
+ *
+ * If `common_name` is `NULL` the default common name will be applied.
+ * If `valid_until` is `0` the default certificate lifetime will be
+ * applied.
+ * If the key type is `ECC` and `named_curve` is `NULL`, the default
+ * named curve will be used.
+ * If the key type is `RSA` and `modulus_length` is `0`, the default
+ * amount of bits will be used. The same applies to the
+ * `sign_algorithm` if it has been set to `NONE`.
+ */
+enum rawrtc_code rawrtc_certificate_options_create(
+    struct rawrtc_certificate_options** const optionsp,  // de-referenced
+    enum rawrtc_certificate_key_type const key_type,
+    char* common_name,  // nullable, copied
+    uint_fast32_t valid_until,
+    enum rawrtc_certificate_sign_algorithm sign_algorithm,
+    char* named_curve,  // nullable, copied, ignored for RSA
+    uint_fast32_t modulus_length  // ignored for ECC
+) {
+    struct rawrtc_certificate_options* options;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!optionsp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > LONG_MAX)
+    if (valid_until > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+#if (UINT_FAST32_MAX > INT_MAX)
+    if (modulus_length > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Set defaults
+    if (!common_name) {
+        common_name = rawrtc_default_certificate_options.common_name;
+    }
+    if (!valid_until) {
+        valid_until = rawrtc_default_certificate_options.valid_until;
+    }
+
+    // Check sign algorithm/set default
+    // Note: We say 'no' to SHA1 intentionally
+    // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to libre)
+    switch (sign_algorithm) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE:
+            sign_algorithm = rawrtc_default_certificate_options.sign_algorithm;
+            break;
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256:
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set defaults depending on key type
+    switch (key_type) {
+        case RAWRTC_CERTIFICATE_KEY_TYPE_RSA:
+            // Unset ECC vars
+            named_curve = NULL;
+
+            // Prevent user from being stupid
+            if (modulus_length < RAWRTC_MODULUS_LENGTH_MIN) {
+                modulus_length = rawrtc_default_certificate_options.modulus_length;
+            }
+
+            break;
+
+        case RAWRTC_CERTIFICATE_KEY_TYPE_EC:
+            // Unset RSA vars
+            modulus_length = 0;
+
+            // Set default named curve (if required)
+            if (!named_curve) {
+                named_curve = rawrtc_default_certificate_options.named_curve;
+            }
+
+            break;
+
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Allocate
+    options = mem_zalloc(sizeof(*options), rawrtc_certificate_options_destroy);
+    if (!options) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    options->key_type = key_type;
+    if (common_name) {
+        error = rawrtc_strdup(&options->common_name, common_name);
+        if (error) {
+            goto out;
+        }
+    }
+    options->valid_until = valid_until;
+    options->sign_algorithm = sign_algorithm;
+    if (named_curve) {
+        error = rawrtc_strdup(&options->named_curve, named_curve);
+        if (error) {
+            goto out;
+        }
+    }
+    options->modulus_length = modulus_length;
+
+out:
+    if (error) {
+        mem_deref(options);
+    } else {
+        // Set pointer
+        *optionsp = options;
+    }
+    return error;
+}
+
+/*
+ * Destructor for existing certificate.
+ */
+static void rawrtc_certificate_destroy(void* arg) {
+    struct rawrtc_certificate* const certificate = arg;
+
+    // Free
+    if (certificate->certificate) {
+        X509_free(certificate->certificate);
+    }
+    if (certificate->key) {
+        EVP_PKEY_free(certificate->key);
+    }
+}
+
+/*
+ * Create and generate a self-signed certificate.
+ *
+ * Sane and safe default options will be applied if `options` is
+ * `NULL`.
+ *
+ * `*certificatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_certificate_generate(
+    struct rawrtc_certificate** const certificatep,
+    struct rawrtc_certificate_options* options  // nullable
+) {
+    struct rawrtc_certificate* certificate;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!certificatep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Default options
+    if (!options) {
+        options = &rawrtc_default_certificate_options;
+    }
+
+    // Allocate
+    certificate = mem_zalloc(sizeof(*certificate), rawrtc_certificate_destroy);
+    if (!certificate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Generate key pair
+    switch (options->key_type) {
+        case RAWRTC_CERTIFICATE_KEY_TYPE_RSA:
+            error = generate_key_rsa(&certificate->key, options->modulus_length);
+            break;
+        case RAWRTC_CERTIFICATE_KEY_TYPE_EC:
+            error = generate_key_ecc(&certificate->key, options->named_curve);
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+    if (error) {
+        goto out;
+    }
+
+    // Generate certificate
+    error = generate_self_signed_certificate(
+        &certificate->certificate, certificate->key, options->common_name, options->valid_until,
+        options->sign_algorithm);
+    if (error) {
+        goto out;
+    }
+
+    // Set key type
+    certificate->key_type = options->key_type;
+
+out:
+    if (error) {
+        mem_deref(certificate);
+    } else {
+        // Set pointer
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+/*
+ * Copy a certificate.
+ * References the x509 certificate and private key.
+ */
+enum rawrtc_code rawrtc_certificate_copy(
+    struct rawrtc_certificate** const certificatep,  // de-referenced
+    struct rawrtc_certificate* const source_certificate) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    struct rawrtc_certificate* certificate;
+
+    // Check arguments
+    if (!certificatep || !source_certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    certificate = mem_zalloc(sizeof(*certificate), rawrtc_certificate_destroy);
+    if (!certificate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Increment reference count of certificate and private key, copy the pointers
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+    if (!X509_up_ref(source_certificate->certificate)) {
+        goto out;
+    }
+#else
+    if (!CRYPTO_add(&source_certificate->certificate->references, 1, CRYPTO_LOCK_X509)) {
+        goto out;
+    }
+#endif
+    certificate->certificate = source_certificate->certificate;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+    if (!EVP_PKEY_up_ref(source_certificate->key)) {
+        goto out;
+    }
+#else
+    if (!CRYPTO_add(&source_certificate->key->references, 1, CRYPTO_LOCK_EVP_PKEY)) {
+        goto out;
+    }
+#endif
+    certificate->key = source_certificate->key;
+    certificate->key_type = source_certificate->key_type;
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(certificate);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointer
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+static enum rawrtc_code what_to_encode(
+    enum rawrtc_certificate_encode const to_encode,
+    bool* encode_certificatep,  // de-referenced
+    bool* encode_keyp  // de-referenced
+) {
+    *encode_certificatep = false;
+    *encode_keyp = false;
+
+    // What to encode?
+    switch (to_encode) {
+        case RAWRTC_CERTIFICATE_ENCODE_CERTIFICATE:
+            *encode_certificatep = true;
+            break;
+        case RAWRTC_CERTIFICATE_ENCODE_PRIVATE_KEY:
+            *encode_keyp = true;
+            break;
+        case RAWRTC_CERTIFICATE_ENCODE_BOTH:
+            *encode_certificatep = true;
+            *encode_keyp = true;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get PEM of the certificate and/or the private key if requested.
+ * *pemp will NOT be null-terminated!
+ */
+enum rawrtc_code rawrtc_certificate_get_pem(
+    char** const pemp,  // de-referenced
+    size_t* const pem_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode) {
+    bool encode_certificate;
+    bool encode_key;
+    enum rawrtc_code error;
+    BIO* bio = NULL;
+    char* pem = NULL;
+    uint64_t length;
+
+    // Check arguments
+    if (!pemp || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // What to encode?
+    error = what_to_encode(to_encode, &encode_certificate, &encode_key);
+    if (error) {
+        return error;
+    }
+    error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Create bio structure
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(OPENSSL_IS_BORINGSSL)
+    bio = BIO_new(BIO_s_secmem());
+#else
+    bio = BIO_new(BIO_s_mem());
+#endif
+
+    // Write certificate
+    if (encode_certificate && !PEM_write_bio_X509(bio, certificate->certificate)) {
+        goto out;
+    }
+
+    // Write private key (if requested)
+    if (encode_key && !PEM_write_bio_PrivateKey(bio, certificate->key, NULL, NULL, 0, 0, NULL)) {
+        goto out;
+    }
+
+    // Allocate buffer
+    length = BIO_number_written(bio);
+#if (UINT64_MAX > INT_MAX)
+    if (length > INT_MAX) {
+        goto out;
+    }
+#endif
+    pem = mem_alloc(length, NULL);
+    if (!pem) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Copy to buffer
+    if (BIO_read(bio, pem, (int) length) < (int) length) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (bio) {
+        BIO_free(bio);
+    }
+    if (error) {
+        mem_deref(pem);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointers
+        *pemp = pem;
+        *pem_lengthp = length;
+    }
+    return error;
+}
+
+/*
+ * Get DER of the certificate and/or the private key if requested.
+ * *derp will NOT be null-terminated!
+ */
+enum rawrtc_code rawrtc_certificate_get_der(
+    uint8_t** const derp,  // de-referenced
+    size_t* const der_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode) {
+    bool encode_certificate;
+    bool encode_key;
+    enum rawrtc_code error;
+    int length_certificate = 0;
+    int length_key = 0;
+    size_t length;
+    uint8_t* der = NULL;
+    uint8_t* der_i2d;
+
+    // Check arguments
+    if (!derp || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // What to encode?
+    error = what_to_encode(to_encode, &encode_certificate, &encode_key);
+    if (error) {
+        return error;
+    }
+    error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Allocate buffer
+    if (encode_certificate) {
+        length_certificate = i2d_X509(certificate->certificate, NULL);
+        if (length_certificate < 1) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+    if (encode_key) {
+        length_key = i2d_PrivateKey(certificate->key, NULL);
+        if (length_key < 1) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+    length = (size_t)(length_certificate + length_key);
+    der = mem_alloc(length, NULL);
+    if (!der) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    der_i2d = der;
+
+    // Write certificate
+    if (encode_certificate && i2d_X509(certificate->certificate, &der_i2d) < length_certificate) {
+        goto out;
+    }
+
+    // Write private key (if requested)
+    if (encode_key && i2d_PrivateKey(certificate->key, &der_i2d) < length_key) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(der);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointers
+        *derp = der;
+        *der_lengthp = length;
+    }
+    return error;
+}
+
+/*
+ * Get certificate's fingerprint.
+ * Caller must ensure that `buffer` has space for
+ * `RAWRTC_FINGERPRINT_MAX_SIZE_HEX` bytes
+ */
+enum rawrtc_code rawrtc_certificate_get_fingerprint(
+    char** const fingerprint,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    EVP_MD const* sign_function;
+    uint8_t bytes_buffer[RAWRTC_FINGERPRINT_MAX_SIZE_HEX];
+    uint_least32_t length;
+
+    // Check arguments
+    if (!fingerprint || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get sign function for algorithm
+    sign_function = rawrtc_get_sign_function(algorithm);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Generate certificate fingerprint
+    if (!X509_digest(certificate->certificate, sign_function, bytes_buffer, &length)) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+    if (length < 1) {
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Convert bytes to hex
+    return rawrtc_bin_to_colon_hex(fingerprint, bytes_buffer, (size_t) length);
+}
+
+/*
+ * Copy and append a certificate to a list.
+ */
+static enum rawrtc_code copy_and_append_certificate(
+    struct list* const certificate_list,  // de-referenced, not checked
+    struct rawrtc_certificate* const certificate  // copied
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* copied_certificate;
+
+    // Check arguments
+    if (!certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy certificate
+    // Note: Copying is needed as the 'le' element cannot be associated to multiple lists
+    error = rawrtc_certificate_copy(&copied_certificate, certificate);
+    if (error) {
+        return error;
+    }
+
+    // Append to list
+    list_append(certificate_list, &copied_certificate->le, copied_certificate);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Copy an array of certificates to a list.
+ * Warning: The list will be flushed on error.
+ */
+enum rawrtc_code rawrtc_certificate_array_to_list(
+    struct list* const certificate_list,  // de-referenced, copied into
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates) {
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!certificate_list || !certificates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Append and reference certificates
+    for (i = 0; i < n_certificates; ++i) {
+        error = copy_and_append_certificate(certificate_list, certificates[i]);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        list_flush(certificate_list);
+    }
+    return error;
+}
+
+/*
+ * Copy a certificate list.
+ * Warning: The destination list will be flushed on error.
+ */
+enum rawrtc_code rawrtc_certificate_list_copy(
+    struct list* const destination_list,  // de-referenced, copied into
+    struct list* const source_list  // de-referenced, copied (each item)
+) {
+    struct le* le;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!destination_list || !source_list) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Append and reference certificates
+    for (le = list_head(source_list); le != NULL; le = le->next) {
+        struct rawrtc_certificate* const certificate = le->data;
+        error = copy_and_append_certificate(destination_list, certificate);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        list_flush(destination_list);
+    }
+    return error;
+}
diff --git a/src/certificate/certificate.h b/src/certificate/certificate.h
new file mode 100644
index 0000000..4409928
--- /dev/null
+++ b/src/certificate/certificate.h
@@ -0,0 +1,90 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <re.h>
+#include <openssl/evp.h>  // EVP_*
+#include <openssl/x509.h>  // X509
+
+/*
+ * Maximum digest size of certificate fingerprint.
+ */
+enum {
+    RAWRTC_MODULUS_LENGTH_MIN = 1024,
+    RAWRTC_FINGERPRINT_MAX_SIZE = EVP_MAX_MD_SIZE,
+    RAWRTC_FINGERPRINT_MAX_SIZE_HEX = (EVP_MAX_MD_SIZE * 2),
+};
+
+/*
+ * Certificate options.
+ */
+struct rawrtc_certificate_options {
+    enum rawrtc_certificate_key_type key_type;
+    char* common_name;  // copied
+    uint_fast32_t valid_until;
+    enum rawrtc_certificate_sign_algorithm sign_algorithm;
+    char* named_curve;  // nullable, copied, ignored for RSA
+    uint_fast32_t modulus_length;  // ignored for ECC
+};
+
+/*
+ * Certificate.
+ */
+struct rawrtc_certificate {
+    struct le le;
+    X509* certificate;
+    EVP_PKEY* key;
+    enum rawrtc_certificate_key_type key_type;
+};
+
+extern struct rawrtc_certificate_options rawrtc_default_certificate_options;
+
+enum rawrtc_code rawrtc_certificate_copy(
+    struct rawrtc_certificate** const certificatep,  // de-referenced
+    struct rawrtc_certificate* const source_certificate);
+
+enum rawrtc_code rawrtc_certificate_get_pem(
+    char** const pemp,  // de-referenced
+    size_t* const pem_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode);
+
+enum rawrtc_code rawrtc_certificate_get_der(
+    uint8_t** const derp,  // de-referenced
+    size_t* const der_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode);
+
+enum rawrtc_code rawrtc_certificate_get_fingerprint(
+    char** const fingerprint,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_sign_algorithm const algorithm);
+
+enum rawrtc_code rawrtc_certificate_array_to_list(
+    struct list* const certificate_list,  // de-referenced, copied into
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates);
+
+enum rawrtc_code rawrtc_certificate_list_copy(
+    struct list* const destination_list,  // de-referenced, copied into
+    struct list* const source_list  // de-referenced, copied (each item)
+);
+
+enum tls_keytype rawrtc_certificate_key_type_to_tls_keytype(
+    const enum rawrtc_certificate_key_type type);
+
+enum rawrtc_code rawrtc_tls_keytype_to_certificate_key_type(
+    enum rawrtc_certificate_key_type* const typep,  // de-referenced
+    enum tls_keytype const re_type);
+
+enum rawrtc_code rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+    enum tls_fingerprint* const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm);
+
+enum rawrtc_code rawrtc_tls_fingerprint_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    enum tls_fingerprint re_algorithm);
+
+EVP_MD const* rawrtc_get_sign_function(enum rawrtc_certificate_sign_algorithm type);
+
+enum rawrtc_code rawrtc_get_sign_algorithm_length(
+    size_t* const sizep,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const type);
diff --git a/src/certificate/meson.build b/src/certificate/meson.build
new file mode 100644
index 0000000..9d0e932
--- /dev/null
+++ b/src/certificate/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'certificate.c',
+    'utils.c',
+])
diff --git a/src/certificate/utils.c b/src/certificate/utils.c
new file mode 100644
index 0000000..e9ab58c
--- /dev/null
+++ b/src/certificate/utils.c
@@ -0,0 +1,177 @@
+#include "certificate.h"
+#include <rawrtc/certificate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Translate a certificate key type to the corresponding re type.
+ */
+enum tls_keytype rawrtc_certificate_key_type_to_tls_keytype(
+    enum rawrtc_certificate_key_type const type) {
+    // No conversion needed
+    return (enum tls_keytype) type;
+}
+
+/*
+ * Translate a re key type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_tls_keytype_to_certificate_key_type(
+    enum rawrtc_certificate_key_type* const typep,  // de-referenced
+    enum tls_keytype const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case TLS_KEYTYPE_RSA:
+            *typep = RAWRTC_CERTIFICATE_KEY_TYPE_RSA;
+            return RAWRTC_CODE_SUCCESS;
+        case TLS_KEYTYPE_EC:
+            *typep = RAWRTC_CERTIFICATE_KEY_TYPE_EC;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate a certificate sign algorithm to the corresponding re fingerprint algorithm.
+ */
+enum rawrtc_code rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+    enum tls_fingerprint* const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    switch (algorithm) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384:
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512:
+            // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to re)
+            return RAWRTC_CODE_UNSUPPORTED_ALGORITHM;
+        default:
+            break;
+    }
+
+    // No conversion needed
+    *fingerprintp = (enum tls_fingerprint) algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Translate a re fingerprint algorithm to the corresponding rawrtc algorithm.
+ */
+enum rawrtc_code rawrtc_tls_fingerprint_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    enum tls_fingerprint re_algorithm) {
+    // Check arguments
+    if (!algorithmp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to libre)
+    switch (re_algorithm) {
+        case TLS_FINGERPRINT_SHA256:
+            *algorithmp = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+static enum rawrtc_certificate_sign_algorithm const map_enum_certificate_sign_algorithm[] = {
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384,
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512,
+};
+
+static char const* const map_str_certificate_sign_algorithm[] = {
+    "sha-256",
+    "sha-384",
+    "sha-512",
+};
+
+static size_t const map_certificate_sign_algorithm_length =
+    ARRAY_SIZE(map_enum_certificate_sign_algorithm);
+
+/*
+ * Translate a certificate sign algorithm to str.
+ */
+char const* rawrtc_certificate_sign_algorithm_to_str(
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    size_t i;
+
+    for (i = 0; i < map_certificate_sign_algorithm_length; ++i) {
+        if (map_enum_certificate_sign_algorithm[i] == algorithm) {
+            return map_str_certificate_sign_algorithm[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to a certificate sign algorithm (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!algorithmp || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_certificate_sign_algorithm_length; ++i) {
+        if (str_casecmp(map_str_certificate_sign_algorithm[i], str) == 0) {
+            *algorithmp = map_enum_certificate_sign_algorithm[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Get the EVP_MD* structure for a certificate sign algorithm type.
+ */
+EVP_MD const* rawrtc_get_sign_function(enum rawrtc_certificate_sign_algorithm const type) {
+    switch (type) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256:
+            return EVP_sha256();
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384:
+            return EVP_sha384();
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512:
+            return EVP_sha512();
+        default:
+            return NULL;
+    }
+}
+
+/*
+ * Get the length of the fingerprint to a certificate sign algorithm type.
+ */
+enum rawrtc_code rawrtc_get_sign_algorithm_length(
+    size_t* const sizep,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const type) {
+    EVP_MD const* sign_function;
+    int size;
+
+    // Get sign algorithm function
+    sign_function = rawrtc_get_sign_function(type);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get length
+    size = EVP_MD_size(sign_function);
+    if (size < 1) {
+        return RAWRTC_CODE_UNSUPPORTED_ALGORITHM;
+    }
+
+    // Set size
+    *sizep = (size_t) size;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/diffie_hellman_parameters/meson.build b/src/diffie_hellman_parameters/meson.build
new file mode 100644
index 0000000..7505db0
--- /dev/null
+++ b/src/diffie_hellman_parameters/meson.build
@@ -0,0 +1 @@
+sources += files('parameters.c')
diff --git a/src/diffie_hellman_parameters/parameters.c b/src/diffie_hellman_parameters/parameters.c
new file mode 100644
index 0000000..7783e59
--- /dev/null
+++ b/src/diffie_hellman_parameters/parameters.c
@@ -0,0 +1,190 @@
+#include "parameters.h"
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <openssl/bio.h>  // BIO_new_mem_buf
+#include <openssl/dh.h>  // DH, DH_check_params
+#include <openssl/err.h>  // ERR_clear_error
+#include <openssl/pem.h>  // PEM_read_bio_DHparams
+#include <openssl/ssl.h>  // SSL_CTX_set_tmp_dh, SSL_CTX_set_ecdh_auto
+#include <limits.h>  // INT_MAX, LONG_MAX
+
+#define DEBUG_MODULE "diffie-hellman-parameters"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Apply Diffie-Hellman parameters on an OpenSSL context.
+ */
+static enum rawrtc_code set_dh_parameters(
+    struct ssl_ctx_st* const ssl_context,  // not checked
+    DH const* const dh  // not checked
+) {
+    int codes;
+
+    // Check that the parameters are "likely enough to be valid"
+#if OPENSSL_VERSION_NUMBER < 0x1010000fL || defined(OPENSSL_IS_BORINGSSL)
+    if (!DH_check(dh, &codes)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#else
+    if (!DH_check_params(dh, &codes)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+    if (codes) {
+#if defined(DH_CHECK_P_NOT_PRIME)
+        if (codes & DH_CHECK_P_NOT_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: p is not prime\n");
+        }
+#endif
+#if defined(DH_CHECK_P_NOT_SAFE_PRIME)
+        if (codes & DH_CHECK_P_NOT_SAFE_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: p is not safe prime\n");
+        }
+#endif
+#if defined(DH_UNABLE_TO_CHECK_GENERATOR)
+        if (codes & DH_UNABLE_TO_CHECK_GENERATOR) {
+            DEBUG_WARNING("set_dh_parameters: generator g cannot be checked\n");
+        }
+#endif
+#if defined(DH_NOT_SUITABLE_GENERATOR)
+        if (codes & DH_NOT_SUITABLE_GENERATOR) {
+            DEBUG_WARNING("set_dh_parameters: generator g is not suitable\n");
+        }
+#endif
+#if defined(DH_CHECK_Q_NOT_PRIME)
+        if (codes & DH_CHECK_Q_NOT_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: q is not prime\n");
+        }
+#endif
+#if defined(DH_CHECK_INVALID_Q_VALUE)
+        if (codes & DH_CHECK_INVALID_Q_VALUE) {
+            DEBUG_WARNING("set_dh_parameters: q is invalid\n");
+        }
+#endif
+#if defined(DH_CHECK_INVALID_J_VALUE)
+        if (codes & DH_CHECK_INVALID_J_VALUE) {
+            DEBUG_WARNING("set_dh_parameters: j is invalid\n");
+        }
+#endif
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Apply Diffie-Hellman parameters
+    if (!SSL_CTX_set_tmp_dh(ssl_context, dh)) {
+        DEBUG_WARNING("set_dh_parameters: set_tmp_dh failed\n");
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set Diffie-Hellman parameters on an OpenSSL context using DER encoding.
+ */
+enum rawrtc_code rawrtc_set_dh_parameters_der(
+    struct tls* const tls, uint8_t const* const der, size_t const der_size) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+    DH* dh = NULL;
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Check arguments
+    if (!ssl_context || !der || der_size == 0 || der_size > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Decode PKCS#3 Diffie-Hellman parameters
+    dh = d2i_DHparams(NULL, (unsigned char const**) &der, der_size);
+    if (!dh) {
+        goto out;
+    }
+
+    // Apply Diffie-Hellman parameters
+    error = set_dh_parameters(ssl_context, dh);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (dh) {
+        DH_free(dh);
+    }
+    if (error) {
+        ERR_clear_error();
+    }
+    return error;
+}
+
+/**
+ * Set Diffie-Hellman parameters on an OpenSSL context using PEM encoding.
+ */
+enum rawrtc_code rawrtc_set_dh_parameters_pem(
+    struct tls* const tls, char const* const pem, size_t const pem_size) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+    BIO* bio = NULL;
+    DH* dh = NULL;
+    enum rawrtc_code error = RAWRTC_CODE_NO_MEMORY;
+
+    // Check arguments
+    if (!ssl_context || !pem || pem_size == 0 || pem_size > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create memory sink
+    bio = BIO_new_mem_buf(pem, (int) pem_size);
+    if (!bio) {
+        goto out;
+    }
+
+    // Read Diffie-Hellman parameters into memory sink
+    dh = PEM_read_bio_DHparams(bio, NULL, 0, NULL);
+    if (!dh)
+        goto out;
+
+    // Apply Diffie-Hellman parameters
+    error = set_dh_parameters(ssl_context, dh);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (dh) {
+        DH_free(dh);
+    }
+    if (bio) {
+        BIO_free(bio);
+    }
+    if (error) {
+        ERR_clear_error();
+    }
+    return error;
+}
+
+/*
+ * Enable elliptic-curve Diffie-Hellman on an OpenSSL context.
+ */
+enum rawrtc_code rawrtc_enable_ecdh(struct tls* const tls) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+
+    // Check arguments
+    if (!ssl_context) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Enable elliptic-curve Diffie-Hellman
+    if (!SSL_CTX_set_ecdh_auto(ssl_context, (long) 1)) {
+        DEBUG_WARNING("set_dh_params: set_ecdh_auto failed\n");
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/diffie_hellman_parameters/parameters.h b/src/diffie_hellman_parameters/parameters.h
new file mode 100644
index 0000000..fecf924
--- /dev/null
+++ b/src/diffie_hellman_parameters/parameters.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_set_dh_parameters_der(
+    struct tls* const tls, uint8_t const* const der, size_t const der_size);
+
+enum rawrtc_code rawrtc_set_dh_parameters_pem(
+    struct tls* const tls, char const* const pem, size_t const pem_size);
+
+enum rawrtc_code rawrtc_enable_ecdh(struct tls* const tls);
diff --git a/src/dtls_fingerprint/attributes.c b/src/dtls_fingerprint/attributes.c
new file mode 100644
index 0000000..dc52659
--- /dev/null
+++ b/src/dtls_fingerprint/attributes.c
@@ -0,0 +1,38 @@
+#include "fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the DTLS certificate fingerprint's sign algorithm.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const sign_algorithmp,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint) {
+    // Check arguments
+    if (!sign_algorithmp || !fingerprint) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set sign algorithm
+    *sign_algorithmp = fingerprint->algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the DTLS certificate's fingerprint value.
+ * `*valuep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_value(
+    char** const valuep,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint) {
+    // Check arguments
+    if (!valuep || !fingerprint) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *valuep = mem_ref(fingerprint->value);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_fingerprint/fingerprint.c b/src/dtls_fingerprint/fingerprint.c
new file mode 100644
index 0000000..bfa3b0a
--- /dev/null
+++ b/src/dtls_fingerprint/fingerprint.c
@@ -0,0 +1,74 @@
+#include "fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing DTLS fingerprint instance.
+ */
+static void rawrtc_dtls_fingerprint_destroy(void* arg) {
+    struct rawrtc_dtls_fingerprint* const fingerprint = arg;
+
+    // Un-reference
+    mem_deref(fingerprint->value);
+}
+
+/*
+ * Create a new DTLS fingerprint instance.
+ * `*fingerprintp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_create(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm,
+    char* const value  // copied
+) {
+    struct rawrtc_dtls_fingerprint* fingerprint;
+    enum rawrtc_code error;
+
+    // Allocate
+    fingerprint = mem_zalloc(sizeof(*fingerprint), rawrtc_dtls_fingerprint_destroy);
+    if (!fingerprint) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    fingerprint->algorithm = algorithm;
+    error = rawrtc_strdup(&fingerprint->value, value);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(fingerprint);
+    } else {
+        // Set pointer
+        *fingerprintp = fingerprint;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS fingerprint instance without any value.
+ * The caller MUST set the `value` field after creation.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_create_empty(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    struct rawrtc_dtls_fingerprint* fingerprint;
+
+    // Allocate
+    fingerprint = mem_zalloc(sizeof(*fingerprint), rawrtc_dtls_fingerprint_destroy);
+    if (!fingerprint) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    fingerprint->algorithm = algorithm;
+
+    // Set pointer
+    *fingerprintp = fingerprint;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_fingerprint/fingerprint.h b/src/dtls_fingerprint/fingerprint.h
new file mode 100644
index 0000000..30b788b
--- /dev/null
+++ b/src/dtls_fingerprint/fingerprint.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_dtls_fingerprint {
+    struct le le;
+    enum rawrtc_certificate_sign_algorithm algorithm;
+    char* value;  // copied
+};
+
+enum rawrtc_code rawrtc_dtls_fingerprint_create_empty(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm);
diff --git a/src/dtls_fingerprint/meson.build b/src/dtls_fingerprint/meson.build
new file mode 100644
index 0000000..2e32bbf
--- /dev/null
+++ b/src/dtls_fingerprint/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'fingerprint.c',
+])
diff --git a/src/dtls_parameters/attributes.c b/src/dtls_parameters/attributes.c
new file mode 100644
index 0000000..e8a6a30
--- /dev/null
+++ b/src/dtls_parameters/attributes.c
@@ -0,0 +1,39 @@
+#include "parameters.h"
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the DTLS parameter's role value.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_role(
+    enum rawrtc_dtls_role* rolep,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters) {
+    // Check arguments
+    if (!rolep || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *rolep = parameters->role;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the DTLS parameter's fingerprint array.
+ * `*fingerprintsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_fingerprints(
+    struct rawrtc_dtls_fingerprints** const fingerprintsp,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters) {
+    // Check arguments
+    if (!fingerprintsp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *fingerprintsp = mem_ref(parameters->fingerprints);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_parameters/meson.build b/src/dtls_parameters/meson.build
new file mode 100644
index 0000000..8710eb0
--- /dev/null
+++ b/src/dtls_parameters/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'parameters.c',
+    'utils.c',
+])
diff --git a/src/dtls_parameters/parameters.c b/src/dtls_parameters/parameters.c
new file mode 100644
index 0000000..32dc5a2
--- /dev/null
+++ b/src/dtls_parameters/parameters.c
@@ -0,0 +1,173 @@
+#include "parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing DTLS parameter's fingerprints instance.
+ */
+static void rawrtc_dtls_parameters_fingerprints_destroy(void* arg) {
+    struct rawrtc_dtls_fingerprints* const fingerprints = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        mem_deref(fingerprints->fingerprints[i]);
+    }
+}
+
+/*
+ * Destructor for an existing DTLS parameters instance.
+ */
+static void rawrtc_dtls_parameters_destroy(void* arg) {
+    struct rawrtc_dtls_parameters* const parameters = arg;
+
+    // Un-reference
+    mem_deref(parameters->fingerprints);
+}
+
+/*
+ * Common code to allocate a DTLS parameters instance.
+ */
+static enum rawrtc_code rawrtc_dtls_parameters_allocate(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    size_t const n_fingerprints) {
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    struct rawrtc_dtls_parameters* parameters;
+    size_t fingerprints_size;
+
+    // Allocate parameters
+    parameters = mem_zalloc(sizeof(*parameters), rawrtc_dtls_parameters_destroy);
+    if (!parameters) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set role
+    parameters->role = role;
+
+    // Allocate fingerprints array & set length immediately
+    fingerprints_size = sizeof(*parameters) * n_fingerprints;
+    parameters->fingerprints = mem_zalloc(
+        sizeof(*parameters) + fingerprints_size, rawrtc_dtls_parameters_fingerprints_destroy);
+    if (!parameters->fingerprints) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    parameters->fingerprints->n_fingerprints = n_fingerprints;
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_create(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct rawrtc_dtls_fingerprint* const fingerprints[],  // referenced (each item)
+    size_t const n_fingerprints) {
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_code error;
+    size_t i;
+
+    // Check arguments
+    if (!parametersp || !fingerprints || n_fingerprints < 1) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create parameters
+    error = rawrtc_dtls_parameters_allocate(&parameters, role, n_fingerprints);
+    if (error) {
+        goto out;
+    }
+
+    // Reference and set each fingerprint
+    for (i = 0; i < n_fingerprints; ++i) {
+        // Null?
+        if (!fingerprints[i]) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Check algorithm
+        if (fingerprints[i]->algorithm == RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Reference and set fingerprint
+        parameters->fingerprints->fingerprints[i] = mem_ref(fingerprints[i]);
+    }
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
+
+/*
+ * Create parameters from the internal vars of a DTLS transport
+ * instance.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_create_internal(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct list* const fingerprints) {
+    size_t n_fingerprints;
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_code error;
+    struct le* le;
+    size_t i;
+
+    // Check arguments
+    if (!parametersp || !fingerprints) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get fingerprints length
+    n_fingerprints = list_count(fingerprints);
+
+    // Create parameters
+    error = rawrtc_dtls_parameters_allocate(&parameters, role, n_fingerprints);
+    if (error) {
+        goto out;
+    }
+
+    // Reference and set each fingerprint
+    for (le = list_head(fingerprints), i = 0; le != NULL; le = le->next, ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint = le->data;
+
+        // Check algorithm
+        if (fingerprint->algorithm == RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Reference and set fingerprint
+        parameters->fingerprints->fingerprints[i] = mem_ref(fingerprint);
+    }
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
diff --git a/src/dtls_parameters/parameters.h b/src/dtls_parameters/parameters.h
new file mode 100644
index 0000000..ac018a2
--- /dev/null
+++ b/src/dtls_parameters/parameters.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_dtls_parameters {
+    enum rawrtc_dtls_role role;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+};
+
+enum rawrtc_code rawrtc_dtls_parameters_create_internal(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct list* const fingerprints);
+
+int rawrtc_dtls_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_dtls_parameters const* const parameters);
diff --git a/src/dtls_parameters/utils.c b/src/dtls_parameters/utils.c
new file mode 100644
index 0000000..e79e4c5
--- /dev/null
+++ b/src/dtls_parameters/utils.c
@@ -0,0 +1,40 @@
+#include "parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_transport.h>
+#include <re.h>
+
+/*
+ * Print debug information for DTLS parameters.
+ */
+int rawrtc_dtls_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_dtls_parameters const* const parameters) {
+    int err = 0;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    size_t i;
+
+    // Check arguments
+    if (!parameters) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  DTLS Parameters <%p>:\n", parameters);
+
+    // Role
+    err |= re_hprintf(pf, "    role=%s\n", rawrtc_dtls_role_to_str(parameters->role));
+
+    // Fingerprints
+    fingerprints = parameters->fingerprints;
+    err |= re_hprintf(pf, "    Fingerprints <%p>:\n", fingerprints);
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        // Fingerprint
+        err |= re_hprintf(
+            pf, "      algorithm=%s value=%s\n",
+            rawrtc_certificate_sign_algorithm_to_str(fingerprints->fingerprints[i]->algorithm),
+            fingerprints->fingerprints[i]->value);
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/dtls_transport/attributes.c b/src/dtls_transport/attributes.c
new file mode 100644
index 0000000..411e611
--- /dev/null
+++ b/src/dtls_transport/attributes.c
@@ -0,0 +1,40 @@
+#include "transport.h"
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Check for an existing data transport (on top of DTLS).
+ */
+enum rawrtc_code rawrtc_dtls_transport_have_data_transport(
+    bool* const have_data_transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!have_data_transportp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if a receive handler has been set.
+    if (transport->receive_handler) {
+        *have_data_transportp = true;
+    } else {
+        *have_data_transportp = false;
+    }
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current state of the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_state(
+    enum rawrtc_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_transport/external.c b/src/dtls_transport/external.c
new file mode 100644
index 0000000..2f56096
--- /dev/null
+++ b/src/dtls_transport/external.c
@@ -0,0 +1,64 @@
+#include "transport.h"
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+
+/*
+ * Get external DTLS role.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_external_role(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!rolep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert role
+    switch (transport->role) {
+        case RAWRTC_DTLS_ROLE_AUTO:
+            // Unable to convert in this state
+            return RAWRTC_CODE_INVALID_STATE;
+        case RAWRTC_DTLS_ROLE_CLIENT:
+            *rolep = RAWRTC_EXTERNAL_DTLS_ROLE_CLIENT;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_ROLE_SERVER:
+            *rolep = RAWRTC_EXTERNAL_DTLS_ROLE_SERVER;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+}
+
+/*
+ * Convert DTLS transport state to external DTLS transport state.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_external_state(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert DTLS transport state to external DTLS transport state
+    switch (transport->state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_NEW:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_NEW_OR_CONNECTING;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_NEW_OR_CONNECTING;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CONNECTED;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+}
diff --git a/src/dtls_transport/meson.build b/src/dtls_transport/meson.build
new file mode 100644
index 0000000..3bc7451
--- /dev/null
+++ b/src/dtls_transport/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'attributes.c',
+    'external.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/dtls_transport/transport.c b/src/dtls_transport/transport.c
new file mode 100644
index 0000000..8219697
--- /dev/null
+++ b/src/dtls_transport/transport.c
@@ -0,0 +1,1059 @@
+#include "transport.h"
+#include "../certificate/certificate.h"
+#include "../diffie_hellman_parameters/parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include "../dtls_parameters/parameters.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_transport/transport.h"
+#include "../main/config.h"
+#include "../utils/utils.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/message_buffer.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+#include <string.h>  // memcmp
+
+#define DEBUG_MODULE "dtls-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Embedded DH parameters in DER encoding (bits: 2048)
+ */
+uint8_t const rawrtc_default_dh_parameters[] = {
+    0x30, 0x82, 0x01, 0x08, 0x02, 0x82, 0x01, 0x01, 0x00, 0xaa, 0x4c, 0x1f, 0x1e, 0xc9, 0xed, 0xfe,
+    0x5c, 0x50, 0x2d, 0xff, 0xf4, 0x95, 0xf4, 0x80, 0x69, 0xcf, 0xc3, 0x84, 0x29, 0x87, 0xd5, 0x2c,
+    0x4f, 0xf6, 0x9e, 0x88, 0xa2, 0x5b, 0x61, 0xd2, 0x7d, 0x78, 0x97, 0xce, 0x47, 0x39, 0x9d, 0xc0,
+    0x95, 0x14, 0x98, 0x1f, 0xa9, 0xa3, 0x42, 0x93, 0x58, 0x49, 0x3d, 0xad, 0xeb, 0x6c, 0x3d, 0x79,
+    0x2d, 0x27, 0x94, 0x67, 0x4c, 0xdc, 0x94, 0x31, 0xbf, 0xc1, 0x00, 0x9d, 0x96, 0x4a, 0x91, 0xa7,
+    0x4f, 0xab, 0x48, 0x44, 0xcc, 0x54, 0x1a, 0x4e, 0x2a, 0x8e, 0xa1, 0x81, 0x4b, 0xeb, 0xea, 0xc3,
+    0xba, 0xd6, 0x03, 0xfb, 0xf2, 0x9a, 0x48, 0x1f, 0xc8, 0xba, 0x73, 0x89, 0x86, 0x25, 0x2e, 0xba,
+    0x10, 0x80, 0x2a, 0xeb, 0xf9, 0xe2, 0x28, 0xf1, 0xcf, 0x85, 0x0d, 0xeb, 0x2f, 0x61, 0x51, 0x11,
+    0xe1, 0xe7, 0x82, 0xe5, 0xa7, 0x5d, 0x71, 0x0a, 0xef, 0x8a, 0xe1, 0x97, 0x48, 0x41, 0xac, 0xd7,
+    0xc5, 0xf7, 0xce, 0xd5, 0xcd, 0x66, 0x1e, 0x6b, 0x0e, 0x82, 0x4e, 0x77, 0x5d, 0x89, 0x3b, 0xe2,
+    0x94, 0x7a, 0x10, 0xee, 0x5b, 0x5d, 0x36, 0x07, 0x29, 0x8b, 0x06, 0xb6, 0x49, 0x1e, 0x17, 0x17,
+    0x57, 0xc8, 0xc1, 0x80, 0x24, 0x15, 0x22, 0x9c, 0xb8, 0x59, 0x55, 0x08, 0x41, 0x67, 0x07, 0xca,
+    0xa8, 0x54, 0x1a, 0xd1, 0xb7, 0x91, 0x2f, 0x41, 0x78, 0xc0, 0xcd, 0x2f, 0x07, 0x49, 0x4b, 0xb9,
+    0x05, 0xf4, 0xea, 0x72, 0x3a, 0xcf, 0x04, 0x69, 0xcb, 0x5b, 0xe4, 0xcb, 0x4f, 0x72, 0x40, 0xe4,
+    0x56, 0x1f, 0xca, 0xee, 0x33, 0x2b, 0x29, 0x1a, 0x80, 0xda, 0x01, 0x3f, 0x03, 0xa6, 0xbf, 0x32,
+    0x02, 0x6c, 0xfb, 0xb1, 0xb5, 0x81, 0xda, 0x32, 0x6f, 0xa1, 0x4b, 0x9f, 0x42, 0x2e, 0x17, 0xc9,
+    0x95, 0x30, 0xda, 0x16, 0xb7, 0x9a, 0x7c, 0xf4, 0x83, 0x02, 0x01, 0x02,
+};
+size_t const rawrtc_default_dh_parameters_length = ARRAY_SIZE(rawrtc_default_dh_parameters);
+
+/*
+ * List of default DTLS cipher suites.
+ */
+char const* rawrtc_default_dtls_cipher_suites[] = {
+    "ECDHE-ECDSA-CHACHA20-POLY1305",
+    "ECDHE-RSA-CHACHA20-POLY1305",
+    "ECDHE-ECDSA-AES128-GCM-SHA256",  // recommended
+    "ECDHE-RSA-AES128-GCM-SHA256",
+    "ECDHE-ECDSA-AES256-GCM-SHA384",
+    "ECDHE-RSA-AES256-GCM-SHA384",
+    "DHE-RSA-AES128-GCM-SHA256",
+    "DHE-RSA-AES256-GCM-SHA384",
+    "ECDHE-ECDSA-AES128-SHA256",
+    "ECDHE-RSA-AES128-SHA256",
+    "ECDHE-ECDSA-AES128-SHA",  // required
+    "ECDHE-RSA-AES256-SHA384",
+    "ECDHE-RSA-AES128-SHA",
+    "ECDHE-ECDSA-AES256-SHA384",
+    "ECDHE-ECDSA-AES256-SHA",
+    "ECDHE-RSA-AES256-SHA",
+    "DHE-RSA-AES128-SHA256",
+    "DHE-RSA-AES128-SHA",
+    "DHE-RSA-AES256-SHA256",
+    "DHE-RSA-AES256-SHA",
+};
+size_t const rawrtc_default_dtls_cipher_suites_length =
+    ARRAY_SIZE(rawrtc_default_dtls_cipher_suites);
+
+/*
+ * Handle outgoing buffered DTLS messages.
+ */
+static bool dtls_outgoing_buffer_handler(
+    struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+    (void) context;
+
+    // Send
+    error = rawrtc_dtls_transport_send(transport, buffer);
+    if (error) {
+        DEBUG_WARNING("Could not send buffered packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Change the state of the ICE transport.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_state(
+    struct rawrtc_dtls_transport* const transport, enum rawrtc_dtls_transport_state const state) {
+    // Closed or failed: Remove connection
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CLOSED ||
+        state == RAWRTC_DTLS_TRANSPORT_STATE_FAILED) {
+        // Remove connection
+        transport->connection = mem_deref(transport->connection);
+
+        // Remove self from ICE transport (if attached)
+        transport->ice_transport->dtls_transport = NULL;
+    }
+
+    // Set state
+    transport->state = state;
+
+    // Connected?
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        // Send buffered outgoing DTLS messages
+        enum rawrtc_code const error = rawrtc_message_buffer_clear(
+            &transport->buffered_messages_out, dtls_outgoing_buffer_handler, transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Could not send buffered messages, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+/*
+ * Check if the state is 'closed' or 'failed'.
+ */
+static bool is_closed(struct rawrtc_dtls_transport* const transport  // not checked
+) {
+    switch (transport->state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            return true;
+        default:
+            return false;
+    }
+}
+
+/*
+ * DTLS connection closed handler.
+ */
+static void close_handler(int err, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+
+    // Closed?
+    if (!is_closed(transport)) {
+        DEBUG_INFO("DTLS connection closed, reason: %m\n", err);
+
+        // Set to failed if not closed normally
+        if (err != ECONNRESET) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_FAILED);
+        }
+
+        // Stop
+        error = rawrtc_dtls_transport_stop(transport);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS connection closed, could not stop transport: %s\n",
+                rawrtc_code_to_str(error));
+        }
+    } else {
+        DEBUG_PRINTF(
+            "DTLS connection closed (but state is already closed anyway), reason: %m\n", err);
+    }
+}
+
+/*
+ * Handle incoming DTLS messages.
+ */
+static void dtls_receive_handler(struct mbuf* buffer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_PRINTF("Ignoring incoming DTLS message, transport is closed\n");
+        return;
+    }
+
+    // Handle (if receive handler exists and connected)
+    // Note: Checking for 'connected' state ensures that no data will be received before the
+    //       fingerprints have been verified.
+    if (transport->receive_handler && transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        transport->receive_handler(buffer, transport->receive_handler_arg);
+        return;
+    }
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&transport->buffered_messages_in, buffer, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not buffer incoming packet, reason: %s\n", rawrtc_code_to_str(error));
+    } else {
+        DEBUG_PRINTF("Buffered incoming packet of size %zu\n", mbuf_get_left(buffer));
+    }
+}
+
+/*
+ * Either called by a DTLS connection established event or by the
+ * `start` method of the DTLS transport.
+ * The caller MUST make sure that remote parameters are available and
+ * that the state is NOT 'closed' or 'failed'!
+ */
+static void verify_certificate(struct rawrtc_dtls_transport* const transport  // not checked
+) {
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    bool valid = false;
+    enum tls_fingerprint algorithm;
+    uint8_t expected_fingerprint[RAWRTC_FINGERPRINT_MAX_SIZE];
+    uint8_t actual_fingerprint[RAWRTC_FINGERPRINT_MAX_SIZE];
+
+    // Verify the peer's certificate
+    // TODO: Fix this. Testing the fingerprint alone is okay for now though.
+    // error = rawrtc_error_to_code(tls_peer_verify(transport->connection));
+    // if (error) {
+    //     goto out;
+    // }
+    // DEBUG_PRINTF("Peer's certificate verified\n");
+
+    // Check if *any* of the fingerprints provided matches
+    // Note: We don't verify the peer's certificate since it will almost always
+    //       be self-signed.
+    for (i = 0; i < transport->remote_parameters->fingerprints->n_fingerprints; ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint =
+            transport->remote_parameters->fingerprints->fingerprints[i];
+        size_t length;
+        size_t bytes_written;
+
+        // Get algorithm
+        error = rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+            &algorithm, fingerprint->algorithm);
+        if (error) {
+            if (error == RAWRTC_CODE_UNSUPPORTED_ALGORITHM) {
+                continue;
+            }
+            goto out;
+        }
+
+        // Get algorithm digest size
+        error = rawrtc_get_sign_algorithm_length(&length, fingerprint->algorithm);
+        if (error) {
+            if (error == RAWRTC_CODE_UNSUPPORTED_ALGORITHM) {
+                continue;
+            }
+            goto out;
+        }
+
+        // Convert hex-encoded value to binary
+        error = rawrtc_colon_hex_to_bin(
+            &bytes_written, expected_fingerprint, length, fingerprint->value);
+        if (error) {
+            if (error == RAWRTC_CODE_INSUFFICIENT_SPACE) {
+                DEBUG_WARNING("Hex-encoded fingerprint exceeds buffer size!\n");
+            } else {
+                DEBUG_WARNING(
+                    "Could not convert hex-encoded fingerprint to binary, reason: %s\n",
+                    rawrtc_code_to_str(error));
+            }
+            continue;
+        }
+
+        // Validate length
+        if (bytes_written != length) {
+            DEBUG_WARNING(
+                "Hex-encoded fingerprint should have been %zu bytes but was %zu bytes\n", length,
+                bytes_written);
+            continue;
+        }
+
+        // Get remote fingerprint
+        error = rawrtc_error_to_code(tls_peer_fingerprint(
+            transport->connection, algorithm, actual_fingerprint, sizeof(actual_fingerprint)));
+        if (error) {
+            goto out;
+        }
+
+        // Compare fingerprints
+        if (memcmp(expected_fingerprint, actual_fingerprint, length) == 0) {
+            DEBUG_PRINTF("Peer's certificate fingerprint is valid\n");
+            valid = true;
+        }
+    }
+
+out:
+    if (error || !valid) {
+        DEBUG_WARNING("Verifying certificate failed, reason: %s\n", rawrtc_code_to_str(error));
+        if (!is_closed(transport)) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_FAILED);
+        }
+
+        // Stop
+        error = rawrtc_dtls_transport_stop(transport);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS connection closed, could not stop transport: %s\n",
+                rawrtc_code_to_str(error));
+        }
+    } else {
+        // Connected
+        set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED);
+    }
+}
+
+/*
+ * Handle DTLS connection established event.
+ */
+static void establish_handler(void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_WARNING("Ignoring established DTLS connection, transport is closed\n");
+        return;
+    }
+
+    // Connection established
+    // Note: State is either 'NEW', 'CONNECTING' or 'FAILED' here
+    DEBUG_INFO("DTLS connection established\n");
+    transport->connection_established = true;
+
+    // Verify certificate & fingerprint (if remote parameters are available)
+    if (transport->remote_parameters) {
+        verify_certificate(transport);
+    }
+}
+
+/*
+ * Handle incoming DTLS connection.
+ */
+static void connect_handler(const struct sa* peer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    bool role_is_server;
+    bool have_connection;
+    int err;
+    (void) peer;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_PRINTF("Ignoring incoming DTLS connection, transport is closed\n");
+        return;
+    }
+
+    // Update role if "auto"
+    if (transport->role == RAWRTC_DTLS_ROLE_AUTO) {
+        DEBUG_PRINTF("Switching role 'auto' -> 'server'\n");
+        transport->role = RAWRTC_DTLS_ROLE_SERVER;
+    }
+
+    // Accept?
+    role_is_server = transport->role == RAWRTC_DTLS_ROLE_SERVER;
+    have_connection = transport->connection != NULL;
+    if (role_is_server && !have_connection) {
+        // Set state to connecting (if not already set)
+        if (transport->state != RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING);
+        }
+
+        // Accept and create connection
+        DEBUG_PRINTF("Accepting incoming DTLS connection from %J\n", peer);
+        err = dtls_accept(
+            &transport->connection, transport->context, transport->socket, establish_handler,
+            dtls_receive_handler, close_handler, transport);
+        if (err) {
+            DEBUG_WARNING("Could not accept incoming DTLS connection, reason: %m\n", err);
+        }
+    } else {
+        if (have_connection) {
+            DEBUG_WARNING("Incoming DTLS connect but we already have a connection\n");
+        }
+        if (!role_is_server) {
+            DEBUG_WARNING("Incoming DTLS connect but role is 'client'\n");
+        }
+    }
+}
+
+/*
+ * Initiate a DTLS connect.
+ */
+static enum rawrtc_code do_connect(
+    struct rawrtc_dtls_transport* const transport, const struct sa* const peer) {
+    // Connect
+    DEBUG_PRINTF("Starting DTLS connection to %J\n", peer);
+    return rawrtc_error_to_code(dtls_connect(
+        &transport->connection, transport->context, transport->socket, peer, establish_handler,
+        dtls_receive_handler, close_handler, transport));
+}
+
+/*
+ * Handle outgoing DTLS messages.
+ */
+static int send_handler(
+    struct tls_conn* tc, struct sa const* original_destination, struct mbuf* buffer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct trice* const ice = transport->ice_transport->gatherer->ice;
+    bool closed = is_closed(transport);
+    struct ice_candpair* candidate_pair;
+    struct udp_sock* udp_socket;
+    int err;
+    (void) tc;
+    (void) original_destination;
+
+    // Note: No need to check if closed as only non-application data may be sent if the
+    //       transport is already closed.
+
+    // Get candidate pair with highest priority
+    // Note: This ignores whatever is nominated
+    // TODO: Should we rather use the nominated candidate pair?
+    candidate_pair = list_ledata(list_head(trice_validl(ice)));
+    if (!candidate_pair) {
+        if (!closed) {
+            DEBUG_WARNING("Cannot send message, no valid candidate pair\n");
+        }
+        return ECONNRESET;
+    }
+
+    // Get local candidate's UDP socket
+    // TODO: What about TCP?
+    udp_socket = trice_lcand_sock(ice, candidate_pair->lcand);
+    if (!udp_socket) {
+        if (!closed) {
+            DEBUG_WARNING("Cannot send message, selected candidate pair has no socket\n");
+        }
+        return ECONNRESET;
+    }
+
+    // Send
+    // TODO: Is destination correct?
+    DEBUG_PRINTF(
+        "Sending DTLS message (%zu bytes) to %J (originally: %J) from %J\n", mbuf_get_left(buffer),
+        &candidate_pair->rcand->attr.addr, original_destination, &candidate_pair->lcand->attr.addr);
+    err = udp_send(udp_socket, &candidate_pair->rcand->attr.addr, buffer);
+    if (err) {
+        DEBUG_WARNING("Could not send, error: %m\n", err);
+    }
+    return err;
+}
+
+/*
+ * Handle MTU queries.
+ */
+static size_t mtu_handler(struct tls_conn* tc, void* arg) {
+    (void) tc;
+    (void) arg;
+    // TODO: Choose a sane value.
+    return 1400;
+}
+
+/*
+ * Handle received UDP messages.
+ */
+static bool udp_receive_handler(struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct sa* source = context;
+    struct sa const* peer;
+
+    // TODO: This handler should be moved into ICE transport
+    // https://tools.ietf.org/search/rfc7983#section-7
+
+    // Update remote peer address (if changed and connection exists)
+    if (transport->connection) {
+        // TODO: It would be cleaner to check if source is in our list of remote candidates
+
+        // TODO: SCTP - Retest path MTU and reset congestion state to the initial state
+        // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+
+        // Update if changed
+        peer = dtls_peer(transport->connection);
+        if (!sa_cmp(peer, source, SA_ALL)) {
+            DEBUG_PRINTF("Remote changed its peer address from %J to %J\n", peer, source);
+            dtls_set_peer(transport->connection, source);
+        }
+    }
+
+    // Decrypt & receive
+    // Note: No need to check if the transport is already closed as the messages will re-appear in
+    //       the `dtls_receive_handler`.
+    dtls_receive(transport->socket, source, buffer);
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Handle received UDP messages (UDP receive helper).
+ */
+static bool udp_receive_helper(struct sa* source, struct mbuf* buffer, void* arg) {
+    // Receive
+    udp_receive_handler(buffer, source, arg);
+
+    // Handled
+    return true;
+}
+
+/*
+ * Destructor for an existing DTLS transport.
+ */
+static void rawrtc_dtls_transport_destroy(void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct le* le;
+
+    // Stop transport
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_dtls_transport_stop(transport);
+
+    // TODO: Remove once ICE transport and DTLS transport have been separated properly
+    for (le = list_head(&transport->ice_transport->gatherer->local_candidates); le != NULL;
+         le = le->next) {
+        struct rawrtc_candidate_helper* const candidate_helper = le->data;
+        mem_deref(candidate_helper->udp_helper);
+        // TODO: Be aware that UDP packets go to nowhere now...
+    }
+
+    // Un-reference
+    mem_deref(transport->connection);
+    mem_deref(transport->socket);
+    mem_deref(transport->context);
+    list_flush(&transport->fingerprints);
+    list_flush(&transport->buffered_messages_out);
+    list_flush(&transport->buffered_messages_in);
+    mem_deref(transport->remote_parameters);
+    list_flush(&transport->certificates);
+    mem_deref(transport->ice_transport);
+}
+
+/*
+ * Create a new DTLS transport (internal)
+ */
+enum rawrtc_code rawrtc_dtls_transport_create_internal(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct list* certificates,  // de-referenced, copied (shallow)
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_dtls_transport* transport;
+    enum rawrtc_code error;
+    struct le* le;
+    struct rawrtc_certificate* certificate;
+    uint8_t* certificate_der;
+    size_t certificate_der_length;
+
+    // Check arguments
+    if (!transportp || !ice_transport || !certificates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Check certificates expiration dates
+
+    // Check ICE transport state
+    if (ice_transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Check if another DTLS transport is associated to the ICE transport
+    if (ice_transport->dtls_transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_dtls_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    transport->state = RAWRTC_DTLS_TRANSPORT_STATE_NEW;  // TODO: Raise state (delayed)?
+    transport->ice_transport = mem_ref(ice_transport);
+    transport->certificates = *certificates;
+    transport->state_change_handler = state_change_handler;
+    transport->error_handler = error_handler;
+    transport->arg = arg;
+    transport->role = RAWRTC_DTLS_ROLE_AUTO;
+    transport->connection_established = false;
+    list_init(&transport->buffered_messages_in);
+    list_init(&transport->buffered_messages_out);
+    list_init(&transport->fingerprints);
+
+    // Create (D)TLS context
+    DEBUG_PRINTF("Creating DTLS context\n");
+    error = rawrtc_error_to_code(tls_alloc(&transport->context, TLS_METHOD_DTLS, NULL, NULL));
+    if (error) {
+        goto out;
+    }
+
+    // Get DER encoded certificate of choice
+    // TODO: Which certificate should we use?
+    certificate = list_ledata(list_head(&transport->certificates));
+    error = rawrtc_certificate_get_der(
+        &certificate_der, &certificate_der_length, certificate, RAWRTC_CERTIFICATE_ENCODE_BOTH);
+    if (error) {
+        goto out;
+    }
+
+    // Set certificate
+    DEBUG_PRINTF("Setting certificate on DTLS context\n");
+    error = rawrtc_error_to_code(tls_set_certificate_der(
+        transport->context, rawrtc_certificate_key_type_to_tls_keytype(certificate->key_type),
+        certificate_der, certificate_der_length, NULL, 0));
+    mem_deref(certificate_der);
+    if (error) {
+        goto out;
+    }
+
+    // Set Diffie-Hellman parameters
+    // TODO: Get whether to apply DH parameters from config
+    // TODO: Get DH params from config
+    DEBUG_PRINTF("Setting DH parameters on DTLS context\n");
+    error = rawrtc_set_dh_parameters_der(
+        transport->context, rawrtc_default_dh_parameters, rawrtc_default_dh_parameters_length);
+    if (error) {
+        goto out;
+    }
+
+    // Enable elliptic-curve Diffie-Hellman
+    // TODO: Get whether to enable ECDH from config
+    DEBUG_PRINTF("Enabling ECDH on DTLS context\n");
+    error = rawrtc_enable_ecdh(transport->context);
+    if (error) {
+        goto out;
+    }
+
+    // Set cipher suites
+    // TODO: Get cipher suites from config
+    DEBUG_PRINTF("Setting cipher suites on DTLS context\n");
+    error = rawrtc_error_to_code(tls_set_ciphers(
+        transport->context, rawrtc_default_dtls_cipher_suites,
+        rawrtc_default_dtls_cipher_suites_length));
+    if (error) {
+        goto out;
+    }
+
+    // Send client certificate (client) / request client certificate (server)
+    tls_set_verify_client(transport->context);
+
+    // Create DTLS socket
+    DEBUG_PRINTF("Creating DTLS socket\n");
+    error = rawrtc_error_to_code(dtls_socketless(
+        &transport->socket, 1, connect_handler, send_handler, mtu_handler, transport));
+    if (error) {
+        goto out;
+    }
+
+    // Attach to existing candidate pairs
+    for (le = list_head(trice_validl(ice_transport->gatherer->ice)); le != NULL; le = le->next) {
+        struct ice_candpair* candidate_pair = le->data;
+        error = rawrtc_dtls_transport_add_candidate_pair(transport, candidate_pair);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS transport could not attach to candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+    // Attach to ICE transport
+    // Note: We cannot reference ourselves here as that would introduce a cyclic reference
+    ice_transport->dtls_transport = transport;
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS transport.
+ * `*transport` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_transport_create(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates,
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    struct list certificates_list = LIST_INIT;
+
+    // Append and reference certificates
+    error = rawrtc_certificate_array_to_list(&certificates_list, certificates, n_certificates);
+    if (error) {
+        return error;
+    }
+
+    // Create DTLS transport
+    return rawrtc_dtls_transport_create_internal(
+        transportp, ice_transport, &certificates_list, state_change_handler, error_handler, arg);
+}
+
+/*
+ * Let the DTLS transport attach itself to a candidate pair.
+ * TODO: Separate ICE transport and DTLS transport properly (like data transport)
+ */
+enum rawrtc_code rawrtc_dtls_transport_add_candidate_pair(
+    struct rawrtc_dtls_transport* const transport, struct ice_candpair* const candidate_pair) {
+    enum rawrtc_code error;
+    struct rawrtc_candidate_helper* candidate_helper = NULL;
+
+    // Check arguments
+    if (!transport || !candidate_pair) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Check if already attached
+
+    // Find candidate helper
+    error = rawrtc_candidate_helper_find(
+        &candidate_helper, &transport->ice_transport->gatherer->local_candidates,
+        candidate_pair->lcand);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not find matching candidate helper for candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Receive buffered packets
+    error = rawrtc_message_buffer_clear(
+        &transport->ice_transport->gatherer->buffered_messages, udp_receive_handler, transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not handle buffered packets on candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Attach this transport's receive handler
+    error = rawrtc_candidate_helper_set_receive_handler(
+        candidate_helper, udp_receive_helper, transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not find matching candidate helper for candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Do connect (if client and no connection)
+    if (transport->role == RAWRTC_DTLS_ROLE_CLIENT && !transport->connection) {
+        error = do_connect(transport, &candidate_pair->rcand->attr.addr);
+        if (error) {
+            DEBUG_WARNING(
+                "Could not start DTLS connection for candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+out:
+    if (!error) {
+        DEBUG_PRINTF("Attached DTLS transport to candidate pair\n");
+    }
+    return error;
+}
+
+/*
+ * Start the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_start(
+    struct rawrtc_dtls_transport* const transport,
+    struct rawrtc_dtls_parameters* const remote_parameters  // referenced
+) {
+    enum rawrtc_code error;
+    enum rawrtc_ice_role ice_role;
+
+    // Check arguments
+    if (!transport || !remote_parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate parameters
+    if (remote_parameters->fingerprints->n_fingerprints < 1) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    // Note: Checking for 'remote_parameters' ensures that 'start' is not called twice
+    if (transport->remote_parameters || is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set state to connecting (if not already set)
+    if (transport->state != RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+        set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING);
+    }
+
+    // Get ICE role
+    error = rawrtc_ice_transport_get_role(&ice_role, transport->ice_transport);
+    if (error) {
+        return error;
+    }
+
+    // Determine role
+    if (remote_parameters->role == RAWRTC_DTLS_ROLE_AUTO) {
+        switch (ice_role) {
+            case RAWRTC_ICE_ROLE_CONTROLLED:
+                transport->role = RAWRTC_DTLS_ROLE_CLIENT;
+                DEBUG_PRINTF("Switching role 'auto' -> 'client'\n");
+                break;
+            case RAWRTC_ICE_ROLE_CONTROLLING:
+                transport->role = RAWRTC_DTLS_ROLE_SERVER;
+                DEBUG_PRINTF("Switching role 'auto' -> 'server'\n");
+                break;
+            default:
+                // Cannot continue if ICE transport role is unknown
+                DEBUG_WARNING("ICE role must be set before DTLS transport can be started!\n");
+                return RAWRTC_CODE_INVALID_STATE;
+        }
+    } else if (remote_parameters->role == RAWRTC_DTLS_ROLE_SERVER) {
+        transport->role = RAWRTC_DTLS_ROLE_CLIENT;
+        DEBUG_PRINTF("Switching role 'server' -> 'client'\n");
+    } else {
+        transport->role = RAWRTC_DTLS_ROLE_SERVER;
+        DEBUG_PRINTF("Switching role 'client' -> 'server'\n");
+    }
+
+    // Connect (if client)
+    if (transport->role == RAWRTC_DTLS_ROLE_CLIENT) {
+        struct ice_candpair* candidate_pair;
+
+        // Reset existing connections
+        if (transport->connection) {
+            // Note: This is needed as ORTC requires us to reset previous DTLS connections
+            //       if the remote role is 'server'
+            DEBUG_PRINTF("Resetting DTLS connection\n");
+            transport->connection = mem_deref(transport->connection);
+            transport->connection_established = false;
+        }
+
+        // Get selected candidate pair
+        candidate_pair =
+            list_ledata(list_head(trice_validl(transport->ice_transport->gatherer->ice)));
+
+        // Do connect (if we have a valid candidate pair)
+        if (candidate_pair) {
+            error = do_connect(transport, &candidate_pair->rcand->attr.addr);
+            if (error) {
+                goto out;
+            }
+        }
+    } else {
+        // Verify certificate & fingerprint (if connection is established)
+        if (transport->connection_established) {
+            verify_certificate(transport);
+        }
+    }
+
+out:
+    if (error) {
+        transport->connection = mem_deref(transport->connection);
+    } else {
+        // Set remote parameters
+        transport->remote_parameters = mem_ref(remote_parameters);
+    }
+    return error;
+}
+
+/*
+ * Pipe buffered messages into the data receive handler that has a
+ * different signature.
+ */
+static bool intermediate_receive_handler(
+    struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    (void) context;
+
+    // Pipe into the actual receive handler
+    if (transport->receive_handler) {
+        transport->receive_handler(buffer, transport->receive_handler_arg);
+    } else {
+        DEBUG_WARNING("No receive handler, discarded %zu bytes\n", mbuf_get_left(buffer));
+    }
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Set a data transport on the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_set_data_transport(
+    struct rawrtc_dtls_transport* const transport,
+    rawrtc_dtls_transport_receive_handler const receive_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+
+    // Check arguments
+    if (!transport || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check for existing data transport
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, transport);
+    if (error) {
+        return error;
+    }
+    if (have_data_transport) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set handler
+    transport->receive_handler = receive_handler;
+    transport->receive_handler_arg = arg;
+
+    // Receive buffered messages
+    error = rawrtc_message_buffer_clear(
+        &transport->buffered_messages_in, intermediate_receive_handler, transport);
+    if (error) {
+        return error;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Remove an existing data transport from the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_clear_data_transport(
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Clear buffered messages (?)
+
+    // Clear handler and argument
+    transport->receive_handler = NULL;
+    transport->receive_handler_arg = NULL;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Send a data message over the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_send(
+    struct rawrtc_dtls_transport* const transport, struct mbuf* const buffer) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !buffer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Connected?
+    if (transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        return rawrtc_error_to_code(dtls_send(transport->connection, buffer));
+    }
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&transport->buffered_messages_out, buffer, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not buffer outgoing packet, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Buffered message
+    DEBUG_PRINTF("Buffered outgoing packet of size %zu\n", mbuf_get_left(buffer));
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Stop and close the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_stop(struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update state
+    set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get local DTLS parameters of a transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_local_parameters(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // TODO: Get config from struct
+    enum rawrtc_certificate_sign_algorithm const algorithm = rawrtc_default_config.sign_algorithm;
+    struct le* le;
+    struct rawrtc_dtls_fingerprint* fingerprint;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!parametersp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Lazy-create fingerprints
+    if (list_isempty(&transport->fingerprints)) {
+        for (le = list_head(&transport->certificates); le != NULL; le = le->next) {
+            struct rawrtc_certificate* certificate = le->data;
+
+            // Create fingerprint
+            error = rawrtc_dtls_fingerprint_create_empty(&fingerprint, algorithm);
+            if (error) {
+                return error;
+            }
+
+            // Get and set fingerprint of certificate
+            error = rawrtc_certificate_get_fingerprint(&fingerprint->value, certificate, algorithm);
+            if (error) {
+                return error;
+            }
+
+            // Append fingerprint
+            list_append(&transport->fingerprints, &fingerprint->le, fingerprint);
+        }
+    }
+
+    // Create and return DTLS parameters instance
+    return rawrtc_dtls_parameters_create_internal(
+        parametersp, transport->role, &transport->fingerprints);
+}
diff --git a/src/dtls_transport/transport.h b/src/dtls_transport/transport.h
new file mode 100644
index 0000000..a954d0a
--- /dev/null
+++ b/src/dtls_transport/transport.h
@@ -0,0 +1,68 @@
+#pragma once
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Handle inbound application data.
+ */
+typedef void (*rawrtc_dtls_transport_receive_handler)(struct mbuf* const buffer, void* const arg);
+
+struct rawrtc_dtls_transport {
+    enum rawrtc_dtls_transport_state state;
+    struct rawrtc_ice_transport* ice_transport;  // referenced
+    struct list certificates;  // deep-copied
+    rawrtc_dtls_transport_state_change_handler state_change_handler;  // nullable
+    rawrtc_dtls_transport_error_handler error_handler;  // nullable
+    void* arg;  // nullable
+    struct rawrtc_dtls_parameters* remote_parameters;  // referenced
+    enum rawrtc_dtls_role role;
+    bool connection_established;
+    struct list buffered_messages_in;
+    struct list buffered_messages_out;
+    struct list fingerprints;
+    struct tls* context;
+    struct dtls_sock* socket;
+    struct tls_conn* connection;
+    rawrtc_dtls_transport_receive_handler receive_handler;
+    void* receive_handler_arg;
+};
+
+enum rawrtc_code rawrtc_dtls_transport_create_internal(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct list* certificates,  // de-referenced, copied (shallow)
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+);
+
+enum rawrtc_code rawrtc_dtls_transport_add_candidate_pair(
+    struct rawrtc_dtls_transport* const transport, struct ice_candpair* const candidate_pair);
+
+enum rawrtc_code rawrtc_dtls_transport_have_data_transport(
+    bool* const have_data_transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_set_data_transport(
+    struct rawrtc_dtls_transport* const transport,
+    rawrtc_dtls_transport_receive_handler const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_dtls_transport_clear_data_transport(
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_send(
+    struct rawrtc_dtls_transport* const transport, struct mbuf* const buffer);
+
+enum rawrtc_code rawrtc_dtls_transport_get_external_role(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_get_external_state(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
diff --git a/src/dtls_transport/utils.c b/src/dtls_transport/utils.c
new file mode 100644
index 0000000..7b96fc5
--- /dev/null
+++ b/src/dtls_transport/utils.c
@@ -0,0 +1,75 @@
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_dtls_transport_state_to_name(enum rawrtc_dtls_transport_state const state) {
+    switch (state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            return "failed";
+        default:
+            return "???";
+    }
+}
+
+static enum rawrtc_dtls_role const map_enum_dtls_role[] = {
+    RAWRTC_DTLS_ROLE_AUTO,
+    RAWRTC_DTLS_ROLE_CLIENT,
+    RAWRTC_DTLS_ROLE_SERVER,
+};
+
+static char const* const map_str_dtls_role[] = {
+    "auto",
+    "client",
+    "server",
+};
+
+static size_t const map_dtls_role_length = ARRAY_SIZE(map_enum_dtls_role);
+
+/*
+ * Translate a DTLS role to str.
+ */
+char const* rawrtc_dtls_role_to_str(enum rawrtc_dtls_role const role) {
+    size_t i;
+
+    for (i = 0; i < map_dtls_role_length; ++i) {
+        if (map_enum_dtls_role[i] == role) {
+            return map_str_dtls_role[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to a DTLS role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_dtls_role(
+    enum rawrtc_dtls_role* const rolep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!rolep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_dtls_role_length; ++i) {
+        if (str_casecmp(map_str_dtls_role[i], str) == 0) {
+            *rolep = map_enum_dtls_role[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
diff --git a/src/ice_candidate/attributes.c b/src/ice_candidate/attributes.c
new file mode 100644
index 0000000..822c770
--- /dev/null
+++ b/src/ice_candidate/attributes.c
@@ -0,0 +1,296 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the ICE candidate's foundation.
+ * `*foundationp` will be set to a copy of the foundation that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_foundation(
+    char** const foundationp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !foundationp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied foundation
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            return rawrtc_strdup(foundationp, candidate->candidate.raw_candidate->foundation);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_strdup(
+                foundationp, candidate->candidate.local_candidate->attr.foundation);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_strdup(
+                foundationp, candidate->candidate.remote_candidate->attr.foundation);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's priority.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_priority(
+    uint32_t* const priorityp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !priorityp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set priority
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *priorityp = candidate->candidate.raw_candidate->priority;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            *priorityp = candidate->candidate.local_candidate->attr.prio;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            *priorityp = candidate->candidate.remote_candidate->attr.prio;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's IP address.
+ * `*ipp` will be set to a copy of the IP address that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_ip(
+    char** const ipp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !ipp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied IP address
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            return rawrtc_strdup(ipp, candidate->candidate.raw_candidate->ip);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_sdprintf(ipp, "%j", &candidate->candidate.local_candidate->attr.addr);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_sdprintf(ipp, "%j", &candidate->candidate.remote_candidate->attr.addr);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's protocol.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    int ipproto;
+
+    // Check arguments
+    if (!candidate || !protocolp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set protocol
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *protocolp = candidate->candidate.raw_candidate->protocol;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            ipproto = candidate->candidate.local_candidate->attr.proto;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            ipproto = candidate->candidate.remote_candidate->attr.proto;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+    return rawrtc_ipproto_to_ice_protocol(protocolp, ipproto);
+}
+
+/*
+ * Get the ICE candidate's port.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !portp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *portp = candidate->candidate.raw_candidate->port;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            *portp = sa_port(&candidate->candidate.local_candidate->attr.addr);
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            *portp = sa_port(&candidate->candidate.remote_candidate->attr.addr);
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's type.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_type(
+    enum rawrtc_ice_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set type
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *typep = candidate->candidate.raw_candidate->type;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_ice_cand_type_to_ice_candidate_type(
+                typep, candidate->candidate.local_candidate->attr.type);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_ice_cand_type_to_ice_candidate_type(
+                typep, candidate->candidate.remote_candidate->attr.type);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's TCP type.
+ * Return `RAWRTC_CODE_NO_VALUE` in case the protocol is not TCP.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_tcp_type(
+    enum rawrtc_ice_tcp_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate;
+
+    // Check arguments
+    if (!candidate || !typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set type/get re candidate
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *typep = candidate->candidate.raw_candidate->tcp_type;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set type from re candidate if TCP
+    if (re_candidate->proto == IPPROTO_TCP) {
+        return rawrtc_ice_tcptype_to_ice_tcp_candidate_type(typep, re_candidate->tcptype);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the ICE candidate's related IP address.
+ * `*related_address` will be set to a copy of the related address that
+ * must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related address exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_address(
+    char** const related_addressp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate = NULL;
+
+    // Check arguments
+    if (!candidate || !related_addressp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied related IP address/get re candidate
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            if (candidate->candidate.raw_candidate->related_address) {
+                return rawrtc_strdup(
+                    related_addressp, candidate->candidate.raw_candidate->related_address);
+            }
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set copied related IP address from re candidate
+    if (re_candidate && sa_isset(&re_candidate->rel_addr, SA_ADDR)) {
+        return rawrtc_sdprintf(
+            related_addressp, "%j", &candidate->candidate.local_candidate->attr.rel_addr);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the ICE candidate's related IP address' port.
+ * `*related_portp` will be set to a copy of the related address'
+ * port.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related port exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_port(
+    uint16_t* const related_portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate = NULL;
+
+    // Check arguments
+    if (!candidate || !related_portp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            if (candidate->candidate.raw_candidate->related_address) {
+                *related_portp = candidate->candidate.raw_candidate->related_port;
+                return RAWRTC_CODE_SUCCESS;
+            }
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set copied related IP address' port from re candidate
+    if (re_candidate && sa_isset(&re_candidate->rel_addr, SA_PORT)) {
+        *related_portp = sa_port(&candidate->candidate.local_candidate->attr.rel_addr);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/ice_candidate/candidate.c b/src/ice_candidate/candidate.c
new file mode 100644
index 0000000..b5db1b0
--- /dev/null
+++ b/src/ice_candidate/candidate.c
@@ -0,0 +1,274 @@
+#include "candidate.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+#define DEBUG_MODULE "ice-candidate"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Calculate the ICE candidate priority.
+ *
+ * We prefer:
+ *
+ * 1. UDP over TCP, then
+ * 2. IPv6 over IPv4, then
+ * 3. Older candidates over newer candidates.
+ *
+ * TODO: We should follow ICE Dual-Stack Recommendations.
+ *       See: https://tools.ietf.org/html/rfc8421#section-4
+ */
+uint32_t rawrtc_ice_candidate_calculate_priority(
+    uint32_t const n_candidates,
+    enum ice_cand_type const candidate_type,
+    int const protocol,
+    int const address_family,
+    enum ice_tcptype const tcp_type) {
+    uint16_t const age =
+        n_candidates > (1 << 13) ? (uint16_t) 0 : (uint16_t)((1 << 13) - n_candidates);
+    uint16_t const is_udp = protocol == IPPROTO_UDP ? (uint16_t) 1 : (uint16_t) 0;
+    uint16_t const is_ipv6 = address_family == AF_INET6 ? (uint16_t) 1 : (uint16_t) 0;
+    (void) tcp_type;
+    // TODO: Set correct component ID
+    return ice_cand_calc_prio(candidate_type, age | is_ipv6 << 14 | is_udp << 15, 1);
+}
+
+/*
+ * Destructor for an existing ICE candidate.
+ */
+static void rawrtc_ice_candidate_raw_destroy(void* arg) {
+    struct rawrtc_ice_candidate_raw* const candidate = arg;
+
+    // Un-reference
+    mem_deref(candidate->related_address);
+    mem_deref(candidate->ip);
+    mem_deref(candidate->foundation);
+}
+
+/*
+ * Create a raw ICE candidate (pending candidate).
+ */
+static enum rawrtc_code rawrtc_ice_candidate_raw_create(
+    struct rawrtc_ice_candidate_raw** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct rawrtc_ice_candidate_raw* candidate;
+    enum rawrtc_code error;
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_raw_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    error = rawrtc_error_to_code(pl_strdup(&candidate->foundation, foundation));
+    if (error) {
+        goto out;
+    }
+    candidate->priority = priority;
+    error = rawrtc_error_to_code(pl_strdup(&candidate->ip, ip));
+    if (error) {
+        goto out;
+    }
+    candidate->protocol = protocol;
+    candidate->port = port;
+    candidate->type = type;
+    candidate->tcp_type = tcp_type;
+    if (pl_isset(related_address)) {
+        error = rawrtc_error_to_code(pl_strdup(&candidate->related_address, related_address));
+        if (error) {
+            goto out;
+        }
+    }
+    candidate->related_port = related_port;
+
+out:
+    if (error) {
+        mem_deref(candidate);
+    } else {
+        // Set pointer
+        *candidatep = candidate;
+        DEBUG_PRINTF("Created candidate (raw): %r\n", ip);
+    }
+    return error;
+}
+
+/*
+ * Destructor for an existing ICE candidate.
+ */
+static void rawrtc_ice_candidate_destroy(void* arg) {
+    struct rawrtc_ice_candidate* const candidate = arg;
+
+    // Un-reference
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            mem_deref(candidate->candidate.raw_candidate);
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            mem_deref(candidate->candidate.local_candidate);
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            mem_deref(candidate->candidate.remote_candidate);
+            break;
+    }
+}
+
+/*
+ * Create an ICE candidate (pl variant).
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_internal(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct rawrtc_ice_candidate* candidate;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!candidatep || !pl_isset(foundation) || !pl_isset(ip)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_RAW;
+
+    // Create raw candidate
+    error = rawrtc_ice_candidate_raw_create(
+        &candidate->candidate.raw_candidate, foundation, priority, ip, protocol, port, type,
+        tcp_type, related_address, related_port);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(candidate);
+    } else {
+        // Set pointer
+        *candidatep = candidate;
+    }
+    return error;
+}
+
+/*
+ * Create an ICE candidate.
+ * `*candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    char* const foundation,  // copied
+    uint32_t const priority,
+    char* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    char* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct pl foundation_pl;
+    struct pl ip_pl;
+    struct pl related_address_pl = PL_INIT;
+
+    // Check arguments
+    if (!foundation || !ip) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&foundation_pl, foundation);
+    pl_set_str(&ip_pl, ip);
+    if (related_address) {
+        pl_set_str(&related_address_pl, related_address);
+    }
+
+    // Create ICE candidate
+    return rawrtc_ice_candidate_create_internal(
+        candidatep, &foundation_pl, priority, &ip_pl, protocol, port, type, tcp_type,
+        &related_address_pl, related_port);
+}
+
+/*
+ * Create an ICE candidate instance from an existing local candidate.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_from_local_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_lcand* const local_candidate  // referenced
+) {
+    struct rawrtc_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !local_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type and reference local candidate
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_LCAND;
+    candidate->candidate.local_candidate = mem_ref(local_candidate);
+
+    // Set pointer
+    *candidatep = candidate;
+    DEBUG_PRINTF(
+        "Created candidate (lcand): %J\n", &candidate->candidate.local_candidate->attr.addr);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create an ICE candidate instance from an existing remote candidate.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_from_remote_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_rcand* const remote_candidate  // referenced
+) {
+    struct rawrtc_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !remote_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type and reference remote candidate
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_RCAND;
+    candidate->candidate.remote_candidate = mem_ref(remote_candidate);
+
+    // Set pointer
+    *candidatep = candidate;
+    DEBUG_PRINTF(
+        "Created candidate (rcand): %j\n", &candidate->candidate.remote_candidate->attr.addr);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_candidate/candidate.h b/src/ice_candidate/candidate.h
new file mode 100644
index 0000000..3dec216
--- /dev/null
+++ b/src/ice_candidate/candidate.h
@@ -0,0 +1,86 @@
+#pragma once
+#include "../ice_candidate/candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * ICE candidate storage type (internal).
+ */
+enum rawrtc_ice_candidate_storage {
+    RAWRTC_ICE_CANDIDATE_STORAGE_RAW,
+    RAWRTC_ICE_CANDIDATE_STORAGE_LCAND,
+    RAWRTC_ICE_CANDIDATE_STORAGE_RCAND,
+};
+
+/*
+ * Raw ICE candidate (pending candidate).
+ */
+struct rawrtc_ice_candidate_raw {
+    char* foundation;  // copied
+    uint32_t priority;
+    char* ip;  // copied
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address;  // copied, nullable
+    uint16_t related_port;
+};
+
+struct rawrtc_ice_candidate {
+    enum rawrtc_ice_candidate_storage storage_type;
+    union {
+        struct rawrtc_ice_candidate_raw* raw_candidate;
+        struct ice_lcand* local_candidate;
+        struct ice_rcand* remote_candidate;
+    } candidate;
+};
+
+// Note: Cannot be public until it uses fixed size types in signature (stdint)
+uint32_t rawrtc_ice_candidate_calculate_priority(
+    uint32_t const n_candidates,
+    enum ice_cand_type const candidate_type,
+    int const protocol,
+    int const address_family,
+    enum ice_tcptype const tcp_type);
+
+enum rawrtc_code rawrtc_ice_candidate_create_internal(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port);
+
+enum rawrtc_code rawrtc_ice_candidate_create_from_local_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_lcand* const local_candidate  // referenced
+);
+
+enum rawrtc_code rawrtc_ice_candidate_create_from_remote_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_rcand* const remote_candidate  // referenced
+);
+
+int rawrtc_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_ice_candidate* const candidate);
+
+enum ice_cand_type rawrtc_ice_candidate_type_to_ice_cand_type(
+    enum rawrtc_ice_candidate_type const type);
+
+enum rawrtc_code rawrtc_ice_cand_type_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    const enum ice_cand_type re_type);
+
+enum ice_tcptype rawrtc_ice_tcp_candidate_type_to_ice_tcptype(
+    const enum rawrtc_ice_tcp_candidate_type type);
+
+enum rawrtc_code rawrtc_ice_tcptype_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    const enum ice_tcptype re_type);
diff --git a/src/ice_candidate/helper.c b/src/ice_candidate/helper.c
new file mode 100644
index 0000000..ff74c71
--- /dev/null
+++ b/src/ice_candidate/helper.c
@@ -0,0 +1,209 @@
+#include "helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_server/server.h"
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Destructor for an existing candidate helper.
+ */
+static void rawrtc_candidate_helper_destroy(void* arg) {
+    struct rawrtc_candidate_helper* const local_candidate = arg;
+
+    // Un-reference
+    list_flush(&local_candidate->stun_sessions);
+    mem_deref(local_candidate->udp_helper);
+    mem_deref(local_candidate->candidate);
+    mem_deref(local_candidate->gatherer);
+}
+
+/*
+ * Create a candidate helper.
+ */
+enum rawrtc_code rawrtc_candidate_helper_create(
+    struct rawrtc_candidate_helper** const candidate_helperp,  // de-referenced
+    struct rawrtc_ice_gatherer* gatherer,
+    struct ice_lcand* const candidate,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg) {
+    struct rawrtc_candidate_helper* candidate_helper;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!candidate_helperp || !gatherer || !candidate || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create candidate helper
+    candidate_helper = mem_zalloc(sizeof(*candidate_helper), rawrtc_candidate_helper_destroy);
+    if (!candidate_helper) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    candidate_helper->gatherer = mem_ref(gatherer);
+    candidate_helper->candidate = mem_ref(candidate);
+    candidate_helper->srflx_pending_count = 0;
+    candidate_helper->relay_pending_count = 0;
+
+    // Set receive handler
+    error = rawrtc_candidate_helper_set_receive_handler(candidate_helper, receive_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(candidate_helper);
+    } else {
+        // Set pointer
+        *candidate_helperp = candidate_helper;
+    }
+    return error;
+}
+
+/*
+ * Set a candidate helper's receive handler.
+ */
+enum rawrtc_code rawrtc_candidate_helper_set_receive_handler(
+    struct rawrtc_candidate_helper* const candidate_helper,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    struct udp_helper* udp_helper;
+    struct udp_sock* udp_socket;
+
+    // Check arguments
+    if (!candidate_helper || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate's UDP socket
+    udp_socket = trice_lcand_sock(candidate_helper->gatherer->ice, candidate_helper->candidate);
+    if (!udp_socket) {
+        return RAWRTC_CODE_NO_SOCKET;
+    }
+
+    // Create UDP helper
+    error = rawrtc_error_to_code(udp_register_helper(
+        &udp_helper, udp_socket, RAWRTC_LAYER_DTLS_SRTP_STUN, NULL, receive_handler, arg));
+    if (error) {
+        return error;
+    }
+
+    // Unset current helper (if any) and set new helper
+    mem_deref(candidate_helper->udp_helper);
+    candidate_helper->udp_helper = udp_helper;
+
+    // TODO: What about TCP helpers?
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Find a specific candidate helper by re candidate.
+ */
+enum rawrtc_code rawrtc_candidate_helper_find(
+    struct rawrtc_candidate_helper** const candidate_helperp,
+    struct list* const candidate_helpers,
+    struct ice_lcand* re_candidate) {
+    struct le* le;
+
+    // Check arguments
+    if (!candidate_helperp || !candidate_helpers || !re_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Lookup candidate helper
+    for (le = list_head(candidate_helpers); le != NULL; le = le->next) {
+        struct rawrtc_candidate_helper* const candidate_helper = le->data;
+        if (candidate_helper->candidate->us == re_candidate->us) {
+            // Found
+            *candidate_helperp = candidate_helper;
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+static void rawrtc_candidate_helper_stun_session_destroy(void* arg) {
+    struct rawrtc_candidate_helper_stun_session* const session = arg;
+
+    // Remove from list
+    list_unlink(&session->le);
+
+    // Un-reference
+    mem_deref(session->url);
+    mem_deref(session->stun_keepalive);
+    mem_deref(session->candidate_helper);
+}
+
+/*
+ * Create a STUN session.
+ */
+enum rawrtc_code rawrtc_candidate_helper_stun_session_create(
+    struct rawrtc_candidate_helper_stun_session** const sessionp,  // de-referenced
+    struct rawrtc_ice_server_url* const url) {
+    struct rawrtc_candidate_helper_stun_session* session;
+
+    // Check arguments
+    if (!sessionp || !url) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    session = mem_zalloc(sizeof(*session), rawrtc_candidate_helper_stun_session_destroy);
+    if (!session) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    session->url = mem_ref(url);
+    session->pending = true;
+
+    // Set pointer & done
+    *sessionp = session;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add a STUN session to a candidate helper.
+ */
+enum rawrtc_code rawrtc_candidate_helper_stun_session_add(
+    struct rawrtc_candidate_helper_stun_session* const session,
+    struct rawrtc_candidate_helper* const candidate_helper,
+    struct stun_keepalive* const stun_keepalive) {
+    // Check arguments
+    if (!session || !candidate_helper || !stun_keepalive) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set fields/reference
+    session->candidate_helper = mem_ref(candidate_helper);
+    session->stun_keepalive = mem_ref(stun_keepalive);
+
+    // Append to STUN sessions
+    list_append(&candidate_helper->stun_sessions, &session->le, session);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Remove STUN sessions list handler (for candidate helper lists).
+ */
+bool rawrtc_candidate_helper_remove_stun_sessions_handler(struct le* le, void* arg) {
+    struct rawrtc_candidate_helper* const candidate_helper = le->data;
+    (void) arg;
+
+    // Flush STUN sessions
+    list_flush(&candidate_helper->stun_sessions);
+
+    return false;  // continue traversing
+}
diff --git a/src/ice_candidate/helper.h b/src/ice_candidate/helper.h
new file mode 100644
index 0000000..9446574
--- /dev/null
+++ b/src/ice_candidate/helper.h
@@ -0,0 +1,58 @@
+#pragma once
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Local candidate helper.
+ */
+struct rawrtc_candidate_helper {
+    struct le le;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct ice_lcand* candidate;
+    struct udp_helper* udp_helper;
+    uint_fast8_t srflx_pending_count;
+    struct list stun_sessions;
+    uint_fast8_t relay_pending_count;
+};
+
+/*
+ * STUN keep-alive session.
+ */
+struct rawrtc_candidate_helper_stun_session {
+    struct le le;
+    struct rawrtc_candidate_helper* candidate_helper;
+    struct stun_keepalive* stun_keepalive;
+    struct rawrtc_ice_server_url* url;
+    bool pending;
+};
+
+enum rawrtc_code rawrtc_candidate_helper_create(
+    struct rawrtc_candidate_helper** const candidate_helperp,  // de-referenced
+    struct rawrtc_ice_gatherer* gatherer,
+    struct ice_lcand* const candidate,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_candidate_helper_set_receive_handler(
+    struct rawrtc_candidate_helper* const candidate_helper,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_candidate_helper_find(
+    struct rawrtc_candidate_helper** const candidate_helperp,
+    struct list* const candidate_helpers,
+    struct ice_lcand* re_candidate);
+
+enum rawrtc_code rawrtc_candidate_helper_stun_session_create(
+    struct rawrtc_candidate_helper_stun_session** const sessionp,  // de-referenced
+    struct rawrtc_ice_server_url* const url);
+
+enum rawrtc_code rawrtc_candidate_helper_stun_session_add(
+    struct rawrtc_candidate_helper_stun_session* const session,
+    struct rawrtc_candidate_helper* const candidate_helper,
+    struct stun_keepalive* const stun_keepalive);
+
+bool rawrtc_candidate_helper_remove_stun_sessions_handler(struct le* le, void* arg);
diff --git a/src/ice_candidate/meson.build b/src/ice_candidate/meson.build
new file mode 100644
index 0000000..abd4455
--- /dev/null
+++ b/src/ice_candidate/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'attributes.c',
+    'candidate.c',
+    'helper.c',
+    'utils.c',
+])
diff --git a/src/ice_candidate/utils.c b/src/ice_candidate/utils.c
new file mode 100644
index 0000000..a6bee78
--- /dev/null
+++ b/src/ice_candidate/utils.c
@@ -0,0 +1,476 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <netinet/in.h>  // IPPROTO_UDP, IPPROTO_TCP
+
+/*
+ * Translate an ICE candidate type to the corresponding re type.
+ */
+enum ice_cand_type rawrtc_ice_candidate_type_to_ice_cand_type(
+    enum rawrtc_ice_candidate_type const type) {
+    // No conversion needed
+    return (enum ice_cand_type) type;
+}
+
+/*
+ * Translate a re ICE candidate type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_ice_cand_type_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    enum ice_cand_type const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case ICE_CAND_TYPE_HOST:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_HOST;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_SRFLX:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_SRFLX;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_PRFLX:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_PRFLX;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_RELAY:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_RELAY;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate an ICE TCP candidate type to the corresponding re type.
+ */
+enum ice_tcptype rawrtc_ice_tcp_candidate_type_to_ice_tcptype(
+    enum rawrtc_ice_tcp_candidate_type const type) {
+    // No conversion needed
+    return (enum ice_tcptype) type;
+}
+
+/*
+ * Translate a re ICE TCP candidate type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_ice_tcptype_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    enum ice_tcptype const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case ICE_TCP_ACTIVE:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_TCP_PASSIVE:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_TCP_SO:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_SO;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate a protocol to the corresponding IPPROTO_*.
+ */
+int rawrtc_ice_protocol_to_ipproto(enum rawrtc_ice_protocol const protocol) {
+    // No conversion needed
+    return (int) protocol;
+}
+
+/*
+ * Translate a IPPROTO_* to the corresponding protocol.
+ */
+enum rawrtc_code rawrtc_ipproto_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    int const ipproto) {
+    // Check arguments
+    if (!protocolp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert IPPROTO_*
+    switch (ipproto) {
+        case IPPROTO_UDP:
+            *protocolp = RAWRTC_ICE_PROTOCOL_UDP;
+            return RAWRTC_CODE_SUCCESS;
+        case IPPROTO_TCP:
+            *protocolp = RAWRTC_ICE_PROTOCOL_TCP;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+static enum rawrtc_ice_protocol const map_enum_ice_protocol[] = {
+    RAWRTC_ICE_PROTOCOL_UDP,
+    RAWRTC_ICE_PROTOCOL_TCP,
+};
+
+static char const* const map_str_ice_protocol[] = {
+    "udp",
+    "tcp",
+};
+
+static size_t const map_ice_protocol_length = ARRAY_SIZE(map_enum_ice_protocol);
+
+/*
+ * Translate an ICE protocol to str.
+ */
+char const* rawrtc_ice_protocol_to_str(enum rawrtc_ice_protocol const protocol) {
+    size_t i;
+
+    for (i = 0; i < map_ice_protocol_length; ++i) {
+        if (map_enum_ice_protocol[i] == protocol) {
+            return map_str_ice_protocol[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a pl to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!protocolp || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_protocol_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_protocol[i]) == 0) {
+            *protocolp = map_enum_ice_protocol[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_protocol(protocolp, &pl);
+}
+
+static enum rawrtc_ice_candidate_type const map_enum_ice_candidate_type[] = {
+    RAWRTC_ICE_CANDIDATE_TYPE_HOST,
+    RAWRTC_ICE_CANDIDATE_TYPE_SRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_PRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_RELAY,
+};
+
+static char const* const map_str_ice_candidate_type[] = {
+    "host",
+    "srflx",
+    "prflx",
+    "relay",
+};
+
+static size_t const map_ice_candidate_type_length = ARRAY_SIZE(map_enum_ice_candidate_type);
+
+/*
+ * Translate an ICE candidate type to str.
+ */
+char const* rawrtc_ice_candidate_type_to_str(enum rawrtc_ice_candidate_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_candidate_type_length; ++i) {
+        if (map_enum_ice_candidate_type[i] == type) {
+            return map_str_ice_candidate_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a pl to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_candidate_type_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_candidate_type[i]) == 0) {
+            *typep = map_enum_ice_candidate_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_candidate_type(typep, &pl);
+}
+
+static enum rawrtc_ice_tcp_candidate_type const map_enum_ice_tcp_candidate_type[] = {
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_SO,
+};
+
+static char const* const map_str_ice_tcp_candidate_type[] = {
+    "active",
+    "passive",
+    "so",
+};
+
+static size_t const map_ice_tcp_candidate_type_length = ARRAY_SIZE(map_enum_ice_tcp_candidate_type);
+
+/*
+ * Translate an ICE TCP candidate type to str.
+ */
+char const* rawrtc_ice_tcp_candidate_type_to_str(enum rawrtc_ice_tcp_candidate_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_tcp_candidate_type_length; ++i) {
+        if (map_enum_ice_tcp_candidate_type[i] == type) {
+            return map_str_ice_tcp_candidate_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_tcp_candidate_type_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_tcp_candidate_type[i]) == 0) {
+            *typep = map_enum_ice_tcp_candidate_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_tcp_candidate_type(typep, &pl);
+}
+
+static char const* const map_str_ice_candidate_storage[] = {
+    "raw",
+    "lcand",
+    "rcand",
+};
+
+static enum rawrtc_ice_candidate_storage const map_enum_ice_candidate_storage[] = {
+    RAWRTC_ICE_CANDIDATE_STORAGE_RAW,
+    RAWRTC_ICE_CANDIDATE_STORAGE_LCAND,
+    RAWRTC_ICE_CANDIDATE_STORAGE_RCAND,
+};
+
+static size_t const map_ice_candidate_storage_length = ARRAY_SIZE(map_enum_ice_candidate_storage);
+
+/*
+ * Translate an ICE candidate storage type to str.
+ */
+static char const* ice_candidate_storage_to_str(enum rawrtc_ice_candidate_storage const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_candidate_storage_length; ++i) {
+        if (map_enum_ice_candidate_storage[i] == type) {
+            return map_str_ice_candidate_storage[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Print debug information for an ICE candidate.
+ */
+int rawrtc_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_ice_candidate* const candidate) {
+    int err = 0;
+    enum rawrtc_code error;
+    char* foundation = NULL;
+    uint32_t priority;
+    char* ip = NULL;
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address = NULL;
+    uint16_t related_port;
+
+    // Check arguments
+    if (!candidate) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Candidate <%p>:\n", candidate);
+
+    // Storage type
+    err |= re_hprintf(
+        pf, "    storage_type=%s\n", ice_candidate_storage_to_str(candidate->storage_type));
+
+    // Foundation
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    foundation=\"%s\"\n", foundation);
+
+    // Priority
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    priority=%" PRIu32 "\n", priority);
+
+    // IP
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    ip=%s\n", ip);
+
+    // Protocol
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    protocol=%s\n", rawrtc_ice_protocol_to_str(protocol));
+
+    // Port
+    error = rawrtc_ice_candidate_get_port(&port, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    port=%" PRIu16 "\n", port);
+
+    // Type
+    error = rawrtc_ice_candidate_get_type(&type, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    type=%s\n", rawrtc_ice_candidate_type_to_str(type));
+
+    // TCP type (if any)
+    err |= re_hprintf(pf, "    tcp_type=");
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%s\n", rawrtc_ice_tcp_candidate_type_to_str(tcp_type));
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+    // Related address (if any)
+    err |= re_hprintf(pf, "    related_address=");
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%s\n", related_address);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+    // Related port (if any)
+    err |= re_hprintf(pf, "    related_port=");
+    error = rawrtc_ice_candidate_get_related_port(&related_port, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%" PRIu16 "\n", related_port);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(related_address);
+    mem_deref(ip);
+    mem_deref(foundation);
+
+    // Translate error & done
+    if (!err && error) {
+        err = EINVAL;
+    }
+    return err;
+}
diff --git a/src/ice_gather_options/meson.build b/src/ice_gather_options/meson.build
new file mode 100644
index 0000000..1161f68
--- /dev/null
+++ b/src/ice_gather_options/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'options.c',
+    'utils.c',
+])
diff --git a/src/ice_gather_options/options.c b/src/ice_gather_options/options.c
new file mode 100644
index 0000000..c2847ab
--- /dev/null
+++ b/src/ice_gather_options/options.c
@@ -0,0 +1,93 @@
+#include "options.h"
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing ICE gather options instance.
+ */
+static void rawrtc_ice_gather_options_destroy(void* arg) {
+    struct rawrtc_ice_gather_options* const options = arg;
+
+    // Un-reference
+    list_flush(&options->ice_servers);
+}
+
+/*
+ * Create a new ICE gather options instance.
+ * `*optionsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_create(
+    struct rawrtc_ice_gather_options** const optionsp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy) {
+    struct rawrtc_ice_gather_options* options;
+
+    // Check arguments
+    if (!optionsp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    options = mem_zalloc(sizeof(*options), rawrtc_ice_gather_options_destroy);
+    if (!options) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    options->gather_policy = gather_policy;
+    list_init(&options->ice_servers);
+
+    // Set pointer and return
+    *optionsp = options;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server instance to the gather options.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_add_server_internal(
+    struct rawrtc_ice_gather_options* const options, struct rawrtc_ice_server* const server) {
+    // Check arguments
+    if (!options || !server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Add to options
+    list_append(&options->ice_servers, &server->le, server);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server to the gather options.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_add_server(
+    struct rawrtc_ice_gather_options* const options,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!options) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure there are less than 2^8 servers
+    // TODO: This check should be in some common location
+    if (list_count(&options->ice_servers) == UINT8_MAX) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Create ICE server
+    error = rawrtc_ice_server_create(&server, urls, n_urls, username, credential, credential_type);
+    if (error) {
+        return error;
+    }
+
+    // Add to options
+    return rawrtc_ice_gather_options_add_server_internal(options, server);
+}
diff --git a/src/ice_gather_options/options.h b/src/ice_gather_options/options.h
new file mode 100644
index 0000000..c43b9c8
--- /dev/null
+++ b/src/ice_gather_options/options.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_ice_gather_options {
+    enum rawrtc_ice_gather_policy gather_policy;
+    struct list ice_servers;
+};
+
+enum rawrtc_code rawrtc_ice_gather_options_add_server_internal(
+    struct rawrtc_ice_gather_options* const configuration, struct rawrtc_ice_server* const server);
+
+int rawrtc_ice_gather_options_debug(
+    struct re_printf* const pf, struct rawrtc_ice_gather_options const* const options);
diff --git a/src/ice_gather_options/utils.c b/src/ice_gather_options/utils.c
new file mode 100644
index 0000000..ae51caa
--- /dev/null
+++ b/src/ice_gather_options/utils.c
@@ -0,0 +1,87 @@
+#include "options.h"
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+static enum rawrtc_ice_gather_policy const map_enum_ice_gather_policy[] = {
+    RAWRTC_ICE_GATHER_POLICY_ALL,
+    RAWRTC_ICE_GATHER_POLICY_NOHOST,
+    RAWRTC_ICE_GATHER_POLICY_RELAY,
+};
+
+static char const* const map_str_ice_gather_policy[] = {
+    "all",
+    "nohost",
+    "relay",
+};
+
+static size_t const map_ice_gather_policy_length = ARRAY_SIZE(map_enum_ice_gather_policy);
+
+/*
+ * Translate an ICE gather policy to str.
+ */
+char const* rawrtc_ice_gather_policy_to_str(enum rawrtc_ice_gather_policy const policy) {
+    size_t i;
+
+    for (i = 0; i < map_ice_gather_policy_length; ++i) {
+        if (map_enum_ice_gather_policy[i] == policy) {
+            return map_str_ice_gather_policy[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE gather policy (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_gather_policy(
+    enum rawrtc_ice_gather_policy* const policyp,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!policyp || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_gather_policy_length; ++i) {
+        if (str_casecmp(map_str_ice_gather_policy[i], str) == 0) {
+            *policyp = map_enum_ice_gather_policy[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Print debug information for the ICE gather options.
+ */
+int rawrtc_ice_gather_options_debug(
+    struct re_printf* const pf, struct rawrtc_ice_gather_options const* const options) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!options) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "----- ICE Gather Options <%p> -----\n", options);
+
+    // Gather policy
+    err |= re_hprintf(
+        pf, "  gather_policy=%s\n", rawrtc_ice_gather_policy_to_str(options->gather_policy));
+
+    // ICE servers
+    for (le = list_head(&options->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const server = le->data;
+        err |= re_hprintf(pf, "%H", rawrtc_ice_server_debug, server);
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/ice_gatherer/attributes.c b/src/ice_gatherer/attributes.c
new file mode 100644
index 0000000..92c6fb5
--- /dev/null
+++ b/src/ice_gatherer/attributes.c
@@ -0,0 +1,19 @@
+#include "gatherer.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtcc/code.h>
+
+/*
+ * Get the current state of an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!statep || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = gatherer->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_gatherer/gatherer.c b/src/ice_gatherer/gatherer.c
new file mode 100644
index 0000000..51002fe
--- /dev/null
+++ b/src/ice_gatherer/gatherer.c
@@ -0,0 +1,1073 @@
+#include "gatherer.h"
+#include "../ice_candidate/candidate.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gather_options/options.h"
+#include "../ice_server/address.h"
+#include "../ice_server/resolver.h"
+#include "../ice_server/server.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/message_buffer.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+#include <string.h>  // memcpy
+#include <sys/socket.h>  // AF_INET, AF_INET6
+
+#define DEBUG_MODULE "ice-gatherer"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#define RAWRTC_DEBUG_ICE_GATHERER 0  // TODO: Remove
+#include <rawrtcc/debug.h>
+
+/*
+ * Destructor for an existing ICE gatherer.
+ */
+static void rawrtc_ice_gatherer_destroy(void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+
+    // Close gatherer
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_ice_gatherer_close(gatherer);
+
+    // Un-reference
+    mem_deref(gatherer->dns_client);
+    mem_deref(gatherer->ice);
+    list_flush(&gatherer->local_candidates);
+    list_flush(&gatherer->buffered_messages);
+    list_flush(&gatherer->url_resolvers);
+    list_flush(&gatherer->url_addresses);
+    mem_deref(gatherer->options);
+}
+
+/*
+ * Create a new ICE gatherer.
+ * `*gathererp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_create(
+    struct rawrtc_ice_gatherer** const gathererp,  // de-referenced
+    struct rawrtc_ice_gather_options* const options,  // referenced
+    rawrtc_ice_gatherer_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_gatherer_error_handler const error_handler,  // nullable
+    rawrtc_ice_gatherer_local_candidate_handler const local_candidate_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_ice_gatherer* gatherer;
+    int err;
+    struct sa dns_servers[RAWRTC_ICE_GATHERER_DNS_SERVERS] = {0};
+    uint32_t n_dns_servers = ARRAY_SIZE(dns_servers);
+    uint32_t i;
+
+    // Check arguments
+    if (!gathererp || !options) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    gatherer = mem_zalloc(sizeof(*gatherer), rawrtc_ice_gatherer_destroy);
+    if (!gatherer) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    gatherer->state = RAWRTC_ICE_GATHERER_STATE_NEW;  // TODO: Raise state (delayed)?
+    gatherer->options = mem_ref(options);
+    gatherer->state_change_handler = state_change_handler;
+    gatherer->error_handler = error_handler;
+    gatherer->local_candidate_handler = local_candidate_handler;
+    gatherer->arg = arg;
+    list_init(&gatherer->url_addresses);
+    list_init(&gatherer->url_resolvers);
+    list_init(&gatherer->buffered_messages);
+    list_init(&gatherer->local_candidates);
+
+    // Generate random username fragment and password for ICE
+    rand_str(gatherer->ice_username_fragment, sizeof(gatherer->ice_username_fragment));
+    rand_str(gatherer->ice_password, sizeof(gatherer->ice_password));
+
+    // Set ICE configuration and create trice instance
+    // TODO: Get from config
+    gatherer->ice_config.nom = ICE_NOMINATION_AGGRESSIVE;
+    gatherer->ice_config.debug = RAWRTC_DEBUG_ICE_GATHERER ? true : false;
+    gatherer->ice_config.trace = RAWRTC_DEBUG_ICE_GATHERER ? true : false;
+    gatherer->ice_config.ansi = true;
+    gatherer->ice_config.enable_prflx = true;
+    gatherer->ice_config.optimize_loopback_pairing = true;
+    err = trice_alloc(
+        &gatherer->ice, &gatherer->ice_config, ICE_ROLE_UNKNOWN, gatherer->ice_username_fragment,
+        gatherer->ice_password);
+    if (err) {
+        DEBUG_WARNING("Unable to create trickle ICE instance, reason: %m\n", err);
+        goto out;
+    }
+
+    // Get local DNS servers
+    err = dns_srv_get(NULL, 0, dns_servers, &n_dns_servers);
+    if (err) {
+        DEBUG_WARNING("Unable to retrieve local DNS servers, reason: %m\n", err);
+        goto out;
+    }
+
+    // Print local DNS servers
+    if (n_dns_servers == 0) {
+        DEBUG_NOTICE("No DNS servers found\n");
+    }
+    for (i = 0; i < n_dns_servers; ++i) {
+        DEBUG_PRINTF("DNS server: %j\n", &dns_servers[i]);
+    }
+
+    // Create DNS client (for resolving ICE server IPs)
+    err = dnsc_alloc(&gatherer->dns_client, NULL, dns_servers, n_dns_servers);
+    if (err) {
+        DEBUG_WARNING("Unable to create DNS client instance, reason: %m\n", err);
+        goto out;
+    }
+
+    // Done
+    DEBUG_PRINTF("ICE gatherer created:\n%H", rawrtc_ice_gather_options_debug, gatherer->options);
+
+out:
+    if (err) {
+        mem_deref(gatherer);
+    } else {
+        // Set pointer
+        *gathererp = gatherer;
+    }
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Change the state of the ICE gatherer.
+ * Will call the corresponding handler.
+ * TODO: https://github.com/w3c/ortc/issues/606
+ */
+static void set_state(
+    struct rawrtc_ice_gatherer* const gatherer, enum rawrtc_ice_gatherer_state const state) {
+    // Set state
+    gatherer->state = state;
+
+    // Call handler (if any)
+    if (gatherer->state_change_handler) {
+        gatherer->state_change_handler(state, gatherer->arg);
+    }
+}
+
+/*
+ * Close the ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_close(struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Already closed?
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // TODO: Stop ICE transport
+
+    // Stop timeout timer
+    tmr_cancel(&gatherer->timeout_timer);
+
+    // Remove STUN sessions from local candidate helpers
+    // Note: Needed to purge remaining references to the gatherer so it can be free'd.
+    list_apply(
+        &gatherer->local_candidates, true, rawrtc_candidate_helper_remove_stun_sessions_handler,
+        NULL);
+
+    // Flush local candidate helpers
+    list_flush(&gatherer->local_candidates);
+
+    // Remove ICE server URL resolvers
+    list_flush(&gatherer->url_resolvers);
+
+    // Remove ICE server URL addresses
+    list_flush(&gatherer->url_addresses);
+
+    // Stop ICE checklist (if running)
+    trice_checklist_stop(gatherer->ice);
+
+    // Remove ICE agent
+    gatherer->ice = mem_deref(gatherer->ice);
+
+    // Set state to closed and return
+    set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Handle received UDP messages.
+ */
+static bool udp_receive_handler(struct sa* source, struct mbuf* buffer, void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    enum rawrtc_code error;
+
+    // Allocate context and copy source address
+    void* const context = mem_zalloc(sizeof(*source), NULL);
+    if (!context) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    memcpy(context, source, sizeof(*source));
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&gatherer->buffered_messages, buffer, context);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    DEBUG_PRINTF("Buffered UDP packet of size %zu\n", mbuf_get_left(buffer));
+
+out:
+    if (error) {
+        DEBUG_WARNING("Could not buffer UDP packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+
+    // Un-reference
+    mem_deref(context);
+
+    // Handled
+    return true;
+}
+
+/*
+ * Announce a local candidate.
+ */
+static enum rawrtc_code announce_candidate(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct ice_lcand* const re_candidate,  // nullable
+    char const* const url  // nullable
+) {
+    enum rawrtc_code error;
+
+    // Don't announce in the completed state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_COMPLETE) {
+        DEBUG_PRINTF("Not announcing candidate, gathering state is complete\n");
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create ICE candidate
+    if (gatherer->local_candidate_handler) {
+        struct rawrtc_ice_candidate* ice_candidate = NULL;
+
+        // Create ICE candidate
+        if (re_candidate) {
+            error = rawrtc_ice_candidate_create_from_local_candidate(&ice_candidate, re_candidate);
+            if (error) {
+                return error;
+            }
+        }
+
+        // Call candidate handler and un-reference
+        gatherer->local_candidate_handler(ice_candidate, url, gatherer->arg);
+        mem_deref(ice_candidate);
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Check if the gathering process is complete.
+ */
+static void check_gathering_complete(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    bool const force_complete) {
+    struct le* le;
+    enum rawrtc_code error;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Check or force completion?
+    if (!force_complete) {
+        // Ensure no URL resolvers are in flight
+        if (!list_isempty(&gatherer->url_resolvers)) {
+            struct rawrtc_ice_server_url_resolver* const resolver =
+                list_head(&gatherer->url_resolvers)->data;
+            (void) resolver;
+            DEBUG_PRINTF(
+                "Gathering still in progress, resolving URL (%s [%s])\n", resolver->url->url,
+                dns_rr_typename(resolver->dns_type));
+            return;
+        }
+
+        // Ensure every local candidate has no pending srflx/relay candidates
+        for (le = list_head(&gatherer->local_candidates); le != NULL; le = le->next) {
+            struct rawrtc_candidate_helper* const candidate = le->data;
+
+            // Check counters
+            if (candidate->srflx_pending_count > 0 || candidate->relay_pending_count > 0) {
+                // Nope
+                DEBUG_PRINTF(
+                    "Gathering still in progress at candidate %j, #srflx=%" PRIuFAST8
+                    ", #relay=%" PRIuFAST8 "\n",
+                    &candidate->candidate->attr.addr, candidate->srflx_pending_count,
+                    candidate->relay_pending_count);
+                return;
+            }
+        }
+    }
+
+    // Stop timeout timer
+    tmr_cancel(&gatherer->timeout_timer);
+
+    // TODO: Skip the remaining code below when using continuous gathering
+
+    // Announce candidate gathering complete
+    error = announce_candidate(gatherer, NULL, NULL);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not announce end-of-candidates, reason: %s\n", rawrtc_code_to_str(error));
+
+        // This should never happen, so close on failure
+        rawrtc_ice_gatherer_close(gatherer);
+        return;
+    }
+
+    // Update state & done
+    if (gatherer->state != RAWRTC_ICE_GATHERER_STATE_COMPLETE) {
+        DEBUG_PRINTF("Gathering complete:\n%H", trice_debug, gatherer->ice);
+        set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_COMPLETE);
+    }
+}
+
+/*
+ * Find an existing local candidate.
+ * TODO: This should probably be moved into a PR towards rew
+ */
+static struct ice_lcand* find_candidate(
+    struct trice* const ice,
+    enum ice_cand_type type,  // set to -1 if it should not be checked
+    unsigned const component_id,  // set to 0 if it should not be checked
+    int const protocol,
+    struct sa const* const address,  // nullable
+    enum sa_flag const address_flag,
+    struct sa const* base_address,  // nullable
+    enum sa_flag const base_address_flags) {
+    struct le* le;
+
+    // Check arguments
+    if (!ice) {
+        return NULL;
+    }
+
+    // If base address and address have an identical IP, ignore the base address and the type
+    if (address && base_address && sa_cmp(address, base_address, SA_ADDR)) {
+        base_address = NULL;
+        type = (enum ice_cand_type) - 1;
+    }
+
+    for (le = list_head(trice_lcandl(ice)); le != NULL; le = le->next) {
+        struct ice_lcand* candidate = le->data;
+
+        // Check type (if requested)
+        if (type != (enum ice_cand_type) - 1 && type != candidate->attr.type) {
+            continue;
+        }
+
+        // Check component id (if requested)
+        if (component_id && candidate->attr.compid != component_id) {
+            continue;
+        }
+
+        // Check protocol
+        if (candidate->attr.proto != protocol) {
+            continue;
+        }
+
+        // Check address
+        if (address && !sa_cmp(&candidate->attr.addr, address, address_flag)) {
+            continue;
+        }
+
+        // Check base address
+        if (base_address && !sa_cmp(&candidate->base_addr, base_address, base_address_flags)) {
+            continue;
+        }
+
+        // Found
+        return candidate;
+    }
+
+    // Not found
+    return NULL;
+}
+
+/*
+ * Gather relay candidates on an ICE server.
+ */
+static enum rawrtc_code gather_relay_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    // Check ICE server is enabled for TURN
+    if (server_address->url->type != RAWRTC_ICE_SERVER_TYPE_TURN) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // TODO: Create TURN request
+    (void) candidate;
+    DEBUG_NOTICE(
+        "TODO: Gather relay candidates using server %J (%s)\n", &server_address->address,
+        server_address->url->url);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Handle gathered server reflexive candidate.
+ */
+static void reflexive_candidate_handler(
+    int err,
+    struct sa const* address,  // not checked
+    void* arg  // not checked
+) {
+    struct rawrtc_candidate_helper_stun_session* const session = arg;
+    struct rawrtc_candidate_helper* const candidate = session->candidate_helper;
+    struct rawrtc_ice_gatherer* const gatherer = candidate->gatherer;
+    struct ice_lcand* const re_candidate = candidate->candidate;
+    struct ice_lcand* re_other_candidate;
+    uint32_t priority;
+    struct ice_lcand* srflx_candidate;
+    enum rawrtc_code error;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Error?
+    if (err) {
+        DEBUG_NOTICE("STUN request failed, reason: %m\n", err);
+        goto out;
+    }
+
+    // Check if a local candidate with the same base and same attributes (apart from the port)
+    // exists
+    re_other_candidate = find_candidate(
+        gatherer->ice, ICE_CAND_TYPE_SRFLX, re_candidate->attr.compid, re_candidate->attr.proto,
+        address, SA_ADDR, &re_candidate->attr.addr, SA_ALL);
+    if (re_other_candidate) {
+        DEBUG_PRINTF(
+            "Ignoring server reflexive candidate with same base %J and public IP %j (%s)"
+            "\n",
+            &re_candidate->attr.addr, address, session->url->url);
+        goto out;
+    }
+
+    // Add server reflexive candidate
+    // TODO: Using the candidate's protocol, TCP type and component id correct?
+    priority = rawrtc_ice_candidate_calculate_priority(
+        list_count(trice_lcandl(gatherer->ice)), ICE_CAND_TYPE_SRFLX, re_candidate->attr.proto,
+        sa_af(address), re_candidate->attr.tcptype);
+    err = trice_lcand_add(
+        &srflx_candidate, gatherer->ice, re_candidate->attr.compid, re_candidate->attr.proto,
+        priority, address, &re_candidate->attr.addr, ICE_CAND_TYPE_SRFLX, &re_candidate->attr.addr,
+        re_candidate->attr.tcptype, NULL, RAWRTC_LAYER_ICE);
+    if (err) {
+        DEBUG_WARNING("Could not add server reflexive candidate, reason: %m\n", err);
+        goto out;
+    }
+    DEBUG_PRINTF(
+        "Added %s server reflexive candidate for interface %j (%s)\n",
+        net_proto2name(srflx_candidate->attr.proto), address, session->url->url);
+
+    // Announce candidate to handler
+    error = announce_candidate(gatherer, srflx_candidate, session->url->url);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not announce server reflexive candidate, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+out:
+    // Decrease counter & check if done gathering
+    if (session->pending) {
+        --candidate->srflx_pending_count;
+        session->pending = false;
+    }
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Gather server reflexive candidates on an ICE server.
+ */
+static enum rawrtc_code gather_reflexive_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    enum rawrtc_code error;
+    struct ice_lcand* const re_candidate = candidate->candidate;
+    struct ice_cand_attr* const attribute = &candidate->candidate->attr;
+    int const af = sa_af(&attribute->addr);
+    enum rawrtc_ice_candidate_type type;
+    char const* type_str;
+    struct rawrtc_candidate_helper_stun_session* session = NULL;
+    struct stun_conf stun_config = {
+        // TODO: Make this configurable!
+        .rto = STUN_DEFAULT_RTO,  // 500ms
+        .rc = 3,  // Send at: 0ms, 500ms, 1500ms
+        .rm = 6,  // Additional wait: 3000ms
+        .ti = 4500,  // Total timeout: 4500ms
+        .tos = 0x00,
+    };
+    struct stun_keepalive* stun_keepalive = NULL;
+
+    // Ignore IPv6 addresses
+    // Note: If you have a use case for IPv6 server reflexive candidates, let me know.
+    if (af == AF_INET6) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Ensure the candidate's IP version matches the server address's IP version
+    if (af != sa_af(&server_address->address)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Convert ICE candidate type
+    error = rawrtc_ice_cand_type_to_ice_candidate_type(&type, attribute->type);
+    if (error) {
+        goto out;
+    }
+    type_str = rawrtc_ice_candidate_type_to_str(type);
+    (void) type_str;
+
+    // TODO: Handle TCP/TLS/DTLS transports
+
+    // Create STUN session
+    error = rawrtc_candidate_helper_stun_session_create(&session, server_address->url);
+    if (error) {
+        goto out;
+    }
+
+    // Create STUN keep-alive session
+    // TODO: We're using the candidate's protocol which conflicts with the ICE server URL transport
+    DEBUG_PRINTF(
+        "Creating STUN request for %s %s candidate %J using ICE server %J (%s)\n",
+        net_proto2name(attribute->proto), type_str, &attribute->addr, &server_address->address,
+        server_address->url->url);
+    error = rawrtc_error_to_code(stun_keepalive_alloc(
+        &stun_keepalive, re_candidate->attr.proto, re_candidate->us, RAWRTC_LAYER_STUN,
+        &server_address->address, &stun_config, reflexive_candidate_handler, session));
+    if (error) {
+        goto out;
+    }
+
+    // Add the STUN session to the candidate
+    error = rawrtc_candidate_helper_stun_session_add(session, candidate, stun_keepalive);
+    if (error) {
+        goto out;
+    }
+
+    // Increase counter, start the STUN session & done
+    ++candidate->srflx_pending_count;
+    stun_keepalive_enable(stun_keepalive, rawrtc_default_config.stun_keepalive_interval);
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        DEBUG_WARNING("Could not create STUN request, reason: %s\n", rawrtc_code_to_str(error));
+        mem_deref(session);
+    }
+
+    // Un-reference & done
+    mem_deref(stun_keepalive);
+    return error;
+}
+
+/*
+ * Gather server reflexive and relay candidates using a specific ICE
+ * server.
+ */
+static void gather_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    struct sa* const address = &candidate->candidate->attr.addr;
+    int af;
+    enum rawrtc_code error;
+
+    // Skip IPv4, IPv6 (server [!] addresses)?
+    // TODO: Get config from struct
+    af = sa_af(&server_address->address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF(
+            "Ignoring ICE server address %j (family disabled)\n", &server_address->address);
+        return;
+    }
+
+    // Ignore 'any', loopback and link-local server (!) addresses
+    if (sa_is_any(&server_address->address) || sa_is_loopback(&server_address->address) ||
+        sa_is_linklocal(&server_address->address)) {
+        DEBUG_NOTICE("Ignoring ICE server address %j\n", &server_address->address);
+        return;
+    }
+
+    // Ignore loopback and link-local candidate (!) addresses (there is no mapped NAT address since
+    // the addresses aren't reachable from outside of the local network)
+    if (sa_is_linklocal(address) || sa_is_loopback(address)) {
+        return;
+    }
+
+    // Gather reflexive candidates
+    error = gather_reflexive_candidates(candidate, server_address);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not gather server reflexive candidates, reason: %s", rawrtc_code_to_str(error));
+        // Note: Considered non-critical, continuing
+    }
+
+    // Gather relay candidates
+    error = gather_relay_candidates(candidate, server_address);
+    if (error) {
+        DEBUG_WARNING("Could not gather relay candidates, reason: %s", rawrtc_code_to_str(error));
+        // Note: Considered non-critical, continuing
+    }
+}
+
+/*
+ * Gather server reflexive and relay candidates using a newly resolved
+ * ICE server URL address.
+ */
+static void gather_candidates_using_server(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_ice_server_url_address* const address  // not checked
+) {
+    struct le* le;
+    for (le = list_head(&gatherer->local_candidates); le != NULL; le = le->next) {
+        struct rawrtc_candidate_helper* const candidate = le->data;
+
+        // Gather candidates
+        gather_candidates(candidate, address);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Gather server reflexive candidates of a local candidate using
+ * already resolved ICE servers.
+ */
+static void gather_candidates_using_resolved_servers(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_candidate_helper* const candidate  // not checked
+) {
+    struct le* le;
+    for (le = list_head(&gatherer->url_addresses); le != NULL; le = le->next) {
+        struct rawrtc_ice_server_url_address* const address = le->data;
+
+        // Gather candidates
+        gather_candidates(candidate, address);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Add local candidate, gather server reflexive and relay candidates.
+ */
+static enum rawrtc_code add_candidate(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct sa const* const address,  // not checked
+    enum rawrtc_ice_protocol const protocol,
+    enum ice_tcptype const tcp_type) {
+    uint32_t priority;
+    int const ipproto = rawrtc_ice_protocol_to_ipproto(protocol);
+    struct ice_lcand* re_candidate;
+    int err;
+    struct rawrtc_candidate_helper* candidate;
+    enum rawrtc_code error;
+
+    // Add host candidate
+    priority = rawrtc_ice_candidate_calculate_priority(
+        list_count(trice_lcandl(gatherer->ice)), ICE_CAND_TYPE_HOST, ipproto, sa_af(address),
+        tcp_type);
+    // TODO: Set component id properly
+    err = trice_lcand_add(
+        &re_candidate, gatherer->ice, 1, ipproto, priority, address, NULL, ICE_CAND_TYPE_HOST, NULL,
+        tcp_type, NULL, RAWRTC_LAYER_ICE);
+    if (err) {
+        DEBUG_WARNING("Could not add host candidate, reason: %m\n", err);
+        return rawrtc_error_to_code(err);
+    }
+
+    // Create candidate helper (attaches receive handler)
+    error = rawrtc_candidate_helper_create(
+        &candidate, gatherer, re_candidate, udp_receive_handler, gatherer);
+    if (error) {
+        DEBUG_WARNING("Could not create candidate helper, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Add to local candidates list
+    list_append(&gatherer->local_candidates, &candidate->le, candidate);
+    DEBUG_PRINTF(
+        "Added %s host candidate for interface %j\n", rawrtc_ice_protocol_to_str(protocol),
+        address);
+
+    // Announce host candidate to handler
+    error = announce_candidate(gatherer, re_candidate, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not announce host candidate, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Gather server reflexive and relay candidates
+    gather_candidates_using_resolved_servers(gatherer, candidate);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Local interfaces callback.
+ * TODO: Consider ICE gather policy
+ * TODO: https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-01
+ */
+static bool interface_handler(
+    char const* interface,  // not checked
+    struct sa const* address,  // not checked
+    void* arg  // not checked
+) {
+    int af;
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    (void) interface;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return true;  // Don't continue gathering
+    }
+
+    // Ignore 'any' address
+    if (sa_is_any(address)) {
+        DEBUG_PRINTF("Ignoring gathered 'any' address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Ignore loopback address
+    // TODO: Make this configurable
+    if (sa_is_loopback(address)) {
+        DEBUG_PRINTF("Ignoring gathered loopback address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Ignore link-local address
+    // TODO: Make this configurable
+    if (sa_is_linklocal(address)) {
+        DEBUG_PRINTF("Ignoring gathered link-local address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Skip IPv4, IPv6?
+    // TODO: Get config from struct
+    af = sa_af(address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF("Ignoring gathered address %j (family disabled)\n", address);
+        return false;  // Continue gathering
+    }
+
+    // TODO: Ignore interfaces gathered twice
+
+    DEBUG_PRINTF("Gathered local interface %j\n", address);
+
+    // Add UDP candidate
+    if (rawrtc_default_config.udp_enable) {
+        error = add_candidate(gatherer, address, RAWRTC_ICE_PROTOCOL_UDP, ICE_TCP_ACTIVE);
+        if (error) {
+            DEBUG_WARNING("Could not add candidate, reason: %s", rawrtc_code_to_str(error));
+            goto out;
+        }
+
+        // Check state
+        if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+            return true;  // Don't continue gathering
+        }
+    }
+
+    // Add TCP candidate
+    if (rawrtc_default_config.tcp_enable) {
+        // TODO
+        // add_candidate(gatherer, address, RAWRTC_ICE_PROTOCOL_TCP, ICE_TCP_SO);
+        DEBUG_WARNING("TODO: Add TCP host candidate for interface %j\n", address);
+    }
+
+out:
+    if (error) {
+        // Close and don't continue gathering
+        rawrtc_ice_gatherer_close(gatherer);
+        return true;
+    } else {
+        return false;  // Continue gathering
+    }
+}
+
+/*
+ * ICE server URL address resolved handler.
+ */
+static bool ice_server_url_address_result_handler(
+    struct rawrtc_ice_server_url_address* const address,  // not checked, referenced
+    void* const arg  // not checked
+) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    DEBUG_INFO("Resolved URL %s to address %J\n", address->url->url, &address->address);
+
+    // Append to list of URL addresses
+    list_append(&gatherer->url_addresses, &address->le, mem_ref(address));
+
+    // Gather on the newly created address
+    gather_candidates_using_server(gatherer, address);
+
+    // Done, stop traversing, one address per family is sufficient
+    return true;
+}
+
+/*
+ * Resolve ICE server IP addresses.
+ */
+static enum rawrtc_code resolve_ice_server_addresses(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_ice_gather_options* const options  // not checked
+) {
+    struct le* le;
+
+    // Remove all ICE server URL resolvers
+    // Note: This will cancel pending URL resolve processes
+    list_flush(&gatherer->url_resolvers);
+
+    // Remove all resolved ICE server URL addresses
+    list_flush(&gatherer->url_addresses);
+
+    for (le = list_head(&options->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const ice_server = le->data;
+        struct le* url_le;
+        enum rawrtc_code error;
+
+        for (url_le = list_head(&ice_server->urls); url_le != NULL; url_le = url_le->next) {
+            struct rawrtc_ice_server_url* const url = url_le->data;
+            // URL already resolved (decoded IP address)?
+            if (!sa_is_any(&url->resolved_address)) {
+                struct rawrtc_ice_server_url_address* address;
+
+                // Create URL address from resolved URL
+                error = rawrtc_ice_server_url_address_create(&address, url, &url->resolved_address);
+                if (error) {
+                    DEBUG_WARNING(
+                        "Unable to create ICE server URL address, reason: %s\n",
+                        rawrtc_code_to_str(error));
+                    // Continue - not considered critical
+                } else {
+                    // Append to list of URL addresses
+                    list_append(&gatherer->url_addresses, &address->le, address);
+                }
+            } else {
+                // Create URL resolver for A record (if enabled)
+                if (rawrtc_default_config.ipv4_enable) {
+                    struct rawrtc_ice_server_url_resolver* resolver;
+                    error = rawrtc_ice_server_url_resolver_create(
+                        &resolver, gatherer->dns_client, DNS_TYPE_A, url,
+                        ice_server_url_address_result_handler, gatherer);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to query A record for URL %s, reason: %s\n", url->url,
+                            rawrtc_code_to_str(error));
+                        // Continue - not considered critical
+                    } else {
+                        // Append to list of URL resolvers
+                        list_append(&gatherer->url_resolvers, &resolver->le, resolver);
+                    }
+                }
+
+                // Create URL resolver for AAAA record (if enabled)
+                if (rawrtc_default_config.ipv6_enable) {
+                    struct rawrtc_ice_server_url_resolver* resolver;
+                    error = rawrtc_ice_server_url_resolver_create(
+                        &resolver, gatherer->dns_client, DNS_TYPE_AAAA, url,
+                        ice_server_url_address_result_handler, gatherer);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to query AAAA record for URL %s, reason: %s\n", url->url,
+                            rawrtc_code_to_str(error));
+                        // Continue - not considered critical
+                    } else {
+                        // Append to list of URL resolvers
+                        list_append(&gatherer->url_resolvers, &resolver->le, resolver);
+                    }
+                }
+            }
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Gathering timeout handler.
+ * Note: This timeout has no effect when using continuous gathering.
+ */
+static void gather_timeout_handler(void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+
+    // Force gathering complete
+    check_gathering_complete(gatherer, true);
+}
+
+/*
+ * Start gathering using an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_gather(
+    struct rawrtc_ice_gatherer* const gatherer,
+    struct rawrtc_ice_gather_options* options  // referenced, nullable
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+    if (!options) {
+        options = gatherer->options;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Already gathering?
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_GATHERING) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Resolve ICE server IP addresses
+    error = resolve_ice_server_addresses(gatherer, options);
+    if (error) {
+        return error;
+    }
+
+    // Update state
+    set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_GATHERING);
+
+    // Start timeout timer
+    // TODO: Make the timeout configurable
+    tmr_start(&gatherer->timeout_timer, 6000, gather_timeout_handler, gatherer);
+
+    // Start gathering host candidates
+    if (options->gather_policy != RAWRTC_ICE_GATHER_POLICY_NOHOST) {
+        net_if_apply(interface_handler, gatherer);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get local ICE parameters of an ICE gatherer.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_parameters(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!parametersp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Create and return ICE parameters instance
+    return rawrtc_ice_parameters_create(
+        parametersp, gatherer->ice_username_fragment, gatherer->ice_password, false);
+}
+
+/*
+ * Destructor for an existing local candidates array.
+ */
+static void rawrtc_ice_gatherer_local_candidates_destroy(void* arg) {
+    struct rawrtc_ice_candidates* const candidates = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < candidates->n_candidates; ++i) {
+        mem_deref(candidates->candidates[i]);
+    }
+}
+
+/*
+ * Get local ICE candidates of an ICE gatherer.
+ * `*candidatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_candidates(
+    struct rawrtc_ice_candidates** const candidatesp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    size_t n;
+    struct rawrtc_ice_candidates* candidates;
+    struct le* le;
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!candidatesp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get length
+    n = list_count(trice_lcandl(gatherer->ice));
+
+    // Allocate & set length immediately
+    candidates = mem_zalloc(
+        sizeof(*candidates) + (sizeof(struct rawrtc_ice_candidate*) * n),
+        rawrtc_ice_gatherer_local_candidates_destroy);
+    if (!candidates) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+    candidates->n_candidates = n;
+
+    // Copy each ICE candidate
+    for (le = list_head(trice_lcandl(gatherer->ice)), i = 0; le != NULL; le = le->next, ++i) {
+        struct ice_lcand* re_candidate = le->data;
+
+        // Create ICE candidate
+        error = rawrtc_ice_candidate_create_from_local_candidate(
+            &candidates->candidates[i], re_candidate);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(candidates);
+    } else {
+        // Set pointers
+        *candidatesp = candidates;
+    }
+    return error;
+}
diff --git a/src/ice_gatherer/gatherer.h b/src/ice_gatherer/gatherer.h
new file mode 100644
index 0000000..7f2d447
--- /dev/null
+++ b/src/ice_gatherer/gatherer.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <rawrtc/ice_gatherer.h>
+#include <re.h>
+#include <rew.h>
+
+enum {
+    RAWRTC_ICE_GATHERER_DNS_SERVERS = 10,
+    RAWRTC_ICE_USERNAME_FRAGMENT_LENGTH = 16,
+    RAWRTC_ICE_PASSWORD_LENGTH = 32,
+};
+
+struct rawrtc_ice_gatherer {
+    enum rawrtc_ice_gatherer_state state;
+    struct rawrtc_ice_gather_options* options;  // referenced
+    rawrtc_ice_gatherer_state_change_handler state_change_handler;  // nullable
+    rawrtc_ice_gatherer_error_handler error_handler;  // nullable
+    rawrtc_ice_gatherer_local_candidate_handler local_candidate_handler;  // nullable
+    void* arg;  // nullable
+    struct tmr timeout_timer;
+    struct list url_addresses;
+    struct list url_resolvers;
+    struct list buffered_messages;  // TODO: Can this be added to the candidates list?
+    struct list local_candidates;  // TODO: Hash list instead?
+    char ice_username_fragment[RAWRTC_ICE_USERNAME_FRAGMENT_LENGTH + 1];
+    char ice_password[RAWRTC_ICE_PASSWORD_LENGTH + 1];
+    struct trice* ice;
+    struct trice_conf ice_config;
+    struct dnsc* dns_client;
+};
diff --git a/src/ice_gatherer/meson.build b/src/ice_gatherer/meson.build
new file mode 100644
index 0000000..1276f93
--- /dev/null
+++ b/src/ice_gatherer/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'gatherer.c',
+    'utils.c',
+])
diff --git a/src/ice_gatherer/utils.c b/src/ice_gatherer/utils.c
new file mode 100644
index 0000000..982a5cb
--- /dev/null
+++ b/src/ice_gatherer/utils.c
@@ -0,0 +1,19 @@
+#include <rawrtc/ice_gatherer.h>
+
+/*
+ * Get the corresponding name for an ICE gatherer state.
+ */
+char const* rawrtc_ice_gatherer_state_to_name(enum rawrtc_ice_gatherer_state const state) {
+    switch (state) {
+        case RAWRTC_ICE_GATHERER_STATE_NEW:
+            return "new";
+        case RAWRTC_ICE_GATHERER_STATE_GATHERING:
+            return "gathering";
+        case RAWRTC_ICE_GATHERER_STATE_COMPLETE:
+            return "complete";
+        case RAWRTC_ICE_GATHERER_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/ice_parameters/attributes.c b/src/ice_parameters/attributes.c
new file mode 100644
index 0000000..a4bf66f
--- /dev/null
+++ b/src/ice_parameters/attributes.c
@@ -0,0 +1,54 @@
+#include "parameters.h"
+#include <rawrtc/ice_parameters.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the ICE parameter's username fragment value.
+ * `*username_fragmentp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!username_fragmentp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *username_fragmentp = mem_ref(parameters->username_fragment);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the ICE parameter's password value.
+ * `*passwordp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_password(
+    char** const passwordp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!passwordp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *passwordp = mem_ref(parameters->password);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the ICE parameter's ICE lite value.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_ice_lite(
+    bool* const ice_litep,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!ice_litep || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *ice_litep = parameters->ice_lite;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_parameters/meson.build b/src/ice_parameters/meson.build
new file mode 100644
index 0000000..8710eb0
--- /dev/null
+++ b/src/ice_parameters/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'parameters.c',
+    'utils.c',
+])
diff --git a/src/ice_parameters/parameters.c b/src/ice_parameters/parameters.c
new file mode 100644
index 0000000..e36cffb
--- /dev/null
+++ b/src/ice_parameters/parameters.c
@@ -0,0 +1,60 @@
+#include "parameters.h"
+#include <rawrtc/ice_parameters.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing ICE parameters instance.
+ */
+static void rawrtc_ice_parameters_destroy(void* arg) {
+    struct rawrtc_ice_parameters* const parameters = arg;
+
+    // Un-reference
+    mem_deref(parameters->username_fragment);
+    mem_deref(parameters->password);
+}
+
+/*
+ * Create a new ICE parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_create(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    char* const username_fragment,  // copied
+    char* const password,  // copied
+    bool const ice_lite) {
+    struct rawrtc_ice_parameters* parameters;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!parametersp || !username_fragment || !password) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    parameters = mem_zalloc(sizeof(*parameters), rawrtc_ice_parameters_destroy);
+    if (!parameters) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    error = rawrtc_strdup(&parameters->username_fragment, username_fragment);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_strdup(&parameters->password, password);
+    if (error) {
+        goto out;
+    }
+    parameters->ice_lite = ice_lite;
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
diff --git a/src/ice_parameters/parameters.h b/src/ice_parameters/parameters.h
new file mode 100644
index 0000000..a425ca9
--- /dev/null
+++ b/src/ice_parameters/parameters.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <re.h>
+
+struct rawrtc_ice_parameters {
+    char* username_fragment;  // copied
+    char* password;  // copied
+    bool ice_lite;
+};
+
+int rawrtc_ice_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_ice_parameters const* const parameters);
diff --git a/src/ice_parameters/utils.c b/src/ice_parameters/utils.c
new file mode 100644
index 0000000..53bfdb7
--- /dev/null
+++ b/src/ice_parameters/utils.c
@@ -0,0 +1,29 @@
+#include "parameters.h"
+#include <re.h>
+
+/*
+ * Print debug information for ICE parameters.
+ */
+int rawrtc_ice_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_ice_parameters const* const parameters) {
+    int err = 0;
+
+    // Check arguments
+    if (!parameters) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Parameters <%p>:\n", parameters);
+
+    // Username fragment
+    err |= re_hprintf(pf, "    username_fragment=\"%s\"\n", parameters->username_fragment);
+
+    // Password
+    err |= re_hprintf(pf, "    password=\"%s\"\n", parameters->password);
+
+    // ICE lite
+    err |= re_hprintf(pf, "    ice_lite=%s\n", parameters->ice_lite ? "yes" : "no");
+
+    // Done
+    return err;
+}
diff --git a/src/ice_server/address.c b/src/ice_server/address.c
new file mode 100644
index 0000000..a6fb176
--- /dev/null
+++ b/src/ice_server/address.c
@@ -0,0 +1,47 @@
+#include "address.h"
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Destructor for an ICE server URL address.
+ */
+static void rawrtc_ice_server_url_address_destroy(void* arg) {
+    struct rawrtc_ice_server_url_address* const address = arg;
+
+    // Remove from list
+    list_unlink(&address->le);
+
+    // Un-reference
+    mem_deref(address->url);
+}
+
+/*
+ * Create an ICE server URL address.
+ */
+enum rawrtc_code rawrtc_ice_server_url_address_create(
+    struct rawrtc_ice_server_url_address** const addressp,  // de-referenced
+    struct rawrtc_ice_server_url* const url,  // referenced
+    struct sa* const address  // copied
+) {
+    struct rawrtc_ice_server_url_address* url_address;
+
+    // Check arguments
+    if (!addressp || !url || !address) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    url_address = mem_zalloc(sizeof(*url_address), rawrtc_ice_server_url_address_destroy);
+    if (!url_address) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    url_address->url = mem_ref(url);
+    url_address->address = *address;
+
+    // Set pointer & done
+    *addressp = url_address;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_server/address.h b/src/ice_server/address.h
new file mode 100644
index 0000000..d39fc25
--- /dev/null
+++ b/src/ice_server/address.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server URL's resolved address.
+ */
+struct rawrtc_ice_server_url_address {
+    struct le le;
+    struct rawrtc_ice_server_url* url;  // referenced
+    struct sa address;
+};
+
+enum rawrtc_code rawrtc_ice_server_url_address_create(
+    struct rawrtc_ice_server_url_address** const addressp,  // de-referenced
+    struct rawrtc_ice_server_url* const url,  // referenced
+    struct sa* const address  // copied
+);
diff --git a/src/ice_server/meson.build b/src/ice_server/meson.build
new file mode 100644
index 0000000..0135c72
--- /dev/null
+++ b/src/ice_server/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'address.c',
+    'resolver.c',
+    'server.c',
+    'utils.c',
+])
diff --git a/src/ice_server/resolver.c b/src/ice_server/resolver.c
new file mode 100644
index 0000000..65ed350
--- /dev/null
+++ b/src/ice_server/resolver.c
@@ -0,0 +1,182 @@
+#include "resolver.h"
+#include "address.h"
+#include "server.h"
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+#define DEBUG_MODULE "ice-server-url-resolver"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * DNS A or AAAA record handler.
+ */
+static bool dns_record_result_handler(struct dnsrr* resource_record, void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+    struct rawrtc_ice_server_url* const url = resolver->url;
+    struct sa address;
+    enum rawrtc_code error;
+    struct rawrtc_ice_server_url_address* url_address;
+    bool stop;
+    DEBUG_PRINTF("DNS resource record: %H\n", dns_rr_print, resource_record);
+
+    // Set IP address
+    sa_cpy(&address, &url->resolved_address);
+    switch (resource_record->type) {
+        case DNS_TYPE_A:
+            // Set IPv4 address
+            sa_set_in(&address, resource_record->rdata.a.addr, sa_port(&address));
+            break;
+
+        case DNS_TYPE_AAAA:
+            // Set IPv6 address
+            sa_set_in6(&address, resource_record->rdata.aaaa.addr, sa_port(&address));
+            break;
+
+        default:
+            DEBUG_WARNING(
+                "Invalid DNS resource record, expected A/AAAA record, got: %H\n", dns_rr_print,
+                resource_record);
+            return true;  // stop traversing
+    }
+
+    // Create URL address
+    error = rawrtc_ice_server_url_address_create(&url_address, url, &address);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to create ICE server URL address, reason: %s\n", rawrtc_code_to_str(error));
+        return true;  // stop traversing
+    }
+
+    // Announce resolved IP address
+    stop = resolver->address_handler(url_address, resolver->arg);
+
+    // Un-reference
+    mem_deref(url_address);
+
+    // Done (continue or stop traversing)
+    return stop;
+}
+
+/*
+ * DNS query result handler.
+ */
+static void dns_query_handler(
+    int err,
+    struct dnshdr const* header,
+    struct list* answer_records,
+    struct list* authoritive_records,
+    struct list* additional_records,
+    void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+    (void) header;
+    (void) authoritive_records;
+    (void) additional_records;
+
+    // Handle error (if any)
+    if (err) {
+        DEBUG_WARNING("Could not query DNS record for '%r', reason: %m\n", &resolver->url->host);
+        goto out;
+    } else if (header->rcode != 0) {
+        DEBUG_NOTICE(
+            "DNS record query for '%r' unsuccessful: %s (%" PRIu8 ")\n", &resolver->url->host,
+            dns_hdr_rcodename(header->rcode), header->rcode);
+        goto out;
+    }
+
+    // Unlink self from any list
+    list_unlink(&resolver->le);
+
+    // Handle A or AAAA record
+    dns_rrlist_apply2(
+        answer_records, NULL, DNS_TYPE_A, DNS_TYPE_AAAA, DNS_CLASS_IN, true,
+        dns_record_result_handler, resolver);
+
+out:
+    // Unlink & un-reference self
+    // Note: We're unlinking twice here since the above unlink may be skipped in an error case.
+    //       This is perfectly safe.
+    list_unlink(&resolver->le);
+    mem_deref(resolver);
+}
+
+/*
+ * Destructor for an ICE server URL.
+ */
+static void rawrtc_ice_server_url_resolver_destroy(void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+
+    // Remove from list
+    list_unlink(&resolver->le);
+
+    // Un-reference
+    mem_deref(resolver->dns_query);
+    mem_deref(resolver->url);
+}
+
+/*
+ * Create an ICE server URL resolver.
+ *
+ * Important: Once the handler has been called, the resolver will unlink
+ *            from an associated list and un-reference itself.
+ */
+enum rawrtc_code rawrtc_ice_server_url_resolver_create(
+    struct rawrtc_ice_server_url_resolver** const resolverp,  // de-referenced
+    struct dnsc* const dns_client,
+    uint_fast16_t const dns_type,
+    struct rawrtc_ice_server_url* const url,  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_server_url_resolver* resolver;
+    char* host_str;
+
+    // Check arguments
+    if (!resolverp || !dns_client || !url || !address_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    resolver = mem_zalloc(sizeof(*resolver), rawrtc_ice_server_url_resolver_destroy);
+    if (!resolver) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    resolver->url = mem_ref(url);
+    resolver->address_handler = address_handler;
+    resolver->arg = arg;
+    resolver->dns_type = dns_type;
+
+    // Copy URL to str
+    error = rawrtc_error_to_code(pl_strdup(&host_str, &url->host));
+    if (error) {
+        goto out;
+    }
+
+    // Query A or AAAA record
+    error = rawrtc_error_to_code(dnsc_query(
+        &resolver->dns_query, dns_client, host_str, (uint16_t) dns_type, DNS_CLASS_IN, true,
+        dns_query_handler, resolver));
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(host_str);
+
+    if (error) {
+        mem_deref(resolver);
+    } else {
+        // Set pointer & done
+        *resolverp = resolver;
+    }
+
+    return error;
+}
diff --git a/src/ice_server/resolver.h b/src/ice_server/resolver.h
new file mode 100644
index 0000000..63e9c0f
--- /dev/null
+++ b/src/ice_server/resolver.h
@@ -0,0 +1,38 @@
+#pragma once
+#include "address.h"
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server URL address resolved handler.
+ *
+ * `*resolverp` must be referenced if used.
+ *
+ * Return `true` if you want to continue receiving further addresses
+ * from the URL's address entry. Be aware that you will be offered at
+ * least one IPv4 address and one IPv6 address per URL (if available)
+ * even if you always return `false`.
+ */
+typedef bool (*rawrtc_ice_server_url_address_resolved_handler)(
+    struct rawrtc_ice_server_url_address* const address, void* const arg);
+
+/*
+ * ICE server URL resolver.
+ */
+struct rawrtc_ice_server_url_resolver {
+    struct le le;
+    struct rawrtc_ice_server_url* url;  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler;
+    void* arg;
+    uint_fast16_t dns_type;
+    struct dns_query* dns_query;
+};
+
+enum rawrtc_code rawrtc_ice_server_url_resolver_create(
+    struct rawrtc_ice_server_url_resolver** const resolverp,  // de-referenced
+    struct dnsc* const dns_client,
+    uint_fast16_t const dns_type,
+    struct rawrtc_ice_server_url* const url,  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler,
+    void* const arg);
diff --git a/src/ice_server/server.c b/src/ice_server/server.c
new file mode 100644
index 0000000..2c52d0d
--- /dev/null
+++ b/src/ice_server/server.c
@@ -0,0 +1,449 @@
+#include "server.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "ice-server"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * ICE server URL-related regular expressions.
+ */
+static char const ice_server_url_regex[] = "[a-z]+:[^?]+[^]*";
+static char const ice_server_host_port_regex[] = "[^:]+[:]*[0-9]*";
+static char const ice_server_host_port_ipv6_regex[] = "\\[[0-9a-f:]+\\][:]*[0-9]*";
+static char const ice_server_transport_regex[] = "\\?transport=[a-z]+";
+
+/*
+ * Valid ICE server schemes.
+ *
+ * Note: Update `ice_server_scheme_type_mapping`,
+ * `ice_server_scheme_secure_mapping` and
+ * `ice_server_scheme_port_mapping` if changed.
+ */
+static char const* const ice_server_schemes[] = {
+    "stun",
+    "stuns",
+    "turn",
+    "turns",
+};
+static size_t const ice_server_schemes_length = ARRAY_SIZE(ice_server_schemes);
+
+/*
+ * ICE server scheme to server type mapping.
+ */
+static enum rawrtc_ice_server_type ice_server_scheme_type_mapping[] = {
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+};
+
+/*
+ * ICE server scheme to secure mapping.
+ */
+static bool ice_server_scheme_secure_mapping[] = {
+    false,
+    true,
+    false,
+    true,
+};
+
+/*
+ * ICE server scheme to default port mapping.
+ */
+static uint_fast16_t ice_server_scheme_port_mapping[] = {
+    3478,
+    5349,
+    3478,
+    5349,
+};
+
+/*
+ * Valid ICE server transports.
+ *
+ * Note: Update `ice_server_transport_normal_transport_mapping` and
+ * `ice_server_transport_secure_transport_mapping` if changed.
+ */
+static char const* const ice_server_transports[] = {
+    "udp",
+    "tcp",
+};
+static size_t const ice_server_transports_length = ARRAY_SIZE(ice_server_transports);
+
+/*
+ * ICE server transport to non-secure transport mapping.
+ */
+static enum rawrtc_ice_server_transport ice_server_transport_normal_transport_mapping[] = {
+    RAWRTC_ICE_SERVER_TRANSPORT_UDP,
+    RAWRTC_ICE_SERVER_TRANSPORT_TCP,
+};
+
+/*
+ * ICE server transport to secure transport mapping.
+ */
+static enum rawrtc_ice_server_transport ice_server_transport_secure_transport_mapping[] = {
+    RAWRTC_ICE_SERVER_TRANSPORT_DTLS,
+    RAWRTC_ICE_SERVER_TRANSPORT_TLS,
+};
+
+/*
+ * Parse ICE server's transport.
+ */
+static enum rawrtc_code decode_ice_server_transport(
+    enum rawrtc_ice_server_transport* const transportp,  // de-referenced, not checked
+    struct pl* const query,  // not checked
+    bool const secure) {
+    enum rawrtc_code error;
+    struct pl transport;
+    size_t i;
+
+    // Decode transport
+    error =
+        rawrtc_error_to_code(re_regex(query->p, query->l, ice_server_transport_regex, &transport));
+    if (error) {
+        return error;
+    }
+
+    // Translate transport to ICE server transport
+    for (i = 0; i < ice_server_transports_length; ++i) {
+        if (pl_strcmp(&transport, ice_server_transports[i]) == 0) {
+            if (!secure) {
+                *transportp = ice_server_transport_normal_transport_mapping[i];
+            } else {
+                *transportp = ice_server_transport_secure_transport_mapping[i];
+            }
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_INVALID_ARGUMENT;
+}
+
+/*
+ * Parse an ICE scheme to an ICE server type, 'secure' flag and
+ * default port.
+ */
+static enum rawrtc_code decode_ice_server_scheme(
+    enum rawrtc_ice_server_type* const typep,  // de-referenced, not checked
+    bool* const securep,  // de-referenced, not checked
+    uint_fast16_t* const portp,  // de-referenced, not checked
+    struct pl* const scheme  // not checked
+) {
+    size_t i;
+
+    // Translate scheme to ICE server type (and set if secure)
+    for (i = 0; i < ice_server_schemes_length; ++i) {
+        if (pl_strcmp(scheme, ice_server_schemes[i]) == 0) {
+            // Set values
+            *typep = ice_server_scheme_type_mapping[i];
+            *securep = ice_server_scheme_secure_mapping[i];
+            *portp = ice_server_scheme_port_mapping[i];
+
+            // Done
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_INVALID_ARGUMENT;
+}
+
+/*
+ * Parse an ICE server URL according to RFC 7064 and RFC 7065
+ * (although the `transport` part is inaccurate for RFC 7064 but it
+ * seems useful)
+ */
+static enum rawrtc_code decode_ice_server_url(
+    struct rawrtc_ice_server_url* const url  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl scheme;
+    struct pl host_port;
+    struct pl query;
+    bool secure;
+    struct pl port_pl;
+    uint_fast16_t port;
+
+    // Decode URL
+    error = rawrtc_error_to_code(
+        re_regex(url->url, strlen(url->url), ice_server_url_regex, &scheme, &host_port, &query));
+    if (error) {
+        DEBUG_WARNING("Invalid ICE server URL: %s\n", url->url);
+        goto out;
+    }
+
+    // TODO: Can scheme or host be NULL?
+
+    // Get server type, secure flag and default port from scheme
+    error = decode_ice_server_scheme(&url->type, &secure, &port, &scheme);
+    if (error) {
+        DEBUG_WARNING("Invalid scheme in ICE server URL (%s): %r\n", url->url, &scheme);
+        goto out;
+    }
+
+    // Set default address
+    sa_set_in(&url->resolved_address, INADDR_ANY, (uint16_t) port);
+
+    // Decode host: Either IPv4 or IPv6 including the port (if any)
+    // Try IPv6 first, then normal hostname/IPv4.
+    error = rawrtc_error_to_code(re_regex(
+        host_port.p, host_port.l, ice_server_host_port_ipv6_regex, &url->host, NULL, &port_pl));
+    if (error) {
+        error = rawrtc_error_to_code(re_regex(
+            host_port.p, host_port.l, ice_server_host_port_regex, &url->host, NULL, &port_pl));
+        if (error) {
+            DEBUG_WARNING(
+                "Invalid host or port in ICE server URL (%s): %r\n", url->url, &host_port);
+            goto out;
+        }
+
+        // Try decoding IPv4
+        sa_set(&url->resolved_address, &url->host, (uint16_t) port);
+    } else {
+        // Try decoding IPv6
+        error = rawrtc_error_to_code(sa_set(&url->resolved_address, &url->host, (uint16_t) port));
+        if (error) {
+            DEBUG_WARNING(
+                "Invalid IPv6 address in ICE server URL (%s): %r\n", url->url, &host_port);
+            goto out;
+        }
+    }
+
+    // Decode port (if any)
+    if (pl_isset(&port_pl)) {
+        uint_fast32_t port_u32;
+
+        // Get port
+        port_u32 = pl_u32(&port_pl);
+        if (port_u32 == 0 || port_u32 > UINT16_MAX) {
+            DEBUG_WARNING(
+                "Invalid port number in ICE server URL (%s): %" PRIu32 "\n", url->url, port_u32);
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Set port
+        sa_set_port(&url->resolved_address, (uint16_t) port_u32);
+    }
+
+    // Translate transport (if any) & secure flag to ICE server transport
+    if (pl_isset(&query)) {
+        error = decode_ice_server_transport(&url->transport, &query, secure);
+        if (error) {
+            DEBUG_WARNING("Invalid transport in ICE server URL (%s): %r\n", url->url, &query);
+            goto out;
+        }
+    } else {
+        // Set default transport (depending on secure flag)
+        if (secure) {
+            url->transport = rawrtc_default_config.ice_server_secure_transport;
+        } else {
+            url->transport = rawrtc_default_config.ice_server_normal_transport;
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    return error;
+}
+
+/*
+ * Destructor for URLs of the ICE gatherer.
+ */
+static void rawrtc_ice_server_url_destroy(void* arg) {
+    struct rawrtc_ice_server_url* const url = arg;
+
+    // Remove from list
+    list_unlink(&url->le);
+
+    // Un-reference
+    mem_deref(url->url);
+}
+
+/*
+ * Copy a URL for the ICE gatherer.
+ */
+static enum rawrtc_code rawrtc_ice_server_url_create(
+    struct rawrtc_ice_server_url** const urlp,  // de-referenced
+    char* const url_s  // copied
+) {
+    struct rawrtc_ice_server_url* url;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!urlp || !url_s) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    url = mem_zalloc(sizeof(*url), rawrtc_ice_server_url_destroy);
+    if (!url) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Copy URL
+    error = rawrtc_strdup(&url->url, url_s);
+    if (error) {
+        goto out;
+    }
+
+    // Parse URL
+    // Note: `url->host` points inside `url->url`, so we MUST have copied the URL first.
+    error = decode_ice_server_url(url);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(url);
+    } else {
+        // Set pointer
+        *urlp = url;
+    }
+    return error;
+}
+
+/*
+ * Destructor for an existing ICE server.
+ */
+static void rawrtc_ice_server_destroy(void* arg) {
+    struct rawrtc_ice_server* const server = arg;
+
+    // Un-reference
+    list_flush(&server->urls);
+    mem_deref(server->username);
+    mem_deref(server->credential);
+}
+
+/*
+ * Create an ICE server.
+ */
+enum rawrtc_code rawrtc_ice_server_create(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    size_t i;
+
+    // Check arguments
+    if (!serverp || !urls) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    server = mem_zalloc(sizeof(*server), rawrtc_ice_server_destroy);
+    if (!server) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Copy URLs to list
+    list_init(&server->urls);
+    for (i = 0; i < n_urls; ++i) {
+        struct rawrtc_ice_server_url* url;
+
+        // Ensure URLs aren't null
+        if (!urls[i]) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Copy URL
+        error = rawrtc_ice_server_url_create(&url, urls[i]);
+        if (error) {
+            goto out;
+        }
+
+        // Append URL to list
+        list_append(&server->urls, &url->le, url);
+    }
+
+    // Set fields
+    if (credential_type != RAWRTC_ICE_CREDENTIAL_TYPE_NONE) {
+        if (username) {
+            error = rawrtc_strdup(&server->username, username);
+            if (error) {
+                goto out;
+            }
+        }
+        if (credential) {
+            error = rawrtc_strdup(&server->credential, credential);
+            if (error) {
+                goto out;
+            }
+        }
+    }
+    server->credential_type = credential_type;  // TODO: Validation needed in case TOKEN is used?
+
+out:
+    if (error) {
+        mem_deref(server);
+    } else {
+        // Set pointer
+        *serverp = server;
+    }
+    return error;
+}
+
+/*
+ * Copy an ICE server.
+ */
+enum rawrtc_code rawrtc_ice_server_copy(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    struct rawrtc_ice_server* const source_server) {
+    size_t n_urls;
+    char** urls = NULL;
+    struct le* le;
+    size_t i;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!serverp || !source_server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create temporary ICE server URL array
+    n_urls = list_count(&source_server->urls);
+    if (n_urls > 0) {
+        urls = mem_alloc(sizeof(char*) * n_urls, NULL);
+        if (!urls) {
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+    }
+
+    // Copy ICE server URL (str) pointers
+    for (le = list_head(&source_server->urls), i = 0; le != NULL; le = le->next, ++i) {
+        struct rawrtc_ice_server_url* const url = le->data;
+        urls[i] = url->url;
+    }
+
+    // Copy
+    error = rawrtc_ice_server_create(
+        serverp, urls, n_urls, source_server->username, source_server->credential,
+        source_server->credential_type);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(urls);
+    return error;
+}
diff --git a/src/ice_server/server.h b/src/ice_server/server.h
new file mode 100644
index 0000000..da9c9a6
--- /dev/null
+++ b/src/ice_server/server.h
@@ -0,0 +1,49 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server type.
+ * Note: Update `ice_server_schemes` if changed.
+ */
+enum rawrtc_ice_server_type {
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+};
+
+struct rawrtc_ice_server {
+    struct le le;
+    struct list urls;  // deep-copied
+    char* username;  // copied
+    char* credential;  // copied
+    enum rawrtc_ice_credential_type credential_type;
+};
+
+/*
+ * ICE server URL. (list element)
+ */
+struct rawrtc_ice_server_url {
+    struct le le;
+    char* url;  // copied
+    struct pl host;  // points inside `url`
+    enum rawrtc_ice_server_type type;
+    enum rawrtc_ice_server_transport transport;
+    struct sa resolved_address;
+};
+
+enum rawrtc_code rawrtc_ice_server_create(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type);
+
+enum rawrtc_code rawrtc_ice_server_copy(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    struct rawrtc_ice_server* const source_server);
+
+int rawrtc_ice_server_debug(
+    struct re_printf* const pf, struct rawrtc_ice_server const* const server);
diff --git a/src/ice_server/utils.c b/src/ice_server/utils.c
new file mode 100644
index 0000000..0b1ac90
--- /dev/null
+++ b/src/ice_server/utils.c
@@ -0,0 +1,103 @@
+#include "server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+/*
+ * Get the corresponding name for an ICE server type.
+ */
+static char const* ice_server_type_to_name(enum rawrtc_ice_server_type const type) {
+    switch (type) {
+        case RAWRTC_ICE_SERVER_TYPE_STUN:
+            return "stun";
+        case RAWRTC_ICE_SERVER_TYPE_TURN:
+            return "turn";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for an ICE server transport.
+ */
+static char const* ice_server_transport_to_name(enum rawrtc_ice_server_transport const transport) {
+    switch (transport) {
+        case RAWRTC_ICE_SERVER_TRANSPORT_UDP:
+            return "udp";
+        case RAWRTC_ICE_SERVER_TRANSPORT_TCP:
+            return "tcp";
+        case RAWRTC_ICE_SERVER_TRANSPORT_DTLS:
+            return "dtls";
+        case RAWRTC_ICE_SERVER_TRANSPORT_TLS:
+            return "tls";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for an ICE credential type.
+ */
+static char const* ice_credential_type_to_name(enum rawrtc_ice_credential_type const type) {
+    switch (type) {
+        case RAWRTC_ICE_CREDENTIAL_TYPE_NONE:
+            return "n/a";
+        case RAWRTC_ICE_CREDENTIAL_TYPE_PASSWORD:
+            return "password";
+        case RAWRTC_ICE_CREDENTIAL_TYPE_TOKEN:
+            return "token";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Print debug information for an ICE server.
+ */
+int rawrtc_ice_server_debug(
+    struct re_printf* const pf, struct rawrtc_ice_server const* const server) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!server) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Server <%p>:\n", server);
+
+    // Credential type
+    err |= re_hprintf(
+        pf, "    credential_type=%s\n", ice_credential_type_to_name(server->credential_type));
+    if (server->credential_type != RAWRTC_ICE_CREDENTIAL_TYPE_NONE) {
+        // Username
+        err |= re_hprintf(pf, "    username=");
+        if (server->username) {
+            err |= re_hprintf(pf, "\"%s\"\n", server->username);
+        } else {
+            err |= re_hprintf(pf, "n/a\n");
+        }
+
+        // Credential
+        err |= re_hprintf(pf, "    credential=");
+        if (server->credential) {
+            err |= re_hprintf(pf, "\"%s\"\n", server->credential);
+        } else {
+            err |= re_hprintf(pf, "n/a\n");
+        }
+    }
+
+    // URLs
+    for (le = list_head(&server->urls); le != NULL; le = le->next) {
+        struct rawrtc_ice_server_url* const url = le->data;
+
+        // URL, STUN/TURN, transport, currently gathering?
+        err |= re_hprintf(
+            pf, "    URL=\"%s\" type=%s transport=%s resolved=%s\n", url->url,
+            ice_server_type_to_name(url->type), ice_server_transport_to_name(url->transport),
+            sa_is_any(&url->resolved_address) ? "no" : "yes");
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/ice_transport/attributes.c b/src/ice_transport/attributes.c
new file mode 100644
index 0000000..36956f0
--- /dev/null
+++ b/src/ice_transport/attributes.c
@@ -0,0 +1,58 @@
+#include "transport.h"
+#include "../ice_gatherer/gatherer.h"
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the current ICE role of the ICE transport.
+ * Return `RAWRTC_CODE_NO_VALUE` code in case the ICE role has not been
+ * determined yet.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    struct rawrtc_ice_transport* const transport) {
+    enum ice_role re_role;
+    enum rawrtc_code error;
+    enum rawrtc_ice_role role;
+
+    // Check arguments
+    if (!rolep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get libre role from ICE instance
+    re_role = trice_local_role(transport->gatherer->ice);
+
+    // Translate role
+    error = rawrtc_re_ice_role_to_ice_role(&role, re_role);
+    if (error) {
+        return error;
+    }
+
+    // Unknown?
+    if (re_role == ICE_ROLE_UNKNOWN) {
+        return RAWRTC_CODE_NO_VALUE;
+    } else {
+        // Set pointer
+        *rolep = role;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current state of the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_ice_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_transport/meson.build b/src/ice_transport/meson.build
new file mode 100644
index 0000000..0227c31
--- /dev/null
+++ b/src/ice_transport/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/ice_transport/transport.c b/src/ice_transport/transport.c
new file mode 100644
index 0000000..a6c914e
--- /dev/null
+++ b/src/ice_transport/transport.c
@@ -0,0 +1,579 @@
+#include "transport.h"
+#include "../dtls_transport/transport.h"
+#include "../ice_candidate/candidate.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_parameters/parameters.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+#define DEBUG_MODULE "ice-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Destructor for an existing ICE transport.
+ */
+static void rawrtc_ice_transport_destroy(void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+
+    // Stop transport
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_ice_transport_stop(transport);
+
+    // Un-reference
+    mem_deref(transport->stun_client);
+    mem_deref(transport->remote_parameters);
+    mem_deref(transport->gatherer);
+}
+
+/*
+ * Create a new ICE transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_transport_create(
+    struct rawrtc_ice_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced, nullable
+    rawrtc_ice_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_transport_candidate_pair_change_handler const
+        candidate_pair_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_ice_transport* transport;
+    struct stun_conf stun_config = {
+        // TODO: Make this configurable!
+        .rto = 100,  // 100ms
+        .rc = 7,  // Send at: 0ms, 100ms, 300ms, 700ms, 1500ms, 3100ms, 6300ms
+        .rm = 60,  // Additional wait: 60*100 -> 6000ms
+        .ti = 12300,  // Timeout after: 12300ms
+        .tos = 0x00,
+    };
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transportp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check ICE gatherer state
+    // TODO: Check if gatherer.component is RTCP -> invalid state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_ice_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    transport->state = RAWRTC_ICE_TRANSPORT_STATE_NEW;  // TODO: Raise state (delayed)?
+    transport->gatherer = mem_ref(gatherer);
+    transport->state_change_handler = state_change_handler;
+    transport->candidate_pair_change_handler = candidate_pair_change_handler;
+    transport->arg = arg;
+    transport->remote_end_of_candidates = false;
+
+    // Create STUN client
+    error = rawrtc_error_to_code(stun_alloc(&transport->stun_client, &stun_config, NULL, NULL));
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
+
+/*
+ * Change the state of the ICE transport.
+ * Will call the corresponding handler.
+ */
+static void set_state(
+    struct rawrtc_ice_transport* const transport, enum rawrtc_ice_transport_state const state) {
+    // Set state
+    transport->state = state;
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+/*
+ * Check if the ICE checklist process is complete.
+ */
+static void check_ice_checklist_complete(
+    struct rawrtc_ice_transport* const transport  // not checked
+) {
+    struct trice* const ice = transport->gatherer->ice;
+
+    // Completed all candidate pairs?
+    if (trice_checklist_iscompleted(ice)) {
+        struct le;
+
+        DEBUG_INFO("Checklist completed\n");
+        DEBUG_PRINTF("%H", trice_debug, ice);
+
+        // Stop the checklist
+        trice_checklist_stop(ice);
+
+        // Remove STUN and TURN sessions from local candidate helpers since the keep-alive
+        // mechanism now moves over to the peers themselves.
+        list_apply(
+            &transport->gatherer->local_candidates, true,
+            rawrtc_candidate_helper_remove_stun_sessions_handler, NULL);
+
+        // Start keep-alive for active candidate pairs
+        // TODO: Implement!
+        //        start_keepalive(transport);
+
+        // Do we have one candidate pair that succeeded?
+        if (!list_isempty(trice_validl(ice))) {
+            // Have we received the remote end-of-candidates indication?
+            if (transport->remote_end_of_candidates) {
+                DEBUG_INFO("ICE connection completed\n");
+                set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_COMPLETED);
+            }
+        } else {
+            // No, transition to failed
+            DEBUG_INFO("ICE connection failed\n");
+            set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_FAILED);
+        }
+    }
+}
+
+/*
+ * ICE connection established callback.
+ */
+static void ice_established_handler(
+    struct ice_candpair* candidate_pair, struct stun_msg const* message, void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+    enum rawrtc_code error;
+    (void) message;
+
+    DEBUG_PRINTF("Candidate pair established: %H\n", trice_candpair_debug, candidate_pair);
+
+    // Ignore if closed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return;
+    }
+
+    // State: checking -> connected
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CHECKING) {
+        DEBUG_INFO("ICE connection established\n");
+        set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_CONNECTED);
+    }
+
+    // Ignore if completed or failed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED) {
+        return;
+    }
+
+    // Offer candidate pair to DTLS transport (if any)
+    // TODO: Offer to whatever transport lays above so we are SRTP/QUIC compatible
+    if (transport->dtls_transport) {
+        error = rawrtc_dtls_transport_add_candidate_pair(transport->dtls_transport, candidate_pair);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS transport could not attach to candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+
+            // Important: Removing a candidate pair can lead to segfaults due to STUN transaction
+            //            timers looking up the pair. Don't do it!
+        }
+    }
+
+    // TODO: Call candidate_pair_change_handler (?)
+
+    // ICE checklist process complete?
+    check_ice_checklist_complete(transport);
+}
+
+/*
+ * ICE connection failed callback.
+ */
+static void ice_failed_handler(
+    int err, uint16_t stun_code, struct ice_candpair* candidate_pair, void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+    (void) err;
+    (void) stun_code;
+    (void) candidate_pair;
+
+    DEBUG_PRINTF(
+        "Candidate pair failed: %H (%m %" PRIu16 ")\n", trice_candpair_debug, candidate_pair, err,
+        stun_code);
+
+    // Ignore if closed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return;
+    }
+
+    // Ignore if completed or failed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED) {
+        return;
+    }
+
+    // ICE checklist process complete?
+    check_ice_checklist_complete(transport);
+
+    // Important: Removing the failed candidate pair can lead to segfaults due to STUN transaction
+    //            timers looking up the pair. Don't do it!
+}
+
+/*
+ * Start the ICE transport.
+ * TODO https://github.com/w3c/ortc/issues/607
+ */
+enum rawrtc_code rawrtc_ice_transport_start(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced
+    struct rawrtc_ice_parameters* const remote_parameters,  // referenced
+    enum rawrtc_ice_role const role) {
+    bool ice_transport_closed;
+    bool ice_gatherer_closed;
+    enum ice_role translated_role;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !gatherer || !remote_parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate parameters
+    if (!remote_parameters->username_fragment || !remote_parameters->password) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Handle ICE lite
+    if (remote_parameters->ice_lite) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // TODO: Check that components of ICE gatherer and ICE transport match
+
+    // Check state
+    ice_transport_closed = transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED;
+    ice_gatherer_closed = gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED;
+    if (ice_transport_closed || ice_gatherer_closed) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Handle ICE restart when called again
+    if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check if gatherer instance is different
+    // TODO https://github.com/w3c/ortc/issues/607
+    if (transport->gatherer != gatherer) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Set role (abort if unknown or something entirely weird)
+    translated_role = rawrtc_ice_role_to_re_ice_role(role);
+    error = rawrtc_error_to_code(trice_set_role(transport->gatherer->ice, translated_role));
+    if (error) {
+        return error;
+    }
+
+    // New/first remote parameters?
+    if (transport->remote_parameters != remote_parameters) {
+        // Apply username fragment and password on trice
+        error = rawrtc_error_to_code(
+            trice_set_remote_ufrag(transport->gatherer->ice, remote_parameters->username_fragment));
+        if (error) {
+            return error;
+        }
+        error = rawrtc_error_to_code(
+            trice_set_remote_pwd(transport->gatherer->ice, remote_parameters->password));
+        if (error) {
+            return error;
+        }
+
+        // Replace
+        mem_deref(transport->remote_parameters);
+        transport->remote_parameters = mem_ref(remote_parameters);
+    }
+
+    // Set state to checking
+    // TODO: Get more states from trice
+    set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_CHECKING);
+
+    // Start checklist (if remote candidates exist)
+    if (!list_isempty(trice_rcandl(transport->gatherer->ice))) {
+        // TODO: Get config from struct
+        DEBUG_INFO("Starting checklist due to start event\n");
+        error = rawrtc_error_to_code(trice_checklist_start(
+            transport->gatherer->ice, transport->stun_client, rawrtc_default_config.pacing_interval,
+            ice_established_handler, ice_failed_handler, transport));
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Stop and close the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_stop(struct rawrtc_ice_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Already closed?
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Stop ICE checklist (if running)
+    if (trice_checklist_isrunning(transport->gatherer->ice)) {
+        trice_checklist_stop(transport->gatherer->ice);
+    }
+
+    // TODO: Remove remote candidates, role, username fragment and password from rew
+
+    // TODO: Remove from RTCICETransportController (once we have it)
+
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add a remote candidate ot the ICE transport.
+ * Note: 'candidate' must be NULL to inform the transport that the
+ * remote site finished gathering.
+ */
+enum rawrtc_code rawrtc_ice_transport_add_remote_candidate(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* candidate  // nullable
+) {
+    struct ice_rcand* re_candidate = NULL;
+    enum rawrtc_code error;
+    char* ip = NULL;
+    uint16_t port;
+    struct sa address = {0};
+    int af;
+    enum rawrtc_ice_protocol protocol;
+    char* foundation = NULL;
+    uint32_t priority;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address = NULL;
+
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check ICE transport state
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Remote site completed gathering?
+    if (!candidate) {
+        if (!transport->remote_end_of_candidates) {
+            DEBUG_PRINTF(
+                "Remote site gathering complete\n%H", trice_debug, transport->gatherer->ice);
+
+            // Transition to 'complete' if the checklist is done
+            // Note: 'completed' and 'failed' states are covered in checks above
+            if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW &&
+                !trice_checklist_isrunning(transport->gatherer->ice)) {
+                set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_COMPLETED);
+            }
+
+            // Mark that we've received end-of-candidates
+            transport->remote_end_of_candidates = true;
+        }
+
+        // Done
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // New remote candidate after end-of-candidates indication?
+    if (transport->remote_end_of_candidates) {
+        DEBUG_NOTICE("Tried to add a remote candidate after end-of-candidates\n");
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get IP and port
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_port(&port, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_error_to_code(sa_set_str(&address, ip, port));
+    if (error) {
+        goto out;
+    }
+
+    // Skip IPv4, IPv6 if requested
+    // TODO: Get config from struct
+    af = sa_af(&address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF("Skipping remote candidate due to IP version: %J\n", &address);
+        goto out;
+    }
+
+    // Get protocol
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+
+    // Skip UDP/TCP if requested
+    // TODO: Get config from struct
+    if ((!rawrtc_default_config.udp_enable && protocol == RAWRTC_ICE_PROTOCOL_UDP) ||
+        (!rawrtc_default_config.tcp_enable && protocol == RAWRTC_ICE_PROTOCOL_TCP)) {
+        DEBUG_PRINTF("Skipping remote candidate due to protocol: %J\n", &address);
+        goto out;
+    }
+
+    // Get necessary vars
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_type(&type, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            // Doesn't matter what we choose here, protocol is not TCP anyway
+            tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+            break;
+        default:
+            goto out;
+    }
+
+    // Add remote candidate
+    // TODO: Set correct component ID
+    error = rawrtc_error_to_code(trice_rcand_add(
+        &re_candidate, transport->gatherer->ice, 1, foundation,
+        rawrtc_ice_protocol_to_ipproto(protocol), priority, &address,
+        rawrtc_ice_candidate_type_to_ice_cand_type(type),
+        rawrtc_ice_tcp_candidate_type_to_ice_tcptype(tcp_type)));
+    if (error) {
+        goto out;
+    }
+
+    // Set related address (if any)
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate);
+    if (!error) {
+        error = rawrtc_ice_candidate_get_related_port(&port, candidate);
+        if (!error) {
+            error = rawrtc_error_to_code(
+                sa_set_str(&re_candidate->attr.rel_addr, related_address, port));
+            if (error) {
+                goto out;
+            }
+        }
+    }
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+
+    // TODO: Add TURN permission
+
+    // Done
+    DEBUG_PRINTF("Added remote candidate: %J\n", &address);
+    error = RAWRTC_CODE_SUCCESS;
+
+    // Start checklist (if not new, not started and not completed or failed)
+    // TODO: Get config from struct
+    if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW &&
+        transport->state != RAWRTC_ICE_TRANSPORT_STATE_COMPLETED &&
+        transport->state != RAWRTC_ICE_TRANSPORT_STATE_FAILED &&
+        !trice_checklist_isrunning(transport->gatherer->ice)) {
+        DEBUG_INFO("Starting checklist due to new remote candidate\n");
+        error = rawrtc_error_to_code(trice_checklist_start(
+            transport->gatherer->ice, transport->stun_client, rawrtc_default_config.pacing_interval,
+            ice_established_handler, ice_failed_handler, transport));
+        if (error) {
+            DEBUG_WARNING("Could not start checklist, reason: %s\n", rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(re_candidate);  // TODO: Not entirely sure about that
+    }
+
+    // Free vars
+    mem_deref(related_address);
+    mem_deref(foundation);
+    mem_deref(ip);
+
+    return error;
+}
+
+/*
+ * Set the remote candidates on the ICE transport overwriting all
+ * existing remote candidates.
+ */
+enum rawrtc_code rawrtc_ice_transport_set_remote_candidates(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* const candidates[],  // referenced (each item)
+    size_t const n_candidates) {
+    size_t i;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !candidates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Our implementation is incorrect here, it should remove
+    //       previously added remote candidates and replace them. Fix this
+    //       once we can handle an ICE restart.
+
+    // Add each remote candidate
+    for (i = 0; i < n_candidates; ++i) {
+        error = rawrtc_ice_transport_add_remote_candidate(transport, candidates[i]);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_transport/transport.h b/src/ice_transport/transport.h
new file mode 100644
index 0000000..60d938b
--- /dev/null
+++ b/src/ice_transport/transport.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+struct rawrtc_ice_transport {
+    enum rawrtc_ice_transport_state state;
+    struct rawrtc_ice_gatherer* gatherer;  // referenced
+    rawrtc_ice_transport_state_change_handler state_change_handler;  // nullable
+    rawrtc_ice_transport_candidate_pair_change_handler candidate_pair_change_handler;  // nullable
+    void* arg;  // nullable
+    struct stun* stun_client;
+    struct rawrtc_ice_parameters* remote_parameters;  // referenced
+    struct rawrtc_dtls_transport* dtls_transport;  // referenced, nullable
+    bool remote_end_of_candidates;
+};
+
+enum ice_role rawrtc_ice_role_to_re_ice_role(enum rawrtc_ice_role const role);
+
+enum rawrtc_code rawrtc_re_ice_role_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    enum ice_role const re_role);
diff --git a/src/ice_transport/utils.c b/src/ice_transport/utils.c
new file mode 100644
index 0000000..32969d8
--- /dev/null
+++ b/src/ice_transport/utils.c
@@ -0,0 +1,114 @@
+#include "transport.h"
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_ice_transport_state_to_name(enum rawrtc_ice_transport_state const state) {
+    switch (state) {
+        case RAWRTC_ICE_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_ICE_TRANSPORT_STATE_CHECKING:
+            return "checking";
+        case RAWRTC_ICE_TRANSPORT_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_ICE_TRANSPORT_STATE_COMPLETED:
+            return "completed";
+        case RAWRTC_ICE_TRANSPORT_STATE_DISCONNECTED:
+            return "disconnected";
+        case RAWRTC_ICE_TRANSPORT_STATE_FAILED:
+            return "failed";
+        case RAWRTC_ICE_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
+
+static enum rawrtc_ice_role const map_enum_ice_role[] = {
+    RAWRTC_ICE_ROLE_CONTROLLING,
+    RAWRTC_ICE_ROLE_CONTROLLED,
+};
+
+static char const* const map_str_ice_role[] = {
+    "controlling",
+    "controlled",
+};
+
+static size_t const map_ice_role_length = ARRAY_SIZE(map_enum_ice_role);
+
+/*
+ * Translate an ICE role to str.
+ */
+char const* rawrtc_ice_role_to_str(enum rawrtc_ice_role const role) {
+    size_t i;
+
+    for (i = 0; i < map_ice_role_length; ++i) {
+        if (map_enum_ice_role[i] == role) {
+            return map_str_ice_role[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!rolep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_role_length; ++i) {
+        if (str_casecmp(map_str_ice_role[i], str) == 0) {
+            *rolep = map_enum_ice_role[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate an ICE role to the corresponding re type.
+ */
+enum ice_role rawrtc_ice_role_to_re_ice_role(enum rawrtc_ice_role const role) {
+    // No conversion needed
+    return (enum ice_role) role;
+}
+
+/*
+ * Translate a re ICE role to the corresponding rawrtc role.
+ */
+enum rawrtc_code rawrtc_re_ice_role_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    enum ice_role const re_role) {
+    // Check arguments
+    if (!rolep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Translate role
+    switch (re_role) {
+        case ICE_ROLE_CONTROLLING:
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLING;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_ROLE_CONTROLLED:
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLED;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_ROLE_UNKNOWN:
+            *rolep = RAWRTC_ICE_ROLE_UNKNOWN;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
diff --git a/src/main/config.c b/src/main/config.c
new file mode 100644
index 0000000..2817bf0
--- /dev/null
+++ b/src/main/config.c
@@ -0,0 +1,27 @@
+#include "config.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+/*
+ * Default rawrtc configuration.
+ */
+struct rawrtc_config rawrtc_default_config = {
+    .pacing_interval = 20,
+    .ipv4_enable = true,
+    .ipv6_enable = true,
+    .udp_enable = true,
+    .tcp_enable = false,  // TODO: true by default
+    .sign_algorithm = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    .ice_server_normal_transport = RAWRTC_ICE_SERVER_TRANSPORT_UDP,
+    .ice_server_secure_transport = RAWRTC_ICE_SERVER_TRANSPORT_TLS,
+    .stun_keepalive_interval = 25,
+    .stun_config =
+        {
+            .rto = STUN_DEFAULT_RTO,
+            .rc = STUN_DEFAULT_RC,
+            .rm = STUN_DEFAULT_RM,
+            .ti = STUN_DEFAULT_TI,
+            .tos = 0x00,
+        },
+};
diff --git a/src/main/config.h b/src/main/config.h
new file mode 100644
index 0000000..f00c2fd
--- /dev/null
+++ b/src/main/config.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+struct rawrtc_config {
+    uint32_t pacing_interval;
+    bool ipv4_enable;
+    bool ipv6_enable;
+    bool udp_enable;
+    bool tcp_enable;
+    enum rawrtc_certificate_sign_algorithm sign_algorithm;
+    enum rawrtc_ice_server_transport ice_server_normal_transport;
+    enum rawrtc_ice_server_transport ice_server_secure_transport;
+    uint32_t stun_keepalive_interval;
+    struct stun_conf stun_config;
+};
+
+extern struct rawrtc_config rawrtc_default_config;
diff --git a/src/main/main.c b/src/main/main.c
new file mode 100644
index 0000000..aa25b20
--- /dev/null
+++ b/src/main/main.c
@@ -0,0 +1,75 @@
+#include "main.h"
+#include <rawrtc/config.h>
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/main.h>
+#include <re.h>
+
+#define DEBUG_MODULE "rawrtc-main"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+struct rawrtc_global rawrtc_global;
+
+/*
+ * Handle RAWRTCDC timer tick expired.
+ */
+static inline void rawrtcdc_timer_tick_expired_handler(void* arg) {
+    (void) arg;
+
+    // Restart timer
+    tmr_start(
+        &rawrtc_global.rawrtcdc_timer, (uint64_t) rawrtc_global.rawrtcdc_timer_interval,
+        rawrtcdc_timer_tick_expired_handler, NULL);
+
+    // Handle timer tick
+    rawrtcdc_timer_tick(rawrtc_global.rawrtcdc_timer_interval);
+}
+
+/*
+ * RAWRTCDC timer handler.
+ */
+static inline enum rawrtc_code rawrtcdc_timer_tick_handler(
+    bool const on, uint_fast16_t const interval) {
+    // Start or stop timer?
+    if (on) {
+        // Store interval, initialise & start timer
+        rawrtc_global.rawrtcdc_timer_interval = interval;
+        tmr_start(
+            &rawrtc_global.rawrtcdc_timer, (uint64_t) rawrtc_global.rawrtcdc_timer_interval,
+            rawrtcdc_timer_tick_expired_handler, NULL);
+        DEBUG_PRINTF("Started RAWRTCDC timer\n");
+    } else {
+        tmr_cancel(&rawrtc_global.rawrtcdc_timer);
+        DEBUG_PRINTF("Stopped RAWRTCDC timer\n");
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Initialise RAWRTC. Must be called before making a call to any other
+ * function.
+ *
+ * Note: In case `init_re` is not set to `true`, you MUST initialise
+ *       re yourselves before calling this function.
+ */
+enum rawrtc_code rawrtc_init(bool const init_re) {
+    // Initialise timer
+    tmr_init(&rawrtc_global.rawrtcdc_timer);
+
+    // Initialise RAWRTCDC
+    return rawrtcdc_init(init_re, rawrtcdc_timer_tick_handler);
+}
+
+/*
+ * Close RAWRTC and free up all resources.
+ *
+ * Note: In case `close_re` is not set to `true`, you MUST close
+ *       re yourselves.
+ */
+enum rawrtc_code rawrtc_close(bool const close_re) {
+    // Close RAWRTCDC
+    return rawrtcdc_close(close_re);
+}
diff --git a/src/main/main.h b/src/main/main.h
new file mode 100644
index 0000000..8b485d6
--- /dev/null
+++ b/src/main/main.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <re.h>
+
+extern struct rawrtc_global rawrtc_global;
+
+/*
+ * Global RAWRTC vars.
+ */
+struct rawrtc_global {
+    struct tmr rawrtcdc_timer;
+    uint_fast16_t rawrtcdc_timer_interval;
+};
diff --git a/src/main/meson.build b/src/main/meson.build
new file mode 100644
index 0000000..8e92d19
--- /dev/null
+++ b/src/main/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'config.c',
+    'main.c',
+])
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..7c17410
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,25 @@
+sources = []
+
+subdir('certificate')
+subdir('diffie_hellman_parameters')
+subdir('dtls_fingerprint')
+subdir('dtls_parameters')
+subdir('dtls_transport')
+subdir('ice_candidate')
+subdir('ice_gather_options')
+subdir('ice_gatherer')
+subdir('ice_parameters')
+subdir('ice_server')
+subdir('ice_transport')
+subdir('main')
+subdir('peer_connection')
+subdir('peer_connection_configuration')
+subdir('peer_connection_description')
+subdir('peer_connection_ice_candidate')
+subdir('peer_connection_state')
+subdir('sctp_common')
+if get_option('sctp_redirect_transport')
+    subdir('sctp_redirect_transport')
+endif
+subdir('sctp_transport')
+subdir('utils')
diff --git a/src/peer_connection/attributes.c b/src/peer_connection/attributes.c
new file mode 100644
index 0000000..392bad1
--- /dev/null
+++ b/src/peer_connection/attributes.c
@@ -0,0 +1,492 @@
+#include "connection.h"
+#include "../peer_connection_description/description.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/data_channel.h>
+#include <re.h>
+
+/*
+ * Get local description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no local description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference description (if any)
+    if (connection->local_description) {
+        *descriptionp = mem_ref(connection->local_description);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get remote description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_remote_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference description (if any)
+    if (connection->remote_description) {
+        *descriptionp = mem_ref(connection->remote_description);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the current signalling state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state(
+    enum rawrtc_signaling_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = connection->signaling_state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current ICE gathering state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    // Note: The W3C spec requires us to return 'new' in case no ICE gatherer exists.
+    // Note: Theoretically there's no 'closed' state on the peer connection variant. We ignore
+    //       that here.
+    if (connection->context.ice_gatherer) {
+        return rawrtc_ice_gatherer_get_state(statep, connection->context.ice_gatherer);
+    } else {
+        *statep = RAWRTC_ICE_GATHERER_STATE_NEW;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current ICE connection state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    // Note: The W3C spec requires us to return 'new' in case no ICE transport exists.
+    if (connection->context.ice_transport) {
+        return rawrtc_ice_transport_get_state(statep, connection->context.ice_transport);
+    } else {
+        *statep = RAWRTC_ICE_TRANSPORT_STATE_NEW;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current (peer) connection state of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state(
+    enum rawrtc_peer_connection_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = connection->connection_state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get indication whether the remote peer accepts trickled ICE
+ * candidates.
+ *
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_can_trickle_ice_candidates(
+    bool* const can_trickle_ice_candidatesp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!can_trickle_ice_candidatesp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set flag (if remote description set)
+    if (connection->remote_description) {
+        *can_trickle_ice_candidatesp = connection->remote_description->trickle_ice;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Unset the handler argument and all handlers of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_unset_handlers(
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Unset handler argument
+    connection->arg = NULL;
+
+    // Unset all handlers
+    connection->data_channel_handler = NULL;
+    connection->connection_state_change_handler = NULL;
+    connection->ice_gathering_state_change_handler = NULL;
+    connection->ice_connection_state_change_handler = NULL;
+    connection->signaling_state_change_handler = NULL;
+    connection->local_candidate_error_handler = NULL;
+    connection->local_candidate_handler = NULL;
+    connection->negotiation_needed_handler = NULL;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the peer connection's negotiation needed handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_negotiation_needed_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set negotiation needed handler & done
+    connection->negotiation_needed_handler = negotiation_needed_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's negotiation needed handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_negotiation_needed_handler(
+    rawrtc_negotiation_needed_handler* const negotiation_needed_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!negotiation_needed_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get negotiation needed handler (if any)
+    if (connection->negotiation_needed_handler) {
+        *negotiation_needed_handlerp = connection->negotiation_needed_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ICE local candidate handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set local candidate handler & done
+    connection->local_candidate_handler = local_candidate_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ICE local candidate handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_handler(
+    rawrtc_peer_connection_local_candidate_handler* const
+        local_candidate_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!local_candidate_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate handler (if any)
+    if (connection->local_candidate_handler) {
+        *local_candidate_handlerp = connection->local_candidate_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ICE local candidate error handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_error_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set local candidate error handler & done
+    connection->local_candidate_error_handler = local_candidate_error_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ICE local candidate error handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_error_handler(
+    rawrtc_peer_connection_local_candidate_error_handler* const
+        local_candidate_error_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!local_candidate_error_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate error handler (if any)
+    if (connection->local_candidate_error_handler) {
+        *local_candidate_error_handlerp = connection->local_candidate_error_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's signaling state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_signaling_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set signaling state change handler & done
+    connection->signaling_state_change_handler = signaling_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's signaling state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state_change_handler(
+    rawrtc_signaling_state_change_handler* const signaling_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!signaling_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get signaling state change handler (if any)
+    if (connection->signaling_state_change_handler) {
+        *signaling_state_change_handlerp = connection->signaling_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ice connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_transport_state_change_handler const ice_connection_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set ice connection state change handler & done
+    connection->ice_connection_state_change_handler = ice_connection_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ice connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state_change_handler(
+    rawrtc_ice_transport_state_change_handler* const
+        ice_connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!ice_connection_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get ice connection state change handler (if any)
+    if (connection->ice_connection_state_change_handler) {
+        *ice_connection_state_change_handlerp = connection->ice_connection_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ice gathering state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_gathering_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set ice gathering state change handler & done
+    connection->ice_gathering_state_change_handler = ice_gathering_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ice gathering state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state_change_handler(
+    rawrtc_ice_gatherer_state_change_handler* const
+        ice_gathering_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!ice_gathering_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get ice gathering state change handler (if any)
+    if (connection->ice_gathering_state_change_handler) {
+        *ice_gathering_state_change_handlerp = connection->ice_gathering_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's (peer) connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set (peer) connection state change handler & done
+    connection->connection_state_change_handler = connection_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's (peer) connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state_change_handler(
+    rawrtc_peer_connection_state_change_handler* const
+        connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get (peer) connection state change handler (if any)
+    if (connection->connection_state_change_handler) {
+        *connection_state_change_handlerp = connection->connection_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's data channel handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_data_channel_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_data_channel_handler const data_channel_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set data channel handler & done
+    connection->data_channel_handler = data_channel_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's data channel handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_data_channel_handler(
+    rawrtc_data_channel_handler* const data_channel_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!data_channel_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get data channel handler (if any)
+    if (connection->data_channel_handler) {
+        *data_channel_handlerp = connection->data_channel_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/peer_connection/connection.c b/src/peer_connection/connection.c
new file mode 100644
index 0000000..ceb1a43
--- /dev/null
+++ b/src/peer_connection/connection.c
@@ -0,0 +1,1465 @@
+#include "connection.h"
+#include "../certificate/certificate.h"
+#include "../dtls_transport/transport.h"
+#include "../ice_gather_options/options.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_server/server.h"
+#include "../peer_connection_configuration/configuration.h"
+#include "../peer_connection_description/description.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "peer-connection"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+#include <src/peer_connection_configuration/configuration.h>
+
+/*
+ * Change the signalling state.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_signaling_state(
+    struct rawrtc_peer_connection* const connection,  // not checked
+    enum rawrtc_signaling_state const state) {
+    // Set state
+    connection->signaling_state = state;
+
+    // Call handler (if any)
+    if (connection->signaling_state_change_handler) {
+        connection->signaling_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Change the connection state to a specific state.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_connection_state(
+    struct rawrtc_peer_connection* const connection,  // not checked
+    enum rawrtc_peer_connection_state const state) {
+    // Set state
+    connection->connection_state = state;
+
+    // Call handler (if any)
+    if (connection->connection_state_change_handler) {
+        connection->connection_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Update connection state.
+ * Will call the corresponding handler.
+ */
+static void update_connection_state(struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    enum rawrtc_ice_transport_state ice_transport_state = RAWRTC_ICE_TRANSPORT_STATE_NEW;
+    enum rawrtc_dtls_transport_state dtls_transport_state = RAWRTC_DTLS_TRANSPORT_STATE_NEW;
+    enum rawrtc_peer_connection_state connection_state;
+
+    // Nothing beats the closed state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return;
+    }
+
+    // Get ICE transport and DTLS transport states
+    if (connection->context.ice_transport) {
+        error =
+            rawrtc_ice_transport_get_state(&ice_transport_state, connection->context.ice_transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to get ICE transport state, reason: %s\n", rawrtc_error_to_code(error));
+        }
+    }
+    if (connection->context.dtls_transport) {
+        error = rawrtc_dtls_transport_get_state(
+            &dtls_transport_state, connection->context.dtls_transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to get DTLS transport state, reason: %s\n", rawrtc_error_to_code(error));
+        }
+    }
+
+    // Note: This follows the mindbogglingly confusing W3C spec description - it's just not
+    //       super-obvious. We start with states that are easy to detect and remove more and more
+    //       states from the equation.
+
+    // Failed: Any in the 'failed' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_FAILED ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_FAILED) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_FAILED;
+        goto out;
+    }
+
+    // Connecting: Any in the 'connecting' or 'checking' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_CHECKING ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_CONNECTING;
+        goto out;
+    }
+
+    // Disconnected: Any in the 'disconnected' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_DISCONNECTED) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_DISCONNECTED;
+        goto out;
+    }
+
+    // New: Any in 'new' or all in 'closed'
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_NEW ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_NEW ||
+        (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED &&
+         dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_CLOSED)) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_NEW;
+        goto out;
+    }
+
+    // Connected
+    connection_state = RAWRTC_PEER_CONNECTION_STATE_CONNECTED;
+
+out:
+    // Debug
+    DEBUG_PRINTF(
+        "ICE (%s) + DTLS (%s) = PC %s\n", rawrtc_ice_transport_state_to_name(ice_transport_state),
+        rawrtc_dtls_transport_state_to_name(dtls_transport_state),
+        rawrtc_peer_connection_state_to_name(connection_state));
+
+    // Check if the state would change
+    if (connection->connection_state == connection_state) {
+        return;
+    }
+
+    // Set state
+    connection->connection_state = connection_state;
+
+    // Call handler (if any)
+    if (connection->connection_state_change_handler) {
+        connection->connection_state_change_handler(connection_state, connection->arg);
+    }
+}
+
+/*
+ * Start the SCTP transport.
+ */
+static enum rawrtc_code sctp_transport_start(
+    struct rawrtc_sctp_transport* const sctp_transport,  // not checked
+    struct rawrtc_peer_connection* const connection,  // not checked
+    struct rawrtc_peer_connection_description* const description  // not checked
+) {
+    enum rawrtc_code error;
+
+    // Start SCTP transport
+    error = rawrtc_sctp_transport_start(
+        sctp_transport, description->sctp_capabilities, description->sctp_port);
+    if (error) {
+        return error;
+    }
+
+    // Set MTU (if necessary)
+    if (connection->configuration->sctp.mtu != 0) {
+        error = rawrtc_sctp_transport_set_mtu(sctp_transport, connection->configuration->sctp.mtu);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Enable path MTU discovery (if necessary)
+    if (connection->configuration->sctp.mtu_discovery) {
+        error = rawrtc_sctp_transport_enable_mtu_discovery(sctp_transport);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * All the nasty SDP stuff has been done. Fire it all up - YAY!
+ */
+static enum rawrtc_code peer_connection_start(
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context* const context = &connection->context;
+    struct rawrtc_peer_connection_description* description;
+    enum rawrtc_ice_role ice_role;
+    enum rawrtc_data_transport_type data_transport_type;
+    void* data_transport;
+    struct le* le;
+
+    // Check if it's too early to start
+    if (!connection->local_description || !connection->remote_description) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    DEBUG_INFO("Local and remote description set, starting transports\n");
+    description = connection->remote_description;
+
+    // Determine ICE role
+    // TODO: Is this correct?
+    switch (description->type) {
+        case RAWRTC_SDP_TYPE_OFFER:
+            ice_role = RAWRTC_ICE_ROLE_CONTROLLED;
+            break;
+        case RAWRTC_SDP_TYPE_ANSWER:
+            ice_role = RAWRTC_ICE_ROLE_CONTROLLING;
+            break;
+        default:
+            DEBUG_WARNING(
+                "Cannot determine ICE role from SDP type %s, report this!\n",
+                rawrtc_sdp_type_to_str(description->type));
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Start ICE transport
+    error = rawrtc_ice_transport_start(
+        context->ice_transport, context->ice_gatherer, description->ice_parameters, ice_role);
+    if (error) {
+        return error;
+    }
+
+    // Get data transport
+    error = rawrtc_data_transport_get_transport(
+        &data_transport_type, &data_transport, context->data_transport);
+    if (error) {
+        return error;
+    }
+
+    // Start data transport
+    switch (data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+            // Start DTLS transport
+            error =
+                rawrtc_dtls_transport_start(context->dtls_transport, description->dtls_parameters);
+            if (error) {
+                goto out;
+            }
+
+            // Start SCTP transport
+            error = sctp_transport_start(data_transport, connection, description);
+            if (error) {
+                goto out;
+            }
+            break;
+        }
+        default:
+            DEBUG_WARNING(
+                "Invalid data transport type: %s\n",
+                rawrtc_data_transport_type_to_str(data_transport_type));
+            error = RAWRTC_CODE_UNSUPPORTED_PROTOCOL;
+            goto out;
+    }
+
+    // Add remote ICE candidates
+    for (le = list_head(&description->ice_candidates); le != NULL; le = le->next) {
+        struct rawrtc_peer_connection_ice_candidate* const candidate = le->data;
+        error = rawrtc_peer_connection_add_ice_candidate(connection, candidate);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to add remote candidate, reason: %s\n", rawrtc_code_to_str(error));
+            // Note: Continuing here since other candidates may work
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    mem_deref(data_transport);
+    return error;
+}
+
+/*
+ * Remove all instances that have been created which are not
+ * associated to the peer connection.
+ */
+static void revert_context(
+    struct rawrtc_peer_connection_context* const new,  // not checked
+    struct rawrtc_peer_connection_context* const current  // not checked
+) {
+    if (new->data_transport != current->data_transport) {
+        mem_deref(new->data_transport);
+    }
+    if (new->dtls_transport != current->dtls_transport) {
+        mem_deref(new->dtls_transport);
+    }
+    // TODO: This check is brittle...
+    if (!list_isempty(&new->certificates) && list_isempty(&current->certificates)) {
+        list_flush(&new->certificates);
+    }
+    if (new->ice_transport != current->ice_transport) {
+        mem_deref(new->ice_transport);
+    }
+    if (new->ice_gatherer != current->ice_gatherer) {
+        mem_deref(new->ice_gatherer);
+    }
+    if (new->gather_options != current->gather_options) {
+        mem_deref(new->gather_options);
+    }
+}
+
+/*
+ * Apply all instances on a peer connection.
+ * Return if anything inside the context has changed.
+ */
+static bool apply_context(
+    struct rawrtc_peer_connection_context* const new,  // not checked
+    struct rawrtc_peer_connection_context* const current  // not checked
+) {
+    bool changed = false;
+    if (new->data_transport != current->data_transport) {
+        current->data_transport = new->data_transport;
+        changed = true;
+    }
+    if (new->dtls_transport != current->dtls_transport) {
+        current->dtls_transport = new->dtls_transport;
+        str_ncpy(current->dtls_id, new->dtls_id, RAWRTC_DTLS_ID_LENGTH + 1);
+        changed = true;
+    }
+    // TODO: This check is brittle...
+    if (!list_isempty(&new->certificates) && list_isempty(&current->certificates)) {
+        current->certificates = new->certificates;
+        changed = true;
+    }
+    if (new->ice_transport != current->ice_transport) {
+        current->ice_transport = new->ice_transport;
+        changed = true;
+    }
+    if (new->ice_gatherer != current->ice_gatherer) {
+        current->ice_gatherer = new->ice_gatherer;
+        changed = true;
+    }
+    if (new->gather_options != current->gather_options) {
+        current->gather_options = new->gather_options;
+        changed = true;
+    }
+    return changed;
+}
+
+/*
+ * Wrap an ORTC ICE candidate to a peer connection ICE candidate.
+ */
+static enum rawrtc_code local_ortc_candidate_to_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced, not checked
+    struct rawrtc_ice_candidate* const ortc_candidate,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    char* username_fragment;
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Copy username fragment (is going to be referenced later)
+    error =
+        rawrtc_strdup(&username_fragment, connection->context.ice_gatherer->ice_username_fragment);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to copy username fragment from ICE gatherer, reason: %s\n",
+            rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Create candidate
+    // Note: The local description will exist at this point since we start gathering when the
+    //       local description is being set.
+    error = rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+        &candidate, ortc_candidate, connection->local_description->mid,
+        &connection->local_description->media_line_index, username_fragment);
+    if (error) {
+        goto out;
+    }
+
+    // Set pointer & done
+    *candidatep = candidate;
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(username_fragment);
+    return error;
+}
+
+/*
+ * Add candidate to description and announce candidate.
+ */
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char const* const url,  // nullable
+    void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_ice_candidate* candidate = NULL;
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_FAILED ||
+        connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        DEBUG_NOTICE(
+            "Ignoring candidate in the %s state\n",
+            rawrtc_peer_connection_state_to_name(connection->connection_state));
+        return;
+    }
+
+    // Wrap candidate (if any ORTC candidate)
+    if (ortc_candidate) {
+        error = local_ortc_candidate_to_candidate(&candidate, ortc_candidate, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create local candidate from ORTC candidate, reason: %s\n",
+                rawrtc_code_to_str(error));
+            return;
+        }
+    }
+
+    // Add candidate (or end-of-candidate) to description
+    error =
+        rawrtc_peer_connection_description_add_candidate(connection->local_description, candidate);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to add local candidate to local description, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Call handler (if any)
+    if (connection->local_candidate_handler) {
+        connection->local_candidate_handler(candidate, url, connection->arg);
+    }
+
+out:
+    // Un-reference
+    mem_deref(candidate);
+}
+
+/*
+ * Announce ICE gatherer error as ICE candidate error.
+ */
+static void ice_gatherer_error_handler(
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char const* const url,
+    uint16_t const error_code,
+    char const* const error_text,
+    void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_ice_candidate* candidate = NULL;
+
+    // Wrap candidate (if any ORTC candidate)
+    if (ortc_candidate) {
+        error = local_ortc_candidate_to_candidate(&candidate, ortc_candidate, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create local candidate from ORTC candidate, reason: %s\n",
+                rawrtc_code_to_str(error));
+            return;
+        }
+    }
+
+    // Call handler (if any)
+    if (connection->local_candidate_error_handler) {
+        connection->local_candidate_error_handler(
+            candidate, url, error_code, error_text, connection->arg);
+    }
+}
+
+/*
+ * Filter ICE gatherer state and announce it.
+ */
+static void ice_gatherer_state_change_handler(
+    enum rawrtc_ice_gatherer_state const state, void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // The only difference to the ORTC gatherer states is that there's no 'closed' state.
+    if (state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Call handler (if any)
+    if (connection->ice_gathering_state_change_handler) {
+        connection->ice_gathering_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Lazy-create an ICE gatherer.
+ */
+static enum rawrtc_code get_ice_gatherer(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_gather_options* options;
+    struct rawrtc_ice_gatherer* gatherer = NULL;
+    struct le* le;
+
+    // Already created?
+    if (context->ice_gatherer) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create ICE gather options
+    error = rawrtc_ice_gather_options_create(&options, connection->configuration->gather_policy);
+    if (error) {
+        return error;
+    }
+
+    // Add ICE servers to gather options
+    for (le = list_head(&connection->configuration->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const source_server = le->data;
+        struct rawrtc_ice_server* server;
+
+        // Copy ICE server
+        error = rawrtc_ice_server_copy(&server, source_server);
+        if (error) {
+            goto out;
+        }
+
+        // Add ICE server to gather options
+        error = rawrtc_ice_gather_options_add_server_internal(options, server);
+        if (error) {
+            mem_deref(server);
+            goto out;
+        }
+    }
+
+    // Create ICE gatherer
+    error = rawrtc_ice_gatherer_create(
+        &gatherer, options, ice_gatherer_state_change_handler, ice_gatherer_error_handler,
+        ice_gatherer_local_candidate_handler, connection);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(gatherer);
+        mem_deref(options);
+    } else {
+        // Set pointers & done
+        context->gather_options = options;
+        context->ice_gatherer = gatherer;
+    }
+
+    return error;
+}
+
+static void ice_transport_candidate_pair_change_handler(
+    struct rawrtc_ice_candidate* const local,  // read-only
+    struct rawrtc_ice_candidate* const remote,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    (void) local;
+    (void) remote;
+    (void) arg;
+
+    // There's no handler that could potentially print this, so we print it here for debug purposes
+    DEBUG_PRINTF("ICE transport candidate pair change\n");
+}
+
+static void ice_transport_state_change_handler(
+    enum rawrtc_ice_transport_state const state, void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // Call handler (if any)
+    if (connection->ice_connection_state_change_handler) {
+        connection->ice_connection_state_change_handler(state, connection->arg);
+    }
+
+    // Update connection state
+    update_connection_state(connection);
+}
+
+/*
+ * Lazy-create an ICE transport.
+ */
+static enum rawrtc_code get_ice_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+
+    // Already created?
+    if (context->ice_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Get ICE gatherer
+    error = get_ice_gatherer(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Create ICE transport
+    return rawrtc_ice_transport_create(
+        &context->ice_transport, context->ice_gatherer, ice_transport_state_change_handler,
+        ice_transport_candidate_pair_change_handler, connection);
+}
+
+/*
+ * Lazy-generate a certificate list.
+ */
+static enum rawrtc_code get_certificates(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection_configuration* const configuration  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* certificate;
+
+    // Already created?
+    if (!list_isempty(&context->certificates)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Certificates in the configuration? Copy them.
+    if (!list_isempty(&configuration->certificates)) {
+        return rawrtc_certificate_list_copy(&context->certificates, &configuration->certificates);
+    }
+
+    // Generate a certificate
+    error = rawrtc_certificate_generate(&certificate, NULL);
+    if (error) {
+        return error;
+    }
+
+    // Add certificate to the list
+    list_append(&context->certificates, &certificate->le, certificate);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+static void dtls_transport_error_handler(
+    // TODO: error.message (probably from OpenSSL)
+    void* const arg) {
+    (void) arg;
+    // TODO: Print error message
+    DEBUG_WARNING("DTLS transport error: %s\n", "???");
+}
+
+static void dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state, void* const arg) {
+    struct rawrtc_peer_connection* connection = arg;
+    (void) state;
+
+    // Update connection state
+    update_connection_state(connection);
+}
+
+/*
+ * Lazy-create a DTLS transport.
+ */
+static enum rawrtc_code get_dtls_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct list certificates = LIST_INIT;
+
+    // Already created?
+    if (context->dtls_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Get ICE transport
+    error = get_ice_transport(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Get certificates
+    error = get_certificates(context, connection->configuration);
+    if (error) {
+        return error;
+    }
+
+    // Copy certificates list
+    error = rawrtc_certificate_list_copy(&certificates, &context->certificates);
+    if (error) {
+        return error;
+    }
+
+    // Generate random DTLS ID
+    rand_str(context->dtls_id, sizeof(context->dtls_id));
+
+    // Create DTLS transport
+    return rawrtc_dtls_transport_create_internal(
+        &context->dtls_transport, context->ice_transport, &certificates,
+        dtls_transport_state_change_handler, dtls_transport_error_handler, connection);
+}
+
+static void sctp_transport_state_change_handler(
+    enum rawrtc_sctp_transport_state const state, void* const arg) {
+    (void) arg;
+    (void) state;
+
+    // There's no handler that could potentially print this, so we print it here for debug purposes
+    DEBUG_PRINTF("SCTP transport state change: %s\n", rawrtc_sctp_transport_state_to_name(state));
+}
+
+/*
+ * Lazy-create an SCTP transport.
+ */
+static enum rawrtc_code get_sctp_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_sctp_transport* sctp_transport;
+
+    // Get DTLS transport
+    error = get_dtls_transport(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Create SCTP transport
+    error = rawrtc_sctp_transport_create(
+        &sctp_transport, context->dtls_transport, RAWRTC_PEER_CONNECTION_SCTP_TRANSPORT_PORT,
+        connection->data_channel_handler, sctp_transport_state_change_handler, connection->arg);
+    if (error) {
+        return error;
+    }
+
+    // Set send/receive buffer length (if necessary)
+    if (connection->configuration->sctp.send_buffer_length != 0 &&
+        connection->configuration->sctp.receive_buffer_length != 0) {
+        error = rawrtc_sctp_transport_set_buffer_length(
+            sctp_transport, connection->configuration->sctp.send_buffer_length,
+            connection->configuration->sctp.receive_buffer_length);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Set congestion control algorithm (if necessary)
+    if (connection->configuration->sctp.congestion_ctrl_algorithm !=
+        RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581) {
+        error = rawrtc_sctp_transport_set_congestion_ctrl_algorithm(
+            sctp_transport, connection->configuration->sctp.congestion_ctrl_algorithm);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Get data transport
+    error = rawrtc_sctp_transport_get_data_transport(&context->data_transport, sctp_transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    // Note: As the data transport has a reference to the SCTP transport, we can
+    //       still retrieve the reference later.
+    mem_deref(sctp_transport);
+    return error;
+}
+
+/*
+ * Lazy-create the requested data transport.
+ */
+static enum rawrtc_code get_data_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    // Already created?
+    if (context->data_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create data transport depending on what we want to have
+    switch (connection->data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+            return get_sctp_transport(context, connection);
+        }
+        default:
+            return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+}
+
+/*
+ * Destructor for an existing peer connection.
+ */
+static void rawrtc_peer_connection_destroy(void* arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // Unset all handlers
+    rawrtc_peer_connection_unset_handlers(connection);
+
+    // Close peer connection
+    rawrtc_peer_connection_close(connection);
+
+    // Un-reference
+    mem_deref(connection->context.data_transport);
+    mem_deref(connection->context.dtls_transport);
+    list_flush(&connection->context.certificates);
+    mem_deref(connection->context.ice_transport);
+    mem_deref(connection->context.ice_gatherer);
+    mem_deref(connection->context.gather_options);
+    mem_deref(connection->remote_description);
+    mem_deref(connection->local_description);
+    mem_deref(connection->configuration);
+}
+
+/*
+ * Create a new peer connection.
+ * `*connectionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create(
+    struct rawrtc_peer_connection** const connectionp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* configuration,  // referenced
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler,  // nullable
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler,  // nullable
+    rawrtc_ice_transport_state_change_handler const
+        ice_connection_state_change_handler,  // nullable
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler,  // nullable
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler,  // nullable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_peer_connection* connection;
+
+    // Check arguments
+    if (!connectionp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    connection = mem_zalloc(sizeof(*connection), rawrtc_peer_connection_destroy);
+    if (!connection) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    connection->connection_state = RAWRTC_PEER_CONNECTION_STATE_NEW;
+    connection->signaling_state = RAWRTC_SIGNALING_STATE_STABLE;
+    connection->configuration = mem_ref(configuration);
+    connection->negotiation_needed_handler = negotiation_needed_handler;
+    connection->local_candidate_handler = local_candidate_handler;
+    connection->local_candidate_error_handler = local_candidate_error_handler;
+    connection->signaling_state_change_handler = signaling_state_change_handler;
+    connection->ice_connection_state_change_handler = ice_connection_state_change_handler;
+    connection->ice_gathering_state_change_handler = ice_gathering_state_change_handler;
+    connection->connection_state_change_handler = connection_state_change_handler;
+    connection->data_channel_handler = data_channel_handler;
+    connection->data_transport_type = RAWRTC_DATA_TRANSPORT_TYPE_SCTP;
+    connection->arg = arg;
+
+    // Set pointer & done
+    *connectionp = connection;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Close the peer connection. This will stop all underlying transports
+ * and results in a final 'closed' state.
+ */
+enum rawrtc_code rawrtc_peer_connection_close(struct rawrtc_peer_connection* const connection) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update signalling & connection state
+    // Note: We need to do this early or the 'closed' states when tearing down the transports may
+    //       lead to surprising peer connection states such as 'connected' at the very end.
+    set_signaling_state(connection, RAWRTC_SIGNALING_STATE_CLOSED);
+    set_connection_state(connection, RAWRTC_PEER_CONNECTION_STATE_CLOSED);
+
+    // Stop data transport (if any)
+    if (connection->context.data_transport) {
+        enum rawrtc_data_transport_type data_transport_type;
+        void* data_transport;
+
+        // Get data transport
+        error = rawrtc_data_transport_get_transport(
+            &data_transport_type, &data_transport, connection->context.data_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to get data transport, reason: %s\n", rawrtc_code_to_str(error));
+        } else {
+            // Stop transport
+            switch (data_transport_type) {
+                case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+                    struct rawrtc_sctp_transport* const sctp_transport = data_transport;
+                    error = rawrtc_sctp_transport_stop(sctp_transport);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to stop SCTP transport, reason: %s\n",
+                            rawrtc_code_to_str(error));
+                    }
+                    break;
+                }
+                default:
+                    DEBUG_WARNING(
+                        "Invalid data transport type: %s\n",
+                        rawrtc_data_transport_type_to_str(data_transport_type));
+                    break;
+            }
+
+            // Un-reference
+            mem_deref(data_transport);
+        }
+    }
+
+    // Stop DTLS transport (if any)
+    if (connection->context.dtls_transport) {
+        error = rawrtc_dtls_transport_stop(connection->context.dtls_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to stop DTLS transport, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Stop ICE transport (if any)
+    if (connection->context.ice_transport) {
+        error = rawrtc_ice_transport_stop(connection->context.ice_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to stop ICE transport, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Close ICE gatherer (if any)
+    if (connection->context.ice_gatherer) {
+        error = rawrtc_ice_gatherer_close(connection->context.ice_gatherer);
+        if (error) {
+            DEBUG_WARNING("Unable to close ICE gatherer, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create an offer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_offer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    bool const ice_restart) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Support ICE restart
+    if (ice_restart) {
+        DEBUG_WARNING("ICE restart currently not supported\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow subsequent offers
+    if (connection->local_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Create description
+    return rawrtc_peer_connection_description_create_internal(descriptionp, connection, true);
+}
+
+/*
+ * Create an answer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_answer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow subsequent answers
+    if (connection->local_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Create description
+    return rawrtc_peer_connection_description_create_internal(descriptionp, connection, false);
+}
+
+/*
+ * Set and apply the local description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+) {
+    bool initial_description = true;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!connection || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Ensure it has been created by the local peer connection.
+    if (description->connection != connection) {
+        // Yeah, sorry, nope, I'm not parsing all this SDP nonsense again just to check
+        // what kind of nasty things could have been done in the meantime.
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Allow changing the local description
+    if (connection->local_description) {
+        initial_description = false;
+        (void) initial_description;
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (description->type != RAWRTC_SDP_TYPE_OFFER && description->type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check SDP type
+    DEBUG_PRINTF(
+        "Set local description: %s (local), %s (remote)\n",
+        rawrtc_sdp_type_to_str(description->type),
+        connection->remote_description
+            ? rawrtc_sdp_type_to_str(connection->remote_description->type)
+            : "n/a");
+    if (connection->remote_description) {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have a remote description and get an offer. This requires renegotiation we
+                // currently don't support.
+                // TODO: Add support for this
+                DEBUG_WARNING("There's no support for renegotiation at the moment.\n");
+                return RAWRTC_CODE_NOT_IMPLEMENTED;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have a remote description and get an answer. Sanity-check that the remote
+                // description is an offer.
+                if (connection->remote_description->type != RAWRTC_SDP_TYPE_OFFER) {
+                    DEBUG_WARNING(
+                        "Got 'answer' but remote description is '%s'\n",
+                        rawrtc_sdp_type_to_str(connection->remote_description->type));
+                    return RAWRTC_CODE_INVALID_STATE;
+                }
+                break;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    } else {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have no remote description and get an offer. Fine.
+                break;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have no remote description and get an answer. Not going to work.
+                DEBUG_WARNING("Got 'answer' but have no remote description\n");
+                return RAWRTC_CODE_INVALID_STATE;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+
+    // Remove reference to self
+    description->connection = mem_deref(description->connection);
+
+    // Set local description
+    connection->local_description = mem_ref(description);
+
+    // Start gathering (if initial description)
+    if (initial_description) {
+        error = rawrtc_ice_gatherer_gather(connection->context.ice_gatherer, NULL);
+        if (error) {
+            DEBUG_WARNING("Unable to start gathering, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+    }
+
+    // Start peer connection if both description are set
+    error = peer_connection_start(connection);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        DEBUG_WARNING("Unable to start peer connection, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Update signalling state
+    switch (connection->signaling_state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            // Can only be an offer or it would not have been accepted
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            // Update of the local offer, nothing to do
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            // Can only be an answer or it would not have been accepted
+            // Note: This may change once we accept PR answers
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_STABLE);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            // Impossible state
+            break;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set and apply the remote description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_remote_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context context;
+
+    // Check arguments
+    if (!connection || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow changing the remote description
+    if (connection->remote_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (description->type != RAWRTC_SDP_TYPE_OFFER && description->type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check SDP type
+    DEBUG_PRINTF(
+        "Set remote description: %s (local), %s (remote)\n",
+        connection->local_description ? rawrtc_sdp_type_to_str(connection->local_description->type)
+                                      : "n/a",
+        rawrtc_sdp_type_to_str(description->type));
+    if (connection->local_description) {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have a local description and get an offer. This requires renegotiation we
+                // currently don't support.
+                // TODO: Add support for this
+                DEBUG_WARNING("There's no support for renegotiation at the moment.\n");
+                return RAWRTC_CODE_NOT_IMPLEMENTED;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have a local description and get an answer. Sanity-check that the local
+                // description is an offer.
+                if (connection->local_description->type != RAWRTC_SDP_TYPE_OFFER) {
+                    DEBUG_WARNING(
+                        "Got 'answer' but local description is '%s'\n",
+                        rawrtc_sdp_type_to_str(connection->local_description->type));
+                    return RAWRTC_CODE_INVALID_STATE;
+                }
+                break;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    } else {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have no local description and get an offer. Fine.
+                break;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have no local description and get an answer. Not going to work.
+                DEBUG_WARNING("Got 'answer' but have no local description\n");
+                return RAWRTC_CODE_INVALID_STATE;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+
+    // No trickle ICE? Ensure we have all candidates
+    if (!description->trickle_ice && !description->end_of_candidates) {
+        DEBUG_NOTICE("No trickle ICE indicated but don't have all candidates\n");
+        // Note: We continue since we still accept further candidates.
+    }
+
+    // No remote media 'application' line?
+    if (!description->remote_media_line) {
+        DEBUG_WARNING("No remote media 'application' line for data channels found\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No ICE parameters?
+    // Note: We either have valid ICE parameters or none at this point
+    if (!description->ice_parameters) {
+        DEBUG_WARNING("Required ICE parameters not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No DTLS parameters?
+    // Note: We either have valid DTLS parameters or none at this point
+    if (!description->dtls_parameters) {
+        DEBUG_WARNING("Required DTLS parameters not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No SCTP capabilities or port?
+    // Note: We either have valid SCTP capabilities or none at this point
+    if (!description->sctp_capabilities) {
+        DEBUG_WARNING("Required SCTP capabilities not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+    if (description->sctp_port == 0) {
+        DEBUG_WARNING("Invalid SCTP port (0)\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set remote description
+    connection->remote_description = mem_ref(description);
+
+    // Initialise context
+    context = connection->context;
+
+    // Create a data transport if we're answering
+    if (description->type == RAWRTC_SDP_TYPE_OFFER) {
+        // Get data transport
+        error = get_data_transport(&context, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create data transport, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+
+        // Apply context
+        apply_context(&context, &connection->context);
+    }
+
+    // Start peer connection if both descriptions are set
+    error = peer_connection_start(connection);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        DEBUG_WARNING("Unable to start peer connection, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Update signalling state
+    switch (connection->signaling_state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            // Can only be an offer or it would not have been accepted
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            // Can only be an answer or it would not have been accepted
+            // Note: This may change once we accept PR answers
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_STABLE);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            // Update of the remote offer, nothing to do
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            // Impossible state
+            break;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE candidate to the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_add_ice_candidate(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_description* description;
+
+    // Check arguments
+    if (!connection || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Ensure there's a remote description
+    description = connection->remote_description;
+    if (!description) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Note: We can be sure that either 'mid' or the media line index is present at this point.
+
+    // Check if the 'mid' matches (if any)
+    // TODO: Once we support further media lines, we need to look up the appropriate transport here
+    if (candidate->mid && description->mid && str_cmp(candidate->mid, description->mid) != 0) {
+        DEBUG_WARNING("No matching 'mid' in remote description\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if the media line index matches (if any)
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX &&
+        ((uint8_t) candidate->media_line_index) != description->media_line_index) {
+        DEBUG_WARNING("No matching media line index in remote description\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if the username fragment matches (if any)
+    // TODO: This would need to be done across ICE generations
+    if (candidate->username_fragment) {
+        char* username_fragment;
+        bool matching;
+
+        // Get username fragment from the remote ICE parameters
+        error = rawrtc_ice_parameters_get_username_fragment(
+            &username_fragment, description->ice_parameters);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to retrieve username fragment, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+
+        // Compare username fragments
+        matching = str_cmp(candidate->username_fragment, username_fragment) == 0;
+        mem_deref(username_fragment);
+        if (!matching) {
+            DEBUG_WARNING("Username fragments don't match\n");
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+    }
+
+    // Add ICE candidate
+    return rawrtc_ice_transport_add_remote_candidate(
+        connection->context.ice_transport, candidate->candidate);
+}
+
+/*
+ * Create a data channel on a peer connection.
+ * `*channelp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_data_channel(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_data_channel_parameters* const parameters,  // referenced
+    rawrtc_data_channel_open_handler const open_handler,  // nullable
+    rawrtc_data_channel_buffered_amount_low_handler const buffered_amount_low_handler,  // nullable
+    rawrtc_data_channel_error_handler const error_handler,  // nullable
+    rawrtc_data_channel_close_handler const close_handler,  // nullable
+    rawrtc_data_channel_message_handler const message_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context context;
+    struct rawrtc_data_channel* channel = NULL;
+
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Initialise context
+    context = connection->context;
+
+    // Get data transport (if no description has been set, yet)
+    if (!connection->local_description && !connection->remote_description) {
+        error = get_data_transport(&context, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create data transport, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+    }
+
+    // Create data channel
+    // TODO: Fix data channel cannot be created before transports have been started
+    error = rawrtc_data_channel_create(
+        &channel, context.data_transport, parameters, open_handler, buffered_amount_low_handler,
+        error_handler, close_handler, message_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        // Un-reference
+        mem_deref(channel);
+
+        // Remove all newly created instances
+        revert_context(&context, &connection->context);
+    } else {
+        // Apply context
+        bool const negotiation_needed = apply_context(&context, &connection->context);
+
+        // Set pointer
+        *channelp = channel;
+
+        // Negotiation needed?
+        if (negotiation_needed) {
+            connection->negotiation_needed_handler(connection->arg);
+        }
+    }
+    return error;
+}
diff --git a/src/peer_connection/connection.h b/src/peer_connection/connection.h
new file mode 100644
index 0000000..ef7cff6
--- /dev/null
+++ b/src/peer_connection/connection.h
@@ -0,0 +1,49 @@
+#pragma once
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_configuration.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_transport.h>
+#include <re.h>
+
+enum {
+    RAWRTC_PEER_CONNECTION_SCTP_TRANSPORT_PORT = 5000,
+    RAWRTC_DTLS_ID_LENGTH = 32,
+};
+
+/*
+ * Peer connection context.
+ */
+struct rawrtc_peer_connection_context {
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_gatherer* ice_gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct list certificates;
+    char dtls_id[RAWRTC_DTLS_ID_LENGTH + 1];
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_data_transport* data_transport;
+};
+
+struct rawrtc_peer_connection {
+    enum rawrtc_peer_connection_state connection_state;
+    enum rawrtc_signaling_state signaling_state;
+    struct rawrtc_peer_connection_configuration* configuration;  // referenced
+    rawrtc_negotiation_needed_handler negotiation_needed_handler;  // nullable
+    rawrtc_peer_connection_local_candidate_handler local_candidate_handler;  // nullable
+    rawrtc_peer_connection_local_candidate_error_handler local_candidate_error_handler;  // nullable
+    rawrtc_signaling_state_change_handler signaling_state_change_handler;  // nullable
+    rawrtc_ice_transport_state_change_handler ice_connection_state_change_handler;  // nullable
+    rawrtc_ice_gatherer_state_change_handler ice_gathering_state_change_handler;  // nullable
+    rawrtc_peer_connection_state_change_handler connection_state_change_handler;  // nullable
+    rawrtc_data_channel_handler data_channel_handler;  // nullable
+    enum rawrtc_data_transport_type data_transport_type;
+    struct rawrtc_peer_connection_description* local_description;  // referenced
+    struct rawrtc_peer_connection_description* remote_description;  // referenced
+    struct rawrtc_peer_connection_context context;
+    void* arg;  // nullable
+};
diff --git a/src/peer_connection/meson.build b/src/peer_connection/meson.build
new file mode 100644
index 0000000..16bd198
--- /dev/null
+++ b/src/peer_connection/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'connection.c',
+])
diff --git a/src/peer_connection_configuration/configuration.c b/src/peer_connection_configuration/configuration.c
new file mode 100644
index 0000000..c1f2b71
--- /dev/null
+++ b/src/peer_connection_configuration/configuration.c
@@ -0,0 +1,260 @@
+#include "configuration.h"
+#include "../certificate/certificate.h"
+#include "../ice_server/server.h"
+#include "../utils/utils.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/peer_connection_configuration.h>
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+#include <limits.h>  // INT_MAX
+
+/*
+ * Destructor for an existing peer connection configuration.
+ */
+static void rawrtc_peer_connection_configuration_destroy(void* arg) {
+    struct rawrtc_peer_connection_configuration* const configuration = arg;
+
+    // Un-reference
+    list_flush(&configuration->certificates);
+    list_flush(&configuration->ice_servers);
+}
+
+/*
+ * Create a new peer connection configuration.
+ * `*configurationp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_create(
+    struct rawrtc_peer_connection_configuration** const configurationp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy) {
+    struct rawrtc_peer_connection_configuration* configuration;
+
+    // Check arguments
+    if (!configurationp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    configuration =
+        mem_zalloc(sizeof(*configuration), rawrtc_peer_connection_configuration_destroy);
+    if (!configuration) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    configuration->gather_policy = gather_policy;
+    list_init(&configuration->ice_servers);
+    list_init(&configuration->certificates);
+    configuration->sctp_sdp_05 = true;
+    configuration->sctp.send_buffer_length = 0;
+    configuration->sctp.receive_buffer_length = 0;
+    configuration->sctp.congestion_ctrl_algorithm = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581;
+    configuration->sctp.mtu = 0;
+    configuration->sctp.mtu_discovery = false;
+
+    // Set pointer and return
+    *configurationp = configuration;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server instance to the peer connection configuration.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server_internal(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    struct rawrtc_ice_server* const server) {
+    // Check arguments
+    if (!configuration || !server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Add to configuration
+    list_append(&configuration->ice_servers, &server->le, server);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server to the peer connection configuration.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure there are less than 2^8 servers
+    // TODO: This check should be in some common location
+    if (list_count(&configuration->ice_servers) == UINT8_MAX) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Create ICE server
+    error = rawrtc_ice_server_create(&server, urls, n_urls, username, credential, credential_type);
+    if (error) {
+        return error;
+    }
+
+    // Add to configuration
+    return rawrtc_peer_connection_configuration_add_ice_server_internal(configuration, server);
+}
+
+/*
+ * Get ICE servers from the peer connection configuration.
+ * `*serversp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_ice_servers(
+    struct rawrtc_ice_servers** const serversp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration) {
+    // Check arguments
+    if (!serversp || !configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Hand out list as array
+    // Note: ICE servers handed out cannot be added to other lists
+    //       without copying since the items are only referenced.
+    return rawrtc_list_to_array(
+        (struct rawrtc_array_container**) serversp, &configuration->ice_servers, true);
+}
+
+/*
+ * Add a certificate to the peer connection configuration to be used
+ * instead of an ephemerally generated one.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_certificate(
+    struct rawrtc_peer_connection_configuration* configuration,
+    struct rawrtc_certificate* const certificate  // copied
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* certificate_copy;
+
+    // Check arguments
+    if (!configuration || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy certificate
+    // Note: Copying is needed as the 'le' element cannot be associated to multiple lists
+    error = rawrtc_certificate_copy(&certificate_copy, certificate);
+    if (error) {
+        return error;
+    }
+
+    // Append to list
+    list_append(&configuration->certificates, &certificate_copy->le, certificate_copy);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get certificates from the peer connection configuration.
+ * `*certificatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_certificates(
+    struct rawrtc_certificates** const certificatesp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration) {
+    // Check arguments
+    if (!certificatesp || !configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Hand out list as array
+    // Note: Certificates handed out cannot be added to other lists
+    //       without copying since the items are only referenced.
+    return rawrtc_list_to_array(
+        (struct rawrtc_array_container**) certificatesp, &configuration->certificates, true);
+}
+
+/*
+ * Set whether to use legacy SDP for data channel parameter encoding.
+ * Note: Legacy SDP for data channels is on by default due to parsing problems in Chrome.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_sdp_05(
+    struct rawrtc_peer_connection_configuration* configuration, bool const on) {
+    // Check parameters
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp_sdp_05 = on;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's send and receive buffer length in bytes.
+ * If both values are zero, the default buffer length will be used. Otherwise,
+ * zero is invalid.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_buffer_length(
+    struct rawrtc_peer_connection_configuration* configuration,
+    uint32_t const send_buffer_length,
+    uint32_t const receive_buffer_length) {
+    // Check arguments
+    if (!configuration || send_buffer_length > INT_MAX || receive_buffer_length > INT_MAX ||
+        (send_buffer_length == 0 && receive_buffer_length != 0) ||
+        (send_buffer_length != 0 && receive_buffer_length == 0)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set length for send/receive buffer
+    configuration->sctp.send_buffer_length = send_buffer_length;
+    configuration->sctp.receive_buffer_length = receive_buffer_length;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_congestion_ctrl_algorithm(
+    struct rawrtc_peer_connection_configuration* configuration,
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.congestion_ctrl_algorithm = algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's maximum transmission unit (MTU).
+ * A value of zero indicates that the default MTU should be used.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu(
+    struct rawrtc_peer_connection_configuration* configuration, uint32_t const mtu) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.mtu = mtu;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Enable or disable MTU discovery on the SCTP transport.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu_discovery(
+    struct rawrtc_peer_connection_configuration* configuration, bool const on) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.mtu_discovery = on;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/peer_connection_configuration/configuration.h b/src/peer_connection_configuration/configuration.h
new file mode 100644
index 0000000..34fd7c1
--- /dev/null
+++ b/src/peer_connection_configuration/configuration.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+struct rawrtc_peer_connection_configuration {
+    enum rawrtc_ice_gather_policy gather_policy;
+    struct list ice_servers;
+    struct list certificates;
+    bool sctp_sdp_05;
+    struct {
+        uint32_t send_buffer_length;
+        uint32_t receive_buffer_length;
+        enum rawrtc_sctp_transport_congestion_ctrl congestion_ctrl_algorithm;
+        uint32_t mtu;
+        bool mtu_discovery;
+    } sctp;
+};
+
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server_internal(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    struct rawrtc_ice_server* const server);
diff --git a/src/peer_connection_configuration/meson.build b/src/peer_connection_configuration/meson.build
new file mode 100644
index 0000000..3d01bb6
--- /dev/null
+++ b/src/peer_connection_configuration/meson.build
@@ -0,0 +1 @@
+sources += files('configuration.c')
diff --git a/src/peer_connection_description/attributes.c b/src/peer_connection_description/attributes.c
new file mode 100644
index 0000000..60f9e0a
--- /dev/null
+++ b/src/peer_connection_description/attributes.c
@@ -0,0 +1,39 @@
+#include "description.h"
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Get the SDP type of the description.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    struct rawrtc_peer_connection_description* const description) {
+    // Check arguments
+    if (!typep || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set SDP type
+    *typep = description->type;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SDP of the description.
+ * `*sdpp` will be set to a copy of the SDP that must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_description* const description) {
+    // Check arguments
+    if (!sdpp || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy SDP
+    return rawrtc_sdprintf(sdpp, "%b", description->sdp->buf, description->sdp->end);
+}
diff --git a/src/peer_connection_description/description.c b/src/peer_connection_description/description.c
new file mode 100644
index 0000000..b4da9eb
--- /dev/null
+++ b/src/peer_connection_description/description.c
@@ -0,0 +1,1091 @@
+#include "description.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include "../dtls_parameters/parameters.h"
+#include "../peer_connection/connection.h"
+#include "../peer_connection_configuration/configuration.h"
+#include "../peer_connection_description/description.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "peer-connection-description"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+// Constants
+static uint16_t const discard_port = 9;
+static char const sdp_application_dtls_sctp_regex[] = "application [0-9]+ [^ ]+";
+static char const* const sdp_application_dtls_sctp_variants[] = {
+    "DTLS/SCTP",
+    "UDP/DTLS/SCTP",
+    "TCP/DTLS/SCTP",
+};
+static size_t const sdp_application_dtls_sctp_variants_length =
+    ARRAY_SIZE(sdp_application_dtls_sctp_variants);
+static char const sdp_group_regex[] = "group:BUNDLE [^]+";
+static char const sdp_mid_regex[] = "mid:[^]+";
+static char const sdp_ice_options_trickle[] = "ice-options:trickle";
+static char const sdp_ice_username_fragment_regex[] = "ice-ufrag:[^]+";
+static char const sdp_ice_password_regex[] = "ice-pwd:[^]+";
+static char const sdp_ice_lite[] = "ice-lite";
+static char const sdp_dtls_role_regex[] = "setup:[^]+";
+static enum rawrtc_dtls_role const map_enum_dtls_role[] = {
+    RAWRTC_DTLS_ROLE_AUTO,
+    RAWRTC_DTLS_ROLE_CLIENT,
+    RAWRTC_DTLS_ROLE_SERVER,
+};
+static char const* const map_str_dtls_role[] = {
+    "actpass",
+    "active",
+    "passive",
+};
+static size_t const map_dtls_role_length = ARRAY_SIZE(map_enum_dtls_role);
+static char const sdp_dtls_fingerprint_regex[] = "fingerprint:[^ ]+ [^]+";
+static char const sdp_sctp_port_sctmap_regex[] = "sctpmap:[0-9]+[^]*";
+static char const sdp_sctp_port_regex[] = "sctp-port:[0-9]+";
+static char const sdp_sctp_maximum_message_size_regex[] = "max-message-size:[0-9]+";
+static char const sdp_ice_end_of_candidates[] = "end-of-candidates";
+static char const sdp_ice_candidate_head[] = "candidate:";
+static size_t const sdp_ice_candidate_head_length = ARRAY_SIZE(sdp_ice_candidate_head);
+
+// Candidate line
+struct candidate_line {
+    struct le le;
+    struct pl line;
+};
+
+/*
+ * Set session boilerplate
+ */
+static enum rawrtc_code set_session_boilerplate(
+    struct mbuf* const sdp,  // not checked
+    char const* const version,  // not checked
+    uint32_t const id) {
+    int err;
+
+    // Write session boilerplate
+    err = mbuf_write_str(sdp, "v=0\r\n");
+    err |=
+        mbuf_printf(sdp, "o=sdpartanic-rawrtc-%s %" PRIu32 " 1 IN IP4 127.0.0.1\r\n", version, id);
+    err |= mbuf_write_str(sdp, "s=-\r\n");
+    err |= mbuf_write_str(sdp, "t=0 0\r\n");
+
+    // Done
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Set session attributes on SDP.
+ */
+static enum rawrtc_code set_session_attributes(
+    struct mbuf* const sdp,  // not checked
+    bool const trickle_ice,
+    char const* const bundled_mids) {
+    int err = 0;
+
+    // Trickle ICE
+    if (trickle_ice) {
+        err = mbuf_write_str(sdp, "a=ice-options:trickle\r\n");
+    }
+
+    // WebRTC identity not supported as of now
+
+    // Bundle media (we currently only support a single SCTP transport and nothing else)
+    if (bundled_mids) {
+        err |= mbuf_printf(sdp, "a=group:BUNDLE %s\r\n", bundled_mids);
+    }
+
+    // Done
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Get general attributes from an SDP line.
+ */
+static enum rawrtc_code get_general_attributes(
+    char** const bundled_midsp,  // de-referenced, not checked
+    char** const midp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl value;
+
+    // Bundle groups
+    if (!re_regex(line->p, line->l, sdp_group_regex, &value)) {
+        // Check if there is more than one group
+        if (pl_strchr(&value, ' ')) {
+            DEBUG_WARNING("Only one bundle group is supported\n");
+            error = RAWRTC_CODE_NOT_IMPLEMENTED;
+            return error;
+        }
+
+        // Copy group
+        error = rawrtc_error_to_code(pl_strdup(bundled_midsp, &value));
+        if (error) {
+            DEBUG_WARNING("Couldn't copy bundle group\n");
+            return error;
+        }
+    }
+
+    // Media line identification tag
+    if (!re_regex(line->p, line->l, sdp_mid_regex, &value)) {
+        // Copy 'mid'
+        error = rawrtc_error_to_code(pl_strdup(midp, &value));
+        if (error) {
+            DEBUG_WARNING("Couldn't copy 'mid'\n");
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Add ICE attributes to SDP media line.
+ */
+static enum rawrtc_code add_ice_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_peer_connection_context* const context  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_parameters* parameters;
+    char* username_fragment = NULL;
+    char* password = NULL;
+    int err;
+
+    // Get ICE parameters
+    error = rawrtc_ice_gatherer_get_local_parameters(&parameters, context->ice_gatherer);
+    if (error) {
+        return error;
+    }
+
+    // Get values
+    error = rawrtc_ice_parameters_get_username_fragment(&username_fragment, parameters);
+    error |= rawrtc_ice_parameters_get_password(&password, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Set username fragment and password
+    err = mbuf_printf(sdp, "a=ice-ufrag:%s\r\n", username_fragment);
+    err |= mbuf_printf(sdp, "a=ice-pwd:%s\r\n", password);
+    error = rawrtc_error_to_code(err);
+
+out:
+    mem_deref(password);
+    mem_deref(username_fragment);
+    mem_deref(parameters);
+    return error;
+}
+
+/*
+ * Get ICE attributes from SDP line.
+ */
+static enum rawrtc_code get_ice_attributes(
+    bool* const trickle_icep,  // de-referenced, not checked
+    char** const username_fragmentp,  // de-referenced, not checked
+    char** const passwordp,  // de-referenced, not checked
+    bool* const ice_litep,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl value;
+
+    // ICE options trickle
+    if (pl_strcmp(line, sdp_ice_options_trickle) == 0) {
+        *trickle_icep = true;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // ICE username fragment
+    if (!re_regex(line->p, line->l, sdp_ice_username_fragment_regex, &value)) {
+        return rawrtc_sdprintf(username_fragmentp, "%r", &value);
+    }
+
+    // ICE password
+    if (!re_regex(line->p, line->l, sdp_ice_password_regex, &value)) {
+        return rawrtc_sdprintf(passwordp, "%r", &value);
+    }
+
+    // ICE lite
+    if (pl_strcmp(line, sdp_ice_lite) == 0) {
+        *ice_litep = true;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Add DTLS fingerprint attributes to SDP media line.
+ */
+static enum rawrtc_code add_dtls_fingerprint_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_dtls_parameters* const parameters  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    size_t i;
+    char* value = NULL;
+
+    // Get fingerprints
+    error = rawrtc_dtls_parameters_get_fingerprints(&fingerprints, parameters);
+    if (error) {
+        return error;
+    }
+
+    // Add fingerprints
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint = fingerprints->fingerprints[i];
+        enum rawrtc_certificate_sign_algorithm sign_algorithm;
+
+        // Get sign algorithm
+        error = rawrtc_dtls_fingerprint_get_sign_algorithm(&sign_algorithm, fingerprint);
+        if (error) {
+            goto out;
+        }
+
+        // Get fingerprint value
+        error = rawrtc_dtls_fingerprint_get_value(&value, fingerprint);
+        if (error) {
+            goto out;
+        }
+
+        // Add fingerprint attribute
+        error = rawrtc_error_to_code(mbuf_printf(
+            sdp, "a=fingerprint:%s %s\r\n",
+            rawrtc_certificate_sign_algorithm_to_str(sign_algorithm), value));
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(value);
+    mem_deref(fingerprints);
+    return error;
+}
+
+/*
+ * Get DTLS fingerprint attribute from an SDP line.
+ */
+static enum rawrtc_code get_dtls_fingerprint_attributes(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl algorithm_pl;
+    struct pl value_pl;
+    enum rawrtc_code error;
+    char* algorithm_str = NULL;
+    char* value_str = NULL;
+    enum rawrtc_certificate_sign_algorithm algorithm;
+
+    // Parse DTLS fingerprint
+    if (re_regex(line->p, line->l, sdp_dtls_fingerprint_regex, &algorithm_pl, &value_pl)) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    // Copy certificate sign algorithm and value to string
+    error = rawrtc_sdprintf(&algorithm_str, "%r", &algorithm_pl);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_sdprintf(&value_str, "%r", &value_pl);
+    if (error) {
+        goto out;
+    }
+
+    // Convert certificate sign algorithm
+    error = rawrtc_str_to_certificate_sign_algorithm(&algorithm, algorithm_str);
+    if (error) {
+        // This is allowed to fail, some people still use SHA-1 and we don't support it. But there
+        // may be further fingerprints.
+        DEBUG_WARNING("Unsupported certificate sign algorithm: %r\n", &algorithm_pl);
+        error = RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Create DTLS fingerprint
+    error = rawrtc_dtls_fingerprint_create(fingerprintp, algorithm, value_str);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(value_str);
+    mem_deref(algorithm_str);
+    return error;
+}
+
+/*
+ * Add DTLS transport attributes to SDP media line.
+ */
+static enum rawrtc_code add_dtls_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    bool const offering) {
+    enum rawrtc_code error;
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_dtls_role role;
+    char const* setup_str;
+
+    // Get DTLS parameters
+    error = rawrtc_dtls_transport_get_local_parameters(&parameters, context->dtls_transport);
+    if (error) {
+        return error;
+    }
+
+    // Get DTLS role
+    error = rawrtc_dtls_parameters_get_role(&role, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Add setup attribute
+    if (offering) {
+        // Note: When offering, we MUST use 'actpass' as specified in JSEP
+        setup_str = "actpass";
+    } else {
+        switch (role) {
+            case RAWRTC_DTLS_ROLE_AUTO:
+                setup_str = "active";
+                break;
+            case RAWRTC_DTLS_ROLE_CLIENT:
+                setup_str = "active";
+                break;
+            case RAWRTC_DTLS_ROLE_SERVER:
+                setup_str = "passive";
+                break;
+            default:
+                error = RAWRTC_CODE_INVALID_STATE;
+                goto out;
+        }
+    }
+    error = rawrtc_error_to_code(mbuf_printf(sdp, "a=setup:%s\r\n", setup_str));
+    if (error) {
+        goto out;
+    }
+
+    // Add fingerprints
+    error = add_dtls_fingerprint_attributes(sdp, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Add (D)TLS ID
+    error = rawrtc_error_to_code(mbuf_printf(sdp, "a=tls-id:%s\r\n", context->dtls_id));
+    if (error) {
+        goto out;
+    }
+
+out:
+    mem_deref(parameters);
+    return error;
+}
+
+/*
+ * Get DTLS transport attribute from an SDP line.
+ */
+static enum rawrtc_code get_dtls_attributes(
+    enum rawrtc_dtls_role* const rolep,  // de-referenced, not checked
+    struct list* const fingerprints,  // not checked
+    struct pl* const line  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl role_pl;
+    struct rawrtc_dtls_fingerprint* fingerprint;
+
+    // DTLS role
+    if (!re_regex(line->p, line->l, sdp_dtls_role_regex, &role_pl)) {
+        size_t i;
+        for (i = 0; i < map_dtls_role_length; ++i) {
+            if (pl_strcmp(&role_pl, map_str_dtls_role[i]) == 0) {
+                *rolep = map_enum_dtls_role[i];
+                return RAWRTC_CODE_SUCCESS;
+            }
+        }
+    }
+
+    // DTLS fingerprint
+    error = get_dtls_fingerprint_attributes(&fingerprint, line);
+    if (!error) {
+        list_append(fingerprints, &fingerprint->le, fingerprint);
+    }
+    return error;
+}
+
+/*
+ * Add SCTP transport attributes to SDP session.
+ */
+static enum rawrtc_code add_sctp_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    bool const offering,
+    char const* const remote_media_line,
+    char const* const mid,
+    bool const sctp_sdp_05) {
+    enum rawrtc_code error;
+    uint16_t sctp_port;
+    uint16_t sctp_n_streams;
+    int err;
+
+    // Get SCTP port
+    error = rawrtc_sctp_transport_get_port(&sctp_port, transport);
+    if (error) {
+        return error;
+    }
+
+    // Get SCTP #streams
+    error = rawrtc_sctp_transport_get_n_streams(&sctp_n_streams, transport);
+    if (error) {
+        return error;
+    }
+
+    // Add media section
+    if (remote_media_line) {
+        // Just repeat the remote media line.
+        err = mbuf_printf(sdp, "m=%s\r\n", remote_media_line);
+    } else {
+        if (!sctp_sdp_05) {
+            // Note: We choose UDP here although communication may still happen over ICE-TCP
+            //       candidates.
+            // See also: https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-25#section-12.2
+            err = mbuf_printf(
+                sdp, "m=application %" PRIu16 " UDP/DTLS/SCTP webrtc-datachannel\r\n",
+                discard_port);
+        } else {
+            err = mbuf_printf(
+                sdp, "m=application %" PRIu16 " DTLS/SCTP %" PRIu16 "\r\n", discard_port,
+                sctp_port);
+        }
+    }
+    // Add dummy 'c'-line
+    err |= mbuf_write_str(sdp, "c=IN IP4 0.0.0.0\r\n");
+    // Add 'mid' line (if any)
+    if (mid) {
+        err |= mbuf_printf(sdp, "a=mid:%s\r\n", mid);
+    }
+    // Add direction line
+    err |= mbuf_write_str(sdp, "a=sendrecv\r\n");
+    if (err) {
+        return rawrtc_error_to_code(err);
+    }
+
+    // Add ICE attributes
+    error = add_ice_attributes(sdp, context);
+    if (error) {
+        return error;
+    }
+
+    // Add DTLS attributes
+    error = add_dtls_attributes(sdp, context, offering);
+    if (error) {
+        return error;
+    }
+
+    // Set attributes
+    if (!sctp_sdp_05) {
+        // Set SCTP port
+        // Note: Last time I checked, Chrome wasn't able to cope with this
+        err = mbuf_printf(sdp, "a=sctp-port:%" PRIu16 "\r\n", sctp_port);
+    } else {
+        // Set SCTP port, upper layer protocol and number of streams
+        err = mbuf_printf(
+            sdp, "a=sctpmap:%" PRIu16 " webrtc-datachannel %" PRIu16 "\r\n", sctp_port,
+            sctp_n_streams);
+    }
+    if (err) {
+        return rawrtc_error_to_code(err);
+    }
+
+    // Set maximum message size
+    // Note: This isn't part of the 05 version but Firefox can only parse 'max-message-size' but
+    //       doesn't understand the old 'sctpmap' one (from 06 to 21).
+    err = mbuf_write_str(sdp, "a=max-message-size:0\r\n");
+    error = rawrtc_error_to_code(err);
+    if (error) {
+        return error;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get SCTP transport attributes from an SDP line.
+ */
+static enum rawrtc_code get_sctp_attributes(
+    uint16_t* const portp,  // de-referenced, not checked
+    uint64_t* const max_message_sizep,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl port_pl;
+    uint32_t port;
+    struct pl max_message_size_pl;
+
+    // SCTP port (from 'sctpmap' or 'sctp-port')
+    if (!re_regex(line->p, line->l, sdp_sctp_port_sctmap_regex, &port_pl, NULL) ||
+        !re_regex(line->p, line->l, sdp_sctp_port_regex, &port_pl)) {
+        port = pl_u32(&port_pl);
+
+        // Validate port
+        if (port == 0 || port > UINT16_MAX) {
+            DEBUG_WARNING("Invalid SCTP port: %" PRIu32 "\n", port);
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+
+        // Set port & done
+        *portp = (uint16_t) port;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // SCTP maximum message size
+    // Note: Theoretically, there's another approach as part of 'sctmap' which has been deprecated
+    //       but I doubt anyone ever implemented that.
+    if (!re_regex(line->p, line->l, sdp_sctp_maximum_message_size_regex, &max_message_size_pl)) {
+        *max_message_sizep = pl_u64(&max_message_size_pl);
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Get an ICE candidate from the description.
+ */
+static enum rawrtc_code get_ice_candidate_attributes(
+    struct list* const candidate_lines,  // not checked
+    bool* const end_of_candidatesp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    bool add_candidate_line = false;
+    struct pl* use_line = NULL;
+
+    // ICE candidate
+    if (line->l >= sdp_ice_candidate_head_length) {
+        struct pl candidate_pl = {
+            .p = line->p,
+            .l = sdp_ice_candidate_head_length - 1,
+        };
+        if (pl_strcmp(&candidate_pl, sdp_ice_candidate_head) == 0) {
+            add_candidate_line = true;
+            use_line = line;
+        }
+    }
+
+    // End of candidates
+    if (!add_candidate_line && pl_strcmp(line, sdp_ice_end_of_candidates) == 0) {
+        add_candidate_line = true;
+        use_line = NULL;
+        *end_of_candidatesp = true;
+    }
+
+    // Create candidate line (if any)
+    if (add_candidate_line) {
+        struct candidate_line* const candidate_line = mem_zalloc(sizeof(*candidate_line), NULL);
+        if (!candidate_line) {
+            DEBUG_WARNING("Unable to create candidate line, no memory\n");
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+
+        // Set fields
+        // Warning: The line is NOT copied - it's just a pointer to some memory provided by
+        //          the caller!
+        if (use_line) {
+            candidate_line->line = *use_line;
+        }
+
+        // Add candidate line to list & done
+        list_append(candidate_lines, &candidate_line->le, candidate_line);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Destructor for an existing peer connection description.
+ */
+static void rawrtc_peer_connection_description_destroy(void* arg) {
+    struct rawrtc_peer_connection_description* const description = arg;
+
+    // Un-reference
+    mem_deref(description->sdp);
+    mem_deref(description->sctp_capabilities);
+    mem_deref(description->dtls_parameters);
+    mem_deref(description->ice_parameters);
+    list_flush(&description->ice_candidates);
+    mem_deref(description->mid);
+    mem_deref(description->remote_media_line);
+    mem_deref(description->bundled_mids);
+    mem_deref(description->connection);
+}
+
+/*
+ * Create a description by creating an offer or answer.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_create_internal(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    bool const offering) {
+    struct rawrtc_peer_connection_context* context;
+    struct rawrtc_peer_connection_description* remote_description;
+    struct rawrtc_peer_connection_description* local_description;
+    enum rawrtc_code error;
+    struct mbuf* sdp = NULL;
+    enum rawrtc_data_transport_type data_transport_type;
+    void* data_transport = NULL;
+
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get context
+    context = &connection->context;
+
+    // Ensure a data transport has been set (otherwise, there would be nothing to do)
+    if (!context->data_transport) {
+        DEBUG_WARNING("No data transport set\n");
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    // Ensure a remote description is available (when answering)
+    remote_description = connection->remote_description;
+    if (!offering && !remote_description) {
+        DEBUG_WARNING("No remote description set\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    local_description =
+        mem_zalloc(sizeof(*local_description), rawrtc_peer_connection_description_destroy);
+    if (!local_description) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set initial values
+    local_description->connection = mem_ref(connection);  // Warning: Circular reference
+    local_description->end_of_candidates = false;
+    if (offering) {
+        local_description->type = RAWRTC_SDP_TYPE_OFFER;
+        local_description->trickle_ice = true;
+        error =
+            rawrtc_strdup(&local_description->bundled_mids, RAWRTC_PEER_CONNECTION_DESCRIPTION_MID);
+        if (error) {
+            goto out;
+        }
+        local_description->media_line_index = 0;  // Since we only support one media line...
+        error = rawrtc_strdup(&local_description->mid, RAWRTC_PEER_CONNECTION_DESCRIPTION_MID);
+        if (error) {
+            goto out;
+        }
+        local_description->sctp_sdp_05 = connection->configuration->sctp_sdp_05;
+    } else {
+        local_description->type = RAWRTC_SDP_TYPE_ANSWER;
+        local_description->trickle_ice = remote_description->trickle_ice;
+        local_description->bundled_mids = mem_ref(remote_description->bundled_mids);
+        local_description->remote_media_line = mem_ref(remote_description->remote_media_line);
+        local_description->media_line_index = remote_description->media_line_index;
+        local_description->mid = mem_ref(remote_description->mid);
+        local_description->sctp_sdp_05 = remote_description->sctp_sdp_05;
+    }
+
+    // Create buffer for local description
+    sdp = mbuf_alloc(RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SIZE);
+    if (!sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Set session boilerplate
+    error = set_session_boilerplate(sdp, RAWRTC_VERSION, rand_u32());
+    if (error) {
+        goto out;
+    }
+
+    // Set session attributes
+    error = set_session_attributes(
+        sdp, local_description->trickle_ice, local_description->bundled_mids);
+    if (error) {
+        goto out;
+    }
+
+    // Get data transport
+    error = rawrtc_data_transport_get_transport(
+        &data_transport_type, &data_transport, context->data_transport);
+    if (error) {
+        return error;
+    }
+
+    // Add data transport
+    switch (data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP:
+            // Add SCTP transport
+            error = add_sctp_attributes(
+                sdp, data_transport, context, offering, local_description->remote_media_line,
+                local_description->mid, local_description->sctp_sdp_05);
+            if (error) {
+                goto out;
+            }
+            break;
+        default:
+            error = RAWRTC_CODE_UNKNOWN_ERROR;
+            goto out;
+    }
+
+    // Reference SDP
+    local_description->sdp = mem_ref(sdp);
+
+    // Debug
+    DEBUG_PRINTF(
+        "Description (internal):\n%H\n", rawrtc_peer_connection_description_debug,
+        local_description);
+
+out:
+    mem_deref(data_transport);
+    mem_deref(sdp);
+    if (error) {
+        mem_deref(local_description);
+    } else {
+        // Set pointer & done
+        *descriptionp = local_description;
+    }
+    return error;
+}
+
+/*
+ * Add an ICE candidate to the description.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_add_candidate(
+    struct rawrtc_peer_connection_description* const description,
+    struct rawrtc_peer_connection_ice_candidate* const candidate  // nullable
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Write candidate or end of candidates indication
+    if (candidate) {
+        char* candidate_sdp;
+
+        // Already written?
+        if (description->end_of_candidates) {
+            return RAWRTC_CODE_INVALID_STATE;
+        }
+
+        // Get candidate SDP
+        error = rawrtc_peer_connection_ice_candidate_get_sdp(&candidate_sdp, candidate);
+        if (error) {
+            return error;
+        }
+
+        // TODO: We would have to get the associated 'mid', media line index and username fragment
+        //       as well and...
+        //
+        //       * inject the candidate at the correct place (compare 'mid' or line index), and
+        //       * compare the username fragment against the one that's currently active (once we
+        //         support ICE restarts).
+
+        // Write candidate to SDP
+        // Note: We only have one media line, so it should be fine to append this to the end
+        error = rawrtc_error_to_code(mbuf_printf(description->sdp, "a=%s\r\n", candidate_sdp));
+        if (error) {
+            DEBUG_WARNING(
+                "Couldn't write candidate to description, reason: %s\n", rawrtc_code_to_str(error));
+            mem_deref(candidate_sdp);
+            return error;
+        }
+
+        // Debug
+        DEBUG_PRINTF("Added candidate line: %s\n", candidate_sdp);
+        mem_deref(candidate_sdp);
+    } else {
+        // Already written?
+        if (description->end_of_candidates) {
+            DEBUG_WARNING("End of candidates has already been written\n");
+            return RAWRTC_CODE_SUCCESS;
+        }
+
+        // Write end of candidates into SDP
+        error = rawrtc_error_to_code(mbuf_write_str(description->sdp, "a=end-of-candidates\r\n"));
+        if (error) {
+            return error;
+        }
+        description->end_of_candidates = true;
+
+        // Debug
+        DEBUG_PRINTF(
+            "Description (end-of-candidates):\n%H\n", rawrtc_peer_connection_description_debug,
+            description);
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+// Helper for parsing SDP attributes
+#define HANDLE_ATTRIBUTE(code) \
+    error = code; \
+    if (error == RAWRTC_CODE_SUCCESS) { \
+        break; \
+    } else if (error != RAWRTC_CODE_NO_VALUE) { \
+        goto out; \
+        break; \
+    }
+
+/*
+ * Create a description by parsing it from SDP.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_create(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    enum rawrtc_sdp_type const type,
+    char const* const sdp) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_description* remote_description;
+    char const* cursor;
+    bool media_line = false;
+    struct le* le;
+
+    // ICE parameters
+    char* ice_username_fragment = NULL;
+    char* ice_password = NULL;
+    bool ice_lite = false;
+
+    // DTLS parameters
+    enum rawrtc_dtls_role dtls_role = RAWRTC_DTLS_ROLE_AUTO;
+    struct list dtls_fingerprints = LIST_INIT;
+
+    // SCTP capabilities
+    uint64_t sctp_max_message_size = RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_MAX_MESSAGE_SIZE;
+
+    // ICE candidate lines (temporarily stored, so it can be parsed later)
+    struct list ice_candidate_lines = LIST_INIT;
+
+    // Check arguments
+    if (!descriptionp || !sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (type != RAWRTC_SDP_TYPE_OFFER && type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Allocate
+    remote_description =
+        mem_zalloc(sizeof(*remote_description), rawrtc_peer_connection_description_destroy);
+    if (!remote_description) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields to initial values
+    remote_description->type = type;
+    remote_description->trickle_ice = false;
+    remote_description->media_line_index = 0;  // Since we only support one media line...
+    remote_description->sctp_sdp_05 = true;
+    list_init(&remote_description->ice_candidates);
+    remote_description->sctp_port = RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SCTP_PORT;
+
+    // Find required session and media attributes
+    cursor = sdp;
+    while (*cursor != '\0') {
+        struct pl line;
+        char sdp_type;
+
+        // Ignore lines beginning with '\r' or '\n'
+        if (*cursor == '\r' || *cursor == '\n') {
+            ++cursor;
+            continue;
+        }
+
+        // Find next line or end of string
+        for (line.p = cursor, line.l = 0; *cursor != '\r' && *cursor != '\n' && *cursor != '\0';
+             ++cursor, ++line.l) {
+        }
+
+        // Get line type and move line cursor to value
+        if (line.l < 2) {
+            DEBUG_WARNING("Invalid SDP line: %r\n", &line);
+            break;
+        }
+        sdp_type = *line.p;
+        pl_advance(&line, 2);
+
+        // Are we interested in this line?
+        switch (sdp_type) {
+            case 'a': {
+                // Be aware we're using a macro here which does the following:
+                //
+                // * if the function returns 'success', break (and therefore don't continue
+                //   parsing other attributes on this line).
+                // * if the function returns 'no value', do nothing (and therefore continue parsing
+                //   other attributes on this line).
+                // * if the function returns anything else (which indicates an error), set 'error'
+                //   and jump to 'out'.
+                HANDLE_ATTRIBUTE(get_general_attributes(
+                    &remote_description->bundled_mids, &remote_description->mid, &line));
+                HANDLE_ATTRIBUTE(get_ice_attributes(
+                    &remote_description->trickle_ice, &ice_username_fragment, &ice_password,
+                    &ice_lite, &line));
+                HANDLE_ATTRIBUTE(get_dtls_attributes(&dtls_role, &dtls_fingerprints, &line));
+                HANDLE_ATTRIBUTE(get_sctp_attributes(
+                    &remote_description->sctp_port, &sctp_max_message_size, &line));
+                HANDLE_ATTRIBUTE(get_ice_candidate_attributes(
+                    &ice_candidate_lines, &remote_description->end_of_candidates, &line));
+                break;
+            }
+            case 'm': {
+                struct pl application;
+                size_t i;
+
+                // Ensure amount of media lines is exactly one
+                if (media_line) {
+                    DEBUG_WARNING("Unable to handle more than one media line\n");
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Parse media line
+                if (re_regex(line.p, line.l, sdp_application_dtls_sctp_regex, NULL, &application)) {
+                    DEBUG_WARNING("Unsupport media line: %r\n", &line);
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Check if the application matches some kind of DTLS/SCTP variant (ugh...)
+                for (i = 0; i < sdp_application_dtls_sctp_variants_length; ++i) {
+                    if (pl_strcmp(&application, sdp_application_dtls_sctp_variants[i]) == 0) {
+                        media_line = true;
+                    }
+                }
+                if (!media_line) {
+                    DEBUG_WARNING("Unsupported application on media line: %r\n", &application);
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Copy media line
+                error = rawrtc_sdprintf(&remote_description->remote_media_line, "%r", &line);
+                if (error) {
+                    goto out;
+                }
+
+                // Done
+                break;
+            }
+            default:
+                DEBUG_PRINTF(
+                    "Ignoring %s line: %c=%r\n", media_line ? "media" : "session", sdp_type, &line);
+                break;
+        }
+    }
+
+    // Return 'no value' in case there was no media line
+    if (!media_line) {
+        error = RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Create ICE parameters (if possible)
+    if (ice_username_fragment && ice_password) {
+        error = rawrtc_ice_parameters_create(
+            &remote_description->ice_parameters, ice_username_fragment, ice_password, ice_lite);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create DTLS parameters (if possible)
+    if (!list_isempty(&dtls_fingerprints)) {
+        error = rawrtc_dtls_parameters_create_internal(
+            &remote_description->dtls_parameters, dtls_role, &dtls_fingerprints);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create SCTP capabilities
+    error = rawrtc_sctp_capabilities_create(
+        &remote_description->sctp_capabilities, sctp_max_message_size);
+    if (error) {
+        goto out;
+    }
+
+    // Late parsing of ICE candidates.
+    // Note: This is required since the 'mid' and the username fragment may be parsed after a
+    //       candidate has been found.
+    for (le = list_head(&ice_candidate_lines); le != NULL; le = le->next) {
+        struct candidate_line* const candidate_line = le->data;
+
+        // Create ICE candidate
+        struct rawrtc_peer_connection_ice_candidate* candidate;
+        error = rawrtc_peer_connection_ice_candidate_create_internal(
+            &candidate, &candidate_line->line, remote_description->mid,
+            &remote_description->media_line_index, ice_username_fragment);
+        if (error) {
+            goto out;
+        }
+
+        // Add ICE candidate to the list
+        DEBUG_PRINTF("Adding ICE candidate to description\n");
+        list_append(&remote_description->ice_candidates, &candidate->le, candidate);
+    }
+
+    // Copy SDP
+    remote_description->sdp = mbuf_alloc(strlen(sdp));
+    if (!remote_description->sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    mbuf_write_str(remote_description->sdp, sdp);
+
+    // Debug
+    DEBUG_PRINTF(
+        "Description (parsed):\n%H\n", rawrtc_peer_connection_description_debug,
+        remote_description);
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    list_flush(&ice_candidate_lines);
+    list_flush(&dtls_fingerprints);
+    mem_deref(ice_password);
+    mem_deref(ice_username_fragment);
+    if (error) {
+        mem_deref(remote_description);
+    } else {
+        // Set pointer & done
+        *descriptionp = remote_description;
+    }
+    return error;
+}
diff --git a/src/peer_connection_description/description.h b/src/peer_connection_description/description.h
new file mode 100644
index 0000000..eabf2e0
--- /dev/null
+++ b/src/peer_connection_description/description.h
@@ -0,0 +1,47 @@
+#pragma once
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <re.h>
+
+#define RAWRTC_PEER_CONNECTION_DESCRIPTION_MID "rawrtc-sctp-dc"
+
+struct rawrtc_peer_connection_description {
+    struct rawrtc_peer_connection* connection;
+    enum rawrtc_sdp_type type;
+    bool trickle_ice;
+    char* bundled_mids;
+    char* remote_media_line;
+    uint8_t media_line_index;
+    char* mid;
+    bool sctp_sdp_05;
+    bool end_of_candidates;
+    struct list ice_candidates;
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct rawrtc_sctp_capabilities* sctp_capabilities;
+    uint16_t sctp_port;
+    struct mbuf* sdp;
+};
+
+enum {
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SIZE = 1024,
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_MAX_MESSAGE_SIZE = 65536,
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SCTP_PORT = 5000,
+};
+
+enum rawrtc_code rawrtc_peer_connection_description_create_internal(
+    struct rawrtc_peer_connection_description** const descriptionp,
+    struct rawrtc_peer_connection* const connection,
+    bool const offering);
+
+enum rawrtc_code rawrtc_peer_connection_description_add_candidate(
+    struct rawrtc_peer_connection_description* const description,
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+int rawrtc_peer_connection_description_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_description* const description);
diff --git a/src/peer_connection_description/meson.build b/src/peer_connection_description/meson.build
new file mode 100644
index 0000000..54c8f27
--- /dev/null
+++ b/src/peer_connection_description/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'description.c',
+    'utils.c',
+])
diff --git a/src/peer_connection_description/utils.c b/src/peer_connection_description/utils.c
new file mode 100644
index 0000000..d05d13b
--- /dev/null
+++ b/src/peer_connection_description/utils.c
@@ -0,0 +1,149 @@
+#include "description.h"
+#include "../dtls_parameters/parameters.h"
+#include "../ice_parameters/parameters.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <re.h>
+
+static enum rawrtc_sdp_type const map_enum_sdp_type[] = {
+    RAWRTC_SDP_TYPE_OFFER,
+    RAWRTC_SDP_TYPE_PROVISIONAL_ANSWER,
+    RAWRTC_SDP_TYPE_ANSWER,
+    RAWRTC_SDP_TYPE_ROLLBACK,
+};
+
+static char const* const map_str_sdp_type[] = {
+    "offer",
+    "pranswer",
+    "answer",
+    "rollback",
+};
+
+static size_t const map_sdp_type_length = ARRAY_SIZE(map_enum_sdp_type);
+
+/*
+ * Translate an SDP type to str.
+ */
+char const* rawrtc_sdp_type_to_str(enum rawrtc_sdp_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_sdp_type_length; ++i) {
+        if (map_enum_sdp_type[i] == type) {
+            return map_str_sdp_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an SDP type.
+ */
+enum rawrtc_code rawrtc_str_to_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_sdp_type_length; ++i) {
+        if (str_casecmp(map_str_sdp_type[i], str) == 0) {
+            *typep = map_enum_sdp_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Print debug information for a peer connection description.
+ */
+int rawrtc_peer_connection_description_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_description* const description) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!description) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "----- Peer Connection Description <%p>\n", description);
+
+    // Print general fields
+    err |= re_hprintf(pf, "  peer_connection=");
+    if (description->connection) {
+        err |= re_hprintf(pf, "%p\n", description->connection);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  sdp_type=%s\n", rawrtc_sdp_type_to_str(description->type));
+    err |= re_hprintf(pf, "  trickle_ice=%s\n", description->trickle_ice ? "yes" : "no");
+    err |= re_hprintf(pf, "  bundled_mids=");
+    if (description->bundled_mids) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->bundled_mids);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  remote_media_line=");
+    if (description->remote_media_line) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->remote_media_line);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  media_line_index=%" PRIu8 "\n", description->media_line_index);
+    err |= re_hprintf(pf, "  mid=");
+    if (description->mid) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->mid);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  sctp_sdp_05=%s\n", description->sctp_sdp_05 ? "yes" : "no");
+    err |=
+        re_hprintf(pf, "  end_of_candidates=%s\n", description->end_of_candidates ? "yes" : "no");
+
+    // Print ICE parameters
+    if (description->ice_parameters) {
+        err |= re_hprintf(pf, "%H", rawrtc_ice_parameters_debug, description->ice_parameters);
+    } else {
+        err |= re_hprintf(pf, "  ICE Parameters <n/a>\n");
+    }
+
+    // Print ICE candidates
+    le = list_head(&description->ice_candidates);
+    if (le) {
+        for (; le != NULL; le = le->next) {
+            struct rawrtc_peer_connection_ice_candidate* const candidate = le->data;
+            err |= re_hprintf(pf, "%H", rawrtc_peer_connection_ice_candidate_debug, candidate);
+        }
+    } else {
+        err |= re_hprintf(pf, "  ICE Candidates <n/a>\n");
+    }
+
+    // Print DTLS parameters
+    if (description->dtls_parameters) {
+        err |= re_hprintf(pf, "%H", rawrtc_dtls_parameters_debug, description->dtls_parameters);
+    } else {
+        err |= re_hprintf(pf, "  DTLS Parameters <n/a>\n");
+    }
+
+    // Print SCTP capabilities & port
+    if (description->sctp_capabilities) {
+        err |= re_hprintf(pf, "%H", rawrtc_sctp_capabilities_debug, description->sctp_capabilities);
+    } else {
+        err |= re_hprintf(pf, "  SCTP Capabilities <n/a>\n");
+    }
+    err |= re_hprintf(pf, "  sctp_port=%" PRIu16 "\n", description->sctp_port);
+
+    // Print SDP
+    err |= re_hprintf(pf, "  sdp=\n%b", description->sdp->buf, description->sdp->end);
+
+    // Done
+    return err;
+}
diff --git a/src/peer_connection_ice_candidate/attributes.c b/src/peer_connection_ice_candidate/attributes.c
new file mode 100644
index 0000000..eed0125
--- /dev/null
+++ b/src/peer_connection_ice_candidate/attributes.c
@@ -0,0 +1,220 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Encode the ICE candidate into SDP.
+ * `*sdpp` will be set to a copy of the SDP attribute that must be
+ * unreferenced.
+ *
+ * Note: This is equivalent to the `candidate` attribute of the W3C
+ *       WebRTC specification's `RTCIceCandidateInit`.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    enum rawrtc_code error;
+    char* foundation = NULL;
+    uint16_t component_id = 1;
+    enum rawrtc_ice_protocol protocol;
+    char const* protocol_str;
+    uint32_t priority;
+    char* ip = NULL;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    char const* type_str;
+    struct mbuf* sdp = NULL;
+    char* related_address = NULL;
+    uint16_t related_port = 0;
+    enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+    char const* tcp_type_str;
+
+    // Check arguments
+    if (!sdpp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get values for mandatory fields
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    // TODO: Get component ID from candidate/gatherer/transport
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_port(&port, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_type(&type, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate->candidate);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_related_port(&related_port, candidate->candidate);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+    protocol_str = rawrtc_ice_protocol_to_str(protocol);
+    type_str = rawrtc_ice_candidate_type_to_str(type);
+
+    // Initialise SDP attribute buffer
+    sdp = mbuf_alloc(RAWRTC_PEER_CONNECTION_CANDIDATE_DEFAULT_SIZE);
+    if (!sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Encode candidate's mandatory fields
+    error = rawrtc_error_to_code(mbuf_printf(
+        sdp, "candidate:%s %" PRIu16 " %s %" PRIu32 " %s %" PRIu16 " typ %s", foundation,
+        component_id, protocol_str, priority, ip, port, type_str));
+    if (error) {
+        goto out;
+    }
+    if (related_address) {
+        error = rawrtc_error_to_code(mbuf_printf(sdp, " raddr %s", related_address));
+        if (error) {
+            goto out;
+        }
+    }
+    if (related_port > 0) {
+        error = rawrtc_error_to_code(mbuf_printf(sdp, " rport %" PRIu16, related_port));
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Get value for 'tcptype' extension field and encode it (if available)
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate->candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            tcp_type_str = rawrtc_ice_tcp_candidate_type_to_str(tcp_type);
+            mbuf_printf(sdp, " tcptype %s", tcp_type_str);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            break;
+        default:
+            goto out;
+    }
+
+    // Copy SDP attribute
+    error = rawrtc_sdprintf(sdpp, "%b", sdp->buf, sdp->end);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(related_address);
+    mem_deref(sdp);
+    mem_deref(ip);
+    mem_deref(foundation);
+    return error;
+}
+
+/*
+ * Get the media stream identification tag the ICE candidate is
+ * associated to.
+ * `*midp` will be set to a copy of the candidate's mid and must be
+ * unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no 'mid' has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*midp* must
+ * be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_mid(
+    char** const midp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!midp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy mid (if any)
+    if (candidate->mid) {
+        return rawrtc_strdup(midp, candidate->mid);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the media stream line index the ICE candidate is associated to.
+ * Return `RAWRTC_CODE_NO_VALUE` in case no media line index has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_media_line_index(
+    uint8_t* const media_line_index,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!media_line_index || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set media line index (if any)
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX) {
+        *media_line_index = (uint8_t) candidate->media_line_index;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the username fragment the ICE candidate is associated to.
+ * `*username_fragmentp` will be set to a copy of the candidate's
+ * username fragment and must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no username fragment has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*username_fragmentp* must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!username_fragmentp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy username fragment (if any)
+    if (candidate->username_fragment) {
+        return rawrtc_strdup(username_fragmentp, candidate->username_fragment);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the underlying ORTC ICE candidate from the ICE candidate.
+ * `*ortc_candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_ortc_candidate(
+    struct rawrtc_ice_candidate** const ortc_candidatep,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!ortc_candidatep || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference ORTC ICE candidate
+    *ortc_candidatep = mem_ref(candidate->candidate);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/peer_connection_ice_candidate/candidate.c b/src/peer_connection_ice_candidate/candidate.c
new file mode 100644
index 0000000..4f2c7fc
--- /dev/null
+++ b/src/peer_connection_ice_candidate/candidate.c
@@ -0,0 +1,252 @@
+#include "candidate.h"
+#include "../ice_candidate/candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+static char const sdp_ice_candidate_regex[] =
+    "candidate:[^ ]+ [0-9]+ [^ ]+ [0-9]+ [^ ]+ [0-9]+ typ [^ ]+[^]*";
+static char const sdp_ice_candidate_related_address_regex[] = "[^]* raddr [^ ]+";
+static char const sdp_ice_candidate_related_port_regex[] = "[^]* rport [0-9]+";
+static char const sdp_ice_candidate_tcp_type_regex[] = "[^]* tcptype [^ ]+";
+
+/*
+ * Destructor for an existing peer connection.
+ */
+static void rawrtc_peer_connection_ice_candidate_destroy(void* arg) {
+    struct rawrtc_peer_connection_ice_candidate* const candidate = arg;
+
+    // Un-reference
+    mem_deref(candidate->username_fragment);
+    mem_deref(candidate->mid);
+    mem_deref(candidate->candidate);
+}
+
+/*
+ * Create a new ICE candidate from an existing (ORTC) ICE candidate.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+) {
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Ensure either 'mid' or the media line index is present
+    if (!mid && !media_line_index) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_peer_connection_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    candidate->candidate = mem_ref(ortc_candidate);
+    candidate->mid = mem_ref(mid);
+    candidate->media_line_index = (int16_t)(media_line_index ? *media_line_index : -1);
+    candidate->username_fragment = mem_ref(username_fragment);
+
+    // Set pointer & done
+    *candidatep = candidate;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create a new ICE candidate from SDP (pl variant).
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create_internal(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const sdp,
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+) {
+    enum rawrtc_code error;
+    struct pl optional;
+    uint32_t value_u32;
+
+    // Mandatory fields
+    struct pl foundation_pl;
+    struct pl component_id_pl;
+    struct pl protocol_pl;
+    struct pl priority_pl;
+    struct pl ip_pl;
+    struct pl port_pl;
+    struct pl type_pl;
+    uint32_t priority;
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+
+    // Optional fields
+    struct pl related_address_pl = PL_INIT;
+    struct pl related_port_pl = PL_INIT;
+    struct pl tcp_type_pl = PL_INIT;
+    uint16_t related_port = 0;
+    enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+
+    // (ORTC) ICE candidate
+    struct rawrtc_ice_candidate* ortc_candidate;
+
+    // ICE candidate
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure either 'mid' or the media line index is present
+    if (!mid && !media_line_index) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    if (pl_isset(sdp)) {
+        // Get mandatory ICE candidate fields
+        if (re_regex(
+                sdp->p, sdp->l, sdp_ice_candidate_regex, &foundation_pl, &component_id_pl,
+                &protocol_pl, &priority_pl, &ip_pl, &port_pl, &type_pl, &optional)) {
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+
+        // Get optional ICE candidate fields
+        re_regex(
+            optional.p, optional.l, sdp_ice_candidate_related_address_regex, NULL,
+            &related_address_pl);
+        re_regex(
+            optional.p, optional.l, sdp_ice_candidate_related_port_regex, NULL, &related_port_pl);
+        re_regex(optional.p, optional.l, sdp_ice_candidate_tcp_type_regex, NULL, &tcp_type_pl);
+
+        // Component ID
+        // TODO: Handle
+        (void) component_id_pl;
+
+        // Protocol
+        error = rawrtc_pl_to_ice_protocol(&protocol, &protocol_pl);
+        if (error) {
+            return error;
+        }
+
+        // Priority
+        priority = pl_u32(&priority_pl);
+
+        // Port
+        value_u32 = pl_u32(&port_pl);
+        if (value_u32 > UINT16_MAX) {
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+        port = (uint16_t) value_u32;
+
+        // Type
+        error = rawrtc_pl_to_ice_candidate_type(&type, &type_pl);
+        if (error) {
+            return error;
+        }
+
+        // Related port (if any)
+        if (pl_isset(&related_port_pl)) {
+            value_u32 = pl_u32(&related_port_pl);
+            if (value_u32 > UINT16_MAX) {
+                return RAWRTC_CODE_INVALID_ARGUMENT;
+            }
+            related_port = (uint16_t) value_u32;
+        }
+
+        // TCP type (if any)
+        if (pl_isset(&tcp_type_pl)) {
+            error = rawrtc_pl_to_ice_tcp_candidate_type(&tcp_type, &tcp_type_pl);
+            if (error) {
+                return error;
+            }
+        }
+
+        // Create (ORTC) ICE candidate
+        error = rawrtc_ice_candidate_create_internal(
+            &ortc_candidate, &foundation_pl, priority, &ip_pl, protocol, port, type, tcp_type,
+            &related_address_pl, related_port);
+        if (error) {
+            return error;
+        }
+    } else {
+        ortc_candidate = NULL;
+    }
+
+    // Create ICE candidate
+    error = rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+        &candidate, ortc_candidate, mid, media_line_index, username_fragment);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(ortc_candidate);
+    if (!error) {
+        // Set pointer & done
+        *candidatep = candidate;
+    }
+    return error;
+}
+
+/*
+ * Create a new ICE candidate from SDP.
+ * `*candidatesp` must be unreferenced.
+ *
+ * Note: This is equivalent to creating an `RTCIceCandidate` from an
+ *       `RTCIceCandidateInit` instance in the W3C WebRTC
+ *       specification.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    char* const sdp,
+    char* const mid,  // nullable, copied
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, copied
+) {
+    struct pl sdp_pl;
+    enum rawrtc_code error;
+    char* mid_copy = NULL;
+    char* username_fragment_copy = NULL;
+
+    // Check arguments (not checked in the internal function)
+    if (!sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert SDP str to pl
+    pl_set_str(&sdp_pl, sdp);
+
+    // Copy arguments that will be referenced
+    if (mid) {
+        error = rawrtc_strdup(&mid_copy, mid);
+        if (error) {
+            goto out;
+        }
+    }
+    if (username_fragment) {
+        error = rawrtc_strdup(&username_fragment_copy, username_fragment);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create ICE candidate
+    error = rawrtc_peer_connection_ice_candidate_create_internal(
+        candidatep, &sdp_pl, mid_copy, media_line_index, username_fragment_copy);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(username_fragment_copy);
+    mem_deref(mid_copy);
+    return error;
+}
diff --git a/src/peer_connection_ice_candidate/candidate.h b/src/peer_connection_ice_candidate/candidate.h
new file mode 100644
index 0000000..6fbd828
--- /dev/null
+++ b/src/peer_connection_ice_candidate/candidate.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <rawrtc.h>
+
+enum {
+    RAWRTC_PEER_CONNECTION_CANDIDATE_DEFAULT_SIZE = 256,
+};
+
+struct rawrtc_peer_connection_ice_candidate {
+    struct le le;
+    struct rawrtc_ice_candidate* candidate;
+    char* mid;
+    int16_t media_line_index;
+    char* username_fragment;
+};
+
+int rawrtc_peer_connection_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+);
+
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create_internal(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const sdp,
+    char* const mid,  // nullable
+    uint8_t const* const media_line_index,  // nullable
+    char* const username_fragment);
diff --git a/src/peer_connection_ice_candidate/meson.build b/src/peer_connection_ice_candidate/meson.build
new file mode 100644
index 0000000..6ff3140
--- /dev/null
+++ b/src/peer_connection_ice_candidate/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'candidate.c',
+    'utils.c',
+])
diff --git a/src/peer_connection_ice_candidate/utils.c b/src/peer_connection_ice_candidate/utils.c
new file mode 100644
index 0000000..49cd11e
--- /dev/null
+++ b/src/peer_connection_ice_candidate/utils.c
@@ -0,0 +1,47 @@
+#include "candidate.h"
+#include "../ice_candidate/candidate.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Print debug information for an ICE candidate.
+ */
+int rawrtc_peer_connection_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    int err = 0;
+
+    // Check arguments
+    if (!candidate) {
+        return 0;
+    }
+
+    // ORTC ICE candidate
+    err |= re_hprintf(pf, "%H", rawrtc_ice_candidate_debug, candidate->candidate);
+
+    // Media line identification tag
+    err |= re_hprintf(pf, "    mid=");
+    if (candidate->mid) {
+        err |= re_hprintf(pf, "\"%s\"\n", candidate->mid);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Media line index
+    err |= re_hprintf(pf, "    media_line_index=");
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX) {
+        err |= re_hprintf(pf, "%" PRId16 "\n", candidate->media_line_index);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Username fragment
+    err |= re_hprintf(pf, "    username_fragment=");
+    if (candidate->username_fragment) {
+        err |= re_hprintf(pf, "\"%s\"\n", candidate->username_fragment);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/peer_connection_state/meson.build b/src/peer_connection_state/meson.build
new file mode 100644
index 0000000..55395eb
--- /dev/null
+++ b/src/peer_connection_state/meson.build
@@ -0,0 +1 @@
+sources += files('state.c')
diff --git a/src/peer_connection_state/state.c b/src/peer_connection_state/state.c
new file mode 100644
index 0000000..5aa274c
--- /dev/null
+++ b/src/peer_connection_state/state.c
@@ -0,0 +1,45 @@
+#include <rawrtc/peer_connection_state.h>
+
+/*
+ * Get the corresponding name for a signaling state.
+ */
+char const* rawrtc_signaling_state_to_name(enum rawrtc_signaling_state const state) {
+    switch (state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            return "stable";
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            return "have-local-offer";
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            return "have-remote-offer";
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            return "have-local-pranswer";
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            return "have-remote-pranswer";
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for a peer connection state.
+ */
+char const* rawrtc_peer_connection_state_to_name(enum rawrtc_peer_connection_state const state) {
+    switch (state) {
+        case RAWRTC_PEER_CONNECTION_STATE_NEW:
+            return "new";
+        case RAWRTC_PEER_CONNECTION_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_PEER_CONNECTION_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_PEER_CONNECTION_STATE_DISCONNECTED:
+            return "disconnected";
+        case RAWRTC_PEER_CONNECTION_STATE_CLOSED:
+            return "closed";
+        case RAWRTC_PEER_CONNECTION_STATE_FAILED:
+            return "failed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/sctp_common/common.c b/src/sctp_common/common.c
new file mode 100644
index 0000000..b40bdac
--- /dev/null
+++ b/src/sctp_common/common.c
@@ -0,0 +1,112 @@
+#include "common.h"
+#include "../dtls_transport/transport.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+
+// Note: Although shared with the redirect transport, this name is accurate enough for both.
+#define DEBUG_MODULE "sctp-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * External DTLS role getter.
+ * Warning: `rolep` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_dtls_role_getter(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced, not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    return rawrtc_dtls_transport_get_external_role(rolep, dtls_transport);
+}
+
+/*
+ * Get the external DTLS transport state.
+ * Warning: `statep` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_dtls_transport_state_getter(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced, not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    return rawrtc_dtls_transport_get_external_state(statep, dtls_transport);
+}
+
+/*
+ * Outbound data handler of the SCTP transport.
+ * `buffer` will be a fake `mbuf` structure.
+ *
+ * Warning: `buffer` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_sctp_transport_outbound_handler(
+    struct mbuf* const buffer,  // not checked
+    uint8_t const tos,
+    uint8_t const set_df,
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    enum rawrtc_code error;
+
+    // TODO: Handle
+    (void) tos;
+    (void) set_df;
+
+    // Note: We only need to copy the buffer if we add it to the outgoing queue
+    if (dtls_transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        // Send
+        error = rawrtc_dtls_transport_send(dtls_transport, buffer);
+    } else {
+        int err;
+        struct mbuf* copied_buffer;
+
+        // Get length
+        size_t const length = mbuf_get_left(buffer);
+
+        // Allocate
+        copied_buffer = mbuf_alloc(length);
+        if (!copied_buffer) {
+            DEBUG_WARNING("Could not create buffer for outgoing packet, no memory\n");
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+
+        // Copy and set position
+        err = mbuf_write_mem(copied_buffer, mbuf_buf(buffer), length);
+        if (err) {
+            DEBUG_WARNING("Could not write to buffer, reason: %m\n", err);
+            mem_deref(copied_buffer);
+            return rawrtc_error_to_code(err);
+        }
+        mbuf_set_pos(copied_buffer, 0);
+
+        // Send (well, actually buffer...)
+        error = rawrtc_dtls_transport_send(dtls_transport, copied_buffer);
+        mem_deref(copied_buffer);
+    }
+
+    // Handle error & done
+    if (error) {
+        DEBUG_WARNING("Could not send packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    return error;
+}
+
+/*
+ * Detach the SCTP transport from the DTLS transport and therefore
+ * don't feed any DTLS application data to the SCTP transport.
+ * Warning: `arg` will not be validated.
+ */
+void rawrtc_sctp_common_sctp_transport_detach_handler(void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Detach from DTLS transport
+    enum rawrtc_code error = rawrtc_dtls_transport_clear_data_transport(dtls_transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to detach from DTLS transport, reason: %s\n", rawrtc_code_to_str(error));
+    }
+}
diff --git a/src/sctp_common/common.h b/src/sctp_common/common.h
new file mode 100644
index 0000000..cf441f7
--- /dev/null
+++ b/src/sctp_common/common.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_sctp_common_dtls_role_getter(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced, not checked
+    void* const arg  // not checked
+);
+
+enum rawrtc_code rawrtc_sctp_common_dtls_transport_state_getter(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced, not checked
+    void* const arg  // not checked
+);
+
+enum rawrtc_code rawrtc_sctp_common_sctp_transport_outbound_handler(
+    struct mbuf* const buffer,  // not checked
+    uint8_t const tos,
+    uint8_t const set_df,
+    void* const arg  // not checked
+);
+
+void rawrtc_sctp_common_sctp_transport_detach_handler(void* const arg  // not checked
+);
diff --git a/src/sctp_common/meson.build b/src/sctp_common/meson.build
new file mode 100644
index 0000000..5ed4f3e
--- /dev/null
+++ b/src/sctp_common/meson.build
@@ -0,0 +1 @@
+sources += files('common.c')
diff --git a/src/sctp_redirect_transport/meson.build b/src/sctp_redirect_transport/meson.build
new file mode 100644
index 0000000..d516026
--- /dev/null
+++ b/src/sctp_redirect_transport/meson.build
@@ -0,0 +1 @@
+sources += files('transport.c')
diff --git a/src/sctp_redirect_transport/transport.c b/src/sctp_redirect_transport/transport.c
new file mode 100644
index 0000000..d0d022e
--- /dev/null
+++ b/src/sctp_redirect_transport/transport.c
@@ -0,0 +1,111 @@
+#include "../dtls_transport/transport.h"
+#include "../sctp_common/common.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/sctp_redirect_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "sctp-redirect-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Pass DTLS application data to the SCTP redirect transport as inbound
+ * data.
+ */
+static void sctp_redirect_transport_inbound_handler(
+    struct mbuf* const buffer,  // not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_sctp_redirect_transport* const transport = arg;
+
+    // Feed data
+    enum rawrtc_code const error = rawrtc_sctp_redirect_transport_feed_inbound(transport, buffer);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to feed data into the SCTP redirect transport, reason: %s\n",
+            rawrtc_code_to_str(error));
+    }
+}
+
+/*
+ * Destructor for an existing SCTP redirect transport.
+ */
+static void rawrtc_sctp_redirect_transport_destroy(void* const arg) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Un-reference
+    mem_deref(dtls_transport);
+}
+
+/*
+ * Create an SCTP redirect transport.
+ * `*transportp` must be unreferenced.
+ *
+ * `port` defaults to `5000` if set to `0`.
+ * `redirect_ip` is the target IP SCTP packets will be redirected to
+ *  and must be a IPv4 address.
+ * `redirect_port` is the target SCTP port packets will be redirected
+ *  to.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_create(
+    struct rawrtc_sctp_redirect_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    char* const redirect_ip,  // copied
+    uint16_t const redirect_port,
+    rawrtc_sctp_redirect_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+    struct rawrtc_sctp_redirect_transport* transport = NULL;
+
+    // Create SCTP transport context
+    struct rawrtc_sctp_transport_context context = {
+        .role_getter = NULL,
+        .state_getter = rawrtc_sctp_common_dtls_transport_state_getter,
+        .outbound_handler = rawrtc_sctp_common_sctp_transport_outbound_handler,
+        .detach_handler = rawrtc_sctp_common_sctp_transport_detach_handler,
+        .destroyed_handler = rawrtc_sctp_redirect_transport_destroy,
+        .arg = mem_ref(dtls_transport),
+    };
+
+    // Check if a data transport is already registered
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, dtls_transport);
+    if (error) {
+        goto out;
+    }
+    if (have_data_transport) {
+        error = RAWRTC_CODE_INVALID_ARGUMENT;
+        goto out;
+    }
+
+    // Create SCTP redirect transport
+    error = rawrtc_sctp_redirect_transport_create_from_external(
+        &transport, &context, port, redirect_ip, redirect_port, state_change_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+    // Attach to DTLS transport
+    DEBUG_PRINTF("Attaching as data transport\n");
+    error = rawrtc_dtls_transport_set_data_transport(
+        dtls_transport, sctp_redirect_transport_inbound_handler, transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+        mem_deref(dtls_transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
diff --git a/src/sctp_transport/meson.build b/src/sctp_transport/meson.build
new file mode 100644
index 0000000..d516026
--- /dev/null
+++ b/src/sctp_transport/meson.build
@@ -0,0 +1 @@
+sources += files('transport.c')
diff --git a/src/sctp_transport/transport.c b/src/sctp_transport/transport.c
new file mode 100644
index 0000000..c329d63
--- /dev/null
+++ b/src/sctp_transport/transport.c
@@ -0,0 +1,108 @@
+#include "../dtls_transport/transport.h"
+#include "../sctp_common/common.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/sctp_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "sctp-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Pass DTLS application data to the SCTP transport as inbound data.
+ */
+static void sctp_transport_inbound_handler(
+    struct mbuf* const buffer,  // not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_sctp_transport* const transport = arg;
+
+    // Feed data
+    // TODO: What about ECN bits?
+    enum rawrtc_code const error = rawrtc_sctp_transport_feed_inbound(transport, buffer, 0x00);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to feed data into the SCTP transport, reason: %s\n", rawrtc_code_to_str(error));
+    }
+}
+
+/*
+ * Destructor for an existing SCTP transport.
+ */
+static void rawrtc_sctp_transport_destroy(void* const arg) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Un-reference
+    mem_deref(dtls_transport);
+}
+
+/*
+ * Create an SCTP transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_create(
+    struct rawrtc_sctp_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    rawrtc_sctp_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+    struct rawrtc_sctp_transport* transport = NULL;
+
+    // Create SCTP transport context
+    struct rawrtc_sctp_transport_context context = {
+        .role_getter = rawrtc_sctp_common_dtls_role_getter,
+        .state_getter = rawrtc_sctp_common_dtls_transport_state_getter,
+        .outbound_handler = rawrtc_sctp_common_sctp_transport_outbound_handler,
+        .detach_handler = rawrtc_sctp_common_sctp_transport_detach_handler,
+        .destroyed_handler = rawrtc_sctp_transport_destroy,
+        .trace_packets = false,  // TODO: Make this configurable
+        .arg = mem_ref(dtls_transport),
+    };
+
+    // Check if a data transport is already registered
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, dtls_transport);
+    if (error) {
+        goto out;
+    }
+    if (have_data_transport) {
+        error = RAWRTC_CODE_INVALID_ARGUMENT;
+        goto out;
+    }
+
+    // Create SCTP transport
+    error = rawrtc_sctp_transport_create_from_external(
+        &transport, &context, port, data_channel_handler, state_change_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+    // TODO: Set MTU (1200|1280 (IPv4|IPv6) - UDP - DTLS (cipher suite dependent) - SCTP (12)
+
+    // Attach to DTLS transport
+    DEBUG_PRINTF("Attaching as data transport\n");
+    error = rawrtc_dtls_transport_set_data_transport(
+        dtls_transport, sctp_transport_inbound_handler, transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+        mem_deref(dtls_transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
diff --git a/src/utils/meson.build b/src/utils/meson.build
new file mode 100644
index 0000000..d82c551
--- /dev/null
+++ b/src/utils/meson.build
@@ -0,0 +1 @@
+sources += files('utils.c')
diff --git a/src/utils/utils.c b/src/utils/utils.c
new file mode 100644
index 0000000..26af2a9
--- /dev/null
+++ b/src/utils/utils.c
@@ -0,0 +1,163 @@
+#include "utils.h"
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <stdarg.h>  // va_*
+#include <stdio.h>  // sprintf
+#include <string.h>  // strlen
+
+/*
+ * Convert binary to hex string where each value is separated by a
+ * colon.
+ */
+enum rawrtc_code rawrtc_bin_to_colon_hex(
+    char** const destinationp,  // de-referenced
+    uint8_t* const source,
+    size_t const length) {
+    char* hex_str;
+    char* hex_ptr;
+    size_t i;
+    int ret;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!destinationp || !source) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate hex string
+    hex_str = mem_zalloc(length > 0 ? (length * 3) : 1, NULL);
+    if (!hex_str) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Bin to hex
+    hex_ptr = hex_str;
+    for (i = 0; i < length; ++i) {
+        if (i > 0) {
+            *hex_ptr = ':';
+            ++hex_ptr;
+        }
+        ret = sprintf(hex_ptr, "%02X", source[i]);
+        if (ret != 2) {
+            error = RAWRTC_CODE_UNKNOWN_ERROR;
+            goto out;
+        } else {
+            hex_ptr += ret;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(hex_str);
+    } else {
+        // Set pointer
+        *destinationp = hex_str;
+    }
+    return error;
+}
+
+/*
+ * Convert hex string with colon-separated hex values to binary.
+ */
+enum rawrtc_code rawrtc_colon_hex_to_bin(
+    size_t* const bytes_written,  // de-referenced
+    uint8_t* const buffer,  // written into
+    size_t const buffer_size,
+    char* source) {
+    size_t hex_length;
+    size_t bin_length;
+    size_t i;
+
+    // Check arguments
+    if (!bytes_written || !buffer || !source) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate length
+    hex_length = strlen(source);
+    if (hex_length > 0 && hex_length % 3 != 2) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Determine size
+    bin_length = hex_length > 0 ? (size_t)((hex_length + 1) / 3) : 0;
+    if (bin_length > buffer_size) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Hex to bin
+    for (i = 0; i < bin_length; ++i) {
+        if (i > 0) {
+            // Skip colon
+            ++source;
+        }
+        buffer[i] = ch_hex(*source) << 4;
+        ++source;
+        buffer[i] += ch_hex(*source);
+        ++source;
+    }
+
+    // Done
+    *bytes_written = bin_length;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Destructor for an existing array container that did reference each
+ * item.
+ */
+static void rawrtc_array_container_destroy(void* arg) {
+    struct rawrtc_array_container* const container = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < container->n_items; ++i) {
+        mem_deref(container->items[i]);
+    }
+}
+
+/*
+ * Convert a list to a dynamically allocated array container.
+ *
+ * If `reference` is set to `true`, each item in the list will be
+ * referenced and a destructor will be added that unreferences each
+ * item when unreferencing the array.
+ */
+enum rawrtc_code rawrtc_list_to_array(
+    struct rawrtc_array_container** containerp,  // de-referenced
+    struct list const* const list,
+    bool reference) {
+    size_t n;
+    struct rawrtc_array_container* container;
+    struct le* le;
+    size_t i;
+
+    // Check arguments
+    if (!containerp || !list) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get list length
+    n = list_count(list);
+
+    // Allocate array & set length immediately
+    container = mem_zalloc(
+        sizeof(*container) + sizeof(void*) * n, reference ? rawrtc_array_container_destroy : NULL);
+    if (!container) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+    container->n_items = n;
+
+    // Copy pointer to each item
+    for (le = list_head(list), i = 0; le != NULL; le = le->next, ++i) {
+        if (reference) {
+            mem_ref(le->data);
+        }
+        container->items[i] = le->data;
+    }
+
+    // Set pointer & done
+    *containerp = container;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/utils/utils.h b/src/utils/utils.h
new file mode 100644
index 0000000..9557277
--- /dev/null
+++ b/src/utils/utils.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_bin_to_colon_hex(
+    char** const destinationp,  // de-referenced
+    uint8_t* const source,
+    size_t const length);
+
+enum rawrtc_code rawrtc_colon_hex_to_bin(
+    size_t* const bytes_written,  // de-referenced
+    uint8_t* const buffer,  // written into
+    size_t const buffer_size,
+    char* source);
+
+enum rawrtc_code rawrtc_list_to_array(
+    struct rawrtc_array_container** containerp,  // de-referenced
+    struct list const* const list,
+    bool reference);
diff --git a/subprojects/rawrtcc.wrap b/subprojects/rawrtcc.wrap
new file mode 100644
index 0000000..fb3e064
--- /dev/null
+++ b/subprojects/rawrtcc.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory = rawrtcc
+url = https://github.com/rawrtc/rawrtc-common.git
+revision = v0.1.2
diff --git a/subprojects/rawrtcdc.wrap b/subprojects/rawrtcdc.wrap
new file mode 100644
index 0000000..f94711b
--- /dev/null
+++ b/subprojects/rawrtcdc.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory = rawrtcdc
+url = https://github.com/rawrtc/rawrtc-data-channel.git
+revision = v0.1.3
diff --git a/subprojects/re.wrap b/subprojects/re.wrap
new file mode 100644
index 0000000..329ac5a
--- /dev/null
+++ b/subprojects/re.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory = re
+url = https://github.com/rawrtc/re.git
+revision = 9384f3a5f38a03c871270fda566045b3bf57bbee
diff --git a/subprojects/rew.wrap b/subprojects/rew.wrap
new file mode 100644
index 0000000..f5b307e
--- /dev/null
+++ b/subprojects/rew.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory = rew
+url = https://github.com/rawrtc/rew.git
+revision = 92e5e6190281fd1003ea629a2baa394e0673b2c0
diff --git a/subprojects/usrsctp.wrap b/subprojects/usrsctp.wrap
new file mode 100644
index 0000000..340062f
--- /dev/null
+++ b/subprojects/usrsctp.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory = usrsctp
+url = https://github.com/sctplab/usrsctp.git
+revision = master
diff --git a/tools/data-channel-sctp-echo.c b/tools/data-channel-sctp-echo.c
new file mode 100644
index 0000000..8dd10f0
--- /dev/null
+++ b/tools/data-channel-sctp-echo.c
@@ -0,0 +1,419 @@
+#include "helper/handler.h"
+#include "helper/parameters.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // memcpy
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "data-channel-sctp-echo-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+};
+
+struct parameters {
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_ice_candidates* ice_candidates;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct sctp_parameters sctp_parameters;
+};
+
+// Note: Shadows struct client
+struct data_channel_sctp_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_transport* sctp_transport;
+    struct rawrtc_data_transport* data_transport;
+    struct list data_channels;
+    struct parameters local_parameters;
+    struct parameters remote_parameters;
+};
+
+static void print_local_parameters(struct data_channel_sctp_client* client);
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Print local parameters (if last candidate)
+    if (!candidate) {
+        print_local_parameters(client);
+    }
+}
+
+/*
+ * Print the data channel's received message's size and echo the
+ * message back.
+ */
+static void data_channel_message_handler(
+    struct mbuf* const buffer,
+    enum rawrtc_data_channel_message_flag const flags,
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_client* const client =
+        (struct data_channel_sctp_client*) channel->client;
+    enum rawrtc_code error;
+    (void) flags;
+
+    // Print message size
+    default_data_channel_message_handler(buffer, flags, arg);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(
+        channel->channel, buffer,
+        flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_BINARY ? true : false);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+}
+
+/*
+ * Handle the newly created data channel.
+ */
+static void data_channel_handler(
+    struct rawrtc_data_channel* const channel,  // read-only, MUST be referenced when used
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct data_channel_sctp_client* const client = arg;
+    struct data_channel_helper* channel_helper;
+
+    // Print channel
+    default_data_channel_handler(channel, arg);
+
+    // Create data channel helper instance & add to list
+    // Note: In this case we need to reference the channel because we have not created it
+    data_channel_helper_create_from_channel(&channel_helper, mem_ref(channel), arg, NULL);
+    list_append(&client->data_channels, &channel_helper->le, channel_helper);
+
+    // Set handler argument & handlers
+    EOE(rawrtc_data_channel_set_arg(channel, channel_helper));
+    EOE(rawrtc_data_channel_set_open_handler(channel, default_data_channel_open_handler));
+    EOE(rawrtc_data_channel_set_buffered_amount_low_handler(
+        channel, default_data_channel_buffered_amount_low_handler));
+    EOE(rawrtc_data_channel_set_error_handler(channel, default_data_channel_error_handler));
+    EOE(rawrtc_data_channel_set_close_handler(channel, default_data_channel_close_handler));
+    EOE(rawrtc_data_channel_set_message_handler(channel, data_channel_message_handler));
+}
+
+static void client_init(struct data_channel_sctp_client* const client) {
+    struct rawrtc_certificate* certificates[1];
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&client->certificate, NULL));
+    certificates[0] = client->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &client->gatherer, client->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, client));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &client->ice_transport, client->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, client));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &client->dtls_transport, client->ice_transport, certificates, ARRAY_SIZE(certificates),
+        default_dtls_transport_state_change_handler, default_dtls_transport_error_handler, client));
+
+    // Create SCTP transport
+    EOE(rawrtc_sctp_transport_create(
+        &client->sctp_transport, client->dtls_transport,
+        client->local_parameters.sctp_parameters.port, data_channel_handler,
+        default_sctp_transport_state_change_handler, client));
+    EOE(rawrtc_sctp_transport_set_buffer_length(
+        client->sctp_transport, TRANSPORT_BUFFER_LENGTH, TRANSPORT_BUFFER_LENGTH));
+
+    // Get data transport
+    EOE(rawrtc_sctp_transport_get_data_transport(&client->data_transport, client->sctp_transport));
+}
+
+static void client_start_gathering(struct data_channel_sctp_client* const client) {
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(client->gatherer, NULL));
+}
+
+static void client_start_transports(struct data_channel_sctp_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        client->ice_transport, client->gatherer, remote_parameters->ice_parameters, client->role));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(client->dtls_transport, remote_parameters->dtls_parameters));
+
+    // Start SCTP transport
+    EOE(rawrtc_sctp_transport_start(
+        client->sctp_transport, remote_parameters->sctp_parameters.capabilities,
+        remote_parameters->sctp_parameters.port));
+}
+
+static void parameters_destroy(struct parameters* const parameters) {
+    // Un-reference
+    parameters->ice_parameters = mem_deref(parameters->ice_parameters);
+    parameters->ice_candidates = mem_deref(parameters->ice_candidates);
+    parameters->dtls_parameters = mem_deref(parameters->dtls_parameters);
+    if (parameters->sctp_parameters.capabilities) {
+        parameters->sctp_parameters.capabilities =
+            mem_deref(parameters->sctp_parameters.capabilities);
+    }
+}
+
+static void client_stop(struct data_channel_sctp_client* const client) {
+    // Clear data channels
+    list_flush(&client->data_channels);
+
+    // Stop all transports & gatherer
+    if (client->sctp_transport) {
+        EOE(rawrtc_sctp_transport_stop(client->sctp_transport));
+    }
+    if (client->dtls_transport) {
+        EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    }
+    if (client->ice_transport) {
+        EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    }
+    if (client->gatherer) {
+        EOE(rawrtc_ice_gatherer_close(client->gatherer));
+    }
+
+    // Un-reference & close
+    parameters_destroy(&client->remote_parameters);
+    parameters_destroy(&client->local_parameters);
+    client->data_transport = mem_deref(client->data_transport);
+    client->sctp_transport = mem_deref(client->sctp_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+    client->gather_options = mem_deref(client->gather_options);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void client_set_parameters(struct data_channel_sctp_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Set remote ICE candidates
+    EOE(rawrtc_ice_transport_set_remote_candidates(
+        client->ice_transport, remote_parameters->ice_candidates->candidates,
+        remote_parameters->ice_candidates->n_candidates));
+}
+
+static void parse_remote_parameters(int flags, void* arg) {
+    struct data_channel_sctp_client* const client = arg;
+    enum rawrtc_code error;
+    struct odict* dict = NULL;
+    struct odict* node = NULL;
+    struct rawrtc_ice_parameters* ice_parameters = NULL;
+    struct rawrtc_ice_candidates* ice_candidates = NULL;
+    struct rawrtc_dtls_parameters* dtls_parameters = NULL;
+    struct sctp_parameters sctp_parameters = {0};
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&node, dict, "iceParameters", ODICT_OBJECT, true);
+    error |= get_ice_parameters(&ice_parameters, node);
+    error |= dict_get_entry(&node, dict, "iceCandidates", ODICT_ARRAY, true);
+    error |= get_ice_candidates(&ice_candidates, node, arg);
+    error |= dict_get_entry(&node, dict, "dtlsParameters", ODICT_OBJECT, true);
+    error |= get_dtls_parameters(&dtls_parameters, node);
+    error |= dict_get_entry(&node, dict, "sctpParameters", ODICT_OBJECT, true);
+    error |= get_sctp_parameters(&sctp_parameters, node);
+
+    // Ok?
+    if (error) {
+        DEBUG_WARNING("Invalid remote parameters\n");
+        if (sctp_parameters.capabilities) {
+            mem_deref(sctp_parameters.capabilities);
+        }
+        goto out;
+    }
+
+    // Set parameters & start transports
+    client->remote_parameters.ice_parameters = mem_ref(ice_parameters);
+    client->remote_parameters.ice_candidates = mem_ref(ice_candidates);
+    client->remote_parameters.dtls_parameters = mem_ref(dtls_parameters);
+    memcpy(&client->remote_parameters.sctp_parameters, &sctp_parameters, sizeof(sctp_parameters));
+    DEBUG_INFO("Applying remote parameters\n");
+    client_set_parameters(client);
+    client_start_transports(client);
+
+out:
+    // Un-reference
+    mem_deref(dtls_parameters);
+    mem_deref(ice_candidates);
+    mem_deref(ice_parameters);
+    mem_deref(dict);
+
+    // Exit?
+    if (error == RAWRTC_CODE_NO_VALUE) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        client_stop(client);
+        re_cancel();
+    }
+}
+
+static void client_get_parameters(struct data_channel_sctp_client* const client) {
+    struct parameters* const local_parameters = &client->local_parameters;
+
+    // Get local ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(
+        &local_parameters->ice_parameters, client->gatherer));
+
+    // Get local ICE candidates
+    EOE(rawrtc_ice_gatherer_get_local_candidates(
+        &local_parameters->ice_candidates, client->gatherer));
+
+    // Get local DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local_parameters->dtls_parameters, client->dtls_transport));
+
+    // Get local SCTP parameters
+    EOE(rawrtc_sctp_transport_get_capabilities(&local_parameters->sctp_parameters.capabilities));
+    EOE(rawrtc_sctp_transport_get_port(
+        &local_parameters->sctp_parameters.port, client->sctp_transport));
+}
+
+static void print_local_parameters(struct data_channel_sctp_client* client) {
+    struct odict* dict;
+    struct odict* node;
+
+    // Get local parameters
+    client_get_parameters(client);
+
+    // Create dict
+    EOR(odict_alloc(&dict, 16));
+
+    // Create nodes
+    EOR(odict_alloc(&node, 16));
+    set_ice_parameters(client->local_parameters.ice_parameters, node);
+    EOR(odict_entry_add(dict, "iceParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_ice_candidates(client->local_parameters.ice_candidates, node);
+    EOR(odict_entry_add(dict, "iceCandidates", ODICT_ARRAY, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_dtls_parameters(client->local_parameters.dtls_parameters, node);
+    EOR(odict_entry_add(dict, "dtlsParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_sctp_parameters(client->sctp_transport, &client->local_parameters.sctp_parameters, node);
+    EOR(odict_entry_add(dict, "sctpParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+
+    // Print JSON
+    DEBUG_INFO("Local Parameters:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING("Usage: %s <0|1 (ice-role)> [<sctp-port>] [<ice-candidate-type> ...]", program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct data_channel_sctp_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 2) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get ICE role
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get SCTP port (optional)
+    if (argc >= 3 && !str_to_uint16(&client.local_parameters.sctp_parameters.port, argv[2])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 4) {
+        ice_candidate_types = &argv[3];
+        n_ice_candidate_types = (size_t) argc - 3;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.gather_options = gather_options;
+    client.role = role;
+    list_init(&client.data_channels);
+
+    // Setup client
+    client_init(&client);
+
+    // Start gathering
+    client_start_gathering(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_parameters, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}
diff --git a/tools/data-channel-sctp-loopback.c b/tools/data-channel-sctp-loopback.c
new file mode 100644
index 0000000..0426209
--- /dev/null
+++ b/tools/data-channel-sctp-loopback.c
@@ -0,0 +1,348 @@
+#include "helper/handler.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "data-channel-sctp-loopback-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+};
+
+// Note: Shadows struct client
+struct data_channel_sctp_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct rawrtc_sctp_capabilities* sctp_capabilities;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    uint16_t sctp_port;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_transport* sctp_transport;
+    struct rawrtc_data_transport* data_transport;
+    struct data_channel_helper* data_channel_negotiated;
+    struct data_channel_helper* data_channel;
+    struct data_channel_sctp_client* other_client;
+};
+
+static struct tmr timer = {0};
+
+static void timer_handler(void* arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_client* const client =
+        (struct data_channel_sctp_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+    enum rawrtc_dtls_role role;
+
+    // Compose message (16 MiB)
+    buffer = mbuf_alloc(1 << 24);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+
+    // Get DTLS role
+    EOE(rawrtc_dtls_parameters_get_role(&role, client->dtls_parameters));
+    if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+        // Close bear-noises
+        DEBUG_PRINTF("(%s) Closing channel\n", client->name, channel->label);
+        EOR(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+}
+
+static void data_channel_open_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_client* const client =
+        (struct data_channel_sctp_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+
+    // Print open event
+    default_data_channel_open_handler(arg);
+
+    // Send data delayed on bear-noises
+    if (str_cmp(channel->label, "bear-noises") == 0) {
+        tmr_start(&timer, 1000, timer_handler, channel);
+        return;
+    }
+
+    // Compose message (256 KiB)
+    buffer = mbuf_alloc(1 << 18);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+}
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Add to other client as remote candidate (if type enabled)
+    add_to_other_if_ice_candidate_type_enabled(arg, candidate, client->other_client->ice_transport);
+}
+
+static void dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_client* const client = arg;
+
+    // Print state
+    default_dtls_transport_state_change_handler(state, arg);
+
+    // Open? Create new data channel
+    // TODO: Move this once we can create data channels earlier
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        enum rawrtc_dtls_role role;
+
+        // Renew DTLS parameters
+        mem_deref(client->dtls_parameters);
+        EOE(rawrtc_dtls_transport_get_local_parameters(
+            &client->dtls_parameters, client->dtls_transport));
+
+        // Get DTLS role
+        EOE(rawrtc_dtls_parameters_get_role(&role, client->dtls_parameters));
+        DEBUG_PRINTF("(%s) DTLS role: %s\n", client->name, rawrtc_dtls_role_to_str(role));
+
+        // Client? Create data channel
+        if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+            struct rawrtc_data_channel_parameters* channel_parameters;
+
+            // Create data channel helper
+            data_channel_helper_create(
+                &client->data_channel, (struct client*) client, "bear-noises");
+
+            // Create data channel parameters
+            EOE(rawrtc_data_channel_parameters_create(
+                &channel_parameters, client->data_channel->label,
+                RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0, NULL, false, 0));
+
+            // Create data channel
+            EOE(rawrtc_data_channel_create(
+                &client->data_channel->channel, client->data_transport, channel_parameters,
+                data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+                default_data_channel_error_handler, default_data_channel_close_handler,
+                default_data_channel_message_handler, client->data_channel));
+
+            // Un-reference
+            mem_deref(channel_parameters);
+        }
+    }
+}
+
+static void client_init(struct data_channel_sctp_client* const local) {
+    struct rawrtc_certificate* certificates[1];
+    struct rawrtc_data_channel_parameters* channel_parameters;
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&local->certificate, NULL));
+    certificates[0] = local->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &local->gatherer, local->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, local));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &local->ice_transport, local->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, local));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &local->dtls_transport, local->ice_transport, certificates, ARRAY_SIZE(certificates),
+        dtls_transport_state_change_handler, default_dtls_transport_error_handler, local));
+
+    // Create SCTP transport
+    EOE(rawrtc_sctp_transport_create(
+        &local->sctp_transport, local->dtls_transport, local->sctp_port,
+        default_data_channel_handler, default_sctp_transport_state_change_handler, local));
+    EOE(rawrtc_sctp_transport_set_buffer_length(
+        local->sctp_transport, TRANSPORT_BUFFER_LENGTH, TRANSPORT_BUFFER_LENGTH));
+
+    // Get SCTP capabilities
+    EOE(rawrtc_sctp_transport_get_capabilities(&local->sctp_capabilities));
+
+    // Get data transport
+    EOE(rawrtc_sctp_transport_get_data_transport(&local->data_transport, local->sctp_transport));
+
+    // Create data channel helper
+    data_channel_helper_create(
+        &local->data_channel_negotiated, (struct client*) local, "cat-noises");
+
+    // Create data channel parameters
+    EOE(rawrtc_data_channel_parameters_create(
+        &channel_parameters, local->data_channel_negotiated->label,
+        RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED, 0, NULL, true, 0));
+
+    // Create pre-negotiated data channel
+    EOE(rawrtc_data_channel_create(
+        &local->data_channel_negotiated->channel, local->data_transport, channel_parameters,
+        data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+        default_data_channel_error_handler, default_data_channel_close_handler,
+        default_data_channel_message_handler, local->data_channel_negotiated));
+
+    // Un-reference
+    mem_deref(channel_parameters);
+}
+
+static void client_start(
+    struct data_channel_sctp_client* const local, struct data_channel_sctp_client* const remote) {
+    // Get & set ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(&local->ice_parameters, remote->gatherer));
+
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(local->gatherer, NULL));
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        local->ice_transport, local->gatherer, local->ice_parameters, local->role));
+
+    // Get DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &remote->dtls_parameters, remote->dtls_transport));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(local->dtls_transport, remote->dtls_parameters));
+
+    // Start SCTP transport
+    EOE(rawrtc_sctp_transport_start(
+        local->sctp_transport, remote->sctp_capabilities, remote->sctp_port));
+}
+
+static void client_stop(struct data_channel_sctp_client* const client) {
+    // Stop transports & close gatherer
+    if (client->data_channel) {
+        EOE(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+    EOE(rawrtc_data_channel_close(client->data_channel_negotiated->channel));
+    EOE(rawrtc_sctp_transport_stop(client->sctp_transport));
+    EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    EOE(rawrtc_ice_gatherer_close(client->gatherer));
+
+    // Un-reference & close
+    client->data_channel = mem_deref(client->data_channel);
+    client->data_channel_negotiated = mem_deref(client->data_channel_negotiated);
+    client->sctp_capabilities = mem_deref(client->sctp_capabilities);
+    client->dtls_parameters = mem_deref(client->dtls_parameters);
+    client->ice_parameters = mem_deref(client->ice_parameters);
+    client->data_transport = mem_deref(client->data_transport);
+    client->sctp_transport = mem_deref(client->sctp_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct data_channel_sctp_client a = {0};
+    struct data_channel_sctp_client b = {0};
+    (void) a.ice_candidate_types;
+    (void) a.n_ice_candidate_types;
+    (void) b.ice_candidate_types;
+    (void) b.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc > 1) {
+        ice_candidate_types = &argv[1];
+        n_ice_candidate_types = (size_t) argc - 1;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Setup client A
+    a.name = "A";
+    a.ice_candidate_types = ice_candidate_types;
+    a.n_ice_candidate_types = n_ice_candidate_types;
+    a.gather_options = gather_options;
+    a.role = RAWRTC_ICE_ROLE_CONTROLLING;
+    a.sctp_port = 6000;
+    a.other_client = &b;
+
+    // Setup client B
+    b.name = "B";
+    b.ice_candidate_types = ice_candidate_types;
+    b.n_ice_candidate_types = n_ice_candidate_types;
+    b.gather_options = gather_options;
+    b.role = RAWRTC_ICE_ROLE_CONTROLLED;
+    b.sctp_port = 5000;
+    b.other_client = &a;
+
+    // Initialise clients
+    client_init(&a);
+    client_init(&b);
+
+    // Start clients
+    client_start(&a, &b);
+    client_start(&b, &a);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, stop_on_return_handler, NULL));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop clients
+    client_stop(&a);
+    client_stop(&b);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+
+    // Free
+    mem_deref(gather_options);
+
+    // Bye
+    before_exit();
+    return 0;
+}
diff --git a/tools/data-channel-sctp-streamed.c b/tools/data-channel-sctp-streamed.c
new file mode 100644
index 0000000..c998238
--- /dev/null
+++ b/tools/data-channel-sctp-streamed.c
@@ -0,0 +1,545 @@
+#include "helper/handler.h"
+#include "helper/parameters.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // memcpy
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "data-channel-sctp-streamed-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+    DEFAULT_MESSAGE_LENGTH = 1073741823,  // 1 GiB
+};
+
+struct parameters {
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_ice_candidates* ice_candidates;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct sctp_parameters sctp_parameters;
+};
+
+// Note: Shadows struct client
+struct data_channel_sctp_streamed_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_transport* sctp_transport;
+    struct rawrtc_data_transport* data_transport;
+    struct data_channel_helper* data_channel_negotiated;
+    struct data_channel_helper* data_channel;
+    struct list other_data_channels;
+    struct parameters local_parameters;
+    struct parameters remote_parameters;
+};
+
+static void print_local_parameters(struct data_channel_sctp_streamed_client* client);
+
+static struct tmr timer = {0};
+
+static void timer_handler(void* arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_streamed_client* const client =
+        (struct data_channel_sctp_streamed_client*) channel->client;
+    uint64_t max_message_size = DEFAULT_MESSAGE_LENGTH;  // Default to 1 GiB
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+    enum rawrtc_dtls_role role;
+
+    // Get the remote peer's maximum message size
+    EOE(rawrtc_sctp_capabilities_get_max_message_size(
+        &max_message_size, client->remote_parameters.sctp_parameters.capabilities));
+    if (max_message_size > 0) {
+        max_message_size = min(DEFAULT_MESSAGE_LENGTH, max_message_size);
+    } else {
+        max_message_size = DEFAULT_MESSAGE_LENGTH;
+    }
+
+    // Compose message
+    buffer = mbuf_alloc(max_message_size);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    // TODO: Send streamed once this is implemented
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+
+    // Get DTLS role
+    EOE(rawrtc_dtls_parameters_get_role(&role, client->local_parameters.dtls_parameters));
+    if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+        // Close bear-noises
+        DEBUG_PRINTF("(%s) Closing channel\n", client->name, channel->label);
+        EOR(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+}
+
+static void data_channel_open_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_streamed_client* const client =
+        (struct data_channel_sctp_streamed_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+
+    // Print open event
+    default_data_channel_open_handler(arg);
+
+    // Send data delayed on bear-noises
+    if (str_cmp(channel->label, "bear-noises") == 0) {
+        tmr_start(&timer, 30000, timer_handler, channel);
+        return;
+    }
+
+    // Compose message (8 KiB)
+    buffer = mbuf_alloc(1 << 13);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    // TODO: Send streamed once this is implemented
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+}
+
+static void data_channel_handler(
+    struct rawrtc_data_channel* const channel,  // read-only, MUST be referenced when used
+    void* const arg) {
+    struct data_channel_sctp_streamed_client* const client = arg;
+    struct data_channel_helper* channel_helper;
+
+    // Print data channel event
+    default_data_channel_handler(channel, arg);
+
+    // Create data channel helper instance & add to list
+    // Note: In this case we need to reference the channel because we have not created it
+    data_channel_helper_create_from_channel(&channel_helper, mem_ref(channel), arg, NULL);
+    list_append(&client->other_data_channels, &channel_helper->le, channel_helper);
+
+    // Set handler argument & handlers
+    EOE(rawrtc_data_channel_set_arg(channel, channel_helper));
+    EOE(rawrtc_data_channel_set_open_handler(channel, data_channel_open_handler));
+    EOE(rawrtc_data_channel_set_buffered_amount_low_handler(
+        channel, default_data_channel_buffered_amount_low_handler));
+    EOE(rawrtc_data_channel_set_error_handler(channel, default_data_channel_error_handler));
+    EOE(rawrtc_data_channel_set_close_handler(channel, default_data_channel_close_handler));
+    EOE(rawrtc_data_channel_set_message_handler(channel, default_data_channel_message_handler));
+
+    // Enable streaming mode
+    EOE(rawrtc_data_channel_set_streaming(channel, true));
+}
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_streamed_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Print local parameters (if last candidate)
+    if (!candidate) {
+        print_local_parameters(client);
+    }
+}
+
+static void dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_streamed_client* const client = arg;
+
+    // Print state
+    default_dtls_transport_state_change_handler(state, arg);
+
+    // Open? Create new data channel
+    // TODO: Move this once we can create data channels earlier
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        enum rawrtc_dtls_role role;
+
+        // Renew DTLS parameters
+        mem_deref(client->local_parameters.dtls_parameters);
+        EOE(rawrtc_dtls_transport_get_local_parameters(
+            &client->local_parameters.dtls_parameters, client->dtls_transport));
+
+        // Get DTLS role
+        EOE(rawrtc_dtls_parameters_get_role(&role, client->local_parameters.dtls_parameters));
+        DEBUG_PRINTF("(%s) DTLS role: %s\n", client->name, rawrtc_dtls_role_to_str(role));
+
+        // Client? Create data channel
+        if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+            struct rawrtc_data_channel_parameters* channel_parameters;
+
+            // Create data channel helper
+            data_channel_helper_create(
+                &client->data_channel, (struct client*) client, "bear-noises");
+
+            // Create data channel parameters
+            EOE(rawrtc_data_channel_parameters_create(
+                &channel_parameters, client->data_channel->label,
+                RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0, NULL, false, 0));
+
+            // Create data channel
+            EOE(rawrtc_data_channel_create(
+                &client->data_channel->channel, client->data_transport, channel_parameters,
+                data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+                default_data_channel_error_handler, default_data_channel_close_handler,
+                default_data_channel_message_handler, client->data_channel));
+
+            // Enable streaming mode
+            EOE(rawrtc_data_channel_set_streaming(client->data_channel->channel, true));
+
+            // Un-reference
+            mem_deref(channel_parameters);
+        }
+    }
+}
+
+static void client_init(struct data_channel_sctp_streamed_client* const client) {
+    struct rawrtc_certificate* certificates[1];
+    struct rawrtc_data_channel_parameters* channel_parameters;
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&client->certificate, NULL));
+    certificates[0] = client->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &client->gatherer, client->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, client));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &client->ice_transport, client->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, client));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &client->dtls_transport, client->ice_transport, certificates, ARRAY_SIZE(certificates),
+        dtls_transport_state_change_handler, default_dtls_transport_error_handler, client));
+
+    // Create SCTP transport
+    EOE(rawrtc_sctp_transport_create(
+        &client->sctp_transport, client->dtls_transport,
+        client->local_parameters.sctp_parameters.port, data_channel_handler,
+        default_sctp_transport_state_change_handler, client));
+    EOE(rawrtc_sctp_transport_set_buffer_length(
+        client->sctp_transport, TRANSPORT_BUFFER_LENGTH, TRANSPORT_BUFFER_LENGTH));
+
+    // Get data transport
+    EOE(rawrtc_sctp_transport_get_data_transport(&client->data_transport, client->sctp_transport));
+
+    // Create data channel helper
+    data_channel_helper_create(
+        &client->data_channel_negotiated, (struct client*) client, "cat-noises");
+
+    // Create data channel parameters
+    EOE(rawrtc_data_channel_parameters_create(
+        &channel_parameters, client->data_channel_negotiated->label,
+        RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED, 0, NULL, true, 0));
+
+    // Create pre-negotiated data channel
+    EOE(rawrtc_data_channel_create(
+        &client->data_channel_negotiated->channel, client->data_transport, channel_parameters,
+        data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+        default_data_channel_error_handler, default_data_channel_close_handler,
+        default_data_channel_message_handler, client->data_channel_negotiated));
+
+    // Enable streaming mode
+    EOE(rawrtc_data_channel_set_streaming(client->data_channel_negotiated->channel, true));
+
+    // Un-reference
+    mem_deref(channel_parameters);
+}
+
+static void client_start_gathering(struct data_channel_sctp_streamed_client* const client) {
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(client->gatherer, NULL));
+}
+
+static void client_start_transports(struct data_channel_sctp_streamed_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        client->ice_transport, client->gatherer, remote_parameters->ice_parameters, client->role));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(client->dtls_transport, remote_parameters->dtls_parameters));
+
+    // Start SCTP transport
+    EOE(rawrtc_sctp_transport_start(
+        client->sctp_transport, remote_parameters->sctp_parameters.capabilities,
+        remote_parameters->sctp_parameters.port));
+}
+
+static void parameters_destroy(struct parameters* const parameters) {
+    // Un-reference
+    parameters->ice_parameters = mem_deref(parameters->ice_parameters);
+    parameters->ice_candidates = mem_deref(parameters->ice_candidates);
+    parameters->dtls_parameters = mem_deref(parameters->dtls_parameters);
+    if (parameters->sctp_parameters.capabilities) {
+        parameters->sctp_parameters.capabilities =
+            mem_deref(parameters->sctp_parameters.capabilities);
+    }
+}
+
+static void client_stop(struct data_channel_sctp_streamed_client* const client) {
+    // Clear other data channels
+    list_flush(&client->other_data_channels);
+
+    if (client->sctp_transport) {
+        EOE(rawrtc_sctp_transport_stop(client->sctp_transport));
+    }
+    if (client->dtls_transport) {
+        EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    }
+    if (client->ice_transport) {
+        EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    }
+    if (client->gatherer) {
+        EOE(rawrtc_ice_gatherer_close(client->gatherer));
+    }
+
+    // Un-reference & close
+    parameters_destroy(&client->remote_parameters);
+    parameters_destroy(&client->local_parameters);
+    client->data_channel = mem_deref(client->data_channel);
+    client->data_channel_negotiated = mem_deref(client->data_channel_negotiated);
+    client->data_transport = mem_deref(client->data_transport);
+    client->sctp_transport = mem_deref(client->sctp_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+    client->gather_options = mem_deref(client->gather_options);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void client_set_parameters(struct data_channel_sctp_streamed_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Set remote ICE candidates
+    EOE(rawrtc_ice_transport_set_remote_candidates(
+        client->ice_transport, remote_parameters->ice_candidates->candidates,
+        remote_parameters->ice_candidates->n_candidates));
+}
+
+static void parse_remote_parameters(int flags, void* arg) {
+    struct data_channel_sctp_streamed_client* const client = arg;
+    enum rawrtc_code error;
+    struct odict* dict = NULL;
+    struct odict* node = NULL;
+    struct rawrtc_ice_parameters* ice_parameters = NULL;
+    struct rawrtc_ice_candidates* ice_candidates = NULL;
+    struct rawrtc_dtls_parameters* dtls_parameters = NULL;
+    struct sctp_parameters sctp_parameters = {0};
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&node, dict, "iceParameters", ODICT_OBJECT, true);
+    error |= get_ice_parameters(&ice_parameters, node);
+    error |= dict_get_entry(&node, dict, "iceCandidates", ODICT_ARRAY, true);
+    error |= get_ice_candidates(&ice_candidates, node, arg);
+    error |= dict_get_entry(&node, dict, "dtlsParameters", ODICT_OBJECT, true);
+    error |= get_dtls_parameters(&dtls_parameters, node);
+    error |= dict_get_entry(&node, dict, "sctpParameters", ODICT_OBJECT, true);
+    error |= get_sctp_parameters(&sctp_parameters, node);
+
+    // Ok?
+    if (error) {
+        DEBUG_WARNING("Invalid remote parameters\n");
+        if (sctp_parameters.capabilities) {
+            mem_deref(sctp_parameters.capabilities);
+        }
+        goto out;
+    }
+
+    // Set parameters & start transports
+    client->remote_parameters.ice_parameters = mem_ref(ice_parameters);
+    client->remote_parameters.ice_candidates = mem_ref(ice_candidates);
+    client->remote_parameters.dtls_parameters = mem_ref(dtls_parameters);
+    memcpy(&client->remote_parameters.sctp_parameters, &sctp_parameters, sizeof(sctp_parameters));
+    DEBUG_INFO("Applying remote parameters\n");
+    client_set_parameters(client);
+    client_start_transports(client);
+
+out:
+    // Un-reference
+    mem_deref(dtls_parameters);
+    mem_deref(ice_candidates);
+    mem_deref(ice_parameters);
+    mem_deref(dict);
+
+    // Exit?
+    if (error == RAWRTC_CODE_NO_VALUE) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        client_stop(client);
+        tmr_cancel(&timer);
+        re_cancel();
+    }
+}
+
+static void client_get_parameters(struct data_channel_sctp_streamed_client* const client) {
+    struct parameters* const local_parameters = &client->local_parameters;
+
+    // Get local ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(
+        &local_parameters->ice_parameters, client->gatherer));
+
+    // Get local ICE candidates
+    EOE(rawrtc_ice_gatherer_get_local_candidates(
+        &local_parameters->ice_candidates, client->gatherer));
+
+    // Get local DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local_parameters->dtls_parameters, client->dtls_transport));
+
+    // Get local SCTP parameters
+    EOE(rawrtc_sctp_transport_get_capabilities(&local_parameters->sctp_parameters.capabilities));
+    EOE(rawrtc_sctp_transport_get_port(
+        &local_parameters->sctp_parameters.port, client->sctp_transport));
+}
+
+static void print_local_parameters(struct data_channel_sctp_streamed_client* client) {
+    struct odict* dict;
+    struct odict* node;
+
+    // Get local parameters
+    client_get_parameters(client);
+
+    // Create dict
+    EOR(odict_alloc(&dict, 16));
+
+    // Create nodes
+    EOR(odict_alloc(&node, 16));
+    set_ice_parameters(client->local_parameters.ice_parameters, node);
+    EOR(odict_entry_add(dict, "iceParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_ice_candidates(client->local_parameters.ice_candidates, node);
+    EOR(odict_entry_add(dict, "iceCandidates", ODICT_ARRAY, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_dtls_parameters(client->local_parameters.dtls_parameters, node);
+    EOR(odict_entry_add(dict, "dtlsParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_sctp_parameters(client->sctp_transport, &client->local_parameters.sctp_parameters, node);
+    EOR(odict_entry_add(dict, "sctpParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+
+    // Print JSON
+    DEBUG_INFO("Local Parameters:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING("Usage: %s <0|1 (ice-role)> [<sctp-port>] [<ice-candidate-type> ...]", program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct data_channel_sctp_streamed_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 2) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get ICE role
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get SCTP port (optional)
+    if (argc >= 3 && !str_to_uint16(&client.local_parameters.sctp_parameters.port, argv[2])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 4) {
+        ice_candidate_types = &argv[3];
+        n_ice_candidate_types = (size_t) argc - 3;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.gather_options = gather_options;
+    client.role = role;
+    list_init(&client.other_data_channels);
+
+    // Setup client
+    client_init(&client);
+
+    // Start gathering
+    client_start_gathering(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_parameters, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}
diff --git a/tools/data-channel-sctp-throughput.c b/tools/data-channel-sctp-throughput.c
new file mode 100644
index 0000000..3bd9aea
--- /dev/null
+++ b/tools/data-channel-sctp-throughput.c
@@ -0,0 +1,556 @@
+#include "helper/handler.h"
+#include "helper/parameters.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // memcpy
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "data-channel-sctp-throughput-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+};
+
+struct parameters {
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_ice_candidates* ice_candidates;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct sctp_parameters sctp_parameters;
+};
+
+// Note: Shadows struct client
+struct data_channel_sctp_throughput_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    uint64_t message_size;
+    uint16_t n_times_left;
+    uint32_t buffer_length;
+    enum rawrtc_sctp_transport_congestion_ctrl congestion_ctrl_algorithm;
+    uint32_t mtu;
+    struct rawrtc_ice_gather_options* gather_options;
+    enum rawrtc_ice_role role;
+    struct mbuf* start_buffer;
+    struct mbuf* throughput_buffer;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_transport* sctp_transport;
+    struct rawrtc_data_transport* data_transport;
+    struct data_channel_helper* data_channel;
+    struct parameters local_parameters;
+    struct parameters remote_parameters;
+    uint64_t start_time;
+};
+
+static void print_local_parameters(struct data_channel_sctp_throughput_client* client);
+
+static struct tmr timer = {0};
+
+static void timer_handler(void* arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_throughput_client* const client =
+        (struct data_channel_sctp_throughput_client*) channel->client;
+    enum rawrtc_code error;
+    enum rawrtc_dtls_role role;
+
+    // Send start indicator
+    mbuf_set_pos(client->start_buffer, 0);
+    DEBUG_PRINTF("(%s) Sending start indicator\n", client->name);
+    error = rawrtc_data_channel_send(channel->channel, client->start_buffer, false);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Send message
+    DEBUG_PRINTF(
+        "(%s) Sending %zu bytes\n", client->name, mbuf_get_left(client->throughput_buffer));
+    error = rawrtc_data_channel_send(channel->channel, client->throughput_buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+out:
+    // Get DTLS role
+    EOE(rawrtc_dtls_parameters_get_role(&role, client->local_parameters.dtls_parameters));
+    if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+        // Close bear-noises
+        DEBUG_PRINTF("(%s) Closing channel\n", client->name, channel->label);
+        EOR(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+}
+
+static void data_channel_message_handler(
+    struct mbuf* const buffer, enum rawrtc_data_channel_message_flag const flags, void* const arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_throughput_client* const client =
+        (struct data_channel_sctp_throughput_client*) channel->client;
+    size_t const length = mbuf_get_left(buffer);
+
+    // Check role
+    if (client->role != RAWRTC_ICE_ROLE_CONTROLLED) {
+        DEBUG_WARNING(
+            "(%s) Unexpected message on data channel %s of size %zu\n", client->name,
+            channel->label, length);
+    }
+
+    if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_STRING) {
+        // Start indicator message
+        uint64_t expected_size;
+
+        // Check size
+        if (mbuf_get_left(buffer) < 8) {
+            EOE(RAWRTC_CODE_INVALID_MESSAGE);
+        }
+
+        // Parse message
+        expected_size = sys_ntohll(mbuf_read_u64(buffer));
+        EOE(expected_size > 0 ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_INVALID_MESSAGE);
+        client->start_time = tmr_jiffies();
+        DEBUG_INFO(
+            "(%s) Started throughput test of %.2f MiB\n", client->name,
+            ((double) expected_size) / 1048576);
+        return;
+    } else if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_BINARY) {
+        // Check expected message size and print results
+        double const delta = ((double) (tmr_jiffies() - client->start_time)) / 1000;
+        DEBUG_INFO(
+            "(%s) Completed throughput test after %.2f seconds: %.2f Mbit/s\n", client->name, delta,
+            ((double) length) / 131072 / delta);
+
+        // Check size
+        if (length != client->message_size) {
+            DEBUG_WARNING(
+                "(%s) Expected %zu bytes, received %zu bytes\n", client->name, client->message_size,
+                length);
+            return;
+        }
+    }
+}
+
+static void start_throughput_test(struct data_channel_helper* const channel) {
+    struct data_channel_sctp_throughput_client* const client =
+        (struct data_channel_sctp_throughput_client*) channel->client;
+
+    // Start throughput test delayed (if controlling)
+    if (client->role == RAWRTC_ICE_ROLE_CONTROLLING && client->n_times_left > 0) {
+        size_t length;
+        mbuf_set_pos(client->throughput_buffer, 0);
+        length = mbuf_get_left(client->throughput_buffer);
+        DEBUG_INFO(
+            "Starting throughput test of %.2f MiB in 1 second\n",
+            (double) length / (double) 1048576);
+        tmr_start(&timer, 1000, timer_handler, channel);
+        --client->n_times_left;
+    }
+}
+
+static void data_channel_buffered_amount_low_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+
+    // Print buffered amount low event
+    default_data_channel_buffered_amount_low_handler(arg);
+
+    // Restart throughput test
+    start_throughput_test(channel);
+}
+
+static void data_channel_open_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+
+    // Print open event
+    default_data_channel_open_handler(arg);
+
+    // Start throughput test
+    start_throughput_test(channel);
+}
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_throughput_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Print local parameters (if last candidate)
+    if (!candidate) {
+        print_local_parameters(client);
+    }
+}
+
+static void client_init(struct data_channel_sctp_throughput_client* const client) {
+    struct rawrtc_certificate* certificates[1];
+    struct rawrtc_data_channel_parameters* channel_parameters;
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&client->certificate, NULL));
+    certificates[0] = client->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &client->gatherer, client->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, client));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &client->ice_transport, client->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, client));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &client->dtls_transport, client->ice_transport, certificates, ARRAY_SIZE(certificates),
+        default_dtls_transport_state_change_handler, default_dtls_transport_error_handler, client));
+
+    // Create SCTP transport
+    EOE(rawrtc_sctp_transport_create(
+        &client->sctp_transport, client->dtls_transport,
+        client->local_parameters.sctp_parameters.port, default_data_channel_handler,
+        default_sctp_transport_state_change_handler, client));
+    EOE(rawrtc_sctp_transport_set_buffer_length(
+        client->sctp_transport, client->buffer_length, client->buffer_length));
+    EOE(rawrtc_sctp_transport_set_congestion_ctrl_algorithm(
+        client->sctp_transport, client->congestion_ctrl_algorithm));
+
+    // Get data transport
+    EOE(rawrtc_sctp_transport_get_data_transport(&client->data_transport, client->sctp_transport));
+
+    // Create data channel helper
+    data_channel_helper_create(&client->data_channel, (struct client*) client, "throughput");
+
+    // Create data channel parameters
+    EOE(rawrtc_data_channel_parameters_create(
+        &channel_parameters, client->data_channel->label, RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED,
+        0, NULL, true, 0));
+
+    // Create pre-negotiated data channel
+    EOE(rawrtc_data_channel_create(
+        &client->data_channel->channel, client->data_transport, channel_parameters,
+        data_channel_open_handler, data_channel_buffered_amount_low_handler,
+        default_data_channel_error_handler, default_data_channel_close_handler,
+        data_channel_message_handler, client->data_channel));
+
+    // Un-reference
+    mem_deref(channel_parameters);
+}
+
+static void client_start_gathering(struct data_channel_sctp_throughput_client* const client) {
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(client->gatherer, NULL));
+}
+
+static void client_start_transports(struct data_channel_sctp_throughput_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        client->ice_transport, client->gatherer, remote_parameters->ice_parameters, client->role));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(client->dtls_transport, remote_parameters->dtls_parameters));
+
+    // Start SCTP transport
+    EOE(rawrtc_sctp_transport_start(
+        client->sctp_transport, remote_parameters->sctp_parameters.capabilities,
+        remote_parameters->sctp_parameters.port));
+    if (client->mtu != 0) {
+        EOE(rawrtc_sctp_transport_set_mtu(client->sctp_transport, client->mtu));
+    }
+}
+
+static void parameters_destroy(struct parameters* const parameters) {
+    // Un-reference
+    parameters->ice_parameters = mem_deref(parameters->ice_parameters);
+    parameters->ice_candidates = mem_deref(parameters->ice_candidates);
+    parameters->dtls_parameters = mem_deref(parameters->dtls_parameters);
+    if (parameters->sctp_parameters.capabilities) {
+        parameters->sctp_parameters.capabilities =
+            mem_deref(parameters->sctp_parameters.capabilities);
+    }
+}
+
+static void client_stop(struct data_channel_sctp_throughput_client* const client) {
+    if (client->sctp_transport) {
+        EOE(rawrtc_sctp_transport_stop(client->sctp_transport));
+    }
+    if (client->dtls_transport) {
+        EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    }
+    if (client->ice_transport) {
+        EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    }
+    if (client->gatherer) {
+        EOE(rawrtc_ice_gatherer_close(client->gatherer));
+    }
+
+    // Un-reference & close
+    parameters_destroy(&client->remote_parameters);
+    parameters_destroy(&client->local_parameters);
+    client->data_channel = mem_deref(client->data_channel);
+    client->data_transport = mem_deref(client->data_transport);
+    client->sctp_transport = mem_deref(client->sctp_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+    client->throughput_buffer = mem_deref(client->throughput_buffer);
+    client->start_buffer = mem_deref(client->start_buffer);
+    client->gather_options = mem_deref(client->gather_options);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void client_set_parameters(struct data_channel_sctp_throughput_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Set remote ICE candidates
+    EOE(rawrtc_ice_transport_set_remote_candidates(
+        client->ice_transport, remote_parameters->ice_candidates->candidates,
+        remote_parameters->ice_candidates->n_candidates));
+}
+
+static void parse_remote_parameters(int flags, void* arg) {
+    struct data_channel_sctp_throughput_client* const client = arg;
+    enum rawrtc_code error;
+    struct odict* dict = NULL;
+    struct odict* node = NULL;
+    struct rawrtc_ice_parameters* ice_parameters = NULL;
+    struct rawrtc_ice_candidates* ice_candidates = NULL;
+    struct rawrtc_dtls_parameters* dtls_parameters = NULL;
+    struct sctp_parameters sctp_parameters = {0};
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&node, dict, "iceParameters", ODICT_OBJECT, true);
+    error |= get_ice_parameters(&ice_parameters, node);
+    error |= dict_get_entry(&node, dict, "iceCandidates", ODICT_ARRAY, true);
+    error |= get_ice_candidates(&ice_candidates, node, arg);
+    error |= dict_get_entry(&node, dict, "dtlsParameters", ODICT_OBJECT, true);
+    error |= get_dtls_parameters(&dtls_parameters, node);
+    error |= dict_get_entry(&node, dict, "sctpParameters", ODICT_OBJECT, true);
+    error |= get_sctp_parameters(&sctp_parameters, node);
+
+    // Ok?
+    if (error) {
+        DEBUG_WARNING("Invalid remote parameters\n");
+        if (sctp_parameters.capabilities) {
+            mem_deref(sctp_parameters.capabilities);
+        }
+        goto out;
+    }
+
+    // Set parameters & start transports
+    client->remote_parameters.ice_parameters = mem_ref(ice_parameters);
+    client->remote_parameters.ice_candidates = mem_ref(ice_candidates);
+    client->remote_parameters.dtls_parameters = mem_ref(dtls_parameters);
+    memcpy(&client->remote_parameters.sctp_parameters, &sctp_parameters, sizeof(sctp_parameters));
+    DEBUG_INFO("Applying remote parameters\n");
+    client_set_parameters(client);
+    client_start_transports(client);
+
+out:
+    // Un-reference
+    mem_deref(dtls_parameters);
+    mem_deref(ice_candidates);
+    mem_deref(ice_parameters);
+    mem_deref(dict);
+
+    // Exit?
+    if (error == RAWRTC_CODE_NO_VALUE) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        client_stop(client);
+        tmr_cancel(&timer);
+        re_cancel();
+    }
+}
+
+static void client_get_parameters(struct data_channel_sctp_throughput_client* const client) {
+    struct parameters* const local_parameters = &client->local_parameters;
+
+    // Get local ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(
+        &local_parameters->ice_parameters, client->gatherer));
+
+    // Get local ICE candidates
+    EOE(rawrtc_ice_gatherer_get_local_candidates(
+        &local_parameters->ice_candidates, client->gatherer));
+
+    // Get local DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local_parameters->dtls_parameters, client->dtls_transport));
+
+    // Get local SCTP parameters
+    EOE(rawrtc_sctp_transport_get_capabilities(&local_parameters->sctp_parameters.capabilities));
+    EOE(rawrtc_sctp_transport_get_port(
+        &local_parameters->sctp_parameters.port, client->sctp_transport));
+}
+
+static void print_local_parameters(struct data_channel_sctp_throughput_client* client) {
+    struct odict* dict;
+    struct odict* node;
+
+    // Get local parameters
+    client_get_parameters(client);
+
+    // Create dict
+    EOR(odict_alloc(&dict, 16));
+
+    // Create nodes
+    EOR(odict_alloc(&node, 16));
+    set_ice_parameters(client->local_parameters.ice_parameters, node);
+    EOR(odict_entry_add(dict, "iceParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_ice_candidates(client->local_parameters.ice_candidates, node);
+    EOR(odict_entry_add(dict, "iceCandidates", ODICT_ARRAY, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_dtls_parameters(client->local_parameters.dtls_parameters, node);
+    EOR(odict_entry_add(dict, "dtlsParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_sctp_parameters(client->sctp_transport, &client->local_parameters.sctp_parameters, node);
+    EOR(odict_entry_add(dict, "sctpParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+
+    // Print JSON
+    DEBUG_INFO("Local Parameters:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING(
+        "Usage: %s <0|1 (ice-role)> <message-size> [<n-times>] [<sctp-port>] "
+        "[<buffer-length>] [<cc-algorithm>] [<mtu>] [<ice-candidate-type> ...]\n",
+        program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    struct rawrtc_ice_gather_options* gather_options;
+    struct data_channel_sctp_throughput_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 3) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get ICE role
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get message size
+    if (!str_to_uint64(&client.message_size, argv[2])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get number of times the test should run (optional)
+    client.n_times_left = 1;
+    if (argc >= 4 && !str_to_uint16(&client.n_times_left, argv[3])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // TODO: Add possibility to turn checksum generation/validation on or off
+
+    // Get SCTP port (optional)
+    if (argc >= 5 && !str_to_uint16(&client.local_parameters.sctp_parameters.port, argv[4])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get send/receiver buffer length (optional)
+    client.buffer_length = TRANSPORT_BUFFER_LENGTH;
+    if (argc >= 6 && !str_to_uint32(&client.buffer_length, argv[5])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get congestion control algorithm (optional)
+    client.congestion_ctrl_algorithm = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581;
+    if (argc >= 7 && get_congestion_control_algorithm(&client.congestion_ctrl_algorithm, argv[6])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get MTU (optional)
+    if (argc >= 8 && !str_to_uint32(&client.mtu, argv[7])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 9) {
+        ice_candidate_types = &argv[8];
+        n_ice_candidate_types = (size_t) argc - 8;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.gather_options = gather_options;
+    client.role = role;
+
+    // Pre-generate messages (if 'controlling')
+    if (role == RAWRTC_ICE_ROLE_CONTROLLING) {
+        // Start indicator
+        client.start_buffer = mbuf_alloc(8);
+        EOE(client.start_buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+        EOR(mbuf_write_u64(client.start_buffer, sys_htonll(client.message_size)));
+
+        // Throughput test buffer
+        client.throughput_buffer = mbuf_alloc(client.message_size);
+        EOE(client.throughput_buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+        EOR(mbuf_fill(client.throughput_buffer, 0x01, mbuf_get_space(client.throughput_buffer)));
+    }
+
+    // Setup client
+    client_init(&client);
+
+    // Start gathering
+    client_start_gathering(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_parameters, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}
diff --git a/tools/data-channel-sctp.c b/tools/data-channel-sctp.c
new file mode 100644
index 0000000..7de1cd2
--- /dev/null
+++ b/tools/data-channel-sctp.c
@@ -0,0 +1,505 @@
+#include "helper/handler.h"
+#include "helper/parameters.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // memcpy
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "data-channel-sctp-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+    DEFAULT_MESSAGE_LENGTH = 1073741823,  // 1 GiB
+};
+
+struct parameters {
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_ice_candidates* ice_candidates;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct sctp_parameters sctp_parameters;
+};
+
+// Note: Shadows struct client
+struct data_channel_sctp_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_transport* sctp_transport;
+    struct rawrtc_data_transport* data_transport;
+    struct data_channel_helper* data_channel_negotiated;
+    struct data_channel_helper* data_channel;
+    struct parameters local_parameters;
+    struct parameters remote_parameters;
+};
+
+static void print_local_parameters(struct data_channel_sctp_client* client);
+
+static struct tmr timer = {0};
+
+static void timer_handler(void* arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_client* const client =
+        (struct data_channel_sctp_client*) channel->client;
+    uint64_t max_message_size = DEFAULT_MESSAGE_LENGTH;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+    enum rawrtc_dtls_role role;
+
+    // Get the remote peer's maximum message size
+    EOE(rawrtc_sctp_capabilities_get_max_message_size(
+        &max_message_size, client->remote_parameters.sctp_parameters.capabilities));
+    if (max_message_size > 0) {
+        max_message_size = min(DEFAULT_MESSAGE_LENGTH, max_message_size);
+    } else {
+        max_message_size = DEFAULT_MESSAGE_LENGTH;
+    }
+
+    // Compose message
+    buffer = mbuf_alloc(max_message_size);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+
+    // Get DTLS role
+    EOE(rawrtc_dtls_parameters_get_role(&role, client->local_parameters.dtls_parameters));
+    if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+        // Close bear-noises
+        DEBUG_PRINTF("(%s) Closing channel\n", client->name, channel->label);
+        EOR(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+}
+
+static void data_channel_open_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+    struct data_channel_sctp_client* const client =
+        (struct data_channel_sctp_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+
+    // Print open event
+    default_data_channel_open_handler(arg);
+
+    // Send data delayed on bear-noises
+    if (str_cmp(channel->label, "bear-noises") == 0) {
+        tmr_start(&timer, 30000, timer_handler, channel);
+        return;
+    }
+
+    // Compose message (8 KiB)
+    buffer = mbuf_alloc(1 << 13);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+}
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Print local parameters (if last candidate)
+    if (!candidate) {
+        print_local_parameters(client);
+    }
+}
+
+static void dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state,  // read-only
+    void* const arg) {
+    struct data_channel_sctp_client* const client = arg;
+
+    // Print state
+    default_dtls_transport_state_change_handler(state, arg);
+
+    // Open? Create new data channel
+    // TODO: Move this once we can create data channels earlier
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        enum rawrtc_dtls_role role;
+
+        // Renew DTLS parameters
+        mem_deref(client->local_parameters.dtls_parameters);
+        EOE(rawrtc_dtls_transport_get_local_parameters(
+            &client->local_parameters.dtls_parameters, client->dtls_transport));
+
+        // Get DTLS role
+        EOE(rawrtc_dtls_parameters_get_role(&role, client->local_parameters.dtls_parameters));
+        DEBUG_PRINTF("(%s) DTLS role: %s\n", client->name, rawrtc_dtls_role_to_str(role));
+
+        // Client? Create data channel
+        if (role == RAWRTC_DTLS_ROLE_CLIENT) {
+            struct rawrtc_data_channel_parameters* channel_parameters;
+
+            // Create data channel helper
+            data_channel_helper_create(
+                &client->data_channel, (struct client*) client, "bear-noises");
+
+            // Create data channel parameters
+            EOE(rawrtc_data_channel_parameters_create(
+                &channel_parameters, client->data_channel->label,
+                RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0, NULL, false, 0));
+
+            // Create data channel
+            EOE(rawrtc_data_channel_create(
+                &client->data_channel->channel, client->data_transport, channel_parameters,
+                data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+                default_data_channel_error_handler, default_data_channel_close_handler,
+                default_data_channel_message_handler, client->data_channel));
+
+            // Un-reference
+            mem_deref(channel_parameters);
+        }
+    }
+}
+
+static void client_init(struct data_channel_sctp_client* const client) {
+    struct rawrtc_certificate* certificates[1];
+    struct rawrtc_data_channel_parameters* channel_parameters;
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&client->certificate, NULL));
+    certificates[0] = client->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &client->gatherer, client->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, client));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &client->ice_transport, client->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, client));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &client->dtls_transport, client->ice_transport, certificates, ARRAY_SIZE(certificates),
+        dtls_transport_state_change_handler, default_dtls_transport_error_handler, client));
+
+    // Create SCTP transport
+    EOE(rawrtc_sctp_transport_create(
+        &client->sctp_transport, client->dtls_transport,
+        client->local_parameters.sctp_parameters.port, default_data_channel_handler,
+        default_sctp_transport_state_change_handler, client));
+    EOE(rawrtc_sctp_transport_set_buffer_length(
+        client->sctp_transport, TRANSPORT_BUFFER_LENGTH, TRANSPORT_BUFFER_LENGTH));
+
+    // Get data transport
+    EOE(rawrtc_sctp_transport_get_data_transport(&client->data_transport, client->sctp_transport));
+
+    // Create data channel helper
+    data_channel_helper_create(
+        &client->data_channel_negotiated, (struct client*) client, "cat-noises");
+
+    // Create data channel parameters
+    EOE(rawrtc_data_channel_parameters_create(
+        &channel_parameters, client->data_channel_negotiated->label,
+        RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED, 0, NULL, true, 0));
+
+    // Create pre-negotiated data channel
+    EOE(rawrtc_data_channel_create(
+        &client->data_channel_negotiated->channel, client->data_transport, channel_parameters,
+        data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+        default_data_channel_error_handler, default_data_channel_close_handler,
+        default_data_channel_message_handler, client->data_channel_negotiated));
+
+    // Un-reference
+    mem_deref(channel_parameters);
+}
+
+static void client_start_gathering(struct data_channel_sctp_client* const client) {
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(client->gatherer, NULL));
+}
+
+static void client_start_transports(struct data_channel_sctp_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        client->ice_transport, client->gatherer, remote_parameters->ice_parameters, client->role));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(client->dtls_transport, remote_parameters->dtls_parameters));
+
+    // Start SCTP transport
+    EOE(rawrtc_sctp_transport_start(
+        client->sctp_transport, remote_parameters->sctp_parameters.capabilities,
+        remote_parameters->sctp_parameters.port));
+}
+
+static void parameters_destroy(struct parameters* const parameters) {
+    // Un-reference
+    parameters->ice_parameters = mem_deref(parameters->ice_parameters);
+    parameters->ice_candidates = mem_deref(parameters->ice_candidates);
+    parameters->dtls_parameters = mem_deref(parameters->dtls_parameters);
+    if (parameters->sctp_parameters.capabilities) {
+        parameters->sctp_parameters.capabilities =
+            mem_deref(parameters->sctp_parameters.capabilities);
+    }
+}
+
+static void client_stop(struct data_channel_sctp_client* const client) {
+    if (client->sctp_transport) {
+        EOE(rawrtc_sctp_transport_stop(client->sctp_transport));
+    }
+    if (client->dtls_transport) {
+        EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    }
+    if (client->ice_transport) {
+        EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    }
+    if (client->gatherer) {
+        EOE(rawrtc_ice_gatherer_close(client->gatherer));
+    }
+
+    // Un-reference & close
+    parameters_destroy(&client->remote_parameters);
+    parameters_destroy(&client->local_parameters);
+    client->data_channel = mem_deref(client->data_channel);
+    client->data_channel_negotiated = mem_deref(client->data_channel_negotiated);
+    client->data_transport = mem_deref(client->data_transport);
+    client->sctp_transport = mem_deref(client->sctp_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+    client->gather_options = mem_deref(client->gather_options);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void client_set_parameters(struct data_channel_sctp_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Set remote ICE candidates
+    EOE(rawrtc_ice_transport_set_remote_candidates(
+        client->ice_transport, remote_parameters->ice_candidates->candidates,
+        remote_parameters->ice_candidates->n_candidates));
+}
+
+static void parse_remote_parameters(int flags, void* arg) {
+    struct data_channel_sctp_client* const client = arg;
+    enum rawrtc_code error;
+    struct odict* dict = NULL;
+    struct odict* node = NULL;
+    struct rawrtc_ice_parameters* ice_parameters = NULL;
+    struct rawrtc_ice_candidates* ice_candidates = NULL;
+    struct rawrtc_dtls_parameters* dtls_parameters = NULL;
+    struct sctp_parameters sctp_parameters = {0};
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&node, dict, "iceParameters", ODICT_OBJECT, true);
+    error |= get_ice_parameters(&ice_parameters, node);
+    error |= dict_get_entry(&node, dict, "iceCandidates", ODICT_ARRAY, true);
+    error |= get_ice_candidates(&ice_candidates, node, arg);
+    error |= dict_get_entry(&node, dict, "dtlsParameters", ODICT_OBJECT, true);
+    error |= get_dtls_parameters(&dtls_parameters, node);
+    error |= dict_get_entry(&node, dict, "sctpParameters", ODICT_OBJECT, true);
+    error |= get_sctp_parameters(&sctp_parameters, node);
+
+    // Ok?
+    if (error) {
+        DEBUG_WARNING("Invalid remote parameters\n");
+        if (sctp_parameters.capabilities) {
+            mem_deref(sctp_parameters.capabilities);
+        }
+        goto out;
+    }
+
+    // Set parameters & start transports
+    client->remote_parameters.ice_parameters = mem_ref(ice_parameters);
+    client->remote_parameters.ice_candidates = mem_ref(ice_candidates);
+    client->remote_parameters.dtls_parameters = mem_ref(dtls_parameters);
+    memcpy(&client->remote_parameters.sctp_parameters, &sctp_parameters, sizeof(sctp_parameters));
+    DEBUG_INFO("Applying remote parameters\n");
+    client_set_parameters(client);
+    client_start_transports(client);
+
+out:
+    // Un-reference
+    mem_deref(dtls_parameters);
+    mem_deref(ice_candidates);
+    mem_deref(ice_parameters);
+    mem_deref(dict);
+
+    // Exit?
+    if (error == RAWRTC_CODE_NO_VALUE) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        client_stop(client);
+        tmr_cancel(&timer);
+        re_cancel();
+    }
+}
+
+static void client_get_parameters(struct data_channel_sctp_client* const client) {
+    struct parameters* const local_parameters = &client->local_parameters;
+
+    // Get local ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(
+        &local_parameters->ice_parameters, client->gatherer));
+
+    // Get local ICE candidates
+    EOE(rawrtc_ice_gatherer_get_local_candidates(
+        &local_parameters->ice_candidates, client->gatherer));
+
+    // Get local DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local_parameters->dtls_parameters, client->dtls_transport));
+
+    // Get local SCTP parameters
+    EOE(rawrtc_sctp_transport_get_capabilities(&local_parameters->sctp_parameters.capabilities));
+    EOE(rawrtc_sctp_transport_get_port(
+        &local_parameters->sctp_parameters.port, client->sctp_transport));
+}
+
+static void print_local_parameters(struct data_channel_sctp_client* client) {
+    struct odict* dict;
+    struct odict* node;
+
+    // Get local parameters
+    client_get_parameters(client);
+
+    // Create dict
+    EOR(odict_alloc(&dict, 16));
+
+    // Create nodes
+    EOR(odict_alloc(&node, 16));
+    set_ice_parameters(client->local_parameters.ice_parameters, node);
+    EOR(odict_entry_add(dict, "iceParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_ice_candidates(client->local_parameters.ice_candidates, node);
+    EOR(odict_entry_add(dict, "iceCandidates", ODICT_ARRAY, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_dtls_parameters(client->local_parameters.dtls_parameters, node);
+    EOR(odict_entry_add(dict, "dtlsParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_sctp_parameters(client->sctp_transport, &client->local_parameters.sctp_parameters, node);
+    EOR(odict_entry_add(dict, "sctpParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+
+    // Print JSON
+    DEBUG_INFO("Local Parameters:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING("Usage: %s <0|1 (ice-role)> [<sctp-port>] [<ice-candidate-type> ...]", program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct data_channel_sctp_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 2) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get ICE role
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get SCTP port (optional)
+    if (argc >= 3 && !str_to_uint16(&client.local_parameters.sctp_parameters.port, argv[2])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 4) {
+        ice_candidate_types = &argv[3];
+        n_ice_candidate_types = (size_t) argc - 3;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.gather_options = gather_options;
+    client.role = role;
+
+    // Setup client
+    client_init(&client);
+
+    // Start gathering
+    client_start_gathering(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_parameters, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}
diff --git a/tools/dtls-transport-loopback.c b/tools/dtls-transport-loopback.c
new file mode 100644
index 0000000..bf05dd5
--- /dev/null
+++ b/tools/dtls-transport-loopback.c
@@ -0,0 +1,177 @@
+#include "helper/handler.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "dtls-transport-loopback-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+// Note: Shadows struct client
+struct dtls_transport_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct dtls_transport_client* other_client;
+};
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct dtls_transport_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Add to other client as remote candidate (if type enabled)
+    add_to_other_if_ice_candidate_type_enabled(arg, candidate, client->other_client->ice_transport);
+}
+
+static void client_init(struct dtls_transport_client* const local) {
+    struct rawrtc_certificate* certificates[1];
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&local->certificate, NULL));
+    certificates[0] = local->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &local->gatherer, local->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, local));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &local->ice_transport, local->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, local));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &local->dtls_transport, local->ice_transport, certificates, ARRAY_SIZE(certificates),
+        default_dtls_transport_state_change_handler, default_dtls_transport_error_handler, local));
+}
+
+static void client_start(
+    struct dtls_transport_client* const local, struct dtls_transport_client* const remote) {
+    // Get & set ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(&local->ice_parameters, remote->gatherer));
+
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(local->gatherer, NULL));
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        local->ice_transport, local->gatherer, local->ice_parameters, local->role));
+
+    // Get & set DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local->dtls_parameters, remote->dtls_transport));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(local->dtls_transport, local->dtls_parameters));
+}
+
+static void client_stop(struct dtls_transport_client* const client) {
+    // Stop transports & close gatherer
+    EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    EOE(rawrtc_ice_gatherer_close(client->gatherer));
+
+    // Un-reference & close
+    client->dtls_parameters = mem_deref(client->dtls_parameters);
+    client->ice_parameters = mem_deref(client->ice_parameters);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct dtls_transport_client a = {0};
+    struct dtls_transport_client b = {0};
+    (void) a.ice_candidate_types;
+    (void) a.n_ice_candidate_types;
+    (void) b.ice_candidate_types;
+    (void) b.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc > 1) {
+        ice_candidate_types = &argv[1];
+        n_ice_candidate_types = (size_t) argc - 1;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Setup client A
+    a.name = "A";
+    a.ice_candidate_types = ice_candidate_types;
+    a.n_ice_candidate_types = n_ice_candidate_types;
+    a.gather_options = gather_options;
+    a.role = RAWRTC_ICE_ROLE_CONTROLLING;
+    a.other_client = &b;
+
+    // Setup client B
+    b.name = "B";
+    b.ice_candidate_types = ice_candidate_types;
+    b.n_ice_candidate_types = n_ice_candidate_types;
+    b.gather_options = gather_options;
+    b.role = RAWRTC_ICE_ROLE_CONTROLLED;
+    b.other_client = &a;
+
+    // Initialise clients
+    client_init(&a);
+    client_init(&b);
+
+    // Start clients
+    client_start(&a, &b);
+    client_start(&b, &a);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, stop_on_return_handler, NULL));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop clients
+    client_stop(&a);
+    client_stop(&b);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+
+    // Free
+    mem_deref(gather_options);
+
+    // Bye
+    before_exit();
+    return 0;
+}
diff --git a/tools/helper/common.c b/tools/helper/common.c
new file mode 100644
index 0000000..e4be7a2
--- /dev/null
+++ b/tools/helper/common.c
@@ -0,0 +1,225 @@
+#include "common.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // strerror
+
+#define DEBUG_MODULE "helper-common"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+/*
+ * Ignore success code list.
+ */
+enum rawrtc_code const ignore_success[] = {RAWRTC_CODE_SUCCESS};
+size_t const ignore_success_length = ARRAY_SIZE(ignore_success);
+
+/*
+ * Function to be called before exiting.
+ */
+void before_exit(void) {
+    // Close
+    rawrtc_close(true);
+
+    // Check memory leaks
+    tmr_debug();
+    mem_debug();
+}
+
+/*
+ * Exit on error code.
+ */
+void exit_on_error(
+    enum rawrtc_code const code,
+    enum rawrtc_code const ignore[],
+    size_t const n_ignore,
+    char const* const file,
+    uint32_t const line) {
+    size_t i;
+
+    // Ignore?
+    for (i = 0; i < n_ignore; ++i) {
+        if (code == ignore[i]) {
+            return;
+        }
+    }
+
+    // Handle
+    switch (code) {
+        case RAWRTC_CODE_SUCCESS:
+            return;
+        case RAWRTC_CODE_NOT_IMPLEMENTED:
+            DEBUG_WARNING("Not implemented in %s %" PRIu32 "\n", file, line);
+            return;
+        default:
+            DEBUG_WARNING(
+                "Error in %s %" PRIu32 " (%d): %s\n", file, line, code, rawrtc_code_to_str(code));
+            before_exit();
+            exit((int) code);
+    }
+}
+
+/*
+ * Exit on POSIX error code.
+ */
+void exit_on_posix_error(int code, char const* const file, uint32_t line) {
+    if (code != 0) {
+        DEBUG_WARNING("Error in %s %" PRIu32 " (%d): %s\n", file, line, code, strerror(code));
+        before_exit();
+        exit(code);
+    }
+}
+
+/*
+ * Exit with a custom error message.
+ */
+void exit_with_error(char const* const file, uint32_t line, char const* const formatter, ...) {
+    char* message;
+
+    // Format message
+    va_list ap;
+    va_start(ap, formatter);
+    re_vsdprintf(&message, formatter, ap);
+    va_end(ap);
+
+    // Print message
+    DEBUG_WARNING("%s %" PRIu32 ": %s\n", file, line, message);
+
+    // Un-reference & bye
+    mem_deref(message);
+    before_exit();
+    exit(1);
+}
+
+/*
+ * Check if the ICE candidate type is enabled.
+ */
+bool ice_candidate_type_enabled(
+    struct client* const client, enum rawrtc_ice_candidate_type const type) {
+    char const* const type_str = rawrtc_ice_candidate_type_to_str(type);
+    size_t i;
+
+    // All enabled?
+    if (client->n_ice_candidate_types == 0) {
+        return true;
+    }
+
+    // Specifically enabled?
+    for (i = 0; i < client->n_ice_candidate_types; ++i) {
+        if (str_cmp(client->ice_candidate_types[i], type_str) == 0) {
+            return true;
+        }
+    }
+
+    // Nope
+    return false;
+}
+
+/*
+ * Print ICE candidate information.
+ */
+void print_ice_candidate(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    struct rawrtc_peer_connection_ice_candidate* const pc_candidate,  // nullable
+    struct client* const client) {
+    if (candidate) {
+        enum rawrtc_code const ignore[] = {RAWRTC_CODE_NO_VALUE};
+        enum rawrtc_code error;
+        char* foundation;
+        enum rawrtc_ice_protocol protocol;
+        uint32_t priority;
+        char* ip;
+        uint16_t port;
+        enum rawrtc_ice_candidate_type type;
+        enum rawrtc_ice_tcp_candidate_type tcp_type;
+        char const* tcp_type_str = "n/a";
+        char* related_address = NULL;
+        uint16_t related_port = 0;
+        char* mid = NULL;
+        uint8_t media_line_index = UINT8_MAX;
+        char* media_line_index_str = NULL;
+        char* username_fragment = NULL;
+        bool is_enabled;
+        int level;
+
+        // Get candidate information
+        EOE(rawrtc_ice_candidate_get_foundation(&foundation, candidate));
+        EOE(rawrtc_ice_candidate_get_protocol(&protocol, candidate));
+        EOE(rawrtc_ice_candidate_get_priority(&priority, candidate));
+        EOE(rawrtc_ice_candidate_get_ip(&ip, candidate));
+        EOE(rawrtc_ice_candidate_get_port(&port, candidate));
+        EOE(rawrtc_ice_candidate_get_type(&type, candidate));
+        error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+        switch (error) {
+            case RAWRTC_CODE_SUCCESS:
+                tcp_type_str = rawrtc_ice_tcp_candidate_type_to_str(tcp_type);
+                break;
+            case RAWRTC_CODE_NO_VALUE:
+                break;
+            default:
+                EOE(error);
+                break;
+        }
+        EOEIGN(rawrtc_ice_candidate_get_related_address(&related_address, candidate), ignore);
+        EOEIGN(rawrtc_ice_candidate_get_related_port(&related_port, candidate), ignore);
+        if (pc_candidate) {
+            EOEIGN(rawrtc_peer_connection_ice_candidate_get_sdp_mid(&mid, pc_candidate), ignore);
+            error = rawrtc_peer_connection_ice_candidate_get_sdp_media_line_index(
+                &media_line_index, pc_candidate);
+            switch (error) {
+                case RAWRTC_CODE_SUCCESS:
+                    EOE(rawrtc_sdprintf(&media_line_index_str, "%" PRIu8, media_line_index));
+                    break;
+                case RAWRTC_CODE_NO_VALUE:
+                    break;
+                default:
+                    EOE(error);
+                    break;
+            }
+            EOEIGN(
+                rawrtc_peer_connection_ice_candidate_get_username_fragment(
+                    &username_fragment, pc_candidate),
+                ignore);
+        }
+        is_enabled = ice_candidate_type_enabled(client, type);
+
+        // Print candidate (meh, lot's of repeated code... feel free to suggest an alternative)
+        level = is_enabled ? DBG_INFO : DBG_DEBUG;
+        if (!pc_candidate) {
+            dbg_printf(
+                level,
+                "(%s) ICE candidate: foundation=%s, protocol=%s"
+                ", priority=%" PRIu32 ", ip=%s, port=%" PRIu16 ", type=%s, tcp-type=%s"
+                ", related-address=%s, related-port=%" PRIu16 "; URL: %s; %s\n",
+                client->name, foundation, rawrtc_ice_protocol_to_str(protocol), priority, ip, port,
+                rawrtc_ice_candidate_type_to_str(type), tcp_type_str,
+                related_address ? related_address : "n/a", related_port, url ? url : "n/a",
+                is_enabled ? "enabled" : "disabled");
+        } else {
+            dbg_printf(
+                level,
+                "(%s) ICE candidate: foundation=%s, protocol=%s"
+                ", priority=%" PRIu32 ", ip=%s, port=%" PRIu16 ", type=%s, tcp-type=%s"
+                ", related-address=%s, related-port=%" PRIu16 "; URL: %s"
+                "; mid=%s, media_line_index=%s, username_fragment=%s; %s\n",
+                client->name, foundation, rawrtc_ice_protocol_to_str(protocol), priority, ip, port,
+                rawrtc_ice_candidate_type_to_str(type), tcp_type_str,
+                related_address ? related_address : "n/a", related_port, url ? url : "n/a",
+                mid ? mid : "n/a", media_line_index_str ? media_line_index_str : "n/a",
+                username_fragment ? username_fragment : "n/a", is_enabled ? "enabled" : "disabled");
+        }
+
+        // Unreference
+        mem_deref(username_fragment);
+        mem_deref(media_line_index_str);
+        mem_deref(mid);
+        mem_deref(related_address);
+        mem_deref(ip);
+        mem_deref(foundation);
+    } else {
+        DEBUG_INFO("(%s) ICE gatherer last local candidate\n", client->name);
+    }
+}
diff --git a/tools/helper/common.h b/tools/helper/common.h
new file mode 100644
index 0000000..e2a34ad
--- /dev/null
+++ b/tools/helper/common.h
@@ -0,0 +1,95 @@
+#pragma once
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+enum {
+    PARAMETERS_MAX_LENGTH = 8192,
+};
+
+/*
+ * SCTP parameters that need to be negotiated.
+ */
+struct sctp_parameters {
+    struct rawrtc_sctp_capabilities* capabilities;
+    uint16_t port;
+};
+
+/*
+ * Client structure. Can be extended.
+ */
+struct client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+};
+
+/*
+ * Data channel helper structure. Can be extended.
+ */
+struct data_channel_helper {
+    struct le le;
+    struct rawrtc_data_channel* channel;
+    char* label;
+    struct client* client;
+    void* arg;
+};
+
+/*
+ * Ignore success code list.
+ */
+extern enum rawrtc_code const ignore_success[];
+extern size_t const ignore_success_length;
+
+/*
+ * Helper macros for exiting with error messages.
+ */
+#define EOE(code) exit_on_error(code, ignore_success, ignore_success_length, __FILE__, __LINE__)
+#define EOEIGN(code, ignore) exit_on_error(code, ignore, ARRAY_SIZE(ignore), __FILE__, __LINE__)
+#define EOR(code) exit_on_posix_error(code, __FILE__, __LINE__)
+#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (__GNUC__ >= 3)
+#    define EWE(...) exit_with_error(__FILE__, __LINE__, __VA_ARGS__)
+#elif defined(__GNUC__)
+#    define EWE(args...) exit_with_error(__FILE__, __LINE__, args)
+#endif
+
+/*
+ * Function to be called before exiting.
+ */
+void before_exit(void);
+
+/*
+ * Exit on error code.
+ */
+void exit_on_error(
+    enum rawrtc_code const code,
+    enum rawrtc_code const ignore[],
+    size_t const n_ignore,
+    char const* const file,
+    uint32_t const line);
+
+/*
+ * Exit on POSIX error code.
+ */
+void exit_on_posix_error(int code, char const* const file, uint32_t line);
+
+/*
+ * Exit with a custom error message.
+ */
+void exit_with_error(char const* const file, uint32_t line, char const* const formatter, ...);
+
+/*
+ * Check if the ICE candidate type is enabled.
+ */
+bool ice_candidate_type_enabled(
+    struct client* const client, enum rawrtc_ice_candidate_type const type);
+
+/*
+ * Print ICE candidate information.
+ */
+void print_ice_candidate(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    struct rawrtc_peer_connection_ice_candidate* const pc_candidate,  // nullable
+    struct client* const client);
diff --git a/tools/helper/handler.c b/tools/helper/handler.c
new file mode 100644
index 0000000..f6d8d1a
--- /dev/null
+++ b/tools/helper/handler.c
@@ -0,0 +1,337 @@
+#include "handler.h"
+#include "common.h"
+#include "utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "helper-handler"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+/*
+ * Print the ICE gatherer's state.
+ */
+void default_ice_gatherer_state_change_handler(
+    enum rawrtc_ice_gatherer_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_ice_gatherer_state_to_name(state);
+    (void) arg;
+    DEBUG_PRINTF("(%s) ICE gatherer state: %s\n", client->name, state_name);
+}
+
+/*
+ * Print the ICE gatherer's error event.
+ */
+void default_ice_gatherer_error_handler(
+    struct rawrtc_ice_candidate* const candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    (void) candidate;
+    (void) error_code;
+    (void) arg;
+    DEBUG_NOTICE("(%s) ICE gatherer error, URL: %s, reason: %s\n", client->name, url, error_text);
+}
+
+/*
+ * Print the newly gathered local candidate.
+ */
+void default_ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    (void) arg;
+    print_ice_candidate(candidate, url, NULL, client);
+}
+
+/*
+ * Print the ICE transport's state.
+ */
+void default_ice_transport_state_change_handler(
+    enum rawrtc_ice_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_ice_transport_state_to_name(state);
+    (void) arg;
+    DEBUG_PRINTF("(%s) ICE transport state: %s\n", client->name, state_name);
+}
+
+/*
+ * Print the ICE candidate pair change event.
+ */
+void default_ice_transport_candidate_pair_change_handler(
+    struct rawrtc_ice_candidate* const local,  // read-only
+    struct rawrtc_ice_candidate* const remote,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    (void) local;
+    (void) remote;
+    DEBUG_PRINTF("(%s) ICE transport candidate pair change\n", client->name);
+}
+
+/*
+ * Print the DTLS transport's state.
+ */
+void default_dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_dtls_transport_state_to_name(state);
+    DEBUG_PRINTF("(%s) DTLS transport state change: %s\n", client->name, state_name);
+}
+
+/*
+ * Print the DTLS transport's error event.
+ */
+void default_dtls_transport_error_handler(
+    // TODO: error.message (probably from OpenSSL)
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    // TODO: Print error message
+    DEBUG_WARNING("(%s) DTLS transport error: %s\n", client->name, "???");
+}
+
+#if RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
+/*
+ * Print the SCTP redirect transport's state.
+ */
+void default_sctp_redirect_transport_state_change_handler(
+    enum rawrtc_sctp_redirect_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_sctp_redirect_transport_state_to_name(state);
+    DEBUG_PRINTF("(%s) SCTP redirect transport state change: %s\n", client->name, state_name);
+}
+#endif
+
+/*
+ * Print the SCTP transport's state.
+ */
+void default_sctp_transport_state_change_handler(
+    enum rawrtc_sctp_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_sctp_transport_state_to_name(state);
+    DEBUG_PRINTF("(%s) SCTP transport state change: %s\n", client->name, state_name);
+}
+
+/*
+ * Print the newly created data channel's parameter.
+ */
+void default_data_channel_handler(
+    struct rawrtc_data_channel* const channel,  // read-only, MUST be referenced when used
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    struct rawrtc_data_channel_parameters* parameters;
+    enum rawrtc_code const ignore[] = {RAWRTC_CODE_NO_VALUE};
+    char* label = NULL;
+
+    // Get data channel label and protocol
+    EOE(rawrtc_data_channel_get_parameters(&parameters, channel));
+    EOEIGN(rawrtc_data_channel_parameters_get_label(&label, parameters), ignore);
+    DEBUG_INFO("(%s) New data channel instance: %s\n", client->name, label ? label : "n/a");
+    mem_deref(label);
+    mem_deref(parameters);
+}
+
+/*
+ * Print the data channel open event.
+ */
+void default_data_channel_open_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct client* const client = channel->client;
+    DEBUG_PRINTF("(%s) Data channel open: %s\n", client->name, channel->label);
+}
+
+/*
+ * Print the data channel buffered amount low event.
+ */
+void default_data_channel_buffered_amount_low_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct client* const client = channel->client;
+    DEBUG_PRINTF("(%s) Data channel buffered amount low: %s\n", client->name, channel->label);
+}
+
+/*
+ * Print the data channel error event.
+ */
+void default_data_channel_error_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct client* const client = channel->client;
+    DEBUG_WARNING("(%s) Data channel error: %s\n", client->name, channel->label);
+}
+
+/*
+ * Print the data channel close event.
+ */
+void default_data_channel_close_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct client* const client = channel->client;
+    DEBUG_PRINTF("(%s) Data channel closed: %s\n", client->name, channel->label);
+}
+
+char const* const separator = ", ";
+
+static int debug_data_channel_message_flags(
+    struct re_printf* const pf, enum rawrtc_data_channel_message_flag const flags) {
+    int err = 0;
+    char const* prefix = "";
+
+    if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_ABORTED) {
+        err |= re_hprintf(pf, "%saborted", prefix);
+        prefix = separator;
+    }
+    if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_COMPLETE) {
+        err |= re_hprintf(pf, "%scomplete", prefix);
+        prefix = separator;
+    }
+    if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_STRING) {
+        err |= re_hprintf(pf, "%sstring", prefix);
+        prefix = separator;
+    }
+    if (flags & RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_BINARY) {
+        err |= re_hprintf(pf, "%sbinary", prefix);
+    }
+
+    return err;
+}
+
+/*
+ * Print the data channel's received message's size.
+ */
+void default_data_channel_message_handler(
+    struct mbuf* const buffer,
+    enum rawrtc_data_channel_message_flag const flags,
+    void* const arg  // will be casted to `struct data_channel_helper*`
+) {
+    struct data_channel_helper* const channel = arg;
+    struct client* const client = channel->client;
+    DEBUG_PRINTF(
+        "(%s) Incoming message for data channel %s: %zu bytes; flags=(%H)\n", client->name,
+        channel->label, mbuf_get_left(buffer), debug_data_channel_message_flags, flags);
+}
+
+/*
+ * Print negotiation needed (duh!)
+ */
+void default_negotiation_needed_handler(void* const arg) {
+    struct client* const client = arg;
+    DEBUG_PRINTF("(%s) Negotiation needed\n", client->name);
+}
+
+/*
+ * Print the peer connection's state.
+ */
+void default_peer_connection_state_change_handler(
+    enum rawrtc_peer_connection_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_peer_connection_state_to_name(state);
+    DEBUG_PRINTF("(%s) Peer connection state change: %s\n", client->name, state_name);
+}
+
+/*
+ * Print the newly gathered local candidate (peer connection variant).
+ */
+void default_peer_connection_local_candidate_handler(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct client* const client = arg;
+    struct rawrtc_ice_candidate* ortc_candidate = NULL;
+
+    // Get underlying ORTC ICE candidate (if any)
+    if (candidate) {
+        EOE(rawrtc_peer_connection_ice_candidate_get_ortc_candidate(&ortc_candidate, candidate));
+    }
+
+    // Print local candidate
+    print_ice_candidate(ortc_candidate, url, candidate, client);
+    mem_deref(ortc_candidate);
+}
+
+/*
+ * Print the peer connections local candidate error event.
+ */
+void default_peer_connection_local_candidate_error_handler(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    struct client* const client = arg;
+    (void) candidate;
+    (void) error_code;
+    (void) arg;
+    DEBUG_NOTICE("(%s) ICE candidate error, URL: %s, reason: %s\n", client->name, url, error_text);
+}
+
+/*
+ * Print the signaling state.
+ */
+void default_signaling_state_change_handler(
+    enum rawrtc_signaling_state const state,  // read-only
+    void* const arg) {
+    struct client* const client = arg;
+    char const* const state_name = rawrtc_signaling_state_to_name(state);
+    DEBUG_PRINTF("(%s) Signaling state change: %s\n", client->name, state_name);
+}
+
+/*
+ * Stop the main loop.
+ */
+void default_signal_handler(int sig) {
+    DEBUG_INFO("Got signal: %d, terminating...\n", sig);
+    re_cancel();
+}
+
+/*
+ * FD-listener that stops the main loop in case the input buffer
+ * contains a line feed or a carriage return.
+ */
+void stop_on_return_handler(int flags, void* arg) {
+    char buffer[128];
+    size_t length;
+    (void) flags;
+    (void) arg;
+
+    // Get message from stdin
+    if (!fgets((char*) buffer, 128, stdin)) {
+        EOR(errno);
+    }
+    length = strlen(buffer);
+
+    // Exit?
+    if (length > 0 && length < 3 && (buffer[0] == '\n' || buffer[0] == '\r')) {
+        // Stop main loop
+        DEBUG_INFO("Exiting\n");
+        re_cancel();
+    }
+}
diff --git a/tools/helper/handler.h b/tools/helper/handler.h
new file mode 100644
index 0000000..56d4211
--- /dev/null
+++ b/tools/helper/handler.h
@@ -0,0 +1,181 @@
+#pragma once
+#include "common.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+/*
+ * Print the ICE gatherer's state.
+ */
+void default_ice_gatherer_state_change_handler(
+    enum rawrtc_ice_gatherer_state const state,  // read-only
+    void* const arg);
+
+/*
+ * Print the ICE gatherer's error event.
+ */
+void default_ice_gatherer_error_handler(
+    struct rawrtc_ice_candidate* const host_candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the newly gatherered local candidate.
+ * Will print local parameters on stdout in case the client is not
+ * used in loopback mode.
+ */
+void default_ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the ICE transport's state.
+ */
+void default_ice_transport_state_change_handler(
+    enum rawrtc_ice_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the ICE candidate pair change event.
+ */
+void default_ice_transport_candidate_pair_change_handler(
+    struct rawrtc_ice_candidate* const local,  // read-only
+    struct rawrtc_ice_candidate* const remote,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the DTLS transport's state.
+ */
+void default_dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the DTLS transport's error event.
+ */
+void default_dtls_transport_error_handler(
+    // TODO: error.message (probably from OpenSSL)
+    void* const arg  // will be casted to `struct client*`
+);
+
+#if RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
+/*
+ * Print the SCTP redirect transport's state.
+ */
+void default_sctp_redirect_transport_state_change_handler(
+    enum rawrtc_sctp_redirect_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+);
+#endif
+
+/*
+ * Print the SCTP transport's state.
+ */
+void default_sctp_transport_state_change_handler(
+    enum rawrtc_sctp_transport_state const state,
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the newly created data channel's parameter.
+ */
+void default_data_channel_handler(
+    struct rawrtc_data_channel* const data_channel,  // read-only, MUST be referenced when used
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print the data channel open event.
+ */
+void default_data_channel_open_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print the data channel buffered amount low event.
+ */
+void default_data_channel_buffered_amount_low_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print the data channel error event.
+ */
+void default_data_channel_error_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print the data channel close event.
+ */
+void default_data_channel_close_handler(
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print the data channel's received message's size.
+ */
+void default_data_channel_message_handler(
+    struct mbuf* const buffer,
+    enum rawrtc_data_channel_message_flag const flags,
+    void* const arg  // will be casted to `struct data_channel_helper*`
+);
+
+/*
+ * Print negotiation needed (duh!)
+ */
+void default_negotiation_needed_handler(void* const arg);
+
+/*
+ * Print the peer connection's state.
+ */
+void default_peer_connection_state_change_handler(
+    enum rawrtc_peer_connection_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the newly gathered local candidate (peer connection variant).
+ */
+void default_peer_connection_local_candidate_handler(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg);
+
+/*
+ * Print the peer connections local candidate error event.
+ */
+void default_peer_connection_local_candidate_error_handler(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,  // read-only, nullable
+    char const* const url,  // read-only
+    uint16_t const error_code,  // read-only
+    char const* const error_text,  // read-only
+    void* const arg  // will be casted to `struct client*`
+);
+
+/*
+ * Print the signaling state.
+ */
+void default_signaling_state_change_handler(
+    enum rawrtc_signaling_state const state,  // read-only
+    void* const arg);
+
+/*
+ * Stop the main loop.
+ */
+void default_signal_handler(int sig);
+
+/*
+ * FD-listener that stops the main loop in case the input buffer is
+ * empty.
+ */
+void stop_on_return_handler(int flags, void* arg);
diff --git a/tools/helper/meson.build b/tools/helper/meson.build
new file mode 100644
index 0000000..b9ef950
--- /dev/null
+++ b/tools/helper/meson.build
@@ -0,0 +1,6 @@
+helper_sources = files([
+    'common.c',
+    'handler.c',
+    'parameters.c',
+    'utils.c',
+])
diff --git a/tools/helper/parameters.c b/tools/helper/parameters.c
new file mode 100644
index 0000000..46ae22b
--- /dev/null
+++ b/tools/helper/parameters.c
@@ -0,0 +1,448 @@
+#include "parameters.h"
+#include "common.h"
+#include "utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+#define DEBUG_MODULE "helper-parameters"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+/*
+ * Set ICE parameters in dictionary.
+ */
+void set_ice_parameters(struct rawrtc_ice_parameters* const parameters, struct odict* const dict) {
+    char* username_fragment;
+    char* password;
+    bool ice_lite;
+
+    // Get values
+    EOE(rawrtc_ice_parameters_get_username_fragment(&username_fragment, parameters));
+    EOE(rawrtc_ice_parameters_get_password(&password, parameters));
+    EOE(rawrtc_ice_parameters_get_ice_lite(&ice_lite, parameters));
+
+    // Set ICE parameters
+    EOR(odict_entry_add(dict, "usernameFragment", ODICT_STRING, username_fragment));
+    EOR(odict_entry_add(dict, "password", ODICT_STRING, password));
+    EOR(odict_entry_add(dict, "iceLite", ODICT_BOOL, ice_lite));
+
+    // Un-reference values
+    mem_deref(password);
+    mem_deref(username_fragment);
+}
+
+/*
+ * Set ICE candidates in dictionary.
+ */
+void set_ice_candidates(struct rawrtc_ice_candidates* const parameters, struct odict* const array) {
+    size_t i;
+    struct odict* node;
+
+    // Set ICE candidates
+    for (i = 0; i < parameters->n_candidates; ++i) {
+        enum rawrtc_code error;
+        struct rawrtc_ice_candidate* const candidate = parameters->candidates[i];
+        char* foundation;
+        uint32_t priority;
+        char* ip;
+        enum rawrtc_ice_protocol protocol;
+        uint16_t port;
+        enum rawrtc_ice_candidate_type type;
+        enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+        char* related_address = NULL;
+        uint16_t related_port = 0;
+        char* key;
+
+        // Create object
+        EOR(odict_alloc(&node, 16));
+
+        // Get values
+        EOE(rawrtc_ice_candidate_get_foundation(&foundation, candidate));
+        EOE(rawrtc_ice_candidate_get_priority(&priority, candidate));
+        EOE(rawrtc_ice_candidate_get_ip(&ip, candidate));
+        EOE(rawrtc_ice_candidate_get_protocol(&protocol, candidate));
+        EOE(rawrtc_ice_candidate_get_port(&port, candidate));
+        EOE(rawrtc_ice_candidate_get_type(&type, candidate));
+        error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+        EOE(error == RAWRTC_CODE_NO_VALUE ? RAWRTC_CODE_SUCCESS : error);
+        error = rawrtc_ice_candidate_get_related_address(&related_address, candidate);
+        EOE(error == RAWRTC_CODE_NO_VALUE ? RAWRTC_CODE_SUCCESS : error);
+        error = rawrtc_ice_candidate_get_related_port(&related_port, candidate);
+        EOE(error == RAWRTC_CODE_NO_VALUE ? RAWRTC_CODE_SUCCESS : error);
+
+        // Set ICE candidate values
+        EOR(odict_entry_add(node, "foundation", ODICT_STRING, foundation));
+        EOR(odict_entry_add(node, "priority", ODICT_INT, (int64_t) priority));
+        EOR(odict_entry_add(node, "ip", ODICT_STRING, ip));
+        EOR(odict_entry_add(node, "protocol", ODICT_STRING, rawrtc_ice_protocol_to_str(protocol)));
+        EOR(odict_entry_add(node, "port", ODICT_INT, (int64_t) port));
+        EOR(odict_entry_add(node, "type", ODICT_STRING, rawrtc_ice_candidate_type_to_str(type)));
+        if (protocol == RAWRTC_ICE_PROTOCOL_TCP) {
+            EOR(odict_entry_add(
+                node, "tcpType", ODICT_STRING, rawrtc_ice_tcp_candidate_type_to_str(tcp_type)));
+        }
+        if (related_address) {
+            EOR(odict_entry_add(node, "relatedAddress", ODICT_STRING, related_address));
+        }
+        if (related_port > 0) {
+            EOR(odict_entry_add(node, "relatedPort", (int64_t) ODICT_INT, related_port));
+        }
+
+        // Add to array
+        EOE(rawrtc_sdprintf(&key, "%zu", i));
+        EOR(odict_entry_add(array, key, ODICT_OBJECT, node));
+
+        // Un-reference values
+        mem_deref(key);
+        mem_deref(related_address);
+        mem_deref(ip);
+        mem_deref(foundation);
+        mem_deref(node);
+    }
+}
+
+/*
+ * Set DTLS parameters in dictionary.
+ */
+void set_dtls_parameters(
+    struct rawrtc_dtls_parameters* const parameters, struct odict* const dict) {
+    enum rawrtc_dtls_role role;
+    struct odict* array;
+    struct odict* node;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    size_t i;
+
+    // Get and set DTLS role
+    EOE(rawrtc_dtls_parameters_get_role(&role, parameters));
+    EOR(odict_entry_add(dict, "role", ODICT_STRING, rawrtc_dtls_role_to_str(role)));
+
+    // Create array
+    EOR(odict_alloc(&array, 16));
+
+    // Get and set fingerprints
+    EOE(rawrtc_dtls_parameters_get_fingerprints(&fingerprints, parameters));
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint = fingerprints->fingerprints[i];
+        enum rawrtc_certificate_sign_algorithm sign_algorithm;
+        char* value;
+        char* key;
+
+        // Create object
+        EOR(odict_alloc(&node, 16));
+
+        // Get values
+        EOE(rawrtc_dtls_fingerprint_get_sign_algorithm(&sign_algorithm, fingerprint));
+        EOE(rawrtc_dtls_fingerprint_get_value(&value, fingerprint));
+
+        // Set fingerprint values
+        EOR(odict_entry_add(
+            node, "algorithm", ODICT_STRING,
+            rawrtc_certificate_sign_algorithm_to_str(sign_algorithm)));
+        EOR(odict_entry_add(node, "value", ODICT_STRING, value));
+
+        // Add to array
+        EOE(rawrtc_sdprintf(&key, "%zu", i));
+        EOR(odict_entry_add(array, key, ODICT_OBJECT, node));
+
+        // Un-reference values
+        mem_deref(key);
+        mem_deref(value);
+        mem_deref(node);
+    }
+
+    // Un-reference fingerprints
+    mem_deref(fingerprints);
+
+    // Add array to object
+    EOR(odict_entry_add(dict, "fingerprints", ODICT_ARRAY, array));
+    mem_deref(array);
+}
+
+/*
+ * Set SCTP parameters in dictionary.
+ */
+void set_sctp_parameters(
+    struct rawrtc_sctp_transport* const transport,
+    struct sctp_parameters* const parameters,
+    struct odict* const dict) {
+    uint64_t max_message_size;
+    uint16_t port;
+
+    // Get values
+    EOE(rawrtc_sctp_capabilities_get_max_message_size(&max_message_size, parameters->capabilities));
+    EOE(rawrtc_sctp_transport_get_port(&port, transport));
+
+    // Ensure maximum message size fits into int64
+    if (max_message_size > INT64_MAX) {
+        EOE(RAWRTC_CODE_INSUFFICIENT_SPACE);
+    }
+
+    // Set ICE parameters
+    EOR(odict_entry_add(dict, "maxMessageSize", ODICT_INT, (int64_t) max_message_size));
+    EOR(odict_entry_add(dict, "port", ODICT_INT, (int64_t) port));
+}
+
+#if RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
+/*
+ * Set SCTP redirect parameters in dictionary.
+ */
+void set_sctp_redirect_parameters(
+    struct rawrtc_sctp_redirect_transport* const transport,
+    struct sctp_parameters* const parameters,
+    struct odict* const dict) {
+    uint64_t max_message_size;
+    uint16_t port;
+
+    // Get values
+    EOE(rawrtc_sctp_capabilities_get_max_message_size(&max_message_size, parameters->capabilities));
+    EOE(rawrtc_sctp_redirect_transport_get_port(&port, transport));
+
+    // Ensure maximum message size fits into int64
+    if (max_message_size > INT64_MAX) {
+        EOE(RAWRTC_CODE_INSUFFICIENT_SPACE);
+    }
+
+    // Set ICE parameters
+    EOR(odict_entry_add(dict, "maxMessageSize", ODICT_INT, (int64_t) max_message_size));
+    EOR(odict_entry_add(dict, "port", ODICT_INT, (int64_t) port));
+}
+#endif
+
+/*
+ * Get ICE parameters from dictionary.
+ */
+enum rawrtc_code get_ice_parameters(
+    struct rawrtc_ice_parameters** const parametersp, struct odict* const dict) {
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    char* username_fragment;
+    char* password;
+    bool ice_lite;
+
+    // Get ICE parameters
+    error |= dict_get_entry(&username_fragment, dict, "usernameFragment", ODICT_STRING, true);
+    error |= dict_get_entry(&password, dict, "password", ODICT_STRING, true);
+    error |= dict_get_entry(&ice_lite, dict, "iceLite", ODICT_BOOL, true);
+    if (error) {
+        return error;
+    }
+
+    // Create ICE parameters instance
+    return rawrtc_ice_parameters_create(parametersp, username_fragment, password, ice_lite);
+}
+
+static void ice_candidates_destroy(void* arg) {
+    struct rawrtc_ice_candidates* const candidates = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < candidates->n_candidates; ++i) {
+        mem_deref(candidates->candidates[i]);
+    }
+}
+
+/*
+ * Get ICE candidates from dictionary.
+ * Filter by enabled ICE candidate types if `client` argument is set to
+ * non-NULL.
+ */
+enum rawrtc_code get_ice_candidates(
+    struct rawrtc_ice_candidates** const candidatesp,
+    struct odict* const dict,
+    struct client* const client) {
+    size_t n;
+    struct rawrtc_ice_candidates* candidates;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    struct le* le;
+
+    // Get length
+    n = list_count(&dict->lst) + 1;
+
+    // Allocate & set length immediately
+    // Note: We allocate more than we need in case ICE candidate types are being filtered but... meh
+    candidates = mem_zalloc(
+        sizeof(*candidates) + (sizeof(struct rawrtc_ice_candidate*) * n), ice_candidates_destroy);
+    if (!candidates) {
+        EWE("No memory to allocate ICE candidates array");
+    }
+    candidates->n_candidates = 0;
+
+    // Get ICE candidates
+    for (le = list_head(&dict->lst); le != NULL; le = le->next) {
+        struct odict* const node = ((struct odict_entry*) le->data)->u.odict;
+        char const* type_str = NULL;
+        enum rawrtc_ice_candidate_type type;
+        char* foundation;
+        uint32_t priority;
+        char* ip;
+        char const* protocol_str = NULL;
+        enum rawrtc_ice_protocol protocol;
+        uint16_t port;
+        char const* tcp_type_str = NULL;
+        enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+        char* related_address = NULL;
+        uint16_t related_port = 0;
+        struct rawrtc_ice_candidate* candidate;
+
+        // Get ICE candidate
+        error |= dict_get_entry(&type_str, node, "type", ODICT_STRING, true);
+        error |= rawrtc_str_to_ice_candidate_type(&type, type_str);
+        error |= dict_get_entry(&foundation, node, "foundation", ODICT_STRING, true);
+        error |= dict_get_uint32(&priority, node, "priority", true);
+        error |= dict_get_entry(&ip, node, "ip", ODICT_STRING, true);
+        error |= dict_get_entry(&protocol_str, node, "protocol", ODICT_STRING, true);
+        error |= rawrtc_str_to_ice_protocol(&protocol, protocol_str);
+        error |= dict_get_uint16(&port, node, "port", true);
+        if (protocol == RAWRTC_ICE_PROTOCOL_TCP) {
+            error |= dict_get_entry(&tcp_type_str, node, "tcpType", ODICT_STRING, true);
+            error |= rawrtc_str_to_ice_tcp_candidate_type(&tcp_type, tcp_type_str);
+        }
+        dict_get_entry(&related_address, node, "relatedAddress", ODICT_STRING, false);
+        dict_get_uint16(&related_port, node, "relatedPort", false);
+        if (error) {
+            goto out;
+        }
+
+        // Create and add ICE candidate
+        error = rawrtc_ice_candidate_create(
+            &candidate, foundation, priority, ip, protocol, port, type, tcp_type, related_address,
+            related_port);
+        if (error) {
+            goto out;
+        }
+
+        // Print ICE candidate
+        print_ice_candidate(candidate, NULL, NULL, client);
+
+        // Store if ICE candidate type enabled
+        if (ice_candidate_type_enabled(client, type)) {
+            candidates->candidates[candidates->n_candidates++] = candidate;
+        } else {
+            mem_deref(candidate);
+        }
+    }
+
+    // End-of-candidates
+    candidates->candidates[candidates->n_candidates++] = NULL;
+
+out:
+    if (error) {
+        mem_deref(candidates);
+    } else {
+        // Set pointer
+        *candidatesp = candidates;
+    }
+    return error;
+}
+
+static void dtls_fingerprints_destroy(void* arg) {
+    struct rawrtc_dtls_fingerprints* const fingerprints = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        mem_deref(fingerprints->fingerprints[i]);
+    }
+}
+
+/*
+ * Get DTLS parameters from dictionary.
+ */
+enum rawrtc_code get_dtls_parameters(
+    struct rawrtc_dtls_parameters** const parametersp, struct odict* const dict) {
+    size_t n;
+    struct rawrtc_dtls_parameters* parameters = NULL;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    enum rawrtc_code error;
+    char const* role_str = NULL;
+    enum rawrtc_dtls_role role;
+    struct odict* node;
+    struct le* le;
+    size_t i;
+
+    // Get fingerprints array and length
+    error = dict_get_entry(&node, dict, "fingerprints", ODICT_ARRAY, true);
+    if (error) {
+        return error;
+    }
+    n = list_count(&node->lst);
+
+    // Allocate & set length immediately
+    fingerprints = mem_zalloc(
+        sizeof(*fingerprints) + (sizeof(struct rawrtc_dtls_fingerprints*) * n),
+        dtls_fingerprints_destroy);
+    if (!fingerprints) {
+        EWE("No memory to allocate DTLS fingerprint array");
+    }
+    fingerprints->n_fingerprints = n;
+
+    // Get role
+    error |= dict_get_entry(&role_str, dict, "role", ODICT_STRING, true);
+    error |= rawrtc_str_to_dtls_role(&role, role_str);
+    if (error) {
+        role = RAWRTC_DTLS_ROLE_AUTO;
+    }
+
+    // Get fingerprints
+    for (le = list_head(&node->lst), i = 0; le != NULL; le = le->next, ++i) {
+        char* algorithm_str = NULL;
+        enum rawrtc_certificate_sign_algorithm algorithm;
+        char* value;
+        node = ((struct odict_entry*) le->data)->u.odict;
+
+        // Get fingerprint
+        error |= dict_get_entry(&algorithm_str, node, "algorithm", ODICT_STRING, true);
+        error |= rawrtc_str_to_certificate_sign_algorithm(&algorithm, algorithm_str);
+        error |= dict_get_entry(&value, node, "value", ODICT_STRING, true);
+        if (error) {
+            goto out;
+        }
+
+        // Create and add fingerprint
+        error = rawrtc_dtls_fingerprint_create(&fingerprints->fingerprints[i], algorithm, value);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create DTLS parameters
+    error = rawrtc_dtls_parameters_create(
+        &parameters, role, fingerprints->fingerprints, fingerprints->n_fingerprints);
+
+out:
+    mem_deref(fingerprints);
+
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
+
+/*
+ * Get SCTP parameters from dictionary.
+ */
+enum rawrtc_code get_sctp_parameters(
+    struct sctp_parameters* const parameters, struct odict* const dict) {
+    enum rawrtc_code error;
+    uint64_t max_message_size;
+
+    // Get maximum message size
+    error = dict_get_entry(&max_message_size, dict, "maxMessageSize", ODICT_INT, true);
+    if (error) {
+        return error;
+    }
+
+    // Get port
+    error = dict_get_uint16(&parameters->port, dict, "port", false);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        // Note: Nothing to do in NO VALUE case as port has been set to 0 by default
+        return error;
+    }
+
+    // Create SCTP capabilities instance
+    return rawrtc_sctp_capabilities_create(&parameters->capabilities, max_message_size);
+}
diff --git a/tools/helper/parameters.h b/tools/helper/parameters.h
new file mode 100644
index 0000000..bc6a5aa
--- /dev/null
+++ b/tools/helper/parameters.h
@@ -0,0 +1,67 @@
+#pragma once
+#include "common.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+/*
+ * Set ICE parameters in dictionary.
+ */
+void set_ice_parameters(struct rawrtc_ice_parameters* const parameters, struct odict* const dict);
+
+/*
+ * Set ICE candidates in dictionary.
+ */
+void set_ice_candidates(struct rawrtc_ice_candidates* const parameters, struct odict* const array);
+
+/*
+ * Set DTLS parameters in dictionary.
+ */
+void set_dtls_parameters(struct rawrtc_dtls_parameters* const parameters, struct odict* const dict);
+
+/*
+ * Set SCTP parameters in dictionary.
+ */
+void set_sctp_parameters(
+    struct rawrtc_sctp_transport* const transport,
+    struct sctp_parameters* const parameters,
+    struct odict* const dict);
+
+#if RAWRTC_HAVE_SCTP_REDIRECT_TRANSPORT
+/*
+ * Set SCTP redirect parameters in dictionary.
+ */
+void set_sctp_redirect_parameters(
+    struct rawrtc_sctp_redirect_transport* const transport,
+    struct sctp_parameters* const parameters,
+    struct odict* const dict);
+#endif
+
+/*
+ * Get ICE parameters from dictionary.
+ */
+enum rawrtc_code get_ice_parameters(
+    struct rawrtc_ice_parameters** const parametersp, struct odict* const dict);
+
+/*
+ * Get ICE candidates from dictionary.
+ * Filter by enabled ICE candidate types if `client` argument is set to
+ * non-NULL.
+ */
+enum rawrtc_code get_ice_candidates(
+    struct rawrtc_ice_candidates** const candidatesp,
+    struct odict* const dict,
+    struct client* const client);
+
+/*
+ * Get DTLS parameters from dictionary.
+ */
+enum rawrtc_code get_dtls_parameters(
+    struct rawrtc_dtls_parameters** const parametersp, struct odict* const dict);
+
+/*
+ * Get SCTP parameters from dictionary.
+ */
+enum rawrtc_code get_sctp_parameters(
+    struct sctp_parameters* const parameters, struct odict* const dict);
diff --git a/tools/helper/utils.c b/tools/helper/utils.c
new file mode 100644
index 0000000..2c15b00
--- /dev/null
+++ b/tools/helper/utils.c
@@ -0,0 +1,380 @@
+#include "utils.h"
+#include "common.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <limits.h>  // ULONG_MAX
+#include <stdlib.h>  // strto*
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "helper-utils"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+/*
+ * Convert string to uint16.
+ */
+bool str_to_uint16(uint16_t* const numberp, char* const str) {
+    char* end;
+    unsigned long number = strtoul(str, &end, 10);
+
+    // Check result (this function is insane, srsly...)
+    if (*end != '\0' || (number == ULONG_MAX && errno == ERANGE)) {
+        return false;
+    }
+
+    // Check bounds
+#if (ULONG_MAX > UINT16_MAX)
+    if (number > UINT16_MAX) {
+        return false;
+    }
+#endif
+
+    // Done
+    *numberp = (uint16_t) number;
+    return true;
+}
+
+/*
+ * Convert string to uint64.
+ */
+bool str_to_uint64(uint64_t* const numberp, char* const str) {
+    char* end;
+    unsigned long long number = strtoull(str, &end, 10);
+
+    // Check result (this function is insane, srsly...)
+    if (*end != '\0' || (number == ULONG_MAX && errno == ERANGE)) {
+        return false;
+    }
+
+    // Check bounds
+#if (ULONG_MAX > UINT64_MAX)
+    if (number > UINT64_MAX) {
+        return false;
+    }
+#endif
+
+    // Done
+    *numberp = (uint64_t) number;
+    return true;
+}
+
+/*
+ * Convert string to uint32.
+ */
+bool str_to_uint32(uint32_t* const numberp, char* const str) {
+    uint64_t number;
+    bool success = str_to_uint64(&number, str);
+
+    // Validate
+    if (!success || number > UINT32_MAX) {
+        return false;
+    }
+
+    // Done
+    *numberp = (uint32_t) number;
+    return true;
+}
+
+/*
+ * Get a dictionary entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_entry(
+    void* const valuep,
+    struct odict* const parent,
+    char* const key,
+    enum odict_type const type,
+    bool required) {
+    struct odict_entry const* entry;
+
+    // Check arguments
+    if (!valuep || !parent || !key) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Do lookup
+    entry = odict_lookup(parent, key);
+
+    // Check for entry
+    if (!entry) {
+        if (required) {
+            DEBUG_WARNING("'%s' missing\n", key);
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        } else {
+            return RAWRTC_CODE_NO_VALUE;
+        }
+    }
+
+    // Check for type
+    if (entry->type != type) {
+        DEBUG_WARNING("'%s' is of different type than expected\n", key);
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value according to type
+    switch (type) {
+        case ODICT_OBJECT:
+        case ODICT_ARRAY:
+            *((struct odict * * const) valuep) = entry->u.odict;
+            break;
+        case ODICT_STRING:
+            *((char** const) valuep) = entry->u.str;
+            break;
+        case ODICT_INT:
+            *((int64_t* const) valuep) = entry->u.integer;
+            break;
+        case ODICT_DOUBLE:
+            *((double* const) valuep) = entry->u.dbl;
+            break;
+        case ODICT_BOOL:
+            *((bool* const) valuep) = entry->u.boolean;
+            break;
+        case ODICT_NULL:
+            *((char** const) valuep) = NULL;  // meh!
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get a uint32 entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_uint32(
+    uint32_t* const valuep, struct odict* const parent, char* const key, bool required) {
+    enum rawrtc_code error;
+    int64_t value;
+
+    // Check arguments
+    if (!valuep || !parent || !key) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get int64_t
+    error = dict_get_entry(&value, parent, key, ODICT_INT, required);
+    if (error) {
+        return error;
+    }
+
+    // Check bounds
+    if (value < 0 || value > UINT32_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value & done
+    *valuep = (uint32_t) value;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get a uint16 entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_uint16(
+    uint16_t* const valuep, struct odict* const parent, char* const key, bool required) {
+    enum rawrtc_code error;
+    int64_t value;
+
+    // Check arguments
+    if (!valuep || !parent || !key) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get int64_t
+    error = dict_get_entry(&value, parent, key, ODICT_INT, required);
+    if (error) {
+        return error;
+    }
+
+    // Check bounds
+    if (value < 0 || value > UINT16_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value & done
+    *valuep = (uint16_t) value;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get JSON from stdin and parse it to a dictionary.
+ */
+enum rawrtc_code get_json_stdin(struct odict** const dictp  // de-referenced
+) {
+    char buffer[PARAMETERS_MAX_LENGTH];
+    size_t length;
+
+    // Get message from stdin
+    if (!fgets((char*) buffer, PARAMETERS_MAX_LENGTH, stdin)) {
+        EWE("Error polling stdin");
+    }
+    length = strlen(buffer);
+
+    // Exit?
+    if (length == 1 && buffer[0] == '\n') {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    // Decode JSON
+    EOR(json_decode_odict(dictp, 16, buffer, length, 3));
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the ICE role from a string.
+ */
+enum rawrtc_code get_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    char const* const str) {
+    // Get ICE role
+    switch (str[0]) {
+        case '0':
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLED;
+            return RAWRTC_CODE_SUCCESS;
+        case '1':
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLING;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Get the congestion control algorithm from a string.
+ */
+enum rawrtc_code get_congestion_control_algorithm(
+    enum rawrtc_sctp_transport_congestion_ctrl* const algorithmp,  // de-referenced
+    char const* const str) {
+    if (str_casecmp(str, "RFC2581") == 0) {
+        *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581;
+        return RAWRTC_CODE_SUCCESS;
+    } else if (str_casecmp(str, "HSTCP") == 0) {
+        *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HSTCP;
+        return RAWRTC_CODE_SUCCESS;
+    } else if (str_casecmp(str, "HTCP") == 0) {
+        *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HTCP;
+        return RAWRTC_CODE_SUCCESS;
+    } else if (str_casecmp(str, "RTCC") == 0) {
+        *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RTCC;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+static void data_channel_helper_destroy(void* arg) {
+    struct data_channel_helper* const channel = arg;
+
+    // Unset handler argument & handlers of the channel
+    if (channel->channel) {
+        EOE(rawrtc_data_channel_unset_handlers(channel->channel));
+    }
+
+    // Remove from list
+    list_unlink(&channel->le);
+
+    // Un-reference
+    mem_deref(channel->arg);
+    mem_deref(channel->label);
+    mem_deref(channel->channel);
+}
+
+/*
+ * Create a data channel helper instance.
+ */
+void data_channel_helper_create(
+    struct data_channel_helper** const channel_helperp,  // de-referenced
+    struct client* const client,
+    char* const label) {
+    // Allocate
+    struct data_channel_helper* const channel =
+        mem_zalloc(sizeof(*channel), data_channel_helper_destroy);
+    if (!channel) {
+        EOE(RAWRTC_CODE_NO_MEMORY);
+        return;
+    }
+
+    // Set fields
+    channel->client = client;
+    EOE(rawrtc_strdup(&channel->label, label));
+
+    // Set pointer & done
+    *channel_helperp = channel;
+}
+
+/*
+ * Create a data channel helper instance from parameters.
+ */
+void data_channel_helper_create_from_channel(
+    struct data_channel_helper** const channel_helperp,  // de-referenced
+    struct rawrtc_data_channel* channel,
+    struct client* const client,
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    struct rawrtc_data_channel_parameters* parameters;
+    char* label;
+
+    // Allocate
+    struct data_channel_helper* const channel_helper =
+        mem_zalloc(sizeof(*channel_helper), data_channel_helper_destroy);
+    if (!channel_helper) {
+        EOE(RAWRTC_CODE_NO_MEMORY);
+        return;
+    }
+
+    // Get parameters
+    EOE(rawrtc_data_channel_get_parameters(&parameters, channel));
+
+    // Get & set label
+    error = rawrtc_data_channel_parameters_get_label(&label, parameters);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            EOE(rawrtc_strdup(&channel_helper->label, label));
+            mem_deref(label);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            EOE(rawrtc_strdup(&channel_helper->label, "n/a"));
+            break;
+        default:
+            EOE(error);
+    }
+
+    // Set fields
+    channel_helper->client = client;
+    channel_helper->channel = channel;
+    channel_helper->arg = mem_ref(arg);
+
+    // Set pointer
+    *channel_helperp = channel_helper;
+
+    // Un-reference & done
+    mem_deref(parameters);
+}
+
+/*
+ * Add the ICE candidate to the remote ICE transport if the ICE
+ * candidate type is enabled.
+ */
+void add_to_other_if_ice_candidate_type_enabled(
+    struct client* const client,
+    struct rawrtc_ice_candidate* const candidate,
+    struct rawrtc_ice_transport* const transport) {
+    if (candidate) {
+        enum rawrtc_ice_candidate_type type;
+
+        // Get ICE candidate type
+        EOE(rawrtc_ice_candidate_get_type(&type, candidate));
+
+        // Add to other client as remote candidate (if type enabled)
+        if (ice_candidate_type_enabled(client, type)) {
+            EOE(rawrtc_ice_transport_add_remote_candidate(transport, candidate));
+        }
+    } else {
+        // Last candidate is always being added
+        EOE(rawrtc_ice_transport_add_remote_candidate(transport, candidate));
+    }
+}
diff --git a/tools/helper/utils.h b/tools/helper/utils.h
new file mode 100644
index 0000000..d521b80
--- /dev/null
+++ b/tools/helper/utils.h
@@ -0,0 +1,90 @@
+#pragma once
+#include "common.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+/*
+ * Convert string to uint16.
+ */
+bool str_to_uint16(uint16_t* const numberp, char* const str);
+
+/*
+ * Convert string to uint64.
+ */
+bool str_to_uint64(uint64_t* const numberp, char* const str);
+
+/*
+ * Convert string to uint32.
+ */
+bool str_to_uint32(uint32_t* const numberp, char* const str);
+
+/*
+ * Get a dictionary entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_entry(
+    void* const valuep,
+    struct odict* const parent,
+    char* const key,
+    enum odict_type const type,
+    bool required);
+
+/*
+ * Get a uint32 entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_uint32(
+    uint32_t* const valuep, struct odict* const parent, char* const key, bool required);
+
+/*
+ * Get a uint16 entry and store it in `*valuep`.
+ */
+enum rawrtc_code dict_get_uint16(
+    uint16_t* const valuep, struct odict* const parent, char* const key, bool required);
+
+/*
+ * Get JSON from stdin and parse it to a dictionary.
+ */
+enum rawrtc_code get_json_stdin(struct odict** const dictp  // de-referenced
+);
+
+/*
+ * Get the ICE role from a string.
+ */
+enum rawrtc_code get_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    char const* const str);
+
+/*
+ * Get the congestion control algorithm from a string.
+ */
+enum rawrtc_code get_congestion_control_algorithm(
+    enum rawrtc_sctp_transport_congestion_ctrl* const algorithmp,  // de-referenced
+    char const* const str);
+
+/*
+ * Create a data channel helper instance.
+ */
+void data_channel_helper_create(
+    struct data_channel_helper** const channel_helperp,  // de-referenced
+    struct client* const client,
+    char* const label);
+
+/*
+ * Create a data channel helper instance from parameters.
+ */
+void data_channel_helper_create_from_channel(
+    struct data_channel_helper** const channel_helperp,  // de-referenced
+    struct rawrtc_data_channel* channel,
+    struct client* const client,
+    void* const arg  // nullable
+);
+
+/*
+ * Add the ICE candidate to the remote ICE transport if the ICE
+ * candidate type is enabled.
+ */
+void add_to_other_if_ice_candidate_type_enabled(
+    struct client* const client,
+    struct rawrtc_ice_candidate* const candidate,
+    struct rawrtc_ice_transport* const transport);
diff --git a/tools/ice-gatherer.c b/tools/ice-gatherer.c
new file mode 100644
index 0000000..bd490d7
--- /dev/null
+++ b/tools/ice-gatherer.c
@@ -0,0 +1,81 @@
+#include "helper/handler.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+
+#define DEBUG_MODULE "ice-gatherer-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+/*
+ * Print the ICE gatherer's state. Stop once complete.
+ */
+static void gatherer_state_change_handler(
+    enum rawrtc_ice_gatherer_state const state,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    default_ice_gatherer_state_change_handler(state, arg);
+    if (state == RAWRTC_ICE_GATHERER_STATE_COMPLETE) {
+        re_cancel();
+    }
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_gatherer* gatherer;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    char* const stun_google_com_ip_urls[] = {"stun:[2a00:1450:400c:c08::7f]:19302",
+                                             "stun:74.125.140.127:19302"};
+    char* const unreachable_urls[] = {"stun:example.com:12345",
+                                      "stun:lets.assume.no-one-will-ever-register-this"};
+    struct client client = {0};
+    (void) argv;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, stun_google_com_ip_urls, ARRAY_SIZE(stun_google_com_ip_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, unreachable_urls, ARRAY_SIZE(unreachable_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Setup client
+    client.name = "A";
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &gatherer, gather_options, gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, default_ice_gatherer_local_candidate_handler, &client));
+
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(gatherer, NULL));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Close gatherer
+    EOE(rawrtc_ice_gatherer_close(gatherer));
+
+    // Un-reference & close
+    mem_deref(gatherer);
+    mem_deref(gather_options);
+
+    // Bye
+    before_exit();
+    return 0;
+}
diff --git a/tools/ice-transport-loopback.c b/tools/ice-transport-loopback.c
new file mode 100644
index 0000000..008c9a0
--- /dev/null
+++ b/tools/ice-transport-loopback.c
@@ -0,0 +1,152 @@
+#include "helper/handler.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "ice-transport-loopback-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+// Note: Shadows struct client
+struct ice_transport_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_parameters* ice_parameters;
+    enum rawrtc_ice_role role;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct ice_transport_client* other_client;
+};
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct ice_transport_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Add to other client as remote candidate (if type enabled)
+    add_to_other_if_ice_candidate_type_enabled(arg, candidate, client->other_client->ice_transport);
+}
+
+static void client_init(struct ice_transport_client* const local) {
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &local->gatherer, local->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, local));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &local->ice_transport, local->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, local));
+}
+
+static void client_start(
+    struct ice_transport_client* const local, struct ice_transport_client* const remote) {
+    // Get & set ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(&local->ice_parameters, remote->gatherer));
+
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(local->gatherer, NULL));
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        local->ice_transport, local->gatherer, local->ice_parameters, local->role));
+}
+
+static void client_stop(struct ice_transport_client* const client) {
+    // Stop transport & close gatherer
+    EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    EOE(rawrtc_ice_gatherer_close(client->gatherer));
+
+    // Un-reference & close
+    client->ice_parameters = mem_deref(client->ice_parameters);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct ice_transport_client a = {0};
+    struct ice_transport_client b = {0};
+    (void) a.ice_candidate_types;
+    (void) a.n_ice_candidate_types;
+    (void) b.ice_candidate_types;
+    (void) b.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc > 1) {
+        ice_candidate_types = &argv[1];
+        n_ice_candidate_types = (size_t) argc - 1;
+    }
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Setup client A
+    a.name = "A";
+    a.ice_candidate_types = ice_candidate_types;
+    a.n_ice_candidate_types = n_ice_candidate_types;
+    a.gather_options = gather_options;
+    a.role = RAWRTC_ICE_ROLE_CONTROLLING;
+    a.other_client = &b;
+
+    // Setup client B
+    b.name = "B";
+    b.ice_candidate_types = ice_candidate_types;
+    b.n_ice_candidate_types = n_ice_candidate_types;
+    b.gather_options = gather_options;
+    b.role = RAWRTC_ICE_ROLE_CONTROLLED;
+    b.other_client = &a;
+
+    // Initialise clients
+    client_init(&a);
+    client_init(&b);
+
+    // Start clients
+    client_start(&a, &b);
+    client_start(&b, &a);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, stop_on_return_handler, NULL));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop clients
+    client_stop(&a);
+    client_stop(&b);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+
+    // Free
+    mem_deref(gather_options);
+
+    // Bye
+    before_exit();
+    return 0;
+}
diff --git a/tools/meson.build b/tools/meson.build
new file mode 100644
index 0000000..62a1e59
--- /dev/null
+++ b/tools/meson.build
@@ -0,0 +1,32 @@
+# Build helper library
+subdir('helper')
+rawrtc_helper = static_library('rawrtc-helper', helper_sources,
+    dependencies: dependencies,
+    include_directories: include_dir)
+
+# Tools and their sources
+tools = {
+    'data-channel-sctp': files('data-channel-sctp.c'),
+    'data-channel-sctp-echo': files('data-channel-sctp-echo.c'),
+    'data-channel-sctp-loopback': files('data-channel-sctp-loopback.c'),
+    'data-channel-sctp-streamed': files('data-channel-sctp-streamed.c'),
+    'data-channel-sctp-throughput': files('data-channel-sctp-throughput.c'),
+    'dtls-transport-loopback': files('dtls-transport-loopback.c'),
+    'ice-gatherer': files('ice-gatherer.c'),
+    'ice-transport-loopback': files('ice-transport-loopback.c'),
+    'peer-connection': files('peer-connection.c'),
+}
+if get_option('sctp_redirect_transport')
+    tools += {'sctp-redirect-transport': files('sctp-redirect-transport.c')}
+endif
+
+# Build executables
+foreach name, sources : tools
+    executable(
+        name,
+        sources,
+        dependencies: [re_dep, rawrtcc_dep, rawrtcdc_dep],
+        include_directories: include_dir,
+        install: true,
+        link_with: [rawrtc, rawrtc_helper])
+endforeach
diff --git a/tools/peer-connection.c b/tools/peer-connection.c
new file mode 100644
index 0000000..1156244
--- /dev/null
+++ b/tools/peer-connection.c
@@ -0,0 +1,368 @@
+#include "helper/handler.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "peer-connection-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+enum {
+    TRANSPORT_BUFFER_LENGTH = 1048576,  // 1 MiB
+};
+
+// Note: Shadows struct client
+struct peer_connection_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    bool offering;
+    struct rawrtc_peer_connection_configuration* configuration;
+    struct rawrtc_peer_connection* connection;
+    struct data_channel_helper* data_channel_negotiated;
+    struct data_channel_helper* data_channel;
+};
+
+static void print_local_description(struct peer_connection_client* const client);
+
+static struct tmr timer = {0};
+
+static void timer_handler(void* arg) {
+    struct data_channel_helper* const channel = arg;
+    struct peer_connection_client* const client = (struct peer_connection_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+
+    // Compose message (16 KiB)
+    buffer = mbuf_alloc(1 << 14);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+
+    // Close if offering
+    if (client->offering) {
+        // Close bear-noises
+        DEBUG_PRINTF("(%s) Closing channel\n", client->name, channel->label);
+        EOR(rawrtc_data_channel_close(client->data_channel->channel));
+    }
+}
+
+static void data_channel_open_handler(void* const arg) {
+    struct data_channel_helper* const channel = arg;
+    struct peer_connection_client* const client = (struct peer_connection_client*) channel->client;
+    struct mbuf* buffer;
+    enum rawrtc_code error;
+
+    // Print open event
+    default_data_channel_open_handler(arg);
+
+    // Send data delayed on bear-noises
+    if (str_cmp(channel->label, "bear-noises") == 0) {
+        tmr_start(&timer, 30000, timer_handler, channel);
+        return;
+    }
+
+    // Compose message (8 KiB)
+    buffer = mbuf_alloc(1 << 13);
+    EOE(buffer ? RAWRTC_CODE_SUCCESS : RAWRTC_CODE_NO_MEMORY);
+    EOR(mbuf_fill(buffer, 'M', mbuf_get_space(buffer)));
+    mbuf_set_pos(buffer, 0);
+
+    // Send message
+    DEBUG_PRINTF("(%s) Sending %zu bytes\n", client->name, mbuf_get_left(buffer));
+    error = rawrtc_data_channel_send(channel->channel, buffer, true);
+    if (error) {
+        DEBUG_WARNING("Could not send, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    mem_deref(buffer);
+}
+
+static void negotiation_needed_handler(void* const arg) {
+    struct peer_connection_client* const client = arg;
+
+    // Print negotiation needed
+    default_negotiation_needed_handler(arg);
+
+    // Offering: Create and set local description
+    if (client->offering) {
+        struct rawrtc_peer_connection_description* description;
+        EOE(rawrtc_peer_connection_create_offer(&description, client->connection, false));
+        EOE(rawrtc_peer_connection_set_local_description(client->connection, description));
+        mem_deref(description);
+    }
+}
+
+static void connection_state_change_handler(
+    enum rawrtc_peer_connection_state const state,  // read-only
+    void* const arg) {
+    struct peer_connection_client* const client = arg;
+
+    // Print state
+    default_peer_connection_state_change_handler(state, arg);
+
+    // Open? Create new channel
+    // Note: Since this state can switch from 'connected' to 'disconnected' and back again, we
+    //       need to make sure we don't re-create data channels unintended.
+    // TODO: Move this once we can create data channels earlier
+    if (!client->data_channel && state == RAWRTC_PEER_CONNECTION_STATE_CONNECTED) {
+        struct rawrtc_data_channel_parameters* channel_parameters;
+        char* const label = client->offering ? "bear-noises" : "lion-noises";
+
+        // Create data channel helper for in-band negotiated data channel
+        data_channel_helper_create(&client->data_channel, (struct client*) client, label);
+
+        // Create data channel parameters
+        EOE(rawrtc_data_channel_parameters_create(
+            &channel_parameters, client->data_channel->label,
+            RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0, NULL, false, 0));
+
+        // Create data channel
+        EOE(rawrtc_peer_connection_create_data_channel(
+            &client->data_channel->channel, client->connection, channel_parameters,
+            data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+            default_data_channel_error_handler, default_data_channel_close_handler,
+            default_data_channel_message_handler, client->data_channel));
+
+        // Un-reference data channel parameters
+        mem_deref(channel_parameters);
+    }
+}
+
+static void local_candidate_handler(
+    struct rawrtc_peer_connection_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct peer_connection_client* const client = arg;
+
+    // Print local candidate
+    default_peer_connection_local_candidate_handler(candidate, url, arg);
+
+    // Print local description (if last candidate)
+    if (!candidate) {
+        print_local_description(client);
+    }
+}
+
+static void client_init(struct peer_connection_client* const client) {
+    struct rawrtc_data_channel_parameters* channel_parameters;
+
+    // Create peer connection
+    EOE(rawrtc_peer_connection_create(
+        &client->connection, client->configuration, negotiation_needed_handler,
+        local_candidate_handler, default_peer_connection_local_candidate_error_handler,
+        default_signaling_state_change_handler, default_ice_transport_state_change_handler,
+        default_ice_gatherer_state_change_handler, connection_state_change_handler,
+        default_data_channel_handler, client));
+
+    // Create data channel helper for pre-negotiated data channel
+    data_channel_helper_create(
+        &client->data_channel_negotiated, (struct client*) client, "cat-noises");
+
+    // Create data channel parameters
+    EOE(rawrtc_data_channel_parameters_create(
+        &channel_parameters, client->data_channel_negotiated->label,
+        RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED, 0, NULL, true, 0));
+
+    // Create pre-negotiated data channel
+    EOE(rawrtc_peer_connection_create_data_channel(
+        &client->data_channel_negotiated->channel, client->connection, channel_parameters,
+        data_channel_open_handler, default_data_channel_buffered_amount_low_handler,
+        default_data_channel_error_handler, default_data_channel_close_handler,
+        default_data_channel_message_handler, client->data_channel_negotiated));
+
+    // TODO: Create in-band negotiated data channel
+    // TODO: Return some kind of promise that resolves once the data channel can be created
+
+    // Un-reference data channel parameters
+    mem_deref(channel_parameters);
+}
+
+static void client_stop(struct peer_connection_client* const client) {
+    EOE(rawrtc_peer_connection_close(client->connection));
+
+    // Un-reference & close
+    client->data_channel = mem_deref(client->data_channel);
+    client->data_channel_negotiated = mem_deref(client->data_channel_negotiated);
+    client->connection = mem_deref(client->connection);
+    client->configuration = mem_deref(client->configuration);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void parse_remote_description(int flags, void* arg) {
+    struct peer_connection_client* const client = arg;
+    enum rawrtc_code error;
+    bool do_exit = false;
+    struct odict* dict = NULL;
+    char* type_str;
+    char* sdp;
+    enum rawrtc_sdp_type type;
+    struct rawrtc_peer_connection_description* remote_description = NULL;
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        do_exit = error == RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&type_str, dict, "type", ODICT_STRING, true);
+    error |= dict_get_entry(&sdp, dict, "sdp", ODICT_STRING, true);
+    if (error) {
+        DEBUG_WARNING("Invalid remote description\n");
+        goto out;
+    }
+
+    // Convert to description
+    error = rawrtc_str_to_sdp_type(&type, type_str);
+    if (error) {
+        DEBUG_WARNING("Invalid SDP type in remote description: '%s'\n", type_str);
+        goto out;
+    }
+    error = rawrtc_peer_connection_description_create(&remote_description, type, sdp);
+    if (error) {
+        DEBUG_WARNING("Cannot parse remote description: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Set remote description
+    DEBUG_INFO("Applying remote description\n");
+    EOE(rawrtc_peer_connection_set_remote_description(client->connection, remote_description));
+
+    // Answering: Create and set local description
+    if (!client->offering) {
+        struct rawrtc_peer_connection_description* local_description;
+        EOE(rawrtc_peer_connection_create_answer(&local_description, client->connection));
+        EOE(rawrtc_peer_connection_set_local_description(client->connection, local_description));
+        mem_deref(local_description);
+    }
+
+out:
+    // Un-reference
+    mem_deref(remote_description);
+    mem_deref(dict);
+
+    // Exit?
+    if (do_exit) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        tmr_cancel(&timer);
+        re_cancel();
+    }
+}
+
+static void print_local_description(struct peer_connection_client* const client) {
+    struct rawrtc_peer_connection_description* description;
+    enum rawrtc_sdp_type type;
+    char* sdp;
+    struct odict* dict;
+
+    // Get description
+    EOE(rawrtc_peer_connection_get_local_description(&description, client->connection));
+
+    // Get SDP type & the SDP itself
+    EOE(rawrtc_peer_connection_description_get_sdp_type(&type, description));
+    EOE(rawrtc_peer_connection_description_get_sdp(&sdp, description));
+
+    // Create dict & add entries
+    EOR(odict_alloc(&dict, 16));
+    EOR(odict_entry_add(dict, "type", ODICT_STRING, rawrtc_sdp_type_to_str(type)));
+    EOR(odict_entry_add(dict, "sdp", ODICT_STRING, sdp));
+
+    // Print local description as JSON
+    DEBUG_INFO("Local Description:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+    mem_deref(sdp);
+    mem_deref(description);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING("Usage: %s <0|1 (offering)> [<ice-candidate-type> ...]", program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    struct rawrtc_peer_connection_configuration* configuration;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct peer_connection_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 2) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get role
+    // Note: We handle it as an ICE role (because that is pretty close)
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 3) {
+        ice_candidate_types = &argv[2];
+        n_ice_candidate_types = (size_t) argc - 2;
+    }
+
+    // Create peer connection configuration
+    EOE(rawrtc_peer_connection_configuration_create(&configuration, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to configuration
+    EOE(rawrtc_peer_connection_configuration_add_ice_server(
+        configuration, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Set the SCTP transport's buffer length
+    EOE(rawrtc_peer_connection_configuration_set_sctp_buffer_length(
+        configuration, TRANSPORT_BUFFER_LENGTH, TRANSPORT_BUFFER_LENGTH));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.configuration = configuration;
+    client.offering = role == RAWRTC_ICE_ROLE_CONTROLLING ? true : false;
+
+    // Setup client
+    client_init(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_description, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}
diff --git a/tools/sctp-redirect-transport.c b/tools/sctp-redirect-transport.c
new file mode 100644
index 0000000..ac1199e
--- /dev/null
+++ b/tools/sctp-redirect-transport.c
@@ -0,0 +1,370 @@
+#include "helper/handler.h"
+#include "helper/parameters.h"
+#include "helper/utils.h"
+#include <rawrtc.h>
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+#include <re.h>
+#include <stdlib.h>  // exit
+#include <string.h>  // memcpy
+#include <unistd.h>  // STDIN_FILENO
+
+#define DEBUG_MODULE "sctp-redirect-transport-app"
+#define DEBUG_LEVEL 7
+#include <re_dbg.h>
+
+struct parameters {
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_ice_candidates* ice_candidates;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct sctp_parameters sctp_parameters;
+};
+
+// Note: Shadows struct client
+struct sctp_redirect_transport_client {
+    char* name;
+    char** ice_candidate_types;
+    size_t n_ice_candidate_types;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* redirect_ip;
+    uint16_t redirect_port;
+    enum rawrtc_ice_role role;
+    struct rawrtc_certificate* certificate;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_sctp_redirect_transport* sctp_redirect_transport;
+    struct parameters local_parameters;
+    struct parameters remote_parameters;
+};
+
+static void print_local_parameters(struct sctp_redirect_transport_client* client);
+
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const candidate,
+    char const* const url,  // read-only
+    void* const arg) {
+    struct sctp_redirect_transport_client* const client = arg;
+
+    // Print local candidate
+    default_ice_gatherer_local_candidate_handler(candidate, url, arg);
+
+    // Print local parameters (if last candidate)
+    if (!candidate) {
+        print_local_parameters(client);
+    }
+}
+
+static void client_init(struct sctp_redirect_transport_client* const client) {
+    struct rawrtc_certificate* certificates[1];
+
+    // Generate certificates
+    EOE(rawrtc_certificate_generate(&client->certificate, NULL));
+    certificates[0] = client->certificate;
+
+    // Create ICE gatherer
+    EOE(rawrtc_ice_gatherer_create(
+        &client->gatherer, client->gather_options, default_ice_gatherer_state_change_handler,
+        default_ice_gatherer_error_handler, ice_gatherer_local_candidate_handler, client));
+
+    // Create ICE transport
+    EOE(rawrtc_ice_transport_create(
+        &client->ice_transport, client->gatherer, default_ice_transport_state_change_handler,
+        default_ice_transport_candidate_pair_change_handler, client));
+
+    // Create DTLS transport
+    EOE(rawrtc_dtls_transport_create(
+        &client->dtls_transport, client->ice_transport, certificates, ARRAY_SIZE(certificates),
+        default_dtls_transport_state_change_handler, default_dtls_transport_error_handler, client));
+
+    // Create SCTP redirect transport
+    EOE(rawrtc_sctp_redirect_transport_create(
+        &client->sctp_redirect_transport, client->dtls_transport, 0, client->redirect_ip,
+        client->redirect_port, default_sctp_redirect_transport_state_change_handler, client));
+}
+
+static void client_start_gathering(struct sctp_redirect_transport_client* const client) {
+    // Start gathering
+    EOE(rawrtc_ice_gatherer_gather(client->gatherer, NULL));
+}
+
+static void client_set_parameters(struct sctp_redirect_transport_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Set remote ICE candidates
+    EOE(rawrtc_ice_transport_set_remote_candidates(
+        client->ice_transport, remote_parameters->ice_candidates->candidates,
+        remote_parameters->ice_candidates->n_candidates));
+}
+
+static void client_start_transports(struct sctp_redirect_transport_client* const client) {
+    struct parameters* const remote_parameters = &client->remote_parameters;
+
+    // Start ICE transport
+    EOE(rawrtc_ice_transport_start(
+        client->ice_transport, client->gatherer, remote_parameters->ice_parameters, client->role));
+
+    // Start DTLS transport
+    EOE(rawrtc_dtls_transport_start(client->dtls_transport, remote_parameters->dtls_parameters));
+
+    // Start SCTP redirect transport
+    EOE(rawrtc_sctp_redirect_transport_start(
+        client->sctp_redirect_transport, remote_parameters->sctp_parameters.capabilities,
+        remote_parameters->sctp_parameters.port));
+}
+
+static void parameters_destroy(struct parameters* const parameters) {
+    // Un-reference
+    parameters->ice_parameters = mem_deref(parameters->ice_parameters);
+    parameters->ice_candidates = mem_deref(parameters->ice_candidates);
+    parameters->dtls_parameters = mem_deref(parameters->dtls_parameters);
+    if (parameters->sctp_parameters.capabilities) {
+        parameters->sctp_parameters.capabilities =
+            mem_deref(parameters->sctp_parameters.capabilities);
+    }
+}
+
+static void client_stop(struct sctp_redirect_transport_client* const client) {
+    if (client->sctp_redirect_transport) {
+        EOE(rawrtc_sctp_redirect_transport_stop(client->sctp_redirect_transport));
+    }
+    if (client->dtls_transport) {
+        EOE(rawrtc_dtls_transport_stop(client->dtls_transport));
+    }
+    if (client->ice_transport) {
+        EOE(rawrtc_ice_transport_stop(client->ice_transport));
+    }
+    if (client->gatherer) {
+        EOE(rawrtc_ice_gatherer_close(client->gatherer));
+    }
+
+    // Un-reference & close
+    parameters_destroy(&client->remote_parameters);
+    parameters_destroy(&client->local_parameters);
+    client->sctp_redirect_transport = mem_deref(client->sctp_redirect_transport);
+    client->dtls_transport = mem_deref(client->dtls_transport);
+    client->ice_transport = mem_deref(client->ice_transport);
+    client->gatherer = mem_deref(client->gatherer);
+    client->certificate = mem_deref(client->certificate);
+    client->gather_options = mem_deref(client->gather_options);
+
+    // Stop listening on STDIN
+    fd_close(STDIN_FILENO);
+}
+
+static void parse_remote_parameters(int flags, void* arg) {
+    struct sctp_redirect_transport_client* const client = arg;
+    enum rawrtc_code error;
+    struct odict* dict = NULL;
+    struct odict* node = NULL;
+    struct rawrtc_ice_parameters* ice_parameters = NULL;
+    struct rawrtc_ice_candidates* ice_candidates = NULL;
+    struct rawrtc_dtls_parameters* dtls_parameters = NULL;
+    struct sctp_parameters sctp_parameters = {0};
+    (void) flags;
+
+    // Get dict from JSON
+    error = get_json_stdin(&dict);
+    if (error) {
+        goto out;
+    }
+
+    // Decode JSON
+    error |= dict_get_entry(&node, dict, "iceParameters", ODICT_OBJECT, true);
+    error |= get_ice_parameters(&ice_parameters, node);
+    error |= dict_get_entry(&node, dict, "iceCandidates", ODICT_ARRAY, true);
+    error |= get_ice_candidates(&ice_candidates, node, arg);
+    error |= dict_get_entry(&node, dict, "dtlsParameters", ODICT_OBJECT, true);
+    error |= get_dtls_parameters(&dtls_parameters, node);
+    error |= dict_get_entry(&node, dict, "sctpParameters", ODICT_OBJECT, true);
+    error |= get_sctp_parameters(&sctp_parameters, node);
+
+    // Ok?
+    if (error) {
+        DEBUG_WARNING("Invalid remote parameters\n");
+        if (sctp_parameters.capabilities) {
+            mem_deref(sctp_parameters.capabilities);
+        }
+        goto out;
+    }
+
+    // Set parameters & start transports
+    client->remote_parameters.ice_parameters = mem_ref(ice_parameters);
+    client->remote_parameters.ice_candidates = mem_ref(ice_candidates);
+    client->remote_parameters.dtls_parameters = mem_ref(dtls_parameters);
+    memcpy(&client->remote_parameters.sctp_parameters, &sctp_parameters, sizeof(sctp_parameters));
+    DEBUG_INFO("Applying remote parameters\n");
+    client_set_parameters(client);
+    client_start_transports(client);
+
+out:
+    // Un-reference
+    mem_deref(dtls_parameters);
+    mem_deref(ice_candidates);
+    mem_deref(ice_parameters);
+    mem_deref(dict);
+
+    // Exit?
+    if (error == RAWRTC_CODE_NO_VALUE) {
+        DEBUG_NOTICE("Exiting\n");
+
+        // Stop client & bye
+        client_stop(client);
+        re_cancel();
+    }
+}
+
+static void client_get_parameters(struct sctp_redirect_transport_client* const client) {
+    struct parameters* const local_parameters = &client->local_parameters;
+
+    // Get local ICE parameters
+    EOE(rawrtc_ice_gatherer_get_local_parameters(
+        &local_parameters->ice_parameters, client->gatherer));
+
+    // Get local ICE candidates
+    EOE(rawrtc_ice_gatherer_get_local_candidates(
+        &local_parameters->ice_candidates, client->gatherer));
+
+    // Get local DTLS parameters
+    EOE(rawrtc_dtls_transport_get_local_parameters(
+        &local_parameters->dtls_parameters, client->dtls_transport));
+
+    // Get redirected local SCTP port
+    EOE(rawrtc_sctp_redirect_transport_get_port(
+        &local_parameters->sctp_parameters.port, client->sctp_redirect_transport));
+}
+
+static void print_local_parameters(struct sctp_redirect_transport_client* client) {
+    struct odict* dict;
+    struct odict* node;
+
+    // Get local parameters
+    client_get_parameters(client);
+
+    // Create dict
+    EOR(odict_alloc(&dict, 16));
+
+    // Create nodes
+    EOR(odict_alloc(&node, 16));
+    set_ice_parameters(client->local_parameters.ice_parameters, node);
+    EOR(odict_entry_add(dict, "iceParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_ice_candidates(client->local_parameters.ice_candidates, node);
+    EOR(odict_entry_add(dict, "iceCandidates", ODICT_ARRAY, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_dtls_parameters(client->local_parameters.dtls_parameters, node);
+    EOR(odict_entry_add(dict, "dtlsParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+    EOR(odict_alloc(&node, 16));
+    set_sctp_redirect_parameters(
+        client->sctp_redirect_transport, &client->local_parameters.sctp_parameters, node);
+    EOR(odict_entry_add(dict, "sctpParameters", ODICT_OBJECT, node));
+    mem_deref(node);
+
+    // Print JSON
+    DEBUG_INFO("Local Parameters:\n%H\n", json_encode_odict, dict);
+
+    // Un-reference
+    mem_deref(dict);
+}
+
+static void exit_with_usage(char* program) {
+    DEBUG_WARNING(
+        "Usage: %s <0|1 (ice-role)> <redirect-ip> <redirect-port> [<sctp-port>] "
+        "[<maximum-message-size>] [<ice-candidate-type> ...]",
+        program);
+    exit(1);
+}
+
+int main(int argc, char* argv[argc + 1]) {
+    char** ice_candidate_types = NULL;
+    size_t n_ice_candidate_types = 0;
+    enum rawrtc_ice_role role;
+    uint16_t redirect_port;
+    uint64_t maximum_message_size;
+    struct rawrtc_ice_gather_options* gather_options;
+    char* const turn_zwuenf_org_urls[] = {"stun:turn.zwuenf.org"};
+    struct sctp_redirect_transport_client client = {0};
+    (void) client.ice_candidate_types;
+    (void) client.n_ice_candidate_types;
+
+    // Debug
+    dbg_init(DBG_DEBUG, DBG_ALL);
+    DEBUG_PRINTF("Init\n");
+
+    // Initialise
+    EOE(rawrtc_init(true));
+
+    // Check arguments length
+    if (argc < 4) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get ICE role
+    if (get_ice_role(&role, argv[1])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get redirect port
+    if (!str_to_uint16(&redirect_port, argv[3])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get SCTP port (optional)
+    if (argc >= 5 && !str_to_uint16(&client.local_parameters.sctp_parameters.port, argv[4])) {
+        exit_with_usage(argv[0]);
+    }
+
+    // Get maximum message size (optional)
+    if (argc >= 6 && !str_to_uint64(&maximum_message_size, argv[5])) {
+        exit_with_usage(argv[0]);
+    } else {
+        maximum_message_size = 0;
+    }
+
+    // Get enabled ICE candidate types to be added (optional)
+    if (argc >= 7) {
+        ice_candidate_types = &argv[6];
+        n_ice_candidate_types = (size_t) argc - 6;
+    }
+
+    // Create local SCTP capabilities
+    rawrtc_sctp_capabilities_create(
+        &client.local_parameters.sctp_parameters.capabilities, maximum_message_size);
+
+    // Create ICE gather options
+    EOE(rawrtc_ice_gather_options_create(&gather_options, RAWRTC_ICE_GATHER_POLICY_ALL));
+
+    // Add ICE servers to ICE gather options
+    EOE(rawrtc_ice_gather_options_add_server(
+        gather_options, turn_zwuenf_org_urls, ARRAY_SIZE(turn_zwuenf_org_urls), NULL, NULL,
+        RAWRTC_ICE_CREDENTIAL_TYPE_NONE));
+
+    // Set client fields
+    client.name = "A";
+    client.ice_candidate_types = ice_candidate_types;
+    client.n_ice_candidate_types = n_ice_candidate_types;
+    client.gather_options = gather_options;
+    client.role = role;
+    client.redirect_ip = argv[2];
+    client.redirect_port = redirect_port;
+
+    // Setup client
+    client_init(&client);
+
+    // Start gathering
+    client_start_gathering(&client);
+
+    // Listen on stdin
+    EOR(fd_listen(STDIN_FILENO, FD_READ, parse_remote_parameters, &client));
+
+    // Start main loop
+    EOR(re_main(default_signal_handler));
+
+    // Stop client & bye
+    client_stop(&client);
+    before_exit();
+    return 0;
+}