273 lines
12 KiB
C#
273 lines
12 KiB
C#
using System.Net;
|
|
using FinalFactory.Rendezvous.Client;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.JoinAttempts;
|
|
using FinalFactory.Rendezvous.Server.State;
|
|
using FinalFactory.Rendezvous.Server.Transport;
|
|
using FinalFactory.Rendezvous.Tests.JoinAttempts;
|
|
using LiteNetLib;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace FinalFactory.Rendezvous.Tests.Client;
|
|
|
|
public sealed class RendezvousCoordinatorIntegrationTests
|
|
{
|
|
[Fact]
|
|
public async Task CallerOwnedManagersCompleteAuthenticatedDirectConnectionAndRejectTicketReplay()
|
|
{
|
|
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(8));
|
|
using JoinAttemptFixture fixture = new();
|
|
(RegisterSessionResponse registration, _) = fixture.CreateHost();
|
|
CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "sdk-direct-connect");
|
|
HostJoinAttempt hostAttempt = fixture.Service.BrowseForHost(
|
|
registration.ListingId,
|
|
ContractLimits.ContractVersion,
|
|
registration.LeaseToken,
|
|
ContractLimits.BrowserPageMaxItems,
|
|
null).Value!.Items.Single(item => item.AttemptId == created.AttemptId);
|
|
NatMediationProcessor processor = new(
|
|
fixture.Sessions.Store,
|
|
fixture.Sessions.Capabilities,
|
|
fixture.Service);
|
|
using UdpMediatorService mediatorService = new(
|
|
Options.Create(new UdpMediatorOptions
|
|
{
|
|
ListenAddress = IPAddress.Loopback.ToString(),
|
|
Port = 0,
|
|
PollIntervalMilliseconds = 1,
|
|
}),
|
|
NullLogger<UdpMediatorService>.Instance,
|
|
processor);
|
|
await mediatorService.StartAsync(timeout.Token);
|
|
|
|
RendezvousNetListener hostEvents = new();
|
|
RendezvousNetListener clientEvents = new();
|
|
bool gameplayConnectionRequestHandled = false;
|
|
hostEvents.GameplayEvents.ConnectionRequestEvent += _ =>
|
|
gameplayConnectionRequestHandled = true;
|
|
EventBasedNatPunchListener hostPunch = hostEvents.PunchEvents;
|
|
EventBasedNatPunchListener clientPunch = clientEvents.PunchEvents;
|
|
NetManager hostManager = hostEvents.CreateManager();
|
|
NetManager clientManager = clientEvents.CreateManager();
|
|
string? introductionToken = null;
|
|
string? hostIntroductionToken = null;
|
|
clientPunch.NatIntroductionSuccess += (_, _, token) => introductionToken = token;
|
|
hostPunch.NatIntroductionSuccess += (_, _, token) => hostIntroductionToken = token;
|
|
|
|
try
|
|
{
|
|
Assert.True(hostManager.Start(0));
|
|
Assert.True(clientManager.Start(0));
|
|
IPEndPoint mediator = Assert.IsType<IPEndPoint>(mediatorService.LocalEndpoint);
|
|
FakeJoinClient joinClient = new([hostAttempt]);
|
|
FixedCoordinatorClock clock = new(fixture.Sessions.Clock.UtcNow);
|
|
using ConnectionTicketValidator tickets = new(1_024, clock);
|
|
using RendezvousHostCoordinator host = new(
|
|
hostManager,
|
|
hostEvents,
|
|
mediator,
|
|
new PublishedSession(registration),
|
|
joinClient,
|
|
FastOptions(),
|
|
clock,
|
|
tickets);
|
|
using RendezvousClientCoordinator client = new(
|
|
clientManager,
|
|
clientEvents,
|
|
mediator,
|
|
created,
|
|
FastOptions(),
|
|
clock);
|
|
List<RendezvousHostAttemptCompletedEventArgs> hostCompletions = [];
|
|
List<RendezvousConnectionCompletedEventArgs> clientCompletions = [];
|
|
host.AttemptCompleted += (_, completion) => hostCompletions.Add(completion);
|
|
client.Completed += (_, completion) => clientCompletions.Add(completion);
|
|
Assert.True((await host.RefreshJoinAttemptsAsync(timeout.Token)).IsSuccess);
|
|
|
|
while ((!client.IsCompleted || hostCompletions.Count == 0)
|
|
&& !timeout.IsCancellationRequested)
|
|
{
|
|
host.Poll();
|
|
client.Poll();
|
|
await Task.Delay(2);
|
|
}
|
|
|
|
Assert.True(
|
|
client.State == RendezvousConnectionState.Connected,
|
|
$"Client ended in {client.State}; host pending={host.PendingAttemptCount}; "
|
|
+ $"host completions={hostCompletions.Count}; introduction={introductionToken is not null}; "
|
|
+ $"host introduction={hostIntroductionToken is not null}; "
|
|
+ $"client digest={NatIntroductionTokenCodec.MatchesDigest(introductionToken, created.ConnectionTicketDigest)}; "
|
|
+ $"host digest={NatIntroductionTokenCodec.MatchesDigest(hostIntroductionToken, hostAttempt.ConnectionTicketDigest)}.");
|
|
Assert.NotNull(client.ConnectedPeer);
|
|
Assert.Equal(
|
|
RendezvousConnectionState.Connected,
|
|
Assert.Single(clientCompletions).State);
|
|
RendezvousHostAttemptCompletedEventArgs hostCompletion = Assert.Single(hostCompletions);
|
|
Assert.Equal(created.AttemptId, hostCompletion.AttemptId);
|
|
Assert.Equal(RendezvousConnectionState.Connected, hostCompletion.State);
|
|
Assert.NotNull(hostCompletion.Peer);
|
|
Assert.False(gameplayConnectionRequestHandled);
|
|
Assert.True(NatIntroductionTokenCodec.TryDecode(
|
|
introductionToken,
|
|
out NatIntroductionToken? introduction));
|
|
Assert.NotNull(introduction);
|
|
|
|
EventBasedNetListener replayEvents = new();
|
|
bool replayConnected = false;
|
|
replayEvents.PeerConnectedEvent += _ => replayConnected = true;
|
|
NetManager replayManager = new(replayEvents);
|
|
try
|
|
{
|
|
Assert.True(replayManager.Start(0));
|
|
replayManager.Connect(
|
|
new IPEndPoint(IPAddress.Loopback, hostManager.LocalPort),
|
|
DirectConnectionRequestCodec.Encode(
|
|
created.AttemptId,
|
|
introduction.ConnectionTicket));
|
|
DateTime replayDeadline = DateTime.UtcNow.AddSeconds(1);
|
|
while (DateTime.UtcNow < replayDeadline && !replayConnected)
|
|
{
|
|
host.Poll();
|
|
replayManager.PollEvents();
|
|
await Task.Delay(2, timeout.Token);
|
|
}
|
|
|
|
Assert.False(replayConnected);
|
|
Assert.False(gameplayConnectionRequestHandled);
|
|
Assert.Single(hostCompletions);
|
|
}
|
|
finally
|
|
{
|
|
replayManager.Stop();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
hostManager.Stop();
|
|
clientManager.Stop();
|
|
await mediatorService.StopAsync(CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HostDefersADirectRequestUntilTheMatchingNatIntroductionArrives()
|
|
{
|
|
using JoinAttemptFixture fixture = new();
|
|
(RegisterSessionResponse registration, _) = fixture.CreateHost();
|
|
CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "direct-before-nat");
|
|
HostJoinAttempt hostAttempt = fixture.Service.BrowseForHost(
|
|
registration.ListingId,
|
|
ContractLimits.ContractVersion,
|
|
registration.LeaseToken,
|
|
ContractLimits.BrowserPageMaxItems,
|
|
null).Value!.Items.Single(item => item.AttemptId == created.AttemptId);
|
|
IntroductionEndpoints introduction = fixture.Introduce(registration, created);
|
|
ConnectionTicketGrant grant = Assert.IsType<ConnectionTicketGrant>(
|
|
fixture.Service.IssueConnectionTicket(introduction.Attempt).Value);
|
|
RendezvousNetListener hostEvents = new();
|
|
EventBasedNatPunchListener hostPunch = hostEvents.PunchEvents;
|
|
EventBasedNetListener clientEvents = new();
|
|
bool clientConnected = false;
|
|
clientEvents.PeerConnectedEvent += _ => clientConnected = true;
|
|
NetManager hostManager = hostEvents.CreateManager();
|
|
NetManager clientManager = new(clientEvents);
|
|
try
|
|
{
|
|
Assert.True(hostManager.Start(0));
|
|
Assert.True(clientManager.Start(0));
|
|
FixedCoordinatorClock clock = new(fixture.Sessions.Clock.UtcNow);
|
|
FakeJoinClient joins = new([hostAttempt]);
|
|
using ConnectionTicketValidator tickets = new(16, clock);
|
|
using RendezvousHostCoordinator host = new(
|
|
hostManager,
|
|
hostEvents,
|
|
new IPEndPoint(IPAddress.Loopback, 65_000),
|
|
new PublishedSession(registration),
|
|
joins,
|
|
FastOptions(),
|
|
clock,
|
|
tickets);
|
|
List<RendezvousHostAttemptCompletedEventArgs> completions = [];
|
|
host.AttemptCompleted += (_, completion) => completions.Add(completion);
|
|
Assert.True((await host.RefreshJoinAttemptsAsync()).IsSuccess);
|
|
host.Poll();
|
|
bool observedDeferredRequest = false;
|
|
hostEvents.RendezvousConnectionRequest += _ =>
|
|
{
|
|
observedDeferredRequest = host.DeferredRequestCount == 1;
|
|
((INatPunchListener)hostPunch).OnNatIntroductionSuccess(
|
|
new IPEndPoint(IPAddress.Loopback, clientManager.LocalPort),
|
|
NatAddressType.External,
|
|
grant.Ticket);
|
|
};
|
|
|
|
clientManager.Connect(
|
|
new IPEndPoint(IPAddress.Loopback, hostManager.LocalPort),
|
|
DirectConnectionRequestCodec.Encode(created.AttemptId, grant.Ticket));
|
|
DateTime connectedDeadline = DateTime.UtcNow.AddSeconds(1);
|
|
while ((!clientConnected || completions.Count == 0)
|
|
&& DateTime.UtcNow < connectedDeadline)
|
|
{
|
|
host.Poll();
|
|
clientManager.PollEvents();
|
|
await Task.Delay(2);
|
|
}
|
|
|
|
Assert.True(
|
|
clientConnected,
|
|
$"Deferred observed={observedDeferredRequest}; deferred={host.DeferredRequestCount}; "
|
|
+ $"pending={host.PendingAttemptCount}; completions={completions.Count}.");
|
|
Assert.True(observedDeferredRequest);
|
|
Assert.Equal(0, host.DeferredRequestCount);
|
|
Assert.Equal(
|
|
RendezvousConnectionState.Connected,
|
|
Assert.Single(completions).State);
|
|
}
|
|
finally
|
|
{
|
|
hostManager.Stop();
|
|
clientManager.Stop();
|
|
}
|
|
}
|
|
|
|
private static RendezvousCoordinatorOptions FastOptions() => new()
|
|
{
|
|
MaximumPunchRequests = 20,
|
|
InitialPunchRetryDelay = TimeSpan.FromMilliseconds(10),
|
|
MaximumPunchRetryDelay = TimeSpan.FromMilliseconds(100),
|
|
JitterRatio = 0,
|
|
};
|
|
|
|
private sealed class FakeJoinClient(IReadOnlyList<HostJoinAttempt> attempts) : IRendezvousJoinClient
|
|
{
|
|
public Task<RendezvousClientResult<CreateJoinAttemptResponse>> CreateAsync(
|
|
CreateJoinAttemptRequest request,
|
|
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
|
|
public Task<RendezvousClientResult<bool>> CancelAsync(
|
|
CreateJoinAttemptResponse attempt,
|
|
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
|
|
public Task<RendezvousClientResult<BrowseHostJoinAttemptsResponse>> BrowseForHostAsync(
|
|
PublishedSession session,
|
|
int pageSize = ContractLimits.BrowserPageMaxItems,
|
|
string? cursor = null,
|
|
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
|
|
public Task<RendezvousClientResult<IReadOnlyList<HostJoinAttempt>>> BrowseAllForHostAsync(
|
|
PublishedSession session,
|
|
int maximumPages = 100,
|
|
CancellationToken cancellationToken = default) => Task.FromResult(
|
|
RendezvousClientResult.Success(attempts));
|
|
}
|
|
|
|
private sealed class FixedCoordinatorClock(DateTimeOffset now) :
|
|
IRendezvousCoordinatorClock,
|
|
IConnectionTicketClock
|
|
{
|
|
public DateTimeOffset UtcNow { get; } = now;
|
|
}
|
|
}
|