Geek++ · Dossier 17 / 25

Audit d’une implémentation serveur eD2K : eNode

Étude technique publique · 2,341 mots · ≈ 11 min de lecture attentive

Étude intégraleSans réflexion CODEXSources & constantes conservées
Anatomie serveur
TCP
eNode
UDP
IndexClientsSources

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 1

Si 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 255

Observation

La requête ne contient pas :

WHERE s.online = 1

Elle 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 255

et 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.

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.

================================================================================================


Dossier 17 terminé

Audit d’une implémentation serveur eD2K : eNode

← Dossier 16Dossier 18 →