Utiliser MySQL pour stocker les zones DNS avec MySQL BIND SDB

Bonjour,

Aujourd’hui, je vais vous présenter un module de BIND qui permet à celui-ci de récupérer vos zones DNS depuis une base de données MySQL.

J’ai longtemps utilisé la Soapi OVH pour ajouter des entrées à la demande (ajout de sous-domaines lors de la livraison de machines virtuelles) à mes zones.
Mais avec l’augmentation des demandes, la plupart de mes requêtes étaient refusées par l’API, et une gestion fine devenait complexe. C’est pourquoi j’ai choisi d’utiliser mes propres serveurs DNS avec un backend plus simple à gérer que des fichiers.

BIND dispose de 2 API qui permettent de modifier le backend de stockage des zones. Nous allons utiliser un module qui utilise Simple Database API (SDB). Le module à installer est MySQL BIND SDB Driver.

Le site explique relativement bien la procédure, mais je vais vous la réexpliquer pas à pas, avec les commandes complètes pour éviter d’avoir à chercher.

Compilation de BIND

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apt-get install dpkg-dev build-essential libssl-dev libmysqlclient-dev
cd /usr/local/src
wget http://downloads.sourceforge.net/project/mysql-bind/mysql-bind/mysql-bind-0.2%20src/mysql-bind.tar.gz
tar xvf mysql-bind.tar.gz
apt-get source bind9
apt-get build-dep bind9
ln -s $(pwd)/mysql-bind/mysqldb.c bind9*/bin/named/
ln -s $(pwd)/mysql-bind/mysqldb.h bind9*/bin/named/include/named/
cd bind9*
sed -i 's/\(DBDRIVER_OBJS =.*\)/\1 mysqldb.@O@/' bin/named/Makefile.in
sed -i 's/\(DBDRIVER_SRCS =.*\)/\1 mysqldb.c/' bin/named/Makefile.in
sed -i "s#\(DBDRIVER_INCLUDES =.*\)#\1 $(mysql_config --cflags)#" bin/named/Makefile.in
sed -i "s#\(DBDRIVER_LIBS =.*\)#\1 $(mysql_config --libs)#" bin/named/Makefile.in
sed -i '/include "xxdb.h"/ a\
#include "named/mysqldb.h"' bin/named/main.c
sed -i '/xxdb_init();/ a\
\tmysqldb_init();' bin/named/main.c
sed -i '/xxdb_clear();/ a\
\tmysqldb_clear();' bin/named/main.c
./configure
make
dpkg-buildpackage -us -uc
cd ..
dpkg -i *.deb

Petit résumé des opérations effectuées:

  • Installation des paquets nécessaires à la compilation
  • Récupération des sources de mysql-bind
  • Récupération des sources de bind via les dépots et installation des dépendances
  • Modification des sources de bind pour intégrer mysql-bind
  • Compilation
  • Construction du package Debian
  • Installation du package créé

Suite à ces étapes, vous avez la même configuration qu’une installation depuis les dépôts Debian, mais avec le support de MySQL.
C’est un peu plus compliqué qu’un simple make install, mais ça apporte des fichiers de configuration par défaut, le fichier dans init.d, etc.

Configuration de named.conf.local

Vu qu’on a une configuration Debian classique, c’est dans le fichier /etc/bind/named.conf.local qu’il faut ajouter notre zone.

/etc/bind/named.conf.local
1
2
3
4
zone "domain.tld" {
type master;
database "mysqldb dbname tablename hostname user password";
}

Il faut remplacer dbname par le nom de la base de données qui gère la configuration de bind, tablename par la table qui contient votre zone, ainsi que hostname, user et password avec les informations de connexions au serveur de bases de données.

Création de la base de données

On crée une table pour gérer la zone selon la structure suivante:

1
2
3
4
5
6
CREATE TABLE domain_tld (
name varchar(255) default NULL,
ttl int(11) default NULL,
rdtype varchar(255) default NULL,
rdata varchar(255) default NULL
);

Exemple de base de données

Et voilà à quoi devrait ressembler votre base de données.

