Unreal Engine Plugin for Quiche Library Integration

Hello everyone,

I’m reaching out for assistance with a project I’ve been working on involving Unreal Engine and the Quiche library.

Background:
I’ve been working on integrating the Quiche library into Unreal Engine to develop a client that can connect to a Quic server. So far, I’ve successfully integrated the Quiche library into Unreal Engine and have written a simple UDP client in Unreal Engine to send and receive messages using Unreal’s socket functionalities. Additionally, I’ve also developed a C client using the Quiche library to understand its workings, and both of these clients work fine.

Issue:
However, when I attempt to write a client in Unreal Engine using the Quiche library, I encounter an issue during the handshake process. I can see the initial data packet being sent by the client to the Quic server, but after that, I’m unable to make any progress. It seems like there’s a hurdle in establishing the handshake. I am new to unreal engine and the quiche library and I have been struggling with this for a while now.

Code:

***** Header File *****


#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include <Interfaces/IPv4/IPv4Endpoint.h>
#include <Networking/Public/Common/UdpSocketBuilder.h>
#include <Interfaces/IPv4/IPv4Address.h>
#include <Sockets/Public/IPAddress.h>
#include <SocketSubsystem.h>
#include <Sockets.h>

#include "quiche_api.h"

class RDNCPPTEST_API FRdnCppTestModule : public IModuleInterface
{
public:

	int init();
	int send();
	int recv(quiche_recv_info* recvInfo);
	void driver();

private:

	FSocket* socket;
	ISocketSubsystem* SocketSubsystem;
	FIPv4Address ip;

	quiche_config* config;
	quiche_conn* conn;

	struct sockaddr localAddr;
	struct sockaddr peerAddr;

	const char* host = "172.27.224.121"; // Hard code the IP for now
};


***** Header File *****

***** CPP File *****

#include "RdnCppTest.h"

#define LOCTEXT_NAMESPACE "FRdnCppTestModule"

#define MAX_DATAGRAM_SIZE 1500
#define LOCAL_CONN_ID_LEN 16


int FRdnCppTestModule::init()
{
	/*UNREAL*/
	SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
	socket = SocketSubsystem->CreateSocket(NAME_DGram, "mySocket", false);

	if (!socket)
	{
		UE_LOG(LogTemp, Log, TEXT("Error creating socket."));
		return -1;
	}
	UE_LOG(LogTemp, Log, TEXT("Socket successfully created."));

	socket->SetNonBlocking();

	TSharedPtr <FInternetAddr> serveraddr = SocketSubsystem->CreateInternetAddr();
	FIPv4Address::Parse("172.27.224.121", ip);
	serveraddr->SetIp(ip.Value);
	serveraddr->SetPlatformPort(8080);

	bool connected = socket->Connect(*serveraddr);

	if (!connected)
	{
		UE_LOG(LogTemp, Log, TEXT("Error in socket connection"));
		return -1;
	}
	UE_LOG(LogTemp, Log, TEXT("Socket successfully connected to server IP"));

	/* QUICHE */
	quiche_enable_debug_logging([](const char* line, void* argp) {
		UE_LOG(LogTemp, Log, TEXT("Quiche: %s"), UTF8_TO_TCHAR(line));
		},
		nullptr
	);

	config = quiche_config_new(0xbabababa);
	if (config == NULL)
	{
		UE_LOG(LogTemp, Log, TEXT("Failed to create config."));
		return -1;
	}
	UE_LOG(LogTemp, Log, TEXT("Config created."));

	quiche_config_set_application_protos(config,
		(uint8_t*)"\x0ahq-interop\x05hq-29\x05hq-28\x05hq-27\x08http/0.9", 38);
	quiche_config_set_max_idle_timeout(config, 5000);
	quiche_config_set_max_recv_udp_payload_size(config, MAX_DATAGRAM_SIZE);
	quiche_config_set_max_send_udp_payload_size(config, MAX_DATAGRAM_SIZE);
	quiche_config_set_initial_max_data(config, 10000000);
	quiche_config_set_initial_max_stream_data_bidi_local(config, 1000000);
	quiche_config_set_initial_max_stream_data_bidi_remote(config, 1000000);
	quiche_config_set_initial_max_streams_bidi(config, 100);
	quiche_config_set_initial_max_streams_uni(config, 100);
	quiche_config_set_disable_active_migration(config, true);

	uint8_t scid[QUICHE_MAX_CONN_ID_LEN] = {};
	for (size_t i = 0; i < QUICHE_MAX_CONN_ID_LEN; ++i)
		scid[i] = static_cast<unsigned char>(i);
	size_t scid_len = sizeof(scid);

	memset(&localAddr, 0, sizeof(localAddr));
	struct sockaddr_in* localIPv4Addr = (struct sockaddr_in*)&localAddr;
	localIPv4Addr->sin_family = AF_INET;
	localIPv4Addr->sin_addr.s_addr = inet_addr("127.0.0.1");
	unsigned short localPort = socket->GetPortNo();
	localIPv4Addr->sin_port = htons(8080);
	size_t localIPv4Len = sizeof(localIPv4Addr);

	memset(&peerAddr, 0, sizeof(peerAddr));
	struct sockaddr_in* peerIPv4Addr = (struct sockaddr_in*)&peerAddr;
	peerIPv4Addr->sin_family = AF_INET;
	peerIPv4Addr->sin_addr.s_addr = inet_addr(host);
	peerIPv4Addr->sin_port = htons(8080);
	size_t peerIPv4Len = sizeof(peerIPv4Addr);

	conn = quiche_connect(host, (const uint8_t*)scid, scid_len,
		&localAddr, sizeof(localAddr),
		&peerAddr, sizeof(peerAddr),
		config);

	if (conn == nullptr)
	{
		UE_LOG(LogTemp, Log, TEXT("Quiche Connect Error"));
		return -1;
	}
	UE_LOG(LogTemp, Log, TEXT("Quiche Connect Successful"));
	return 0;
}

