diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45b24b01b4..f90f3dd1fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ### Next
 
+- Add server side ICE consent checks to detect silent WebRTC disconnections ([PR #1332](https://github.com/versatica/mediasoup/pull/1332)).
 - Fix regression (crash) in transport-cc feedback generation ([PR #1339](https://github.com/versatica/mediasoup/pull/1339)).
 
 ### 3.13.19
diff --git a/node/src/Router.ts b/node/src/Router.ts
index 7b87818bf7..56245fe877 100644
--- a/node/src/Router.ts
+++ b/node/src/Router.ts
@@ -454,6 +454,7 @@ export class Router<
 		numSctpStreams = { OS: 1024, MIS: 1024 },
 		maxSctpMessageSize = 262144,
 		sctpSendBufferSize = 262144,
+		iceConsentTimeout = 30,
 		appData,
 	}: WebRtcTransportOptions<WebRtcTransportAppData>): Promise<
 		WebRtcTransport<WebRtcTransportAppData>
@@ -596,7 +597,8 @@ export class Router<
 				enableUdp,
 				enableTcp,
 				preferUdp,
-				preferTcp
+				preferTcp,
+				iceConsentTimeout
 			);
 
 		const requestOffset = new FbsRouter.CreateWebRtcTransportRequestT(
diff --git a/node/src/WebRtcTransport.ts b/node/src/WebRtcTransport.ts
index bdfb534e59..1e0a8d5b49 100644
--- a/node/src/WebRtcTransport.ts
+++ b/node/src/WebRtcTransport.ts
@@ -96,6 +96,11 @@ export type WebRtcTransportOptionsBase<WebRtcTransportAppData> = {
 	 */
 	preferTcp?: boolean;
 
+	/**
+	 * ICE consent timeout (in seconds). If 0 it is disabled. Default 30.
+	 */
+	iceConsentTimeout?: number;
+
 	/**
 	 * Initial available outgoing bitrate (in bps). Default 600000.
 	 */
@@ -836,7 +841,6 @@ function createConnectRequest({
 	// Serialize DtlsParameters. This can throw.
 	const dtlsParametersOffset = serializeDtlsParameters(builder, dtlsParameters);
 
-	// Create request.
 	return FbsWebRtcTransport.ConnectRequest.createConnectRequest(
 		builder,
 		dtlsParametersOffset
diff --git a/node/src/test/test-WebRtcTransport.ts b/node/src/test/test-WebRtcTransport.ts
index 72bfd48e4a..c605c05cb5 100644
--- a/node/src/test/test-WebRtcTransport.ts
+++ b/node/src/test/test-WebRtcTransport.ts
@@ -299,12 +299,16 @@ test('webRtcTransport.connect() succeeds', async () => {
 	};
 
 	await expect(
-		webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters })
+		webRtcTransport.connect({
+			dtlsParameters: dtlsRemoteParameters,
+		})
 	).resolves.toBeUndefined();
 
 	// Must fail if connected.
 	await expect(
-		webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters })
+		webRtcTransport.connect({
+			dtlsParameters: dtlsRemoteParameters,
+		})
 	).rejects.toThrow(Error);
 
 	expect(webRtcTransport.dtlsParameters.role).toBe('server');
diff --git a/rust/src/messages.rs b/rust/src/messages.rs
index c05eed1e08..d06fcaaf52 100644
--- a/rust/src/messages.rs
+++ b/rust/src/messages.rs
@@ -669,6 +669,7 @@ pub(crate) struct RouterCreateWebrtcTransportData {
     enable_tcp: bool,
     prefer_udp: bool,
     prefer_tcp: bool,
+    ice_consent_timeout: u8,
     enable_sctp: bool,
     num_sctp_streams: NumSctpStreams,
     max_sctp_message_size: u32,
@@ -701,6 +702,7 @@ impl RouterCreateWebrtcTransportData {
             enable_tcp: webrtc_transport_options.enable_tcp,
             prefer_udp: webrtc_transport_options.prefer_udp,
             prefer_tcp: webrtc_transport_options.prefer_tcp,
+            ice_consent_timeout: webrtc_transport_options.ice_consent_timeout,
             enable_sctp: webrtc_transport_options.enable_sctp,
             num_sctp_streams: webrtc_transport_options.num_sctp_streams,
             max_sctp_message_size: webrtc_transport_options.max_sctp_message_size,
@@ -726,6 +728,7 @@ impl RouterCreateWebrtcTransportData {
             enable_tcp: self.enable_tcp,
             prefer_udp: self.prefer_udp,
             prefer_tcp: self.prefer_tcp,
+            ice_consent_timeout: self.ice_consent_timeout,
         }
     }
 }
diff --git a/rust/src/router/webrtc_transport.rs b/rust/src/router/webrtc_transport.rs
index a50a9dfa31..e3f8c3babe 100644
--- a/rust/src/router/webrtc_transport.rs
+++ b/rust/src/router/webrtc_transport.rs
@@ -129,6 +129,9 @@ pub struct WebRtcTransportOptions {
     /// Prefer TCP.
     /// Default false.
     pub prefer_tcp: bool,
+    /// ICE consent timeout (in seconds). If 0 it is disabled.
+    /// Default 30.
+    pub ice_consent_timeout: u8,
     /// Create a SCTP association.
     /// Default false.
     pub enable_sctp: bool,
@@ -155,6 +158,7 @@ impl WebRtcTransportOptions {
             enable_tcp: false,
             prefer_udp: false,
             prefer_tcp: false,
+            ice_consent_timeout: 30,
             enable_sctp: false,
             num_sctp_streams: NumSctpStreams::default(),
             max_sctp_message_size: 262_144,
@@ -172,6 +176,7 @@ impl WebRtcTransportOptions {
             enable_tcp: true,
             prefer_udp: false,
             prefer_tcp: false,
+            ice_consent_timeout: 30,
             enable_sctp: false,
             num_sctp_streams: NumSctpStreams::default(),
             max_sctp_message_size: 262_144,
diff --git a/worker/Dockerfile b/worker/Dockerfile
index 37ab88d260..64ae430df2 100644
--- a/worker/Dockerfile
+++ b/worker/Dockerfile
@@ -26,8 +26,8 @@ ENV LANG="C.UTF-8"
 ENV CC="clang"
 ENV CXX="clang++"
 
-ENV MEDIASOUP_LOCAL_DEV=true
-ENV KEEP_BUILD_ARTIFACTS=1
+ENV MEDIASOUP_LOCAL_DEV="true"
+ENV KEEP_BUILD_ARTIFACTS="1"
 
 WORKDIR /mediasoup
 
diff --git a/worker/Dockerfile.alpine b/worker/Dockerfile.alpine
index bde3577dcc..68cf11664d 100644
--- a/worker/Dockerfile.alpine
+++ b/worker/Dockerfile.alpine
@@ -9,8 +9,8 @@ ENV LANG="C.UTF-8"
 ENV CC="gcc"
 ENV CXX="g++"
 
-ENV MEDIASOUP_LOCAL_DEV=true
-ENV KEEP_BUILD_ARTIFACTS=1
+ENV MEDIASOUP_LOCAL_DEV="true"
+ENV KEEP_BUILD_ARTIFACTS="1"
 
 WORKDIR /mediasoup
 
diff --git a/worker/fbs/webRtcTransport.fbs b/worker/fbs/webRtcTransport.fbs
index 4c12fb968c..4d66c7a511 100644
--- a/worker/fbs/webRtcTransport.fbs
+++ b/worker/fbs/webRtcTransport.fbs
@@ -23,6 +23,7 @@ table WebRtcTransportOptions {
     enable_tcp: bool = true;
     prefer_udp: bool = false;
     prefer_tcp: bool = false;
+    ice_consent_timeout: uint8 = 30;
 }
 
 enum FingerprintAlgorithm: uint8 {
diff --git a/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp b/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp
index 2110f8f0e1..37d567778d 100644
--- a/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp
+++ b/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp
@@ -1,6 +1,9 @@
 #include "RTC/FuzzerStunPacket.hpp"
 #include "RTC/StunPacket.hpp"
 
+static constexpr size_t StunSerializeBufferSize{ 65536 };
+thread_local static uint8_t StunSerializeBuffer[StunSerializeBufferSize];
+
 void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len)
 {
 	if (!::RTC::StunPacket::IsStun(data, len))
@@ -21,6 +24,7 @@ void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len)
 	packet->GetData();
 	packet->GetSize();
 	packet->SetUsername("foo", 3);
+	packet->SetPassword("lalala");
 	packet->SetPriority(123);
 	packet->SetIceControlling(123);
 	packet->SetIceControlled(123);
@@ -37,13 +41,21 @@ void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len)
 	packet->GetErrorCode();
 	packet->HasMessageIntegrity();
 	packet->HasFingerprint();
-	packet->CheckAuthentication("foo", "bar");
-	// TODO: packet->CreateSuccessResponse(); // This cannot be easily tested.
-	// TODO: packet->CreateErrorResponse(); // This cannot be easily tested.
-	packet->Authenticate("lalala");
-	// TODO: Cannot test Serialize() because we don't know the exact required
-	// buffer size (setters above may change the total size).
-	// TODO: packet->Serialize();
+	packet->CheckAuthentication("foo", "xxx");
+
+	if (packet->GetClass() == ::RTC::StunPacket::Class::REQUEST)
+	{
+		auto* successResponse = packet->CreateSuccessResponse();
+		auto* errorResponse   = packet->CreateErrorResponse(444);
+
+		delete successResponse;
+		delete errorResponse;
+	}
+
+	if (len < StunSerializeBufferSize - 1000)
+	{
+		packet->Serialize(StunSerializeBuffer);
+	}
 
 	delete packet;
 }
diff --git a/worker/include/RTC/IceServer.hpp b/worker/include/RTC/IceServer.hpp
index bde22a4642..72d7710363 100644
--- a/worker/include/RTC/IceServer.hpp
+++ b/worker/include/RTC/IceServer.hpp
@@ -2,15 +2,17 @@
 #define MS_RTC_ICE_SERVER_HPP
 
 #include "common.hpp"
+#include "Utils.hpp"
 #include "FBS/webRtcTransport.h"
 #include "RTC/StunPacket.hpp"
 #include "RTC/TransportTuple.hpp"
+#include "handles/TimerHandle.hpp"
 #include <list>
 #include <string>
 
 namespace RTC
 {
-	class IceServer
+	class IceServer : public TimerHandle::Listener
 	{
 	public:
 		enum class IceState
@@ -53,8 +55,12 @@ namespace RTC
 		};
 
 	public:
-		IceServer(Listener* listener, const std::string& usernameFragment, const std::string& password);
-		~IceServer();
+		IceServer(
+		  Listener* listener,
+		  const std::string& usernameFragment,
+		  const std::string& password,
+		  uint8_t consentTimeoutSec);
+		~IceServer() override;
 
 	public:
 		void ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple);
@@ -74,28 +80,7 @@ namespace RTC
 		{
 			return this->selectedTuple;
 		}
-		void RestartIce(const std::string& usernameFragment, const std::string& password)
-		{
-			if (!this->oldUsernameFragment.empty())
-			{
-				this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
-			}
-
-			this->oldUsernameFragment = this->usernameFragment;
-			this->usernameFragment    = usernameFragment;
-
-			this->oldPassword = this->password;
-			this->password    = password;
-
-			this->remoteNomination = 0u;
-
-			// Notify the listener.
-			this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
-
-			// NOTE: Do not call listener->OnIceServerLocalUsernameFragmentRemoved()
-			// yet with old usernameFragment. Wait until we receive a STUN packet
-			// with the new one.
-		}
+		void RestartIce(const std::string& usernameFragment, const std::string& password);
 		bool IsValidTuple(const RTC::TransportTuple* tuple) const;
 		void RemoveTuple(RTC::TransportTuple* tuple);
 		/**
@@ -105,6 +90,9 @@ namespace RTC
 		void MayForceSelectedTuple(const RTC::TransportTuple* tuple);
 
 	private:
+		void ProcessStunRequest(RTC::StunPacket* request, RTC::TransportTuple* tuple);
+		void ProcessStunIndication(RTC::StunPacket* indication);
+		void ProcessStunResponse(RTC::StunPacket* response);
 		void HandleTuple(
 		  RTC::TransportTuple* tuple, bool hasUseCandidate, bool hasNomination, uint32_t nomination);
 		/**
@@ -120,19 +108,37 @@ namespace RTC
 		 * NOTE: The given tuple MUST be already stored within the list.
 		 */
 		void SetSelectedTuple(RTC::TransportTuple* storedTuple);
+		bool IsConsentCheckSupported() const
+		{
+			return this->consentTimeoutMs != 0u;
+		}
+		bool IsConsentCheckRunning() const
+		{
+			return (this->consentCheckTimer && this->consentCheckTimer->IsActive());
+		}
+		void StartConsentCheck();
+		void RestartConsentCheck();
+		void StopConsentCheck();
+
+		/* Pure virtual methods inherited from TimerHandle::Listener. */
+	public:
+		void OnTimer(TimerHandle* timer) override;
 
 	private:
 		// Passed by argument.
 		Listener* listener{ nullptr };
-		// Others.
 		std::string usernameFragment;
 		std::string password;
+		uint16_t consentTimeoutMs{ 30000u };
+		// Others.
 		std::string oldUsernameFragment;
 		std::string oldPassword;
 		IceState state{ IceState::NEW };
 		uint32_t remoteNomination{ 0u };
 		std::list<RTC::TransportTuple> tuples;
 		RTC::TransportTuple* selectedTuple{ nullptr };
+		TimerHandle* consentCheckTimer{ nullptr };
+		uint64_t lastConsentRequestReceivedAtMs{ 0u };
 	};
 } // namespace RTC
 
diff --git a/worker/include/RTC/StunPacket.hpp b/worker/include/RTC/StunPacket.hpp
index ab4846e237..1d781fff43 100644
--- a/worker/include/RTC/StunPacket.hpp
+++ b/worker/include/RTC/StunPacket.hpp
@@ -50,7 +50,7 @@ namespace RTC
 		{
 			OK           = 0,
 			UNAUTHORIZED = 1,
-			BAD_REQUEST  = 2
+			BAD_MESSAGE  = 2
 		};
 
 	public:
@@ -95,6 +95,10 @@ namespace RTC
 		{
 			return this->size;
 		}
+		const uint8_t* GetTransactionId() const
+		{
+			return this->transactionId;
+		}
 		void SetUsername(const char* username, size_t len)
 		{
 			this->username.assign(username, len);
@@ -127,6 +131,10 @@ namespace RTC
 		{
 			this->errorCode = errorCode;
 		}
+		void SetSoftware(const char* software, size_t len)
+		{
+			this->software.assign(software, len);
+		}
 		void SetMessageIntegrity(const uint8_t* messageIntegrity)
 		{
 			this->messageIntegrity = messageIntegrity;
@@ -139,6 +147,7 @@ namespace RTC
 		{
 			return this->username;
 		}
+		void SetPassword(const std::string& password);
 		uint32_t GetPriority() const
 		{
 			return this->priority;
@@ -171,6 +180,10 @@ namespace RTC
 		{
 			return this->errorCode;
 		}
+		std::string GetSoftware() const
+		{
+			return this->software;
+		}
 		bool HasMessageIntegrity() const
 		{
 			return (this->messageIntegrity != nullptr);
@@ -180,10 +193,11 @@ namespace RTC
 			return this->hasFingerprint;
 		}
 		Authentication CheckAuthentication(
-		  const std::string& localUsername, const std::string& localPassword);
+		  // The first username fragment in the USERNAME attribute.
+		  const std::string& usernameFragment1,
+		  const std::string& password);
 		StunPacket* CreateSuccessResponse();
 		StunPacket* CreateErrorResponse(uint16_t errorCode);
-		void Authenticate(const std::string& password);
 		void Serialize(uint8_t* buffer);
 
 	private:
@@ -194,7 +208,8 @@ namespace RTC
 		uint8_t* data{ nullptr };                // Pointer to binary data.
 		size_t size{ 0u };                       // The full message size (including header).
 		// STUN attributes.
-		std::string username;          // Less than 513 bytes.
+		std::string username; // Less than 513 bytes.
+		std::string password;
 		uint32_t priority{ 0u };       // 4 bytes unsigned integer.
 		uint64_t iceControlling{ 0u }; // 8 bytes unsigned integer.
 		uint64_t iceControlled{ 0u };  // 8 bytes unsigned integer.
@@ -205,7 +220,7 @@ namespace RTC
 		bool hasFingerprint{ false };                       // 4 bytes.
 		const struct sockaddr* xorMappedAddress{ nullptr }; // 8 or 20 bytes.
 		uint16_t errorCode{ 0u };                           // 4 bytes (no reason phrase).
-		std::string password;
+		std::string software;                               // Less than 763 bytes.
 	};
 } // namespace RTC
 
diff --git a/worker/src/RTC/IceServer.cpp b/worker/src/RTC/IceServer.cpp
index e486b4629b..756cae8eb5 100644
--- a/worker/src/RTC/IceServer.cpp
+++ b/worker/src/RTC/IceServer.cpp
@@ -2,6 +2,7 @@
 // #define MS_LOG_DEV_LEVEL 3
 
 #include "RTC/IceServer.hpp"
+#include "DepLibUV.hpp"
 #include "Logger.hpp"
 
 namespace RTC
@@ -11,6 +12,8 @@ namespace RTC
 	static constexpr size_t StunSerializeBufferSize{ 65536 };
 	thread_local static uint8_t StunSerializeBuffer[StunSerializeBufferSize];
 	static constexpr size_t MaxTuples{ 8 };
+	static constexpr uint8_t ConsentCheckMinTimeoutSec{ 10u };
+	static constexpr uint8_t ConsentCheckMaxTimeoutSec{ 60u };
 
 	/* Class methods. */
 	IceServer::IceState IceStateFromFbs(FBS::WebRtcTransport::IceState state)
@@ -67,11 +70,40 @@ namespace RTC
 
 	/* Instance methods. */
 
-	IceServer::IceServer(Listener* listener, const std::string& usernameFragment, const std::string& password)
+	IceServer::IceServer(
+	  Listener* listener,
+	  const std::string& usernameFragment,
+	  const std::string& password,
+	  uint8_t consentTimeoutSec)
 	  : listener(listener), usernameFragment(usernameFragment), password(password)
 	{
 		MS_TRACE();
 
+		if (consentTimeoutSec == 0u)
+		{
+			// 0 means disabled so it's a valid value.
+		}
+		else if (consentTimeoutSec < ConsentCheckMinTimeoutSec)
+		{
+			MS_WARN_TAG(
+			  ice,
+			  "consentTimeoutSec cannot be lower than %" PRIu8 " seconds, fixing it",
+			  ConsentCheckMinTimeoutSec);
+
+			consentTimeoutSec = ConsentCheckMinTimeoutSec;
+		}
+		else if (consentTimeoutSec > ConsentCheckMaxTimeoutSec)
+		{
+			MS_WARN_TAG(
+			  ice,
+			  "consentTimeoutSec cannot be higher than %" PRIu8 " seconds, fixing it",
+			  ConsentCheckMaxTimeoutSec);
+
+			consentTimeoutSec = ConsentCheckMaxTimeoutSec;
+		}
+
+		this->consentTimeoutMs = consentTimeoutSec * 1000;
+
 		// Notify the listener.
 		this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
 	}
@@ -90,6 +122,7 @@ namespace RTC
 			this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
 		}
 
+		// Clear all tuples.
 		for (const auto& it : this->tuples)
 		{
 			auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));
