Skip to content

Commit

Permalink
Added sketch channel based on QUIC network protocol (requires .NET 9.0).
Browse files Browse the repository at this point in the history
  • Loading branch information
yallie committed Nov 30, 2024
1 parent 7c6d7c0 commit 792f579
Show file tree
Hide file tree
Showing 5 changed files with 555 additions and 0 deletions.
43 changes: 43 additions & 0 deletions CoreRemoting.Channels.Quic/CertificateHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace CoreRemoting.Channels.Quic;

internal class CertificateHelper
{
public static X509Certificate2 LoadFromPfx(string pfxFilePath, string pfxPassword) =>
X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, pfxPassword);

public static X509Certificate2 GenerateSelfSigned(string hostName = "localhost")
{
// generate a new certificate
var now = DateTimeOffset.UtcNow;
SubjectAlternativeNameBuilder sanBuilder = new();
sanBuilder.AddDnsName(hostName);

using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
CertificateRequest req = new($"CN={hostName}", ec, HashAlgorithmName.SHA256);

// Adds purpose
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection
{
new("1.3.6.1.5.5.7.3.1") // serverAuth
},
false));

// Adds usage
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));

// Adds subject alternate names
req.CertificateExtensions.Add(sanBuilder.Build());

// Sign
using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this type

var password = Guid.NewGuid().ToString();
var pfx = crt.Export(X509ContentType.Pfx, password);
var cert = X509CertificateLoader.LoadPkcs12(pfx, password);
return cert;
}
}
33 changes: 33 additions & 0 deletions CoreRemoting.Channels.Quic/CoreRemoting.Channels.Quic.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0.0</TargetFramework>
<RootNamespace>CoreRemoting.Channels.Quic</RootNamespace>
<AssemblyName>CoreRemoting.Channels.Quic</AssemblyName>
<PackageVersion>1.2.1</PackageVersion>
<Authors>Alexey Yakovlev</Authors>
<Description>Quic channels for CoreRemoting</Description>
<Copyright>2024 Alexey Yakovlev</Copyright>
<PackageProjectUrl>https://github.com/theRainbird/CoreRemoting</PackageProjectUrl>
<PackageLicenseUrl></PackageLicenseUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Title>CoreRemoting.Channels.Quic</Title>
<RepositoryUrl>https://github.com/theRainbird/CoreRemoting.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<AssemblyVersion>1.2.1</AssemblyVersion>
<LangVersion>10</LangVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;CA1416</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;1416</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\CoreRemoting\CoreRemoting.csproj" />
</ItemGroup>

</Project>
223 changes: 223 additions & 0 deletions CoreRemoting.Channels.Quic/QuicClientChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Text;
using System.Threading.Tasks;

namespace CoreRemoting.Channels.Quic;

/// <summary>
/// Client side QUIC channel implementation based on System.Net.Quic.
/// </summary>
public class QuicClientChannel : IClientChannel, IRawMessageTransport
{
internal const int MaxMessageSize = 1024 * 1024 * 128;
internal const string ProtocolName = nameof(CoreRemoting);

/// <summary>
/// Gets or sets the URL this channel is connected to.
/// </summary>
public string Url { get; private set; }

private Uri Uri { get; set; }

private IRemotingClient Client { get; set; }

private QuicClientConnectionOptions Options { get; set; }

private QuicConnection Connection { get; set; }

private QuicStream ClientStream { get; set; }

private BinaryReader ClientReader { get; set; }

private BinaryWriter ClientWriter { get; set; }

/// <inheritdoc />
public bool IsConnected { get; private set; }

/// <inheritdoc />
public IRawMessageTransport RawMessageTransport => this;

/// <inheritdoc />
public NetworkException LastException { get; set; }

/// <summary>
/// Event: fires when the channel is connected.
/// </summary>
public event Action Connected;

/// <inheritdoc />
public event Action Disconnected;

/// <inheritdoc />
public event Action<byte[]> ReceiveMessage;

/// <inheritdoc />
public event Action<string, Exception> ErrorOccured;

/// <inheritdoc />
public void Init(IRemotingClient client)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
if (!QuicConnection.IsSupported)
throw new NotSupportedException("QUIC is not supported.");

Url =
"quic://" +
client.Config.ServerHostName + ":" +
Convert.ToString(client.Config.ServerPort) +
"/rpc";

Uri = new Uri(Url);

// prepare QUIC client connection options
Options = new QuicClientConnectionOptions
{
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, Uri.Port), //new DnsEndPoint(Uri.Host, Uri.Port),
DefaultStreamErrorCode = 0x0A,
DefaultCloseErrorCode = 0x0B,
MaxInboundUnidirectionalStreams = 10,
MaxInboundBidirectionalStreams = 100,
ClientAuthenticationOptions = new SslClientAuthenticationOptions()
{
// accept self-signed certificates generated on-the-fly
RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true,
ApplicationProtocols = new List<SslApplicationProtocol>()
{
new SslApplicationProtocol(ProtocolName)
}
}
};
}

