Я всегда считал traceroute одной из самых интересных сетевых утилит, поскольку она дает представление о том, как данные проходят через сеть. Если вы отправляете эхо-запрос на сервер и получаете ответ, легко забыть, что данные прошли через каскад сетевых устройств, пока не достигли места назначения. Если вы выполните трассировку, вы увидите IP-адреса и время ответа всех устройств (мостов, маршрутизаторов и шлюзов) между вами и сервером. Хотя это помогает сетевому администратору отлаживать сетевые проблемы, мне было интересно наблюдать за потоком данных. Тем более, когда он сочетается с программным обеспечением геолокации: с помощью бесплатных инструментов, таких как Open Visual Traceroute или веб-версий, таких как Online Visual Traceroute, вы можете увидеть результат, визуализированный на карте. Но поскольку пакеты данных имеют только IP-адрес источника и назначения, как можно узнать маршрут, по которому они проходят через сеть? Чтобы понять, как работает traceroute, я решил создать свой собственный код на JavaScript, используя Node.js.

Что такое traceroute и как он работает?

Traceroute - это сетевая утилита, которая отправляет множество запросов (зондов) и собирает ответы всех сетевых устройств между ними. Но почему эти устройства отправляют ответ отправителю? В нормальных условиях устройство получает IP-пакет, определяет, куда ему нужно идти, и пересылает его. Перед этим он уменьшает значение TTL (время жизни) IP-пакета. Значение TTL на самом деле не определяет время жизни, это скорее количество переходов для жизни. Переход - это часть сетевого пути между одним сетевым устройством и другим. Поэтому, если вы отправляете TCP-пакет со значением TTL, равным 3, первое сетевое устройство на пути (скажем, это ваш домашний маршрутизатор) уменьшит значение TTL до 2 и передаст пакет вашему интернет-провайдеру. Первое устройство в сети Интернет-провайдера будет уменьшать его до значения 1 и снова пересылать пакет. Следующее устройство в цепочке снова уменьшит его на 1, и значение TTL теперь равно 0. Пакет со значением TTL, равным 0, больше не будет передаваться. Вместо этого устройство сбросит упаковку. С точки зрения отправителя, пакет был сброшен где-то по пути, без понятия, когда и где это произошло. К счастью, большинство устройств достаточно любезны, чтобы сообщить отправителю, что они отказались от пакета. Они отправляют ответ по протоколу ICMP с сообщением: Time-to-live exceeded (время жизни превышено в пути). Значения TTL по умолчанию, установленные операционной системой достаточно высоки, чтобы пакеты могли достичь места назначения до того, как они умрут. Но если мы явно установим низкое значение TTL, мы сможем вызвать ответ от устройств между нами и местом назначения. Это именно то, что делает traceroute. Он отправляет пакеты с TTL 1, 2, 3 и так далее, пока место назначения не будет достигнуто. Каждый раз, когда пакет может пройти еще один переход, и каждый раз, когда он истекает по пути, мы получаем ответ.

Давайте посмотрим на это в действии

Запустите сниффер пакетов, например Wireshark (и установите фильтр на udp and ip.addr == 8.8.8.8) или откройте консоль и запустите tcpdump, набрав sudo tcpdump -v‘ icmp or udp ’. Перейдите в другое окно консоли и запустите traceroute на общедоступный DNS-сервер Google, имеющий IP-адрес 8.8.8.8, выполнив traceroute 8.8.8.8. Вы увидите результат, подобный этому:

Число слева - это переход, за которым следует IP-адрес устройства и 3 времени ответа в миллисекундах. Идея в том, что эти 3 раза дают вам представление о среднем времени. Таким образом, для достижения сервера 8.8.8.8 от моего ПК требуется 9 переходов. Для переходов 7 и 8 я получал ответ с другого IP-адреса каждый раз, когда отправлял посылку. Давайте проверим записанный вывод Wireshark, чтобы увидеть, что произошло за кулисами:

Мы видим, что в пункт назначения 8.8.8.8 были отправлены три пакета UDP. Все они имеют значение TTL 1. Поскольку срок их действия истек на первом переходе, мой маршрутизатор отправил мне три (по одному для каждого запроса) ответа ICMP с сообщением Превышено время жизни. Затем traceroute отправил три зонда со значением TTL 2, но на них Wireshark так и не получил ответа. Похоже, мой интернет-провайдер блокирует ответы ICMP на своем шлюзе. Такой прыжок называют черной дырой. Затем traceroute отправлял UDP-пакеты с TTL 3, 4, 5 и так далее, пока, наконец, не получил ответ от целевого сервера (в нашем случае это сервер с ip 8.8.8.8). Сервер обычно отвечает ICMP Назначение недоступно (порт недоступен). Это связано с тем, что UDP-пакеты отправляются с номером порта назначения от 33434 до 33534, который является зарезервированным диапазоном для traceroute и не является допустимым портом для приложения. Довольно просто, но при этом очень умно, да? Давайте посмотрим, как мы могли бы создать такой инструмент на Node.js. Что нам нужно?

  1. Нам нужно отправлять UDP-пакеты с указанным TTL.
  2. Нам нужно поймать ответы ICMP и измерить время, прошедшее между запросом и ответом. Мы можем использовать порт назначения в ответе, чтобы четко сопоставить его с запросом и, следовательно, увеличить порт назначения для каждого запроса.
  3. Traceroute может преобразовать доменное имя в IP-адрес, а также выполнить обратный поиск в DNS для IP-адресов, чтобы получить символическое имя. Итак, давайте также реализуем это.
  4. О, и давайте попробуем сделать все, используя максимум 100 строк кода. Почему? Потому что я уверен, что сможем!

Простая реализация traceroute в Node.js

Сначала давайте импортируем некоторые модули, которые нам нужны. Dgram позволяет нам отправлять UDP-пакеты, и мы используем raw-socket для перехвата ответов ICMP. Dns-then - это просто оболочка для модуля DNS, которую мы используем для поиска по DNS и обратного просмотра.

Теперь мы создаем функцию, которая всегда отправляет 3 пакета, прежде чем увеличивать значение TTL и порт:

Затем нам нужно перехватить ответы ICMP, сопоставить номер порта с текущим запросом и при необходимости выполнить обратный поиск DNS.

Функция handleReply, вызываемая в приведенном выше фрагменте, отвечает за форматирование и регистрацию результата. Если будет достигнута цель или максимальное количество прыжков, программа выйдет. В противном случае снова вызывается функция sendPacket.

Это все строительные блоки, необходимые для базовой функциональности traceroute. Теперь мы склеиваем все вместе, и все готово. Проверьте и загрузите полный исходный код.

Давайте запустим его и посмотрим, что у нас получится:

И давайте сравним его с нативной реализацией:

Как видите, два маршрута трассировки очень похожи, но переходы 7 и 8 разные. Это связано с тем, что пакеты маршрутизируются динамически и не всегда идут по одному и тому же маршруту, т.е. когда вы запускаете один и тот же traceroute дважды, вы можете получить два разных результата.

Последние мысли

Это только очень простая реализация для понимания основной концепции. Утилита traceroute имеет ряд дополнительных функций, таких как поиск ASN или поддержка различных протоколов. Бывают случаи, когда брандмауэры блокируют пакеты UDP, и вы хотите вернуться к ECHO_PING для своих запросов. Поиграйте с кодом и реализуйте дополнительные функции по своему усмотрению. Надеюсь, вы повеселились и чему-то научились.