@@ -98,296 +131,383 @@ namespace RTC
 			this->listener->OnIceServerTupleRemoved(this, storedTuple);
 		}
 
+		// Clear all tuples.
+		// NOTE: Do it after notifying the listener since the listener may need to
+		// use/read the tuple being removed so we cannot free it yet.
 		this->tuples.clear();
+
+		// Unset selected tuple.
+		this->selectedTuple = nullptr;
+
+		// Delete the ICE consent check timer.
+		delete this->consentCheckTimer;
+		this->consentCheckTimer = nullptr;
 	}
 
 	void IceServer::ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple)
 	{
 		MS_TRACE();
 
-		// Must be a Binding method.
-		if (packet->GetMethod() != RTC::StunPacket::Method::BINDING)
+		switch (packet->GetClass())
 		{
-			if (packet->GetClass() == RTC::StunPacket::Class::REQUEST)
+			case RTC::StunPacket::Class::REQUEST:
 			{
-				MS_WARN_TAG(
-				  ice,
-				  "unknown method %#.3x in STUN Request => 400",
-				  static_cast<unsigned int>(packet->GetMethod()));
+				ProcessStunRequest(packet, tuple);
 
-				// Reply 400.
-				RTC::StunPacket* response = packet->CreateErrorResponse(400);
+				break;
+			}
 
-				response->Serialize(StunSerializeBuffer);
-				this->listener->OnIceServerSendStunPacket(this, response, tuple);
+			case RTC::StunPacket::Class::INDICATION:
+			{
+				ProcessStunIndication(packet);
 
-				delete response;
+				break;
 			}
-			else
+
+			case RTC::StunPacket::Class::SUCCESS_RESPONSE:
+			case RTC::StunPacket::Class::ERROR_RESPONSE:
 			{
-				MS_WARN_TAG(
-				  ice,
-				  "ignoring STUN Indication or Response with unknown method %#.3x",
-				  static_cast<unsigned int>(packet->GetMethod()));
+				ProcessStunResponse(packet);
+
+				break;
 			}
 
-			return;
+			default:
+			{
+				MS_WARN_TAG(ice, "unknown STUN class %" PRIu16 ", discarded", packet->GetClass());
+			}
 		}
+	}
 