/// <inheritdoc />
public void Connect()
{
ConnectTask = ConnectTask ?? Task.Factory.StartNew(async () =>
{
// connect and open duplex stream
Connection = await QuicConnection.ConnectAsync(Options);
ClientStream = await Connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
ClientReader = new BinaryReader(ClientStream, Encoding.UTF8, leaveOpen: true);
ClientWriter = new BinaryWriter(ClientStream, Encoding.UTF8, leaveOpen: true);

// prepare handshake message
var handshakeMessage = Array.Empty<byte>();
if (Client.MessageEncryption)
{
handshakeMessage = Client.PublicKey;
}

// send handshake message
SendMessage(handshakeMessage);
IsConnected = true;
Connected?.Invoke();

// start listening for incoming messages
_ = Task.Factory.StartNew(() => StartListening());
});

ConnectTask.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}

private Task ConnectTask { get; set; }

private void StartListening()
{
try
{
while (IsConnected)
{
var messageSize = ClientReader.Read7BitEncodedInt();
var message = ClientReader.ReadBytes(Math.Min(messageSize, MaxMessageSize));
if (message.Length > 0)
{
ReceiveMessage(message);
}
}
}
catch (Exception ex)
{
LastException = ex as NetworkException ??
new NetworkException(ex.Message, ex);

ErrorOccured?.Invoke(ex.Message, ex);
Disconnected?.Invoke();
}
finally
{
Disconnect();
}
}

/// <inheritdoc />
public bool SendMessage(byte[] rawMessage)
{
try
{
if (rawMessage.Length > MaxMessageSize)
throw new InvalidOperationException("Message is too large. Max size: " +
MaxMessageSize + ", actual size: " + rawMessage.Length);

// message length + message body
ClientWriter.Write7BitEncodedInt(rawMessage.Length);
ClientWriter.Write(rawMessage, 0, rawMessage.Length);
return true;
}
catch (Exception ex)
{
LastException = ex as NetworkException ??
new NetworkException(ex.Message, ex);

ErrorOccured?.Invoke(ex.Message, ex);
return false;
}
}

private Task DisconnectTask { get; set; }

/// <inheritdoc />
public void Disconnect()
{
DisconnectTask = DisconnectTask ?? Task.Factory.StartNew(async () =>
{
await Connection.CloseAsync(0x0C);
IsConnected = false;
Disconnected?.Invoke();
});
}

/// <inheritdoc />
public void Dispose()
{
if (Connection == null)
return;

if (IsConnected)
Disconnect();

var task = DisconnectTask;
if (task != null)
task.ConfigureAwait(false)
.GetAwaiter()
.GetResult();

Connection.DisposeAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
Connection = null;

// clean up readers/writers
ClientReader.Dispose();
ClientReader = null;
ClientWriter.Dispose();
ClientWriter = null;
}
}
Loading

0 comments on commit 792f579

Please sign in to comment.