• FR
  • EN
  • Kagami


    Introduction

    Minecraft est l’un des jeux les plus populaires de tous les temps. L’une des choses qui a massivement contribué à sa croissance est sa capacité de modding. Un mod est une extension du code du jeu ajoutée par l’utilisateur. Cependant, avec la popularité des jeux PvP, beaucoup de gens ont commencé à créer des mods qui leur donnaient des avantages injustes sur les autres joueurs, lançant ainsi l’une de ses plus grandes industries, les logiciels de triche.

    Après l’échec des anti-cheats côté serveur, certains des acteurs les plus influents ont tenté de créer leurs propres clients Minecraft, intégrant des anti-cheats côté client. Le compromis était que les utilisateurs ne pouvaient plus utiliser leurs propres mods, ils devaient utiliser ce que le client de l’entreprise fournissait.

    Une fois de plus, ce fut un échec massif. Les gens utilisaient ces clients, principalement parce que c’était plus facile que de chercher des dizaines de mods et de se rappeler comment chacun d’eux fonctionnait, mais la partie anti-triche était coûteuse et, au final, elle ne fonctionnait pas bien. Actuellement, tous ces anti-cheats ont été retirés de leurs clients en raison de leur inefficacité.

    Cependant, pour la plupart des gens, rien n’a changé. Ils étaient là pour le confort, la facilité d’utilisation, et pour certains d’entre eux, ils avaient des cosmétiques et d’autres choses pour lesquelles ils avaient payé. Alors, que se passe-t-il lorsqu’un utilisateur veut personnaliser quelque chose ? Eh bien, il ne peut pas. Mais savez-vous qui peut ? Les tricheurs, ils avaient la réponse depuis longtemps et n’ont jamais eu à s’en soucier. Certains développeurs ont décidé de suivre cette voie et de créer un modloader en utilisant l’injection.

    J’ai décidé de poursuivre une autre direction. Je connaissais certains projets utilisant des proxies comme moyen d’interagir avec le jeu. Tous semblaient être basés sur une bibliothèque NodeJS appelée prismarine-proxy.

    Pourquoi un proxy ?

    Cela vient de l’idée que toutes les données que vous voyez à l’écran ont été envoyées par le serveur et, dans le sens opposé, tout ce que le serveur sait sur vous a été envoyé par votre client. En créant un proxy entre le client et le serveur, vous avez accès à toutes les données qui sont transmises.

    Pourquoi Kagami ?

    Le plus grand obstacle que j’ai rencontré en construisant des applications autour de Minecraft était la quantité de données que vous pouvez recueillir de l’extérieur. L’année dernière, j’ai construit Flow, un outil d’automatisation qui pouvait mettre à jour automatiquement les commandes Twitch en fonction de la configuration du jeu de l’utilisateur. Flow, comme de nombreux outils similaires, utilise les logs du jeu, qui contiennent le chat et d’autres données de débogage. Les logs sont très pauvres en comparaison avec la quantité de données que le jeu possède. Avec plus de données, vous pouvez construire plus de choses.

    J’aime Rust, mais je trouve difficile de justifier de construire avec, donc je voulais un projet qui en bénéficierait grandement. De plus, je voulais un défi, quelque chose qui, je le savais, me ferait apprendre beaucoup de nouveaux concepts. Cette approche est également très méconnue, je voulais donc une façon pour les gens de créer des choses facilement.

    Le début

    Puisque le jeu utilise le protocole TCP (Transmission Control Protocol) pour communiquer, créer un proxy basique consistait simplement à créer deux flux TCP : l’un qui communiquerait avec le client et l’autre avec le serveur. Ensuite, chaque buffer serait reflété de l’autre côté et voilà, vous avez un proxy.

    Les Buffers, yay

    Donc maintenant, nous devrions avoir accès à toutes les données qui sont envoyées soit au client soit au serveur, n’est-ce pas ?

    Well yes, but actually no

    Techniquement, c’est le cas, mais pas dans un format lisible par les humains. Si un client sait ce qu’il doit recevoir et vice versa, les deux machines n’ont pas besoin de nommer quoi que ce soit pour se comprendre. Chaque action en jeu est un paquet, une liste d’octets qui représente l’action, et chaque octet fait partie d’une structure de données que seul le jeu connaît.

    Cela permet d’économiser de la bande passante puisque vous pouvez, par exemple, stocker un nombre allant jusqu’à 255 dans un seul octet sans gaspiller 10 octets supplémentaires pour lui donner un nom. Nos buffers contiennent tous deux une quantité aléatoire d’octets. Cela n’était pas un problème pour un proxy basique, car nous pouvions simplement copier les mêmes octets, mais si nous voulons modifier un paquet, nous devons comprendre les données que l’on transfert.

    Désérialiser les paquets

    Un paquet suit des règles simples : son premier octet représente la longueur du paquet. L’octet suivant est l’ID du paquet, cela indique au client comment désérialiser les octets suivants. Par exemple, un paquet de chat provenant du client a un ID de 0x01 et contient uniquement un string UTF-8 précédée de la longueur du string.

    L’un des plus grands défis que j’ai rencontrés était le nombre de structures de données utilisées dans ces paquets. Les octets uniques et les booléens sont faciles à lire, mais les structures de données créées par les développeurs du jeu étaient très déroutantes.

    Une fois cela fait, nous pouvons lire le contenu des paquets dans un format lisible par les humains. Cela pourrait sufir à ce que les gens puissent recueillir plus de données que ce qu’ils pouvaient en utilisant d’autres méthodes.

    Demo :

    Interface

    J’ai commencé à travailler sur une interface simple pour la bibliothèque. Un développeur peut facilement ajouter un gestionnaire en appelant add_read_handler et en lui fournissant une closure qui prend le paquet correspondant comme argument. Nous conservons deux types de callbacks, un pour les opérations de lecture et un pour les opérations d’écriture. De cette manière, nous pouvons exécuter de manière asynchrone des callbacks qui ne modifient pas le paquet, après qu’il ait été envoyé au serveur et donc sans ajouter de délai.

    Un autre avantage de ces closures est que nous n’avons pas besoin de désérialiser tous les paquets. Une fois que nous avons l’ID d’un paquet, nous pouvons vérifier s’il a un callback et seulement dans ce cas, désérialiser le paquet entier.

    mc.handlers.add_read_handler(|packet: &client::WindowClick| {
        Box::pin(async move { println!("Slot clicked: {:#?}", packet.item) })
    });
    

    Sérialiser les paquets

    Les opérations d’écriture sont un peu plus complexes, car vous devez les effectuer avant que le paquet ne soit envoyé. Vous avez également besoin d’un moyen de dire au proxy ce que vous voulez qu’il fasse avec le paquet. Par exemple, vous souhaitez créer une commande personnalisée qui, lorsqu’elle est envoyée, déclenche une action sur le système de l’utilisateur. Vous devriez désérialiser le paquet de chat, détecter si le message envoyé est la commande souhaitée, puis décider de ne pas envoyer ce paquet spécifique au serveur.

    Ce n’est pas la seule action dont nous avons besoin. J’ai ajouté une action pour définir explicitement si un paquet a été modifié ou non. Si un paquet n’a pas été altéré de quelque manière que ce soit, alors le proxy peut simplement écrire les octets bruts au lieu de sérialiser le paquet. (ce qui est également beaucoup plus facile pour les paquets qui ne sont pas suffisamment connus pour être entièrement re-sérialisés).

    mc.handlers.add_write_handler(|packet: &mut client::Chat| {
        Box::pin(async move {
            // Modifie le message s'il contient "foo"
            if packet.message.contains("foo") {
                packet.message = "I never said that!".into();
                return PacketAction::Edit;
            }
    
            // Filtre le message s'il contient "bar"
            else if packet.message.contains("bar") {
                return PacketAction::Filter;
            }
    
            // Envoie le message au serveur
            PacketAction::Default
        })
    });
    

    Envoyer un paquet généré par le proxy

    Modifier les paquets, c’est bien, mais si nous voulons atteindre l’interactivité, nous devons être capables de communiquer de toutes les manières possibles, ce qui signifie que le proxy doit pouvoir parler avec le client et le serveur avec ses propres paquets.

    Par exemple, si vous voulez ajouter un retour à la commande personnalisée dont nous avons parlé. Vous devez pouvoir envoyer un paquet de chat du serveur au client. Comme nous avons ajouté la prise en charge de la sérialisation des paquets, il est assez facile de générer un paquet, mais pour écrire le paquet dans un des buffers, nous avons besoin d’y accéder, et cela doit être suffisamment sécurisé pour être utilisé dans un environnement asynchrone. J’ai utilisé la communication par canaux internes pour y parvenir. Les résultats étaient prometteurs : lors de l’envoi de paquets aussi rapidement que possible au client (des milliers par seconde), le client avait du mal a gérér le flux alors que le proxy fonctionnait à la perfection.

    Je n’ai pas encore ajouté cette interface aux closures, mais cela ne devrait pas poser de problème. Les canaux peuvent être envoyés n’importe où sans problème, vous pouvez même exécuter des tâches asynchrones à l’intérieur sans problème.

    Plans

    C’est un projet amusant, je ne suis pas loin de pouvoir construire des choses avec. Ce que je ferai certainement à l’avenir, mais ce n’est pas la raison pour laquelle j’ai décidé de créer Kagami. Je veux que Kagami soit la meilleure solution qu’un développeur puisse utiliser pour ce cas d’utilisation. Les performances en sont un aspect, mais l’expérience développeur (DX) est ce que je vise avant tout. Les closures étaient la première étape, le gestionnaire de commandes personnalisées sera la deuxième. Ensuite, j’ajouterai la prise en charge des interfaces utilisateur personnalisées via l’inventaire.

    Il y aura un repo github pour cela, mais pour l’instant je me concentre sur la création de quelque chose qui fonctionne, puis je rendrai le code plus structuré pour la première version.

    Liens