Browse Source

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
v2_dev
Evgeny 3 months ago
parent
commit
8d8fc4be6e
  1. 27
      changelog.txt
  2. 471
      etcp.txt
  3. 273
      etcp_plan.txt
  4. 90
      src/connection.c
  5. 3
      src/connection.h
  6. 45
      src/etcp.c
  7. 16
      src/etcp.h
  8. 95
      src/sc_lib.c
  9. 32
      src/sc_lib.h
  10. 24
      tests/test_sc_lib.c
  11. 30
      tests/test_udp_secure.c

27
changelog.txt

@ -68,3 +68,30 @@ Thu Jan 15 2026 17:49: Унификация модуля u_async для осно
- Обновлен Makefile net_emulator: использует общие объектные файлы из ../obj/src/, добавлен -I../u_async
- Удалены тестовые файлы net_emulator (test_timeout_heap.c, test_uasync_random.c) как несовместимые с общей реализацией
- Все цели сборки (основной проект и 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
- Все тесты компилируются и проходят успешно

471
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; // Отправленные пакеты (для повторной передачи)
// Метрики
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 last_delivered_id; // Последний ID, переданный в output_queue
// Таймеры
void* next_tx_timer; // Таймер для следующей передачи
void* retransmit_timer; // Таймер для повторных передач
struct rx_packet* rx_list; // Полученные пакеты (отсортированный по ID)
struct sent_packet* sent_list; // Отправленные пакеты (ждут ACK)
// Обратный вызов для отправки
// Идентификаторы и тайминги
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 от пира
// Метрики и управление потоком
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; // Флаг блокировки передачи
// Буферы служебной информации
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; // Количество запросов
// Отслеживание прогресса (forward progress)
uint16_t oldest_missing_id; // Самый старый отсутствующий ID
uint16_t missing_since_time; // Время первого обнаружения пропуска
// 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
// Таймеры (uasync-based)
void* next_tx_timer; // Таймер следующей передачи
void* retransmit_timer; // Таймер проверки ретрансмиссий
uasync_t* ua; // Инстанс uasync
// Callback отправки
etcp_tx_callback_t tx_callback;
void* tx_callback_arg;
// История RTT для усреднения
uint16_t rtt_history[100];
uint8_t rtt_history_idx;
uint8_t rtt_history_count;
// Ожидающие подтверждения
uint16_t pending_ack_ids[32];
uint16_t pending_ack_timestamps[32];
uint8_t pending_ack_count;
// Ожидающие запросы на повторную передачу
uint16_t pending_retransmit_ids[32];
uint8_t pending_retransmit_count;
// Управление окном
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; // Флаг: передача заблокирована из-за ограничения окна
// Отслеживание прогресса доставки
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 байта на пакет + опциональные метаданные
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

273
etcp_plan.txt

@ -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 сюда не попадаютю просто парсим сразу метрици и всё.
};
```
## Формат пакета
```
<id:2> <timestamp:2> [<hdr:1> <metrics_data>]* <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 на основе метрик

90
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);
}
@ -741,3 +749,25 @@ static void update_stats_from_etcp(conn_handle_t* conn)
/* 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 */
}

3
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);

45
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;

16
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

95
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;

32
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
}

24
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;

30
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));

Loading…
Cancel
Save