Работа напрямую с сетевыми сокетами
Работа напрямую с сетевыми сокетами
Если вы «продвинутый системный администратор», вы можете решить, что вызывать внешнюю программу не следует. Вы можете захотеть реализовать запросы к 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 (тайм-ауты, повторные попытки и списки поиска). Рассматривая все три подхода, представленные здесь, обязательно обратите внимание на такие различия.
Преимущества этого подхода заключаются в следующем:
- Он не зависит от каких-либо других программ. Нет необходимости разбираться в работе других людей.
- Это настолько же быстро, а может быть, и еще быстрее, чем вызов внешней программы.
- Проще обработать параметры ситуации (тайм-ауты и прочее). Недостатки же такого подхода в том, что:
- Для написания подобной программы понадобится больше времени и, кроме того, она сложнее предыдущей.
- Этот подход требует дополнительных знаний, не имеющих прямого отношения к вашей задаче (т. е. вам, возможно, потребуется узнать, как вручную собирать DNS-пакеты, чего при использовании nslookup знать было не нужно).
- Вам, вероятно, придется самостоятельно справиться с различиями между операционными системами (в предыдущем подходе они были скрыты благодаря тому, что эту работу выполнил автор внешней программы).