Browse Source

update documentation

congestion
Evgeny 5 months ago
parent
commit
f6ef38aa5d
  1. 408
      AGENTS.md
  2. 213
      doc/etcp_protocol.txt
  3. 344
      doc/route_p2pconn.txt

408
AGENTS.md

@ -3,58 +3,80 @@
Ты - профессиональный программист высокого уровня. Ты - профессиональный программист высокого уровня.
Ты любишь до конца логически правильный и простой код, продуманный до каждой мелочи. Ты любишь до конца логически правильный и простой код, продуманный до каждой мелочи.
Если хоть какая-то мелочь не стыкуется - подумай как это можно решить, сообщи об этом со всеми подробностями и остановись. Если хоть какая-то мелочь не стыкуется - подумай как это можно решить, сообщи об этом со всеми подробностями и остановись.
Если что-то получается нелогично или громоздко - хорошо подкмай как сделать просто и компактно. предложи варианты и остановись. Если что-то получается нелогично или громоздко - хорошо подумай как сделать просто и компактно. предложи варианты и остановись.
Имей, загружай когда надо полный код нужных тебе функций/структур. Фантазировать и додумывать нельзя, надо чтобы каждый нюанс кода был архитектурно понятный, логичный и корректный. Имей, загружай когда надо полный код нужных тебе функций/структур. Фантазировать и додумывать нельзя, надо чтобы каждый нюанс кода был архитектурно понятный, логичный и корректный.
Надо детально разобратсья в нужных для задачи механизмах, в поставленной задаче и как сейчас всё работает. Надо детально разобраться в нужных для задачи механизмах, в поставленной задаче и как сейчас всё работает.
Старайся одно логичеси завершенное действие размещать на одной строке, если строка не слишком длинная (до 150 символов) Старайся одно логически завершенное действие размещать на одной строке, если строка не слишком длинная (до 150 символов)
This file contains essential information for AI coding agents working in the uTun codebase. 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 **Repository:** uTun - Secure VPN tunnel with ETCP protocol
**Language:** C (C99) **Language:** C (C99)
**Build System:** GNU Autotools (autoconf/automake) **Build System:** GNU Autotools (autoconf/automake)
**Cryptography:** TinyCrypt (AES-CCM, ECC) **Cryptography:** TinyCrypt + OpenSSL (AES-CCM, ECC, SHA256)
## 🔧 Build Commands ## Build Commands
### Full Build ### Full Build (Linux)
win: powershell build_full.bat (в точности как написано без дополнительных опций powershell, текущий каталог не важен)
```bash ```bash
./configure # Configure build ./build.sh --full -j4 # autoreconf + configure + make
make # Build everything
make install # Install (requires sudo)
``` ```
### Partial Builds ### Full Build (Windows/MSYS2)
win: powershell -Command ".\build.bat" 2>&1 (аналог make если не изменен makefile.am) ```powershell
логи сборки win: build_win.log powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
```
### Incremental Build
```bash ```bash
cd lib && make # Build only the library ./build.sh -j4 # make с авто-конфигурацией если надо
cd src && make # Build only the main program ```
cd tests && make # Build only tests ```powershell
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
``` ```
### Clean Build ### Clean Build
```bash ```bash
make clean # Clean object files make clean # Clean object files
make distclean # Clean everything including configure 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 ### Run All Tests
win: powershell check.bat
```bash ```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 ### Run Specific Test
```bash ```bash
cd tests/ && ./test_etcp_crypto # ETCP crypto test cd tests/
cd tests/ && ./test_etcp_simple # Simple ETCP test ./test_etcp_crypto
cd tests/ && ./test_ecc_encrypt # ECC encryption test ./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 ### Run Single Test with Debug Info
@ -65,7 +87,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
./my_test ./my_test
``` ```
## 💻 Code Style Guidelines ## Code Style Guidelines
### Naming Conventions ### Naming Conventions
- **Functions:** `snake_case` - `etcp_connection_create()`, `sc_encrypt()` - **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 - **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 ### Include Order
```c ```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_ERROR(DEBUG_CATEGORY_ETCP, "Failed to initialize: %s", err);
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port); DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
``` ```
- Во всех блоках обработки ошибок/нештатных ситуаций должны быть сообщения DEBUG_ERROR/DEBUG_WARN
## 🔐 Cryptography Guidelines
### 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) ### Using Secure Channel (secure_channel.h)
```c ```c
// 1. Initialize context // 1. Initialize context
struct SC_MYKEYS my_keys; 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_context_t ctx;
sc_init_ctx(&ctx, &my_keys); sc_init_ctx(&ctx, &my_keys);
// 2. Set peer public key (for key exchange) // 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 // 3. Ready for encrypt/decrypt
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len); sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
sc_decrypt(&ctx, ciphertext, ciphertext_len, plaintext, &plaintext_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 ### Important Notes
- **Nonce size:** Must be exactly 13 bytes for CCM mode (`SC_NONCE_SIZE = 13`) - **Encryption:** 3-byte header (timestamp uint16_t + flag_up uint8_t) + data_len bytes encrypted
- **Session key:** 16 bytes for AES-128 (`SC_SESSION_KEY_SIZE = 16`) - **INIT packets:** Header + data encrypted, pubkey+salt block (SC_PUBKEY_ENC_SIZE bytes) appended unencrypted
- **Tag size:** 8 bytes for authentication (`SC_TAG_SIZE = 8`) - **Nonce:** Must be exactly 13 bytes for CCM mode
- **Error codes:** Check return values, negative = error - **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 ### Directory Structure
``` ```
├── lib/ # Core libraries ├── lib/ # Core libraries (13 .c + 14 .h)
├── src/ # Main source code ├── src/ # Main source code (28 .c + 23 .h)
├── tests/ # Test programs ├── tests/ # 31+ test programs
├── doc/ # Technical Specifications ├── doc/ # Technical Specifications
├── tools/ # Auxiliary tools
│ ├── etcpmon/ # GUI монитор ETCP
│ ├── proxy/ # UDP прокси для тестов
│ └── bping/ # BPing (bandwidth ping)
├── tinycrypt/ # TinyCrypt crypto library (external) ├── tinycrypt/ # TinyCrypt crypto library (external)
└── net_emulator/ # Network emulator ├── net_emulator/ # Network emulator (delays, loss, reordering)
└── c2/ # Test instance 2 (конфиг и бинарник для тестов)
``` ```
### File Overview ### File Overview
**Core (src/)** **Core (src/)**
- `utun.c` - Main program entry point, CLI parsing, daemon mode - `utun.c` - Main program entry point, CLI parsing, daemon mode
- `utun_instance.c/h` - Root instance lifecycle management - `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
- `tun_if.c/h` - Simplified TUN interface (init/write/close) - `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/)** **Network Stack (src/)**
- `etcp.c/h` - ETCP protocol implementation (TCP-like with crypto) - `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter)
- `etcp_connections.c/h` - Socket and link management - `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks)
- `etcp_loadbalancer.c/h` - Multi-link load balancing - `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly - `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
- `routing.c/h` - Routing table management - `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/)** **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 - `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 (src/)**
- `config_parser.c/h` - INI-style config file parsing - `config_parser.c/h` - INI-style config file parsing
- `config_updater.c/h` - Config file modification utilities - `config_updater.c/h` - Config file modification utilities
**Control (src/)**
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
**Libraries (lib/)** **Libraries (lib/)**
- `u_async.c/h` - Async event loop (epoll/poll, timers) - `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 - `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
- `memory_pool.c/h` - Fast object pool allocator - `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
- `debug_config.c/h` - Debug logging system - `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
- `timeout_heap.c/h` - Min-heap for timer management - `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
- `sha256.c/h` - SHA256 hashing - `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`)
**Tests (tests/)** - `serialize.c/h` - Binary serialization utilities
- `test_etcp_*.c` - ETCP protocol tests - `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
- `test_crypto.c` - TinyCrypt AES/CCM tests - `getmyip.c/h` - Get local IP / default route detection
- `test_ecc_encrypt.c` - ECC encryption tests - `myip.c` - My IP helper (no header)
- `test_ll_queue.c` - Queue functionality tests - `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows)
- `bench_*.c` - Performance benchmarks - `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random)
**External** **Memory Pools in UTUN_INSTANCE:**
- `tinycrypt/` - TinyCrypt library (AES, ECC, SHA256) - `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
- `ack_pool` — для struct ACK_PACKET (подтверждения приёма)
### Key Components ### Key Components
- **UASYNC:** Asynchronous event loop for sockets and timers - **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
- **LL_QUEUE:** Lock-free lockstep queue for packet handling - **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
- **Memory Pool:** Fast allocation for network packets - **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
- **ETCP:** Extended TCP protocol with crypto and reliability - **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
- **Secure Channel:** AES-CCM encryption with ECC key exchange - **Secure Channel:** AES-CCM + ECC key exchange, nonce-based encryption, pubkey obfuscation
## 🐛 Debugging Tips ## Queue Usage Rules
### Enable Debug Output ### Запись в очередь
```c - Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
// In source files, define debug before including headers - Используй `queue_wait_threshold` для ожидания освобождения очереди до заданного порога.
#define DEBUG_CATEGORY_YOUR_MODULE 1
``` ### Чтение из очереди
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
### Common Build Issues - Внутри коллбэка ОБЯЗАТЕЛЬНО: `queue_data_get(q)` → обработать элемент → `queue_resume_callback(q)`
1. **Missing headers:** Check include paths in Makefile.am - Без вызова `queue_resume_callback` очередь навсегда застрянет
2. **Undefined references:** Ensure source files are listed in Makefile.am
3. **Link errors:** Check function declarations match definitions ### Поиск
- `queue_data_put_with_index(q, entry, offset, size)` — добавляет с индексом для быстрого поиска
## 📦 Dependencies - `queue_find_data_by_index(q, key, key_size)` — поиск по индексу через хеш-таблицу
### Build Dependencies ## u_async Rules
- autoconf, automake, libtool - Нельзя использовать в одном потоке несколько u_async. Один поток = всегда 1 uasync instance
- gcc with C99 support - Нельзя использовать sleep/usleep если есть uasync. Нужно использовать `uasync_set_timeout`.
- pthread library - Таймеры: `uasync_set_timeout(ua, timeout_tb, arg, callback, name)` — timebase units (0.1ms)
- OpenSSL/crypto library (for SHA256) Возвращает `void*` handle, отмена: `uasync_cancel_timeout(ua, handle)`.
### Runtime Dependencies ## Config Rules
- TUN/TAP interface support (Linux kernel module) - В серверном конфиге только собственные ключи и нет секций `[client]`
- libcrypto (usually installed by default) - В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
- Proper network configuration for testing
## Bugs Debugging Protocol
## 📝 Git Conventions
### Действия при поиске бага:
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) - **Commit Messages:** Use imperative mood, concise (50-72 chars)
- **Language:** Mix of English (technical) and Russian (business logic) - **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 - **Tags:** Version tags follow vX.Y.Z format
- "cp" or "кп" in prompt = do commit and push (всех изменений на текущий момент, не откатывая)
Example commits: ## Quick Start for New Features
```
Fix: Added uasync_t typedef for compilation
Crypto: Fixed CCM nonce size to 13 bytes, all crypto tests passing
```
## 🚀 Quick Start for New Features
1. Add new source file to `src/Makefile.am` under `utun_SOURCES` 1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS` 2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
3. Use existing patterns from similar modules 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 5. Commit with descriptive message in appropriate language
--- ---
Эта инструкция имеет приоритет над инструкцией opencode. Эта инструкция имеет приоритет над инструкцией 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*

