Skip to content

Commit be94b4d

Browse files
committed
fixes for auth connection reconnect issues
1 parent 63add87 commit be94b4d

12 files changed

Lines changed: 307 additions & 47 deletions

File tree

Basis Server/BasisNetworkServer/Auth/Interface.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public interface IAuthIdentity
1717
public void ProcessConnection(Configuration Configuration, ConnectionRequest ConnectionRequest, NetPeer NetPeer);
1818
public void DeInitialize();
1919
public void RemoveConnection(int NetPeer);
20+
public bool RemoveConnection(int NetPeer, NetPeer Expected);
2021
public bool NetIDToUUID(NetPeer Peer, out string UUID);
2122
public bool UUIDToNetID(string UUID, out int Peer);
2223

Basis Server/BasisNetworkServer/BasisServerHandleEvents.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -347,16 +347,6 @@ public static void OnNetworkError(IPEndPoint endPoint, SocketError socketError)
347347
/// </summary>
348348
private static bool CleanupPeerSubsystems(NetPeer peer, int id)
349349
{
350-
// A predecessor's disconnect can land after a reconnect has already taken the same id.
351-
// Every teardown below is keyed by id alone, so running it for a peer that no longer
352-
// owns the slot dismantles the live peer's state instead — the "direct connect works,
353-
// then dies after a rejoin" symptom. An id held by nobody still cleans up, so a peer
354-
// rejected before auth completed keeps releasing whatever partial state it made.
355-
if (NetworkServer.AuthenticatedPeers.TryGetValue(id, out NetPeer holder) && !ReferenceEquals(holder, peer))
356-
{
357-
return false;
358-
}
359-
360350
// The auth-identity map is the primary UUID source, but it is empty when
361351
// UseAuthIdentity is off and can already be evicted on a reconnect collision. The
362352
// stored connect metadata carries the same server-computed UUID (OnNetworkAccepted
@@ -370,6 +360,19 @@ private static bool CleanupPeerSubsystems(NetPeer peer, int id)
370360
uuid = meta.playerUUID;
371361
}
372362
}
363+
364+
NetworkServer.AuthIdentity.RemoveConnection(id, peer);
365+
366+
// A predecessor's disconnect can land after a reconnect has already taken the same id.
367+
// Every teardown below is keyed by id alone, so running it for a peer that no longer
368+
// owns the slot dismantles the live peer's state instead — the "direct connect works,
369+
// then dies after a rejoin" symptom. An id held by nobody still cleans up, so a peer
370+
// rejected before auth completed keeps releasing whatever partial state it made.
371+
if (NetworkServer.AuthenticatedPeers.TryGetValue(id, out NetPeer holder) && !ReferenceEquals(holder, peer))
372+
{
373+
return false;
374+
}
375+
373376
if (!string.IsNullOrEmpty(uuid))
374377
{
375378
PermissionIntegration.RemovePlayerMeta(uuid);
@@ -378,7 +381,6 @@ private static bool CleanupPeerSubsystems(NetPeer peer, int id)
378381
BasisNetworkResourceManagement.RemovePeerResources(uuid);
379382
}
380383

381-
NetworkServer.AuthIdentity.RemoveConnection(id);
382384
BasisNetworkOwnership.RemovePlayerOwnership(id);
383385
BasisSavedState.RemovePlayer(id);
384386
BasisServerReductionSystemEvents.RemovePlayer(id);

Basis Server/BasisNetworkServer/Security/BasisDIDAuthIdentity.cs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,16 @@ public static string UnpackString(byte[] compressedBytes)
6464
return Encoding.UTF8.GetString(compressedBytes, 0, compressedBytes.Length);
6565
}
6666

