Automatic zone replication with BIND9 in Plesk 11.5 + Debian Wheezy + PHP

This tutorial is aimed at showing how to set up zone replication from a master to a slave server, without the help of the user input on Debian Wheezy with Plesk 11.5 and PHP

Let’s decide how to call the servers we are gonna use in the setup.

  • master

Main server, that hosts Plesk and BIND9 DNS server in Master mode

  • slave

Slave DNS server that hosts BIND9 DNS server in Slave mode

To be able to get a list of available domains on the main server we need mysql-client, on the DNS server, the reason for it is so we can get a list of domains from the primary server, so we can check if they exist or not so we can create them if necesseary.

So let’s install the client and some other requirements on the slave server, we can do so really easy by issuing the following command:

apt-get install mysql-client php5-cli php-pear php5-mysql

Once this is complete let’s go to the masters server and configure mysql to allow access from the slave server.

First thing first, let’s connect to the MySQL server on the master and create a user that will be only for viewing the domains records on the server.

If you need passwords you can use pwgen to generate random passwords, let’s generate 4 passwords with length 50.

$ pwgen 50 4
aed6Chu5AiphieJaeteirawei7yohrahZ2zaig8Eey9gePh4jo
ohw0sheiPhae1yae0eeghiesh8aevee5aineiyequae8ookaev
noh4icaigh2weiGaiph3sahDoeThaequ5HeGha2oeMeezee6re
Xaeb3uN6nib5cae2yotooz5quai5zohx8OhSah7caeshu3xeQu

Now let’s execute this SQL command to create a user for our case

CREATE USER 'dns'@'%' identified by 'noh4icaigh2weiGaiph3sahDoeThaequ5HeGha2oeMeezee6re';
GRANT USAGE ON *.* TO 'dns'@'%' with MAX_QUERIES_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
GRANT SELECT ON psa.domains TO 'dns'@'%';
FLUSH PRIVILEGES;

These commands will enable you to query only the domains tables and nothing else, in case someone manages to find out the password they will only be able to query the domains we have. In our case this will allow a connection from anywhere if identified by the password, if you want to be more restrictive you can always specify the IP instead of % in the SQL.

Before we start we need to make sure we can add new zones in the system without restarting the server, to do so, we need to add to the options part of named.conf, the option to allow new zones, so we edit the file and add the following option in the config file.

allow-new-zones yes;

Now we can use rndc to create, update, read, delete the zones

Now, on to the next problem, we have the data, now we need to generate the zones when there is something new to add, we can do this by creating a script that is gonna run every 15 mins and query the dataset to check if there is new data that we need to input. We can do this simply in PHP, at the moment this is simplest took me 4 mins to write the whole code, so let’s take a look at the finished script:

The property parentDomainId, which I use to check which domains are TLD, at the moment, this script has been running for ~30 days and so far no issues at all.

#!/usr/bin/env php
<?php
define("PSA_USER", 'dns');
define("PSA_PASSWORD", 'noh4icaigh2weiGaiph3sahDoeThaequ5HeGha2oeMeezee6re');
define("PSA_DB", "psa");
define("PSA_HOST", "master");
define("BIND_VIEW", "slave");
define("PSA_SQL", "select name from domains where parentDomainId = 0");
define("LOCAL_DOMAIN_CACHE", "/opt/zones.cache");
define("NS_IPS", "10.0.0.1;");
define("TEMPLATE", "%%domain%% '{type slave; file \"slave/%%domain%%\"; masters { " . NS_IPS . " }; };'");

openlog("slave.configurator.php", LOG_PID | LOG_PERROR, LOG_LOCAL0);

$cache = array();

if (file_exists(LOCAL_DOMAIN_CACHE)) {
    $cache = json_decode(file_get_contents(LOCAL_DOMAIN_CACHE), true);
    if (is_null($cache)) {
        $cache = array();
    }
}

$link = mysqli_connect(PSA_HOST, PSA_USER, PSA_PASSWORD, PSA_DB) or die("Error " . mysqli_error($link));

if (!$link) {
    $err = mysqli_error($link);
    syslog(LOG_ERR, $err);
    die("Error: $err");
}

$result = $link->query(PSA_SQL);

$database_domains = array();

while ($domain = mysqli_fetch_assoc($result)) {
    array_push($database_domains, $domain['name']);
}

$process = array_diff($database_domains, $cache);
$full = array_unique(array_merge($database_domains, $process));

file_put_contents(LOCAL_DOMAIN_CACHE, json_encode($full));

$format = "Y-m-d H:i:s - ";

syslog(LOG_INFO,
        date($format, microtime(true)) . "Total available domains in cache: " . count($cache));
syslog(LOG_INFO,
        date($format, microtime(true)) . "Total available domains in database: " . count($database_domains));
syslog(LOG_INFO,
        date($format, microtime(true)) . "Difference elements between cache and database: " . count($process) . ", domains: " . (count($process) > 0 ? join(",",
                        $process) : "None available"));
syslog(LOG_INFO,
        date($format, microtime(true)) . "Total domains in system: " . count($full));

if (count($process) > 0) {
    foreach ($process as $domain) {
        echo date($format, microtime(true)) . "Processing domain: $domain\n";
        $template = str_replace("%%domain%%", $domain, TEMPLATE);
        syslog(LOG_INFO, "/usr/sbin/rndc addzone $template");
        exec("/usr/sbin/rndc addzone $template");
    }
    exec("/usr/sbin/rndc reload");
} else {
    syslog(LOG_INFO, date($format, microtime(true)) . "Nothing to process.");
}

NS_IPS, is a ; separated list of all your nameservers, you should put your IP of your master nameserver here, in the demo script I put 10.0.0.1, but you should replace it with your own master nameserver, the IP should always have a ; at the end of the IP in the script it was 10.0.0.1;

If /usr/sbin/rndc is not the correct path for rndc, you should change the script accordingly;

Now you can add it to crontab this script I saved mine under /opt/slave.configurator.php, and my crontab entry is like so, I run this script under root.

*/15 * * * * /opt/slave.configurator.php

Just don’t forget to run:

chmod +x /opt/slave.configurator.php