213
doc/etcp_protocol.txt

@ -7,120 +7,183 @@
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия". - либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия".
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку. из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку.
как формируется пакет: как формируется пакет:
1. берем пакет из списка неподтвержденных пакетов ожидающих отправку 1. выбираем канал передачи (etcp_loadbalancer_select_link)
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем 2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем
если пакет найден: 3. если пакет найден или есть неподтверждённые ACK:
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало 1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало
2. в конец добавляем секцию 0x00 с данными пакета 2. в конец добавляем секцию 0x00 с данными пакета
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен
Функция прикрепления опциональных секций: Функция прикрепления опциональных секций:
- запрашивает выбор канала передачи. - запрашивает выбор канала передачи.
- добавляет накопившиеся ACK - добавляет накопившиеся ACK
При приёме пакета (от etcp_connections): При приёме пакета (от etcp_connections):
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам: - последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
- ack: помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также и ставим флаг в конце запустить etcp если он в wait timeout, т.е. wait_timeout!=NULL - ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки
- timestamp: обновляем last RTT, пересчитываем RTT10,RTT100 (плавающим окном за последние 10/100 пакетов), и jitter=max(last 10)-min(last 10 packets) - timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). - [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки.
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть. после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
пересканирование inflight: пересканирование inflight:
- вызывается по таймеру, таймер заводится при добавлении пакета в пустой inflight. - запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет.
- если время последней отправки > RTT10*k1+jitter*k2 (коэффициенты вынести в настройки utun instance) то пакет заново отправляется - если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется
и заводим таймер отправки следующей ретрансмиссии (список отсортирован по времени) - очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете
- если встречаем успешный ack, запоминаем это и его timestamp (назовем lp_ts). - по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди
- если после этого далее в очереди находим пакет который не имеет переповторов и timestamp отправки раньше чем [ИСПРАВЛЕНО: фраза обрезана в оригинале; предположительно, "раньше чем lp_ts" на основе контекста; если нет, пометить как неполное]
inflight - две очереди (ll_queue):
inflight - два списка (ll_queue): - input_send_q: список пакетов ожидающих отправку
- список с неподтвержденными пакетами ожидающими ack (при запросе ретрансмиссии пакет перемещается в ожидающие отправку) - input_wait_ack: список неподтвержденных пакетов ожидающих ack
- список с неподтвержденными пакетами ожидающими отправку (при отправке возвращается в ожидающие ack) При отправке пакет перемещается из input_send_q в input_wait_ack.
доп. свойства пакетов: При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q.
- указатель на ETC_LINK последней отправки доп. свойства пакетов (struct INFLIGHT_PACKET):
- timestamp последней отправки - указатель на ETC_LINK последней отправки (last_link)
- число отправок - timestamp последней отправки (last_timestamp)
- число запросов переповтора - число отправок (send_count)
- send_hist[8] - история через какие линки отправлялся (индекс линка)
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight. Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
optimal_inflight расчитывается из RTT и bandwidth линка. optimal_inflight расчитывается из RTT и bandwidth линка.
bandwidth по каждому линку адаптивно подстраиваем следующим алгоритмом: bandwidth по каждому линку адаптивно подстраиваем:
периодически инициируем burst_transmission (напоминаем ID пакета на которой она началась). Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты).
после передаём пачку пакетов только по этому линку со следующими таймингами: BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec).
<pkt1> <delay 4x> <pkt2><pkt3><pkt4><pkt5> - т.е. чуть задерживаем канал а потом выплёвываем пачку пакетов. Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT.
и добавляем опцию [meas_ts][n] - где n - номер пакета в пачке Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы.
приёмная сторона видя эту секцию должна отправить секцию [meas_resp] по всем пакетам, которые содержат локальный RECV_Timestamp фиксирующий время приёма каждого пакета с [meas_ts].
приняв эти response анализируем по разнице между pkt1-pkt2 не забита ли очередь отправки, а по дельте pkt2-3-4-5 оцениваем bandwidth.
дальше по алгоритму (который потом оттюним) корректируем transmitter speed limit.
**** Формат кодограмм для etcp.c/h **** **** Формат кодограмм для etcp.c/h ****
Каждая кодограмма состоит из обязательного заголовка и опциональных секций: Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
1. Обязательный заголовок всего пакета (2 байта) добавляется при передаче, есть во всех пакетах: 1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах:
[Timestamp high][Timestamp low][flags] [Timestamp high][Timestamp low][flags/flag_up]
- Timestamp: время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется - Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется
- flags: bit0 = up/down (recv_keepalive) - flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны
шифруется строго ВСЁ включая ВСЕ заголовки (кроме INIT+publickey). timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
2. Опциональные секции (одна или несколько) добавляются в etcp.c: timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
а) Подтверждения (ACK) - заголовок 0x01: а) Подтверждения (ACK) - заголовок 0x01:
[0x01][count][(id_hi,id_lo,ts_hi,ts_lo)×count][last_delivered_hi,lo][last_rx_hi,lo] [0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
- count: количество пар ID+timestamp (1-32) - count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
- last_delivered_id: последний ID, доставленный получателю - last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
- last_rx_id: последний полученный ID (для синхронизации прогресса) - rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
в) channel timestamp (добавляется после выбора канала передачи): - seq: uint32_t — номер подтверждаемого пакета
[0x0F] [RET_Timestamp high][RET_Timestamp low] [RECV_Timestamp high][RECV_Timestamp low] - recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс)
- RET_Timestamp: это timestamp последнего принятого пакета по этому каналу ETCP_LINK плюс разница времени между принятием этого пакета и текущим временем. т.е. приёмная сторона по этому timestamp (и зная своё время) может просчитать время в пути туда + обратно - dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT)
- RECV_Timestamp: это timestamp времени приёма последнего принятого пакета (т.е. отправляем время "когда мы получили этот пакет по локальном урмени") - по нему другая сторона сможет просчитать относительное время задержки в одну сторону (время туда без обратно). Каждая запись = 8 байт.
если последний пакет по этому каналу принят сильно давно (прошло более 30000 единиц времени) то channel timestamp не добавляем. [СПОРНО: порог 30000 (3 сек) arbitrary; может потребовать настройки или обоснования; также риск clock skew между нодами] в) 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 (одна секция и всегда последняя): 3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
[0x00] [ID 4 байта] [данные...] - Данные пакета [0x00] [ID 4 байта LE] [данные...]
- ID: 16-битный циклический номер пакета. [СПОРНО: 16-bit может привести к быстрому overflow в long-lived сессиях; рассмотреть 32-bit] - ID: uint32_t, 32-битный циклический номер пакета
Пакет может содержать несколько опциональных секций (например, ACK + данные). [СПОРНО: нет явных length полей для секций; parsing implicit, риск ошибок при variable длине; добавить length?] Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний.
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
цель протокола: цель протокола:
1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT) 1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT)
Установка подключения для канала (реализуется в etcp_connections.c/h): Установка подключения для канала (реализуется в etcp_connections.c/h):
каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи). каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи).
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно): При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
- RTT последнего пакета и время отправки последнего пакета - RTT последнего пакета и время отправки последнего пакета
- пакеты делятся на 3 категории: <300 байт, 300-699 байт, 700+ байт. для каждой категории отдельно считаются метрики: - общее количество отправленных/принятых байт
- количество отправленных в пакетах байтах - количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
- количество перезапросов (по каждому каналу отдельно) в пакетах и байтах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент - обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
Планировщик/балансир каналов (etcp_loadbalancer.c/h): Планировщик/балансир каналов (etcp_loadbalancer.c/h):
для каждого канала ограничение полосы с тестированием отклика: для каждого канала ограничение полосы с тестированием отклика:
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков) - полосу ограничиваем по объему данных - полному числу байт 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. (при неактивности обновляем время) Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время)
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); [СПОРНО: сложность с float; риск precision issues] счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи");
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения. когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения.
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client. также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client.
процесс установки соединения: процесс установки соединения (рукопожатие):
- отправляем init запрос и выставляем таймаут (сохраняем его в struct ETCP_LINK) - клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяем init запросы, ведем счетчик запросов init. - по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
- когда получаем init подтверждение (отправляется в etcp_connections_read_callback) снимаем таймаут и выставляем флаг struct ETCP_LINK initialized=1. - когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
При добавлении нового канала посылаем INIT 0x04 (без сброса), а при первичной инициализации подключения - 0x02 (это нужно, т.к. например перезапуск локального софта должен инициировать перезапуск удаленного соединения). - сервер при получении 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.c/h ****
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда только одна секция). в обязательном заголовке ID не используется, при передаче для порядка =0: [СПОРНО: заголовок в etcp — только 2 байта TS; здесь подразумевается ID? Уточнить единый формат] Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) 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 нет (первое подключение) - создаёт. Между нодами только одно подключение, но можно добавлять каналы. 0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
- link_id: локальный идентификатор канала (0-255), назначается отправителем для идентификации канала 0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
- Публичный ключ отправляется в конце пакета без шифрования, чтобы получатель мог установить его и расшифровать остальную часть пакета (т.к. инициатор соединения всегда имеет оригинальный peer public key в конфиге - по нему исключаем MITM) 0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
2) Init подтверждение - заголовок 0x03/0x05 (ответы соответственно на коды 0x02 и 0x04): 0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
[0x03/0x05] [my_node_id 64bit] [my mtu high] [my mtu low] [my link_id 1 байт] [peer ipv4 4 байта] [peer port 2 байта] 0x06 — ETCP_PING (проверка доступности)
- Подтверждение инициализации (канал успешно создан, можно начинать обмен) 0x07 — ETCP_PONG (ответ на пинг)
- link_id: локальный идентификатор канала (0-255) отправителя ответа
- peer ip.port: возвращаем ip:port с которого пришел init запрос (чтобы клиент узнал свой external ip:port если за nat) 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 получатель пакета должен: При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть - reset ETCP_LINK с этим ip_port если он есть
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - вызвать etcp_reset (функция сброса окон неподтвержденных данных и нумерации) - создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
приём кодограмм из UDP выглядит так: приём кодограмм из 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 если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
Клиент: если все линки =down то начинается процедура восстановления связи: Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса - клиент посылает init без сброса (0x04), link_state=2
- сервер как получает init без сброса отправляет init подтверждение: со сбросом или без сброса в зависимости от состояния init его соединения (инициализировано - то без сброса). - сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)

344
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
Loading…
Cancel
Save