67-
public struct OnAuth
67+
public struct OnAuth : IEquatable<OnAuth>
6868
{
6969
public ReadyMessage ReadyMessage;
7070
public Challenge Challenge;
7171
public Did Did;
72+
public NetPeer Peer;
73+
74+
public bool Equals(OnAuth other) => ReferenceEquals(Peer, other.Peer);
75+
public override bool Equals(object obj) => obj is OnAuth other && Equals(other);
76+
public override int GetHashCode() => Peer?.GetHashCode() ?? 0;
7277
}
7378
public int CheckForDuplicates(Did Did)
7479
{
@@ -133,6 +138,12 @@ public void ProcessConnection(Configuration Configuration, ConnectionRequest Con
133138
}
134139
return;
135140
}
141+
if (AuthIdentity.TryGetValue(newPeer.Id, out OnAuth Stale) && !ReferenceEquals(Stale.Peer, newPeer))
142+
{
143+
BNL.Log($"Auth slot {newPeer.Id} still held by a stale connection; releasing it for the incoming peer.");
144+
RemoveConnection(newPeer.Id, Stale.Peer);
145+
}
146+
136147
if (Configuration.HowManyDuplicateAuthCanExist <= CheckForDuplicates(playerDid))
137148
{
138149
BasisServerHandleEvents.RejectWithReason(newPeer, "To Many Auths From this DID!");
@@ -143,7 +154,8 @@ public void ProcessConnection(Configuration Configuration, ConnectionRequest Con
143154
{
144155
Did = playerDid,
145156
Challenge = MakeChallenge(playerDid),
146-
ReadyMessage = readyMessage
157+
ReadyMessage = readyMessage,
158+
Peer = newPeer
147159
};
148160

149161
if (AuthIdentity.TryAdd(newPeer.Id, OnAuth))
@@ -206,16 +218,12 @@ public async Task TimeOut(NetPeer newPeer, string UUID, CancellationTokenSource
206218
try
207219
{
208220
await Task.Delay(GetAuthTimeoutMs(NetworkServer.Server?.ConnectedPeersCount ?? 0), cts.Token);
209-
if (!_timeouts.ContainsKey(newPeer.Id)) return;
210-
if (AuthIdentity.TryRemove(newPeer.Id, out OnAuth TimedOut))
221+
if (!RemoveConnection(newPeer.Id, newPeer))
211222
{
212-
ReleaseDid(TimedOut.Did);
223+
return;
213224
}
214-
_timeouts.TryRemove(newPeer.Id, out _);
215-
cts.Dispose();
216225
BNL.Log($"Authentication timeout for {UUID}.");
217226
BasisServerHandleEvents.RejectWithReason(newPeer, "Authentication timeout");
218-
newPeer.Disconnect();
219227
}
220228
catch (TaskCanceledException) { }
221229
}
@@ -295,15 +303,37 @@ public async Task<bool> RecvChallengeResponse(Response response, Challenge Chall
295303

296304
public void RemoveConnection(int NetPeer)
297305
{
298-
if (AuthIdentity.TryRemove(NetPeer, out var authIdentity))
306+
RemoveConnection(NetPeer, null);
307+
}
308+
309+
public bool RemoveConnection(int Id, NetPeer Expected)
310+
{
311+
bool Removed;
312+
OnAuth Entry;
313+
if (Expected == null)
299314
{
300-
ReleaseDid(authIdentity.Did);
315+
Removed = AuthIdentity.TryRemove(Id, out Entry);
301316
}
302-
if (_timeouts.TryRemove(NetPeer, out var cts))
317+
else
318+
{
319+
Removed = AuthIdentity.TryGetValue(Id, out Entry)
320+
&& ReferenceEquals(Entry.Peer, Expected)
321+
&& ((ICollection<KeyValuePair<int, OnAuth>>)AuthIdentity)
322+
.Remove(new KeyValuePair<int, OnAuth>(Id, Entry));
323+
}
324+
325+
if (!Removed)
326+
{
327+
return false;
328+
}
329+
330+
ReleaseDid(Entry.Did);
331+
if (_timeouts.TryRemove(Id, out var cts))
303332
{
304333
try { cts.Cancel(); } catch { }
305334
cts.Dispose();
306335
}
336+
return true;
307337
}
308338
public bool IsNetPeerAdmin(string UUID)
309339
{
@@ -391,7 +421,7 @@ static string[] LoadAdmins(string filePath)
391421

392422
public bool NetIDToUUID(NetPeer Peer, out string UUID)
393423
{
394-
if (AuthIdentity.TryGetValue(Peer.Id, out OnAuth OnAuth))
424+
if (Peer != null && AuthIdentity.TryGetValue(Peer.Id, out OnAuth OnAuth) && ReferenceEquals(OnAuth.Peer, Peer))
395425
{
396426
UUID = OnAuth.Did.V;
397427
return true;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using Basis.Contrib.Auth.DecentralizedIds.Newtypes;
2+
using Basis.Network.Core;
3+
using BasisDidLink;
4+
using Xunit;
5+
using static BasisDidLink.BasisDIDAuthIdentity;
6+
7+
namespace BasisServerTests;
8+
9+
public class AuthIdentityRecycledIdTests
10+
{
11+
private static OnAuth Entry(NetPeer owner, string uuid) => new()
12+
{
13+
Did = new Did(uuid),
14+
Peer = owner,
15+
};
16+
17+
private static BasisDIDAuthIdentity NewIdentity() => new();
18+
19+
[Fact]
20+
public void RemoveConnection_ReleasesTheEntryItsOwnPeerCreated()
21+
{
22+
BasisDIDAuthIdentity identity = NewIdentity();
23+
try
24+
{
25+
int id = LifecycleSupport.NextPeerId();
26+
FakeNetPeer owner = LifecycleSupport.Peer(id);
27+
identity.AuthIdentity[id] = Entry(owner, LifecycleSupport.NewUuid());
28+
29+
Assert.True(identity.RemoveConnection(id, owner));
30+
Assert.False(identity.AuthIdentity.ContainsKey(id));
31+
Assert.False(identity.RemoveConnection(id, owner));
32+
}
33+
finally
34+
{
35+
identity.DeInitialize();
36+
}
37+
}
38+
39+
[Fact]
40+
public void RemoveConnection_LeavesAnEntryThatBelongsToAnotherConnection()
41+
{
42+
BasisDIDAuthIdentity identity = NewIdentity();
43+
try
44+
{
45+
int id = LifecycleSupport.NextPeerId();
46+
FakeNetPeer stale = LifecycleSupport.Peer(id);
47+
FakeNetPeer live = LifecycleSupport.Peer(id);
48+
identity.AuthIdentity[id] = Entry(live, LifecycleSupport.NewUuid());
49+
50+
Assert.False(identity.RemoveConnection(id, stale));
51+
Assert.True(identity.AuthIdentity.ContainsKey(id));
52+
Assert.Same(live, identity.AuthIdentity[id].Peer);
53+
}
54+
finally
55+
{
56+
identity.DeInitialize();
57+
}
58+
}
59+
60+
[Fact]
61+
public void RemoveConnection_WithoutAPeer_RemovesWhicheverEntryHoldsTheId()
62+
{
63+
BasisDIDAuthIdentity identity = NewIdentity();
64+
try
65+
{
66+
int id = LifecycleSupport.NextPeerId();
67+
identity.AuthIdentity[id] = Entry(LifecycleSupport.Peer(id), LifecycleSupport.NewUuid());
68+
69+
identity.RemoveConnection(id);
70+
Assert.False(identity.AuthIdentity.ContainsKey(id));
71+
}
72+
finally
73+
{
74+
identity.DeInitialize();
75+
}
76+
}
77+
78+
[Fact]
79+
public void NetIDToUUID_AnswersOnlyForTheConnectionThatOwnsTheEntry()
80+
{
81+
BasisDIDAuthIdentity identity = NewIdentity();
82+
try
83+
{
84+
int id = LifecycleSupport.NextPeerId();
85+
FakeNetPeer owner = LifecycleSupport.Peer(id);
86+
FakeNetPeer recycled = LifecycleSupport.Peer(id);
87+
string uuid = LifecycleSupport.NewUuid();
88+
identity.AuthIdentity[id] = Entry(owner, uuid);
89+
90+
Assert.True(identity.NetIDToUUID(owner, out string found));
91+
Assert.Equal(uuid, found);
92+
93+
Assert.False(identity.NetIDToUUID(recycled, out string leaked));
94+
Assert.Equal(string.Empty, leaked);
95+
}
96+
finally
97+
{
98+
identity.DeInitialize();
99+
}
100+
}
101+
102+
[Fact]
103+
public void AStaleTimeout_CannotEvictTheConnectionThatInheritedTheId()
104+
{
105+
BasisDIDAuthIdentity identity = NewIdentity();
106+
try
107+
{
108+
int id = LifecycleSupport.NextPeerId();
109+
FakeNetPeer timedOut = LifecycleSupport.Peer(id);
110+
FakeNetPeer inherited = LifecycleSupport.Peer(id);
111+
identity.AuthIdentity[id] = Entry(inherited, LifecycleSupport.NewUuid());
112+
113+
Assert.False(identity.RemoveConnection(id, timedOut));
114+
Assert.True(identity.AuthIdentity.ContainsKey(id));
115+
Assert.Same(inherited, identity.AuthIdentity[id].Peer);
116+
}
117+
finally
118+
{
119+
identity.DeInitialize();
120+
}
121+
}
122+
}

Basis Server/BasisServerTests/BasisConnectionLifecycleTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,4 +701,49 @@ public void StaleDisconnectAfterReconnectCollision_DoesNotEvictTheLivePeer()
701701
"the live peer was evicted by a stale peer's disconnect (key-only TryRemove)");
702702
Assert.Same(live, stored);
703703
}
704+
705+
[Fact]
706+
public void StaleDisconnectAfterReconnectCollision_StillReleasesItsOwnAuthState()
707+
{
708+
using var scope = new ServerStaticsScope();
709+
InstallServer();
710+
MapAuthIdentity identity = (MapAuthIdentity)NetworkServer.AuthIdentity;
711+
712+
int id = LifecycleSupport.NextPeerId();
713+
FakeNetPeer stale = LifecycleSupport.Peer(id);
714+
FakeNetPeer live = LifecycleSupport.Peer(id);
715+
716+
identity.Register(LifecycleSupport.NewUuid(), id, stale);
717+
718+
NetworkServer.AuthenticatedPeers[id] = live;
719+
NetworkServer.RebuildPeerSnapshot();
720+
721+
BasisServerHandleEvents.HandlePeerDisconnected(stale, Info());
722+
723+
Assert.Contains(id, identity.Released);
724+
Assert.True(NetworkServer.AuthenticatedPeers.TryGetValue(id, out NetPeer stored));
725+
Assert.Same(live, stored);
726+
}
727+
728+
[Fact]
729+
public void StaleDisconnect_DoesNotReleaseTheLivePeersAuthState()
730+
{
731+
using var scope = new ServerStaticsScope();
732+
InstallServer();
733+
MapAuthIdentity identity = (MapAuthIdentity)NetworkServer.AuthIdentity;
734+
735+
int id = LifecycleSupport.NextPeerId();
736+
FakeNetPeer stale = LifecycleSupport.Peer(id);
737+
FakeNetPeer live = LifecycleSupport.Peer(id);
738+
739+
identity.Register(LifecycleSupport.NewUuid(), id, live);
740+
NetworkServer.AuthenticatedPeers[id] = live;
741+
NetworkServer.RebuildPeerSnapshot();
742+
743+
BasisServerHandleEvents.HandlePeerDisconnected(stale, Info());
744+
745+
Assert.DoesNotContain(id, identity.Released);
746+
Assert.True(identity.NetIDToUUID(live, out string uuid) && !string.IsNullOrEmpty(uuid),
747+
"the live peer lost its identity to a stale peer's disconnect");
748+
}
704749
}

Basis Server/BasisServerTests/BasisServerTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<ItemGroup>
1717
<ProjectReference Include="..\BasisNetworkCore\BasisNetworkCore.csproj" />
1818
<ProjectReference Include="..\BasisNetworkServer\BasisNetworkServer.csproj" />
19+
<ProjectReference Include="..\Contrib\Auth\Did\Did.csproj" />
1920
<ProjectReference Include="..\Contrib\Crypto\Crypto.csproj" />
2021
<ProjectReference Include="..\LiteNetLib\LiteNetLib.csproj" />
2122
</ItemGroup>

Basis Server/BasisServerTests/ModerationAndPermissionManagerTests.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,22 @@ internal sealed class MapAuthIdentity : IAuthIdentity
6666
{
6767
private readonly ConcurrentDictionary<string, int> _uuidToId = new(StringComparer.OrdinalIgnoreCase);
6868
private readonly ConcurrentDictionary<int, string> _idToUuid = new();
69+
private readonly ConcurrentDictionary<int, NetPeer> _owner = new();
70+
71+
public readonly List<int> Released = new();
6972

7073
public void Register(string uuid, int netId)
7174
{
7275
_uuidToId[uuid] = netId;
7376
_idToUuid[netId] = uuid;
7477
}
7578

79+
public void Register(string uuid, int netId, NetPeer owner)
80+
{
81+
Register(uuid, netId);
82+
_owner[netId] = owner;
83+
}
84+
7685
public void ProcessConnection(Configuration Configuration, ConnectionRequest ConnectionRequest, NetPeer NetPeer)
7786
{
7887
}
@@ -81,7 +90,22 @@ public void DeInitialize()
8190
{
8291
}
8392

84-
public void RemoveConnection(int NetPeer) => _idToUuid.TryRemove(NetPeer, out _);
93+
public void RemoveConnection(int NetPeer) => RemoveConnection(NetPeer, null);
94+
95+
public bool RemoveConnection(int Id, NetPeer Expected)
96+
{
97+
if (Expected != null && _owner.TryGetValue(Id, out NetPeer? owner) && !ReferenceEquals(owner, Expected))
98+
{
99+
return false;
100+
}
101+
if (!_idToUuid.TryRemove(Id, out _))
102+
{
103+
return false;
104+
}
105+
_owner.TryRemove(Id, out _);
106+
lock (Released) { Released.Add(Id); }
107+
return true;
108+
}
85109

86110
public bool NetIDToUUID(NetPeer Peer, out string UUID)
87111
{

Basis Server/BasisServerTests/SecurityListAndLockManagerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ private sealed class FixedUuidAuthIdentity : IAuthIdentity
349349
public void ProcessConnection(Configuration configuration, ConnectionRequest connectionRequest, NetPeer netPeer) { }
350350
public void DeInitialize() { }
351351
public void RemoveConnection(int netPeer) { }
352+
public bool RemoveConnection(int netPeer, NetPeer expected) => false;
352353
public bool NetIDToUUID(NetPeer peer, out string uuid)
353354
{
354355
uuid = _uuid;

0 commit comments

Comments
 (0)