using System.Net; using System.Net.Sockets; using FinalFactory.Rendezvous.Contracts; 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.Server; public sealed class UdpMediatorServiceTests { [Fact] public async Task ServiceBindsAnEphemeralLiteNetLibPortAndStopsCleanly() { using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); NatMediationProcessor processor = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, fixture.Service); using UdpMediatorService service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Port = 0, MaxDatagramsPerPoll = 8, PollIntervalMilliseconds = 1, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); IPEndPoint? boundEndpoint = service.LocalEndpoint; Assert.NotNull(boundEndpoint); Assert.Equal(IPAddress.Loopback, boundEndpoint.Address); Assert.InRange(boundEndpoint.Port, 1, 65_535); Assert.Null(service.LocalIpv6Endpoint); await service.StopAsync(timeout.Token); Assert.Null(service.LocalEndpoint); } [Fact] public async Task OptionalIpv6BindingNeverWidensTheRequiredIpv4Binding() { if (!Socket.OSSupportsIPv6) { return; } using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); NatMediationProcessor processor = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, fixture.Service); using UdpMediatorService service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Ipv6ListenAddress = IPAddress.IPv6Loopback.ToString(), Port = 0, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); Assert.Equal(IPAddress.Loopback, service.LocalEndpoint!.Address); Assert.Equal(IPAddress.IPv6Loopback, service.LocalIpv6Endpoint!.Address); Assert.Equal(service.LocalEndpoint.Port, service.LocalIpv6Endpoint.Port); IPAddress? otherIpv4 = Dns.GetHostAddresses(Dns.GetHostName()) .FirstOrDefault(address => address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(address)); if (otherIpv4 is not null) { using UdpClient scopeProbe = new(new IPEndPoint(otherIpv4, service.LocalEndpoint.Port)); Assert.Equal(otherIpv4, ((IPEndPoint)scopeProbe.Client.LocalEndPoint!).Address); } await service.StopAsync(timeout.Token); } [Fact] public async Task NativeLiteNetLibRequestsIntroduceTheAuthorizedPair() { using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "native-litenet"); 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 service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Port = 0, MaxDatagramsPerPoll = 8, PollIntervalMilliseconds = 1, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); EventBasedNetListener hostListener = new(); EventBasedNetListener clientListener = new(); NetManager host = new(hostListener) { NatPunchEnabled = true }; NetManager client = new(clientListener) { NatPunchEnabled = true }; EventBasedNatPunchListener hostPunch = new(); EventBasedNatPunchListener clientPunch = new(); List hostTickets = []; List clientTickets = []; hostPunch.NatIntroductionSuccess += (_, _, ticket) => hostTickets.Add(ticket); clientPunch.NatIntroductionSuccess += (_, _, ticket) => clientTickets.Add(ticket); host.NatPunchModule.Init(hostPunch); client.NatPunchModule.Init(clientPunch); try { Assert.True(host.Start(0)); Assert.True(client.Start(0)); IPEndPoint mediator = Assert.IsType(service.LocalEndpoint); host.NatPunchModule.SendNatIntroduceRequest( mediator, NatPunchRequestTokenCodec.Encode( NatPunchPeerRole.Host, created.MediationHandle, hostAttempt.HostPunchCapability)); client.NatPunchModule.SendNatIntroduceRequest( mediator, NatPunchRequestTokenCodec.Encode( NatPunchPeerRole.Client, created.MediationHandle, created.ClientPunchCapability)); while ((hostTickets.Count == 0 || clientTickets.Count == 0) && !timeout.IsCancellationRequested) { host.PollEvents(); host.NatPunchModule.PollEvents(); client.PollEvents(); client.NatPunchModule.PollEvents(); await Task.Delay(5, timeout.Token); } string hostTicket = Assert.Single(hostTickets.Distinct(StringComparer.Ordinal)); string clientTicket = Assert.Single(clientTickets.Distinct(StringComparer.Ordinal)); Assert.Equal(hostTicket, clientTicket); Assert.True(NatIntroductionTokenCodec.TryDecode( hostTicket, out NatIntroductionToken? introduction)); Assert.NotNull(introduction); Assert.Equal(created.AttemptId, introduction.AttemptId); Assert.Equal(43, introduction.ConnectionTicket.Length); } finally { host.Stop(); client.Stop(); await service.StopAsync(CancellationToken.None); } } [Fact] public async Task FrozenV1EnvelopeIsConsumedOnTheLiteNetSocketWithinAmplificationBudget() { using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "v1-envelope"); 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 service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Port = 0, MaxDatagramsPerPoll = 8, PollIntervalMilliseconds = 1, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); using UdpClient host = new(new IPEndPoint(IPAddress.Loopback, 0)); using UdpClient client = new(new IPEndPoint(IPAddress.Loopback, 0)); IPEndPoint mediator = Assert.IsType(service.LocalEndpoint); byte[] hostDatagram = RendezvousUdpCodec.Encode(new PresenceDatagram { MessageType = UdpPresenceMessageType.HostPresence, MediationHandle = created.MediationHandle, AddressFamily = AddressFamilyKind.Ipv4, LocalAddress = "192.168.1.10", LocalPort = 41_000, Capability = hostAttempt.HostPunchCapability, }); byte[] clientDatagram = RendezvousUdpCodec.Encode(new PresenceDatagram { MessageType = UdpPresenceMessageType.ClientPresence, MediationHandle = created.MediationHandle, AddressFamily = AddressFamilyKind.Ipv4, LocalAddress = "192.168.1.11", LocalPort = 42_000, Capability = created.ClientPunchCapability, }); try { await host.SendAsync(hostDatagram, mediator, timeout.Token); await client.SendAsync(clientDatagram, mediator, timeout.Token); UdpReceiveResult hostIntroduction = await host.ReceiveAsync(timeout.Token); UdpReceiveResult clientIntroduction = await client.ReceiveAsync(timeout.Token); Assert.True( hostIntroduction.Buffer.Length + clientIntroduction.Buffer.Length <= clientDatagram.Length * 2, $"The completing authenticated contribution exceeded the 2.0 response-byte budget: " + $"responses={hostIntroduction.Buffer.Length + clientIntroduction.Buffer.Length}, " + $"request={clientDatagram.Length}."); } finally { await service.StopAsync(CancellationToken.None); } } [Fact] public async Task OversizedMalformedAndGameplayDatagramsReceiveNoResponse() { using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); NatMediationProcessor processor = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, fixture.Service); using UdpMediatorService service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Port = 0, MaxDatagramsPerPoll = 8, PollIntervalMilliseconds = 1, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); using UdpClient sender = new(new IPEndPoint(IPAddress.Loopback, 0)); IPEndPoint mediator = Assert.IsType(service.LocalEndpoint); byte[] oversized = new byte[ContractLimits.UdpDatagramMaxBytes + 1]; oversized[0] = RendezvousUdpCodec.MagicFirst; oversized[1] = RendezvousUdpCodec.MagicSecond; byte[] gameplayPayload = [0x01, 0x02, 0x03, 0x04]; byte[] malformedNative = [17, 0]; try { await sender.SendAsync(oversized, mediator, timeout.Token); await sender.SendAsync(gameplayPayload, mediator, timeout.Token); await sender.SendAsync(malformedNative, mediator, timeout.Token); using CancellationTokenSource noResponse = new(TimeSpan.FromMilliseconds(150)); await Assert.ThrowsAnyAsync(async () => await sender.ReceiveAsync(noResponse.Token)); } finally { await service.StopAsync(CancellationToken.None); } } [Fact] public async Task ForgedNativeIntroductionResponseCannotReflectToPayloadEndpoint() { using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); using JoinAttemptFixture fixture = new(); NatMediationProcessor processor = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, fixture.Service); using UdpMediatorService service = new( Options.Create(new UdpMediatorOptions { ListenAddress = IPAddress.Loopback.ToString(), Port = 0, MaxDatagramsPerPoll = 8, PollIntervalMilliseconds = 1, }), NullLogger.Instance, processor); await service.StartAsync(timeout.Token); using UdpClient reflectedTarget = new(new IPEndPoint(IPAddress.Loopback, 0)); using UdpClient responseCapture = new(new IPEndPoint(IPAddress.Loopback, 0)); using UdpClient attacker = new(new IPEndPoint(IPAddress.Loopback, 0)); LiteNetManager generator = new(new EventBasedLiteNetListener()) { NatPunchEnabled = true }; try { Assert.True(generator.Start(0)); IPEndPoint target = (IPEndPoint)reflectedTarget.Client.LocalEndPoint!; IPEndPoint capture = (IPEndPoint)responseCapture.Client.LocalEndPoint!; generator.NatPunchModule.NatIntroduce( target, new IPEndPoint(IPAddress.Loopback, 9), capture, capture, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); byte[] forgedResponse = (await responseCapture.ReceiveAsync(timeout.Token)).Buffer; await attacker.SendAsync( forgedResponse, Assert.IsType(service.LocalEndpoint), timeout.Token); using CancellationTokenSource noReflection = new(TimeSpan.FromMilliseconds(150)); await Assert.ThrowsAnyAsync(async () => await reflectedTarget.ReceiveAsync(noReflection.Token)); } finally { generator.Stop(); await service.StopAsync(CancellationToken.None); } } }