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.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(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 hostCompletions = []; List 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( 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 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 attempts) : IRendezvousJoinClient { public Task> CreateAsync( CreateJoinAttemptRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> CancelAsync( CreateJoinAttemptResponse attempt, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> BrowseForHostAsync( PublishedSession session, int pageSize = ContractLimits.BrowserPageMaxItems, string? cursor = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task>> 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; } }