diff --git a/src/GameCult.Mesh/CultMeshClient.cs b/src/GameCult.Mesh/CultMeshClient.cs index 230ce55b..5db03ca5 100644 --- a/src/GameCult.Mesh/CultMeshClient.cs +++ b/src/GameCult.Mesh/CultMeshClient.cs @@ -587,7 +587,7 @@ namespace GameCult.Mesh using var snapshot = await CultMeshSnapshotSession.ConnectAsync( _sessions, target, - CultMeshProtocols.Content, + CultMeshProtocols.Documents, new CultMeshSnapshotRequestOptions { ConnectTimeout = TimeSpan.FromSeconds(5), diff --git a/src/GameCult.Mesh/CultMeshSessionIdentity.cs b/src/GameCult.Mesh/CultMeshSessionIdentity.cs index 43a49162..3fad99f9 100644 --- a/src/GameCult.Mesh/CultMeshSessionIdentity.cs +++ b/src/GameCult.Mesh/CultMeshSessionIdentity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Concurrent; using System.Linq; using System.Security.Cryptography; using System.Threading; @@ -70,7 +71,9 @@ namespace GameCult.Mesh private readonly HashSet _protocolIds; private readonly HashSet _routeGenerations; private readonly Dictionary _proofSigners; + private readonly ConcurrentDictionary _acceptedPeers = new(); private readonly Func _handler; + private readonly ICultNetSchemaServerPeerLifecycle? _peerLifecycle; private bool _disposed; public CultMeshSessionIdentityServer( @@ -90,18 +93,33 @@ namespace GameCult.Mesh .ToDictionary(signer => signer.Route.Generation, StringComparer.Ordinal); _handler = HandleAsync; _server.OnCultNet(_handler); + _peerLifecycle = server as ICultNetSchemaServerPeerLifecycle; + if (_peerLifecycle != null) + _peerLifecycle.PeerDisconnected += ForgetPeer; } + /// + /// Resolves the runtime identity established by this peer's accepted session-open handshake. + /// Application ingress must use this transport-bound value instead of trusting payload identity fields. + /// + public bool TryGetSourceRuntimeId(ICultNetSchemaServerPeer peer, out string sourceRuntimeId) => + _acceptedPeers.TryGetValue(peer, out sourceRuntimeId!); + public void Dispose() { if (_disposed) return; _disposed = true; _server.RemoveCultNetMessageListener(_handler); + if (_peerLifecycle != null) + _peerLifecycle.PeerDisconnected -= ForgetPeer; + _acceptedPeers.Clear(); } private Task HandleAsync(CultMeshSessionOpenMessage request, ICultNetSchemaServerPeer peer) { var error = Validate(request); + if (error == null) + error = BindPeer(peer, request.SourceRuntimeId); _proofSigners.TryGetValue(request.RouteGeneration ?? string.Empty, out var signer); peer.SendCultNet(new CultMeshSessionAcceptedMessage { @@ -119,6 +137,19 @@ namespace GameCult.Mesh return Task.CompletedTask; } + private void ForgetPeer(ICultNetSchemaServerPeer peer) => _acceptedPeers.TryRemove(peer, out _); + + private string? BindPeer(ICultNetSchemaServerPeer peer, string sourceRuntimeId) + { + if (_acceptedPeers.TryGetValue(peer, out var established)) + return string.Equals(established, sourceRuntimeId, StringComparison.Ordinal) + ? null + : "session-source-runtime-rebind-forbidden"; + return _acceptedPeers.TryAdd(peer, sourceRuntimeId) + ? null + : BindPeer(peer, sourceRuntimeId); + } + private string? Validate(CultMeshSessionOpenMessage request) { if (string.IsNullOrWhiteSpace(request.MessageId)) return "session-message-id-required"; diff --git a/tests/GameCult.Mesh.Tests/CultMeshTransportModularityTests.cs b/tests/GameCult.Mesh.Tests/CultMeshTransportModularityTests.cs index 979c7230..e5d3867e 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshTransportModularityTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshTransportModularityTests.cs @@ -29,7 +29,9 @@ public sealed class CultMeshTransportModularityTests new[] { RouteGeneration }); server.OnCultNet((message, peer) => { - peer.SendCultNet(new CultNetErrorMessage { Error = "tcp:" + message.RuntimeId }); + identity.TryGetSourceRuntimeId(peer, out var sourceRuntimeId).Should().BeTrue( + "application ingress must only trust a runtime identity bound by the session-open handshake"); + peer.SendCultNet(new CultNetErrorMessage { Error = $"tcp:{message.RuntimeId}:{sourceRuntimeId}" }); return Task.CompletedTask; }); var tcpEndpoint = $"cultnet+tcp://127.0.0.1:{server.LocalEndPoint.Port}"; @@ -55,7 +57,7 @@ public sealed class CultMeshTransportModularityTests var received = await response.Task.WaitAsync(TimeSpan.FromSeconds(5)); session.State.Path!.Endpoint.Should().Be(tcpEndpoint); - received.Error.Should().Be("tcp:eve-unity"); + received.Error.Should().Be("tcp:eve-unity:cultmesh-client"); legacy.ConnectCount.Should().Be(0, "a lower transport tier is fallback, not a race against the preferred TCP tier"); } diff --git a/tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj b/tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj index 14e99d63..292e42df 100644 --- a/tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj +++ b/tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json index b957b18c..cae02b50 100644 --- a/unity/org.gamecult.cultlib/package.json +++ b/unity/org.gamecult.cultlib/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.cultlib", "displayName": "CultLib", - "version": "1.0.49", + "version": "1.0.50", "unity": "2021.3", "description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.", "license": "MIT",