Работа напрямую с сетевыми сокетами



Работа напрямую с сетевыми сокетами

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


Содержание раздела