-		// Must use FINGERPRINT (optional for ICE STUN indications).
-		if (!packet->HasFingerprint() && packet->GetClass() != RTC::StunPacket::Class::INDICATION)
+	void IceServer::RestartIce(const std::string& usernameFragment, const std::string& password)
+	{
+		MS_TRACE();
+
+		if (!this->oldUsernameFragment.empty())
 		{
-			if (packet->GetClass() == RTC::StunPacket::Class::REQUEST)
-			{
-				MS_WARN_TAG(ice, "STUN Binding Request without FINGERPRINT => 400");
+			this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
+		}
 
-				// Reply 400.
-				RTC::StunPacket* response = packet->CreateErrorResponse(400);
+		this->oldUsernameFragment = this->usernameFragment;
+		this->usernameFragment    = usernameFragment;
 
-				response->Serialize(StunSerializeBuffer);
-				this->listener->OnIceServerSendStunPacket(this, response, tuple);
+		this->oldPassword = this->password;
+		this->password    = password;
 
-				delete response;
-			}
-			else
+		this->remoteNomination = 0u;
+
+		// Notify the listener.
+		this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
+
+		// NOTE: Do not call listener->OnIceServerLocalUsernameFragmentRemoved()
+		// yet with old usernameFragment. Wait until we receive a STUN packet
+		// with the new one.
+
+		// Restart ICE consent check (if running) to give some time to the
+		// client to establish ICE again.
+		if (IsConsentCheckSupported() && IsConsentCheckRunning())
+		{
+			RestartConsentCheck();
+		}
+	}
+
+	bool IceServer::IsValidTuple(const RTC::TransportTuple* tuple) const
+	{
+		MS_TRACE();
+
+		return HasTuple(tuple) != nullptr;
+	}
+
+	void IceServer::RemoveTuple(RTC::TransportTuple* tuple)
+	{
+		MS_TRACE();
+
+		RTC::TransportTuple* removedTuple{ nullptr };
+
+		// Find the removed tuple.
+		auto it = this->tuples.begin();
+
+		for (; it != this->tuples.end(); ++it)
+		{
+			RTC::TransportTuple* storedTuple = std::addressof(*it);
+
+			if (storedTuple->Compare(tuple))
 			{
-				MS_WARN_TAG(ice, "ignoring STUN Binding Response without FINGERPRINT");
+				removedTuple = storedTuple;
+
+				break;
 			}
+		}
 
+		// If not found, ignore.
+		if (!removedTuple)
+		{
 			return;
 		}
 
-		switch (packet->GetClass())
-		{
-			case RTC::StunPacket::Class::REQUEST:
-			{
-				// USERNAME, MESSAGE-INTEGRITY and PRIORITY are required.
-				if (!packet->HasMessageIntegrity() || (packet->GetPriority() == 0u) || packet->GetUsername().empty())
-				{
-					MS_WARN_TAG(ice, "mising required attributes in STUN Binding Request => 400");
+		// Notify the listener.
+		this->listener->OnIceServerTupleRemoved(this, removedTuple);
 
-					// Reply 400.
-					RTC::StunPacket* response = packet->CreateErrorResponse(400);
+		// Remove it from the list of tuples.
+		// NOTE: Do it after notifying the listener since the listener may need to
+		// use/read the tuple being removed so we cannot free it yet.
+		this->tuples.erase(it);
 
-					response->Serialize(StunSerializeBuffer);
-					this->listener->OnIceServerSendStunPacket(this, response, tuple);
+		// If this is the selected tuple, do things.
+		if (removedTuple == this->selectedTuple)
+		{
+			this->selectedTuple = nullptr;
 
-					delete response;
+			// Mark the first tuple as selected tuple (if any) but only if state was
+			// 'connected' or 'completed'.
+			if (
+			  (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED) &&
+			  this->tuples.begin() != this->tuples.end())
+			{
+				SetSelectedTuple(std::addressof(*this->tuples.begin()));
 
-					return;
+				// Restart ICE consent check to let the client send new consent requests
+				// on the new selected tuple.
+				if (IsConsentCheckSupported())
+				{
+					RestartConsentCheck();
 				}
+			}
+			// Or just emit 'disconnected'.
+			else
+			{
+				// Update state.
+				this->state = IceState::DISCONNECTED;
 
-				// Check authentication.
-				switch (packet->CheckAuthentication(this->usernameFragment, this->password))
-				{
-					case RTC::StunPacket::Authentication::OK:
-					{
-						if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty())
-						{
-							MS_DEBUG_TAG(ice, "new ICE credentials applied");
+				// Reset remote nomination.
+				this->remoteNomination = 0u;
 
-							// Notify the listener.
-							this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
+				// Notify the listener.
+				this->listener->OnIceServerDisconnected(this);
 
-							this->oldUsernameFragment.clear();
-							this->oldPassword.clear();
-						}
+				if (IsConsentCheckSupported() && IsConsentCheckRunning())
+				{
+					StopConsentCheck();
+				}
+			}
+		}
+	}
 
-						break;
-					}
+	void IceServer::ProcessStunRequest(RTC::StunPacket* request, RTC::TransportTuple* tuple)
+	{
+		MS_TRACE();
 
-					case RTC::StunPacket::Authentication::UNAUTHORIZED:
-					{
-						// We may have changed our usernameFragment and password, so check
-						// the old ones.
-						// clang-format off
-						if (
-							!this->oldUsernameFragment.empty() &&
-							!this->oldPassword.empty() &&
-							packet->CheckAuthentication(this->oldUsernameFragment, this->oldPassword) == RTC::StunPacket::Authentication::OK
-						)
-						// clang-format on
-						{
-							MS_DEBUG_TAG(ice, "using old ICE credentials");
+		MS_DEBUG_DEV("processing STUN request");
 
-							break;
-						}
+		// Must be a Binding method.
+		if (request->GetMethod() != RTC::StunPacket::Method::BINDING)
+		{
+			MS_WARN_TAG(
+			  ice,
+			  "STUN request with unknown method %#.3x => 400",
+			  static_cast<unsigned int>(request->GetMethod()));
 
-						MS_WARN_TAG(ice, "wrong authentication in STUN Binding Request => 401");
+			// Reply 400.
+			RTC::StunPacket* response = request->CreateErrorResponse(400);
 
-						// Reply 401.
-						RTC::StunPacket* response = packet->CreateErrorResponse(401);
+			response->Serialize(StunSerializeBuffer);
+			this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-						response->Serialize(StunSerializeBuffer);
-						this->listener->OnIceServerSendStunPacket(this, response, tuple);
+			delete response;
 
-						delete response;
+			return;
+		}
 
-						return;
-					}
+		// Must have FINGERPRINT attribute.
+		if (!request->HasFingerprint())
+		{
+			MS_WARN_TAG(ice, "STUN Binding request without FINGERPRINT attribute => 400");
 
-					case RTC::StunPacket::Authentication::BAD_REQUEST:
-					{
-						MS_WARN_TAG(ice, "cannot check authentication in STUN Binding Request => 400");
+			// Reply 400.
+			RTC::StunPacket* response = request->CreateErrorResponse(400);
 
-						// Reply 400.
-						RTC::StunPacket* response = packet->CreateErrorResponse(400);
+			response->Serialize(StunSerializeBuffer);
+			this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-						response->Serialize(StunSerializeBuffer);
-						this->listener->OnIceServerSendStunPacket(this, response, tuple);
+			delete response;
 
-						delete response;
+			return;
+		}
 
-						return;
-					}
-				}
+		// PRIORITY attribute is required.
+		if (request->GetPriority() == 0u)
+		{
+			MS_WARN_TAG(ice, "STUN Binding request without PRIORITY attribute => 400");
 
-				// The remote peer must be ICE controlling.
-				if (packet->GetIceControlled())
-				{
-					MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding Request => 487");
+			// Reply 400.
+			RTC::StunPacket* response = request->CreateErrorResponse(400);
 
-					// Reply 487 (Role Conflict).
-					RTC::StunPacket* response = packet->CreateErrorResponse(487);
+			response->Serialize(StunSerializeBuffer);
+			this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-					response->Serialize(StunSerializeBuffer);
-					this->listener->OnIceServerSendStunPacket(this, response, tuple);
+			delete response;
 
-					delete response;
+			return;
+		}
 
-					return;
-				}
+		// Check authentication.
+		switch (request->CheckAuthentication(this->usernameFragment, this->password))
+		{
+			case RTC::StunPacket::Authentication::OK:
+			{
+				if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty())
+				{
+					MS_DEBUG_TAG(ice, "new ICE credentials applied");
 
-				MS_DEBUG_DEV(
-				  "processing STUN Binding Request [Priority:%" PRIu32 ", UseCandidate:%s]",
-				  static_cast<uint32_t>(packet->GetPriority()),
-				  packet->HasUseCandidate() ? "true" : "false");
+					// Notify the listener.
+					this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
 
-				// Create a success response.
-				RTC::StunPacket* response = packet->CreateSuccessResponse();
+					this->oldUsernameFragment.clear();
+					this->oldPassword.clear();
+				}
 
-				// Add XOR-MAPPED-ADDRESS.
-				response->SetXorMappedAddress(tuple->GetRemoteAddress());
+				break;
+			}
 
-				// Authenticate the response.
-				if (this->oldPassword.empty())
-				{
-					response->Authenticate(this->password);
-				}
-				else
+			case RTC::StunPacket::Authentication::UNAUTHORIZED:
+			{
+				// We may have changed our usernameFragment and password, so check the
+				// old ones.
+				// clang-format off
+				if (
+				  !this->oldUsernameFragment.empty() &&
+				  !this->oldPassword.empty() &&
+				  request->CheckAuthentication(
+				    this->oldUsernameFragment, this->oldPassword
+				  ) == RTC::StunPacket::Authentication::OK
+				)
+				// clang-format on
 				{
-					response->Authenticate(this->oldPassword);
+					MS_DEBUG_TAG(ice, "using old ICE credentials");
+
+					break;
 				}
 
-				// Send back.
+				MS_WARN_TAG(ice, "wrong authentication in STUN Binding request => 401");
+
+				// Reply 401.
+				RTC::StunPacket* response = request->CreateErrorResponse(401);
+
 				response->Serialize(StunSerializeBuffer);
 				this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
 				delete response;
 
-				uint32_t nomination{ 0u };
-
-				if (packet->HasNomination())
-				{
-					nomination = packet->GetNomination();
-				}
-
-				// Handle the tuple.
-				HandleTuple(tuple, packet->HasUseCandidate(), packet->HasNomination(), nomination);
-
-				break;
+				return;
 			}
 
-			case RTC::StunPacket::Class::INDICATION:
+			case RTC::StunPacket::Authentication::BAD_MESSAGE:
 			{
-				MS_DEBUG_TAG(ice, "STUN Binding Indication processed");
+				MS_WARN_TAG(ice, "cannot check authentication in STUN Binding request => 400");
 
-				break;
-			}
-
-			case RTC::StunPacket::Class::SUCCESS_RESPONSE:
-			{
-				MS_DEBUG_TAG(ice, "STUN Binding Success Response processed");
+				// Reply 400.
+				RTC::StunPacket* response = request->CreateErrorResponse(400);
 
-				break;
-			}
+				response->Serialize(StunSerializeBuffer);
+				this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-			case RTC::StunPacket::Class::ERROR_RESPONSE:
-			{
-				MS_DEBUG_TAG(ice, "STUN Binding Error Response processed");
+				delete response;
 
-				break;
+				return;
 			}
 		}
-	}
 
-	bool IceServer::IsValidTuple(const RTC::TransportTuple* tuple) const
-	{
-		MS_TRACE();
+		// The remote peer must be ICE controlling.
+		if (request->GetIceControlled())
+		{
+			MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding request => 487");
 
-		return HasTuple(tuple) != nullptr;
-	}
+			// Reply 487 (Role Conflict).
+			RTC::StunPacket* response = request->CreateErrorResponse(487);
 
-	void IceServer::RemoveTuple(RTC::TransportTuple* tuple)
-	{
-		MS_TRACE();
+			response->Serialize(StunSerializeBuffer);
+			this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-		RTC::TransportTuple* removedTuple{ nullptr };
+			delete response;
 
-		// Find the removed tuple.
-		auto it = this->tuples.begin();
+			return;
+		}
 
-		for (; it != this->tuples.end(); ++it)
-		{
-			RTC::TransportTuple* storedTuple = std::addressof(*it);
+		MS_DEBUG_DEV(
+		  "valid STUN Binding request [priority:%" PRIu32 ", useCandidate:%s]",
+		  static_cast<uint32_t>(request->GetPriority()),
+		  request->HasUseCandidate() ? "true" : "false");
 
-			if (storedTuple->Compare(tuple))
-			{
-				removedTuple = storedTuple;
+		// Create a success response.
+		RTC::StunPacket* response = request->CreateSuccessResponse();
 
-				break;
-			}
-		}
+		// Add XOR-MAPPED-ADDRESS.
+		response->SetXorMappedAddress(tuple->GetRemoteAddress());
 
-		// If not found, ignore.
-		if (!removedTuple)
+		// Authenticate the response.
+		if (this->oldPassword.empty())
 		{
-			return;
+			response->SetPassword(this->password);
+		}
+		else
+		{
+			response->SetPassword(this->oldPassword);
 		}
 
-		// Notify the listener.
-		this->listener->OnIceServerTupleRemoved(this, removedTuple);
+		// Send back.
+		response->Serialize(StunSerializeBuffer);
+		this->listener->OnIceServerSendStunPacket(this, response, tuple);
 
-		// Remove it from the list of tuples.
-		// NOTE: Do it after notifying the listener since the listener may need to
-		// use/read the tuple being removed so we cannot free it yet.
-		this->tuples.erase(it);
+		delete response;
 
-		// If this is the selected tuple, do things.
-		if (removedTuple == this->selectedTuple)
+		uint32_t nomination{ 0u };
+
+		if (request->HasNomination())
 		{
-			this->selectedTuple = nullptr;
+			nomination = request->GetNomination();
+		}
 
-			// Mark the first tuple as selected tuple (if any).
-			if (this->tuples.begin() != this->tuples.end())
+		// Handle the tuple.
+		HandleTuple(tuple, request->HasUseCandidate(), request->HasNomination(), nomination);
+
+		// If state is 'connected' or 'completed' after handling the tuple, then
+		// start or restart ICE consent check (if supported).
+		if (IsConsentCheckSupported() && (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED))
+		{
+			if (IsConsentCheckRunning())
 			{
-				SetSelectedTuple(std::addressof(*this->tuples.begin()));
+				RestartConsentCheck();
 			}
-			// Or just emit 'disconnected'.
 			else
 			{
-				// Update state.
-				this->state = IceState::DISCONNECTED;
-
-				// Reset remote nomination.
-				this->remoteNomination = 0u;
-
-				// Notify the listener.
-				this->listener->OnIceServerDisconnected(this);
+				StartConsentCheck();
 			}
 		}
 	}
 
+	void IceServer::ProcessStunIndication(RTC::StunPacket* indication)
+	{
+		MS_TRACE();
+
+		MS_DEBUG_DEV("STUN indication received, discarded");
+
+		// Nothig else to do. We just discard STUN indications.
+	}
+
+	void IceServer::ProcessStunResponse(RTC::StunPacket* response)
+	{
+		MS_TRACE();
+
+		const std::string responseType = response->GetClass() == RTC::StunPacket::Class::SUCCESS_RESPONSE
+		                                   ? "success"
+		                                   : std::to_string(response->GetErrorCode()) + " error";
+
+		MS_DEBUG_DEV("processing STUN %s response received, discarded", responseType.c_str());
+
+		// Nothig else to do. We just discard STUN responses because we do not
+		// generate STUN requests.
+	}
+
 	void IceServer::MayForceSelectedTuple(const RTC::TransportTuple* tuple)
 	{
 		MS_TRACE();
@@ -408,7 +528,6 @@ namespace RTC
 			return;
 		}
 
-		// Mark it as selected tuple.
 		SetSelectedTuple(storedTuple);
 	}
 
