Время прочтения: 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. Его возможности намного шире рассмотренного случая, но это уже другая история.