diff --git a/NETCODE-RND.md b/NETCODE-RND.md index 1f0887f..41c65c5 100644 --- a/NETCODE-RND.md +++ b/NETCODE-RND.md @@ -163,3 +163,34 @@ Chaque test = ~4 min (CI + redeploy). Bugs serveur secondaires à corriger : `Co **Décision : checkpoint.** Tout est commité/documenté. La suite = RE netcode profonde multi-session ; et même résolue, ce n'est que le login — la simulation gameplay reste hors de portée réaliste. + +## Flux de chiffrement DTLS d'UE 4.27 (recherche, 2026-07-17) + +Sources : KB Epic « Enable Encryption via Packet Handler Components », doc API +(EnableEncryptionServer/SetEncryptionData/FEncryptionData), UE-95508 (ack chiffré si +renvoyé), UE-171638 (module DTLS exige OpenSSL), logs DTLS/PSK réels (Satisfactory #453). +Le source du plugin DTLS est gated (points reconstruits signalés). + +**Corrections clés :** +- **Le CLIENT envoie le ClientHello ; le serveur ATTEND (accept), ne parle jamais en premier.** + Donc l'AES-GCM autonome = impasse (c'est bien du DTLS, records `16 fe fd` standard OpenSSL). +- **`NMT_EncryptionAck` ne porte AUCUNE clé.** À sa réception, le client appelle + `ReceivedNetworkEncryptionAck` → **résout lui-même** sa `FEncryptionData` (Key + Identifier) + → appelle **`EnableEncryption` côté client** → **c'est CE qui déclenche son ClientHello**. +- **Séquence serveur** : `SetEncryptionData(Key,Identifier)` → `NMT_EncryptionAck` **en clair, sans + payload** → **puis** `EnableEncryption` (mode accept). Ne jamais chiffrer l'ack. +- **Le `NMT_Challenge` doit être DIFFÉRÉ** : le handler DTLS met les bunches applicatifs en file + jusqu'à `Handshaking completed`. (Notre impl l'envoyait tout de suite → à corriger.) +- **PSK** : PSK = `FEncryptionData.Key`, identité PSK = `FEncryptionData.Identifier` ; ressortie via + `DTLSPSKServerCallback` (hook OpenSSL) quand le ClientHello arrive. Le serveur n'envoie jamais la clé. +- **Couches** : DTLS **sous** le stateless (entrée : retirer stateless puis déchiffrer DTLS ; + Outgoing forward / Incoming reverse). + +**Si le client ferme SANS ClientHello (notre cas)** → cause la plus probable = **échec de résolution +de clé côté client** (le vrai client shipping attend peut-être la clé de SON backend/matchmaking, +non émulé — ou dérivation/Identifier différents), pas notre framing DTLS. C'est potentiellement un +mur dur (dépend du backend réel du client). + +**Test informé en cours** : `EncryptionAck` seul (clair, sans payload) + DTLS **accept** (attente) + +**PAS de Challenge** → observer si le client envoie enfin un ClientHello `16 fe fd`. Si oui → progrès ; +si non → mur « résolution de clé côté client ». diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index 38e54b1..91f91e1 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -318,14 +318,22 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable // paquets post-ack sont EN CLAIR → il attend une voie serveur chiffrée, // sans handshake DTLS). On envoie l'EncryptionAck EN CLAIR, on active // AES-256-GCM keyé par la PSK, puis le Challenge part CHIFFRÉ. - var aesKey = DtlsPsks.Value.Get(encryptionToken); - if (aesKey != null && connection.AesComponent != null) + // Flux DTLS d'UE (cf. recherche) : NMT_EncryptionAck EN CLAIR sans + // payload -> le client résout SA clé (ReceivedNetworkEncryptionAck), + // appelle EnableEncryption côté client, ce qui déclenche SON ClientHello. + // Le serveur passe en mode ACCEPT (attend le ClientHello, ne parle pas + // en premier) et NE DOIT PAS envoyer le Challenge maintenant : il est + // différé jusqu'à la fin du handshake DTLS. + var pskKey = DtlsPsks.Value.Get(encryptionToken); + if (pskKey != null && connection.DtlsComponent != null) { - Logger.Information("AES-GCM: identité {Token} connue -> EncryptionAck (clair) + activation + Challenge (chiffré)", encryptionToken); + Logger.Information("DTLS: identité {Token} -> EncryptionAck (clair) + accept (attente ClientHello, PAS de Challenge)", encryptionToken); NMT_EncryptionAck.Send(connection); - connection.FlushNet(); // ack en clair (AES pas encore actif) - connection.AesComponent.Activate(aesKey); // à partir d'ici, sortie chiffrée - connection.SendChallengeControlMessage(); // Challenge chiffré + connection.FlushNet(); + connection.DtlsComponent.BeginHandshake( + DtlsPsks.Value, encryptionToken, + rec => connection.LowLevelSend(rec, rec.Length * 8, new FOutPacketTraits())); + // Challenge différé : envoyé une fois le handshake DTLS terminé. } else {