Время прочтения: 6 мин.

Как эффективно принимать и обрабатывать поврежденные сетевые пакеты? Давайте разбираться.

В информационных системах, когда требуется высокая скорость передачи данных, довольно часто на транспортном уровне используется протокол UDP. В некоторых ситуациях при передаче UDP пакетов по сети требуется отключение вычисления контрольной суммы (сокращенно CRC – cycle reduce code) на стороне отправителя с целью ускорения времени формирования и отправки пакета (данный протокол предусматривает такую возможность путем обнуления CRC). И вот тут кроется проблема на приёмной стороне: сетевая карта приёмника может либо на аппаратном уровне вычислять CRC и отбрасывать пакеты с несовпадающими контрольными суммами, либо это делает драйвер сетевой карты. Как следствие, нужные пакеты не доходят до сокета приложения, что делает приложение неработоспособным.

Есть два пути перехвата UDP пакетов с неправильной контрольной суммой:

1. Использование сетевого анализатора пакетов Wireshark, который позволяет перехватывать абсолютно любые пакеты, переводя сетевую карту в так называемый неразборчивый режим. В Wireshark можно настроить фильтр на прием пакетов только от заданного адресата и затем выгрузить полученные пакеты в файл для дальнейшей обработки (см. рисунок ниже).

Минусы: помимо полезных данных в файл выгружается большое количество ненужной формации о пакете, из-за чего усложняется процесс выгрузки необходимой информации (на рисунке ниже красным выделены полезные данные, именно их нужно извлечь из файла):

2. Использование утилиты Scapy, которая позволяет удобно манипулировать сетевыми пакетами различных сетевых протоколов, причём это можно делать как в интерактивном режиме, так и создавая скрипты на языке Python, импортируя Scapy как обычную библиотеку. Данная утилита, как и сетевой анализатор пакетов Wireshark, способна перехватывать абсолютно любые сетевые пакеты и обрабатывать их. Все это позволяет объединить и автоматизировать сбор и обработку данных в одном приложении. Подробнее о возможностях Scapy можно прочитать здесь.

Смоделирую проблемную ситуацию с помощью Scapy. Ниже приведен код для генерации 1000 UDP-пакетов с нулевой контрольной сумой (параметр chksum=0x0000 слоя UDP в функции sendp, по умолчанию CRC вычисляется автоматически). В качестве данных буду передавать текущее время и числа от 1 до 128.

from scapy.all import *
from scapy.layers.inet import IP, UDP
from scapy.layers.l2 import Ether
import time
import binascii

# будем отправлять данные сами себе
ip, mac = '192.168.43.44', '00:F4:8D:EF:72:8D'
# dst_port – порт получателя, src_port – порт отправителя
dst_port, src_port = 11111, 11110
# packet_count - количество отправляемых пакетов, n - счетчик пакетов
packet_count, n = 1000, 1
# Формирование тестовых данных
gen_data = [f'{i:02x}' for i in range(1, 129)]
gen_data_str = ''.join(gen_data)

while n <= packet_count:
    # Добавление текущего времени в начало паекты данных
    curr_t = time.localtime()
    h, m, s = curr_t.tm_hour, curr_t.tm_min, curr_t.tm_sec
    # Формирование итоговых данных для отправки
    data = binascii.unhexlify(f'{h:02x}{m:02x}{s:02x}{gen_data_str}')
    # Формирование и отправка UDP-пакета
    sendp(Ether(dst=mac)/
          IP(dst=ip)/
          UDP(sport=src_port, dport=dst_port, chksum=0x0000)/
          data)
    # Задержка на 0.1 сек
    time.sleep(0.1)
    n += 1

Обработка данных от WireShark

Необходимо из полученного дампа WireShark’а выделить полезные данные, сделаю это с помощью регулярных выражений, зная, что строки с данными нумеруются числами в шестнадцатеричном формате размером 2 байта. В найденных полезных данных извлекаю время и следующий за ним набор чисел от 1 до 128. Скрипт анализатора данных приведен ниже:

from scapy.all import *
from datetime import time
import re

# Регулярное выражения для поиска полезных данных
reg_expr = r'^[0-9a-f]{4}'
reg_expr_compiled = re.compile(reg_expr)

with open('result.txt', 'r') as f:
    list_result = []
    while True:
        # читаем стоку в line
        line = f.readline()
        if line == '':
            # EOF
            break
        # если не конец файла
        else:
            # проверяем строку на наличие в начале четырех цифр (это номер      
строки с данными)
            if reg_expr_compiled.findall(line):
                if line[16] == ' ':
                    # Это последняя строка пакета
                    # Получаем строки из чисел в шестнадцатеричном формате,              
                    # которые нужно перевести в десятичный формат
                    # что естественно не так просто сделать
                    list_result.extend(line[6:14].split(' '))
                    # сначала получим объект bytearray, декодируя строку
                    h = bytearray.fromhex(list_result[0])
                    m = bytearray.fromhex(list_result[1])
                    s = bytearray.fromhex(list_result[2])
                    # затем преобразуем объект bytearray в число типа 
                    # unsigned char (число размером 1 байт)
                    h = struct.unpack('B', h)[0]
                    m = struct.unpack('B', m)[0]
                    s = struct.unpack('B', s)[0]
                    # Выведем время в красивом виде
                    print(time(h, m, s))
                    # Преобразуем остальные данные аналогичным образом
                    map_data = list(map(lambda x: struct.unpack('B',
                                    bytearray.fromhex(x))[0],
                                    list_result[3:]))
                    print(map_data)
                    list_result = []
                else:
                    list_result.extend(line[6:53].split(' '))

Как видно из рисунка ниже, получила верный результат:

Минусы такого способа:

  • Необходимость дополнительных действий для сбора нужных пакетов в WireShark и их последующей выгрузке в файл;
  • Сложности при получении полезных данных из дампа WireShark.

Обработка данных модулем Scapy

Модуль Scapy позволяет более изящным, простым и удобным способом принимать и обрабатывать любые пакеты в сети. Для прослушивания сети используется функция sniff, которая может либо накопить заданное количество пакетов и вернуть их в виде списка, или можно передать функцию для обработчика в качестве параметра lfilter, которая будет вызываться для каждого принимаемого пакета. Первый вариант предпочтительней, когда скорость передачи данных высокая, а обработка требует некоторого времени. Полезные данные из принятого пакета извлекаются очень просто: для этого необходимо в квадратных скобках указать слой (в нашем случае UDP) и обратиться к параметру payload. Скрипт приёма и обработки пакетов представлен ниже:

from scapy.all import *
from scapy.layers.inet import IP, UDP
import datetime


# IP и порт получателя  
ip, dst_port = '192.168.43.44', 11110
# Прием пакетов в количестве count=1000 для хоста с заданным IP-адреом и портом 
rec_packets = sniff(count=1000, filter=f"host {ip} and port {dst_port}")
# Обработка принятых пакетов
for pack in rec_packets:
    # Извлечем время (первые 3 бата)
    # Извлечем время (первые 3 байта)
    t = list(bytes(pack[UDP].payload)[:3])
    time = datetime.time(t[0], t[1], t[2])
    print(time)
    # Извлечем числа
    data = list(bytes(pack[UDP].payload)[3:])
    map_data = map(int, data)
    print(data)

Результат получила тот же самый, что и при анализе данных от WireShark’а, но количество строк кода и действий существенно сократилось:

Таким образом, убедилась насколько просто можно принимать, обрабатывать, генерировать и отправлять «битые» пакеты с помощью Scapy. Его возможности намного шире рассмотренного случая, но это уже другая история.