int FRdnCppTestModule::send()
{
	int err = 0;
	int32 bytesSent = 0;
	static uint8_t send_buffer[1350];
	quiche_send_info send_info;

	while (true)
	{
		/*QUICHE PACKET WRITE*/
		ssize_t quichePacketWrite = quiche_conn_send(conn, send_buffer, sizeof(send_buffer), &send_info);
		if (quichePacketWrite == QUICHE_ERR_DONE)
		{
			UE_LOG(LogTemp, Log, TEXT("Nothing to send from quiche."));
			break;
		}
		if (quichePacketWrite < 0)
		{
			UE_LOG(LogTemp, Log, TEXT("Failed to create packet from quiche."));
			err = -1;
			break;
		}
		UE_LOG(LogTemp, Log, TEXT("Packet write from quiche successful. Bytes written: %zd"), quichePacketWrite);

		/*UNREAL SOCKET*/
		bool sent = socket->Send(send_buffer, sizeof(send_buffer), bytesSent);
		if (!sent)
		{
			UE_LOG(LogTemp, Log, TEXT("Failed to send packet over the socket."));
			err = -1;
			break;
		}
		UE_LOG(LogTemp, Log, TEXT("Bytes sent over the socket: %d"), bytesSent);
	}

	return err;
}

int FRdnCppTestModule::recv(quiche_recv_info* recvInfo)
{
	int err = 0;
	int32 bytesRead = 0;
	static uint8_t recvBuffer[65535];

	while (true)
	{
		err = 0;
		bool received = socket->Recv(recvBuffer, sizeof(recvBuffer), bytesRead, ESocketReceiveFlags::None);
		if (!received)
		{
			UE_LOG(LogTemp, Log, TEXT("Read error from socket. Error Value: %d"), bytesRead);
			err = -1;
			break;
		}
		if (!bytesRead)
		{
			UE_LOG(LogTemp, Log, TEXT("Nothing to read from the socket"));
			break;
		}
		UE_LOG(LogTemp, Log, TEXT("Successfully read from the socket. Bytes read: %d"), bytesRead);

		ssize_t quichePacketRead = quiche_conn_recv(conn, recvBuffer, sizeof(recvBuffer), recvInfo);
		if (quichePacketRead < 0)
		{
			UE_LOG(LogTemp, Log, TEXT("Error processing packet in quiche. Error: %zd"), quichePacketRead);
			err = -1;
			break;;
		}
		UE_LOG(LogTemp, Log, TEXT("Packet successfully processed. Bytes processed: %zd"), quichePacketRead);
	}

	return err;
}

