Как отправлять пакеты на сервер
Сетевое программирование для разработчиков игр. Часть 2: прием и передача пакетов данных
Прием и передача пакетов данных
Введение
Привет, меня зовут Гленн Фидлер и я приветствую вас в своей второй статье из цикла “Сетевое программирование для разработчиков игр”.
В предыдущей статье мы обсудили различные способы передачи данных между компьютерами по сети, и в конце решили использовать протокол UDP, а не TCP. UDP мы решили использовать для того, чтобы иметь возможность пересылать данные без задержек, связанных с ожиданием повторной пересылки пакетов.
А сейчас я собираюсь рассказать вам, как на практике использовать UDP для отправки и приема пакетов.
BSD сокеты
В большинстве современных ОС имеется какая-нибудь реализация сокетов, основанная на BSD сокетах (сокетах Беркли).
Сокеты BSD оперируют простыми функциями, такими, как “socket”, “bind”, “sendto” и “recvfrom”. Конечно, вы можете обращаться к этим функциями напрямую, но в таком случае ваш код будет зависим от платформы, так как их реализации в разных ОС могут немного отличаться.
Поэтому, хоть я далее и приведу первый простой пример взаимодействия с BSD сокетами, в дальнейшем мы не будем использовать их напрямую. Вместо этого, после освоения базового функционала, мы напишем несколько классов, которые абстрагируют всю работу с сокетами, чтобы в дальнейшем наш код был платформонезависимым.
Особенности разных ОС
Для начала напишем код, который будет определять текущую ОС, чтобы мы могли учесть различия в работе сокетов:
Теперь подключим заголовочные файлы, нужные для работы с сокетами. Так как набор необходимых заголовочных файлов зависит от текущей ОС, здесь мы используем код #define, написанный выше, чтобы определить, какие файлы нужно подключать.
В UNIX системах функции работы с сокетами входят в стандартные системные библиотеки, поэтому никакие сторонние библиотеки нам в этом случае не нужны. Однако в Windows для этих целей нам нужно подключить библиотеку winsock.
Вот небольшая хитрость, как можно это сделать без изменения проекта или makefile’а:
Мне нравится этот прием потому, что я ленивый. Вы, конечно, можете подключить библиотеку в проект или в makefile.
Инициализация сокетов
В большинстве unix-like операционных систем (включая macosx) не требуется никаких особых действий для инициализации функционала работы с сокетами, но в Windows нужно сначала сделать пару па — нужно вызвать функцию “WSAStartup” перед использованием любых функций работы с сокетами, а после окончания работы — вызвать “WSACleanup”.
Давайте добавим две новые функции:
Теперь мы имеем независимый от платформы код инициализации и завершения работы с сокетами. На платформах, которые не требуют инициализации, данный код просто не делает ничего.
Создаем сокет
Теперь мы можем создать UDP сокет. Это делается так:
Далее мы должны привязать сокет к определенному номеру порта (к примеру, 30000). У каждого сокета должен быть свой уникальный порт, так как, когда приходит новый пакет, номер порта определяет, какому сокету его передать. Не используйте номера портов меньшие, чем 1024 — они зарезервированы системой.
Если вам все равно, какой номер порта использовать для сокета, вы можете просто передать в функцию “0”, и тогда система сама выделит вам какой-нибудь незанятый порт.
Теперь наш сокет готов для передачи и приема пакетов данных.
Но что это за таинственная функция “htons” вызывается в коде? Это просто небольшая вспомогательная функция, которая переводит порядок следования байтов в 16-битном целом числе — из текущего (little- или big-endian) в big-endian, который используется при сетевом взаимодействии. Ее нужно вызывать каждый раз, когда вы используете целые числа при работе с сокетами напрямую.
Вы встретите функцию “htons” и ее 32-битного двойника — “htonl” в этой статье еще несколько раз, так что будьте внимательны.
Перевод сокета в неблокирующий режим
По умолчанию сокеты находится в так называемом “блокирующем режиме”. Это означает, что если вы попытаетесь прочитать из него данные с помощью “recvfrom”, функция не вернет значение, пока не сокет не получит пакет с данными, которые можно прочитать. Такое поведение нам совсем не подходит. Игры — это приложения, работающие в реальном времени, со скоростью от 30 до 60 кадров в секунду, и игра не может просто остановиться и ждать, пока не придет пакет с данными!
Решить эту проблему можно переведя сокет в “неблокирующий режим” после его создания. В этом режиме функция “recvfrom”, если отсутствуют данные для чтения из сокета, сразу возвращает определенное значение, показывающее, что нужно будет вызвать ее еще раз, когда в сокете появятся данные.
Перевести сокет в неблокирующий режим можно следующим образом:
Как вы можете видеть, в Windows нет функции “fcntl”, поэтому вместе нее мы используем “ioctlsocket”.
Отправка пакетов
UDP — это протокол без поддержки соединений, поэтому при каждой отправке пакета нам нужно указывать адрес получателя. Можно использовать один и тот же UDP сокет для отправки пакетов на разные IP адреса — на другом конце сокета не обязательно должен быть один компьютер.
Переслать пакет на определенный адрес можно следующим образом:
Обратите внимание — возвращаемое функцией “sendto” значение показывает только, был ли пакет успешно отправлен с локального компьютера. Но оно не показывает, был ли пакет принят адресатом! В UDP нет средств для определения, дошел ли пакет по назначению или нет.
В коде, приведенном выше, мы передаем структуру “sockaddr_in” в качестве адреса назначения. Как нам получить эту структуру?
Допустим, мы хотим отправить пакет по адресу 207.45.186.98:30000.
Запишем адрес в следующей форме:
И нужно сделать еще пару преобразований, чтобы привести его к форме, которую понимает “sendto”:
Как видно, сначала мы объединяем числа a, b, c, d (которые лежат в диапазоне [0, 255]) в одно целое число, в котором каждый байт — это одно из исходных чисел. Затем мы инициализируем структуру “sockaddr_in” нашими адресом назначения и портом, при этом не забыв конвертировать порядок байтов с помощью функций “htonl” и “htons”.
Отдельно стоит выделить случай, когда нужно передать пакет самому себе: при этом не нужно выяснять IP адрес локальной машины, а можно просто использовать 127.0.0.1 в качестве адреса (адрес локальной петли), и пакет будет отправлен на локальный компьютер.
Прием пакетов
После того, как мы привязали UDP сокет к порту, все UDP пакеты, приходящие на IP адрес и порт нашего сокета, будут ставиться в очередь. Поэтому для приема пакетов мы просто в цикле вызываем “recvfrom”, пока он не выдаст ошибку, означающую, что пакетов для чтения в очерели не осталось.
Так как протокол UDP не поддерживает соединения, пакеты могут приходить с множества различных компьютеров сети. Каждый раз, когда мы принимаем пакет, функция “recvfrom” выдает нам IP адрес и порт отправителя, и поэтому мы знаем, кто отправил этот пакет.
Код приема пакетов в цикле:
Пакеты, размер которых больше, чем размер буфера приема, будут просто втихую удалены из очереди. Так что, если вы используете буфер размером 256 байтов, как в примере выше, и кто-то присылает вам пакет в 300 байт, он будет отброшен. Вы не получите просто первые 256 байтов из пакета.
Но, поскольку мы пишем свой собственный протокол, для нас это не станет проблемой. Просто всегда будьте внимательны и проверяете, чтобы размер буфера приема был достаточно большим, и мог вместить самый большой пакет, который вам могут прислать.
Закрытие сокета
На большинстве unix-like систем, сокеты представляют собой файловые дескрипторы, поэтому для того, чтобы закрыть сокеты после использования, можно использовать стандартную функцию “close”. Однако, Windows, как всегда, выделяется, и в ней нам нужно использовать “closesocket”.
Так держать, Windows!
Класс сокета
Итак, мы разобрались со всеми основными операциями: создание сокета, привязка его к порту, перевод в неблокирующий режим, отправка и прием пакетов, и, в конце, закрытие сокета.
Но, как вы могли заметить, все эти операции немного отличаются от платформы к платформе, и, конечно, трудно каждый раз при работе с сокетами вспоминать особенности разных платформ и писать все эти #ifdef.
Поэтому мы сделаем класс-обертку “Socket” для всех этих операций. Также мы создадим класс “Address”, чтобы было проще работать с IP адресами. Он позволит не проводить все манипуляции с “sockaddr_in” каждый раз, когда мы захотим отправить или принять пакет.
Итак, наш класс Socket:
Использовать их для приема и передачи нужно следующим образом:
Как видите, это намного проще, чем работать с BSD сокетами напрямую. И также этот код будет одинаков для всех ОС, потому весь платформозависимый функционал находится внутри классов Socket и Address.
Заключение
Теперь у нас есть независимый от платформы инструмент для отправки и према UDP пакетов.
UDP не поддерживает соединения, и мне хотелось сделать пример, который бы четко это показал. Поэтому я написал небольшую программу, которая считывает список IP адресов из текстового файла и рассылает им пакеты, по одному в секунду. Каждый раз, когда программа принимает пакет, она выводит в консоль адрес и порт компьютера-отправителя и размер принятого пакета.
Вы можете легко настроить программу так, чтобы даже на локальной машине получить несколько узлов, обменивающихся пакетами друг с другом. Для этого просто разным экземплярам программы задайте разные порты, например:
> Node 30000
> Node 30001
> Node 30002
И т.д…
Каждый из узлов будет пересылать пакеты всем остальным узлам, образуя нечто вроде мини peer-to-peer системы.
Я разрабатывал эту программу на MacOSX, но она должна компилироваться на любой unix-like ОС и на Windows, однако если вам для этого потребуется делать какие-либо доработки, сообщите мне.
При необходимости отправки небольших пакетов данных через TCP особенно важна конструкция приложения Winsock. Конструкция, которая не учитывает взаимодействие задержки подтверждения, алгоритма Nagle и буферизации Winsock, может резко повлиять на производительность. В этой статье обсуждаются эти проблемы с помощью нескольких исследований. Кроме того, в нем содержится ряд рекомендаций по эффективному отправке небольших пакетов данных из приложения Winsock.
Оригинальная версия продукта: Winsock
Исходный номер КБ: 214397
Общие сведения
Когда стек Microsoft TCP получает пакет данных, отключается 200-ms timer delay. При отправлении ACK время задержки сброшено и при следующем получении пакета данных начнется еще одна задержка в 200 мс. Чтобы повысить эффективность как в Интернете, так и в интрасетях, стек TCP использует следующие критерии, чтобы решить, когда отправить один ACK в полученные пакеты данных:
Чтобы не захламлить сеть небольшими пакетами данных, стек TCP по умолчанию включает алгоритм Nagle, который совмещая небольшой буфер данных из нескольких вызовов и задержек отправки, пока не будет получен ACK для предыдущего пакета данных, отправленного из удаленного хоста. Ниже 2 исключения из алгоритма Nagle:
Если стек совмещая буфер данных, размером больше максимального блока передачи (MTU), полный пакет отправляется немедленно, не дожидаясь ACK с удаленного хоста. В сети Ethernet MTU для TCP/IP составляет 1460 bytes.
Параметр socket применяется для отключения алгоритма Nagle, чтобы небольшие пакеты данных доставлялись удаленному хосту TCP_NODELAY без задержек.
Чтобы оптимизировать производительность на уровне приложений, Winsock копирует буферы данных из приложения, отправив вызовы в буфер ядра Winsock. Затем стек использует собственную юристику (например, алгоритм Nagle), чтобы определить, когда действительно поместить пакет на провод. Можно изменить количество буфера ядра Winsock, выделенного в розетке, с помощью параметра SO_SNDBUF (это 8K по умолчанию). При необходимости Winsock может буферить больше размера SO_SNDBUF буфера. В большинстве случаев завершение отправки в приложении указывает только на то, что буфер данных в вызове отправки приложения копируется в буфер ядра Winsock и не указывает, что данные попали в сетевой носитель. Единственным исключением является отключение буферизации Winsock с помощью параметра SO_SNDBUF 0.
Winsock использует следующие правила, чтобы указать завершение отправки в приложение (в зависимости от того, как вызывается отправка, уведомление о завершении может быть функцией, возвращаемой из блокирующего вызова, сигнализирующей событие или вызываемой функцией уведомления и так далее):
Если розетка по-прежнему SO_SNDBUF квота, Winsock копирует данные из отправки приложения и указывает завершение отправки в приложение.
Если в буфере ядра стека остается только одна ранее буферная отправка, Winsock копирует данные из отправки приложения и указывает на завершение отправки в SO_SNDBUF приложение.
Если в буфере ядра стека больше одной ранее буферной отправки, winsock копирует данные из отправки SO_SNDBUF приложения. Winsock не указывает завершение отправки в приложение до тех пор, пока стек не завершит достаточное количество отправки, чтобы вернуть розетку в рамках квоты или только одно невыполненное условие SO_SNDBUF отправки.
Пример 1
Клиент TCP Winsock должен отправить 10000 записей на сервер TCP Winsock для хранения в базе данных. Размер записей варьируется от 20 до 100 bytes длиной. Чтобы упростить логику приложения, выполните проект следующим образом:
Производительность
Во время тестирования разработчик находит, что клиент может отправлять на сервер только пять записей в секунду. Общее число записей 10000, максимум 976 кб данных (10000 * 100 / 1024), для отправки на сервер занимает более получаса.
Анализ
Так как клиент не за набором параметра, алгоритм Nagle заставляет Стек TCP ждать ACK, прежде чем он сможет отправить другой пакет TCP_NODELAY на проводе. Однако клиент отключил буферику Winsock, установив SO_SNDBUF параметр 0. Поэтому 10000 отправленных вызовов должны отправляться и ACK’ed по отдельности. Каждый ACK задерживается на 200 мс, так как в Стеке TCP сервера происходит следующее:
Как улучшить
Существует две проблемы с этим дизайном. Во-первых, существует проблема времени задержки. Клиент должен иметь возможность отправлять два пакета на сервер в пределах 200 мс. Поскольку клиент использует алгоритм Nagle по умолчанию, он должен просто использовать буферию Winsock по умолчанию, а не SO_SNDBUF 0. После того, как стек TCP совмещая буфер размером больше максимального блока передачи (MTU), полный пакет отправляется немедленно, не дожидаясь ACK от удаленного хоста.
Во-вторых, эта конструкция вызывает один отправку для каждой записи такого небольшого размера. Отправка этого небольшого размера неэффективна. В этом случае разработчику может потребоваться вкладки каждой записи до 100 bytes и отправка 80 записей одновременно с одного клиентского вызова отправки. Чтобы сервер знал, сколько записей будет отправлено в общей сложности, клиент может начать общение с загона размером с исправление, содержащее количество записей, которые следует выполнять.
Пример 2
Клиентская заявка Winsock TCP открывает два подключения с серверным приложением Winsock TCP, предоставляющим службу котировок акций. Первое подключение используется в качестве командного канала для отправки символа акций на сервер. Второе подключение используется в качестве канала данных для получения котировки акций. После того как эти два подключения установлены, клиент отправляет символ акций на сервер через командный канал и ждет, когда котировки акций будут возвращаться через канал данных. Следующий запрос символа акций отправляется на сервер только после того, как будет получена первая котировка акций. Клиент и сервер не устанавливают параметр SO_SNDBUF и TCP_NODELAY параметр.
Производительность
Во время тестирования разработчик находит, что клиент может получать только пять кавычка в секунду.
Анализ
Эта конструкция позволяет одновременно запрашивать только один непогашенный запрос на котировки акций. Первый символ запаса отправляется на сервер через командный канал (подключение), а ответ немедленно возвращается с сервера клиенту через канал данных (подключение). Затем клиент немедленно отправляет второй запрос символа запаса, и отправка возвращается сразу же, так как буфер запроса в вызове отправки копируется в буфер ядра Winsock. Однако клиентский пакет TCP не может отправить запрос из буфера ядра сразу, так как первая отправка через командный канал еще не подтверждена. После истечения срока действия времени задержки в 200 мс на командном канале сервера ACK для первого запроса символов возвращается клиенту. Затем второй запрос на кавычка успешно отправляется на сервер после задержки в 200 мс. Кавычка для второго символа акций возвращается сразу через канал данных, так как в это время истек срок действия времени задержки в клиентской ленте данных. Сервер получает ACK для предыдущего ответа на кавычка. (Помните, что клиент не мог отправить второй запрос на котировки акций в течение 200 мс, что дает время для времени истечения срока действия времени задержки клиента и отправки ACK на сервер.) В результате клиент получает второй ответ на кавычка и может выдавать другой запрос на кавычка, который подлежит этому же циклу.
Как улучшить
Здесь нет необходимости в проектировании двух подключений (каналов). Если для запроса и ответа на котировки акций используется только одно подключение, ACK для запроса на кавычка может быть свинарным в ответе на кавычка и немедленно возвращается. Чтобы повысить производительность, клиент может использовать несколько запросов на котировки акций в один почтовый вызов на сервер, а сервер также может несколько ответов на несколько цитат в один вызов для клиента. Если два однонаправленных канала по какой-либо причине необходимы, обе стороны должны установить параметр, чтобы небольшие пакеты можно было отправить немедленно, не дожидаясь ACK для предыдущего TCP_NODELAY пакета.
Рекомендации
Хотя эти два примера сфабрикованы, они помогают проиллюстрировать некоторые худшие сценарии. При разработке приложения, в которое входит обширный небольшой сегмент данных, отправляет recvs и, следует учитывать следующие рекомендации:
Если сегменты данных не являются критически важными во времени, приложение должно совмесить их в больший блок данных для передачи на вызов отправки. Так как буфер отправки, скорее всего, будет скопирован в буфер ядра Winsock, буфер не должен быть слишком большим. Чуть менее 8K является эффективным. Пока ядро Winsock получает блок больше MTU, он будет отправлять несколько полноразлиберных пакетов и последний пакет с тем, что осталось. Отправляемая сторона, за исключением последнего пакета, не будет поражена 200-ms timer delay. Последний пакет, если это нечетный пакет, по-прежнему подчиняется алгоритму задержки подтверждения. Если конечный стек отправки получает еще один блок больше MTU, он может обойти алгоритм Nagle.
По возможности избегайте подключений к розетке с однонаправленным потоком данных. На связь над однонаправленными розетками легче влияют алгоритмы nagle и задержки подтверждения. Если сообщение следует запросу и потоку откликов, следует использовать одну розетку для отправки и для того, чтобы ACK можно было перенаправить в recvs ответ.
Если все небольшие сегменты данных должны быть отправлены немедленно, установите TCP_NODELAY параметр в конце отправки.
Если вы не хотите гарантировать отправку пакета по проводу, если завершение отправки указывает Winsock, не следует задать значение SO_SNDBUF нулю. По сути, буфер 8K по умолчанию был по умолчанию настроен на то, чтобы работать хорошо в большинстве ситуаций, и изменить его не следует, если только вы не протестировали, что новый параметр буфера Winsock обеспечивает лучшую производительность, чем по умолчанию. Кроме того, установка нуля в основном полезна для приложений, которые SO_SNDBUF массово передают данные. Даже в этом случае для максимальной эффективности следует использовать его совместно с двойной буферикой (несколько непогашенных отправки в любой момент времени) и перекрытием I/O.
Если доставка данных не должна быть гарантирована, используйте UDP.
Ссылки
Дополнительные сведения о задержке подтверждения и алгоритме Nagle см. в следующих сведениях:
Braden, R.[1989], RFC 1122, Requirements for Internet Hosts—Communication Layers, Internet Engineering Task Force.
Создать «пакет» и отправить?
Поясните/Разъясните пожалуйста для «курицы» понятие о пакетах такого рода:
Хочу создать пакет с какими то данными и отправить его.
Теперь важное. Как я понимаю.
Я леплю свой пакет, кидаю на роутер.
Как осуществляется отправка этого пакета на роутер?
Я так понимаю, без софта не обойтись, т.е. нет способа отправить пакет данных на роутер, допустим через командную строку?
Например хочу сделать пакет и отправить (без понятия как) его на свой роутер. В пакете данные для роутера, допустим перезагрузись или скинь настройки в дефолт.
Вообщем как это все происходит? Может кто как для «курицы» пояснить как это все делается?
P.S. Извиняюсь за такого рода глупый вопрос!
Пакет это некоторый объем упакованной информации, имеющий размер, адрес и еще некоторых характеристики.
Я просто не пойму, можно/как собрать этот пакет руками!
Я просто не пойму, можно/как собрать этот пакет руками!(если можно вообще)
Не понятно, почему вы зациклились на отправке пакетов роутеру? Когда отправляете пакет, то указываете адрес назначения пакета, а протокол сам решает как конкретно будет отправлен ваш пакет. Даже если это адрес yandex.ru и вы не знаете где он фактически находится, то TCP/IP + DNS разберутся с этим сами.
Packet crafting как он есть
Создание пакетов или packet crafting — это техника, которая позволяет сетевым инженерам или пентестерам исследовать сети, проверять правила фаерволлов и находить уязвимые места.
Делается это обычно вручную, отправляя пакеты на различные устройства в сети.
В качестве цели может быть брандмауэр, системы обнаружения вторжений (IDS), маршрутизаторы и любые другие участники сети.
Создание пакетов вручную не означает, что нужно писать код на каком-либо высокоуровневом языке программирования, можно воспользоваться готовым инструментом, например, Scapy.
Scapy — это один из лучших, если не самый лучший, инструмент для создания пакетов вручную.
Утилита написана с использованием языка Python, автором является Philippe Biondi.
Возможности утилиты практически безграничны — это и сборка пакетов с последующей отправкой их в сеть, и захват пакетов, и чтение их из сохраненного ранее дампа, и исследование сети, и многое другое.
Всё это можно делать как в интерактивном режиме, так и создавая скрипты.
С помощью Scapy можно проводить сканирование, трассировку, исследования, атаки и обнаружение хостов в сети.
Scapy предоставляет среду или даже фреймворк, чем-то похожий на Wireshark, только без красивой графической оболочки.
Утилита разрабатывается под UNIX-подобные операционные системы, но тем не менее, некоторым удается запустить ее и в среде Windows.
Эта утилита так же может взаимодействовать и с другими программами: для наглядного декодирования пакетов можно подключать тот же Wireshark, для рисования графиков — GnuPlot и Vpython.
Для работы потребуется права суперпользователя (root, UID 0), так как это достаточно низкоуровневая утилита и работает напрямую с сетевой картой.
И что важно, для работы с этой утилитой не потребуются глубокие знания программирования на Python.
Приступаем
Официальный сайт проекта — www.secdev.org/projects/scapy
Установку можно провести разными способами, например apt-get install python-scapy, в случае дистрибутивов на основе Debian.
Так же можно просто скачать свежую версию с сайта разработчиков:
# cd /tmp
# wget scapy.net
# unzip scapy-latest.zip
# cd scapy-2.*
# sudo python setup.py install
После этого запуск происходит непосредственно командой scapy.
На экране отобразится примерно так:
Мы видим стандартное приглашение для ввода, все действия будут выполняться в интерактивном режиме.
Выход происходит комбинацией Ctrl+D, либо набрав функцию exit().
Изучаем инструмент
На самом деле Scapy сильно отличается от привычных утилит. Он работает в текстовом режиме, но любое взаимодействие осуществляется не через привычные ключи и параметры командной строки, а через интерпретатор Python’а.
Такой подход вначале может показаться несколько неудобным и непривычным, но со временем и с практикой приходит понимание того, что это было правильным решением, и что это действительно удобно.
Вначале посмотрим на поддерживаемые протоколы, для этого вызовем функцию ls().
Вывалится более 300 разнообразных протоколов, с которыми можно работать, включая прикладные вроде HTTP, транспортные TCP и UDP, сетевого уровня IPv4 и IPv6 и канального уровня Ether (Ethernet).
Важно обращать внимание на регистр: большинство протоколов пишутся в Scapy с заглавными буквами.
Для того чтобы подробно рассмотреть поля определенного протокола, можно вызвать функцую ls() с указанием протокола: ls(TCP)
В результате будут выведены все поля, которые можно модифицировать в процессе создания пакетов. В скобках показаны значения, которые используются по умолчанию, можно заметить, что порт отправителя 20 (это ftp-data) и порт получателя – 80 (это естественно HTTP), так же установлен флаг SYN (flags = 2).
К примеру, если рассмотреть канальный уровень (Ethernet), то тут возможностей будет поменьше:
В дополнение к функции ls(), есть полезная функция lsc(), которая выведет практически весь основной функционал Scapy:
Для того чтобы получить более подробную информацию о каждой функции, можно использовать help(имя_функции), например:
Видим нечто похожее на MAN страницу в Unix системах.
Для выхода можно использовать опять же привычную в Linux клавишу Q.
Мы посмотрели на протоколы и функции, теперь можно перейти к делу — к созданию пакетов.
Крафтим
Можно создавать сразу пакеты высоких уровней (сетевого и прикладного), и Scapy автоматически дополнит низлежащие уровни, а можно вручную собирать, начиная с канального уровня.
Разделяются уровни модели OSI символом прямого слэша (/).
Нужно обратить внимание на то, что Scapy читает пакет от нижнего уровня слева, до более высокого справа. Поначалу это может немного сбивать с толку, но после небольшой практики всё станет вполне привычно.
К слову, в терминологии Scapy сетевой пакет разделяется на слои, и каждый слой представляется как экземпляр объекта.
Собранный пакет в упрощенном виде может выглядеть как:
Ether()/IP()/TCP()/”App Data”
В большинстве случаев используется только уровень L3, предоставляя Scapy возможность самостоятельно заполнять канальный уровень, на основе информации из ОС.
Меняя значения полей каждого протокола мы меняем стандартные значения (их выводит функция ls()).
Теперь создадим какой-нибудь простой пакет.
Всё очень просто: мы указали адрес назначения, порт и вобщем-то нагрузку в виде слова «TEST».
Сам пакет был незамысловато назван packet, мы увидим очень подробно и развернуто наш свежесозданный пакет:
И теперь, выполнив знакомую уже функцию ls(packet):
Уровни в нем разделяются символами «—«.
Вместо того, чтобы создавать пакет за один раз можно создавать его частями:
В этом примере мы создали переменные под каждый уровень модели OSI.
В качестве имен переменных можно использовать и буквы и цифры, при этом, не забывая о регистре.
И теперь собираем всё в один пакет:
Видно, что результат получится аналогичный.
Углубляемся в пакеты
Мы уже смотрели на вывод функции ls(), но не всегда нужна такая подробная информация о пакете.
Достаточно просто набрать имя переменной и сразу увидеть краткую сводку:
Так же можно использовать метод summary():
Если же нужно чуть больше информации, то есть метод show():
Кроме того, можно просмотреть любое поле, просто указав его:
Разумеется, это работает только в том случае, если такие поля уникальны в пределах пакета.
Если, например, взять поле flags, которое присутствует как в TCP, так и в IP, тут уже нужно конкретизировать, что мы хотим увидеть. В противном случае Scapy выведет значение первого найденного поля (IP flags в нашем примере).
Конкретизация происходит путем указания протокола в квадратных скобках:
К слову, по умолчанию установленные флаги выводятся в цифровом представлении.
Если все управляющие биты будут включены (установлены в 1), то получим значение равное 255. В нашем случае значение 2 говорит о том, что установлен SYN бит.
Но существует возможность отобразить управляющие биты и в символьном отображении:
Как уже говорилось, в любой момент можно достаточно просто поменять значение любого поля:
А в случае, если поле не является уникальным, то нужно указать протокол:
Вторым способом является использование конструкции payload, которая позволяет перепрыгнуть через один уровень (через L3 в нашем случае):
Здесь мы вначале просматриваем вывод слоев над L3, затем просматриваем значение TCP флагов и устанавливаем для них новое значение.
Кстати, можно даже несколько раз вызвать payload, поднимаясь при этом выше и выше:
Можно еще посмотреть на содержимое пакета в шестнадцатеричном виде, для этого есть функция hexdump():
Разбираемся с адресацией
Scapy и в деле указания адреса получателя так же проявляет большую гибкость.
Масса вариантов — здесь и привычная десятичная форма, и доменное имя и CIDR нотация:
В последнем случае пакет будет отправлен на каждый адрес в подсети.
Множество адресов можно задать, просто разделяя их запятой, не забыв про квадратные скобки:
На этом этапе может возникнуть мысль: «А что если нужно задать множество портов?».
И тут Scapy предоставляет широкие возможности, можно указать как диапазон, так и просто перечислить множество:
Обращаю внимание на различие в скобках, в случае диапазона они круглые, а в случае множества – квадратные.
И завершая разговор про указание целей, рассмотрим ситуацию, когда нужно отправить множество пакетов на множество портов.
Для того, чтобы увидеть какие пакеты будут отправлены придется задействовать цикл for, не забываем, что язык программирования у нас Python.
На самом деле ничего сложного, всё очень логично:
Вначале мы уже привычно создаем пакет, в котором указываем подсеть и диапазон портов.
Затем, используя цикл, создаем список, где переменной «а» присваивается каждый элемент структуры пакета. В Python’е отсутствуют массивы в привычном понимании. Вместо них для хранения объектов используются списки.
Мы используем цикл for, для того чтобы «распаковать» всю структуру и отобразить ее в таком наглядном виде.
Отправляем пакеты в путь
Но вместе с тем существует много дополнительных опций, которые могут быть иногда полезны.
Например, timeout – укажет, сколько времени (в секундах) нужно ждать до получения ответного пакета, retry – сколько раз нужно повторно слать пакет, если ответ не был получен и одна из самых полезных опций – это filter, синтаксис которого очень похож на tcpdump.
В качестве наглядного примера отправим пакет в сеть:
Здесь мы используем функцию, которая после отправки ожидает ответ, устанавливаем таймаут 0.1 секунды и фильтруем ответы, которые подпадают под указанное правило.
Как поступать с ответными пакетами?
Можно взять и назначить переменную, которая и будет содержать ответ:
А смотреть уже привычным способом, просто вызывая переменную response.
Видно, что ответ сохранился в двух вариантах – Results и Unanswered, результаты и без ответа, соответственно.
Указывая смещение, можно вывести только необходимую часть ответа:
Или подробную информацию:
Если же пакет был отправлен в сеть без указания переменной (например, просто функцией sr()), то по умолчанию пакет будет числиться за переменной «_» (символ подчеркивания).
Чтобы достать оттуда эти пакеты, можно использовать конструкцию:
При этом разные результаты сохранятся в двух разных переменных (res и unans).
Более подробный вывод достигается опять же путем указания смещения:
Ловим ответные пакеты
Теперь рассмотрим ситуацию, если пакетов в ответ приходит много.
То, что мы увидели, было по сути, самое что ни есть сканирование портов.
Открытые порты будут с флагами SA (SYN/ACK), например:
Мы смотрим именно на пакет по номеру, счет традиционно начинается нуля.
Можно пойти дальше и распаковать этот результат:
Здесь мы извлекли из результата отправленный пакет (под номером 21) и ответ на него.
Но это только один пакет, а как быть, если нужно обработать все пакеты?
В таком случае придется вновь обращаться к циклу for:
Берем и разбиваем каждый элемент списка res на части a и b. Затем обрезаем часть “a”, заливая это всё в список “allsent”.
Аналогично создаем список allrec, только уже оставляем другую часть.
Всё это, конечно, хорошо, но хотелось бы в более удобном виде получить список открытых и закрытых портов.
Еще раз посмотрим на список res, a точнее на элемент res[0], который состоит из двух частей: пакет, который мы отправили res[0][0], и ответ, который получили res[0][1].
В ответе можно обнаружить три части — заголовок IP (res[0][1][0]), заголовок TCP (res[0][1][1]) и собственно сами данные (res[0][1][2]).
Используем цикл for для извлечения каждого элемента res[N] в переменную «а».
Теперь в переменной «a»содержится результат для каждого пакета. Другими словами «а» представляет собой ans[N].
Нам остается только проверить значения a[1][1], которые будут означать res[N][1][1] в заголовке TCP.
Если быть еще более точным, требуется значение 18, которое означает установленные флаги SYN-ACK.
В тех случаях, когда это условие сработает, мы еще выведем порт отправителя из заголовка TCP:
В итоге, получим результат в виде списка открытых портов.
Все вышеозначенные конструкции набираются за один раз, важно так же уделять внимание отступам (обычно это 4 пробела).
Мы только что вручную написали простой сканер портов, не больше и не меньше.
Сниффер и наоборот
В Scapy входит также и небольшой сниффер, за который отвечает функция sniff().
Естественно, с ним можно использовать фильтры (похожие на фильтры tcpdump), за это отвечает параметр filter, так же можно ограничивать количество пакетов с помощью параметра count.
Как всегда вызов help(sniff) выведет вполне подробную информацию по этой функции.
Не следует забывать, что это сильно упрощенный сниффер, и ожидать от него хорошей скорости особо не приходится.
Стандартная комбинация Ctrl+C прервет процесс захвата трафика и выведет результат.
Как и любая неопределенная переменная, результат попадет в «_».
Выполнив метод summary(), можно увидеть статистику по захваченным пакетам:
Вместо захвата трафика из сети, можно прочитать его из заранее сохраненного дампа (pcap файла).
Кроме того, можно и наоборот, записать пойманные пакеты в файл, используя функцию wrpcap():
И завершая тему сниффинга, можно вызвать Wireshark прямо из интерфейса Scapy, для этого можно использовать одноименную функцию wireshark().
Подробно про Wireshark можно в моей предыдущей статье по адресу http://linkmeup.ru/blog/115.html.
Автоматизация
Казалось бы, что уже всё готово, но не тут то было.
При очередной попытке подгрузить ospf модуль:
>>> load_contrib(‘ospf’), получаем всё ту же ошибу «ERROR: No module named contrib.ospf»
Для того, чтобы модуль окончательно заработал, осталось создать скрипт инициализации (пустой файл):
touch /usr/lib/python2.7/dist-packages/scapy/contrib/__init__.py
После этого, уже можно будет создавать пакеты для OSPF.
Создаем трехэтапное TCP-соединение
Для этого нужно будет поймать SYN/ACK ответ, извлечь из него TCP sequence number, увеличить значение на единицу и, собственно, и поместить полученное значение в поле acknowledgement number.
Непростая задача на первый взгляд, но Scapy может справиться и с ней.
Вначале рассмотрим, что нам нужно, для того чтобы всё прошло успешно.
Для того чтобы стало еще понятней, рассмотрим уже более подробно, с использованием произвольно взятых значений.
К примеру, соединение прошло таким образом:
192.168.10.200 1024 > 192.168.10.50 80 flags=SYN seq=12345
192.168.10.50 80 > 192.168.10.200 1024 flags=SYN, ACK seq=9998 ack=12346
192.168.10.200 1024 > 192.168.10.50 80 flags=ACK seq=12346 ack=9999
Что в итоге нужно было сделать.
Здесь уже всё должно быть знакомым: пакет собираем из двух частей, инкапсулируя TCP в IP.
Теперь помня о том, что нам нужно будет перехватить ответ, извлечь оттуда sequence number и увеличить на единицу, делаем:
Происходит следующее – функция sr1 отправляет ранее созданный пакет в сеть, а первый пришедший ответ помещается в переменную SYNACK.
А затем, используя конструкцию SYNACK.seq, извлекаем TCP sequence number, увеличиваем его на единицу и сохраняем в переменной my_ack.
Создаем новый заголовок TCP и называем его ACK. В нем устанавливается другой флаг (A — ACK) и увеличивается значение sequence number.
Кроме того, в качестве acknowledgement указывается переменная my_ack.
Затем собранный пакет выбрасывается в сеть командой send (помним, что это L3 команда, которая даже не слушает, что придет в ответ).
Если всё было сделано правильно, то классическое TCP-соединение состоялось.
Осталось только создать TCP сегмент без каких-либо флагов и тоже отправить в сеть.
Как можно увидеть, мы в очередной раз создали экземпляр TCP заголовка (в этот раз, назвав его PUSH), без флагов и со всеми остальными знакомыми уже значениями.
После этого добавили немного данных, используя переменную data, и отправили в сеть, используя ту же функцию send.
И соответственно от получателя должен будет прийти acknowledgement на этот сегмент.
>>> ip=IP(src=«192.168.10.200»,dst=«192.168.10.50»)
>>> SYN=TCP(sport=1024,dport=80,flags=«S»,seq=12345)
>>> packet=ip/SYN
>>> SYNACK=sr1(packet)
>>> my_ack=SYNACK.seq+1
>>> ACK=TCP(sport=1024,dport=80,flags=«A»,seq=12346,ack=my+ack)
>>>send(ip/ACK)
Но здесь есть и несколько подводных камней.
Если посмотреть на этот обмен в Wireshark, можно увидеть, что до того как ушел наш ACK пакет, внезапно был отправлен RST:
Продолжаем исследования
Используя Scapy, можно находить хосты в сети, среди указанного множества адресов:
В этом случае мы используем протокол ICMP и применяем знакомый прием по разделению полученных ответов.
По умолчанию, установлен 8-й тип для ICMP, это и есть классический эхо-запрос.
Углубляясь в тему ИБ, попробуем определить версию ОС используя Scapy и nmap.
Итак, рассмотрим что было сделано.
Вначале был подключен внешний модуль, в данном случае nmap.
Затем проверяем, что у нас есть файл (nmap-os-fingerprints) с отпечатками различных ОС.
И запускаем непосредственно определение удаленной операционной системы, за это отвечает функция nmap_fp, где в качестве параметров помимо самой цели, можно еще указать открытый (oport) и закрытый (cport) порты.
Правильно указанные порты помогут сильно улучшить точность определения ОС.
Визуализируем пакеты
Все время мы смотрели на текстовый вывод, местами была псевдографика, но Scapy умеет и выводить некоторые результаты в графическом виде.
Посмотрим, что нам предлагается.
Самое простое — это метод conversations():
При его выполнении, запустится окно ImageMagick, в котором отрисуется схема нашего обмена пакетами, не ахти красиво, но достаточно наглядно.
Это способ, вероятно, лучше всего подойдет, для визуализации дампов с трафиком.
Второй способ заключается в построении 2D графиков, с последующим экспортом их в pdf-файл.
За это уже отвечает функция pdfdump().
Результаты выглядят примерно так:
В данном случае уже вполне неплохо.
Кроме того, функция graph() опять откроет окно ImageMagick, но уже с детальной прорисовкой:
Здесь мы видим результат трассировки, с подробным отображением автономных систем и прочей визуализацией.
И, завершая тему визуализации, а вместе с ней и статью, посмотрим на 3D отображения трассы.
Для этого потребуется VPython и команда trace3D().
Здесь отображена трасса из предыдущего графика.
Но иногда бывают и такие варианты:
В этом примере была проведена трассировка сразу нескольких целей, с использованием нескольких (80, 443) tcp портов.
Левый клик на любом объекте приведет к появлению IP-адреса над ним, а левый клик с зажатой клавишей CTRL – к отображению более подробной информации — портам, как в этом случае.
Эпилог
Итак, мы рассмотрели лишь малую часть утилиты Scapy, но уже это впечатляет.
Возможности, которые предоставляются действительно очень большие.
Статья призвана вызвать у читателя интерес в изучении сетевых протоколов, и не является исчерпывающим руководством по инструменту Scapy.
За использование этой утилиты в каких-либо противоправных целях автор ответственности не несет.
В процессе написания статьи использовались материалы Института SANS и официальная документация проекта.






