J'ai toujours pensé que la meilleure manière de comprendre vraiment une technologie consiste à la pousser dans des directions stupides et impraticables, et à voir ce qui casse. Inspiré par l'affirmation de Corey Quinn selon laquelle Amazon Route 53 serait en fait une base de données, je me suis demandé : et l'inverse ? Si DNS peut servir de base de données, quelle est la façon la plus absurde d'implémenter un serveur DNS ?
Cette pensée m'a mené à ce projet : utiliser les metadata de Stripe comme datastore pour un serveur DNS pleinement fonctionnel.
Les metadata
Quand on travaille avec Stripe, les metadata sont un concept central. Elles simplifient considérablement le lien entre le modèle de données de Stripe et celui de votre propre application. Identifiants de commande, appartenance à un programme de fidélité, ID de schémas de parrainage - Stripe ne sait pas (et n'a pas à savoir) ce que ces concepts représentent dans votre application, mais nous laisse stocker l'information sur les objets Stripe pour faciliter le rapprochement des deux systèmes.
On peut s'en servir pour étiqueter un client avec un ID interne :
"internal_user_id": "abcd1234"
Ou pour suivre qui a déclenché un remboursement :
"refunded_by": "support_team_bot"
En fin de compte, les metadata sont un simple key-value store dans lequel on peut stocker des valeurs arbitraires sur les objets Stripe. Customers, Subscriptions, Payment Intents, Products, etc. - tous les objets centraux de Stripe disposent d'un champ metadata, dans lequel on peut stocker jusqu'à 50 paires clé-valeur. Bien que les metadata Stripe ne soient qu'un key-value store, avec un peu d'aplatissement et de sérialisation (via des techniques comme JSON.stringify()), on peut les contraindre à stocker des records DNS structurés - à condition d'être prêt à malmener quelques bonnes pratiques en chemin.
Les records DNS sont essentiellement une correspondance record -> emplacement, ce qui ressemble fortement à quelque chose qu'on pourrait modéliser avec du clé-valeur.
Mettre en place le serveur DNS
Pour le serveur DNS, on va utiliser dns2. Cela nous permet d'écouter les requêtes DNS et de construire les réponses :
const dns2 = require('dns2')
class StripeDnsServer {
constructor(options = {}) {
this.stripeClient = options.stripeClient || stripe
this.port = options.port || 5333
this.address = options.address || '0.0.0.0'
this.server = this._createServer()
}
_createServer() {
return dns2.createServer({
udp: true,
handle: this._handleRequest.bind(this),
})
}
start() {
this.server.listen({
udp: { port: this.port, address: this.address, type: 'udp4' },
tcp: { port: this.port, address: this.address },
})
return this
}
}
const dnsServer = new StripeDnsServer({ port: 5333 }).start()
Cela écoute sur le port 5333 (UDP et TCP) et passe les requêtes entrantes à une méthode _handleRequest, qu'il nous reste à définir.
Structurer les records DNS pour le stockage en metadata
En DNS, on a typiquement des records de la forme suivante :
| Type | Nom | Contenu | Préférence |
|---|---|---|---|
| A | @ | 192.168.1.2 | |
| A | www | 192.168.1.3 | |
| MX | @ | mail01.google.com | 10 |
| CNAME | blog | blog.wordpress.com. |
Idéalement, on voudrait représenter cela dans un format structuré comme JSON :
{
"@": { "A": ["192.168.1.2"] },
"www": { "A": ["192.168.1.3"] },
"blog": { "CNAME": ["blog.wordpress.com"] },
"_mx": {
"MX": [{ "preference": 10, "exchange": "mail01.google.com" }]
}
}
Premier problème en mappant cela aux metadata Stripe : on a plus qu'une simple relation clé-valeur. Les metadata Stripe ne supportent pas les structures complexes, les tableaux et structures imbriquées ne passent pas tels quels. C'est strictement clé-valeur, où la valeur est une chaîne de caractères (jusqu'à 500 caractères).
Cette limite ne nous arrête pas, elle nous oblige juste à aplatir les données nous-mêmes via JSON.stringify() avant stockage, puis à les re-déplier via JSON.parse() à la lecture. On stocke la structure JSON entière sous forme de chaîne, sous une seule clé de metadata, dns_records :
metadata.dns_records: '{"blog":{"CNAME":["blog.example.net."]},"www":{"A":["192.168.1.3"]},"@":{"A":["192.168.1.2"],"MX":[{"priority":10,"exchange":"mail1.example.com."}]}}'
Pour associer cela à un domaine, on ajoute une autre clé de metadata : dns_domain = example.com.
Stocker la donnée dans Stripe
Sur quel objet attache-t-on ces metadata ? Les Customer Stripe sont parfaits. On peut les créer via l'API sans avoir besoin de moyens de paiement ou de données financières réelles. On utilise effectivement Stripe comme une base clé-valeur (très inhabituelle) en free tier.
const customer = await stripe.customers.create({
name: 'DNS records for example.com',
metadata: {
dns_domain: 'example.com',
dns_records: '{"www":{"A":["192.168.1.3"]}, ... }'
}
});
Le piège de l'API Search
Le premier réflexe pour rechercher des records pourrait être l'API Search de Stripe :
const customers = await stripe.customers.search({
query: "metadata['dns_domain']:'${domain}'"
});
Cela semble correct. Sauf qu'il y a un défaut critique : l'API Search de Stripe est en cohérence à terme (eventually consistent). La documentation déconseille explicitement de l'utiliser dans des scénarios read-after-write. En conditions normales, la donnée devient cherchable en moins d'une minute, mais des délais de propagation peuvent survenir lors d'incidents.
Un délai potentiel d'une minute entre la création/mise à jour d'un record DNS et sa résolution effective n'est pas acceptable pour notre serveur DNS. Il nous faut un mécanisme read-after-write fiable et immédiat.
Le contournement par l'API List
Les opérations list standard sur les objets Stripe (comme stripe.customers.list) sont strongly consistent sur certains filtres. On ne peut pas filtrer directement sur des metadata arbitraires en list, mais on peut filtrer sur des champs standard comme email.
D'où le contournement : à la création du Customer, on stocke un identifiant unique pour le domaine dans le champ email. Utilisons dns@ suivi du nom de domaine :
const customer = await stripe.customers.create({
name: `DNS records for ${domain}`,
email: `dns@${domain}`,
metadata: { dns_domain: domain, dns_records: '...' }
});
On peut désormais implémenter la lookup via stripe.customers.list filtré par email, ce qui nous donne une cohérence immédiate.
Limites (il y en a beaucoup)
- Taille des metadata : la limite de 500 caractères par valeur est une contrainte dure. Les zones complexes la dépassent vite.
- Vitesse : c'est lent. Les appels à l'API Stripe sont généralement rapides, mais pour un serveur DNS, des temps de réponse ultra-faibles sont essentiels.
- Aucun cache : aucun cache DNS n'est mis en place côté serveur. Chaque requête frappe l'API Stripe.
- Types de records limités : seuls A, CNAME et MX sont implémentés ici.
- Gestion d'erreurs / tests : c'est du code de proof-of-concept. La gestion d'erreurs, la validation d'entrée et les tests sont minimaux.
Si l'idée vous a effleuré de mettre cela en production, vous représentez un danger pour vous-même et pour les autres.
Mais à quoi tout cela sert-il ?
Est-ce une bonne idée ? Non. Une option viable si vous êtes pressé ? Non plus. Une application pratique de cette technologie ? Absolument pas - c'est le projet de blague le plus blagueur qui soit.
Ce projet n'est pas de qualité production (s'il vous plaît, non), mais il met en lumière :
- À quel point les metadata Stripe peuvent être détournées pour stocker de la donnée structurée.
- Les limites du stockage clé-valeur plat, et comment les contourner.
- L'importance de comprendre les modèles de cohérence des APIs (Search vs List).
- Que les « mauvaises idées » sont souvent les meilleurs supports d'apprentissage.
L'objectif ici est de montrer à quel point les metadata sont une technologie puissante et flexible - si elles peuvent porter un serveur DNS fonctionnel (quoique terrible), imaginez les usages bien plus pratiques que vous pouvez en tirer dans vos propres workflows liés au paiement.