@@ -437,12 +556,12 @@ namespace RTC
 					// Store the tuple.
 					auto* storedTuple = AddTuple(tuple);
 
-					// Mark it as selected tuple.
-					SetSelectedTuple(storedTuple);
-
 					// Update state.
 					this->state = IceState::CONNECTED;
 
+					// Mark it as selected tuple.
+					SetSelectedTuple(storedTuple);
+
 					// Notify the listener.
 					this->listener->OnIceServerConnected(this);
 				}
@@ -461,12 +580,12 @@ namespace RTC
 						  hasNomination ? "true" : "false",
 						  nomination);
 
-						// Mark it as selected tuple.
-						SetSelectedTuple(storedTuple);
-
 						// Update state.
 						this->state = IceState::COMPLETED;
 
+						// Mark it as selected tuple.
+						SetSelectedTuple(storedTuple);
+
 						// Update nomination.
 						if (hasNomination && nomination > this->remoteNomination)
 						{
@@ -499,12 +618,12 @@ namespace RTC
 					// Store the tuple.
 					auto* storedTuple = AddTuple(tuple);
 
-					// Mark it as selected tuple.
-					SetSelectedTuple(storedTuple);
-
 					// Update state.
 					this->state = IceState::CONNECTED;
 
+					// Mark it as selected tuple.
+					SetSelectedTuple(storedTuple);
+
 					// Notify the listener.
 					this->listener->OnIceServerConnected(this);
 				}
