Служба доменных имен (DNS)
Несмотря на то, что NIS и WINS крайне полезны, им все же недостает некоторых свойств, что делает их непригодными для использования во «всем Интернете».
Масштабируемость
Хотя эти схемы применяются к нескольким серверам, каждый сервер должен обладать полной копией информации о топологии сети.1 Такую информацию следует скопировать на каждый сервер, а этот процесс требует времени, если сеть становится достаточно большой. Кроме того, WINS страдает из-за своей динамической модели регистрации. Некоторое число WINS-клиентов своими регистрационными запросами может расплавить от перегрузки любое количество WINS-серверов для всего Интернета.
Управление
До сих пор мы говорили только о технических аспектах, но это не единственная сторона администрирования. NIS. в особенности, требует единственного центра администрирования. Тот, кто управляет главным сервером, управляет и всем NIS-домеиом, который этот сервер «возглавляет». Любые изменения в пространстве сетевых имен должны пройти через такого сторожа. Этот принцип не будет работать в пространстве имен размером во весь Интернет.
Для борьбы с недостатками, присущими сопровождению файлов узлов или NIS/NIS+/WINS-noflo6HbiM системам, была создана новая модель под названием служба доменных имен (DNS). В DNS пространство имен сети разделено на несколько «доменов верхнего уровня». Любой из них можно разделить на домены меньшего размера и т. д. В каждой точке деления следует назначить сторону, ответственную за контроль над этой частью пространства имен, что позволяет разобраться с вопросами администрирования.
Клиенты в сети обращаются к ближайшему по иерархии серверу имен. Если информацию, которую ищет клиент, можно найти на данном локальном сервере, она возвращается клиенту. В большинстве сетей основная часть запросов, касающихся разыменования адресов, относится к машинам из той же сети, поэтому локальные серверы обрабатывают большую часть локального трафика. Это позволяет избавиться от проблемы масштабируемости. Можно настроить несколько DNS-cepверов (также известных как вторичные или подчиненные), чтобы распределить загрузку и повысить надежность.
Если запрос к DNS-серверу относится к части пространства имен, не контролируемой или не известной серверу, он может либо сказать клиенту, что поиск необходимо проводить в каком-то другом месте (обычно выше по дереву), либо получить необходимую информацию, обратившись от имени клиента к другим DNS-серверам.
В такой схеме ни один сервер не должен знать о топологии всей сети, большинство запросов обрабатывается локально, за локальными администраторами сохраняется локальный контроль, и в результате все счастливы. У DNS есть преимущество перед другими службами -большинство других систем, подобных NIS и WINS, можно интегрировать с DNS. Например, NIS-серверы в SunOS можно настроить так, чтобы они обращались к DNS-серверу, если клиент запрашивает у них имя узла, о котором сервер не знает. Результаты этого запроса возвращаются как стандартные NIS-ответы, так что клиенты и не догадываются о каких-либо дополнительных действиях. DNS-серверы Microsoft обладают схожей функциональностью: если клиент запрашшшет у DNS-сервера Microsoft адрес локальной машины, о которой ему неизвестно, то DNS-сервер можно настроить так, чтобы он пересылал этот запрос WINS-серверу от имени клиента.
Генерирование конфигурационных файлов DNS
Процесс создания конфигурационных файлов DNS очень похож на тот, который мы использовали для создания файлов узлов и исходных файлов NIS:
В случае с DNS второй шаг необходимо расширить, поскольку здесь процесс преобразования оказывается более сложным. Сложности нужно преодолевать, поэтому было бы неплохо иметь под рукой книгу Пола Альбица (Paul Albitz) и Крикета Лью (Cricket Liu) DNS and BIND («DNS и BIND», O'Reilly), содержащую, в том числе, сведения о конфигурационных файлах, создание которых рассматривается ниже.
Создание административного заголовка
Конфигурационные файлы DNS начинаются с административного заголовка, в нем представлена информация о сервере и данных, которые он обслуживает. Самая важная часть этого заголовка - запись о ресурсах SOA (Start of Authority). Запись SOA содержит:
Вот как может выглядеть этот заголовок:
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial
10800 ; refresh 3
600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
Булыпая часть информации добавляется в начало конфигурационного файла каждый раз при его создании. Единственное, о чем нужно побеспокоиться, - это о порядковом номере. Один раз в X секунд (X определяется из значения регенерации) вторичные серверы имен сверяются с первичными серверами, чтобы узнать, нужно ли обновить данные. Современные вторичные DNS-серверы (подобные BIND v8+ или Microsoft DNS) могут быть сконфигурированы так, что будут сверяться с основным сервером в то время, когда на последнем меняются данные. В обоих случаях вторичный сервер запрашивает на первичном запись SOA. Если порядковый номер записи SOA первичного сервера больше порядкового номера, хранимого на вторичном сервере, то произойдет перенос информации о зоне (вторичный сервер загрузит новые данные), В итоге, важно увеличивать порядковый номер каждый раз при создании нового конфигурационного файла. Многие из проблем с DNS вызваны неполадками при обновлении порядкового номера.
Существует по крайней мере два способа сделать так, чтобы порядковый номер всегда увеличивался:
Ниже приведен пример программы, где применяется комбинация этих двух методов для создания допустимого заголовка файла зоны DNS. Порядковый номер будет представлен в виде, который рекомендуют использовать Альбиц и Лью в своей книге (YYYYMMDDXX, где Y=rofl, М=месяц, В=день и ХХ=двузначный счетчик, позволяющий вносить более одного изменения за день):
ft получаем текущую дату в формате YYYYMMDD
@localtime = localtime;
Stoday = sprintf("%04d%02d%02d",$localtime[5]+1900,
$localtime[4]+1,
$localtime[3]);
# имя пользователя как в NT/2000, так и в Unix
$user = ($"0 eq "MSWin32")? $ENV{USERNAME} :
(getpwuid($<))[6]." (".(getpwuid($<))[0].")";
sub GenerateHeader{ my($header);
открываем старый файл, если это возможно, и считываем
# порядковый номер, принимая во внимание формат старого файла
if (open (OLDZONE,$target)){ while (<OLDZONE>) {
next unless (/(\d{8}).«serial/); Soldserial = $1; last; }
close (OLDZONE); } else {
Soldserial = "00000000";
иначе начинаем с О >
К если предыдущий порядковый номер соответствует
# сегодняшнему дню, то увеличиваем последние 2 цифры, в
# противном случае используем новый номер для сегодняшнего дня
Solddate = substr($oldserial,0,8);
Scount = ((Solddate == $today) ? substr($oldserial,8,2)+1 : 0);
Sserial = sprintf("%8d%02d",$today,Scount);
П начало заголовка
$header .= "; Файл зоны dns - СОЗДАН
$0\л": Sheader .= ";
НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ1\п:\п"; Sheader .= ";
преобразован пользователем $user в ".scalar((localtime)). "\n;\n";
# пересчитать число записей для каждого отдела и сообщить
foreach my Sentry (keys %entries){
$depts{$entries{$entry}->{department}}++;
}
foreach my $dept (keys %depts) {
Sheader .= "; число узлов в отделе Sdept:
$depts{$dept}.\n"; } Sheader .= ";
всего узлов: ",scalar(keys %entries)."\n;\n\n";
Sheader .= «"EOH";
@ IN SOA dns.oog.org. hostmaster.oog.org. (
Sserial ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL
@ IN NS dns.oog.org.
EOH
return Sheader;
}
В примере осуществляется попытка прочитать предыдущий конфигурационный файл для определения последнего порядкового номера. Затем это значение разбивается на поля даты и счетчика. Если прочитанная дата совпадает с текущей, необходимо увеличить значение счетчика. Если нет, то в новом порядковом номере поле даты совпадает с текущей датой, а значение счетчика равно 00. Теперь, когда порядковый номер проверен, можно вывести заголовок в правильном виде.
Создание нескольких конфигурационных файлов
После того как написан верный заголовок для конфигурационных файлов, осталось решить еще одну проблему. Правильно настроенный DNS-c-зрвер поддерживает как прямое преобразование (имен в IP-адреса), так и обратное преобразование (IP-адресов в имена) для каждого домена (или зоны), который он обслуживает. Для этого надо иметь два конфигурационных файла на каждую зону. Самый лучший способ их синхронизировать - создавать файлы в одно и то же время.
Рассмотрим в данной главе последний пример генерирования файлов, поэтому соберем воедино все, что обсуждали раньше. Приведенный сценарий использует для создания конфигурационных файлов зоны DNS простой файл базы данных.
Чтобы не усложнять сценарий, я сделал ряд предположений относительно данных, самые важные из которых касаются топологии сети и пространства имен. Я считаю, что сеть состоит из одной подсети класса С с одной зоной DNS. В результате, необходимо создать один файл для прямого преобразования имен и один для обратного. Добавить код для работы с несколькими подсетями и зонами (т. е. создать отдельные файлы для каждой) будет несложно.
Вот, вкратце, что мы делаем:
Вот как выглядит пример и получаемые в результате файлы:
use Res;
Sdatafile = "./database";
база данных узлов
Soutputfile = "zone.$$";
временный файл для вывода
$target = "zone.db";
получаемый файл
$revtarget = "rev.db";
получаемый файл для обратного преобразования
$defzone = ".oog.org";
# создаваемая по умолчанию зона
Srecordsep = "-=-\n";
получаем текущую дату в формате YYYYMMDD
@localtime = localtime;
$today = sprintf("%04d%02d%02d",$localtime[5]+1900,
$localtime[4]+l,
$localtime[3]);
имя пользователя, как в NT/2000, так и
Unix $user = ($"0 eq "MSWin32")? $ENV{USERNAME} :
(getpwuid($<))[6]." (".(getpwuid($<))[0].")"; $/ = Srecordsep;
считываем файл базы данных
open(DATA, Sdatafile) or die "Ошибка! Невозможно открыть datafile;$!\n";
while (<DATA>) {
chomp; # удаляем разделитель записей
разбиваем на key!,value"! @record = split /:\s*|\n/m;
$record ={}; # создаем ссылку на пустой хэш
%{$record} = @record;
# заполняем его значениями из ©record
в ищем ошибки в именах узлов
if ($record->{name} =" /["-.a-zA-ZO-9]/) {
warn "!!!! ",$record->{name} .
встретились недопустимые в именах узлов символы, пропускаем.. Дп";
next; }
# ищем ошибки в псевдонимах
if ($record->{aliases} =" /["-.a-zA-ZO-9\s]/) {
warn "!!!! " . $record->{name} .
встретились недопустимые в псевдонимах символы, пропускаем.. .\п";
next; }
# ищем пропущенные адреса unless ($record->{address}) {
warn "!!!! " . $record->{name} .
нет IP-адреса, пропускаем.. Дп"; next; }
# ищем повторяющиеся адреса
if (defined $addrs{$record->{address}}) {
warn "!!!! Повторение IP-адреса:" . $record->{name}.
" & " . $addrs{$record->{address}} . ", пропускаем. . Дп"; next;
>
else {
$addrs{$record->{address}} = $record->{name};
}
$entries{$record->{name}} = Srecord; # добавляем это в хэш хэшей
}
close(DATA);
Sheader = &GenerateHeader;
создаем файл прямого преобразования open(OUTPUT,"> Soutputfile") or
die "Ошибка! Невозможно записать в $outputfile:$!\n": print OUTPUT $header;
foreach my Sentry (sort byaddress keys %entries) { print OUTPUT
"; Владелец -- ",$entries{$_}->{owner},"
(", $entries{$entry}->{department},"):
Sentries{$entry}->{building},"/", Sentries{$entry}->{room), "\n";
tt выводим запись А
printf OUTPUT "%-20s\tIN A %s\n",
Sentries{Sentry}->{name},Sentries{Sentry>->{address};
it выводим записи CNAMES (псевдонимы)
if (defined $entries{$entry}->{aliases}){
foreach my Salias (split(p ',$entries{$entry}->{aliases}))
{
printf OUTPUT "%-20s\tIN CNAME %s\n",Salias,
Sentries{Sentry}->{name}: > }
print OUTPUT "\n"; }
close(OUTPUT);
Rcs->bindir('/usr/local/bin'); my Srcsobj = Rcs->new;
$rcsobj->file($target);
$rcsobj->co('-!');
rename($outputfile,Starget) or
die "Ошибка! Невозможно переименовать
Soutputfile в Starget:$!\n": $rcsobj->ci("-u","-m"."
Преобразовано пользователем Suser в ".scalar(localtime));
ft создаем файл обратного преобразования open(OUTPUT,"> Soutputfile") or
die "Ошибка! Невозможно записать в $outputfile:$!\n"; print OUTPUT Sheader;
foreach my Sentry (sort byaddress keys %entries) { print OUTPUT
"; Владелец-- ",$entries{$entry}->{owner}," (",
Sentries{Sentry}->{department},"): ",
$entries{$entry}->{building},"/",
Sentries{Sentry}->{room},"\n";
printf OUTPUT "%-3d\tIN PTR %s$defzone.\n\n",
(split/\./,$entries{$entry}->{address})[3],
$entnes{$entry}->{name};
clOse(OUTPUT);
$rcsobj->file($revtarget);
$rcsob]->co( '-1');
предполагаем, что целевой файл по крайней
# мере один раз извлекался из репозитория rename($outputfile,Srevtarget) or
die "Ошибка! Невозможно переименовать
Soutputfile в Srevtarget;$!\n";
$rcsobj->ci("-u","-m"."Преобразовано пользователем
$user в ".scalar(localtime));
sub GenerateHeader{ my(Sheader);
if (open(OLDZONE,$target)){ while (<OLDZONE>) {
next unless (/(\d{8}).«serial/); $oldserial = $1; last; }
close(OLDZONE); } else {
Soldserial = "000000"; }
$olddate = substr($oldserial,0,6);
$count = (Solddate == $today) ? substr($oldserial,6,2)+1 : 0;
Sserial = sprintf("%6d%02d",$today,Scount);
$header .= "; файл зоны dns - СОЗДАН $0\п";
Sheader .= "; HE РЕДАКТИРУЙТЕ ВРУЧНУЮ!\n;\n";
Sheader .= "; Преобразован пользователем Suser в ".scalar(localtime)."\n;\n":
П подсчитываем число узлов в каждом отделе foreach Sentry (keys %entries){
$depts{$entries{$entry)->{department}}++; } foreach $dept (keys %depts) {
Sheader .= "; в отделе $dept $depts{$dept} машин.\n";
X
Sheader ,= "; общее число машин: ".scalar(keys %entries)."\nff\n\n";
Sheader .= «"EOH";
@ IN SOA dns.oog.org. hostmaster.oog.org. (
$serial : serial 10800 ; refresh 3600 : retry 604800 ; expire 43200) ; TTL
@ IN NS dns.dog.org
ЕОН
return $header; }
sub byaddress {
@a = split(/\./,$entries{$a}->{address});
@b = split(/\./,$entries{$b}->{address});
($a[0]<=>$b[0]) ||
($a[1]<=>$b[1]) ||
($a[2]<=>$b[2]) ||
($a[3]<=>$b[3]); }
Вот какой файл получается для прямого преобразования (zone.db):
файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!
Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998
в отделе design 1 машин. в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL
@ IN NS dns.oog.org.
; Владелец -- Cindy Coltrane (marketing): west/143 bendir IN A 192.168.1.3
ben IN CNAME bendir
bendoodles IN CNAME bendir
; Владелец -- David Davis (software): main/909 shimmer IN A 192.168.1.11
shim IN CNAME shimmer
shimmy IN CNAME shimmer
shimmydoodles IN CNAME shimmer
; Владелец -- Ellen Monk (design): main/1116 Sulawesi IN A 192.168.1.12
sula IN CNAME Sulawesi
su-lee IN CNAME Sulawesi
; Владелец -- Alex Rollins (IT): main/1101 sender IN A 192.168.1.55
sandy IN CNAME sander
micky IN CNAME sander
mickydoo IN CNAME sander
А вот как выглядит файл для обратного преобразования (rev.db):
файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!
Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998
в отделе design 1 машин, в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL
@ IN NS dns.oog.org.
; Владелец -- Cindy Coltrane (marketing): west/143 3 IN PTR bendir.oog.org.
; Владелец -- David Davis (software): main/909
11 IN PTR shimmer.oog.org.
; Владелец -- Ellen Monk (design): main/1116
12 IN PTR sulawesi.oog.org.
; Владелец -- Alex Rollins (IT): main/1101 55 IN PTR sander.oog.org.
Этот метод создания файлов открывает перед нами много возможностей. До сих пор мы генерировали файлы, используя содержимое одного текстового файла базы данных. Запись из базы данных считывалась и записывалась в файл, возможно, подвергаясь при этом форматированию. Таким образом, в создаваемые файлы попадали только записи из базы данных.
Иногда бывает полезно, чтобы сценарий добавлял в процессе преобразования свои предопределенные данные. Например, в случае с конфигурационными файлами DNS можно улучшить сценарий преобразования так, чтобы он добавлял записи MX (Mail eXchange), указывающие на центральный почтовый сервер, для каждого узла из базы данных. Простое изменение нескольких строк кода с таких:
К выводим запись А
printf OUTPUT "%-20s\tIN A %s\rT, Sentries{Sentry}->{name},Sentries{Sentry}->{address}:
на следующие:
# выводим запись А
printf OUTPUT "%-20s\tIN A %s\n",
$entries{Sentry}->{name},Sentries{Sentry}->{address};
и выводим запись MX
print OUTPUT " IN MX 10 $mailserver\n";
приведет к тому, что почта, посылаемая на любой из узлов домена, будет направляться на машину $mailserver. Если эта машина настроена так, что может обрабатывать почту для всего домена, то мы задействовали очень важный компонент инфраструктуры (централизованную обработку почты), добавив всего лишь одну строчку кода на Perl.
Проверка работы DNS: итеративный подход
Мы потратили значительное время на создание конфигурационных файлов, используемых сетевыми службами имен, но это всего лишь одна из задач системного и сетевого администратора. Для поддержания сети в рабочем состоянии необходимо постоянно проверять данные службы, чтобы убедиться, что они ведут себя верно.
Например, для системного/сетевого администратора очень многое зависит от ответа на вопрос «Все ли DNS-серверы работают?». В ситуации, когда необходимо найти неисправности, практически настолько же важно знать, «Все ли серверы работают с одной и той же информацией?», или, более точно, «Отвечают ли они одинаково на одинаковые запросы? Синхронизированы ли они?». Данный раздел посвящен подобным вопросам.
По главе 2 можно судить, как действует основной принцип Perl «Всегда существует несколько способов сделать это». Именно такое свойство делает Perl отличным языком для «итеративной разработки». Итеративная разработка - это один из способов описания эволюционного процесса, имеющего место при создании программ системного администрирования (и не только), выполняющих определенную задачу. В случае с Perl можно быстро написать рабочую программу на скорую руку, а позднее вернуться к сценарию и переписать его более элегантным образом. Возможно, будет еще и третья итерация, на этот раз уже с использованием другого подхода к решению задачи.
Существует три различных подхода к одной и той же проблеме проверки согласованности DNS. Они представлены в том порядке, которому, действительно, мог бы последовать человек, пытаясь найти решение, а затем его совершенствуя. Этот порядок отражает взгляд на то, как решение проблемы может развиваться в Perl; ибо ваше отношение к подходу может меняться. Третий способ, использующий модуль Net: : DNS, вероятно, самый простой и наиболее защищенный от ошибок. Но существуют ситуации, когда Net: : DNS применять нельзя, поэтому сначала приведем несколько собственных решений. Обязательно обратите внимание на все за и против, перечисленные после каждого рассмотренного подхода.
Вот наша задача: написать сценарий на Perl, принимающий имя узла и проверяющий список DNS-серверов, чтобы убедиться, что все они возвращают одну и ту же информацию об узле. Чтобы упростить задачу, будем считать, что узел имеет единственный статический IP-адрес (т. е. у него один сетевой адаптер и один IP-адрес).
Перед тем как перейти к рассмотрению всех подходов, взглянем на сердцевину кода, который будем применять:
$hostname = $ARGV[0];
©servers = qw(nameserver1 nameserver2 nameserverS);
# серверы имен
foreach $server (servers) {
&lookupadrjress($hostname, $server);
заполняем %results
}
%inv = reverse %results;
# инвертируем полученный хэш
if (keys %inv > 1) {
print "Между DNS-серверами есть разногласия";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]), "\n"; }
Для каждого из DNS-серверов, перечисленных в списке @servers, вызывается подпрограмма &lookupaddress(), которая обращается к DNS-серверу, чтобы получить IP-адрес заданного имени узла, и помещает результаты в хэш %results. Для каждого DNS-сервера в хэше %results есть запись, значением которой является IP-адрес, возвращаемый этим сервером (ключом является имя сервера).
Существует много способов определить, равны ли друг другу значения из хэша %results (т. е. убедиться, что все DNS-серверы возвращают одну и ту же информацию в ответ на запрос). Мы инвертируем хэш %results в другую хэш-таблицу, преобразовывая все ключи в значения и наоборот. Если все значения из %results одинаковы, то в инвертированном хэше должен быть только один ключ. Если ключей несколько, значит, мы выловили прокол, и поэтому вызываем Data: :Duniper->Durrip() для СлужОа доменных имен вывода содержимого %results, над которым будет ломать голову системный администратор.
Вот как может выглядеть примерный результат, если что-то идет не так:
Между DNS-серверами есть разногласия: $results = {
nameserverl => '192.168.1.2',
nameserver2 => '192. 168. 1.5' ,
nameserverS => ' 192. 168. 1.2' ,
Теперь посмотрим на альтернативы подпрограмме &lookupaddress( ).
Использование nslookup
Если у вас есть опыт работы в Unix или вы уже программировали на других языках сценариев помимо Perl, то первая попытка может сильно походить на сценарий командного интерпретатора. Внешняя программа, вызываемая из Perl сценария, выполняет всю сложную работу:
use Data::Dumper;
Shostname = $ARGV[0];
Snslookup = "/usr/local/bin/nslookup";
# путь к nslookup ©servers = qw(nameserver1 nameserver2 nameserverS);
имена серверов имен foreach Sserver (©servers) {
&lookupaddress($hostname,Sserver); в заполняем %results }
%inv = reverse %results;
# инвертируем полученный хэш
if (scalar(keys %inv) > 1) {
print "Между DNS-серверами есть разногласияДп";
print Data::Dumper->Dump([\%results],["results"]),"\n"; }
» обращаемся к серверу, чтобы получить IP-адрес и прочую
# информацию для имени узла, переданного в программу в
командной строке. Результаты записываем в хэш %results sub lookupaddress
my($hostname,Sserver) = @_;
open(NSLOOK,"$nslookup Shostname Sserver|") or
die "Невозможно запустить nslookup:$!\n";
while (<NSLOOK>) {
« игнорировать, пока не дойдем до "Name: next until (/"Name:/);
следующая строка - это ответ Address: chomp($results{$server} = <NSLOOK>);
# удаляем имя поля
die "Ошибка, вывода nslookup \n" unless /Address/;
$results{$server} =" s/Addfess(es)?:\s+//;
все, с nslookup мы закончили last;
}
close(NSLOOK);
}
Преимущества такого подхода:
Недостатки такого подхода:
обрабатывает тайм-ауты сервера, повторные попытки запросов и дописывает списки поисков доменов.
Работа напрямую с сетевыми сокетами
Если вы «продвинутый системный администратор», вы можете решить, что вызывать внешнюю программу не следует. Вы можете захотеть реализовать запросы к DNS, не используя ничего, кроме Perl. Это означает, что нужно будет создавать вручную сетевые пакеты, передавать их по сети и затем анализировать результаты, получаемые от сервера.
Вероятно, это самый сложный пример из всех, приведенных в книге. Написан он после обращения к дополнительным источникам информации, в которых можно найти несколько примеров существующего кода (включая модуль Майкла Фура (Michael Fuhr), показанный в следующем разделе). Вот что происходит на самом деле. Запрос к DNS-серверу состоит из создания специального сетевого пакета с определенным заголовком и содержимым, отправки его на DNS-сервер, получения ответа от сервера и его анализа.
Каждый DNS-пакет ( из тех, которые нас интересуют) может иметь до пяти различных разделов:
Header(Заголовок)
Содержит флаги и счетчики, относящиеся к запросу или ответу (присутствует всегда).
Question (Запрос)
Содержит вопрос к серверу (присутствует в запросе и повторяется при ответе).
Answer (Ответ)
Содержит все данные для ответа на DNS-запрос (присутствует в пакете DNS-ответа).
Authority (Полномочия)
Содержит информацию о том, можно ли получать авторитетные ответы.
Additional (Дополнительно)
Содержит любую информацию, которую вернет сервер помимо прямого ответа на вопрос.
Наша программа имеет дело только с первыми тремя из этих разделов. Для создания необходимой структуры данных для заголовка DNS-na-кета и его содержимого используется набор команд oack(). Эти структуры данных передаются модулю 10: :Socket, который посылает их в виде пакета. Этот же модуль получает ответ и возвращает его для обработки (при помощи unpackO). Умозрительно такой процесс не очень сложен.
Но перед тем как посмотреть на саму программу, нужно сказать об одной особенности в этом процессе. В RFC1035 (Раздел 4.1.4) определяются два способа представления доменных имен в DNS-пакетах: несжатые и сжатые. Под несжатым доменным именем подразумевается полное имя домена (например host.oog.org)
в пакете. Этот способ ничем не примечателен. Но если это же доменное имя встретится в пакете еще несколько раз, то, скорее всего, оно будет представлено в сжатом виде во всех вхождениях, кроме первого. В сжатом представлении информация (или ее часть) о домене заменяется двубайтовым указателем на несжатое представление этого же доменного имени. Это позволяет использовать в пакете hostl, host2 и
hostS в longsubdomain.longsubdomain.oog.org, вместо того чтобы каждый раз включать лишние байты для longsubdo-main.longsubdomain.oog.org. Нам необходимо обработать оба представления, поэтому и существует подпрограмма &decompress. Дальше обойдемся без фанфар и взглянем на код:
use 10: '.Socket;
Shostname = $ARGV[0];
$defdomain = ".oog.org"; # домен по умолчанию
^servers = qw(nameserver1 nameserver2 nameserverS);
имена серверов имен
foreach Iserver (©servers) {
<Slookupaddress($nostname,$server);
# записываем значения в
%results
}
%inv = reverse ^results; # инвертируем полученный хэш
if (scalar(keys %inv) > 1) { # проверяем, сколько в нем элементов
print "Между DNS-серверами есть разногласия:\п";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
sub lookupaddress{
my($hostname,$server) = @_;
my($qname,$rna(tre,$header,$question,$lformat,@>labels,$count);
local($position,$buf);
Конструируем заголовок пакета
$header = pack("n C2 n4",
++$id, # идентификатор запроса
1, # поля qr, opcode, aa, tc, rd (установлено только rd)
0, # rd, ra
1, один вопрос (qdcount)
0, нет ответов (ancount)
О, п нет записей ns в разделе authority (nscount)
0); tf нет rr addtl (arcount)
если в имени узла нет разделителей,
дописываем домен по умолчанию
(index($hostname,'.') == -1) {
Shostname .= Sdefdomain;
} # конструируем раздел qname пакета (требуемое доменное имя)
for (split(/\./,$riostname)) {
$lformat .= "С а* ";
$labels[$count++]=length;
$labels[$count++]=$_;
}
да конструируем вопрос
да
Squestion = pack($lformat."С п2",
©labels,
0, # конец меток
1, # qtype A
1); # qclass IN
да
да посылаем пакет серверу и читаем ответ
$sock = new 10::Socket::INET(PeerAddr => Sserver,
PeerPort => "domain",
Proto => "udp");
$sock->send($header.$question);
используется UDP, так что максимальный размер пакета известен
$sock->recv($buf,512);
close($sock);
узнаем размер ответа, так как мы собираемся отслеживать
позицию в пакете при его анализе (через Sposition)
Srespsize = length($buf);
распаковываем раздел заголовка
да
($id,
$qr_opcode_aa_tc_rd,
$rd_ra,
Sqdcount,
$ancount,
Snscount,
Sarcount) = unpack("n C2 n4",$buf);
if (!$ancount) <
warn "Невозможно получить информацию для $hostname с Sserver!\n";
return;
}
распаковываем раздел вопроса
tt раздел вопроса начинается после 12 байтов
($position,$qname) = <Sdecompress(12);
($qtype,$qclass)=unpack('§'.Sposition.'n2',Sbuf);
tt переходим к концу вопроса
Sposition += 4;
nntt
tttttt распаковываем все записи о ресурсах
ttntt
for ( ;$ancount;$ancount--){
(Sposition,$rname) = &decompress($position);
(Srtype,Srclass,$rttl,$rdlength)=
unpack('@'.Sposition.'n2 N n',$buf);
Sposition +=10;
tt следующую строку можно изменить и использовать более
# сложную структуру данных; сейчас мы подбираем
последнюю возвращенную запись
$results{$server}=
join('.',unpack('@'.Sposition.'C'.$rdlength,$buf));
Sposition +=$rdlength; } >
О обрабатываем информацию, "сжатую" в соответствии с RFC1035
# мы переходим в первую позицию в пакете и возвращаем
# найденное там имя (после того как разберемся с указателем
# сжатого формата) и место, которое мы оставили в конце
# найденного имени sub decompress {
my($start) = $_[0]; my($domain,$i,Slenoct);
for ($i=$start;$i<=$respsize;) {
$lenoct=unpack('@'.$i.'C', $buf); n длина метки
if (! Slenoct){ tt 0 означает, что этот раздел обработан
$i++;
last; }
if (Slenoct == 192) { tt встретили указатель,
tt следовательно, выполняем рекурсию
Sdomain.=(&decompress((unpack('@'.$i.'n',$buf) & 1023)))[1];
$i+=2;
last } else { tt в противном случае это простая метка
$domain.=unpack('@г.++$i.'a'.Slenoct,$buf).'. ';
$i += Slenoct; }
return($i,Sdomain);
}
Надо заметить, что эта программа не является точным эквивалентом предыдущего примера, потому что мы не пытаемся эмулировать все нюансы поведения nslookup (тайм-ауты, повторные попытки и списки поиска). Рассматривая все три подхода, представленные здесь, обязательно обратите внимание на такие различия.
Преимущества этого подхода заключаются в следующем:
Использование Net::DNS
Как уже говорилось в главе 1, одна из сильных сторон Perl заключается в поддержке обширным сообществом разработчиков, создающих программы, которые могут применяться другими людьми. Если необходимо сделать на Perl нечто, на ваш взгляд, универсальное, то высока вероятность того, что кто-то уже написал модуль для работы с подобной проблемой. В данном случае можно воспользоваться отличным модулем Net: :DNS Майкла Фура (Michael Fuhr), который упростит работу. Чтобы справиться с нашей задачей, необходимо создать новый объект, передать ему имя DNS-сервера, к которому следует обратиться, указать, что нужно послать запрос, и затем применить имеющиеся методы для анализа ответов:
use Net::DNS;
&lookupaddress($hostname,$server); # заполняем значениями %results }
%inv = reverse %results; » инвертируем полученный хэш if (scalar(keys %inv) > 1) { tt проверяем, сколько в нем элементов
print "Между DNS-серверами есть разногласия:\п";
use Data:: Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n"; }
tt всего лишь несколько измененный пример из страниц руководства по Net::DNS sub lookupaddress{
my($hostname,$server) = @_;
$res = new Net::DNS::Resolver; $res->nameservers($server); Spacket = $res->query($hostname);
if (!$packet) {
warn "Невозможно получить данные о Shostname с $server!\n";
return; }
# сохраняем последний полученный ответ RR foreach $rr ($packet->answer) {
$results{$server}=$rr->address;
}
}
Преимущества такого подхода:
Недостатки данного подхода:
В большинстве случаев я предпочитаю использовать уже существующие модули. Тем не менее, для выполнения поставленной задачи подойдет любой подход. Существует несколько способов сделать одно и то же - значит, вперед, действуйте!