feat(client): add rendezvous traversal coordinators (#12)
quality-gate / quality (push) Successful in 56s

This commit is contained in:
KyuubiYoru
2026-07-16 08:39:05 +02:00
parent 6d076c281a
commit b4b6072fe1
28 changed files with 2949 additions and 57 deletions
@@ -66,7 +66,15 @@ internal sealed class JoinAttemptService(
string derivationSalt = capabilities.CreateDerivationSalt();
string hostCapability = Derive("join-host-punch", clientSubject, request, requestFingerprint, derivationSalt);
string clientCapability = Derive("join-client-punch", clientSubject, request, requestFingerprint, derivationSalt);
string connectionTicket = Derive("connection-ticket", clientSubject, request, requestFingerprint, derivationSalt);
JoinAttemptId attemptId = new(capabilities.DeriveGuid(
"join-attempt-id",
clientSubject,
request.IdempotencyKey,
requestFingerprint,
derivationSalt));
string connectionTicket = NatIntroductionTokenCodec.Encode(
attemptId,
Derive("connection-ticket", clientSubject, request, requestFingerprint, derivationSalt));
if (!CredentialLengthsAreValid(hostCapability, clientCapability, connectionTicket)
|| !capabilities.TryFingerprint(hostCapability, out SecretFingerprint hostFingerprint)
|| !capabilities.TryFingerprint(clientCapability, out SecretFingerprint clientFingerprint)
@@ -75,12 +83,6 @@ internal sealed class JoinAttemptService(
throw new InvalidOperationException("Derived join credentials violated their contract invariants.");
}
JoinAttemptId attemptId = new(capabilities.DeriveGuid(
"join-attempt-id",
clientSubject,
request.IdempotencyKey,
requestFingerprint,
derivationSalt));
MediationHandle mediationHandle = new(capabilities.DeriveGuid(
"join-mediation-handle",
clientSubject,
@@ -126,6 +128,8 @@ internal sealed class JoinAttemptService(
AttemptId = persisted.AttemptId,
MediationHandle = persisted.MediationHandle,
ClientPunchCapability = clientCapability,
ConnectionTicketDigest = NatIntroductionTokenCodec.ComputeDigest(
CreateConnectionTicket(persisted)),
ExpiresAt = persisted.ExpiresAt,
});
}
@@ -203,7 +207,7 @@ internal sealed class JoinAttemptService(
StoredJoinAttempt attempt)
{
ArgumentNullException.ThrowIfNull(attempt);
if (!attempt.IntroductionConsumed)
if (!attempt.IntroductionConsumed || attempt.IsCancelled)
{
return new(RendezvousErrorCode.Conflict);
}
@@ -213,12 +217,7 @@ internal sealed class JoinAttemptService(
return new(RendezvousErrorCode.Expired);
}
string ticket = Derive(
"connection-ticket",
attempt.ClientSubject,
attempt.IdempotencyKey,
attempt.RequestFingerprint,
attempt.CapabilityDerivationSalt);
string ticket = CreateConnectionTicket(attempt);
if (!ContractValidation.IsConnectionTicketValid(ticket)
|| !capabilities.TryFingerprint(ticket, out SecretFingerprint fingerprint)
|| fingerprint != attempt.ConnectionTicketFingerprint)
@@ -249,10 +248,23 @@ internal sealed class JoinAttemptService(
AttemptId = attempt.AttemptId,
MediationHandle = attempt.MediationHandle,
HostPunchCapability = capability,
ConnectionTicketDigest = NatIntroductionTokenCodec.ComputeDigest(
CreateConnectionTicket(attempt)),
IsCancelled = attempt.IsCancelled,
ExpiresAt = attempt.ExpiresAt,
};
}
private string CreateConnectionTicket(StoredJoinAttempt attempt) =>
NatIntroductionTokenCodec.Encode(
attempt.AttemptId,
Derive(
"connection-ticket",
attempt.ClientSubject,
attempt.IdempotencyKey,
attempt.RequestFingerprint,
attempt.CapabilityDerivationSalt));
private static RendezvousErrorCode ValidateCreate(CreateJoinAttemptRequest request)
{
RendezvousErrorCode version = ContractValidation.ValidateContractVersion(request.ContractVersion);
@@ -286,6 +286,7 @@ internal sealed record StoredJoinAttempt
public AttemptEndpointBinding? ClientEndpoint { get; init; }
public required bool IntroductionConsumed { get; init; }
public required bool ConnectionTicketConsumed { get; init; }
public required bool IsCancelled { get; init; }
public override string ToString() => $"[StoredJoinAttempt {AttemptId}; credentials redacted]";
}
@@ -420,7 +420,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
IReadOnlyList<StoredJoinAttempt> attempts = _attempts.Values
.Where(entry => entry.Command.ListingId == query.ListingId
&& !entry.IntroductionConsumed
&& (!entry.IntroductionConsumed || entry.IsCancelled)
&& (!query.AfterAttemptId.HasValue
|| entry.Command.AttemptId.Value.CompareTo(query.AfterAttemptId.Value.Value) > 0))
.OrderBy(static entry => entry.Command.AttemptId.Value)
@@ -451,12 +451,12 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return new(StoreResultCode.NotFound);
}
if (attempt.IntroductionConsumed)
if (attempt.IsCancelled)
{
return new(StoreResultCode.Conflict);
return new(StoreResultCode.Success, true, true);
}
RemoveAttempt(command.AttemptId);
attempt.IsCancelled = true;
return new(StoreResultCode.Success, true);
}, cancellationToken);
@@ -480,7 +480,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
if (!_attemptHandles.TryGetValue(command.Handle, out JoinAttemptId attemptId)
|| !_attempts.TryGetValue(attemptId, out AttemptEntry? attempt)
|| attempt.Deadline <= now)
|| attempt.Deadline <= now
|| attempt.IsCancelled)
{
return new(StoreResultCode.NotFound);
}
@@ -532,7 +533,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
if (!_attemptHandles.TryGetValue(handle, out JoinAttemptId attemptId)
|| !_attempts.TryGetValue(attemptId, out AttemptEntry? attempt)
|| attempt.Deadline <= now)
|| attempt.Deadline <= now
|| attempt.IsCancelled)
{
return new(StoreResultCode.NotFound);
}
@@ -590,6 +592,11 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return new(StoreResultCode.Conflict);
}
if (attempt.IsCancelled)
{
return new(StoreResultCode.Conflict);
}
if (!attempt.TicketDeadline.HasValue || attempt.TicketDeadline.Value <= now)
{
return new(StoreResultCode.Expired);
@@ -874,6 +881,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
ClientEndpoint = entry.ClientEndpoint,
IntroductionConsumed = entry.IntroductionConsumed,
ConnectionTicketConsumed = entry.ConnectionTicketConsumed,
IsCancelled = entry.IsCancelled,
};
private static void RemoveExpired(Dictionary<string, TimeSpan> entries, TimeSpan now)
@@ -1020,6 +1028,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
public AttemptEndpointBinding? ClientEndpoint { get; set; }
public bool IntroductionConsumed { get; set; }
public bool ConnectionTicketConsumed { get; set; }
public bool IsCancelled { get; set; }
}
private sealed record IdempotencyEntry(
@@ -17,7 +17,7 @@ internal sealed record NatIntroductionPlan(
IPEndPoint HostPublic,
IPEndPoint ClientLocal,
IPEndPoint ClientPublic,
string ConnectionTicket)
string IntroductionToken)
{
public override string ToString() => "[NatIntroductionPlan: endpoints and ticket redacted]";
}
@@ -185,7 +185,7 @@ internal sealed class NatMediationProcessor(
try
{
introductionSink.Introduce(CreatePlan(consumed.Value, ticket.Value.Ticket));
introductionSink.Introduce(CreatePlan(consumed.Value, ticket.Value));
return NatMediationResult.Introduced;
}
catch (Exception exception) when (exception is SocketException
@@ -198,7 +198,7 @@ internal sealed class NatMediationProcessor(
private static NatIntroductionPlan CreatePlan(
IntroductionEndpoints endpoints,
string connectionTicket)
ConnectionTicketGrant ticket)
{
IPEndPoint hostPublic = ToIpEndpoint(endpoints.Host.PublicEndpoint);
IPEndPoint clientPublic = ToIpEndpoint(endpoints.Client.PublicEndpoint);
@@ -209,7 +209,12 @@ internal sealed class NatMediationProcessor(
IPEndPoint clientLocal = sameNat && endpoints.Client.LocalEndpoint is { } clientCandidate
? ToIpEndpoint(clientCandidate)
: clientPublic;
return new(hostLocal, hostPublic, clientLocal, clientPublic, connectionTicket);
return new(
hostLocal,
hostPublic,
clientLocal,
clientPublic,
ticket.Ticket);
}
private static bool TryCreateObservedEndpoint(
@@ -155,7 +155,7 @@ internal sealed partial class UdpMediatorService : BackgroundService
plan.HostPublic,
plan.ClientLocal,
plan.ClientPublic,
plan.ConnectionTicket);
plan.IntroductionToken);
}
private sealed class RendezvousPacketLayer(NatMediationProcessor processor) : PacketLayerBase(0)