From 8d8fc4be6e2db14710f0a2c6f446267209e15c16 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 15 Jan 2026 23:13:08 +0300 Subject: [PATCH] Update secure channel library API with CRC32 integration - Add CRC32 checksum support using IEEE 802.3 polynomial - Update sc_encrypt/sc_decrypt API: combine tag and CRC32 into single buffer - Add SC_CRC32_SIZE, SC_MAX_OVERHEAD constants and SC_ERR_CRC_FAILED error code - Implement CRC32 calculation before encryption and verification after decryption - Update connection.c to use new API with error statistics tracking - Modify test files for compatibility with new API - All tests pass successfully --- changelog.txt | 29 ++- etcp.txt | 461 +++++++++++++++++++++------------------- etcp_plan.txt | 273 ------------------------ src/connection.c | 90 +++++--- src/connection.h | 3 + src/etcp.c | 45 +++- src/etcp.h | 16 ++ src/sc_lib.c | 95 +++++++-- src/sc_lib.h | 32 +-- tests/test_sc_lib.c | 24 ++- tests/test_udp_secure.c | 30 +-- 11 files changed, 514 insertions(+), 584 deletions(-) delete mode 100644 etcp_plan.txt diff --git a/changelog.txt b/changelog.txt index 6f5db2e..98cd9b0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -67,4 +67,31 @@ Thu Jan 15 2026 17:49: Унификация модуля u_async для осно - Обновлен net_emulator: удалены локальные копии u_async.c, u_async.h, timeout_heap.c, timeout_heap.h - Обновлен Makefile net_emulator: использует общие объектные файлы из ../obj/src/, добавлен -I../u_async - Удалены тестовые файлы net_emulator (test_timeout_heap.c, test_uasync_random.c) как несовместимые с общей реализацией -- Все цели сборки (основной проект и net_emulator) компилируются успешно \ No newline at end of file +- Все цели сборки (основной проект и net_emulator) компилируются успешно +Thu Jan 15 2026 19:14: Проверка test_utun_fork и улучшения + +- Исправлен путь к бинарнику utun в тесте (заменен ../utun на ./utun) +- Добавлены замеры времени выполнения каждого этапа теста +- Добавлена проверка логов на наличие ошибок (error/fatal/fail) +- Тест успешно проходит, утечки памяти не обнаружены в логах +- Добавлен вывод общего времени выполнения теста + +Thu Jan 15 2026 20:31: Внедрение ETCP reset механизма с интеграцией в connection.c + +- Добавлен reset callback в connection.c: функция conn_etcp_reset_callback для сброса состояния pkt_normalizer +- Установка callback через etcp_set_reset_callback в conn_connect +- Добавлена блокировка tx_process для data packets во время reset_pending && !reset_ack_received (служебные пакеты разрешены) +- Добавлена блокировка retransmit_check во время reset handshake +- Все изменения совместимы с существующими тестами, тесты проходят успешно +- Устранен warning о неиспользуемом параметре epkt в callback + +Чт янв 15 2026 22:54: Обновление API библиотеки защищённого канала с добавлением CRC32 + +- Добавлена поддержка CRC32 для проверки целостности данных (полином IEEE 802.3) +- Обновлен API sc_encrypt/sc_decrypt: объединены tag и CRC32 в один выходной буфер, удалены отдельные параметры tag +- Добавлены константы SC_CRC32_SIZE, SC_MAX_OVERHEAD и код ошибки SC_ERR_CRC_FAILED +- Реализовано вычисление CRC32 перед шифрованием и проверка после дешифрования +- Обновлены функции packer_output_bridge и etcp_output_bridge в connection.c для использования нового API +- Добавлена статистика ошибок (encryption_errors, decryption_errors, crc_errors) в структуру conn_stats_t +- Обновлены тесты test_sc_lib.c и test_udp_secure.c для совместимости с новым API +- Все тесты компилируются и проходят успешно diff --git a/etcp.txt b/etcp.txt index ec9b9ef..1ad4920 100755 --- a/etcp.txt +++ b/etcp.txt @@ -2,281 +2,296 @@ ETCP - Extended Transmission Control Protocol ============================================= Протокол для надежной передачи данных поверх UDP с восстановлением порядка, -повторной передачей потерянных пакетов и управлением потоком. +повторной передачей потерянных пакетов и управлением потоком. Работает в связке +с pkt_normalizer для фрагментации/сборки и connection для криптографии. -Основные задачи: -- Передача пакетов через UDP с учетом его особенностей (потери, негарантированный порядок) -- Восстановление правильного порядка пакетов на приемной стороне -- Повторная передача потерянных пакетов -- Управление потоком на основе измерения RTT и пропускной способности -- Обеспечение прогресса доставки даже при длительных потерях +Ключевые принципы: +- Минимальные накладные расходы (4 байта заголовка + опциональные метаданные) +- Циклические 16-битные счетчики (ID, timestamp) для компактности +- Адаптивное управление окном на основе RTT и bandwidth +- Механизм forward progress для гарантии доставки даже при потерях +- Reset handshake для синхронизации состояния соединения -Архитектура: ------------- - -На передающей стороне: -1. Очередь передачи (tx_queue) - данные, ожидающие отправки -2. Список отправленных пакетов (sent_list) - пакеты, ожидающие подтверждения -3. Таймеры для управления передачей и повторной отправкой - -На приемной стороне: -1. Сортированный список принятых пакетов (rx_list) - пакеты в порядке ID -2. Выходная очередь (output_queue) - собранные в правильном порядке данные -3. Буферы для накопления подтверждений и запросов на повторную передачу - -Структура данных: ------------------ +Структура данных epkt: +----------------------- struct epkt { // Очереди - ll_queue_t* tx_queue; // Очередь данных для отправки - ll_queue_t* output_queue; // Выходная очередь (собранные данные) + ll_queue_t* tx_queue; // Данные для отправки (от app → pkt_normalizer) + ll_queue_t* output_queue; // Собранные данные (к app через pkt_normalizer) // Списки пакетов - struct rx_packet* rx_list; // Полученные пакеты (отсортированный список) - struct sent_packet* sent_list; // Отправленные пакеты (для повторной передачи) + struct rx_packet* rx_list; // Полученные пакеты (отсортированный по ID) + struct sent_packet* sent_list; // Отправленные пакеты (ждут ACK) - // Метрики - uint16_t rtt_last; // Последнее RTT (в единицах времени 0.1 мс) - uint16_t rtt_avg_10; // Среднее RTT за последние 10 пакетов - uint16_t rtt_avg_100; // Среднее RTT за последние 100 пакетов - uint16_t jitter; // Джиттер (усредненный) - uint16_t bandwidth; // Текущая пропускная способность (байты за единицу времени) - uint32_t bytes_sent_total; // Общее количество отправленных байт - uint16_t last_sent_timestamp; // Временная метка последнего отправленного пакета - uint32_t bytes_allowed; // Рассчитанное количество разрешенных к отправке байт - - // Состояние передачи - uint16_t next_tx_id; // Следующий ID для передачи - uint16_t last_sent_id; // Последний отправленный ID (для ретрансмиссии самого нового пакета) - uint16_t last_rx_id; // Последний полученный ID (для подтверждения) + // Идентификаторы и тайминги + uint16_t next_tx_id; // Следующий ID для передачи (1..65535, циклический) + uint16_t last_sent_id; // Последний отправленный ID (для ретрансмиссии) + uint16_t last_rx_id; // Последний полученный ID uint16_t last_delivered_id; // Последний ID, переданный в output_queue + uint16_t last_acked_id; // Последний подтвержденный ID + uint16_t last_rx_ack_id; // Последний полученный ACK ID от пира - // Таймеры - void* next_tx_timer; // Таймер для следующей передачи - void* retransmit_timer; // Таймер для повторных передач + // Метрики и управление потоком + uint16_t rtt_last; // Последнее RTT (единицы 0.1 мс) + uint16_t rtt_avg_10; // Среднее RTT за 10 пакетов + uint16_t rtt_avg_100; // Среднее RTT за 100 пакетов + uint16_t jitter; // Усредненный джиттер + uint16_t bandwidth; // Пропускная способность (байт/ед.времени) + uint32_t bytes_allowed; // Разрешенные к отправке байты (накопление) + uint32_t unacked_bytes; // Байты в пути (отправлены, но не подтверждены) + uint32_t window_size; // Текущий размер окна (RTT × bandwidth × 2) + uint8_t window_blocked; // Флаг блокировки передачи - // Обратный вызов для отправки - etcp_tx_callback_t tx_callback; - void* tx_callback_arg; + // Буферы служебной информации + uint16_t pending_ack_ids[32]; // ID пакетов для подтверждения + uint16_t pending_ack_timestamps[32]; // Timestamps для RTT расчета + uint8_t pending_ack_count; // Количество ожидающих ACK + uint16_t pending_retransmit_ids[32]; // ID пакетов для ретрансмиссии + uint8_t pending_retransmit_count; // Количество запросов - // История RTT для усреднения - uint16_t rtt_history[100]; - uint8_t rtt_history_idx; - uint8_t rtt_history_count; + // Отслеживание прогресса (forward progress) + uint16_t oldest_missing_id; // Самый старый отсутствующий ID + uint16_t missing_since_time; // Время первого обнаружения пропуска - // Ожидающие подтверждения - uint16_t pending_ack_ids[32]; - uint16_t pending_ack_timestamps[32]; - uint8_t pending_ack_count; + // Reset состояние + uint8_t reset_pending; // Отправлен reset, ждем ACK + uint8_t reset_ack_received; // Получен reset ACK + uint8_t initialized; // 1 = соединение инициализировано после первого handshake + void* reset_timer; // Таймер повторной отправки reset + uint16_t reset_retry_count; // Счетчик попыток reset + etcp_reset_callback_t reset_callback; // Callback для уведомления о reset + void* reset_callback_arg; // Аргумент callback - // Ожидающие запросы на повторную передачу - uint16_t pending_retransmit_ids[32]; - uint8_t pending_retransmit_count; + // Таймеры (uasync-based) + void* next_tx_timer; // Таймер следующей передачи + void* retransmit_timer; // Таймер проверки ретрансмиссий + uasync_t* ua; // Инстанс uasync - // Управление окном - uint32_t unacked_bytes; // Количество байт, отправленных но еще не подтвержденных - uint32_t window_size; // Текущий размер окна в байтах (рассчитывается) - uint16_t last_acked_id; // Последний подтвержденный ID пакета - uint16_t last_rx_ack_id; // Последний полученный ID подтверждения от получателя - uint16_t retrans_timer_period; // Текущий период таймера повторной передачи (в единицах времени) - uint16_t next_retrans_time; // Время следующей проверки повторной передачи - uint8_t window_blocked; // Флаг: передача заблокирована из-за ограничения окна + // Callback отправки + etcp_tx_callback_t tx_callback; + void* tx_callback_arg; - // Отслеживание прогресса доставки - uint16_t oldest_missing_id; // Самый старый отсутствующий ID пакета - uint16_t missing_since_time; // Время, когда самый старый отсутствующий пакет был впервые обнаружен + // Статистика + uint32_t retransmissions_count; // Счетчик ретрансмиссий + uint32_t ack_packets_count; // Счетчик ACK пакетов + uint32_t control_packets_count; // Счетчик управляющих пакетов + uint32_t total_packets_sent; // Всего отправлено пакетов + uint32_t unique_packets_sent; // Уникальных отправленных пакетов + uint32_t bytes_sent_total; // Всего отправлено байт + uint32_t bytes_received_total; // Всего получено байт }; -Формат пакета: ---------------- +Формат кодограммы: +------------------ -Пакет состоит из обязательного заголовка и опциональных секций: +Каждая кодограмма состоит из обязательного заголовка и опциональных секций: 1. Обязательный заголовок (4 байта): - - ID пакета (2 байта): циклический порядковый номер (0 для пакетов только с метриками) - - Timestamp (2 байта): время отправки в единицах 0.1 мс (циклическое, 16 бит) + [ID high][ID low][Timestamp high][Timestamp low] + - ID: 16-битный циклический номер (0 = служебный пакет без данных) + - Timestamp: время отправки в единицах 0.1 мс (циклическое) -2. Опциональные секции (одна или несколько, каждая начинается с байта-заголовка): +2. Опциональные секции (одна или несколько): а) Подтверждения (ACK) - заголовок 0x01: - [0x01] [count] [(id, timestamp) × count] [last_delivered_id] [last_rx_id] - - count: количество пар ID+timestamp (1 байт) - - Для каждого подтверждаемого пакета: ID (2 байта) + timestamp получения (2 байта) - - last_delivered_id (2 байта): последний ID, доставленный в выходную очередь - - last_rx_id (2 байта): последний полученный ID (новейший известный пакет) - - б) Запросы на повторную передачу - заголовок 0x10-0x2F: - [0x10 + (count-1)] [IDs × count] [last_delivered_id] [last_rx_id] - - count: (заголовок & 0x0F) + 1 (от 1 до 32) - - Для каждого запрашиваемого пакета: ID (2 байта) - - last_delivered_id (2 байта): последний ID, доставленный в выходную очередь - - last_rx_id (2 байта): последний полученный ID - - в) Полезные данные - заголовок 0x00: - [0x00] [данные...] - - Данные произвольной длины (до конца пакета) - -Пакет может содержать несколько секций (например, ACK + данные). Секция с полезными данными -обычно идет последней, если присутствует. + [0x01][count][(id_hi,id_lo,ts_hi,ts_lo)×count][last_delivered_hi,lo][last_rx_hi,lo] + - count: количество пар ID+timestamp (1-32) + - last_delivered_id: последний ID, доставленный получателю + - last_rx_id: последний полученный ID (для синхронизации прогресса) -Алгоритмы: ----------- + б) Запросы ретрансмиссии - заголовок 0x10-0x2F: + [0x10+(count-1)][(id_hi,id_lo)×count][last_delivered_hi,lo][last_rx_hi,lo] + - count: (заголовок & 0x0F) + 1 (1-32) + - last_delivered_id, last_rx_id: для синхронизации состояния -1. Управление передачей: - - Передача происходит при наличии данных в tx_queue и доступной полосы пропускания - - Полоса пропускания контролируется через bytes_allowed, который накапливается со временем - - Размер окна рассчитывается как: window_size = RTT × bandwidth × 2 - - Передача блокируется, если unacked_bytes превышает window_size - -2. Подтверждение и RTT измерение: - - При получении пакета его ID и timestamp добавляются в pending_ack_ids - - При следующей отправке эти подтверждения включаются в пакет - - Получатель вычисляет RTT как разницу между текущим временем и полученным timestamp - - RTT усредняется за последние 10 и 100 пакетов - -3. Повторная передача: - - Отправленные пакеты хранятся в sent_list до подтверждения - - Таймер retransmit_timer периодически проверяет пакеты старше 1.5×RTT - - Если пакет не подтвержден, его ID добавляется в pending_retransmit_ids - - Новейший неподтвержденный пакет (last_sent_id) повторно передается после 2×RTT - - Запросы на повторную передачу от получателя также обрабатываются - -4. Сборка пакетов на приемной стороне: - - Полученные пакеты вставляются в отсортированный rx_list - - При обнаружении пропусков (gaps) отправляются запросы на повторную передачу - - Непрерывная последовательность пакетов перемещается в output_queue - - last_delivered_id отслеживает последний доставленный ID - -5. Обеспечение прогресса доставки (forward progress): - - Отслеживается самый старый отсутствующий пакет (oldest_missing_id) - - Если пакет отсутствует дольше 3×RTT (минимум 6 мс), last_delivered_id продвигается вперед - - Это предотвращает бесконечное ожидание потерянных пакетов - -6. Синхронизация состояния: - - Поля last_delivered_id и last_rx_id передаются в ACK и запросах на повторную передачу - - Получатель обновляет свой last_delivered_id, если полученное значение новее - - Это позволяет синхронизировать прогресс доставки между отправителем и получателем - -Таймеры: --------- - -1. Таймер передачи (next_tx_timer): - - Срабатывает, когда передача невозможна (нет полосы или окно заполнено) - - Перезапускает процесс передачи - -2. Таймер повторной передачи (retransmit_timer): - - Период: max(RTT/2, 2 мс) - - Проверяет sent_list на наличие неподтвержденных пакетов старше 1.5×RTT - - Планирует повторную передачу + в) Полезная нагрузка - заголовок 0x00: + [0x00][данные...] + - Данные произвольной длины -API функции: ------------- + г) Reset запрос - заголовок 0x02: + [0x02] + - Инициирует reset handshake -epkt_t* etcp_init(void); - Инициализирует новый экземпляр ETCP + д) Reset подтверждение - заголовок 0x03: + [0x03] + - Подтверждение получения reset -void etcp_free(epkt_t* epkt); - Освобождает экземпляр ETCP и все связанные ресурсы +Пакет может содержать несколько секций (например, ACK + данные). Секция с данными +обычно идет последней. -void etcp_set_callback(epkt_t* epkt, etcp_tx_callback_t cb, void* arg); - Устанавливает обратный вызов для отправки пакетов через UDP +Очереди и потоки данных: +------------------------ -int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len); - Обрабатывает полученный UDP пакет +1. Отправка (app → сеть): + app_input_queue → pkt_normalizer(packer) → tx_queue → tx_process() → UDP -int etcp_tx_put(epkt_t* epkt, uint8_t* data, uint16_t len); - Помещает данные в очередь передачи +2. Прием (сеть → app): + UDP → etcp_rx_input() → rx_list → output_queue → pkt_normalizer(unpacker) → app_output_queue -ll_queue_t* etcp_get_output_queue(epkt_t* epkt); - Возвращает выходную очередь для чтения полученных данных +3. Управление потоком: + - tx_process() учитывает bandwidth (bytes_allowed) и window_size + - Данные блокируются при reset_pending && !reset_ack_received + - Служебные пакеты (ACK, retransmit requests) обрабатываются всегда -void etcp_set_bandwidth(epkt_t* epkt, uint16_t bandwidth); - Устанавливает ограничение пропускной способности +Алгоритмы: +---------- -int etcp_tx_queue_size(epkt_t* epkt); - Возвращает общее количество пакетов, ожидающих в очередях передачи +1. Передача (tx_process): + - Проверка bandwidth: bytes_allowed накапливается со временем + - Проверка окна: unacked_bytes ≤ window_size + - Блокировка data packets при reset handshake + - Формирование пакета: заголовок + ACK + retransmit requests + данные + - Сохранение в sent_list для возможной ретрансмиссии + - Обновление unacked_bytes + +2. Подтверждение и RTT: + - При получении пакета: ID+timestamp добавляются в pending_ack_ids + - При отправке: pending_ack_ids включаются в пакет + - Получатель вычисляет RTT = current_time - received_timestamp + - Усреднение: rtt_avg_10 (скользящее), rtt_avg_100 (история) + +3. Ретрансмиссия (retransmit_check): + - Проверка sent_list каждые retrans_timer_period = max(RTT/2, 2ms) + - Порог ретрансмиссии: 1.5×RTT + - Новейший неподтвержденный пакет (last_sent_id) ретрансмитится через 2×RTT + - Пропускается при reset_pending && !reset_ack_received + +4. Сборка на приеме: + - Вставка в отсортированный rx_list (по ID) + - Обнаружение пропусков → запросы ретрансмиссии + - Перемещение непрерывной последовательности в output_queue + - Обновление last_delivered_id + +5. Forward progress: + - Отслеживание oldest_missing_id и missing_since_time + - Если пакет отсутствует >3×RTT (минимум 6 мс), last_delivered_id продвигается + - Предотвращает бесконечное ожидание потерянных пакетов + +Reset механизм: +--------------- -void etcp_reset(epkt_t* epkt); - Сбрасывает состояние соединения (очищает очереди, метрики, таймеры) +Цель: синхронизация состояния ETCP и pkt_normalizer при сбоях. + +1. Инициализация соединения: + - При создании соединения: initialized=0 + - Первый обмен reset (0x02) → reset_ack (0x03) → initialized=1 + - Последующие resets вызывают локальный сброс состояния + +2. Локальный reset (etcp_reset_connection): + - Установка reset_pending=1, reset_ack_received=0 + - Отправка reset пакета (0x02) + - Таймер повторной отправки каждые 100 мс (до 10 попыток) + - Блокировка data packets, разрешение service packets + +3. Удаленный reset (получение 0x02): + - Если initialized=0 (первый handshake): только reset_ack (0x03) + - Если initialized=1: reset_ack + вызов etcp_reset() + - etcp_reset() вызывает reset_callback (очистка pkt_normalizer) + +4. Callback интеграция с connection: + - etcp_set_reset_callback(epkt, conn_etcp_reset_callback, conn) + - Callback очищает состояние packer/unpacker нормализатора + - Данные в очередях приложения сохраняются для повторной отправки + +5. Завершение handshake: + - Получение reset_ack (0x03) → reset_ack_received=1, reset_pending=0 + - initialized=1 (если еще не установлен) + - Отмена reset_timer + - Возобновление передачи data packets + +Взаимодействие с pkt_normalizer: +--------------------------------- + +1. Фрагментация исходящих данных: + - pkt_normalizer(packer) разбивает данные на фрагменты ≤ 65535 байт + - Добавляет заголовки фрагментации (0xFC/0xFD/0xFE/0xFF) + - Передает в tx_queue через queue bridge + +2. Сборка входящих данных: + - output_queue → pkt_normalizer(unpacker) собирает фрагменты + - Удаляет заголовки фрагментации + - Передает в app_output_queue + +3. Reset очистка: + - pkt_normalizer_reset_state() сбрасывает буферы сборки + - Сохраняет сервисное состояние (service packets) + +Статистика и отладка: +--------------------- -uint16_t etcp_get_rtt(epkt_t* epkt); - Возвращает текущее RTT +Счетчики в структуре epkt позволяют отслеживать: +- Эффективность ретрансмиссий (retransmissions_count / unique_packets_sent) +- Накладные расходы (control_packets_count / total_packets_sent) +- Прогресс доставки (last_delivered_id vs last_rx_id) -uint16_t etcp_get_jitter(epkt_t* epkt); - Возвращает текущий джиттер +Отладочные макросы (при -DETCP_DEBUG -DETCP_DEBUG_EXT): +- ETCP_LOG() - основные события +- ETCP_DEBUG_LOG() - детальная отладка -Внутренние структуры: ---------------------- +API функции: +------------ + +// Основные +epkt_t* etcp_init(uasync_t* ua); +void etcp_free(epkt_t* epkt); +void etcp_set_callback(epkt_t* epkt, etcp_tx_callback_t cb, void* arg); +void etcp_set_reset_callback(epkt_t* epkt, etcp_reset_callback_t cb, void* arg); +int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len); +int etcp_tx_put(epkt_t* epkt, uint8_t* data, uint16_t len); +ll_queue_t* etcp_get_output_queue(epkt_t* epkt); -typedef struct rx_packet { - struct rx_packet* next; - uint16_t id; - uint16_t timestamp; - uint8_t* data; - uint16_t data_len; - uint8_t has_payload; -} rx_packet_t; - -typedef struct sent_packet { - struct sent_packet* next; - uint16_t id; - uint16_t timestamp; - uint8_t* data; - uint16_t data_len; // Общая длина пакета - uint16_t payload_len; // Длина полезных данных (для учета окна) - uint16_t send_time; // Время отправки - uint8_t need_ack; // Требуется подтверждение - uint8_t need_retransmit; // Требуется повторная передача -} sent_packet_t; +// Конфигурация +void etcp_set_bandwidth(epkt_t* epkt, uint16_t bandwidth); +void etcp_reset(epkt_t* epkt); // Локальный сброс состояния +void etcp_reset_connection(epkt_t* epkt); // Инициация reset handshake + +// Мониторинг +uint16_t etcp_get_rtt(epkt_t* epkt); +uint16_t etcp_get_jitter(epkt_t* epkt); +int etcp_tx_queue_size(epkt_t* epkt); +void etcp_get_stats(epkt_t* epkt, ...); // Полная статистика Особенности реализации: ----------------------- 1. Циклические счетчики: - - ID пакетов: 16-битные, циклические (0-65535, затем 0) - - Timestamp: 16-битные, циклические (0-65535 единиц по 0.1 мс ≈ 6.55 секунд) - - Сравнение с учетом цикличности через функцию id_compare() + - Функция id_compare(a,b) учитывает переполнение 65535→0 + - Timestamp использует ту же логику для расчета RTT 2. Единицы времени: - Базовый интервал: 0.1 мс (100 микросекунд) - - Все таймеры и измерения RTT используют эту единицу + - Все таймеры, RTT, jitter в этих единицах + - get_current_timestamp() возвращает монотонное время -3. Ограничения: - - Максимум 32 ожидающих подтверждения или запроса на повторную передачу - - История RTT хранит до 100 измерений - - Максимальный размер окна: 2^32-1 байт +3. Ограничения буферов: + - pending_ack_ids, pending_retransmit_ids: до 32 элементов + - rtt_history: 100 измерений для усреднения + - Максимальный размер данных в пакете: 65535 байт -4. Обработка дубликатов: - - При получении пакета с уже существующим ID он игнорируется - - Повторная передача пакета имеет тот же ID, но новый timestamp +4. Интеграция с uasync: + - Каждый epkt имеет ссылку на uasync_t* ua + - Таймеры создаются через uasync_set_timeout() + - Асинхронная обработка без блокировок -Пример использования: +Пример потока данных: --------------------- -1. Инициализация: - epkt_t* epkt = etcp_init(); - etcp_set_callback(epkt, udp_send_callback, udp_socket); - -2. Отправка данных: - etcp_tx_put(epkt, data, len); +1. App отправляет данные → conn_send() → app_input_queue +2. Queue callback → pkt_normalizer(packer) → фрагментация +3. Packer output → tx_queue → tx_process() +4. tx_process() формирует пакет → tx_callback → UDP send +5. Пакет в сети → UDP recv → etcp_rx_input() +6. Обработка ACK/данных → output_queue → pkt_normalizer(unpacker) +7. Сборка фрагментов → app_output_queue → app callback -3. Обработка входящих пакетов: - etcp_rx_input(epkt, pkt, len); - -4. Чтение полученных данных: - ll_queue_t* output = etcp_get_output_queue(epkt); - ll_entry_t* entry = queue_entry_get(output); - // Обработка entry... - -5. Освобождение: - etcp_free(epkt); - -Примечания: ------------ +Reset сценарий: +--------------- -- Протокол предназначен для работы в условиях умеренных потерь и реордеринга -- Механизм forward progress обеспечивает доставку даже при потере начальных пакетов -- Управление окном предотвращает перегрузку сети -- Поддержка двунаправленной связи (каждая сторона может быть одновременно отправителем и получателем) -- Минимальные накладные расходы: 4 байта на пакет + опциональные метаданные \ No newline at end of file +1. Обнаружена проблема → etcp_reset_connection() +2. reset_pending=1, блокировка data packets +3. Отправка 0x02, таймер повторной отправки +4. Получатель: получен 0x02 → отправка 0x03 +5. Если initialized=1 → etcp_reset() → reset_callback → очистка pkt_normalizer +6. Отправитель: получен 0x03 → reset_ack_received=1, reset_pending=0 +7. Возобновление передачи, повторная отправка данных из app_input_queue \ No newline at end of file diff --git a/etcp_plan.txt b/etcp_plan.txt deleted file mode 100644 index 2480b69..0000000 --- a/etcp_plan.txt +++ /dev/null @@ -1,273 +0,0 @@ -# План реализации ETCP (Extended Transmission Control Protocol) - -## Обзор -Протокол поверх UDP с восстановлением порядка, повторными передачами, метриками RTT/jitter и ограничением полосы. Использует две очереди: сортированный linked-list для принятых пакетов и ll_queue для выходных данных. - -## Структуры данных - -### struct epkt (etcp.h) -```c -typedef struct epkt epkt_t; - -struct epkt { - // Очереди - ll_queue_t* tx_queue; // Очередь пакетов на передачу - ll_queue_t* output_queue; // Выходная очередь (собранные данные) - struct rx_packet* rx_list; // Сортированный linked-list принятых пакетов - - // Метрики - uint16_t rtt_last; // Последний RTT (timebase 0.1ms) - uint16_t rtt_avg_10; // Среднее RTT за последние 10 пакетов - uint16_t rtt_avg_100; // Среднее RTT за последние 100 пакетов - uint16_t jitter; // Jitter (усреднённый) - uint16_t bandwidth; // Текущая полоса пропускания (байт/таймбазу) - uint32_t bytes_sent_total; // Всего отправлено байт - uint16_t last_sent_timestamp; // Timestamp последней отправки - uint32_t bytes_allowed; // Расчётное число байт, которое можно отправить - - // Состояние - uint16_t next_tx_id; // Следующий ID для передачи - uint16_t last_rx_id; // Последний принятый ID (для ACK) - uint16_t last_delivered_id; // Последний доставленный в output_queue ID - - // Таймеры - void* next_tx_timer; // Таймер следующей передачи - void* retransmit_timer; // Таймер повторных передач - - // Callback'и - void (*tx_callback)(epkt_t*, uint8_t*, uint16_t); // Отправка в UDP - void* tx_callback_arg; - - // Буферы для метрик - uint16_t rtt_history[100]; // История RTT для усреднения - uint8_t rtt_history_idx; - uint8_t rtt_history_count; - - // Накопленные timestamp'ы для отчётов - uint16_t pending_ack_ids[32]; // ID пакетов, для которых нужно отправить ACK - uint16_t pending_ack_timestamps[32]; // Соответствующие timestamp'ы - uint8_t pending_ack_count; -}; -``` - -### struct rx_packet (внутренняя) -```c -struct rx_packet { - struct rx_packet* next; - uint16_t id; - uint16_t timestamp; - uint8_t* data; - uint16_t data_len; -// uint8_t has_payload; // 1 если содержит payload (hdr=0) - пакеты без payload сюда не попадаютю просто парсим сразу метрици и всё. -}; -``` - -## Формат пакета -``` - [ ]* -``` - -### Заголовки (hdr): -- `0x00`: payload (данные начинаются со следующего байта) -- `0x01`: отчёт о timestamp принятого пакета (4 байта: id:2 + timestamp:2) -- `0x10`-`0x2F`: перезапрос пакетов + ACK: - - hdr & 0x0F = количество записей (1-32) - - Далее N*2 байт: ID пакетов для повторной передачи - - Последние 2 байта: номер последнего доставленного пакета (ACK) - -### Пакет только с метриками: -Если очередь передачи пуста, отправляется пакет с id=0 и без hdr=0+payload. - -## Алгоритмы - -### 1. Инициализация -- Создать очереди tx_queue и output_queue через queue_new() -- Инициализировать метрики нулями -- Установить bandwidth по умолчанию (например, 100000 байт/таймбазу) - -### 2. Приём пакетов (etcp_rx_input) -1. Парсинг: - - Читаем id, timestamp - - Пока есть данные, читаем hdr: - - hdr=0x00: запоминаем payload - - hdr=0x01: обрабатываем отчёт о timestamp (обновляем метрики) - - hdr=0x10-0x2F: обрабатываем перезапрос (добавляем ID в очередь повторной передачи) -2. Для пакетов с hdr!=0 вызываем upd_metric_for_transmitter: - - rtt_last = текущее время - timestamp - - Обновляем rtt_avg_10 и rtt_avg_100 (скользящее среднее) - - jitter += (abs(rtt_avg_10 - rtt_last) - jitter) * 0.1 -3. Добавляем пакет в сортированный rx_list: - - Ищем позицию по id (сравнение через (int16_t)(id1-id2)) - - Пропускаем дубликаты - - Вставляем в нужное место -4. Перемещаем непрерывную последовательность в output_queue: - - Начиная с last_delivered_id+1, проверяем наличие пакетов в rx_list - - При нахождении непрерывной цепочки перемещаем payload в output_queue - - Освобождаем rx_packet структуры -5. Накопление ACK: - - Для каждого принятого пакета сохраняем id и timestamp в pending_ack_* - - При следующей отправке включаем эти ACK в пакет - -### 3. Передача пакетов -1. Очередь tx_queue содержит данные для отправки -2. Функция tx_process вызывается по таймеру или при добавлении в пустую очередь: - - Проверяем ограничение полосы: - - delta_time = текущее_время - last_sent_timestamp - - bytes_allowed += delta_time * bandwidth - - Если bytes_allowed < размер_пакета, планируем таймер и выходим - - Формируем пакет: - - Базовый заголовок: next_tx_id++, текущий timestamp - - Добавляем pending_ack (hdr=0x01 для каждого) - - Добавляем перезапросы если нужно (на основе метрик) - - Добавляем payload из tx_queue (hdr=0x00) - - Если payload нет и есть метрики - отправляем пакет с id=0 - - Отправляем через tx_callback - - Обновляем bytes_sent_total, last_sent_timestamp, bytes_allowed - - Сохраняем пакет в список отправленных (для возможной ретрансмиссии) -3. Повторные передачи: - - Для каждого отправленного пакета отслеживаем время отправки - - Если (текущее_время - время_отправки) > rtt_avg_10*1.2 + jitter*2 - - Добавляем ID в очередь повторной передачи - -### 4. Ограничение полосы -- bandwidth: константа (байт/таймбазу), timebase = 0.1ms -- При инициализации: bytes_allowed = 0, last_sent_timestamp = текущее_время -- При отправке: - - current_time = uasync время - - delta = (int16_t)(current_time - last_sent_timestamp) (циклическое) - - bytes_allowed += delta * bandwidth - - Если bytes_allowed >= размер_пакета: - - bytes_allowed -= размер_пакета - - last_sent_timestamp = current_time - - Отправляем пакет - - Иначе: - - wait_time = (размер_пакета - bytes_allowed) / bandwidth - - Устанавливаем таймер на wait_time - -## API функции - -### Основные: -```c -epkt_t* etcp_init(void); -void etcp_free(epkt_t* epkt); -void etcp_set_callback(epkt_t* epkt, void (*cb)(epkt_t*, uint8_t*, uint16_t), void* arg); -int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len); -int etcp_tx_queue_size(epkt_t* epkt); -``` - -### Вспомогательные: -```c -void etcp_set_bandwidth(epkt_t* epkt, uint16_t bandwidth); -uint16_t etcp_get_rtt(epkt_t* epkt); -uint16_t etcp_get_jitter(epkt_t* epkt); -ll_queue_t* etcp_get_output_queue(epkt_t* epkt); // Для извлечения данных -int etcp_tx_put(epkt_t* epkt, uint8_t* data, uint16_t len); // Добавить данные на передачу -``` - -## Интеграция с проектом - -### Зависимости: -- `ll_queue.c/.h` - для очередей -- `u_async.h` - для таймеров -- `stdint.h`, `stdlib.h`, `string.h` - стандартные библиотеки - -### Таймеры: -- Использовать `uasync_set_timeout` для: - - Планирования следующей передачи (при ограничении полосы) - - Повторных передач - - Очистки старых пакетов в rx_list -- Все callback'и должны быть быстрыми, не блокирующими - -### Обработка циклических значений: -- ID: uint16_t, сравнение через `(int16_t)(a - b)` -- Timestamp: uint16_t, timebase 0.1us, сравнение аналогично -- При вычислении дельты времени учитывать переполнение - -## План тестирования - -1. **Unit-тесты для парсинга:** - - Корректность разбора различных hdr - - Обработка циклических ID - -2. **Тесты очередей:** - - Сортировка в rx_list - - Перемещение в output_queue - - Обработка дубликатов - -3. **Тесты метрик:** - - Расчет RTT (скользящее среднее) - - Расчет jitter - - Обновление bandwidth - -4. **Интеграционный тест:** - - Два экземпляра ETCP, обмен данными через эмуляцию UDP - - Проверка восстановления порядка при потере пакетов - - Проверка ограничения полосы - -5. **Тест производительности:** - - Минимальные задержки - - Корректность работы при высокой нагрузке - -## Последовательность реализации - -1. Создать etcp.h с определениями структур и API -2. Реализовать etcp.c в следующем порядке: - a) Базовая структура и init/free - b) Внутренние функции для работы с rx_list - c) Парсинг пакетов (etcp_rx_input) - d) Функции метрик (upd_metric_for_transmitter) - e) Механизм передачи (tx_process, ограничение полосы) - f) Повторные передачи - g) Интеграция с таймерами u_async -3. Создать тестовую программу test_etcp.c -4. Протестировать, исправить ошибки -5. Интегрировать в Makefile - -## Риски и неопределённости - -1. **Переполнение буферов:** Нужны лиматиры на размер rx_list и pending_ack -3. **Производительность:** Сортированный linked-list может быть медленным при большом количестве пакетов. Возможно, нужна оптимизация. - не должно быть много пакетов в списке перезапросов -4. **Интеграция с существующим кодом:** Проверить совместимость стиля кодирования и соглашений об именовании. - -## Дополнительные вопросы - -1. Нужны ли callback'и для событий (доставка данных, изменение метрик)? - нет, но нужна структура из которой эти метрики можно считывать. -2. Как обрабатывать очень старые пакеты в rx_list (таймаут)? - никак. надо сделать reset_connection - он обнуляет все очереди, метрики итд. -3. Как определять начальный bandwidth и адаптировать его? - пока константа. адаптировать позже. - -## Статус реализации (13.01.2026) - -### Реализовано: -1. Структуры epkt, rx_packet, sent_packet -2. API функции: etcp_init, etcp_free, etcp_set_callback, etcp_rx_input, etcp_tx_queue_size, etcp_tx_put, etcp_get_output_queue, etcp_set_bandwidth, etcp_get_rtt, etcp_get_jitter -3. Парсинг пакетов с поддержкой заголовков 0x00 (payload), 0x01 (timestamp report), 0x10-0x2F (retransmission request) -4. Сортированный rx_list с учётом циклических ID (сравнение через (int16_t)(id1-id2)) -5. Перемещение непрерывной последовательности пакетов в output_queue -6. Ограничение полосы пропускания (bandwidth, bytes_allowed) -7. Отправка пакетов с данными и метриками, включая пакеты только с метриками (id=0) -8. Накопление ACK для полученных пакетов и отправка их в следующий пакет -9. Повторные передачи на основе RTT и jitter -10. Обновление метрик RTT (скользящее среднее за 10 и 100 пакетов) и jitter -11. Интеграция с u_async для таймеров передачи и проверки повторных отправок -12. Unit-тесты, покрывающие базовую функциональность (инициализация, передача, приём, реordering) - -### Особенности реализации: -- Timebase: 0.1ms (совместимость с u_async) -- Bandwidth по умолчанию: 10000 байт/таймбазу -- Максимальное количество pending ACK: 32 -- Максимальное количество pending retransmit: 32 -- RTT история: 100 измерений - -### Ограничения и упрощения: -- Отсутствует таймаут для старых пакетов в rx_list -- Отсутствует адаптация bandwidth -- get_current_timestamp использует статический счётчик (для тестов) -- Нет обработки reset_connection -- Нет защиты от переполнения буферов при очень большом количестве пакетов - -### Планируемые улучшения: -1. Реализация reset_connection для сброса состояния -2. Использование системного времени для get_current_timestamp -3. Добавление таймаута для rx_list -4. Оптимизация производительности при большом количестве пакетов -5. Адаптация bandwidth на основе метрик diff --git a/src/connection.c b/src/connection.c index cbd9594..16cf46d 100644 --- a/src/connection.c +++ b/src/connection.c @@ -65,6 +65,7 @@ static void etcp_tx_callback(epkt_t* epkt, uint8_t* data, uint16_t len, void* ar static void app_output_callback(ll_queue_t* q, ll_entry_t* entry, void* arg); static void process_received_packet(conn_handle_t* conn, uint8_t* data, size_t len); static void update_stats_from_etcp(conn_handle_t* conn); +static void conn_etcp_reset_callback(epkt_t* epkt, void* arg); /* Bridge functions for data flow */ static void app_input_bridge(ll_queue_t* q, ll_entry_t* entry, void* arg); @@ -215,6 +216,7 @@ int conn_connect(conn_handle_t* conn, /* Настройка callback'ов ETCP */ etcp_set_callback(conn->etcp, etcp_tx_callback, conn); + etcp_set_reset_callback(conn->etcp, conn_etcp_reset_callback, conn); /* Начальная полоса пропускания: максимальная для uint16_t ≈ 6553 байт/мс */ etcp_set_bandwidth(conn->etcp, 65535); /* в единицах 0.1мс: 6553.5 * 10 */ @@ -455,34 +457,34 @@ static void packer_output_bridge(ll_queue_t* q, ll_entry_t* entry, void* arg) uint8_t* data_to_send = data; uint16_t data_len = (uint16_t)len; + uint8_t* encrypted_buffer = NULL; - /* Encrypt if crypto is ready - TEMPORARILY DISABLED */ + /* Encrypt if crypto is ready */ if (conn->crypto_ready) { - /* Allocate buffer for ciphertext + tag */ - uint8_t* ciphertext = malloc(len); - uint8_t tag[SC_TAG_SIZE]; - sc_status_t status = sc_encrypt(&conn->crypto_ctx, data, len, ciphertext, tag); - if (status == SC_OK) { - /* Combine ciphertext and tag into single buffer */ - uint8_t* encrypted_data = malloc(len + SC_TAG_SIZE); - if (encrypted_data) { - memcpy(encrypted_data, ciphertext, len); - memcpy(encrypted_data + len, tag, SC_TAG_SIZE); - data_to_send = encrypted_data; - data_len = (uint16_t)(len + SC_TAG_SIZE); + /* Allocate buffer for encrypted data (plaintext + CRC32 + tag) */ + size_t encrypted_len = 0; + encrypted_buffer = malloc(len + SC_MAX_OVERHEAD); + if (encrypted_buffer) { + sc_status_t status = sc_encrypt(&conn->crypto_ctx, data, len, encrypted_buffer, &encrypted_len); + if (status == SC_OK && encrypted_len > 0 && encrypted_len <= len + SC_MAX_OVERHEAD) { + data_to_send = encrypted_buffer; + data_len = (uint16_t)encrypted_len; + } else { + /* Encryption failed, free buffer and fall back to plaintext */ + free(encrypted_buffer); + encrypted_buffer = NULL; + /* Увеличиваем счетчик ошибок шифрования */ + conn->stats.encryption_errors++; } - /* Free ciphertext buffer */ - free(ciphertext); } - /* If encryption failed, fall back to plaintext */ } /* Note: etcp_tx_put copies data, we can free entry after call */ int result = etcp_tx_put(conn->etcp, data_to_send, data_len); - /* Free encrypted buffer if it was allocated */ - if (conn->crypto_ready && data_to_send != data) { - free(data_to_send); + /* Free encrypted buffer if it was allocated and used */ + if (encrypted_buffer && data_to_send == encrypted_buffer) { + free(encrypted_buffer); } queue_entry_free(entry_to_process); @@ -512,22 +514,28 @@ static void etcp_output_bridge(ll_queue_t* q, ll_entry_t* entry, void* arg) ll_entry_t* new_entry = NULL; - /* Decrypt if crypto is ready - TEMPORARILY DISABLED */ + /* Decrypt if crypto is ready */ if (conn->crypto_ready) { - size_t ciphertext_len = len - SC_TAG_SIZE; - uint8_t* ciphertext = data; - uint8_t* tag = data + ciphertext_len; - - /* Allocate buffer for plaintext */ - uint8_t* plaintext = malloc(ciphertext_len); + /* Allocate buffer for plaintext (max size: len - SC_MAX_OVERHEAD) */ + size_t plaintext_len = 0; + uint8_t* plaintext = malloc(len); /* Достаточно места для худшего случая */ if (plaintext) { - sc_status_t status = sc_decrypt(&conn->crypto_ctx, ciphertext, ciphertext_len, tag, plaintext); - if (status == SC_OK) { + sc_status_t status = sc_decrypt(&conn->crypto_ctx, data, len, plaintext, &plaintext_len); + if (status == SC_OK && plaintext_len > 0 && plaintext_len <= len - SC_MAX_OVERHEAD) { /* Create new entry with plaintext */ - new_entry = queue_entry_new(ciphertext_len); + new_entry = queue_entry_new(plaintext_len); if (new_entry) { - memcpy(ll_entry_data(new_entry), plaintext, ciphertext_len); + memcpy(ll_entry_data(new_entry), plaintext, plaintext_len); + } + } else { + /* Ошибка расшифрования */ + if (status == SC_ERR_CRC_FAILED) { + conn->stats.crc_errors++; + } else if (status == SC_ERR_AUTH_FAILED) { + conn->stats.decryption_errors++; } + /* Другие ошибки (SC_ERR_INVALID_ARG, SC_ERR_NOT_INITIALIZED и т.д.) + не увеличивают счетчики специфических ошибок */ } free(plaintext); } @@ -740,4 +748,26 @@ static void update_stats_from_etcp(conn_handle_t* conn) conn->stats.jitter_ms = etcp_get_jitter(conn->etcp) / 10; /* TODO: получение счетчика ретрансмиссий из ETCP */ +} + +static void conn_etcp_reset_callback(epkt_t* epkt, void* arg) +{ + (void)epkt; /* unused parameter */ + conn_handle_t* conn = (conn_handle_t*)arg; + if (!conn || !conn->normalizer) { + return; + } + + ETCP_LOG("Connection reset callback invoked, resetting pkt_normalizer state\n"); + + /* Сброс состояния нормализатора пакетов */ + if (conn->normalizer->packer) { + pkt_normalizer_reset_state(conn->normalizer->packer); + } + if (conn->normalizer->unpacker) { + pkt_normalizer_reset_state(conn->normalizer->unpacker); + } + + /* Note: Данные в очередях приложения (app_input_queue, app_output_queue) не очищаются, + они остаются для повторной отправки после завершения reset handshake */ } \ No newline at end of file diff --git a/src/connection.h b/src/connection.h index e17d518..51b4206 100644 --- a/src/connection.h +++ b/src/connection.h @@ -118,6 +118,9 @@ typedef struct { uint32_t fragments_assembled; uint16_t current_rtt_ms; /* Текущее RTT в миллисекундах */ uint16_t jitter_ms; /* Джиттер в миллисекундах */ + uint32_t encryption_errors; /* Ошибки шифрования */ + uint32_t decryption_errors; /* Ошибки расшифрования */ + uint32_t crc_errors; /* Ошибки CRC32 */ } conn_stats_t; int conn_get_stats(conn_handle_t* conn, conn_stats_t* stats); diff --git a/src/etcp.c b/src/etcp.c index eda8207..9b218c9 100644 --- a/src/etcp.c +++ b/src/etcp.c @@ -127,6 +127,15 @@ epkt_t* etcp_init(uasync_t* ua) { epkt->oldest_missing_id = 0; epkt->missing_since_time = 0; + // Reset state initialization + epkt->reset_pending = 0; + epkt->reset_ack_received = 0; + epkt->reset_timer = NULL; + epkt->reset_retry_count = 0; + epkt->initialized = 0; + epkt->reset_callback = NULL; + epkt->reset_callback_arg = NULL; + // No timers yet epkt->next_tx_timer = NULL; epkt->retransmit_timer = NULL; @@ -184,6 +193,12 @@ void etcp_set_callback(epkt_t* epkt, etcp_tx_callback_t cb, void* arg) { epkt->tx_callback_arg = arg; } +void etcp_set_reset_callback(epkt_t* epkt, etcp_reset_callback_t cb, void* arg) { + if (!epkt) return; + epkt->reset_callback = cb; + epkt->reset_callback_arg = arg; +} + // Get output queue ll_queue_t* etcp_get_output_queue(epkt_t* epkt) { return epkt ? epkt->output_queue : NULL; @@ -229,6 +244,11 @@ void etcp_update_window(epkt_t* epkt) { void etcp_reset(epkt_t* epkt) { if (!epkt) return; + // Call reset callback before clearing state + if (epkt->reset_callback) { + epkt->reset_callback(epkt, epkt->reset_callback_arg); + } + // Cancel timers if (epkt->next_tx_timer) { uasync_cancel_timeout(epkt->ua, epkt->next_tx_timer); @@ -485,6 +505,18 @@ static void tx_process(epkt_t* epkt) { data_len = ll_entry_size(entry); } + // Block data packets during reset handshake (allow service packets: ACKs, retransmit requests) + // Reset packets are sent via etcp_send_reset independently + if (data_packet && epkt->reset_pending && !epkt->reset_ack_received) { + // Data packet during reset handshake, block transmission + queue_entry_put_first(epkt->tx_queue, entry); + // Schedule next attempt after retransmit timer period + if (!epkt->next_tx_timer) { + epkt->next_tx_timer = uasync_set_timeout(epkt->ua, epkt->retrans_timer_period, epkt, tx_timer_callback); + } + return; + } + // Check window size for data packets if (data_packet && epkt->window_size != (uint32_t)-1) { if (epkt->unacked_bytes + data_len > epkt->window_size) { @@ -676,6 +708,11 @@ static void tx_process(epkt_t* epkt) { static void retransmit_check(epkt_t* epkt) { if (!epkt) return; + // Skip retransmission checks during reset handshake + if (epkt->reset_pending && !epkt->reset_ack_received) { + return; + } + uint16_t current_time = get_current_timestamp(); // Threshold = RTT * 1.5 uint16_t threshold = epkt->rtt_avg_10 + epkt->rtt_avg_10 / 2; @@ -890,13 +927,17 @@ int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len) { ETCP_LOG("Reset request received\n"); // Send reset ACK etcp_send_reset_ack(epkt); - // Reset our own state - etcp_reset(epkt); + // Reset our own state only if already initialized + if (epkt->initialized) { + etcp_reset(epkt); + } } else if (hdr == ETCP_RESET_ACK_HEADER) { // Reset ACK received ETCP_LOG("Reset ACK received\n"); epkt->reset_ack_received = 1; epkt->reset_pending = 0; + // Connection is now initialized + epkt->initialized = 1; if (epkt->reset_timer) { uasync_cancel_timeout(epkt->ua, epkt->reset_timer); epkt->reset_timer = NULL; diff --git a/src/etcp.h b/src/etcp.h index 3a51053..79feefe 100644 --- a/src/etcp.h +++ b/src/etcp.h @@ -31,6 +31,9 @@ typedef struct epkt epkt_t; // Тип обратного вызова для отправки пакетов через UDP typedef void (*etcp_tx_callback_t)(epkt_t* epkt, uint8_t* pkt, uint16_t len, void* arg); +// Тип обратного вызова для уведомления о reset +typedef void (*etcp_reset_callback_t)(epkt_t* epkt, void* arg); + // Основная структура ETCP struct epkt { // Очереди @@ -108,6 +111,11 @@ struct epkt { uint8_t reset_ack_received; // Reset ACK received void* reset_timer; // Timer for reset retransmission uint16_t reset_retry_count; // Number of reset retries + + // Reset callback and initialization state + uint8_t initialized; // 1 = connection initialized after first reset handshake + etcp_reset_callback_t reset_callback; // Callback for reset notification + void* reset_callback_arg; // Callback argument }; // Функции API @@ -133,6 +141,14 @@ void etcp_free(epkt_t* epkt); */ void etcp_set_callback(epkt_t* epkt, etcp_tx_callback_t cb, void* arg); +/** + * @brief Установить обратный вызов для уведомления о reset + * @param epkt Экземпляр ETCP + * @param cb Функция обратного вызова + * @param arg Пользовательский аргумент, передаваемый в обратный вызов + */ +void etcp_set_reset_callback(epkt_t* epkt, etcp_reset_callback_t cb, void* arg); + /** * @brief Обработать полученный UDP пакет * @param epkt Экземпляр ETCP diff --git a/src/sc_lib.c b/src/sc_lib.c index 6b3ecc6..066e444 100644 --- a/src/sc_lib.c +++ b/src/sc_lib.c @@ -16,6 +16,42 @@ static const struct uECC_Curve_t *curve = NULL; +/* CRC32 implementation (IEEE 802.3 polynomial 0xEDB88320) */ +static uint32_t crc32_table[256]; +static int crc32_table_initialized = 0; + +static void init_crc32_table(void) { + if (crc32_table_initialized) return; + + uint32_t polynomial = 0xEDB88320; + for (uint32_t i = 0; i < 256; i++) { + uint32_t crc = i; + for (int j = 0; j < 8; j++) { + if (crc & 1) { + crc = (crc >> 1) ^ polynomial; + } else { + crc >>= 1; + } + } + crc32_table[i] = crc; + } + crc32_table_initialized = 1; +} + +static uint32_t crc32(const uint8_t *data, size_t len) { + if (!crc32_table_initialized) { + init_crc32_table(); + } + + uint32_t crc = 0xFFFFFFFF; + for (size_t i = 0; i < len; i++) { + uint8_t byte = data[i]; + uint8_t table_index = (crc ^ byte) & 0xFF; + crc = (crc >> 8) ^ crc32_table[table_index]; + } + return crc ^ 0xFFFFFFFF; +} + static int sc_rng(uint8_t *dest, unsigned size) { int fd = open("/dev/urandom", O_RDONLY); @@ -160,15 +196,17 @@ sc_status_t sc_encrypt(sc_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext, - uint8_t *tag) + size_t *ciphertext_len) { uint8_t nonce[SC_NONCE_SIZE]; struct tc_aes_key_sched_struct sched; struct tc_ccm_mode_struct ccm_state; TCCcmMode_t c = &ccm_state; - uint8_t combined_output[plaintext_len + SC_TAG_SIZE]; + size_t total_plaintext_len = plaintext_len + SC_CRC32_SIZE; + uint8_t plaintext_with_crc[total_plaintext_len]; + uint8_t combined_output[total_plaintext_len + SC_TAG_SIZE]; - if (!ctx || !plaintext || !ciphertext || !tag) { + if (!ctx || !plaintext || !ciphertext || !ciphertext_len) { return SC_ERR_INVALID_ARG; } @@ -180,6 +218,14 @@ sc_status_t sc_encrypt(sc_context_t *ctx, return SC_ERR_INVALID_ARG; } + /* Добавляем CRC32 к данным */ + memcpy(plaintext_with_crc, plaintext, plaintext_len); + uint32_t crc = crc32(plaintext, plaintext_len); + plaintext_with_crc[plaintext_len] = (crc >> 0) & 0xFF; + plaintext_with_crc[plaintext_len + 1] = (crc >> 8) & 0xFF; + plaintext_with_crc[plaintext_len + 2] = (crc >> 16) & 0xFF; + plaintext_with_crc[plaintext_len + 3] = (crc >> 24) & 0xFF; + /* Initialize AES key schedule */ if (tc_aes128_set_encrypt_key(&sched, ctx->session_key) != TC_CRYPTO_SUCCESS) { return SC_ERR_CRYPTO; @@ -196,14 +242,14 @@ sc_status_t sc_encrypt(sc_context_t *ctx, /* Encrypt and generate tag */ if (tc_ccm_generation_encryption(combined_output, sizeof(combined_output), NULL, 0, /* no associated data */ - plaintext, plaintext_len, + plaintext_with_crc, total_plaintext_len, c) != TC_CRYPTO_SUCCESS) { return SC_ERR_CRYPTO; } - /* Split combined output into ciphertext and tag */ - memcpy(ciphertext, combined_output, plaintext_len); - memcpy(tag, combined_output + plaintext_len, SC_TAG_SIZE); + /* Copy ciphertext + tag to output buffer */ + memcpy(ciphertext, combined_output, total_plaintext_len + SC_TAG_SIZE); + *ciphertext_len = total_plaintext_len + SC_TAG_SIZE; ctx->tx_counter++; @@ -213,16 +259,17 @@ sc_status_t sc_encrypt(sc_context_t *ctx, sc_status_t sc_decrypt(sc_context_t *ctx, const uint8_t *ciphertext, size_t ciphertext_len, - const uint8_t *tag, - uint8_t *plaintext) + uint8_t *plaintext, + size_t *plaintext_len) { uint8_t nonce[SC_NONCE_SIZE]; struct tc_aes_key_sched_struct sched; struct tc_ccm_mode_struct ccm_state; TCCcmMode_t c = &ccm_state; - uint8_t combined_input[ciphertext_len + SC_TAG_SIZE]; + size_t total_plaintext_len = ciphertext_len - SC_TAG_SIZE; + uint8_t plaintext_with_crc[total_plaintext_len]; - if (!ctx || !ciphertext || !tag || !plaintext) { + if (!ctx || !ciphertext || !plaintext || !plaintext_len) { return SC_ERR_INVALID_ARG; } @@ -230,14 +277,10 @@ sc_status_t sc_decrypt(sc_context_t *ctx, return SC_ERR_NOT_INITIALIZED; } - if (ciphertext_len == 0) { + if (ciphertext_len < SC_TAG_SIZE + SC_CRC32_SIZE) { return SC_ERR_INVALID_ARG; } - /* Combine ciphertext and tag for CCM input */ - memcpy(combined_input, ciphertext, ciphertext_len); - memcpy(combined_input + ciphertext_len, tag, SC_TAG_SIZE); - /* Initialize AES key schedule */ if (tc_aes128_set_encrypt_key(&sched, ctx->session_key) != TC_CRYPTO_SUCCESS) { return SC_ERR_CRYPTO; @@ -252,13 +295,29 @@ sc_status_t sc_decrypt(sc_context_t *ctx, } /* Decrypt and verify tag */ - if (tc_ccm_decryption_verification(plaintext, ciphertext_len, + if (tc_ccm_decryption_verification(plaintext_with_crc, total_plaintext_len, NULL, 0, /* no associated data */ - combined_input, ciphertext_len + SC_TAG_SIZE, + ciphertext, ciphertext_len, c) != TC_CRYPTO_SUCCESS) { return SC_ERR_AUTH_FAILED; } + /* Проверяем CRC32 */ + size_t data_len = total_plaintext_len - SC_CRC32_SIZE; + uint32_t expected_crc = crc32(plaintext_with_crc, data_len); + uint32_t received_crc = (plaintext_with_crc[data_len] << 0) | + (plaintext_with_crc[data_len + 1] << 8) | + (plaintext_with_crc[data_len + 2] << 16) | + (plaintext_with_crc[data_len + 3] << 24); + + if (expected_crc != received_crc) { + return SC_ERR_CRC_FAILED; + } + + /* Копируем данные без CRC32 */ + memcpy(plaintext, plaintext_with_crc, data_len); + *plaintext_len = data_len; + ctx->rx_counter++; return SC_OK; diff --git a/src/sc_lib.h b/src/sc_lib.h index d3a384d..a95b173 100644 --- a/src/sc_lib.h +++ b/src/sc_lib.h @@ -16,6 +16,8 @@ extern "C" { #define SC_SESSION_KEY_SIZE 16 /* AES-128 key size */ #define SC_NONCE_SIZE 13 #define SC_TAG_SIZE 16 +#define SC_CRC32_SIZE 4 /* CRC-32 checksum */ +#define SC_MAX_OVERHEAD (SC_TAG_SIZE + SC_CRC32_SIZE) /* Максимальное расширение пакета */ /* ===== Коды ошибок ===== */ @@ -24,7 +26,8 @@ typedef enum { SC_ERR_INVALID_ARG, SC_ERR_CRYPTO, SC_ERR_NOT_INITIALIZED, - SC_ERR_AUTH_FAILED + SC_ERR_AUTH_FAILED, + SC_ERR_CRC_FAILED } sc_status_t; /* ===== Контекст защищённого канала ===== */ @@ -83,34 +86,37 @@ sc_status_t sc_set_peer_public_key(sc_context_t *ctx, const uint8_t *peer_public_key); /** - * @brief Зашифровать сообщение + * @brief Зашифровать сообщение с добавлением CRC32 * * @param ctx Контекст * @param plaintext Входные данные - * @param plaintext_len Длина - * @param ciphertext Выход (может совпадать с plaintext) - * @param tag MAC-тег (16 байт) + * @param plaintext_len Длина входных данных + * @param ciphertext Выходной буфер (должен быть размером plaintext_len + SC_MAX_OVERHEAD) + * @param ciphertext_len [out] Длина выходных данных (ciphertext + tag + crc32) + * @return SC_OK при успехе, иначе код ошибки */ sc_status_t sc_encrypt(sc_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext, - uint8_t *tag); + size_t *ciphertext_len); /** - * @brief Расшифровать и проверить сообщение + * @brief Расшифровать и проверить сообщение (включая CRC32) * * @param ctx Контекст - * @param ciphertext Зашифрованные данные - * @param ciphertext_len Длина - * @param tag MAC-тег - * @param plaintext Выход + * @param ciphertext Зашифрованные данные (ciphertext + tag + crc32) + * @param ciphertext_len Длина входных данных + * @param plaintext Выходной буфер (должен быть размером ciphertext_len - SC_MAX_OVERHEAD) + * @param plaintext_len [out] Длина расшифрованных данных + * @return SC_OK при успехе, SC_ERR_AUTH_FAILED при ошибке аутентификации, + * SC_ERR_CRC_FAILED при несовпадении CRC32 */ sc_status_t sc_decrypt(sc_context_t *ctx, const uint8_t *ciphertext, size_t ciphertext_len, - const uint8_t *tag, - uint8_t *plaintext); + uint8_t *plaintext, + size_t *plaintext_len); #ifdef __cplusplus } diff --git a/tests/test_sc_lib.c b/tests/test_sc_lib.c index da531ab..ccdf721 100644 --- a/tests/test_sc_lib.c +++ b/tests/test_sc_lib.c @@ -74,8 +74,9 @@ static int test_encrypt_decrypt(void) const char *plaintext = "Hello, secure channel!"; size_t plaintext_len = strlen(plaintext) + 1; uint8_t ciphertext[256]; - uint8_t tag[SC_TAG_SIZE]; + size_t ciphertext_len; uint8_t decrypted[256]; + size_t decrypted_len; memset(&client_ctx, 0, sizeof(client_ctx)); memset(&server_ctx, 0, sizeof(server_ctx)); @@ -95,22 +96,25 @@ static int test_encrypt_decrypt(void) /* Client encrypts */ status = sc_encrypt(&client_ctx, (const uint8_t *)plaintext, plaintext_len, - ciphertext, tag); + ciphertext, &ciphertext_len); TEST_ASSERT(status == SC_OK, "encryption"); + TEST_ASSERT(ciphertext_len == plaintext_len + SC_MAX_OVERHEAD, "ciphertext length includes overhead"); /* Server decrypts */ - status = sc_decrypt(&server_ctx, ciphertext, plaintext_len, tag, decrypted); + status = sc_decrypt(&server_ctx, ciphertext, ciphertext_len, decrypted, &decrypted_len); TEST_ASSERT(status == SC_OK, "decryption"); + TEST_ASSERT(decrypted_len == plaintext_len, "decrypted length matches original"); /* Verify decrypted matches original */ TEST_ASSERT(memcmp(plaintext, decrypted, plaintext_len) == 0, "decrypted matches original"); - /* Verify tag verification fails with wrong tag */ - uint8_t wrong_tag[SC_TAG_SIZE]; - memset(wrong_tag, 0xAA, sizeof(wrong_tag)); - status = sc_decrypt(&server_ctx, ciphertext, plaintext_len, wrong_tag, decrypted); - TEST_ASSERT(status == SC_ERR_AUTH_FAILED, "wrong tag detection"); + /* Verify tag verification fails with modified ciphertext */ + uint8_t corrupted_ciphertext[256]; + memcpy(corrupted_ciphertext, ciphertext, ciphertext_len); + corrupted_ciphertext[10] ^= 0xAA; /* Изменяем байт шифротекста */ + status = sc_decrypt(&server_ctx, corrupted_ciphertext, ciphertext_len, decrypted, &decrypted_len); + TEST_ASSERT(status == SC_ERR_AUTH_FAILED, "modified ciphertext detection"); /* Verify counter increment */ TEST_ASSERT(client_ctx.tx_counter == 1, "client tx_counter incremented"); @@ -123,7 +127,7 @@ static int test_error_handling(void) { sc_context_t ctx; uint8_t buffer[32]; - uint8_t tag[SC_TAG_SIZE]; + size_t out_len; sc_status_t status; memset(&ctx, 0, sizeof(ctx)); @@ -142,7 +146,7 @@ static int test_error_handling(void) status = sc_generate_keypair(&ctx); TEST_ASSERT(status == SC_OK, "key generation for error test"); - status = sc_encrypt(&ctx, buffer, sizeof(buffer), buffer, tag); + status = sc_encrypt(&ctx, buffer, sizeof(buffer), buffer, &out_len); TEST_ASSERT(status == SC_ERR_NOT_INITIALIZED, "encrypt without session"); return 0; diff --git a/tests/test_udp_secure.c b/tests/test_udp_secure.c index e1e2ff1..dfecb9e 100644 --- a/tests/test_udp_secure.c +++ b/tests/test_udp_secure.c @@ -82,23 +82,25 @@ static void client_b_read_callback(int fd, void* arg) { } printf("Client B received %zd bytes\n", n); - // Expect encrypted message + tag - size_t ciphertext_len = n - SC_TAG_SIZE; - if (ciphertext_len <= 0 || ciphertext_len > sizeof(state->rx_buffer) - SC_TAG_SIZE) { + // Expect encrypted message (ciphertext + tag + crc32) + size_t ciphertext_len = n; + if (ciphertext_len <= SC_MAX_OVERHEAD || ciphertext_len > sizeof(state->rx_buffer)) { printf("ERROR: Invalid received length\n"); test_failed = 1; return; } uint8_t* ciphertext = state->rx_buffer; - uint8_t* tag = state->rx_buffer + ciphertext_len; uint8_t decrypted[1024]; + size_t decrypted_len; - sc_status_t status = sc_decrypt(&state->ctx, ciphertext, ciphertext_len, tag, decrypted); + const char* expected = "Hello from client A over UDP!"; + size_t expected_len = strlen(expected) + 1; + sc_status_t status = sc_decrypt(&state->ctx, ciphertext, ciphertext_len, decrypted, &decrypted_len); TEST_ASSERT(status == SC_OK, "decryption of received message"); + TEST_ASSERT(decrypted_len == expected_len, "decrypted length matches original"); - const char* expected = "Hello from client A over UDP!"; - TEST_ASSERT(memcmp(decrypted, expected, strlen(expected) + 1) == 0, + TEST_ASSERT(memcmp(decrypted, expected, decrypted_len) == 0, "decrypted matches expected plaintext"); printf("Client B successfully decrypted message: %s\n", (char*)decrypted); @@ -132,18 +134,18 @@ static void generate_keys(client_state_t* client) { static void send_encrypted_message(client_state_t* src, client_state_t* dst) { const char* plaintext = "Hello from client A over UDP!"; size_t plaintext_len = strlen(plaintext) + 1; + size_t ciphertext_len; - uint8_t ciphertext[plaintext_len]; - uint8_t tag[SC_TAG_SIZE]; + uint8_t ciphertext[plaintext_len + SC_MAX_OVERHEAD]; sc_status_t status = sc_encrypt(&src->ctx, (const uint8_t*)plaintext, plaintext_len, - ciphertext, tag); + ciphertext, &ciphertext_len); TEST_ASSERT(status == SC_OK, "encryption for sending"); + TEST_ASSERT(ciphertext_len == plaintext_len + SC_MAX_OVERHEAD, "ciphertext length includes overhead"); - // Combine ciphertext and tag into single packet - memcpy(src->tx_buffer, ciphertext, plaintext_len); - memcpy(src->tx_buffer + plaintext_len, tag, SC_TAG_SIZE); - size_t packet_len = plaintext_len + SC_TAG_SIZE; + // Send ciphertext (already includes tag + crc32) + memcpy(src->tx_buffer, ciphertext, ciphertext_len); + size_t packet_len = ciphertext_len; ssize_t sent = sendto(src->sockfd, src->tx_buffer, packet_len, 0, (struct sockaddr*)&dst->addr, sizeof(dst->addr));