Bind CultMesh application ingress to session identity

This commit is contained in:
Meta Vens 2026-08-18 23:08:59 +02:00
commit 7a6f0be21f
5 changed files with 38 additions and 5 deletions

View file

@ -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),

View file

@ -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<string> _protocolIds;
private readonly HashSet<string> _routeGenerations;
private readonly Dictionary<string, CultMeshSessionProofSigner> _proofSigners;
private readonly ConcurrentDictionary<ICultNetSchemaServerPeer, string> _acceptedPeers = new();
private readonly Func<CultMeshSessionOpenMessage, ICultNetSchemaServerPeer, Task> _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;
}
/// <summary>
/// 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.
/// </summary>
public bool TryGetSourceRuntimeId(ICultNetSchemaServerPeer peer, out string sourceRuntimeId) =>
_acceptedPeers.TryGetValue(peer, out sourceRuntimeId!);
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_server.RemoveCultNetMessageListener<CultMeshSessionOpenMessage>(_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";

View file

@ -29,7 +29,9 @@ public sealed class CultMeshTransportModularityTests
new[] { RouteGeneration });
server.OnCultNet<CultNetHelloMessage>((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");
}

View file

@ -15,7 +15,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NUnit" Version="4.*" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="FluentAssertions" Version="8.9.0" />

View file

@ -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",