#define MS_CLASS "RTC::ICE::IceServer"
#include "RTC/ICE/IceServer.hpp"
#include "Logger.hpp"
#include <string_view>
namespace RTC
{
namespace ICE
{
static constexpr size_t StunResponseFactoryBufferLength{ 65536 };
thread_local uint8_t StunResponseFactoryBuffer[StunResponseFactoryBufferLength];
static constexpr size_t MaxTuples{ 8 };
static constexpr uint8_t ConsentCheckMinTimeoutSec{ 10u };
static constexpr uint8_t ConsentCheckMaxTimeoutSec{ 60u };
std::unordered_map<IceServer::IceState, std::string> IceServer::iceStateToString =
{
{ IceServer::IceState::NEW, "new" },
{ IceServer::IceState::CONNECTED, "connected" },
{ IceServer::IceState::COMPLETED, "completed" },
{ IceServer::IceState::DISCONNECTED, "disconnected" },
};
const std::string& IceServer::IceStateToString(IceState iceState)
{
MS_TRACE();
return IceServer::iceStateToString.at(iceState);
}
FBS::WebRtcTransport::IceState IceServer::IceStateToFbs(IceServer::IceState state)
{
MS_TRACE();
switch (state)
{
case IceServer::IceState::NEW:
{
return FBS::WebRtcTransport::IceState::NEW;
}
case IceServer::IceState::CONNECTED:
{
return FBS::WebRtcTransport::IceState::CONNECTED;
}
case IceServer::IceState::COMPLETED:
{
return FBS::WebRtcTransport::IceState::COMPLETED;
}
case IceServer::IceState::DISCONNECTED:
{
return FBS::WebRtcTransport::IceState::DISCONNECTED;
}
NO_DEFAULT_GCC();
}
}
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)
{
}
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;
this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
}
IceServer::~IceServer()
{
MS_TRACE();
this->listener->OnIceServerLocalUsernameFragmentRemoved(this, usernameFragment);
if (!this->oldUsernameFragment.empty())
{
this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
}
this->isRemovingTuples = true;
for (const auto& it : this->tuples)
{
auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));
this->listener->OnIceServerTupleRemoved(this, storedTuple);
}
this->isRemovingTuples = false;
this->tuples.clear();
this->selectedTuple = nullptr;
delete this->consentCheckTimer;
this->consentCheckTimer = nullptr;
}
void IceServer::Dump(int indentation) const
{
MS_TRACE();
MS_DUMP_CLEAN(indentation, "<IceServer>");
MS_DUMP_CLEAN(indentation, " state: %s", IceServer::IceStateToString(this->state).c_str());
MS_DUMP_CLEAN(indentation, " tuples:");
for (const auto& tuple : this->tuples)
{
tuple.Dump(indentation + 2);
}
if (this->selectedTuple)
{
MS_DUMP_CLEAN(indentation, " selected tuple:");
this->selectedTuple->Dump(indentation + 2);
}
MS_DUMP_CLEAN(indentation, " consent timeout (ms): %" PRIu16, this->consentTimeoutMs);
MS_DUMP_CLEAN(indentation, " remote nomination: %" PRIu32, this->remoteNomination);
MS_DUMP_CLEAN(indentation, "</IceServer>");
}
void IceServer::ProcessStunPacket(const RTC::ICE::StunPacket* packet, RTC::TransportTuple* tuple)
{
MS_TRACE();
switch (packet->GetClass())
{
case RTC::ICE::StunPacket::Class::REQUEST:
{
ProcessStunRequest(packet, tuple);
break;
}
case RTC::ICE::StunPacket::Class::INDICATION:
{
ProcessStunIndication(packet);
break;
}
case RTC::ICE::StunPacket::Class::SUCCESS_RESPONSE:
case RTC::ICE::StunPacket::Class::ERROR_RESPONSE:
{
ProcessStunResponse(packet);
break;
}
default:
{
MS_WARN_TAG(
ice,
"unknown STUN class %" PRIu16 ", discarded",
static_cast<uint16_t>(packet->GetClass()));
}
}
}
void IceServer::RestartIce(const std::string& usernameFragment, const std::string& password)
{
MS_TRACE();
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;
this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
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();
if (this->isRemovingTuples)
{
return;
}
RTC::TransportTuple* removedTuple{ nullptr };
auto it = this->tuples.begin();
for (; it != this->tuples.end(); ++it)
{
RTC::TransportTuple* storedTuple = std::addressof(*it);
if (storedTuple->Compare(tuple))
{
removedTuple = storedTuple;
break;
}
}
if (!removedTuple)
{
return;
}
this->isRemovingTuples = true;
this->listener->OnIceServerTupleRemoved(this, removedTuple);
this->isRemovingTuples = false;
this->tuples.erase(it);
if (removedTuple == this->selectedTuple)
{
this->selectedTuple = nullptr;
if (
(this->state == IceState::CONNECTED || this->state == IceState::COMPLETED) &&
this->tuples.begin() != this->tuples.end())
{
SetSelectedTuple(std::addressof(*this->tuples.begin()));
if (IsConsentCheckSupported())
{
RestartConsentCheck();
}
}
else
{
this->state = IceState::DISCONNECTED;
this->remoteNomination = 0u;
this->listener->OnIceServerDisconnected(this);
if (IsConsentCheckSupported() && IsConsentCheckRunning())
{
StopConsentCheck();
}
}
}
}
void IceServer::ProcessStunRequest(const RTC::ICE::StunPacket* request, RTC::TransportTuple* tuple)
{
MS_TRACE();
MS_DEBUG_DEV("processing STUN request");
if (request->GetMethod() != RTC::ICE::StunPacket::Method::BINDING)
{
MS_WARN_TAG(
ice,
"STUN request with unknown method %#.3x => 400",
static_cast<unsigned int>(request->GetMethod()));
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer, sizeof(StunResponseFactoryBuffer), 400, "unknown method");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
if (!request->HasAttribute(StunPacket::AttributeType::FINGERPRINT))
{
MS_WARN_TAG(ice, "STUN Binding request without FINGERPRINT attribute => 400");
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer,
sizeof(StunResponseFactoryBuffer),
400,
"missing FINGERPRINT attribute in STUN Binding request");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
if (!request->HasAttribute(StunPacket::AttributeType::PRIORITY))
{
MS_WARN_TAG(ice, "STUN Binding request without PRIORITY attribute => 400");
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer,
sizeof(StunResponseFactoryBuffer),
400,
"missing PRIORITY attribute in STUN Binding request");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
switch (request->CheckAuthentication(this->usernameFragment, this->password))
{
case RTC::ICE::StunPacket::AuthenticationResult::OK:
{
if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty())
{
MS_DEBUG_TAG(ice, "new ICE credentials applied");
this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
this->oldUsernameFragment.clear();
this->oldPassword.clear();
}
break;
}
case RTC::ICE::StunPacket::AuthenticationResult::UNAUTHORIZED:
{
if (
!this->oldUsernameFragment.empty() && !this->oldPassword.empty() &&
request->CheckAuthentication(this->oldUsernameFragment, this->oldPassword) ==
RTC::ICE::StunPacket::AuthenticationResult::OK)
{
MS_DEBUG_TAG(ice, "using old ICE credentials");
break;
}
MS_WARN_TAG(ice, "wrong authentication in STUN Binding request => 401");
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer,
sizeof(StunResponseFactoryBuffer),
401,
"wrong authentication in STUN Binding request");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
case RTC::ICE::StunPacket::AuthenticationResult::BAD_MESSAGE:
{
MS_WARN_TAG(ice, "cannot check authentication in STUN Binding request => 400");
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer,
sizeof(StunResponseFactoryBuffer),
400,
"cannot check authentication in STUN Binding request");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
}
if (request->GetIceControlled())
{
MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding request => 487");
auto* response = request->CreateErrorResponse(
StunResponseFactoryBuffer,
sizeof(StunResponseFactoryBuffer),
487,
"invalid ICE-CONTROLLED attribute in STUN Binding request");
response->Protect();
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
MS_DEBUG_DEV(
"valid STUN Binding request [priority:%" PRIu32 ", useCandidate:%s]",
static_cast<uint32_t>(request->GetPriority()),
request->HasAttribute(RTC::ICE::StunPacket::AttributeType::USE_CANDIDATE) ? "true" : "false");
auto* response =
request->CreateSuccessResponse(StunResponseFactoryBuffer, sizeof(StunResponseFactoryBuffer));
response->AddXorMappedAddress(tuple->GetRemoteAddress());
if (this->oldPassword.empty())
{
response->Protect(this->password);
}
else
{
response->Protect(this->oldPassword);
}
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
HandleTuple(
tuple,
request->HasAttribute(StunPacket::AttributeType::USE_CANDIDATE),
request->HasAttribute(StunPacket::AttributeType::NOMINATION),
request->GetNomination());
if (IsConsentCheckSupported() && (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED))
{
if (IsConsentCheckRunning())
{
RestartConsentCheck();
}
else
{
StartConsentCheck();
}
}
}
void IceServer::ProcessStunIndication(const RTC::ICE::StunPacket* )
{
MS_TRACE();
MS_DEBUG_DEV("STUN indication received, ignored");
}
void IceServer::ProcessStunResponse(const RTC::ICE::StunPacket* response)
{
MS_TRACE();
if (response->GetClass() == RTC::ICE::StunPacket::Class::SUCCESS_RESPONSE)
{
MS_DEBUG_DEV("ignoring received STUN successs response", responseType.c_str());
}
else
{
thread_local std::string_view errorReasonPhrase;
response->GetErrorCode(errorReasonPhrase);
MS_DEBUG_DEV("ignoring received STUN error response [errorCode:%" PRIu16 ", reasonPhrase:\"%.*s\""], static_cast<int>(errorReasonPhrase.size()), errorReasonPhrase.data());
}
}
void IceServer::MayForceSelectedTuple(const RTC::TransportTuple* tuple)
{
MS_TRACE();
if (this->state != IceState::CONNECTED && this->state != IceState::COMPLETED)
{
MS_WARN_TAG(ice, "cannot force selected tuple if not in state 'connected' or 'completed'");
return;
}
auto* storedTuple = HasTuple(tuple);
if (!storedTuple)
{
MS_WARN_TAG(ice, "cannot force selected tuple if the given tuple was not already a valid one");
return;
}
SetSelectedTuple(storedTuple);
}
void IceServer::HandleTuple(
RTC::TransportTuple* tuple, bool hasUseCandidate, bool hasNomination, uint32_t nomination)
{
MS_TRACE();
switch (this->state)
{
case IceState::NEW:
{
MS_ASSERT(!this->selectedTuple, "state is 'new' but there is selected tuple");
if (!hasUseCandidate && !hasNomination)
{
MS_DEBUG_TAG(
ice,
"transition from state 'new' to 'connected' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
"]",
hasUseCandidate ? "true" : "false",
hasNomination ? "true" : "false",
nomination);
auto* storedTuple = AddTuple(tuple);
this->state = IceState::CONNECTED;
SetSelectedTuple(storedTuple);
this->listener->OnIceServerConnected(this);
}
else
{
auto* storedTuple = AddTuple(tuple);
const auto isNewNomination = hasNomination && nomination > this->remoteNomination;
if (isNewNomination || !hasNomination)
{
MS_DEBUG_TAG(
ice,
"transition from state 'new' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
"]",
hasUseCandidate ? "true" : "false",
hasNomination ? "true" : "false",
nomination);
this->state = IceState::COMPLETED;
SetSelectedTuple(storedTuple);
if (isNewNomination)
{
this->remoteNomination = nomination;
}
this->listener->OnIceServerCompleted(this);
}
}
break;
}
case IceState::DISCONNECTED:
{
MS_ASSERT(!this->selectedTuple, "state is 'disconnected' but there is selected tuple");
if (!hasUseCandidate && !hasNomination)
{
MS_DEBUG_TAG(
ice,
"transition from state 'disconnected' to 'connected' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
"]",
hasUseCandidate ? "true" : "false",
hasNomination ? "true" : "false",
nomination);
auto* storedTuple = AddTuple(tuple);
this->state = IceState::CONNECTED;
SetSelectedTuple(storedTuple);
this->listener->OnIceServerConnected(this);
}
else
{
auto* storedTuple = AddTuple(tuple);
const auto isNewNomination = hasNomination && nomination > this->remoteNomination;
if (isNewNomination || !hasNomination)
{
MS_DEBUG_TAG(
ice,
"transition from state 'disconnected' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
"]",
hasUseCandidate ? "true" : "false",
hasNomination ? "true" : "false",
nomination);
this->state = IceState::COMPLETED;
SetSelectedTuple(storedTuple);
if (isNewNomination)
{
this->remoteNomination = nomination;
}
this->listener->OnIceServerCompleted(this);
}
}
break;
}
case IceState::CONNECTED:
{
MS_ASSERT(!this->tuples.empty(), "state is 'connected' but there are no tuples");
MS_ASSERT(this->selectedTuple, "state is 'connected' but there is not selected tuple");
if (!hasUseCandidate && !hasNomination)
{
AddTuple(tuple);
}
else
{
MS_DEBUG_TAG(
ice,
"transition from state 'connected' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
"]",
hasUseCandidate ? "true" : "false",
hasNomination ? "true" : "false",
nomination);
auto* storedTuple = AddTuple(tuple);
const auto isNewNomination = hasNomination && nomination > this->remoteNomination;
if (isNewNomination || !hasNomination)
{
this->state = IceState::COMPLETED;
SetSelectedTuple(storedTuple);
if (isNewNomination)
{
this->remoteNomination = nomination;
}
this->listener->OnIceServerCompleted(this);
}
}
break;
}
case IceState::COMPLETED:
{
MS_ASSERT(!this->tuples.empty(), "state is 'completed' but there are no tuples");
MS_ASSERT(this->selectedTuple, "state is 'completed' but there is not selected tuple");
if (!hasUseCandidate && !hasNomination)
{
AddTuple(tuple);
}
else
{
auto* storedTuple = AddTuple(tuple);
const auto isNewNomination = hasNomination && nomination > this->remoteNomination;
if (isNewNomination || hasUseCandidate)
{
SetSelectedTuple(storedTuple);
if (isNewNomination)
{
this->remoteNomination = nomination;
}
}
}
break;
}
}
}
RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple)
{
MS_TRACE();
auto* storedTuple = HasTuple(tuple);
if (storedTuple)
{
MS_DEBUG_DEV("tuple already exists");
return storedTuple;
}
this->tuples.push_front(*tuple);
storedTuple = std::addressof(*this->tuples.begin());
if (storedTuple->GetProtocol() == TransportTuple::Protocol::UDP)
{
storedTuple->StoreUdpRemoteAddress();
}
this->listener->OnIceServerTupleAdded(this, storedTuple);
if (this->tuples.size() > MaxTuples)
{
MS_WARN_TAG(ice, "too too many tuples, removing the oldest non selected one");
RTC::TransportTuple* removedTuple{ nullptr };
auto it = this->tuples.rbegin();
for (; it != this->tuples.rend(); ++it)
{
RTC::TransportTuple* otherStoredTuple = std::addressof(*it);
if (otherStoredTuple != storedTuple && otherStoredTuple != this->selectedTuple)
{
removedTuple = otherStoredTuple;
break;
}
}
MS_ASSERT(removedTuple, "couldn't find any tuple to be removed");
this->isRemovingTuples = true;
this->listener->OnIceServerTupleRemoved(this, removedTuple);
this->isRemovingTuples = false;
this->tuples.erase(std::next(it).base());
}
return storedTuple;
}
RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const
{
MS_TRACE();
if (this->selectedTuple && this->selectedTuple->Compare(tuple))
{
return this->selectedTuple;
}
for (const auto& it : this->tuples)
{
auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));
if (storedTuple->Compare(tuple))
{
return storedTuple;
}
}
return nullptr;
}
void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple)
{
MS_TRACE();
if (storedTuple == this->selectedTuple)
{
return;
}
this->selectedTuple = storedTuple;
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");
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");
MS_ASSERT(
this->state == IceState::COMPLETED || this->state == IceState::CONNECTED,
"ICE consent check timer fired but state is neither 'completed' nor 'connected'");
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");
this->state = IceState::DISCONNECTED;
this->remoteNomination = 0u;
this->isRemovingTuples = true;
for (const auto& it : this->tuples)
{
auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));
this->listener->OnIceServerTupleRemoved(this, storedTuple);
}
this->isRemovingTuples = false;
this->tuples.clear();
this->selectedTuple = nullptr;
this->listener->OnIceServerDisconnected(this);
}
}
} }