Règle du projet : ce rapport sert à comprendre et tester. Il ne présente aucune modification comme une fonction future validée. eNode est utilisé comme laboratoire et implémentation de référence secondaire, jamais comme norme absolue du protocole.
1. Pourquoi ce dossier existe
Le Dossier 10 décrivait essentiellement :
ce que le client eMule attend d'un serveur
D17 examine :
ce qu'une implémentation serveur doit faire
pour satisfaire ces attentes
et cherche aussi à déterminer si eNode peut servir de serveur de laboratoire fiable après remise en état.
2. Source historique primaire
edonkey-protocol.txt v0.6 documente dès 2002 :
OP_LOGINREQUEST
OP_OFFERFILES
OP_GETSERVERLIST
OP_SEARCHREQUEST
OP_GETSOURCES
OP_CALLBACKREQUEST
OP_SERVERLIST
OP_SEARCHRESULT
OP_SERVERSTATUS
OP_CALLBACKREQUESTED
OP_CALLBACKFAILED
OP_SERVERMESSAGE
OP_IDCHANGE
OP_SERVERIDENT
OP_FOUNDSOURCES
Il documente également les équivalents UDP.
Cette source établit le contrat réseau historique, pas l’architecture interne.
3. Source serveur
Repository :
petermrg/eNode
Description :
Ed2k/Emule server written in Node.js
README :
experimental-buggy-testing-pre-alpha
Not ready to use
Création :
2013
Dernier push principal :
29 septembre 2013
Conséquence
Toute incohérence détectée doit être interprétée comme l’état d’un prototype ancien, et non comme représentative de tous les serveurs eD2K.
4. Arborescence eNode
Composants utiles :
ed2k/
tcpserver.js
tcpoperations.js
udpserver.js
udpoperations.js
packet.js
buffer.js
tcpcrypt.js
udpcrypt.js
client.js
lowidclients.js
globals.js
storage/
storage.js
engine.mysql.js
engine.mongodb.js
misc/
enode.sql
Architecture suffisamment simple pour servir de laboratoire.
5. Listener TCP
tcpserver.js crée un net.Server.
Pour chaque socket entrant :
client.info = {
ipv4
logged
storageId
id
hasLowId
}
Puis :
Packet(client)
TcpCrypt éventuel
data → processData()
6. Fin de session
Sur close :
si LowID → lowIdClients.remove()
db.clients.disconnect()
La couche storage marque ensuite :
client offline
sources offline
Le lifecycle est donc explicitement lié à la durée du socket TCP.
7. Packet parser
packet.js traite :
Protocol byte
Length uint32
Opcode
Payload
et sait reconstruire :
fragment partiel
paquet exact
paquet + données excédentaires
Il rappelle une règle importante de TCP :
un read() != un paquet
8. Problème de robustesse du parser
Le code contient explicitement :
TODO do checkings here
avant l’allocation :
this.data = new Buffer(this.size)
Risque conceptuel
Une longueur réseau non bornée peut provoquer :
allocation excessive
exception
DoS mémoire
9. Buffer helpers
buffer.js monkey-patche Buffer.prototype.
Il gère :
uint8
uint16
uint32
uint64
strings
hashes
tags
file lists
search expressions
Cette centralisation est pratique pour un prototype, mais rend une erreur de borne transversale.
10. conf.noAssert
Configuration :
noAssert = false
par défaut.
Donc les primitives Node historiques font normalement des contrôles d’offset.
Si noAssert est activé :
beaucoup de protections runtime disparaissent
À ne jamais activer dans un harness réseau non fiable.
11. Login
receive.loginRequest() lit :
hash 16 bytes
id uint32
port uint16
tags
Puis :
db.clients.isConnected(hash)
avant le test de firewall.
12. Politique doublon UserHash
Backend MySQL :
SELECT id
FROM clients
WHERE hash = ?
AND online = 1
LIMIT 1Si trouvé :
connexion refusée
C’est une politique propre à eNode.
Le protocole historique n’impose pas nécessairement cette règle.
13. Firewall test
isFirewalled() crée un mini-client eD2K serveur→client.
Séquence :
connect TCP vers remoteAddress:announcedPort
si crypt :
handshake obfuscation
Hello
attente HelloAnswer
Si réponse valide :
HighID
sinon :
LowID
14. Timeout firewall
Configuration :
connectionTimeout = 2000 ms
Deux secondes sont un choix du prototype.
Ne pas prendre cette valeur comme comportement Lugdunum ou valeur réseau recommandée.
15. Fallback crypt → plain
Si l’handshake d’obfuscation timeout :
retry isFirewalled(client, false)
Le prototype peut donc tester :
obfusqué
puis
clair
avant de déclarer firewalled.
16. HighID eNode
Si joignable :
client.info.id = client.info.ipv4
hasLowId = false
Ce comportement correspond au modèle IPv4 historique vu depuis le client.
17. LowID eNode
Si firewalled :
lowIdClients.add(client)
puis attribution d’un ID dans :
1 .. 0xFFFFFF
si disponible.
18. Audit critique : lowidclients.js
Cette classe contient plusieurs incohérences manifestes dans le master actuel.
A. _count n’est jamais incrémenté dans add()
Conséquences :
count() reste 0
test de capacité incorrect
remove() peut rendre _count négatif
19. Audit critique : détection de collision LowID
Dans _nextId() :
while (lowIdClients._clients.hasOwnProperty[lowIdClients._next])Le code indexe la fonction hasOwnProperty au lieu de l’appeler.
Il devrait conceptuellement tester :
_clients.hasOwnProperty(id)
Conséquence probable
La boucle d’évitement des IDs déjà utilisés ne fonctionne pas comme prévu.
20. Audit critique : récupération LowID
get(id) vérifie correctement :
_clients.hasOwnProperty(id)
mais retourne :
lowIdClients.clients[id]
au lieu de :
lowIdClients._clients[id]
Conséquence probable
Le callback LowID ne récupère pas correctement le socket stocké.
21. Conséquence sur CallbackRequest
tcpoperations.js fait :
clientWithLowId = lowIdClients.get(lowId)
puis tente :
send.callbackRequested(clientWithLowId, requester)
Le bug précédent rend ce chemin non fiable dans l’état actuel du repository.
Conclusion
eNode ne doit pas être lancé comme serveur de référence sans correction et tests.
22. receive.handShake()
Après attribution ID :
db.clients.connect()
logged = true
storageId
puis envoi :
ServerMessage
ServerMessage(version)
ServerStatus
IDChange
ServerIdent
Cette séquence fournit un bon scénario de test client.
23. Audit critique : API storage incohérente
storage/storage.js expose :
clients.count()
files.count()
files.add()
servers.count()
Mais tcpoperations.js appelle à plusieurs endroits :
db.clients.getCount()
db.files.getCount()
db.servers.getCount()
db.files.addSource()
Ces méthodes ne sont pas exposées sous ces noms dans storage.js.
Effets possibles
ServerStatus TCP cassé
ServerList TCP cassé
OfferFiles cassé
logs de publication cassés
dans l’état exact du master audité.
24. Signification de cette incohérence
Il s’agit vraisemblablement d’un :
refactor inachevé
ou
API renommée sans mise à jour complète
Le README disait déjà :
pre-alpha / buggy
Ces découvertes confirment que le dépôt est avant tout une base de recherche.
25. Backend MySQL : clients
Table :
clients
Colonnes principales :
id interne
hash
id_ed2k
ipv4
port
time_login
online
Le UserHash est unique dans cette table.
26. Backend MySQL : files
Table :
files
Clé logique :
(hash, size)
Colonnes :
time_creation
time_offer
source_id
source_port
sources
completed
27. Backend MySQL : sources
Table relationnelle :
sources
Clé unique :
(id_file, id_client)
Donc :
un client
ne possède qu'une ligne source
par fichier
28. Métadonnées source
La table sources conserve :
name
extension
type
rating
title
artist
album
length
bitrate
codec
online
complete
time_offer
Cette modélisation accepte naturellement plusieurs noms/métadonnées pour le même hash.
29. Publication souhaitée
engine.mysql.js possède :
files.add(file, clientInfo)
qui :
upsert files(hash,size)
→ get file id
→ addSource()
C’est la bonne architecture théorique.
30. Mais OfferFiles ne l’appelle pas
Le chemin TCP appelle actuellement :
db.files.addSource(...)
alors que le wrapper public expose :
db.files.add(...)
Correctif laboratoire évident
Avant tout test eNode :
raccorder OfferFiles à files.add()
puis écrire un test d’intégration.
31. Déconnexion
engine.mysql.js :
UPDATE clients SET online=0
UPDATE sources SET online=0
mais ne supprime rien immédiatement.
Une purge ultérieure n’est pas visible dans le code audité.
32. Problème d’expiration
Le schéma garde :
time_offer
mais aucune tâche de garbage collection complète n’apparaît dans les fichiers audités.
Risque long terme
files/sources historiques
→ croissance permanente
Même si online=0 permet de distinguer l’état actuel.
33. GetSources
files.getSources(hash,size) :
ORDER BY s.online DESC, s.time_offer DESC
LIMIT 255Observation
La requête ne contient pas :
WHERE s.online = 1Elle peut donc renvoyer des sources offline si moins de 255 sources online existent.
34. Conséquence GetSources
Le tri met les online d’abord, mais :
offline != exclu
Cela peut créer :
sources mortes
timeouts côté client
À ne pas généraliser
C’est un comportement d’eNode, pas une preuve sur les serveurs Lugdunum.
35. GetSources UDP v1
getSourcesByHash() cherche par :
hash seul
alors que TCP/GetSources2 peut utiliser :
hash + size
Cohérent avec les limitations des anciennes variantes de protocole.
36. Search engine
Buffer.getSearchString() traduit l’arbre de recherche vers une expression SQL.
Opérations prises en charge :
text
AND
OR
AND NOT
type
extension
codec
size > / <
sources >
bitrate >
duration >
complete sources >
37. SQL injection
Le code utilise :
sql.esc(value)
pour les valeurs.
L’arbre lui-même produit des fragments d’opérateur à partir d’un switch fermé.
C’est meilleur qu’une concaténation directe libre, mais le parser doit rester fuzzé pour la structure, la profondeur, les types et la troncature.
38. Query result
Le backend :
GROUP BY s.id_file
LIMIT 255et sélectionne une ligne de métadonnées source pour représenter le fichier.
Question
Avec plusieurs noms/métadonnées divergents :
quelle source gagne ?
Sans agrégation explicite de chaque champ, le résultat dépend du moteur SQL/plan.
À tester.
39. files.sources et completed
Après addSource(), une requête calcule :
COUNT(*) AS sources
SUM(complete) AS completed
sans filtrer :
online = 1
Conséquence
Les compteurs persistants peuvent inclure des sources offline historiques.
Le serveur de démonstration peut donc surévaluer l’actualité du fichier.
40. UDP Server Status contient volontairement de fausses valeurs
globServStatRes() envoie :
db.clients.count() + 2000
lowIdClients.count() + 1000
avec commentaires :
fake value, for testing
Conclusion importante
Les statistiques produites par eNode ne doivent jamais être utilisées pour inférer le comportement réel du réseau.
41. Soft/Hard Files eNode
Le même paquet contient :
10000 // soft file limit ???
20000 // hard file limit ???
Les commentaires indiquent explicitement l’incertitude.
Conclusion
eNode n’est pas une source pour définir précisément la sémantique Lugdunum de SoftFiles/HardFiles.
42. Server List eNode
Le backend contient une liste codée en dur :
111.222.111.222:1234
123.123.234.234:2345
Ce sont clairement des placeholders.
Ne pas utiliser cette partie comme modèle de propagation réel des serveurs.
43. UDP Search
eNode gère :
OP_GLOBSEARCHREQ
OP_GLOBSEARCHREQ3
mais pas nécessairement toutes les variantes ou subtilités.
Pour v3, le code contient :
TODO: not sure about what to do here
sur le tag SearchTree.
44. UDP GetSources
Implémentations :
OP_GLOBGETSOURCES
OP_GLOBGETSOURCES2
et réponse :
OP_GLOBFOUNDSOURCES
Le code gère aussi les Large Files dans GetSources2.
Bon laboratoire pour compatibilité client.
45. Search UDP response framing
Le source possède un TODO mentionnant la concaténation de plusieurs réponses UDP dans une même frame.
eNode envoie actuellement chaque fichier dans un datagramme séparé.
Question laboratoire
Comparer avec :
ce qu'attend eMule 0.72a
captures de serveurs réels
Wireshark
46. UDP Status Challenge
eNode renvoie le challenge reçu.
Cela correspond bien au modèle attendu côté eMule :
corrélation request/response
47. UDP Description
Deux modes :
ancien
→ name + description
moderne
→ challenge + tags
Le serveur expérimental gère les deux.
48. TCP obfuscation serveur
eNode implémente le handshake DH/RC4 historique décrit au D13.
C’est particulièrement intéressant comme serveur de laboratoire pour tester :
obfuscation client↔server
fallback
flags
ports
après correction du reste.
49. UDP obfuscation serveur
udpcrypt.js pré-calcule de nombreuses clés RC4 dérivées d’une clé serveur et d’un identifiant 16 bits.
Le système est historique et doit être traité uniquement comme compatibilité de protocole.
50. Audit critique UDP key storage
Le code crée un fichier :
udpkeys-<serverKey>.dat
de clés pré-calculées.
Cette optimisation est spécifique à eNode.
Elle n’est pas une exigence eD2K.
51. Packet.make()
Le générateur calcule la taille du payload selon le type d’item.
Types supportés :
uint8
uint16
uint32
string
hash
tags
Il ne couvre donc pas automatiquement tous les types de tags/packets modernes.
52. Extended tags
buffer.js traite une forme de :
Lugdunum extended tag
en exploitant le bit haut du type.
Intérêt :
interop avec serveurs modernes historiques
À comparer avec le parser eMule avant d’en faire une documentation normative.
68. Architecture de serveur de laboratoire proposée
Sans prétendre produire un serveur public :
eNode-Lab
│
├─ parser original corrigé
├─ backend mémoire ou SQLite simple
├─ journal wire
├─ fixtures
├─ tests eMule 0.72a
└─ aucun accès Internet nécessaire
Le but serait :
comprendre et tester le client
pas exploiter un serveur public.
69. Pourquoi un backend mémoire peut être préférable au début
Pour les tests :
déterministe
reset instantané
pas de dépendance MySQL
inspection simple
MySQL peut revenir plus tard pour les tests d’échelle.
70. Oracle de compatibilité
Pour chaque opération serveur :
eDonkey protocol doc
↕
eMule 0.72a client expectation
↕
eNode implementation
Si deux sources concordent et la troisième diverge :
analyser
Ne pas « corriger » automatiquement selon intuition.
71. Wireshark
Le dissector eDonkey du corpus peut servir de quatrième témoin :
layout paquet
opcode
tags
Très utile pour éviter les erreurs d’endianness ou de longueur.
72. Ce qu’eNode apporte malgré ses bugs
Il rend visibles des concepts que le client masque :
client table
file table
source relation
lowID socket map
server-side firewall probe
query execution
disconnect cleanup
C’est précisément la valeur du projet.
73. Ce qu’eNode ne peut pas nous apprendre
Il ne documente pas de façon fiable :
architecture des serveurs Lugdunum
scalabilité réelle
anti-abuse de production
policy Soft/Hard Files exacte
expiration réelle historique
ranking search historique
communication inter-serveurs propriétaire éventuelle
Pour ces points, il faudrait d’autres archives ou code serveur historique.
75. Découverte principale du dossier
Le serveur peut être compris comme deux plans.
Plan persistant/index
Clients
Files
Sources
Metadata
Plan temps réel
TCP sessions
LowID map
firewall test
callbacks
UDP requests
Un serveur fonctionnel doit garder ces deux plans cohérents.
76. Point faible central d’un prototype naïf
Si la base dit :
source online
mais que le socket a disparu, les clients reçoivent une source morte.
Si le socket existe mais la base n’est pas à jour, le fichier devient introuvable.
La cohérence entre :
session
et
index
est donc l’un des vrais problèmes du serveur.
78. Pas de score serveur unique
Même règle que D05/D10 :
SearchQuality
SourceFreshness
CallbackReliability
Reachability
Latency
doivent rester des dimensions séparées.
79. Sécurité du laboratoire
Le serveur de test ne doit pas être exposé publiquement pendant les fuzzings.
Utiliser :
127.0.0.1
LAN isolé
VM
deux instances eMule de test
Cela évite d’envoyer des paquets expérimentaux au réseau eD2K réel.
80. Conclusion interne
D17 confirme deux choses.
Premièrement, l’architecture logique d’un serveur eD2K est maintenant beaucoup plus claire :
session
→ HighID/LowID
→ publication
→ index
→ search/GetSources
→ callback
→ cleanup
Deuxièmement, eNode est extrêmement intéressant comme laboratoire mais n’est pas utilisable tel quel. Son propre README le dit, et l’audit trouve plusieurs incohérences concrètes dans la table LowID et l’API de stockage.
La meilleure suite n’est donc pas d’utiliser eNode directement sur Internet.
La suite raisonnable est :
corriger le laboratoire
écrire les tests
le faire dialoguer avec eMule 0.72a
capturer les paquets
puis comparer avec le protocole historique
C’est probablement la meilleure manière d’obtenir enfin une vue expérimentale des deux côtés du protocole eD2K.
================================================================================================