Squashed 'third_party/rawrtc/rawrtc-data-channel/' content from commit 7b1b8d57c

Change-Id: I84850720e2b51961981d55f67238f4d282314fff
git-subtree-dir: third_party/rawrtc/rawrtc-data-channel
git-subtree-split: 7b1b8d57c6d07da18cc0de8bbca8cc5e8bd06eae
diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 0000000..be44098
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,37 @@
+os: Visual Studio 2017
+
+platform:
+  - x64
+
+environment:
+  # Build matrix
+  matrix:
+    - arch: x86
+      compiler: msvc2015
+    - arch: x64
+      compiler: msvc2015
+    - arch: x86
+      compiler: msvc2017
+    - arch: x64
+      compiler: msvc2017
+
+install:
+  # Download ninja
+  - cmd: mkdir C:\ninja-build
+  - ps: (new-object net.webclient).DownloadFile('https://github.com/mesonbuild/cidata/raw/master/ninja.exe', 'C:\ninja-build\ninja.exe')
+  # Set python root based on architecture
+  - cmd: if %arch%==x86 (set PYTHON_ROOT=C:\python37) else (set PYTHON_ROOT=C:\python37-x64)
+  # Add necessary paths to PATH variable
+  - cmd: set PATH=%cd%;C:\ninja-build;%PYTHON_ROOT%;%PYTHON_ROOT%\Scripts;%PATH%
+  # Install meson
+  - cmd: pip install meson
+  # Set up the build environment
+  - cmd: if %compiler%==msvc2015 ( call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %arch% )
+  - cmd: if %compiler%==msvc2017 ( call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" %arch% )
+
+build_script:
+  - cmd: echo Building on %arch% with %compiler%
+  # Configure
+  - cmd: meson --backend=ninja build
+  # Build
+  - cmd: ninja -C build
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..bffc990
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,184 @@
+version: 2
+
+shared:
+  host: &shared-host
+    working_directory: ~/rawrtc-data-channel
+    steps:
+      - checkout
+
+      # Configure library
+      - run:
+          name: Configure
+          command: |
+            mkdir build
+            meson build --prefix /tmp/prefix
+
+      # Build library
+      - run:
+          name: Build
+          command: |
+            cd build
+            ninja install
+
+  cross: &shared-cross
+    working_directory: ~/rawrtc-data-channel
+    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..666fc71
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,74 @@
+---
+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: '^<rawrtcdc'
+    Priority: 3
+  - Regex: '^<(re\.h|rawrtcc|usrsctp)'
+    Priority: 4
+  - Regex: '.*'
+    Priority: 5
+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..b9db27c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# 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 folders
+/build*
+/docs/_build
+/docs/_xml
+
+# Generated files
+/docs/Doxyfile
+/docs/conf.py
+
+# Subprojects
+/subprojects/*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..dd200dc
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,29 @@
+# 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
+    update: true
+
+# Build library
+script:
+  - |
+    mkdir build
+    meson build --prefix /tmp/prefix
+    cd build
+    ninja install
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8aaa342
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+## [0.1.3] (2019-08-15)
+
+* Add getter/setter for send/receive buffer length
+* Add getter/setter for congestion control algorithm
+
+## [0.1.2] (2019-03-18)
+
+* Fix SSE 4.2 detection
+
+## [0.1.1] (2019-03-10)
+
+* Declare dependency in Meson
+
+## [0.1.0] (2019-03-02)
+
+* Initial release of RAWRTCDC
+
+
+
+[0.1.3]: https://github.com/rawrtc/rawrtc-data-channel/compare/v0.1.2...v0.1.3
+[0.1.2]: https://github.com/rawrtc/rawrtc-data-channel/compare/v0.1.1...v0.1.2
+[0.1.1]: https://github.com/rawrtc/rawrtc-data-channel/compare/v0.1.0...v0.1.1
+[0.1.0]: https://github.com/rawrtc/rawrtc-data-channel/compare/59e65d96a9feb4dd6b4a3b2f3a10ab7c067e2a60...v0.1.0
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..34a8d75
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2018, 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..fb532b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,336 @@
+# RAWRTCDC
+
+[![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 standalone [WebRTC][w3c-webrtc] and [ORTC][w3c-ortc] data channel
+implementation.
+
+## Features
+
+The following list represents all features that are planned for RAWRTCDC.
+Features with a check mark are already implemented.
+
+* [x] C-API based on the [W3C CG ORTC API][w3c-ortc]
+* [x] SCTP-based data channels [[draft-ietf-rtcweb-data-channel-13]][sctp-dc]
+  - [x] DCEP implementation [[draft-ietf-rtcweb-data-protocol-09]][dcep]
+  - [x] Support for arbitrarily sized messages
+  - [ ] Support for SCTP ndata [RFC 8260][sctp-ndata] (partially implemented,
+        see [#14][#14])
+  - [ ] Streaming mode (partially implemented, see [#16][#16])
+  - [x] Hardware CRC32-C checksum offloading (requires SSE4.2)
+
+## FAQ
+
+1. *Who should use this?*
+
+   If you have built a WebRTC stack and...
+   
+   * you want data channel support but you don't have it so far, or
+   * you don't want to maintain your own data channel implementation, or
+   * your data channel implementation is lacking some features,
+   
+   then you should consider integrating this library. :tada:
+
+2. *But I also need ICE/DTLS!*
+
+   Check out [RAWRTC][rawrtc].
+
+3. *How does it work?*
+
+   Basically, you pass in SCTP packets and you get out SCTP packets.
+   Put that on top of your DTLS transport and you're ready to go.
+   
+   See the [*Getting Started*](#getting-started) section on how to set it up.
+
+4. *Can I use it in an event loop?*
+
+   Yes.
+
+5. *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.
+
+6. *Does it create threads?*
+
+   No.
+
+7. *Is this a custom SCTP implementation?*
+
+   No, it uses [usrsctp](https://github.com/sctplab/usrsctp) underneath but
+   handles all the nitty-gritty details for you.
+
+## Prerequisites
+
+The following tools are required:
+
+* [git][git]
+* [ninja][ninja] >= 1.5
+* [meson][meson] >= 0.46.0
+* Optional: pkg-config (`pkgconf` for newer FreeBSD versions)
+
+## Build
+
+```bash
+cd <path-to-rawrtcdc>
+mkdir build
+meson build
+cd build
+ninja
+```
+
+## Getting Started
+
+Now that you've built the library, let's get started integrating this library
+into your stack.
+
+### Initialise
+
+Before doing anything, initialise the library:
+
+```c
+#include <rawrtcc.h>
+#include <rawrtcdc.h>
+
+[...]
+
+enum rawrtc_code error = rawrtcdc_init(init_re, timer_handler);
+if (error) {
+    your_log_function("Initialisation failed: %s", rawrtc_code_to_str(error));
+}
+```
+
+In the following code examples, the error handling will be omitted. But you of
+course still need to handle it in your code.
+
+Unless you're initialising [re][re] yourselves, the `init_re` parameter to
+`rawrtcdc_init` should be `true`. The second is a pointer to a timer handler.
+
+The timer handler function works in the following way (see comments inline):
+
+```c
+enum rawrtc_code timer_handler(bool const on, uint_fast16_t const interval) {
+    if (on) {
+        // Start a timer that calls `rawrtcdc_timer_tick` every `interval`
+        // milliseconds.
+    } else {
+        // Stop the timer.
+    }
+
+    // Indicate success. In case something fails for you, you can also
+    // backpropagate an appropriate error code here.
+    return RAWRTC_CODE_SUCCESS;
+}
+```
+
+### Create an SCTP transport
+
+Before you can create data channels, you will need to create an SCTP transport:
+
+```c
+// Create SCTP transport context
+struct rawrtc_sctp_transport_context context = {
+    .role_getter = dtls_role_getter,
+    .state_getter = dtls_transport_state_getter,
+    .outbound_handler = sctp_transport_outbound_handler,
+    .detach_handler = sctp_transport_detach_handler,
+    .destroyed_handler = sctp_transport_destroy,
+    .trace_packets = false,
+    .arg = your_reference,
+};
+
+// Create SCTP transport
+struct rawrtc_sctp_transport transport;
+error = rawrtc_sctp_transport_create_from_external(
+    &transport, &context, local_sctp_port,
+    data_channel_handler, state_change_handler, arg);
+if (error) {
+    your_log_function("Creating SCTP transport failed: %s",
+                      rawrtc_code_to_str(error));
+    goto out;
+}
+
+// Attach your DTLS transport here
+```
+
+After the transport has been created successfully, `transport` will point to
+some dynamically allocated memory which is reference counted (and the reference
+counter value will be `1` after the function returned). If you want to increase
+the reference, call `mem_ref(transport)`. If you need to decrease the
+reference, call `mem_deref(transport)`. Once the counter value reaches `0`, it
+will run a destructor function and free the memory. However, you should
+normally stop the transport with `rawrtc_sctp_transport_stop` in a more
+graceful manner before doing so. We're pointing this out here since basically
+everything in this library that allocates dynamic memory works that way.
+
+Furthermore, from this moment on your DTLS transport should feed SCTP packets
+into the SCTP transport by calling
+`rawrtc_sctp_transport_feed_inbound(transport, buffer, ecn_bits)`. Check the
+[header file][sctp_transport.h] for details on the parameters.
+
+You're probably already wondering what the SCTP transport context is all about.
+Basically, it contains pointers to some handler functions you will need to
+define. The only exception is the `arg` field which let's you pass an arbitrary
+pointer to the various handler functions. So, let's go through them:
+
+```c
+enum rawrtc_code dtls_role_getter(
+    enum rawrtc_external_dtls_role* const rolep, void* const arg) {
+    // Set the local role of your DTLS transport
+    *rolep = your_dtls_transport.local_role;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+enum rawrtc_code dtls_transport_state_getter(
+    enum rawrtc_external_dtls_transport_state* const statep, void* const arg) {
+    // Set the state of your DTLS transport
+    *statep = your_dtls_transport.state;
+    return RAWRTC_CODE_SUCCESS;
+}
+```
+
+These DTLS handler functions were fairly straightforward. Now to the SCTP
+handler that hands back outbound SCTP packets. These packets will need to be
+fed into the DTLS transport as application data and sent to the other peer:
+
+```c
+enum rawrtc_code sctp_transport_outbound_handler(
+    struct mbuf* const buffer, uint8_t const tos, uint8_t const set_df,
+    void* const arg) {
+    // Feed the data to the DTLS transport
+    your_dtls_transport_send(buffer, tos, set_df);
+    return RAWRTC_CODE_SUCCESS;
+}
+```
+
+The `struct buffer` and its functions are documented [here][re-mbuf]. As a rule
+of thumb, you should call `mbuf_buf(buffer)` to get a pointer to the current
+position and `mbuf_get_left(buffer)` to get the amount of bytes left in the
+buffer.
+Be aware `buffer` in this case is not dynamically allocated and shall not be
+referenced. This has been done for optimisation purposes.
+Check the [header file][external.h] for further details on the various
+parameters passed to this handler function.
+
+The following handler is merely a way to let you know that you should not feed
+any data to the SCTP transport anymore:
+
+```c
+void sctp_transport_detach_handler(void* const arg) {
+    // Detach from DTLS transport
+    your_dtls_transport.stop_feeding_data = true;
+}
+```
+
+The last handler function we need to talk about is a way to tell you that the
+SCTP transport's reference count has been decreased to `0` and its about to be
+free'd. Be aware that you may not call any SCTP transport or data channel
+functions once this handler is being called.
+
+```c
+void sctp_transport_destroy(void* const arg) {
+    // Your cleanup code here
+}
+```
+
+The `trace_packets` attribute allows you to enable writing SCTP packets to a
+trace file. The name of that file is randomly generated and will be placed in
+the current working directory.
+
+That's all for the SCTP transport.
+
+### Create a Data Channel
+
+The data channel API is very similar to the one used in WebRTC and ORTC, so we
+will not go into detail for them. Here's a quick example on how to create a
+data channel with the following properties:
+
+* label: `meow`
+* protocol: `v1.cat-noises`
+* reliable
+* unordered
+* pre-negotiated
+* stream id is fixed to `42`
+
+```c
+// Create data channel parameters
+struct rawrtc_data_channel_parameters parameters;
+error = rawrtc_data_channel_parameters_create(
+    &parameters, "meow", RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED, 0,
+    "v1.cat-noises", true, 42);
+
+// Create the data channel, using the transport and the parameters
+struct rawrtc_data_channel channel;
+error = rawrtc_data_channel_create(
+    &channel, transport, parameters,
+    open_handler, buffered_amount_low_handler, error_handler,
+    close_handler, message_handler, pointer_passed_to_handlers);
+
+mem_deref(parameters);
+```
+
+Instead of adding handler functions on creation, you can also pass `NULL` and
+register handler functions later.
+
+For further explanation on the various parameters, check the
+[header files][headers].
+
+Once the SCTP transport goes into the *connected* state, the created channels
+will open. If you see this happening, this is a good indication that you've set
+everything up correctly. :clap:
+
+### Exit
+
+Once your code exits, you should call `rawrtcdc_close(close_re)`. If the
+`close_re` parameter is `true`, [re][re] will be closed as well.
+
+### Any Questions?
+
+![Draw The Rest Of The Owl Meme][owl-meme]
+
+Do you feel like this now? If yes, please join our [gitter chat][gitter], so we
+can help you and work out what's missing in this little tutorial.
+
+## 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-data-channel.svg?style=shield
+[circleci-url]: https://circleci.com/gh/rawrtc/rawrtc-data-channel
+[travis-ci-badge]: https://travis-ci.org/rawrtc/rawrtc-data-channel.svg?branch=master
+[travis-ci-url]: https://travis-ci.org/rawrtc/rawrtc-data-channel
+
+[gitter]: https://gitter.im/rawrtc/Lobby
+[gitter-icon]: https://badges.gitter.im/rawrtc/Lobby.svg
+
+[w3c-webrtc]: https://www.w3.org/TR/webrtc/
+[w3c-ortc]: https://draft.ortc.org
+[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
+[#14]: https://github.com/rawrtc/rawrtc-data-channel/issues/14
+[#16]: https://github.com/rawrtc/rawrtc-data-channel/issues/16
+[sctp-ndata]: https://tools.ietf.org/html/rfc8260
+
+[rawrtc]: https://github.com/rawrtc/rawrtc
+
+[git]: https://git-scm.com
+[meson]: https://mesonbuild.com
+[ninja]: https://ninja-build.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
+[usrsctp-neat-issue-12]: https://github.com/NEAT-project/usrsctp-neat/issues/12
+[sctp_transport.h]: include/rawrtcdc/sctp_transport.h
+[external.h]: include/rawrtcdc/external.h
+[headers]: include/rawrtcdc
+[re]: https://github.com/creytiv/re
+[re-mbuf]: http://www.creytiv.com/doxygen/re-dox/html/re__mbuf_8h.html
+[owl-meme]: ../assets/draw-the-rest-of-the-owl.jpg?raw=true
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/format-all.sh b/format-all.sh
new file mode 100755
index 0000000..ba846ae
--- /dev/null
+++ b/format-all.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+for dir in "include" "src"; do
+    pushd "${dir}" &>/dev/null
+    find . \
+        -type f \
+        \( -name "*.c" -o -name "*.h" \) \
+        -exec clang-format -i '{}' \;
+    popd &>/dev/null
+done
diff --git a/include/meson.build b/include/meson.build
new file mode 100644
index 0000000..15f8f29
--- /dev/null
+++ b/include/meson.build
@@ -0,0 +1,3 @@
+# Install headers
+install_headers(files('rawrtcdc.h'))
+subdir('rawrtcdc')
diff --git a/include/rawrtcdc.h b/include/rawrtcdc.h
new file mode 100644
index 0000000..68f62a3
--- /dev/null
+++ b/include/rawrtcdc.h
@@ -0,0 +1,13 @@
+#pragma once
+#include "rawrtcdc/config.h"
+
+#include "rawrtcdc/data_channel.h"
+#include "rawrtcdc/data_channel_parameters.h"
+#include "rawrtcdc/data_transport.h"
+#include "rawrtcdc/external.h"
+#include "rawrtcdc/main.h"
+#include "rawrtcdc/sctp_capabilities.h"
+#if RAWRTCDC_HAVE_SCTP_REDIRECT_TRANSPORT
+#    include "rawrtcdc/sctp_redirect_transport.h"
+#endif
+#include "rawrtcdc/sctp_transport.h"
diff --git a/include/rawrtcdc/config.h.in b/include/rawrtcdc/config.h.in
new file mode 100644
index 0000000..3ca6f05
--- /dev/null
+++ b/include/rawrtcdc/config.h.in
@@ -0,0 +1,21 @@
+#pragma once
+
+/// Current version of the library.
+///
+/// Follows [Semantic Versioning 2.0.0](https://semver.org)
+#mesondefine RAWRTCDC_VERSION
+#mesondefine RAWRTCDC_VERSION_MAJOR
+#mesondefine RAWRTCDC_VERSION_MINOR
+#mesondefine RAWRTCDC_VERSION_PATCH
+
+/// Debug level
+#mesondefine RAWRTC_DEBUG_LEVEL
+
+/// Whether hardware CRC32-C has been enabled
+///
+/// Note: This does not necessarily mean that hardware CRC32-C will be used
+///       which depends on whether your CPU supports SSE 4.2 at runtime.
+#mesondefine RAWRTCDC_ENABLE_SSE42_CRC32C
+
+/// Whether the SCTP redirect transport has been compiled
+#mesondefine RAWRTCDC_HAVE_SCTP_REDIRECT_TRANSPORT
diff --git a/include/rawrtcdc/data_channel.h b/include/rawrtcdc/data_channel.h
new file mode 100644
index 0000000..423aafd
--- /dev/null
+++ b/include/rawrtcdc/data_channel.h
@@ -0,0 +1,272 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_data_channel_parameters;
+struct rawrtc_data_transport;
+
+/**
+ * Data channel types.
+ */
+enum rawrtc_data_channel_type {
+    RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED = 0x00,
+    RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED = 0x80,
+    RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_RETRANSMIT = 0x01,
+    RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_RETRANSMIT = 0x81,
+    RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_TIMED = 0x02,
+    RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_TIMED = 0x82,
+};  // IMPORTANT: If you add a new type, ensure that every data channel transport handles it
+    //            correctly! Also, ensure this still works with the unordered bit flag above or
+    //            update the implementations.
+
+/**
+ * Data channel message flags.
+ */
+enum rawrtc_data_channel_message_flag {
+    RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_ABORTED = 1 << 0,
+    RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_COMPLETE = 1 << 1,
+    RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_STRING = 1 << 2,
+    RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_BINARY = 1 << 3,
+};
+
+/**
+ * Data channel state.
+ */
+enum rawrtc_data_channel_state {
+    RAWRTC_DATA_CHANNEL_STATE_CONNECTING,
+    RAWRTC_DATA_CHANNEL_STATE_OPEN,
+    RAWRTC_DATA_CHANNEL_STATE_CLOSING,
+    RAWRTC_DATA_CHANNEL_STATE_CLOSED,
+};
+
+/**
+ * Data channel.
+ */
+struct rawrtc_data_channel;
+
+/**
+ * Data channel open handler.
+ */
+typedef void (*rawrtc_data_channel_open_handler)(void* const arg);
+
+/**
+ * Data channel buffered amount low handler.
+ */
+typedef void (*rawrtc_data_channel_buffered_amount_low_handler)(void* const arg);
+
+/**
+ * Data channel error handler.
+ */
+typedef void (*rawrtc_data_channel_error_handler)(void* const arg);  // TODO: error parameter
+
+/**
+ * Data channel close handler.
+ */
+typedef void (*rawrtc_data_channel_close_handler)(void* const arg);
+
+/**
+ * Data channel message handler.
+ *
+ * Note: `buffer` may be NULL in case partial delivery has been
+ *       requested and a message has been aborted (this can only happen
+ *       on partially reliable channels).
+ */
+typedef void (*rawrtc_data_channel_message_handler)(
+    struct mbuf* const buffer,  // nullable (in case partial delivery has been requested)
+    enum rawrtc_data_channel_message_flag const flags,
+    void* const arg);
+
+/**
+ * Data channel handler.
+ *
+ * Note: You should call `rawrtc_data_channel_set_streaming`
+ *       in this handler before doing anything else if you want to
+ *       enable streamed delivery of data for this channel from the
+ *       beginning of the first incoming message.
+ */
+typedef void (*rawrtc_data_channel_handler)(
+    struct rawrtc_data_channel* const data_channel,  // read-only, MUST be referenced when used
+    void* const arg);
+
+/**
+ * Create a data channel.
+ * `*channelp` must be unreferenced.
+ *
+ * Note: You should call `rawrtc_data_channel_set_streaming`
+ *       directly after this function returned if you want to enable
+ *       streamed delivery of data for this channel from the beginning
+ *       of the first incoming message.
+ */
+enum rawrtc_code rawrtc_data_channel_create(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_data_transport* const transport,  // referenced
+    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
+);
+
+/**
+ * Set the argument of a data channel that is passed to the various
+ * handlers.
+ */
+enum rawrtc_code rawrtc_data_channel_set_arg(
+    struct rawrtc_data_channel* const channel,
+    void* const arg  // nullable
+);
+
+/**
+ * Send data via the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_send(
+    struct rawrtc_data_channel* const channel,
+    struct mbuf* const buffer,  // nullable (if empty message), referenced
+    bool const is_binary);
+
+/**
+ * Close the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_close(struct rawrtc_data_channel* const channel);
+
+/**
+ * Get the current state of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_get_state(
+    enum rawrtc_data_channel_state* const statep,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Get the currently buffered amount (bytes) of outgoing application
+ * data of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount(
+    uint64_t* const buffered_amountp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Set the data channel's buffered amount (bytes) low threshold for
+ * outgoing application data.
+ */
+enum rawrtc_code rawrtc_data_channel_set_buffered_amount_low_threshold(
+    struct rawrtc_data_channel* const channel, uint64_t const buffered_amount_low_threshold);
+
+/**
+ * Get the data channel's buffered amount (bytes) low threshold for
+ * outgoing application data.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount_low_threshold(
+    uint64_t* const buffered_amount_low_thresholdp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Unset the handler argument and all handlers of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_unset_handlers(struct rawrtc_data_channel* const channel);
+
+/**
+ * Get the data channel's parameters.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_get_parameters(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Enable or disable streamed delivery.
+ *
+ * Note: In case an incoming message is currently pending (there are
+ *       queued chunks in the internal reassembly buffer), this will
+ *       fail with an *invalid state* error.
+ */
+enum rawrtc_code rawrtc_data_channel_set_streaming(
+    struct rawrtc_data_channel* const channel, bool const on);
+
+/**
+ * Set the data channel's open handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_open_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_open_handler const open_handler  // nullable
+);
+
+/**
+ * Get the data channel's open handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_open_handler(
+    rawrtc_data_channel_open_handler* const open_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Set the data channel's buffered amount low handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_buffered_amount_low_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_buffered_amount_low_handler const buffered_amount_low_handler  // nullable
+);
+
+/**
+ * Get the data channel's buffered amount low handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount_low_handler(
+    rawrtc_data_channel_buffered_amount_low_handler* const
+        buffered_amount_low_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Set the data channel's error handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_error_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_error_handler const error_handler  // nullable
+);
+
+/**
+ * Get the data channel's error handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_error_handler(
+    rawrtc_data_channel_error_handler* const error_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Set the data channel's close handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_close_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_close_handler const close_handler  // nullable
+);
+
+/**
+ * Get the data channel's close handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_close_handler(
+    rawrtc_data_channel_close_handler* const close_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Set the data channel's message handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_message_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_message_handler const message_handler  // nullable
+);
+
+/**
+ * Get the data channel's message handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_message_handler(
+    rawrtc_data_channel_message_handler* const message_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel);
+
+/**
+ * Get the corresponding name for a data channel state.
+ */
+char const* rawrtc_data_channel_state_to_name(enum rawrtc_data_channel_state const state);
diff --git a/include/rawrtcdc/data_channel_parameters.h b/include/rawrtcdc/data_channel_parameters.h
new file mode 100644
index 0000000..9c91f37
--- /dev/null
+++ b/include/rawrtcdc/data_channel_parameters.h
@@ -0,0 +1,97 @@
+#pragma once
+#include "data_channel.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Data channel parameters.
+ */
+struct rawrtc_data_channel_parameters;
+
+/**
+ * Create data channel parameters.
+ *
+ * For `RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_*`, the reliability parameter
+ * is being ignored.
+ *
+ * When using `RAWRTC_DATA_CHANNEL_TYPE_*_RETRANSMIT`, the reliability
+ * parameter specifies the number of times a retransmission occurs if
+ * not acknowledged before the message is being discarded.
+ *
+ * When using `RAWRTC_DATA_CHANNEL_TYPE_*_TIMED`, the reliability
+ * parameter specifies the time window in milliseconds during which
+ * (re-)transmissions may occur before the message is being discarded.
+ *
+ * `*parametersp` must be unreferenced.
+ *
+ * In case `negotiated` is set to `false`, the `id` is being ignored.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_create(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    char const* const label,  // copied, nullable
+    enum rawrtc_data_channel_type const channel_type,
+    uint32_t const reliability_parameter,
+    char const* const protocol,  // copied, nullable
+    bool const negotiated,
+    uint16_t const id);
+
+/**
+ * Get the label from the data channel parameters.
+ * `*labelp` will be set to a copy of the parameter's label and must be
+ * unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no label has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*parameters*
+ * must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_label(
+    char** const labelp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
+
+/**
+ * Get the channel type from the data channel parameters.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_channel_type(
+    enum rawrtc_data_channel_type* const channel_typep,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
+
+/**
+ * Get the reliability parameter from the data channel parameters.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case the channel type is
+ * `RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_*`. Otherwise,
+ * `RAWRTC_CODE_SUCCESS` will be returned.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_reliability_parameter(
+    uint32_t* const reliability_parameterp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
+
+/**
+ * Get the protocol from the data channel parameters.
+ * `*protocolp` will be set to a copy of the parameter's protocol and
+ * must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no protocol has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*protocolp*
+ * must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_protocol(
+    char** const protocolp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
+
+/**
+ * Get the 'negotiated' flag from the data channel parameters.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_negotiated(
+    bool* const negotiatedp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
+
+/**
+ * Get the negotiated id from the data channel parameters.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case the 'negotiated' flag is set
+ * `false`. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_id(
+    uint16_t* const idp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters);
diff --git a/include/rawrtcdc/data_transport.h b/include/rawrtcdc/data_transport.h
new file mode 100644
index 0000000..e201aaa
--- /dev/null
+++ b/include/rawrtcdc/data_transport.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Data transport type.
+ */
+enum rawrtc_data_transport_type {
+    RAWRTC_DATA_TRANSPORT_TYPE_SCTP,
+};
+
+/**
+ * Generic data transport.
+ */
+struct rawrtc_data_transport;
+
+/**
+ * Get the data transport's type and underlying transport reference.
+ * `*internal_transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_transport_get_transport(
+    enum rawrtc_data_transport_type* const typep,  // de-referenced
+    void** const internal_transportp,  // de-referenced
+    struct rawrtc_data_transport* const transport);
+
+/**
+ * Translate a data transport type to str.
+ */
+char const* rawrtc_data_transport_type_to_str(enum rawrtc_data_transport_type const type);
diff --git a/include/rawrtcdc/external.h b/include/rawrtcdc/external.h
new file mode 100644
index 0000000..7e79063
--- /dev/null
+++ b/include/rawrtcdc/external.h
@@ -0,0 +1,99 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * External DTLS role.
+ */
+enum rawrtc_external_dtls_role {
+    RAWRTC_EXTERNAL_DTLS_ROLE_CLIENT,
+    RAWRTC_EXTERNAL_DTLS_ROLE_SERVER,
+};
+
+/**
+ * External DTLS transport state.
+ */
+enum rawrtc_external_dtls_transport_state {
+    RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_NEW_OR_CONNECTING,
+    RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CONNECTED,
+    RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED,
+};
+
+/**
+ * DTLS role getter.
+ *
+ * `*rolep` will contain the current external DTLS role.
+ * `arg` is the argument passed to the SCTP transport context.
+ *
+ * Return `RAWRTC_CODE_SUCCESS` in case the role has been set or any
+ * other code in case of an error.
+ */
+typedef enum rawrtc_code (*rawrtc_dtls_role_getter)(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced
+    void* const arg);
+
+/**
+ * DTLS transport state getter.
+ *
+ * `*statep` will contain the current external DTLS transport state.
+ * `arg` is the argument passed to the SCTP transport context.
+ *
+ * Return `RAWRTC_CODE_SUCCESS` in case the state has been set or any
+ * other code in case of an error.
+ */
+typedef enum rawrtc_code (*rawrtc_dtls_transport_state_getter)(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced
+    void* const arg);
+
+/**
+ * SCTP transport outbound data handler.
+ *
+ * `buffer` contains the data to be fed to the DTLS transport.
+ * Note that the `mbuf` structure shall not be `mem_ref`ed or
+ * `mem_deref`ed since it hasn't been allocated properly for
+ * optimisation purposes. This has been done since we expect you to
+ * either send this data directly or drop it. There's no need to hold
+ * data back. If you for any reason need the data after the callback
+ * returned, you are required to copy it.
+ * `tos` contains the type of service field as reported by usrsctp.
+ * `set_df` TODO: Probably don't fragment bit? Dunno...
+ *
+ * Return `RAWRTC_CODE_SUCCESS` in case the packet has been sent (or
+ * dropped) or any other code in case of an error.
+ */
+typedef enum rawrtc_code (*rawrtc_sctp_transport_outbound_handler)(
+    struct mbuf* const buffer, uint8_t const tos, uint8_t const set_df, void* const arg);
+
+/**
+ * SCTP transport detach handler.
+ * Will be called when the SCTP transport is about to be closed and
+ * should be detached from the underlying DTLS transport. At this
+ * point, no further data should be fed to the SCTP transport.
+ *
+ * `arg` is the argument passed to the SCTP transport context.
+ */
+typedef void (*rawrtc_sctp_transport_detach_handler)(void* const arg);
+
+/**
+ * SCTP transport destroyed handler.
+ * Will be called when the SCTP transport is about to be free'd.
+ *
+ * Note: This handler only exists for cleanup purposes. You may not use
+ *       any of the transport's functions at this point.
+ *
+ * `arg` is the argument passed to the SCTP transport context.
+ */
+typedef void (*rawrtc_sctp_transport_destroyed_handler)(void* const arg);
+
+/**
+ * SCTP transport context.
+ */
+struct rawrtc_sctp_transport_context {
+    rawrtc_dtls_role_getter role_getter;
+    rawrtc_dtls_transport_state_getter state_getter;
+    rawrtc_sctp_transport_outbound_handler outbound_handler;
+    rawrtc_sctp_transport_detach_handler detach_handler;  // nullable
+    rawrtc_sctp_transport_destroyed_handler destroyed_handler;  // nullable
+    bool trace_packets;
+    void* arg;  // nullable
+};
diff --git a/include/rawrtcdc/main.h b/include/rawrtcdc/main.h
new file mode 100644
index 0000000..41a7f80
--- /dev/null
+++ b/include/rawrtcdc/main.h
@@ -0,0 +1,37 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Timer handler.
+ *
+ * @param on If set to `true`, a timer needs to be scheduled to call
+ * rawrtcdc_timer_tick() every `interval` milliseconds.
+ * If set to `false`, the timer needs to be cancelled.
+ * @param interval the interval for the timer. Should be ignored in
+ * case `on` is `false`.
+ */
+typedef enum rawrtc_code (*rawrtcdc_timer_handler)(bool const on, uint_fast16_t const interval);
+
+/**
+ * Initialise RAWRTCDC. Must be called before making a call to any
+ * other function.
+ *
+ * Note: In case you override the default mutex used by re it's vital
+ *       that you create a recursive mutex or you will get deadlocks!
+ */
+enum rawrtc_code rawrtcdc_init(bool const init_re, rawrtcdc_timer_handler const timer_handler);
+
+/**
+ * Close RAWRTCDC and free up all resources.
+ *
+ * Note: In case `close_re` is not set to `true`, you MUST close
+ *       re yourselves.
+ */
+enum rawrtc_code rawrtcdc_close(bool const close_re);
+
+/**
+ * Handle timer tick.
+ * `delta` contains the delta milliseconds passed between calls.
+ */
+void rawrtcdc_timer_tick(uint_fast16_t const delta);
diff --git a/include/rawrtcdc/meson.build b/include/rawrtcdc/meson.build
new file mode 100644
index 0000000..d7dc6f7
--- /dev/null
+++ b/include/rawrtcdc/meson.build
@@ -0,0 +1,21 @@
+# Generate configuration header
+configure_file(
+    input: 'config.h.in',
+    output: 'config.h',
+    configuration: configuration,
+    install_dir: '/'.join([get_option('includedir'), 'rawrtcdc']))
+
+# Install headers
+includes = files([
+    'data_channel.h',
+    'data_channel_parameters.h',
+    'data_transport.h',
+    'external.h',
+    'main.h',
+    'sctp_capabilities.h',
+    'sctp_transport.h',
+])
+if get_option('sctp_redirect_transport')
+    includes += files('sctp_redirect_transport.h')
+endif
+install_headers(includes, subdir: 'rawrtcdc')
diff --git a/include/rawrtcdc/sctp_capabilities.h b/include/rawrtcdc/sctp_capabilities.h
new file mode 100644
index 0000000..995382f
--- /dev/null
+++ b/include/rawrtcdc/sctp_capabilities.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * SCTP capabilities.
+ */
+struct rawrtc_sctp_capabilities;
+
+/**
+ * Create a new SCTP transport capabilities instance.
+ * `*capabilitiesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_capabilities_create(
+    struct rawrtc_sctp_capabilities** const capabilitiesp,  // de-referenced
+    uint64_t const max_message_size);
+
+/**
+ * Get the SCTP parameter's maximum message size value.
+ *
+ * Note: A value of `0` indicates that the implementation supports
+ *       receiving messages of arbitrary size.
+ */
+enum rawrtc_code rawrtc_sctp_capabilities_get_max_message_size(
+    uint64_t* const max_message_sizep,  // de-referenced
+    struct rawrtc_sctp_capabilities* const capabilities);
+
+/**
+ * Print debug information for SCTP capabilities.
+ */
+int rawrtc_sctp_capabilities_debug(
+    struct re_printf* const pf, struct rawrtc_sctp_capabilities const* const capabilities);
diff --git a/include/rawrtcdc/sctp_redirect_transport.h b/include/rawrtcdc/sctp_redirect_transport.h
new file mode 100644
index 0000000..7d07f89
--- /dev/null
+++ b/include/rawrtcdc/sctp_redirect_transport.h
@@ -0,0 +1,106 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_sctp_capabilities;
+struct rawrtc_sctp_transport_context;
+
+/**
+ * SCTP redirect transport states.
+ */
+enum rawrtc_sctp_redirect_transport_state {
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_NEW,
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_OPEN,
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_CLOSED,
+};
+
+/**
+ * Redirect transport.
+ */
+struct rawrtc_sctp_redirect_transport;
+
+/**
+ * SCTP redirect transport state change handler.
+ */
+typedef void (*rawrtc_sctp_redirect_transport_state_change_handler)(
+    enum rawrtc_sctp_redirect_transport_state const state, void* const arg);
+
+/**
+ * Create an SCTP redirect transport from an external DTLS 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.
+ *
+ * Note: The underlying DTLS transport is supposed to be immediately
+ *       attached after creation of this transport.
+ * Important: The redirect transport requires to be run inside re's
+ *            event loop (`re_main`).
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_create_from_external(
+    struct rawrtc_sctp_redirect_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport_context* const context,  // copied
+    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
+);
+
+/**
+ * Start an SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_start(
+    struct rawrtc_sctp_redirect_transport* const transport,
+    struct rawrtc_sctp_capabilities const* const remote_capabilities,  // copied
+    uint16_t remote_port  // zeroable
+);
+
+/**
+ * Stop and close the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_stop(
+    struct rawrtc_sctp_redirect_transport* const transport);
+
+/**
+ * Feed inbound data to the SCTP redirect transport (that will be sent
+ * out via the raw socket).
+ *
+ * `buffer` contains the data to be fed to the raw transport. Since
+ * the data is not going to be referenced, you can pass a *fake* `mbuf`
+ * structure that hasn't been allocated with `mbuf_alloc` to avoid
+ * copying.
+ *
+ * Return `RAWRTC_CODE_INVALID_STATE` in case the transport is closed.
+ * In case the buffer could not be sent due to the raw socket's buffer
+ * being too full, `RAWRTC_CODE_TRY_AGAIN_LATER` will be returned. You
+ * can safely ignore this code since SCTP will retransmit data on a
+ * reliable stream.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` is being returned.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_feed_inbound(
+    struct rawrtc_sctp_redirect_transport* const transport, struct mbuf* const buffer);
+
+/**
+ * Get the current state of the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_get_state(
+    enum rawrtc_sctp_redirect_transport_state* const statep,  // de-referenced
+    struct rawrtc_sctp_redirect_transport* const transport);
+
+/**
+ * Get the redirected local SCTP port of the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_sctp_redirect_transport* const transport);
+
+/**
+ * Get the corresponding name for an SCTP redirect transport state.
+ */
+char const* rawrtc_sctp_redirect_transport_state_to_name(
+    enum rawrtc_sctp_redirect_transport_state const state);
diff --git a/include/rawrtcdc/sctp_transport.h b/include/rawrtcdc/sctp_transport.h
new file mode 100644
index 0000000..44460f0
--- /dev/null
+++ b/include/rawrtcdc/sctp_transport.h
@@ -0,0 +1,263 @@
+#pragma once
+#include "data_channel.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+// Dependencies
+struct rawrtc_data_transport;
+struct rawrtc_sctp_capabilities;
+struct rawrtc_sctp_transport_context;
+
+/**
+ * Missing in usrsctp.h
+ */
+#define SCTP_PLUGGABLE_CC 0x00001202
+
+/**
+ * SCTP transport checksum configuration flags.
+ * TODO: Add configuration to make these applyable.
+ */
+enum rawrtc_sctp_transport_checksum_flags {
+    RAWRTC_SCTP_TRANSPORT_CHECKSUM_ENABLE_ALL = 0,
+    RAWRTC_SCTP_TRANSPORT_CHECKSUM_DISABLE_INBOUND = 1 << 0,
+    RAWRTC_SCTP_TRANSPORT_CHECKSUM_DISABLE_OUTBOUND = 1 << 1,
+    RAWRTC_SCTP_TRANSPORT_CHECKSUM_DISABLE_ALL = (1 << 0 | 1 << 1),
+};
+
+/**
+ * SCTP transport state.
+ */
+enum rawrtc_sctp_transport_state {
+    RAWRTC_SCTP_TRANSPORT_STATE_NEW,
+    RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING,
+    RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED,
+    RAWRTC_SCTP_TRANSPORT_STATE_CLOSED,
+};
+
+/**
+ * SCTP transport congestion control algorithm.
+ *
+ * Note: RFC 2581 is the original default algorithm for TCP.
+ */
+enum rawrtc_sctp_transport_congestion_ctrl {
+    RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581,
+    RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HSTCP,
+    RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HTCP,
+    RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RTCC,
+};
+
+/**
+ * SCTP transport default MTU.
+ */
+enum {
+    RAWRTC_SCTP_TRANSPORT_DEFAULT_MTU = 1200,
+};
+
+/**
+ * SCTP transport.
+ */
+struct rawrtc_sctp_transport;
+
+/**
+ * SCTP transport state change handler.
+ */
+typedef void (*rawrtc_sctp_transport_state_change_handler)(
+    enum rawrtc_sctp_transport_state const state, void* const arg);
+
+/**
+ * Create an SCTP transport from an external DTLS transport.
+ * `*transportp` must be unreferenced.
+ *
+ * Note: The underlying DTLS transport is supposed to be immediately
+ *       attached after creation of this transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_create_from_external(
+    struct rawrtc_sctp_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport_context* const context,  // copied
+    uint16_t 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
+);
+
+/**
+ * Get the SCTP data transport instance.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_data_transport(
+    struct rawrtc_data_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport* const sctp_transport  // referenced
+);
+
+/**
+ * Start the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_start(
+    struct rawrtc_sctp_transport* const transport,
+    struct rawrtc_sctp_capabilities const* const remote_capabilities,  // copied
+    uint16_t remote_port  // zeroable
+);
+
+/**
+ * Stop and close the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_stop(struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Feed inbound data to the SCTP transport.
+ *
+ * `buffer` contains the data to be fed to the SCTP transport. Since
+ * the data is not going to be referenced, you can pass a *fake* `mbuf`
+ * structure that hasn't been allocated with `mbuf_alloc` to avoid
+ * copying.
+ * `ecn_bits` are the explicit congestion notification bits to be
+ * passed to usrsctp.
+ *
+ * Return `RAWRTC_CODE_INVALID_STATE` in case the transport is closed.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` is being returned.
+ */
+enum rawrtc_code rawrtc_sctp_transport_feed_inbound(
+    struct rawrtc_sctp_transport* const transport,
+    struct mbuf* const buffer,
+    uint8_t const ecn_bits);
+
+/**
+ * Set the SCTP transport's send and receive buffer length in bytes.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_buffer_length(
+    struct rawrtc_sctp_transport* const transport,
+    uint32_t const send_buffer_length,
+    uint32_t const receive_buffer_length);
+
+/**
+ * Get the SCTP transport's send and receive buffer length in bytes.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_buffer_length(
+    uint32_t* const send_buffer_lengthp,
+    uint32_t* const receive_buffer_lengthp,
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Set the SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_congestion_ctrl_algorithm(
+    struct rawrtc_sctp_transport* const transport,
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm);
+
+/**
+ * Get the current SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_congestion_ctrl_algorithm(
+    enum rawrtc_sctp_transport_congestion_ctrl* const algorithmp,
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Set the SCTP transport's maximum transmission unit (MTU).
+ * This will disable MTU discovery.
+ *
+ * Note: The MTU cannot be set before the SCTP transport has been
+ *       started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_mtu(
+    struct rawrtc_sctp_transport* const transport, uint32_t mtu);
+
+/**
+ * Get the current SCTP transport's maximum transmission unit (MTU)
+ * and an indication whether MTU discovery is enabled.
+ *
+ * Note: The MTU cannot be retrieved before the SCTP transport has been
+ *       started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_mtu(
+    uint32_t* const mtup,  // de-referenced
+    bool* const mtu_discovery_enabledp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Enable MTU discovery for the SCTP transport.
+ *
+ * Note: MTU discovery cannot be enabled before the SCTP transport has
+ *       been started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_enable_mtu_discovery(
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Set the SCTP transport's context.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_context(
+    struct rawrtc_sctp_transport* const transport,
+    struct rawrtc_sctp_transport_context* const context  // copied
+);
+
+/**
+ * Get the current state of the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_state(
+    enum rawrtc_sctp_transport_state* const statep,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Get the local port of the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Get the number of streams allocated for the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_n_streams(
+    uint16_t* const n_streamsp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Get the local SCTP transport capabilities (static).
+ * `*capabilitiesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_capabilities(
+    struct rawrtc_sctp_capabilities** const capabilitiesp  // de-referenced
+);
+
+/**
+ * Set the SCTP transport's data channel handler.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_data_channel_handler(
+    struct rawrtc_sctp_transport* const transport,
+    rawrtc_data_channel_handler const data_channel_handler  // nullable
+);
+
+/**
+ * Get the SCTP transport's data channel handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_data_channel_handler(
+    rawrtc_data_channel_handler* const data_channel_handlerp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Set the SCTP transport's state change handler.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_state_change_handler(
+    struct rawrtc_sctp_transport* const transport,
+    rawrtc_sctp_transport_state_change_handler const state_change_handler  // nullable
+);
+
+/**
+ * Get the SCTP transport's state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_state_change_handler(
+    rawrtc_sctp_transport_state_change_handler* const state_change_handlerp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport);
+
+/**
+ * Get the corresponding name for an SCTP transport state.
+ */
+char const* rawrtc_sctp_transport_state_to_name(enum rawrtc_sctp_transport_state const state);
+
+/*
+ * Get the corresponding name for a congestion control algorithm.
+ */
+char const* rawrtc_sctp_transport_congestion_ctrl_algorithm_to_name(
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm);
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..23a2630
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,117 @@
+# Project definition
+project('rawrtcdc', 'c',
+    version: '0.1.3',
+    default_options: ['c_std=c99'],
+    meson_version: '>=0.46.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: 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: usrsctp
+sctp_debug = get_option('debug_level') >= 7
+usrsctp_dep = dependency('usrsctp',
+    version: ['>=1.0.0', '<2'],
+    fallback: ['usrsctp', 'usrsctp_dep'],
+    default_options: [
+        'sctp_build_programs=false',
+        'sctp_debug=@0@'.format(sctp_debug),
+        'sctp_inet=false',
+        'sctp_inet6=false',
+    ],
+    required: true)
+
+# Dependency: rawrtcc
+rawrtcc_dep = dependency('rawrtcc',
+    version: '>=0.1.3',
+    fallback: ['rawrtcc', 'rawrtcc_dep'],
+    required: true)
+
+# Dependencies list
+dependencies = [
+    re_dep,
+    usrsctp_dep,
+    rawrtcc_dep,
+]
+
+# Feature: Hardware CRC32-C (requires SSE 4.2)
+# Note: If it compiles, it only enables the check for SSE 4.2 at runtime.
+code = '''
+#include <stdint.h>
+int main(int argc, char* argv[]) {
+    uint32_t eax, ecx;
+    eax = 1;
+    __asm__("cpuid" : "=c"(ecx) : "a"(eax) : "%ebx", "%edx");
+    return 0;
+}
+'''
+have_cpuid = compiler.compiles(code) and host_machine.cpu() == 'x86_64'
+configuration.set10('RAWRTCDC_ENABLE_SSE42_CRC32C', have_cpuid)
+
+# Options
+configuration.set10('RAWRTCDC_HAVE_SCTP_REDIRECT_TRANSPORT', get_option('sctp_redirect_transport'))
+
+# Version
+version = meson.project_version()
+version_array = version.split('.')
+configuration.set_quoted('RAWRTCDC_VERSION', version)
+configuration.set('RAWRTCDC_VERSION_MAJOR', version_array[0])
+configuration.set('RAWRTCDC_VERSION_MINOR', version_array[1])
+configuration.set('RAWRTCDC_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
+rawrtcdc = library(meson.project_name(), sources,
+    dependencies: dependencies,
+    include_directories: include_dir,
+    install: true,
+    version: version)
+
+# Declare dependency
+rawrtcdc_dep = declare_dependency(
+    include_directories: include_dir,
+    link_with: rawrtcdc)
+
+# Generate pkg-config file
+pkg = import('pkgconfig')
+pkg.generate(rawrtcdc,
+    name: meson.project_name(),
+    description: 'A standalone WebRTC and ORTC data channel implementation.',
+    url: 'https://github.com/rawrtc/rawrtc-data-channel')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..934989c
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,4 @@
+option('debug_level', type: 'integer', min: 0, max: 7, value: 5,
+    description: 'Global debug level', yield: true)
+option('sctp_redirect_transport', type: 'boolean', value: false,
+    description: 'Build SCTP redirect transport', yield: true)
diff --git a/src/crc32c/crc32c.c b/src/crc32c/crc32c.c
new file mode 100644
index 0000000..d22550e
--- /dev/null
+++ b/src/crc32c/crc32c.c
@@ -0,0 +1,36 @@
+#include "crc32c.h"
+#include "software.h"
+#include "../main/main.h"
+#include <rawrtcdc/config.h>
+#include <re.h>
+
+#if RAWRTCDC_ENABLE_SSE42_CRC32C
+#    include "sse42.h"
+#endif
+
+#define DEBUG_MODULE "crc32c"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Initialise CRC32-C.
+ */
+void rawrtc_crc32c_init(void) {
+#if RAWRTCDC_ENABLE_SSE42_CRC32C
+    if (rawrtc_crc32c_sse42_supported()) {
+        rawrtc_crc32c_init_sse42();
+        rawrtcdc_global.crc32c_handler = rawrtc_crc32c_sse42;
+        DEBUG_PRINTF("Initialised CRC32-C (sse42)\n");
+        return;
+    }
+#endif
+    rawrtcdc_global.crc32c_handler = rawrtc_crc32c_software;
+    DEBUG_PRINTF("Initialised CRC32-C (software)\n");
+}
+
+/*
+ * Compute CRC-32C using whatever method has been established.
+ */
+uint32_t rawrtc_crc32c(void const* buffer, size_t length) {
+    return rawrtcdc_global.crc32c_handler(buffer, length);
+}
diff --git a/src/crc32c/crc32c.h b/src/crc32c/crc32c.h
new file mode 100644
index 0000000..591817c
--- /dev/null
+++ b/src/crc32c/crc32c.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <re.h>
+
+void rawrtc_crc32c_init(void);
+
+uint32_t rawrtc_crc32c(void const* buffer, size_t length);
diff --git a/src/crc32c/meson.build b/src/crc32c/meson.build
new file mode 100644
index 0000000..190919a
--- /dev/null
+++ b/src/crc32c/meson.build
@@ -0,0 +1,4 @@
+sources += files('crc32c.c')
+if have_cpuid
+    sources += files('sse42.c')
+endif
diff --git a/src/crc32c/software.h b/src/crc32c/software.h
new file mode 100644
index 0000000..090d00a
--- /dev/null
+++ b/src/crc32c/software.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <re.h>
+#include <usrsctp.h>
+
+static inline uint32_t rawrtc_crc32c_software(void const* buffer, size_t length) {
+    return usrsctp_crc32c((void*) buffer, length);
+}
diff --git a/src/crc32c/sse42.c b/src/crc32c/sse42.c
new file mode 100644
index 0000000..436479b
--- /dev/null
+++ b/src/crc32c/sse42.c
@@ -0,0 +1,260 @@
+/*
+ * crc32c.c -- compute CRC-32C using the Intel crc32 instruction
+ * Copyright (C) 2013 Mark Adler
+ * Version 1.1  1 Aug 2013  Mark Adler
+ *
+ * Use hardware CRC instruction on Intel SSE 4.2 processors.  This computes a
+ * CRC-32C, *not* the CRC-32 used by Ethernet and zip, gzip, etc.  A software
+ * version is provided as a fall-back, as well as for speed comparisons.
+ *
+ * (Modified for the use in RAWRTC. Software fallback has been stripped.)
+ *
+ *
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the author be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ *
+ * Mark Adler
+ * madler@alumni.caltech.edu
+ */
+#include "sse42.h"
+#include <re.h>
+
+/* CRC-32C (iSCSI) polynomial in reversed bit order. */
+#define POLY 0x82f63b78
+
+/* Multiply a matrix times a vector over the Galois field of two elements,
+   GF(2).  Each element is a bit in an unsigned integer.  mat must have at
+   least as many entries as the power of two for most significant one bit in
+   vec. */
+static inline uint32_t gf2_matrix_times(uint32_t* mat, uint32_t vec) {
+    uint32_t sum;
+
+    sum = 0;
+    while (vec) {
+        if (vec & 1) {
+            sum ^= *mat;
+        }
+        vec >>= 1;
+        mat++;
+    }
+    return sum;
+}
+
+/* Multiply a matrix by itself over GF(2).  Both mat and square must have 32
+   rows. */
+static inline void gf2_matrix_square(uint32_t* square, uint32_t* mat) {
+    int n;
+
+    for (n = 0; n < 32; n++) {
+        square[n] = gf2_matrix_times(mat, mat[n]);
+    }
+}
+
+/* Construct an operator to apply len zeros to a crc.  len must be a power of
+   two.  If len is not a power of two, then the result is the same as for the
+   largest power of two less than len.  The result for len == 0 is the same as
+   for len == 1.  A version of this routine could be easily written for any
+   len, but that is not needed for this application. */
+static void crc32c_zeros_op(uint32_t* even, size_t len) {
+    int n;
+    uint32_t row;
+    uint32_t odd[32]; /* odd-power-of-two zeros operator */
+
+    /* put operator for one zero bit in odd */
+    odd[0] = POLY; /* CRC-32C polynomial */
+    row = 1;
+    for (n = 1; n < 32; n++) {
+        odd[n] = row;
+        row <<= 1;
+    }
+
+    /* put operator for two zero bits in even */
+    gf2_matrix_square(even, odd);
+
+    /* put operator for four zero bits in odd */
+    gf2_matrix_square(odd, even);
+
+    /* first square will put the operator for one zero byte (eight zero bits),
+       in even -- next square puts operator for two zero bytes in odd, and so
+       on, until len has been rotated down to zero */
+    do {
+        gf2_matrix_square(even, odd);
+        len >>= 1;
+        if (len == 0) {
+            return;
+        }
+        gf2_matrix_square(odd, even);
+        len >>= 1;
+    } while (len);
+
+    /* answer ended up in odd -- copy to even */
+    for (n = 0; n < 32; n++) {
+        even[n] = odd[n];
+    }
+}
+
+/* Take a length and build four lookup tables for applying the zeros operator
+   for that length, byte-by-byte on the operand. */
+static void crc32c_zeros(uint32_t zeros[][256], size_t len) {
+    uint32_t n;
+    uint32_t op[32];
+
+    crc32c_zeros_op(op, len);
+    for (n = 0; n < 256; n++) {
+        zeros[0][n] = gf2_matrix_times(op, n);
+        zeros[1][n] = gf2_matrix_times(op, n << 8);
+        zeros[2][n] = gf2_matrix_times(op, n << 16);
+        zeros[3][n] = gf2_matrix_times(op, n << 24);
+    }
+}
+
+/* Apply the zeros operator table to crc. */
+static inline uint32_t crc32c_shift(uint32_t zeros[][256], uint32_t crc) {
+    // clang-format off
+    return zeros[0][crc & 0xff] ^ zeros[1][(crc >> 8) & 0xff] ^
+           zeros[2][(crc >> 16) & 0xff] ^ zeros[3][crc >> 24];
+    // clang-format on
+}
+
+/* Block sizes for three-way parallel crc computation.  LONG and SHORT must
+   both be powers of two.  The associated string constants must be set
+   accordingly, for use in constructing the assembler instructions. */
+#define LONG 8192
+#define LONGx1 "8192"
+#define LONGx2 "16384"
+#define SHORT 256
+#define SHORTx1 "256"
+#define SHORTx2 "512"
+
+/* Tables for hardware crc that shift a crc by LONG and SHORT zeros. */
+static uint32_t crc32c_long[4][256];
+static uint32_t crc32c_short[4][256];
+
+/* Compute CRC-32C using the Intel hardware instruction. */
+static uint32_t crc32c_hw(uint32_t crc, const void* buf, size_t len) {
+    const unsigned char* next = buf;
+    const unsigned char* end;
+    uint64_t crc0, crc1, crc2; /* need to be 64 bits for crc32q */
+
+    /* pre-process the crc */
+    crc0 = crc ^ 0xffffffff;
+
+    /* compute the crc for up to seven leading bytes to bring the data pointer
+       to an eight-byte boundary */
+    while (len && ((uintptr_t) next & 7) != 0) {
+        __asm__("crc32b\t"
+                "(%1), %0"
+                : "=r"(crc0)
+                : "r"(next), "0"(crc0));
+        next++;
+        len--;
+    }
+
+    /* compute the crc on sets of LONG*3 bytes, executing three independent crc
+       instructions, each on LONG bytes -- this is optimized for the Nehalem,
+       Westmere, Sandy Bridge, and Ivy Bridge architectures, which have a
+       throughput of one crc per cycle, but a latency of three cycles */
+    while (len >= LONG * 3) {
+        crc1 = 0;
+        crc2 = 0;
+        end = next + LONG;
+        do {
+            __asm__("crc32q\t"
+                    "(%3), %0\n\t"
+                    "crc32q\t" LONGx1 "(%3), %1\n\t"
+                    "crc32q\t" LONGx2 "(%3), %2"
+                    : "=r"(crc0), "=r"(crc1), "=r"(crc2)
+                    : "r"(next), "0"(crc0), "1"(crc1), "2"(crc2));
+            next += 8;
+        } while (next < end);
+        crc0 = crc32c_shift(crc32c_long, (uint32_t) crc0) ^ crc1;
+        crc0 = crc32c_shift(crc32c_long, (uint32_t) crc0) ^ crc2;
+        next += LONG * 2;
+        len -= LONG * 3;
+    }
+
+    /* do the same thing, but now on SHORT*3 blocks for the remaining data less
+       than a LONG*3 block */
+    while (len >= SHORT * 3) {
+        crc1 = 0;
+        crc2 = 0;
+        end = next + SHORT;
+        do {
+            __asm__("crc32q\t"
+                    "(%3), %0\n\t"
+                    "crc32q\t" SHORTx1 "(%3), %1\n\t"
+                    "crc32q\t" SHORTx2 "(%3), %2"
+                    : "=r"(crc0), "=r"(crc1), "=r"(crc2)
+                    : "r"(next), "0"(crc0), "1"(crc1), "2"(crc2));
+            next += 8;
+        } while (next < end);
+        crc0 = crc32c_shift(crc32c_short, (uint32_t) crc0) ^ crc1;
+        crc0 = crc32c_shift(crc32c_short, (uint32_t) crc0) ^ crc2;
+        next += SHORT * 2;
+        len -= SHORT * 3;
+    }
+
+    /* compute the crc on the remaining eight-byte units less than a SHORT*3
+       block */
+    end = next + (len - (len & 7));
+    while (next < end) {
+        __asm__("crc32q\t"
+                "(%1), %0"
+                : "=r"(crc0)
+                : "r"(next), "0"(crc0));
+        next += 8;
+    }
+    len &= 7;
+
+    /* compute the crc for up to seven trailing bytes */
+    while (len) {
+        __asm__("crc32b\t"
+                "(%1), %0"
+                : "=r"(crc0)
+                : "r"(next), "0"(crc0));
+        next++;
+        len--;
+    }
+
+    /* return a post-processed crc */
+    return (uint32_t) crc0 ^ 0xffffffff;
+}
+
+/*
+ * Probe for SSE 4.2 support.
+ */
+bool rawrtc_crc32c_sse42_supported(void) {
+    uint32_t eax, ecx;
+    eax = 1;
+    __asm__("cpuid" : "=c"(ecx) : "a"(eax) : "%ebx", "%edx");
+    return ((ecx >> 20) & 1) != 0;
+}
+
+/*
+ * Initialize tables for shifting CRCs.
+ */
+void rawrtc_crc32c_init_sse42(void) {
+    crc32c_zeros(crc32c_long, LONG);
+    crc32c_zeros(crc32c_short, SHORT);
+}
+
+/*
+ * Compute CRC-32C using hardware instructions.
+ */
+uint32_t rawrtc_crc32c_sse42(void const* buffer, size_t length) {
+    return crc32c_hw(0x00000000, buffer, length);
+}
diff --git a/src/crc32c/sse42.h b/src/crc32c/sse42.h
new file mode 100644
index 0000000..5365888
--- /dev/null
+++ b/src/crc32c/sse42.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <re.h>
+
+bool rawrtc_crc32c_sse42_supported(void);
+
+void rawrtc_crc32c_init_sse42(void);
+
+uint32_t rawrtc_crc32c_sse42(void const* buffer, size_t length);
diff --git a/src/data_channel/attributes.c b/src/data_channel/attributes.c
new file mode 100644
index 0000000..31abf35
--- /dev/null
+++ b/src/data_channel/attributes.c
@@ -0,0 +1,351 @@
+#include "channel.h"
+#include "../data_transport/transport.h"
+#include <rawrtcdc/config.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+#define DEBUG_MODULE "data-channel"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Get the current state of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_get_state(
+    enum rawrtc_data_channel_state* const statep,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!statep || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = channel->state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the currently buffered amount (bytes) of outgoing application
+ * data of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount(
+    uint64_t* const buffered_amountp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!buffered_amountp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Implement this!
+    return RAWRTC_CODE_NOT_IMPLEMENTED;
+}
+
+/*
+ * Set the data channel's buffered amount (bytes) low threshold for
+ * outgoing application data.
+ */
+enum rawrtc_code rawrtc_data_channel_set_buffered_amount_low_threshold(
+    struct rawrtc_data_channel* const channel, uint64_t const buffered_amount_low_threshold) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Implement this!
+    (void) buffered_amount_low_threshold;
+    return RAWRTC_CODE_NOT_IMPLEMENTED;
+}
+
+/*
+ * Get the data channel's buffered amount (bytes) low threshold for
+ * outgoing application data.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount_low_threshold(
+    uint64_t* const buffered_amount_low_thresholdp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!buffered_amount_low_thresholdp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Implement this!
+    return RAWRTC_CODE_NOT_IMPLEMENTED;
+}
+
+/*
+ * Unset the handler argument and all handlers of the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_unset_handlers(struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Unset handler argument
+    channel->arg = NULL;
+
+    // Unset all handlers
+    channel->message_handler = NULL;
+    channel->close_handler = NULL;
+    channel->error_handler = NULL;
+    channel->buffered_amount_low_handler = NULL;
+    channel->open_handler = NULL;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's parameters.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_get_parameters(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!parametersp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer & done
+    *parametersp = mem_ref(channel->parameters);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Enable or disable streamed delivery.
+ *
+ * Note: In case an incoming message is currently pending (there are
+ *       queued chunks in the internal reassembly buffer), this will
+ *       fail with a *still in use* error.
+ */
+enum rawrtc_code rawrtc_data_channel_set_streaming(
+    struct rawrtc_data_channel* const channel, bool const on) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (channel->state == RAWRTC_DATA_CHANNEL_STATE_CLOSING ||
+        channel->state == RAWRTC_DATA_CHANNEL_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Does anything change?
+    if ((on && channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_STREAMED) ||
+        (!on && !(channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_STREAMED))) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Let the transport know we want to enable/disable streaming
+    error = channel->transport->channel_set_streaming(channel, on);
+    if (error) {
+        return error;
+    }
+
+    // Enable/disable streaming & done
+    if (on) {
+        channel->flags |= RAWRTC_DATA_CHANNEL_FLAGS_STREAMED;
+        DEBUG_PRINTF("Enabled streaming mode\n");
+    } else {
+        channel->flags &= ~RAWRTC_DATA_CHANNEL_FLAGS_STREAMED;
+        DEBUG_PRINTF("Disabled streaming mode\n");
+    }
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the data channel's open handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_open_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_open_handler const open_handler  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set open handler & done
+    channel->open_handler = open_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's open handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_open_handler(
+    rawrtc_data_channel_open_handler* const open_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!open_handlerp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get open handler (if any)
+    if (channel->open_handler) {
+        *open_handlerp = channel->open_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the data channel's buffered amount low handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_buffered_amount_low_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_buffered_amount_low_handler const buffered_amount_low_handler  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set buffered amount low handler & done
+    channel->buffered_amount_low_handler = buffered_amount_low_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's buffered amount low handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_buffered_amount_low_handler(
+    rawrtc_data_channel_buffered_amount_low_handler* const
+        buffered_amount_low_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!buffered_amount_low_handlerp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get buffered amount low handler (if any)
+    if (channel->buffered_amount_low_handler) {
+        *buffered_amount_low_handlerp = channel->buffered_amount_low_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the data channel's error handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_error_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_error_handler const error_handler  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set error handler & done
+    channel->error_handler = error_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's error handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_error_handler(
+    rawrtc_data_channel_error_handler* const error_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!error_handlerp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get error handler (if any)
+    if (channel->error_handler) {
+        *error_handlerp = channel->error_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the data channel's close handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_close_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_close_handler const close_handler  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set close handler & done
+    channel->close_handler = close_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's close handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_close_handler(
+    rawrtc_data_channel_close_handler* const close_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!close_handlerp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get close handler (if any)
+    if (channel->close_handler) {
+        *close_handlerp = channel->close_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the data channel's message handler.
+ */
+enum rawrtc_code rawrtc_data_channel_set_message_handler(
+    struct rawrtc_data_channel* const channel,
+    rawrtc_data_channel_message_handler const message_handler  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set message handler & done
+    channel->message_handler = message_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the data channel's message handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_data_channel_get_message_handler(
+    rawrtc_data_channel_message_handler* const message_handlerp,  // de-referenced
+    struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!message_handlerp || !channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get message handler (if any)
+    if (channel->message_handler) {
+        *message_handlerp = channel->message_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/data_channel/channel.c b/src/data_channel/channel.c
new file mode 100644
index 0000000..e134430
--- /dev/null
+++ b/src/data_channel/channel.c
@@ -0,0 +1,251 @@
+#include "channel.h"
+#include "../data_channel_parameters/parameters.h"
+#include "../data_transport/transport.h"
+#include <rawrtcdc/config.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+#define DEBUG_MODULE "data-channel"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Change the state of the data channel.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+void rawrtc_data_channel_set_state(
+    struct rawrtc_data_channel* const channel,  // not checked
+    enum rawrtc_data_channel_state const state) {
+    // Set state
+    // Note: Keep this here as it will prevent infinite recursion during closing/destroying
+    channel->state = state;
+    DEBUG_PRINTF(
+        "Data channel '%s' state changed to %s\n",
+        channel->parameters->label ? channel->parameters->label : "n/a",
+        rawrtc_data_channel_state_to_name(state));
+
+    // TODO: Clear options flag?
+
+    // Call transport handler (if any) and user application handler
+    switch (state) {
+        case RAWRTC_DATA_CHANNEL_STATE_OPEN:
+            // Call handler
+            if (channel->open_handler) {
+                channel->open_handler(channel->arg);
+            }
+            break;
+
+        case RAWRTC_DATA_CHANNEL_STATE_CLOSING:
+            // Nothing to do.
+            break;
+
+        case RAWRTC_DATA_CHANNEL_STATE_CLOSED:
+            // Call handler
+            if (channel->close_handler) {
+                channel->close_handler(channel->arg);
+            }
+            break;
+        default:
+            break;
+    }
+}
+
+/*
+ * Destructor for an existing data channel.
+ */
+static void rawrtc_data_channel_destroy(void* arg) {
+    struct rawrtc_data_channel* const channel = arg;
+
+    // Unset all handlers
+    rawrtc_data_channel_unset_handlers(channel);
+
+    // Close channel
+    // Note: The function will ensure that the channel is not closed before it's initialised
+    rawrtc_data_channel_close(channel);
+
+    // Un-reference
+    mem_deref(channel->transport);
+    mem_deref(channel->transport_arg);
+    mem_deref(channel->parameters);
+}
+
+/*
+ * Create a data channel (internal).
+ */
+enum rawrtc_code rawrtc_data_channel_create_internal(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_data_transport* const transport,  // referenced
+    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
+    bool const call_handler) {
+    enum rawrtc_code error;
+    struct rawrtc_data_channel* channel;
+
+    // Check arguments
+    if (!channelp || !transport || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    channel = mem_zalloc(sizeof(*channel), rawrtc_data_channel_destroy);
+    if (!channel) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    channel->flags = 0;
+    channel->state = RAWRTC_DATA_CHANNEL_STATE_CONNECTING;
+    channel->transport = mem_ref(transport);
+    channel->parameters = mem_ref(parameters);
+    channel->open_handler = open_handler;
+    channel->buffered_amount_low_handler = buffered_amount_low_handler;
+    channel->error_handler = error_handler;
+    channel->close_handler = close_handler;
+    channel->message_handler = message_handler;
+    channel->arg = arg;
+
+    // Create data channel on transport
+    if (call_handler) {
+        error = transport->channel_create(transport, channel, parameters);
+        if (error) {
+            goto out;
+        }
+    } else {
+        error = RAWRTC_CODE_SUCCESS;
+    }
+
+    // Done
+    DEBUG_PRINTF(
+        "Created data channel: label=%s, type=%d, reliability-parameter=%" PRIu32 ", "
+        "protocol=%s, negotiated=%s, id=%" PRIu16 "\n",
+        parameters->label ? parameters->label : "n/a", parameters->channel_type,
+        parameters->reliability_parameter, parameters->protocol ? parameters->protocol : "n/a",
+        parameters->negotiated ? "yes" : "no", parameters->id);
+
+out:
+    if (error) {
+        mem_deref(channel);
+    } else {
+        // Update flags & set pointer
+        channel->flags |= RAWRTC_DATA_CHANNEL_FLAGS_INITIALIZED;
+        *channelp = channel;
+    }
+    return error;
+}
+
+/*
+ * Call the data channel handler (internal).
+ *
+ * Important: Data transport implementations SHALL call this function
+ * instead of calling the channel handler directly.
+ */
+void rawrtc_data_channel_call_channel_handler(
+    struct rawrtc_data_channel* const channel,  // not checked
+    rawrtc_data_channel_handler const channel_handler,  // nullable
+    void* const arg) {
+    // Call handler (if any)
+    if (channel_handler) {
+        channel_handler(channel, arg);
+    }
+}
+
+/*
+ * Create a data channel.
+ * `*channelp` must be unreferenced.
+ *
+ * Note: You should call `rawrtc_data_channel_set_streaming`
+ *       directly after this function returned if you want to enable
+ *       streamed delivery of data for this channel from the beginning
+ *       of the first incoming message.
+ */
+enum rawrtc_code rawrtc_data_channel_create(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_data_transport* const transport,  // referenced
+    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 const error = rawrtc_data_channel_create_internal(
+        channelp, transport, parameters, open_handler, buffered_amount_low_handler, error_handler,
+        close_handler, message_handler, arg, true);
+
+    // Done
+    return error;
+}
+
+/*
+ * Set the argument of a data channel that is passed to the various
+ * handlers.
+ */
+enum rawrtc_code rawrtc_data_channel_set_arg(
+    struct rawrtc_data_channel* const channel,
+    void* const arg  // nullable
+) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set handler argument & done
+    channel->arg = arg;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Send data via the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_send(
+    struct rawrtc_data_channel* const channel,
+    struct mbuf* const buffer,  // nullable (if empty message), referenced
+    bool const is_binary) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    // TODO: Is this correct or can we send during `connecting` as well?
+    if (channel->state != RAWRTC_DATA_CHANNEL_STATE_OPEN) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Call handler
+    return channel->transport->channel_send(channel, buffer, is_binary);
+}
+
+/*
+ * Close the data channel.
+ */
+enum rawrtc_code rawrtc_data_channel_close(struct rawrtc_data_channel* const channel) {
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Don't close before the channel is initialised
+    // Note: This is needed as this function may be called in the destructor of the data channel
+    if (!(channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_INITIALIZED)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Check state
+    if (channel->state == RAWRTC_DATA_CHANNEL_STATE_CLOSING ||
+        channel->state == RAWRTC_DATA_CHANNEL_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Close channel
+    DEBUG_PRINTF("Closing data channel: %s\n", channel->parameters->label);
+    return channel->transport->channel_close(channel);
+}
diff --git a/src/data_channel/channel.h b/src/data_channel/channel.h
new file mode 100644
index 0000000..c6e8e89
--- /dev/null
+++ b/src/data_channel/channel.h
@@ -0,0 +1,58 @@
+#pragma once
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Data channel flags.
+ */
+enum {
+    RAWRTC_DATA_CHANNEL_FLAGS_INITIALIZED = 1 << 0,
+    RAWRTC_DATA_CHANNEL_FLAGS_STREAMED = 1 << 1,
+};
+
+/*
+ * Data channel type unordered bit flag.
+ */
+enum {
+    RAWRTC_DATA_CHANNEL_TYPE_IS_UNORDERED = 0x80,
+};
+
+/*
+ * Data channel.
+ */
+struct rawrtc_data_channel {
+    uint_fast8_t flags;
+    enum rawrtc_data_channel_state state;
+    struct rawrtc_data_transport* transport;  // referenced
+    void* transport_arg;  // referenced
+    struct rawrtc_data_channel_parameters* parameters;  // referenced
+    rawrtc_data_channel_open_handler open_handler;  // nullable
+    rawrtc_data_channel_buffered_amount_low_handler buffered_amount_low_handler;  // nullable
+    rawrtc_data_channel_error_handler error_handler;  // nullable
+    rawrtc_data_channel_close_handler close_handler;  // nullable
+    rawrtc_data_channel_message_handler message_handler;  // nullable
+    void* arg;  // nullable
+};
+
+void rawrtc_data_channel_set_state(
+    struct rawrtc_data_channel* const channel, enum rawrtc_data_channel_state const state);
+
+enum rawrtc_code rawrtc_data_channel_create_internal(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_data_transport* const transport,  // referenced
+    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
+    bool const call_handler);
+
+void rawrtc_data_channel_call_channel_handler(
+    struct rawrtc_data_channel* const channel,  // not checked
+    rawrtc_data_channel_handler const channel_handler,  // nullable
+    void* const arg);
diff --git a/src/data_channel/meson.build b/src/data_channel/meson.build
new file mode 100644
index 0000000..9ab3083
--- /dev/null
+++ b/src/data_channel/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'channel.c',
+    'utils.c',
+])
diff --git a/src/data_channel/utils.c b/src/data_channel/utils.c
new file mode 100644
index 0000000..31ab581
--- /dev/null
+++ b/src/data_channel/utils.c
@@ -0,0 +1,19 @@
+#include <rawrtcdc/data_channel.h>
+
+/*
+ * Get the corresponding name for a data channel state.
+ */
+char const* rawrtc_data_channel_state_to_name(enum rawrtc_data_channel_state const state) {
+    switch (state) {
+        case RAWRTC_DATA_CHANNEL_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_DATA_CHANNEL_STATE_OPEN:
+            return "open";
+        case RAWRTC_DATA_CHANNEL_STATE_CLOSING:
+            return "closing";
+        case RAWRTC_DATA_CHANNEL_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/data_channel_parameters/attributes.c b/src/data_channel_parameters/attributes.c
new file mode 100644
index 0000000..f09deb6
--- /dev/null
+++ b/src/data_channel_parameters/attributes.c
@@ -0,0 +1,138 @@
+#include "parameters.h"
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the label from the data channel parameters.
+ * `*labelp` will be set to a copy of the parameter's label and must be
+ * unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no label has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*parameters*
+ * must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_label(
+    char** const labelp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!labelp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    if (parameters->label) {
+        *labelp = mem_ref(parameters->label);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the channel type from the data channel parameters.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_channel_type(
+    enum rawrtc_data_channel_type* const channel_typep,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!channel_typep || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *channel_typep = parameters->channel_type;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the reliability parameter from the data channel parameters.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case the channel type is
+ * `RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_*`. Otherwise,
+ * `RAWRTC_CODE_SUCCESS` will be returned.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_reliability_parameter(
+    uint32_t* const reliability_parameterp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!reliability_parameterp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    switch (parameters->channel_type) {
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED:
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED:
+            return RAWRTC_CODE_NO_VALUE;
+        default:
+            *reliability_parameterp = parameters->reliability_parameter;
+            return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the protocol from the data channel parameters.
+ * `*protocolp` will be set to a copy of the parameter's protocol and
+ * must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no protocol has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*protocolp*
+ * must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_protocol(
+    char** const protocolp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!protocolp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    if (parameters->protocol) {
+        *protocolp = mem_ref(parameters->protocol);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the 'negotiated' flag from the data channel parameters.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_negotiated(
+    bool* const negotiatedp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!negotiatedp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *negotiatedp = parameters->negotiated;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the negotiated id from the data channel parameters.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case the 'negotiated' flag is set
+ * `false`. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_get_id(
+    uint16_t* const idp,  // de-referenced
+    struct rawrtc_data_channel_parameters* const parameters) {
+    // Check arguments
+    if (!idp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    if (parameters->negotiated) {
+        *idp = parameters->id;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/data_channel_parameters/meson.build b/src/data_channel_parameters/meson.build
new file mode 100644
index 0000000..c68f3a1
--- /dev/null
+++ b/src/data_channel_parameters/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'parameters.c',
+])
diff --git a/src/data_channel_parameters/parameters.c b/src/data_channel_parameters/parameters.c
new file mode 100644
index 0000000..189850a
--- /dev/null
+++ b/src/data_channel_parameters/parameters.c
@@ -0,0 +1,152 @@
+#include "parameters.h"
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Destructor for existing data channel parameters.
+ */
+static void rawrtc_data_channel_parameters_destroy(void* arg) {
+    struct rawrtc_data_channel_parameters* const parameters = arg;
+
+    // Un-reference
+    mem_deref(parameters->label);
+    mem_deref(parameters->protocol);
+}
+
+/*
+ * Common code to create or copy parameters.
+ */
+static enum rawrtc_code data_parameters_create(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    char* const label,  // referenced, nullable
+    enum rawrtc_data_channel_type const channel_type,
+    uint32_t const reliability_parameter,
+    char* const protocol,  // referenced, nullable
+    bool const negotiated,
+    uint16_t const id) {
+    struct rawrtc_data_channel_parameters* parameters;
+
+    // Check arguments
+    if (!parametersp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    parameters = mem_zalloc(sizeof(*parameters), rawrtc_data_channel_parameters_destroy);
+    if (!parameters) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    parameters->label = label;
+    parameters->protocol = protocol;
+    parameters->channel_type = channel_type;
+    parameters->negotiated = negotiated;
+    if (negotiated) {
+        parameters->id = id;
+    }
+
+    // Set reliability parameter
+    switch (channel_type) {
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED:
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED:
+            parameters->reliability_parameter = 0;
+            break;
+        default:
+            parameters->reliability_parameter = reliability_parameter;
+            break;
+    }
+
+    // Set pointer & done
+    *parametersp = parameters;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create data channel parameters (internal).
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_create_internal(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    char* const label,  // referenced, nullable
+    enum rawrtc_data_channel_type const channel_type,
+    uint32_t const reliability_parameter,
+    char* const protocol,  // referenced, nullable
+    bool const negotiated,
+    uint16_t const id) {
+    enum rawrtc_code error;
+
+    // Create parameters
+    error = data_parameters_create(
+        parametersp, label, channel_type, reliability_parameter, protocol, negotiated, id);
+
+    if (!error) {
+        // Reference label & protocol
+        mem_ref(label);
+        mem_ref(protocol);
+    }
+
+    // Done
+    return error;
+}
+
+/*
+ * Create data channel parameters.
+ *
+ * For `RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_*`, the reliability parameter
+ * is being ignored.
+ *
+ * When using `RAWRTC_DATA_CHANNEL_TYPE_*_RETRANSMIT`, the reliability
+ * parameter specifies the number of times a retransmission occurs if
+ * not acknowledged before the message is being discarded.
+ *
+ * When using `RAWRTC_DATA_CHANNEL_TYPE_*_TIMED`, the reliability
+ * parameter specifies the time window in milliseconds during which
+ * (re-)transmissions may occur before the message is being discarded.
+ *
+ * `*parametersp` must be unreferenced.
+ *
+ * In case `negotiated` is set to `false`, the `id` is being ignored.
+ */
+enum rawrtc_code rawrtc_data_channel_parameters_create(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    char const* const label,  // copied, nullable
+    enum rawrtc_data_channel_type const channel_type,
+    uint32_t const reliability_parameter,
+    char const* const protocol,  // copied, nullable
+    bool const negotiated,
+    uint16_t const id) {
+    char* copied_label;
+    char* copied_protocol;
+    enum rawrtc_code error;
+
+    // Copy label
+    if (label) {
+        rawrtc_strdup(&copied_label, label);
+    } else {
+        copied_label = NULL;
+    }
+
+    // Copy protocol
+    if (protocol) {
+        rawrtc_strdup(&copied_protocol, protocol);
+    } else {
+        copied_protocol = NULL;
+    }
+
+    // Create parameters
+    error = data_parameters_create(
+        parametersp, copied_label, channel_type, reliability_parameter, copied_protocol, negotiated,
+        id);
+
+    if (error) {
+        // Un-reference
+        mem_deref(copied_label);
+        mem_deref(copied_protocol);
+    }
+
+    // Done
+    return error;
+}
diff --git a/src/data_channel_parameters/parameters.h b/src/data_channel_parameters/parameters.h
new file mode 100644
index 0000000..500e0a9
--- /dev/null
+++ b/src/data_channel_parameters/parameters.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Data channel parameters.
+ */
+struct rawrtc_data_channel_parameters {
+    char* label;  // copied
+    enum rawrtc_data_channel_type channel_type;
+    uint32_t reliability_parameter;  // contains either max_packet_lifetime or max_retransmit
+    char* protocol;  // copied
+    bool negotiated;
+    uint16_t id;
+};
+
+enum rawrtc_code rawrtc_data_channel_parameters_create_internal(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced
+    char* const label,  // referenced, nullable
+    enum rawrtc_data_channel_type const channel_type,
+    uint32_t const reliability_parameter,
+    char* const protocol,  // referenced, nullable
+    bool const negotiated,
+    uint16_t const id);
diff --git a/src/data_transport/attributes.c b/src/data_transport/attributes.c
new file mode 100644
index 0000000..6888ab6
--- /dev/null
+++ b/src/data_transport/attributes.c
@@ -0,0 +1,25 @@
+#include "transport.h"
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Get the data transport's type and underlying transport reference.
+ * `*internal_transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_data_transport_get_transport(
+    enum rawrtc_data_transport_type* const typep,  // de-referenced
+    void** const internal_transportp,  // de-referenced
+    struct rawrtc_data_transport* const transport) {
+    // Check arguments
+    if (!typep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set type & transport
+    *typep = transport->type;
+    *internal_transportp = mem_ref(transport->transport);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/data_transport/meson.build b/src/data_transport/meson.build
new file mode 100644
index 0000000..0227c31
--- /dev/null
+++ b/src/data_transport/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/data_transport/transport.c b/src/data_transport/transport.c
new file mode 100644
index 0000000..62be56d
--- /dev/null
+++ b/src/data_transport/transport.c
@@ -0,0 +1,57 @@
+#include "transport.h"
+#include <rawrtcdc/config.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+#define DEBUG_MODULE "data-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Destructor for an existing data transport.
+ */
+static void rawrtc_data_transport_destroy(void* arg) {
+    struct rawrtc_data_transport* const transport = arg;
+
+    // Un-reference
+    mem_deref(transport->transport);
+}
+
+/*
+ * Create a data transport instance.
+ */
+enum rawrtc_code rawrtc_data_transport_create(
+    struct rawrtc_data_transport** const transportp,  // de-referenced
+    enum rawrtc_data_transport_type const type,
+    void* const internal_transport,  // referenced
+    rawrtc_data_transport_channel_create_handler const channel_create_handler,
+    rawrtc_data_transport_channel_close_handler const channel_close_handler,
+    rawrtc_data_transport_channel_send_handler const channel_send_handler,
+    rawrtc_data_transport_channel_set_streaming_handler const channel_set_streaming_handler) {
+    struct rawrtc_data_transport* transport;
+
+    // Check arguments
+    if (!transportp || !internal_transport || !channel_create_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_data_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    transport->type = type;
+    transport->transport = mem_ref(internal_transport);
+    transport->channel_create = channel_create_handler;
+    transport->channel_close = channel_close_handler;
+    transport->channel_send = channel_send_handler;
+    transport->channel_set_streaming = channel_set_streaming_handler;
+
+    // Set pointer & done
+    DEBUG_PRINTF("Created data transport of type %s\n", rawrtc_data_transport_type_to_str(type));
+    *transportp = transport;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/data_transport/transport.h b/src/data_transport/transport.h
new file mode 100644
index 0000000..3b5593e
--- /dev/null
+++ b/src/data_transport/transport.h
@@ -0,0 +1,57 @@
+#pragma once
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Create the data channel (transport handler).
+ */
+typedef enum rawrtc_code (*rawrtc_data_transport_channel_create_handler)(
+    struct rawrtc_data_transport* const transport,
+    struct rawrtc_data_channel* const channel,  // referenced
+    struct rawrtc_data_channel_parameters const* const parameters  // read-only
+);
+
+/*
+ * Close the data channel (transport handler).
+ */
+typedef enum rawrtc_code (*rawrtc_data_transport_channel_close_handler)(
+    struct rawrtc_data_channel* const channel);
+
+/*
+ * Send data via the data channel (transport handler).
+ */
+typedef enum rawrtc_code (*rawrtc_data_transport_channel_send_handler)(
+    struct rawrtc_data_channel* const channel,
+    struct mbuf* buffer,  // nullable (if size 0), referenced
+    bool const is_binary);
+
+/*
+ * Check if the data transport allows changing the delivery mode
+ * (transport handler).
+ */
+typedef enum rawrtc_code (*rawrtc_data_transport_channel_set_streaming_handler)(
+    struct rawrtc_data_channel* const channel, bool const on);
+
+/**
+ * Generic data transport.
+ */
+struct rawrtc_data_transport {
+    enum rawrtc_data_transport_type type;
+    void* transport;
+    rawrtc_data_transport_channel_create_handler channel_create;
+    rawrtc_data_transport_channel_close_handler channel_close;
+    rawrtc_data_transport_channel_send_handler channel_send;
+    rawrtc_data_transport_channel_set_streaming_handler channel_set_streaming;
+};
+
+enum rawrtc_code rawrtc_data_transport_create(
+    struct rawrtc_data_transport** const transportp,  // de-referenced
+    enum rawrtc_data_transport_type const type,
+    void* const internal_transport,  // referenced
+    rawrtc_data_transport_channel_create_handler const channel_create_handler,
+    rawrtc_data_transport_channel_close_handler const channel_close_handler,
+    rawrtc_data_transport_channel_send_handler const channel_send_handler,
+    rawrtc_data_transport_channel_set_streaming_handler const channel_set_streaming_handler);
diff --git a/src/data_transport/utils.c b/src/data_transport/utils.c
new file mode 100644
index 0000000..1216eea
--- /dev/null
+++ b/src/data_transport/utils.c
@@ -0,0 +1,13 @@
+#include <rawrtcdc/data_transport.h>
+
+/*
+ * Translate a data transport type to str.
+ */
+char const* rawrtc_data_transport_type_to_str(enum rawrtc_data_transport_type const type) {
+    switch (type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP:
+            return "SCTP";
+        default:
+            return "???";
+    }
+}
diff --git a/src/main/main.c b/src/main/main.c
new file mode 100644
index 0000000..e17633e
--- /dev/null
+++ b/src/main/main.c
@@ -0,0 +1,79 @@
+#include "main.h"
+#include "../crc32c/crc32c.h"
+#include <rawrtcdc/main.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <usrsctp.h>  // usrsctp_handle_timers
+#include <limits.h>  // INT_MAX
+
+/*
+ * Global RAWRTCDC settings.
+ */
+struct rawrtcdc_global rawrtcdc_global;
+
+/*
+ * Initialise RAWRTCDC. 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 rawrtcdc_init(bool const init_re, rawrtcdc_timer_handler const timer_handler) {
+    // Check arguments
+    if (!timer_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Initialise re (if requested)
+    if (init_re) {
+        if (libre_init()) {
+            return RAWRTC_CODE_INITIALISE_FAIL;
+        }
+    }
+
+    // Initialise CRC32-C
+    rawrtc_crc32c_init();
+
+    // Set timer handler
+    rawrtcdc_global.timer_handler = timer_handler;
+
+    // Set usrsctp initialised counter
+    rawrtcdc_global.usrsctp_initialized = 0;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Close RAWRTCDC and free up all resources.
+ *
+ * Note: In case `close_re` is not set to `true`, you MUST close
+ *       re yourselves.
+ */
+enum rawrtc_code rawrtcdc_close(bool const close_re) {
+    // TODO: Close usrsctp if initialised
+
+    // Remove timer handler
+    rawrtcdc_global.timer_handler = NULL;
+
+    // Close re (if requested)
+    if (close_re) {
+        libre_close();
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Handle timer tick.
+ * `delta` contains the delta milliseconds passed between calls.
+ */
+void rawrtcdc_timer_tick(uint_fast16_t const delta) {
+    // Pass delta ms to usrsctp
+#if (UINT16_MAX > INT_MAX)
+    usrsctp_handle_timers(delta > INT_MAX ? INT_MAX : ((int) delta));
+#else
+    usrsctp_handle_timers((int) delta);
+#endif
+}
diff --git a/src/main/main.h b/src/main/main.h
new file mode 100644
index 0000000..faf0071
--- /dev/null
+++ b/src/main/main.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <rawrtcdc/main.h>
+#include <re.h>
+
+extern struct rawrtcdc_global rawrtcdc_global;
+
+/*
+ * CRC32-C handler to be used.
+ */
+typedef uint32_t (*rawrtc_crc32c_handler)(void const* buffer, size_t length);
+
+/*
+ * Global RAWRTCDC settings.
+ */
+struct rawrtcdc_global {
+    rawrtcdc_timer_handler timer_handler;
+    rawrtc_crc32c_handler crc32c_handler;
+    uint_fast32_t usrsctp_initialized;
+    size_t usrsctp_chunk_size;
+};
diff --git a/src/main/meson.build b/src/main/meson.build
new file mode 100644
index 0000000..b298885
--- /dev/null
+++ b/src/main/meson.build
@@ -0,0 +1 @@
+sources += files('main.c')
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..976bc9d
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,12 @@
+sources = []
+
+subdir('crc32c')
+subdir('data_channel')
+subdir('data_channel_parameters')
+subdir('data_transport')
+subdir('main')
+subdir('sctp_capabilities')
+if get_option('sctp_redirect_transport')
+    subdir('sctp_redirect_transport')
+endif
+subdir('sctp_transport')
diff --git a/src/sctp_capabilities/attributes.c b/src/sctp_capabilities/attributes.c
new file mode 100644
index 0000000..f84ff70
--- /dev/null
+++ b/src/sctp_capabilities/attributes.c
@@ -0,0 +1,23 @@
+#include "capabilities.h"
+#include <rawrtcdc/sctp_capabilities.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the SCTP parameter's maximum message size value.
+ *
+ * Note: A value of `0` indicates that the implementation supports
+ *       receiving messages of arbitrary size.
+ */
+enum rawrtc_code rawrtc_sctp_capabilities_get_max_message_size(
+    uint64_t* const max_message_sizep,  // de-referenced
+    struct rawrtc_sctp_capabilities* const capabilities) {
+    // Check arguments
+    if (!capabilities) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *max_message_sizep = capabilities->max_message_size;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/sctp_capabilities/capabilities.c b/src/sctp_capabilities/capabilities.c
new file mode 100644
index 0000000..8775760
--- /dev/null
+++ b/src/sctp_capabilities/capabilities.c
@@ -0,0 +1,53 @@
+#include "capabilities.h"
+#include <rawrtcdc/sctp_capabilities.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/**
+ * Print debug information for SCTP capabilities.
+ */
+int rawrtc_sctp_capabilities_debug(
+    struct re_printf* const pf, struct rawrtc_sctp_capabilities const* const capabilities) {
+    int err = 0;
+
+    // Check arguments
+    if (!capabilities) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  SCTP Capabilities <%p>:\n", capabilities);
+
+    // Maximum message size
+    err |= re_hprintf(pf, "    max_message_size=%" PRIu64 "\n", capabilities->max_message_size);
+
+    // Done
+    return err;
+}
+
+/*
+ * Create a new SCTP transport capabilities instance.
+ * `*capabilitiesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_capabilities_create(
+    struct rawrtc_sctp_capabilities** const capabilitiesp,  // de-referenced
+    uint64_t const max_message_size) {
+    struct rawrtc_sctp_capabilities* capabilities;
+
+    // Check arguments
+    if (!capabilitiesp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate capabilities
+    capabilities = mem_zalloc(sizeof(*capabilities), NULL);
+    if (!capabilities) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    capabilities->max_message_size = max_message_size;
+
+    // Set pointer & done
+    *capabilitiesp = capabilities;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/sctp_capabilities/capabilities.h b/src/sctp_capabilities/capabilities.h
new file mode 100644
index 0000000..09bdfe6
--- /dev/null
+++ b/src/sctp_capabilities/capabilities.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <re.h>
+
+struct rawrtc_sctp_capabilities {
+    uint64_t max_message_size;
+};
diff --git a/src/sctp_capabilities/meson.build b/src/sctp_capabilities/meson.build
new file mode 100644
index 0000000..db2b827
--- /dev/null
+++ b/src/sctp_capabilities/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'capabilities.c',
+])
diff --git a/src/sctp_redirect_transport/attributes.c b/src/sctp_redirect_transport/attributes.c
new file mode 100644
index 0000000..2a1d163
--- /dev/null
+++ b/src/sctp_redirect_transport/attributes.c
@@ -0,0 +1,36 @@
+#include "transport.h"
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the state of the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_get_state(
+    enum rawrtc_sctp_redirect_transport_state* const statep,  // de-referenced
+    struct rawrtc_sctp_redirect_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the redirected local SCTP port of the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_sctp_redirect_transport* const transport) {
+    // Check arguments
+    if (!portp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port & done
+    *portp = transport->local_port;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/sctp_redirect_transport/meson.build b/src/sctp_redirect_transport/meson.build
new file mode 100644
index 0000000..0227c31
--- /dev/null
+++ b/src/sctp_redirect_transport/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/sctp_redirect_transport/transport.c b/src/sctp_redirect_transport/transport.c
new file mode 100644
index 0000000..2867235
--- /dev/null
+++ b/src/sctp_redirect_transport/transport.c
@@ -0,0 +1,449 @@
+#include "transport.h"
+#include "../crc32c/crc32c.h"
+#include <rawrtcdc/config.h>
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <errno.h>
+#include <netinet/in.h>  // IPPROTO_RAW, ntohs, htons
+#include <string.h>  // memset
+#include <sys/socket.h>  // AF_INET, SOCK_RAW, sendto, recvfrom
+#include <sys/types.h>
+#include <unistd.h>  // close
+
+#define DEBUG_MODULE "redirect-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Patch local and remote port in the SCTP packet header.
+ */
+static void patch_sctp_header(
+    struct mbuf* const buffer, uint16_t const source, uint16_t const destination) {
+    size_t const start = buffer->pos;
+    int err;
+    uint32_t checksum;
+
+    // Patch source port
+    err = mbuf_write_u16(buffer, htons(source));
+    if (err) {
+        DEBUG_WARNING("Could not patch source port, reason: %m\n", err);
+        return;
+    }
+
+    // Patch destination port
+    err = mbuf_write_u16(buffer, htons(destination));
+    if (err) {
+        DEBUG_WARNING("Could not patch destination port, reason: %m\n", err);
+        return;
+    }
+
+    // Skip verification tag
+    mbuf_advance(buffer, 4);
+
+    // Reset checksum field to '0' and rewind back
+    memset(mbuf_buf(buffer), 0, 4);
+    mbuf_set_pos(buffer, start);
+    // Recalculate checksum
+    checksum = rawrtc_crc32c(mbuf_buf(buffer), mbuf_get_left(buffer));
+    // Advance to checksum field, set it and rewind back
+    mbuf_advance(buffer, 8);
+    err = mbuf_write_u32(buffer, checksum);
+    if (err) {
+        DEBUG_WARNING("Could not patch checksum, reason: %m\n", err);
+        return;
+    }
+    mbuf_set_pos(buffer, start);
+}
+
+/*
+ * Handle outgoing messages (that came in from the raw socket).
+ */
+static void redirect_from_raw(
+    int flags,
+    void* arg  // not checked
+) {
+    struct rawrtc_sctp_redirect_transport* const transport = arg;
+    struct mbuf* const buffer = transport->buffer;
+    enum rawrtc_code error;
+    struct sockaddr_in from_address;
+    socklen_t address_length;
+    ssize_t length;
+    struct sa from = {0};
+    size_t header_length;
+    uint16_t source;
+    uint16_t destination;
+
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_DETACHED) {
+        DEBUG_PRINTF("Ignoring SCTP packet ready event, transport is detached\n");
+        return;
+    }
+
+    if ((flags & FD_READ) == FD_READ) {
+        // Receive
+        address_length = sizeof(from_address);
+        length = recvfrom(
+            transport->socket, mbuf_buf(buffer), mbuf_get_space(buffer), 0,
+            (struct sockaddr*) &from_address, &address_length);
+        if (length == -1) {
+            switch (errno) {
+                case EAGAIN:
+#if (defined(EWOULDBLOCK) && EAGAIN != EWOULDBLOCK)
+                case EWOULDBLOCK:
+#endif
+                    break;
+                default:
+                    DEBUG_WARNING("Unable to receive raw message: %m\n", errno);
+                    break;
+            }
+            goto out;
+        }
+        mbuf_set_end(buffer, (size_t) length);
+
+        // Check address
+        error = rawrtc_error_to_code(sa_set_sa(&from, (struct sockaddr*) &from_address));
+        if (error) {
+            DEBUG_WARNING("Invalid sender address: %m\n", error);
+            goto out;
+        }
+        DEBUG_PRINTF("Received %zu bytes via RAW from %j\n", mbuf_get_left(buffer), &from);
+        if (!sa_isset(&from, SA_ADDR) && !sa_cmp(&transport->redirect_address, &from, SA_ADDR)) {
+            DEBUG_WARNING("Ignoring data from unknown address");
+            goto out;
+        }
+
+        // Skip IPv4 header
+        header_length = (size_t)(mbuf_read_u8(buffer) & 0xf);
+        mbuf_advance(buffer, -1);
+        DEBUG_PRINTF("IPv4 header length: %zu\n", header_length);
+        mbuf_advance(buffer, header_length * 4);
+
+        // Read source and destination port
+        source = ntohs(mbuf_read_u16(buffer));
+        destination = ntohs(mbuf_read_u16(buffer));
+        sa_set_port(&from, source);
+        (void) destination;
+        DEBUG_PRINTF("Raw from %J to %" PRIu16 "\n", &from, destination);
+        mbuf_advance(buffer, -4);
+
+        // Is this from the correct source?
+        if (source != sa_port(&transport->redirect_address)) {
+            DEBUG_PRINTF("Ignored data from different source port %" PRIu16 "\n", source);
+            goto out;
+        }
+
+        // Update SCTP header with changed ports
+        patch_sctp_header(buffer, transport->local_port, transport->remote_port);
+
+        // Pass to outbound handler
+        error = transport->context.outbound_handler(buffer, 0x00, 0x00, transport->context.arg);
+        if (error) {
+            DEBUG_WARNING("Could not send packet, reason: %s\n", rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+out:
+    // Rewind buffer
+    mbuf_rewind(buffer);
+}
+
+/*
+ * Change the state of the SCTP redirect transport.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_state(
+    struct rawrtc_sctp_redirect_transport* const transport,  // not checked
+    enum rawrtc_sctp_redirect_transport_state const state) {
+    // Closed?
+    if (state == RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_CLOSED) {
+        // Stop listening and close raw socket
+        if (transport->socket != -1) {
+            fd_close(transport->socket);
+            if (close(transport->socket)) {
+                DEBUG_WARNING("Closing raw socket failed: %m\n", errno);
+            }
+        }
+
+        // Mark as detached & detach from DTLS transport
+        transport->flags |= RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_DETACHED;
+        if (transport->context.detach_handler) {
+            transport->context.detach_handler(transport->context.arg);
+        }
+    }
+
+    // Set state
+    transport->state = state;
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+static enum rawrtc_code validate_context(
+    struct rawrtc_sctp_transport_context* const context  // not checked
+) {
+    // Ensure the context contains all necessary callbacks
+    if (!context->state_getter || !context->outbound_handler || !context->detach_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    } else {
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Destructor for an existing SCTP redirect transport.
+ */
+static void rawrtc_sctp_redirect_transport_destroy(void* arg) {
+    struct rawrtc_sctp_redirect_transport* const transport = arg;
+
+    // Stop transport
+    rawrtc_sctp_redirect_transport_stop(transport);
+
+    // Call 'destroyed' handler (if fully initialised)
+    if (transport->flags & RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_INITIALIZED &&
+        transport->context.destroyed_handler) {
+        transport->context.destroyed_handler(transport->context.arg);
+    }
+
+    // Un-reference
+    mem_deref(transport->buffer);
+}
+
+/*
+ * Create an SCTP redirect transport from an external DTLS transport.
+ * `*transportp` must be unreferenced.
+ *
+ * `redirect_ip` must be a IPv4 address.
+ *
+ * Important: The redirect transport requires to be run inside re's
+ *            event loop (`re_main`).
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_create_from_external(
+    struct rawrtc_sctp_redirect_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport_context* const context,  // copied
+    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;
+    struct sa redirect_address;
+    enum rawrtc_external_dtls_transport_state dtls_transport_state;
+    struct rawrtc_sctp_redirect_transport* transport;
+
+    // Check arguments
+    if (!transportp || !context || !redirect_ip || redirect_port == 0) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure it's an IPv4 address
+    error = rawrtc_error_to_code(sa_set_str(&redirect_address, redirect_ip, redirect_port));
+    if (error) {
+        return error;
+    }
+    if (sa_af(&redirect_address) != AF_INET) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure the context contains all necessary callbacks
+    error = validate_context(context);
+    if (error) {
+        return error;
+    }
+
+    // Check DTLS transport state
+    error = context->state_getter(&dtls_transport_state, context->arg);
+    if (error) {
+        DEBUG_WARNING(
+            "Getting external DTLS transport state failed: %s\n", rawrtc_code_to_str(error));
+        return RAWRTC_CODE_EXTERNAL_ERROR;
+    }
+    if (dtls_transport_state == RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_sctp_redirect_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    transport->context = *context;
+    transport->flags = 0;
+    transport->state = RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_NEW;
+    transport->local_port = port > 0 ? port : RAWRTC_SCTP_REDIRECT_TRANSPORT_DEFAULT_PORT;
+    transport->redirect_address = redirect_address;
+    transport->state_change_handler = state_change_handler;
+    transport->arg = arg;
+
+    // Create buffer
+    transport->buffer = mbuf_alloc(RAWRTC_SCTP_REDIRECT_TRANSPORT_RAW_SOCKET_RECEIVE_SIZE);
+    if (!transport->buffer) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Create raw socket
+    transport->socket = socket(AF_INET, SOCK_RAW, IPPROTO_SCTP);
+    if (transport->socket == -1) {
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Set non-blocking
+    error = rawrtc_error_to_code(net_sockopt_blocking_set(transport->socket, false));
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer & mark as initialised
+        *transportp = transport;
+        transport->flags |= RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_INITIALIZED;
+    }
+    return error;
+}
+
+/*
+ * Start an SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_start(
+    struct rawrtc_sctp_redirect_transport* const transport,
+    struct rawrtc_sctp_capabilities const* const remote_capabilities,  // copied
+    uint16_t remote_port  // zeroable
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !remote_capabilities) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_NEW) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set default port (if 0)
+    if (remote_port == 0) {
+        remote_port = transport->local_port;
+    }
+
+    // Store remote port
+    transport->remote_port = remote_port;
+
+    // Listen on raw socket
+    error =
+        rawrtc_error_to_code(fd_listen(transport->socket, FD_READ, redirect_from_raw, transport));
+    if (error) {
+        goto out;
+    }
+
+    // Update state & done
+    set_state(transport, RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_OPEN);
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        // Stop listening on raw socket
+        fd_close(transport->socket);
+    }
+    return error;
+}
+
+/*
+ * Stop and close the SCTP redirect transport.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_stop(
+    struct rawrtc_sctp_redirect_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->flags & RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_DETACHED ||
+        transport->state == RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update state
+    set_state(transport, RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Feed inbound data to the SCTP redirect transport (that will be sent
+ * out via the raw socket).
+ *
+ * `buffer` contains the data to be fed to the raw transport. Since
+ * the data is not going to be referenced, you can pass a *fake* `mbuf`
+ * structure that hasn't been allocated with `mbuf_alloc` to avoid
+ * copying.
+ *
+ * Return `RAWRTC_CODE_INVALID_STATE` in case the transport is closed.
+ * In case the buffer could not be sent due to the raw socket's buffer
+ * being too full, `RAWRTC_CODE_TRY_AGAIN_LATER` will be returned. You
+ * can safely ignore this code since SCTP will retransmit data on a
+ * reliable stream.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` is being returned.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_feed_inbound(
+    struct rawrtc_sctp_redirect_transport* const transport, struct mbuf* const buffer) {
+    ssize_t length;
+
+    // Check arguments
+    if (!transport || !buffer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_DETACHED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_OPEN) {
+        // Note: Silently ignore inbound packets received as long as the transport is not open.
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update SCTP header with changed ports
+    patch_sctp_header(buffer, transport->local_port, sa_port(&transport->redirect_address));
+
+    // Send over raw socket
+    DEBUG_PRINTF(
+        "Redirecting message (%zu bytes) to %J\n", mbuf_get_left(buffer),
+        &transport->redirect_address);
+    length = sendto(
+        transport->socket, mbuf_buf(buffer), mbuf_get_left(buffer), 0,
+        &transport->redirect_address.u.sa, transport->redirect_address.len);
+    if (length == -1) {
+        switch (errno) {
+            case EAGAIN:
+#if (defined(EWOULDBLOCK) && EAGAIN != EWOULDBLOCK)
+            case EWOULDBLOCK:
+#endif
+                // We're just dropping the packets in this case. SCTP will retransmit eventually.
+                return RAWRTC_CODE_TRY_AGAIN_LATER;
+            default:
+                DEBUG_WARNING("Unable to redirect message: %m\n", errno);
+                return rawrtc_error_to_code(errno);
+        }
+    }
+
+    // Advance buffer & done
+    mbuf_advance(buffer, length);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/sctp_redirect_transport/transport.h b/src/sctp_redirect_transport/transport.h
new file mode 100644
index 0000000..f5998ce
--- /dev/null
+++ b/src/sctp_redirect_transport/transport.h
@@ -0,0 +1,37 @@
+#pragma once
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+enum {
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_DEFAULT_PORT = 5000,
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_RAW_SOCKET_RECEIVE_SIZE = 8192,
+};
+
+/*
+ * SCTP redirect transport flags.
+ */
+enum {
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_INITIALIZED = 1 << 0,
+    // The detached flag is virtually identical to the 'closed' state but is applied before the
+    // detach handler is being called. Thus, any other functions should check for the detached flag
+    // instead of checking for the 'closed' state since that is being set at a later stage.
+    RAWRTC_SCTP_REDIRECT_TRANSPORT_FLAGS_DETACHED = 1 << 1,
+};
+
+/*
+ * SCTP redirect transport.
+ */
+struct rawrtc_sctp_redirect_transport {
+    struct rawrtc_sctp_transport_context context;
+    uint_fast8_t flags;
+    enum rawrtc_sctp_redirect_transport_state state;
+    uint16_t local_port;
+    uint16_t remote_port;
+    struct sa redirect_address;
+    rawrtc_sctp_redirect_transport_state_change_handler state_change_handler;  // nullable
+    void* arg;  // nullable
+    struct mbuf* buffer;
+    int socket;
+};
diff --git a/src/sctp_redirect_transport/utils.c b/src/sctp_redirect_transport/utils.c
new file mode 100644
index 0000000..478ce7f
--- /dev/null
+++ b/src/sctp_redirect_transport/utils.c
@@ -0,0 +1,18 @@
+#include <rawrtcdc/sctp_redirect_transport.h>
+
+/*
+ * Get the corresponding name for an SCTP redirect transport state.
+ */
+char const* rawrtc_sctp_redirect_transport_state_to_name(
+    enum rawrtc_sctp_redirect_transport_state const state) {
+    switch (state) {
+        case RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_OPEN:
+            return "open";
+        case RAWRTC_SCTP_REDIRECT_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/sctp_transport/attributes.c b/src/sctp_transport/attributes.c
new file mode 100644
index 0000000..6bc582c
--- /dev/null
+++ b/src/sctp_transport/attributes.c
@@ -0,0 +1,139 @@
+#include "transport.h"
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+/*
+ * Get the current state of the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_state(
+    enum rawrtc_sctp_transport_state* const statep,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the local port of the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!portp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port & done
+    *portp = transport->port;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the number of streams allocated for the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_n_streams(
+    uint16_t* const n_streamsp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!n_streamsp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set #streams & done
+    *n_streamsp = (uint16_t) transport->n_channels;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the local SCTP transport capabilities (static).
+ * `*capabilitiesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_capabilities(
+    struct rawrtc_sctp_capabilities** const capabilitiesp  // de-referenced
+) {
+    return rawrtc_sctp_capabilities_create(capabilitiesp, RAWRTC_SCTP_TRANSPORT_MAX_MESSAGE_SIZE);
+}
+
+/*
+ * Set the SCTP transport's data channel handler.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_data_channel_handler(
+    struct rawrtc_sctp_transport* const transport,
+    rawrtc_data_channel_handler const data_channel_handler  // nullable
+) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set data channel handler & done
+    transport->data_channel_handler = data_channel_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SCTP transport's data channel handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_data_channel_handler(
+    rawrtc_data_channel_handler* const data_channel_handlerp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!data_channel_handlerp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get message handler (if any)
+    if (transport->data_channel_handler) {
+        *data_channel_handlerp = transport->data_channel_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the SCTP transport's state change handler.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_state_change_handler(
+    struct rawrtc_sctp_transport* const transport,
+    rawrtc_sctp_transport_state_change_handler const state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set data channel handler & done
+    transport->state_change_handler = state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SCTP transport's state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_state_change_handler(
+    rawrtc_sctp_transport_state_change_handler* const state_change_handlerp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!state_change_handlerp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get message handler (if any)
+    if (transport->state_change_handler) {
+        *state_change_handlerp = transport->state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/sctp_transport/meson.build b/src/sctp_transport/meson.build
new file mode 100644
index 0000000..0227c31
--- /dev/null
+++ b/src/sctp_transport/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/sctp_transport/transport.c b/src/sctp_transport/transport.c
new file mode 100644
index 0000000..46e79b2
--- /dev/null
+++ b/src/sctp_transport/transport.c
@@ -0,0 +1,3328 @@
+#include "transport.h"
+#include "../crc32c/crc32c.h"
+#include "../data_channel/channel.h"
+#include "../data_channel_parameters/parameters.h"
+#include "../data_transport/transport.h"
+#include "../main/main.h"
+#include "../sctp_capabilities/capabilities.h"
+#include <rawrtcdc/config.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/message_buffer.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <usrsctp.h>
+#include <errno.h>  // errno
+#include <limits.h>  // INT_MAX
+#include <netinet/in.h>  // IPPROTO_SCTP, htons, ntohs
+#include <stdio.h>  // fopen, ...
+#include <string.h>  // memcpy, strlen
+#include <sys/socket.h>  // AF_CONN, SOCK_STREAM, linger
+
+#define DEBUG_MODULE "sctp-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+// TODO: Get from config
+uint8_t checksum_flags = RAWRTC_SCTP_TRANSPORT_CHECKSUM_ENABLE_ALL;
+
+// SCTP outgoing message context (needed when buffering)
+struct send_context {
+    unsigned int info_type;
+    union {
+        struct sctp_sndinfo sndinfo;
+        struct sctp_sendv_spa spa;
+    } info;
+    int flags;
+};
+
+// Events to subscribe to
+static uint16_t const sctp_events[] = {
+    SCTP_ASSOC_CHANGE,
+    // SCTP_PEER_ADDR_CHANGE,
+    // SCTP_REMOTE_ERROR,
+    SCTP_PARTIAL_DELIVERY_EVENT,
+    SCTP_SEND_FAILED_EVENT,
+    SCTP_SENDER_DRY_EVENT,
+    SCTP_SHUTDOWN_EVENT,
+    // SCTP_ADAPTATION_INDICATION,
+    SCTP_STREAM_CHANGE_EVENT,
+    SCTP_STREAM_RESET_EVENT,
+};
+static size_t const sctp_events_length = ARRAY_SIZE(sctp_events);
+
+static enum rawrtc_code channel_context_create(
+    struct rawrtc_sctp_data_channel_context** const contextp,  // de-referenced, not checked
+    uint16_t const sid,
+    bool const can_send_unordered);
+
+static bool channel_registered(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel  // not checked
+);
+
+static void channel_register(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel,  // referenced, not checked
+    struct rawrtc_sctp_data_channel_context* const context,  // referenced, not checked
+    bool const raise_event);
+
+static enum rawrtc_code sctp_transport_send(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct mbuf* const buffer,  // not checked
+    void* const info,  // not checked
+    socklen_t const info_size,
+    unsigned int const info_type,
+    int const flags);
+
+/*
+ * Parse a data channel open message.
+ */
+static enum rawrtc_code data_channel_open_message_parse(
+    struct rawrtc_data_channel_parameters** const parametersp,  // de-referenced, not checked
+    uint_fast16_t* const priorityp,  // de-referenced, not checked
+    uint16_t const id,
+    struct mbuf* const buffer  // not checked
+) {
+    uint_fast8_t channel_type;
+    uint_fast16_t priority;
+    uint_fast32_t reliability_parameter;
+    uint_fast16_t label_length;
+    uint_fast16_t protocol_length;
+    int err;
+    char* label = NULL;
+    char* protocol = NULL;
+    enum rawrtc_code error;
+
+    // Check length
+    // Note: -1 because we've already removed the message type
+    if (mbuf_get_left(buffer) < (RAWRTC_DCEP_MESSAGE_OPEN_BASE_SIZE - 1)) {
+        return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+
+    // Get fields
+    channel_type = mbuf_read_u8(buffer);
+    priority = ntohs(mbuf_read_u16(buffer));
+    reliability_parameter = ntohl(mbuf_read_u32(buffer));
+    label_length = ntohs(mbuf_read_u16(buffer));
+    protocol_length = ntohs(mbuf_read_u16(buffer));
+
+    // Validate channel type
+    switch (channel_type) {
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_ORDERED:
+        case RAWRTC_DATA_CHANNEL_TYPE_RELIABLE_UNORDERED:
+        case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_RETRANSMIT:
+        case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_RETRANSMIT:
+        case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_TIMED:
+        case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_TIMED:
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+
+        // Get label
+#if (UINT_FAST16_MAX > SIZE_MAX)
+    if (label_length > SIZE_MAX) {
+        return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+#endif
+    if (mbuf_get_left(buffer) < label_length) {
+        return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+    if (label_length > 0) {
+        err = mbuf_strdup(buffer, &label, label_length);
+        if (err) {
+            error = rawrtc_error_to_code(err);
+            goto out;
+        }
+    }
+
+    // Get protocol
+#if (UINT_FAST16_MAX > SIZE_MAX)
+    if (protocol_length > SIZE_MAX) {
+        return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+#endif
+    if (mbuf_get_left(buffer) < protocol_length) {
+        return RAWRTC_CODE_INVALID_MESSAGE;
+    }
+    if (protocol_length > 0) {
+        err = mbuf_strdup(buffer, &protocol, protocol_length);
+        if (err) {
+            error = rawrtc_error_to_code(err);
+            goto out;
+        }
+    }
+
+    // Create data channel parameters
+    error = rawrtc_data_channel_parameters_create_internal(
+        parametersp, label, (enum rawrtc_data_channel_type) channel_type,
+        (uint32_t) reliability_parameter, protocol, false, id);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(label);
+    mem_deref(protocol);
+
+    if (!error) {
+        // Set priority value
+        *priorityp = priority;
+    }
+    return error;
+}
+
+/*
+ * Create a data channel open message.
+ */
+static enum rawrtc_code data_channel_open_message_create(
+    struct mbuf** const bufferp,  // de-referenced, not checked
+    struct rawrtc_data_channel_parameters const* const parameters  // not checked
+) {
+    size_t label_length;
+    size_t protocol_length;
+    struct mbuf* buffer;
+    int err;
+
+    // Get length of label and protocol
+    label_length = parameters->label ? strlen(parameters->label) : 0;
+    protocol_length = parameters->protocol ? strlen(parameters->protocol) : 0;
+
+    // Check string length
+#if (SIZE_MAX > UINT16_MAX)
+    if (label_length > UINT16_MAX || protocol_length > UINT16_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Allocate
+    buffer = mbuf_alloc(RAWRTC_DCEP_MESSAGE_OPEN_BASE_SIZE + label_length + protocol_length);
+    if (!buffer) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    err = mbuf_write_u8(buffer, RAWRTC_DCEP_MESSAGE_TYPE_OPEN);
+    err |= mbuf_write_u8(buffer, parameters->channel_type);
+    err |= mbuf_write_u16(buffer, htons(RAWRTC_DCEP_CHANNEL_PRIORITY_NORMAL));  // TODO: Ok?
+    err |= mbuf_write_u32(buffer, htonl(parameters->reliability_parameter));
+    err |= mbuf_write_u16(buffer, htons((uint16_t) label_length));
+    err |= mbuf_write_u16(buffer, htons((uint16_t) protocol_length));
+    if (parameters->label) {
+        err |= mbuf_write_mem(buffer, (uint8_t*) parameters->label, label_length);
+    }
+    if (parameters->protocol) {
+        err |= mbuf_write_mem(buffer, (uint8_t*) parameters->protocol, protocol_length);
+    }
+
+    if (err) {
+        mem_deref(buffer);
+        return rawrtc_error_to_code(err);
+    } else {
+        // Set position
+        mbuf_set_pos(buffer, 0);
+
+        // Set pointer & done
+        *bufferp = buffer;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Create a data channel ack message.
+ */
+static enum rawrtc_code data_channel_ack_message_create(
+    struct mbuf** const bufferp  // de-referenced, not checked
+) {
+    int err;
+
+    // Allocate
+    struct mbuf* const buffer = mbuf_alloc(RAWRTC_DCEP_MESSAGE_ACK_BASE_SIZE);
+    if (!buffer) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    err = mbuf_write_u8(buffer, RAWRTC_DCEP_MESSAGE_TYPE_ACK);
+
+    if (err) {
+        mem_deref(buffer);
+        return rawrtc_error_to_code(err);
+    } else {
+        // Set position
+        mbuf_set_pos(buffer, 0);
+
+        // Set pointer & done
+        *bufferp = buffer;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Dump an SCTP packet into a trace file.
+ */
+static void trace_packet(
+    struct rawrtc_sctp_transport* const transport,
+    void* const buffer,
+    size_t const length,
+    int const direction) {
+    char* dump_buffer;
+
+    // Have trace handle?
+    if (!transport->trace_handle) {
+        return;
+    }
+
+    // Trace (if trace handle)
+    dump_buffer = usrsctp_dumppacket(buffer, length, direction);
+    if (dump_buffer) {
+        fprintf(transport->trace_handle, "%s", dump_buffer);
+        usrsctp_freedumpbuffer(dump_buffer);
+        fflush(transport->trace_handle);
+    }
+}
+
+/*
+ * Send a deferred SCTP message.
+ */
+static bool sctp_send_deferred_message(
+    struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_sctp_transport* const transport = arg;
+    struct send_context* const send_context = context;
+    enum rawrtc_code error;
+    void* info;
+    socklen_t info_size;
+
+    // Determine info pointer and info size
+    switch (send_context->info_type) {
+        case SCTP_SENDV_SNDINFO:
+            info = (void*) &send_context->info.sndinfo;
+            info_size = sizeof(send_context->info.sndinfo);
+            break;
+        case SCTP_SENDV_SPA:
+            info = (void*) &send_context->info.spa;
+            info_size = sizeof(send_context->info.spa);
+            break;
+        default:
+            error = RAWRTC_CODE_INVALID_STATE;
+            goto out;
+    }
+
+    // Try sending
+    DEBUG_PRINTF("Sending deferred message\n");
+    error = sctp_transport_send(
+        transport, buffer, info, info_size, send_context->info_type, send_context->flags);
+    switch (error) {
+        case RAWRTC_CODE_TRY_AGAIN_LATER:
+            // Stop iterating through message queue
+            return false;
+        case RAWRTC_CODE_MESSAGE_TOO_LONG:
+            DEBUG_WARNING("Incorrect message size guess, report this!\n");
+        default:
+            goto out;
+            break;
+    }
+
+out:
+    if (error) {
+        DEBUG_WARNING("Could not send buffered message, reason: %s\n", rawrtc_code_to_str(error));
+    }
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Send all deferred messages.
+ */
+static enum rawrtc_code sctp_send_deferred_messages(
+    struct rawrtc_sctp_transport* const transport  // not checked
+) {
+    // Send buffered outgoing SCTP packets
+    return rawrtc_message_buffer_clear(
+        &transport->buffered_messages_outgoing, sctp_send_deferred_message, transport);
+}
+
+/*
+ * Create outgoing message context for buffering SCTP messages.
+ */
+static enum rawrtc_code message_send_context_create(
+    struct send_context** const contextp,  // de-referenced, not checked
+    void* const info,  // not checked
+    unsigned int const info_type,
+    int const flags) {
+    enum rawrtc_code error;
+    struct send_context* context;
+
+    // Allocate context
+    context = mem_zalloc(sizeof(*context), NULL);
+    if (!context) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set context fields
+    context->info_type = info_type;
+    context->flags = flags;
+
+    // Copy info data (if any)
+    if (info_type != SCTP_SENDV_NOINFO && info) {
+        // Copy info data according to type
+        // Note: info_size will be ignored for buffered messages
+        switch (info_type) {
+            case SCTP_SENDV_SNDINFO:
+                memcpy(&context->info.sndinfo, info, sizeof(context->info.sndinfo));
+                break;
+            case SCTP_SENDV_SPA:
+                memcpy(&context->info.spa, info, sizeof(context->info.spa));
+                break;
+            default:
+                error = RAWRTC_CODE_INVALID_STATE;
+                goto out;
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(context);
+    } else {
+        // Set pointer
+        *contextp = context;
+    }
+
+    return error;
+}
+
+/*
+ * Send a message via the SCTP transport.
+ */
+static enum rawrtc_code sctp_send(
+    struct rawrtc_sctp_transport* const transport,
+    struct mbuf* const buffer,
+    void* const info,
+    socklen_t const info_size,
+    unsigned int const info_type,
+    int const flags) {
+    struct send_context* context;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !buffer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Clear buffered amount low flag
+    transport->flags &= ~RAWRTC_SCTP_TRANSPORT_FLAGS_BUFFERED_AMOUNT_LOW;
+
+    // Send directly (if connected and no outstanding messages)
+    if (transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED &&
+        list_isempty(&transport->buffered_messages_outgoing)) {
+        // Try sending
+        DEBUG_PRINTF("Message queue is empty, sending directly\n");
+        error = sctp_transport_send(transport, buffer, info, info_size, info_type, flags);
+        switch (error) {
+            case RAWRTC_CODE_SUCCESS:
+                // Done
+                return RAWRTC_CODE_SUCCESS;
+            case RAWRTC_CODE_TRY_AGAIN_LATER:
+                DEBUG_PRINTF("Need to buffer message and wait for a write request\n");
+                break;
+            case RAWRTC_CODE_MESSAGE_TOO_LONG:
+                DEBUG_WARNING("Incorrect message size guess, report this!\n");
+                return error;
+            default:
+                return error;
+        }
+    }
+
+    // Create message context (for buffering)
+    error = message_send_context_create(&context, info, info_type, flags);
+    if (error) {
+        goto out;
+    }
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&transport->buffered_messages_outgoing, buffer, context);
+    if (error) {
+        goto out;
+    }
+    DEBUG_PRINTF("Buffered outgoing message of size %zu\n", mbuf_get_left(buffer));
+
+out:
+    // Un-reference
+    mem_deref(context);
+
+    return error;
+}
+
+/*
+ * Send an SCTP message on the data channel.
+ * TODO: Add EOR marking and some kind of an id (does ndata provide that?)
+ */
+static enum rawrtc_code send_message(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel,  // nullable (if DCEP message)
+    struct rawrtc_sctp_data_channel_context* const context,  // not checked
+    struct mbuf* const buffer,  // not checked
+    uint_fast32_t const ppid) {
+    struct sctp_sendv_spa spa = {0};
+    enum rawrtc_code error;
+
+    // Set stream identifier, protocol identifier and flags
+    spa.sendv_sndinfo.snd_sid = context->sid;
+    spa.sendv_sndinfo.snd_flags = SCTP_EOR;  // TODO: Update signature
+    spa.sendv_sndinfo.snd_ppid = htonl((uint32_t) ppid);
+    spa.sendv_flags = SCTP_SEND_SNDINFO_VALID;
+
+    // Set ordered/unordered and partial reliability policy
+    if (ppid != RAWRTC_SCTP_TRANSPORT_PPID_DCEP) {
+        // Check channel
+        if (!channel) {
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+
+        // Unordered?
+        if (channel->parameters->channel_type & RAWRTC_DATA_CHANNEL_TYPE_IS_UNORDERED &&
+            context->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED) {
+            spa.sendv_sndinfo.snd_flags |= SCTP_UNORDERED;
+        }
+
+        // Partial reliability policy
+        switch (ppid) {
+            case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_RETRANSMIT:
+            case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_RETRANSMIT:
+                // Set amount of retransmissions
+                spa.sendv_prinfo.pr_policy = SCTP_PR_SCTP_RTX;
+                spa.sendv_prinfo.pr_value = channel->parameters->reliability_parameter;
+                spa.sendv_flags |= SCTP_SEND_PRINFO_VALID;
+                break;
+            case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_ORDERED_TIMED:
+            case RAWRTC_DATA_CHANNEL_TYPE_UNRELIABLE_UNORDERED_TIMED:
+                // Set TTL
+                spa.sendv_prinfo.pr_policy = SCTP_PR_SCTP_TTL;
+                spa.sendv_prinfo.pr_value = channel->parameters->reliability_parameter;
+                spa.sendv_flags |= SCTP_SEND_PRINFO_VALID;
+                break;
+            default:
+                // Nothing to do
+                break;
+        }
+    }
+
+    // Send message
+    DEBUG_PRINTF("Sending message with SID %" PRIu16 ", PPID: %" PRIu32 "\n", context->sid, ppid);
+    error = sctp_send(transport, buffer, &spa, sizeof(spa), SCTP_SENDV_SPA, 0);
+    if (error) {
+        DEBUG_WARNING("Unable to send message, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Change the states of all data channels.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_data_channel_states(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    enum rawrtc_data_channel_state const to_state,
+    enum rawrtc_data_channel_state const* const from_state  // optional current state
+) {
+    uint_fast16_t i;
+
+    // Set state on all data channels
+    for (i = 0; i < transport->n_channels; ++i) {
+        struct rawrtc_data_channel* const channel = transport->channels[i];
+        if (!channel) {
+            continue;
+        }
+
+        // Update state
+        if (!from_state || channel->state == *from_state) {
+            rawrtc_data_channel_set_state(channel, to_state);
+        }
+    }
+}
+
+/*
+ * Close all data channels.
+ * Warning: This will not use the closing procedure, use `channel_close_handler` instead.
+ */
+static void close_data_channels(struct rawrtc_sctp_transport* const transport  // not checked
+) {
+    uint_fast16_t i;
+
+    // Set state on all data channels
+    for (i = 0; i < transport->n_channels; ++i) {
+        struct rawrtc_data_channel* const channel = transport->channels[i];
+        if (!channel) {
+            continue;
+        }
+
+        // Update state
+        DEBUG_PRINTF("Closing (non-graceful) channel with SID %" PRIu16 "\n", i);
+        rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_CLOSED);
+
+        // Remove from transport
+        transport->channels[i] = mem_deref(channel);
+    }
+}
+
+/*
+ * Change the state of the SCTP transport.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_state(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    enum rawrtc_sctp_transport_state const state) {
+    // Closed?
+    if (state == RAWRTC_SCTP_TRANSPORT_STATE_CLOSED) {
+        DEBUG_INFO("SCTP connection closed\n");
+
+        // Close all data channels
+        close_data_channels(transport);
+
+        // Mark as detached & detach from DTLS transport
+        transport->flags |= RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED;
+        if (transport->context.detach_handler) {
+            transport->context.detach_handler(transport->context.arg);
+        }
+
+        // Close socket and deregister transport
+        if (transport->socket) {
+            usrsctp_close(transport->socket);
+            usrsctp_deregister_address(transport);
+            transport->socket = NULL;
+        }
+
+        // Close trace file (if any)
+        if (transport->trace_handle) {
+            if (fclose(transport->trace_handle)) {
+                DEBUG_WARNING("Could not close trace file, reason: %m\n", errno);
+            }
+        }
+    }
+
+    // Set state
+    transport->state = state;
+
+    // Connected?
+    // Note: This needs to be done after the state has been updated because it uses the
+    //       send function which checks the state.
+    if (state == RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        enum rawrtc_code error;
+        enum rawrtc_data_channel_state const from_channel_state =
+            RAWRTC_DATA_CHANNEL_STATE_CONNECTING;
+        DEBUG_INFO("SCTP connection established\n");
+
+        // Send deferred messages
+        error = sctp_send_deferred_messages(transport);
+        if (error && error != RAWRTC_CODE_STOP_ITERATION) {
+            DEBUG_WARNING(
+                "Could not send deferred messages, reason: %s\n", rawrtc_code_to_str(error));
+        }
+
+        // Open waiting channels
+        // Note: This call must be above calling the state handler to prevent the user from
+        //       being able to close the transport before the data channels are being opened.
+        set_data_channel_states(transport, RAWRTC_DATA_CHANNEL_STATE_OPEN, &from_channel_state);
+    }
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+/*
+ * Reset the outgoing stream of a data channel.
+ * Note: This function will only return an error in case the stream could not be reset properly
+ *       In this case, the channel will be closed and removed from the transport immediately.
+ */
+static enum rawrtc_code reset_outgoing_stream(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel  // not checked
+) {
+    struct sctp_reset_streams* reset_streams = NULL;
+    size_t length;
+    enum rawrtc_code error;
+
+    // Get context
+    struct rawrtc_sctp_data_channel_context* const context = channel->transport_arg;
+
+    // Check if there are pending outgoing messages
+    if (!list_isempty(&transport->buffered_messages_outgoing)) {
+        context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_STREAM_RESET;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Calculate length
+    length = sizeof(*reset_streams) + sizeof(uint16_t);
+
+    // Allocate
+    reset_streams = mem_zalloc(length, NULL);
+    if (!reset_streams) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Set fields
+    reset_streams->srs_flags = SCTP_STREAM_RESET_OUTGOING;
+    reset_streams->srs_number_streams = 1;
+    reset_streams->srs_stream_list[0] = context->sid;
+
+    // Reset stream
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_RESET_STREAMS, reset_streams,
+            (socklen_t) length)) {
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Done
+    DEBUG_PRINTF("Outgoing stream %" PRIu16 " reset procedure started\n", context->sid);
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(reset_streams);
+
+    if (error) {
+        // Improper closing
+        DEBUG_WARNING(
+            "Could not reset outgoing stream %" PRIu16 ", reason: %s, closing channel "
+            "improperly\n",
+            context->sid, rawrtc_code_to_str(error));
+
+        // Close
+        rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_CLOSED);
+
+        // Sanity check
+        if (!channel_registered(transport, channel)) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+
+        // Remove from transport
+        transport->channels[context->sid] = mem_deref(channel);
+    }
+    return error;
+}
+
+#if DEBUG_LEVEL >= 7
+/*
+ * Print debug information for an SCTP association change event.
+ */
+static int debug_association_change_event(
+    struct re_printf* const pf, struct sctp_assoc_change* const event) {
+    int err = 0;
+    uint_fast32_t length;
+    uint_fast32_t i;
+
+    switch (event->sac_state) {
+        case SCTP_COMM_UP:
+            err |= re_hprintf(pf, "SCTP_COMM_UP");
+            break;
+        case SCTP_COMM_LOST:
+            err |= re_hprintf(pf, "SCTP_COMM_LOST");
+            break;
+        case SCTP_RESTART:
+            err |= re_hprintf(pf, "SCTP_RESTART");
+            break;
+        case SCTP_SHUTDOWN_COMP:
+            err |= re_hprintf(pf, "SCTP_SHUTDOWN_COMP");
+            break;
+        case SCTP_CANT_STR_ASSOC:
+            err |= re_hprintf(pf, "SCTP_CANT_STR_ASSOC");
+            break;
+        default:
+            err |= re_hprintf(pf, "???");
+            break;
+    }
+    err |= re_hprintf(
+        pf, ", streams (in/out) = (%" PRIu16 "/%" PRIu16 ")", event->sac_inbound_streams,
+        event->sac_outbound_streams);
+    length = event->sac_length - sizeof(*event);
+    if (length > 0) {
+        switch (event->sac_state) {
+            case SCTP_COMM_UP:
+            case SCTP_RESTART:
+                err |= re_hprintf(pf, ", supports");
+                for (i = 0; i < length; ++i) {
+                    switch (event->sac_info[i]) {
+                        case SCTP_ASSOC_SUPPORTS_PR:
+                            err |= re_hprintf(pf, " PR");
+                            break;
+                        case SCTP_ASSOC_SUPPORTS_AUTH:
+                            err |= re_hprintf(pf, " AUTH");
+                            break;
+                        case SCTP_ASSOC_SUPPORTS_ASCONF:
+                            err |= re_hprintf(pf, " ASCONF");
+                            break;
+                        case SCTP_ASSOC_SUPPORTS_MULTIBUF:
+                            err |= re_hprintf(pf, " MULTIBUF");
+                            break;
+                        case SCTP_ASSOC_SUPPORTS_RE_CONFIG:
+                            err |= re_hprintf(pf, " RE-CONFIG");
+                            break;
+                        default:
+                            err |= re_hprintf(pf, " ??? (0x%02x)", event->sac_info[i]);
+                            break;
+                    }
+                }
+                break;
+            case SCTP_COMM_LOST:
+            case SCTP_CANT_STR_ASSOC:
+                err |= re_hprintf(pf, ", ABORT =");
+                for (i = 0; i < length; ++i) {
+                    err |= re_hprintf(pf, " 0x%02x", event->sac_info[i]);
+                }
+                break;
+            default:
+                break;
+        }
+    }
+    err |= re_hprintf(pf, "\n");
+    return err;
+}
+
+/*
+ * Print debug information for an SCTP partial delivery event.
+ */
+static int debug_partial_delivery_event(
+    struct re_printf* const pf, struct sctp_pdapi_event* const event) {
+    int err = 0;
+
+    switch (event->pdapi_indication) {
+        case SCTP_PARTIAL_DELIVERY_ABORTED:
+            re_hprintf(pf, "Partial delivery aborted ");
+            break;
+        default:
+            re_hprintf(pf, "??? ");
+            break;
+    }
+    err |= re_hprintf(pf, "(flags = %x) ", event->pdapi_flags);
+    err |= re_hprintf(pf, "stream = %" PRIu32 " ", event->pdapi_stream);
+    err |= re_hprintf(pf, "sn = %" PRIu32, event->pdapi_seq);
+    err |= re_hprintf(pf, "\n");
+    return err;
+}
+
+/*
+ * Print debug information for an SCTP send failed event.
+ */
+static int debug_send_failed_event(
+    struct re_printf* const pf, struct sctp_send_failed_event* const event) {
+    int err = 0;
+
+    if (event->ssfe_flags & SCTP_DATA_UNSENT) {
+        err |= re_hprintf(pf, "Unsent ");
+    }
+    if (event->ssfe_flags & SCTP_DATA_SENT) {
+        err |= re_hprintf(pf, "Sent ");
+    }
+    if (event->ssfe_flags & ~(SCTP_DATA_SENT | SCTP_DATA_UNSENT)) {
+        err |= re_hprintf(pf, "(flags = %x) ", event->ssfe_flags);
+    }
+    err |= re_hprintf(
+        pf,
+        "message with PPID %" PRIu32 ", SID = %" PRIu16 ", flags: 0x%04x due to error = 0x%08x\n",
+        ntohl(event->ssfe_info.snd_ppid), event->ssfe_info.snd_sid, event->ssfe_info.snd_flags,
+        event->ssfe_error);
+    return err;
+}
+
+/*
+ * Print debug information for an SCTP stream reset event.
+ */
+static int debug_stream_reset_event(
+    struct re_printf* const pf, struct sctp_stream_reset_event* const event) {
+    int err = 0;
+    uint_fast32_t length;
+    uint_fast32_t i;
+
+    // Get #sid's
+    length = (event->strreset_length - sizeof(*event)) / sizeof(uint16_t);
+
+    err |= re_hprintf(pf, "flags = %x, ", event->strreset_flags);
+    if (event->strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN) {
+        if (event->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {
+            err |= re_hprintf(pf, "incoming/");
+        }
+        err |= re_hprintf(pf, "incoming ");
+    }
+    if (event->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {
+        err |= re_hprintf(pf, "outgoing ");
+    }
+    err |= re_hprintf(pf, "stream ids = ");
+    for (i = 0; i < length; ++i) {
+        if (i > 0) {
+            err |= re_hprintf(pf, ", ");
+        }
+        err |= re_hprintf(pf, "%" PRIu16, event->strreset_stream_list[i]);
+    }
+    err |= re_hprintf(pf, "\n");
+    return err;
+}
+#endif
+
+/*
+ * Handle SCTP association change event.
+ */
+static void handle_association_change_event(
+    struct rawrtc_sctp_transport* const transport, struct sctp_assoc_change* const event) {
+#if DEBUG_LEVEL >= 7
+    // Print debug output for event
+    DEBUG_PRINTF("Association change: %H", debug_association_change_event, event);
+#endif
+
+    // Handle state
+    switch (event->sac_state) {
+        case SCTP_COMM_UP:
+            // Connected
+            if (transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING) {
+                set_state(transport, RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED);
+            }
+            break;
+        case SCTP_RESTART:
+            // TODO: Handle?
+            break;
+        case SCTP_CANT_STR_ASSOC:
+        case SCTP_SHUTDOWN_COMP:
+        case SCTP_COMM_LOST:
+            // Disconnected
+            if (!(transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED) &&
+                transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CLOSED) {
+                set_state(transport, RAWRTC_SCTP_TRANSPORT_STATE_CLOSED);
+            }
+            break;
+        default:
+            break;
+    }
+}
+
+/*
+ * Handle SCTP partial delivery event.
+ */
+static void handle_partial_delivery_event(
+    struct rawrtc_sctp_transport* const transport, struct sctp_pdapi_event* const event) {
+    uint16_t sid;
+    struct rawrtc_data_channel* channel;
+    struct rawrtc_sctp_data_channel_context* context;
+
+#if DEBUG_LEVEL >= 7
+    // Print debug output for event
+    DEBUG_PRINTF("Partial delivery event: %H", debug_partial_delivery_event, event);
+#endif
+
+    // Validate stream ID
+    if (event->pdapi_stream >= UINT16_MAX) {
+        DEBUG_WARNING(
+            "Invalid stream id in partial delivery event: %" PRIu32 "\n", event->pdapi_stream);
+        return;
+    }
+    sid = (uint16_t) event->pdapi_stream;
+
+    // Check if channel exists
+    // TODO: Need to check if channel is open?
+    if (sid >= transport->n_channels || !transport->channels[sid]) {
+        DEBUG_NOTICE("No channel registered for sid %" PRIu16 "\n", sid);
+        return;
+    }
+
+    // Get channel and context
+    channel = transport->channels[sid];
+    context = channel->transport_arg;
+
+    // Clear pending incoming message flag
+    context->flags &= ~RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+
+    // Abort pending message
+    if (context->buffer_inbound) {
+        DEBUG_NOTICE(
+            "Abort partially delivered message of %zu bytes\n",
+            mbuf_get_left(context->buffer_inbound));
+        context->buffer_inbound = mem_deref(context->buffer_inbound);
+
+        // Sanity-check
+        if (channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_STREAMED) {
+            DEBUG_WARNING("We deliver partially but there was a buffered message?!\n");
+        }
+    }
+
+    // Pass abort notification to handler
+    if (channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_STREAMED) {
+        enum rawrtc_data_channel_message_flag const message_flags =
+            RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_ABORTED;
+        if (channel->message_handler) {
+            channel->message_handler(NULL, message_flags, channel->arg);
+        } else {
+            DEBUG_NOTICE("No message handler, message abort notification has been discarded\n");
+        }
+    }
+}
+
+/*
+ * Handle SCTP send failed event.
+ */
+static void handle_send_failed_event(
+    struct rawrtc_sctp_transport* const transport, struct sctp_send_failed_event* const event) {
+#if DEBUG_LEVEL >= 7
+    // Print debug output for event
+    DEBUG_PRINTF("Send failed event: %H", debug_send_failed_event, event);
+#endif
+    (void) transport;
+    (void) event;
+}
+
+/*
+ * Raise buffered amount low on a data channel.
+ */
+static void raise_buffered_amount_low_event(
+    struct rawrtc_data_channel* const channel  // not checked
+) {
+    // Check for event handler
+    if (channel->buffered_amount_low_handler) {
+        // Get context
+        struct rawrtc_sctp_data_channel_context* const context = channel->transport_arg;
+        (void) context;
+
+        // Raise event
+        DEBUG_PRINTF(
+            "Raising buffered amount low event on channel with SID %" PRIu16 "\n", context->sid);
+        channel->buffered_amount_low_handler(channel->arg);
+    }
+}
+
+/*
+ * Handle SCTP sender dry (no outstanding data) event.
+ */
+static void handle_sender_dry_event(
+    struct rawrtc_sctp_transport* const transport, struct sctp_sender_dry_event* const event) {
+    uint_fast16_t i;
+    uint_fast16_t stop;
+    (void) event;
+
+    // If there are outstanding messages, don't raise an event
+    if (!list_isempty(&transport->buffered_messages_outgoing)) {
+        DEBUG_PRINTF("Pending messages, ignoring sender dry event\n");
+        return;
+    }
+
+    // Set buffered amount low
+    transport->flags |= RAWRTC_SCTP_TRANSPORT_FLAGS_BUFFERED_AMOUNT_LOW;
+
+    // Reset counter if #channels has been reduced
+    if (transport->current_channel_sid >= transport->n_channels) {
+        i = 0;
+    } else {
+        i = transport->current_channel_sid;
+    }
+
+    // Raise event on each data channel
+    stop = i;
+    do {
+        struct rawrtc_data_channel* const channel = transport->channels[i];
+        if (channel) {
+            struct rawrtc_sctp_data_channel_context* const context = channel->transport_arg;
+
+            // Handle flags
+            if (context->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_STREAM_RESET) {
+                // Reset pending outgoing stream
+                // TODO: This should probably be handled earlier but requires having separate
+                //       lists for each data channel to be sure that the stream is not reset before
+                //       all pending outgoing messages of that channel have been sent.
+                reset_outgoing_stream(transport, channel);
+                context->flags &= ~RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_STREAM_RESET;
+            } else {
+                // Raise event
+                raise_buffered_amount_low_event(transport->channels[i]);
+            }
+        }
+
+        // Update/wrap
+        // Note: uint16 is sufficient here as the maximum number of channels is
+        //       65534, so 65535 will still fit
+        i = (i + 1) % transport->n_channels;
+
+        // Stop if the flag has been cleared
+        if (!(transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_BUFFERED_AMOUNT_LOW)) {
+            break;
+        }
+    } while (i != stop);
+
+    // Update current channel SID
+    transport->current_channel_sid = i;
+}
+
+/*
+ * Handle stream reset event (data channel closed).
+ */
+static void handle_stream_reset_event(
+    struct rawrtc_sctp_transport* const transport, struct sctp_stream_reset_event* const event) {
+    uint_fast32_t length;
+    uint_fast32_t i;
+
+    // Get #sid's
+    length = (event->strreset_length - sizeof(*event)) / sizeof(uint16_t);
+
+#if DEBUG_LEVEL >= 7
+    // Print debug output for event
+    DEBUG_PRINTF("Stream reset event: %H", debug_stream_reset_event, event, length);
+#endif
+
+    // Ignore denied/failed events
+    if (event->strreset_flags & SCTP_STREAM_RESET_DENIED ||
+        event->strreset_flags & SCTP_STREAM_RESET_FAILED) {
+        return;
+    }
+
+    // Handle stream resets
+    for (i = 0; i < length; ++i) {
+        uint_fast16_t const sid = (uint_fast16_t) event->strreset_stream_list[i];
+        struct rawrtc_data_channel* channel;
+        struct rawrtc_sctp_data_channel_context* context;
+
+        // Check if channel exists
+        if (sid >= transport->n_channels || !transport->channels[sid]) {
+            DEBUG_NOTICE("No channel registered for sid %" PRIuFAST16 "\n", sid);
+            continue;
+        }
+
+        // Get channel and context
+        channel = transport->channels[sid];
+        context = channel->transport_arg;
+
+        // Incoming stream reset
+        if (event->strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN) {
+            // Note: Assuming that we can be sure that all pending incoming messages have been
+            //       received at that point.
+
+            // Set flag
+            channel->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_INCOMING_STREAM_RESET;
+
+            // Reset outgoing stream (if needed)
+            if (channel->state != RAWRTC_DATA_CHANNEL_STATE_CLOSING &&
+                channel->state != RAWRTC_DATA_CHANNEL_STATE_CLOSED) {
+                if (reset_outgoing_stream(transport, channel)) {
+                    // Error, channel has been closed automatically
+                    continue;
+                }
+
+                // Set to closing
+                rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_CLOSING);
+            }
+        }
+
+        // Outgoing stream reset (this is raised from our own stream reset)
+        if (event->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {
+            // Set flag
+            channel->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_OUTGOING_STREAM_RESET;
+        }
+
+        // Close if both incoming and outgoing stream has been reset
+        if (channel->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_INCOMING_STREAM_RESET &&
+            channel->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_OUTGOING_STREAM_RESET) {
+            // Set to closed
+            rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_CLOSED);
+
+            // Remove from transport
+            transport->channels[context->sid] = mem_deref(channel);
+        }
+    }
+}
+
+/*
+ * Handle SCTP notification.
+ */
+static void handle_notification(
+    struct rawrtc_sctp_transport* const transport, struct mbuf* const buffer) {
+    union sctp_notification* const notification = (union sctp_notification*) buffer->buf;
+
+    // TODO: Are all of these checks necessary or can we reduce that?
+#if (SIZE_MAX > UINT32_MAX)
+    if (buffer->end > UINT32_MAX) {
+        return;
+    }
+#endif
+#if (UINT32_MAX > SIZE_MAX)
+    if (notification->sn_header.sn_length > SIZE_MAX) {
+        return;
+    }
+#endif
+    if (notification->sn_header.sn_length != buffer->end) {
+        return;
+    }
+
+    // Handle notification by type
+    switch (notification->sn_header.sn_type) {
+        case SCTP_ASSOC_CHANGE:
+            handle_association_change_event(transport, &notification->sn_assoc_change);
+            break;
+        case SCTP_PARTIAL_DELIVERY_EVENT:
+            handle_partial_delivery_event(transport, &notification->sn_pdapi_event);
+            break;
+        case SCTP_SEND_FAILED_EVENT:
+            handle_send_failed_event(transport, &notification->sn_send_failed_event);
+            break;
+        case SCTP_SENDER_DRY_EVENT:
+            handle_sender_dry_event(transport, &notification->sn_sender_dry_event);
+            break;
+        case SCTP_SHUTDOWN_EVENT:
+            // TODO: Stop sending (this is a bit tricky to implement, so skipping for now)
+            // handle_shutdown_event(transport, &notification->sn_shutdown_event);
+            break;
+        case SCTP_STREAM_CHANGE_EVENT:
+            // TODO: Handle
+            DEBUG_WARNING("TODO: HANDLE STREAM CHANGE\n");
+            // handle_stream_change_event(transport, ...);
+            break;
+        case SCTP_STREAM_RESET_EVENT:
+            handle_stream_reset_event(transport, &notification->sn_strreset_event);
+            break;
+        default:
+            DEBUG_WARNING(
+                "Unexpected notification event: %" PRIu16 "\n", notification->sn_header.sn_type);
+            break;
+    }
+}
+
+/*
+ * Handle outgoing SCTP messages.
+ */
+static int sctp_packet_handler(
+    void* arg, void* buffer, size_t length, uint8_t tos, uint8_t set_df) {
+    struct rawrtc_sctp_transport* const transport = arg;
+    struct mbuf mbuffer;
+    enum rawrtc_code error;
+
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED) {
+        DEBUG_PRINTF("Ignoring SCTP packet ready event, transport is detached\n");
+        goto out;
+    }
+
+    // Calculate CRC32-C checksum
+    if (!(checksum_flags & RAWRTC_SCTP_TRANSPORT_CHECKSUM_DISABLE_OUTBOUND)) {
+        if (length >= sizeof(struct sctp_common_header)) {
+            struct sctp_common_header* const header = buffer;
+            // Note: The resulting checksum will be in network byte order
+            header->crc32c = rawrtc_crc32c(buffer, length);
+        } else {
+            DEBUG_WARNING("Outbound packet too short (%zu bytes), please report this!\n", length);
+            goto out;
+        }
+    }
+
+    // Trace (if trace handle)
+    // Note: No need to check if NULL as the function does it for us
+    trace_packet(transport, buffer, length, SCTP_DUMP_OUTBOUND);
+
+    // Note: We want to avoid copying if necessary. If the outbound handler needs to queue the data,
+    //       it is required to copy it.
+    mbuffer.buf = buffer;
+    mbuffer.pos = 0;
+    mbuffer.size = length;
+    mbuffer.end = length;
+
+    // Pass to outbound handler
+    error = transport->context.outbound_handler(&mbuffer, tos, set_df, transport->context.arg);
+    if (error) {
+        DEBUG_WARNING("Could not send packet, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+out:
+    // Note: The return code is irrelevant for usrsctp.
+    return 0;
+}
+
+/*
+ * Handle data channel ack message.
+ */
+static void handle_data_channel_ack_message(
+    struct rawrtc_sctp_transport* const transport, struct sctp_rcvinfo* const info) {
+    struct rawrtc_sctp_data_channel_context* context;
+
+    // Get channel and context
+    struct rawrtc_data_channel* const channel = transport->channels[info->rcv_sid];
+    if (!channel) {
+        DEBUG_WARNING("Received ack on an invalid channel with SID %" PRIu16 "\n", info->rcv_sid);
+        goto error;
+    }
+    context = channel->transport_arg;
+
+    // TODO: We should probably track the state and close the channel if an ack is being received
+    //       on an already negotiated channel. For now, we only check that the ack is the first
+    //       message received (which is fair enough but may not be 100% correct in the future).
+    if (context->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED) {
+        DEBUG_WARNING(
+            "Received ack but channel %" PRIu16 " is already negotiated\n", info->rcv_sid);
+        goto error;
+    }
+
+    // Messages may now be sent unordered
+    context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED;
+    return;
+
+error:
+    // TODO: Reset stream with SID on error
+    return;
+}
+
+/*
+ * Handle data channel open message.
+ */
+static void handle_data_channel_open_message(
+    struct rawrtc_sctp_transport* const transport,
+    struct mbuf* const buffer_in,
+    struct sctp_rcvinfo* const info) {
+    enum rawrtc_code error;
+    enum rawrtc_external_dtls_role dtls_role;
+    struct rawrtc_data_channel_parameters* parameters;
+    uint_fast16_t priority;
+    struct rawrtc_data_transport* data_transport = NULL;
+    struct rawrtc_data_channel* channel = NULL;
+    struct rawrtc_sctp_data_channel_context* context = NULL;
+    struct mbuf* buffer_out = NULL;
+
+    // Get DTLS role
+    error = transport->context.role_getter(&dtls_role, transport->context.arg);
+    if (error) {
+        DEBUG_WARNING("Getting external DTLS role failed: %s\n", rawrtc_code_to_str(error));
+        return;
+    }
+
+    // Check SID corresponds to other peer's role
+    switch (dtls_role) {
+        case RAWRTC_EXTERNAL_DTLS_ROLE_CLIENT:
+            // Other peer must have chosen an odd SID
+            if (info->rcv_sid % 2 != 1) {
+                DEBUG_WARNING("Other peer incorrectly chose an even SID\n");
+                return;
+            }
+            break;
+        case RAWRTC_EXTERNAL_DTLS_ROLE_SERVER:
+            // Other peer must have chosen an even SID
+            if (info->rcv_sid % 2 != 0) {
+                DEBUG_WARNING("Other peer incorrectly chose an odd SID\n");
+                return;
+            }
+            break;
+        default:
+            return;
+    }
+
+    // Check if slot is occupied
+    if (transport->channels[info->rcv_sid]) {
+        DEBUG_WARNING("Other peer chose already occupied SID %" PRIu16 "\n", info->rcv_sid);
+        return;
+    }
+
+    // Get parameters from data channel open message
+    error = data_channel_open_message_parse(&parameters, &priority, info->rcv_sid, buffer_in);
+    if (error) {
+        DEBUG_WARNING("Unable to parse DCEP open message, reason: %s\n", rawrtc_code_to_str(error));
+        return;
+    }
+
+    // Get data transport
+    error = rawrtc_sctp_transport_get_data_transport(&data_transport, transport);
+    if (error) {
+        DEBUG_WARNING("Unable to get data transport, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Create data channel
+    error = rawrtc_data_channel_create_internal(
+        &channel, data_transport, parameters, NULL, NULL, NULL, NULL, NULL, NULL, false);
+    if (error) {
+        DEBUG_WARNING("Unable to create data channel, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Allocate context to be used as an argument for the data channel handlers
+    error = channel_context_create(&context, info->rcv_sid, true);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to create data channel context, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // TODO: Store priority for SCTP ndata,
+    //       see https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.4
+    (void) priority;
+
+    // Create ack message
+    buffer_out = NULL;
+    error = data_channel_ack_message_create(&buffer_out);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to create data channel ack message, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Send message
+    DEBUG_PRINTF(
+        "Sending data channel ack message for channel with SID %" PRIu16 "\n", context->sid);
+    error = send_message(transport, NULL, context, buffer_out, RAWRTC_SCTP_TRANSPORT_PPID_DCEP);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to send data channel ack message, reason: %s\n", rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Register data channel
+    channel_register(transport, channel, context, true);
+
+    // TODO: Reset stream with SID on error
+
+out:
+    mem_deref(buffer_out);
+    mem_deref(context);
+    mem_deref(channel);
+    mem_deref(data_transport);
+    mem_deref(parameters);
+}
+
+/*
+ * Buffer incoming messages
+ *
+ * Return `RAWRTC_CODE_SUCCESS` in case the message is complete and
+ * should be handled. Otherwise, return `RAWRTC_CODE_NO_VALUE`.
+ */
+static enum rawrtc_code buffer_message_received_raise_complete(
+    struct mbuf** const buffer_inboundp,  // de-referenced, not checked
+    struct sctp_rcvinfo* const info_inboundp,  // de-referenced, not checked
+    struct mbuf* const message_buffer,  // not checked
+    struct sctp_rcvinfo* const info,  // not checked
+    int const flags) {
+    bool const complete = (flags & MSG_EOR) &&
+                          info->rcv_ppid != RAWRTC_SCTP_TRANSPORT_PPID_UTF16_PARTIAL &&
+                          info->rcv_ppid != RAWRTC_SCTP_TRANSPORT_PPID_BINARY_PARTIAL;
+    enum rawrtc_code error;
+
+    // Reference buffer and copy receive info (if first)
+    if (*buffer_inboundp == NULL) {
+        // Reference & set buffer
+        *buffer_inboundp = mem_ref(message_buffer);
+
+        // Copy receive info
+        memcpy(info_inboundp, info, sizeof(*info));
+
+        // Complete?
+        if (complete) {
+            DEBUG_PRINTF(
+                "Incoming message of size %zu is already complete\n",
+                mbuf_get_left(message_buffer));
+            error = RAWRTC_CODE_SUCCESS;
+            goto out;
+        }
+
+        // Clear headroom (if any)
+        if ((*buffer_inboundp)->pos > 0) {
+            error = rawrtc_error_to_code(mbuf_shift(*buffer_inboundp, -(*buffer_inboundp)->pos));
+            if (error) {
+                goto out;
+            }
+        }
+
+        // Skip to end (for upcoming chunks)
+        mbuf_skip_to_end(*buffer_inboundp);
+    }
+
+    // Copy message into existing buffer
+    error = rawrtc_error_to_code(
+        mbuf_write_mem(*buffer_inboundp, mbuf_buf(message_buffer), mbuf_get_left(message_buffer)));
+    if (error) {
+        goto out;
+    }
+    DEBUG_PRINTF("Buffered incoming message chunk of size %zu\n", mbuf_get_left(message_buffer));
+
+    // Stop (if not last message)
+    if (!complete) {
+        error = RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Set position & done
+    mbuf_set_pos(*buffer_inboundp, 0);
+    DEBUG_PRINTF("Merged incoming message chunks to size %zu\n", mbuf_get_left(*buffer_inboundp));
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        // Discard the message
+        *buffer_inboundp = mem_deref(*buffer_inboundp);
+    }
+    return error;
+}
+
+/*
+ * Handle incoming application data messages.
+ */
+static void handle_application_message(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct mbuf* const buffer,  // not checked
+    struct sctp_rcvinfo* info,  // not checked
+    int const flags) {
+    enum rawrtc_code error;
+    struct rawrtc_sctp_data_channel_context* context = NULL;
+    enum rawrtc_data_channel_message_flag message_flags = (enum rawrtc_data_channel_message_flag) 0;
+
+    // Get channel and context
+    struct rawrtc_data_channel* const channel = transport->channels[info->rcv_sid];
+    if (!channel) {
+        DEBUG_WARNING(
+            "Received application message on an invalid channel with SID %" PRIu16 "\n",
+            info->rcv_sid);
+        error = RAWRTC_CODE_INVALID_MESSAGE;
+        goto out;
+    }
+    context = channel->transport_arg;
+
+    // Messages may now be sent unordered
+    // TODO: Should we update this flag before or after the message has been received completely
+    //       (EOR)? Guessing: Once first chunk has been received.
+    context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED;
+
+    // Handle empty / Buffer if partial delivery is off / deliver directly
+    if (info->rcv_ppid == RAWRTC_SCTP_TRANSPORT_PPID_UTF16_EMPTY ||
+        info->rcv_ppid == RAWRTC_SCTP_TRANSPORT_PPID_BINARY_EMPTY) {
+        // Incomplete empty message?
+        if (!(flags & MSG_EOR)) {
+            DEBUG_WARNING("Empty but incomplete message\n");
+            error = RAWRTC_CODE_INVALID_MESSAGE;
+            goto out;
+        }
+
+        // Reference (because we un-reference at the end and copy info)
+        context->buffer_inbound = mem_ref(buffer);
+
+        // Let the buffer appear to be empty
+        mbuf_skip_to_end(context->buffer_inbound);
+
+        // Empty message is complete
+        context->flags &= ~RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+        message_flags |= RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_COMPLETE;
+
+    } else if (!(channel->flags & RAWRTC_DATA_CHANNEL_FLAGS_STREAMED)) {
+        // Buffer message (if needed) and get complete message (if any)
+        error = buffer_message_received_raise_complete(
+            &context->buffer_inbound, &context->info_inbound, buffer, info, flags);
+        switch (error) {
+            case RAWRTC_CODE_SUCCESS:
+                break;
+            case RAWRTC_CODE_NO_VALUE:
+                // Message buffered, early return here
+                context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+                return;
+            default:
+                DEBUG_WARNING(
+                    "Could not buffer/complete application message, reason: %s\n",
+                    rawrtc_code_to_str(error));
+                goto out;
+                break;
+        }
+
+        // Update info pointer
+        info = &context->info_inbound;
+
+        // Message is complete
+        context->flags &= ~RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+        message_flags |= RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_COMPLETE;
+    } else {
+        // Partial delivery on, pass buffer directly
+        context->buffer_inbound = mem_ref(buffer);
+
+        // Complete?
+        if (flags & MSG_EOR) {
+            context->flags &= ~RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+            message_flags |= RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_COMPLETE;
+        } else {
+            context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE;
+        }
+    }
+
+    // Handle application message
+    switch (info->rcv_ppid) {
+        case RAWRTC_SCTP_TRANSPORT_PPID_UTF16:
+        case RAWRTC_SCTP_TRANSPORT_PPID_UTF16_EMPTY:
+        case RAWRTC_SCTP_TRANSPORT_PPID_UTF16_PARTIAL:
+            message_flags |= RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_STRING;
+            break;
+        case RAWRTC_SCTP_TRANSPORT_PPID_BINARY:
+        case RAWRTC_SCTP_TRANSPORT_PPID_BINARY_EMPTY:
+        case RAWRTC_SCTP_TRANSPORT_PPID_BINARY_PARTIAL:
+            message_flags |= RAWRTC_DATA_CHANNEL_MESSAGE_FLAG_IS_BINARY;
+            break;
+        default:
+            DEBUG_WARNING(
+                "Ignored incoming message with unknown PPID: %" PRIu32 "\n", info->rcv_ppid);
+            error = RAWRTC_CODE_INVALID_MESSAGE;
+            goto out;
+            break;
+    }
+
+    // Pass message to handler
+    if (channel->message_handler) {
+        channel->message_handler(context->buffer_inbound, message_flags, channel->arg);
+    } else {
+        DEBUG_NOTICE(
+            "No message handler, message of %zu bytes has been discarded\n",
+            mbuf_get_left(context->buffer_inbound));
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to handle application message, reason: %s\n", rawrtc_code_to_str(error));
+
+        // TODO: Reset stream with SID
+        // Note: Correct behaviour of pending inbound message flag depends on closing the
+        //       channel on error here!
+    }
+
+    // Un-reference
+    if (context) {
+        context->buffer_inbound = mem_deref(context->buffer_inbound);
+    }
+}
+
+/*
+ * Handle incoming DCEP control message.
+ */
+static void handle_dcep_message(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct mbuf* const buffer,  // not checked
+    struct sctp_rcvinfo* info,  // not checked
+    int const flags) {
+    enum rawrtc_code error;
+    uint_fast16_t message_type;
+
+    // Buffer message (if needed) and get complete message (if any)
+    error = buffer_message_received_raise_complete(
+        &transport->buffer_dcep_inbound, &transport->info_dcep_inbound, buffer, info, flags);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            // Message buffered, early return here
+            return;
+        default:
+            DEBUG_WARNING(
+                "Could not buffer/complete DCEP message, reason: %s\n", rawrtc_code_to_str(error));
+            goto out;
+            break;
+    }
+
+    // Update info pointer
+    info = &transport->info_dcep_inbound;
+
+    // Handle by message type
+    // Note: There MUST be at least a byte present in the buffer as SCTP cannot handle empty
+    //       messages.
+    message_type = mbuf_read_u8(transport->buffer_dcep_inbound);
+    switch (message_type) {
+        case RAWRTC_DCEP_MESSAGE_TYPE_ACK:
+            DEBUG_PRINTF(
+                "Received data channel ack message for channel with SID %" PRIu16 "\n",
+                info->rcv_sid);
+            handle_data_channel_ack_message(transport, info);
+            break;
+        case RAWRTC_DCEP_MESSAGE_TYPE_OPEN:
+            DEBUG_PRINTF(
+                "Received data channel open message for channel with SID %" PRIu16 "\n",
+                info->rcv_sid);
+            handle_data_channel_open_message(transport, transport->buffer_dcep_inbound, info);
+            break;
+        default:
+            DEBUG_WARNING(
+                "Ignored incoming DCEP control message with unknown type: %" PRIu16 "\n",
+                message_type);
+            break;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        DEBUG_WARNING("Unable to handle DCEP message, reason: %s\n", rawrtc_code_to_str(error));
+
+        // TODO: Close channel?
+    }
+
+    // Un-reference
+    transport->buffer_dcep_inbound = mem_deref(transport->buffer_dcep_inbound);
+}
+
+/*
+ * Handle incoming data message.
+ */
+static void data_receive_handler(
+    struct rawrtc_sctp_transport* const transport,
+    struct mbuf* const buffer,
+    struct sctp_rcvinfo* const info,
+    int const flags) {
+    // Convert PPID first
+    info->rcv_ppid = ntohl(info->rcv_ppid);
+    DEBUG_PRINTF(
+        "Received message with SID %" PRIu16 ", PPID: %" PRIu32 "\n", info->rcv_sid,
+        info->rcv_ppid);
+
+    // Handle by PPID
+    if (info->rcv_ppid == RAWRTC_SCTP_TRANSPORT_PPID_DCEP) {
+        handle_dcep_message(transport, buffer, info, flags);
+    } else {
+        handle_application_message(transport, buffer, info, flags);
+    }
+}
+
+/*
+ * Handle usrsctp read event.
+ */
+static int read_event_handler(struct rawrtc_sctp_transport* const transport  // not checked
+) {
+    struct mbuf* buffer;
+    ssize_t length;
+    int ignore_events = RAWRTC_SCTP_EVENT_NONE;
+    struct sctp_rcvinfo info = {0};
+    socklen_t info_length = sizeof(info);
+    unsigned int info_type = 0;
+    int flags = 0;
+
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED) {
+        DEBUG_PRINTF("Ignoring read event, transport is detached\n");
+        return RAWRTC_SCTP_EVENT_ALL;
+    }
+
+    // TODO: Get next message size
+
+    // Create buffer
+    buffer = mbuf_alloc(rawrtcdc_global.usrsctp_chunk_size);
+    if (!buffer) {
+        DEBUG_WARNING("Cannot allocate buffer, no memory");
+        // TODO: This needs to be handled in a better way, otherwise it's probably going
+        // to cause another read call which calls this handler again resulting in an infinite
+        // loop.
+        return RAWRTC_SCTP_EVENT_NONE;
+    }
+
+    // Receive notification or data
+    length = usrsctp_recvv(
+        transport->socket, buffer->buf, buffer->size, NULL, NULL, &info, &info_length, &info_type,
+        &flags);
+    if (length < 0) {
+        switch (errno) {
+            case EAGAIN:
+#if (defined(EWOULDBLOCK) && EAGAIN != EWOULDBLOCK)
+            case EWOULDBLOCK:
+#endif
+                ignore_events = SCTP_EVENT_READ;
+                break;
+            default:
+                // Handle error
+                DEBUG_WARNING("SCTP receive failed, reason: %m\n", errno);
+                // TODO: What now? Close?
+                break;
+        }
+        goto out;
+    }
+
+    // Update buffer position and end
+    mbuf_set_end(buffer, (size_t) length);
+
+    // Handle notification
+    if (flags & MSG_NOTIFICATION) {
+        handle_notification(transport, buffer);
+        goto out;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        DEBUG_WARNING("Ignored incoming data before state 'connected'\n");
+        goto out;
+    }
+
+    // Have info?
+    if (info_type != SCTP_RECVV_RCVINFO) {
+        DEBUG_WARNING("Cannot handle incoming data without SCTP rcvfinfo\n");
+        goto out;
+    }
+
+    // Pass data to handler
+    data_receive_handler(transport, buffer, &info, flags);
+
+out:
+    // Un-reference
+    mem_deref(buffer);
+
+    // Done
+    return ignore_events;
+}
+
+/*
+ * Handle usrsctp write event.
+ */
+static int write_event_handler(struct rawrtc_sctp_transport* const transport  // not checked
+) {
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED) {
+        DEBUG_PRINTF("Ignoring write event, transport is detached\n");
+        return RAWRTC_SCTP_EVENT_ALL;
+    }
+
+    // Send all deferred messages (if not already sending)
+    // TODO: Check if this flag is really necessary
+    if (!(transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_SENDING_IN_PROGRESS)) {
+        enum rawrtc_code error;
+
+        // Send
+        transport->flags |= RAWRTC_SCTP_TRANSPORT_FLAGS_SENDING_IN_PROGRESS;
+        error = sctp_send_deferred_messages(transport);
+        transport->flags &= ~RAWRTC_SCTP_TRANSPORT_FLAGS_SENDING_IN_PROGRESS;
+        switch (error) {
+            case RAWRTC_CODE_SUCCESS:
+            case RAWRTC_CODE_STOP_ITERATION:
+                // We either sent all pending messages or could not send all messages, so there's
+                // no reason to react to further write events in this iteration
+                return SCTP_EVENT_WRITE;
+            default:
+                // TODO: What now? Close?
+                DEBUG_WARNING(
+                    "Could not send deferred messages, reason: %s\n", rawrtc_code_to_str(error));
+                return SCTP_EVENT_WRITE;
+        }
+    } else {
+        DEBUG_WARNING("Sending still in progress!\n");
+        // TODO: Is this correct?
+        return SCTP_EVENT_WRITE;
+    }
+}
+
+/*
+ * Handle usrsctp error event.
+ */
+static bool error_event_handler(struct rawrtc_sctp_transport* const transport  // not checked
+) {
+    // TODO: If we want to do anything with the DTLS transport, we need to check if we are
+    //       detached already.
+
+    // Closed?
+    if (transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CLOSED) {
+        DEBUG_PRINTF("Ignoring error event, transport is closed\n");
+        return RAWRTC_SCTP_EVENT_ALL;
+    }
+
+    // TODO: What am I supposed to do with this information?
+    DEBUG_WARNING("TODO: Handle SCTP error event\n");
+
+    // Continue handling events
+    // TODO: Probably depends on the error, right?
+    return RAWRTC_SCTP_EVENT_NONE;
+}
+
+/*
+ * usrsctp event handler helper.
+ */
+static void upcall_handler_helper(struct socket* socket, void* arg, int flags) {
+    int events = usrsctp_get_events(socket);
+    struct rawrtc_sctp_transport* const transport = arg;
+    int ignore_events = RAWRTC_SCTP_EVENT_NONE;
+    (void) flags;  // TODO: What does this indicate?
+
+    // TODO: This loop may lead to long blocking and is unfair to normal fds.
+    //       It's a compromise because scheduling repetitive timers in re's event loop seems to
+    //       be slow.
+    while (events) {
+        // TODO: This should work but it doesn't because usrsctp keeps switching from read to write
+        //       events endlessly for some reason. So, we need to discard previous events.
+        // ignore_events = RAWRTC_SCTP_EVENT_NONE;
+
+        // Handle error event
+        if (events & SCTP_EVENT_ERROR) {
+            ignore_events |= error_event_handler(transport);
+        }
+
+        // Handle read event
+        if (events & SCTP_EVENT_READ) {
+            ignore_events |= read_event_handler(transport);
+        }
+
+        // Handle write event
+        if (events & SCTP_EVENT_WRITE) {
+            ignore_events |= write_event_handler(transport);
+        }
+
+        // Get upcoming events and remove events that should be ignored
+        events = usrsctp_get_events(socket) & ~ignore_events;
+    }
+}
+
+/*
+ * Destructor for an existing SCTP data channel array.
+ */
+static void data_channels_destroy(void* arg) {
+    struct rawrtc_sctp_transport* const transport = arg;
+    uint_fast16_t i;
+
+    // Un-reference all members
+    for (i = 0; i < transport->n_channels; ++i) {
+        mem_deref(transport->channels[i]);
+    }
+}
+
+/*
+ * Create SCTP data channel array.
+ *
+ * Warning: Will not pre-fill stream IDs of the members!
+ */
+static enum rawrtc_code data_channels_alloc(
+    struct rawrtc_data_channel*** channelsp,  // de-referenced
+    uint_fast16_t const n_channels,
+    uint_fast16_t const n_channels_previously) {
+    size_t i;
+    struct rawrtc_data_channel** channels;
+
+    // Check arguments
+    if (!channelsp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocated before and #channels is decreasing?
+    if (n_channels_previously > 0 && n_channels < n_channels_previously) {
+        // Ensure we're not removing active data channels
+        for (i = 0; i < n_channels_previously; ++i) {
+            if ((*channelsp)[i]) {
+                return RAWRTC_CODE_STILL_IN_USE;
+            }
+        }
+    }
+
+    // Allocate
+    channels = mem_reallocarray(*channelsp, n_channels, sizeof(*channels), data_channels_destroy);
+    if (!channels) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Initialise
+    // Note: We can safely multiply 'n_channels' with size of the struct as 'mem_reallocarray'
+    //       ensures that it does not overflow (returns NULL).
+    if (n_channels > n_channels_previously) {
+        struct rawrtc_data_channel** channels_offset = channels + n_channels_previously;
+        memset(channels_offset, 0, (n_channels - n_channels_previously) * sizeof(*channels));
+    }
+
+    // Set pointer & done
+    *channelsp = channels;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+static enum rawrtc_code validate_context(
+    struct rawrtc_sctp_transport_context* const context  // not checked
+) {
+    // Ensure the context contains all necessary callbacks
+    if (!context->role_getter || !context->state_getter || !context->outbound_handler ||
+        !context->detach_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    } else {
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Destructor for an existing SCTP transport.
+ */
+static void rawrtc_sctp_transport_destroy(void* arg) {
+    struct rawrtc_sctp_transport* const transport = arg;
+
+    // Stop transport
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_sctp_transport_stop(transport);
+
+    // Call 'destroyed' handler (if fully initialised)
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_INITIALIZED &&
+        transport->context.destroyed_handler) {
+        transport->context.destroyed_handler(transport->context.arg);
+    }
+
+    // Un-reference
+    mem_deref(transport->channels);
+    mem_deref(transport->buffer_dcep_inbound);
+    list_flush(&transport->buffered_messages_outgoing);
+
+    // Decrease in-use counter
+    --rawrtcdc_global.usrsctp_initialized;
+
+    // Close usrsctp (if needed)
+    if (rawrtcdc_global.usrsctp_initialized == 0) {
+        // Cancel timer
+        enum rawrtc_code error = rawrtcdc_global.timer_handler(false, 0);
+        if (error) {
+            DEBUG_WARNING("Unable to cancel timer, reason: %s\n", rawrtc_code_to_str(error));
+        }
+
+        // Close
+        usrsctp_finish();
+        DEBUG_PRINTF("Closed usrsctp\n");
+    }
+}
+
+/*
+ * Create an SCTP transport from an external DTLS transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_create_from_external(
+    struct rawrtc_sctp_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport_context* const context,  // copied
+    uint16_t 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;
+    enum rawrtc_external_dtls_transport_state dtls_transport_state;
+    uint_fast16_t n_channels;
+    struct rawrtc_sctp_transport* transport;
+    struct sctp_assoc_value av;
+    struct linger linger_option;
+    struct sctp_event sctp_event = {0};
+    size_t i;
+    int option_value;
+    struct sockaddr_conn peer = {0};
+
+    // Check arguments
+    if (!transportp || !context) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure the context contains all necessary callbacks
+    error = validate_context(context);
+    if (error) {
+        return error;
+    }
+
+    // Check DTLS transport state
+    error = context->state_getter(&dtls_transport_state, context->arg);
+    if (error) {
+        DEBUG_WARNING(
+            "Getting external DTLS transport state failed: %s\n", rawrtc_code_to_str(error));
+        return RAWRTC_CODE_EXTERNAL_ERROR;
+    }
+    if (dtls_transport_state == RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set number of channels
+    // TODO: Get from config or remove this option
+    n_channels = RAWRTC_SCTP_TRANSPORT_DEFAULT_NUMBER_OF_STREAMS;
+
+    // Set default port (if 0)
+    if (port == 0) {
+        port = RAWRTC_SCTP_TRANSPORT_DEFAULT_PORT;
+    }
+
+    // Initialise usrsctp (if needed)
+    if (rawrtcdc_global.usrsctp_initialized == 0) {
+        DEBUG_PRINTF("Initialising usrsctp\n");
+        usrsctp_init(0, sctp_packet_handler, dbg_info);
+
+        // TODO: Debugging depending on options
+#if DEBUG_LEVEL >= 7 && defined(SCTP_DEBUG)
+        usrsctp_sysctl_set_sctp_debug_on(SCTP_DEBUG_ALL);
+#endif
+
+        // Do not send ABORTs in response to INITs (1).
+        // Do not send ABORTs for received Out of the Blue packets (2).
+        usrsctp_sysctl_set_sctp_blackhole(2);
+
+        // Disable the Explicit Congestion Notification extension
+        // TODO: Do we want to re-enable this?
+        usrsctp_sysctl_set_sctp_ecn_enable(0);
+
+        // Disable the Address Reconfiguration extension
+        usrsctp_sysctl_set_sctp_asconf_enable(0);
+
+        // Disable the Authentication extension
+        usrsctp_sysctl_set_sctp_auth_enable(0);
+
+        // Disable the NR-SACK extension (not standardised)
+        usrsctp_sysctl_set_sctp_nrsack_enable(0);
+
+        // Disable the Packet Drop Report extension (not standardised)
+        usrsctp_sysctl_set_sctp_pktdrop_enable(0);
+
+        // Enable the Partial Reliability extension
+        usrsctp_sysctl_set_sctp_pr_enable(1);
+
+        // Set amount of incoming streams
+        usrsctp_sysctl_set_sctp_nr_incoming_streams_default((uint32_t) n_channels);
+
+        // Set amount of outgoing streams
+        usrsctp_sysctl_set_sctp_nr_outgoing_streams_default((uint32_t) n_channels);
+
+        // Enable interleaving messages for different streams (incoming)
+        // See: https://tools.ietf.org/html/rfc6458#section-8.1.20
+        usrsctp_sysctl_set_sctp_default_frag_interleave(2);
+
+        // Disable default CRC32-C checksum calculation
+        // Note: We may or may not calculate and verify the checksum depending on the
+        //       configuration.
+        usrsctp_enable_crc32c_offload();
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_sctp_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Increase in-use counter
+    // Note: This needs to be below allocation to ensure the counter is decreased properly on error
+    ++rawrtcdc_global.usrsctp_initialized;
+
+    // Set fields/reference
+    transport->context = *context;
+    transport->flags = 0;
+    transport->state = RAWRTC_SCTP_TRANSPORT_STATE_NEW;  // TODO: Raise state (delayed)?
+    transport->port = port;
+    transport->data_channel_handler = data_channel_handler;
+    transport->state_change_handler = state_change_handler;
+    transport->arg = arg;
+    list_init(&transport->buffered_messages_outgoing);
+
+    // Start timer (if needed)
+    if (rawrtcdc_global.usrsctp_initialized == 1) {
+        error = rawrtcdc_global.timer_handler(
+            true, (uint_fast16_t) RAWRTC_SCTP_TRANSPORT_TIMER_TIMEOUT);
+        if (error) {
+            DEBUG_WARNING("Scheduling timer failed: %s\n", rawrtc_code_to_str(error));
+            error = RAWRTC_CODE_EXTERNAL_ERROR;
+            goto out;
+        }
+    }
+
+    // Allocate channel array
+    error = data_channels_alloc(&transport->channels, n_channels, 0);
+    if (error) {
+        goto out;
+    }
+    transport->n_channels = n_channels;
+    transport->current_channel_sid = 0;
+
+    // Create packet tracer
+    // TODO: Debug mode only, filename set by debug options
+    if (context->trace_packets) {
+        char trace_handle_id[8];
+        char* trace_handle_name;
+
+        // Create trace handle ID
+        rand_str(trace_handle_id, sizeof(trace_handle_id));
+        error = rawrtc_sdprintf(&trace_handle_name, "trace-sctp-%s.hex", trace_handle_id);
+        if (error) {
+            DEBUG_WARNING("Could create trace file name, reason: %s\n", rawrtc_code_to_str(error));
+        } else {
+            // Open trace file
+            transport->trace_handle = fopen(trace_handle_name, "w");
+            mem_deref(trace_handle_name);
+            if (!transport->trace_handle) {
+                DEBUG_WARNING("Could not open trace file, reason: %m\n", errno);
+            } else {
+                DEBUG_INFO("Using trace handle id: %s\n", trace_handle_id);
+            }
+        }
+    }
+
+    // Create SCTP socket
+    DEBUG_PRINTF("Creating SCTP socket\n");
+    transport->socket = usrsctp_socket(AF_CONN, SOCK_STREAM, IPPROTO_SCTP, NULL, NULL, 0, NULL);
+    if (!transport->socket) {
+        DEBUG_WARNING("Could not create socket, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Register instance
+    usrsctp_register_address(transport);
+
+    // Make socket non-blocking
+    if (usrsctp_set_non_blocking(transport->socket, 1)) {
+        DEBUG_WARNING("Could not set to non-blocking, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Set event callback
+    if (usrsctp_set_upcall(transport->socket, upcall_handler_helper, transport)) {
+        DEBUG_WARNING("Could not set event callback (upcall), reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Determine chunk size
+    if (rawrtcdc_global.usrsctp_initialized == 1) {
+        socklen_t option_size = sizeof(int);  // PD point is int according to spec
+        if (usrsctp_getsockopt(
+                transport->socket, IPPROTO_SCTP, SCTP_PARTIAL_DELIVERY_POINT, &option_value,
+                &option_size)) {
+            DEBUG_WARNING("Could not retrieve partial delivery point, reason: %m\n", errno);
+            error = rawrtc_error_to_code(errno);
+            goto out;
+        }
+
+        // Check value
+        if (option_size != sizeof(int) || option_value < 1) {
+            DEBUG_WARNING("Invalid partial delivery point value: %d\n", option_value);
+            error = RAWRTC_CODE_INITIALISE_FAIL;
+            goto out;
+        }
+
+        // Store value
+        rawrtcdc_global.usrsctp_chunk_size = (size_t) option_value;
+        DEBUG_PRINTF("Chunk size: %zu\n", rawrtcdc_global.usrsctp_chunk_size);
+    }
+
+    // Enable the Stream Reconfiguration extension
+    av.assoc_id = SCTP_ALL_ASSOC;
+    av.assoc_value = SCTP_ENABLE_RESET_STREAM_REQ | SCTP_ENABLE_CHANGE_ASSOC_REQ;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_ENABLE_STREAM_RESET, &av,
+            sizeof(struct sctp_assoc_value))) {
+        DEBUG_WARNING("Could not enable stream reconfiguration extension, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // We want info
+    option_value = 1;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_RECVRCVINFO, &option_value,
+            sizeof(option_value))) {
+        DEBUG_WARNING("Could not set info option, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // TODO: Enable interleaving messages for different streams (outgoing)
+    //       This needs some work: https://github.com/rawrtc/rawrtc-data-channel/issues/14
+    // https://tools.ietf.org/html/draft-ietf-tsvwg-sctp-ndata-08#section-4.3.1
+    //
+    // av.assoc_id = SCTP_ALL_ASSOC;
+    // av.assoc_value = 1;
+    // if (usrsctp_setsockopt(transport->socket, IPPROTO_SCTP, SCTP_INTERLEAVING_SUPPORTED,
+    //         &av, sizeof(struct sctp_assoc_value))) {
+    //     DEBUG_WARNING("Could not enable ndata, reason: %m\n", errno);
+    //     error = rawrtc_error_to_code(errno);
+    //     goto out;
+    // }
+
+    // Discard pending packets when closing
+    // (so we don't get a callback when the transport is already free'd)
+    // TODO: Find a way to use graceful shutdown instead, otherwise the other peer would raise an
+    // error indication (https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.2)
+    linger_option.l_onoff = 1;
+    linger_option.l_linger = 0;
+    if (usrsctp_setsockopt(
+            transport->socket, SOL_SOCKET, SO_LINGER, &linger_option, sizeof(linger_option))) {
+        DEBUG_WARNING("Could not set linger options, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Set no delay option (disable nagle)
+    option_value = 1;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_NODELAY, &option_value, sizeof(option_value))) {
+        DEBUG_WARNING("Could not set no-delay, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Set explicit EOR
+    option_value = 1;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_EXPLICIT_EOR, &option_value,
+            sizeof(option_value))) {
+        DEBUG_WARNING("Could not enable explicit EOR, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // Subscribe to SCTP event notifications
+    sctp_event.se_assoc_id = SCTP_ALL_ASSOC;
+    sctp_event.se_on = 1;
+    for (i = 0; i < sctp_events_length; ++i) {
+        sctp_event.se_type = sctp_events[i];
+        if (usrsctp_setsockopt(
+                transport->socket, IPPROTO_SCTP, SCTP_EVENT, &sctp_event, sizeof(sctp_event))) {
+            DEBUG_WARNING("Could not subscribe to event notification, reason: %m", errno);
+            error = rawrtc_error_to_code(errno);
+            goto out;
+        }
+    }
+
+#if DEBUG_LEVEL >= 7
+    // Print send/receive buffer lengths
+    {
+        uint32_t send_buffer_length;
+        uint32_t receive_buffer_length;
+        error = rawrtc_sctp_transport_get_buffer_length(
+            &send_buffer_length, &receive_buffer_length, transport);
+        if (error) {
+            goto out;
+        }
+        DEBUG_PRINTF(
+            "Send/receive buffer length: %" PRIu32 "/%" PRIu32 "\n", send_buffer_length,
+            receive_buffer_length);
+    }
+
+    // Print congestion control algorithm
+    {
+        enum rawrtc_sctp_transport_congestion_ctrl congestion_ctrl_algorithm;
+        error = rawrtc_sctp_transport_get_congestion_ctrl_algorithm(
+            &congestion_ctrl_algorithm, transport);
+        if (error) {
+            goto out;
+        }
+        DEBUG_PRINTF(
+            "Congestion control algorithm: %s\n",
+            rawrtc_sctp_transport_congestion_ctrl_algorithm_to_name(congestion_ctrl_algorithm));
+    }
+#endif
+
+    // Bind local address
+    peer.sconn_family = AF_CONN;
+    // TODO: Check for existance of sconn_len
+    // sconn.sconn_len = sizeof(peer);
+    peer.sconn_port = htons(transport->port);
+    peer.sconn_addr = transport;
+    if (usrsctp_bind(transport->socket, (struct sockaddr*) &peer, sizeof(peer))) {
+        DEBUG_WARNING("Could not bind local address, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer & mark as initialised
+        *transportp = transport;
+        transport->flags |= RAWRTC_SCTP_TRANSPORT_FLAGS_INITIALIZED;
+    }
+    return error;
+}
+
+/*
+ * Destructor for an existing data channel context.
+ */
+static void channel_context_destroy(void* arg) {
+    struct rawrtc_sctp_data_channel_context* const context = arg;
+
+    // Un-reference
+    mem_deref(context->buffer_inbound);
+}
+
+/*
+ * Allocate data channel context.
+ */
+static enum rawrtc_code channel_context_create(
+    struct rawrtc_sctp_data_channel_context** const contextp,  // de-referenced, not checked
+    uint16_t const sid,
+    bool const can_send_unordered) {
+    // Allocate context
+    struct rawrtc_sctp_data_channel_context* const context =
+        mem_zalloc(sizeof(*context), channel_context_destroy);
+    if (!context) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    context->sid = sid;
+    if (can_send_unordered) {
+        context->flags |= RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED;
+    }
+
+    // Set pointer & done
+    *contextp = context;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Check if a data channel is registered on the transport.
+ */
+static bool channel_registered(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel  // not checked
+) {
+    // Get context
+    struct rawrtc_sctp_data_channel_context* const context = channel->transport_arg;
+
+    // Check status
+    if (transport->channels[context->sid] != channel) {
+        DEBUG_WARNING("Invalid channel instance in slot. Please report this.\n");
+        return false;
+    } else {
+        return true;
+    }
+}
+
+/*
+ * Register data channel on transport.
+ */
+static void channel_register(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel* const channel,  // referenced, not checked
+    struct rawrtc_sctp_data_channel_context* const context,  // referenced, not checked
+    bool const raise_event) {
+    // Update channel with referenced context
+    channel->transport_arg = mem_ref(context);
+    transport->channels[context->sid] = mem_ref(channel);
+
+    // Raise data channel event?
+    if (raise_event) {
+        // Call data channel handler (if any)
+        rawrtc_data_channel_call_channel_handler(
+            channel, transport->data_channel_handler, transport->arg);
+    }
+
+    // Update data channel state
+    if (transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_OPEN);
+    }
+}
+
+/*
+ * Create a negotiated SCTP data channel.
+ */
+static enum rawrtc_code channel_create_negotiated(
+    struct rawrtc_sctp_data_channel_context** const contextp,  // de-referenced, not checked
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel_parameters const* const parameters  // read-only
+) {
+    // Check SID (> max, >= n_channels, or channel already occupied)
+    if (parameters->id > RAWRTC_SCTP_TRANSPORT_SID_MAX || parameters->id >= transport->n_channels ||
+        transport->channels[parameters->id]) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate context to be used as an argument for the data channel handlers
+    // TODO: Is it okay to already allow sending unordered messages here? Assuming: Yes.
+    return channel_context_create(contextp, parameters->id, true);
+}
+
+/*
+ * Create an SCTP data channel that needs negotiation.
+ */
+static enum rawrtc_code channel_create_inband(
+    struct rawrtc_sctp_data_channel_context** const contextp,  // de-referenced, not checked
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_data_channel_parameters const* const parameters  // read-only
+) {
+    enum rawrtc_code error;
+    enum rawrtc_external_dtls_transport_state dtls_transport_state;
+    enum rawrtc_external_dtls_role dtls_role;
+    uint_fast16_t i;
+    struct rawrtc_sctp_data_channel_context* context;
+    struct mbuf* buffer;
+
+    // Get DTLS transport state
+    error = transport->context.state_getter(&dtls_transport_state, transport->context.arg);
+    if (error) {
+        DEBUG_WARNING(
+            "Getting external DTLS transport state failed: %s\n", rawrtc_code_to_str(error));
+        return RAWRTC_CODE_EXTERNAL_ERROR;
+    }
+
+    // Check DTLS state
+    // Note: We need to have an open DTLS connection to determine whether we use odd or even
+    // SIDs.
+    // TODO: Can we fix this somehow to make it possible to create data channels earlier?
+    if (dtls_transport_state != RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CONNECTED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get DTLS role
+    error = transport->context.role_getter(&dtls_role, transport->context.arg);
+    if (error) {
+        DEBUG_WARNING("Getting external DTLS role failed: %s\n", rawrtc_code_to_str(error));
+        return RAWRTC_CODE_EXTERNAL_ERROR;
+    }
+
+    // Use odd or even SIDs
+    switch (dtls_role) {
+        case RAWRTC_EXTERNAL_DTLS_ROLE_CLIENT:
+            i = 0;
+            break;
+        case RAWRTC_EXTERNAL_DTLS_ROLE_SERVER:
+            i = 1;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Find free SID
+    context = NULL;
+    for (; i < transport->n_channels; i += 2) {
+        if (!transport->channels[i]) {
+            // Allocate context to be used as an argument for the data channel handlers
+            error = channel_context_create(&context, (uint16_t) i, false);
+            if (error) {
+                return error;
+            }
+            break;
+        }
+    }
+    if (!context) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Create open message
+    buffer = NULL;
+    error = data_channel_open_message_create(&buffer, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Send message
+    DEBUG_PRINTF(
+        "Sending data channel open message for channel with SID %" PRIu16 "\n", context->sid);
+    error = send_message(transport, NULL, context, buffer, RAWRTC_SCTP_TRANSPORT_PPID_DCEP);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(buffer);
+
+    if (error) {
+        mem_deref(context);
+    } else {
+        // Set pointer
+        *contextp = context;
+    }
+    return error;
+}
+
+/*
+ * Create the SCTP data channel.
+ */
+static enum rawrtc_code channel_create_handler(
+    struct rawrtc_data_transport* const transport,
+    struct rawrtc_data_channel* const channel,  // referenced
+    struct rawrtc_data_channel_parameters const* const parameters  // read-only
+) {
+    struct rawrtc_sctp_transport* sctp_transport;
+    enum rawrtc_code error;
+    struct rawrtc_sctp_data_channel_context* context;
+
+    // Check arguments
+    if (!transport || !channel || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Check if closed?
+
+    // Get SCTP transport
+    sctp_transport = transport->transport;
+
+    // Create negotiated or in-band data channel
+    if (parameters->negotiated) {
+        error = channel_create_negotiated(&context, sctp_transport, parameters);
+    } else {
+        error = channel_create_inband(&context, sctp_transport, parameters);
+    }
+    if (error) {
+        return error;
+    }
+
+    // Register data channel
+    channel_register(sctp_transport, channel, context, false);
+
+    // Un-reference & done
+    mem_deref(context);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Close the data channel (transport handler).
+ */
+static enum rawrtc_code channel_close_handler(struct rawrtc_data_channel* const channel) {
+    struct rawrtc_sctp_transport* transport;
+    struct rawrtc_sctp_data_channel_context* context;
+
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Check if closed?
+
+    // Get SCTP transport & context
+    transport = channel->transport->transport;
+    context = channel->transport_arg;
+
+    // Un-reference channel and clear pointer (if channel was registered before)
+    // Note: The context will be NULL if the channel was not registered before
+    if (context) {
+        DEBUG_PRINTF("Closing (graceful) channel with SID %" PRIu16 "\n", context->sid);
+
+        // Sanity check
+        if (!channel_registered(transport, channel)) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+
+        // Reset outgoing streams
+        // Important: This function will change the state of the channel to CLOSED
+        //            and remove the channel from the transport on error.
+        if (!reset_outgoing_stream(transport, channel)) {
+            rawrtc_data_channel_set_state(channel, RAWRTC_DATA_CHANNEL_STATE_CLOSING);
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Send data via the data channel (transport handler).
+ */
+static enum rawrtc_code channel_send_handler(
+    struct rawrtc_data_channel* const channel,
+    struct mbuf* buffer,  // nullable (if size 0), referenced
+    bool const is_binary) {
+    struct rawrtc_sctp_transport* transport;
+    size_t length;
+    uint_fast32_t ppid;
+    struct mbuf* empty = NULL;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Check if closed?
+
+    // Get SCTP transport
+    transport = channel->transport->transport;
+
+    // We accept both a NULL buffer and a buffer of length 0
+    if (!buffer) {
+        length = 0;
+    } else {
+        length = mbuf_get_left(buffer);
+    }
+
+    // Empty message?
+    if (length == 0) {
+        // Set PPID
+        if (is_binary) {
+            ppid = RAWRTC_SCTP_TRANSPORT_PPID_BINARY_EMPTY;
+        } else {
+            ppid = RAWRTC_SCTP_TRANSPORT_PPID_UTF16_EMPTY;
+        }
+
+        // Create helper message as SCTP is unable to send messages of size 0
+        empty = mbuf_alloc(RAWRTC_SCTP_TRANSPORT_EMPTY_MESSAGE_SIZE);
+        if (!empty) {
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+
+        // Note: The content is being ignored
+        error = rawrtc_error_to_code(mbuf_write_u8(empty, 0));
+        if (error) {
+            goto out;
+        }
+
+        // Set position & pointer
+        mbuf_set_pos(empty, 0);
+        buffer = empty;
+    } else {
+        // Check size
+        if (transport->remote_maximum_message_size != 0 &&
+            length > transport->remote_maximum_message_size) {
+            return RAWRTC_CODE_MESSAGE_TOO_LONG;
+        }
+
+        // Set PPID
+        // Note: We will not use the deprecated fragmentation & reassembly
+        if (is_binary) {
+            ppid = RAWRTC_SCTP_TRANSPORT_PPID_BINARY;
+        } else {
+            ppid = RAWRTC_SCTP_TRANSPORT_PPID_UTF16;
+        }
+    }
+
+    // Send
+    error = send_message(transport, channel, channel->transport_arg, buffer, ppid);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(empty);
+
+    // Done
+    return error;
+}
+
+/*
+ * Check if we can enable or disable streaming.
+ */
+static enum rawrtc_code channel_set_streaming_handler(
+    struct rawrtc_data_channel* const channel, bool const on) {
+    struct rawrtc_sctp_data_channel_context* context;
+    (void) on;
+
+    // Check arguments
+    if (!channel) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get context
+    context = channel->transport_arg;
+
+    // Check if there's a pending incoming message
+    if (context->flags & RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE) {
+        return RAWRTC_CODE_STILL_IN_USE;
+    }
+
+    // Ok, streaming mode can be activated
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SCTP data transport instance.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_data_transport(
+    struct rawrtc_data_transport** const transportp,  // de-referenced
+    struct rawrtc_sctp_transport* const sctp_transport  // referenced
+) {
+    enum rawrtc_code error;
+    struct rawrtc_data_transport* transport;
+
+    // Check arguments
+    if (!sctp_transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check SCTP transport state
+    if (sctp_transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Create data transport
+    error = rawrtc_data_transport_create(
+        &transport, RAWRTC_DATA_TRANSPORT_TYPE_SCTP, sctp_transport, channel_create_handler,
+        channel_close_handler, channel_send_handler, channel_set_streaming_handler);
+    if (error) {
+        return error;
+    }
+
+    // Set pointer & done
+    *transportp = transport;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Start the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_start(
+    struct rawrtc_sctp_transport* const transport,
+    struct rawrtc_sctp_capabilities const* const remote_capabilities,  // copied
+    uint16_t remote_port  // zeroable
+) {
+    struct sockaddr_conn peer = {0};
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !remote_capabilities) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_NEW) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set default port (if 0)
+    if (remote_port == 0) {
+        remote_port = transport->port;
+    }
+
+    // Store maximum message size
+    transport->remote_maximum_message_size = remote_capabilities->max_message_size;
+
+    // Set remote address
+    peer.sconn_family = AF_CONN;
+    // TODO: Check for existance of sconn_len
+    // sconn.sconn_len = sizeof(peer);
+    peer.sconn_port = htons(remote_port);
+    peer.sconn_addr = transport;
+
+    // Connect
+    DEBUG_PRINTF("Connecting to peer\n");
+    if (usrsctp_connect(transport->socket, (struct sockaddr*) &peer, sizeof(peer)) &&
+        errno != EINPROGRESS) {
+        DEBUG_WARNING("Could not connect, reason: %m\n", errno);
+        error = rawrtc_error_to_code(errno);
+        goto out;
+    }
+
+    // TODO: Initiate Path MTU discovery (https://tools.ietf.org/html/rfc4821)
+    // by using probing messages (https://tools.ietf.org/html/rfc4820)
+    // see https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+
+    // Transition to connecting state
+    set_state(transport, RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING);
+
+    // Set remote address
+    transport->remote_address = peer;
+
+    // Set default MTU
+    error = rawrtc_sctp_transport_set_mtu(transport, (uint32_t) RAWRTC_SCTP_TRANSPORT_DEFAULT_MTU);
+    if (error) {
+        DEBUG_WARNING("Could not set MTU, reason: %s\n", rawrtc_code_to_str(error));
+        // Note: Continuing here since it may still work.
+    }
+
+out:
+    if (error) {
+        set_state(transport, RAWRTC_SCTP_TRANSPORT_STATE_CLOSED);
+    }
+    return error;
+}
+
+/*
+ * Stop and close the SCTP transport.
+ */
+enum rawrtc_code rawrtc_sctp_transport_stop(struct rawrtc_sctp_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED ||
+        transport->state == RAWRTC_SCTP_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update state
+    set_state(transport, RAWRTC_SCTP_TRANSPORT_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+
+    // TODO: Anything missing?
+}
+
+/*
+ * Send a message (non-deferred) via the SCTP transport.
+ */
+static enum rawrtc_code sctp_transport_send(
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct mbuf* const buffer,  // not checked
+    void* const info,  // not checked
+    socklen_t const info_size,
+    unsigned int const info_type,
+    int const flags) {
+    struct sctp_sndinfo* send_info;
+    bool eor_set;
+    size_t length;
+    ssize_t written;
+    enum rawrtc_code error;
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get reference to send flags
+    switch (info_type) {
+        case SCTP_SENDV_SNDINFO:
+            send_info = (struct sctp_sndinfo* const) info;
+            break;
+        case SCTP_SENDV_SPA:
+            send_info = &((struct sctp_sendv_spa* const) info)->sendv_sndinfo;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // EOR set?
+    eor_set = send_info->snd_flags & SCTP_EOR ? true : false;
+
+    // Send until buffer is empty
+    do {
+        size_t const left = mbuf_get_left(buffer);
+
+        // Carefully chunk the buffer
+        if (left > rawrtcdc_global.usrsctp_chunk_size) {
+            length = rawrtcdc_global.usrsctp_chunk_size;
+
+            // Unset EOR flag
+            send_info->snd_flags &= ~SCTP_EOR;
+        } else {
+            length = left;
+
+            // Reset EOR flag
+            if (eor_set) {
+                send_info->snd_flags |= SCTP_EOR;
+            }
+        }
+
+        // Send
+        DEBUG_PRINTF("Try sending %zu/%zu bytes\n", length, left);
+        written = usrsctp_sendv(
+            transport->socket, mbuf_buf(buffer), length, NULL, 0, info, info_size, info_type,
+            flags);
+#if DEBUG_LEVEL >= 7
+        DEBUG_PRINTF(
+            "usrsctp_sendv(socket=%p, buffer=%p, length=%zu/%zu, info={sid: %" PRIu16 ", "
+            "ppid: %" PRIu32 ", eor: %s (was %s}) -> %zd (errno: %m)\n",
+            transport->socket, mbuf_buf(buffer), length, left, send_info->snd_sid,
+            ntohl(send_info->snd_ppid), send_info->snd_flags & SCTP_EOR ? "true" : "false",
+            eor_set ? "true" : "false", written, errno);
+#endif
+        if (written < 0) {
+            error = rawrtc_error_to_code(errno);
+            goto out;
+        }
+
+        // TODO: Remove
+        if (written == 0) {
+#if DEBUG_LEVEL >= 7
+            DEBUG_NOTICE("@tuexen: usrsctp_sendv returned 0\n");
+#endif
+            error = RAWRTC_CODE_TRY_AGAIN_LATER;
+            goto out;
+        }
+
+        // If not all bytes have been written, this obviously means that usrsctp's buffer is full
+        // and we need to try again later.
+        if ((size_t) written < length) {
+            // TODO: Comment in and remove section above
+            // error = RAWRTC_CODE_TRY_AGAIN_LATER;
+            // goto out;
+        }
+
+        // Update buffer position
+        mbuf_advance(buffer, written);
+    } while (mbuf_get_left(buffer) > 0);
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Reset EOR flag
+    if (eor_set) {
+        send_info->snd_flags |= SCTP_EOR;
+    }
+
+    return error;
+}
+
+/*
+ * Feed inbound data to the SCTP transport.
+ *
+ * `buffer` contains the data to be fed to the SCTP transport. Since
+ * the data is not going to be referenced, you can pass a *fake* `mbuf`
+ * structure that hasn't been allocated with `mbuf_alloc` to avoid
+ * copying.
+ * `ecn_bits` are the explicit congestion notification bits to be
+ * passed to usrsctp.
+ *
+ * Return `RAWRTC_CODE_INVALID_STATE` in case the transport is closed.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` is being returned.
+ */
+enum rawrtc_code rawrtc_sctp_transport_feed_inbound(
+    struct rawrtc_sctp_transport* const transport,
+    struct mbuf* const buffer,
+    uint8_t const ecn_bits) {
+    void* raw_buffer;
+    size_t length;
+
+    // Check arguments
+    if (!transport || !buffer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Detached?
+    if (transport->flags & RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get buffer & length
+    raw_buffer = mbuf_buf(buffer);
+    length = mbuf_get_left(buffer);
+
+    // Trace (if trace handle)
+    // Note: No need to check if NULL as the function does it for us
+    trace_packet(transport, raw_buffer, length, SCTP_DUMP_INBOUND);
+
+    // Verify CRC32-C checksum
+    if (!(checksum_flags & RAWRTC_SCTP_TRANSPORT_CHECKSUM_DISABLE_INBOUND)) {
+        if (length >= sizeof(struct sctp_common_header)) {
+            struct sctp_common_header* const header = raw_buffer;
+            uint32_t const actual_checksum = header->crc32c;
+            uint32_t expected_checksum;
+
+            // Calculate checksum
+            // Note: The field is still in network byte order (even though we don't convert the
+            // zeroes). Furthermore, the result from `rawrtc_crc32c` is also in network byte order.
+            header->crc32c = 0x00000000;
+            expected_checksum = rawrtc_crc32c(raw_buffer, length);
+            header->crc32c = actual_checksum;
+
+            // Verify checksum is correct
+            if (actual_checksum != expected_checksum) {
+                DEBUG_NOTICE(
+                    "Inbound packet has invalid CRC32-C checksum, expected=%08x, actual=%08x\n",
+                    ntohl(expected_checksum), ntohl(actual_checksum));
+                return RAWRTC_CODE_INVALID_MESSAGE;
+            }
+        } else {
+            DEBUG_WARNING("Inbound packet too short (%zu bytes)!\n", length);
+            return RAWRTC_CODE_INVALID_MESSAGE;
+        }
+    }
+
+    // Feed into SCTP socket
+    DEBUG_PRINTF("Feeding SCTP packet of %zu bytes\n", length);
+    usrsctp_conninput(transport, raw_buffer, length, ecn_bits);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's send and receive buffer length in bytes.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_buffer_length(
+    struct rawrtc_sctp_transport* const transport,
+    uint32_t const send_buffer_length,
+    uint32_t const receive_buffer_length) {
+    int option_value;
+
+    // Check arguments
+    if (!transport || send_buffer_length == 0 || receive_buffer_length == 0 ||
+        send_buffer_length > INT_MAX || receive_buffer_length > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set length for send/receive buffer
+    option_value = (int) send_buffer_length;
+    if (usrsctp_setsockopt(
+            transport->socket, SOL_SOCKET, SO_SNDBUF, &option_value, sizeof(option_value))) {
+        return rawrtc_error_to_code(errno);
+    }
+    option_value = (int) receive_buffer_length;
+    if (usrsctp_setsockopt(
+            transport->socket, SOL_SOCKET, SO_RCVBUF, &option_value, sizeof(option_value))) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Done
+    DEBUG_PRINTF(
+        "Set send/receive buffer length to %" PRIu32 "/%" PRIu32 "\n", send_buffer_length,
+        receive_buffer_length);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SCTP transport's send and receive buffer length in bytes.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_buffer_length(
+    uint32_t* const send_buffer_lengthp,
+    uint32_t* const receive_buffer_lengthp,
+    struct rawrtc_sctp_transport* const transport) {
+    int option_value;
+    socklen_t option_size;
+
+    // Check arguments
+    if (!send_buffer_lengthp || !receive_buffer_lengthp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get length of send/receive buffer
+    option_size = sizeof(option_value);
+    if (usrsctp_getsockopt(transport->socket, SOL_SOCKET, SO_SNDBUF, &option_value, &option_size)) {
+        return rawrtc_error_to_code(errno);
+    }
+    *send_buffer_lengthp = (uint32_t) option_value;
+    option_size = sizeof(option_value);
+    if (usrsctp_getsockopt(transport->socket, SOL_SOCKET, SO_RCVBUF, &option_value, &option_size)) {
+        return rawrtc_error_to_code(errno);
+    }
+    *receive_buffer_lengthp = (uint32_t) option_value;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_congestion_ctrl_algorithm(
+    struct rawrtc_sctp_transport* const transport,
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm) {
+    struct sctp_assoc_value av;
+
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Translate algorithm
+    switch (algorithm) {
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581:
+            av.assoc_value = SCTP_CC_RFC2581;
+            break;
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HSTCP:
+            av.assoc_value = SCTP_CC_HSTCP;
+            break;
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HTCP:
+            av.assoc_value = SCTP_CC_HTCP;
+            break;
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RTCC:
+            av.assoc_value = SCTP_CC_RTCC;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set congestion control algorithm
+    av.assoc_id = SCTP_ALL_ASSOC;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_PLUGGABLE_CC, &av,
+            sizeof(struct sctp_assoc_value))) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Done
+    DEBUG_PRINTF(
+        "Set congestion control algorithm to %s\n",
+        rawrtc_sctp_transport_congestion_ctrl_algorithm_to_name(algorithm));
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_congestion_ctrl_algorithm(
+    enum rawrtc_sctp_transport_congestion_ctrl* const algorithmp,
+    struct rawrtc_sctp_transport* const transport) {
+    struct sctp_assoc_value av;
+    socklen_t option_size;
+
+    // Check arguments
+    if (!algorithmp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get congestion control algorithm
+    option_size = sizeof(av);
+    if (usrsctp_getsockopt(transport->socket, IPPROTO_SCTP, SCTP_PLUGGABLE_CC, &av, &option_size)) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Translate algorithm
+    switch (av.assoc_value) {
+        case SCTP_CC_RFC2581:
+            *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581;
+            break;
+        case SCTP_CC_HSTCP:
+            *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HSTCP;
+            break;
+        case SCTP_CC_HTCP:
+            *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HTCP;
+            break;
+        case SCTP_CC_RTCC:
+            *algorithmp = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RTCC;
+            break;
+        default:
+            return RAWRTC_CODE_UNSUPPORTED_ALGORITHM;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's maximum transmission unit (MTU).
+ * This will disable MTU discovery.
+ *
+ * Note: The MTU cannot be set before the SCTP transport has been
+ *       started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_mtu(
+    struct rawrtc_sctp_transport* const transport, uint32_t mtu) {
+    struct sctp_paddrparams peer_address_parameters = {0};
+
+    // Check arguments
+    if (!transport || !transport->socket) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING &&
+        transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set fixed MTU
+    // See: https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+    // .spp_assoc_id is being ignored for 1:1 associations
+    memcpy(
+        &peer_address_parameters.spp_address, &transport->remote_address,
+        sizeof(transport->remote_address));
+    peer_address_parameters.spp_flags = SPP_PMTUD_DISABLE;
+    peer_address_parameters.spp_pathmtu = mtu;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS, &peer_address_parameters,
+            sizeof(peer_address_parameters))) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Done
+    DEBUG_PRINTF("Set MTU to %" PRIu32 " (and disabled MTU discovery)\n", mtu);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current SCTP transport's maximum transmission unit (MTU)
+ * and an indication whether MTU discovery is enabled.
+ *
+ * Note: The MTU cannot be retrieved before the SCTP transport has been
+ *       started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_get_mtu(
+    uint32_t* const mtup,  // de-referenced
+    bool* const mtu_discovery_enabledp,  // de-referenced
+    struct rawrtc_sctp_transport* const transport) {
+    struct sctp_paddrparams peer_address_parameters = {0};
+    socklen_t option_size;
+
+    // Check arguments
+    if (!mtup || !mtu_discovery_enabledp || !transport || !transport->socket) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING &&
+        transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get MTU info
+    memcpy(
+        &peer_address_parameters.spp_address, &transport->remote_address,
+        sizeof(transport->remote_address));
+    option_size = sizeof(peer_address_parameters);
+    if (usrsctp_getsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS, &peer_address_parameters,
+            &option_size)) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Check value
+    if (option_size != sizeof(peer_address_parameters)) {
+        return RAWRTC_CODE_EXTERNAL_ERROR;
+    }
+
+    // Set value
+    *mtu_discovery_enabledp = (bool) (peer_address_parameters.spp_flags & SPP_PMTUD_ENABLE);
+    if (*mtu_discovery_enabledp) {
+        *mtup = 0;
+    } else {
+        *mtup = peer_address_parameters.spp_pathmtu;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Enable MTU discovery for the SCTP transport.
+ *
+ * Note: MTU discovery cannot be enabled before the SCTP transport has
+ *       been started.
+ */
+enum rawrtc_code rawrtc_sctp_transport_enable_mtu_discovery(
+    struct rawrtc_sctp_transport* const transport) {
+    struct sctp_paddrparams peer_address_parameters;
+
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING &&
+        transport->state != RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Re-enable support for path MTU.
+    //       See: https://github.com/sctplab/usrsctp/issues/205
+    return RAWRTC_CODE_NOT_IMPLEMENTED;
+
+    // Set MTU discovery
+    // See: https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+    // .spp_assoc_id is being ignored for 1:1 associations
+    memcpy(
+        &peer_address_parameters.spp_address, &transport->remote_address,
+        sizeof(transport->remote_address));
+    peer_address_parameters.spp_flags = SPP_PMTUD_ENABLE;
+    if (usrsctp_setsockopt(
+            transport->socket, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS, &peer_address_parameters,
+            sizeof(peer_address_parameters))) {
+        return rawrtc_error_to_code(errno);
+    }
+
+    // Done
+    DEBUG_PRINTF("Enabled MTU discovery\n");
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's context.
+ */
+enum rawrtc_code rawrtc_sctp_transport_set_context(
+    struct rawrtc_sctp_transport* const transport,
+    struct rawrtc_sctp_transport_context* const context  // copied
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !context) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure the context contains all callbacks
+    error = validate_context(context);
+    if (error) {
+        return error;
+    }
+
+    // Set context & done
+    transport->context = *context;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/sctp_transport/transport.h b/src/sctp_transport/transport.h
new file mode 100644
index 0000000..5d845e9
--- /dev/null
+++ b/src/sctp_transport/transport.h
@@ -0,0 +1,120 @@
+#pragma once
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+#include <usrsctp.h>  // SCTP_EVENT_*, ...
+#include <stdio.h>  // FILE
+
+/*
+ * usrsctp event flag extensions for handlers.
+ */
+#define RAWRTC_SCTP_EVENT_NONE (0)
+#define RAWRTC_SCTP_EVENT_ALL (SCTP_EVENT_READ | SCTP_EVENT_WRITE | SCTP_EVENT_ERROR)
+
+enum {
+    RAWRTC_SCTP_TRANSPORT_MAX_MESSAGE_SIZE = 0,
+    RAWRTC_SCTP_TRANSPORT_TIMER_TIMEOUT = 10,
+    RAWRTC_SCTP_TRANSPORT_DEFAULT_PORT = 5000,
+    // As specified by https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+    RAWRTC_SCTP_TRANSPORT_DEFAULT_NUMBER_OF_STREAMS = 65535,
+    RAWRTC_SCTP_TRANSPORT_SID_MAX = 65534,
+    RAWRTC_SCTP_TRANSPORT_EMPTY_MESSAGE_SIZE = 1,
+};
+
+/*
+ * SCTP transport flags.
+ */
+enum {
+    RAWRTC_SCTP_TRANSPORT_FLAGS_INITIALIZED = 1 << 0,
+    // The detached flag is virtually identical to the 'closed' state but is applied before the
+    // detach handler is being called. Thus, any other functions should check for the detached flag
+    // instead of checking for the 'closed' state since that is being set at a later stage.
+    RAWRTC_SCTP_TRANSPORT_FLAGS_DETACHED = 1 << 1,
+    RAWRTC_SCTP_TRANSPORT_FLAGS_SENDING_IN_PROGRESS = 1 << 2,
+    RAWRTC_SCTP_TRANSPORT_FLAGS_BUFFERED_AMOUNT_LOW = 1 << 3,
+};
+
+/*
+ * SCTP data channel flags.
+ */
+enum {
+    RAWRTC_SCTP_DATA_CHANNEL_FLAGS_CAN_SEND_UNORDERED = 1 << 0,
+    RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_INBOUND_MESSAGE = 1 << 1,
+    RAWRTC_SCTP_DATA_CHANNEL_FLAGS_PENDING_STREAM_RESET = 1 << 2,
+    RAWRTC_SCTP_DATA_CHANNEL_FLAGS_INCOMING_STREAM_RESET = 1 << 3,
+    RAWRTC_SCTP_DATA_CHANNEL_FLAGS_OUTGOING_STREAM_RESET = 1 << 4,
+};
+
+/*
+ * DCEP message types.
+ */
+enum {
+    RAWRTC_DCEP_MESSAGE_TYPE_ACK = 0x02,
+    RAWRTC_DCEP_MESSAGE_TYPE_OPEN = 0x03,
+};
+
+/*
+ * DCEP message sizes
+ */
+enum {
+    RAWRTC_DCEP_MESSAGE_ACK_BASE_SIZE = 1,
+    RAWRTC_DCEP_MESSAGE_OPEN_BASE_SIZE = 12,
+};
+
+/*
+ * DCEP message priorities.
+ */
+enum {
+    RAWRTC_DCEP_CHANNEL_PRIORITY_LOW = 128,
+    RAWRTC_DCEP_CHANNEL_PRIORITY_NORMAL = 256,
+    RAWRTC_DCEP_CHANNEL_PRIORITY_HIGH = 512,
+    RAWRTC_DCEP_CHANNEL_PRIORITY_EXTRA_HIGH = 1024,
+};
+
+/*
+ * DCEP payload protocol identifiers.
+ */
+enum {
+    RAWRTC_SCTP_TRANSPORT_PPID_DCEP = 50,
+    RAWRTC_SCTP_TRANSPORT_PPID_UTF16 = 51,
+    RAWRTC_SCTP_TRANSPORT_PPID_UTF16_EMPTY = 56,
+    RAWRTC_SCTP_TRANSPORT_PPID_UTF16_PARTIAL = 54,  // deprecated
+    RAWRTC_SCTP_TRANSPORT_PPID_BINARY = 53,
+    RAWRTC_SCTP_TRANSPORT_PPID_BINARY_EMPTY = 57,
+    RAWRTC_SCTP_TRANSPORT_PPID_BINARY_PARTIAL = 52,  // deprecated
+};
+
+/*
+ * SCTP transport.
+ */
+struct rawrtc_sctp_transport {
+    struct rawrtc_sctp_transport_context context;
+    uint_fast8_t flags;
+    enum rawrtc_sctp_transport_state state;
+    uint16_t port;
+    struct sockaddr_conn remote_address;
+    uint64_t remote_maximum_message_size;
+    rawrtc_data_channel_handler data_channel_handler;  // nullable
+    rawrtc_sctp_transport_state_change_handler state_change_handler;  // nullable
+    void* arg;  // nullable
+    struct list buffered_messages_outgoing;
+    struct mbuf* buffer_dcep_inbound;
+    struct sctp_rcvinfo info_dcep_inbound;
+    struct rawrtc_data_channel** channels;
+    uint_fast16_t n_channels;
+    uint_fast16_t current_channel_sid;
+    FILE* trace_handle;
+    struct socket* socket;
+};
+
+/*
+ * Contextual data required by the SCTP transport used in conjunction with a
+ * data channel.
+ */
+struct rawrtc_sctp_data_channel_context {
+    uint16_t sid;
+    uint_fast8_t flags;
+    struct mbuf* buffer_inbound;
+    struct sctp_rcvinfo info_inbound;
+};
diff --git a/src/sctp_transport/utils.c b/src/sctp_transport/utils.c
new file mode 100644
index 0000000..7c69e5b
--- /dev/null
+++ b/src/sctp_transport/utils.c
@@ -0,0 +1,38 @@
+#include <rawrtcdc/sctp_transport.h>
+
+/*
+ * Get the corresponding name for an SCTP transport state.
+ */
+char const* rawrtc_sctp_transport_state_to_name(enum rawrtc_sctp_transport_state const state) {
+    switch (state) {
+        case RAWRTC_SCTP_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_SCTP_TRANSPORT_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_SCTP_TRANSPORT_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_SCTP_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for a congestion control algorithm.
+ */
+char const* rawrtc_sctp_transport_congestion_ctrl_algorithm_to_name(
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm) {
+    switch (algorithm) {
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581:
+            return "RFC2581";
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HSTCP:
+            return "HSTCP";
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_HTCP:
+            return "HTCP";
+        case RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RTCC:
+            return "RTCC";
+        default:
+            return "???";
+    }
+}
diff --git a/subprojects/rawrtcc.wrap b/subprojects/rawrtcc.wrap
new file mode 100644
index 0000000..6669766
--- /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.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/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