@@ -523,12 +642,12 @@ namespace RTC
 						  hasNomination ? "true" : "false",
 						  nomination);
 
-						// Mark it as selected tuple.
-						SetSelectedTuple(storedTuple);
-
 						// Update state.
 						this->state = IceState::COMPLETED;
 
+						// Mark it as selected tuple.
+						SetSelectedTuple(storedTuple);
+
 						// Update nomination.
 						if (hasNomination && nomination > this->remoteNomination)
 						{
@@ -571,12 +690,12 @@ namespace RTC
 
 					if ((hasNomination && nomination > this->remoteNomination) || !hasNomination)
 					{
-						// Mark it as selected tuple.
-						SetSelectedTuple(storedTuple);
-
 						// Update state.
 						this->state = IceState::COMPLETED;
 
+						// Mark it as selected tuple.
+						SetSelectedTuple(storedTuple);
+
 						// Update nomination.
 						if (hasNomination && nomination > this->remoteNomination)
 						{
@@ -627,7 +746,7 @@ namespace RTC
 		}
 	}
 
-	inline RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple)
+	RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple)
 	{
 		MS_TRACE();
 
@@ -635,7 +754,7 @@ namespace RTC
 
 		if (storedTuple)
 		{
-			MS_DEBUG_DEV('tuple already exists');
+			MS_DEBUG_DEV("tuple already exists");
 
 			return storedTuple;
 		}
@@ -695,7 +814,7 @@ namespace RTC
 		return storedTuple;
 	}
 
