From c6511dd0462894177fe03006762a612385b36897 Mon Sep 17 00:00:00 2001 From: neckfire Date: Fri, 17 Jul 2026 00:13:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(netcode):=20flux=20DTLS=20conforme=20UE=20?= =?UTF-8?q?=E2=80=94=20EncryptionAck=20seul=20+=20accept,=20Challenge=20di?= =?UTF-8?q?ff=C3=A9r=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recherche du flux d'encryption UE 4.27 : le client envoie le ClientHello (serveur en accept), NMT_EncryptionAck sans payload en clair déclenche la résolution de clé côté client puis son EnableEncryption. On active DTLS en accept et on NE PAS envoie le Challenge (différé jusqu'à Handshaking completed). Findings détaillés dans NETCODE-RND. --- NETCODE-RND.md | 31 +++++++++++++++++++++++++++ src/Prospect.Unreal/Runtime/UWorld.cs | 20 +++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) 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 {