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

Change-Id: I38a655a4259b62f591334e90a1315bd4e7e4d8ec
git-subtree-dir: third_party/rawrtc/rawrtc
git-subtree-split: aa3ae4b247275cc6e69c30613b3a4ba7fdc82d1b
diff --git a/src/certificate/certificate.c b/src/certificate/certificate.c
new file mode 100644
index 0000000..9f68b8e
--- /dev/null
+++ b/src/certificate/certificate.c
@@ -0,0 +1,899 @@
+#include "certificate.h"
+#include "../utils/utils.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/bio.h>
+#include <openssl/bn.h>
+#include <openssl/crypto.h>
+#include <openssl/ec.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/objects.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <openssl/x509.h>
+#include <limits.h>  // INT_MAX, LONG_MAX
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "certificate"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Default certificate options.
+ */
+struct rawrtc_certificate_options rawrtc_default_certificate_options = {
+    .key_type = RAWRTC_CERTIFICATE_KEY_TYPE_EC,
+    .common_name = "anonymous@rawrtc.org",
+    .valid_until = 3600 * 24 * 30,  // 30 days
+    .sign_algorithm = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    .named_curve = "prime256v1",
+    .modulus_length = 3072,
+};
+
+/*
+ * Print and flush the OpenSSL error queue.
+ */
+static int print_openssl_error(char const* message, size_t length, void* arg) {
+    (void) message;
+    (void) length;
+    (void) arg;
+    DEBUG_WARNING("%b", message, length);
+
+    // 1 to continue outputting the error queue
+    return 1;
+}
+
+/*
+ * Generates an n-bit RSA key pair.
+ * Caller must call `EVP_PKEY_free(*keyp)` when done.
+ */
+static enum rawrtc_code generate_key_rsa(
+    EVP_PKEY** const keyp,  // de-referenced
+    uint_fast32_t const modulus_length) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    EVP_PKEY* key = NULL;
+    RSA* rsa = NULL;
+    BIGNUM* bn = NULL;
+
+    // Check arguments
+    if (!keyp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > INT_MAX)
+    if (modulus_length > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Create an empty EVP_PKEY structure
+    key = EVP_PKEY_new();
+    if (!key) {
+        DEBUG_WARNING("Could not create EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Initialise RSA structure
+    rsa = RSA_new();
+    if (!rsa) {
+        DEBUG_WARNING("Could not initialise RSA structure\n");
+        goto out;
+    }
+
+    // Allocate BIGNUM
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(OPENSSL_IS_BORINGSSL)
+    bn = BN_secure_new();
+#else
+    bn = BN_new();
+#endif
+    if (!bn) {
+        DEBUG_WARNING("Could not allocate BIGNUM\n");
+        goto out;
+    }
+
+    // Generate RSA key pair and store it in the RSA structure
+    BN_set_word(bn, RSA_F4);
+    if (!RSA_generate_key_ex(rsa, (int) modulus_length, bn, NULL)) {
+        DEBUG_WARNING("Could not generate RSA key pair\n");
+        goto out;
+    }
+
+    // Store the generated RSA key pair in the EVP_PKEY structure
+    if (!EVP_PKEY_set1_RSA(key, rsa)) {
+        DEBUG_WARNING("Could not assign RSA key pair to EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (rsa) {
+        RSA_free(rsa);
+    }
+    if (bn) {
+        BN_free(bn);
+    }
+    if (error) {
+        if (key) {
+            EVP_PKEY_free(key);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *keyp = key;
+    }
+    return error;
+}
+
+/*
+ * Generates an ECC key pair.
+ * Caller must call `EVP_PKEY_free(*keyp)` when done.
+ */
+static enum rawrtc_code generate_key_ecc(
+    EVP_PKEY** const keyp,  // de-referenced
+    char* const named_curve) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    EVP_PKEY* key = NULL;
+    int curve_group_nid;
+    EC_KEY* ecc = NULL;
+
+    // Check arguments
+    if (!keyp || !named_curve) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create an empty EVP_PKEY structure
+    key = EVP_PKEY_new();
+    if (!key) {
+        DEBUG_WARNING("Could not create EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Get NID of named curve
+    curve_group_nid = OBJ_txt2nid(named_curve);
+    if (curve_group_nid == NID_undef) {
+        DEBUG_WARNING("Could not determine group NID of named curve: %s\n", named_curve);
+        goto out;
+    }
+
+    // Initialise EC structure for named curve
+    ecc = EC_KEY_new_by_curve_name(curve_group_nid);
+    if (!ecc) {
+        DEBUG_WARNING("Could not initialise EC structure for named curve\n");
+        goto out;
+    }
+
+    // This is needed to correctly sign the certificate
+    EC_KEY_set_asn1_flag(ecc, OPENSSL_EC_NAMED_CURVE);
+
+    // Generate the ECC key pair and store it in the EC structure
+    if (!EC_KEY_generate_key(ecc)) {
+        DEBUG_WARNING("Could not generate ECC key pair\n");
+        goto out;
+    }
+
+    // Store the generated ECC key pair in the EVP_PKEY structure
+    if (!EVP_PKEY_assign_EC_KEY(key, ecc)) {
+        DEBUG_WARNING("Could not assign ECC key pair to EVP_PKEY structure\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        if (ecc) {
+            EC_KEY_free(ecc);
+        }
+        if (key) {
+            EVP_PKEY_free(key);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *keyp = key;
+    }
+    return error;
+}
+
+/*
+ * Generates a self-signed certificate.
+ * Caller must call `X509_free(*certificatep)` when done.
+ */
+static enum rawrtc_code generate_self_signed_certificate(
+    X509** const certificatep,  // de-referenced
+    EVP_PKEY* const key,
+    char* const common_name,
+    uint_fast32_t const valid_until,
+    enum rawrtc_certificate_sign_algorithm const sign_algorithm) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    X509* certificate = NULL;
+    X509_NAME* name = NULL;
+    EVP_MD const* sign_function;
+
+    // Check arguments
+    if (!certificatep || !key || !common_name) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > LONG_MAX)
+    if (valid_until > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Get sign function
+    sign_function = rawrtc_get_sign_function(sign_algorithm);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate and initialise x509 structure
+    certificate = X509_new();
+    if (!certificate) {
+        DEBUG_WARNING("Could not initialise x509 structure\n");
+        goto out;
+    }
+
+    // Set x509 version
+    // Note: '2' maps to version 3
+    if (!X509_set_version(certificate, 2)) {
+        DEBUG_WARNING("Could not set x509 version\n");
+        goto out;
+    }
+
+    // Set the serial number randomly (doesn't need to be unique as we are self-signing)
+    if (!ASN1_INTEGER_set(X509_get_serialNumber(certificate), rand_u32())) {
+        DEBUG_WARNING("Could not set x509 serial number\n");
+        goto out;
+    }
+
+    // Create an empty X509_NAME structure
+    name = X509_NAME_new();
+    if (!name) {
+        DEBUG_WARNING("Could not create x509_NAME structure\n");
+        goto out;
+    }
+
+    // Set common name field on X509_NAME structure
+    if (!X509_NAME_add_entry_by_txt(
+            name, "CN", MBSTRING_ASC, (uint8_t*) common_name, (int) strlen(common_name), -1, 0)) {
+        DEBUG_WARNING("Could not apply common name (%s) on certificate\n", common_name);
+        goto out;
+    }
+
+    // Set issuer and subject name
+    if (!X509_set_issuer_name(certificate, name) || !X509_set_subject_name(certificate, name)) {
+        DEBUG_WARNING("Could not set issuer name on certificate\n");
+        goto out;
+    }
+
+    // Certificate is valid from now (-1 day) until whatever has been provided in parameters
+    if (!X509_gmtime_adj(X509_get_notBefore(certificate), -3600 * 24) ||
+        !X509_gmtime_adj(X509_get_notAfter(certificate), (long) valid_until)) {
+        DEBUG_WARNING("Could not apply lifetime range to certificate\n");
+        goto out;
+    }
+
+    // Set public key of certificate
+    if (!X509_set_pubkey(certificate, key)) {
+        DEBUG_WARNING("Could not set public key to certificate\n");
+        goto out;
+    }
+
+    // Sign the certificate
+    if (!X509_sign(certificate, key, sign_function)) {
+        DEBUG_WARNING("Could not sign the certificate\n");
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (name) {
+        X509_NAME_free(name);
+    }
+    if (error) {
+        if (certificate) {
+            X509_free(certificate);
+        }
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+/*
+ * Destructor for existing certificate options.
+ */
+static void rawrtc_certificate_options_destroy(void* arg) {
+    struct rawrtc_certificate_options* const options = arg;
+
+    // Un-reference
+    mem_deref(options->named_curve);
+    mem_deref(options->common_name);
+}
+
+/*
+ * Create certificate options.
+ *
+ * All arguments but `key_type` are optional. Sane and safe default
+ * values will be applied, don't worry!
+ *
+ * `*optionsp` must be unreferenced.
+ *
+ * If `common_name` is `NULL` the default common name will be applied.
+ * If `valid_until` is `0` the default certificate lifetime will be
+ * applied.
+ * If the key type is `ECC` and `named_curve` is `NULL`, the default
+ * named curve will be used.
+ * If the key type is `RSA` and `modulus_length` is `0`, the default
+ * amount of bits will be used. The same applies to the
+ * `sign_algorithm` if it has been set to `NONE`.
+ */
+enum rawrtc_code rawrtc_certificate_options_create(
+    struct rawrtc_certificate_options** const optionsp,  // de-referenced
+    enum rawrtc_certificate_key_type const key_type,
+    char* common_name,  // nullable, copied
+    uint_fast32_t valid_until,
+    enum rawrtc_certificate_sign_algorithm sign_algorithm,
+    char* named_curve,  // nullable, copied, ignored for RSA
+    uint_fast32_t modulus_length  // ignored for ECC
+) {
+    struct rawrtc_certificate_options* options;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!optionsp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#if (UINT_FAST32_MAX > LONG_MAX)
+    if (valid_until > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+#if (UINT_FAST32_MAX > INT_MAX)
+    if (modulus_length > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+
+    // Set defaults
+    if (!common_name) {
+        common_name = rawrtc_default_certificate_options.common_name;
+    }
+    if (!valid_until) {
+        valid_until = rawrtc_default_certificate_options.valid_until;
+    }
+
+    // Check sign algorithm/set default
+    // Note: We say 'no' to SHA1 intentionally
+    // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to libre)
+    switch (sign_algorithm) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE:
+            sign_algorithm = rawrtc_default_certificate_options.sign_algorithm;
+            break;
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256:
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set defaults depending on key type
+    switch (key_type) {
+        case RAWRTC_CERTIFICATE_KEY_TYPE_RSA:
+            // Unset ECC vars
+            named_curve = NULL;
+
+            // Prevent user from being stupid
+            if (modulus_length < RAWRTC_MODULUS_LENGTH_MIN) {
+                modulus_length = rawrtc_default_certificate_options.modulus_length;
+            }
+
+            break;
+
+        case RAWRTC_CERTIFICATE_KEY_TYPE_EC:
+            // Unset RSA vars
+            modulus_length = 0;
+
+            // Set default named curve (if required)
+            if (!named_curve) {
+                named_curve = rawrtc_default_certificate_options.named_curve;
+            }
+
+            break;
+
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Allocate
+    options = mem_zalloc(sizeof(*options), rawrtc_certificate_options_destroy);
+    if (!options) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    options->key_type = key_type;
+    if (common_name) {
+        error = rawrtc_strdup(&options->common_name, common_name);
+        if (error) {
+            goto out;
+        }
+    }
+    options->valid_until = valid_until;
+    options->sign_algorithm = sign_algorithm;
+    if (named_curve) {
+        error = rawrtc_strdup(&options->named_curve, named_curve);
+        if (error) {
+            goto out;
+        }
+    }
+    options->modulus_length = modulus_length;
+
+out:
+    if (error) {
+        mem_deref(options);
+    } else {
+        // Set pointer
+        *optionsp = options;
+    }
+    return error;
+}
+
+/*
+ * Destructor for existing certificate.
+ */
+static void rawrtc_certificate_destroy(void* arg) {
+    struct rawrtc_certificate* const certificate = arg;
+
+    // Free
+    if (certificate->certificate) {
+        X509_free(certificate->certificate);
+    }
+    if (certificate->key) {
+        EVP_PKEY_free(certificate->key);
+    }
+}
+
+/*
+ * Create and generate a self-signed certificate.
+ *
+ * Sane and safe default options will be applied if `options` is
+ * `NULL`.
+ *
+ * `*certificatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_certificate_generate(
+    struct rawrtc_certificate** const certificatep,
+    struct rawrtc_certificate_options* options  // nullable
+) {
+    struct rawrtc_certificate* certificate;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!certificatep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Default options
+    if (!options) {
+        options = &rawrtc_default_certificate_options;
+    }
+
+    // Allocate
+    certificate = mem_zalloc(sizeof(*certificate), rawrtc_certificate_destroy);
+    if (!certificate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Generate key pair
+    switch (options->key_type) {
+        case RAWRTC_CERTIFICATE_KEY_TYPE_RSA:
+            error = generate_key_rsa(&certificate->key, options->modulus_length);
+            break;
+        case RAWRTC_CERTIFICATE_KEY_TYPE_EC:
+            error = generate_key_ecc(&certificate->key, options->named_curve);
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+    if (error) {
+        goto out;
+    }
+
+    // Generate certificate
+    error = generate_self_signed_certificate(
+        &certificate->certificate, certificate->key, options->common_name, options->valid_until,
+        options->sign_algorithm);
+    if (error) {
+        goto out;
+    }
+
+    // Set key type
+    certificate->key_type = options->key_type;
+
+out:
+    if (error) {
+        mem_deref(certificate);
+    } else {
+        // Set pointer
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+/*
+ * Copy a certificate.
+ * References the x509 certificate and private key.
+ */
+enum rawrtc_code rawrtc_certificate_copy(
+    struct rawrtc_certificate** const certificatep,  // de-referenced
+    struct rawrtc_certificate* const source_certificate) {
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+    struct rawrtc_certificate* certificate;
+
+    // Check arguments
+    if (!certificatep || !source_certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    certificate = mem_zalloc(sizeof(*certificate), rawrtc_certificate_destroy);
+    if (!certificate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Increment reference count of certificate and private key, copy the pointers
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+    if (!X509_up_ref(source_certificate->certificate)) {
+        goto out;
+    }
+#else
+    if (!CRYPTO_add(&source_certificate->certificate->references, 1, CRYPTO_LOCK_X509)) {
+        goto out;
+    }
+#endif
+    certificate->certificate = source_certificate->certificate;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+    if (!EVP_PKEY_up_ref(source_certificate->key)) {
+        goto out;
+    }
+#else
+    if (!CRYPTO_add(&source_certificate->key->references, 1, CRYPTO_LOCK_EVP_PKEY)) {
+        goto out;
+    }
+#endif
+    certificate->key = source_certificate->key;
+    certificate->key_type = source_certificate->key_type;
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(certificate);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointer
+        *certificatep = certificate;
+    }
+    return error;
+}
+
+static enum rawrtc_code what_to_encode(
+    enum rawrtc_certificate_encode const to_encode,
+    bool* encode_certificatep,  // de-referenced
+    bool* encode_keyp  // de-referenced
+) {
+    *encode_certificatep = false;
+    *encode_keyp = false;
+
+    // What to encode?
+    switch (to_encode) {
+        case RAWRTC_CERTIFICATE_ENCODE_CERTIFICATE:
+            *encode_certificatep = true;
+            break;
+        case RAWRTC_CERTIFICATE_ENCODE_PRIVATE_KEY:
+            *encode_keyp = true;
+            break;
+        case RAWRTC_CERTIFICATE_ENCODE_BOTH:
+            *encode_certificatep = true;
+            *encode_keyp = true;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get PEM of the certificate and/or the private key if requested.
+ * *pemp will NOT be null-terminated!
+ */
+enum rawrtc_code rawrtc_certificate_get_pem(
+    char** const pemp,  // de-referenced
+    size_t* const pem_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode) {
+    bool encode_certificate;
+    bool encode_key;
+    enum rawrtc_code error;
+    BIO* bio = NULL;
+    char* pem = NULL;
+    uint64_t length;
+
+    // Check arguments
+    if (!pemp || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // What to encode?
+    error = what_to_encode(to_encode, &encode_certificate, &encode_key);
+    if (error) {
+        return error;
+    }
+    error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Create bio structure
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(OPENSSL_IS_BORINGSSL)
+    bio = BIO_new(BIO_s_secmem());
+#else
+    bio = BIO_new(BIO_s_mem());
+#endif
+
+    // Write certificate
+    if (encode_certificate && !PEM_write_bio_X509(bio, certificate->certificate)) {
+        goto out;
+    }
+
+    // Write private key (if requested)
+    if (encode_key && !PEM_write_bio_PrivateKey(bio, certificate->key, NULL, NULL, 0, 0, NULL)) {
+        goto out;
+    }
+
+    // Allocate buffer
+    length = BIO_number_written(bio);
+#if (UINT64_MAX > INT_MAX)
+    if (length > INT_MAX) {
+        goto out;
+    }
+#endif
+    pem = mem_alloc(length, NULL);
+    if (!pem) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Copy to buffer
+    if (BIO_read(bio, pem, (int) length) < (int) length) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (bio) {
+        BIO_free(bio);
+    }
+    if (error) {
+        mem_deref(pem);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointers
+        *pemp = pem;
+        *pem_lengthp = length;
+    }
+    return error;
+}
+
+/*
+ * Get DER of the certificate and/or the private key if requested.
+ * *derp will NOT be null-terminated!
+ */
+enum rawrtc_code rawrtc_certificate_get_der(
+    uint8_t** const derp,  // de-referenced
+    size_t* const der_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode) {
+    bool encode_certificate;
+    bool encode_key;
+    enum rawrtc_code error;
+    int length_certificate = 0;
+    int length_key = 0;
+    size_t length;
+    uint8_t* der = NULL;
+    uint8_t* der_i2d;
+
+    // Check arguments
+    if (!derp || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // What to encode?
+    error = what_to_encode(to_encode, &encode_certificate, &encode_key);
+    if (error) {
+        return error;
+    }
+    error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Allocate buffer
+    if (encode_certificate) {
+        length_certificate = i2d_X509(certificate->certificate, NULL);
+        if (length_certificate < 1) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+    if (encode_key) {
+        length_key = i2d_PrivateKey(certificate->key, NULL);
+        if (length_key < 1) {
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+    length = (size_t)(length_certificate + length_key);
+    der = mem_alloc(length, NULL);
+    if (!der) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    der_i2d = der;
+
+    // Write certificate
+    if (encode_certificate && i2d_X509(certificate->certificate, &der_i2d) < length_certificate) {
+        goto out;
+    }
+
+    // Write private key (if requested)
+    if (encode_key && i2d_PrivateKey(certificate->key, &der_i2d) < length_key) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(der);
+        ERR_print_errors_cb(print_openssl_error, NULL);
+    } else {
+        // Set pointers
+        *derp = der;
+        *der_lengthp = length;
+    }
+    return error;
+}
+
+/*
+ * Get certificate's fingerprint.
+ * Caller must ensure that `buffer` has space for
+ * `RAWRTC_FINGERPRINT_MAX_SIZE_HEX` bytes
+ */
+enum rawrtc_code rawrtc_certificate_get_fingerprint(
+    char** const fingerprint,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    EVP_MD const* sign_function;
+    uint8_t bytes_buffer[RAWRTC_FINGERPRINT_MAX_SIZE_HEX];
+    uint_least32_t length;
+
+    // Check arguments
+    if (!fingerprint || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get sign function for algorithm
+    sign_function = rawrtc_get_sign_function(algorithm);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Generate certificate fingerprint
+    if (!X509_digest(certificate->certificate, sign_function, bytes_buffer, &length)) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+    if (length < 1) {
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Convert bytes to hex
+    return rawrtc_bin_to_colon_hex(fingerprint, bytes_buffer, (size_t) length);
+}
+
+/*
+ * Copy and append a certificate to a list.
+ */
+static enum rawrtc_code copy_and_append_certificate(
+    struct list* const certificate_list,  // de-referenced, not checked
+    struct rawrtc_certificate* const certificate  // copied
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* copied_certificate;
+
+    // Check arguments
+    if (!certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy certificate
+    // Note: Copying is needed as the 'le' element cannot be associated to multiple lists
+    error = rawrtc_certificate_copy(&copied_certificate, certificate);
+    if (error) {
+        return error;
+    }
+
+    // Append to list
+    list_append(certificate_list, &copied_certificate->le, copied_certificate);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Copy an array of certificates to a list.
+ * Warning: The list will be flushed on error.
+ */
+enum rawrtc_code rawrtc_certificate_array_to_list(
+    struct list* const certificate_list,  // de-referenced, copied into
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates) {
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!certificate_list || !certificates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Append and reference certificates
+    for (i = 0; i < n_certificates; ++i) {
+        error = copy_and_append_certificate(certificate_list, certificates[i]);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        list_flush(certificate_list);
+    }
+    return error;
+}
+
+/*
+ * Copy a certificate list.
+ * Warning: The destination list will be flushed on error.
+ */
+enum rawrtc_code rawrtc_certificate_list_copy(
+    struct list* const destination_list,  // de-referenced, copied into
+    struct list* const source_list  // de-referenced, copied (each item)
+) {
+    struct le* le;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!destination_list || !source_list) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Append and reference certificates
+    for (le = list_head(source_list); le != NULL; le = le->next) {
+        struct rawrtc_certificate* const certificate = le->data;
+        error = copy_and_append_certificate(destination_list, certificate);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        list_flush(destination_list);
+    }
+    return error;
+}
diff --git a/src/certificate/certificate.h b/src/certificate/certificate.h
new file mode 100644
index 0000000..4409928
--- /dev/null
+++ b/src/certificate/certificate.h
@@ -0,0 +1,90 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <re.h>
+#include <openssl/evp.h>  // EVP_*
+#include <openssl/x509.h>  // X509
+
+/*
+ * Maximum digest size of certificate fingerprint.
+ */
+enum {
+    RAWRTC_MODULUS_LENGTH_MIN = 1024,
+    RAWRTC_FINGERPRINT_MAX_SIZE = EVP_MAX_MD_SIZE,
+    RAWRTC_FINGERPRINT_MAX_SIZE_HEX = (EVP_MAX_MD_SIZE * 2),
+};
+
+/*
+ * Certificate options.
+ */
+struct rawrtc_certificate_options {
+    enum rawrtc_certificate_key_type key_type;
+    char* common_name;  // copied
+    uint_fast32_t valid_until;
+    enum rawrtc_certificate_sign_algorithm sign_algorithm;
+    char* named_curve;  // nullable, copied, ignored for RSA
+    uint_fast32_t modulus_length;  // ignored for ECC
+};
+
+/*
+ * Certificate.
+ */
+struct rawrtc_certificate {
+    struct le le;
+    X509* certificate;
+    EVP_PKEY* key;
+    enum rawrtc_certificate_key_type key_type;
+};
+
+extern struct rawrtc_certificate_options rawrtc_default_certificate_options;
+
+enum rawrtc_code rawrtc_certificate_copy(
+    struct rawrtc_certificate** const certificatep,  // de-referenced
+    struct rawrtc_certificate* const source_certificate);
+
+enum rawrtc_code rawrtc_certificate_get_pem(
+    char** const pemp,  // de-referenced
+    size_t* const pem_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode);
+
+enum rawrtc_code rawrtc_certificate_get_der(
+    uint8_t** const derp,  // de-referenced
+    size_t* const der_lengthp,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_encode const to_encode);
+
+enum rawrtc_code rawrtc_certificate_get_fingerprint(
+    char** const fingerprint,  // de-referenced
+    struct rawrtc_certificate* const certificate,
+    enum rawrtc_certificate_sign_algorithm const algorithm);
+
+enum rawrtc_code rawrtc_certificate_array_to_list(
+    struct list* const certificate_list,  // de-referenced, copied into
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates);
+
+enum rawrtc_code rawrtc_certificate_list_copy(
+    struct list* const destination_list,  // de-referenced, copied into
+    struct list* const source_list  // de-referenced, copied (each item)
+);
+
+enum tls_keytype rawrtc_certificate_key_type_to_tls_keytype(
+    const enum rawrtc_certificate_key_type type);
+
+enum rawrtc_code rawrtc_tls_keytype_to_certificate_key_type(
+    enum rawrtc_certificate_key_type* const typep,  // de-referenced
+    enum tls_keytype const re_type);
+
+enum rawrtc_code rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+    enum tls_fingerprint* const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm);
+
+enum rawrtc_code rawrtc_tls_fingerprint_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    enum tls_fingerprint re_algorithm);
+
+EVP_MD const* rawrtc_get_sign_function(enum rawrtc_certificate_sign_algorithm type);
+
+enum rawrtc_code rawrtc_get_sign_algorithm_length(
+    size_t* const sizep,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const type);
diff --git a/src/certificate/meson.build b/src/certificate/meson.build
new file mode 100644
index 0000000..9d0e932
--- /dev/null
+++ b/src/certificate/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'certificate.c',
+    'utils.c',
+])
diff --git a/src/certificate/utils.c b/src/certificate/utils.c
new file mode 100644
index 0000000..e9ab58c
--- /dev/null
+++ b/src/certificate/utils.c
@@ -0,0 +1,177 @@
+#include "certificate.h"
+#include <rawrtc/certificate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Translate a certificate key type to the corresponding re type.
+ */
+enum tls_keytype rawrtc_certificate_key_type_to_tls_keytype(
+    enum rawrtc_certificate_key_type const type) {
+    // No conversion needed
+    return (enum tls_keytype) type;
+}
+
+/*
+ * Translate a re key type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_tls_keytype_to_certificate_key_type(
+    enum rawrtc_certificate_key_type* const typep,  // de-referenced
+    enum tls_keytype const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case TLS_KEYTYPE_RSA:
+            *typep = RAWRTC_CERTIFICATE_KEY_TYPE_RSA;
+            return RAWRTC_CODE_SUCCESS;
+        case TLS_KEYTYPE_EC:
+            *typep = RAWRTC_CERTIFICATE_KEY_TYPE_EC;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate a certificate sign algorithm to the corresponding re fingerprint algorithm.
+ */
+enum rawrtc_code rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+    enum tls_fingerprint* const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    switch (algorithm) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384:
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512:
+            // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to re)
+            return RAWRTC_CODE_UNSUPPORTED_ALGORITHM;
+        default:
+            break;
+    }
+
+    // No conversion needed
+    *fingerprintp = (enum tls_fingerprint) algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Translate a re fingerprint algorithm to the corresponding rawrtc algorithm.
+ */
+enum rawrtc_code rawrtc_tls_fingerprint_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    enum tls_fingerprint re_algorithm) {
+    // Check arguments
+    if (!algorithmp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    // Note: SHA-384 and SHA-512 are currently not supported (needs to be added to libre)
+    switch (re_algorithm) {
+        case TLS_FINGERPRINT_SHA256:
+            *algorithmp = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+static enum rawrtc_certificate_sign_algorithm const map_enum_certificate_sign_algorithm[] = {
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384,
+    RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512,
+};
+
+static char const* const map_str_certificate_sign_algorithm[] = {
+    "sha-256",
+    "sha-384",
+    "sha-512",
+};
+
+static size_t const map_certificate_sign_algorithm_length =
+    ARRAY_SIZE(map_enum_certificate_sign_algorithm);
+
+/*
+ * Translate a certificate sign algorithm to str.
+ */
+char const* rawrtc_certificate_sign_algorithm_to_str(
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    size_t i;
+
+    for (i = 0; i < map_certificate_sign_algorithm_length; ++i) {
+        if (map_enum_certificate_sign_algorithm[i] == algorithm) {
+            return map_str_certificate_sign_algorithm[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to a certificate sign algorithm (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_certificate_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const algorithmp,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!algorithmp || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_certificate_sign_algorithm_length; ++i) {
+        if (str_casecmp(map_str_certificate_sign_algorithm[i], str) == 0) {
+            *algorithmp = map_enum_certificate_sign_algorithm[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Get the EVP_MD* structure for a certificate sign algorithm type.
+ */
+EVP_MD const* rawrtc_get_sign_function(enum rawrtc_certificate_sign_algorithm const type) {
+    switch (type) {
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256:
+            return EVP_sha256();
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA384:
+            return EVP_sha384();
+        case RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA512:
+            return EVP_sha512();
+        default:
+            return NULL;
+    }
+}
+
+/*
+ * Get the length of the fingerprint to a certificate sign algorithm type.
+ */
+enum rawrtc_code rawrtc_get_sign_algorithm_length(
+    size_t* const sizep,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const type) {
+    EVP_MD const* sign_function;
+    int size;
+
+    // Get sign algorithm function
+    sign_function = rawrtc_get_sign_function(type);
+    if (!sign_function) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get length
+    size = EVP_MD_size(sign_function);
+    if (size < 1) {
+        return RAWRTC_CODE_UNSUPPORTED_ALGORITHM;
+    }
+
+    // Set size
+    *sizep = (size_t) size;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/diffie_hellman_parameters/meson.build b/src/diffie_hellman_parameters/meson.build
new file mode 100644
index 0000000..7505db0
--- /dev/null
+++ b/src/diffie_hellman_parameters/meson.build
@@ -0,0 +1 @@
+sources += files('parameters.c')
diff --git a/src/diffie_hellman_parameters/parameters.c b/src/diffie_hellman_parameters/parameters.c
new file mode 100644
index 0000000..7783e59
--- /dev/null
+++ b/src/diffie_hellman_parameters/parameters.c
@@ -0,0 +1,190 @@
+#include "parameters.h"
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <openssl/bio.h>  // BIO_new_mem_buf
+#include <openssl/dh.h>  // DH, DH_check_params
+#include <openssl/err.h>  // ERR_clear_error
+#include <openssl/pem.h>  // PEM_read_bio_DHparams
+#include <openssl/ssl.h>  // SSL_CTX_set_tmp_dh, SSL_CTX_set_ecdh_auto
+#include <limits.h>  // INT_MAX, LONG_MAX
+
+#define DEBUG_MODULE "diffie-hellman-parameters"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Apply Diffie-Hellman parameters on an OpenSSL context.
+ */
+static enum rawrtc_code set_dh_parameters(
+    struct ssl_ctx_st* const ssl_context,  // not checked
+    DH const* const dh  // not checked
+) {
+    int codes;
+
+    // Check that the parameters are "likely enough to be valid"
+#if OPENSSL_VERSION_NUMBER < 0x1010000fL || defined(OPENSSL_IS_BORINGSSL)
+    if (!DH_check(dh, &codes)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#else
+    if (!DH_check_params(dh, &codes)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+#endif
+    if (codes) {
+#if defined(DH_CHECK_P_NOT_PRIME)
+        if (codes & DH_CHECK_P_NOT_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: p is not prime\n");
+        }
+#endif
+#if defined(DH_CHECK_P_NOT_SAFE_PRIME)
+        if (codes & DH_CHECK_P_NOT_SAFE_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: p is not safe prime\n");
+        }
+#endif
+#if defined(DH_UNABLE_TO_CHECK_GENERATOR)
+        if (codes & DH_UNABLE_TO_CHECK_GENERATOR) {
+            DEBUG_WARNING("set_dh_parameters: generator g cannot be checked\n");
+        }
+#endif
+#if defined(DH_NOT_SUITABLE_GENERATOR)
+        if (codes & DH_NOT_SUITABLE_GENERATOR) {
+            DEBUG_WARNING("set_dh_parameters: generator g is not suitable\n");
+        }
+#endif
+#if defined(DH_CHECK_Q_NOT_PRIME)
+        if (codes & DH_CHECK_Q_NOT_PRIME) {
+            DEBUG_WARNING("set_dh_parameters: q is not prime\n");
+        }
+#endif
+#if defined(DH_CHECK_INVALID_Q_VALUE)
+        if (codes & DH_CHECK_INVALID_Q_VALUE) {
+            DEBUG_WARNING("set_dh_parameters: q is invalid\n");
+        }
+#endif
+#if defined(DH_CHECK_INVALID_J_VALUE)
+        if (codes & DH_CHECK_INVALID_J_VALUE) {
+            DEBUG_WARNING("set_dh_parameters: j is invalid\n");
+        }
+#endif
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Apply Diffie-Hellman parameters
+    if (!SSL_CTX_set_tmp_dh(ssl_context, dh)) {
+        DEBUG_WARNING("set_dh_parameters: set_tmp_dh failed\n");
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set Diffie-Hellman parameters on an OpenSSL context using DER encoding.
+ */
+enum rawrtc_code rawrtc_set_dh_parameters_der(
+    struct tls* const tls, uint8_t const* const der, size_t const der_size) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+    DH* dh = NULL;
+    enum rawrtc_code error = RAWRTC_CODE_UNKNOWN_ERROR;
+
+    // Check arguments
+    if (!ssl_context || !der || der_size == 0 || der_size > LONG_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Decode PKCS#3 Diffie-Hellman parameters
+    dh = d2i_DHparams(NULL, (unsigned char const**) &der, der_size);
+    if (!dh) {
+        goto out;
+    }
+
+    // Apply Diffie-Hellman parameters
+    error = set_dh_parameters(ssl_context, dh);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (dh) {
+        DH_free(dh);
+    }
+    if (error) {
+        ERR_clear_error();
+    }
+    return error;
+}
+
+/**
+ * Set Diffie-Hellman parameters on an OpenSSL context using PEM encoding.
+ */
+enum rawrtc_code rawrtc_set_dh_parameters_pem(
+    struct tls* const tls, char const* const pem, size_t const pem_size) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+    BIO* bio = NULL;
+    DH* dh = NULL;
+    enum rawrtc_code error = RAWRTC_CODE_NO_MEMORY;
+
+    // Check arguments
+    if (!ssl_context || !pem || pem_size == 0 || pem_size > INT_MAX) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create memory sink
+    bio = BIO_new_mem_buf(pem, (int) pem_size);
+    if (!bio) {
+        goto out;
+    }
+
+    // Read Diffie-Hellman parameters into memory sink
+    dh = PEM_read_bio_DHparams(bio, NULL, 0, NULL);
+    if (!dh)
+        goto out;
+
+    // Apply Diffie-Hellman parameters
+    error = set_dh_parameters(ssl_context, dh);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (dh) {
+        DH_free(dh);
+    }
+    if (bio) {
+        BIO_free(bio);
+    }
+    if (error) {
+        ERR_clear_error();
+    }
+    return error;
+}
+
+/*
+ * Enable elliptic-curve Diffie-Hellman on an OpenSSL context.
+ */
+enum rawrtc_code rawrtc_enable_ecdh(struct tls* const tls) {
+    struct ssl_ctx_st* const ssl_context = tls_openssl_context(tls);
+
+    // Check arguments
+    if (!ssl_context) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Enable elliptic-curve Diffie-Hellman
+    if (!SSL_CTX_set_ecdh_auto(ssl_context, (long) 1)) {
+        DEBUG_WARNING("set_dh_params: set_ecdh_auto failed\n");
+        return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/diffie_hellman_parameters/parameters.h b/src/diffie_hellman_parameters/parameters.h
new file mode 100644
index 0000000..fecf924
--- /dev/null
+++ b/src/diffie_hellman_parameters/parameters.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_set_dh_parameters_der(
+    struct tls* const tls, uint8_t const* const der, size_t const der_size);
+
+enum rawrtc_code rawrtc_set_dh_parameters_pem(
+    struct tls* const tls, char const* const pem, size_t const pem_size);
+
+enum rawrtc_code rawrtc_enable_ecdh(struct tls* const tls);
diff --git a/src/dtls_fingerprint/attributes.c b/src/dtls_fingerprint/attributes.c
new file mode 100644
index 0000000..dc52659
--- /dev/null
+++ b/src/dtls_fingerprint/attributes.c
@@ -0,0 +1,38 @@
+#include "fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the DTLS certificate fingerprint's sign algorithm.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_sign_algorithm(
+    enum rawrtc_certificate_sign_algorithm* const sign_algorithmp,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint) {
+    // Check arguments
+    if (!sign_algorithmp || !fingerprint) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set sign algorithm
+    *sign_algorithmp = fingerprint->algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the DTLS certificate's fingerprint value.
+ * `*valuep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_get_value(
+    char** const valuep,  // de-referenced
+    struct rawrtc_dtls_fingerprint* const fingerprint) {
+    // Check arguments
+    if (!valuep || !fingerprint) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *valuep = mem_ref(fingerprint->value);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_fingerprint/fingerprint.c b/src/dtls_fingerprint/fingerprint.c
new file mode 100644
index 0000000..bfa3b0a
--- /dev/null
+++ b/src/dtls_fingerprint/fingerprint.c
@@ -0,0 +1,74 @@
+#include "fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing DTLS fingerprint instance.
+ */
+static void rawrtc_dtls_fingerprint_destroy(void* arg) {
+    struct rawrtc_dtls_fingerprint* const fingerprint = arg;
+
+    // Un-reference
+    mem_deref(fingerprint->value);
+}
+
+/*
+ * Create a new DTLS fingerprint instance.
+ * `*fingerprintp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_create(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm,
+    char* const value  // copied
+) {
+    struct rawrtc_dtls_fingerprint* fingerprint;
+    enum rawrtc_code error;
+
+    // Allocate
+    fingerprint = mem_zalloc(sizeof(*fingerprint), rawrtc_dtls_fingerprint_destroy);
+    if (!fingerprint) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    fingerprint->algorithm = algorithm;
+    error = rawrtc_strdup(&fingerprint->value, value);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(fingerprint);
+    } else {
+        // Set pointer
+        *fingerprintp = fingerprint;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS fingerprint instance without any value.
+ * The caller MUST set the `value` field after creation.
+ */
+enum rawrtc_code rawrtc_dtls_fingerprint_create_empty(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm) {
+    struct rawrtc_dtls_fingerprint* fingerprint;
+
+    // Allocate
+    fingerprint = mem_zalloc(sizeof(*fingerprint), rawrtc_dtls_fingerprint_destroy);
+    if (!fingerprint) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    fingerprint->algorithm = algorithm;
+
+    // Set pointer
+    *fingerprintp = fingerprint;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_fingerprint/fingerprint.h b/src/dtls_fingerprint/fingerprint.h
new file mode 100644
index 0000000..30b788b
--- /dev/null
+++ b/src/dtls_fingerprint/fingerprint.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_dtls_fingerprint {
+    struct le le;
+    enum rawrtc_certificate_sign_algorithm algorithm;
+    char* value;  // copied
+};
+
+enum rawrtc_code rawrtc_dtls_fingerprint_create_empty(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced
+    enum rawrtc_certificate_sign_algorithm const algorithm);
diff --git a/src/dtls_fingerprint/meson.build b/src/dtls_fingerprint/meson.build
new file mode 100644
index 0000000..2e32bbf
--- /dev/null
+++ b/src/dtls_fingerprint/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'fingerprint.c',
+])
diff --git a/src/dtls_parameters/attributes.c b/src/dtls_parameters/attributes.c
new file mode 100644
index 0000000..e8a6a30
--- /dev/null
+++ b/src/dtls_parameters/attributes.c
@@ -0,0 +1,39 @@
+#include "parameters.h"
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the DTLS parameter's role value.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_role(
+    enum rawrtc_dtls_role* rolep,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters) {
+    // Check arguments
+    if (!rolep || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *rolep = parameters->role;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the DTLS parameter's fingerprint array.
+ * `*fingerprintsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_get_fingerprints(
+    struct rawrtc_dtls_fingerprints** const fingerprintsp,  // de-referenced
+    struct rawrtc_dtls_parameters* const parameters) {
+    // Check arguments
+    if (!fingerprintsp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *fingerprintsp = mem_ref(parameters->fingerprints);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_parameters/meson.build b/src/dtls_parameters/meson.build
new file mode 100644
index 0000000..8710eb0
--- /dev/null
+++ b/src/dtls_parameters/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'parameters.c',
+    'utils.c',
+])
diff --git a/src/dtls_parameters/parameters.c b/src/dtls_parameters/parameters.c
new file mode 100644
index 0000000..32dc5a2
--- /dev/null
+++ b/src/dtls_parameters/parameters.c
@@ -0,0 +1,173 @@
+#include "parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing DTLS parameter's fingerprints instance.
+ */
+static void rawrtc_dtls_parameters_fingerprints_destroy(void* arg) {
+    struct rawrtc_dtls_fingerprints* const fingerprints = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        mem_deref(fingerprints->fingerprints[i]);
+    }
+}
+
+/*
+ * Destructor for an existing DTLS parameters instance.
+ */
+static void rawrtc_dtls_parameters_destroy(void* arg) {
+    struct rawrtc_dtls_parameters* const parameters = arg;
+
+    // Un-reference
+    mem_deref(parameters->fingerprints);
+}
+
+/*
+ * Common code to allocate a DTLS parameters instance.
+ */
+static enum rawrtc_code rawrtc_dtls_parameters_allocate(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    size_t const n_fingerprints) {
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    struct rawrtc_dtls_parameters* parameters;
+    size_t fingerprints_size;
+
+    // Allocate parameters
+    parameters = mem_zalloc(sizeof(*parameters), rawrtc_dtls_parameters_destroy);
+    if (!parameters) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set role
+    parameters->role = role;
+
+    // Allocate fingerprints array & set length immediately
+    fingerprints_size = sizeof(*parameters) * n_fingerprints;
+    parameters->fingerprints = mem_zalloc(
+        sizeof(*parameters) + fingerprints_size, rawrtc_dtls_parameters_fingerprints_destroy);
+    if (!parameters->fingerprints) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    parameters->fingerprints->n_fingerprints = n_fingerprints;
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_create(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct rawrtc_dtls_fingerprint* const fingerprints[],  // referenced (each item)
+    size_t const n_fingerprints) {
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_code error;
+    size_t i;
+
+    // Check arguments
+    if (!parametersp || !fingerprints || n_fingerprints < 1) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create parameters
+    error = rawrtc_dtls_parameters_allocate(&parameters, role, n_fingerprints);
+    if (error) {
+        goto out;
+    }
+
+    // Reference and set each fingerprint
+    for (i = 0; i < n_fingerprints; ++i) {
+        // Null?
+        if (!fingerprints[i]) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Check algorithm
+        if (fingerprints[i]->algorithm == RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Reference and set fingerprint
+        parameters->fingerprints->fingerprints[i] = mem_ref(fingerprints[i]);
+    }
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
+
+/*
+ * Create parameters from the internal vars of a DTLS transport
+ * instance.
+ */
+enum rawrtc_code rawrtc_dtls_parameters_create_internal(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct list* const fingerprints) {
+    size_t n_fingerprints;
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_code error;
+    struct le* le;
+    size_t i;
+
+    // Check arguments
+    if (!parametersp || !fingerprints) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get fingerprints length
+    n_fingerprints = list_count(fingerprints);
+
+    // Create parameters
+    error = rawrtc_dtls_parameters_allocate(&parameters, role, n_fingerprints);
+    if (error) {
+        goto out;
+    }
+
+    // Reference and set each fingerprint
+    for (le = list_head(fingerprints), i = 0; le != NULL; le = le->next, ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint = le->data;
+
+        // Check algorithm
+        if (fingerprint->algorithm == RAWRTC_CERTIFICATE_SIGN_ALGORITHM_NONE) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Reference and set fingerprint
+        parameters->fingerprints->fingerprints[i] = mem_ref(fingerprint);
+    }
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
diff --git a/src/dtls_parameters/parameters.h b/src/dtls_parameters/parameters.h
new file mode 100644
index 0000000..ac018a2
--- /dev/null
+++ b/src/dtls_parameters/parameters.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_dtls_parameters {
+    enum rawrtc_dtls_role role;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+};
+
+enum rawrtc_code rawrtc_dtls_parameters_create_internal(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    enum rawrtc_dtls_role const role,
+    struct list* const fingerprints);
+
+int rawrtc_dtls_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_dtls_parameters const* const parameters);
diff --git a/src/dtls_parameters/utils.c b/src/dtls_parameters/utils.c
new file mode 100644
index 0000000..e79e4c5
--- /dev/null
+++ b/src/dtls_parameters/utils.c
@@ -0,0 +1,40 @@
+#include "parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_transport.h>
+#include <re.h>
+
+/*
+ * Print debug information for DTLS parameters.
+ */
+int rawrtc_dtls_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_dtls_parameters const* const parameters) {
+    int err = 0;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    size_t i;
+
+    // Check arguments
+    if (!parameters) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  DTLS Parameters <%p>:\n", parameters);
+
+    // Role
+    err |= re_hprintf(pf, "    role=%s\n", rawrtc_dtls_role_to_str(parameters->role));
+
+    // Fingerprints
+    fingerprints = parameters->fingerprints;
+    err |= re_hprintf(pf, "    Fingerprints <%p>:\n", fingerprints);
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        // Fingerprint
+        err |= re_hprintf(
+            pf, "      algorithm=%s value=%s\n",
+            rawrtc_certificate_sign_algorithm_to_str(fingerprints->fingerprints[i]->algorithm),
+            fingerprints->fingerprints[i]->value);
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/dtls_transport/attributes.c b/src/dtls_transport/attributes.c
new file mode 100644
index 0000000..411e611
--- /dev/null
+++ b/src/dtls_transport/attributes.c
@@ -0,0 +1,40 @@
+#include "transport.h"
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Check for an existing data transport (on top of DTLS).
+ */
+enum rawrtc_code rawrtc_dtls_transport_have_data_transport(
+    bool* const have_data_transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!have_data_transportp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if a receive handler has been set.
+    if (transport->receive_handler) {
+        *have_data_transportp = true;
+    } else {
+        *have_data_transportp = false;
+    }
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current state of the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_state(
+    enum rawrtc_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/dtls_transport/external.c b/src/dtls_transport/external.c
new file mode 100644
index 0000000..2f56096
--- /dev/null
+++ b/src/dtls_transport/external.c
@@ -0,0 +1,64 @@
+#include "transport.h"
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+
+/*
+ * Get external DTLS role.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_external_role(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!rolep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert role
+    switch (transport->role) {
+        case RAWRTC_DTLS_ROLE_AUTO:
+            // Unable to convert in this state
+            return RAWRTC_CODE_INVALID_STATE;
+        case RAWRTC_DTLS_ROLE_CLIENT:
+            *rolep = RAWRTC_EXTERNAL_DTLS_ROLE_CLIENT;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_ROLE_SERVER:
+            *rolep = RAWRTC_EXTERNAL_DTLS_ROLE_SERVER;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+}
+
+/*
+ * Convert DTLS transport state to external DTLS transport state.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_external_state(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert DTLS transport state to external DTLS transport state
+    switch (transport->state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_NEW:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_NEW_OR_CONNECTING;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_NEW_OR_CONNECTING;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CONNECTED;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            *statep = RAWRTC_EXTERNAL_DTLS_TRANSPORT_STATE_CLOSED_OR_FAILED;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+}
diff --git a/src/dtls_transport/meson.build b/src/dtls_transport/meson.build
new file mode 100644
index 0000000..3bc7451
--- /dev/null
+++ b/src/dtls_transport/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'attributes.c',
+    'external.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/dtls_transport/transport.c b/src/dtls_transport/transport.c
new file mode 100644
index 0000000..8219697
--- /dev/null
+++ b/src/dtls_transport/transport.c
@@ -0,0 +1,1059 @@
+#include "transport.h"
+#include "../certificate/certificate.h"
+#include "../diffie_hellman_parameters/parameters.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include "../dtls_parameters/parameters.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_transport/transport.h"
+#include "../main/config.h"
+#include "../utils/utils.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/message_buffer.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+#include <string.h>  // memcmp
+
+#define DEBUG_MODULE "dtls-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Embedded DH parameters in DER encoding (bits: 2048)
+ */
+uint8_t const rawrtc_default_dh_parameters[] = {
+    0x30, 0x82, 0x01, 0x08, 0x02, 0x82, 0x01, 0x01, 0x00, 0xaa, 0x4c, 0x1f, 0x1e, 0xc9, 0xed, 0xfe,
+    0x5c, 0x50, 0x2d, 0xff, 0xf4, 0x95, 0xf4, 0x80, 0x69, 0xcf, 0xc3, 0x84, 0x29, 0x87, 0xd5, 0x2c,
+    0x4f, 0xf6, 0x9e, 0x88, 0xa2, 0x5b, 0x61, 0xd2, 0x7d, 0x78, 0x97, 0xce, 0x47, 0x39, 0x9d, 0xc0,
+    0x95, 0x14, 0x98, 0x1f, 0xa9, 0xa3, 0x42, 0x93, 0x58, 0x49, 0x3d, 0xad, 0xeb, 0x6c, 0x3d, 0x79,
+    0x2d, 0x27, 0x94, 0x67, 0x4c, 0xdc, 0x94, 0x31, 0xbf, 0xc1, 0x00, 0x9d, 0x96, 0x4a, 0x91, 0xa7,
+    0x4f, 0xab, 0x48, 0x44, 0xcc, 0x54, 0x1a, 0x4e, 0x2a, 0x8e, 0xa1, 0x81, 0x4b, 0xeb, 0xea, 0xc3,
+    0xba, 0xd6, 0x03, 0xfb, 0xf2, 0x9a, 0x48, 0x1f, 0xc8, 0xba, 0x73, 0x89, 0x86, 0x25, 0x2e, 0xba,
+    0x10, 0x80, 0x2a, 0xeb, 0xf9, 0xe2, 0x28, 0xf1, 0xcf, 0x85, 0x0d, 0xeb, 0x2f, 0x61, 0x51, 0x11,
+    0xe1, 0xe7, 0x82, 0xe5, 0xa7, 0x5d, 0x71, 0x0a, 0xef, 0x8a, 0xe1, 0x97, 0x48, 0x41, 0xac, 0xd7,
+    0xc5, 0xf7, 0xce, 0xd5, 0xcd, 0x66, 0x1e, 0x6b, 0x0e, 0x82, 0x4e, 0x77, 0x5d, 0x89, 0x3b, 0xe2,
+    0x94, 0x7a, 0x10, 0xee, 0x5b, 0x5d, 0x36, 0x07, 0x29, 0x8b, 0x06, 0xb6, 0x49, 0x1e, 0x17, 0x17,
+    0x57, 0xc8, 0xc1, 0x80, 0x24, 0x15, 0x22, 0x9c, 0xb8, 0x59, 0x55, 0x08, 0x41, 0x67, 0x07, 0xca,
+    0xa8, 0x54, 0x1a, 0xd1, 0xb7, 0x91, 0x2f, 0x41, 0x78, 0xc0, 0xcd, 0x2f, 0x07, 0x49, 0x4b, 0xb9,
+    0x05, 0xf4, 0xea, 0x72, 0x3a, 0xcf, 0x04, 0x69, 0xcb, 0x5b, 0xe4, 0xcb, 0x4f, 0x72, 0x40, 0xe4,
+    0x56, 0x1f, 0xca, 0xee, 0x33, 0x2b, 0x29, 0x1a, 0x80, 0xda, 0x01, 0x3f, 0x03, 0xa6, 0xbf, 0x32,
+    0x02, 0x6c, 0xfb, 0xb1, 0xb5, 0x81, 0xda, 0x32, 0x6f, 0xa1, 0x4b, 0x9f, 0x42, 0x2e, 0x17, 0xc9,
+    0x95, 0x30, 0xda, 0x16, 0xb7, 0x9a, 0x7c, 0xf4, 0x83, 0x02, 0x01, 0x02,
+};
+size_t const rawrtc_default_dh_parameters_length = ARRAY_SIZE(rawrtc_default_dh_parameters);
+
+/*
+ * List of default DTLS cipher suites.
+ */
+char const* rawrtc_default_dtls_cipher_suites[] = {
+    "ECDHE-ECDSA-CHACHA20-POLY1305",
+    "ECDHE-RSA-CHACHA20-POLY1305",
+    "ECDHE-ECDSA-AES128-GCM-SHA256",  // recommended
+    "ECDHE-RSA-AES128-GCM-SHA256",
+    "ECDHE-ECDSA-AES256-GCM-SHA384",
+    "ECDHE-RSA-AES256-GCM-SHA384",
+    "DHE-RSA-AES128-GCM-SHA256",
+    "DHE-RSA-AES256-GCM-SHA384",
+    "ECDHE-ECDSA-AES128-SHA256",
+    "ECDHE-RSA-AES128-SHA256",
+    "ECDHE-ECDSA-AES128-SHA",  // required
+    "ECDHE-RSA-AES256-SHA384",
+    "ECDHE-RSA-AES128-SHA",
+    "ECDHE-ECDSA-AES256-SHA384",
+    "ECDHE-ECDSA-AES256-SHA",
+    "ECDHE-RSA-AES256-SHA",
+    "DHE-RSA-AES128-SHA256",
+    "DHE-RSA-AES128-SHA",
+    "DHE-RSA-AES256-SHA256",
+    "DHE-RSA-AES256-SHA",
+};
+size_t const rawrtc_default_dtls_cipher_suites_length =
+    ARRAY_SIZE(rawrtc_default_dtls_cipher_suites);
+
+/*
+ * Handle outgoing buffered DTLS messages.
+ */
+static bool dtls_outgoing_buffer_handler(
+    struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+    (void) context;
+
+    // Send
+    error = rawrtc_dtls_transport_send(transport, buffer);
+    if (error) {
+        DEBUG_WARNING("Could not send buffered packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Change the state of the ICE transport.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_state(
+    struct rawrtc_dtls_transport* const transport, enum rawrtc_dtls_transport_state const state) {
+    // Closed or failed: Remove connection
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CLOSED ||
+        state == RAWRTC_DTLS_TRANSPORT_STATE_FAILED) {
+        // Remove connection
+        transport->connection = mem_deref(transport->connection);
+
+        // Remove self from ICE transport (if attached)
+        transport->ice_transport->dtls_transport = NULL;
+    }
+
+    // Set state
+    transport->state = state;
+
+    // Connected?
+    if (state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        // Send buffered outgoing DTLS messages
+        enum rawrtc_code const error = rawrtc_message_buffer_clear(
+            &transport->buffered_messages_out, dtls_outgoing_buffer_handler, transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Could not send buffered messages, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+/*
+ * Check if the state is 'closed' or 'failed'.
+ */
+static bool is_closed(struct rawrtc_dtls_transport* const transport  // not checked
+) {
+    switch (transport->state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            return true;
+        default:
+            return false;
+    }
+}
+
+/*
+ * DTLS connection closed handler.
+ */
+static void close_handler(int err, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+
+    // Closed?
+    if (!is_closed(transport)) {
+        DEBUG_INFO("DTLS connection closed, reason: %m\n", err);
+
+        // Set to failed if not closed normally
+        if (err != ECONNRESET) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_FAILED);
+        }
+
+        // Stop
+        error = rawrtc_dtls_transport_stop(transport);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS connection closed, could not stop transport: %s\n",
+                rawrtc_code_to_str(error));
+        }
+    } else {
+        DEBUG_PRINTF(
+            "DTLS connection closed (but state is already closed anyway), reason: %m\n", err);
+    }
+}
+
+/*
+ * Handle incoming DTLS messages.
+ */
+static void dtls_receive_handler(struct mbuf* buffer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    enum rawrtc_code error;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_PRINTF("Ignoring incoming DTLS message, transport is closed\n");
+        return;
+    }
+
+    // Handle (if receive handler exists and connected)
+    // Note: Checking for 'connected' state ensures that no data will be received before the
+    //       fingerprints have been verified.
+    if (transport->receive_handler && transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        transport->receive_handler(buffer, transport->receive_handler_arg);
+        return;
+    }
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&transport->buffered_messages_in, buffer, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not buffer incoming packet, reason: %s\n", rawrtc_code_to_str(error));
+    } else {
+        DEBUG_PRINTF("Buffered incoming packet of size %zu\n", mbuf_get_left(buffer));
+    }
+}
+
+/*
+ * Either called by a DTLS connection established event or by the
+ * `start` method of the DTLS transport.
+ * The caller MUST make sure that remote parameters are available and
+ * that the state is NOT 'closed' or 'failed'!
+ */
+static void verify_certificate(struct rawrtc_dtls_transport* const transport  // not checked
+) {
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    bool valid = false;
+    enum tls_fingerprint algorithm;
+    uint8_t expected_fingerprint[RAWRTC_FINGERPRINT_MAX_SIZE];
+    uint8_t actual_fingerprint[RAWRTC_FINGERPRINT_MAX_SIZE];
+
+    // Verify the peer's certificate
+    // TODO: Fix this. Testing the fingerprint alone is okay for now though.
+    // error = rawrtc_error_to_code(tls_peer_verify(transport->connection));
+    // if (error) {
+    //     goto out;
+    // }
+    // DEBUG_PRINTF("Peer's certificate verified\n");
+
+    // Check if *any* of the fingerprints provided matches
+    // Note: We don't verify the peer's certificate since it will almost always
+    //       be self-signed.
+    for (i = 0; i < transport->remote_parameters->fingerprints->n_fingerprints; ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint =
+            transport->remote_parameters->fingerprints->fingerprints[i];
+        size_t length;
+        size_t bytes_written;
+
+        // Get algorithm
+        error = rawrtc_certificate_sign_algorithm_to_tls_fingerprint(
+            &algorithm, fingerprint->algorithm);
+        if (error) {
+            if (error == RAWRTC_CODE_UNSUPPORTED_ALGORITHM) {
+                continue;
+            }
+            goto out;
+        }
+
+        // Get algorithm digest size
+        error = rawrtc_get_sign_algorithm_length(&length, fingerprint->algorithm);
+        if (error) {
+            if (error == RAWRTC_CODE_UNSUPPORTED_ALGORITHM) {
+                continue;
+            }
+            goto out;
+        }
+
+        // Convert hex-encoded value to binary
+        error = rawrtc_colon_hex_to_bin(
+            &bytes_written, expected_fingerprint, length, fingerprint->value);
+        if (error) {
+            if (error == RAWRTC_CODE_INSUFFICIENT_SPACE) {
+                DEBUG_WARNING("Hex-encoded fingerprint exceeds buffer size!\n");
+            } else {
+                DEBUG_WARNING(
+                    "Could not convert hex-encoded fingerprint to binary, reason: %s\n",
+                    rawrtc_code_to_str(error));
+            }
+            continue;
+        }
+
+        // Validate length
+        if (bytes_written != length) {
+            DEBUG_WARNING(
+                "Hex-encoded fingerprint should have been %zu bytes but was %zu bytes\n", length,
+                bytes_written);
+            continue;
+        }
+
+        // Get remote fingerprint
+        error = rawrtc_error_to_code(tls_peer_fingerprint(
+            transport->connection, algorithm, actual_fingerprint, sizeof(actual_fingerprint)));
+        if (error) {
+            goto out;
+        }
+
+        // Compare fingerprints
+        if (memcmp(expected_fingerprint, actual_fingerprint, length) == 0) {
+            DEBUG_PRINTF("Peer's certificate fingerprint is valid\n");
+            valid = true;
+        }
+    }
+
+out:
+    if (error || !valid) {
+        DEBUG_WARNING("Verifying certificate failed, reason: %s\n", rawrtc_code_to_str(error));
+        if (!is_closed(transport)) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_FAILED);
+        }
+
+        // Stop
+        error = rawrtc_dtls_transport_stop(transport);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS connection closed, could not stop transport: %s\n",
+                rawrtc_code_to_str(error));
+        }
+    } else {
+        // Connected
+        set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED);
+    }
+}
+
+/*
+ * Handle DTLS connection established event.
+ */
+static void establish_handler(void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_WARNING("Ignoring established DTLS connection, transport is closed\n");
+        return;
+    }
+
+    // Connection established
+    // Note: State is either 'NEW', 'CONNECTING' or 'FAILED' here
+    DEBUG_INFO("DTLS connection established\n");
+    transport->connection_established = true;
+
+    // Verify certificate & fingerprint (if remote parameters are available)
+    if (transport->remote_parameters) {
+        verify_certificate(transport);
+    }
+}
+
+/*
+ * Handle incoming DTLS connection.
+ */
+static void connect_handler(const struct sa* peer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    bool role_is_server;
+    bool have_connection;
+    int err;
+    (void) peer;
+
+    // Check state
+    if (is_closed(transport)) {
+        DEBUG_PRINTF("Ignoring incoming DTLS connection, transport is closed\n");
+        return;
+    }
+
+    // Update role if "auto"
+    if (transport->role == RAWRTC_DTLS_ROLE_AUTO) {
+        DEBUG_PRINTF("Switching role 'auto' -> 'server'\n");
+        transport->role = RAWRTC_DTLS_ROLE_SERVER;
+    }
+
+    // Accept?
+    role_is_server = transport->role == RAWRTC_DTLS_ROLE_SERVER;
+    have_connection = transport->connection != NULL;
+    if (role_is_server && !have_connection) {
+        // Set state to connecting (if not already set)
+        if (transport->state != RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+            set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING);
+        }
+
+        // Accept and create connection
+        DEBUG_PRINTF("Accepting incoming DTLS connection from %J\n", peer);
+        err = dtls_accept(
+            &transport->connection, transport->context, transport->socket, establish_handler,
+            dtls_receive_handler, close_handler, transport);
+        if (err) {
+            DEBUG_WARNING("Could not accept incoming DTLS connection, reason: %m\n", err);
+        }
+    } else {
+        if (have_connection) {
+            DEBUG_WARNING("Incoming DTLS connect but we already have a connection\n");
+        }
+        if (!role_is_server) {
+            DEBUG_WARNING("Incoming DTLS connect but role is 'client'\n");
+        }
+    }
+}
+
+/*
+ * Initiate a DTLS connect.
+ */
+static enum rawrtc_code do_connect(
+    struct rawrtc_dtls_transport* const transport, const struct sa* const peer) {
+    // Connect
+    DEBUG_PRINTF("Starting DTLS connection to %J\n", peer);
+    return rawrtc_error_to_code(dtls_connect(
+        &transport->connection, transport->context, transport->socket, peer, establish_handler,
+        dtls_receive_handler, close_handler, transport));
+}
+
+/*
+ * Handle outgoing DTLS messages.
+ */
+static int send_handler(
+    struct tls_conn* tc, struct sa const* original_destination, struct mbuf* buffer, void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct trice* const ice = transport->ice_transport->gatherer->ice;
+    bool closed = is_closed(transport);
+    struct ice_candpair* candidate_pair;
+    struct udp_sock* udp_socket;
+    int err;
+    (void) tc;
+    (void) original_destination;
+
+    // Note: No need to check if closed as only non-application data may be sent if the
+    //       transport is already closed.
+
+    // Get candidate pair with highest priority
+    // Note: This ignores whatever is nominated
+    // TODO: Should we rather use the nominated candidate pair?
+    candidate_pair = list_ledata(list_head(trice_validl(ice)));
+    if (!candidate_pair) {
+        if (!closed) {
+            DEBUG_WARNING("Cannot send message, no valid candidate pair\n");
+        }
+        return ECONNRESET;
+    }
+
+    // Get local candidate's UDP socket
+    // TODO: What about TCP?
+    udp_socket = trice_lcand_sock(ice, candidate_pair->lcand);
+    if (!udp_socket) {
+        if (!closed) {
+            DEBUG_WARNING("Cannot send message, selected candidate pair has no socket\n");
+        }
+        return ECONNRESET;
+    }
+
+    // Send
+    // TODO: Is destination correct?
+    DEBUG_PRINTF(
+        "Sending DTLS message (%zu bytes) to %J (originally: %J) from %J\n", mbuf_get_left(buffer),
+        &candidate_pair->rcand->attr.addr, original_destination, &candidate_pair->lcand->attr.addr);
+    err = udp_send(udp_socket, &candidate_pair->rcand->attr.addr, buffer);
+    if (err) {
+        DEBUG_WARNING("Could not send, error: %m\n", err);
+    }
+    return err;
+}
+
+/*
+ * Handle MTU queries.
+ */
+static size_t mtu_handler(struct tls_conn* tc, void* arg) {
+    (void) tc;
+    (void) arg;
+    // TODO: Choose a sane value.
+    return 1400;
+}
+
+/*
+ * Handle received UDP messages.
+ */
+static bool udp_receive_handler(struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct sa* source = context;
+    struct sa const* peer;
+
+    // TODO: This handler should be moved into ICE transport
+    // https://tools.ietf.org/search/rfc7983#section-7
+
+    // Update remote peer address (if changed and connection exists)
+    if (transport->connection) {
+        // TODO: It would be cleaner to check if source is in our list of remote candidates
+
+        // TODO: SCTP - Retest path MTU and reset congestion state to the initial state
+        // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-5
+
+        // Update if changed
+        peer = dtls_peer(transport->connection);
+        if (!sa_cmp(peer, source, SA_ALL)) {
+            DEBUG_PRINTF("Remote changed its peer address from %J to %J\n", peer, source);
+            dtls_set_peer(transport->connection, source);
+        }
+    }
+
+    // Decrypt & receive
+    // Note: No need to check if the transport is already closed as the messages will re-appear in
+    //       the `dtls_receive_handler`.
+    dtls_receive(transport->socket, source, buffer);
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Handle received UDP messages (UDP receive helper).
+ */
+static bool udp_receive_helper(struct sa* source, struct mbuf* buffer, void* arg) {
+    // Receive
+    udp_receive_handler(buffer, source, arg);
+
+    // Handled
+    return true;
+}
+
+/*
+ * Destructor for an existing DTLS transport.
+ */
+static void rawrtc_dtls_transport_destroy(void* arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    struct le* le;
+
+    // Stop transport
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_dtls_transport_stop(transport);
+
+    // TODO: Remove once ICE transport and DTLS transport have been separated properly
+    for (le = list_head(&transport->ice_transport->gatherer->local_candidates); le != NULL;
+         le = le->next) {
+        struct rawrtc_candidate_helper* const candidate_helper = le->data;
+        mem_deref(candidate_helper->udp_helper);
+        // TODO: Be aware that UDP packets go to nowhere now...
+    }
+
+    // Un-reference
+    mem_deref(transport->connection);
+    mem_deref(transport->socket);
+    mem_deref(transport->context);
+    list_flush(&transport->fingerprints);
+    list_flush(&transport->buffered_messages_out);
+    list_flush(&transport->buffered_messages_in);
+    mem_deref(transport->remote_parameters);
+    list_flush(&transport->certificates);
+    mem_deref(transport->ice_transport);
+}
+
+/*
+ * Create a new DTLS transport (internal)
+ */
+enum rawrtc_code rawrtc_dtls_transport_create_internal(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct list* certificates,  // de-referenced, copied (shallow)
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_dtls_transport* transport;
+    enum rawrtc_code error;
+    struct le* le;
+    struct rawrtc_certificate* certificate;
+    uint8_t* certificate_der;
+    size_t certificate_der_length;
+
+    // Check arguments
+    if (!transportp || !ice_transport || !certificates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Check certificates expiration dates
+
+    // Check ICE transport state
+    if (ice_transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Check if another DTLS transport is associated to the ICE transport
+    if (ice_transport->dtls_transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_dtls_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    transport->state = RAWRTC_DTLS_TRANSPORT_STATE_NEW;  // TODO: Raise state (delayed)?
+    transport->ice_transport = mem_ref(ice_transport);
+    transport->certificates = *certificates;
+    transport->state_change_handler = state_change_handler;
+    transport->error_handler = error_handler;
+    transport->arg = arg;
+    transport->role = RAWRTC_DTLS_ROLE_AUTO;
+    transport->connection_established = false;
+    list_init(&transport->buffered_messages_in);
+    list_init(&transport->buffered_messages_out);
+    list_init(&transport->fingerprints);
+
+    // Create (D)TLS context
+    DEBUG_PRINTF("Creating DTLS context\n");
+    error = rawrtc_error_to_code(tls_alloc(&transport->context, TLS_METHOD_DTLS, NULL, NULL));
+    if (error) {
+        goto out;
+    }
+
+    // Get DER encoded certificate of choice
+    // TODO: Which certificate should we use?
+    certificate = list_ledata(list_head(&transport->certificates));
+    error = rawrtc_certificate_get_der(
+        &certificate_der, &certificate_der_length, certificate, RAWRTC_CERTIFICATE_ENCODE_BOTH);
+    if (error) {
+        goto out;
+    }
+
+    // Set certificate
+    DEBUG_PRINTF("Setting certificate on DTLS context\n");
+    error = rawrtc_error_to_code(tls_set_certificate_der(
+        transport->context, rawrtc_certificate_key_type_to_tls_keytype(certificate->key_type),
+        certificate_der, certificate_der_length, NULL, 0));
+    mem_deref(certificate_der);
+    if (error) {
+        goto out;
+    }
+
+    // Set Diffie-Hellman parameters
+    // TODO: Get whether to apply DH parameters from config
+    // TODO: Get DH params from config
+    DEBUG_PRINTF("Setting DH parameters on DTLS context\n");
+    error = rawrtc_set_dh_parameters_der(
+        transport->context, rawrtc_default_dh_parameters, rawrtc_default_dh_parameters_length);
+    if (error) {
+        goto out;
+    }
+
+    // Enable elliptic-curve Diffie-Hellman
+    // TODO: Get whether to enable ECDH from config
+    DEBUG_PRINTF("Enabling ECDH on DTLS context\n");
+    error = rawrtc_enable_ecdh(transport->context);
+    if (error) {
+        goto out;
+    }
+
+    // Set cipher suites
+    // TODO: Get cipher suites from config
+    DEBUG_PRINTF("Setting cipher suites on DTLS context\n");
+    error = rawrtc_error_to_code(tls_set_ciphers(
+        transport->context, rawrtc_default_dtls_cipher_suites,
+        rawrtc_default_dtls_cipher_suites_length));
+    if (error) {
+        goto out;
+    }
+
+    // Send client certificate (client) / request client certificate (server)
+    tls_set_verify_client(transport->context);
+
+    // Create DTLS socket
+    DEBUG_PRINTF("Creating DTLS socket\n");
+    error = rawrtc_error_to_code(dtls_socketless(
+        &transport->socket, 1, connect_handler, send_handler, mtu_handler, transport));
+    if (error) {
+        goto out;
+    }
+
+    // Attach to existing candidate pairs
+    for (le = list_head(trice_validl(ice_transport->gatherer->ice)); le != NULL; le = le->next) {
+        struct ice_candpair* candidate_pair = le->data;
+        error = rawrtc_dtls_transport_add_candidate_pair(transport, candidate_pair);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS transport could not attach to candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+    // Attach to ICE transport
+    // Note: We cannot reference ourselves here as that would introduce a cyclic reference
+    ice_transport->dtls_transport = transport;
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
+
+/*
+ * Create a new DTLS transport.
+ * `*transport` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_dtls_transport_create(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct rawrtc_certificate* const certificates[],  // copied (each item)
+    size_t const n_certificates,
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    struct list certificates_list = LIST_INIT;
+
+    // Append and reference certificates
+    error = rawrtc_certificate_array_to_list(&certificates_list, certificates, n_certificates);
+    if (error) {
+        return error;
+    }
+
+    // Create DTLS transport
+    return rawrtc_dtls_transport_create_internal(
+        transportp, ice_transport, &certificates_list, state_change_handler, error_handler, arg);
+}
+
+/*
+ * Let the DTLS transport attach itself to a candidate pair.
+ * TODO: Separate ICE transport and DTLS transport properly (like data transport)
+ */
+enum rawrtc_code rawrtc_dtls_transport_add_candidate_pair(
+    struct rawrtc_dtls_transport* const transport, struct ice_candpair* const candidate_pair) {
+    enum rawrtc_code error;
+    struct rawrtc_candidate_helper* candidate_helper = NULL;
+
+    // Check arguments
+    if (!transport || !candidate_pair) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Check if already attached
+
+    // Find candidate helper
+    error = rawrtc_candidate_helper_find(
+        &candidate_helper, &transport->ice_transport->gatherer->local_candidates,
+        candidate_pair->lcand);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not find matching candidate helper for candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Receive buffered packets
+    error = rawrtc_message_buffer_clear(
+        &transport->ice_transport->gatherer->buffered_messages, udp_receive_handler, transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not handle buffered packets on candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Attach this transport's receive handler
+    error = rawrtc_candidate_helper_set_receive_handler(
+        candidate_helper, udp_receive_helper, transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not find matching candidate helper for candidate pair, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Do connect (if client and no connection)
+    if (transport->role == RAWRTC_DTLS_ROLE_CLIENT && !transport->connection) {
+        error = do_connect(transport, &candidate_pair->rcand->attr.addr);
+        if (error) {
+            DEBUG_WARNING(
+                "Could not start DTLS connection for candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+out:
+    if (!error) {
+        DEBUG_PRINTF("Attached DTLS transport to candidate pair\n");
+    }
+    return error;
+}
+
+/*
+ * Start the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_start(
+    struct rawrtc_dtls_transport* const transport,
+    struct rawrtc_dtls_parameters* const remote_parameters  // referenced
+) {
+    enum rawrtc_code error;
+    enum rawrtc_ice_role ice_role;
+
+    // Check arguments
+    if (!transport || !remote_parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate parameters
+    if (remote_parameters->fingerprints->n_fingerprints < 1) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    // Note: Checking for 'remote_parameters' ensures that 'start' is not called twice
+    if (transport->remote_parameters || is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set state to connecting (if not already set)
+    if (transport->state != RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+        set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING);
+    }
+
+    // Get ICE role
+    error = rawrtc_ice_transport_get_role(&ice_role, transport->ice_transport);
+    if (error) {
+        return error;
+    }
+
+    // Determine role
+    if (remote_parameters->role == RAWRTC_DTLS_ROLE_AUTO) {
+        switch (ice_role) {
+            case RAWRTC_ICE_ROLE_CONTROLLED:
+                transport->role = RAWRTC_DTLS_ROLE_CLIENT;
+                DEBUG_PRINTF("Switching role 'auto' -> 'client'\n");
+                break;
+            case RAWRTC_ICE_ROLE_CONTROLLING:
+                transport->role = RAWRTC_DTLS_ROLE_SERVER;
+                DEBUG_PRINTF("Switching role 'auto' -> 'server'\n");
+                break;
+            default:
+                // Cannot continue if ICE transport role is unknown
+                DEBUG_WARNING("ICE role must be set before DTLS transport can be started!\n");
+                return RAWRTC_CODE_INVALID_STATE;
+        }
+    } else if (remote_parameters->role == RAWRTC_DTLS_ROLE_SERVER) {
+        transport->role = RAWRTC_DTLS_ROLE_CLIENT;
+        DEBUG_PRINTF("Switching role 'server' -> 'client'\n");
+    } else {
+        transport->role = RAWRTC_DTLS_ROLE_SERVER;
+        DEBUG_PRINTF("Switching role 'client' -> 'server'\n");
+    }
+
+    // Connect (if client)
+    if (transport->role == RAWRTC_DTLS_ROLE_CLIENT) {
+        struct ice_candpair* candidate_pair;
+
+        // Reset existing connections
+        if (transport->connection) {
+            // Note: This is needed as ORTC requires us to reset previous DTLS connections
+            //       if the remote role is 'server'
+            DEBUG_PRINTF("Resetting DTLS connection\n");
+            transport->connection = mem_deref(transport->connection);
+            transport->connection_established = false;
+        }
+
+        // Get selected candidate pair
+        candidate_pair =
+            list_ledata(list_head(trice_validl(transport->ice_transport->gatherer->ice)));
+
+        // Do connect (if we have a valid candidate pair)
+        if (candidate_pair) {
+            error = do_connect(transport, &candidate_pair->rcand->attr.addr);
+            if (error) {
+                goto out;
+            }
+        }
+    } else {
+        // Verify certificate & fingerprint (if connection is established)
+        if (transport->connection_established) {
+            verify_certificate(transport);
+        }
+    }
+
+out:
+    if (error) {
+        transport->connection = mem_deref(transport->connection);
+    } else {
+        // Set remote parameters
+        transport->remote_parameters = mem_ref(remote_parameters);
+    }
+    return error;
+}
+
+/*
+ * Pipe buffered messages into the data receive handler that has a
+ * different signature.
+ */
+static bool intermediate_receive_handler(
+    struct mbuf* const buffer, void* const context, void* const arg) {
+    struct rawrtc_dtls_transport* const transport = arg;
+    (void) context;
+
+    // Pipe into the actual receive handler
+    if (transport->receive_handler) {
+        transport->receive_handler(buffer, transport->receive_handler_arg);
+    } else {
+        DEBUG_WARNING("No receive handler, discarded %zu bytes\n", mbuf_get_left(buffer));
+    }
+
+    // Continue iterating through message queue
+    return true;
+}
+
+/*
+ * Set a data transport on the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_set_data_transport(
+    struct rawrtc_dtls_transport* const transport,
+    rawrtc_dtls_transport_receive_handler const receive_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+
+    // Check arguments
+    if (!transport || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check for existing data transport
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, transport);
+    if (error) {
+        return error;
+    }
+    if (have_data_transport) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set handler
+    transport->receive_handler = receive_handler;
+    transport->receive_handler_arg = arg;
+
+    // Receive buffered messages
+    error = rawrtc_message_buffer_clear(
+        &transport->buffered_messages_in, intermediate_receive_handler, transport);
+    if (error) {
+        return error;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Remove an existing data transport from the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_clear_data_transport(
+    struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Clear buffered messages (?)
+
+    // Clear handler and argument
+    transport->receive_handler = NULL;
+    transport->receive_handler_arg = NULL;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Send a data message over the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_send(
+    struct rawrtc_dtls_transport* const transport, struct mbuf* const buffer) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !buffer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Connected?
+    if (transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        return rawrtc_error_to_code(dtls_send(transport->connection, buffer));
+    }
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&transport->buffered_messages_out, buffer, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not buffer outgoing packet, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Buffered message
+    DEBUG_PRINTF("Buffered outgoing packet of size %zu\n", mbuf_get_left(buffer));
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Stop and close the DTLS transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_stop(struct rawrtc_dtls_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update state
+    set_state(transport, RAWRTC_DTLS_TRANSPORT_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get local DTLS parameters of a transport.
+ */
+enum rawrtc_code rawrtc_dtls_transport_get_local_parameters(
+    struct rawrtc_dtls_parameters** const parametersp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport) {
+    // TODO: Get config from struct
+    enum rawrtc_certificate_sign_algorithm const algorithm = rawrtc_default_config.sign_algorithm;
+    struct le* le;
+    struct rawrtc_dtls_fingerprint* fingerprint;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!parametersp || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (is_closed(transport)) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Lazy-create fingerprints
+    if (list_isempty(&transport->fingerprints)) {
+        for (le = list_head(&transport->certificates); le != NULL; le = le->next) {
+            struct rawrtc_certificate* certificate = le->data;
+
+            // Create fingerprint
+            error = rawrtc_dtls_fingerprint_create_empty(&fingerprint, algorithm);
+            if (error) {
+                return error;
+            }
+
+            // Get and set fingerprint of certificate
+            error = rawrtc_certificate_get_fingerprint(&fingerprint->value, certificate, algorithm);
+            if (error) {
+                return error;
+            }
+
+            // Append fingerprint
+            list_append(&transport->fingerprints, &fingerprint->le, fingerprint);
+        }
+    }
+
+    // Create and return DTLS parameters instance
+    return rawrtc_dtls_parameters_create_internal(
+        parametersp, transport->role, &transport->fingerprints);
+}
diff --git a/src/dtls_transport/transport.h b/src/dtls_transport/transport.h
new file mode 100644
index 0000000..a954d0a
--- /dev/null
+++ b/src/dtls_transport/transport.h
@@ -0,0 +1,68 @@
+#pragma once
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Handle inbound application data.
+ */
+typedef void (*rawrtc_dtls_transport_receive_handler)(struct mbuf* const buffer, void* const arg);
+
+struct rawrtc_dtls_transport {
+    enum rawrtc_dtls_transport_state state;
+    struct rawrtc_ice_transport* ice_transport;  // referenced
+    struct list certificates;  // deep-copied
+    rawrtc_dtls_transport_state_change_handler state_change_handler;  // nullable
+    rawrtc_dtls_transport_error_handler error_handler;  // nullable
+    void* arg;  // nullable
+    struct rawrtc_dtls_parameters* remote_parameters;  // referenced
+    enum rawrtc_dtls_role role;
+    bool connection_established;
+    struct list buffered_messages_in;
+    struct list buffered_messages_out;
+    struct list fingerprints;
+    struct tls* context;
+    struct dtls_sock* socket;
+    struct tls_conn* connection;
+    rawrtc_dtls_transport_receive_handler receive_handler;
+    void* receive_handler_arg;
+};
+
+enum rawrtc_code rawrtc_dtls_transport_create_internal(
+    struct rawrtc_dtls_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_transport* const ice_transport,  // referenced
+    struct list* certificates,  // de-referenced, copied (shallow)
+    rawrtc_dtls_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_dtls_transport_error_handler const error_handler,  // nullable
+    void* const arg  // nullable
+);
+
+enum rawrtc_code rawrtc_dtls_transport_add_candidate_pair(
+    struct rawrtc_dtls_transport* const transport, struct ice_candpair* const candidate_pair);
+
+enum rawrtc_code rawrtc_dtls_transport_have_data_transport(
+    bool* const have_data_transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_set_data_transport(
+    struct rawrtc_dtls_transport* const transport,
+    rawrtc_dtls_transport_receive_handler const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_dtls_transport_clear_data_transport(
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_send(
+    struct rawrtc_dtls_transport* const transport, struct mbuf* const buffer);
+
+enum rawrtc_code rawrtc_dtls_transport_get_external_role(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
+
+enum rawrtc_code rawrtc_dtls_transport_get_external_state(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced
+    struct rawrtc_dtls_transport* const transport);
diff --git a/src/dtls_transport/utils.c b/src/dtls_transport/utils.c
new file mode 100644
index 0000000..7b96fc5
--- /dev/null
+++ b/src/dtls_transport/utils.c
@@ -0,0 +1,75 @@
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_dtls_transport_state_to_name(enum rawrtc_dtls_transport_state const state) {
+    switch (state) {
+        case RAWRTC_DTLS_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_DTLS_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        case RAWRTC_DTLS_TRANSPORT_STATE_FAILED:
+            return "failed";
+        default:
+            return "???";
+    }
+}
+
+static enum rawrtc_dtls_role const map_enum_dtls_role[] = {
+    RAWRTC_DTLS_ROLE_AUTO,
+    RAWRTC_DTLS_ROLE_CLIENT,
+    RAWRTC_DTLS_ROLE_SERVER,
+};
+
+static char const* const map_str_dtls_role[] = {
+    "auto",
+    "client",
+    "server",
+};
+
+static size_t const map_dtls_role_length = ARRAY_SIZE(map_enum_dtls_role);
+
+/*
+ * Translate a DTLS role to str.
+ */
+char const* rawrtc_dtls_role_to_str(enum rawrtc_dtls_role const role) {
+    size_t i;
+
+    for (i = 0; i < map_dtls_role_length; ++i) {
+        if (map_enum_dtls_role[i] == role) {
+            return map_str_dtls_role[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to a DTLS role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_dtls_role(
+    enum rawrtc_dtls_role* const rolep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!rolep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_dtls_role_length; ++i) {
+        if (str_casecmp(map_str_dtls_role[i], str) == 0) {
+            *rolep = map_enum_dtls_role[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
diff --git a/src/ice_candidate/attributes.c b/src/ice_candidate/attributes.c
new file mode 100644
index 0000000..822c770
--- /dev/null
+++ b/src/ice_candidate/attributes.c
@@ -0,0 +1,296 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the ICE candidate's foundation.
+ * `*foundationp` will be set to a copy of the foundation that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_foundation(
+    char** const foundationp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !foundationp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied foundation
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            return rawrtc_strdup(foundationp, candidate->candidate.raw_candidate->foundation);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_strdup(
+                foundationp, candidate->candidate.local_candidate->attr.foundation);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_strdup(
+                foundationp, candidate->candidate.remote_candidate->attr.foundation);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's priority.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_priority(
+    uint32_t* const priorityp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !priorityp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set priority
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *priorityp = candidate->candidate.raw_candidate->priority;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            *priorityp = candidate->candidate.local_candidate->attr.prio;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            *priorityp = candidate->candidate.remote_candidate->attr.prio;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's IP address.
+ * `*ipp` will be set to a copy of the IP address that must be
+ * unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_ip(
+    char** const ipp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !ipp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied IP address
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            return rawrtc_strdup(ipp, candidate->candidate.raw_candidate->ip);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_sdprintf(ipp, "%j", &candidate->candidate.local_candidate->attr.addr);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_sdprintf(ipp, "%j", &candidate->candidate.remote_candidate->attr.addr);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's protocol.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    int ipproto;
+
+    // Check arguments
+    if (!candidate || !protocolp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set protocol
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *protocolp = candidate->candidate.raw_candidate->protocol;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            ipproto = candidate->candidate.local_candidate->attr.proto;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            ipproto = candidate->candidate.remote_candidate->attr.proto;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+    return rawrtc_ipproto_to_ice_protocol(protocolp, ipproto);
+}
+
+/*
+ * Get the ICE candidate's port.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_port(
+    uint16_t* const portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !portp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *portp = candidate->candidate.raw_candidate->port;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            *portp = sa_port(&candidate->candidate.local_candidate->attr.addr);
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            *portp = sa_port(&candidate->candidate.remote_candidate->attr.addr);
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's type.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_type(
+    enum rawrtc_ice_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    // Check arguments
+    if (!candidate || !typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set type
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *typep = candidate->candidate.raw_candidate->type;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            return rawrtc_ice_cand_type_to_ice_candidate_type(
+                typep, candidate->candidate.local_candidate->attr.type);
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            return rawrtc_ice_cand_type_to_ice_candidate_type(
+                typep, candidate->candidate.remote_candidate->attr.type);
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+}
+
+/*
+ * Get the ICE candidate's TCP type.
+ * Return `RAWRTC_CODE_NO_VALUE` in case the protocol is not TCP.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_tcp_type(
+    enum rawrtc_ice_tcp_candidate_type* typep,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate;
+
+    // Check arguments
+    if (!candidate || !typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set type/get re candidate
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            *typep = candidate->candidate.raw_candidate->tcp_type;
+            return RAWRTC_CODE_SUCCESS;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set type from re candidate if TCP
+    if (re_candidate->proto == IPPROTO_TCP) {
+        return rawrtc_ice_tcptype_to_ice_tcp_candidate_type(typep, re_candidate->tcptype);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the ICE candidate's related IP address.
+ * `*related_address` will be set to a copy of the related address that
+ * must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related address exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_address(
+    char** const related_addressp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate = NULL;
+
+    // Check arguments
+    if (!candidate || !related_addressp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set copied related IP address/get re candidate
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            if (candidate->candidate.raw_candidate->related_address) {
+                return rawrtc_strdup(
+                    related_addressp, candidate->candidate.raw_candidate->related_address);
+            }
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set copied related IP address from re candidate
+    if (re_candidate && sa_isset(&re_candidate->rel_addr, SA_ADDR)) {
+        return rawrtc_sdprintf(
+            related_addressp, "%j", &candidate->candidate.local_candidate->attr.rel_addr);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the ICE candidate's related IP address' port.
+ * `*related_portp` will be set to a copy of the related address'
+ * port.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no related port exists.
+ */
+enum rawrtc_code rawrtc_ice_candidate_get_related_port(
+    uint16_t* const related_portp,  // de-referenced
+    struct rawrtc_ice_candidate* const candidate) {
+    struct ice_cand_attr* re_candidate = NULL;
+
+    // Check arguments
+    if (!candidate || !related_portp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set port
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            if (candidate->candidate.raw_candidate->related_address) {
+                *related_portp = candidate->candidate.raw_candidate->related_port;
+                return RAWRTC_CODE_SUCCESS;
+            }
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            re_candidate = &candidate->candidate.local_candidate->attr;
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            re_candidate = &candidate->candidate.remote_candidate->attr;
+            break;
+        default:
+            return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Set copied related IP address' port from re candidate
+    if (re_candidate && sa_isset(&re_candidate->rel_addr, SA_PORT)) {
+        *related_portp = sa_port(&candidate->candidate.local_candidate->attr.rel_addr);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/ice_candidate/candidate.c b/src/ice_candidate/candidate.c
new file mode 100644
index 0000000..b5db1b0
--- /dev/null
+++ b/src/ice_candidate/candidate.c
@@ -0,0 +1,274 @@
+#include "candidate.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+#define DEBUG_MODULE "ice-candidate"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Calculate the ICE candidate priority.
+ *
+ * We prefer:
+ *
+ * 1. UDP over TCP, then
+ * 2. IPv6 over IPv4, then
+ * 3. Older candidates over newer candidates.
+ *
+ * TODO: We should follow ICE Dual-Stack Recommendations.
+ *       See: https://tools.ietf.org/html/rfc8421#section-4
+ */
+uint32_t rawrtc_ice_candidate_calculate_priority(
+    uint32_t const n_candidates,
+    enum ice_cand_type const candidate_type,
+    int const protocol,
+    int const address_family,
+    enum ice_tcptype const tcp_type) {
+    uint16_t const age =
+        n_candidates > (1 << 13) ? (uint16_t) 0 : (uint16_t)((1 << 13) - n_candidates);
+    uint16_t const is_udp = protocol == IPPROTO_UDP ? (uint16_t) 1 : (uint16_t) 0;
+    uint16_t const is_ipv6 = address_family == AF_INET6 ? (uint16_t) 1 : (uint16_t) 0;
+    (void) tcp_type;
+    // TODO: Set correct component ID
+    return ice_cand_calc_prio(candidate_type, age | is_ipv6 << 14 | is_udp << 15, 1);
+}
+
+/*
+ * Destructor for an existing ICE candidate.
+ */
+static void rawrtc_ice_candidate_raw_destroy(void* arg) {
+    struct rawrtc_ice_candidate_raw* const candidate = arg;
+
+    // Un-reference
+    mem_deref(candidate->related_address);
+    mem_deref(candidate->ip);
+    mem_deref(candidate->foundation);
+}
+
+/*
+ * Create a raw ICE candidate (pending candidate).
+ */
+static enum rawrtc_code rawrtc_ice_candidate_raw_create(
+    struct rawrtc_ice_candidate_raw** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct rawrtc_ice_candidate_raw* candidate;
+    enum rawrtc_code error;
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_raw_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    error = rawrtc_error_to_code(pl_strdup(&candidate->foundation, foundation));
+    if (error) {
+        goto out;
+    }
+    candidate->priority = priority;
+    error = rawrtc_error_to_code(pl_strdup(&candidate->ip, ip));
+    if (error) {
+        goto out;
+    }
+    candidate->protocol = protocol;
+    candidate->port = port;
+    candidate->type = type;
+    candidate->tcp_type = tcp_type;
+    if (pl_isset(related_address)) {
+        error = rawrtc_error_to_code(pl_strdup(&candidate->related_address, related_address));
+        if (error) {
+            goto out;
+        }
+    }
+    candidate->related_port = related_port;
+
+out:
+    if (error) {
+        mem_deref(candidate);
+    } else {
+        // Set pointer
+        *candidatep = candidate;
+        DEBUG_PRINTF("Created candidate (raw): %r\n", ip);
+    }
+    return error;
+}
+
+/*
+ * Destructor for an existing ICE candidate.
+ */
+static void rawrtc_ice_candidate_destroy(void* arg) {
+    struct rawrtc_ice_candidate* const candidate = arg;
+
+    // Un-reference
+    switch (candidate->storage_type) {
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RAW:
+            mem_deref(candidate->candidate.raw_candidate);
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_LCAND:
+            mem_deref(candidate->candidate.local_candidate);
+            break;
+        case RAWRTC_ICE_CANDIDATE_STORAGE_RCAND:
+            mem_deref(candidate->candidate.remote_candidate);
+            break;
+    }
+}
+
+/*
+ * Create an ICE candidate (pl variant).
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_internal(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct rawrtc_ice_candidate* candidate;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!candidatep || !pl_isset(foundation) || !pl_isset(ip)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_RAW;
+
+    // Create raw candidate
+    error = rawrtc_ice_candidate_raw_create(
+        &candidate->candidate.raw_candidate, foundation, priority, ip, protocol, port, type,
+        tcp_type, related_address, related_port);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(candidate);
+    } else {
+        // Set pointer
+        *candidatep = candidate;
+    }
+    return error;
+}
+
+/*
+ * Create an ICE candidate.
+ * `*candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    char* const foundation,  // copied
+    uint32_t const priority,
+    char* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    char* const related_address,  // copied, nullable
+    uint16_t const related_port) {
+    struct pl foundation_pl;
+    struct pl ip_pl;
+    struct pl related_address_pl = PL_INIT;
+
+    // Check arguments
+    if (!foundation || !ip) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&foundation_pl, foundation);
+    pl_set_str(&ip_pl, ip);
+    if (related_address) {
+        pl_set_str(&related_address_pl, related_address);
+    }
+
+    // Create ICE candidate
+    return rawrtc_ice_candidate_create_internal(
+        candidatep, &foundation_pl, priority, &ip_pl, protocol, port, type, tcp_type,
+        &related_address_pl, related_port);
+}
+
+/*
+ * Create an ICE candidate instance from an existing local candidate.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_from_local_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_lcand* const local_candidate  // referenced
+) {
+    struct rawrtc_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !local_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type and reference local candidate
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_LCAND;
+    candidate->candidate.local_candidate = mem_ref(local_candidate);
+
+    // Set pointer
+    *candidatep = candidate;
+    DEBUG_PRINTF(
+        "Created candidate (lcand): %J\n", &candidate->candidate.local_candidate->attr.addr);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create an ICE candidate instance from an existing remote candidate.
+ */
+enum rawrtc_code rawrtc_ice_candidate_create_from_remote_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_rcand* const remote_candidate  // referenced
+) {
+    struct rawrtc_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !remote_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set storage type and reference remote candidate
+    candidate->storage_type = RAWRTC_ICE_CANDIDATE_STORAGE_RCAND;
+    candidate->candidate.remote_candidate = mem_ref(remote_candidate);
+
+    // Set pointer
+    *candidatep = candidate;
+    DEBUG_PRINTF(
+        "Created candidate (rcand): %j\n", &candidate->candidate.remote_candidate->attr.addr);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_candidate/candidate.h b/src/ice_candidate/candidate.h
new file mode 100644
index 0000000..3dec216
--- /dev/null
+++ b/src/ice_candidate/candidate.h
@@ -0,0 +1,86 @@
+#pragma once
+#include "../ice_candidate/candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * ICE candidate storage type (internal).
+ */
+enum rawrtc_ice_candidate_storage {
+    RAWRTC_ICE_CANDIDATE_STORAGE_RAW,
+    RAWRTC_ICE_CANDIDATE_STORAGE_LCAND,
+    RAWRTC_ICE_CANDIDATE_STORAGE_RCAND,
+};
+
+/*
+ * Raw ICE candidate (pending candidate).
+ */
+struct rawrtc_ice_candidate_raw {
+    char* foundation;  // copied
+    uint32_t priority;
+    char* ip;  // copied
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address;  // copied, nullable
+    uint16_t related_port;
+};
+
+struct rawrtc_ice_candidate {
+    enum rawrtc_ice_candidate_storage storage_type;
+    union {
+        struct rawrtc_ice_candidate_raw* raw_candidate;
+        struct ice_lcand* local_candidate;
+        struct ice_rcand* remote_candidate;
+    } candidate;
+};
+
+// Note: Cannot be public until it uses fixed size types in signature (stdint)
+uint32_t rawrtc_ice_candidate_calculate_priority(
+    uint32_t const n_candidates,
+    enum ice_cand_type const candidate_type,
+    int const protocol,
+    int const address_family,
+    enum ice_tcptype const tcp_type);
+
+enum rawrtc_code rawrtc_ice_candidate_create_internal(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const foundation,  // copied
+    uint32_t const priority,
+    struct pl* const ip,  // copied
+    enum rawrtc_ice_protocol const protocol,
+    uint16_t const port,
+    enum rawrtc_ice_candidate_type const type,
+    enum rawrtc_ice_tcp_candidate_type const tcp_type,
+    struct pl* const related_address,  // copied, nullable
+    uint16_t const related_port);
+
+enum rawrtc_code rawrtc_ice_candidate_create_from_local_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_lcand* const local_candidate  // referenced
+);
+
+enum rawrtc_code rawrtc_ice_candidate_create_from_remote_candidate(
+    struct rawrtc_ice_candidate** const candidatep,  // de-referenced
+    struct ice_rcand* const remote_candidate  // referenced
+);
+
+int rawrtc_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_ice_candidate* const candidate);
+
+enum ice_cand_type rawrtc_ice_candidate_type_to_ice_cand_type(
+    enum rawrtc_ice_candidate_type const type);
+
+enum rawrtc_code rawrtc_ice_cand_type_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    const enum ice_cand_type re_type);
+
+enum ice_tcptype rawrtc_ice_tcp_candidate_type_to_ice_tcptype(
+    const enum rawrtc_ice_tcp_candidate_type type);
+
+enum rawrtc_code rawrtc_ice_tcptype_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    const enum ice_tcptype re_type);
diff --git a/src/ice_candidate/helper.c b/src/ice_candidate/helper.c
new file mode 100644
index 0000000..ff74c71
--- /dev/null
+++ b/src/ice_candidate/helper.c
@@ -0,0 +1,209 @@
+#include "helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_server/server.h"
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Destructor for an existing candidate helper.
+ */
+static void rawrtc_candidate_helper_destroy(void* arg) {
+    struct rawrtc_candidate_helper* const local_candidate = arg;
+
+    // Un-reference
+    list_flush(&local_candidate->stun_sessions);
+    mem_deref(local_candidate->udp_helper);
+    mem_deref(local_candidate->candidate);
+    mem_deref(local_candidate->gatherer);
+}
+
+/*
+ * Create a candidate helper.
+ */
+enum rawrtc_code rawrtc_candidate_helper_create(
+    struct rawrtc_candidate_helper** const candidate_helperp,  // de-referenced
+    struct rawrtc_ice_gatherer* gatherer,
+    struct ice_lcand* const candidate,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg) {
+    struct rawrtc_candidate_helper* candidate_helper;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!candidate_helperp || !gatherer || !candidate || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create candidate helper
+    candidate_helper = mem_zalloc(sizeof(*candidate_helper), rawrtc_candidate_helper_destroy);
+    if (!candidate_helper) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    candidate_helper->gatherer = mem_ref(gatherer);
+    candidate_helper->candidate = mem_ref(candidate);
+    candidate_helper->srflx_pending_count = 0;
+    candidate_helper->relay_pending_count = 0;
+
+    // Set receive handler
+    error = rawrtc_candidate_helper_set_receive_handler(candidate_helper, receive_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(candidate_helper);
+    } else {
+        // Set pointer
+        *candidate_helperp = candidate_helper;
+    }
+    return error;
+}
+
+/*
+ * Set a candidate helper's receive handler.
+ */
+enum rawrtc_code rawrtc_candidate_helper_set_receive_handler(
+    struct rawrtc_candidate_helper* const candidate_helper,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    struct udp_helper* udp_helper;
+    struct udp_sock* udp_socket;
+
+    // Check arguments
+    if (!candidate_helper || !receive_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate's UDP socket
+    udp_socket = trice_lcand_sock(candidate_helper->gatherer->ice, candidate_helper->candidate);
+    if (!udp_socket) {
+        return RAWRTC_CODE_NO_SOCKET;
+    }
+
+    // Create UDP helper
+    error = rawrtc_error_to_code(udp_register_helper(
+        &udp_helper, udp_socket, RAWRTC_LAYER_DTLS_SRTP_STUN, NULL, receive_handler, arg));
+    if (error) {
+        return error;
+    }
+
+    // Unset current helper (if any) and set new helper
+    mem_deref(candidate_helper->udp_helper);
+    candidate_helper->udp_helper = udp_helper;
+
+    // TODO: What about TCP helpers?
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Find a specific candidate helper by re candidate.
+ */
+enum rawrtc_code rawrtc_candidate_helper_find(
+    struct rawrtc_candidate_helper** const candidate_helperp,
+    struct list* const candidate_helpers,
+    struct ice_lcand* re_candidate) {
+    struct le* le;
+
+    // Check arguments
+    if (!candidate_helperp || !candidate_helpers || !re_candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Lookup candidate helper
+    for (le = list_head(candidate_helpers); le != NULL; le = le->next) {
+        struct rawrtc_candidate_helper* const candidate_helper = le->data;
+        if (candidate_helper->candidate->us == re_candidate->us) {
+            // Found
+            *candidate_helperp = candidate_helper;
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+static void rawrtc_candidate_helper_stun_session_destroy(void* arg) {
+    struct rawrtc_candidate_helper_stun_session* const session = arg;
+
+    // Remove from list
+    list_unlink(&session->le);
+
+    // Un-reference
+    mem_deref(session->url);
+    mem_deref(session->stun_keepalive);
+    mem_deref(session->candidate_helper);
+}
+
+/*
+ * Create a STUN session.
+ */
+enum rawrtc_code rawrtc_candidate_helper_stun_session_create(
+    struct rawrtc_candidate_helper_stun_session** const sessionp,  // de-referenced
+    struct rawrtc_ice_server_url* const url) {
+    struct rawrtc_candidate_helper_stun_session* session;
+
+    // Check arguments
+    if (!sessionp || !url) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    session = mem_zalloc(sizeof(*session), rawrtc_candidate_helper_stun_session_destroy);
+    if (!session) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    session->url = mem_ref(url);
+    session->pending = true;
+
+    // Set pointer & done
+    *sessionp = session;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add a STUN session to a candidate helper.
+ */
+enum rawrtc_code rawrtc_candidate_helper_stun_session_add(
+    struct rawrtc_candidate_helper_stun_session* const session,
+    struct rawrtc_candidate_helper* const candidate_helper,
+    struct stun_keepalive* const stun_keepalive) {
+    // Check arguments
+    if (!session || !candidate_helper || !stun_keepalive) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set fields/reference
+    session->candidate_helper = mem_ref(candidate_helper);
+    session->stun_keepalive = mem_ref(stun_keepalive);
+
+    // Append to STUN sessions
+    list_append(&candidate_helper->stun_sessions, &session->le, session);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Remove STUN sessions list handler (for candidate helper lists).
+ */
+bool rawrtc_candidate_helper_remove_stun_sessions_handler(struct le* le, void* arg) {
+    struct rawrtc_candidate_helper* const candidate_helper = le->data;
+    (void) arg;
+
+    // Flush STUN sessions
+    list_flush(&candidate_helper->stun_sessions);
+
+    return false;  // continue traversing
+}
diff --git a/src/ice_candidate/helper.h b/src/ice_candidate/helper.h
new file mode 100644
index 0000000..9446574
--- /dev/null
+++ b/src/ice_candidate/helper.h
@@ -0,0 +1,58 @@
+#pragma once
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Local candidate helper.
+ */
+struct rawrtc_candidate_helper {
+    struct le le;
+    struct rawrtc_ice_gatherer* gatherer;
+    struct ice_lcand* candidate;
+    struct udp_helper* udp_helper;
+    uint_fast8_t srflx_pending_count;
+    struct list stun_sessions;
+    uint_fast8_t relay_pending_count;
+};
+
+/*
+ * STUN keep-alive session.
+ */
+struct rawrtc_candidate_helper_stun_session {
+    struct le le;
+    struct rawrtc_candidate_helper* candidate_helper;
+    struct stun_keepalive* stun_keepalive;
+    struct rawrtc_ice_server_url* url;
+    bool pending;
+};
+
+enum rawrtc_code rawrtc_candidate_helper_create(
+    struct rawrtc_candidate_helper** const candidate_helperp,  // de-referenced
+    struct rawrtc_ice_gatherer* gatherer,
+    struct ice_lcand* const candidate,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_candidate_helper_set_receive_handler(
+    struct rawrtc_candidate_helper* const candidate_helper,
+    udp_helper_recv_h* const receive_handler,
+    void* const arg);
+
+enum rawrtc_code rawrtc_candidate_helper_find(
+    struct rawrtc_candidate_helper** const candidate_helperp,
+    struct list* const candidate_helpers,
+    struct ice_lcand* re_candidate);
+
+enum rawrtc_code rawrtc_candidate_helper_stun_session_create(
+    struct rawrtc_candidate_helper_stun_session** const sessionp,  // de-referenced
+    struct rawrtc_ice_server_url* const url);
+
+enum rawrtc_code rawrtc_candidate_helper_stun_session_add(
+    struct rawrtc_candidate_helper_stun_session* const session,
+    struct rawrtc_candidate_helper* const candidate_helper,
+    struct stun_keepalive* const stun_keepalive);
+
+bool rawrtc_candidate_helper_remove_stun_sessions_handler(struct le* le, void* arg);
diff --git a/src/ice_candidate/meson.build b/src/ice_candidate/meson.build
new file mode 100644
index 0000000..abd4455
--- /dev/null
+++ b/src/ice_candidate/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'attributes.c',
+    'candidate.c',
+    'helper.c',
+    'utils.c',
+])
diff --git a/src/ice_candidate/utils.c b/src/ice_candidate/utils.c
new file mode 100644
index 0000000..a6bee78
--- /dev/null
+++ b/src/ice_candidate/utils.c
@@ -0,0 +1,476 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <netinet/in.h>  // IPPROTO_UDP, IPPROTO_TCP
+
+/*
+ * Translate an ICE candidate type to the corresponding re type.
+ */
+enum ice_cand_type rawrtc_ice_candidate_type_to_ice_cand_type(
+    enum rawrtc_ice_candidate_type const type) {
+    // No conversion needed
+    return (enum ice_cand_type) type;
+}
+
+/*
+ * Translate a re ICE candidate type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_ice_cand_type_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    enum ice_cand_type const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case ICE_CAND_TYPE_HOST:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_HOST;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_SRFLX:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_SRFLX;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_PRFLX:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_PRFLX;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_CAND_TYPE_RELAY:
+            *typep = RAWRTC_ICE_CANDIDATE_TYPE_RELAY;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate an ICE TCP candidate type to the corresponding re type.
+ */
+enum ice_tcptype rawrtc_ice_tcp_candidate_type_to_ice_tcptype(
+    enum rawrtc_ice_tcp_candidate_type const type) {
+    // No conversion needed
+    return (enum ice_tcptype) type;
+}
+
+/*
+ * Translate a re ICE TCP candidate type to the corresponding rawrtc type.
+ */
+enum rawrtc_code rawrtc_ice_tcptype_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    enum ice_tcptype const re_type) {
+    // Check arguments
+    if (!typep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert ice_cand_type
+    switch (re_type) {
+        case ICE_TCP_ACTIVE:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_TCP_PASSIVE:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_TCP_SO:
+            *typep = RAWRTC_ICE_TCP_CANDIDATE_TYPE_SO;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+/*
+ * Translate a protocol to the corresponding IPPROTO_*.
+ */
+int rawrtc_ice_protocol_to_ipproto(enum rawrtc_ice_protocol const protocol) {
+    // No conversion needed
+    return (int) protocol;
+}
+
+/*
+ * Translate a IPPROTO_* to the corresponding protocol.
+ */
+enum rawrtc_code rawrtc_ipproto_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    int const ipproto) {
+    // Check arguments
+    if (!protocolp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert IPPROTO_*
+    switch (ipproto) {
+        case IPPROTO_UDP:
+            *protocolp = RAWRTC_ICE_PROTOCOL_UDP;
+            return RAWRTC_CODE_SUCCESS;
+        case IPPROTO_TCP:
+            *protocolp = RAWRTC_ICE_PROTOCOL_TCP;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
+
+static enum rawrtc_ice_protocol const map_enum_ice_protocol[] = {
+    RAWRTC_ICE_PROTOCOL_UDP,
+    RAWRTC_ICE_PROTOCOL_TCP,
+};
+
+static char const* const map_str_ice_protocol[] = {
+    "udp",
+    "tcp",
+};
+
+static size_t const map_ice_protocol_length = ARRAY_SIZE(map_enum_ice_protocol);
+
+/*
+ * Translate an ICE protocol to str.
+ */
+char const* rawrtc_ice_protocol_to_str(enum rawrtc_ice_protocol const protocol) {
+    size_t i;
+
+    for (i = 0; i < map_ice_protocol_length; ++i) {
+        if (map_enum_ice_protocol[i] == protocol) {
+            return map_str_ice_protocol[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a pl to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!protocolp || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_protocol_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_protocol[i]) == 0) {
+            *protocolp = map_enum_ice_protocol[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE protocol (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_protocol(
+    enum rawrtc_ice_protocol* const protocolp,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_protocol(protocolp, &pl);
+}
+
+static enum rawrtc_ice_candidate_type const map_enum_ice_candidate_type[] = {
+    RAWRTC_ICE_CANDIDATE_TYPE_HOST,
+    RAWRTC_ICE_CANDIDATE_TYPE_SRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_PRFLX,
+    RAWRTC_ICE_CANDIDATE_TYPE_RELAY,
+};
+
+static char const* const map_str_ice_candidate_type[] = {
+    "host",
+    "srflx",
+    "prflx",
+    "relay",
+};
+
+static size_t const map_ice_candidate_type_length = ARRAY_SIZE(map_enum_ice_candidate_type);
+
+/*
+ * Translate an ICE candidate type to str.
+ */
+char const* rawrtc_ice_candidate_type_to_str(enum rawrtc_ice_candidate_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_candidate_type_length; ++i) {
+        if (map_enum_ice_candidate_type[i] == type) {
+            return map_str_ice_candidate_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a pl to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_candidate_type_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_candidate_type[i]) == 0) {
+            *typep = map_enum_ice_candidate_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_candidate_type(
+    enum rawrtc_ice_candidate_type* const typep,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_candidate_type(typep, &pl);
+}
+
+static enum rawrtc_ice_tcp_candidate_type const map_enum_ice_tcp_candidate_type[] = {
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_PASSIVE,
+    RAWRTC_ICE_TCP_CANDIDATE_TYPE_SO,
+};
+
+static char const* const map_str_ice_tcp_candidate_type[] = {
+    "active",
+    "passive",
+    "so",
+};
+
+static size_t const map_ice_tcp_candidate_type_length = ARRAY_SIZE(map_enum_ice_tcp_candidate_type);
+
+/*
+ * Translate an ICE TCP candidate type to str.
+ */
+char const* rawrtc_ice_tcp_candidate_type_to_str(enum rawrtc_ice_tcp_candidate_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_tcp_candidate_type_length; ++i) {
+        if (map_enum_ice_tcp_candidate_type[i] == type) {
+            return map_str_ice_tcp_candidate_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_pl_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    struct pl const* const pl) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !pl_isset(pl)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_tcp_candidate_type_length; ++i) {
+        if (pl_strcasecmp(pl, map_str_ice_tcp_candidate_type[i]) == 0) {
+            *typep = map_enum_ice_tcp_candidate_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate a str to an ICE TCP candidate type (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_tcp_candidate_type(
+    enum rawrtc_ice_tcp_candidate_type* const typep,  // de-referenced
+    char const* const str) {
+    struct pl pl;
+
+    // Check arguments
+    if (!str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert str to pl
+    pl_set_str(&pl, str);
+    return rawrtc_pl_to_ice_tcp_candidate_type(typep, &pl);
+}
+
+static char const* const map_str_ice_candidate_storage[] = {
+    "raw",
+    "lcand",
+    "rcand",
+};
+
+static enum rawrtc_ice_candidate_storage const map_enum_ice_candidate_storage[] = {
+    RAWRTC_ICE_CANDIDATE_STORAGE_RAW,
+    RAWRTC_ICE_CANDIDATE_STORAGE_LCAND,
+    RAWRTC_ICE_CANDIDATE_STORAGE_RCAND,
+};
+
+static size_t const map_ice_candidate_storage_length = ARRAY_SIZE(map_enum_ice_candidate_storage);
+
+/*
+ * Translate an ICE candidate storage type to str.
+ */
+static char const* ice_candidate_storage_to_str(enum rawrtc_ice_candidate_storage const type) {
+    size_t i;
+
+    for (i = 0; i < map_ice_candidate_storage_length; ++i) {
+        if (map_enum_ice_candidate_storage[i] == type) {
+            return map_str_ice_candidate_storage[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Print debug information for an ICE candidate.
+ */
+int rawrtc_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_ice_candidate* const candidate) {
+    int err = 0;
+    enum rawrtc_code error;
+    char* foundation = NULL;
+    uint32_t priority;
+    char* ip = NULL;
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address = NULL;
+    uint16_t related_port;
+
+    // Check arguments
+    if (!candidate) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Candidate <%p>:\n", candidate);
+
+    // Storage type
+    err |= re_hprintf(
+        pf, "    storage_type=%s\n", ice_candidate_storage_to_str(candidate->storage_type));
+
+    // Foundation
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    foundation=\"%s\"\n", foundation);
+
+    // Priority
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    priority=%" PRIu32 "\n", priority);
+
+    // IP
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    ip=%s\n", ip);
+
+    // Protocol
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    protocol=%s\n", rawrtc_ice_protocol_to_str(protocol));
+
+    // Port
+    error = rawrtc_ice_candidate_get_port(&port, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    port=%" PRIu16 "\n", port);
+
+    // Type
+    error = rawrtc_ice_candidate_get_type(&type, candidate);
+    if (error) {
+        goto out;
+    }
+    err |= re_hprintf(pf, "    type=%s\n", rawrtc_ice_candidate_type_to_str(type));
+
+    // TCP type (if any)
+    err |= re_hprintf(pf, "    tcp_type=");
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%s\n", rawrtc_ice_tcp_candidate_type_to_str(tcp_type));
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+    // Related address (if any)
+    err |= re_hprintf(pf, "    related_address=");
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%s\n", related_address);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+    // Related port (if any)
+    err |= re_hprintf(pf, "    related_port=");
+    error = rawrtc_ice_candidate_get_related_port(&related_port, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            err |= re_hprintf(pf, "%" PRIu16 "\n", related_port);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            err |= re_hprintf(pf, "n/a\n");
+            break;
+        default:
+            goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(related_address);
+    mem_deref(ip);
+    mem_deref(foundation);
+
+    // Translate error & done
+    if (!err && error) {
+        err = EINVAL;
+    }
+    return err;
+}
diff --git a/src/ice_gather_options/meson.build b/src/ice_gather_options/meson.build
new file mode 100644
index 0000000..1161f68
--- /dev/null
+++ b/src/ice_gather_options/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'options.c',
+    'utils.c',
+])
diff --git a/src/ice_gather_options/options.c b/src/ice_gather_options/options.c
new file mode 100644
index 0000000..c2847ab
--- /dev/null
+++ b/src/ice_gather_options/options.c
@@ -0,0 +1,93 @@
+#include "options.h"
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing ICE gather options instance.
+ */
+static void rawrtc_ice_gather_options_destroy(void* arg) {
+    struct rawrtc_ice_gather_options* const options = arg;
+
+    // Un-reference
+    list_flush(&options->ice_servers);
+}
+
+/*
+ * Create a new ICE gather options instance.
+ * `*optionsp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_create(
+    struct rawrtc_ice_gather_options** const optionsp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy) {
+    struct rawrtc_ice_gather_options* options;
+
+    // Check arguments
+    if (!optionsp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    options = mem_zalloc(sizeof(*options), rawrtc_ice_gather_options_destroy);
+    if (!options) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    options->gather_policy = gather_policy;
+    list_init(&options->ice_servers);
+
+    // Set pointer and return
+    *optionsp = options;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server instance to the gather options.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_add_server_internal(
+    struct rawrtc_ice_gather_options* const options, struct rawrtc_ice_server* const server) {
+    // Check arguments
+    if (!options || !server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Add to options
+    list_append(&options->ice_servers, &server->le, server);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server to the gather options.
+ */
+enum rawrtc_code rawrtc_ice_gather_options_add_server(
+    struct rawrtc_ice_gather_options* const options,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!options) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure there are less than 2^8 servers
+    // TODO: This check should be in some common location
+    if (list_count(&options->ice_servers) == UINT8_MAX) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Create ICE server
+    error = rawrtc_ice_server_create(&server, urls, n_urls, username, credential, credential_type);
+    if (error) {
+        return error;
+    }
+
+    // Add to options
+    return rawrtc_ice_gather_options_add_server_internal(options, server);
+}
diff --git a/src/ice_gather_options/options.h b/src/ice_gather_options/options.h
new file mode 100644
index 0000000..c43b9c8
--- /dev/null
+++ b/src/ice_gather_options/options.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+struct rawrtc_ice_gather_options {
+    enum rawrtc_ice_gather_policy gather_policy;
+    struct list ice_servers;
+};
+
+enum rawrtc_code rawrtc_ice_gather_options_add_server_internal(
+    struct rawrtc_ice_gather_options* const configuration, struct rawrtc_ice_server* const server);
+
+int rawrtc_ice_gather_options_debug(
+    struct re_printf* const pf, struct rawrtc_ice_gather_options const* const options);
diff --git a/src/ice_gather_options/utils.c b/src/ice_gather_options/utils.c
new file mode 100644
index 0000000..ae51caa
--- /dev/null
+++ b/src/ice_gather_options/utils.c
@@ -0,0 +1,87 @@
+#include "options.h"
+#include "../ice_server/server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+static enum rawrtc_ice_gather_policy const map_enum_ice_gather_policy[] = {
+    RAWRTC_ICE_GATHER_POLICY_ALL,
+    RAWRTC_ICE_GATHER_POLICY_NOHOST,
+    RAWRTC_ICE_GATHER_POLICY_RELAY,
+};
+
+static char const* const map_str_ice_gather_policy[] = {
+    "all",
+    "nohost",
+    "relay",
+};
+
+static size_t const map_ice_gather_policy_length = ARRAY_SIZE(map_enum_ice_gather_policy);
+
+/*
+ * Translate an ICE gather policy to str.
+ */
+char const* rawrtc_ice_gather_policy_to_str(enum rawrtc_ice_gather_policy const policy) {
+    size_t i;
+
+    for (i = 0; i < map_ice_gather_policy_length; ++i) {
+        if (map_enum_ice_gather_policy[i] == policy) {
+            return map_str_ice_gather_policy[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE gather policy (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_gather_policy(
+    enum rawrtc_ice_gather_policy* const policyp,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!policyp || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_gather_policy_length; ++i) {
+        if (str_casecmp(map_str_ice_gather_policy[i], str) == 0) {
+            *policyp = map_enum_ice_gather_policy[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Print debug information for the ICE gather options.
+ */
+int rawrtc_ice_gather_options_debug(
+    struct re_printf* const pf, struct rawrtc_ice_gather_options const* const options) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!options) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "----- ICE Gather Options <%p> -----\n", options);
+
+    // Gather policy
+    err |= re_hprintf(
+        pf, "  gather_policy=%s\n", rawrtc_ice_gather_policy_to_str(options->gather_policy));
+
+    // ICE servers
+    for (le = list_head(&options->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const server = le->data;
+        err |= re_hprintf(pf, "%H", rawrtc_ice_server_debug, server);
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/ice_gatherer/attributes.c b/src/ice_gatherer/attributes.c
new file mode 100644
index 0000000..92c6fb5
--- /dev/null
+++ b/src/ice_gatherer/attributes.c
@@ -0,0 +1,19 @@
+#include "gatherer.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtcc/code.h>
+
+/*
+ * Get the current state of an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!statep || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = gatherer->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_gatherer/gatherer.c b/src/ice_gatherer/gatherer.c
new file mode 100644
index 0000000..51002fe
--- /dev/null
+++ b/src/ice_gatherer/gatherer.c
@@ -0,0 +1,1073 @@
+#include "gatherer.h"
+#include "../ice_candidate/candidate.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gather_options/options.h"
+#include "../ice_server/address.h"
+#include "../ice_server/resolver.h"
+#include "../ice_server/server.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/message_buffer.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+#include <string.h>  // memcpy
+#include <sys/socket.h>  // AF_INET, AF_INET6
+
+#define DEBUG_MODULE "ice-gatherer"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#define RAWRTC_DEBUG_ICE_GATHERER 0  // TODO: Remove
+#include <rawrtcc/debug.h>
+
+/*
+ * Destructor for an existing ICE gatherer.
+ */
+static void rawrtc_ice_gatherer_destroy(void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+
+    // Close gatherer
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_ice_gatherer_close(gatherer);
+
+    // Un-reference
+    mem_deref(gatherer->dns_client);
+    mem_deref(gatherer->ice);
+    list_flush(&gatherer->local_candidates);
+    list_flush(&gatherer->buffered_messages);
+    list_flush(&gatherer->url_resolvers);
+    list_flush(&gatherer->url_addresses);
+    mem_deref(gatherer->options);
+}
+
+/*
+ * Create a new ICE gatherer.
+ * `*gathererp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_create(
+    struct rawrtc_ice_gatherer** const gathererp,  // de-referenced
+    struct rawrtc_ice_gather_options* const options,  // referenced
+    rawrtc_ice_gatherer_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_gatherer_error_handler const error_handler,  // nullable
+    rawrtc_ice_gatherer_local_candidate_handler const local_candidate_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_ice_gatherer* gatherer;
+    int err;
+    struct sa dns_servers[RAWRTC_ICE_GATHERER_DNS_SERVERS] = {0};
+    uint32_t n_dns_servers = ARRAY_SIZE(dns_servers);
+    uint32_t i;
+
+    // Check arguments
+    if (!gathererp || !options) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    gatherer = mem_zalloc(sizeof(*gatherer), rawrtc_ice_gatherer_destroy);
+    if (!gatherer) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    gatherer->state = RAWRTC_ICE_GATHERER_STATE_NEW;  // TODO: Raise state (delayed)?
+    gatherer->options = mem_ref(options);
+    gatherer->state_change_handler = state_change_handler;
+    gatherer->error_handler = error_handler;
+    gatherer->local_candidate_handler = local_candidate_handler;
+    gatherer->arg = arg;
+    list_init(&gatherer->url_addresses);
+    list_init(&gatherer->url_resolvers);
+    list_init(&gatherer->buffered_messages);
+    list_init(&gatherer->local_candidates);
+
+    // Generate random username fragment and password for ICE
+    rand_str(gatherer->ice_username_fragment, sizeof(gatherer->ice_username_fragment));
+    rand_str(gatherer->ice_password, sizeof(gatherer->ice_password));
+
+    // Set ICE configuration and create trice instance
+    // TODO: Get from config
+    gatherer->ice_config.nom = ICE_NOMINATION_AGGRESSIVE;
+    gatherer->ice_config.debug = RAWRTC_DEBUG_ICE_GATHERER ? true : false;
+    gatherer->ice_config.trace = RAWRTC_DEBUG_ICE_GATHERER ? true : false;
+    gatherer->ice_config.ansi = true;
+    gatherer->ice_config.enable_prflx = true;
+    gatherer->ice_config.optimize_loopback_pairing = true;
+    err = trice_alloc(
+        &gatherer->ice, &gatherer->ice_config, ICE_ROLE_UNKNOWN, gatherer->ice_username_fragment,
+        gatherer->ice_password);
+    if (err) {
+        DEBUG_WARNING("Unable to create trickle ICE instance, reason: %m\n", err);
+        goto out;
+    }
+
+    // Get local DNS servers
+    err = dns_srv_get(NULL, 0, dns_servers, &n_dns_servers);
+    if (err) {
+        DEBUG_WARNING("Unable to retrieve local DNS servers, reason: %m\n", err);
+        goto out;
+    }
+
+    // Print local DNS servers
+    if (n_dns_servers == 0) {
+        DEBUG_NOTICE("No DNS servers found\n");
+    }
+    for (i = 0; i < n_dns_servers; ++i) {
+        DEBUG_PRINTF("DNS server: %j\n", &dns_servers[i]);
+    }
+
+    // Create DNS client (for resolving ICE server IPs)
+    err = dnsc_alloc(&gatherer->dns_client, NULL, dns_servers, n_dns_servers);
+    if (err) {
+        DEBUG_WARNING("Unable to create DNS client instance, reason: %m\n", err);
+        goto out;
+    }
+
+    // Done
+    DEBUG_PRINTF("ICE gatherer created:\n%H", rawrtc_ice_gather_options_debug, gatherer->options);
+
+out:
+    if (err) {
+        mem_deref(gatherer);
+    } else {
+        // Set pointer
+        *gathererp = gatherer;
+    }
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Change the state of the ICE gatherer.
+ * Will call the corresponding handler.
+ * TODO: https://github.com/w3c/ortc/issues/606
+ */
+static void set_state(
+    struct rawrtc_ice_gatherer* const gatherer, enum rawrtc_ice_gatherer_state const state) {
+    // Set state
+    gatherer->state = state;
+
+    // Call handler (if any)
+    if (gatherer->state_change_handler) {
+        gatherer->state_change_handler(state, gatherer->arg);
+    }
+}
+
+/*
+ * Close the ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_close(struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Already closed?
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // TODO: Stop ICE transport
+
+    // Stop timeout timer
+    tmr_cancel(&gatherer->timeout_timer);
+
+    // Remove STUN sessions from local candidate helpers
+    // Note: Needed to purge remaining references to the gatherer so it can be free'd.
+    list_apply(
+        &gatherer->local_candidates, true, rawrtc_candidate_helper_remove_stun_sessions_handler,
+        NULL);
+
+    // Flush local candidate helpers
+    list_flush(&gatherer->local_candidates);
+
+    // Remove ICE server URL resolvers
+    list_flush(&gatherer->url_resolvers);
+
+    // Remove ICE server URL addresses
+    list_flush(&gatherer->url_addresses);
+
+    // Stop ICE checklist (if running)
+    trice_checklist_stop(gatherer->ice);
+
+    // Remove ICE agent
+    gatherer->ice = mem_deref(gatherer->ice);
+
+    // Set state to closed and return
+    set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_CLOSED);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Handle received UDP messages.
+ */
+static bool udp_receive_handler(struct sa* source, struct mbuf* buffer, void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    enum rawrtc_code error;
+
+    // Allocate context and copy source address
+    void* const context = mem_zalloc(sizeof(*source), NULL);
+    if (!context) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    memcpy(context, source, sizeof(*source));
+
+    // Buffer message
+    error = rawrtc_message_buffer_append(&gatherer->buffered_messages, buffer, context);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    DEBUG_PRINTF("Buffered UDP packet of size %zu\n", mbuf_get_left(buffer));
+
+out:
+    if (error) {
+        DEBUG_WARNING("Could not buffer UDP packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+
+    // Un-reference
+    mem_deref(context);
+
+    // Handled
+    return true;
+}
+
+/*
+ * Announce a local candidate.
+ */
+static enum rawrtc_code announce_candidate(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct ice_lcand* const re_candidate,  // nullable
+    char const* const url  // nullable
+) {
+    enum rawrtc_code error;
+
+    // Don't announce in the completed state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_COMPLETE) {
+        DEBUG_PRINTF("Not announcing candidate, gathering state is complete\n");
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create ICE candidate
+    if (gatherer->local_candidate_handler) {
+        struct rawrtc_ice_candidate* ice_candidate = NULL;
+
+        // Create ICE candidate
+        if (re_candidate) {
+            error = rawrtc_ice_candidate_create_from_local_candidate(&ice_candidate, re_candidate);
+            if (error) {
+                return error;
+            }
+        }
+
+        // Call candidate handler and un-reference
+        gatherer->local_candidate_handler(ice_candidate, url, gatherer->arg);
+        mem_deref(ice_candidate);
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Check if the gathering process is complete.
+ */
+static void check_gathering_complete(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    bool const force_complete) {
+    struct le* le;
+    enum rawrtc_code error;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Check or force completion?
+    if (!force_complete) {
+        // Ensure no URL resolvers are in flight
+        if (!list_isempty(&gatherer->url_resolvers)) {
+            struct rawrtc_ice_server_url_resolver* const resolver =
+                list_head(&gatherer->url_resolvers)->data;
+            (void) resolver;
+            DEBUG_PRINTF(
+                "Gathering still in progress, resolving URL (%s [%s])\n", resolver->url->url,
+                dns_rr_typename(resolver->dns_type));
+            return;
+        }
+
+        // Ensure every local candidate has no pending srflx/relay candidates
+        for (le = list_head(&gatherer->local_candidates); le != NULL; le = le->next) {
+            struct rawrtc_candidate_helper* const candidate = le->data;
+
+            // Check counters
+            if (candidate->srflx_pending_count > 0 || candidate->relay_pending_count > 0) {
+                // Nope
+                DEBUG_PRINTF(
+                    "Gathering still in progress at candidate %j, #srflx=%" PRIuFAST8
+                    ", #relay=%" PRIuFAST8 "\n",
+                    &candidate->candidate->attr.addr, candidate->srflx_pending_count,
+                    candidate->relay_pending_count);
+                return;
+            }
+        }
+    }
+
+    // Stop timeout timer
+    tmr_cancel(&gatherer->timeout_timer);
+
+    // TODO: Skip the remaining code below when using continuous gathering
+
+    // Announce candidate gathering complete
+    error = announce_candidate(gatherer, NULL, NULL);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not announce end-of-candidates, reason: %s\n", rawrtc_code_to_str(error));
+
+        // This should never happen, so close on failure
+        rawrtc_ice_gatherer_close(gatherer);
+        return;
+    }
+
+    // Update state & done
+    if (gatherer->state != RAWRTC_ICE_GATHERER_STATE_COMPLETE) {
+        DEBUG_PRINTF("Gathering complete:\n%H", trice_debug, gatherer->ice);
+        set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_COMPLETE);
+    }
+}
+
+/*
+ * Find an existing local candidate.
+ * TODO: This should probably be moved into a PR towards rew
+ */
+static struct ice_lcand* find_candidate(
+    struct trice* const ice,
+    enum ice_cand_type type,  // set to -1 if it should not be checked
+    unsigned const component_id,  // set to 0 if it should not be checked
+    int const protocol,
+    struct sa const* const address,  // nullable
+    enum sa_flag const address_flag,
+    struct sa const* base_address,  // nullable
+    enum sa_flag const base_address_flags) {
+    struct le* le;
+
+    // Check arguments
+    if (!ice) {
+        return NULL;
+    }
+
+    // If base address and address have an identical IP, ignore the base address and the type
+    if (address && base_address && sa_cmp(address, base_address, SA_ADDR)) {
+        base_address = NULL;
+        type = (enum ice_cand_type) - 1;
+    }
+
+    for (le = list_head(trice_lcandl(ice)); le != NULL; le = le->next) {
+        struct ice_lcand* candidate = le->data;
+
+        // Check type (if requested)
+        if (type != (enum ice_cand_type) - 1 && type != candidate->attr.type) {
+            continue;
+        }
+
+        // Check component id (if requested)
+        if (component_id && candidate->attr.compid != component_id) {
+            continue;
+        }
+
+        // Check protocol
+        if (candidate->attr.proto != protocol) {
+            continue;
+        }
+
+        // Check address
+        if (address && !sa_cmp(&candidate->attr.addr, address, address_flag)) {
+            continue;
+        }
+
+        // Check base address
+        if (base_address && !sa_cmp(&candidate->base_addr, base_address, base_address_flags)) {
+            continue;
+        }
+
+        // Found
+        return candidate;
+    }
+
+    // Not found
+    return NULL;
+}
+
+/*
+ * Gather relay candidates on an ICE server.
+ */
+static enum rawrtc_code gather_relay_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    // Check ICE server is enabled for TURN
+    if (server_address->url->type != RAWRTC_ICE_SERVER_TYPE_TURN) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // TODO: Create TURN request
+    (void) candidate;
+    DEBUG_NOTICE(
+        "TODO: Gather relay candidates using server %J (%s)\n", &server_address->address,
+        server_address->url->url);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Handle gathered server reflexive candidate.
+ */
+static void reflexive_candidate_handler(
+    int err,
+    struct sa const* address,  // not checked
+    void* arg  // not checked
+) {
+    struct rawrtc_candidate_helper_stun_session* const session = arg;
+    struct rawrtc_candidate_helper* const candidate = session->candidate_helper;
+    struct rawrtc_ice_gatherer* const gatherer = candidate->gatherer;
+    struct ice_lcand* const re_candidate = candidate->candidate;
+    struct ice_lcand* re_other_candidate;
+    uint32_t priority;
+    struct ice_lcand* srflx_candidate;
+    enum rawrtc_code error;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Error?
+    if (err) {
+        DEBUG_NOTICE("STUN request failed, reason: %m\n", err);
+        goto out;
+    }
+
+    // Check if a local candidate with the same base and same attributes (apart from the port)
+    // exists
+    re_other_candidate = find_candidate(
+        gatherer->ice, ICE_CAND_TYPE_SRFLX, re_candidate->attr.compid, re_candidate->attr.proto,
+        address, SA_ADDR, &re_candidate->attr.addr, SA_ALL);
+    if (re_other_candidate) {
+        DEBUG_PRINTF(
+            "Ignoring server reflexive candidate with same base %J and public IP %j (%s)"
+            "\n",
+            &re_candidate->attr.addr, address, session->url->url);
+        goto out;
+    }
+
+    // Add server reflexive candidate
+    // TODO: Using the candidate's protocol, TCP type and component id correct?
+    priority = rawrtc_ice_candidate_calculate_priority(
+        list_count(trice_lcandl(gatherer->ice)), ICE_CAND_TYPE_SRFLX, re_candidate->attr.proto,
+        sa_af(address), re_candidate->attr.tcptype);
+    err = trice_lcand_add(
+        &srflx_candidate, gatherer->ice, re_candidate->attr.compid, re_candidate->attr.proto,
+        priority, address, &re_candidate->attr.addr, ICE_CAND_TYPE_SRFLX, &re_candidate->attr.addr,
+        re_candidate->attr.tcptype, NULL, RAWRTC_LAYER_ICE);
+    if (err) {
+        DEBUG_WARNING("Could not add server reflexive candidate, reason: %m\n", err);
+        goto out;
+    }
+    DEBUG_PRINTF(
+        "Added %s server reflexive candidate for interface %j (%s)\n",
+        net_proto2name(srflx_candidate->attr.proto), address, session->url->url);
+
+    // Announce candidate to handler
+    error = announce_candidate(gatherer, srflx_candidate, session->url->url);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not announce server reflexive candidate, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+out:
+    // Decrease counter & check if done gathering
+    if (session->pending) {
+        --candidate->srflx_pending_count;
+        session->pending = false;
+    }
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Gather server reflexive candidates on an ICE server.
+ */
+static enum rawrtc_code gather_reflexive_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    enum rawrtc_code error;
+    struct ice_lcand* const re_candidate = candidate->candidate;
+    struct ice_cand_attr* const attribute = &candidate->candidate->attr;
+    int const af = sa_af(&attribute->addr);
+    enum rawrtc_ice_candidate_type type;
+    char const* type_str;
+    struct rawrtc_candidate_helper_stun_session* session = NULL;
+    struct stun_conf stun_config = {
+        // TODO: Make this configurable!
+        .rto = STUN_DEFAULT_RTO,  // 500ms
+        .rc = 3,  // Send at: 0ms, 500ms, 1500ms
+        .rm = 6,  // Additional wait: 3000ms
+        .ti = 4500,  // Total timeout: 4500ms
+        .tos = 0x00,
+    };
+    struct stun_keepalive* stun_keepalive = NULL;
+
+    // Ignore IPv6 addresses
+    // Note: If you have a use case for IPv6 server reflexive candidates, let me know.
+    if (af == AF_INET6) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Ensure the candidate's IP version matches the server address's IP version
+    if (af != sa_af(&server_address->address)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Convert ICE candidate type
+    error = rawrtc_ice_cand_type_to_ice_candidate_type(&type, attribute->type);
+    if (error) {
+        goto out;
+    }
+    type_str = rawrtc_ice_candidate_type_to_str(type);
+    (void) type_str;
+
+    // TODO: Handle TCP/TLS/DTLS transports
+
+    // Create STUN session
+    error = rawrtc_candidate_helper_stun_session_create(&session, server_address->url);
+    if (error) {
+        goto out;
+    }
+
+    // Create STUN keep-alive session
+    // TODO: We're using the candidate's protocol which conflicts with the ICE server URL transport
+    DEBUG_PRINTF(
+        "Creating STUN request for %s %s candidate %J using ICE server %J (%s)\n",
+        net_proto2name(attribute->proto), type_str, &attribute->addr, &server_address->address,
+        server_address->url->url);
+    error = rawrtc_error_to_code(stun_keepalive_alloc(
+        &stun_keepalive, re_candidate->attr.proto, re_candidate->us, RAWRTC_LAYER_STUN,
+        &server_address->address, &stun_config, reflexive_candidate_handler, session));
+    if (error) {
+        goto out;
+    }
+
+    // Add the STUN session to the candidate
+    error = rawrtc_candidate_helper_stun_session_add(session, candidate, stun_keepalive);
+    if (error) {
+        goto out;
+    }
+
+    // Increase counter, start the STUN session & done
+    ++candidate->srflx_pending_count;
+    stun_keepalive_enable(stun_keepalive, rawrtc_default_config.stun_keepalive_interval);
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        DEBUG_WARNING("Could not create STUN request, reason: %s\n", rawrtc_code_to_str(error));
+        mem_deref(session);
+    }
+
+    // Un-reference & done
+    mem_deref(stun_keepalive);
+    return error;
+}
+
+/*
+ * Gather server reflexive and relay candidates using a specific ICE
+ * server.
+ */
+static void gather_candidates(
+    struct rawrtc_candidate_helper* const candidate,  // not checked
+    struct rawrtc_ice_server_url_address* const server_address  // not checked
+) {
+    struct sa* const address = &candidate->candidate->attr.addr;
+    int af;
+    enum rawrtc_code error;
+
+    // Skip IPv4, IPv6 (server [!] addresses)?
+    // TODO: Get config from struct
+    af = sa_af(&server_address->address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF(
+            "Ignoring ICE server address %j (family disabled)\n", &server_address->address);
+        return;
+    }
+
+    // Ignore 'any', loopback and link-local server (!) addresses
+    if (sa_is_any(&server_address->address) || sa_is_loopback(&server_address->address) ||
+        sa_is_linklocal(&server_address->address)) {
+        DEBUG_NOTICE("Ignoring ICE server address %j\n", &server_address->address);
+        return;
+    }
+
+    // Ignore loopback and link-local candidate (!) addresses (there is no mapped NAT address since
+    // the addresses aren't reachable from outside of the local network)
+    if (sa_is_linklocal(address) || sa_is_loopback(address)) {
+        return;
+    }
+
+    // Gather reflexive candidates
+    error = gather_reflexive_candidates(candidate, server_address);
+    if (error) {
+        DEBUG_WARNING(
+            "Could not gather server reflexive candidates, reason: %s", rawrtc_code_to_str(error));
+        // Note: Considered non-critical, continuing
+    }
+
+    // Gather relay candidates
+    error = gather_relay_candidates(candidate, server_address);
+    if (error) {
+        DEBUG_WARNING("Could not gather relay candidates, reason: %s", rawrtc_code_to_str(error));
+        // Note: Considered non-critical, continuing
+    }
+}
+
+/*
+ * Gather server reflexive and relay candidates using a newly resolved
+ * ICE server URL address.
+ */
+static void gather_candidates_using_server(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_ice_server_url_address* const address  // not checked
+) {
+    struct le* le;
+    for (le = list_head(&gatherer->local_candidates); le != NULL; le = le->next) {
+        struct rawrtc_candidate_helper* const candidate = le->data;
+
+        // Gather candidates
+        gather_candidates(candidate, address);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Gather server reflexive candidates of a local candidate using
+ * already resolved ICE servers.
+ */
+static void gather_candidates_using_resolved_servers(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_candidate_helper* const candidate  // not checked
+) {
+    struct le* le;
+    for (le = list_head(&gatherer->url_addresses); le != NULL; le = le->next) {
+        struct rawrtc_ice_server_url_address* const address = le->data;
+
+        // Gather candidates
+        gather_candidates(candidate, address);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+}
+
+/*
+ * Add local candidate, gather server reflexive and relay candidates.
+ */
+static enum rawrtc_code add_candidate(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct sa const* const address,  // not checked
+    enum rawrtc_ice_protocol const protocol,
+    enum ice_tcptype const tcp_type) {
+    uint32_t priority;
+    int const ipproto = rawrtc_ice_protocol_to_ipproto(protocol);
+    struct ice_lcand* re_candidate;
+    int err;
+    struct rawrtc_candidate_helper* candidate;
+    enum rawrtc_code error;
+
+    // Add host candidate
+    priority = rawrtc_ice_candidate_calculate_priority(
+        list_count(trice_lcandl(gatherer->ice)), ICE_CAND_TYPE_HOST, ipproto, sa_af(address),
+        tcp_type);
+    // TODO: Set component id properly
+    err = trice_lcand_add(
+        &re_candidate, gatherer->ice, 1, ipproto, priority, address, NULL, ICE_CAND_TYPE_HOST, NULL,
+        tcp_type, NULL, RAWRTC_LAYER_ICE);
+    if (err) {
+        DEBUG_WARNING("Could not add host candidate, reason: %m\n", err);
+        return rawrtc_error_to_code(err);
+    }
+
+    // Create candidate helper (attaches receive handler)
+    error = rawrtc_candidate_helper_create(
+        &candidate, gatherer, re_candidate, udp_receive_handler, gatherer);
+    if (error) {
+        DEBUG_WARNING("Could not create candidate helper, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Add to local candidates list
+    list_append(&gatherer->local_candidates, &candidate->le, candidate);
+    DEBUG_PRINTF(
+        "Added %s host candidate for interface %j\n", rawrtc_ice_protocol_to_str(protocol),
+        address);
+
+    // Announce host candidate to handler
+    error = announce_candidate(gatherer, re_candidate, NULL);
+    if (error) {
+        DEBUG_WARNING("Could not announce host candidate, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Gather server reflexive and relay candidates
+    gather_candidates_using_resolved_servers(gatherer, candidate);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Local interfaces callback.
+ * TODO: Consider ICE gather policy
+ * TODO: https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-01
+ */
+static bool interface_handler(
+    char const* interface,  // not checked
+    struct sa const* address,  // not checked
+    void* arg  // not checked
+) {
+    int af;
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    (void) interface;
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return true;  // Don't continue gathering
+    }
+
+    // Ignore 'any' address
+    if (sa_is_any(address)) {
+        DEBUG_PRINTF("Ignoring gathered 'any' address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Ignore loopback address
+    // TODO: Make this configurable
+    if (sa_is_loopback(address)) {
+        DEBUG_PRINTF("Ignoring gathered loopback address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Ignore link-local address
+    // TODO: Make this configurable
+    if (sa_is_linklocal(address)) {
+        DEBUG_PRINTF("Ignoring gathered link-local address %j\n", address);
+        return false;  // Continue gathering
+    }
+
+    // Skip IPv4, IPv6?
+    // TODO: Get config from struct
+    af = sa_af(address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF("Ignoring gathered address %j (family disabled)\n", address);
+        return false;  // Continue gathering
+    }
+
+    // TODO: Ignore interfaces gathered twice
+
+    DEBUG_PRINTF("Gathered local interface %j\n", address);
+
+    // Add UDP candidate
+    if (rawrtc_default_config.udp_enable) {
+        error = add_candidate(gatherer, address, RAWRTC_ICE_PROTOCOL_UDP, ICE_TCP_ACTIVE);
+        if (error) {
+            DEBUG_WARNING("Could not add candidate, reason: %s", rawrtc_code_to_str(error));
+            goto out;
+        }
+
+        // Check state
+        if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+            return true;  // Don't continue gathering
+        }
+    }
+
+    // Add TCP candidate
+    if (rawrtc_default_config.tcp_enable) {
+        // TODO
+        // add_candidate(gatherer, address, RAWRTC_ICE_PROTOCOL_TCP, ICE_TCP_SO);
+        DEBUG_WARNING("TODO: Add TCP host candidate for interface %j\n", address);
+    }
+
+out:
+    if (error) {
+        // Close and don't continue gathering
+        rawrtc_ice_gatherer_close(gatherer);
+        return true;
+    } else {
+        return false;  // Continue gathering
+    }
+}
+
+/*
+ * ICE server URL address resolved handler.
+ */
+static bool ice_server_url_address_result_handler(
+    struct rawrtc_ice_server_url_address* const address,  // not checked, referenced
+    void* const arg  // not checked
+) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+    DEBUG_INFO("Resolved URL %s to address %J\n", address->url->url, &address->address);
+
+    // Append to list of URL addresses
+    list_append(&gatherer->url_addresses, &address->le, mem_ref(address));
+
+    // Gather on the newly created address
+    gather_candidates_using_server(gatherer, address);
+
+    // Done, stop traversing, one address per family is sufficient
+    return true;
+}
+
+/*
+ * Resolve ICE server IP addresses.
+ */
+static enum rawrtc_code resolve_ice_server_addresses(
+    struct rawrtc_ice_gatherer* const gatherer,  // not checked
+    struct rawrtc_ice_gather_options* const options  // not checked
+) {
+    struct le* le;
+
+    // Remove all ICE server URL resolvers
+    // Note: This will cancel pending URL resolve processes
+    list_flush(&gatherer->url_resolvers);
+
+    // Remove all resolved ICE server URL addresses
+    list_flush(&gatherer->url_addresses);
+
+    for (le = list_head(&options->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const ice_server = le->data;
+        struct le* url_le;
+        enum rawrtc_code error;
+
+        for (url_le = list_head(&ice_server->urls); url_le != NULL; url_le = url_le->next) {
+            struct rawrtc_ice_server_url* const url = url_le->data;
+            // URL already resolved (decoded IP address)?
+            if (!sa_is_any(&url->resolved_address)) {
+                struct rawrtc_ice_server_url_address* address;
+
+                // Create URL address from resolved URL
+                error = rawrtc_ice_server_url_address_create(&address, url, &url->resolved_address);
+                if (error) {
+                    DEBUG_WARNING(
+                        "Unable to create ICE server URL address, reason: %s\n",
+                        rawrtc_code_to_str(error));
+                    // Continue - not considered critical
+                } else {
+                    // Append to list of URL addresses
+                    list_append(&gatherer->url_addresses, &address->le, address);
+                }
+            } else {
+                // Create URL resolver for A record (if enabled)
+                if (rawrtc_default_config.ipv4_enable) {
+                    struct rawrtc_ice_server_url_resolver* resolver;
+                    error = rawrtc_ice_server_url_resolver_create(
+                        &resolver, gatherer->dns_client, DNS_TYPE_A, url,
+                        ice_server_url_address_result_handler, gatherer);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to query A record for URL %s, reason: %s\n", url->url,
+                            rawrtc_code_to_str(error));
+                        // Continue - not considered critical
+                    } else {
+                        // Append to list of URL resolvers
+                        list_append(&gatherer->url_resolvers, &resolver->le, resolver);
+                    }
+                }
+
+                // Create URL resolver for AAAA record (if enabled)
+                if (rawrtc_default_config.ipv6_enable) {
+                    struct rawrtc_ice_server_url_resolver* resolver;
+                    error = rawrtc_ice_server_url_resolver_create(
+                        &resolver, gatherer->dns_client, DNS_TYPE_AAAA, url,
+                        ice_server_url_address_result_handler, gatherer);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to query AAAA record for URL %s, reason: %s\n", url->url,
+                            rawrtc_code_to_str(error));
+                        // Continue - not considered critical
+                    } else {
+                        // Append to list of URL resolvers
+                        list_append(&gatherer->url_resolvers, &resolver->le, resolver);
+                    }
+                }
+            }
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Gathering timeout handler.
+ * Note: This timeout has no effect when using continuous gathering.
+ */
+static void gather_timeout_handler(void* arg) {
+    struct rawrtc_ice_gatherer* const gatherer = arg;
+
+    // Force gathering complete
+    check_gathering_complete(gatherer, true);
+}
+
+/*
+ * Start gathering using an ICE gatherer.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_gather(
+    struct rawrtc_ice_gatherer* const gatherer,
+    struct rawrtc_ice_gather_options* options  // referenced, nullable
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+    if (!options) {
+        options = gatherer->options;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Already gathering?
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_GATHERING) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Resolve ICE server IP addresses
+    error = resolve_ice_server_addresses(gatherer, options);
+    if (error) {
+        return error;
+    }
+
+    // Update state
+    set_state(gatherer, RAWRTC_ICE_GATHERER_STATE_GATHERING);
+
+    // Start timeout timer
+    // TODO: Make the timeout configurable
+    tmr_start(&gatherer->timeout_timer, 6000, gather_timeout_handler, gatherer);
+
+    // Start gathering host candidates
+    if (options->gather_policy != RAWRTC_ICE_GATHER_POLICY_NOHOST) {
+        net_if_apply(interface_handler, gatherer);
+    }
+
+    // Gathering complete?
+    check_gathering_complete(gatherer, false);
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get local ICE parameters of an ICE gatherer.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_parameters(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    // Check arguments
+    if (!parametersp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Create and return ICE parameters instance
+    return rawrtc_ice_parameters_create(
+        parametersp, gatherer->ice_username_fragment, gatherer->ice_password, false);
+}
+
+/*
+ * Destructor for an existing local candidates array.
+ */
+static void rawrtc_ice_gatherer_local_candidates_destroy(void* arg) {
+    struct rawrtc_ice_candidates* const candidates = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < candidates->n_candidates; ++i) {
+        mem_deref(candidates->candidates[i]);
+    }
+}
+
+/*
+ * Get local ICE candidates of an ICE gatherer.
+ * `*candidatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_gatherer_get_local_candidates(
+    struct rawrtc_ice_candidates** const candidatesp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer) {
+    size_t n;
+    struct rawrtc_ice_candidates* candidates;
+    struct le* le;
+    size_t i;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!candidatesp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get length
+    n = list_count(trice_lcandl(gatherer->ice));
+
+    // Allocate & set length immediately
+    candidates = mem_zalloc(
+        sizeof(*candidates) + (sizeof(struct rawrtc_ice_candidate*) * n),
+        rawrtc_ice_gatherer_local_candidates_destroy);
+    if (!candidates) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+    candidates->n_candidates = n;
+
+    // Copy each ICE candidate
+    for (le = list_head(trice_lcandl(gatherer->ice)), i = 0; le != NULL; le = le->next, ++i) {
+        struct ice_lcand* re_candidate = le->data;
+
+        // Create ICE candidate
+        error = rawrtc_ice_candidate_create_from_local_candidate(
+            &candidates->candidates[i], re_candidate);
+        if (error) {
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(candidates);
+    } else {
+        // Set pointers
+        *candidatesp = candidates;
+    }
+    return error;
+}
diff --git a/src/ice_gatherer/gatherer.h b/src/ice_gatherer/gatherer.h
new file mode 100644
index 0000000..7f2d447
--- /dev/null
+++ b/src/ice_gatherer/gatherer.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <rawrtc/ice_gatherer.h>
+#include <re.h>
+#include <rew.h>
+
+enum {
+    RAWRTC_ICE_GATHERER_DNS_SERVERS = 10,
+    RAWRTC_ICE_USERNAME_FRAGMENT_LENGTH = 16,
+    RAWRTC_ICE_PASSWORD_LENGTH = 32,
+};
+
+struct rawrtc_ice_gatherer {
+    enum rawrtc_ice_gatherer_state state;
+    struct rawrtc_ice_gather_options* options;  // referenced
+    rawrtc_ice_gatherer_state_change_handler state_change_handler;  // nullable
+    rawrtc_ice_gatherer_error_handler error_handler;  // nullable
+    rawrtc_ice_gatherer_local_candidate_handler local_candidate_handler;  // nullable
+    void* arg;  // nullable
+    struct tmr timeout_timer;
+    struct list url_addresses;
+    struct list url_resolvers;
+    struct list buffered_messages;  // TODO: Can this be added to the candidates list?
+    struct list local_candidates;  // TODO: Hash list instead?
+    char ice_username_fragment[RAWRTC_ICE_USERNAME_FRAGMENT_LENGTH + 1];
+    char ice_password[RAWRTC_ICE_PASSWORD_LENGTH + 1];
+    struct trice* ice;
+    struct trice_conf ice_config;
+    struct dnsc* dns_client;
+};
diff --git a/src/ice_gatherer/meson.build b/src/ice_gatherer/meson.build
new file mode 100644
index 0000000..1276f93
--- /dev/null
+++ b/src/ice_gatherer/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'gatherer.c',
+    'utils.c',
+])
diff --git a/src/ice_gatherer/utils.c b/src/ice_gatherer/utils.c
new file mode 100644
index 0000000..982a5cb
--- /dev/null
+++ b/src/ice_gatherer/utils.c
@@ -0,0 +1,19 @@
+#include <rawrtc/ice_gatherer.h>
+
+/*
+ * Get the corresponding name for an ICE gatherer state.
+ */
+char const* rawrtc_ice_gatherer_state_to_name(enum rawrtc_ice_gatherer_state const state) {
+    switch (state) {
+        case RAWRTC_ICE_GATHERER_STATE_NEW:
+            return "new";
+        case RAWRTC_ICE_GATHERER_STATE_GATHERING:
+            return "gathering";
+        case RAWRTC_ICE_GATHERER_STATE_COMPLETE:
+            return "complete";
+        case RAWRTC_ICE_GATHERER_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/ice_parameters/attributes.c b/src/ice_parameters/attributes.c
new file mode 100644
index 0000000..a4bf66f
--- /dev/null
+++ b/src/ice_parameters/attributes.c
@@ -0,0 +1,54 @@
+#include "parameters.h"
+#include <rawrtc/ice_parameters.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Get the ICE parameter's username fragment value.
+ * `*username_fragmentp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!username_fragmentp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *username_fragmentp = mem_ref(parameters->username_fragment);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the ICE parameter's password value.
+ * `*passwordp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_password(
+    char** const passwordp,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!passwordp || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set pointer (and reference)
+    *passwordp = mem_ref(parameters->password);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the ICE parameter's ICE lite value.
+ */
+enum rawrtc_code rawrtc_ice_parameters_get_ice_lite(
+    bool* const ice_litep,  // de-referenced
+    struct rawrtc_ice_parameters* const parameters) {
+    // Check arguments
+    if (!ice_litep || !parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set value
+    *ice_litep = parameters->ice_lite;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_parameters/meson.build b/src/ice_parameters/meson.build
new file mode 100644
index 0000000..8710eb0
--- /dev/null
+++ b/src/ice_parameters/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'parameters.c',
+    'utils.c',
+])
diff --git a/src/ice_parameters/parameters.c b/src/ice_parameters/parameters.c
new file mode 100644
index 0000000..e36cffb
--- /dev/null
+++ b/src/ice_parameters/parameters.c
@@ -0,0 +1,60 @@
+#include "parameters.h"
+#include <rawrtc/ice_parameters.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Destructor for an existing ICE parameters instance.
+ */
+static void rawrtc_ice_parameters_destroy(void* arg) {
+    struct rawrtc_ice_parameters* const parameters = arg;
+
+    // Un-reference
+    mem_deref(parameters->username_fragment);
+    mem_deref(parameters->password);
+}
+
+/*
+ * Create a new ICE parameters instance.
+ * `*parametersp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_parameters_create(
+    struct rawrtc_ice_parameters** const parametersp,  // de-referenced
+    char* const username_fragment,  // copied
+    char* const password,  // copied
+    bool const ice_lite) {
+    struct rawrtc_ice_parameters* parameters;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!parametersp || !username_fragment || !password) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    parameters = mem_zalloc(sizeof(*parameters), rawrtc_ice_parameters_destroy);
+    if (!parameters) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/copy
+    error = rawrtc_strdup(&parameters->username_fragment, username_fragment);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_strdup(&parameters->password, password);
+    if (error) {
+        goto out;
+    }
+    parameters->ice_lite = ice_lite;
+
+out:
+    if (error) {
+        mem_deref(parameters);
+    } else {
+        // Set pointer
+        *parametersp = parameters;
+    }
+    return error;
+}
diff --git a/src/ice_parameters/parameters.h b/src/ice_parameters/parameters.h
new file mode 100644
index 0000000..a425ca9
--- /dev/null
+++ b/src/ice_parameters/parameters.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <re.h>
+
+struct rawrtc_ice_parameters {
+    char* username_fragment;  // copied
+    char* password;  // copied
+    bool ice_lite;
+};
+
+int rawrtc_ice_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_ice_parameters const* const parameters);
diff --git a/src/ice_parameters/utils.c b/src/ice_parameters/utils.c
new file mode 100644
index 0000000..53bfdb7
--- /dev/null
+++ b/src/ice_parameters/utils.c
@@ -0,0 +1,29 @@
+#include "parameters.h"
+#include <re.h>
+
+/*
+ * Print debug information for ICE parameters.
+ */
+int rawrtc_ice_parameters_debug(
+    struct re_printf* const pf, struct rawrtc_ice_parameters const* const parameters) {
+    int err = 0;
+
+    // Check arguments
+    if (!parameters) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Parameters <%p>:\n", parameters);
+
+    // Username fragment
+    err |= re_hprintf(pf, "    username_fragment=\"%s\"\n", parameters->username_fragment);
+
+    // Password
+    err |= re_hprintf(pf, "    password=\"%s\"\n", parameters->password);
+
+    // ICE lite
+    err |= re_hprintf(pf, "    ice_lite=%s\n", parameters->ice_lite ? "yes" : "no");
+
+    // Done
+    return err;
+}
diff --git a/src/ice_server/address.c b/src/ice_server/address.c
new file mode 100644
index 0000000..a6fb176
--- /dev/null
+++ b/src/ice_server/address.c
@@ -0,0 +1,47 @@
+#include "address.h"
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Destructor for an ICE server URL address.
+ */
+static void rawrtc_ice_server_url_address_destroy(void* arg) {
+    struct rawrtc_ice_server_url_address* const address = arg;
+
+    // Remove from list
+    list_unlink(&address->le);
+
+    // Un-reference
+    mem_deref(address->url);
+}
+
+/*
+ * Create an ICE server URL address.
+ */
+enum rawrtc_code rawrtc_ice_server_url_address_create(
+    struct rawrtc_ice_server_url_address** const addressp,  // de-referenced
+    struct rawrtc_ice_server_url* const url,  // referenced
+    struct sa* const address  // copied
+) {
+    struct rawrtc_ice_server_url_address* url_address;
+
+    // Check arguments
+    if (!addressp || !url || !address) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    url_address = mem_zalloc(sizeof(*url_address), rawrtc_ice_server_url_address_destroy);
+    if (!url_address) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    url_address->url = mem_ref(url);
+    url_address->address = *address;
+
+    // Set pointer & done
+    *addressp = url_address;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_server/address.h b/src/ice_server/address.h
new file mode 100644
index 0000000..d39fc25
--- /dev/null
+++ b/src/ice_server/address.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server URL's resolved address.
+ */
+struct rawrtc_ice_server_url_address {
+    struct le le;
+    struct rawrtc_ice_server_url* url;  // referenced
+    struct sa address;
+};
+
+enum rawrtc_code rawrtc_ice_server_url_address_create(
+    struct rawrtc_ice_server_url_address** const addressp,  // de-referenced
+    struct rawrtc_ice_server_url* const url,  // referenced
+    struct sa* const address  // copied
+);
diff --git a/src/ice_server/meson.build b/src/ice_server/meson.build
new file mode 100644
index 0000000..0135c72
--- /dev/null
+++ b/src/ice_server/meson.build
@@ -0,0 +1,6 @@
+sources += files([
+    'address.c',
+    'resolver.c',
+    'server.c',
+    'utils.c',
+])
diff --git a/src/ice_server/resolver.c b/src/ice_server/resolver.c
new file mode 100644
index 0000000..65ed350
--- /dev/null
+++ b/src/ice_server/resolver.c
@@ -0,0 +1,182 @@
+#include "resolver.h"
+#include "address.h"
+#include "server.h"
+#include <rawrtc/config.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+#define DEBUG_MODULE "ice-server-url-resolver"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * DNS A or AAAA record handler.
+ */
+static bool dns_record_result_handler(struct dnsrr* resource_record, void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+    struct rawrtc_ice_server_url* const url = resolver->url;
+    struct sa address;
+    enum rawrtc_code error;
+    struct rawrtc_ice_server_url_address* url_address;
+    bool stop;
+    DEBUG_PRINTF("DNS resource record: %H\n", dns_rr_print, resource_record);
+
+    // Set IP address
+    sa_cpy(&address, &url->resolved_address);
+    switch (resource_record->type) {
+        case DNS_TYPE_A:
+            // Set IPv4 address
+            sa_set_in(&address, resource_record->rdata.a.addr, sa_port(&address));
+            break;
+
+        case DNS_TYPE_AAAA:
+            // Set IPv6 address
+            sa_set_in6(&address, resource_record->rdata.aaaa.addr, sa_port(&address));
+            break;
+
+        default:
+            DEBUG_WARNING(
+                "Invalid DNS resource record, expected A/AAAA record, got: %H\n", dns_rr_print,
+                resource_record);
+            return true;  // stop traversing
+    }
+
+    // Create URL address
+    error = rawrtc_ice_server_url_address_create(&url_address, url, &address);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to create ICE server URL address, reason: %s\n", rawrtc_code_to_str(error));
+        return true;  // stop traversing
+    }
+
+    // Announce resolved IP address
+    stop = resolver->address_handler(url_address, resolver->arg);
+
+    // Un-reference
+    mem_deref(url_address);
+
+    // Done (continue or stop traversing)
+    return stop;
+}
+
+/*
+ * DNS query result handler.
+ */
+static void dns_query_handler(
+    int err,
+    struct dnshdr const* header,
+    struct list* answer_records,
+    struct list* authoritive_records,
+    struct list* additional_records,
+    void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+    (void) header;
+    (void) authoritive_records;
+    (void) additional_records;
+
+    // Handle error (if any)
+    if (err) {
+        DEBUG_WARNING("Could not query DNS record for '%r', reason: %m\n", &resolver->url->host);
+        goto out;
+    } else if (header->rcode != 0) {
+        DEBUG_NOTICE(
+            "DNS record query for '%r' unsuccessful: %s (%" PRIu8 ")\n", &resolver->url->host,
+            dns_hdr_rcodename(header->rcode), header->rcode);
+        goto out;
+    }
+
+    // Unlink self from any list
+    list_unlink(&resolver->le);
+
+    // Handle A or AAAA record
+    dns_rrlist_apply2(
+        answer_records, NULL, DNS_TYPE_A, DNS_TYPE_AAAA, DNS_CLASS_IN, true,
+        dns_record_result_handler, resolver);
+
+out:
+    // Unlink & un-reference self
+    // Note: We're unlinking twice here since the above unlink may be skipped in an error case.
+    //       This is perfectly safe.
+    list_unlink(&resolver->le);
+    mem_deref(resolver);
+}
+
+/*
+ * Destructor for an ICE server URL.
+ */
+static void rawrtc_ice_server_url_resolver_destroy(void* arg) {
+    struct rawrtc_ice_server_url_resolver* const resolver = arg;
+
+    // Remove from list
+    list_unlink(&resolver->le);
+
+    // Un-reference
+    mem_deref(resolver->dns_query);
+    mem_deref(resolver->url);
+}
+
+/*
+ * Create an ICE server URL resolver.
+ *
+ * Important: Once the handler has been called, the resolver will unlink
+ *            from an associated list and un-reference itself.
+ */
+enum rawrtc_code rawrtc_ice_server_url_resolver_create(
+    struct rawrtc_ice_server_url_resolver** const resolverp,  // de-referenced
+    struct dnsc* const dns_client,
+    uint_fast16_t const dns_type,
+    struct rawrtc_ice_server_url* const url,  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler,
+    void* const arg) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_server_url_resolver* resolver;
+    char* host_str;
+
+    // Check arguments
+    if (!resolverp || !dns_client || !url || !address_handler) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    resolver = mem_zalloc(sizeof(*resolver), rawrtc_ice_server_url_resolver_destroy);
+    if (!resolver) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    resolver->url = mem_ref(url);
+    resolver->address_handler = address_handler;
+    resolver->arg = arg;
+    resolver->dns_type = dns_type;
+
+    // Copy URL to str
+    error = rawrtc_error_to_code(pl_strdup(&host_str, &url->host));
+    if (error) {
+        goto out;
+    }
+
+    // Query A or AAAA record
+    error = rawrtc_error_to_code(dnsc_query(
+        &resolver->dns_query, dns_client, host_str, (uint16_t) dns_type, DNS_CLASS_IN, true,
+        dns_query_handler, resolver));
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(host_str);
+
+    if (error) {
+        mem_deref(resolver);
+    } else {
+        // Set pointer & done
+        *resolverp = resolver;
+    }
+
+    return error;
+}
diff --git a/src/ice_server/resolver.h b/src/ice_server/resolver.h
new file mode 100644
index 0000000..63e9c0f
--- /dev/null
+++ b/src/ice_server/resolver.h
@@ -0,0 +1,38 @@
+#pragma once
+#include "address.h"
+#include "server.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server URL address resolved handler.
+ *
+ * `*resolverp` must be referenced if used.
+ *
+ * Return `true` if you want to continue receiving further addresses
+ * from the URL's address entry. Be aware that you will be offered at
+ * least one IPv4 address and one IPv6 address per URL (if available)
+ * even if you always return `false`.
+ */
+typedef bool (*rawrtc_ice_server_url_address_resolved_handler)(
+    struct rawrtc_ice_server_url_address* const address, void* const arg);
+
+/*
+ * ICE server URL resolver.
+ */
+struct rawrtc_ice_server_url_resolver {
+    struct le le;
+    struct rawrtc_ice_server_url* url;  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler;
+    void* arg;
+    uint_fast16_t dns_type;
+    struct dns_query* dns_query;
+};
+
+enum rawrtc_code rawrtc_ice_server_url_resolver_create(
+    struct rawrtc_ice_server_url_resolver** const resolverp,  // de-referenced
+    struct dnsc* const dns_client,
+    uint_fast16_t const dns_type,
+    struct rawrtc_ice_server_url* const url,  // referenced
+    rawrtc_ice_server_url_address_resolved_handler address_handler,
+    void* const arg);
diff --git a/src/ice_server/server.c b/src/ice_server/server.c
new file mode 100644
index 0000000..2c52d0d
--- /dev/null
+++ b/src/ice_server/server.c
@@ -0,0 +1,449 @@
+#include "server.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "ice-server"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * ICE server URL-related regular expressions.
+ */
+static char const ice_server_url_regex[] = "[a-z]+:[^?]+[^]*";
+static char const ice_server_host_port_regex[] = "[^:]+[:]*[0-9]*";
+static char const ice_server_host_port_ipv6_regex[] = "\\[[0-9a-f:]+\\][:]*[0-9]*";
+static char const ice_server_transport_regex[] = "\\?transport=[a-z]+";
+
+/*
+ * Valid ICE server schemes.
+ *
+ * Note: Update `ice_server_scheme_type_mapping`,
+ * `ice_server_scheme_secure_mapping` and
+ * `ice_server_scheme_port_mapping` if changed.
+ */
+static char const* const ice_server_schemes[] = {
+    "stun",
+    "stuns",
+    "turn",
+    "turns",
+};
+static size_t const ice_server_schemes_length = ARRAY_SIZE(ice_server_schemes);
+
+/*
+ * ICE server scheme to server type mapping.
+ */
+static enum rawrtc_ice_server_type ice_server_scheme_type_mapping[] = {
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+};
+
+/*
+ * ICE server scheme to secure mapping.
+ */
+static bool ice_server_scheme_secure_mapping[] = {
+    false,
+    true,
+    false,
+    true,
+};
+
+/*
+ * ICE server scheme to default port mapping.
+ */
+static uint_fast16_t ice_server_scheme_port_mapping[] = {
+    3478,
+    5349,
+    3478,
+    5349,
+};
+
+/*
+ * Valid ICE server transports.
+ *
+ * Note: Update `ice_server_transport_normal_transport_mapping` and
+ * `ice_server_transport_secure_transport_mapping` if changed.
+ */
+static char const* const ice_server_transports[] = {
+    "udp",
+    "tcp",
+};
+static size_t const ice_server_transports_length = ARRAY_SIZE(ice_server_transports);
+
+/*
+ * ICE server transport to non-secure transport mapping.
+ */
+static enum rawrtc_ice_server_transport ice_server_transport_normal_transport_mapping[] = {
+    RAWRTC_ICE_SERVER_TRANSPORT_UDP,
+    RAWRTC_ICE_SERVER_TRANSPORT_TCP,
+};
+
+/*
+ * ICE server transport to secure transport mapping.
+ */
+static enum rawrtc_ice_server_transport ice_server_transport_secure_transport_mapping[] = {
+    RAWRTC_ICE_SERVER_TRANSPORT_DTLS,
+    RAWRTC_ICE_SERVER_TRANSPORT_TLS,
+};
+
+/*
+ * Parse ICE server's transport.
+ */
+static enum rawrtc_code decode_ice_server_transport(
+    enum rawrtc_ice_server_transport* const transportp,  // de-referenced, not checked
+    struct pl* const query,  // not checked
+    bool const secure) {
+    enum rawrtc_code error;
+    struct pl transport;
+    size_t i;
+
+    // Decode transport
+    error =
+        rawrtc_error_to_code(re_regex(query->p, query->l, ice_server_transport_regex, &transport));
+    if (error) {
+        return error;
+    }
+
+    // Translate transport to ICE server transport
+    for (i = 0; i < ice_server_transports_length; ++i) {
+        if (pl_strcmp(&transport, ice_server_transports[i]) == 0) {
+            if (!secure) {
+                *transportp = ice_server_transport_normal_transport_mapping[i];
+            } else {
+                *transportp = ice_server_transport_secure_transport_mapping[i];
+            }
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_INVALID_ARGUMENT;
+}
+
+/*
+ * Parse an ICE scheme to an ICE server type, 'secure' flag and
+ * default port.
+ */
+static enum rawrtc_code decode_ice_server_scheme(
+    enum rawrtc_ice_server_type* const typep,  // de-referenced, not checked
+    bool* const securep,  // de-referenced, not checked
+    uint_fast16_t* const portp,  // de-referenced, not checked
+    struct pl* const scheme  // not checked
+) {
+    size_t i;
+
+    // Translate scheme to ICE server type (and set if secure)
+    for (i = 0; i < ice_server_schemes_length; ++i) {
+        if (pl_strcmp(scheme, ice_server_schemes[i]) == 0) {
+            // Set values
+            *typep = ice_server_scheme_type_mapping[i];
+            *securep = ice_server_scheme_secure_mapping[i];
+            *portp = ice_server_scheme_port_mapping[i];
+
+            // Done
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    // Not found
+    return RAWRTC_CODE_INVALID_ARGUMENT;
+}
+
+/*
+ * Parse an ICE server URL according to RFC 7064 and RFC 7065
+ * (although the `transport` part is inaccurate for RFC 7064 but it
+ * seems useful)
+ */
+static enum rawrtc_code decode_ice_server_url(
+    struct rawrtc_ice_server_url* const url  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl scheme;
+    struct pl host_port;
+    struct pl query;
+    bool secure;
+    struct pl port_pl;
+    uint_fast16_t port;
+
+    // Decode URL
+    error = rawrtc_error_to_code(
+        re_regex(url->url, strlen(url->url), ice_server_url_regex, &scheme, &host_port, &query));
+    if (error) {
+        DEBUG_WARNING("Invalid ICE server URL: %s\n", url->url);
+        goto out;
+    }
+
+    // TODO: Can scheme or host be NULL?
+
+    // Get server type, secure flag and default port from scheme
+    error = decode_ice_server_scheme(&url->type, &secure, &port, &scheme);
+    if (error) {
+        DEBUG_WARNING("Invalid scheme in ICE server URL (%s): %r\n", url->url, &scheme);
+        goto out;
+    }
+
+    // Set default address
+    sa_set_in(&url->resolved_address, INADDR_ANY, (uint16_t) port);
+
+    // Decode host: Either IPv4 or IPv6 including the port (if any)
+    // Try IPv6 first, then normal hostname/IPv4.
+    error = rawrtc_error_to_code(re_regex(
+        host_port.p, host_port.l, ice_server_host_port_ipv6_regex, &url->host, NULL, &port_pl));
+    if (error) {
+        error = rawrtc_error_to_code(re_regex(
+            host_port.p, host_port.l, ice_server_host_port_regex, &url->host, NULL, &port_pl));
+        if (error) {
+            DEBUG_WARNING(
+                "Invalid host or port in ICE server URL (%s): %r\n", url->url, &host_port);
+            goto out;
+        }
+
+        // Try decoding IPv4
+        sa_set(&url->resolved_address, &url->host, (uint16_t) port);
+    } else {
+        // Try decoding IPv6
+        error = rawrtc_error_to_code(sa_set(&url->resolved_address, &url->host, (uint16_t) port));
+        if (error) {
+            DEBUG_WARNING(
+                "Invalid IPv6 address in ICE server URL (%s): %r\n", url->url, &host_port);
+            goto out;
+        }
+    }
+
+    // Decode port (if any)
+    if (pl_isset(&port_pl)) {
+        uint_fast32_t port_u32;
+
+        // Get port
+        port_u32 = pl_u32(&port_pl);
+        if (port_u32 == 0 || port_u32 > UINT16_MAX) {
+            DEBUG_WARNING(
+                "Invalid port number in ICE server URL (%s): %" PRIu32 "\n", url->url, port_u32);
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Set port
+        sa_set_port(&url->resolved_address, (uint16_t) port_u32);
+    }
+
+    // Translate transport (if any) & secure flag to ICE server transport
+    if (pl_isset(&query)) {
+        error = decode_ice_server_transport(&url->transport, &query, secure);
+        if (error) {
+            DEBUG_WARNING("Invalid transport in ICE server URL (%s): %r\n", url->url, &query);
+            goto out;
+        }
+    } else {
+        // Set default transport (depending on secure flag)
+        if (secure) {
+            url->transport = rawrtc_default_config.ice_server_secure_transport;
+        } else {
+            url->transport = rawrtc_default_config.ice_server_normal_transport;
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    return error;
+}
+
+/*
+ * Destructor for URLs of the ICE gatherer.
+ */
+static void rawrtc_ice_server_url_destroy(void* arg) {
+    struct rawrtc_ice_server_url* const url = arg;
+
+    // Remove from list
+    list_unlink(&url->le);
+
+    // Un-reference
+    mem_deref(url->url);
+}
+
+/*
+ * Copy a URL for the ICE gatherer.
+ */
+static enum rawrtc_code rawrtc_ice_server_url_create(
+    struct rawrtc_ice_server_url** const urlp,  // de-referenced
+    char* const url_s  // copied
+) {
+    struct rawrtc_ice_server_url* url;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!urlp || !url_s) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    url = mem_zalloc(sizeof(*url), rawrtc_ice_server_url_destroy);
+    if (!url) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Copy URL
+    error = rawrtc_strdup(&url->url, url_s);
+    if (error) {
+        goto out;
+    }
+
+    // Parse URL
+    // Note: `url->host` points inside `url->url`, so we MUST have copied the URL first.
+    error = decode_ice_server_url(url);
+    if (error) {
+        goto out;
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    if (error) {
+        mem_deref(url);
+    } else {
+        // Set pointer
+        *urlp = url;
+    }
+    return error;
+}
+
+/*
+ * Destructor for an existing ICE server.
+ */
+static void rawrtc_ice_server_destroy(void* arg) {
+    struct rawrtc_ice_server* const server = arg;
+
+    // Un-reference
+    list_flush(&server->urls);
+    mem_deref(server->username);
+    mem_deref(server->credential);
+}
+
+/*
+ * Create an ICE server.
+ */
+enum rawrtc_code rawrtc_ice_server_create(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+    size_t i;
+
+    // Check arguments
+    if (!serverp || !urls) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    server = mem_zalloc(sizeof(*server), rawrtc_ice_server_destroy);
+    if (!server) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Copy URLs to list
+    list_init(&server->urls);
+    for (i = 0; i < n_urls; ++i) {
+        struct rawrtc_ice_server_url* url;
+
+        // Ensure URLs aren't null
+        if (!urls[i]) {
+            error = RAWRTC_CODE_INVALID_ARGUMENT;
+            goto out;
+        }
+
+        // Copy URL
+        error = rawrtc_ice_server_url_create(&url, urls[i]);
+        if (error) {
+            goto out;
+        }
+
+        // Append URL to list
+        list_append(&server->urls, &url->le, url);
+    }
+
+    // Set fields
+    if (credential_type != RAWRTC_ICE_CREDENTIAL_TYPE_NONE) {
+        if (username) {
+            error = rawrtc_strdup(&server->username, username);
+            if (error) {
+                goto out;
+            }
+        }
+        if (credential) {
+            error = rawrtc_strdup(&server->credential, credential);
+            if (error) {
+                goto out;
+            }
+        }
+    }
+    server->credential_type = credential_type;  // TODO: Validation needed in case TOKEN is used?
+
+out:
+    if (error) {
+        mem_deref(server);
+    } else {
+        // Set pointer
+        *serverp = server;
+    }
+    return error;
+}
+
+/*
+ * Copy an ICE server.
+ */
+enum rawrtc_code rawrtc_ice_server_copy(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    struct rawrtc_ice_server* const source_server) {
+    size_t n_urls;
+    char** urls = NULL;
+    struct le* le;
+    size_t i;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!serverp || !source_server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Create temporary ICE server URL array
+    n_urls = list_count(&source_server->urls);
+    if (n_urls > 0) {
+        urls = mem_alloc(sizeof(char*) * n_urls, NULL);
+        if (!urls) {
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+    }
+
+    // Copy ICE server URL (str) pointers
+    for (le = list_head(&source_server->urls), i = 0; le != NULL; le = le->next, ++i) {
+        struct rawrtc_ice_server_url* const url = le->data;
+        urls[i] = url->url;
+    }
+
+    // Copy
+    error = rawrtc_ice_server_create(
+        serverp, urls, n_urls, source_server->username, source_server->credential,
+        source_server->credential_type);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(urls);
+    return error;
+}
diff --git a/src/ice_server/server.h b/src/ice_server/server.h
new file mode 100644
index 0000000..da9c9a6
--- /dev/null
+++ b/src/ice_server/server.h
@@ -0,0 +1,49 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * ICE server type.
+ * Note: Update `ice_server_schemes` if changed.
+ */
+enum rawrtc_ice_server_type {
+    RAWRTC_ICE_SERVER_TYPE_STUN,
+    RAWRTC_ICE_SERVER_TYPE_TURN,
+};
+
+struct rawrtc_ice_server {
+    struct le le;
+    struct list urls;  // deep-copied
+    char* username;  // copied
+    char* credential;  // copied
+    enum rawrtc_ice_credential_type credential_type;
+};
+
+/*
+ * ICE server URL. (list element)
+ */
+struct rawrtc_ice_server_url {
+    struct le le;
+    char* url;  // copied
+    struct pl host;  // points inside `url`
+    enum rawrtc_ice_server_type type;
+    enum rawrtc_ice_server_transport transport;
+    struct sa resolved_address;
+};
+
+enum rawrtc_code rawrtc_ice_server_create(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type);
+
+enum rawrtc_code rawrtc_ice_server_copy(
+    struct rawrtc_ice_server** const serverp,  // de-referenced
+    struct rawrtc_ice_server* const source_server);
+
+int rawrtc_ice_server_debug(
+    struct re_printf* const pf, struct rawrtc_ice_server const* const server);
diff --git a/src/ice_server/utils.c b/src/ice_server/utils.c
new file mode 100644
index 0000000..0b1ac90
--- /dev/null
+++ b/src/ice_server/utils.c
@@ -0,0 +1,103 @@
+#include "server.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+/*
+ * Get the corresponding name for an ICE server type.
+ */
+static char const* ice_server_type_to_name(enum rawrtc_ice_server_type const type) {
+    switch (type) {
+        case RAWRTC_ICE_SERVER_TYPE_STUN:
+            return "stun";
+        case RAWRTC_ICE_SERVER_TYPE_TURN:
+            return "turn";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for an ICE server transport.
+ */
+static char const* ice_server_transport_to_name(enum rawrtc_ice_server_transport const transport) {
+    switch (transport) {
+        case RAWRTC_ICE_SERVER_TRANSPORT_UDP:
+            return "udp";
+        case RAWRTC_ICE_SERVER_TRANSPORT_TCP:
+            return "tcp";
+        case RAWRTC_ICE_SERVER_TRANSPORT_DTLS:
+            return "dtls";
+        case RAWRTC_ICE_SERVER_TRANSPORT_TLS:
+            return "tls";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for an ICE credential type.
+ */
+static char const* ice_credential_type_to_name(enum rawrtc_ice_credential_type const type) {
+    switch (type) {
+        case RAWRTC_ICE_CREDENTIAL_TYPE_NONE:
+            return "n/a";
+        case RAWRTC_ICE_CREDENTIAL_TYPE_PASSWORD:
+            return "password";
+        case RAWRTC_ICE_CREDENTIAL_TYPE_TOKEN:
+            return "token";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Print debug information for an ICE server.
+ */
+int rawrtc_ice_server_debug(
+    struct re_printf* const pf, struct rawrtc_ice_server const* const server) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!server) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "  ICE Server <%p>:\n", server);
+
+    // Credential type
+    err |= re_hprintf(
+        pf, "    credential_type=%s\n", ice_credential_type_to_name(server->credential_type));
+    if (server->credential_type != RAWRTC_ICE_CREDENTIAL_TYPE_NONE) {
+        // Username
+        err |= re_hprintf(pf, "    username=");
+        if (server->username) {
+            err |= re_hprintf(pf, "\"%s\"\n", server->username);
+        } else {
+            err |= re_hprintf(pf, "n/a\n");
+        }
+
+        // Credential
+        err |= re_hprintf(pf, "    credential=");
+        if (server->credential) {
+            err |= re_hprintf(pf, "\"%s\"\n", server->credential);
+        } else {
+            err |= re_hprintf(pf, "n/a\n");
+        }
+    }
+
+    // URLs
+    for (le = list_head(&server->urls); le != NULL; le = le->next) {
+        struct rawrtc_ice_server_url* const url = le->data;
+
+        // URL, STUN/TURN, transport, currently gathering?
+        err |= re_hprintf(
+            pf, "    URL=\"%s\" type=%s transport=%s resolved=%s\n", url->url,
+            ice_server_type_to_name(url->type), ice_server_transport_to_name(url->transport),
+            sa_is_any(&url->resolved_address) ? "no" : "yes");
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/ice_transport/attributes.c b/src/ice_transport/attributes.c
new file mode 100644
index 0000000..36956f0
--- /dev/null
+++ b/src/ice_transport/attributes.c
@@ -0,0 +1,58 @@
+#include "transport.h"
+#include "../ice_gatherer/gatherer.h"
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the current ICE role of the ICE transport.
+ * Return `RAWRTC_CODE_NO_VALUE` code in case the ICE role has not been
+ * determined yet.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    struct rawrtc_ice_transport* const transport) {
+    enum ice_role re_role;
+    enum rawrtc_code error;
+    enum rawrtc_ice_role role;
+
+    // Check arguments
+    if (!rolep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get libre role from ICE instance
+    re_role = trice_local_role(transport->gatherer->ice);
+
+    // Translate role
+    error = rawrtc_re_ice_role_to_ice_role(&role, re_role);
+    if (error) {
+        return error;
+    }
+
+    // Unknown?
+    if (re_role == ICE_ROLE_UNKNOWN) {
+        return RAWRTC_CODE_NO_VALUE;
+    } else {
+        // Set pointer
+        *rolep = role;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current state of the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_get_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_ice_transport* const transport) {
+    // Check arguments
+    if (!statep || !transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state & done
+    *statep = transport->state;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_transport/meson.build b/src/ice_transport/meson.build
new file mode 100644
index 0000000..0227c31
--- /dev/null
+++ b/src/ice_transport/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'transport.c',
+    'utils.c',
+])
diff --git a/src/ice_transport/transport.c b/src/ice_transport/transport.c
new file mode 100644
index 0000000..a6c914e
--- /dev/null
+++ b/src/ice_transport/transport.c
@@ -0,0 +1,579 @@
+#include "transport.h"
+#include "../dtls_transport/transport.h"
+#include "../ice_candidate/candidate.h"
+#include "../ice_candidate/helper.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_parameters/parameters.h"
+#include "../main/config.h"
+#include <rawrtc/config.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+#include <rew.h>
+
+#define DEBUG_MODULE "ice-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Destructor for an existing ICE transport.
+ */
+static void rawrtc_ice_transport_destroy(void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+
+    // Stop transport
+    // TODO: Check effects in case transport has been destroyed due to error in create
+    rawrtc_ice_transport_stop(transport);
+
+    // Un-reference
+    mem_deref(transport->stun_client);
+    mem_deref(transport->remote_parameters);
+    mem_deref(transport->gatherer);
+}
+
+/*
+ * Create a new ICE transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_ice_transport_create(
+    struct rawrtc_ice_transport** const transportp,  // de-referenced
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced, nullable
+    rawrtc_ice_transport_state_change_handler const state_change_handler,  // nullable
+    rawrtc_ice_transport_candidate_pair_change_handler const
+        candidate_pair_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_ice_transport* transport;
+    struct stun_conf stun_config = {
+        // TODO: Make this configurable!
+        .rto = 100,  // 100ms
+        .rc = 7,  // Send at: 0ms, 100ms, 300ms, 700ms, 1500ms, 3100ms, 6300ms
+        .rm = 60,  // Additional wait: 60*100 -> 6000ms
+        .ti = 12300,  // Timeout after: 12300ms
+        .tos = 0x00,
+    };
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transportp || !gatherer) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check ICE gatherer state
+    // TODO: Check if gatherer.component is RTCP -> invalid state
+    if (gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Allocate
+    transport = mem_zalloc(sizeof(*transport), rawrtc_ice_transport_destroy);
+    if (!transport) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    transport->state = RAWRTC_ICE_TRANSPORT_STATE_NEW;  // TODO: Raise state (delayed)?
+    transport->gatherer = mem_ref(gatherer);
+    transport->state_change_handler = state_change_handler;
+    transport->candidate_pair_change_handler = candidate_pair_change_handler;
+    transport->arg = arg;
+    transport->remote_end_of_candidates = false;
+
+    // Create STUN client
+    error = rawrtc_error_to_code(stun_alloc(&transport->stun_client, &stun_config, NULL, NULL));
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
+
+/*
+ * Change the state of the ICE transport.
+ * Will call the corresponding handler.
+ */
+static void set_state(
+    struct rawrtc_ice_transport* const transport, enum rawrtc_ice_transport_state const state) {
+    // Set state
+    transport->state = state;
+
+    // Call handler (if any)
+    if (transport->state_change_handler) {
+        transport->state_change_handler(state, transport->arg);
+    }
+}
+
+/*
+ * Check if the ICE checklist process is complete.
+ */
+static void check_ice_checklist_complete(
+    struct rawrtc_ice_transport* const transport  // not checked
+) {
+    struct trice* const ice = transport->gatherer->ice;
+
+    // Completed all candidate pairs?
+    if (trice_checklist_iscompleted(ice)) {
+        struct le;
+
+        DEBUG_INFO("Checklist completed\n");
+        DEBUG_PRINTF("%H", trice_debug, ice);
+
+        // Stop the checklist
+        trice_checklist_stop(ice);
+
+        // Remove STUN and TURN sessions from local candidate helpers since the keep-alive
+        // mechanism now moves over to the peers themselves.
+        list_apply(
+            &transport->gatherer->local_candidates, true,
+            rawrtc_candidate_helper_remove_stun_sessions_handler, NULL);
+
+        // Start keep-alive for active candidate pairs
+        // TODO: Implement!
+        //        start_keepalive(transport);
+
+        // Do we have one candidate pair that succeeded?
+        if (!list_isempty(trice_validl(ice))) {
+            // Have we received the remote end-of-candidates indication?
+            if (transport->remote_end_of_candidates) {
+                DEBUG_INFO("ICE connection completed\n");
+                set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_COMPLETED);
+            }
+        } else {
+            // No, transition to failed
+            DEBUG_INFO("ICE connection failed\n");
+            set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_FAILED);
+        }
+    }
+}
+
+/*
+ * ICE connection established callback.
+ */
+static void ice_established_handler(
+    struct ice_candpair* candidate_pair, struct stun_msg const* message, void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+    enum rawrtc_code error;
+    (void) message;
+
+    DEBUG_PRINTF("Candidate pair established: %H\n", trice_candpair_debug, candidate_pair);
+
+    // Ignore if closed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return;
+    }
+
+    // State: checking -> connected
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CHECKING) {
+        DEBUG_INFO("ICE connection established\n");
+        set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_CONNECTED);
+    }
+
+    // Ignore if completed or failed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED) {
+        return;
+    }
+
+    // Offer candidate pair to DTLS transport (if any)
+    // TODO: Offer to whatever transport lays above so we are SRTP/QUIC compatible
+    if (transport->dtls_transport) {
+        error = rawrtc_dtls_transport_add_candidate_pair(transport->dtls_transport, candidate_pair);
+        if (error) {
+            DEBUG_WARNING(
+                "DTLS transport could not attach to candidate pair, reason: %s\n",
+                rawrtc_code_to_str(error));
+
+            // Important: Removing a candidate pair can lead to segfaults due to STUN transaction
+            //            timers looking up the pair. Don't do it!
+        }
+    }
+
+    // TODO: Call candidate_pair_change_handler (?)
+
+    // ICE checklist process complete?
+    check_ice_checklist_complete(transport);
+}
+
+/*
+ * ICE connection failed callback.
+ */
+static void ice_failed_handler(
+    int err, uint16_t stun_code, struct ice_candpair* candidate_pair, void* arg) {
+    struct rawrtc_ice_transport* const transport = arg;
+    (void) err;
+    (void) stun_code;
+    (void) candidate_pair;
+
+    DEBUG_PRINTF(
+        "Candidate pair failed: %H (%m %" PRIu16 ")\n", trice_candpair_debug, candidate_pair, err,
+        stun_code);
+
+    // Ignore if closed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return;
+    }
+
+    // Ignore if completed or failed
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED) {
+        return;
+    }
+
+    // ICE checklist process complete?
+    check_ice_checklist_complete(transport);
+
+    // Important: Removing the failed candidate pair can lead to segfaults due to STUN transaction
+    //            timers looking up the pair. Don't do it!
+}
+
+/*
+ * Start the ICE transport.
+ * TODO https://github.com/w3c/ortc/issues/607
+ */
+enum rawrtc_code rawrtc_ice_transport_start(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_gatherer* const gatherer,  // referenced
+    struct rawrtc_ice_parameters* const remote_parameters,  // referenced
+    enum rawrtc_ice_role const role) {
+    bool ice_transport_closed;
+    bool ice_gatherer_closed;
+    enum ice_role translated_role;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !gatherer || !remote_parameters) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate parameters
+    if (!remote_parameters->username_fragment || !remote_parameters->password) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Handle ICE lite
+    if (remote_parameters->ice_lite) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // TODO: Check that components of ICE gatherer and ICE transport match
+
+    // Check state
+    ice_transport_closed = transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED;
+    ice_gatherer_closed = gatherer->state == RAWRTC_ICE_GATHERER_STATE_CLOSED;
+    if (ice_transport_closed || ice_gatherer_closed) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Handle ICE restart when called again
+    if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check if gatherer instance is different
+    // TODO https://github.com/w3c/ortc/issues/607
+    if (transport->gatherer != gatherer) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Set role (abort if unknown or something entirely weird)
+    translated_role = rawrtc_ice_role_to_re_ice_role(role);
+    error = rawrtc_error_to_code(trice_set_role(transport->gatherer->ice, translated_role));
+    if (error) {
+        return error;
+    }
+
+    // New/first remote parameters?
+    if (transport->remote_parameters != remote_parameters) {
+        // Apply username fragment and password on trice
+        error = rawrtc_error_to_code(
+            trice_set_remote_ufrag(transport->gatherer->ice, remote_parameters->username_fragment));
+        if (error) {
+            return error;
+        }
+        error = rawrtc_error_to_code(
+            trice_set_remote_pwd(transport->gatherer->ice, remote_parameters->password));
+        if (error) {
+            return error;
+        }
+
+        // Replace
+        mem_deref(transport->remote_parameters);
+        transport->remote_parameters = mem_ref(remote_parameters);
+    }
+
+    // Set state to checking
+    // TODO: Get more states from trice
+    set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_CHECKING);
+
+    // Start checklist (if remote candidates exist)
+    if (!list_isempty(trice_rcandl(transport->gatherer->ice))) {
+        // TODO: Get config from struct
+        DEBUG_INFO("Starting checklist due to start event\n");
+        error = rawrtc_error_to_code(trice_checklist_start(
+            transport->gatherer->ice, transport->stun_client, rawrtc_default_config.pacing_interval,
+            ice_established_handler, ice_failed_handler, transport));
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Stop and close the ICE transport.
+ */
+enum rawrtc_code rawrtc_ice_transport_stop(struct rawrtc_ice_transport* const transport) {
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Already closed?
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Stop ICE checklist (if running)
+    if (trice_checklist_isrunning(transport->gatherer->ice)) {
+        trice_checklist_stop(transport->gatherer->ice);
+    }
+
+    // TODO: Remove remote candidates, role, username fragment and password from rew
+
+    // TODO: Remove from RTCICETransportController (once we have it)
+
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add a remote candidate ot the ICE transport.
+ * Note: 'candidate' must be NULL to inform the transport that the
+ * remote site finished gathering.
+ */
+enum rawrtc_code rawrtc_ice_transport_add_remote_candidate(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* candidate  // nullable
+) {
+    struct ice_rcand* re_candidate = NULL;
+    enum rawrtc_code error;
+    char* ip = NULL;
+    uint16_t port;
+    struct sa address = {0};
+    int af;
+    enum rawrtc_ice_protocol protocol;
+    char* foundation = NULL;
+    uint32_t priority;
+    enum rawrtc_ice_candidate_type type;
+    enum rawrtc_ice_tcp_candidate_type tcp_type;
+    char* related_address = NULL;
+
+    // Check arguments
+    if (!transport) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check ICE transport state
+    if (transport->state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_FAILED ||
+        transport->state == RAWRTC_ICE_TRANSPORT_STATE_COMPLETED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Remote site completed gathering?
+    if (!candidate) {
+        if (!transport->remote_end_of_candidates) {
+            DEBUG_PRINTF(
+                "Remote site gathering complete\n%H", trice_debug, transport->gatherer->ice);
+
+            // Transition to 'complete' if the checklist is done
+            // Note: 'completed' and 'failed' states are covered in checks above
+            if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW &&
+                !trice_checklist_isrunning(transport->gatherer->ice)) {
+                set_state(transport, RAWRTC_ICE_TRANSPORT_STATE_COMPLETED);
+            }
+
+            // Mark that we've received end-of-candidates
+            transport->remote_end_of_candidates = true;
+        }
+
+        // Done
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // New remote candidate after end-of-candidates indication?
+    if (transport->remote_end_of_candidates) {
+        DEBUG_NOTICE("Tried to add a remote candidate after end-of-candidates\n");
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Get IP and port
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_port(&port, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_error_to_code(sa_set_str(&address, ip, port));
+    if (error) {
+        goto out;
+    }
+
+    // Skip IPv4, IPv6 if requested
+    // TODO: Get config from struct
+    af = sa_af(&address);
+    if ((!rawrtc_default_config.ipv6_enable && af == AF_INET6) ||
+        (!rawrtc_default_config.ipv4_enable && af == AF_INET)) {
+        DEBUG_PRINTF("Skipping remote candidate due to IP version: %J\n", &address);
+        goto out;
+    }
+
+    // Get protocol
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+
+    // Skip UDP/TCP if requested
+    // TODO: Get config from struct
+    if ((!rawrtc_default_config.udp_enable && protocol == RAWRTC_ICE_PROTOCOL_UDP) ||
+        (!rawrtc_default_config.tcp_enable && protocol == RAWRTC_ICE_PROTOCOL_TCP)) {
+        DEBUG_PRINTF("Skipping remote candidate due to protocol: %J\n", &address);
+        goto out;
+    }
+
+    // Get necessary vars
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_type(&type, candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            // Doesn't matter what we choose here, protocol is not TCP anyway
+            tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+            break;
+        default:
+            goto out;
+    }
+
+    // Add remote candidate
+    // TODO: Set correct component ID
+    error = rawrtc_error_to_code(trice_rcand_add(
+        &re_candidate, transport->gatherer->ice, 1, foundation,
+        rawrtc_ice_protocol_to_ipproto(protocol), priority, &address,
+        rawrtc_ice_candidate_type_to_ice_cand_type(type),
+        rawrtc_ice_tcp_candidate_type_to_ice_tcptype(tcp_type)));
+    if (error) {
+        goto out;
+    }
+
+    // Set related address (if any)
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate);
+    if (!error) {
+        error = rawrtc_ice_candidate_get_related_port(&port, candidate);
+        if (!error) {
+            error = rawrtc_error_to_code(
+                sa_set_str(&re_candidate->attr.rel_addr, related_address, port));
+            if (error) {
+                goto out;
+            }
+        }
+    }
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+
+    // TODO: Add TURN permission
+
+    // Done
+    DEBUG_PRINTF("Added remote candidate: %J\n", &address);
+    error = RAWRTC_CODE_SUCCESS;
+
+    // Start checklist (if not new, not started and not completed or failed)
+    // TODO: Get config from struct
+    if (transport->state != RAWRTC_ICE_TRANSPORT_STATE_NEW &&
+        transport->state != RAWRTC_ICE_TRANSPORT_STATE_COMPLETED &&
+        transport->state != RAWRTC_ICE_TRANSPORT_STATE_FAILED &&
+        !trice_checklist_isrunning(transport->gatherer->ice)) {
+        DEBUG_INFO("Starting checklist due to new remote candidate\n");
+        error = rawrtc_error_to_code(trice_checklist_start(
+            transport->gatherer->ice, transport->stun_client, rawrtc_default_config.pacing_interval,
+            ice_established_handler, ice_failed_handler, transport));
+        if (error) {
+            DEBUG_WARNING("Could not start checklist, reason: %s\n", rawrtc_code_to_str(error));
+            goto out;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(re_candidate);  // TODO: Not entirely sure about that
+    }
+
+    // Free vars
+    mem_deref(related_address);
+    mem_deref(foundation);
+    mem_deref(ip);
+
+    return error;
+}
+
+/*
+ * Set the remote candidates on the ICE transport overwriting all
+ * existing remote candidates.
+ */
+enum rawrtc_code rawrtc_ice_transport_set_remote_candidates(
+    struct rawrtc_ice_transport* const transport,
+    struct rawrtc_ice_candidate* const candidates[],  // referenced (each item)
+    size_t const n_candidates) {
+    size_t i;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!transport || !candidates) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Our implementation is incorrect here, it should remove
+    //       previously added remote candidates and replace them. Fix this
+    //       once we can handle an ICE restart.
+
+    // Add each remote candidate
+    for (i = 0; i < n_candidates; ++i) {
+        error = rawrtc_ice_transport_add_remote_candidate(transport, candidates[i]);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/ice_transport/transport.h b/src/ice_transport/transport.h
new file mode 100644
index 0000000..60d938b
--- /dev/null
+++ b/src/ice_transport/transport.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+struct rawrtc_ice_transport {
+    enum rawrtc_ice_transport_state state;
+    struct rawrtc_ice_gatherer* gatherer;  // referenced
+    rawrtc_ice_transport_state_change_handler state_change_handler;  // nullable
+    rawrtc_ice_transport_candidate_pair_change_handler candidate_pair_change_handler;  // nullable
+    void* arg;  // nullable
+    struct stun* stun_client;
+    struct rawrtc_ice_parameters* remote_parameters;  // referenced
+    struct rawrtc_dtls_transport* dtls_transport;  // referenced, nullable
+    bool remote_end_of_candidates;
+};
+
+enum ice_role rawrtc_ice_role_to_re_ice_role(enum rawrtc_ice_role const role);
+
+enum rawrtc_code rawrtc_re_ice_role_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    enum ice_role const re_role);
diff --git a/src/ice_transport/utils.c b/src/ice_transport/utils.c
new file mode 100644
index 0000000..32969d8
--- /dev/null
+++ b/src/ice_transport/utils.c
@@ -0,0 +1,114 @@
+#include "transport.h"
+#include <rawrtc/ice_transport.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <rew.h>
+
+/*
+ * Get the corresponding name for an ICE transport state.
+ */
+char const* rawrtc_ice_transport_state_to_name(enum rawrtc_ice_transport_state const state) {
+    switch (state) {
+        case RAWRTC_ICE_TRANSPORT_STATE_NEW:
+            return "new";
+        case RAWRTC_ICE_TRANSPORT_STATE_CHECKING:
+            return "checking";
+        case RAWRTC_ICE_TRANSPORT_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_ICE_TRANSPORT_STATE_COMPLETED:
+            return "completed";
+        case RAWRTC_ICE_TRANSPORT_STATE_DISCONNECTED:
+            return "disconnected";
+        case RAWRTC_ICE_TRANSPORT_STATE_FAILED:
+            return "failed";
+        case RAWRTC_ICE_TRANSPORT_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
+
+static enum rawrtc_ice_role const map_enum_ice_role[] = {
+    RAWRTC_ICE_ROLE_CONTROLLING,
+    RAWRTC_ICE_ROLE_CONTROLLED,
+};
+
+static char const* const map_str_ice_role[] = {
+    "controlling",
+    "controlled",
+};
+
+static size_t const map_ice_role_length = ARRAY_SIZE(map_enum_ice_role);
+
+/*
+ * Translate an ICE role to str.
+ */
+char const* rawrtc_ice_role_to_str(enum rawrtc_ice_role const role) {
+    size_t i;
+
+    for (i = 0; i < map_ice_role_length; ++i) {
+        if (map_enum_ice_role[i] == role) {
+            return map_str_ice_role[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an ICE role (case-insensitive).
+ */
+enum rawrtc_code rawrtc_str_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!rolep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_ice_role_length; ++i) {
+        if (str_casecmp(map_str_ice_role[i], str) == 0) {
+            *rolep = map_enum_ice_role[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Translate an ICE role to the corresponding re type.
+ */
+enum ice_role rawrtc_ice_role_to_re_ice_role(enum rawrtc_ice_role const role) {
+    // No conversion needed
+    return (enum ice_role) role;
+}
+
+/*
+ * Translate a re ICE role to the corresponding rawrtc role.
+ */
+enum rawrtc_code rawrtc_re_ice_role_to_ice_role(
+    enum rawrtc_ice_role* const rolep,  // de-referenced
+    enum ice_role const re_role) {
+    // Check arguments
+    if (!rolep) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Translate role
+    switch (re_role) {
+        case ICE_ROLE_CONTROLLING:
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLING;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_ROLE_CONTROLLED:
+            *rolep = RAWRTC_ICE_ROLE_CONTROLLED;
+            return RAWRTC_CODE_SUCCESS;
+        case ICE_ROLE_UNKNOWN:
+            *rolep = RAWRTC_ICE_ROLE_UNKNOWN;
+            return RAWRTC_CODE_SUCCESS;
+        default:
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+}
diff --git a/src/main/config.c b/src/main/config.c
new file mode 100644
index 0000000..2817bf0
--- /dev/null
+++ b/src/main/config.c
@@ -0,0 +1,27 @@
+#include "config.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+/*
+ * Default rawrtc configuration.
+ */
+struct rawrtc_config rawrtc_default_config = {
+    .pacing_interval = 20,
+    .ipv4_enable = true,
+    .ipv6_enable = true,
+    .udp_enable = true,
+    .tcp_enable = false,  // TODO: true by default
+    .sign_algorithm = RAWRTC_CERTIFICATE_SIGN_ALGORITHM_SHA256,
+    .ice_server_normal_transport = RAWRTC_ICE_SERVER_TRANSPORT_UDP,
+    .ice_server_secure_transport = RAWRTC_ICE_SERVER_TRANSPORT_TLS,
+    .stun_keepalive_interval = 25,
+    .stun_config =
+        {
+            .rto = STUN_DEFAULT_RTO,
+            .rc = STUN_DEFAULT_RC,
+            .rm = STUN_DEFAULT_RM,
+            .ti = STUN_DEFAULT_TI,
+            .tos = 0x00,
+        },
+};
diff --git a/src/main/config.h b/src/main/config.h
new file mode 100644
index 0000000..f00c2fd
--- /dev/null
+++ b/src/main/config.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <rawrtc/certificate.h>
+#include <rawrtc/ice_server.h>
+#include <re.h>
+
+struct rawrtc_config {
+    uint32_t pacing_interval;
+    bool ipv4_enable;
+    bool ipv6_enable;
+    bool udp_enable;
+    bool tcp_enable;
+    enum rawrtc_certificate_sign_algorithm sign_algorithm;
+    enum rawrtc_ice_server_transport ice_server_normal_transport;
+    enum rawrtc_ice_server_transport ice_server_secure_transport;
+    uint32_t stun_keepalive_interval;
+    struct stun_conf stun_config;
+};
+
+extern struct rawrtc_config rawrtc_default_config;
diff --git a/src/main/main.c b/src/main/main.c
new file mode 100644
index 0000000..aa25b20
--- /dev/null
+++ b/src/main/main.c
@@ -0,0 +1,75 @@
+#include "main.h"
+#include <rawrtc/config.h>
+#include <rawrtc/main.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/main.h>
+#include <re.h>
+
+#define DEBUG_MODULE "rawrtc-main"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+struct rawrtc_global rawrtc_global;
+
+/*
+ * Handle RAWRTCDC timer tick expired.
+ */
+static inline void rawrtcdc_timer_tick_expired_handler(void* arg) {
+    (void) arg;
+
+    // Restart timer
+    tmr_start(
+        &rawrtc_global.rawrtcdc_timer, (uint64_t) rawrtc_global.rawrtcdc_timer_interval,
+        rawrtcdc_timer_tick_expired_handler, NULL);
+
+    // Handle timer tick
+    rawrtcdc_timer_tick(rawrtc_global.rawrtcdc_timer_interval);
+}
+
+/*
+ * RAWRTCDC timer handler.
+ */
+static inline enum rawrtc_code rawrtcdc_timer_tick_handler(
+    bool const on, uint_fast16_t const interval) {
+    // Start or stop timer?
+    if (on) {
+        // Store interval, initialise & start timer
+        rawrtc_global.rawrtcdc_timer_interval = interval;
+        tmr_start(
+            &rawrtc_global.rawrtcdc_timer, (uint64_t) rawrtc_global.rawrtcdc_timer_interval,
+            rawrtcdc_timer_tick_expired_handler, NULL);
+        DEBUG_PRINTF("Started RAWRTCDC timer\n");
+    } else {
+        tmr_cancel(&rawrtc_global.rawrtcdc_timer);
+        DEBUG_PRINTF("Stopped RAWRTCDC timer\n");
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Initialise RAWRTC. Must be called before making a call to any other
+ * function.
+ *
+ * Note: In case `init_re` is not set to `true`, you MUST initialise
+ *       re yourselves before calling this function.
+ */
+enum rawrtc_code rawrtc_init(bool const init_re) {
+    // Initialise timer
+    tmr_init(&rawrtc_global.rawrtcdc_timer);
+
+    // Initialise RAWRTCDC
+    return rawrtcdc_init(init_re, rawrtcdc_timer_tick_handler);
+}
+
+/*
+ * Close RAWRTC and free up all resources.
+ *
+ * Note: In case `close_re` is not set to `true`, you MUST close
+ *       re yourselves.
+ */
+enum rawrtc_code rawrtc_close(bool const close_re) {
+    // Close RAWRTCDC
+    return rawrtcdc_close(close_re);
+}
diff --git a/src/main/main.h b/src/main/main.h
new file mode 100644
index 0000000..8b485d6
--- /dev/null
+++ b/src/main/main.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <re.h>
+
+extern struct rawrtc_global rawrtc_global;
+
+/*
+ * Global RAWRTC vars.
+ */
+struct rawrtc_global {
+    struct tmr rawrtcdc_timer;
+    uint_fast16_t rawrtcdc_timer_interval;
+};
diff --git a/src/main/meson.build b/src/main/meson.build
new file mode 100644
index 0000000..8e92d19
--- /dev/null
+++ b/src/main/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'config.c',
+    'main.c',
+])
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..7c17410
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,25 @@
+sources = []
+
+subdir('certificate')
+subdir('diffie_hellman_parameters')
+subdir('dtls_fingerprint')
+subdir('dtls_parameters')
+subdir('dtls_transport')
+subdir('ice_candidate')
+subdir('ice_gather_options')
+subdir('ice_gatherer')
+subdir('ice_parameters')
+subdir('ice_server')
+subdir('ice_transport')
+subdir('main')
+subdir('peer_connection')
+subdir('peer_connection_configuration')
+subdir('peer_connection_description')
+subdir('peer_connection_ice_candidate')
+subdir('peer_connection_state')
+subdir('sctp_common')
+if get_option('sctp_redirect_transport')
+    subdir('sctp_redirect_transport')
+endif
+subdir('sctp_transport')
+subdir('utils')
diff --git a/src/peer_connection/attributes.c b/src/peer_connection/attributes.c
new file mode 100644
index 0000000..392bad1
--- /dev/null
+++ b/src/peer_connection/attributes.c
@@ -0,0 +1,492 @@
+#include "connection.h"
+#include "../peer_connection_description/description.h"
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/data_channel.h>
+#include <re.h>
+
+/*
+ * Get local description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no local description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference description (if any)
+    if (connection->local_description) {
+        *descriptionp = mem_ref(connection->local_description);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get remote description.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_remote_description(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference description (if any)
+    if (connection->remote_description) {
+        *descriptionp = mem_ref(connection->remote_description);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the current signalling state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state(
+    enum rawrtc_signaling_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = connection->signaling_state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the current ICE gathering state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state(
+    enum rawrtc_ice_gatherer_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    // Note: The W3C spec requires us to return 'new' in case no ICE gatherer exists.
+    // Note: Theoretically there's no 'closed' state on the peer connection variant. We ignore
+    //       that here.
+    if (connection->context.ice_gatherer) {
+        return rawrtc_ice_gatherer_get_state(statep, connection->context.ice_gatherer);
+    } else {
+        *statep = RAWRTC_ICE_GATHERER_STATE_NEW;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current ICE connection state of a peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state(
+    enum rawrtc_ice_transport_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    // Note: The W3C spec requires us to return 'new' in case no ICE transport exists.
+    if (connection->context.ice_transport) {
+        return rawrtc_ice_transport_get_state(statep, connection->context.ice_transport);
+    } else {
+        *statep = RAWRTC_ICE_TRANSPORT_STATE_NEW;
+        return RAWRTC_CODE_SUCCESS;
+    }
+}
+
+/*
+ * Get the current (peer) connection state of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state(
+    enum rawrtc_peer_connection_state* const statep,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!statep || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set state
+    *statep = connection->connection_state;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get indication whether the remote peer accepts trickled ICE
+ * candidates.
+ *
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no remote description has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_can_trickle_ice_candidates(
+    bool* const can_trickle_ice_candidatesp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!can_trickle_ice_candidatesp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set flag (if remote description set)
+    if (connection->remote_description) {
+        *can_trickle_ice_candidatesp = connection->remote_description->trickle_ice;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Unset the handler argument and all handlers of the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_unset_handlers(
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Unset handler argument
+    connection->arg = NULL;
+
+    // Unset all handlers
+    connection->data_channel_handler = NULL;
+    connection->connection_state_change_handler = NULL;
+    connection->ice_gathering_state_change_handler = NULL;
+    connection->ice_connection_state_change_handler = NULL;
+    connection->signaling_state_change_handler = NULL;
+    connection->local_candidate_error_handler = NULL;
+    connection->local_candidate_handler = NULL;
+    connection->negotiation_needed_handler = NULL;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the peer connection's negotiation needed handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_negotiation_needed_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set negotiation needed handler & done
+    connection->negotiation_needed_handler = negotiation_needed_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's negotiation needed handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_negotiation_needed_handler(
+    rawrtc_negotiation_needed_handler* const negotiation_needed_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!negotiation_needed_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get negotiation needed handler (if any)
+    if (connection->negotiation_needed_handler) {
+        *negotiation_needed_handlerp = connection->negotiation_needed_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ICE local candidate handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set local candidate handler & done
+    connection->local_candidate_handler = local_candidate_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ICE local candidate handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_handler(
+    rawrtc_peer_connection_local_candidate_handler* const
+        local_candidate_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!local_candidate_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate handler (if any)
+    if (connection->local_candidate_handler) {
+        *local_candidate_handlerp = connection->local_candidate_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ICE local candidate error handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_candidate_error_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set local candidate error handler & done
+    connection->local_candidate_error_handler = local_candidate_error_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ICE local candidate error handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_local_candidate_error_handler(
+    rawrtc_peer_connection_local_candidate_error_handler* const
+        local_candidate_error_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!local_candidate_error_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get local candidate error handler (if any)
+    if (connection->local_candidate_error_handler) {
+        *local_candidate_error_handlerp = connection->local_candidate_error_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's signaling state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_signaling_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set signaling state change handler & done
+    connection->signaling_state_change_handler = signaling_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's signaling state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_signaling_state_change_handler(
+    rawrtc_signaling_state_change_handler* const signaling_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!signaling_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get signaling state change handler (if any)
+    if (connection->signaling_state_change_handler) {
+        *signaling_state_change_handlerp = connection->signaling_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ice connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_transport_state_change_handler const ice_connection_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set ice connection state change handler & done
+    connection->ice_connection_state_change_handler = ice_connection_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ice connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_connection_state_change_handler(
+    rawrtc_ice_transport_state_change_handler* const
+        ice_connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!ice_connection_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get ice connection state change handler (if any)
+    if (connection->ice_connection_state_change_handler) {
+        *ice_connection_state_change_handlerp = connection->ice_connection_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's ice gathering state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_ice_gathering_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set ice gathering state change handler & done
+    connection->ice_gathering_state_change_handler = ice_gathering_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's ice gathering state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_ice_gathering_state_change_handler(
+    rawrtc_ice_gatherer_state_change_handler* const
+        ice_gathering_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!ice_gathering_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get ice gathering state change handler (if any)
+    if (connection->ice_gathering_state_change_handler) {
+        *ice_gathering_state_change_handlerp = connection->ice_gathering_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's (peer) connection state change handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_connection_state_change_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set (peer) connection state change handler & done
+    connection->connection_state_change_handler = connection_state_change_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's (peer) connection state change handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_connection_state_change_handler(
+    rawrtc_peer_connection_state_change_handler* const
+        connection_state_change_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection_state_change_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get (peer) connection state change handler (if any)
+    if (connection->connection_state_change_handler) {
+        *connection_state_change_handlerp = connection->connection_state_change_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Set the peer connection's data channel handler.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_data_channel_handler(
+    struct rawrtc_peer_connection* const connection,
+    rawrtc_data_channel_handler const data_channel_handler  // nullable
+) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set data channel handler & done
+    connection->data_channel_handler = data_channel_handler;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the peer connection's data channel handler.
+ * Returns `RAWRTC_CODE_NO_VALUE` in case no handler has been set.
+ */
+enum rawrtc_code rawrtc_peer_connection_get_data_channel_handler(
+    rawrtc_data_channel_handler* const data_channel_handlerp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!data_channel_handlerp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get data channel handler (if any)
+    if (connection->data_channel_handler) {
+        *data_channel_handlerp = connection->data_channel_handler;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
diff --git a/src/peer_connection/connection.c b/src/peer_connection/connection.c
new file mode 100644
index 0000000..ceb1a43
--- /dev/null
+++ b/src/peer_connection/connection.c
@@ -0,0 +1,1465 @@
+#include "connection.h"
+#include "../certificate/certificate.h"
+#include "../dtls_transport/transport.h"
+#include "../ice_gather_options/options.h"
+#include "../ice_gatherer/gatherer.h"
+#include "../ice_server/server.h"
+#include "../peer_connection_configuration/configuration.h"
+#include "../peer_connection_description/description.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_channel_parameters.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "peer-connection"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+#include <src/peer_connection_configuration/configuration.h>
+
+/*
+ * Change the signalling state.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_signaling_state(
+    struct rawrtc_peer_connection* const connection,  // not checked
+    enum rawrtc_signaling_state const state) {
+    // Set state
+    connection->signaling_state = state;
+
+    // Call handler (if any)
+    if (connection->signaling_state_change_handler) {
+        connection->signaling_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Change the connection state to a specific state.
+ * Will call the corresponding handler.
+ * Caller MUST ensure that the same state is not set twice.
+ */
+static void set_connection_state(
+    struct rawrtc_peer_connection* const connection,  // not checked
+    enum rawrtc_peer_connection_state const state) {
+    // Set state
+    connection->connection_state = state;
+
+    // Call handler (if any)
+    if (connection->connection_state_change_handler) {
+        connection->connection_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Update connection state.
+ * Will call the corresponding handler.
+ */
+static void update_connection_state(struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    enum rawrtc_ice_transport_state ice_transport_state = RAWRTC_ICE_TRANSPORT_STATE_NEW;
+    enum rawrtc_dtls_transport_state dtls_transport_state = RAWRTC_DTLS_TRANSPORT_STATE_NEW;
+    enum rawrtc_peer_connection_state connection_state;
+
+    // Nothing beats the closed state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return;
+    }
+
+    // Get ICE transport and DTLS transport states
+    if (connection->context.ice_transport) {
+        error =
+            rawrtc_ice_transport_get_state(&ice_transport_state, connection->context.ice_transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to get ICE transport state, reason: %s\n", rawrtc_error_to_code(error));
+        }
+    }
+    if (connection->context.dtls_transport) {
+        error = rawrtc_dtls_transport_get_state(
+            &dtls_transport_state, connection->context.dtls_transport);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to get DTLS transport state, reason: %s\n", rawrtc_error_to_code(error));
+        }
+    }
+
+    // Note: This follows the mindbogglingly confusing W3C spec description - it's just not
+    //       super-obvious. We start with states that are easy to detect and remove more and more
+    //       states from the equation.
+
+    // Failed: Any in the 'failed' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_FAILED ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_FAILED) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_FAILED;
+        goto out;
+    }
+
+    // Connecting: Any in the 'connecting' or 'checking' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_CHECKING ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTING) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_CONNECTING;
+        goto out;
+    }
+
+    // Disconnected: Any in the 'disconnected' state
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_DISCONNECTED) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_DISCONNECTED;
+        goto out;
+    }
+
+    // New: Any in 'new' or all in 'closed'
+    if (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_NEW ||
+        dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_NEW ||
+        (ice_transport_state == RAWRTC_ICE_TRANSPORT_STATE_CLOSED &&
+         dtls_transport_state == RAWRTC_DTLS_TRANSPORT_STATE_CLOSED)) {
+        connection_state = RAWRTC_PEER_CONNECTION_STATE_NEW;
+        goto out;
+    }
+
+    // Connected
+    connection_state = RAWRTC_PEER_CONNECTION_STATE_CONNECTED;
+
+out:
+    // Debug
+    DEBUG_PRINTF(
+        "ICE (%s) + DTLS (%s) = PC %s\n", rawrtc_ice_transport_state_to_name(ice_transport_state),
+        rawrtc_dtls_transport_state_to_name(dtls_transport_state),
+        rawrtc_peer_connection_state_to_name(connection_state));
+
+    // Check if the state would change
+    if (connection->connection_state == connection_state) {
+        return;
+    }
+
+    // Set state
+    connection->connection_state = connection_state;
+
+    // Call handler (if any)
+    if (connection->connection_state_change_handler) {
+        connection->connection_state_change_handler(connection_state, connection->arg);
+    }
+}
+
+/*
+ * Start the SCTP transport.
+ */
+static enum rawrtc_code sctp_transport_start(
+    struct rawrtc_sctp_transport* const sctp_transport,  // not checked
+    struct rawrtc_peer_connection* const connection,  // not checked
+    struct rawrtc_peer_connection_description* const description  // not checked
+) {
+    enum rawrtc_code error;
+
+    // Start SCTP transport
+    error = rawrtc_sctp_transport_start(
+        sctp_transport, description->sctp_capabilities, description->sctp_port);
+    if (error) {
+        return error;
+    }
+
+    // Set MTU (if necessary)
+    if (connection->configuration->sctp.mtu != 0) {
+        error = rawrtc_sctp_transport_set_mtu(sctp_transport, connection->configuration->sctp.mtu);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Enable path MTU discovery (if necessary)
+    if (connection->configuration->sctp.mtu_discovery) {
+        error = rawrtc_sctp_transport_enable_mtu_discovery(sctp_transport);
+        if (error) {
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * All the nasty SDP stuff has been done. Fire it all up - YAY!
+ */
+static enum rawrtc_code peer_connection_start(
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context* const context = &connection->context;
+    struct rawrtc_peer_connection_description* description;
+    enum rawrtc_ice_role ice_role;
+    enum rawrtc_data_transport_type data_transport_type;
+    void* data_transport;
+    struct le* le;
+
+    // Check if it's too early to start
+    if (!connection->local_description || !connection->remote_description) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    DEBUG_INFO("Local and remote description set, starting transports\n");
+    description = connection->remote_description;
+
+    // Determine ICE role
+    // TODO: Is this correct?
+    switch (description->type) {
+        case RAWRTC_SDP_TYPE_OFFER:
+            ice_role = RAWRTC_ICE_ROLE_CONTROLLED;
+            break;
+        case RAWRTC_SDP_TYPE_ANSWER:
+            ice_role = RAWRTC_ICE_ROLE_CONTROLLING;
+            break;
+        default:
+            DEBUG_WARNING(
+                "Cannot determine ICE role from SDP type %s, report this!\n",
+                rawrtc_sdp_type_to_str(description->type));
+            return RAWRTC_CODE_UNKNOWN_ERROR;
+    }
+
+    // Start ICE transport
+    error = rawrtc_ice_transport_start(
+        context->ice_transport, context->ice_gatherer, description->ice_parameters, ice_role);
+    if (error) {
+        return error;
+    }
+
+    // Get data transport
+    error = rawrtc_data_transport_get_transport(
+        &data_transport_type, &data_transport, context->data_transport);
+    if (error) {
+        return error;
+    }
+
+    // Start data transport
+    switch (data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+            // Start DTLS transport
+            error =
+                rawrtc_dtls_transport_start(context->dtls_transport, description->dtls_parameters);
+            if (error) {
+                goto out;
+            }
+
+            // Start SCTP transport
+            error = sctp_transport_start(data_transport, connection, description);
+            if (error) {
+                goto out;
+            }
+            break;
+        }
+        default:
+            DEBUG_WARNING(
+                "Invalid data transport type: %s\n",
+                rawrtc_data_transport_type_to_str(data_transport_type));
+            error = RAWRTC_CODE_UNSUPPORTED_PROTOCOL;
+            goto out;
+    }
+
+    // Add remote ICE candidates
+    for (le = list_head(&description->ice_candidates); le != NULL; le = le->next) {
+        struct rawrtc_peer_connection_ice_candidate* const candidate = le->data;
+        error = rawrtc_peer_connection_add_ice_candidate(connection, candidate);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to add remote candidate, reason: %s\n", rawrtc_code_to_str(error));
+            // Note: Continuing here since other candidates may work
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    mem_deref(data_transport);
+    return error;
+}
+
+/*
+ * Remove all instances that have been created which are not
+ * associated to the peer connection.
+ */
+static void revert_context(
+    struct rawrtc_peer_connection_context* const new,  // not checked
+    struct rawrtc_peer_connection_context* const current  // not checked
+) {
+    if (new->data_transport != current->data_transport) {
+        mem_deref(new->data_transport);
+    }
+    if (new->dtls_transport != current->dtls_transport) {
+        mem_deref(new->dtls_transport);
+    }
+    // TODO: This check is brittle...
+    if (!list_isempty(&new->certificates) && list_isempty(&current->certificates)) {
+        list_flush(&new->certificates);
+    }
+    if (new->ice_transport != current->ice_transport) {
+        mem_deref(new->ice_transport);
+    }
+    if (new->ice_gatherer != current->ice_gatherer) {
+        mem_deref(new->ice_gatherer);
+    }
+    if (new->gather_options != current->gather_options) {
+        mem_deref(new->gather_options);
+    }
+}
+
+/*
+ * Apply all instances on a peer connection.
+ * Return if anything inside the context has changed.
+ */
+static bool apply_context(
+    struct rawrtc_peer_connection_context* const new,  // not checked
+    struct rawrtc_peer_connection_context* const current  // not checked
+) {
+    bool changed = false;
+    if (new->data_transport != current->data_transport) {
+        current->data_transport = new->data_transport;
+        changed = true;
+    }
+    if (new->dtls_transport != current->dtls_transport) {
+        current->dtls_transport = new->dtls_transport;
+        str_ncpy(current->dtls_id, new->dtls_id, RAWRTC_DTLS_ID_LENGTH + 1);
+        changed = true;
+    }
+    // TODO: This check is brittle...
+    if (!list_isempty(&new->certificates) && list_isempty(&current->certificates)) {
+        current->certificates = new->certificates;
+        changed = true;
+    }
+    if (new->ice_transport != current->ice_transport) {
+        current->ice_transport = new->ice_transport;
+        changed = true;
+    }
+    if (new->ice_gatherer != current->ice_gatherer) {
+        current->ice_gatherer = new->ice_gatherer;
+        changed = true;
+    }
+    if (new->gather_options != current->gather_options) {
+        current->gather_options = new->gather_options;
+        changed = true;
+    }
+    return changed;
+}
+
+/*
+ * Wrap an ORTC ICE candidate to a peer connection ICE candidate.
+ */
+static enum rawrtc_code local_ortc_candidate_to_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced, not checked
+    struct rawrtc_ice_candidate* const ortc_candidate,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    char* username_fragment;
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Copy username fragment (is going to be referenced later)
+    error =
+        rawrtc_strdup(&username_fragment, connection->context.ice_gatherer->ice_username_fragment);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to copy username fragment from ICE gatherer, reason: %s\n",
+            rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Create candidate
+    // Note: The local description will exist at this point since we start gathering when the
+    //       local description is being set.
+    error = rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+        &candidate, ortc_candidate, connection->local_description->mid,
+        &connection->local_description->media_line_index, username_fragment);
+    if (error) {
+        goto out;
+    }
+
+    // Set pointer & done
+    *candidatep = candidate;
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(username_fragment);
+    return error;
+}
+
+/*
+ * Add candidate to description and announce candidate.
+ */
+static void ice_gatherer_local_candidate_handler(
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char const* const url,  // nullable
+    void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_ice_candidate* candidate = NULL;
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_FAILED ||
+        connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        DEBUG_NOTICE(
+            "Ignoring candidate in the %s state\n",
+            rawrtc_peer_connection_state_to_name(connection->connection_state));
+        return;
+    }
+
+    // Wrap candidate (if any ORTC candidate)
+    if (ortc_candidate) {
+        error = local_ortc_candidate_to_candidate(&candidate, ortc_candidate, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create local candidate from ORTC candidate, reason: %s\n",
+                rawrtc_code_to_str(error));
+            return;
+        }
+    }
+
+    // Add candidate (or end-of-candidate) to description
+    error =
+        rawrtc_peer_connection_description_add_candidate(connection->local_description, candidate);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to add local candidate to local description, reason: %s\n",
+            rawrtc_code_to_str(error));
+        goto out;
+    }
+
+    // Call handler (if any)
+    if (connection->local_candidate_handler) {
+        connection->local_candidate_handler(candidate, url, connection->arg);
+    }
+
+out:
+    // Un-reference
+    mem_deref(candidate);
+}
+
+/*
+ * Announce ICE gatherer error as ICE candidate error.
+ */
+static void ice_gatherer_error_handler(
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char const* const url,
+    uint16_t const error_code,
+    char const* const error_text,
+    void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_ice_candidate* candidate = NULL;
+
+    // Wrap candidate (if any ORTC candidate)
+    if (ortc_candidate) {
+        error = local_ortc_candidate_to_candidate(&candidate, ortc_candidate, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create local candidate from ORTC candidate, reason: %s\n",
+                rawrtc_code_to_str(error));
+            return;
+        }
+    }
+
+    // Call handler (if any)
+    if (connection->local_candidate_error_handler) {
+        connection->local_candidate_error_handler(
+            candidate, url, error_code, error_text, connection->arg);
+    }
+}
+
+/*
+ * Filter ICE gatherer state and announce it.
+ */
+static void ice_gatherer_state_change_handler(
+    enum rawrtc_ice_gatherer_state const state, void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // The only difference to the ORTC gatherer states is that there's no 'closed' state.
+    if (state == RAWRTC_ICE_GATHERER_STATE_CLOSED) {
+        return;
+    }
+
+    // Call handler (if any)
+    if (connection->ice_gathering_state_change_handler) {
+        connection->ice_gathering_state_change_handler(state, connection->arg);
+    }
+}
+
+/*
+ * Lazy-create an ICE gatherer.
+ */
+static enum rawrtc_code get_ice_gatherer(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_gather_options* options;
+    struct rawrtc_ice_gatherer* gatherer = NULL;
+    struct le* le;
+
+    // Already created?
+    if (context->ice_gatherer) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create ICE gather options
+    error = rawrtc_ice_gather_options_create(&options, connection->configuration->gather_policy);
+    if (error) {
+        return error;
+    }
+
+    // Add ICE servers to gather options
+    for (le = list_head(&connection->configuration->ice_servers); le != NULL; le = le->next) {
+        struct rawrtc_ice_server* const source_server = le->data;
+        struct rawrtc_ice_server* server;
+
+        // Copy ICE server
+        error = rawrtc_ice_server_copy(&server, source_server);
+        if (error) {
+            goto out;
+        }
+
+        // Add ICE server to gather options
+        error = rawrtc_ice_gather_options_add_server_internal(options, server);
+        if (error) {
+            mem_deref(server);
+            goto out;
+        }
+    }
+
+    // Create ICE gatherer
+    error = rawrtc_ice_gatherer_create(
+        &gatherer, options, ice_gatherer_state_change_handler, ice_gatherer_error_handler,
+        ice_gatherer_local_candidate_handler, connection);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(gatherer);
+        mem_deref(options);
+    } else {
+        // Set pointers & done
+        context->gather_options = options;
+        context->ice_gatherer = gatherer;
+    }
+
+    return error;
+}
+
+static void ice_transport_candidate_pair_change_handler(
+    struct rawrtc_ice_candidate* const local,  // read-only
+    struct rawrtc_ice_candidate* const remote,  // read-only
+    void* const arg  // will be casted to `struct client*`
+) {
+    (void) local;
+    (void) remote;
+    (void) arg;
+
+    // There's no handler that could potentially print this, so we print it here for debug purposes
+    DEBUG_PRINTF("ICE transport candidate pair change\n");
+}
+
+static void ice_transport_state_change_handler(
+    enum rawrtc_ice_transport_state const state, void* const arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // Call handler (if any)
+    if (connection->ice_connection_state_change_handler) {
+        connection->ice_connection_state_change_handler(state, connection->arg);
+    }
+
+    // Update connection state
+    update_connection_state(connection);
+}
+
+/*
+ * Lazy-create an ICE transport.
+ */
+static enum rawrtc_code get_ice_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+
+    // Already created?
+    if (context->ice_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Get ICE gatherer
+    error = get_ice_gatherer(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Create ICE transport
+    return rawrtc_ice_transport_create(
+        &context->ice_transport, context->ice_gatherer, ice_transport_state_change_handler,
+        ice_transport_candidate_pair_change_handler, connection);
+}
+
+/*
+ * Lazy-generate a certificate list.
+ */
+static enum rawrtc_code get_certificates(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection_configuration* const configuration  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* certificate;
+
+    // Already created?
+    if (!list_isempty(&context->certificates)) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Certificates in the configuration? Copy them.
+    if (!list_isempty(&configuration->certificates)) {
+        return rawrtc_certificate_list_copy(&context->certificates, &configuration->certificates);
+    }
+
+    // Generate a certificate
+    error = rawrtc_certificate_generate(&certificate, NULL);
+    if (error) {
+        return error;
+    }
+
+    // Add certificate to the list
+    list_append(&context->certificates, &certificate->le, certificate);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+static void dtls_transport_error_handler(
+    // TODO: error.message (probably from OpenSSL)
+    void* const arg) {
+    (void) arg;
+    // TODO: Print error message
+    DEBUG_WARNING("DTLS transport error: %s\n", "???");
+}
+
+static void dtls_transport_state_change_handler(
+    enum rawrtc_dtls_transport_state const state, void* const arg) {
+    struct rawrtc_peer_connection* connection = arg;
+    (void) state;
+
+    // Update connection state
+    update_connection_state(connection);
+}
+
+/*
+ * Lazy-create a DTLS transport.
+ */
+static enum rawrtc_code get_dtls_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct list certificates = LIST_INIT;
+
+    // Already created?
+    if (context->dtls_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Get ICE transport
+    error = get_ice_transport(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Get certificates
+    error = get_certificates(context, connection->configuration);
+    if (error) {
+        return error;
+    }
+
+    // Copy certificates list
+    error = rawrtc_certificate_list_copy(&certificates, &context->certificates);
+    if (error) {
+        return error;
+    }
+
+    // Generate random DTLS ID
+    rand_str(context->dtls_id, sizeof(context->dtls_id));
+
+    // Create DTLS transport
+    return rawrtc_dtls_transport_create_internal(
+        &context->dtls_transport, context->ice_transport, &certificates,
+        dtls_transport_state_change_handler, dtls_transport_error_handler, connection);
+}
+
+static void sctp_transport_state_change_handler(
+    enum rawrtc_sctp_transport_state const state, void* const arg) {
+    (void) arg;
+    (void) state;
+
+    // There's no handler that could potentially print this, so we print it here for debug purposes
+    DEBUG_PRINTF("SCTP transport state change: %s\n", rawrtc_sctp_transport_state_to_name(state));
+}
+
+/*
+ * Lazy-create an SCTP transport.
+ */
+static enum rawrtc_code get_sctp_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_sctp_transport* sctp_transport;
+
+    // Get DTLS transport
+    error = get_dtls_transport(context, connection);
+    if (error) {
+        return error;
+    }
+
+    // Create SCTP transport
+    error = rawrtc_sctp_transport_create(
+        &sctp_transport, context->dtls_transport, RAWRTC_PEER_CONNECTION_SCTP_TRANSPORT_PORT,
+        connection->data_channel_handler, sctp_transport_state_change_handler, connection->arg);
+    if (error) {
+        return error;
+    }
+
+    // Set send/receive buffer length (if necessary)
+    if (connection->configuration->sctp.send_buffer_length != 0 &&
+        connection->configuration->sctp.receive_buffer_length != 0) {
+        error = rawrtc_sctp_transport_set_buffer_length(
+            sctp_transport, connection->configuration->sctp.send_buffer_length,
+            connection->configuration->sctp.receive_buffer_length);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Set congestion control algorithm (if necessary)
+    if (connection->configuration->sctp.congestion_ctrl_algorithm !=
+        RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581) {
+        error = rawrtc_sctp_transport_set_congestion_ctrl_algorithm(
+            sctp_transport, connection->configuration->sctp.congestion_ctrl_algorithm);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Get data transport
+    error = rawrtc_sctp_transport_get_data_transport(&context->data_transport, sctp_transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    // Note: As the data transport has a reference to the SCTP transport, we can
+    //       still retrieve the reference later.
+    mem_deref(sctp_transport);
+    return error;
+}
+
+/*
+ * Lazy-create the requested data transport.
+ */
+static enum rawrtc_code get_data_transport(
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    struct rawrtc_peer_connection* const connection  // not checked
+) {
+    // Already created?
+    if (context->data_transport) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Create data transport depending on what we want to have
+    switch (connection->data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+            return get_sctp_transport(context, connection);
+        }
+        default:
+            return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+}
+
+/*
+ * Destructor for an existing peer connection.
+ */
+static void rawrtc_peer_connection_destroy(void* arg) {
+    struct rawrtc_peer_connection* const connection = arg;
+
+    // Unset all handlers
+    rawrtc_peer_connection_unset_handlers(connection);
+
+    // Close peer connection
+    rawrtc_peer_connection_close(connection);
+
+    // Un-reference
+    mem_deref(connection->context.data_transport);
+    mem_deref(connection->context.dtls_transport);
+    list_flush(&connection->context.certificates);
+    mem_deref(connection->context.ice_transport);
+    mem_deref(connection->context.ice_gatherer);
+    mem_deref(connection->context.gather_options);
+    mem_deref(connection->remote_description);
+    mem_deref(connection->local_description);
+    mem_deref(connection->configuration);
+}
+
+/*
+ * Create a new peer connection.
+ * `*connectionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create(
+    struct rawrtc_peer_connection** const connectionp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* configuration,  // referenced
+    rawrtc_negotiation_needed_handler const negotiation_needed_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_handler const local_candidate_handler,  // nullable
+    rawrtc_peer_connection_local_candidate_error_handler const
+        local_candidate_error_handler,  // nullable
+    rawrtc_signaling_state_change_handler const signaling_state_change_handler,  // nullable
+    rawrtc_ice_transport_state_change_handler const
+        ice_connection_state_change_handler,  // nullable
+    rawrtc_ice_gatherer_state_change_handler const ice_gathering_state_change_handler,  // nullable
+    rawrtc_peer_connection_state_change_handler const connection_state_change_handler,  // nullable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    void* const arg  // nullable
+) {
+    struct rawrtc_peer_connection* connection;
+
+    // Check arguments
+    if (!connectionp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    connection = mem_zalloc(sizeof(*connection), rawrtc_peer_connection_destroy);
+    if (!connection) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    connection->connection_state = RAWRTC_PEER_CONNECTION_STATE_NEW;
+    connection->signaling_state = RAWRTC_SIGNALING_STATE_STABLE;
+    connection->configuration = mem_ref(configuration);
+    connection->negotiation_needed_handler = negotiation_needed_handler;
+    connection->local_candidate_handler = local_candidate_handler;
+    connection->local_candidate_error_handler = local_candidate_error_handler;
+    connection->signaling_state_change_handler = signaling_state_change_handler;
+    connection->ice_connection_state_change_handler = ice_connection_state_change_handler;
+    connection->ice_gathering_state_change_handler = ice_gathering_state_change_handler;
+    connection->connection_state_change_handler = connection_state_change_handler;
+    connection->data_channel_handler = data_channel_handler;
+    connection->data_transport_type = RAWRTC_DATA_TRANSPORT_TYPE_SCTP;
+    connection->arg = arg;
+
+    // Set pointer & done
+    *connectionp = connection;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Close the peer connection. This will stop all underlying transports
+ * and results in a final 'closed' state.
+ */
+enum rawrtc_code rawrtc_peer_connection_close(struct rawrtc_peer_connection* const connection) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Update signalling & connection state
+    // Note: We need to do this early or the 'closed' states when tearing down the transports may
+    //       lead to surprising peer connection states such as 'connected' at the very end.
+    set_signaling_state(connection, RAWRTC_SIGNALING_STATE_CLOSED);
+    set_connection_state(connection, RAWRTC_PEER_CONNECTION_STATE_CLOSED);
+
+    // Stop data transport (if any)
+    if (connection->context.data_transport) {
+        enum rawrtc_data_transport_type data_transport_type;
+        void* data_transport;
+
+        // Get data transport
+        error = rawrtc_data_transport_get_transport(
+            &data_transport_type, &data_transport, connection->context.data_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to get data transport, reason: %s\n", rawrtc_code_to_str(error));
+        } else {
+            // Stop transport
+            switch (data_transport_type) {
+                case RAWRTC_DATA_TRANSPORT_TYPE_SCTP: {
+                    struct rawrtc_sctp_transport* const sctp_transport = data_transport;
+                    error = rawrtc_sctp_transport_stop(sctp_transport);
+                    if (error) {
+                        DEBUG_WARNING(
+                            "Unable to stop SCTP transport, reason: %s\n",
+                            rawrtc_code_to_str(error));
+                    }
+                    break;
+                }
+                default:
+                    DEBUG_WARNING(
+                        "Invalid data transport type: %s\n",
+                        rawrtc_data_transport_type_to_str(data_transport_type));
+                    break;
+            }
+
+            // Un-reference
+            mem_deref(data_transport);
+        }
+    }
+
+    // Stop DTLS transport (if any)
+    if (connection->context.dtls_transport) {
+        error = rawrtc_dtls_transport_stop(connection->context.dtls_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to stop DTLS transport, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Stop ICE transport (if any)
+    if (connection->context.ice_transport) {
+        error = rawrtc_ice_transport_stop(connection->context.ice_transport);
+        if (error) {
+            DEBUG_WARNING("Unable to stop ICE transport, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Close ICE gatherer (if any)
+    if (connection->context.ice_gatherer) {
+        error = rawrtc_ice_gatherer_close(connection->context.ice_gatherer);
+        if (error) {
+            DEBUG_WARNING("Unable to close ICE gatherer, reason: %s\n", rawrtc_code_to_str(error));
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create an offer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_offer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    bool const ice_restart) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Support ICE restart
+    if (ice_restart) {
+        DEBUG_WARNING("ICE restart currently not supported\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow subsequent offers
+    if (connection->local_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Create description
+    return rawrtc_peer_connection_description_create_internal(descriptionp, connection, true);
+}
+
+/*
+ * Create an answer.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_answer(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection) {
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow subsequent answers
+    if (connection->local_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Create description
+    return rawrtc_peer_connection_description_create_internal(descriptionp, connection, false);
+}
+
+/*
+ * Set and apply the local description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_local_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+) {
+    bool initial_description = true;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!connection || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Ensure it has been created by the local peer connection.
+    if (description->connection != connection) {
+        // Yeah, sorry, nope, I'm not parsing all this SDP nonsense again just to check
+        // what kind of nasty things could have been done in the meantime.
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // TODO: Allow changing the local description
+    if (connection->local_description) {
+        initial_description = false;
+        (void) initial_description;
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (description->type != RAWRTC_SDP_TYPE_OFFER && description->type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check SDP type
+    DEBUG_PRINTF(
+        "Set local description: %s (local), %s (remote)\n",
+        rawrtc_sdp_type_to_str(description->type),
+        connection->remote_description
+            ? rawrtc_sdp_type_to_str(connection->remote_description->type)
+            : "n/a");
+    if (connection->remote_description) {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have a remote description and get an offer. This requires renegotiation we
+                // currently don't support.
+                // TODO: Add support for this
+                DEBUG_WARNING("There's no support for renegotiation at the moment.\n");
+                return RAWRTC_CODE_NOT_IMPLEMENTED;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have a remote description and get an answer. Sanity-check that the remote
+                // description is an offer.
+                if (connection->remote_description->type != RAWRTC_SDP_TYPE_OFFER) {
+                    DEBUG_WARNING(
+                        "Got 'answer' but remote description is '%s'\n",
+                        rawrtc_sdp_type_to_str(connection->remote_description->type));
+                    return RAWRTC_CODE_INVALID_STATE;
+                }
+                break;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    } else {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have no remote description and get an offer. Fine.
+                break;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have no remote description and get an answer. Not going to work.
+                DEBUG_WARNING("Got 'answer' but have no remote description\n");
+                return RAWRTC_CODE_INVALID_STATE;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+
+    // Remove reference to self
+    description->connection = mem_deref(description->connection);
+
+    // Set local description
+    connection->local_description = mem_ref(description);
+
+    // Start gathering (if initial description)
+    if (initial_description) {
+        error = rawrtc_ice_gatherer_gather(connection->context.ice_gatherer, NULL);
+        if (error) {
+            DEBUG_WARNING("Unable to start gathering, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+    }
+
+    // Start peer connection if both description are set
+    error = peer_connection_start(connection);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        DEBUG_WARNING("Unable to start peer connection, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Update signalling state
+    switch (connection->signaling_state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            // Can only be an offer or it would not have been accepted
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            // Update of the local offer, nothing to do
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            // Can only be an answer or it would not have been accepted
+            // Note: This may change once we accept PR answers
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_STABLE);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            // Impossible state
+            break;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set and apply the remote description.
+ */
+enum rawrtc_code rawrtc_peer_connection_set_remote_description(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_description* const description  // referenced
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context context;
+
+    // Check arguments
+    if (!connection || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // TODO: Allow changing the remote description
+    if (connection->remote_description) {
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (description->type != RAWRTC_SDP_TYPE_OFFER && description->type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Check SDP type
+    DEBUG_PRINTF(
+        "Set remote description: %s (local), %s (remote)\n",
+        connection->local_description ? rawrtc_sdp_type_to_str(connection->local_description->type)
+                                      : "n/a",
+        rawrtc_sdp_type_to_str(description->type));
+    if (connection->local_description) {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have a local description and get an offer. This requires renegotiation we
+                // currently don't support.
+                // TODO: Add support for this
+                DEBUG_WARNING("There's no support for renegotiation at the moment.\n");
+                return RAWRTC_CODE_NOT_IMPLEMENTED;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have a local description and get an answer. Sanity-check that the local
+                // description is an offer.
+                if (connection->local_description->type != RAWRTC_SDP_TYPE_OFFER) {
+                    DEBUG_WARNING(
+                        "Got 'answer' but local description is '%s'\n",
+                        rawrtc_sdp_type_to_str(connection->local_description->type));
+                    return RAWRTC_CODE_INVALID_STATE;
+                }
+                break;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    } else {
+        switch (description->type) {
+            case RAWRTC_SDP_TYPE_OFFER:
+                // We have no local description and get an offer. Fine.
+                break;
+            case RAWRTC_SDP_TYPE_ANSWER:
+                // We have no local description and get an answer. Not going to work.
+                DEBUG_WARNING("Got 'answer' but have no local description\n");
+                return RAWRTC_CODE_INVALID_STATE;
+            default:
+                DEBUG_WARNING("Unknown SDP type, please report this!\n");
+                return RAWRTC_CODE_UNKNOWN_ERROR;
+        }
+    }
+
+    // No trickle ICE? Ensure we have all candidates
+    if (!description->trickle_ice && !description->end_of_candidates) {
+        DEBUG_NOTICE("No trickle ICE indicated but don't have all candidates\n");
+        // Note: We continue since we still accept further candidates.
+    }
+
+    // No remote media 'application' line?
+    if (!description->remote_media_line) {
+        DEBUG_WARNING("No remote media 'application' line for data channels found\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No ICE parameters?
+    // Note: We either have valid ICE parameters or none at this point
+    if (!description->ice_parameters) {
+        DEBUG_WARNING("Required ICE parameters not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No DTLS parameters?
+    // Note: We either have valid DTLS parameters or none at this point
+    if (!description->dtls_parameters) {
+        DEBUG_WARNING("Required DTLS parameters not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // No SCTP capabilities or port?
+    // Note: We either have valid SCTP capabilities or none at this point
+    if (!description->sctp_capabilities) {
+        DEBUG_WARNING("Required SCTP capabilities not present\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+    if (description->sctp_port == 0) {
+        DEBUG_WARNING("Invalid SCTP port (0)\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set remote description
+    connection->remote_description = mem_ref(description);
+
+    // Initialise context
+    context = connection->context;
+
+    // Create a data transport if we're answering
+    if (description->type == RAWRTC_SDP_TYPE_OFFER) {
+        // Get data transport
+        error = get_data_transport(&context, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create data transport, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+
+        // Apply context
+        apply_context(&context, &connection->context);
+    }
+
+    // Start peer connection if both descriptions are set
+    error = peer_connection_start(connection);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        DEBUG_WARNING("Unable to start peer connection, reason: %s\n", rawrtc_code_to_str(error));
+        return error;
+    }
+
+    // Update signalling state
+    switch (connection->signaling_state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            // Can only be an offer or it would not have been accepted
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            // Can only be an answer or it would not have been accepted
+            // Note: This may change once we accept PR answers
+            set_signaling_state(connection, RAWRTC_SIGNALING_STATE_STABLE);
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            // Update of the remote offer, nothing to do
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            // Impossible state
+            // Note: This may change once we accept PR answers
+            break;
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            // Impossible state
+            break;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE candidate to the peer connection.
+ */
+enum rawrtc_code rawrtc_peer_connection_add_ice_candidate(
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_description* description;
+
+    // Check arguments
+    if (!connection || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Ensure there's a remote description
+    description = connection->remote_description;
+    if (!description) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Note: We can be sure that either 'mid' or the media line index is present at this point.
+
+    // Check if the 'mid' matches (if any)
+    // TODO: Once we support further media lines, we need to look up the appropriate transport here
+    if (candidate->mid && description->mid && str_cmp(candidate->mid, description->mid) != 0) {
+        DEBUG_WARNING("No matching 'mid' in remote description\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if the media line index matches (if any)
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX &&
+        ((uint8_t) candidate->media_line_index) != description->media_line_index) {
+        DEBUG_WARNING("No matching media line index in remote description\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check if the username fragment matches (if any)
+    // TODO: This would need to be done across ICE generations
+    if (candidate->username_fragment) {
+        char* username_fragment;
+        bool matching;
+
+        // Get username fragment from the remote ICE parameters
+        error = rawrtc_ice_parameters_get_username_fragment(
+            &username_fragment, description->ice_parameters);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to retrieve username fragment, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+
+        // Compare username fragments
+        matching = str_cmp(candidate->username_fragment, username_fragment) == 0;
+        mem_deref(username_fragment);
+        if (!matching) {
+            DEBUG_WARNING("Username fragments don't match\n");
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+    }
+
+    // Add ICE candidate
+    return rawrtc_ice_transport_add_remote_candidate(
+        connection->context.ice_transport, candidate->candidate);
+}
+
+/*
+ * Create a data channel on a peer connection.
+ * `*channelp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_create_data_channel(
+    struct rawrtc_data_channel** const channelp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    struct rawrtc_data_channel_parameters* const parameters,  // referenced
+    rawrtc_data_channel_open_handler const open_handler,  // nullable
+    rawrtc_data_channel_buffered_amount_low_handler const buffered_amount_low_handler,  // nullable
+    rawrtc_data_channel_error_handler const error_handler,  // nullable
+    rawrtc_data_channel_close_handler const close_handler,  // nullable
+    rawrtc_data_channel_message_handler const message_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_context context;
+    struct rawrtc_data_channel* channel = NULL;
+
+    // Check arguments
+    if (!connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Check state
+    if (connection->connection_state == RAWRTC_PEER_CONNECTION_STATE_CLOSED) {
+        return RAWRTC_CODE_INVALID_STATE;
+    }
+
+    // Initialise context
+    context = connection->context;
+
+    // Get data transport (if no description has been set, yet)
+    if (!connection->local_description && !connection->remote_description) {
+        error = get_data_transport(&context, connection);
+        if (error) {
+            DEBUG_WARNING(
+                "Unable to create data transport, reason: %s\n", rawrtc_code_to_str(error));
+            return error;
+        }
+    }
+
+    // Create data channel
+    // TODO: Fix data channel cannot be created before transports have been started
+    error = rawrtc_data_channel_create(
+        &channel, context.data_transport, parameters, open_handler, buffered_amount_low_handler,
+        error_handler, close_handler, message_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        // Un-reference
+        mem_deref(channel);
+
+        // Remove all newly created instances
+        revert_context(&context, &connection->context);
+    } else {
+        // Apply context
+        bool const negotiation_needed = apply_context(&context, &connection->context);
+
+        // Set pointer
+        *channelp = channel;
+
+        // Negotiation needed?
+        if (negotiation_needed) {
+            connection->negotiation_needed_handler(connection->arg);
+        }
+    }
+    return error;
+}
diff --git a/src/peer_connection/connection.h b/src/peer_connection/connection.h
new file mode 100644
index 0000000..ef7cff6
--- /dev/null
+++ b/src/peer_connection/connection.h
@@ -0,0 +1,49 @@
+#pragma once
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_gatherer.h>
+#include <rawrtc/ice_transport.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_configuration.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_state.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/data_transport.h>
+#include <re.h>
+
+enum {
+    RAWRTC_PEER_CONNECTION_SCTP_TRANSPORT_PORT = 5000,
+    RAWRTC_DTLS_ID_LENGTH = 32,
+};
+
+/*
+ * Peer connection context.
+ */
+struct rawrtc_peer_connection_context {
+    struct rawrtc_ice_gather_options* gather_options;
+    struct rawrtc_ice_gatherer* ice_gatherer;
+    struct rawrtc_ice_transport* ice_transport;
+    struct list certificates;
+    char dtls_id[RAWRTC_DTLS_ID_LENGTH + 1];
+    struct rawrtc_dtls_transport* dtls_transport;
+    struct rawrtc_data_transport* data_transport;
+};
+
+struct rawrtc_peer_connection {
+    enum rawrtc_peer_connection_state connection_state;
+    enum rawrtc_signaling_state signaling_state;
+    struct rawrtc_peer_connection_configuration* configuration;  // referenced
+    rawrtc_negotiation_needed_handler negotiation_needed_handler;  // nullable
+    rawrtc_peer_connection_local_candidate_handler local_candidate_handler;  // nullable
+    rawrtc_peer_connection_local_candidate_error_handler local_candidate_error_handler;  // nullable
+    rawrtc_signaling_state_change_handler signaling_state_change_handler;  // nullable
+    rawrtc_ice_transport_state_change_handler ice_connection_state_change_handler;  // nullable
+    rawrtc_ice_gatherer_state_change_handler ice_gathering_state_change_handler;  // nullable
+    rawrtc_peer_connection_state_change_handler connection_state_change_handler;  // nullable
+    rawrtc_data_channel_handler data_channel_handler;  // nullable
+    enum rawrtc_data_transport_type data_transport_type;
+    struct rawrtc_peer_connection_description* local_description;  // referenced
+    struct rawrtc_peer_connection_description* remote_description;  // referenced
+    struct rawrtc_peer_connection_context context;
+    void* arg;  // nullable
+};
diff --git a/src/peer_connection/meson.build b/src/peer_connection/meson.build
new file mode 100644
index 0000000..16bd198
--- /dev/null
+++ b/src/peer_connection/meson.build
@@ -0,0 +1,4 @@
+sources += files([
+    'attributes.c',
+    'connection.c',
+])
diff --git a/src/peer_connection_configuration/configuration.c b/src/peer_connection_configuration/configuration.c
new file mode 100644
index 0000000..c1f2b71
--- /dev/null
+++ b/src/peer_connection_configuration/configuration.c
@@ -0,0 +1,260 @@
+#include "configuration.h"
+#include "../certificate/certificate.h"
+#include "../ice_server/server.h"
+#include "../utils/utils.h"
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/peer_connection_configuration.h>
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+#include <limits.h>  // INT_MAX
+
+/*
+ * Destructor for an existing peer connection configuration.
+ */
+static void rawrtc_peer_connection_configuration_destroy(void* arg) {
+    struct rawrtc_peer_connection_configuration* const configuration = arg;
+
+    // Un-reference
+    list_flush(&configuration->certificates);
+    list_flush(&configuration->ice_servers);
+}
+
+/*
+ * Create a new peer connection configuration.
+ * `*configurationp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_create(
+    struct rawrtc_peer_connection_configuration** const configurationp,  // de-referenced
+    enum rawrtc_ice_gather_policy const gather_policy) {
+    struct rawrtc_peer_connection_configuration* configuration;
+
+    // Check arguments
+    if (!configurationp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    configuration =
+        mem_zalloc(sizeof(*configuration), rawrtc_peer_connection_configuration_destroy);
+    if (!configuration) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields/reference
+    configuration->gather_policy = gather_policy;
+    list_init(&configuration->ice_servers);
+    list_init(&configuration->certificates);
+    configuration->sctp_sdp_05 = true;
+    configuration->sctp.send_buffer_length = 0;
+    configuration->sctp.receive_buffer_length = 0;
+    configuration->sctp.congestion_ctrl_algorithm = RAWRTC_SCTP_TRANSPORT_CONGESTION_CTRL_RFC2581;
+    configuration->sctp.mtu = 0;
+    configuration->sctp.mtu_discovery = false;
+
+    // Set pointer and return
+    *configurationp = configuration;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server instance to the peer connection configuration.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server_internal(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    struct rawrtc_ice_server* const server) {
+    // Check arguments
+    if (!configuration || !server) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Add to configuration
+    list_append(&configuration->ice_servers, &server->le, server);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Add an ICE server to the peer connection configuration.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    char* const* const urls,  // copied
+    size_t const n_urls,
+    char* const username,  // nullable, copied
+    char* const credential,  // nullable, copied
+    enum rawrtc_ice_credential_type const credential_type) {
+    struct rawrtc_ice_server* server;
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure there are less than 2^8 servers
+    // TODO: This check should be in some common location
+    if (list_count(&configuration->ice_servers) == UINT8_MAX) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Create ICE server
+    error = rawrtc_ice_server_create(&server, urls, n_urls, username, credential, credential_type);
+    if (error) {
+        return error;
+    }
+
+    // Add to configuration
+    return rawrtc_peer_connection_configuration_add_ice_server_internal(configuration, server);
+}
+
+/*
+ * Get ICE servers from the peer connection configuration.
+ * `*serversp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_ice_servers(
+    struct rawrtc_ice_servers** const serversp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration) {
+    // Check arguments
+    if (!serversp || !configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Hand out list as array
+    // Note: ICE servers handed out cannot be added to other lists
+    //       without copying since the items are only referenced.
+    return rawrtc_list_to_array(
+        (struct rawrtc_array_container**) serversp, &configuration->ice_servers, true);
+}
+
+/*
+ * Add a certificate to the peer connection configuration to be used
+ * instead of an ephemerally generated one.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_add_certificate(
+    struct rawrtc_peer_connection_configuration* configuration,
+    struct rawrtc_certificate* const certificate  // copied
+) {
+    enum rawrtc_code error;
+    struct rawrtc_certificate* certificate_copy;
+
+    // Check arguments
+    if (!configuration || !certificate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy certificate
+    // Note: Copying is needed as the 'le' element cannot be associated to multiple lists
+    error = rawrtc_certificate_copy(&certificate_copy, certificate);
+    if (error) {
+        return error;
+    }
+
+    // Append to list
+    list_append(&configuration->certificates, &certificate_copy->le, certificate_copy);
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get certificates from the peer connection configuration.
+ * `*certificatesp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_get_certificates(
+    struct rawrtc_certificates** const certificatesp,  // de-referenced
+    struct rawrtc_peer_connection_configuration* const configuration) {
+    // Check arguments
+    if (!certificatesp || !configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Hand out list as array
+    // Note: Certificates handed out cannot be added to other lists
+    //       without copying since the items are only referenced.
+    return rawrtc_list_to_array(
+        (struct rawrtc_array_container**) certificatesp, &configuration->certificates, true);
+}
+
+/*
+ * Set whether to use legacy SDP for data channel parameter encoding.
+ * Note: Legacy SDP for data channels is on by default due to parsing problems in Chrome.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_sdp_05(
+    struct rawrtc_peer_connection_configuration* configuration, bool const on) {
+    // Check parameters
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp_sdp_05 = on;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's send and receive buffer length in bytes.
+ * If both values are zero, the default buffer length will be used. Otherwise,
+ * zero is invalid.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_buffer_length(
+    struct rawrtc_peer_connection_configuration* configuration,
+    uint32_t const send_buffer_length,
+    uint32_t const receive_buffer_length) {
+    // Check arguments
+    if (!configuration || send_buffer_length > INT_MAX || receive_buffer_length > INT_MAX ||
+        (send_buffer_length == 0 && receive_buffer_length != 0) ||
+        (send_buffer_length != 0 && receive_buffer_length == 0)) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set length for send/receive buffer
+    configuration->sctp.send_buffer_length = send_buffer_length;
+    configuration->sctp.receive_buffer_length = receive_buffer_length;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's congestion control algorithm.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_congestion_ctrl_algorithm(
+    struct rawrtc_peer_connection_configuration* configuration,
+    enum rawrtc_sctp_transport_congestion_ctrl const algorithm) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.congestion_ctrl_algorithm = algorithm;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Set the SCTP transport's maximum transmission unit (MTU).
+ * A value of zero indicates that the default MTU should be used.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu(
+    struct rawrtc_peer_connection_configuration* configuration, uint32_t const mtu) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.mtu = mtu;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Enable or disable MTU discovery on the SCTP transport.
+ */
+enum rawrtc_code rawrtc_peer_connection_configuration_set_sctp_mtu_discovery(
+    struct rawrtc_peer_connection_configuration* configuration, bool const on) {
+    // Check arguments
+    if (!configuration) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set
+    configuration->sctp.mtu_discovery = on;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/peer_connection_configuration/configuration.h b/src/peer_connection_configuration/configuration.h
new file mode 100644
index 0000000..34fd7c1
--- /dev/null
+++ b/src/peer_connection_configuration/configuration.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <rawrtc/ice_gather_options.h>
+#include <rawrtc/ice_server.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+struct rawrtc_peer_connection_configuration {
+    enum rawrtc_ice_gather_policy gather_policy;
+    struct list ice_servers;
+    struct list certificates;
+    bool sctp_sdp_05;
+    struct {
+        uint32_t send_buffer_length;
+        uint32_t receive_buffer_length;
+        enum rawrtc_sctp_transport_congestion_ctrl congestion_ctrl_algorithm;
+        uint32_t mtu;
+        bool mtu_discovery;
+    } sctp;
+};
+
+enum rawrtc_code rawrtc_peer_connection_configuration_add_ice_server_internal(
+    struct rawrtc_peer_connection_configuration* const configuration,
+    struct rawrtc_ice_server* const server);
diff --git a/src/peer_connection_configuration/meson.build b/src/peer_connection_configuration/meson.build
new file mode 100644
index 0000000..3d01bb6
--- /dev/null
+++ b/src/peer_connection_configuration/meson.build
@@ -0,0 +1 @@
+sources += files('configuration.c')
diff --git a/src/peer_connection_description/attributes.c b/src/peer_connection_description/attributes.c
new file mode 100644
index 0000000..60f9e0a
--- /dev/null
+++ b/src/peer_connection_description/attributes.c
@@ -0,0 +1,39 @@
+#include "description.h"
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Get the SDP type of the description.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    struct rawrtc_peer_connection_description* const description) {
+    // Check arguments
+    if (!typep || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set SDP type
+    *typep = description->type;
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get the SDP of the description.
+ * `*sdpp` will be set to a copy of the SDP that must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_description* const description) {
+    // Check arguments
+    if (!sdpp || !description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy SDP
+    return rawrtc_sdprintf(sdpp, "%b", description->sdp->buf, description->sdp->end);
+}
diff --git a/src/peer_connection_description/description.c b/src/peer_connection_description/description.c
new file mode 100644
index 0000000..b4da9eb
--- /dev/null
+++ b/src/peer_connection_description/description.c
@@ -0,0 +1,1091 @@
+#include "description.h"
+#include "../dtls_fingerprint/fingerprint.h"
+#include "../dtls_parameters/parameters.h"
+#include "../peer_connection/connection.h"
+#include "../peer_connection_configuration/configuration.h"
+#include "../peer_connection_description/description.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/certificate.h>
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_fingerprint.h>
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_transport.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+#include <string.h>  // strlen
+
+#define DEBUG_MODULE "peer-connection-description"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+// Constants
+static uint16_t const discard_port = 9;
+static char const sdp_application_dtls_sctp_regex[] = "application [0-9]+ [^ ]+";
+static char const* const sdp_application_dtls_sctp_variants[] = {
+    "DTLS/SCTP",
+    "UDP/DTLS/SCTP",
+    "TCP/DTLS/SCTP",
+};
+static size_t const sdp_application_dtls_sctp_variants_length =
+    ARRAY_SIZE(sdp_application_dtls_sctp_variants);
+static char const sdp_group_regex[] = "group:BUNDLE [^]+";
+static char const sdp_mid_regex[] = "mid:[^]+";
+static char const sdp_ice_options_trickle[] = "ice-options:trickle";
+static char const sdp_ice_username_fragment_regex[] = "ice-ufrag:[^]+";
+static char const sdp_ice_password_regex[] = "ice-pwd:[^]+";
+static char const sdp_ice_lite[] = "ice-lite";
+static char const sdp_dtls_role_regex[] = "setup:[^]+";
+static enum rawrtc_dtls_role const map_enum_dtls_role[] = {
+    RAWRTC_DTLS_ROLE_AUTO,
+    RAWRTC_DTLS_ROLE_CLIENT,
+    RAWRTC_DTLS_ROLE_SERVER,
+};
+static char const* const map_str_dtls_role[] = {
+    "actpass",
+    "active",
+    "passive",
+};
+static size_t const map_dtls_role_length = ARRAY_SIZE(map_enum_dtls_role);
+static char const sdp_dtls_fingerprint_regex[] = "fingerprint:[^ ]+ [^]+";
+static char const sdp_sctp_port_sctmap_regex[] = "sctpmap:[0-9]+[^]*";
+static char const sdp_sctp_port_regex[] = "sctp-port:[0-9]+";
+static char const sdp_sctp_maximum_message_size_regex[] = "max-message-size:[0-9]+";
+static char const sdp_ice_end_of_candidates[] = "end-of-candidates";
+static char const sdp_ice_candidate_head[] = "candidate:";
+static size_t const sdp_ice_candidate_head_length = ARRAY_SIZE(sdp_ice_candidate_head);
+
+// Candidate line
+struct candidate_line {
+    struct le le;
+    struct pl line;
+};
+
+/*
+ * Set session boilerplate
+ */
+static enum rawrtc_code set_session_boilerplate(
+    struct mbuf* const sdp,  // not checked
+    char const* const version,  // not checked
+    uint32_t const id) {
+    int err;
+
+    // Write session boilerplate
+    err = mbuf_write_str(sdp, "v=0\r\n");
+    err |=
+        mbuf_printf(sdp, "o=sdpartanic-rawrtc-%s %" PRIu32 " 1 IN IP4 127.0.0.1\r\n", version, id);
+    err |= mbuf_write_str(sdp, "s=-\r\n");
+    err |= mbuf_write_str(sdp, "t=0 0\r\n");
+
+    // Done
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Set session attributes on SDP.
+ */
+static enum rawrtc_code set_session_attributes(
+    struct mbuf* const sdp,  // not checked
+    bool const trickle_ice,
+    char const* const bundled_mids) {
+    int err = 0;
+
+    // Trickle ICE
+    if (trickle_ice) {
+        err = mbuf_write_str(sdp, "a=ice-options:trickle\r\n");
+    }
+
+    // WebRTC identity not supported as of now
+
+    // Bundle media (we currently only support a single SCTP transport and nothing else)
+    if (bundled_mids) {
+        err |= mbuf_printf(sdp, "a=group:BUNDLE %s\r\n", bundled_mids);
+    }
+
+    // Done
+    return rawrtc_error_to_code(err);
+}
+
+/*
+ * Get general attributes from an SDP line.
+ */
+static enum rawrtc_code get_general_attributes(
+    char** const bundled_midsp,  // de-referenced, not checked
+    char** const midp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl value;
+
+    // Bundle groups
+    if (!re_regex(line->p, line->l, sdp_group_regex, &value)) {
+        // Check if there is more than one group
+        if (pl_strchr(&value, ' ')) {
+            DEBUG_WARNING("Only one bundle group is supported\n");
+            error = RAWRTC_CODE_NOT_IMPLEMENTED;
+            return error;
+        }
+
+        // Copy group
+        error = rawrtc_error_to_code(pl_strdup(bundled_midsp, &value));
+        if (error) {
+            DEBUG_WARNING("Couldn't copy bundle group\n");
+            return error;
+        }
+    }
+
+    // Media line identification tag
+    if (!re_regex(line->p, line->l, sdp_mid_regex, &value)) {
+        // Copy 'mid'
+        error = rawrtc_error_to_code(pl_strdup(midp, &value));
+        if (error) {
+            DEBUG_WARNING("Couldn't copy 'mid'\n");
+            return error;
+        }
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Add ICE attributes to SDP media line.
+ */
+static enum rawrtc_code add_ice_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_peer_connection_context* const context  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_ice_parameters* parameters;
+    char* username_fragment = NULL;
+    char* password = NULL;
+    int err;
+
+    // Get ICE parameters
+    error = rawrtc_ice_gatherer_get_local_parameters(&parameters, context->ice_gatherer);
+    if (error) {
+        return error;
+    }
+
+    // Get values
+    error = rawrtc_ice_parameters_get_username_fragment(&username_fragment, parameters);
+    error |= rawrtc_ice_parameters_get_password(&password, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Set username fragment and password
+    err = mbuf_printf(sdp, "a=ice-ufrag:%s\r\n", username_fragment);
+    err |= mbuf_printf(sdp, "a=ice-pwd:%s\r\n", password);
+    error = rawrtc_error_to_code(err);
+
+out:
+    mem_deref(password);
+    mem_deref(username_fragment);
+    mem_deref(parameters);
+    return error;
+}
+
+/*
+ * Get ICE attributes from SDP line.
+ */
+static enum rawrtc_code get_ice_attributes(
+    bool* const trickle_icep,  // de-referenced, not checked
+    char** const username_fragmentp,  // de-referenced, not checked
+    char** const passwordp,  // de-referenced, not checked
+    bool* const ice_litep,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl value;
+
+    // ICE options trickle
+    if (pl_strcmp(line, sdp_ice_options_trickle) == 0) {
+        *trickle_icep = true;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // ICE username fragment
+    if (!re_regex(line->p, line->l, sdp_ice_username_fragment_regex, &value)) {
+        return rawrtc_sdprintf(username_fragmentp, "%r", &value);
+    }
+
+    // ICE password
+    if (!re_regex(line->p, line->l, sdp_ice_password_regex, &value)) {
+        return rawrtc_sdprintf(passwordp, "%r", &value);
+    }
+
+    // ICE lite
+    if (pl_strcmp(line, sdp_ice_lite) == 0) {
+        *ice_litep = true;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Add DTLS fingerprint attributes to SDP media line.
+ */
+static enum rawrtc_code add_dtls_fingerprint_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_dtls_parameters* const parameters  // not checked
+) {
+    enum rawrtc_code error;
+    struct rawrtc_dtls_fingerprints* fingerprints;
+    size_t i;
+    char* value = NULL;
+
+    // Get fingerprints
+    error = rawrtc_dtls_parameters_get_fingerprints(&fingerprints, parameters);
+    if (error) {
+        return error;
+    }
+
+    // Add fingerprints
+    for (i = 0; i < fingerprints->n_fingerprints; ++i) {
+        struct rawrtc_dtls_fingerprint* const fingerprint = fingerprints->fingerprints[i];
+        enum rawrtc_certificate_sign_algorithm sign_algorithm;
+
+        // Get sign algorithm
+        error = rawrtc_dtls_fingerprint_get_sign_algorithm(&sign_algorithm, fingerprint);
+        if (error) {
+            goto out;
+        }
+
+        // Get fingerprint value
+        error = rawrtc_dtls_fingerprint_get_value(&value, fingerprint);
+        if (error) {
+            goto out;
+        }
+
+        // Add fingerprint attribute
+        error = rawrtc_error_to_code(mbuf_printf(
+            sdp, "a=fingerprint:%s %s\r\n",
+            rawrtc_certificate_sign_algorithm_to_str(sign_algorithm), value));
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    mem_deref(value);
+    mem_deref(fingerprints);
+    return error;
+}
+
+/*
+ * Get DTLS fingerprint attribute from an SDP line.
+ */
+static enum rawrtc_code get_dtls_fingerprint_attributes(
+    struct rawrtc_dtls_fingerprint** const fingerprintp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl algorithm_pl;
+    struct pl value_pl;
+    enum rawrtc_code error;
+    char* algorithm_str = NULL;
+    char* value_str = NULL;
+    enum rawrtc_certificate_sign_algorithm algorithm;
+
+    // Parse DTLS fingerprint
+    if (re_regex(line->p, line->l, sdp_dtls_fingerprint_regex, &algorithm_pl, &value_pl)) {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    // Copy certificate sign algorithm and value to string
+    error = rawrtc_sdprintf(&algorithm_str, "%r", &algorithm_pl);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_sdprintf(&value_str, "%r", &value_pl);
+    if (error) {
+        goto out;
+    }
+
+    // Convert certificate sign algorithm
+    error = rawrtc_str_to_certificate_sign_algorithm(&algorithm, algorithm_str);
+    if (error) {
+        // This is allowed to fail, some people still use SHA-1 and we don't support it. But there
+        // may be further fingerprints.
+        DEBUG_WARNING("Unsupported certificate sign algorithm: %r\n", &algorithm_pl);
+        error = RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Create DTLS fingerprint
+    error = rawrtc_dtls_fingerprint_create(fingerprintp, algorithm, value_str);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(value_str);
+    mem_deref(algorithm_str);
+    return error;
+}
+
+/*
+ * Add DTLS transport attributes to SDP media line.
+ */
+static enum rawrtc_code add_dtls_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    bool const offering) {
+    enum rawrtc_code error;
+    struct rawrtc_dtls_parameters* parameters;
+    enum rawrtc_dtls_role role;
+    char const* setup_str;
+
+    // Get DTLS parameters
+    error = rawrtc_dtls_transport_get_local_parameters(&parameters, context->dtls_transport);
+    if (error) {
+        return error;
+    }
+
+    // Get DTLS role
+    error = rawrtc_dtls_parameters_get_role(&role, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Add setup attribute
+    if (offering) {
+        // Note: When offering, we MUST use 'actpass' as specified in JSEP
+        setup_str = "actpass";
+    } else {
+        switch (role) {
+            case RAWRTC_DTLS_ROLE_AUTO:
+                setup_str = "active";
+                break;
+            case RAWRTC_DTLS_ROLE_CLIENT:
+                setup_str = "active";
+                break;
+            case RAWRTC_DTLS_ROLE_SERVER:
+                setup_str = "passive";
+                break;
+            default:
+                error = RAWRTC_CODE_INVALID_STATE;
+                goto out;
+        }
+    }
+    error = rawrtc_error_to_code(mbuf_printf(sdp, "a=setup:%s\r\n", setup_str));
+    if (error) {
+        goto out;
+    }
+
+    // Add fingerprints
+    error = add_dtls_fingerprint_attributes(sdp, parameters);
+    if (error) {
+        goto out;
+    }
+
+    // Add (D)TLS ID
+    error = rawrtc_error_to_code(mbuf_printf(sdp, "a=tls-id:%s\r\n", context->dtls_id));
+    if (error) {
+        goto out;
+    }
+
+out:
+    mem_deref(parameters);
+    return error;
+}
+
+/*
+ * Get DTLS transport attribute from an SDP line.
+ */
+static enum rawrtc_code get_dtls_attributes(
+    enum rawrtc_dtls_role* const rolep,  // de-referenced, not checked
+    struct list* const fingerprints,  // not checked
+    struct pl* const line  // not checked
+) {
+    enum rawrtc_code error;
+    struct pl role_pl;
+    struct rawrtc_dtls_fingerprint* fingerprint;
+
+    // DTLS role
+    if (!re_regex(line->p, line->l, sdp_dtls_role_regex, &role_pl)) {
+        size_t i;
+        for (i = 0; i < map_dtls_role_length; ++i) {
+            if (pl_strcmp(&role_pl, map_str_dtls_role[i]) == 0) {
+                *rolep = map_enum_dtls_role[i];
+                return RAWRTC_CODE_SUCCESS;
+            }
+        }
+    }
+
+    // DTLS fingerprint
+    error = get_dtls_fingerprint_attributes(&fingerprint, line);
+    if (!error) {
+        list_append(fingerprints, &fingerprint->le, fingerprint);
+    }
+    return error;
+}
+
+/*
+ * Add SCTP transport attributes to SDP session.
+ */
+static enum rawrtc_code add_sctp_attributes(
+    struct mbuf* const sdp,  // not checked
+    struct rawrtc_sctp_transport* const transport,  // not checked
+    struct rawrtc_peer_connection_context* const context,  // not checked
+    bool const offering,
+    char const* const remote_media_line,
+    char const* const mid,
+    bool const sctp_sdp_05) {
+    enum rawrtc_code error;
+    uint16_t sctp_port;
+    uint16_t sctp_n_streams;
+    int err;
+
+    // Get SCTP port
+    error = rawrtc_sctp_transport_get_port(&sctp_port, transport);
+    if (error) {
+        return error;
+    }
+
+    // Get SCTP #streams
+    error = rawrtc_sctp_transport_get_n_streams(&sctp_n_streams, transport);
+    if (error) {
+        return error;
+    }
+
+    // Add media section
+    if (remote_media_line) {
+        // Just repeat the remote media line.
+        err = mbuf_printf(sdp, "m=%s\r\n", remote_media_line);
+    } else {
+        if (!sctp_sdp_05) {
+            // Note: We choose UDP here although communication may still happen over ICE-TCP
+            //       candidates.
+            // See also: https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-25#section-12.2
+            err = mbuf_printf(
+                sdp, "m=application %" PRIu16 " UDP/DTLS/SCTP webrtc-datachannel\r\n",
+                discard_port);
+        } else {
+            err = mbuf_printf(
+                sdp, "m=application %" PRIu16 " DTLS/SCTP %" PRIu16 "\r\n", discard_port,
+                sctp_port);
+        }
+    }
+    // Add dummy 'c'-line
+    err |= mbuf_write_str(sdp, "c=IN IP4 0.0.0.0\r\n");
+    // Add 'mid' line (if any)
+    if (mid) {
+        err |= mbuf_printf(sdp, "a=mid:%s\r\n", mid);
+    }
+    // Add direction line
+    err |= mbuf_write_str(sdp, "a=sendrecv\r\n");
+    if (err) {
+        return rawrtc_error_to_code(err);
+    }
+
+    // Add ICE attributes
+    error = add_ice_attributes(sdp, context);
+    if (error) {
+        return error;
+    }
+
+    // Add DTLS attributes
+    error = add_dtls_attributes(sdp, context, offering);
+    if (error) {
+        return error;
+    }
+
+    // Set attributes
+    if (!sctp_sdp_05) {
+        // Set SCTP port
+        // Note: Last time I checked, Chrome wasn't able to cope with this
+        err = mbuf_printf(sdp, "a=sctp-port:%" PRIu16 "\r\n", sctp_port);
+    } else {
+        // Set SCTP port, upper layer protocol and number of streams
+        err = mbuf_printf(
+            sdp, "a=sctpmap:%" PRIu16 " webrtc-datachannel %" PRIu16 "\r\n", sctp_port,
+            sctp_n_streams);
+    }
+    if (err) {
+        return rawrtc_error_to_code(err);
+    }
+
+    // Set maximum message size
+    // Note: This isn't part of the 05 version but Firefox can only parse 'max-message-size' but
+    //       doesn't understand the old 'sctpmap' one (from 06 to 21).
+    err = mbuf_write_str(sdp, "a=max-message-size:0\r\n");
+    error = rawrtc_error_to_code(err);
+    if (error) {
+        return error;
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Get SCTP transport attributes from an SDP line.
+ */
+static enum rawrtc_code get_sctp_attributes(
+    uint16_t* const portp,  // de-referenced, not checked
+    uint64_t* const max_message_sizep,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    struct pl port_pl;
+    uint32_t port;
+    struct pl max_message_size_pl;
+
+    // SCTP port (from 'sctpmap' or 'sctp-port')
+    if (!re_regex(line->p, line->l, sdp_sctp_port_sctmap_regex, &port_pl, NULL) ||
+        !re_regex(line->p, line->l, sdp_sctp_port_regex, &port_pl)) {
+        port = pl_u32(&port_pl);
+
+        // Validate port
+        if (port == 0 || port > UINT16_MAX) {
+            DEBUG_WARNING("Invalid SCTP port: %" PRIu32 "\n", port);
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+
+        // Set port & done
+        *portp = (uint16_t) port;
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // SCTP maximum message size
+    // Note: Theoretically, there's another approach as part of 'sctmap' which has been deprecated
+    //       but I doubt anyone ever implemented that.
+    if (!re_regex(line->p, line->l, sdp_sctp_maximum_message_size_regex, &max_message_size_pl)) {
+        *max_message_sizep = pl_u64(&max_message_size_pl);
+        return RAWRTC_CODE_SUCCESS;
+    }
+
+    // Done
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Get an ICE candidate from the description.
+ */
+static enum rawrtc_code get_ice_candidate_attributes(
+    struct list* const candidate_lines,  // not checked
+    bool* const end_of_candidatesp,  // de-referenced, not checked
+    struct pl* const line  // not checked
+) {
+    bool add_candidate_line = false;
+    struct pl* use_line = NULL;
+
+    // ICE candidate
+    if (line->l >= sdp_ice_candidate_head_length) {
+        struct pl candidate_pl = {
+            .p = line->p,
+            .l = sdp_ice_candidate_head_length - 1,
+        };
+        if (pl_strcmp(&candidate_pl, sdp_ice_candidate_head) == 0) {
+            add_candidate_line = true;
+            use_line = line;
+        }
+    }
+
+    // End of candidates
+    if (!add_candidate_line && pl_strcmp(line, sdp_ice_end_of_candidates) == 0) {
+        add_candidate_line = true;
+        use_line = NULL;
+        *end_of_candidatesp = true;
+    }
+
+    // Create candidate line (if any)
+    if (add_candidate_line) {
+        struct candidate_line* const candidate_line = mem_zalloc(sizeof(*candidate_line), NULL);
+        if (!candidate_line) {
+            DEBUG_WARNING("Unable to create candidate line, no memory\n");
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+
+        // Set fields
+        // Warning: The line is NOT copied - it's just a pointer to some memory provided by
+        //          the caller!
+        if (use_line) {
+            candidate_line->line = *use_line;
+        }
+
+        // Add candidate line to list & done
+        list_append(candidate_lines, &candidate_line->le, candidate_line);
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Destructor for an existing peer connection description.
+ */
+static void rawrtc_peer_connection_description_destroy(void* arg) {
+    struct rawrtc_peer_connection_description* const description = arg;
+
+    // Un-reference
+    mem_deref(description->sdp);
+    mem_deref(description->sctp_capabilities);
+    mem_deref(description->dtls_parameters);
+    mem_deref(description->ice_parameters);
+    list_flush(&description->ice_candidates);
+    mem_deref(description->mid);
+    mem_deref(description->remote_media_line);
+    mem_deref(description->bundled_mids);
+    mem_deref(description->connection);
+}
+
+/*
+ * Create a description by creating an offer or answer.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_create_internal(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    struct rawrtc_peer_connection* const connection,
+    bool const offering) {
+    struct rawrtc_peer_connection_context* context;
+    struct rawrtc_peer_connection_description* remote_description;
+    struct rawrtc_peer_connection_description* local_description;
+    enum rawrtc_code error;
+    struct mbuf* sdp = NULL;
+    enum rawrtc_data_transport_type data_transport_type;
+    void* data_transport = NULL;
+
+    // Check arguments
+    if (!descriptionp || !connection) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get context
+    context = &connection->context;
+
+    // Ensure a data transport has been set (otherwise, there would be nothing to do)
+    if (!context->data_transport) {
+        DEBUG_WARNING("No data transport set\n");
+        return RAWRTC_CODE_NO_VALUE;
+    }
+
+    // Ensure a remote description is available (when answering)
+    remote_description = connection->remote_description;
+    if (!offering && !remote_description) {
+        DEBUG_WARNING("No remote description set\n");
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    local_description =
+        mem_zalloc(sizeof(*local_description), rawrtc_peer_connection_description_destroy);
+    if (!local_description) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set initial values
+    local_description->connection = mem_ref(connection);  // Warning: Circular reference
+    local_description->end_of_candidates = false;
+    if (offering) {
+        local_description->type = RAWRTC_SDP_TYPE_OFFER;
+        local_description->trickle_ice = true;
+        error =
+            rawrtc_strdup(&local_description->bundled_mids, RAWRTC_PEER_CONNECTION_DESCRIPTION_MID);
+        if (error) {
+            goto out;
+        }
+        local_description->media_line_index = 0;  // Since we only support one media line...
+        error = rawrtc_strdup(&local_description->mid, RAWRTC_PEER_CONNECTION_DESCRIPTION_MID);
+        if (error) {
+            goto out;
+        }
+        local_description->sctp_sdp_05 = connection->configuration->sctp_sdp_05;
+    } else {
+        local_description->type = RAWRTC_SDP_TYPE_ANSWER;
+        local_description->trickle_ice = remote_description->trickle_ice;
+        local_description->bundled_mids = mem_ref(remote_description->bundled_mids);
+        local_description->remote_media_line = mem_ref(remote_description->remote_media_line);
+        local_description->media_line_index = remote_description->media_line_index;
+        local_description->mid = mem_ref(remote_description->mid);
+        local_description->sctp_sdp_05 = remote_description->sctp_sdp_05;
+    }
+
+    // Create buffer for local description
+    sdp = mbuf_alloc(RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SIZE);
+    if (!sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Set session boilerplate
+    error = set_session_boilerplate(sdp, RAWRTC_VERSION, rand_u32());
+    if (error) {
+        goto out;
+    }
+
+    // Set session attributes
+    error = set_session_attributes(
+        sdp, local_description->trickle_ice, local_description->bundled_mids);
+    if (error) {
+        goto out;
+    }
+
+    // Get data transport
+    error = rawrtc_data_transport_get_transport(
+        &data_transport_type, &data_transport, context->data_transport);
+    if (error) {
+        return error;
+    }
+
+    // Add data transport
+    switch (data_transport_type) {
+        case RAWRTC_DATA_TRANSPORT_TYPE_SCTP:
+            // Add SCTP transport
+            error = add_sctp_attributes(
+                sdp, data_transport, context, offering, local_description->remote_media_line,
+                local_description->mid, local_description->sctp_sdp_05);
+            if (error) {
+                goto out;
+            }
+            break;
+        default:
+            error = RAWRTC_CODE_UNKNOWN_ERROR;
+            goto out;
+    }
+
+    // Reference SDP
+    local_description->sdp = mem_ref(sdp);
+
+    // Debug
+    DEBUG_PRINTF(
+        "Description (internal):\n%H\n", rawrtc_peer_connection_description_debug,
+        local_description);
+
+out:
+    mem_deref(data_transport);
+    mem_deref(sdp);
+    if (error) {
+        mem_deref(local_description);
+    } else {
+        // Set pointer & done
+        *descriptionp = local_description;
+    }
+    return error;
+}
+
+/*
+ * Add an ICE candidate to the description.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_add_candidate(
+    struct rawrtc_peer_connection_description* const description,
+    struct rawrtc_peer_connection_ice_candidate* const candidate  // nullable
+) {
+    enum rawrtc_code error;
+
+    // Check arguments
+    if (!description) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Write candidate or end of candidates indication
+    if (candidate) {
+        char* candidate_sdp;
+
+        // Already written?
+        if (description->end_of_candidates) {
+            return RAWRTC_CODE_INVALID_STATE;
+        }
+
+        // Get candidate SDP
+        error = rawrtc_peer_connection_ice_candidate_get_sdp(&candidate_sdp, candidate);
+        if (error) {
+            return error;
+        }
+
+        // TODO: We would have to get the associated 'mid', media line index and username fragment
+        //       as well and...
+        //
+        //       * inject the candidate at the correct place (compare 'mid' or line index), and
+        //       * compare the username fragment against the one that's currently active (once we
+        //         support ICE restarts).
+
+        // Write candidate to SDP
+        // Note: We only have one media line, so it should be fine to append this to the end
+        error = rawrtc_error_to_code(mbuf_printf(description->sdp, "a=%s\r\n", candidate_sdp));
+        if (error) {
+            DEBUG_WARNING(
+                "Couldn't write candidate to description, reason: %s\n", rawrtc_code_to_str(error));
+            mem_deref(candidate_sdp);
+            return error;
+        }
+
+        // Debug
+        DEBUG_PRINTF("Added candidate line: %s\n", candidate_sdp);
+        mem_deref(candidate_sdp);
+    } else {
+        // Already written?
+        if (description->end_of_candidates) {
+            DEBUG_WARNING("End of candidates has already been written\n");
+            return RAWRTC_CODE_SUCCESS;
+        }
+
+        // Write end of candidates into SDP
+        error = rawrtc_error_to_code(mbuf_write_str(description->sdp, "a=end-of-candidates\r\n"));
+        if (error) {
+            return error;
+        }
+        description->end_of_candidates = true;
+
+        // Debug
+        DEBUG_PRINTF(
+            "Description (end-of-candidates):\n%H\n", rawrtc_peer_connection_description_debug,
+            description);
+    }
+
+    // Done
+    return RAWRTC_CODE_SUCCESS;
+}
+
+// Helper for parsing SDP attributes
+#define HANDLE_ATTRIBUTE(code) \
+    error = code; \
+    if (error == RAWRTC_CODE_SUCCESS) { \
+        break; \
+    } else if (error != RAWRTC_CODE_NO_VALUE) { \
+        goto out; \
+        break; \
+    }
+
+/*
+ * Create a description by parsing it from SDP.
+ * `*descriptionp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_description_create(
+    struct rawrtc_peer_connection_description** const descriptionp,  // de-referenced
+    enum rawrtc_sdp_type const type,
+    char const* const sdp) {
+    enum rawrtc_code error;
+    struct rawrtc_peer_connection_description* remote_description;
+    char const* cursor;
+    bool media_line = false;
+    struct le* le;
+
+    // ICE parameters
+    char* ice_username_fragment = NULL;
+    char* ice_password = NULL;
+    bool ice_lite = false;
+
+    // DTLS parameters
+    enum rawrtc_dtls_role dtls_role = RAWRTC_DTLS_ROLE_AUTO;
+    struct list dtls_fingerprints = LIST_INIT;
+
+    // SCTP capabilities
+    uint64_t sctp_max_message_size = RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_MAX_MESSAGE_SIZE;
+
+    // ICE candidate lines (temporarily stored, so it can be parsed later)
+    struct list ice_candidate_lines = LIST_INIT;
+
+    // Check arguments
+    if (!descriptionp || !sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // We only accept 'offer' or 'answer' at the moment
+    // TODO: Handle the other ones as well
+    if (type != RAWRTC_SDP_TYPE_OFFER && type != RAWRTC_SDP_TYPE_ANSWER) {
+        DEBUG_WARNING("Only 'offer' or 'answer' descriptions can be handled at the moment\n");
+        return RAWRTC_CODE_NOT_IMPLEMENTED;
+    }
+
+    // Allocate
+    remote_description =
+        mem_zalloc(sizeof(*remote_description), rawrtc_peer_connection_description_destroy);
+    if (!remote_description) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields to initial values
+    remote_description->type = type;
+    remote_description->trickle_ice = false;
+    remote_description->media_line_index = 0;  // Since we only support one media line...
+    remote_description->sctp_sdp_05 = true;
+    list_init(&remote_description->ice_candidates);
+    remote_description->sctp_port = RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SCTP_PORT;
+
+    // Find required session and media attributes
+    cursor = sdp;
+    while (*cursor != '\0') {
+        struct pl line;
+        char sdp_type;
+
+        // Ignore lines beginning with '\r' or '\n'
+        if (*cursor == '\r' || *cursor == '\n') {
+            ++cursor;
+            continue;
+        }
+
+        // Find next line or end of string
+        for (line.p = cursor, line.l = 0; *cursor != '\r' && *cursor != '\n' && *cursor != '\0';
+             ++cursor, ++line.l) {
+        }
+
+        // Get line type and move line cursor to value
+        if (line.l < 2) {
+            DEBUG_WARNING("Invalid SDP line: %r\n", &line);
+            break;
+        }
+        sdp_type = *line.p;
+        pl_advance(&line, 2);
+
+        // Are we interested in this line?
+        switch (sdp_type) {
+            case 'a': {
+                // Be aware we're using a macro here which does the following:
+                //
+                // * if the function returns 'success', break (and therefore don't continue
+                //   parsing other attributes on this line).
+                // * if the function returns 'no value', do nothing (and therefore continue parsing
+                //   other attributes on this line).
+                // * if the function returns anything else (which indicates an error), set 'error'
+                //   and jump to 'out'.
+                HANDLE_ATTRIBUTE(get_general_attributes(
+                    &remote_description->bundled_mids, &remote_description->mid, &line));
+                HANDLE_ATTRIBUTE(get_ice_attributes(
+                    &remote_description->trickle_ice, &ice_username_fragment, &ice_password,
+                    &ice_lite, &line));
+                HANDLE_ATTRIBUTE(get_dtls_attributes(&dtls_role, &dtls_fingerprints, &line));
+                HANDLE_ATTRIBUTE(get_sctp_attributes(
+                    &remote_description->sctp_port, &sctp_max_message_size, &line));
+                HANDLE_ATTRIBUTE(get_ice_candidate_attributes(
+                    &ice_candidate_lines, &remote_description->end_of_candidates, &line));
+                break;
+            }
+            case 'm': {
+                struct pl application;
+                size_t i;
+
+                // Ensure amount of media lines is exactly one
+                if (media_line) {
+                    DEBUG_WARNING("Unable to handle more than one media line\n");
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Parse media line
+                if (re_regex(line.p, line.l, sdp_application_dtls_sctp_regex, NULL, &application)) {
+                    DEBUG_WARNING("Unsupport media line: %r\n", &line);
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Check if the application matches some kind of DTLS/SCTP variant (ugh...)
+                for (i = 0; i < sdp_application_dtls_sctp_variants_length; ++i) {
+                    if (pl_strcmp(&application, sdp_application_dtls_sctp_variants[i]) == 0) {
+                        media_line = true;
+                    }
+                }
+                if (!media_line) {
+                    DEBUG_WARNING("Unsupported application on media line: %r\n", &application);
+                    error = RAWRTC_CODE_NOT_IMPLEMENTED;
+                    goto out;
+                }
+
+                // Copy media line
+                error = rawrtc_sdprintf(&remote_description->remote_media_line, "%r", &line);
+                if (error) {
+                    goto out;
+                }
+
+                // Done
+                break;
+            }
+            default:
+                DEBUG_PRINTF(
+                    "Ignoring %s line: %c=%r\n", media_line ? "media" : "session", sdp_type, &line);
+                break;
+        }
+    }
+
+    // Return 'no value' in case there was no media line
+    if (!media_line) {
+        error = RAWRTC_CODE_NO_VALUE;
+        goto out;
+    }
+
+    // Create ICE parameters (if possible)
+    if (ice_username_fragment && ice_password) {
+        error = rawrtc_ice_parameters_create(
+            &remote_description->ice_parameters, ice_username_fragment, ice_password, ice_lite);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create DTLS parameters (if possible)
+    if (!list_isempty(&dtls_fingerprints)) {
+        error = rawrtc_dtls_parameters_create_internal(
+            &remote_description->dtls_parameters, dtls_role, &dtls_fingerprints);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create SCTP capabilities
+    error = rawrtc_sctp_capabilities_create(
+        &remote_description->sctp_capabilities, sctp_max_message_size);
+    if (error) {
+        goto out;
+    }
+
+    // Late parsing of ICE candidates.
+    // Note: This is required since the 'mid' and the username fragment may be parsed after a
+    //       candidate has been found.
+    for (le = list_head(&ice_candidate_lines); le != NULL; le = le->next) {
+        struct candidate_line* const candidate_line = le->data;
+
+        // Create ICE candidate
+        struct rawrtc_peer_connection_ice_candidate* candidate;
+        error = rawrtc_peer_connection_ice_candidate_create_internal(
+            &candidate, &candidate_line->line, remote_description->mid,
+            &remote_description->media_line_index, ice_username_fragment);
+        if (error) {
+            goto out;
+        }
+
+        // Add ICE candidate to the list
+        DEBUG_PRINTF("Adding ICE candidate to description\n");
+        list_append(&remote_description->ice_candidates, &candidate->le, candidate);
+    }
+
+    // Copy SDP
+    remote_description->sdp = mbuf_alloc(strlen(sdp));
+    if (!remote_description->sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+    mbuf_write_str(remote_description->sdp, sdp);
+
+    // Debug
+    DEBUG_PRINTF(
+        "Description (parsed):\n%H\n", rawrtc_peer_connection_description_debug,
+        remote_description);
+
+    // Done
+    error = RAWRTC_CODE_SUCCESS;
+
+out:
+    // Un-reference
+    list_flush(&ice_candidate_lines);
+    list_flush(&dtls_fingerprints);
+    mem_deref(ice_password);
+    mem_deref(ice_username_fragment);
+    if (error) {
+        mem_deref(remote_description);
+    } else {
+        // Set pointer & done
+        *descriptionp = remote_description;
+    }
+    return error;
+}
diff --git a/src/peer_connection_description/description.h b/src/peer_connection_description/description.h
new file mode 100644
index 0000000..eabf2e0
--- /dev/null
+++ b/src/peer_connection_description/description.h
@@ -0,0 +1,47 @@
+#pragma once
+#include <rawrtc/dtls_parameters.h>
+#include <rawrtc/ice_parameters.h>
+#include <rawrtc/peer_connection.h>
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <re.h>
+
+#define RAWRTC_PEER_CONNECTION_DESCRIPTION_MID "rawrtc-sctp-dc"
+
+struct rawrtc_peer_connection_description {
+    struct rawrtc_peer_connection* connection;
+    enum rawrtc_sdp_type type;
+    bool trickle_ice;
+    char* bundled_mids;
+    char* remote_media_line;
+    uint8_t media_line_index;
+    char* mid;
+    bool sctp_sdp_05;
+    bool end_of_candidates;
+    struct list ice_candidates;
+    struct rawrtc_ice_parameters* ice_parameters;
+    struct rawrtc_dtls_parameters* dtls_parameters;
+    struct rawrtc_sctp_capabilities* sctp_capabilities;
+    uint16_t sctp_port;
+    struct mbuf* sdp;
+};
+
+enum {
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SIZE = 1024,
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_MAX_MESSAGE_SIZE = 65536,
+    RAWRTC_PEER_CONNECTION_DESCRIPTION_DEFAULT_SCTP_PORT = 5000,
+};
+
+enum rawrtc_code rawrtc_peer_connection_description_create_internal(
+    struct rawrtc_peer_connection_description** const descriptionp,
+    struct rawrtc_peer_connection* const connection,
+    bool const offering);
+
+enum rawrtc_code rawrtc_peer_connection_description_add_candidate(
+    struct rawrtc_peer_connection_description* const description,
+    struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+int rawrtc_peer_connection_description_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_description* const description);
diff --git a/src/peer_connection_description/meson.build b/src/peer_connection_description/meson.build
new file mode 100644
index 0000000..54c8f27
--- /dev/null
+++ b/src/peer_connection_description/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'description.c',
+    'utils.c',
+])
diff --git a/src/peer_connection_description/utils.c b/src/peer_connection_description/utils.c
new file mode 100644
index 0000000..d05d13b
--- /dev/null
+++ b/src/peer_connection_description/utils.c
@@ -0,0 +1,149 @@
+#include "description.h"
+#include "../dtls_parameters/parameters.h"
+#include "../ice_parameters/parameters.h"
+#include "../peer_connection_ice_candidate/candidate.h"
+#include <rawrtc/peer_connection_description.h>
+#include <rawrtcc/code.h>
+#include <rawrtcdc/sctp_capabilities.h>
+#include <re.h>
+
+static enum rawrtc_sdp_type const map_enum_sdp_type[] = {
+    RAWRTC_SDP_TYPE_OFFER,
+    RAWRTC_SDP_TYPE_PROVISIONAL_ANSWER,
+    RAWRTC_SDP_TYPE_ANSWER,
+    RAWRTC_SDP_TYPE_ROLLBACK,
+};
+
+static char const* const map_str_sdp_type[] = {
+    "offer",
+    "pranswer",
+    "answer",
+    "rollback",
+};
+
+static size_t const map_sdp_type_length = ARRAY_SIZE(map_enum_sdp_type);
+
+/*
+ * Translate an SDP type to str.
+ */
+char const* rawrtc_sdp_type_to_str(enum rawrtc_sdp_type const type) {
+    size_t i;
+
+    for (i = 0; i < map_sdp_type_length; ++i) {
+        if (map_enum_sdp_type[i] == type) {
+            return map_str_sdp_type[i];
+        }
+    }
+
+    return "???";
+}
+
+/*
+ * Translate a str to an SDP type.
+ */
+enum rawrtc_code rawrtc_str_to_sdp_type(
+    enum rawrtc_sdp_type* const typep,  // de-referenced
+    char const* const str) {
+    size_t i;
+
+    // Check arguments
+    if (!typep || !str) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    for (i = 0; i < map_sdp_type_length; ++i) {
+        if (str_casecmp(map_str_sdp_type[i], str) == 0) {
+            *typep = map_enum_sdp_type[i];
+            return RAWRTC_CODE_SUCCESS;
+        }
+    }
+
+    return RAWRTC_CODE_NO_VALUE;
+}
+
+/*
+ * Print debug information for a peer connection description.
+ */
+int rawrtc_peer_connection_description_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_description* const description) {
+    int err = 0;
+    struct le* le;
+
+    // Check arguments
+    if (!description) {
+        return 0;
+    }
+
+    err |= re_hprintf(pf, "----- Peer Connection Description <%p>\n", description);
+
+    // Print general fields
+    err |= re_hprintf(pf, "  peer_connection=");
+    if (description->connection) {
+        err |= re_hprintf(pf, "%p\n", description->connection);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  sdp_type=%s\n", rawrtc_sdp_type_to_str(description->type));
+    err |= re_hprintf(pf, "  trickle_ice=%s\n", description->trickle_ice ? "yes" : "no");
+    err |= re_hprintf(pf, "  bundled_mids=");
+    if (description->bundled_mids) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->bundled_mids);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  remote_media_line=");
+    if (description->remote_media_line) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->remote_media_line);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  media_line_index=%" PRIu8 "\n", description->media_line_index);
+    err |= re_hprintf(pf, "  mid=");
+    if (description->mid) {
+        err |= re_hprintf(pf, "\"%s\"\n", description->mid);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+    err |= re_hprintf(pf, "  sctp_sdp_05=%s\n", description->sctp_sdp_05 ? "yes" : "no");
+    err |=
+        re_hprintf(pf, "  end_of_candidates=%s\n", description->end_of_candidates ? "yes" : "no");
+
+    // Print ICE parameters
+    if (description->ice_parameters) {
+        err |= re_hprintf(pf, "%H", rawrtc_ice_parameters_debug, description->ice_parameters);
+    } else {
+        err |= re_hprintf(pf, "  ICE Parameters <n/a>\n");
+    }
+
+    // Print ICE candidates
+    le = list_head(&description->ice_candidates);
+    if (le) {
+        for (; le != NULL; le = le->next) {
+            struct rawrtc_peer_connection_ice_candidate* const candidate = le->data;
+            err |= re_hprintf(pf, "%H", rawrtc_peer_connection_ice_candidate_debug, candidate);
+        }
+    } else {
+        err |= re_hprintf(pf, "  ICE Candidates <n/a>\n");
+    }
+
+    // Print DTLS parameters
+    if (description->dtls_parameters) {
+        err |= re_hprintf(pf, "%H", rawrtc_dtls_parameters_debug, description->dtls_parameters);
+    } else {
+        err |= re_hprintf(pf, "  DTLS Parameters <n/a>\n");
+    }
+
+    // Print SCTP capabilities & port
+    if (description->sctp_capabilities) {
+        err |= re_hprintf(pf, "%H", rawrtc_sctp_capabilities_debug, description->sctp_capabilities);
+    } else {
+        err |= re_hprintf(pf, "  SCTP Capabilities <n/a>\n");
+    }
+    err |= re_hprintf(pf, "  sctp_port=%" PRIu16 "\n", description->sctp_port);
+
+    // Print SDP
+    err |= re_hprintf(pf, "  sdp=\n%b", description->sdp->buf, description->sdp->end);
+
+    // Done
+    return err;
+}
diff --git a/src/peer_connection_ice_candidate/attributes.c b/src/peer_connection_ice_candidate/attributes.c
new file mode 100644
index 0000000..eed0125
--- /dev/null
+++ b/src/peer_connection_ice_candidate/attributes.c
@@ -0,0 +1,220 @@
+#include "candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+/*
+ * Encode the ICE candidate into SDP.
+ * `*sdpp` will be set to a copy of the SDP attribute that must be
+ * unreferenced.
+ *
+ * Note: This is equivalent to the `candidate` attribute of the W3C
+ *       WebRTC specification's `RTCIceCandidateInit`.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp(
+    char** const sdpp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    enum rawrtc_code error;
+    char* foundation = NULL;
+    uint16_t component_id = 1;
+    enum rawrtc_ice_protocol protocol;
+    char const* protocol_str;
+    uint32_t priority;
+    char* ip = NULL;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+    char const* type_str;
+    struct mbuf* sdp = NULL;
+    char* related_address = NULL;
+    uint16_t related_port = 0;
+    enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+    char const* tcp_type_str;
+
+    // Check arguments
+    if (!sdpp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get values for mandatory fields
+    error = rawrtc_ice_candidate_get_foundation(&foundation, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    // TODO: Get component ID from candidate/gatherer/transport
+    error = rawrtc_ice_candidate_get_protocol(&protocol, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_priority(&priority, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_ip(&ip, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_port(&port, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_type(&type, candidate->candidate);
+    if (error) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_related_address(&related_address, candidate->candidate);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+    error = rawrtc_ice_candidate_get_related_port(&related_port, candidate->candidate);
+    if (error && error != RAWRTC_CODE_NO_VALUE) {
+        goto out;
+    }
+    protocol_str = rawrtc_ice_protocol_to_str(protocol);
+    type_str = rawrtc_ice_candidate_type_to_str(type);
+
+    // Initialise SDP attribute buffer
+    sdp = mbuf_alloc(RAWRTC_PEER_CONNECTION_CANDIDATE_DEFAULT_SIZE);
+    if (!sdp) {
+        error = RAWRTC_CODE_NO_MEMORY;
+        goto out;
+    }
+
+    // Encode candidate's mandatory fields
+    error = rawrtc_error_to_code(mbuf_printf(
+        sdp, "candidate:%s %" PRIu16 " %s %" PRIu32 " %s %" PRIu16 " typ %s", foundation,
+        component_id, protocol_str, priority, ip, port, type_str));
+    if (error) {
+        goto out;
+    }
+    if (related_address) {
+        error = rawrtc_error_to_code(mbuf_printf(sdp, " raddr %s", related_address));
+        if (error) {
+            goto out;
+        }
+    }
+    if (related_port > 0) {
+        error = rawrtc_error_to_code(mbuf_printf(sdp, " rport %" PRIu16, related_port));
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Get value for 'tcptype' extension field and encode it (if available)
+    error = rawrtc_ice_candidate_get_tcp_type(&tcp_type, candidate->candidate);
+    switch (error) {
+        case RAWRTC_CODE_SUCCESS:
+            tcp_type_str = rawrtc_ice_tcp_candidate_type_to_str(tcp_type);
+            mbuf_printf(sdp, " tcptype %s", tcp_type_str);
+            break;
+        case RAWRTC_CODE_NO_VALUE:
+            break;
+        default:
+            goto out;
+    }
+
+    // Copy SDP attribute
+    error = rawrtc_sdprintf(sdpp, "%b", sdp->buf, sdp->end);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(related_address);
+    mem_deref(sdp);
+    mem_deref(ip);
+    mem_deref(foundation);
+    return error;
+}
+
+/*
+ * Get the media stream identification tag the ICE candidate is
+ * associated to.
+ * `*midp` will be set to a copy of the candidate's mid and must be
+ * unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no 'mid' has been set.
+ * Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and `*midp* must
+ * be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_mid(
+    char** const midp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!midp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy mid (if any)
+    if (candidate->mid) {
+        return rawrtc_strdup(midp, candidate->mid);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the media stream line index the ICE candidate is associated to.
+ * Return `RAWRTC_CODE_NO_VALUE` in case no media line index has been
+ * set.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_sdp_media_line_index(
+    uint8_t* const media_line_index,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!media_line_index || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Set media line index (if any)
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX) {
+        *media_line_index = (uint8_t) candidate->media_line_index;
+        return RAWRTC_CODE_SUCCESS;
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the username fragment the ICE candidate is associated to.
+ * `*username_fragmentp` will be set to a copy of the candidate's
+ * username fragment and must be unreferenced.
+ *
+ * Return `RAWRTC_CODE_NO_VALUE` in case no username fragment has been
+ * set. Otherwise, `RAWRTC_CODE_SUCCESS` will be returned and
+ * `*username_fragmentp* must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_username_fragment(
+    char** const username_fragmentp,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!username_fragmentp || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Copy username fragment (if any)
+    if (candidate->username_fragment) {
+        return rawrtc_strdup(username_fragmentp, candidate->username_fragment);
+    } else {
+        return RAWRTC_CODE_NO_VALUE;
+    }
+}
+
+/*
+ * Get the underlying ORTC ICE candidate from the ICE candidate.
+ * `*ortc_candidatep` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_get_ortc_candidate(
+    struct rawrtc_ice_candidate** const ortc_candidatep,  // de-referenced
+    struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    // Check arguments
+    if (!ortc_candidatep || !candidate) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Reference ORTC ICE candidate
+    *ortc_candidatep = mem_ref(candidate->candidate);
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/peer_connection_ice_candidate/candidate.c b/src/peer_connection_ice_candidate/candidate.c
new file mode 100644
index 0000000..4f2c7fc
--- /dev/null
+++ b/src/peer_connection_ice_candidate/candidate.c
@@ -0,0 +1,252 @@
+#include "candidate.h"
+#include "../ice_candidate/candidate.h"
+#include <rawrtc/ice_candidate.h>
+#include <rawrtc/peer_connection_ice_candidate.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <re.h>
+
+static char const sdp_ice_candidate_regex[] =
+    "candidate:[^ ]+ [0-9]+ [^ ]+ [0-9]+ [^ ]+ [0-9]+ typ [^ ]+[^]*";
+static char const sdp_ice_candidate_related_address_regex[] = "[^]* raddr [^ ]+";
+static char const sdp_ice_candidate_related_port_regex[] = "[^]* rport [0-9]+";
+static char const sdp_ice_candidate_tcp_type_regex[] = "[^]* tcptype [^ ]+";
+
+/*
+ * Destructor for an existing peer connection.
+ */
+static void rawrtc_peer_connection_ice_candidate_destroy(void* arg) {
+    struct rawrtc_peer_connection_ice_candidate* const candidate = arg;
+
+    // Un-reference
+    mem_deref(candidate->username_fragment);
+    mem_deref(candidate->mid);
+    mem_deref(candidate->candidate);
+}
+
+/*
+ * Create a new ICE candidate from an existing (ORTC) ICE candidate.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+) {
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Ensure either 'mid' or the media line index is present
+    if (!mid && !media_line_index) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate
+    candidate = mem_zalloc(sizeof(*candidate), rawrtc_peer_connection_ice_candidate_destroy);
+    if (!candidate) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Set fields
+    candidate->candidate = mem_ref(ortc_candidate);
+    candidate->mid = mem_ref(mid);
+    candidate->media_line_index = (int16_t)(media_line_index ? *media_line_index : -1);
+    candidate->username_fragment = mem_ref(username_fragment);
+
+    // Set pointer & done
+    *candidatep = candidate;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Create a new ICE candidate from SDP (pl variant).
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create_internal(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const sdp,
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+) {
+    enum rawrtc_code error;
+    struct pl optional;
+    uint32_t value_u32;
+
+    // Mandatory fields
+    struct pl foundation_pl;
+    struct pl component_id_pl;
+    struct pl protocol_pl;
+    struct pl priority_pl;
+    struct pl ip_pl;
+    struct pl port_pl;
+    struct pl type_pl;
+    uint32_t priority;
+    enum rawrtc_ice_protocol protocol;
+    uint16_t port;
+    enum rawrtc_ice_candidate_type type;
+
+    // Optional fields
+    struct pl related_address_pl = PL_INIT;
+    struct pl related_port_pl = PL_INIT;
+    struct pl tcp_type_pl = PL_INIT;
+    uint16_t related_port = 0;
+    enum rawrtc_ice_tcp_candidate_type tcp_type = RAWRTC_ICE_TCP_CANDIDATE_TYPE_ACTIVE;
+
+    // (ORTC) ICE candidate
+    struct rawrtc_ice_candidate* ortc_candidate;
+
+    // ICE candidate
+    struct rawrtc_peer_connection_ice_candidate* candidate;
+
+    // Check arguments
+    if (!candidatep || !sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Ensure either 'mid' or the media line index is present
+    if (!mid && !media_line_index) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    if (pl_isset(sdp)) {
+        // Get mandatory ICE candidate fields
+        if (re_regex(
+                sdp->p, sdp->l, sdp_ice_candidate_regex, &foundation_pl, &component_id_pl,
+                &protocol_pl, &priority_pl, &ip_pl, &port_pl, &type_pl, &optional)) {
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+
+        // Get optional ICE candidate fields
+        re_regex(
+            optional.p, optional.l, sdp_ice_candidate_related_address_regex, NULL,
+            &related_address_pl);
+        re_regex(
+            optional.p, optional.l, sdp_ice_candidate_related_port_regex, NULL, &related_port_pl);
+        re_regex(optional.p, optional.l, sdp_ice_candidate_tcp_type_regex, NULL, &tcp_type_pl);
+
+        // Component ID
+        // TODO: Handle
+        (void) component_id_pl;
+
+        // Protocol
+        error = rawrtc_pl_to_ice_protocol(&protocol, &protocol_pl);
+        if (error) {
+            return error;
+        }
+
+        // Priority
+        priority = pl_u32(&priority_pl);
+
+        // Port
+        value_u32 = pl_u32(&port_pl);
+        if (value_u32 > UINT16_MAX) {
+            return RAWRTC_CODE_INVALID_ARGUMENT;
+        }
+        port = (uint16_t) value_u32;
+
+        // Type
+        error = rawrtc_pl_to_ice_candidate_type(&type, &type_pl);
+        if (error) {
+            return error;
+        }
+
+        // Related port (if any)
+        if (pl_isset(&related_port_pl)) {
+            value_u32 = pl_u32(&related_port_pl);
+            if (value_u32 > UINT16_MAX) {
+                return RAWRTC_CODE_INVALID_ARGUMENT;
+            }
+            related_port = (uint16_t) value_u32;
+        }
+
+        // TCP type (if any)
+        if (pl_isset(&tcp_type_pl)) {
+            error = rawrtc_pl_to_ice_tcp_candidate_type(&tcp_type, &tcp_type_pl);
+            if (error) {
+                return error;
+            }
+        }
+
+        // Create (ORTC) ICE candidate
+        error = rawrtc_ice_candidate_create_internal(
+            &ortc_candidate, &foundation_pl, priority, &ip_pl, protocol, port, type, tcp_type,
+            &related_address_pl, related_port);
+        if (error) {
+            return error;
+        }
+    } else {
+        ortc_candidate = NULL;
+    }
+
+    // Create ICE candidate
+    error = rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+        &candidate, ortc_candidate, mid, media_line_index, username_fragment);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(ortc_candidate);
+    if (!error) {
+        // Set pointer & done
+        *candidatep = candidate;
+    }
+    return error;
+}
+
+/*
+ * Create a new ICE candidate from SDP.
+ * `*candidatesp` must be unreferenced.
+ *
+ * Note: This is equivalent to creating an `RTCIceCandidate` from an
+ *       `RTCIceCandidateInit` instance in the W3C WebRTC
+ *       specification.
+ */
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    char* const sdp,
+    char* const mid,  // nullable, copied
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, copied
+) {
+    struct pl sdp_pl;
+    enum rawrtc_code error;
+    char* mid_copy = NULL;
+    char* username_fragment_copy = NULL;
+
+    // Check arguments (not checked in the internal function)
+    if (!sdp) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Convert SDP str to pl
+    pl_set_str(&sdp_pl, sdp);
+
+    // Copy arguments that will be referenced
+    if (mid) {
+        error = rawrtc_strdup(&mid_copy, mid);
+        if (error) {
+            goto out;
+        }
+    }
+    if (username_fragment) {
+        error = rawrtc_strdup(&username_fragment_copy, username_fragment);
+        if (error) {
+            goto out;
+        }
+    }
+
+    // Create ICE candidate
+    error = rawrtc_peer_connection_ice_candidate_create_internal(
+        candidatep, &sdp_pl, mid_copy, media_line_index, username_fragment_copy);
+    if (error) {
+        goto out;
+    }
+
+out:
+    // Un-reference
+    mem_deref(username_fragment_copy);
+    mem_deref(mid_copy);
+    return error;
+}
diff --git a/src/peer_connection_ice_candidate/candidate.h b/src/peer_connection_ice_candidate/candidate.h
new file mode 100644
index 0000000..6fbd828
--- /dev/null
+++ b/src/peer_connection_ice_candidate/candidate.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <rawrtc.h>
+
+enum {
+    RAWRTC_PEER_CONNECTION_CANDIDATE_DEFAULT_SIZE = 256,
+};
+
+struct rawrtc_peer_connection_ice_candidate {
+    struct le le;
+    struct rawrtc_ice_candidate* candidate;
+    char* mid;
+    int16_t media_line_index;
+    char* username_fragment;
+};
+
+int rawrtc_peer_connection_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_ice_candidate* const candidate);
+
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_from_ortc_candidate(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct rawrtc_ice_candidate* const ortc_candidate,  // nullable
+    char* const mid,  // nullable, referenced
+    uint8_t const* const media_line_index,  // nullable, copied
+    char* const username_fragment  // nullable, referenced
+);
+
+enum rawrtc_code rawrtc_peer_connection_ice_candidate_create_internal(
+    struct rawrtc_peer_connection_ice_candidate** const candidatep,  // de-referenced
+    struct pl* const sdp,
+    char* const mid,  // nullable
+    uint8_t const* const media_line_index,  // nullable
+    char* const username_fragment);
diff --git a/src/peer_connection_ice_candidate/meson.build b/src/peer_connection_ice_candidate/meson.build
new file mode 100644
index 0000000..6ff3140
--- /dev/null
+++ b/src/peer_connection_ice_candidate/meson.build
@@ -0,0 +1,5 @@
+sources += files([
+    'attributes.c',
+    'candidate.c',
+    'utils.c',
+])
diff --git a/src/peer_connection_ice_candidate/utils.c b/src/peer_connection_ice_candidate/utils.c
new file mode 100644
index 0000000..49cd11e
--- /dev/null
+++ b/src/peer_connection_ice_candidate/utils.c
@@ -0,0 +1,47 @@
+#include "candidate.h"
+#include "../ice_candidate/candidate.h"
+#include <rawrtcc/code.h>
+#include <re.h>
+
+/*
+ * Print debug information for an ICE candidate.
+ */
+int rawrtc_peer_connection_ice_candidate_debug(
+    struct re_printf* const pf, struct rawrtc_peer_connection_ice_candidate* const candidate) {
+    int err = 0;
+
+    // Check arguments
+    if (!candidate) {
+        return 0;
+    }
+
+    // ORTC ICE candidate
+    err |= re_hprintf(pf, "%H", rawrtc_ice_candidate_debug, candidate->candidate);
+
+    // Media line identification tag
+    err |= re_hprintf(pf, "    mid=");
+    if (candidate->mid) {
+        err |= re_hprintf(pf, "\"%s\"\n", candidate->mid);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Media line index
+    err |= re_hprintf(pf, "    media_line_index=");
+    if (candidate->media_line_index >= 0 && candidate->media_line_index <= UINT8_MAX) {
+        err |= re_hprintf(pf, "%" PRId16 "\n", candidate->media_line_index);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Username fragment
+    err |= re_hprintf(pf, "    username_fragment=");
+    if (candidate->username_fragment) {
+        err |= re_hprintf(pf, "\"%s\"\n", candidate->username_fragment);
+    } else {
+        err |= re_hprintf(pf, "n/a\n");
+    }
+
+    // Done
+    return err;
+}
diff --git a/src/peer_connection_state/meson.build b/src/peer_connection_state/meson.build
new file mode 100644
index 0000000..55395eb
--- /dev/null
+++ b/src/peer_connection_state/meson.build
@@ -0,0 +1 @@
+sources += files('state.c')
diff --git a/src/peer_connection_state/state.c b/src/peer_connection_state/state.c
new file mode 100644
index 0000000..5aa274c
--- /dev/null
+++ b/src/peer_connection_state/state.c
@@ -0,0 +1,45 @@
+#include <rawrtc/peer_connection_state.h>
+
+/*
+ * Get the corresponding name for a signaling state.
+ */
+char const* rawrtc_signaling_state_to_name(enum rawrtc_signaling_state const state) {
+    switch (state) {
+        case RAWRTC_SIGNALING_STATE_STABLE:
+            return "stable";
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER:
+            return "have-local-offer";
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER:
+            return "have-remote-offer";
+        case RAWRTC_SIGNALING_STATE_HAVE_LOCAL_PROVISIONAL_ANSWER:
+            return "have-local-pranswer";
+        case RAWRTC_SIGNALING_STATE_HAVE_REMOTE_PROVISIONAL_ANSWER:
+            return "have-remote-pranswer";
+        case RAWRTC_SIGNALING_STATE_CLOSED:
+            return "closed";
+        default:
+            return "???";
+    }
+}
+
+/*
+ * Get the corresponding name for a peer connection state.
+ */
+char const* rawrtc_peer_connection_state_to_name(enum rawrtc_peer_connection_state const state) {
+    switch (state) {
+        case RAWRTC_PEER_CONNECTION_STATE_NEW:
+            return "new";
+        case RAWRTC_PEER_CONNECTION_STATE_CONNECTING:
+            return "connecting";
+        case RAWRTC_PEER_CONNECTION_STATE_CONNECTED:
+            return "connected";
+        case RAWRTC_PEER_CONNECTION_STATE_DISCONNECTED:
+            return "disconnected";
+        case RAWRTC_PEER_CONNECTION_STATE_CLOSED:
+            return "closed";
+        case RAWRTC_PEER_CONNECTION_STATE_FAILED:
+            return "failed";
+        default:
+            return "???";
+    }
+}
diff --git a/src/sctp_common/common.c b/src/sctp_common/common.c
new file mode 100644
index 0000000..b40bdac
--- /dev/null
+++ b/src/sctp_common/common.c
@@ -0,0 +1,112 @@
+#include "common.h"
+#include "../dtls_transport/transport.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+
+// Note: Although shared with the redirect transport, this name is accurate enough for both.
+#define DEBUG_MODULE "sctp-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * External DTLS role getter.
+ * Warning: `rolep` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_dtls_role_getter(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced, not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    return rawrtc_dtls_transport_get_external_role(rolep, dtls_transport);
+}
+
+/*
+ * Get the external DTLS transport state.
+ * Warning: `statep` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_dtls_transport_state_getter(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced, not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    return rawrtc_dtls_transport_get_external_state(statep, dtls_transport);
+}
+
+/*
+ * Outbound data handler of the SCTP transport.
+ * `buffer` will be a fake `mbuf` structure.
+ *
+ * Warning: `buffer` and `arg` will not be validated.
+ */
+enum rawrtc_code rawrtc_sctp_common_sctp_transport_outbound_handler(
+    struct mbuf* const buffer,  // not checked
+    uint8_t const tos,
+    uint8_t const set_df,
+    void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+    enum rawrtc_code error;
+
+    // TODO: Handle
+    (void) tos;
+    (void) set_df;
+
+    // Note: We only need to copy the buffer if we add it to the outgoing queue
+    if (dtls_transport->state == RAWRTC_DTLS_TRANSPORT_STATE_CONNECTED) {
+        // Send
+        error = rawrtc_dtls_transport_send(dtls_transport, buffer);
+    } else {
+        int err;
+        struct mbuf* copied_buffer;
+
+        // Get length
+        size_t const length = mbuf_get_left(buffer);
+
+        // Allocate
+        copied_buffer = mbuf_alloc(length);
+        if (!copied_buffer) {
+            DEBUG_WARNING("Could not create buffer for outgoing packet, no memory\n");
+            return RAWRTC_CODE_NO_MEMORY;
+        }
+
+        // Copy and set position
+        err = mbuf_write_mem(copied_buffer, mbuf_buf(buffer), length);
+        if (err) {
+            DEBUG_WARNING("Could not write to buffer, reason: %m\n", err);
+            mem_deref(copied_buffer);
+            return rawrtc_error_to_code(err);
+        }
+        mbuf_set_pos(copied_buffer, 0);
+
+        // Send (well, actually buffer...)
+        error = rawrtc_dtls_transport_send(dtls_transport, copied_buffer);
+        mem_deref(copied_buffer);
+    }
+
+    // Handle error & done
+    if (error) {
+        DEBUG_WARNING("Could not send packet, reason: %s\n", rawrtc_code_to_str(error));
+    }
+    return error;
+}
+
+/*
+ * Detach the SCTP transport from the DTLS transport and therefore
+ * don't feed any DTLS application data to the SCTP transport.
+ * Warning: `arg` will not be validated.
+ */
+void rawrtc_sctp_common_sctp_transport_detach_handler(void* const arg  // not checked
+) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Detach from DTLS transport
+    enum rawrtc_code error = rawrtc_dtls_transport_clear_data_transport(dtls_transport);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to detach from DTLS transport, reason: %s\n", rawrtc_code_to_str(error));
+    }
+}
diff --git a/src/sctp_common/common.h b/src/sctp_common/common.h
new file mode 100644
index 0000000..cf441f7
--- /dev/null
+++ b/src/sctp_common/common.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <rawrtcc/code.h>
+#include <rawrtcdc/external.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_sctp_common_dtls_role_getter(
+    enum rawrtc_external_dtls_role* const rolep,  // de-referenced, not checked
+    void* const arg  // not checked
+);
+
+enum rawrtc_code rawrtc_sctp_common_dtls_transport_state_getter(
+    enum rawrtc_external_dtls_transport_state* const statep,  // de-referenced, not checked
+    void* const arg  // not checked
+);
+
+enum rawrtc_code rawrtc_sctp_common_sctp_transport_outbound_handler(
+    struct mbuf* const buffer,  // not checked
+    uint8_t const tos,
+    uint8_t const set_df,
+    void* const arg  // not checked
+);
+
+void rawrtc_sctp_common_sctp_transport_detach_handler(void* const arg  // not checked
+);
diff --git a/src/sctp_common/meson.build b/src/sctp_common/meson.build
new file mode 100644
index 0000000..5ed4f3e
--- /dev/null
+++ b/src/sctp_common/meson.build
@@ -0,0 +1 @@
+sources += files('common.c')
diff --git a/src/sctp_redirect_transport/meson.build b/src/sctp_redirect_transport/meson.build
new file mode 100644
index 0000000..d516026
--- /dev/null
+++ b/src/sctp_redirect_transport/meson.build
@@ -0,0 +1 @@
+sources += files('transport.c')
diff --git a/src/sctp_redirect_transport/transport.c b/src/sctp_redirect_transport/transport.c
new file mode 100644
index 0000000..d0d022e
--- /dev/null
+++ b/src/sctp_redirect_transport/transport.c
@@ -0,0 +1,111 @@
+#include "../dtls_transport/transport.h"
+#include "../sctp_common/common.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/sctp_redirect_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/sctp_redirect_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "sctp-redirect-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Pass DTLS application data to the SCTP redirect transport as inbound
+ * data.
+ */
+static void sctp_redirect_transport_inbound_handler(
+    struct mbuf* const buffer,  // not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_sctp_redirect_transport* const transport = arg;
+
+    // Feed data
+    enum rawrtc_code const error = rawrtc_sctp_redirect_transport_feed_inbound(transport, buffer);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to feed data into the SCTP redirect transport, reason: %s\n",
+            rawrtc_code_to_str(error));
+    }
+}
+
+/*
+ * Destructor for an existing SCTP redirect transport.
+ */
+static void rawrtc_sctp_redirect_transport_destroy(void* const arg) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Un-reference
+    mem_deref(dtls_transport);
+}
+
+/*
+ * Create an SCTP redirect transport.
+ * `*transportp` must be unreferenced.
+ *
+ * `port` defaults to `5000` if set to `0`.
+ * `redirect_ip` is the target IP SCTP packets will be redirected to
+ *  and must be a IPv4 address.
+ * `redirect_port` is the target SCTP port packets will be redirected
+ *  to.
+ */
+enum rawrtc_code rawrtc_sctp_redirect_transport_create(
+    struct rawrtc_sctp_redirect_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    char* const redirect_ip,  // copied
+    uint16_t const redirect_port,
+    rawrtc_sctp_redirect_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+    struct rawrtc_sctp_redirect_transport* transport = NULL;
+
+    // Create SCTP transport context
+    struct rawrtc_sctp_transport_context context = {
+        .role_getter = NULL,
+        .state_getter = rawrtc_sctp_common_dtls_transport_state_getter,
+        .outbound_handler = rawrtc_sctp_common_sctp_transport_outbound_handler,
+        .detach_handler = rawrtc_sctp_common_sctp_transport_detach_handler,
+        .destroyed_handler = rawrtc_sctp_redirect_transport_destroy,
+        .arg = mem_ref(dtls_transport),
+    };
+
+    // Check if a data transport is already registered
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, dtls_transport);
+    if (error) {
+        goto out;
+    }
+    if (have_data_transport) {
+        error = RAWRTC_CODE_INVALID_ARGUMENT;
+        goto out;
+    }
+
+    // Create SCTP redirect transport
+    error = rawrtc_sctp_redirect_transport_create_from_external(
+        &transport, &context, port, redirect_ip, redirect_port, state_change_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+    // Attach to DTLS transport
+    DEBUG_PRINTF("Attaching as data transport\n");
+    error = rawrtc_dtls_transport_set_data_transport(
+        dtls_transport, sctp_redirect_transport_inbound_handler, transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+        mem_deref(dtls_transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
diff --git a/src/sctp_transport/meson.build b/src/sctp_transport/meson.build
new file mode 100644
index 0000000..d516026
--- /dev/null
+++ b/src/sctp_transport/meson.build
@@ -0,0 +1 @@
+sources += files('transport.c')
diff --git a/src/sctp_transport/transport.c b/src/sctp_transport/transport.c
new file mode 100644
index 0000000..c329d63
--- /dev/null
+++ b/src/sctp_transport/transport.c
@@ -0,0 +1,108 @@
+#include "../dtls_transport/transport.h"
+#include "../sctp_common/common.h"
+#include <rawrtc/config.h>
+#include <rawrtc/dtls_transport.h>
+#include <rawrtc/sctp_transport.h>
+#include <rawrtcc/code.h>
+#include <rawrtcc/utils.h>
+#include <rawrtcdc/data_channel.h>
+#include <rawrtcdc/external.h>
+#include <rawrtcdc/sctp_transport.h>
+#include <re.h>
+
+#define DEBUG_MODULE "sctp-transport"
+//#define RAWRTC_DEBUG_MODULE_LEVEL 7 // Note: Uncomment this to debug this module only
+#include <rawrtcc/debug.h>
+
+/*
+ * Pass DTLS application data to the SCTP transport as inbound data.
+ */
+static void sctp_transport_inbound_handler(
+    struct mbuf* const buffer,  // not checked
+    void* const arg  // not checked
+) {
+    struct rawrtc_sctp_transport* const transport = arg;
+
+    // Feed data
+    // TODO: What about ECN bits?
+    enum rawrtc_code const error = rawrtc_sctp_transport_feed_inbound(transport, buffer, 0x00);
+    if (error) {
+        DEBUG_WARNING(
+            "Unable to feed data into the SCTP transport, reason: %s\n", rawrtc_code_to_str(error));
+    }
+}
+
+/*
+ * Destructor for an existing SCTP transport.
+ */
+static void rawrtc_sctp_transport_destroy(void* const arg) {
+    struct rawrtc_dtls_transport* const dtls_transport = arg;
+
+    // Un-reference
+    mem_deref(dtls_transport);
+}
+
+/*
+ * Create an SCTP transport.
+ * `*transportp` must be unreferenced.
+ */
+enum rawrtc_code rawrtc_sctp_transport_create(
+    struct rawrtc_sctp_transport** const transportp,  // de-referenced
+    struct rawrtc_dtls_transport* const dtls_transport,  // referenced
+    uint16_t const port,  // zeroable
+    rawrtc_data_channel_handler const data_channel_handler,  // nullable
+    rawrtc_sctp_transport_state_change_handler const state_change_handler,  // nullable
+    void* const arg  // nullable
+) {
+    enum rawrtc_code error;
+    bool have_data_transport;
+    struct rawrtc_sctp_transport* transport = NULL;
+
+    // Create SCTP transport context
+    struct rawrtc_sctp_transport_context context = {
+        .role_getter = rawrtc_sctp_common_dtls_role_getter,
+        .state_getter = rawrtc_sctp_common_dtls_transport_state_getter,
+        .outbound_handler = rawrtc_sctp_common_sctp_transport_outbound_handler,
+        .detach_handler = rawrtc_sctp_common_sctp_transport_detach_handler,
+        .destroyed_handler = rawrtc_sctp_transport_destroy,
+        .trace_packets = false,  // TODO: Make this configurable
+        .arg = mem_ref(dtls_transport),
+    };
+
+    // Check if a data transport is already registered
+    error = rawrtc_dtls_transport_have_data_transport(&have_data_transport, dtls_transport);
+    if (error) {
+        goto out;
+    }
+    if (have_data_transport) {
+        error = RAWRTC_CODE_INVALID_ARGUMENT;
+        goto out;
+    }
+
+    // Create SCTP transport
+    error = rawrtc_sctp_transport_create_from_external(
+        &transport, &context, port, data_channel_handler, state_change_handler, arg);
+    if (error) {
+        goto out;
+    }
+
+    // TODO: Set MTU (1200|1280 (IPv4|IPv6) - UDP - DTLS (cipher suite dependent) - SCTP (12)
+
+    // Attach to DTLS transport
+    DEBUG_PRINTF("Attaching as data transport\n");
+    error = rawrtc_dtls_transport_set_data_transport(
+        dtls_transport, sctp_transport_inbound_handler, transport);
+    if (error) {
+        goto out;
+    }
+
+out:
+    if (error) {
+        mem_deref(transport);
+        mem_deref(dtls_transport);
+    } else {
+        // Set pointer
+        *transportp = transport;
+    }
+    return error;
+}
diff --git a/src/utils/meson.build b/src/utils/meson.build
new file mode 100644
index 0000000..d82c551
--- /dev/null
+++ b/src/utils/meson.build
@@ -0,0 +1 @@
+sources += files('utils.c')
diff --git a/src/utils/utils.c b/src/utils/utils.c
new file mode 100644
index 0000000..26af2a9
--- /dev/null
+++ b/src/utils/utils.c
@@ -0,0 +1,163 @@
+#include "utils.h"
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+#include <stdarg.h>  // va_*
+#include <stdio.h>  // sprintf
+#include <string.h>  // strlen
+
+/*
+ * Convert binary to hex string where each value is separated by a
+ * colon.
+ */
+enum rawrtc_code rawrtc_bin_to_colon_hex(
+    char** const destinationp,  // de-referenced
+    uint8_t* const source,
+    size_t const length) {
+    char* hex_str;
+    char* hex_ptr;
+    size_t i;
+    int ret;
+    enum rawrtc_code error = RAWRTC_CODE_SUCCESS;
+
+    // Check arguments
+    if (!destinationp || !source) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Allocate hex string
+    hex_str = mem_zalloc(length > 0 ? (length * 3) : 1, NULL);
+    if (!hex_str) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+
+    // Bin to hex
+    hex_ptr = hex_str;
+    for (i = 0; i < length; ++i) {
+        if (i > 0) {
+            *hex_ptr = ':';
+            ++hex_ptr;
+        }
+        ret = sprintf(hex_ptr, "%02X", source[i]);
+        if (ret != 2) {
+            error = RAWRTC_CODE_UNKNOWN_ERROR;
+            goto out;
+        } else {
+            hex_ptr += ret;
+        }
+    }
+
+out:
+    if (error) {
+        mem_deref(hex_str);
+    } else {
+        // Set pointer
+        *destinationp = hex_str;
+    }
+    return error;
+}
+
+/*
+ * Convert hex string with colon-separated hex values to binary.
+ */
+enum rawrtc_code rawrtc_colon_hex_to_bin(
+    size_t* const bytes_written,  // de-referenced
+    uint8_t* const buffer,  // written into
+    size_t const buffer_size,
+    char* source) {
+    size_t hex_length;
+    size_t bin_length;
+    size_t i;
+
+    // Check arguments
+    if (!bytes_written || !buffer || !source) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Validate length
+    hex_length = strlen(source);
+    if (hex_length > 0 && hex_length % 3 != 2) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Determine size
+    bin_length = hex_length > 0 ? (size_t)((hex_length + 1) / 3) : 0;
+    if (bin_length > buffer_size) {
+        return RAWRTC_CODE_INSUFFICIENT_SPACE;
+    }
+
+    // Hex to bin
+    for (i = 0; i < bin_length; ++i) {
+        if (i > 0) {
+            // Skip colon
+            ++source;
+        }
+        buffer[i] = ch_hex(*source) << 4;
+        ++source;
+        buffer[i] += ch_hex(*source);
+        ++source;
+    }
+
+    // Done
+    *bytes_written = bin_length;
+    return RAWRTC_CODE_SUCCESS;
+}
+
+/*
+ * Destructor for an existing array container that did reference each
+ * item.
+ */
+static void rawrtc_array_container_destroy(void* arg) {
+    struct rawrtc_array_container* const container = arg;
+    size_t i;
+
+    // Un-reference each item
+    for (i = 0; i < container->n_items; ++i) {
+        mem_deref(container->items[i]);
+    }
+}
+
+/*
+ * Convert a list to a dynamically allocated array container.
+ *
+ * If `reference` is set to `true`, each item in the list will be
+ * referenced and a destructor will be added that unreferences each
+ * item when unreferencing the array.
+ */
+enum rawrtc_code rawrtc_list_to_array(
+    struct rawrtc_array_container** containerp,  // de-referenced
+    struct list const* const list,
+    bool reference) {
+    size_t n;
+    struct rawrtc_array_container* container;
+    struct le* le;
+    size_t i;
+
+    // Check arguments
+    if (!containerp || !list) {
+        return RAWRTC_CODE_INVALID_ARGUMENT;
+    }
+
+    // Get list length
+    n = list_count(list);
+
+    // Allocate array & set length immediately
+    container = mem_zalloc(
+        sizeof(*container) + sizeof(void*) * n, reference ? rawrtc_array_container_destroy : NULL);
+    if (!container) {
+        return RAWRTC_CODE_NO_MEMORY;
+    }
+    container->n_items = n;
+
+    // Copy pointer to each item
+    for (le = list_head(list), i = 0; le != NULL; le = le->next, ++i) {
+        if (reference) {
+            mem_ref(le->data);
+        }
+        container->items[i] = le->data;
+    }
+
+    // Set pointer & done
+    *containerp = container;
+    return RAWRTC_CODE_SUCCESS;
+}
diff --git a/src/utils/utils.h b/src/utils/utils.h
new file mode 100644
index 0000000..9557277
--- /dev/null
+++ b/src/utils/utils.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <rawrtc/utils.h>
+#include <rawrtcc/code.h>
+#include <re.h>
+
+enum rawrtc_code rawrtc_bin_to_colon_hex(
+    char** const destinationp,  // de-referenced
+    uint8_t* const source,
+    size_t const length);
+
+enum rawrtc_code rawrtc_colon_hex_to_bin(
+    size_t* const bytes_written,  // de-referenced
+    uint8_t* const buffer,  // written into
+    size_t const buffer_size,
+    char* source);
+
+enum rawrtc_code rawrtc_list_to_array(
+    struct rawrtc_array_container** containerp,  // de-referenced
+    struct list const* const list,
+    bool reference);