-	inline RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const
+	RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const
 	{
 		MS_TRACE();
 
@@ -719,7 +838,7 @@ namespace RTC
 		return nullptr;
 	}
 
-	inline void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple)
+	void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple)
 	{
 		MS_TRACE();
 
@@ -734,4 +853,88 @@ namespace RTC
 		// Notify the listener.
 		this->listener->OnIceServerSelectedTuple(this, this->selectedTuple);
 	}
+
+	void IceServer::StartConsentCheck()
+	{
+		MS_TRACE();
+
+		MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
+		MS_ASSERT(!IsConsentCheckRunning(), "ICE consent check already running");
+		MS_ASSERT(this->selectedTuple, "no selected tuple");
+
+		// Create the ICE consent check timer if it doesn't exist.
+		if (!this->consentCheckTimer)
+		{
+			this->consentCheckTimer = new TimerHandle(this);
+		}
+
+		this->consentCheckTimer->Start(this->consentTimeoutMs);
+	}
+
+	void IceServer::RestartConsentCheck()
+	{
+		MS_TRACE();
+
+		MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
+		MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running");
+		MS_ASSERT(this->selectedTuple, "no selected tuple");
+
+		this->consentCheckTimer->Restart();
+	}
+
+	void IceServer::StopConsentCheck()
+	{
+		MS_TRACE();
+
+		MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
+		MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running");
+
+		this->consentCheckTimer->Stop();
+	}
+
+	inline void IceServer::OnTimer(TimerHandle* timer)
+	{
+		MS_TRACE();
+
+		if (timer == this->consentCheckTimer)
+		{
+			MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
+
+			// State must be 'connected' or 'completed'.
+			MS_ASSERT(
+			  this->state == IceState::COMPLETED || this->state == IceState::CONNECTED,
+			  "ICE consent check timer fired but state is neither 'completed' nor 'connected'");
+
+			// There should be a selected tuple.
+			MS_ASSERT(this->selectedTuple, "ICE consent check timer fired but there is not selected tuple");
+
+			MS_WARN_TAG(ice, "ICE consent expired due to timeout, moving to 'disconnected' state");
+
+			// Update state.
+			this->state = IceState::DISCONNECTED;
+
+			// Reset remote nomination.
+			this->remoteNomination = 0u;
+
+			// Clear all tuples.
+			for (const auto& it : this->tuples)
+			{
+				auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));
+
+				// Notify the listener.
+				this->listener->OnIceServerTupleRemoved(this, storedTuple);
+			}
+
+			// Clear all tuples.
+			// NOTE: Do it after notifying the listener since the listener may need to
+			// use/read the tuple being removed so we cannot free it yet.
+			this->tuples.clear();
+
+			// Unset selected tuple.
+			this->selectedTuple = nullptr;
+
+			// Notify the listener.
+			this->listener->OnIceServerDisconnected(this);
+		}
+	}
 } // namespace RTC
diff --git a/worker/src/RTC/KeyFrameRequestManager.cpp b/worker/src/RTC/KeyFrameRequestManager.cpp
index 1112901b49..540414ae0c 100644
--- a/worker/src/RTC/KeyFrameRequestManager.cpp
+++ b/worker/src/RTC/KeyFrameRequestManager.cpp
@@ -4,7 +4,7 @@
 #include "RTC/KeyFrameRequestManager.hpp"
 #include "Logger.hpp"
 
-static constexpr uint32_t KeyFrameRetransmissionWaitTime{ 1000 };
+static constexpr uint32_t KeyFrameRetransmissionWaitTime{ 1000u };
 
 /* PendingKeyFrameInfo methods. */
 
diff --git a/worker/src/RTC/StunPacket.cpp b/worker/src/RTC/StunPacket.cpp
index 518ca8ff97..6747afebd1 100644
--- a/worker/src/RTC/StunPacket.cpp
+++ b/worker/src/RTC/StunPacket.cpp
@@ -24,23 +24,21 @@ namespace RTC
 			return nullptr;
 		}
 
