feat: implement authenticated NAT mediator (#11)
quality-gate / quality (push) Successful in 59s
quality-gate / quality (push) Successful in 59s
Closes #11
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
using FinalFactory.Rendezvous.Server.Sessions;
|
||||
using FinalFactory.Rendezvous.Server.State;
|
||||
using FinalFactory.Rendezvous.Server.Transport;
|
||||
using FinalFactory.Rendezvous.Tests.Sessions;
|
||||
using FinalFactory.Rendezvous.Tests.State;
|
||||
using FinalFactory.Rendezvous.Tests.JoinAttempts;
|
||||
using LiteNetLib;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -13,55 +12,24 @@ namespace FinalFactory.Rendezvous.Tests.Server;
|
||||
public sealed class UdpMediatorServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void AuthenticatedHostDatagramGatesVisibilityUsingObservedGameplaySocket()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
using UdpMediatorService service = new(
|
||||
Options.Create(new UdpMediatorOptions { ListenAddress = "127.0.0.1", Port = 0 }),
|
||||
NullLogger<UdpMediatorService>.Instance,
|
||||
fixture.Store,
|
||||
fixture.Capabilities);
|
||||
PresenceDatagram presence = new()
|
||||
{
|
||||
MessageType = UdpPresenceMessageType.HostPresence,
|
||||
MediationHandle = registration.HostPresenceHandle,
|
||||
AddressFamily = AddressFamilyKind.Ipv4,
|
||||
LocalAddress = "192.168.1.50",
|
||||
LocalPort = 40_000,
|
||||
Capability = registration.HostPresenceCapability,
|
||||
};
|
||||
IPEndPoint observedGameplaySocket = new(IPAddress.Parse("203.0.113.77"), 51_234);
|
||||
presence.Capability = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
Assert.Equal(
|
||||
UdpPresenceProcessingResult.HostPresenceRejected,
|
||||
service.ProcessDatagram(RendezvousUdpCodec.Encode(presence), observedGameplaySocket));
|
||||
Assert.Empty(fixture.Browse());
|
||||
|
||||
presence.Capability = registration.HostPresenceCapability;
|
||||
Assert.Equal(
|
||||
UdpPresenceProcessingResult.HostPresenceAccepted,
|
||||
service.ProcessDatagram(RendezvousUdpCodec.Encode(presence), observedGameplaySocket));
|
||||
Assert.Equal(registration.ListingId, Assert.Single(fixture.Browse()).Definition.ListingId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceBindsAnEphemeralUdpPortAndStopsCleanly()
|
||||
public async Task ServiceBindsAnEphemeralLiteNetLibPortAndStopsCleanly()
|
||||
{
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
UdpMediatorOptions options = new()
|
||||
{
|
||||
ListenAddress = IPAddress.Loopback.ToString(),
|
||||
Port = 0,
|
||||
};
|
||||
ManualRendezvousClock clock = new();
|
||||
InMemoryEphemeralRendezvousStore store = new(new EphemeralStoreOptions(), clock, clock);
|
||||
using EphemeralCapabilityIssuer capabilities = new();
|
||||
using JoinAttemptFixture fixture = new();
|
||||
NatMediationProcessor processor = new(
|
||||
fixture.Sessions.Store,
|
||||
fixture.Sessions.Capabilities,
|
||||
fixture.Service);
|
||||
using UdpMediatorService service = new(
|
||||
Options.Create(options),
|
||||
Options.Create(new UdpMediatorOptions
|
||||
{
|
||||
ListenAddress = IPAddress.Loopback.ToString(),
|
||||
Port = 0,
|
||||
MaxDatagramsPerPoll = 8,
|
||||
PollIntervalMilliseconds = 1,
|
||||
}),
|
||||
NullLogger<UdpMediatorService>.Instance,
|
||||
store,
|
||||
capabilities);
|
||||
processor);
|
||||
|
||||
await service.StartAsync(timeout.Token);
|
||||
|
||||
@@ -69,9 +37,300 @@ public sealed class UdpMediatorServiceTests
|
||||
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<UdpMediatorService>.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<UdpMediatorService>.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<string> hostTickets = [];
|
||||
List<string> 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<IPEndPoint>(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.Equal(43, hostTicket.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<UdpMediatorService>.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<IPEndPoint>(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.");
|
||||
}
|
||||
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<UdpMediatorService>.Instance,
|
||||
processor);
|
||||
await service.StartAsync(timeout.Token);
|
||||
using UdpClient sender = new(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
IPEndPoint mediator = Assert.IsType<IPEndPoint>(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<OperationCanceledException>(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<UdpMediatorService>.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<IPEndPoint>(service.LocalEndpoint),
|
||||
timeout.Token);
|
||||
|
||||
using CancellationTokenSource noReflection = new(TimeSpan.FromMilliseconds(150));
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
|
||||
await reflectedTarget.ReceiveAsync(noReflection.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
generator.Stop();
|
||||
await service.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user