Aller au contenu

Matrix et les ponts mautrix

Comment DNF implémente le serveur Matrix (Synapse) et les ponts mautrix. Pour l’exploitation au quotidien, voir Matrix et les ponts.

Tout est porté par dnf/modules/service/matrix.nix, avec deux modules satellites : element.nix (client web statique) et turn.nix (relais coturn pour l’audio/vidéo).

Diagram
  • Caddy route /_matrix/* et /_synapse/client/* vers Synapse et sert les fichiers /.well-known/matrix/{client,server} (découverte des clients et fédération).
  • Synapse écoute sur dnfConfig.network.ports.matrix, base PostgreSQL locale (créée avec collation C par une unité matrix-db-init).
  • Kanidm fournit l’authentification : client OIDC provisionné via darkone.service.idm.oauth2.matrix, local part dérivée du preferred_username (voir Authentification & IDM).
  • coturn est branché automatiquement si le service turn est déclaré (turn_uris, secret partagé via sops). Il relaie les appels à deux des clients historiques, seule pile qu’Element Classic sache utiliser.
  • MatrixRTC (matrixRtc.enable) ajoute la pile Element Call : un SFU LiveKit et son service d’autorisation, servis sur le vhost Matrix sous /livekit/sfu et /livekit/jwt. Le client échange un jeton OpenID de Synapse contre un JWT LiveKit, puis envoie son média directement au SFU, hors du proxy. Requis par Element X, qui ne connaît pas l’autre pile.

matrix.nix est un lib.mkMerge de blocs indépendants :

BlocConditionContenu
Basetoujoursclient OIDC, reverse proxy, persistance
Serveurmatrix.enableSynapse, PostgreSQL, secrets communs, appservice doublepuppet
Un bloc par pontmatrix.enable && bridges.<x>.enablesecrets sops + service mautrix du pont

Chaque pont est donc débrayable individuellement (darkone.service.matrix.bridges.{whatsapp,signal,telegram,messenger,discord}.enable), ses secrets sops n’étant déclarés que si le pont est actif.

DNF s’appuie sur les modules nixpkgs services.mautrix-*, qui génèrent la registration appservice et l’enregistrent auprès de Synapse au démarrage. Deux générations de ponts cohabitent, avec des clés de configuration différentes :

PontGénérationDouble puppetingNiveau
whatsappbridgev2 (Go)double_puppet.secretsuser
signalbridgev2 (Go)double_puppet.secretsuser
messenger (meta)bridgev2 (Go)double_puppet.secretsuser
telegramlegacy (Python)bridge.login_shared_secret_mapfull
discordlegacy (Go)bridge.login_shared_secret_mapuser

Le helper mkBridgePermissions (dans matrix.nix) construit la même politique pour tous les ponts :

permissions:
"domain.tld": user # tout compte local, avec son propre compte distant
"@alice:domain.tld": admin # le matrix.admin déclaré dans etc/config.yaml

Les sessions distantes sont isolées par utilisateur Matrix : la généralisation à tous les comptes du domaine est sans risque d’interférence.

Méthode officielle appservice 🡕 : un appservice doublepuppet est enregistré dans Synapse (app_service_config_files) avec un namespace couvrant @.*:domain.tld. Son as_token autorise chaque pont à émettre des événements au nom du vrai compte de l’utilisateur.

Diagram

Le token est unique pour tous les ponts (mautrix-doublepuppet-as-token) et injecté dans chaque config par substitution de variables d’environnement (as_token:$MAUTRIX_DOUBLEPUPPET_AS_TOKEN), les modules nixpkgs passant la configuration par envsubst avec le environmentFile du service.

La registration d’un appservice est le contrat entre Synapse et le pont. Elle contient deux tokens, un par sens d’authentification :

TokenSensUsage
as_tokenpont → Synapsele pont s’authentifie sur l’API client (envoi de messages, sync…)
hs_tokenSynapse → pontSynapse s’authentifie quand il pousse les événements au pont (/transactions)

Par défaut, les modules nixpkgs génèrent ces tokens aléatoirement au premier démarrage et les stockent dans /var/lib/mautrix-*. Conséquence : toute réinitialisation du répertoire d’état change la paire de tokens, alors que Synapse — qui ne lit les registrations qu’au démarrage — garde l’ancienne en mémoire ; le pont est alors rejeté (« The as_token was not accepted »).

DNF fixe donc les deux tokens de chaque pont via sops : la registration devient une fonction pure des secrets. Réinitialiser l’état d’un pont n’invalide plus rien côté Synapse, et l’ensemble est reproductible (redéploiement, migration, restauration de sauvegarde).

La fédération Matrix est la communication serveur à serveur : un compte @alice:domain.tld dialogue avec @bob:autre.org sans compte sur l’autre serveur. DNF la rend configurable et explicite via darkone.service.matrix.federation.

Deux notions distinctes, souvent confondues :

  • Découverte : apparaître dans les annuaires distants (recherche de salons, de profils). DNF la verrouille par défaut (allow_public_rooms_over_federation = false, annuaire utilisateurs désactivé, profils privés sur la fédération). Le réseau reste joignable mais non cherchable.
  • Connectivité : la capacité d’échanger des événements avec un autre serveur. C’est ce que pilote la fédération.

Le module traduit deux options en une seule clé Synapse federation_domain_whitelist, dont la présence change le sens :

RéglageClé SynapseEffet
enable = false[ ] (liste vide)fédération totalement bloquée
enable = true; whitelist = [ ](absente)fédération ouverte à tous
enable = true; whitelist = [ "ami.org" ][ "ami.org" ]liste blanche (entrant + sortant)

La liste blanche est bidirectionnelle : hors des domaines listés, ni entrant ni sortant. C’est le réglage recommandé pour un réseau familial. La délégation (/.well-known/matrix/servermatrix.domain.tld:443) étant servie par Caddy, aucun port 8448 dédié n’est nécessaire.

Deux origines de comptes cohabitent sur le même serveur :

  • Utilisateurs déclarés : authentifiés par le SSO Kanidm (OIDC). Aucun mot de passe Matrix, aucune création de compte (voir Authentification & IDM).
  • Amis : comptes locaux à mot de passe, ouverts par inscription sur jeton (friendRegistration.enable). Sans jeton, aucune inscription possible.

Les deux types partagent les mêmes droits (ponts inclus). Le jeton se fabrique sur l’hôte avec dnf-mas, ou via l’API d’administration Synapse quand MAS est désactivé (voir le guide d’exploitation).

Surtout, les deux origines puisent dans le même espace de noms, et un identifiant Matrix est définitif : la table users de MAS impose son unicité et une désactivation ne le libère jamais. Déclarer un utilisateur dont l’identifiant est déjà pris par un ami fait donc échouer sa première connexion SSO — MAS refuse de rattacher tout seul (claims_imports.localpart.on_conflict laissé sur fail, sinon n’importe quel homonyme prendrait le contrôle du compte). Résolution : collision d’identifiant.

Optionnel (darkone.service.matrix.mas.enable), le Matrix Authentication Service 🡕 (MAS) déporte toute l’authentification hors de Synapse : connexion, comptes, sessions, portail self-service /account. C’est le socle d’Element X 🡕 (connexion par QR code) et la voie officielle, l’authentification interne de Synapse étant en fin de vie.

Diagram
  • Un seul vhost : MAS partage matrix.domain.tld. Caddy lui route la racine (pages de connexion, /account, /oauth2/*) et toute la surface de compatibilité /_matrix/client/<version>/{login,logout,refresh}, sous-chemins compris (logout/all et l’ancien login/sso/redirect des clients pré-OIDC, auxquels Synapse répond M_UNRECOGNIZED une fois délégué) ; Synapse garde /_matrix/* et /_synapse/*. Aucun sous-domaine ni DNS supplémentaire — le backend MatrixRTC s’y greffe de la même façon, sous /livekit/*.
  • Kanidm reste la source d’identité : MAS devient le client OIDC à la place de Synapse (mêmes identifiant et secret), déclaré comme fournisseur amont sous un ULID fixe. Synapse ne fait plus que demander à MAS de valider chaque token (secret partagé, loopback).
  • Découverte automatique : relié à MAS, Synapse sert lui-même auth_metadata (MSC2965 🡕). Les clients trouvent MAS sans modification du .well-known.
  • Ponts inchangés : registrations et double puppeting fonctionnent à l’identique. Seule adaptation, automatique : les ponts gèrent leurs devices e2ee via MSC4190 🡕, l’API /login d’appservice n’existant plus.
  • Amis : l’inscription sur jeton bascule de Synapse vers MAS (friendRegistration.enable reste le commutateur) ; les comptes à mot de passe continuent de fonctionner via la couche de compatibilité.

Activation et migration : voir le guide d’exploitation.

  • Secrets : déclarés par bloc dans matrix.nix (sops.secrets.*), et assemblés en fichiers d’environnement par des sops.templates.* possédés par l’utilisateur système du pont. Liste complète dans la page d’exploitation.
  • Ports : matrix, matrixTelegram et matrixDiscord sont fixés par le registre dnf/config/network.nix ; les ports appservice par défaut des autres ponts (29318, 29319, 29328) y sont listés en reserved pour éviter toute collision future.
  • Ports MatrixRTC : le registre porte aussi livekit, livekitJwt et les bornes média livekitRtcUdpStart/End. Deux valeurs par défaut amont y sont écartées volontairement : le service d’autorisation quitte 8080 (pris par headscale) et la plage média passe sous celle de coturn, qu’elle chevauchait.
  • Statuts WhatsApp : network.enable_status_broadcast = false dans la config du pont, sinon le bridge recrée indéfiniment le salon « WhatsApp Status Broadcast » pour chaque utilisateur.