-		/*
-		  The message type field is decomposed further into the following
-		    structure:
-
-		    0                 1
-		    2  3  4 5 6 7 8 9 0 1 2 3 4 5
-		       +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
-		       |M |M |M|M|M|C|M|M|M|C|M|M|M|M|
-		       |11|10|9|8|7|1|6|5|4|0|3|2|1|0|
-		       +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
-
-		    Figure 3: Format of STUN Message Type Field
-
-		   Here the bits in the message type field are shown as most significant
-		   (M11) through least significant (M0).  M11 through M0 represent a 12-
-		   bit encoding of the method.  C1 and C0 represent a 2-bit encoding of
-		   the class.
+		/**
+		 * The message type field is decomposed further into the following
+		 * structure:
+		 *
+		 *  0                 1
+		 *  2  3  4 5 6 7 8 9 0 1 2 3 4 5
+		 * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
+		 * |M |M |M|M|M|C|M|M|M|C|M|M|M|M|
+		 * |11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+		 * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
+		 *
+		 * Here the bits in the message type field are shown as most significant
+		 * (M11) through least significant (M0).  M11 through M0 represent a 12-bit
+		 * encoding of the method.  C1 and C0 represent a 2-bit encoding of the
+		 * class.
 		 */
 
 		// Get type field.
@@ -49,7 +47,8 @@ namespace RTC
 		// Get length field.
 		const uint16_t msgLength = Utils::Byte::Get2Bytes(data, 2);
 
-		// length field must be total size minus header's 20 bytes, and must be multiple of 4 Bytes.
+		// length field must be total size minus header's 20 bytes, and must be
+		// multiple of 4 Bytes.
 		if ((static_cast<size_t>(msgLength) != len - 20) || ((msgLength & 0x03) != 0))
 		{
 			MS_WARN_TAG(
@@ -67,35 +66,38 @@ namespace RTC
 		// Get STUN class.
 		const uint16_t msgClass = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4);
 
-		// Create a new StunPacket (data + 8 points to the received TransactionID field).
+		// Create a new StunPacket (data + 8 points to the received TransactionID
+		// field).
 		auto* packet = new StunPacket(
 		  static_cast<Class>(msgClass), static_cast<Method>(msgMethod), data + 8, data, len);
 
-		/*
-		    STUN Attributes
-
-		    After the STUN header are zero or more attributes.  Each attribute
-		    MUST be TLV encoded, with a 16-bit type, 16-bit length, and value.
-		    Each STUN attribute MUST end on a 32-bit boundary.  As mentioned
-		    above, all fields in an attribute are transmitted most significant
-		    bit first.
-
-		        0                   1                   2                   3
-		        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
-		       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-		       |         Type                  |            Length             |
-		       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-		       |                         Value (variable)                ....
-		       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+		/**
+		 * STUN Attributes
+		 *
+		 * After the STUN header are zero or more attributes. Each attribute MUST
+		 * be TLV encoded, with a 16-bit type, 16-bit length, and value. Each STUN
+		 * attribute MUST end on a 32-bit boundary.  As mentioned above, all fields
+		 * in an attribute are transmitted most significant bit first.
+		 *
+		 *  0                   1                   2                   3
+		 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+		 * |         Type                  |            Length             |
+		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+		 * |                         Value (variable)                ....
+		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 		 */
 
 		// Start looking for attributes after STUN header (Byte #20).
 		size_t pos{ 20 };
-		// Flags (positions) for special MESSAGE-INTEGRITY and FINGERPRINT attributes.
+		// Flags (positions) for special MESSAGE-INTEGRITY and FINGERPRINT
+		// attributes.
 		bool hasMessageIntegrity{ false };
 		bool hasFingerprint{ false };
-		size_t fingerprintAttrPos; // Will point to the beginning of the attribute.
-		uint32_t fingerprint;      // Holds the value of the FINGERPRINT attribute.
+		// Will point to the beginning of the attribute.
+		size_t fingerprintAttrPos;
+		// Holds the value of the FINGERPRINT attribute.
+		uint32_t fingerprint;
 
 		// Ensure there are at least 4 remaining bytes (attribute with 0 length).
 		while (pos + 4 <= len)
@@ -285,6 +287,24 @@ namespace RTC
 					break;
 				}
 
+				case Attribute::SOFTWARE:
+				{
+					// Ensure attribute length is less than 763 bytes.
+					if (attrLength >= 763)
+					{
+						MS_WARN_TAG(
+						  ice, "attribute SOFTWARE must be less than 763 bytes length, packet discarded");
+
+						delete packet;
+						return nullptr;
+					}
+
+					packet->SetSoftware(
+					  reinterpret_cast<const char*>(attrValuePos), static_cast<size_t>(attrLength));
+
+					break;
+				}
+
 				default:;
 			}
 
@@ -351,16 +371,16 @@ namespace RTC
 		switch (this->klass)
 		{
 			case Class::REQUEST:
-				klass = "Request";
+				klass = "request";
 				break;
 			case Class::INDICATION:
-				klass = "Indication";
+				klass = "indication";
 				break;
 			case Class::SUCCESS_RESPONSE:
-				klass = "SuccessResponse";
+				klass = "success response";
 				break;
 			case Class::ERROR_RESPONSE:
-				klass = "ErrorResponse";
+				klass = "error response";
 				break;
 		}
 		if (this->method == Method::BINDING)
@@ -374,14 +394,11 @@ namespace RTC
 		}
 		MS_DUMP("  size: %zu bytes", this->size);
 
-		char transactionId[25];
+		auto transactionId1 = Utils::Byte::Get4Bytes(this->transactionId, 0);
+		auto transactionId2 = Utils::Byte::Get8Bytes(this->transactionId, 4);
 
-		for (int i{ 0 }; i < 12; ++i)
-		{
-			// NOTE: n must be 3 because snprintf adds a \0 after printed chars.
-			std::snprintf(transactionId + (i * 2), 3, "%.2x", this->transactionId[i]);
-		}
-		MS_DUMP("  transactionId: %s", transactionId);
+		MS_DUMP("  transactionId (first 4 bytes): %" PRIu32, transactionId1);
+		MS_DUMP("  transactionId (last 8 bytes): %" PRIu64, transactionId2);
 		if (this->errorCode != 0u)
 		{
 			MS_DUMP("  errorCode: %" PRIu16, this->errorCode);
@@ -406,6 +423,10 @@ namespace RTC
 		{
 			MS_DUMP("  useCandidate");
 		}
+		if (!this->software.empty())
+		{
+			MS_DUMP("  software: %s", this->software.c_str());
+		}
 		if (this->xorMappedAddress != nullptr)
 		{
 			int family;
@@ -435,8 +456,21 @@ namespace RTC
 		MS_DUMP("</StunPacket>");
 	}
 
+	void StunPacket::SetPassword(const std::string& password)
+	{
+		// Just for request, indication and success response messages.
+		if (this->klass == Class::ERROR_RESPONSE)
+		{
+			MS_ERROR("cannot set password for error responses");
+
+			return;
+		}
+
+		this->password = password;
+	}
+
 	StunPacket::Authentication StunPacket::CheckAuthentication(
-	  const std::string& localUsername, const std::string& localPassword)
+	  const std::string& usernameFragment1, const std::string& password)
 	{
 		MS_TRACE();
 
@@ -445,46 +479,82 @@ namespace RTC
 			case Class::REQUEST:
 			case Class::INDICATION:
 			{
-				// Both USERNAME and MESSAGE-INTEGRITY must be present.
-				if (!this->messageIntegrity || this->username.empty())
+				// usernameFragment1 must be given.
+				if (usernameFragment1.empty())
+				{
+					MS_WARN_TAG(ice, "usernameFragment1 not given, cannot authenticate request or indication");
+
+					return Authentication::BAD_MESSAGE;
+				}
+
+				// USERNAME attribute must be present.
+				if (this->username.empty())
+				{
+					MS_WARN_TAG(ice, "missing USERNAME attribute, cannot authenticate request or indication");
+
+					return Authentication::BAD_MESSAGE;
+				}
+
+				// MESSAGE-INTEGRITY attribute must be present.
+				if (!this->messageIntegrity)
 				{
-					return Authentication::BAD_REQUEST;
+					MS_WARN_TAG(
+					  ice, "missing MESSAGE-INTEGRITY attribute, cannot authenticate request or indication");
+
+					return Authentication::BAD_MESSAGE;
 				}
 
-				// Check that USERNAME attribute begins with our local username plus ":".
-				const size_t localUsernameLen = localUsername.length();
+				// Check that the USERNAME attribute begins with the first username
+				// fragment plus ":".
+				const size_t usernameFragment1Len = usernameFragment1.length();
 
 				if (
-				  this->username.length() <= localUsernameLen || this->username.at(localUsernameLen) != ':' ||
-				  (this->username.compare(0, localUsernameLen, localUsername) != 0))
+				  this->username.length() <= usernameFragment1Len ||
+				  this->username.at(usernameFragment1Len) != ':' ||
+				  this->username.compare(0, usernameFragment1Len, usernameFragment1) != 0)
 				{
 					return Authentication::UNAUTHORIZED;
 				}
 
 				break;
 			}
