From f6ef38aa5dcc5242e5233e580752178123b61b5f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 25 Apr 2026 18:42:20 +0300 Subject: [PATCH] update documentation --- AGENTS.md | 408 ++++++++++++++++++++++++------------------ doc/etcp_protocol.txt | 213 ++++++++++++++-------- doc/route_p2pconn.txt | 344 +++++++++++++++++++++++++++++++++++ 3 files changed, 720 insertions(+), 245 deletions(-) create mode 100644 doc/route_p2pconn.txt diff --git a/AGENTS.md b/AGENTS.md index 12db9954..71b94405 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,58 +3,80 @@ Ты - профессиональный программист высокого уровня. Ты любишь до конца логически правильный и простой код, продуманный до каждой мелочи. Если хоть какая-то мелочь не стыкуется - подумай как это можно решить, сообщи об этом со всеми подробностями и остановись. -Если что-то получается нелогично или громоздко - хорошо подкмай как сделать просто и компактно. предложи варианты и остановись. +Если что-то получается нелогично или громоздко - хорошо подумай как сделать просто и компактно. предложи варианты и остановись. Имей, загружай когда надо полный код нужных тебе функций/структур. Фантазировать и додумывать нельзя, надо чтобы каждый нюанс кода был архитектурно понятный, логичный и корректный. -Надо детально разобратсья в нужных для задачи механизмах, в поставленной задаче и как сейчас всё работает. -Старайся одно логичеси завершенное действие размещать на одной строке, если строка не слишком длинная (до 150 символов) +Надо детально разобраться в нужных для задачи механизмах, в поставленной задаче и как сейчас всё работает. +Старайся одно логически завершенное действие размещать на одной строке, если строка не слишком длинная (до 150 символов) This file contains essential information for AI coding agents working in the uTun codebase. -## 📋 Quick Reference +## Quick Reference -**Repository:** uTun - Secure VPN tunnel with ETCP protocol -**Language:** C (C99) -**Build System:** GNU Autotools (autoconf/automake) -**Cryptography:** TinyCrypt (AES-CCM, ECC) +**Repository:** uTun - Secure VPN tunnel with ETCP protocol +**Language:** C (C99) +**Build System:** GNU Autotools (autoconf/automake) +**Cryptography:** TinyCrypt + OpenSSL (AES-CCM, ECC, SHA256) -## 🔧 Build Commands +## Build Commands -### Full Build -win: powershell build_full.bat (в точности как написано без дополнительных опций powershell, текущий каталог не важен) +### Full Build (Linux) ```bash -./configure # Configure build -make # Build everything -make install # Install (requires sudo) +./build.sh --full -j4 # autoreconf + configure + make ``` -### Partial Builds -win: powershell -Command ".\build.bat" 2>&1 (аналог make если не изменен makefile.am) -логи сборки win: build_win.log +### Full Build (Windows/MSYS2) +```powershell +powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64 +``` + +### Incremental Build ```bash -cd lib && make # Build only the library -cd src && make # Build only the main program -cd tests && make # Build only tests +./build.sh -j4 # make с авто-конфигурацией если надо +``` +```powershell +powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log ``` ### Clean Build ```bash -make clean # Clean object files -make distclean # Clean everything including configure files +make clean # Clean object files +make distclean # Clean everything including configure files +./build.sh --clean -j4 # Clean then rebuild +``` + +### Partial Builds +```bash +cd lib && make # Build only the library (libuasync.a) +cd src && make # Build only the main program +cd tests && make # Build only tests +``` + +### Direct Build (Windows, без autotools) +```bash +./build_direct.sh # Компиляция вручную с x86_64-w64-mingw32-gcc ``` -## 🧪 Test Commands +## Test Commands ### Run All Tests -win: powershell check.bat ```bash -make check # Run all tests via automake +make check # Run all tests via automake, логи в tests/logs/ +``` +```powershell +powershell check.bat # Windows, запускает каждый .exe из tests/ ``` ### Run Specific Test ```bash -cd tests/ && ./test_etcp_crypto # ETCP crypto test -cd tests/ && ./test_etcp_simple # Simple ETCP test -cd tests/ && ./test_ecc_encrypt # ECC encryption test +cd tests/ +./test_etcp_crypto +./test_etcp_two_instances +./test_etcp_simple_traffic +./test_pkt_normalizer_etcp +./test_etcp_api +./test_ll_queue +./test_nat_detection +./test_bgp_route_exchange ``` ### Run Single Test with Debug Info @@ -65,7 +87,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \ ./my_test ``` -## 💻 Code Style Guidelines +## Code Style Guidelines ### Naming Conventions - **Functions:** `snake_case` - `etcp_connection_create()`, `sc_encrypt()` @@ -85,7 +107,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \ } ``` - **Comments:** Primary language is Russian for business logic, English for API docs -- **Line Length:** Aim for 80-100 characters +- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent ### Include Order ```c @@ -108,195 +130,241 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \ DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to initialize: %s", err); DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port); ``` - -## 🔐 Cryptography Guidelines +- Во всех блоках обработки ошибок/нештатных ситуаций должны быть сообщения DEBUG_ERROR/DEBUG_WARN + +### Memory Management +- Use `u_malloc`/`u_calloc`/`u_realloc`/`u_free`/`u_strdup` from `lib/mem.h` (wrappers with leak tracking) +- Memory pools: `memory_pool_alloc()` / `memory_pool_free()` for hot-path allocations +- Queue entries: `queue_entry_new_from_pool()` / `queue_entry_free()` / `queue_dgram_free()` + +## Cryptography Guidelines + +### Key Sizes +| Constant | Value | Description | +|----------|-------|-------------| +| `SC_PRIVKEY_SIZE` | 32 | ECC private key | +| `SC_PUBKEY_SIZE` | 64 | ECC public key | +| `SC_NONCE_SIZE` | 13 | CCM nonce (exactly 13 bytes) | +| `SC_SESSION_KEY_SIZE` | 16 | AES-128 session key | +| `SC_TAG_SIZE` | 16 | CCM auth tag | +| `SC_CRC32_SIZE` | 4 | CRC32 checksum | +| `SC_PUBKEY_ENC_SALT_SIZE` | 8 | Salt for pubkey obfuscation | +| `SC_PUBKEY_ENC_SIZE` | 72 | Total pubkey+salt block sent unencrypted | ### Using Secure Channel (secure_channel.h) ```c // 1. Initialize context struct SC_MYKEYS my_keys; -// ... fill keys ... +sc_init_local_keys(&my_keys, public_key_hex, private_key_hex); +// or sc_generate_keypair(&my_keys); + sc_context_t ctx; sc_init_ctx(&ctx, &my_keys); // 2. Set peer public key (for key exchange) -sc_set_peer_public_key(&ctx, peer_public_key, 0); // 0 = binary format +sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex // 3. Ready for encrypt/decrypt sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len); sc_decrypt(&ctx, ciphertext, ciphertext_len, plaintext, &plaintext_len); + +// 4. Pubkey obfuscation (used in INIT/PING packets): +// salt(8) + XOR(SHA256(salt||peer_pubkey) || SHA256(peer_pubkey||salt), my_pubkey) +sc_obfuscate_pubkey(salt, peer_pubkey_bin, my_pubkey_bin, obfuscated_output); ``` ### Important Notes -- **Nonce size:** Must be exactly 13 bytes for CCM mode (`SC_NONCE_SIZE = 13`) -- **Session key:** 16 bytes for AES-128 (`SC_SESSION_KEY_SIZE = 16`) -- **Tag size:** 8 bytes for authentication (`SC_TAG_SIZE = 8`) -- **Error codes:** Check return values, negative = error +- **Encryption:** 3-byte header (timestamp uint16_t + flag_up uint8_t) + data_len bytes encrypted +- **INIT packets:** Header + data encrypted, pubkey+salt block (SC_PUBKEY_ENC_SIZE bytes) appended unencrypted +- **Nonce:** Must be exactly 13 bytes for CCM mode +- **Error codes:** Check return values, negative = error (SC_OK=0, SC_ERR_* < 0) + +## Debug System + +### Debug Levels (по возрастанию) +`none < error < warn < info < debug < trace` + +### Debug Categories (21 категория) +``` +NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6, +TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13, +SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20 +``` -## 🏗️ Architecture +### Настройка отладки +- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`) +- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config` +- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE` +- `log_dump(prefix, data, len)` — hex dump в лог + +### Dual Output +- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`) +- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()` + +## Architecture ### Directory Structure ``` -├── lib/ # Core libraries -├── src/ # Main source code -├── tests/ # Test programs +├── lib/ # Core libraries (13 .c + 14 .h) +├── src/ # Main source code (28 .c + 23 .h) +├── tests/ # 31+ test programs ├── doc/ # Technical Specifications +├── tools/ # Auxiliary tools +│ ├── etcpmon/ # GUI монитор ETCP +│ ├── proxy/ # UDP прокси для тестов +│ └── bping/ # BPing (bandwidth ping) ├── tinycrypt/ # TinyCrypt crypto library (external) -└── net_emulator/ # Network emulator +├── net_emulator/ # Network emulator (delays, loss, reordering) +└── c2/ # Test instance 2 (конфиг и бинарник для тестов) ``` ### File Overview **Core (src/)** - `utun.c` - Main program entry point, CLI parsing, daemon mode -- `utun_instance.c/h` - Root instance lifecycle management -- `tun_if.c/h` - Simplified TUN interface (init/write/close) +- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init +- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform) +- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations +- `tun_route.c/h` - TUN routing table sync **Network Stack (src/)** -- `etcp.c/h` - ETCP protocol implementation (TCP-like with crypto) -- `etcp_connections.c/h` - Socket and link management -- `etcp_loadbalancer.c/h` - Multi-link load balancing -- `pkt_normalizer.c/h` - Packet fragmentation/reassembly -- `routing.c/h` - Routing table management +- `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter) +- `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks) +- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG +- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper +- `etcp_debug.c/h` - ETCP packet dump/formatting +- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker) +- `packet_dump.c/h` - Packet hex dump utility +- `firewall.c/h` - Firewall rules (per-interface filtering) +- `dummynet.c/h` - Network emulator integrated into utun (for testing) + +**Routing (src/)** +- `routing.c/h` - Routing table management (local routes) +- `route_lib.c/h` - Routing library utilities +- `route_bgp.c/h` - BGP-style route exchange between peers +- `route_ping.c/h` - Route ping probing (NAT check, liveness) +- `route_node.c/h` - Route node (peer) management **Crypto (src/)** -- `secure_channel.c/h` - AES-CCM encryption with ECC key exchange +- `secure_channel.c/h` - AES-CCM encryption with ECC key exchange, pubkey obfuscation - `crc32.c/h` - CRC32 checksums +**NAT (src/)** +- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine +- `nat_transport.c/h` - NAT transport layer (packet relay) + **Config (src/)** - `config_parser.c/h` - INI-style config file parsing - `config_updater.c/h` - Config file modification utilities +**Control (src/)** +- `control_server.c/h` - Control/monitoring server (etcpmon backend API) + **Libraries (lib/)** -- `u_async.c/h` - Async event loop (epoll/poll, timers) -- `ll_queue.c/h` - Lock-free linked list queue with callbacks -- `memory_pool.c/h` - Fast object pool allocator -- `debug_config.c/h` - Debug logging system -- `timeout_heap.c/h` - Min-heap for timer management -- `sha256.c/h` - SHA256 hashing - -**Tests (tests/)** -- `test_etcp_*.c` - ETCP protocol tests -- `test_crypto.c` - TinyCrypt AES/CCM tests -- `test_ecc_encrypt.c` - ECC encryption tests -- `test_ll_queue.c` - Queue functionality tests -- `bench_*.c` - Performance benchmarks - -**External** -- `tinycrypt/` - TinyCrypt library (AES, ECC, SHA256) +- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap) +- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter +- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks) +- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config) +- `timeout_heap.c/h` - Min-heap for timer management (used by u_async) +- `sha256.c/h` - SHA256 hashing (fallback; OpenSSL preferred via USE_OPENSSL) +- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`) +- `serialize.c/h` - Binary serialization utilities +- `swm_min.c/h` - Sliding window minimum (for RTT min tracking) +- `getmyip.c/h` - Get local IP / default route detection +- `myip.c` - My IP helper (no header) +- `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows) +- `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random) + +**Memory Pools in UTUN_INSTANCE:** +- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами +- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём) +- `ack_pool` — для struct ACK_PACKET (подтверждения приёма) ### Key Components -- **UASYNC:** Asynchronous event loop for sockets and timers -- **LL_QUEUE:** Lock-free lockstep queue for packet handling -- **Memory Pool:** Fast allocation for network packets -- **ETCP:** Extended TCP protocol with crypto and reliability -- **Secure Channel:** AES-CCM encryption with ECC key exchange - -## 🐛 Debugging Tips - -### Enable Debug Output -```c -// In source files, define debug before including headers -#define DEBUG_CATEGORY_YOUR_MODULE 1 -``` - -### Common Build Issues -1. **Missing headers:** Check include paths in Makefile.am -2. **Undefined references:** Ensure source files are listed in Makefile.am -3. **Link errors:** Check function declarations match definitions - -## 📦 Dependencies - -### Build Dependencies -- autoconf, automake, libtool -- gcc with C99 support -- pthread library -- OpenSSL/crypto library (for SHA256) - -### Runtime Dependencies -- TUN/TAP interface support (Linux kernel module) -- libcrypto (usually installed by default) -- Proper network configuration for testing - -## 📝 Git Conventions - +- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap +- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter +- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments) +- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing +- **Secure Channel:** AES-CCM + ECC key exchange, nonce-based encryption, pubkey obfuscation + +## Queue Usage Rules + +### Запись в очередь +- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой. +- Используй `queue_wait_threshold` для ожидания освобождения очереди до заданного порога. + +### Чтение из очереди +- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`. +- Внутри коллбэка ОБЯЗАТЕЛЬНО: `queue_data_get(q)` → обработать элемент → `queue_resume_callback(q)` +- Без вызова `queue_resume_callback` очередь навсегда застрянет + +### Поиск +- `queue_data_put_with_index(q, entry, offset, size)` — добавляет с индексом для быстрого поиска +- `queue_find_data_by_index(q, key, key_size)` — поиск по индексу через хеш-таблицу + +## u_async Rules +- Нельзя использовать в одном потоке несколько u_async. Один поток = всегда 1 uasync instance +- Нельзя использовать sleep/usleep если есть uasync. Нужно использовать `uasync_set_timeout`. +- Таймеры: `uasync_set_timeout(ua, timeout_tb, arg, callback, name)` — timebase units (0.1ms) + Возвращает `void*` handle, отмена: `uasync_cancel_timeout(ua, handle)`. + +## Config Rules +- В серверном конфиге только собственные ключи и нет секций `[client]` +- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]` + +## Bugs Debugging Protocol + +### Действия при поиске бага: +1. Создать комит или бэкап всего что меняешь +2. Когда причина бага найдена и устранена - верни всё остальное что менял в исходное состояние +3. Проверь что тесты проходят и всё работает. `make clean` перед сборкой обязательно. +4. Если баг не устранён - продолжай поиск или если время заканчивается - верни всё в исходное состояние + +### Эффективная диагностика: +0. Сосредоточься на поиске конкретной ошибки и доведи его до конца +1. Прочитай полностью код функций с ошибкой и код всех функции которые участвуют в ошибочном алгоритме +2. Мысленно выполни предполагаемый сценарий ошибки (нельзя додумывать - нужна точность): + - Убедись что точно понимаешь как алгоритм приходит к ошибке + - Все функции которые участвуют в сценарии ошибки проанализированы + - Последовательно отсекай логически законченные блоки которые проверены и точно правильно работают + - Если функция работает логически корректно - поправь описание. Если неверно - добавь в todo.txt +3. Если исправление меняет поведение функции: + - Просмотри где используется эта функция + - Убедись что изменение поведения не повлияет на остальные места + +### Прочие правила: +- sed для редактирования исходников - запрещено +- Проверяй на дублирование кода - не сделано ли это уже в другом месте +- Не делай функций-посредников: лучше сразу вызывать target функцию без вложенных вызовов +- Нельзя ничего восстанавливать из репозитория не спрашивая +- Для отладки не printf а DEBUG_* +- Перед сборкой всегда make clean +- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства + +## Key Documentation Files +- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive) +- `/doc/etcp_arch.md` - ETCP архитектура +- `/doc/etcp_config.txt` - Конфигурация ETCP +- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения + +## Runtime +- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh` +- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh` +- Логи: `utun.log` (stdout), `utun_err.log` (stderr) +- Тестовые логи: `tests/logs/` + +## Git Conventions - **Commit Messages:** Use imperative mood, concise (50-72 chars) - **Language:** Mix of English (technical) and Russian (business logic) -- **Branching:** Use feature branches, merge to master via pull request - **Tags:** Version tags follow vX.Y.Z format +- "cp" or "кп" in prompt = do commit and push (всех изменений на текущий момент, не откатывая) -Example commits: -``` -Fix: Added uasync_t typedef for compilation -Crypto: Fixed CCM nonce size to 13 bytes, all crypto tests passing -``` - -## 🚀 Quick Start for New Features - +## Quick Start for New Features 1. Add new source file to `src/Makefile.am` under `utun_SOURCES` 2. Add test file to `tests/Makefile.am` under `check_PROGRAMS` 3. Use existing patterns from similar modules -4. Run `make check` after changes (win: build.bat and run_tests.bat) +4. Run `make check` after changes 5. Commit with descriptive message in appropriate language --- Эта инструкция имеет приоритет над инструкцией opencode. - -"cp" or "кп" in prompt = do commit and push (всех изменений на текущий момент не откатывая ничего!) - -Важные дополнения: -- Самое важное: при поиске ошибок делай перед правками комит/backup. Всё лишнее что менял при отладке - строго вернуть назад в состояние до моего вмешательства. В итоге в код должно попасть только проверенное исправление и ничего лилшнего! -- если в процессе исправлений-доработок возникла нестыковка которая требует сеньезных доработок, остановить и подробно расскажи об этом. -- sed для редактирования исходников - запрещено пользоваться! -- Проверяй на дублирование кода - не сделано ли это уже в другом месте. -- Не делай функций-посредников: лучше сразу вызывать target функцию без вложенных вызовов. -- Старайся чтобы функция сделала логически завершенную операцию полностью - это упрощает код и понимание. -- нельзя ничего восстанавливать из репозитория не прашивая -- Когда пишешь код всегда загружай в контекст и мысленно разберись как работают все функции/струтуры, назначение переменных которые используешь. -- Для отладки не printf а DEBUG_* -- перед сборкой всегда make clean -- прежде чем вносить правки хорошо разберись как должно работать, просмотри все нужные функции полностью, нужно целостное понимание. -- при написании кода мысленно анализируй логику работы, думай как сделать проще и удобнее. Проверяй дубликаты и что уже есть, продумай логику до тех пор пока не будет полного понимания -- во всех блоках обработки ошибок/нештатных ситуаций должны быть сообщения DEBUG_ERROR/DEBUG_WARN -- тесты: запускаё все сразу (make check) - ошибок быть не должно. если что не такт смотри логи - каждый тест пишет лог. - -Действия при поиске бага: -1. создать комит или бэкап всего что меняешь -2. когда причина бага найдена и устранена - верни всё остальное что менял в исходное состояние -3. проверь что тесты проходят и всё работает. make clean перед сбркой обязательно. -4. если баг не устранён - продолжай поиск или если время заканчивается - верни всё в исходное состояние - -Как более точно эффективно проводить диагностику ошибки: -0. Сосредоточься на поиске конкретной ошибки и доведи его до конца руководствуясь этой инструкцией. -1. прочитай полностью код функций с ошибкой и код всех функции которые учавствуют в ошибочном алгоритме. -2. еще раз мысленно выполни предполагаемый сценарий ошибки (если чего-то не хватает до полной картины - обязательно дочитывай все ветви функций по ходу: нельзя додумывать - нужна точность) и убедись что: - - ты точно понимаешь как алгоритм прихдит к ошибке - - все функции которые учавствуют в сценарии ошибки проанализированы и их поведение проверено и понятно - - последовательно отсекай логически законченные блоки которые проверены и точно правильно работают - - если ты полностью проанализировал функцию и к ней нет описания или описание неточное (неполное), и функция работает логически корректно - поправь описание. Если функция работает логически неверно - добавь запись об этом в todo.txt -3. Если исправление как либо меняет поведение функции: - - просмотри где используется эта функция - - убедись что изменение поведения не повлияет на остальные места где функция используется. особенно важный пункт для библиотек и модулей коммуникаций - -Работа с очередями. -- Запись в очередь: очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой. Пользуйся наблюдателем queue_wait_threshold, он специально сделан для добавления в очередь. -- Чтение из очереди: пользуйся queue_set_callback: при вызове callback , обработай один или несколько элементов, потом вызови resume_callback. - -Работа с u_async -- нельзя использовать в одном потоке несколько u_async. один поток = всегда 1 uasync instance -- нельзя использовать sleep/usleep если есть uasync. Нужно использовать uasync_set_timeout. - -Конфиги: -- в серверном только собственные ключи и нет секций [client] -- в клиентском есть собственные ключи и pubkey каждого сервера в секции [client] - -тех задания для реализации в каталоге /doc. -/doc/etcp_protocol.txt - основной протокол (похож на TCP+QUIC, поддеиживает шифрования, load balancing multi-link, работу а неустойчивых каналах, утилизацию полосы и недопущение перегрузки каналов связи) - - реализация в /src/etcp*.c/h - -запуск utun от root (для tun) - /home/vnc1/proj/utun3/utun_start.sh -стоп utun: sudo /home/vnc1/proj/utun3/utun_stop1.sh -логи - utun.log - - -*Last updated: 2026-02-13 - c_util fully functional with toc/description/show/edit commands* \ No newline at end of file diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index 6464ce4e..c5a53e40 100644 --- a/doc/etcp_protocol.txt +++ b/doc/etcp_protocol.txt @@ -7,120 +7,183 @@ - либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия". из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку. как формируется пакет: - 1. берем пакет из списка неподтвержденных пакетов ожидающих отправку + 1. выбираем канал передачи (etcp_loadbalancer_select_link) 2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем - если пакет найден: - 1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало - 2. в конец добавляем секцию 0x00 с данными пакета + 3. если пакет найден или есть неподтверждённые ACK: + 1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало + 2. в конец добавляем секцию 0x00 с данными пакета при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен Функция прикрепления опциональных секций: - запрашивает выбор канала передачи. - добавляет накопившиеся ACK При приёме пакета (от etcp_connections): - последовательно сканируем секции и отдаём их на обработку нужным обработчикам: - - ack: помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также и ставим флаг в конце запустить etcp если он в wait timeout, т.е. wait_timeout!=NULL - - timestamp: обновляем last RTT, пересчитываем RTT10,RTT100 (плавающим окном за последние 10/100 пакетов), и jitter=max(last 10)-min(last 10 packets) - - [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). + - ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки + - timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма + - [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки. после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть. пересканирование inflight: - - вызывается по таймеру, таймер заводится при добавлении пакета в пустой inflight. - - если время последней отправки > RTT10*k1+jitter*k2 (коэффициенты вынести в настройки utun instance) то пакет заново отправляется - и заводим таймер отправки следующей ретрансмиссии (список отсортирован по времени) - - если встречаем успешный ack, запоминаем это и его timestamp (назовем lp_ts). - - если после этого далее в очереди находим пакет который не имеет переповторов и timestamp отправки раньше чем [ИСПРАВЛЕНО: фраза обрезана в оригинале; предположительно, "раньше чем lp_ts" на основе контекста; если нет, пометить как неполное] - -inflight - два списка (ll_queue): - - список с неподтвержденными пакетами ожидающими ack (при запросе ретрансмиссии пакет перемещается в ожидающие отправку) - - список с неподтвержденными пакетами ожидающими отправку (при отправке возвращается в ожидающие ack) - доп. свойства пакетов: - - указатель на ETC_LINK последней отправки - - timestamp последней отправки - - число отправок - - число запросов переповтора + - запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет. + - если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется + - очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете + - по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди + +inflight - две очереди (ll_queue): + - input_send_q: список пакетов ожидающих отправку + - input_wait_ack: список неподтвержденных пакетов ожидающих ack + При отправке пакет перемещается из input_send_q в input_wait_ack. + При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q. + доп. свойства пакетов (struct INFLIGHT_PACKET): + - указатель на ETC_LINK последней отправки (last_link) + - timestamp последней отправки (last_timestamp) + - число отправок (send_count) + - send_hist[8] - история через какие линки отправлялся (индекс линка) + - inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight. optimal_inflight расчитывается из RTT и bandwidth линка. -bandwidth по каждому линку адаптивно подстраиваем следующим алгоритмом: -периодически инициируем burst_transmission (напоминаем ID пакета на которой она началась). -после передаём пачку пакетов только по этому линку со следующими таймингами: - - т.е. чуть задерживаем канал а потом выплёвываем пачку пакетов. -и добавляем опцию [meas_ts][n] - где n - номер пакета в пачке -приёмная сторона видя эту секцию должна отправить секцию [meas_resp] по всем пакетам, которые содержат локальный RECV_Timestamp фиксирующий время приёма каждого пакета с [meas_ts]. -приняв эти response анализируем по разнице между pkt1-pkt2 не забита ли очередь отправки, а по дельте pkt2-3-4-5 оцениваем bandwidth. -дальше по алгоритму (который потом оттюним) корректируем transmitter speed limit. +bandwidth по каждому линку адаптивно подстраиваем: +Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты). +BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec). +Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT. +Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы. **** Формат кодограмм для etcp.c/h **** Каждая кодограмма состоит из обязательного заголовка и опциональных секций: -1. Обязательный заголовок всего пакета (2 байта) добавляется при передаче, есть во всех пакетах: - [Timestamp high][Timestamp low][flags] - - Timestamp: время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется - - flags: bit0 = up/down (recv_keepalive) - - шифруется строго ВСЁ включая ВСЕ заголовки (кроме INIT+publickey). timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ -2. Опциональные секции (одна или несколько) добавляются в etcp.c: +1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах: + [Timestamp high][Timestamp low][flags/flag_up] + - Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется + - flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны + + шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах). + timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке). + Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных. +2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка): а) Подтверждения (ACK) - заголовок 0x01: - [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 (для синхронизации прогресса) - в) channel timestamp (добавляется после выбора канала передачи): - [0x0F] [RET_Timestamp high][RET_Timestamp low] [RECV_Timestamp high][RECV_Timestamp low] - - RET_Timestamp: это timestamp последнего принятого пакета по этому каналу ETCP_LINK плюс разница времени между принятием этого пакета и текущим временем. т.е. приёмная сторона по этому timestamp (и зная своё время) может просчитать время в пути туда + обратно - - RECV_Timestamp: это timestamp времени приёма последнего принятого пакета (т.е. отправляем время "когда мы получили этот пакет по локальном урмени") - по нему другая сторона сможет просчитать относительное время задержки в одну сторону (время туда без обратно). - если последний пакет по этому каналу принят сильно давно (прошло более 30000 единиц времени) то channel timestamp не добавляем. [СПОРНО: порог 30000 (3 сек) arbitrary; может потребовать настройки или обоснования; также риск clock skew между нодами] + [0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count] + - count: количество ACK записей (1 байт; до 32 ограничено размером пакета) + - last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд) + - rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне) + - seq: uint32_t — номер подтверждаемого пакета + - recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс) + - dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT) + Каждая запись = 8 байт. + в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные): + [0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)] + - RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time) + (оценка текущего времени на передающей стороне в момент отправки) + - RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp + (относительное смещение между локальным и удалённым временем) + Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки) + и если прошло не более ~100ms (dt < 1000000 таймбаз). 3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя): - [0x00] [ID 4 байта] [данные...] - Данные пакета - - ID: 16-битный циклический номер пакета. [СПОРНО: 16-bit может привести к быстрому overflow в long-lived сессиях; рассмотреть 32-bit] -Пакет может содержать несколько опциональных секций (например, ACK + данные). [СПОРНО: нет явных length полей для секций; parsing implicit, риск ошибок при variable длине; добавить length?] + [0x00] [ID 4 байта LE] [данные...] + - ID: uint32_t, 32-битный циклический номер пакета +Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные), +всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний. +Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции). + цель протокола: 1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT) + Установка подключения для канала (реализуется в etcp_connections.c/h): каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи). При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно): - RTT последнего пакета и время отправки последнего пакета -- пакеты делятся на 3 категории: <300 байт, 300-699 байт, 700+ байт. для каждой категории отдельно считаются метрики: -- количество отправленных в пакетах байтах -- количество перезапросов (по каждому каналу отдельно) в пакетах и байтах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики. +- общее количество отправленных/принятых байт +- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики. - обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент Планировщик/балансир каналов (etcp_loadbalancer.c/h): для каждого канала ограничение полосы с тестированием отклика: - полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков) Ограничение реализуем следующим образом: - - в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности (т.е. было ненулевое время передачи одного байта). + - в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности. Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время) - счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); [СПОРНО: сложность с float; риск precision issues] + счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения. также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client. -процесс установки соединения: -- отправляем init запрос и выставляем таймаут (сохраняем его в struct ETCP_LINK) -- по таймауту повторяем init запросы, ведем счетчик запросов init. -- когда получаем init подтверждение (отправляется в etcp_connections_read_callback) снимаем таймаут и выставляем флаг struct ETCP_LINK initialized=1. -При добавлении нового канала посылаем INIT 0x04 (без сброса), а при первичной инициализации подключения - 0x02 (это нужно, т.к. например перезапуск локального софта должен инициировать перезапуск удаленного соединения). +процесс установки соединения (рукопожатие): +- клиент отправляет init запрос и выставляет таймаут +- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init. +- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive. +- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03). +При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04). +link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected. **** Формат кодограмм для etcp_connections.c/h **** -Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда только одна секция). в обязательном заголовке ID не используется, при передаче для порядка =0: [СПОРНО: заголовок в etcp — только 2 байта TS; здесь подразумевается ID? Уточнить единый формат] - 1) Init запрос - заголовок 0x02 (со сбросом etcp сессии) или 0x04 (без сброса): - [0x02/0x04] [my_node_id 64bit] [my mtu high] [my mtu low] [keepalive high] [keepalive low] [recovery high] [recovery low] [my link_id 1 байт] [my public key (64 байта, не шифруется)] - - Инициирует новый connection для tcp instance. если tcp instance нет (первое подключение) - создаёт. Между нодами только одно подключение, но можно добавлять каналы. - - link_id: локальный идентификатор канала (0-255), назначается отправителем для идентификации канала - - Публичный ключ отправляется в конце пакета без шифрования, чтобы получатель мог установить его и расшифровать остальную часть пакета (т.к. инициатор соединения всегда имеет оригинальный peer public key в конфиге - по нему исключаем MITM) - 2) Init подтверждение - заголовок 0x03/0x05 (ответы соответственно на коды 0x02 и 0x04): - [0x03/0x05] [my_node_id 64bit] [my mtu high] [my mtu low] [my link_id 1 байт] [peer ipv4 4 байта] [peer port 2 байта] - - Подтверждение инициализации (канал успешно создан, можно начинать обмен) - - link_id: локальный идентификатор канала (0-255) отправителя ответа - - peer ip.port: возвращаем ip:port с которого пришел init запрос (чтобы клиент узнал свой external ip:port если за nat) +Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется. + +Типы пакетов: + 0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии) + 0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом) + 0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса) + 0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса) + 0x06 — ETCP_PING (проверка доступности) + 0x07 — ETCP_PONG (ответ на пинг) + + 1) Init запрос (0x02 / 0x04): + [code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)] + [recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)] + [random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] + - code: 0x02 (со сбросом) или 0x04 (без сброса) + - node_id: 64 бита, идентификатор узла (big-endian) + - session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian) + - mtu_local: uint16_t локальный MTU + - keepalive_interval: uint16_t интервал keepalive в мс + - recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100) + - local_link_id: uint8_t (0-255), назначается отправителем + - sock_id: uint8_t, уникальный ID сокета отправителя + - only_local: uint8_t, флаг "только локальные подключения" + - type: uint8_t, тип сокета (CFG_SERVER_TYPE_PUBLIC/NAT/PRIVATE) + - random_padding: случайные байты до размера handshake (защита от fingerprinting) + - salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES, + но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета + в noencrypt_len байтах (не попадает под AES-шифрование) + Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт. + Между нодами только одно подключение, но можно добавлять каналы. + + 2) Init подтверждение (0x03 / 0x05): + [code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)] + [remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)] + [random_padding...] + - code: 0x03 (со сбросом) или 0x05 (без сброса) + - node_id: 64 бита, big-endian + - session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует) + - mtu_local: uint16_t локальный MTU сервера + - local_link_id: uint8_t идентификатор канала сервера + - remote_socket_id: uint8_t (копируется из запроса) + - only_local: uint8_t флаг сервера + - type: uint8_t тип сокета сервера + - peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal) + - peer_port: 2 байта (порт клиента как его видит сервер) + - random_padding: случайные байты + + 3) PING (0x06): + [0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] + [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] + Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией). + Используется для проверки доступности узла и NAT-тестирования. + + 4) PONG (0x07): + [0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] + [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] + Ответ на PING. nonce копируется из запроса для сопоставления. + При получении init получатель пакета должен: - reset ETCP_LINK с этим ip_port если он есть -- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - вызвать etcp_reset (функция сброса окон неподтвержденных данных и нумерации) +- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение. приём кодограмм из UDP выглядит так: -uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue +uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue -Keepalive: пакет без секций (только timestamp+flags) +Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0). keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика. +Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента). +flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты). Клиент: если все линки =down то начинается процедура восстановления связи: -- клиент посылает init без сброса -- сервер как получает init без сброса отправляет init подтверждение: со сбросом или без сброса в зависимости от состояния init его соединения (инициализировано - то без сброса). - +- клиент посылает init без сброса (0x04), link_state=2 +- сервер как получает init проверяет session_id: + - если session_id совпадает — шлёт подтверждение без сброса (0x05) + - если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03) diff --git a/doc/route_p2pconn.txt b/doc/route_p2pconn.txt new file mode 100644 index 00000000..90c801b0 --- /dev/null +++ b/doc/route_p2pconn.txt @@ -0,0 +1,344 @@ +Архитектура P2P Direct Connection (установка оптимального подключения между узлами) + +Цель: в большой сети узлов каждый узел умеет находить быстрые подключения +к любому другому узлу — либо прямое (direct), либо через минимальное число +промежуточных узлов с минимальным суммарным RTT. Оба узла пробуют доступные +варианты пингом и выбирают лучший. + + +=== Что уже есть в коде (используем, не дублируем) === + +1. NODEINFO уже содержит поле tranzit_nodes + структуру NODEINFO_TRANZIT_NODE + (node_id, rtt, link_q) — "лучшие транзитные узлы, выбирается/обновляется узлом", + но пока не заполняется и не используется. + +2. Пинг-инфраструктура: route_ping_send_req_addr (запрос удалённого пинга через + третий узел), route_ping_handle_resp (приём результата). Также есть + etcp_send_ping_to_socket — прямой пинг с конкретного сокета на конкретный адрес + с pubkey для шифрования. + +3. NAT-детекция уже работает: проверка типа NAT через третий узел, обмен NAT_INFO. + +4. Path system: NODEINFO_PATH с hop_count, routes → ROUTE_ENTRY → NODEINFO_Q → paths — + уже выбирается путь для маршрутизации пакетов. + +5. Константа ROUTE_SUBCMD_CONN_REQ (0x0B) зарезервирована в route_bgp.h, но не реализована. + +6. NODEINFO_Q уже имеет best_socket, last_ping_time, last_rtt. + + +=== Общий алгоритм (на примере узлов A и B) === + +A ──(ETCP через релейные узлы)──> B [текущий путь, hop_count > 1] + +1. A замечает большой объём трафика к B → запускает CONN_REQ +2. A шлёт B через существующий ETCP-путь: свои адреса-кандидаты + лучших транзитных соседей +3. B получает CONN_REQ, сразу начинает пинговать адреса A, собирает свои данные +4. B шлёт CONN_RESP: свои адреса + транзитных соседей + результаты пробных пингов +5. A получает CONN_RESP, пингует все адреса B +6. Оба вычисляют лучший вариант (прямой или релейный с минимальным RTT) +7. Оба шлют друг другу CONN_RESULT с выбором +8. Оба пытаются создать ETCP link к выбранному адресу (если прямой) +9. Лишние/худшие линки закрываются позже — все линки в рамках одного ETCP_CONN + + +=== Фаза 1: Обмен кандидатами (CONN_REQ → CONN_RESP) === + +Узел-инициатор A собирает и отправляет: + - Свои адреса-кандидаты: из своих ETCP_SOCKET (interface_addr + nat_addr). + Включаются только сокеты с NAT_VERIFIED_* / EIM / PUBLIC (type >= NAT_VERIFIED_UNKNOWN). + Для каждого: ip, port, type, socket_id. + - Лучших транзитных соседей: top-N directly-connected узлов (hop_count == 1) + отсортированных по NODEINFO_Q.last_rtt (лучший RTT первым). + +Узел B при получении CONN_REQ: + - Сохраняет кандидатов A + - Сразу запускает пробные пинги ко всем адресам A (etcp_send_ping_to_socket с pubkey) + - Собирает свои адреса-кандидаты + - Собирает своих транзитных соседей + - Шлёт CONN_RESP со своими данными + уже готовыми результатами проб + + +=== Фаза 2: Пробы (CONN_RESP → Пинги) === + +Узел A при получении CONN_RESP: + - Запускает пинги ко всем адресам B: + каждый адрес B пингуется с каждого своего сокета (N×M проб) + - Использует etcp_send_ping_to_socket с pubkey узла B (из NODEINFO) + - Пинг идёт напрямую по UDP (не через ETCP) — проверяет реальную достижимость + включая NAT traversal + - Ждёт все результаты либо таймаут + +Параллельно B тоже завершает свои пробы (запущенные на шаге 3). + + +=== Фаза 3: Выбор лучшего пути (→ CONN_RESULT) === + +Каждая сторона независимо вычисляет: + + best = {mode: none, rtt: 65535} + + Прямые варианты: + for each (мой_сокет, peer_addr) где probe.ok: + if probe.rtt < best.rtt: + best = {direct, my_sock_idx, peer_addr_idx, probe.rtt} + + Релейные варианты: + for each общий_транзитный_узел R (доступен и A и B): + // RTT(A↔R) берём из своих замеров (A знает RTT до своих соседей) + // RTT(R↔B) берём из транзитных соседей B (присланы в CONN_RESP) + total_rtt = A→R_rtt + R→B_rtt + if total_rtt < best.rtt: + best = {relay, R, total_rtt} + +Обе стороны приходят к одинаковому выводу (информация симметрична). + +Шлют CONN_RESULT с выбором. +Если прямой вариант — оба пытаются создать ETCP link к адресу пира. +Если релейный — используют существующий путь (уже работает). + + +=== Триггер запуска negotiation === + +В route_pkt (routing.c) при отправке пакета узлу с hop_count > 1: + - Накапливаем счётчик байт к этому узлу (per-node counter в bgp) + - При превышении порога (например 64KB) и если negotiation ещё не запущен — стартуем + - Cooldown: не чаще чем раз в 30 секунд для одной пары узлов + - Если negotiation уже в процессе для этой пары — не дублируем + + +=== Tranzit nodes — автоматическое заполнение === + +В route_bgp_update_my_nodeinfo (route_node.c) при каждом обновлении NODEINFO: + - Сканируем directly-connected соседей (из bgp->nodes, где hop_count == 1) + - Сортируем по NODEINFO_Q.last_rtt (лучший RTT первым) + - Берём top-N (до 4) и упаковываем в NODEINFO.tranzit_nodes как массив + NODEINFO_TRANZIT_NODE (node_id, rtt, link_q) + +Плюс периодический refill (раз в ~10 сек) для свежести RTT замеров. + + +=== Структуры данных (новые, route_p2pconn.h) === + +#define P2P_MAX_ADDRS 8 +#define P2P_MAX_RELAYS 8 +#define P2P_MAX_PROBES (P2P_MAX_ADDRS * P2P_MAX_ADDRS) // до 64 + +#define P2P_PHASE_WAIT_RESP 0 // ждём CONN_RESP от пира +#define P2P_PHASE_PROBING 1 // пингуем адреса пира +#define P2P_PHASE_SELECTING 2 // выбор лучшего, отправка CONN_RESULT +#define P2P_PHASE_DONE 3 // завершено + +// Один адрес-кандидат +struct P2P_ADDR { + uint32_t ip; // network byte order + uint16_t port; + uint8_t type; // NAT_VERIFIED_* + uint8_t socket_id; +}; + +// Один транзитный узел +struct P2P_RELAY { + uint64_t node_id; + uint16_t rtt; // x0.1ms + uint16_t link_q; +}; + +// Результат одного пробного пинга +struct P2P_PROBE_RESULT { + uint8_t my_sock_idx; // индекс в my_addrs[] + uint8_t peer_addr_idx; // индекс в peer_addrs[] + uint16_t rtt; // x0.1ms, 0 = fail +}; + +// Состояние одних переговоров (хранится в хеш-таблице bgp->p2p_negotiations) +struct P2P_NEGOTIATION { + struct ll_entry ll; + uint32_t request_id; + uint64_t peer_node_id; + uint8_t phase; + + // Мои данные + struct P2P_ADDR my_addrs[P2P_MAX_ADDRS]; + uint8_t my_addr_count; + struct P2P_RELAY my_relays[P2P_MAX_RELAYS]; + uint8_t my_relay_count; + + // Данные пира (заполняются из CONN_RESP) + struct P2P_ADDR peer_addrs[P2P_MAX_ADDRS]; + uint8_t peer_addr_count; + struct P2P_RELAY peer_relays[P2P_MAX_RELAYS]; + uint8_t peer_relay_count; + + // Результаты проб + struct P2P_PROBE_RESULT probes[P2P_MAX_PROBES]; + uint8_t probe_total; // сколько всего запланировано + uint8_t probe_done; // сколько завершилось (ok + fail) + + // Лучший выбор + uint8_t best_mode; // 1=direct, 2=relay + uint64_t best_relay; // node_id релея (если mode=relay) + uint16_t best_rtt; // x0.1ms + uint8_t best_my_sock_idx; // индекс в my_addrs (если direct) + uint8_t best_peer_addr_idx;// индекс в peer_addrs (если direct) + + void* timeout_timer; // общий таймаут на всю negotiation +}; + +// Счётчик трафика per-node (для триггера, хранится в bgp) +struct P2P_TRAFFIC_COUNTER { + uint64_t node_id; + uint64_t bytes_sent; // накоплено байт + uint64_t last_negotiation_time; // время последней попытки (0 = не было) +}; + + +=== Протокольные пакеты (добавляются в route_bgp.h) === + +ROUTE_SUBCMD_CONN_REQ 0x0B // запрос прямого подключения (уже зарезервирован) +ROUTE_SUBCMD_CONN_RESP 0x0C // ответ с адресами + результаты проб +ROUTE_SUBCMD_CONN_RESULT 0x0E // финальный выбор + +// Пакет CONN_REQ (A → B) +struct BGP_CONN_REQ { + uint8_t cmd; // ETCP_ID_ROUTE_ENTRY + uint8_t subcmd; // ROUTE_SUBCMD_CONN_REQ + uint32_t request_id; // для корреляции + uint8_t addr_count; // число адресов-кандидатов + uint8_t relay_count; // число транзитных узлов + uint8_t reserved[2]; + // далее динамически (размер = addr_count*8 + relay_count*12): + // [addr_count × {ip[4] port[2] type[1] socket_id[1]}] + // [relay_count × {node_id[8] rtt[2] link_q[2]}] +}; + +// Пакет CONN_RESP (B → A) +struct BGP_CONN_RESP { + uint8_t cmd; // ETCP_ID_ROUTE_ENTRY + uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESP + uint32_t request_id; + uint8_t addr_count; // адреса B + uint8_t relay_count; // транзитные узлы B + uint8_t probe_count; // готовые результаты проб B→A + uint8_t reserved; + // [addr_count × {ip[4] port[2] type[1] socket_id[1]}] + // [relay_count × {node_id[8] rtt[2] link_q[2]}] + // [probe_count × {peer_addr_idx[1] my_sock_idx[1] rtt[2] ok[1]}] +}; + +// Пакет CONN_RESULT (A ↔ B, финальный) +struct BGP_CONN_RESULT { + uint8_t cmd; + uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESULT + uint32_t request_id; + uint8_t chosen_mode; // 1=direct, 2=relay + uint8_t reserved; + uint16_t chosen_rtt; // x0.1ms + // Если direct: + uint32_t peer_ip; // IP пира к которому подключаемся + uint16_t peer_port; + uint8_t my_socket_id; // свой сокет для подключения + uint8_t peer_socket_id; // сокет пира + // Если relay (оверлей тех же байт): + // uint64_t relay_node_id; +}; + + +=== API модуля route_p2pconn === + +// Запуск negotiation к узлу peer_node_id. +// Вызывается из route_pkt при превышении порога трафика. +int p2p_start_negotiation(struct ROUTE_BGP* bgp, uint64_t peer_node_id); + +// Проверка: запущена ли уже negotiation для этой пары +int p2p_is_negotiating(struct ROUTE_BGP* bgp, uint64_t peer_node_id); + +// Сбор своих адресов-кандидатов (из etcp_sockets) +int p2p_collect_my_addrs(struct ROUTE_BGP* bgp, struct P2P_ADDR* out, uint8_t max); + +// Сбор лучших транзитных соседей (из directly-connected nodes) +int p2p_collect_my_relays(struct ROUTE_BGP* bgp, struct P2P_RELAY* out, uint8_t max); + +// Обработчики входящих пакетов (вызываются из route_bgp_receive_cbk): +void p2p_handle_conn_req(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, + const uint8_t* data, size_t len); +void p2p_handle_conn_resp(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, + const uint8_t* data, size_t len); +void p2p_handle_conn_result(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, + const uint8_t* data, size_t len); + +// Отмена negotiation при удалении conn +void p2p_cancel_for_conn(struct ROUTE_BGP* bgp, struct ETCP_CONN* conn); + +// Очистка всех negotiation (при destroy bgp) +void p2p_destroy_all(struct ROUTE_BGP* bgp); + + +=== Схема состояний negotiation === + + IDLE ──(трафик > порог)──→ WAIT_RESP ──(CONN_RESP получен)──→ PROBING + ↑ ↑ │ + │ (таймаут) (все пробы готовы) + │ ↓ │ + └──────────────────────────── DONE ←──(CONN_RESULT отправлен)── SELECTING + + +=== Интеграция (какие файлы меняются) === + +Новые файлы: + src/route_p2pconn.h — структуры, константы, прототипы + src/route_p2pconn.c — вся логика negotiation + +Изменения в существующих: + src/route_bgp.h — добавить ROUTE_SUBCMD_CONN_RESP 0x0C, + ROUTE_SUBCMD_CONN_RESULT 0x0E, + структуры пакетов BGP_CONN_REQ/RESP/RESULT + src/route_bgp.c — в route_bgp_receive_cbk добавить обработку + новых subcmd. В struct ROUTE_BGP добавить + поле p2p_negotiations (очередь/хеш negotiation) + и p2p_traffic_counters (per-node counters) + src/route_node.c — в route_bgp_update_my_nodeinfo заполнять + tranzit_nodes из RTT directly-connected соседей + src/routing.c — в route_pkt добавить накопление счётчика трафика + и вызов p2p_start_negotiation при превышении порога + src/Makefile.am — добавить route_p2pconn.c в сборку + + +=== Внутренняя логика route_p2pconn.c === + +p2p_start_negotiation(bgp, peer_node_id): + 1. Проверить что нет активной negotiation для этой пары + 2. Проверить cooldown (30 сек) + 3. Создать struct P2P_NEGOTIATION, заполнить my_addrs, my_relays + 4. Сформировать BGP_CONN_REQ и отправить через существующий путь к пиру + 5. Поставить таймаут на всю negotiation (например 5 сек) + +p2p_handle_conn_req(bgp, from_conn, data, len): + 1. Распарсить BGP_CONN_REQ + 2. Сохранить адреса и релеи инициатора в P2P_NEGOTIATION + 3. Собрать свои my_addrs, my_relays + 4. Запустить пробные пинги к адресам инициатора (p2p_start_probes) + 5. Когда пробы готовы — отправить CONN_RESP с результатами + +p2p_start_probes(neg): + 1. Для каждой пары (мой_сокет, peer_addr) запланировать пинг + 2. Вызвать etcp_send_ping_to_socket с pubkey пира + 3. В коллбэке сохранить результат в probes[], инкрементировать probe_done + 4. Когда probe_done == probe_total → вызвать p2p_select_best + +p2p_select_best(neg): + 1. Прямые: найти пару (my_sock, peer_addr) с минимальным rtt > 0 + 2. Релейные: для каждого общего транзитного узла посчитать суммарный rtt, + найти минимум + 3. Сравнить прямой vs релейный лучший rtt + 4. Записать выбор в neg->best_* + 5. Отправить CONN_RESULT пиру + 6. Вызвать p2p_establish_link(neg) + +p2p_establish_link(neg): + 1. Если best_mode == direct: + - Найти/создать ETCP_CONN к пиру (по node_id) + - Вызвать etcp_link_new с chosen address + 2. Если best_mode == relay: + - Ничего не делаем, существующий путь уже работает + 3. Пометить negotiation как DONE