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; } }