-			// This method cannot check authentication in received responses (as we
-			// are ICE-Lite and don't generate requests).
+
 			case Class::SUCCESS_RESPONSE:
 			case Class::ERROR_RESPONSE:
 			{
-				MS_ERROR("cannot check authentication for a STUN response");
+				// MESSAGE-INTEGRITY attribute must be present.
+				if (!this->messageIntegrity)
+				{
+					MS_WARN_TAG(ice, "missing MESSAGE-INTEGRITY attribute, cannot authenticate response");
 
-				return Authentication::BAD_REQUEST;
+					return Authentication::BAD_MESSAGE;
+				}
+
+				break;
+			}
+
+			default:
+			{
+				MS_WARN_TAG(ice, "unknown STUN class %" PRIu16 ", cannot authenticate", this->klass);
+
+				return Authentication::BAD_MESSAGE;
 			}
 		}
 
-		// If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY calculation,
-		// so the header length field must be modified (and later restored).
+		// If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY
+		// calculation, so the header length field must be modified (and later
+		// restored).
 		if (this->hasFingerprint)
 		{
-			// Set the header length field: full size - header length (20) - FINGERPRINT length (8).
+			// Set the header length field: full size - header length (20) -
+			// FINGERPRINT length (8).
 			Utils::Byte::Set2Bytes(this->data, 2, static_cast<uint16_t>(this->size - 20 - 8));
 		}
 
-		// Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY rules.
-		const uint8_t* computedMessageIntegrity = Utils::Crypto::GetHmacSha1(
-		  localPassword, this->data, (this->messageIntegrity - 4) - this->data);
+		// Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY
+		// rules.
+		const uint8_t* computedMessageIntegrity =
+		  Utils::Crypto::GetHmacSha1(password, this->data, (this->messageIntegrity - 4) - this->data);
 
 		Authentication result;
 
@@ -513,7 +583,7 @@ namespace RTC
 
 		MS_ASSERT(
 		  this->klass == Class::REQUEST,
-		  "attempt to create a success response for a non Request STUN packet");
+		  "attempt to create a success response for a non request STUN packet");
 
 		return new StunPacket(Class::SUCCESS_RESPONSE, this->method, this->transactionId, nullptr, 0);
 	}
@@ -524,7 +594,7 @@ namespace RTC
 
 		MS_ASSERT(
 		  this->klass == Class::REQUEST,
-		  "attempt to create an error response for a non Request STUN packet");
+		  "attempt to create an error response for a non request STUN packet");
 
 		auto* response =
 		  new StunPacket(Class::ERROR_RESPONSE, this->method, this->transactionId, nullptr, 0);
@@ -534,19 +604,6 @@ namespace RTC
 		return response;
 	}
 
-	void StunPacket::Authenticate(const std::string& password)
-	{
-		// Just for Request, Indication and SuccessResponse messages.
-		if (this->klass == Class::ERROR_RESPONSE)
-		{
-			MS_ERROR("cannot set password for ErrorResponse messages");
-
-			return;
-		}
-
-		this->password = password;
-	}
-
 	void StunPacket::Serialize(uint8_t* buffer)
 	{
 		MS_TRACE();
@@ -807,7 +864,8 @@ namespace RTC
 				Utils::Byte::Set2Bytes(buffer, 2, static_cast<uint16_t>(this->size - 20 - 8));
 			}
 
-			// Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY rules.
+			// Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY
+			// rules.
 			const uint8_t* computedMessageIntegrity =
 			  Utils::Crypto::GetHmacSha1(this->password, buffer, pos);
 
diff --git a/worker/src/RTC/Transport.cpp b/worker/src/RTC/Transport.cpp
index 830b31ddb7..ea5426970f 100644
--- a/worker/src/RTC/Transport.cpp
+++ b/worker/src/RTC/Transport.cpp
@@ -3083,8 +3083,8 @@ namespace RTC
 
 			/*
 			 * The interval between RTCP packets is varied randomly over the range
-			 * [1.0,1.5] times the calculated interval to avoid unintended synchronization
-			 * of all participants.
+			 * [1.0, 1.5] times the calculated interval to avoid unintended
+			 * synchronization of all participants.
 			 */
 			interval *= static_cast<float>(Utils::Crypto::GetRandomUInt(10, 15)) / 10;
 
diff --git a/worker/src/RTC/WebRtcServer.cpp b/worker/src/RTC/WebRtcServer.cpp
index bd1c095c13..362b43d455 100644
--- a/worker/src/RTC/WebRtcServer.cpp
+++ b/worker/src/RTC/WebRtcServer.cpp
@@ -499,7 +499,7 @@ namespace RTC
 
 		if (this->mapTupleWebRtcTransport.find(tuple->hash) == this->mapTupleWebRtcTransport.end())
 		{
-			MS_WARN_TAG(ice, "tuple hash not found in the table");
+			MS_DEBUG_TAG(ice, "tuple hash not found in the table");
 
 			return;
 		}
diff --git a/worker/src/RTC/WebRtcTransport.cpp b/worker/src/RTC/WebRtcTransport.cpp
index c718c1179a..a5deb26fe0 100644
--- a/worker/src/RTC/WebRtcTransport.cpp
+++ b/worker/src/RTC/WebRtcTransport.cpp
@@ -160,9 +160,11 @@ namespace RTC
 				iceLocalPreferenceDecrement += 100;
 			}
 
+			auto iceConsentTimeout = options->iceConsentTimeout();
+
 			// Create a ICE server.
 			this->iceServer = new RTC::IceServer(
-			  this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32));
+			  this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32), iceConsentTimeout);
 
 			// Create a DTLS transport.
 			this->dtlsTransport = new RTC::DtlsTransport(this);
@@ -227,9 +229,11 @@ namespace RTC
 				MS_THROW_TYPE_ERROR("empty iceCandidates");
 			}
 
+			auto iceConsentTimeout = options->iceConsentTimeout();
+
 			// Create a ICE server.
 			this->iceServer = new RTC::IceServer(
-			  this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32));
+			  this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32), iceConsentTimeout);
 
 			// Create a DTLS transport.
 			this->dtlsTransport = new RTC::DtlsTransport(this);
@@ -435,7 +439,8 @@ namespace RTC
 					MS_THROW_ERROR("connect() already called");
 				}
 
-				const auto* body           = request->data->body_as<FBS::WebRtcTransport::ConnectRequest>();
+				const auto* body = request->data->body_as<FBS::WebRtcTransport::ConnectRequest>();
+
 				const auto* dtlsParameters = body->dtlsParameters();
 
 				RTC::DtlsTransport::Fingerprint dtlsRemoteFingerprint;