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/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;
+}