OnlineSubsystemNull: Listen Server w/out microphone will drop clients with microphone if voice is enabled

NMouzourakis
Mon, 06 Mar 2017 18:22:25 +0000
Audio

Note: I am using the UE4 source from the release branch of UE4.15.0

Using OnlineSubsystemNull, with voice chat properly enabled, and if a Listen Server has no microphone, and if a connecting Client does have a microphone, and the connecting Client’s voice is enabled and broadcasting, and if the Listen Server receives any voice data from the connecting Client, the Listen Server will kick the connecting Client immediately. Debugging the engine’s source code, the reason this happens is slightly convoluted, as follows…

The problem begins when FOnlineSubsystemNull::GetVoiceInterface() attempts to Init() its FOnlineVoiceImpl implementing IOnlineVoice on the Listen Server:

IOnlineVoicePtr FOnlineSubsystemNull::GetVoiceInterface() const
{
	if (VoiceInterface.IsValid() && !bVoiceInterfaceInitialized)
	{
		if (!VoiceInterface->Init())
		{
			VoiceInterface = nullptr;
		}

		bVoiceInterfaceInitialized = true;
	}

	return VoiceInterface;
}

Looking at FOnlineVoiceImpl::Init(), we see:

bool FOnlineVoiceImpl::Init()
{
	bool bSuccess = false;

	// irrelevant code...

	bool bHasVoiceEnabled = false;
	if (GConfig->GetBool(TEXT("OnlineSubsystem"), TEXT("bHasVoiceEnabled"), bHasVoiceEnabled, GEngineIni) && bHasVoiceEnabled)
	{
		// irrelevant code...

		if (bSuccess)
		{
			const bool bVoiceEngineForceDisable = OnlineSubsystem->IsDedicated() || GIsBuildMachine;
			if (!bVoiceEngineForceDisable)
			{
				VoiceEngine = MakeShareable(new FVoiceEngineImpl(OnlineSubsystem));
				bSuccess = VoiceEngine->Init(MaxLocalTalkers, MaxRemoteTalkers);
			}

			// irrelevant code...
		}

		// irrelevant code...
	}

	// irrelevant code...

	return bSuccess;
}

Which calls bSuccess = VoiceEngine->Init(MaxLocalTalkers, MaxRemoteTalkers);, so looking at FVoiceEngineImpl::Init(), we see:

bool FVoiceEngineImpl::Init(int32 MaxLocalTalkers, int32 MaxRemoteTalkers)
{
	bool bSuccess = false;

	if (!OnlineSubsystem->IsDedicated())
	{
		FVoiceModule& VoiceModule = FVoiceModule::Get();
		if (VoiceModule.IsVoiceEnabled())
		{
			VoiceCapture = VoiceModule.CreateVoiceCapture();
			VoiceEncoder = VoiceModule.CreateVoiceEncoder();

			bSuccess = VoiceCapture.IsValid() && VoiceEncoder.IsValid();

			// irrelevant code...
		}

		// irrelevant code...
	}

	return bSuccess;
}

Which is where we start to see potential problems, namely with VoiceCapture = VoiceModule.CreateVoiceCapture();. I could continue to go deeper into the code, but suffice it to say that because a microphone doesn’t exist, bSuccess returns as false, and the FOnlineSubsystemNull::VoiceInterface->Init(); fails, which causes the entire FOnlineSubsystemNull::VoiceInterface to remain nullptr throughout the rest of the program’s execution. This looks like it should be fine, after all, code exists to account for a nullptr FOnlineSubsystemNull::VoiceInterface, but the real problem arises later on, when our first microphone-enabled Client joins our game: this Client’s FOnlineSubsystemNull::VoiceInterface is not nullptr because their microphone does exist, and therefore, they begin to send voice packets to the Listen Server, and their serialization is at some point delegated to UVoiceChannel::ReceivedBunch():

void UVoiceChannel::ReceivedBunch(FInBunch& Bunch)
{
	if (Connection->Driver && Connection->Driver->World)
	{
		while (!Bunch.AtEnd())
		{
			// Give the data to the local voice processing
			TSharedPtr<FVoicePacket> VoicePacket = UOnlineEngineInterface::Get()->SerializeRemotePacket(Connection->Driver->World, Bunch);
			if (VoicePacket.IsValid())
			{
				// irrelevant code...
			}
			else
			{
				// Unable to serialize the data because the serializer doesn't exist or there was a problem with this packet
				Bunch.SetError();
				break;
			}
		}
	}
}

And, again, looking at UOnlineEngineInterfaceImpl::SerializeRemotePacket():

TSharedPtr<FVoicePacket> UOnlineEngineInterfaceImpl::SerializeRemotePacket(UWorld* World, FArchive& Ar)
{
	IOnlineVoicePtr VoiceInt = Online::GetVoiceInterface(World);
	if (VoiceInt.IsValid())
	{
		return VoiceInt->SerializeRemotePacket(Ar);
	}
	return nullptr;
}

We see that it will return an invalid VoicePacket, because of the null FOnlineSubsystemNull::VoiceInterface, causing Bunch.SetError(); to be called. We now travel up the callstack to UChannel::ReceivedRawBunch() where we encounter…

UE_LOG( LogNetTraffic, Error, TEXT( "UChannel::ReceivedRawBunch: Bunch.IsError() after ReceivedNextBunch 1" ) );

…which finally causes UNetConnection::ReceivedPacket() to log the following error and disconnect the client for fear of a server crash attack.

UE_LOG( LogNetTraffic, Error, TEXT("Received corrupted packet data from client %s. Disconnecting."), *LowLevelGetRemoteAddress() );

Given all of this diagnostic info, it looks as though there is a structural weakness in the design of the voice system for the given case. A recommended fix that would not break the current design at a high level, or create messy code would be to support the FVoiceEngineImpl existing without a VoiceCapture, and just passing through silence should a steam of voice data be necessary.

16 posts - 6 participants