feat(client): add rendezvous traversal coordinators (#12)
quality-gate / quality (push) Successful in 56s

This commit is contained in:
KyuubiYoru
2026-07-16 08:39:05 +02:00
parent 6d076c281a
commit b4b6072fe1
28 changed files with 2949 additions and 57 deletions
@@ -0,0 +1,290 @@
using System.Net;
using FinalFactory.Rendezvous.Contracts;
using LiteNetLib;
namespace FinalFactory.Rendezvous.Client;
public sealed class RendezvousClientCoordinator : IDisposable
{
private readonly NetManager _manager;
private readonly RendezvousNetListener _networkEvents;
private readonly EventBasedNatPunchListener _punchEvents;
private readonly IPEndPoint _mediator;
private readonly CreateJoinAttemptResponse _attempt;
private readonly IRendezvousCoordinatorClock _clock;
private readonly RendezvousPunchRetrySchedule _retry;
private NetPeer? _connectingPeer;
private bool _cancelRequested;
private int _polling;
private bool _subscriptionsReleased;
private bool _disposed;
public RendezvousClientCoordinator(
NetManager manager,
RendezvousNetListener networkEvents,
IPEndPoint mediator,
CreateJoinAttemptResponse attempt,
RendezvousCoordinatorOptions? options = null)
: this(
manager,
networkEvents,
mediator,
attempt,
options,
new SystemRendezvousCoordinatorClock())
{
}
internal RendezvousClientCoordinator(
NetManager manager,
RendezvousNetListener networkEvents,
IPEndPoint mediator,
CreateJoinAttemptResponse attempt,
RendezvousCoordinatorOptions? options,
IRendezvousCoordinatorClock clock)
{
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
_networkEvents = networkEvents ?? throw new ArgumentNullException(nameof(networkEvents));
_punchEvents = _networkEvents.PunchEvents;
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_attempt = attempt ?? throw new ArgumentNullException(nameof(attempt));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
RendezvousCoordinatorOptions validated = (options ?? new RendezvousCoordinatorOptions())
.CopyAndValidate();
_retry = new(validated, _clock);
RendezvousManagerGuard.Validate(_manager, _networkEvents);
if (_mediator.Port is < 1 or > 65_535
|| _attempt.AttemptId.Value == Guid.Empty
|| _attempt.MediationHandle.Value == Guid.Empty
|| !ContractValidation.IsCapabilityValid(_attempt.ClientPunchCapability)
|| !ContractValidation.IsConnectionTicketValid(_attempt.ConnectionTicketDigest)
|| _attempt.ExpiresAt <= _clock.UtcNow)
{
throw new ArgumentException("The client traversal inputs are invalid.");
}
_networkEvents.RendezvousPeerConnected += OnPeerConnected;
_networkEvents.RendezvousPeerDisconnected += OnPeerDisconnected;
_punchEvents.NatIntroductionSuccess += OnNatIntroductionSuccess;
}
public event EventHandler<RendezvousConnectionCompletedEventArgs>? Completed;
public RendezvousConnectionState State { get; private set; } = RendezvousConnectionState.Punching;
public NetPeer? ConnectedPeer { get; private set; }
public bool IsCompleted => IsTerminal(State);
public void Cancel() => Volatile.Write(ref _cancelRequested, true);
public async Task<RendezvousClientResult<bool>> CancelAsync(
IRendezvousJoinClient joinClient,
CancellationToken cancellationToken = default)
{
if (joinClient is null)
{
throw new ArgumentNullException(nameof(joinClient));
}
ThrowIfDisposed();
Cancel();
return await joinClient.CancelAsync(_attempt, cancellationToken).ConfigureAwait(false);
}
public void Poll()
{
ThrowIfDisposed();
if (IsCompleted)
{
return;
}
if (Interlocked.Exchange(ref _polling, 1) != 0)
{
throw new InvalidOperationException("The Rendezvous coordinator cannot be polled concurrently or recursively.");
}
try
{
if (Volatile.Read(ref _cancelRequested))
{
DisconnectPendingPeer();
Complete(RendezvousConnectionState.Cancelled);
return;
}
if (!_manager.IsRunning)
{
Complete(RendezvousConnectionState.ManagerStopped);
return;
}
_manager.PollEvents();
_manager.NatPunchModule.PollEvents();
if (IsCompleted)
{
return;
}
DateTimeOffset now = _clock.UtcNow;
if (Volatile.Read(ref _cancelRequested))
{
DisconnectPendingPeer();
Complete(RendezvousConnectionState.Cancelled);
}
else if (!_manager.IsRunning)
{
Complete(RendezvousConnectionState.ManagerStopped);
}
else if (now >= _attempt.ExpiresAt)
{
DisconnectPendingPeer();
Complete(RendezvousConnectionState.TimedOut);
}
else if (State == RendezvousConnectionState.Punching && _retry.IsDue(now))
{
if (_retry.IsExhausted)
{
Complete(RendezvousConnectionState.TimedOut);
return;
}
_manager.NatPunchModule.SendNatIntroduceRequest(
_mediator,
NatPunchRequestTokenCodec.Encode(
NatPunchPeerRole.Client,
_attempt.MediationHandle,
_attempt.ClientPunchCapability));
_retry.RecordRequest();
}
}
finally
{
Volatile.Write(ref _polling, 0);
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
if (!IsCompleted)
{
Complete(RendezvousConnectionState.Disposed);
}
ReleaseSubscriptions();
_disposed = true;
}
public override string ToString() =>
$"[RendezvousClientCoordinator {_attempt.AttemptId}; credentials redacted]";
private void OnNatIntroductionSuccess(
IPEndPoint target,
NatAddressType addressType,
string encodedIntroduction)
{
_ = addressType;
if (State != RendezvousConnectionState.Punching
|| !NatIntroductionTokenCodec.TryDecode(
encodedIntroduction,
out NatIntroductionToken? introduction)
|| introduction is null
|| introduction.AttemptId != _attempt.AttemptId
|| !NatIntroductionTokenCodec.MatchesDigest(
introduction.ConnectionTicket,
_attempt.ConnectionTicketDigest))
{
return;
}
byte[] connectionData = DirectConnectionRequestCodec.Encode(
introduction.AttemptId,
introduction.ConnectionTicket);
_connectingPeer = _manager.Connect(target, connectionData);
if (_connectingPeer is null
|| _connectingPeer.ConnectionState != ConnectionState.Outgoing)
{
_connectingPeer = null;
Complete(RendezvousConnectionState.Rejected);
return;
}
State = RendezvousConnectionState.Connecting;
}
private void OnPeerConnected(NetPeer peer)
{
if (State != RendezvousConnectionState.Connecting
|| !ReferenceEquals(peer, _connectingPeer))
{
return;
}
ConnectedPeer = peer;
Complete(RendezvousConnectionState.Connected, peer);
}
private void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo)
{
_ = disconnectInfo;
if (State == RendezvousConnectionState.Connecting
&& ReferenceEquals(peer, _connectingPeer))
{
Complete(RendezvousConnectionState.Rejected);
}
}
private void DisconnectPendingPeer()
{
if (_connectingPeer is not null && State == RendezvousConnectionState.Connecting)
{
_connectingPeer.Disconnect();
}
}
private void Complete(RendezvousConnectionState terminalState, NetPeer? peer = null)
{
if (IsCompleted)
{
return;
}
State = terminalState;
ReleaseSubscriptions();
Completed?.Invoke(this, new(terminalState, peer));
}
private void ReleaseSubscriptions()
{
if (_subscriptionsReleased)
{
return;
}
_networkEvents.RendezvousPeerConnected -= OnPeerConnected;
_networkEvents.RendezvousPeerDisconnected -= OnPeerDisconnected;
_punchEvents.NatIntroductionSuccess -= OnNatIntroductionSuccess;
_subscriptionsReleased = true;
}
private static bool IsTerminal(RendezvousConnectionState state) => state is
RendezvousConnectionState.Connected
or RendezvousConnectionState.Cancelled
or RendezvousConnectionState.TimedOut
or RendezvousConnectionState.Rejected
or RendezvousConnectionState.ManagerStopped
or RendezvousConnectionState.Disposed;
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(RendezvousClientCoordinator));
}
}
}