feat(netcode): flux DTLS conforme UE — EncryptionAck seul + accept, Challenge différé
Build Game Server / build (push) Successful in 23s

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.
This commit is contained in:
2026-07-17 00:13:53 +02:00
parent cc392238f7
commit c6511dd046
2 changed files with 45 additions and 6 deletions
+31
View File
@@ -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 ».
+14 -6
View File
@@ -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
{