void FRdnCppTestModule::driver()
{
	int err = 0;
	bool protos_sent = false;

	/* Initializing Local Addr and Peer Addr */
	/*struct sockaddr localAddr;
	struct sockaddr peerAddr;
	socklen_t localAddrLength = sizeof(localAddr);
	memset(&localAddr, 0, localAddrLength);
	socklen_t peerAddrLength = sizeof(peerAddr);
	memset(&peerAddr, 0, peerAddrLength);*/

	if (init() < 0) // Socket construction and quiche initialization
	{
		UE_LOG(LogTemp, Log, TEXT("Error in the init function."));
		return;
	}

	quiche_recv_info recvInfo =
	{
			(struct sockaddr*)&peerAddr,
			sizeof(peerAddr),
			(struct sockaddr*)&localAddr,
			sizeof(localAddr),
	};

	/* Main Loop */
	while (true)
	{
		err = send();
		if (err)
		{
			UE_LOG(LogTemp, Log, TEXT("Error in the send function!"));
			break;
		}
		err = recv(&recvInfo);
		if (err)
		{
			UE_LOG(LogTemp, Log, TEXT("Error in the receive function!"));
			break;
		}
		if (quiche_conn_is_established(conn) && !protos_sent)
		{
			const uint8_t* app_proto;
			size_t app_proto_len;
			quiche_conn_application_proto(conn, &app_proto, &app_proto_len); // Returns the negotiated ALPN
			UE_LOG(LogTemp, Log, TEXT("Connection Established and ALPN negotiated."));
			const static uint8_t r[] = "GET /index.html\r\n";
			if (quiche_conn_stream_send(conn, 4, r, sizeof(r), true) < 0)
			{
				UE_LOG(LogTemp, Log, TEXT("Failed to send request."));
				break;
			}
			UE_LOG(LogTemp, Log, TEXT("Request sent after ALPN negototiation."));
			protos_sent = true;
		}
		if (quiche_conn_is_established(conn))
		{
			UE_LOG(LogTemp, Log, TEXT("Connection established successfully!"));
			// TODO: Send data over a stream
		}
		if (quiche_conn_is_closed(conn))
		{
			UE_LOG(LogTemp, Log, TEXT("Connection Closed."));
			break;
		}
	}

	quiche_conn_free(conn);
	quiche_config_free(config);
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FRdnCppTestModule, RdnCppTest)

***** CPP File *****


***** Unreal Output *****

LogTemp: Socket successfully created.
LogTemp: Socket successfully connected to server IP
LogTemp: Config created.
LogTemp: Quiche Connect Successful
LogTemp: Quiche: quiche::tls: 000102030405060708090a0b0c0d0e0f10111213 write message lvl=Initial len=277
LogTemp: Quiche: quiche: 000102030405060708090a0b0c0d0e0f10111213 tx pkt Initial version=babababa dcid=f30a9f1d527d83397b803de36f65c9ac scid=000102030405060708090a0b0c0d0e0f10111213 len=281 pn=0 src:127.0.0.1:8080 dst:172.27.224.121:8080
LogTemp: Quiche: quiche: 000102030405060708090a0b0c0d0e0f10111213 tx frm CRYPTO off=0 len=277
LogTemp: Quiche: quiche::recovery: 000102030405060708090a0b0c0d0e0f10111213 timer=998.2987ms latest_rtt=0ns srtt=None min_rtt=0ns rttvar=166.5ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=15000 ssthresh=18446744073709551615 bytes_in_flight=344 app_limited=true congestion_recovery_start_time=None Rate { delivered: 0, delivered_time: Instant { t: 563857.6140141s }, first_sent_time: Instant { t: 563857.6140141s }, end_of_app_limited: 1, last_sent_packet: 0, largest_acked: 0, rate_sample: RateSample { delivery_rate: 0, is_app_limited: false, interval: 0ns, delivered: 0, prior_delivere
d: 0, prior_time: None, send_elapsed: 0ns, ack_elapsed: 0ns, rtt: 0ns } } pacer=Pacer { enabled: true, capacity: 15000, used: 0, rate: 0, last_update: Instant { t: 563857.6140141s }, next_time: Instant { t: 563857.6140141s }, max_datagram_size: 1500, last_packet_size: None, iv: 0ns, max_pacing_rate: None } hystart=window_end=None last_round_min_rtt=18446744073709551615.999999999s current_round_min_rtt=18446744073709551615.999999999s css_baseline_min_rtt=18446744073709551615.999999999s rtt_sample_count=0 css_start_time=None css_round_count=0 cubic={ k=0 w_max=0 } 
LogTemp: Packet write from quiche successful. Bytes written: 1200
LogTemp: Bytes sent over the socket: 1350
LogTemp: Nothing to send from quiche.
LogTemp: Read error from socket. Error Value: 0
LogTemp: Error in the receive function!

***** Unreal Output *****


***** Server Output *****

172.27.224.1
version negotiation
sent 47 bytes
Trying again, recv would block

***** Server Output *****