1
2
3
4
5
6
7
8
9
10
11
12
INSERT INTO domain_tld VALUES ('mydomain.com', 259200, 'SOA', 'mydomain.com. www.mydomain.com. 200309181 28800 7200 86400 28800');
INSERT INTO domain_tld VALUES ('mydomain.com', 259200, 'NS', 'ns0.mydomain.com.');
INSERT INTO domain_tld VALUES ('mydomain.com', 259200, 'NS', 'ns1.mydomain.com.');
INSERT INTO domain_tld VALUES ('mydomain.com', 259200, 'MX', '10 mail.mydomain.com.');
INSERT INTO domain_tld VALUES ('w0.mydomain.com', 259200, 'A', '192.168.1.1');
INSERT INTO domain_tld VALUES ('w1.mydomain.com', 259200, 'A', '192.168.1.2');
INSERT INTO domain_tld VALUES ('mydomain.com', 259200, 'Cname', 'w0.mydomain.com.');
INSERT INTO domain_tld VALUES ('mail.mydomain.com', 259200, 'Cname', 'w0.mydomain.com.');
INSERT INTO domain_tld VALUES ('ns0.mydomain.com', 259200, 'Cname', 'w0.mydomain.com.');
INSERT INTO domain_tld VALUES ('ns1.mydomain.com', 259200, 'Cname', 'w1.mydomain.com.');
INSERT INTO domain_tld VALUES ('www.mydomain.com', 259200, 'Cname', 'w0.mydomain.com.');
INSERT INTO domain_tld VALUES ('ftp.mydomain.com', 259200, 'Cname', 'w0.mydomain.com.');

Conclusion

Vous pouvez maintenant stocker vos zone DNS directemment dans MySQL, et ne plus avoir besoin de modifier les fichiers de configuration.

En revanche, j’ai relevé deux inconvénients:

  • Chaque requête DNS provoque un accès MySQL, sur un serveur à forte charge, cela peut-être problématique.
  • Dans le cas d’une architecture master/slave, le master ne fait pas de notification aux slaves en cas de changement, il faut donc baisser le temps de validité de la zone au niveau du champ SOA (ne descendez pas en dessous de 20 minutes). Les slaves conserveront un cache pendant le temps de validité et iront recharger la configuration depuis le master lorsque le temps est écoulé.

Bonus

Et en bonus, une classe PHP qui vous permet de gérer vos zones, avec mise à jour du serial du champ SOA.

Dns.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php
class Dns
{
private $_dbh;
public function __construct($connection)
{
$this->_dbh = $connection;
}
private static function name2table($name) {
return str_replace('.', '_', $name);
}
private function upsert($table, $name, $rdtype, $rdata) {
$stmt = $this->_dbh->prepare('SELECT * FROM '.$table.' WHERE name = ?');
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->execute();
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
try {
$this->_dbh->beginTransaction();
if($res && count($res) > 1) {
$stmt = $this->_dbh->prepare('DELETE FROM '.$table.' WHERE name = ?');
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->execute();
}
if($res && count($res) === 1) {
if($res[0]['rdtype'] != $rdtype || $res[0]['rdata'] != $rdata) {
$stmt = $this->_dbh->prepare('UPDATE '.$table.'
SET rdtype = ?, rdata = ? WHERE name = ?');
$stmt->bindValue(1, $rdtype, PDO::PARAM_STR);
$stmt->bindValue(2, $rdata, PDO::PARAM_STR);
$stmt->bindValue(3, $name, PDO::PARAM_STR);
$stmt->execute();
}
} else {
$stmt = $this->_dbh->prepare('INSERT INTO '.$table.'
SET name = ?, ttl = 3600, rdtype = ?, rdata = ?');
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $rdtype, PDO::PARAM_STR);
$stmt->bindValue(3, $rdata, PDO::PARAM_STR);
$stmt->execute();
}
$this->_dbh->commit();
return true;
} catch(PDOException $e) {
$this->_dbh->rollBack();
return false;
}
}
public function ajoutSousDomaine($domain, $name, $ip) {
return $this->upsert(
self::name2table($domain),
$name . '.' . $domain,
'A',
$ip
);
}
public function incSerial($domain) {
$stmt = $this->_dbh->prepare('SELECT rdata FROM '.self::name2table($domain).' WHERE rdtype = ? LIMIT 1');
$stmt->bindValue(1, 'SOA', PDO::PARAM_STR);
$stmt->execute();
$res = $stmt->fetch(PDO::FETCH_ASSOC);
$rdata = explode(' ',$res['rdata']);
$begin = substr($rdata[2], 0, -2);
$end = substr($rdata[2], -2);
$date = date("Ymd");
if($begin == $date) {
$end = sprintf("%02d",(intval($end) + 1) % 100);
} else {
$begin = $date;
$end = "01";
}
$rdata[2] = $begin.$end;
$rdata = implode(' ',$rdata);
$stmt = $this->_dbh->prepare('UPDATE '.self::name2table($domain).' SET rdata = ? WHERE rdtype = ?');
$stmt->bindValue(1, $rdata, PDO::PARAM_STR);
$stmt->bindValue(2, 'SOA', PDO::PARAM_STR);
$stmt->execute();
}
}