Давно порывался найти какое-либо бесплатное и толковое решение для хранения доменных зон в базе данных, и управлять всем этим с лёгкостью. В интернете безумное множество решений, начиная от бесплатных, заканчивая платными и дорогими. Но, к сожалению ни одно из них не оправдало моих надежд. Какие-то продукты были кривые и не управляемые, какие-то не могли использоваться для чего-либо ещё. В конечном итоге нашёл время и написал свой скрипт, который грузит данные о домене и записывает их в фай для named.
Скрипт написан на PHP и пусть кто-то говорит, что данный язык программирования и языком назвать сложно, но он работает, а это главное! Будем считать, что у Вас уже есть машина, где крутится сервер имён, установлен PHP и база данных MySQL. Я писал структуру базы данных не только для named, но и для использования другими программами, такими как, dovecot, postfix, ftp и т.д. В конечном итоге должна будет получится панелька управления
И так структура таблицы:
CREATE TABLE IF NOT EXISTS `bill_domains` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(10) unsigned NOT NULL DEFAULT '0',
`domain` varchar(150) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;
INSERT INTO `bill_domains` (`id`,`client_id`,`domain`,`status`) VALUES ('1','1','mydomain.ru','1');
CREATE TABLE IF NOT EXISTS `bill_dns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(10) unsigned NOT NULL DEFAULT '0',
`domain_id` int(10) unsigned NOT NULL DEFAULT '0',
`name` varchar(75) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`type` enum('A','AAAA',’CNAME’,’PTR’,'NS','MX','TXT','SPF') COLLATE utf8_unicode_ci NOT NULL DEFAULT 'A',
`data` varchar(150) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`aux` int(10) unsigned NOT NULL DEFAULT '0',
`status` int(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
KEY `domain_id` (`domain_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;
INSERT INTO `bill_dns` (`id`, `client_id`, `domain_id`, `name`, `type`, `data`, `aux`, `status`) VALUES
(1, 1, 1, 'www', 'A', '192,168.1.1', 0, 1);
CREATE TABLE IF NOT EXISTS `bill_dns_log` (
`domain_id` int(10) NOT NULL,
`time` int(10) NOT NULL,
`flag` int(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`domain_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Я убрал из структуры лишние поля, что бы не мешали понять суть работы скрипта, оставил только самое необходимое.
После того как мы создали три таблицы, у нас есть куда сложить сами домены, записи типа: А, АААА, NS, CNAME, MX, PTR, SPF и таблица, в которой будут записи с индексом домена, отпечатком времени, когда были сделаны изменения и флаг, сообщающий нам, применились ли изменения.
Дальше мы будем писать на PHP.
Файл для соединения с базой данных и работой с данными целиком приводить не стану, только лишь опишу методы, с которыми будет связана дальнейшая работа.
MySQL.php:
class mysql {
// логин к ДБ
var $sql_login;
// пароль к ДБ
var $sql_passwd;
// имя ДБ
var $sql_database;
// MySQL хост
var $sql_host;
// MySQL идентификатор соединения
var $con_id;
// соединяемся с ДБ
function connect() {
$this->con_id = mysql_connect($this->sql_host,$this->sql_login,$this->sql_passwd);
mysql_select_db($this->sql_database,$this->con_id);
mysql_query("SET NAMES `utf-8`;",$this->con_id);
mysql_query("SET CHARSET utf-8;",$this->con_id);
if (!$this->con_id){
die("Not connected");
}
}
// Выполняем запрос
function query($query, $file = false, $line = false) {
if ( !$query ) {
die("Нет запроса для выполнения");
} else {
$r = mysql_query($query,$this->con_id) or trigger_error(mysql_error());
}
// Если запрос предназначен для вставки данных возвращаем ИД ряда, в противном случае возвращаем сам результат.
return (preg_match("/^insert/i",$query) ? $this->l_id() : $r);
}
function listing($query, $file = false, $line = false){
// получаем список записей по запросу и складываем их в нормальный массив.
// если кому-то будет надо, поделюсь кодом этого файла.
return $listing;
}
// получаем последний индекс от последнего INSERT запроса
function l_id() {
return mysql_insert_id($this->con_id);
}
// закрываем соединение
function close() {
mysql_close($this->con_id);
}
Целиком не стал приводить код этого файла т.к. он немного шире и используется не только в описываемом сценарии.
Файл настроек для работы скрипта:
config.php:
<?php
$dbuser = "…"; //имя пользователя для соединения с ДБ
$dbpass = "…"; //пароль для соединения с ДБ
$dbhost = "…"; // имя хосьа ДБ
$dbname = "…"; // имя самой ДБ
$dbpref = "…"; // Префикс названия таблиц, я стараюсь всегда использовать префиксы. Исторически так сложилось.
$timeout = 180; // кол-во секунд за которое будем смотреть изменения в ДБ
// Параметры для named
$global_ttl = 1200; // seconds. 1200 = 20 min; 1800 = 30 min; 3600 = 1h; 86400 = 24h;
$chroot_zone = "zones/"; // директория где будут лежать файлы зон
$zone_folder = dirname(__FILE__)."/zones/"; // абсолютный путь
$zone_config = dirname(__FILE__)."/zones.conf"; // конфигурационный файл
$server_name = "ns.mydomain.ru"; // имя сервера на котором запускается скрипт
$other_ns = array("ns3. mydomain.ru ","ns1. mydomain.ru ","ns2.mydomain.ru"); // список всех наших ДНС серверов
$mail_server = array(10=>"mail.mydomain.ru"); //приоритетный почтовый сервер для всех доменов, если не указан другой
$log_file = dirname(__FILE__)."/error.log"; // логфайл
$zone_master = "hostmaster"; // имя администратора зоны
// цифровые значения для зон. Могут быть разными. Я выбрал такие.
$refresh = 100;
$retry = 180;
$expire = 86400;
$negative = 3600;
?>
Теперь собственно то, ради чего писалась вся эта статья dns.php:
#!/usr/local/bin/php -f
<?php
// устанавливаем рабочую директорию
chdir( dirname( __FILE__ ) );
// грузим необходимые файлы
require_once "mysql.php";
require_once "config.php";
// функция для записи в лог файл
function logf($message, $file, $line){
global $log_file;
$handle = fopen($log_file, "a");
fwrite($handle, "[".date("d.m.Y H:i:s")."] Error: ".$message." In file: ".$file." at ".$line."n");
fclose($handle);
}
// функция создания файла зоны
function create_zone($zone){
global $zone_folder;
touch($zone_folder.$zone);
return true;
}
// функция создания т.н. заголовка для файла зоны
function set_header($zone, $counter = 1){
global $global_ttl, $server_name, $zone_master, $refresh, $retry, $expire, $negative;
$header = "$TTL ".$global_ttl.";n";
$header .= $zone.".tINtSOAt".$server_name.".t".$zone_master.".".$zone.". (n";
$header .= "tt".date("Ymd").sprintf("%02d",$counter)."; Serialn";
$header .= "tt".$refresh."; Refresh periodn";
$header .= "tt".$retry."; Retryn";
$header .= "tt".$expire."; Expiren";
$header .= "tt".$negative."; Negative cachingn";
$header .= "t)n";
return $header;
}
// функция для указания почтовых серверов для домена
function set_mx($servers = false){
global $mail_server;
$content = "";
if (!$servers || !is_array($servers)){ // если небыло передано ни одного параметра, устанавливаем значение по умолчанию
foreach ($mail_server as $k => $v){
$content .= "tIN MX ".$k." ".$v.".n";
}
} else { // в противном случае, указываем что было передано в виде параметра, пример: $servers = array("10"=>"mx.google.com","20"=>"mx2.google.com")
foreach ($servers as $k => $v){
$content .= "tIN MX ".$k." ".$v.".n";
}
}
return $content;
}
function set_ns($servers = false){ // определяем на каких ДНС серверах будет висеть наш домен. По сути всё тоже что и для почтового сервера.
global $server_name, $other_ns;
$content = "tIN NS ".$server_name.".n";
if (!$servers || !is_array($servers)){
for($i=0;$i<count($other_ns);$i++){
$content .= "tIN NS ".$other_ns[$i].".n";
}
} else {
for($i=0;$i<count($servers);$i++){
$content .= "tIN NS ".$servers[$i].".n";
}
}
return $content;
}
// функция записи всех данных в файл зоны
function write_zone($content, $zone){
global $zone_folder;
$handle = fopen($zone_folder.$zone, "w");
fwrite($handle, $content);
fclose($handle);
}
$parsed_zones = array(); // здесь будут все имена доменов которые на данный момент уже есть в конфиге zones.conf
$new_zones = array(); // список новых доменов
// парсим конфиг с существующими зонами
$handle = fopen($zone_config, "r");
$zone_content = "";
while (!feof($handle)){
$zone_content .= fread($handle, 8192);
}
fclose($handle);
$lines = explode("n",$zone_content);
foreach($lines as $k => $v){
if (preg_match("/zones"(w+)"/i",$v, $matches)){
$parsed_zones[] = $matches[1];
}
}
// соединяемся с ДБ
$db = new mysql();
$db->sql_login = $dbuser;
$db->sql_passwd = $dbpass;
$db->sql_database = $dbname;
$db->sql_host = $dbhost;
$db->connect();
define("DB_TABLE_PREFIX", $dbpref);
$fetch_all = false;
if (!file_exists($zone_folder)){ // если директория с файлами зон не найдена, пытаемся её создать
$fetch_all = true; // и указываем, что будем выкачивать все зоны из ДБ
if (!mkdir($zone_folder, 0644, true)){
logf("Can not create directory ".$zone_folder, __FILE__, __LINE__);
exit;
}
}
if ($fetch_all){ // Выкачиваем все активные домены от активных клиентов
$sql = "SELECT
m.id,
m.domain
FROM `".DB_TABLE_PREFIX."domains` AS m
LEFT JOIN `".DB_TABLE_PREFIX."clients` AS c ON (m.client_id = c.id)
WHERE
m.status = '1' and
c.status = '1'
ORDER BY m.domain ASC;";
$list = $db->listing($sql, __FILE__, __LINE__); // получаем результат в виде массива
if ($list){ // если массив не пустой и там есть домены, получаем список НС записей к каждому домену
$c = count($list);
for($i=0;$i<$c;$i++){
$sql = "SELECT
m.name,
m.type,
m.data,
m.aux
FROM `".DB_TABLE_PREFIX."dns` AS m
WHERE
m.status = '1' and
m.domain_id = '".$list[$i]["id"]."'";
$list[$i]["zones"] = $db->listing($sql, __FILE__, __LINE__);
}
for ($i=0;$i<$c;$i++){
$ns_flag = false;
$mx_flag = false;
$mail = array();
$ns = array();
$source = "";
if (!$list[$i]["zones"]) continue;
if (!in_array($list[$i]["domain"], $parsed_zones)){
$new_zones[] = $list[$i]["domain"];
}
// раскладываем все данные по полочкам, что бы понимать какие данные у домена уже есть
$z = count($list[$i]["zones"]);
for($zi=0;$zi<$z;$zi++){
if ($list[$i]["zones"][$zi]["type"]=="MX"){ // если есть МХ запись, устанавливаем соответствующий флаг
$mx_flag = true;
$mail[$list[$i]["zones"][$zi]["aux"]] = $list[$i]["zones"][$zi]["data"];
}
if ($list[$i]["zones"][$zi]["type"]=="NS"){ // тоже что и МХ но для NS записей
$ns_flag = true;
$ns[] = $list[$i]["zones"][$zi]["data"];
}
if ( // здесь собираем остальные данные
$list[$i]["zones"][$zi]["type"]=="A" ||
$list[$i]["zones"][$zi]["type"]=="AAAA" ||
$list[$i]["zones"][$zi]["type"]=="CNAME"
){
$source .= $list[$i]["zones"][$zi]["name"]."t".($list[$i]["zones"][$zi]["type"]=="CNAME" ? "CNAME" : "IN ".$list[$i]["zones"][$zi]["type"])." ".$list[$i]["zones"][$zi]["data"]."n";
}
}
// ну и собственно, пишем данные в файл.
$content = "";
create_zone($list[$i]["domain"]);
$content .= set_header($list[$i]["domain"], $i);
$content .= set_ns($ns);
$content .= set_mx($mail);
$content .= "n";
$content .= $source;
write_zone($content, $list[$i]["domain"]);
}
} else {
logf("No domain names for downloading", __FILE__, __LINE__);
}
} else { // если директория с файлами зон существует, получаем только список доменов у которых были изменения. Всё остальное будет как описано выше, только для изменённоых доменов
$sql = "SELECT
m.domain_id,
d.domain,
d.id
FROM `".DB_TABLE_PREFIX."dns_log` AS m
LEFT JOIN `".DB_TABLE_PREFIX."domains` AS d ON (m.domain_id = d.id)
LEFT JOIN `".DB_TABLE_PREFIX."clients` AS c ON (d.client_id = c.id)
WHERE
m.flag = '0' and
m.time >= '".(time()-$timeout)."' and
d.status = '1' and
c.status = '1'
ORDER BY d.domain ASC";
$list = $db->listing($sql, __FILE__, __LINE__);
if ($list){
$c = count($list);
for($i=0;$i<$c;$i++){
$sql = "SELECT
m.name,
m.type,
m.data,
m.aux
FROM `".DB_TABLE_PREFIX."dns` AS m
WHERE
m.status = '1' and
m.domain_id = '".$list[$i]["id"]."'";
$list[$i]["zones"] = $db->listing($sql, __FILE__, __LINE__);
}
for ($i=0;$i<$c;$i++){
$ns_flag = false;
$mx_flag = false;
$mail = array();
$ns = array();
$source = "";
if (!$list[$i]["zones"]) continue;
if (!in_array($list[$i]["domain"], $parsed_zones)){
$new_zones[] = $list[$i]["domain"];
}
$z = count($list[$i]["zones"]);
for($zi=0;$zi<$z;$zi++){
if ($list[$i]["zones"][$zi]["type"]=="MX"){
$mx_flag = true;
$mail[][$list[$i]["zones"][$zi]["aux"]] = $list[$i]["zones"][$zi]["data"];
}
if ($list[$i]["zones"][$zi]["type"]=="NS"){
$ns_flag = true;
$ns[] = $list[$i]["zones"][$zi]["data"];
}
if (
$list[$i]["zones"][$zi]["type"]=="A" ||
$list[$i]["zones"][$zi]["type"]=="AAAA" ||
$list[$i]["zones"][$zi]["type"]=="CNAME"
){
$source .= $list[$i]["zones"][$zi]["name"]."t".($list[$i]["zones"][$zi]["type"]=="CNAME" ? "CNAME" : "IN ".$list[$i]["zones"][$zi]["type"])." ".$list[$i]["zones"][$zi]["data"]."n";
}
}
$content = "";
create_zone($list[$i]["domain"]);
$content .= set_header($list[$i]["domain"], $i);
$content .= set_ns($ns);
$content .= set_mx($mail);
$content .= "n";
$content .= $source;
write_zone($content, $list[$i]["domain"]);
$sql = "UPDATE `".DB_TABLE_PREFIX."dns_log` SET `flag` = '1' WHERE `domain_id`='".$list[$i]["domain_id"]."'";
$db->query($sql, __FILE__, __LINE__);
}
}
}
if (is_array($new_zones) && count($new_zones)>0){
$handle = fopen($zone_config,"w");
$new_array = array();
foreach($parsed_zones as $k => $v){
$new_array[] = $v;
}
foreach($new_zones as $k => $v){
$new_array[] = $v;
}
natsort($new_array);
$content = "";
foreach($new_array as $k => $v){
$content .= "zone "".$v."" {n";
$content .= "ttype master;n";
$content .= "tfile "".$chroot_zone.$v."";n";
$content .= "};nn";
}
fwrite($handle, $content);
fclose($handle);
}
shell_exec('rndc reload');
?>
В данном примере, я не стал пока описывать проверку записей типа SPF и углубляться в регулярные выражения, на предмет всевозможных уязвимостей, поскольку такого рода проверки у меня идут ещё на стадии добавления данных в ДБ. Именно по этой причине я уверен, что получаю оттуда проверенные данные, которые не могут нанести урон серверу.
Дальше устанавливаем chmod + x dns.php и проверяем наш скрипт.
Если в логфайле не появилось страшных надписей с угрозами, можем прописать его в кронтаб.
Поскольку, в конфиге у нас указано 180 секунд, а это 3 минуты, то запись в кроне должна быть примерно следующей
*/3 * * * * root /путь/до/dns.php
P.S. Чуть не забыл!
В самом конфиге named.conf, в самом конце ставим такую строку:
include "zones.conf";
Внутри этого файла, после срабатывания скрипта должны быть такого содержания строки:
zone «mydomain.ru» {
type master;
file «zones/maidomain.ru»;
};
Ну и собственно в файле с одноимённым названием будет конфиг нашей зоны:
$TTL 1200; 5 min
mydomain.ru. IN SOA ns.mydomain.ru. hostmaster.mydomain.ru. (
2012070591 ; Serial
300 ; refresh
600 ; retry
8640000 ; expire
300) ; minimum
IN NS ns.mydomain.ru.
IN MX 10 mail.mydomain.ru.
www IN A 192.168.1.1
Данная статья не претендует на какие-либо титулы, а рассчитана на тех, кто хочет хранить свои домены в ДБ у управлять ими не из консоли и в то же время не использовать не понятные, платные продукты.
Комментарии, вопросы и рекомендации приветствуются. Любые замечания по статье будут учтены.
Автор: intrade