From d678f44282e8a85166a062113f9c439685ca4667 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Wed, 12 Jun 2024 09:38:25 +0100
Subject: [PATCH 1/5] feat: support setting ICE ufrag and pwd, and missing
 config values

Updates the implementation to match the latest libdatachannel with
features for setting ICE ufrag/pwd and reading the remote cert
fingerprint.

Also adds pass-through for missing config values.
---
 API.md                              | 28 ++++++++++++
 src/cpp/peer-connection-wrapper.cpp | 70 ++++++++++++++++++++++++++++-
 src/cpp/peer-connection-wrapper.h   |  1 +
 src/lib/index.ts                    |  5 ++-
 src/lib/types.ts                    | 19 ++++++++
 5 files changed, 119 insertions(+), 4 deletions(-)

diff --git a/API.md b/API.md
index e95f9a9..c4aa5c9 100644
--- a/API.md
+++ b/API.md
@@ -16,12 +16,19 @@ export interface RtcConfig {
     bindAddress?: string;
     enableIceTcp?: boolean;
     enableIceUdpMux?: boolean;
+    disableAutoNegotiation?: boolean;
+    disableFingerprintVerification?: boolean;
+    disableAutoGathering?: boolean;
+    forceMediaTransport?: boolean;
     portRangeBegin?: number;
     portRangeEnd?: number;
     maxMessageSize?: number;
     mtu?: number;
     iceTransportPolicy?: TransportPolicy;
     disableFingerprintVerification?: boolean;
+    certificatePemFile?: string;
+    keyPemFile?: string;
+    keyPemPass?: string;
 }
 
 export const enum RelayType {
@@ -69,6 +76,27 @@ export const enum DescriptionType {
 }
 ```
 
+**setLocalDescription: (sdp: string, init?: LocalDescriptionInit) => void**
+
+Set Local Description and optionally the ICE ufrag/pwd to use. These should not
+be set as they will be generated automatically as per the spec.
+```
+export interface LocalDescriptionInit {
+    iceUfrag?: string;
+    icePwd?: string;
+}
+```
+
+**remoteFingerprint: () => CertificateFingerprint**
+
+Returns the certificate fingerprint used by the remote peer
+```
+export interface CertificateFingerprint {
+    value: string;
+    algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2';
+}
+```
+
 **addRemoteCandidate: (candidate: string, mid: string) => void**
 
 Add remote candidate info
diff --git a/src/cpp/peer-connection-wrapper.cpp b/src/cpp/peer-connection-wrapper.cpp
index 4e3d9e7..200a9ce 100644
--- a/src/cpp/peer-connection-wrapper.cpp
+++ b/src/cpp/peer-connection-wrapper.cpp
@@ -41,6 +41,7 @@ Napi::Object PeerConnectionWrapper::Init(Napi::Env env, Napi::Object exports)
             InstanceMethod("setRemoteDescription", &PeerConnectionWrapper::setRemoteDescription),
             InstanceMethod("localDescription", &PeerConnectionWrapper::localDescription),
             InstanceMethod("remoteDescription", &PeerConnectionWrapper::remoteDescription),
+            InstanceMethod("remoteFingerprint", &PeerConnectionWrapper::remoteFingerprint),
             InstanceMethod("addRemoteCandidate", &PeerConnectionWrapper::addRemoteCandidate),
             InstanceMethod("createDataChannel", &PeerConnectionWrapper::createDataChannel),
             InstanceMethod("addTrack", &PeerConnectionWrapper::addTrack),
@@ -214,6 +215,10 @@ PeerConnectionWrapper::PeerConnectionWrapper(const Napi::CallbackInfo &info) : N
     if (config.Get("disableAutoNegotiation").IsBoolean())
         rtcConfig.disableAutoNegotiation = config.Get("disableAutoNegotiation").As<Napi::Boolean>();
 
+    // disableAutoGathering option
+    if (config.Get("disableAutoGathering").IsBoolean())
+        rtcConfig.disableAutoGathering = config.Get("disableAutoGathering").As<Napi::Boolean>();
+
     // forceMediaTransport option
     if (config.Get("forceMediaTransport").IsBoolean())
         rtcConfig.forceMediaTransport = config.Get("forceMediaTransport").As<Napi::Boolean>();
@@ -251,6 +256,17 @@ PeerConnectionWrapper::PeerConnectionWrapper(const Napi::CallbackInfo &info) : N
         rtcConfig.disableFingerprintVerification = config.Get("disableFingerprintVerification").As<Napi::Boolean>();
     }
 
+    // Specify certificate to use if set
+    if (config.Get("certificatePemFile").IsString()) {
+        rtcConfig.certificatePemFile = config.Get("certificatePemFile").As<Napi::String>().ToString();
+    }
+    if (config.Get("keyPemFile").IsString()) {
+        rtcConfig.keyPemFile = config.Get("keyPemFile").As<Napi::String>().ToString();
+    }
+    if (config.Get("keyPemPass").IsString()) {
+        rtcConfig.keyPemPass = config.Get("keyPemPass").As<Napi::String>().ToString();
+    }
+
     // Create peer-connection
     try
     {
@@ -331,6 +347,7 @@ void PeerConnectionWrapper::setLocalDescription(const Napi::CallbackInfo &info)
     }
 
     rtc::Description::Type type = rtc::Description::Type::Unspec;
+    rtc::LocalDescriptionInit init;
 
     // optional
     if (length > 0)
@@ -356,7 +373,29 @@ void PeerConnectionWrapper::setLocalDescription(const Napi::CallbackInfo &info)
             type = rtc::Description::Type::Rollback;
     }
 
-    mRtcPeerConnPtr->setLocalDescription(type);
+    // optional
+    if (length > 1)
+    {
+        PLOG_DEBUG << "setLocalDescription() called with LocalDescriptionInit";
+
+        if (info[1].IsObject())
+        {
+            PLOG_DEBUG << "setLocalDescription() called with LocalDescriptionInit as object";
+            Napi::Object obj = info[1].As<Napi::Object>();
+
+            if (obj.Get("iceUfrag").IsString()) {
+                PLOG_DEBUG << "setLocalDescription() has ufrag";
+                init.iceUfrag = obj.Get("iceUfrag").As<Napi::String>();
+            }
+
+            if (obj.Get("icePwd").IsString()) {
+                PLOG_DEBUG << "setLocalDescription() has password";
+                init.icePwd = obj.Get("icePwd").As<Napi::String>();
+            }
+        }
+    }
+
+    mRtcPeerConnPtr->setLocalDescription(type, init);
 }
 
 void PeerConnectionWrapper::setRemoteDescription(const Napi::CallbackInfo &info)
@@ -1002,7 +1041,34 @@ Napi::Value PeerConnectionWrapper::maxMessageSize(const Napi::CallbackInfo &info
 
     try
     {
-        return Napi::Number::New(env, mRtcPeerConnPtr->remoteMaxMessageSize());
+        return Napi::Array::New(env, mRtcPeerConnPtr->remoteMaxMessageSize());
+    }
+    catch (std::exception &ex)
+    {
+        Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException();
+        return Napi::Number::New(info.Env(), 0);
+    }
+}
+
+Napi::Value PeerConnectionWrapper::remoteFingerprint(const Napi::CallbackInfo &info)
+{
+    PLOG_DEBUG << "remoteFingerprints() called";
+    Napi::Env env = info.Env();
+
+    if (!mRtcPeerConnPtr)
+    {
+        return Napi::Number::New(info.Env(), 0);
+    }
+
+    try
+    {
+        auto fingerprint = mRtcPeerConnPtr->remoteFingerprint();
+
+        Napi::Object fingerprintObject = Napi::Object::New(env);
+        fingerprintObject.Set("value", fingerprint.value);
+        fingerprintObject.Set("algorithm", rtc::CertificateFingerprint::AlgorithmIdentifier(fingerprint.algorithm));
+
+        return fingerprintObject;
     }
     catch (std::exception &ex)
     {
diff --git a/src/cpp/peer-connection-wrapper.h b/src/cpp/peer-connection-wrapper.h
index 5896e30..3865c08 100644
--- a/src/cpp/peer-connection-wrapper.h
+++ b/src/cpp/peer-connection-wrapper.h
@@ -33,6 +33,7 @@ class PeerConnectionWrapper : public Napi::ObjectWrap<PeerConnectionWrapper>
   Napi::Value iceState(const Napi::CallbackInfo &info);
   Napi::Value signalingState(const Napi::CallbackInfo &info);
   Napi::Value gatheringState(const Napi::CallbackInfo &info);
+  Napi::Value remoteFingerprint(const Napi::CallbackInfo &info);
 
   // Callbacks
   void onLocalDescription(const Napi::CallbackInfo &info);
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 8b1dd82..5bafb2e 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,7 +1,7 @@
 import nodeDataChannel from './node-datachannel';
 import _DataChannelStream from './datachannel-stream';
 import { WebSocketServer } from './websocket-server';
-import { Channel, DataChannelInitConfig, DescriptionType, Direction, LogLevel, RtcConfig, RTCIceConnectionState, RTCIceGatheringState, RTCPeerConnectionState, RTCSignalingState, SctpSettings, SelectedCandidateInfo } from './types';
+import { CertificateFingerprint, Channel, DataChannelInitConfig, DescriptionType, Direction, LocalDescriptionInit, LogLevel, RtcConfig, RTCIceConnectionState, RTCIceGatheringState, RTCPeerConnectionState, RTCSignalingState, SctpSettings, SelectedCandidateInfo } from './types';
 import { WebSocket } from './websocket';
 
 export function preload(): void { nodeDataChannel.preload(); }
@@ -113,10 +113,11 @@ export const DataChannel: {
 
 export interface PeerConnection {
     close(): void;
-    setLocalDescription(type?: DescriptionType): void;
+    setLocalDescription(type?: DescriptionType, init?: LocalDescriptionInit): void;
     setRemoteDescription(sdp: string, type: DescriptionType): void;
     localDescription(): { type: DescriptionType; sdp: string } | null;
     remoteDescription(): { type: DescriptionType; sdp: string } | null;
+    remoteFingerprint(): CertificateFingerprint;
     addRemoteCandidate(candidate: string, mid: string): void;
     createDataChannel(label: string, config?: DataChannelInitConfig): DataChannel;
     addTrack(media: Video | Audio): Track;
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 75541e3..a14a9a7 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -77,6 +77,10 @@ export interface RtcConfig {
     mtu?: number;
     iceTransportPolicy?: TransportPolicy;
     disableFingerprintVerification?: boolean;
+    disableAutoGathering?: boolean;
+    certificatePemFile?: string;
+    keyPemFile?: string;
+    keyPemPass?: string;
 }
 
 // Lowercase to match the description type string from libdatachannel
@@ -97,6 +101,10 @@ export type RTCIceGathererState = "complete" | "gathering" | "new";
 export type RTCIceGatheringState = "complete" | "gathering" | "new";
 export type RTCSignalingState = "closed" | "have-local-offer" | "have-local-pranswer" | "have-remote-offer" | "have-remote-pranswer" | "stable";
 
+export interface LocalDescriptionInit {
+    iceUfrag?: string;
+    icePwd?: string;
+}
 
 export interface DataChannelInitConfig {
     protocol?: string;
@@ -107,6 +115,17 @@ export interface DataChannelInitConfig {
     maxRetransmits?: number; // Reliability
 }
 
+export interface CertificateFingerprint {
+    /**
+     * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#value
+     */
+    value: string;
+    /**
+     * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#algorithm
+     */
+    algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2';
+}
+
 export interface SelectedCandidateInfo {
     address: string;
     port: number;

From f3211b15dd6e4931dfc600b7f787befc5a5afa5b Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Tue, 14 Jan 2025 17:54:59 +0100
Subject: [PATCH 2/5] chore: fix linting

---
 src/lib/node-datachannel.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib/node-datachannel.ts b/src/lib/node-datachannel.ts
index 4a08e83..1929f2a 100644
--- a/src/lib/node-datachannel.ts
+++ b/src/lib/node-datachannel.ts
@@ -1,2 +1,2 @@
-let nodeDataChannel = require('../../build/Release/node_datachannel.node');
+import nodeDataChannel = require('../../build/Release/node_datachannel.node');
 export default nodeDataChannel;

From bd58537b31af38664119270ac2788b34a9b384e2 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Thu, 16 Jan 2025 19:12:55 +0100
Subject: [PATCH 3/5] feat: support different callbacks for different ports

---
 src/cpp/rtc-wrapper.cpp | 59 +++++++++++++++++++++++++++++++++++++++++
 src/cpp/rtc-wrapper.h   |  2 ++
 src/lib/index.ts        |  2 ++
 3 files changed, 63 insertions(+)

diff --git a/src/cpp/rtc-wrapper.cpp b/src/cpp/rtc-wrapper.cpp
index db2a298..0b9a224 100644
--- a/src/cpp/rtc-wrapper.cpp
+++ b/src/cpp/rtc-wrapper.cpp
@@ -18,6 +18,7 @@ Napi::Object RtcWrapper::Init(Napi::Env env, Napi::Object exports)
     exports.Set("cleanup", Napi::Function::New(env, &RtcWrapper::cleanup));
     exports.Set("preload", Napi::Function::New(env, &RtcWrapper::preload));
     exports.Set("setSctpSettings", Napi::Function::New(env, &RtcWrapper::setSctpSettings));
+    exports.Set("onUnhandledStunRequest", Napi::Function::New(env, &RtcWrapper::onUnhandledStunRequest));
 
     return exports;
 }
@@ -172,3 +173,61 @@ void RtcWrapper::setSctpSettings(const Napi::CallbackInfo &info)
 
     rtc::SetSctpSettings(settings);
 }
+
+void RtcWrapper::onUnhandledStunRequest(const Napi::CallbackInfo &info)
+{
+    PLOG_DEBUG << "onUnhandledStunRequest() called";
+    Napi::Env env = info.Env();
+    int length = info.Length();
+
+    if (length < 1 || !info[0].IsString())
+    {
+        Napi::TypeError::New(env, "host (String) expected").ThrowAsJavaScriptException();
+        return;
+    }
+    Napi::String host = info[0].As<Napi::String>();
+
+    if (length < 2 || !info[1].IsNumber())
+    {
+        Napi::TypeError::New(env, "port (Number) expected").ThrowAsJavaScriptException();
+        return;
+    }
+    Napi::Number port = info[1].As<Napi::Number>();
+
+    // unbind listener if cb is null, undefined, or was omitted
+    if (length == 2 || (info[2].IsNull() || info[2].IsUndefined())) {
+        unboundStunCallbacks.erase(port.ToNumber().Uint32Value());
+        rtc::OnUnhandledStunRequest(host.ToString(), port.ToNumber());
+        return;
+    }
+
+    if (length < 3 || !info[2].IsFunction())
+    {
+        Napi::TypeError::New(env, "cb (Function) expected").ThrowAsJavaScriptException();
+        return;
+    }
+    Napi::Function cb = info[2].As<Napi::Function>();
+
+    std::unique_ptr<ThreadSafeCallback> callback = std::make_unique<ThreadSafeCallback>(cb);
+    unboundStunCallbacks[port.ToNumber().Uint32Value()] = std::move(callback);
+    void * ptr = &unboundStunCallbacks[port.ToNumber().Uint32Value()];
+
+    rtc::OnUnhandledStunRequest(host.ToString(), port.ToNumber(), [&](rtc::UnhandledStunRequest request, void *userPtr) {
+        PLOG_DEBUG << "mOnUnhandledStunRequestCallback call(1)";
+
+        std::unique_ptr<ThreadSafeCallback> &callback = *(std::unique_ptr<ThreadSafeCallback> *)userPtr;
+
+        if (callback) {
+            callback->call([request = std::move(request)](Napi::Env env, std::vector<napi_value> &args) {
+                Napi::Object reqObj = Napi::Object::New(env);
+                reqObj.Set("ufrag", request.remoteUfrag.c_str());
+                reqObj.Set("host", request.remoteHost.c_str());
+                reqObj.Set("port", request.remotePort);
+
+                args = {reqObj};
+            });
+        }
+
+        PLOG_DEBUG << "mOnUnhandledStunRequestCallback call(2)";
+    }, ptr);
+}
diff --git a/src/cpp/rtc-wrapper.h b/src/cpp/rtc-wrapper.h
index 42ce8ed..65330ef 100644
--- a/src/cpp/rtc-wrapper.h
+++ b/src/cpp/rtc-wrapper.h
@@ -18,8 +18,10 @@ class RtcWrapper
     static void initLogger(const Napi::CallbackInfo &info);
     static void cleanup(const Napi::CallbackInfo &info);
     static void setSctpSettings(const Napi::CallbackInfo &info);
+    static void onUnhandledStunRequest(const Napi::CallbackInfo &info);
 private:
     static inline std::unique_ptr<ThreadSafeCallback> logCallback = nullptr;
+    static inline std::map<u_int16_t, std::unique_ptr<ThreadSafeCallback>> unboundStunCallbacks;
 };
 
 #endif // RTC_WRAPPER_H
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 5bafb2e..64c0fb6 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -8,6 +8,7 @@ export function preload(): void { nodeDataChannel.preload(); }
 export function initLogger(level: LogLevel): void { nodeDataChannel.initLogger(level); }
 export function cleanup(): void { nodeDataChannel.cleanup(); }
 export function setSctpSettings(settings: SctpSettings): void { nodeDataChannel.setSctpSettings(settings); }
+export function onUnhandledStunRequest(host: string, port: number, cb?: (req: { ufrag: string, host: string, port: number }) => void): void { nodeDataChannel.onUnhandledStunRequest(host, port, cb); }
 
 export interface Audio {
     addAudioCodec(payloadType: number, codec: string, profile?: string): void;
@@ -159,6 +160,7 @@ export default {
     cleanup,
     preload,
     setSctpSettings,
+    onUnhandledStunRequest,
     RtcpReceivingSession,
     Track,
     Video,

From 122950dc441c6026812b377f3e9c3310ffbd6399 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Fri, 17 Jan 2025 18:05:17 +0100
Subject: [PATCH 4/5] chore: align with libdatachannel

---
 src/cpp/rtc-wrapper.cpp           | 57 ++++++++++++++++---------------
 src/cpp/rtc-wrapper.h             |  5 +--
 src/lib/index.ts                  |  4 +--
 src/lib/node-datachannel.ts       |  1 +
 src/lib/types.ts                  | 22 ++----------
 src/polyfill/RTCPeerConnection.ts |  5 +--
 6 files changed, 42 insertions(+), 52 deletions(-)

diff --git a/src/cpp/rtc-wrapper.cpp b/src/cpp/rtc-wrapper.cpp
index 0b9a224..3faa92d 100644
--- a/src/cpp/rtc-wrapper.cpp
+++ b/src/cpp/rtc-wrapper.cpp
@@ -18,7 +18,7 @@ Napi::Object RtcWrapper::Init(Napi::Env env, Napi::Object exports)
     exports.Set("cleanup", Napi::Function::New(env, &RtcWrapper::cleanup));
     exports.Set("preload", Napi::Function::New(env, &RtcWrapper::preload));
     exports.Set("setSctpSettings", Napi::Function::New(env, &RtcWrapper::setSctpSettings));
-    exports.Set("onUnhandledStunRequest", Napi::Function::New(env, &RtcWrapper::onUnhandledStunRequest));
+    exports.Set("listenIceUdpMux", Napi::Function::New(env, &RtcWrapper::listenIceUdpMux));
 
     return exports;
 }
@@ -174,48 +174,46 @@ void RtcWrapper::setSctpSettings(const Napi::CallbackInfo &info)
     rtc::SetSctpSettings(settings);
 }
 
-void RtcWrapper::onUnhandledStunRequest(const Napi::CallbackInfo &info)
+void RtcWrapper::listenIceUdpMux(const Napi::CallbackInfo &info)
 {
-    PLOG_DEBUG << "onUnhandledStunRequest() called";
+    PLOG_DEBUG << "listenIceUdpMux() called";
     Napi::Env env = info.Env();
     int length = info.Length();
 
-    if (length < 1 || !info[0].IsString())
+    if (length < 1 || !info[0].IsNumber())
     {
-        Napi::TypeError::New(env, "host (String) expected").ThrowAsJavaScriptException();
+        Napi::TypeError::New(env, "port (Number) expected").ThrowAsJavaScriptException();
         return;
     }
-    Napi::String host = info[0].As<Napi::String>();
+    int port = info[0].As<Napi::Number>().ToNumber();
 
-    if (length < 2 || !info[1].IsNumber())
+    Napi::Function listener;
+    if (length > 1 && info[1].IsFunction())
     {
-        Napi::TypeError::New(env, "port (Number) expected").ThrowAsJavaScriptException();
-        return;
+       listener = info[1].As<Napi::Function>();
     }
-    Napi::Number port = info[1].As<Napi::Number>();
 
-    // unbind listener if cb is null, undefined, or was omitted
-    if (length == 2 || (info[2].IsNull() || info[2].IsUndefined())) {
-        unboundStunCallbacks.erase(port.ToNumber().Uint32Value());
-        rtc::OnUnhandledStunRequest(host.ToString(), port.ToNumber());
-        return;
+    std::string host;
+    if (length > 2 && info[2].IsString())
+    {
+       host = info[2].As<Napi::String>().ToString();
     }
 
-    if (length < 3 || !info[2].IsFunction())
+    // unbind listener if cb is null, undefined, or was omitted
+    if (!listener)
     {
-        Napi::TypeError::New(env, "cb (Function) expected").ThrowAsJavaScriptException();
+        std::lock_guard<std::mutex> guard(iceUdpMuxListenersMutex);
+        iceUdpMuxListeners.erase(port);
+        rtc::ListenIceUdpMux(port, nullptr, host);
         return;
     }
-    Napi::Function cb = info[2].As<Napi::Function>();
-
-    std::unique_ptr<ThreadSafeCallback> callback = std::make_unique<ThreadSafeCallback>(cb);
-    unboundStunCallbacks[port.ToNumber().Uint32Value()] = std::move(callback);
-    void * ptr = &unboundStunCallbacks[port.ToNumber().Uint32Value()];
 
-    rtc::OnUnhandledStunRequest(host.ToString(), port.ToNumber(), [&](rtc::UnhandledStunRequest request, void *userPtr) {
-        PLOG_DEBUG << "mOnUnhandledStunRequestCallback call(1)";
+    auto uniqueCallback = std::make_unique<ThreadSafeCallback>(listener);
+    auto callback = std::shared_ptr<ThreadSafeCallback>(std::move(uniqueCallback));
 
-        std::unique_ptr<ThreadSafeCallback> &callback = *(std::unique_ptr<ThreadSafeCallback> *)userPtr;
+    rtc::IceUdpMuxCallback iceUdpMuxCallback = [callback](rtc::IceUdpMuxRequest request)
+    {
+        PLOG_DEBUG << "listenIceUdpMux IceUdpMuxCallback call(1)";
 
         if (callback) {
             callback->call([request = std::move(request)](Napi::Env env, std::vector<napi_value> &args) {
@@ -228,6 +226,11 @@ void RtcWrapper::onUnhandledStunRequest(const Napi::CallbackInfo &info)
             });
         }
 
-        PLOG_DEBUG << "mOnUnhandledStunRequestCallback call(2)";
-    }, ptr);
+        PLOG_DEBUG << "listenIceUdpMux IceUdpMuxCallback call(2)";
+    };
+
+    std::lock_guard<std::mutex> guard(iceUdpMuxListenersMutex);
+    iceUdpMuxListeners[port] = std::move(iceUdpMuxCallback);
+
+    rtc::ListenIceUdpMux(port, &iceUdpMuxListeners[port], host);
 }
diff --git a/src/cpp/rtc-wrapper.h b/src/cpp/rtc-wrapper.h
index 65330ef..49e6c03 100644
--- a/src/cpp/rtc-wrapper.h
+++ b/src/cpp/rtc-wrapper.h
@@ -18,10 +18,11 @@ class RtcWrapper
     static void initLogger(const Napi::CallbackInfo &info);
     static void cleanup(const Napi::CallbackInfo &info);
     static void setSctpSettings(const Napi::CallbackInfo &info);
-    static void onUnhandledStunRequest(const Napi::CallbackInfo &info);
+    static void listenIceUdpMux(const Napi::CallbackInfo &info);
 private:
     static inline std::unique_ptr<ThreadSafeCallback> logCallback = nullptr;
-    static inline std::map<u_int16_t, std::unique_ptr<ThreadSafeCallback>> unboundStunCallbacks;
+    static inline std::mutex iceUdpMuxListenersMutex;
+    static inline std::map<u_int16_t, rtc::IceUdpMuxCallback> iceUdpMuxListeners;
 };
 
 #endif // RTC_WRAPPER_H
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 64c0fb6..9df0cb6 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -8,7 +8,7 @@ export function preload(): void { nodeDataChannel.preload(); }
 export function initLogger(level: LogLevel): void { nodeDataChannel.initLogger(level); }
 export function cleanup(): void { nodeDataChannel.cleanup(); }
 export function setSctpSettings(settings: SctpSettings): void { nodeDataChannel.setSctpSettings(settings); }
-export function onUnhandledStunRequest(host: string, port: number, cb?: (req: { ufrag: string, host: string, port: number }) => void): void { nodeDataChannel.onUnhandledStunRequest(host, port, cb); }
+export function listenIceUdpMux(port: number, cb?: (req: { ufrag: string, host: string, port: number }) => void, host?: string): void { nodeDataChannel.listenIceUdpMux(port, cb, host); }
 
 export interface Audio {
     addAudioCodec(payloadType: number, codec: string, profile?: string): void;
@@ -160,7 +160,7 @@ export default {
     cleanup,
     preload,
     setSctpSettings,
-    onUnhandledStunRequest,
+    listenIceUdpMux,
     RtcpReceivingSession,
     Track,
     Video,
diff --git a/src/lib/node-datachannel.ts b/src/lib/node-datachannel.ts
index 1929f2a..fb87e3c 100644
--- a/src/lib/node-datachannel.ts
+++ b/src/lib/node-datachannel.ts
@@ -1,2 +1,3 @@
+// @ts-expect-error no types
 import nodeDataChannel = require('../../build/Release/node_datachannel.node');
 export default nodeDataChannel;
diff --git a/src/lib/types.ts b/src/lib/types.ts
index a14a9a7..ed3619d 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -47,11 +47,7 @@ export interface ProxyServer {
     password?: string;
 }
 
-export const enum RelayType {
-    TurnUdp = 'TurnUdp',
-    TurnTcp = 'TurnTcp',
-    TurnTls = 'TurnTls',
-}
+export type RelayType = 'TurnUdp' | 'TurnTcp' | 'TurnTls'
 
 export interface IceServer {
     hostname: string;
@@ -84,13 +80,7 @@ export interface RtcConfig {
 }
 
 // Lowercase to match the description type string from libdatachannel
-export enum DescriptionType {
-    Unspec = 'unspec',
-    Offer = 'offer',
-    Answer = 'answer',
-    Pranswer = 'pranswer',
-    Rollback = 'rollback',
-}
+export type DescriptionType = 'unspec' | 'offer' | 'answer' | 'pranswer' | 'rollback'
 
 export type RTCSdpType = 'answer' | 'offer' | 'pranswer' | 'rollback';
 
@@ -137,10 +127,4 @@ export interface SelectedCandidateInfo {
 }
 
 // Must be same as rtc enum class Direction
-export const enum Direction {
-    SendOnly = 'SendOnly',
-    RecvOnly = 'RecvOnly',
-    SendRecv = 'SendRecv',
-    Inactive = 'Inactive',
-    Unknown = 'Unknown',
-}
+export type Direction = 'SendOnly' | 'RecvOnly' | 'SendRecv' | 'Inactive' | 'Unknown'
diff --git a/src/polyfill/RTCPeerConnection.ts b/src/polyfill/RTCPeerConnection.ts
index a809c01..520aab3 100644
--- a/src/polyfill/RTCPeerConnection.ts
+++ b/src/polyfill/RTCPeerConnection.ts
@@ -121,7 +121,8 @@ export default class RTCPeerConnection extends EventTarget implements globalThis
 
         try {
             const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`;
-            this.#peerConnection = new PeerConnection(peerIdentity,
+            // @ts-expect-error fixme
+            this.#peerConnection = config.peerConnection ?? new PeerConnection(peerIdentity,
                 {
                     ...config,
                     iceServers:
@@ -282,7 +283,7 @@ export default class RTCPeerConnection extends EventTarget implements globalThis
         return this.#peerConnection.signalingState();
     }
 
-    async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | RTCIceCandidate): Promise<void> {
+    async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise<void> {
         if (!candidate || !candidate.candidate) {
             return;
         }

From 29f1679d69f42ebb01a8270657ebc173678b6448 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Fri, 17 Jan 2025 18:06:36 +0100
Subject: [PATCH 5/5] chore: align with libdatachannel

---
 src/lib/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib/index.ts b/src/lib/index.ts
index 9df0cb6..74e41de 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -8,7 +8,7 @@ export function preload(): void { nodeDataChannel.preload(); }
 export function initLogger(level: LogLevel): void { nodeDataChannel.initLogger(level); }
 export function cleanup(): void { nodeDataChannel.cleanup(); }
 export function setSctpSettings(settings: SctpSettings): void { nodeDataChannel.setSctpSettings(settings); }
-export function listenIceUdpMux(port: number, cb?: (req: { ufrag: string, host: string, port: number }) => void, host?: string): void { nodeDataChannel.listenIceUdpMux(port, cb, host); }
+export function listenIceUdpMux(port: number, cb?: (req: { ufrag: string, host: string, port: number }) => void | null, host?: string | null): void { nodeDataChannel.listenIceUdpMux(port, cb, host); }
 
 export interface Audio {
     addAudioCodec(payloadType: number, codec: string, profile?: string): void;