Remplacer la conversation Whatsapp familiale par un serveur XMPP
Tout le monde aujourd’hui a, dans sa famille, une conversation Whatsapp/Telegram/Messenger/autre, en remplacement des SMS de la décennie précédente. Mais avec l’enshitification ambiante d’Internet, il devient de plus en plus essentiel d’avoir des options pour rester en contact avec ses proches sans passer par des applications propriétaires peu respectueuses de la vie privée. XMPP est une excellente solution à ça, et en installer un serveur est un projet sympathique et assez facile.
XMPP est un protocol décentralisé d’échange de données en temps réel. Plus simplement, c’est un très bon protocol pour construire des systèmes fédérés de discussion instantannée, ce qui est exactement ce que l’on va faire. Le protocol est défini via quelques RFC, et définit également des extensions à travers des XEP. Plusieurs de ces XEP sont aujourd’hui très standard et sont installées et activées par défaut (comme la XEP-0012). Enfin, XMPP propose aujourd’hui du chiffrement bout-en-bout mature et automatique (XEP-0384).
Les différentes étapes de cet article sont :
Environnement
Pour monter un serveur XMPP, il est nécessaire d’avoir au moins un nom de domaine public, et si possible plusieurs sous-domaines dédiés au projet. Il est également nécessaire d’avoir une ip publique, avec a minima les ports tcp 5222 et 5269 accessibles.
Dans cet article, je prends comme noms de domaines
example.org
et ses sous-domaines, et comme ip publique198.51.100.5
. Le serveur lui-même est sur l’ip192.168.1.200/24
, derrière un NAT. J’utilise également un reverse-proxy dédié (puisque je n’ai qu’une ip publique), dont l’ip privée est192.168.1.100
.
Prosody et edjabberd sont deux implémentations modernes de serveur XMPP, et les deux sont des choix très pertinents. Pour ma part, j’ai choisi Prosody.
Pour installer Prosody, j’utilise un serveur Debian 11, avec 2 vCPU, 2Go de ram et 32Go de disque (à titre purement indicatif, je n’ai pas fait de benchmark pour vérifier la pertinence de ces ressources).
Installation et configuration initiale
L’installation sous debian se fait via le repo officiel de Prosody :
echo deb http://packages.prosody.im/debian $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/prosody.list
sudo wget https://prosody.im/files/prosody-debian-packages.key -O /etc/apt/trusted.gpg.d/prosody.gpg
sudo apt update
sudo apt install prosody
Toute la configuration se fait ensuite dans /etc/prosody/prosody.cfg.lua
.
Le fichier de configuration complet est disponible à la fin du guide. J’ai enlevé les commentaires du fichier par défaut par soucis de clareté, mais je vous conseille de les laisser !
La configuration commence par charger une liste de modules. La liste par défaut est un bon point de départ ; j’ai simplement décommenté le module mam
, qui permet de conserver un historique des messages sur le serveur.
On peut ensuite indiquer à Prosody quelle ip publique est utilisée pour accéder au service, ce qui lui permet de vérifier lui même si la configuration DNS est correcte :
external_addresses = { "198.51.100.5" }
Enfin, il faut créer un VirtualHost, ce qui correspond à un domaine sur lequel Prosody va écouter (en gros, un serveur XMPP séparé). Attention, toutes les options de configuration définies après ne s’appliqueront qu’au VirtualHost !
VirtualHost "chat.example.org"
Et voilà, juste avec ça, on a un serveur XMPP fonctionnel ! Mais avant de l’utiliser, nous allons au moins mettre en place un certificat TLS pour sécuriser les connexions entre les clients et le serveur, et entre notre serveur et les autres. Pour cela, un peu de DNS et de reverse proxy s’impose. Faisons donc pointer chat.example.org
sur notre ip publique, puis ajoutons juste ce qu’il faut de reverse proxy :
J’utilise NixOS pour gérer mon reverse proxy. Je vous laisse adapter cette configuration pour nginx si ce n’est pas votre cas.
services.nginx."chat.example.org" = {
locations = {
"/".proxyPass = "http://192.168.1.200:80";
};
};
Cette configuration de nginx n’écoute que sur le port 80, et reverse-proxyfie les requêtes sur le port 80 de la VM Prosody. Elle ne sert qu’à faire fonctionner le challenge Let’s Encrypt qui va venir :
$ certbot certonly --standalone -d chat.example.org
$ prosodyctl --root cert import /etc/letsencrypt/live
Pour vérifier que tout fonctionne, on peut utiliser prosodyctl
:
$ prosodyctl check certs
Checking certificates...
Checking certificate for chat.example.org
Certificate: /etc/prosody/certs/chat.example.org.crt
Et voilà ! N’oubliez pas de forward les ports tcp 5222 et 5269 sur votre firewall, et votre serveur XMPP devrait être fonctionnel. Pour ajouter des utilisateurs, utilisez la cli :
$ prosodyctl adduser user@chat.example.org
File sharing et MUC
Afin de rajouter quelques fonctionnalités à notre serveur, nous allons rajouter un vrai service de partage de fichier (actuellement, le partage de fichiers fonctionne, mais uniquement en peer-to-peer), ainsi que la possibilité de créer des rooms de discussion.
Ces deux fonctionnalités s’ajoutent via des Components, qui se configurent à la suite du VirtualHost :
disco_items = {
{ "file.example.org", "file sharing service" },
}
http_paths = {
file_share = "/";
}
Component "file.example.org" "http_file_share"
http_file_share_global_quota = 16*1024*1024*1024 -- 16 Go
http_external_url = "https://file.example.org/"
trusted_proxies = { "192.168.1.100", }
Component "rooms.chat.example.org" "muc"
modules_enabled = { "muc_mam" }
name = "Chatrooms"
restrict_room_creation = "local"
muc_room_default_public = false
Le service de partage de fichier, qui va être configuré derrière le reverse proxy, a besoin de connaitre son url d’accès pour générer correctement les liens. On lui configure également son quota global, et l’adresse ip du serveur proxy afin de l’autoriser.
Pour éviter que les liens générés soient préfixés par file_share/
, on redéfinit son http_path
. Enfin, pour que les clients puisse découvrir le service et parce qu’il n’est pas sur un sous-domaine de chat.example.org
, on le définit dans la liste des services à découvrir.
Côté MUC, on active également le stockage des messages récents sur le serveur, on autorise uniquement les comptes locaux à créer de nouvelles rooms (même si n’importe qui peut ensuite les rejoindre), et on crée les rooms comme étant privées par défaut.
Afin que le reverse proxy fonctionne, il faut également faire écouter le port HTTP de Prosody sur 0.0.0.0
au lieu de localhost
par défaut :
http_interfaces = { "*", "::" }
On peut à présent configurer notre reverse proxy, cette fois avec HTTPS :
services.nginx."file.example.org" = {
enableACME = true;
forceSSL = true;
locations = {
"/".proxyPass = "http://192.168.1.200:5280";
};
};
Fichier de configuration final
external_addresses = { "198.51.100.5" }
modules_enabled = {
"disco"; -- Service discovery
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"blocklist"; -- Allow users to block communications with other users
"bookmarks"; -- Synchronise the list of open rooms between clients
"carbons"; -- Keep multiple online clients in sync
"dialback"; -- Support for verifying remote servers using DNS
"limits"; -- Enable bandwidth limiting for XMPP connections
"pep"; -- Allow users to store public and private data in their account
"private"; -- Legacy account storage mechanism (XEP-0049)
"smacks"; -- Stream management and resumption (XEP-0198)
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
"csi_simple"; -- Simple but effective traffic optimizations for mobile devices
"invites"; -- Create and manage invites
"invites_adhoc"; -- Allow admins/users to create invitations via their client
"invites_register"; -- Allows invited users to create accounts
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"time"; -- Let others know the time here on this server
"uptime"; -- Report how long server has been running
"version"; -- Replies to server version requests
"mam"; -- Store recent messages to allow multi-device synchronization
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
"admin_shell"; -- Allow secure administration via 'prosodyctl shell'
}
s2s_secure_auth = true
limits = {
c2s = {
rate = "10kb/s";
};
s2sin = {
rate = "30kb/s";
};
}
pidfile = "/var/run/prosody/prosody.pid"
authentication = "internal_hashed"
archive_expires_after = "1w"
log = {
info = "/var/log/prosody/prosody.log";
error = "/var/log/prosody/prosody.err";
}
certificates = "certs"
http_interfaces = { "*", "::" }
VirtualHost "chat.example.org"
disco_items = {
{ "file.example.org", "file sharing service" },
}
http_paths = {
file_share = "/";
}
Component "file.example.org" "http_file_share"
http_file_share_global_quota = 16*1024*1024*1024 -- 16 Go
http_external_url = "https://file.example.org/"
trusted_proxies = { "192.168.1.100", }
Component "rooms.chat.example.org" "muc"
modules_enabled = { "muc_mam" }
name = "Chatrooms"
restrict_room_creation = "local"
muc_room_default_public = false