using Org.BouncyCastle.Tls;
using Prospect.Unreal.Net.Dtls;
using Prospect.Unreal.Serialization;
using Serilog;
namespace Prospect.Unreal.Net;
///
/// Composant de chiffrement DTLS-PSK côté serveur, pendant du DTLSHandlerComponent
/// d'UE (la pile client est [DTLSHandlerComponent, StatelessConnectHandlerComponent]).
///
/// Le client exige le chiffrement : il annonce son identité PSK (= user_id) dans
/// NMT_Hello, et on répond NMT_EncryptionAck puis on établit un DTLS-PSK. Ici on
/// implémente le côté serveur (BouncyCastle) ; la clé provient du DtlsPskStore.
///
/// ⚠️ Première implémentation — le framing exact DTLS-sur-PacketHandler d'UE et
/// l'injection des records de handshake dans la voie d'émission demandent une
/// itération EN LIVE contre le vrai client (invalidable hors client).
///
public sealed class DTLSHandlerComponent : HandlerComponent
{
private static readonly ILogger Logger = Log.ForContext();
private DtlsPacketTransport? _transport;
private DtlsTransport? _dtls; // non-null une fois le handshake terminé
private Thread? _handshakeThread;
private volatile bool _established;
private volatile bool _failed;
public DTLSHandlerComponent(PacketHandler handler) : base(handler, nameof(DTLSHandlerComponent))
{
RequiresHandshake = true;
}
public override void Initialize()
{
// Inactif tant qu'on n'a pas la PSK (via BeginHandshake) : les connexions
// sans EncryptionToken passent en clair.
SetActive(false);
Initialized();
}
public override bool IsValid() => true;
///
/// Démarre le serveur DTLS-PSK pour une identité (user_id) donnée.
/// émet un record DTLS vers le client (voie bas-niveau, hors chiffrement).
///
public void BeginHandshake(DtlsPskStore store, string identity, Action sendRecord)
{
if (store.Get(identity) == null)
{
Logger.Warning("Pas de PSK pour l'identité {Identity} — DTLS impossible (fournir PROSPECT_DTLS_PSKS)", identity);
_failed = true;
return;
}
_transport = new DtlsPacketTransport(rec =>
{
Logger.Information("[DTLS] Outgoing record {N}o head={H}", rec.Length,
Convert.ToHexString(rec.AsSpan(0, Math.Min(rec.Length, 12))));
sendRecord(rec);
});
SetActive(true);
Logger.Information("[DTLS] BeginHandshake pour {Identity} — serveur DTLS-PSK démarré", identity);
_handshakeThread = new Thread(() =>
{
try
{
var server = new ProspectPskTlsServer(store);
_dtls = new DtlsServerProtocol().Accept(server, _transport);
_established = true;
Logger.Information("Handshake DTLS-PSK établi pour {Identity}", identity);
}
catch (Exception ex)
{
_failed = true;
Logger.Error(ex, "Handshake DTLS-PSK échoué pour {Identity}", identity);
}
})
{
IsBackground = true,
Name = "dtls-handshake",
};
_handshakeThread.Start();
}
public override void Incoming(FBitReader packet)
{
Logger.Information("[DTLS] Incoming CALLED active={A} transportNull={T} failed={F} established={E} bitsLeft={B}",
IsActive(), _transport == null, _failed, _established, packet.GetBitsLeft());
if (!IsActive() || _transport == null || _failed)
{
return; // passthrough (connexion non chiffrée)
}
var record = ReadAlignedBytes(packet);
Logger.Information("[DTLS] Incoming nbytes={N} head={H}", record.Length,
Convert.ToHexString(record.AsSpan(0, Math.Min(record.Length, 12))));
if (record.Length == 0)
{
return;
}
// Alimente la pile DTLS (handshake OU données applicatives).
_transport.Feed(record);
if (!_established || _dtls == null)
{
// Record de handshake : consommé par le thread, rien à remonter au pipeline.
packet.SetData(Array.Empty(), 0);
return;
}
// Données applicatives : déchiffrer et remplacer le contenu du paquet.
var plain = new byte[_dtls.GetReceiveLimit()];
var n = _dtls.Receive(plain, 0, plain.Length, 0);
if (n > 0)
{
packet.SetData(plain[..n], (long)n * 8);
}
else
{
packet.SetData(Array.Empty(), 0);
}
}
public override void Outgoing(ref FBitWriter packet, FOutPacketTraits traits)
{
if (!IsActive() || !_established || _dtls == null)
{
return; // avant établissement : le handshake gère ses propres records
}
var plain = packet.GetData();
var nbytes = (int)packet.GetNumBytes();
_dtls.Send(plain, 0, nbytes); // le record chiffré part via le callback du transport
}
public override int GetReservedPacketBits()
{
// Marge d'en-tête DTLS (record 13o + AEAD ~40o) — valeur à affiner en live.
return 64 * 8;
}
public bool IsEstablished => _established;
public bool HasFailed => _failed;
private static byte[] ReadAlignedBytes(FBitReader packet)
{
var bytes = packet.GetBytesLeft();
if (bytes <= 0)
{
return Array.Empty();
}
var buf = new byte[bytes];
unsafe
{
fixed (byte* p = buf)
{
packet.Serialize(p, bytes);
}
}
return buf;
}
}