Browse Source

Убраны TCP_TIMESTAMPS и TCP_KEEPALIVE из lwip_tcp_opts.h (не используются, конфликтовали с ws2ipdef.h на Windows)

topo_upd
Evgeny 3 months ago
parent
commit
0381f4df89
  1. 1086
      ARCHITECTURE.md
  2. 784
      USER_MANUAL.md
  3. 25
      _desc.md
  4. 112
      lib/debug_config_doc.md
  5. 34
      lib/getmyip_doc.md
  6. 234
      lib/ll_queue_doc.md
  7. 48
      lib/mem_doc.md
  8. 57
      lib/memory_pool_doc.md
  9. 81
      lib/platform_compat_doc.md
  10. 97
      lib/radix_doc.md
  11. 117
      lib/serialize_doc.md
  12. 32
      lib/sha256_doc.md
  13. 99
      lib/socket_compat_doc.md
  14. 39
      lib/swm_min_doc.md
  15. 155
      lib/tcp_io_doc.md
  16. 107
      lib/timeout_heap_doc.md
  17. 174
      lib/u_async_doc.md
  18. 98
      src/config_parser_doc.md
  19. 67
      src/config_updater_doc.md
  20. 145
      src/conn_mgr_doc.md
  21. 49
      src/control_server_doc.md
  22. 35
      src/crc32_doc.md
  23. 239
      src/db_sync_doc.md
  24. 86
      src/dummynet_doc.md
  25. 47
      src/eim_nat_doc.md
  26. 121
      src/etcp_api_doc.md
  27. 95
      src/etcp_bbr_doc.md
  28. 71
      src/etcp_connect_doc.md
  29. 198
      src/etcp_connections_doc.md
  30. 39
      src/etcp_debug_doc.md
  31. 289
      src/etcp_doc.md
  32. 59
      src/etcp_dump_doc.md
  33. 51
      src/etcp_loadbalancer_doc.md
  34. 131
      src/etcp_router_doc.md
  35. 28
      src/firewall_doc.md
  36. 72
      src/lwip_tcp/lwip_pbuf_doc.md
  37. 461
      src/lwip_tcp/lwip_tcp_doc.md
  38. 104
      src/lwip_tcp/lwip_tcp_in_doc.md
  39. 2
      src/lwip_tcp/lwip_tcp_opts.h
  40. 167
      src/lwip_tcp/lwip_tcp_out_doc.md
  41. 69
      src/msg_transport_doc.md
  42. 44
      src/nat_transport_doc.md
  43. 25
      src/ntp_node_time_doc.md
  44. 27
      src/ntp_time_doc.md
  45. 59
      src/packet_dump_doc.md
  46. 95
      src/pkt_normalizer_doc.md
  47. 77
      src/proxy/icmp_proxy_doc.md
  48. 132
      src/proxy/socks_proxy_doc.md
  49. 204
      src/proxy/tcp_proxy_client_doc.md
  50. 108
      src/proxy/tcp_proxy_server_doc.md
  51. 73
      src/proxy/udp_proxy_doc.md
  52. 50
      src/route6_lib_doc.md
  53. 64
      src/route_connectivity_doc.md
  54. 68
      src/route_lib_doc.md
  55. 62
      src/route_ping_doc.md
  56. 44
      src/routing_doc.md
  57. 352
      src/secure_channel_doc.md
  58. 55
      src/stcp_client_doc.md
  59. 52
      src/stcp_doc.md
  60. 73
      src/stcp_link_doc.md
  61. 44
      src/stcp_server_doc.md
  62. 305
      src/topo_group_doc.md
  63. 170
      src/topo_node_doc.md
  64. 39
      src/topo_node_sqlite_doc.md
  65. 54
      src/tun_if_doc.md
  66. 49
      src/tun_route_doc.md
  67. 50
      src/utun_doc.md
  68. 116
      src/utun_instance_doc.md
  69. 50
      tools/bping/bping_doc.md
  70. 216
      tools/chatgui/chatgui_doc.md
  71. 120
      tools/etcpmon/etcpmon_doc.md
  72. 29
      tools/proxy/proxy_doc.md

1086
ARCHITECTURE.md

File diff suppressed because it is too large Load Diff

784
USER_MANUAL.md

@ -0,0 +1,784 @@
# uTun — Руководство пользователя
uTun — децентрализованная VPN-система с шифрованием, надёжной доставкой данных, поддержкой mesh-сетей и встроенным TCP-прокси. Работает поверх UDP (основной транспорт) и TCP (STCP-резерв). Всё шифруется через X25519 + AES-128-CCM. Топология узлов обменивается автоматически (BGP-подобный протокол).
---
## 1. Быстрый старт
### 1.1. Сборка
```bash
git clone <репозиторий> && cd utun3
./build.sh --full -j4 # autoreconf + configure + make
```
Зависимости: `gcc`, `make`, `autotools` (autoconf/automake/libtool), `OpenSSL` (libssl-dev).
### 1.2. Генерация ключей
Ключи генерируются автоматически, если их нет в конфиге. Достаточно указать `my_node_name` — `config_updater` создаст пару X25519 и node_id:
```ini
[global]
my_node_name=my-first-node
```
После первого запуска ключи пропишутся в конфиг автоматически.
### 1.3. Первый запуск
```bash
sudo ./src/utun -f -c myconfig.conf
```
- `-f` — foreground (без демонизации, видно логи в консоли)
- `-c myconfig.conf` — путь к конфигу
- `-p /var/run/utun.pid` — PID-файл
- `-l /var/log/utun.log` — лог-файл
Без `-f` процесс уходит в демон (`fork` + `setsid`).
### 1.4. Проверка работы
```bash
ip addr show tun3 # TUN-интерфейс создан?
ping 10.23.1.1 # пингуем TUN-IP узла
ss -uln | grep 1333 # UDP-сокет слушает?
```
---
## 2. Архитектура (кратко)
uTun — **однопоточная асинхронная** система. Один event loop (`epoll`/`poll`) на поток. Никаких `sleep()`, никакого блокирующего I/O.
**Стек протоколов (5 уровней):**
| Уровень | Назначение |
|---------|------------|
| 1. UDP/TUN/STCP | Физический транспорт: UDP-сокеты, TUN-интерфейс, TCP-резерв |
| 2. secure_channel | X25519 ECDH + AES-128-CCM шифрование + Ed25519 подписи |
| 3. ETCP | Надёжный транспорт (inflight, ACK, ретрансмиссии, BBR congestion control) |
| 4. LoadBalancer | Multi-link балансировка (min inflight + round-robin) |
| 5. etcp_router | Сервисная маршрутизация (seq, dedup, reorder, transit forwarding) |
Два уровня надёжности:
- **ETCP** — per-connection, RTT-based ретрансмиссии
- **etcp_router** — per-service, 300ms ретрансмиссии с 17 попытками (~5 сек)
Node identity: `SHA256(private_key)` → первые 8 байт → 63-bit `node_id`.
---
## 3. Конфигурация
Формат: INI-style. Секции `[section]`, строки `key=value`, комментарии `#`.
### 3.1. `[global]` — Идентичность и базовые настройки
```ini
[global]
my_node_name=home-server # имя узла (человекочитаемое)
my_node_id=5f75c7445af88e1f # ID узла (авто-генерируется)
my_private_key=d065b784...4fe441 # X25519 приватный ключ (hex, 64 символа)
my_public_key=86b51a8b...6a2d19 # X25519 публичный ключ (hex, 64 символа)
tun_enabled=yes # создавать TUN-интерфейс (по умолчанию yes)
tun_ifname=tun3 # имя TUN (по умолчанию tun0)
tun_ip=10.23.1.1 # IP на TUN-интерфейсе
mtu=1500 # MTU для всех соединений
debug_level=error # глобальный уровень отладки (none/error/warn/info/debug/trace)
log_file=/var/log/utun.log # путь к лог-файлу
```
### 3.2. `[server:NAME]` — Локальные UDP-сокеты
```ini
[server: lan1]
addr=192.168.1.10:1333 # IP:порт для приёма входящих
type=public # public / nat / private / local
transport=udp # udp (по умолчанию) или tcp (STCP)
mtu=1400 # MTU для этого канала
so_mark=100 # SO_MARK для policy routing
netif=eth0 # привязка к интерфейсу
only_local=0 # 1 = только локальные подключения
```
Типы адресов:
- `public` — узел на публичном IP (прямая связь)
- `nat` — узел за NAT (нужен NAT traversal)
- `private` — локальная сеть (только соседи в том же сегменте)
- `local` — только loopback/локальные подключения
### 3.3. `[client:NAME]` — Подключение к удалённым узлам
```ini
[client: aeza]
link=lan1:85.192.42.96:1333 # server_name:ip:port (можно несколько link=)
link=lan1:85.192.42.96:1433 # второй канал к тому же узлу
peer_public_key=0206dbee...fbcc16 # публичный ключ удалённого узла
keepalive=1 # интервал keepalive (сек)
```
Формат `link`: `локальный_сервер:удалённый_ip:удалённый_порт`. Серверная секция должна быть объявлена выше по файлу.
### 3.4. `[routing]` — Маршруты
```ini
[routing]
my_subnet=10.23.1.0/24 # своя подсеть — анонсируется соседям через BGP
route_subnet=10.23.0.0/16 # добавляется в системную таблицу маршрутов через TUN
route_subnet=fd00:1234::/32 # IPv6-подсеть
```
### 3.5. `[debug]` — Настройка отладки
Формат: `категория=уровень`. Уровни: `none`, `error`, `warn`, `info`, `debug`, `trace`.
```ini
[debug]
etcp=trace # всё про ETCP
crypto=info # криптографические операции
bgp=info # обмен топологией
connection=info # сокеты и линки
general=info # общая информация
```
Основные категории (всего 26): `uasync`, `ll_queue`, `connection`, `etcp`, `crypto`, `memory`, `timing`, `config`, `tun`, `routing`, `timers`, `normalizer`, `bgp`, `socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcпroute`, `bbr`, `etcp_dump`.
### 3.6. `[firewall]` — Правила фильтрации
```ini
[firewall]
allow=all # разрешить всё (bypass)
allow=192.168.1.0/24 # разрешить подсеть
allow=10.0.0.1:80 # разрешить конкретный IP:порт
```
### 3.7. `[nat]` — EIM NAT
```ini
[nat]
nat_enabled=yes
nat_tun_ifname=tun_external # внешний TUN (выход в интернет)
nat_via=gateway_ip # шлюз
port_start=10000
port_end=20000
forward=tcp:10.0.0.5:80:8080 # проброс: протокол:ip:внутр_порт:внеш_порт
```
### 3.8. `[tcp_proxy_client]` / `[tcp_proxy_server]` — TCP-прокси
**Клиент (выход через exit-узел):**
```ini
[tcp_proxy_client]
enabled=yes
tun_name=tun_proxy # имя TUN для прокси-трафика
tun_ip=10.200.30.1 # IP клиентского TUN
via_node=50e9e88658e91977 # node_id exit-узла
socks_enabled=yes # встроенный SOCKS5-прокси
socks_addr=127.0.0.1:1082 # слушать SOCKS5 здесь
http_proxy_enabled=yes # встроенный HTTP CONNECT-прокси
http_proxy_addr=127.0.0.1:8080
```
**Exit-узел (сервер):**
```ini
[tcp_proxy_server]
enabled=yes
```
Клиент настраивает браузер на SOCKS5 `127.0.0.1:1082` — весь TCP-трафик идёт через exit-узел.
### 3.9. `[control]` — Сервер мониторинга
```ini
[control]
control_ip=192.168.29.117 # IP для приёма подключений etcpmon
control_port=9090
control_allow=192.168.0.0/16 # разрешённые подсети (можно несколько)
```
### 3.10. `[allowed_keys]` — Ограничение подключений
```ini
[allowed_keys]
allow_all=1 # разрешить все ключи
# key=86b51a8b...6a2d19 # разрешить конкретный pubkey
```
### 3.11. `[ntp]` — Синхронизация времени
```ini
[ntp]
enabled=yes
server=pool.ntp.org
server=time.google.com
interval=3600 # интервал синхронизации (сек)
```
### 3.12. `[network:NAME]` — Именованные сети
```ini
[network: mynet]
id=5f75c7445af88e1f # 56-bit ID сети
pubkey=86b51a8b...6a2d19 # публичный ключ сети
signing_key=d065b784...4fe441 # ключ подписи (Ed25519)
```
### 3.13. Серверный vs клиентский конфиг
| Параметр | Сервер | Клиент |
|----------|--------|--------|
| `[server]` секция | Обязательна (свои сокеты) | Обязательна (свои сокеты) |
| `[client]` секция | Нет | Обязательна (к кому подключаться) |
| `peer_public_key` | Нет | Обязателен в `[client]` |
| `my_private_key` | Свой | Свой |
| `my_public_key` | Свой | Свой |
Важно: `[server]` обязателен в **обоих** случаях — это рабочий сокет узла. Клиент дополнительно указывает `[client]` с `peer_public_key` и `link`.
---
## 4. Сценарии использования
### 4.1. VPN между двумя узлами
**Сервер** (публичный IP `85.192.42.96`):
```ini
[global]
my_node_name=server
my_private_key=<hex>
my_public_key=<hex>
tun_ip=10.23.1.1
[server: main]
addr=85.192.42.96:1333
type=public
[routing]
my_subnet=10.23.1.0/24
```
**Клиент** (за NAT, подключается к серверу):
```ini
[global]
my_node_name=client
my_private_key=<hex>
my_public_key=<hex>
tun_ip=10.23.2.1
[server: uplink]
addr=0.0.0.0:1333 # можно 0.0.0.0 — клиенту не нужен фиксированный порт
type=nat
[client: server]
link=uplink:85.192.42.96:1333
peer_public_key=<pubkey_сервера>
keepalive=1
[routing]
my_subnet=10.23.2.0/24
```
**Проверка связности:**
```bash
# На сервере
ping 10.23.2.1
# На клиенте
ping 10.23.1.1
```
### 4.2. Mesh-сеть из нескольких узлов
Каждый узел = сервер (свои сокеты) + клиент (подключения к соседям). Пример для узла B, который подключается к A и C:
```ini
[global]
my_node_name=node-b
my_private_key=<hex>
my_public_key=<hex>
tun_ip=10.23.2.1
[server: main]
addr=0.0.0.0:1333
type=public
[client: node-a]
link=main:85.192.42.96:1333
peer_public_key=<pubkey_A>
[client: node-c]
link=main:203.0.113.10:1333
peer_public_key=<pubkey_C>
[routing]
my_subnet=10.23.2.0/24
```
Что происходит автоматически:
1. B устанавливает ETCP-соединения с A и C
2. Через BGP A узнаёт о подсетях C (и наоборот)
3. Узел A может отправлять пакеты к подсети C через B (transit forwarding)
4. `etcp_router` обеспечивает надёжную маршрутизацию через промежуточные узлы
### 4.3. Exit-прокси (выход в интернет через удалённый узел)
**Exit-узел** (с прямым выходом в интернет):
```ini
[global]
my_node_name=exit-node
my_private_key=<hex>
my_public_key=<hex>
[server: main]
addr=85.192.42.96:1333
type=public
[tcp_proxy_server]
enabled=yes
```
**Клиент** (выходит в интернет через exit-узел):
```ini
[global]
my_node_name=client
my_private_key=<hex>
my_public_key=<hex>
[server: main]
addr=0.0.0.0:1333
[client: exit]
link=main:85.192.42.96:1333
peer_public_key=<pubkey_exit>
[tcp_proxy_client]
enabled=yes
tun_name=tun_proxy
tun_ip=10.200.30.1
via_node=<node_id_exit>
socks_enabled=yes
socks_addr=127.0.0.1:1082
```
Настройка браузера: SOCKS5 proxy `127.0.0.1:1082`. TCP-трафик идёт: браузер → SOCKS5 → lwIP → ETCP → exit-узел → интернет.
### 4.4. EIM NAT (раздача интернета через NAT)
```ini
[nat]
nat_enabled=yes
nat_tun_ifname=eth0 # внешний интерфейс
port_start=20000
port_end=30000
forward=tcp:192.168.1.100:80:8080 # проброс порта 8080 → 192.168.1.100:80
forward=udp:192.168.1.100:53:53 # проброс DNS
```
### 4.5. Чат (chatgui)
Chatgui — десктопный GUI-чат (Qt 6) со встроенным uTun-узлом. Вся связь — напрямую между узлами (P2P), без центральных серверов.
**Сборка:**
```bash
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
cd tools/chatgui && mkdir -p build && cd build
cmake .. && make -j4
./chatgui
```
**Запуск:**
```bash
setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown
```
Chatgui работает в двух потоках:
- **GUI-поток** — Qt, отрисовка, чтение из SQLite
- **uasync-поток** — uTun-узел, сеть, криптография, ETCP, запись в SQLite
**Основные возможности:**
- Создание каналов и групп
- Обмен сообщениями с анимированными эмодзи (TGS)
- Invite-ссылки (`utun://...`) с QR-кодами
- P2P синхронизация через Merkle-деревья
- Авто-подключение к известным узлам из БД
---
## 5. Топология сети
### 5.1. Типы адресов
| Тип | Описание | Пример |
|-----|----------|--------|
| `INTERFACE` | Адрес на сетевом интерфейсе | `eth0: 192.168.1.10` |
| `NAT` | Внешний адрес после NAT (детектируется) | `85.192.42.96:54321` |
| `REAL` | Публичный адрес (совпадает interface == NAT) | `85.192.42.96:1333` |
### 5.2. Как узлы находят друг друга
1. **Прямое подключение** — через `[client]` секцию с явным IP:портом
2. **BGP-обмен** — при подключении к одному узлу, узнаёшь о всех его соседях
3. **Локальное сканирование** — `conn_mgr` периодически сканирует локальную сеть
4. **Invite-ссылки** — `utun://` ссылки с закодированными адресами и ключами
### 5.3. NAT traversal
`conn_mgr` реализует трёхфазное подключение:
1. **DIRECT** (5 сек) — прямое INIT-рукопожатие со всеми известными адресами
2. **REVERSE** (15 сек) — если клиент за NAT, сервер с прямым IP: клиент шлёт свои адреса через BGP, сервер подключается сам
3. **INDIRECT** (15 сек) — оба за NAT: выбираются общие кандидаты-посредники по min RTT, трафик идёт через `etcp_router`
Типы NAT, которые определяет uTun:
- **EIM** (Endpoint-Independent Mapping) — один внешний порт для всех назначений
- **Strict** (Address/Restricted/Symmetric) — разные порты
### 5.4. Группы
Два типа групп:
- **UTUN** (`group_id=0x8000000000000000`) — VPN-сеть: обмен подсетями, маршруты
- **CHAT** — чат-группа: только узлы, без подсетей, персистентность в SQLite
Узлы разных групп изолированы.
---
## 6. Мониторинг и отладка
### 6.1. Уровни отладки
```
none < error < warn < info < debug < trace
```
Настройка через `-d` в CLI (имеет приоритет) или секцию `[debug]` в конфиге:
```bash
./src/utun -f -c myconfig.conf -d "etcp=trace,crypto=info"
```
Или эквивалент в конфиге:
```ini
[debug]
etcp=trace
crypto=info
bgp=info
```
### 6.2. Категории отладки (26 шт)
| Категория | Что логирует |
|-----------|-------------|
| `etcp` | Протокол ETCP: очереди, retrans, ACK, RTT |
| `crypto` | Шифрование/дешифрование, ключи, nonce |
| `connection` | Сокеты, линки, INIT handshake, keepalive |
| `bgp` | Обмен топологией (NODEINFO, WITHDRAW) |
| `routing` | Таблица маршрутов, поиск путей |
| `tun` | TUN-интерфейс: чтение/запись |
| `traffic` | Данные трафика |
| `general` | Общая информация |
| `bbr` | BBR congestion control |
| `keepalive` | Адаптивный keepalive |
| `memory` | Выделение/освобождение памяти |
| `dump` | Hex-дампы пакетов |
| `etcp_dump` | Декодирование ETCP-пакетов |
### 6.3. etcpmon — Windows GUI монитор
Подключается к `control_server` узла по TCP (порт из `control_port`). Показывает:
- Список соединений и их состояние
- RTT, потери, inflight
- Графики метрик
- Дамп пакетов
### 6.4. control_server — TCP API
```ini
[control]
control_ip=0.0.0.0
control_port=9090
control_allow=192.168.0.0/16
```
Любой TCP-клиент может подключиться и получать события в реальном времени (JSON-подобный формат).
### 6.5. Логи и дампы
Лог-файл указывается через `-l`:
```bash
./src/utun -f -c myconfig.conf -l /tmp/utun.log
```
Hex-дамп пакетов включается категорией `dump=trace`:
```ini
[debug]
dump=trace
```
---
## 7. NAT и Firewall
### 7.1. Как uTun определяет тип NAT
1. Клиент подключается к серверу
2. Сервер видит внешний IP:порт клиента (из UDP-пакета)
3. Сервер через **третий узел** (посредник) пингует клиента на этот внешний IP:порт
4. Если клиент ответил — NAT EIM (один mapping для всех)
5. Если нет — NAT Strict
Результат сохраняется в `link->nat_type` и broadcast-ится всем соседям через BGP.
### 7.2. Firewall
```ini
[firewall]
allow=all # всё разрешено
allow=10.0.0.0/8 # разрешить подсеть
allow=192.168.1.5:22 # разрешить IP:порт
```
По умолчанию без секции `[firewall]` — фильтрация выключена.
### 7.3. Проброс портов
```ini
[nat]
forward=tcp:192.168.1.100:80:8080 # входящий порт 8080 → 192.168.1.100:80
forward=udp:10.0.0.53:53:53 # DNS
```
---
## 8. Устранение неполадок
### 8.1. Нет связи — проверка INIT handshake
Включите:
```ini
[debug]
connection=debug
crypto=debug
```
В логах ищите:
- `INIT_REQUEST sent to ...` / `INIT_RESPONSE received from ...` — handshake
- `session_ready=1` — ключи согласованы, шифрование работает
- `ETCP_KEEPALIVE` — keepalive-пакеты идут
- Если нет — проверьте firewall на портах, доступность IP
### 8.2. Пакеты теряются — проверка метрик
```bash
# В логах с etcp=debug смотрите:
grep "retrans" /tmp/utun.log # ретрансмиссии
grep "rtt=" /tmp/utun.log # задержки
grep "lost" /tmp/utun.log # потери
```
Каждые 10 минут пишутся гистограммы метрик с RTT-бакетами и loss-бакетами.
### 8.3. Высокая задержка — keepalive
Адаптивный keepalive: период растёт от 200ms до 10s при отсутствии трафика, сбрасывается к 200ms при появлении. Таймаут = период × 10.
Проверьте:
```ini
[debug]
keepalive=debug
```
### 8.4. Утечки памяти — проверка пулов
```ini
[debug]
memory=debug
```
При завершении `u_report_unfreed_blocks()` показывает все неосвобождённые блоки.
### 8.5. Дамп состояния
ETCP предоставляет функцию `etcp_dump_all_conns()` для вывода полного состояния всех соединений (очереди, inflight, счётчики). Включается через `etcp_dump=trace`.
### 8.6. ASAN-сборка (поиск повреждений памяти)
```bash
./build.sh --asan -j4
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -c myconfig.conf
```
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
---
## 9. Производительность
### 9.1. BBR congestion control
BBR работает per-link. Основные настройки:
```ini
[global]
bbr_max_cwnd=1048576 # максимум окна перегрузки (1MB по умолчанию)
```
BBR автоматически измеряет пропускную способность через burst-измерения (12 пакетов пачкой, замер inter-packet gap). Периодичность burst — не чаще 500ms.
### 9.2. Memory pools
Три пула в `UTUN_INSTANCE`:
- `data_pool` — данные пакетов (payload)
- `pkt_pool` — `struct ETCP_DGRAM` (сетевые пакеты)
- `ack_pool` — `struct ACK_PACKET` (подтверждения)
Пулы аллоцируются upfront при старте. Статистику можно посмотреть через control_server.
### 9.3. Multi-link балансировка
LoadBalancer выбирает линк с минимальным inflight, с round-robin для равных. Traffic shaper ограничивает отправку до `optimal_inflight`:
```
timeout = (rtt_avg_10 * 32 + jitter * 32) / 16
```
Оптимальное окно: `optimal_inflight = sum(inflight_lim_bytes)`
### 9.4. Keepalive
Адаптивный режим:
- Начальный период = `keepalive` из `[client]` (сек)
- При трафике → сброс к начальному
- Без трафика → растёт ×1.05 до 10 секунд
- Таймаут = период × 10
---
## 10. CLI-справка
```
utun [опции]
Опции:
-f Foreground (без демонизации)
-c FILE Конфигурационный файл (по умолчанию: utun.conf)
-p FILE PID-файл (по умолчанию: /var/run/utun.pid)
-l FILE Лог-файл (по умолчанию: utun.log)
-d CONFIG Отладка: категория=уровень,... (переопределяет конфиг)
-h Справка
```
**Сигналы:**
- `SIGTERM` / `SIGINT` — graceful shutdown (закрытие соединений, освобождение ресурсов)
- `SIGHUP` — reload конфига (селективный: меняются только изменённые сокеты/клиенты; полный — при изменении ключей или TUN)
---
## 11. Типовые конфигурации
### Минимальный сервер
```ini
[global]
my_node_name=server
tun_ip=10.23.1.1
[server: main]
addr=0.0.0.0:1333
```
### Минимальный клиент
```ini
[global]
my_node_name=client
tun_ip=10.23.2.1
[server: main]
addr=0.0.0.0:1333
[client: server]
link=main:85.192.42.96:1333
peer_public_key=<pubkey_сервера>
```
### Mesh-узел с несколькими пирами
```ini
[global]
my_node_name=mesh-node-3
tun_ip=10.23.3.1
[server: main]
addr=0.0.0.0:1333
[client: node-1]
link=main:10.0.0.1:1333
peer_public_key=<pubkey_1>
[client: node-2]
link=main:10.0.0.2:1333
peer_public_key=<pubkey_2>
[routing]
my_subnet=10.23.3.0/24
```
### Exit-узел с TCP-прокси
```ini
[global]
my_node_name=exit-eu
tun_ip=10.23.100.1
[server: main]
addr=85.192.42.96:1333
type=public
[tcp_proxy_server]
enabled=yes
[routing]
my_subnet=10.23.100.0/24
```
### Клиент с SOCKS5-прокси
```ini
[global]
my_node_name=home-laptop
[server: main]
addr=0.0.0.0:1333
[client: exit]
link=main:85.192.42.96:1333
peer_public_key=<pubkey_exit>
[tcp_proxy_client]
enabled=yes
socks_enabled=yes
socks_addr=127.0.0.1:1082
via_node=<node_id_exit>
```

25
_desc.md

@ -0,0 +1,25 @@
Задача сделать полное архитектурное описание проекта.
Как делаем
1. определяем список проектов. продумываем последовательность разбора проектов - зависимости - что от чего зависит, что лучше разбирать в начале.
Далее по каждому проекту:
2. разбиваешь весь проект на логические модули.
поручаешь субагентам (параллельно запускаешь до 8 потоков), балансируй нагрузку на агентов (по объему кода): если модули короткие - можно 2-3 простых одному субагенту.
- проанализировать логику работы модуля
- понять как этим пользоваться с позиции пользователя (ты - архитектор и пользователь - типовой сценарий использования модуля с точки зрения удобства и лёгкости использования).
какие есть потенциальные проблемные места, можно ли пользоваться в многопоточной архитектуре, можно ли в асинхронной архитектуре, можно ли делать close из callback (если есть коллбэки и модулю асинхронный). какие есть еще нюансы и ограничения.
- создать (или обновить если есть) описание работы модуля - файл рядом с исходниками называется [module_name]_doc.md
описание должно быть с точки зрения пользователя - в первую очередь как пользоваться и какие нюансы. понятным языком (если используется терминология специфичная для модуля - то должна быть понятна или пояснения).
Схема описания:
1. назначение модуля, зачем нужен, что делает
2. как пользоваться, лучше типовой сценарий (с нюансами если есть),
3. кратко пояснения по апи и нюансы если есть
далее читаешь полностью все эти описания и формируешь итоговое описание проекта.
Продумай какая структура документации лучше подходит, разбей на главы.
И сделай общее описние проекта (user manual).
твоя задача на основе сформированной документации разобраться как работает utun. ключевое слово =- хорошо. хорошо понять все модули, их взаимодействое, возможности, облести применимости и
ограничения. понять архитектурно как работает проект собранный из этих модулей. и только после этого написать документацию.

112
lib/debug_config_doc.md

@ -0,0 +1,112 @@
# debug_config — Runtime-система отладочного логирования
## 1. Назначение
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
## 2. Как пользоваться
### 2.1. Инициализация
```c
#include "debug_config.h"
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
```
### 2.2. Макросы логирования
```c
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
```
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
### 2.3. Конфигурационная строка
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
```ini
debug = etcp=trace,config=info,crypto=error
```
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
Уровни: `disabled, none, error, warn, info, debug, trace`.
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
### 2.4. Двойной вывод (консоль + файл)
- По умолчанию вывод в **stdout**.
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
- `debug_disable_file_output()` — закрывает файл.
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
### 2.5. Вспомогательные утилиты
```c
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
```
### 2.6. Нюансы
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
## 3. API
| Функция/макрос | Назначение |
|---|---|
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
| `debug_disable_file_output()` | Закрыть файл вывода |
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
| `debug_get_level()` | Получить текущий глобальный уровень |
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
| `debug_get_category_name(cat)` | Получить имя категории по ID |
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
**Структура `debug_config_t`:**
- `level` — глобальный уровень (по умолчанию ERROR)
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
- `file_output` — FILE* файла (NULL если вывод в консоль)
- `console_enabled` — вывод в stdout
- `thread_marker` — числовой маркер потока (0 = нет)

34
lib/getmyip_doc.md

@ -0,0 +1,34 @@
# Get My IP (getmyip.h)
## 1. Назначение
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
## 2. Как пользоваться
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
```c
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
struct sockaddr_storage remote; // AF_INET6, адрес сервера
struct sockaddr_storage chosen_ip;
// ... заполнить local_hint и remote ...
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
// chosen_ip.sin6_port == 0
}
```
**Нюансы:**
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
- Порт в `result` всегда 0 — только IP
## 3. API
### Функция
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
### Зависимости
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`

234
lib/ll_queue_doc.md

@ -0,0 +1,234 @@
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
## 1. Назначение
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
## 2. Как пользоваться
### 2.1. Создание очереди
```c
// Без хеша (простая очередь)
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
// С хешем — для быстрого поиска по ключу внутри data[]
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
```
### 2.2. Создание элемента
Три способа, по убыванию производительности:
```c
// 1. Из memory pool (самый быстрый)
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
// 2. Через malloc (data[] фиксированного размера одним блоком)
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
memcpy(e->dgram, src_data, dgram_len);
e->len = dgram_len;
```
### 2.3. Запись в очередь (backpressure)
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
```c
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
queue_set_threshold(q, 0, 0);
// 2. В структуре-производителе завести handle:
struct queue_waiter_handle my_waiter = {0};
// 3. При готовности отправить — зарегистрировать ожидание:
static void my_send_cb(struct ll_queue* q, void* arg) {
struct my_producer* p = (struct my_producer*)arg;
struct ll_entry* e = ... // создать элемент
e->len = data_len;
queue_data_put_with_index(q, e);
// Снова зарегистрироваться на следующую порцию
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
}
// Первичная регистрация:
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
// 4. При деинициализации — отменить ожидание:
queue_waiter_cancel(q, &my_waiter);
```
### 2.4. Чтение из очереди (callback)
```c
// Установить callback — будет вызываться при появлении элементов
queue_set_callback(q, my_callback, my_data);
static void my_callback(struct ll_queue* q, void* arg) {
// 1. ИЗВЛЕЧЬ элемент
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// 2. ОБРАБОТАТЬ
if (e->dgram && e->len > 0) {
do_something(e->dgram, e->len);
queue_dgram_free(e); // сначала освободить dgram
}
queue_entry_free(e); // потом освободить сам entry
// 3. ОБЯЗАТЕЛЬНО возобновить callback
queue_resume_callback(q);
}
```
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
```c
// ПРАВИЛЬНО:
if (!e) { queue_resume_callback(q); return; }
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
// НЕПРАВИЛЬНО — очередь зависнет:
if (!e) return;
```
### 2.5. Поиск по индексу
```c
// Создать очередь с хешем (ключ 4 байта по смещению 0):
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
queue_data_put_with_index(q, entry);
// Искать:
uint32_t key = 42;
struct ll_entry* found = queue_find_data_by_index(q, &key);
// Если несколько элементов с одним ключом — продолжать поиск:
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
```
### 2.6. Освобождение ресурсов
```c
// 1. Снять callback
queue_set_callback(q, NULL, NULL);
// 2. Отменить всех waiter-ов
queue_waiter_cancel(q, &my_waiter);
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
struct ll_entry* e;
while ((e = queue_data_get(q)) != NULL) {
queue_dgram_free(e);
queue_entry_free(e);
}
// После извлечения всех элементов resume не нужен — callback уже снят.
// 4. Освободить очередь
queue_free(q);
```
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
### 2.7. Приоритетная вставка (LIFO)
```c
// Вставить в начало очереди (высокий приоритет):
queue_data_put_first(q, entry);
queue_data_put_first_with_index(q, entry);
```
### 2.8. Удаление из середины
```c
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
queue_dgram_free(entry); // освободить dgram отдельно
queue_entry_free(entry); // освободить entry отдельно
```
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
### 2.9. Callback при опустошении
```c
static void on_empty(struct ll_queue* q, void* arg) {
// Очередь стала пустой (count==0)
// Вызывается однократно, после вызова сбрасывается
}
queue_set_empty_callback(q, on_empty, my_data);
```
## 3. API
### Создание / уничтожение
| Функция | Назначение |
|---------|-----------|
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
### Создание / освобождение элементов
| Функция | Назначение |
|---------|-----------|
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
### Чтение (callback)
| Функция | Назначение |
|---------|-----------|
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
### Запись
| Функция | Назначение |
|---------|-----------|
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
### Backpressure
| Функция | Назначение |
|---------|-----------|
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
### Поиск / удаление
| Функция | Назначение |
|---------|-----------|
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
### Утилиты
| Функция | Назначение |
|---------|-----------|
| `queue_entry_count(q)` | Текущее количество элементов. |
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |

48
lib/mem_doc.md

@ -0,0 +1,48 @@
# mem (Memory Wrappers с отслеживанием утечек)
## 1. Назначение
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
Функции потокобезопасны (мьютекс/критическая секция).
## 2. Как пользоваться
```c
#include "mem.h"
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
void* buf = u_malloc(data_size);
char* str = u_strdup("hello");
buf = u_realloc(buf, new_size);
u_free(buf);
u_free(str);
// В конце main (перед return) — отчёт о незакрытых блоках:
u_report_unfreed_blocks();
// Текущее число живых аллокаций (для диагностики):
size_t count = u_get_allocated_count();
```
**Важно:**
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |

57
lib/memory_pool_doc.md

@ -0,0 +1,57 @@
# memory_pool (Object Pool Allocator)
## 1. Назначение
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
Встроенная защита:
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
## 2. Как пользоваться
```c
#include "memory_pool.h"
// 1. Создать пул (обычно в init-функции инстанса):
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
memory_pool_free(pkt_pool, dgram);
// 4. Получить статистику:
size_t allocs, reuse;
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
// 6. Уничтожить пул (при shutdown):
memory_pool_destroy(pkt_pool);
// реально освобождает все кэшированные блоки через u_free() и сам пул.
```
**Важно:**
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |

81
lib/platform_compat_doc.md

@ -0,0 +1,81 @@
# platform_compat — Cross-platform compatibility layer
## 1. Назначение
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
## 2. Как пользоваться
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
### Байтовый порядок
```c
uint16_t v16 = htobe16(x); // host → big-endian
uint32_t v32 = be32toh(x); // big-endian → host
uint64_t v64 = be64toh(x); // big-endian → host
// На Linux использует <endian.h>, на Windows — _byteswap_*
```
### Криптостойкий random
```c
uint8_t salt[8];
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
// Linux: /dev/urandom, Windows: BCryptGenRandom
```
### Сетевые интерфейсы
```c
// Определить интерфейс маршрута по умолчанию
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
// Получить IPv4 адрес интерфейса по индексу
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
// Получить IPv6 адрес (постоянный или временный)
uint8_t ipv6[16];
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
// ipv6 содержит 16 байт адреса
}
```
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
### Время
```c
struct timeval tv;
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
```
### Прочее
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
- `memmem` — на Windows inline-реализация `compat_memmem`
- `pipe` — на Windows `_pipe` с `_O_BINARY`
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
- `ssize_t` — определён если отсутствует
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
- `utun_mkdir` — кроссплатформенный `mkdir`
### Ограничения
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
## 3. API
### Байтовый порядок
| Макрос | Назначение |
|--------|------------|
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
### Основные функции
| Функция | Назначение |
|---------|------------|
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
### Макросы-заменители (условные)
| Макрос | Назначение |
|--------|------------|
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
| `utun_mkdir(path, mode)` | mkdir |

97
lib/radix_doc.md

@ -0,0 +1,97 @@
# Radix Tree (Patricia Trie) для IP-маршрутизации
## 1. Назначение
Бинарное radix-дерево (Patricia trie) для хранения маршрутов и поиска Longest Prefix Match (LPM). Производное от BSD `net/radix.c`. Используется в `route_lib.c` / `route6_lib.c` для таблиц маршрутизации IPv4/IPv6.
Каждый маршрут — пара `(key, mask)`. Ключи — sockaddr-подобные структуры: первый байт хранит длину всей структуры, остальное — IP-адрес. Максимальная длина ключа — 32 байта (`RADIX_MAX_KEY_LEN`).
Потокобезопасность: замки — заглушки (no-op). Подразумевается однопоточное использование с `u_async`.
## 2. Как пользоваться
```c
#include "../lib/radix.h"
// --- Инициализация ---
struct radix_node_head *rnh = NULL;
rn_inithead((void**)&rnh, 1); // off=1 пропускает байт длины в сравнениях
// --- Структура данных маршрута ---
struct route_data {
struct radix_node rn_nodes[2]; // ПАМЯТЬ ДЛЯ ДЕРЕВА — первые 2 поля!
uint8_t key[32];
uint8_t mask[32];
// ... пользовательские поля ...
int metric;
};
// --- Добавление маршрута ---
struct route_data *rd = u_calloc(sizeof(*rd), 1);
rd->key[0] = 17; // длина (1 + 16 байт IPv6)
memcpy(rd->key + 1, addr, 16);
rd->mask[0] = 17;
make_mask(rd->mask + 1, plen);
rn_addroute(rd->key, rd->mask, &rnh->rh, rd->rn_nodes);
// --- Longest Prefix Match ---
uint8_t search_key[17];
search_key[0] = 17;
memcpy(search_key + 1, dst_addr, 16);
struct radix_node *leaf = rn_match(search_key, &rnh->rh);
if (leaf && !(leaf->rn_flags & RNF_ROOT)) {
// leaf = &rd->rn_nodes[0] — восстанавливаем указатель на данные:
struct route_data *found = (struct route_data *)(
(char *)leaf - offsetof(struct route_data, rn_nodes));
}
// --- Точный поиск (key + mask) ---
struct radix_node *node = rn_lookup(key, mask, &rnh->rh);
// --- Удаление ---
rn_delete(rd->key, rd->mask, &rnh->rh);
// --- Обход всех маршрутов ---
static int walk_cb(struct radix_node *rn, void *arg) {
if (rn->rn_flags & RNF_ROOT) return 0;
struct route_data *rd = (struct route_data *)((char *)rn - offsetof(...));
// ... обработать rd ...
return 0; // 0 = продолжить, !=0 = прервать
}
rn_walktree(&rnh->rh, walk_cb, NULL);
// --- Обход с нижней границы (mask-фильтр) ---
uint8_t base_key[...], base_mask[...];
rn_walktree_from(&rnh->rh, base_key, base_mask, walk_cb, NULL);
// --- Завершение ---
rn_detachhead((void**)&rnh);
```
**Ключевые нюансы:**
- `rn_nodes[2]` **должны быть первыми полями** в структуре данных — дерево использует их для хранения.
- `rn_addroute` возвращает `&rd->rn_nodes[0]` при успехе. Для извлечения структуры используйте `offsetof()`.
- `rn_match` возвращает самый специфичный (longest prefix) лист либо `NULL`. Если `RNF_ROOT` — совпадений нет.
- Максимальный размер ключа — 32 байта (включая байт длины).
- Все динамические выделения через `R_Malloc`/`R_Zalloc`/`R_Free` (макросы на `u_malloc`/`u_free`).
- Потокобезопасность не реализована — замки (макросы `RADIX_NODE_HEAD_LOCK` и т.д.) пустые.
## 3. API
| Функция | Назначение |
|---------|-----------|
| `rn_inithead(void **head, int off)` | Выделяет и инициализирует `radix_node_head`. `off` — смещение (байт длины sockaddr). |
| `rn_detachhead(void **head)` | Освобождает дерево и все узлы (через `rn_walktree` + `rn_delete`). |
| `rn_match(key, head)` | Longest Prefix Match. Возвращает лист-победитель или NULL. |
| `rn_lookup(key, mask, head)` | Точный поиск (key + mask). Возвращает узел или NULL. |
| `rn_addroute(key, mask, head, nodes[2])` | Добавляет маршрут. `nodes` — память из структуры данных. |
| `rn_delete(key, mask, head)` | Удаляет маршрут. Возвращает удалённый узел для освобождения. |
| `rn_walktree(head, callback, arg)` | Обход всего дерева (in-order), вызывает `callback(rn, arg)` для каждого листа. |
| `rn_walktree_from(head, base_key, base_mask, callback, arg)` | Обход поддерева, ограниченного `base_mask`. |
| `rn_refines(mask1, mask2)` | Проверяет, является ли `mask1` более специфичной, чем `mask2`. |
| `rn_nextprefix(rn)` | Переход к следующему dupedkey-узлу с тем же ключом. |
**Структуры:**
- `struct radix_node_head` — корень дерева с таблицей функций (`rnh_matchaddr`, `rnh_addaddr`, ...).
- `struct radix_node` — узел: внутренний (bit-offset, left/right) или лист (key, mask, dupedkey).
- `struct radix_mask` — аннотация маски для нетривиальных (non-normal) масок в поддеревьях.

117
lib/serialize_doc.md

@ -0,0 +1,117 @@
# Binary Serialization Library
## 1. Назначение
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
Что сериализуется:
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
Потокобезопасность: нет, однопоточное использование.
## 2. Как пользоваться
```c
#include "../lib/serialize.h"
// --- 1. Определяем структуру ---
typedef struct Node {
uint32_t id;
struct Node *next;
} Node;
typedef struct {
uint8_t version;
uint8_t name_len;
char *name; // ASCIIZ (elem_size=1)
uint16_t addrs_cnt;
uint8_t *addrs; // массив байт (elem_size=1)
Node *list; // linked list
} Message;
// --- 2. Описываем схему ---
static const struct SerializeField msg_fields[] = {
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
};
static const struct SerializeSchema msg_schema = {
.field_count = 4,
.struct_size = sizeof(Message),
.max_size = 4096, // 0 = без лимита
.header_len = 4, // байты заголовка (напр. версия протокола)
.fields = msg_fields
};
// --- 3. Сериализация ---
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
msg.addrs_cnt = 2;
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
uint8_t *buf = NULL;
size_t len;
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
goto fail;
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
// --- 4. Десериализация (buf без заголовка!) ---
Message *restored = NULL;
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
goto fail;
// restored->name == "test", restored->name_len == 4
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
// restored->list == NULL (список пуст — не падает)
// --- 5. Очистка ---
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
u_free(buf);
```
**Ключевые нюансы:**
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
## 3. API
| Функция | Назначение |
|---------|-----------|
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
**Типы полей (`SERIALIZE_TYPE_*`):**
| Тип | `data` | `elem_size` | `len_offset` |
|-----|--------|-------------|--------------|
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
**Коды возврата:**
- `SERIALIZE_ERR_OK (0)` — успех.
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.

32
lib/sha256_doc.md

@ -0,0 +1,32 @@
# SHA-256 (sha256.h)
## 1. Назначение
Чистая C-реализация SHA-256 по FIPS 180-2, без внешних зависимостей. Используется как fallback при отключенной OpenSSL (`USE_OPENSSL` не определён) — в `secure_channel.c` для деривации session key, stream nonce, хеширования pubkey для обфускации и в `db_sync.c` для контрольных сумм БД.
## 2. Как пользоваться
Трёхфазный API: init → update (можно много раз) → final.
```c
SC_SHA256_CTX ctx;
uint8_t hash[SC_SHA256_BLOCK_SIZE]; // 32 байта
sc_sha256_init(&ctx);
sc_sha256_update(&ctx, data1, len1);
sc_sha256_update(&ctx, data2, len2); // можно добавлять данные порциями
sc_sha256_final(&ctx, hash); // hash — 32 байта, после этого ctx непригоден
```
После `sc_sha256_final` контекст нельзя переиспользовать без повторного `sc_sha256_init`.
## 3. API
### Структура
- **`SC_SHA256_CTX`** — контекст хеширования: буфер 64 байта, счётчик данных, счётчик бит, 8 слов состояния.
### Функции
- **`sc_sha256_init(ctx)`** — инициализирует контекст начальными значениями IV (первые 32 бита дробных частей квадратных корней первых 8 простых чисел).
- **`sc_sha256_update(ctx, data, len)`** — добавляет данные в хеш; обрабатывает блоки по 64 байта через `sc_sha256_transform` (внутренняя, раунды SHA-256).
- **`sc_sha256_final(ctx, hash)`** — завершает хеширование: дополняет данные до кратных 512 бит (padding с 0x80 и длиной сообщения), выполняет трансформацию, выдаёт 32-байтовый дайджест в big-endian порядке.
### Константа
- **`SC_SHA256_BLOCK_SIZE`** (32) — размер выходного дайджеста в байтах.

99
lib/socket_compat_doc.md

@ -0,0 +1,99 @@
# socket_compat — Cross-platform socket abstraction
## 1. Назначение
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
## 2. Как пользоваться
### Инициализация (однократно при старте)
```c
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
// ... работа с сокетами ...
socket_platform_cleanup(); // при завершении
```
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
### Создание и настройка сокета
```c
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
if (sock == SOCKET_INVALID) { /* ошибка */ }
socket_set_nonblocking(sock); // обязательно для u_async
socket_set_reuseaddr(sock, 1);
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
socket_set_mark(sock, 42); // только Linux, SO_MARK
```
### Отправка и приём
```c
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
```
### Обработка ошибок
```c
if (sent == SOCKET_ERROR_CODE) {
int err = socket_get_error();
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
DEBUG_ERROR(..., "%s", socket_strerror(err));
}
```
### Закрытие
```c
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
```
### Ключевые нюансы
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
## 3. API
### Типы и константы
| Имя | Назначение |
|-----|------------|
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
### Инициализация
| Функция | Назначение |
|---------|------------|
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
### Создание и настройка
| Функция | Назначение |
|---------|------------|
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
### I/O
| Функция | Назначение |
|---------|------------|
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
### Утилиты
| Функция | Назначение |
|---------|------------|
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |

39
lib/swm_min_doc.md

@ -0,0 +1,39 @@
# Sliding Window Minimum (swm_min.h)
## 1. Назначение
Структура данных для отслеживания минимума в скользящем окне фиксированного размера. Добавление значения и запрос минимума — O(1) амортизированное время. Память — O(window_size). Предназначена для отслеживания минимального RTT в ETCP/BBR (в кодовой базе зарезервирована, ожидает интеграции).
## 2. Как пользоваться
Создать окно заданного размера, добавлять значения по мере поступления, запрашивать текущий минимум.
```c
SlidingWindowMin *swm = swm_create(10); // окно из 10 последних значений
for (int i = 0; i < 100; i++) {
swm_add(swm, get_new_rtt());
int min_rtt = swm_get_min(swm); // минимум последних ≤10 значений
}
swm_destroy(swm);
```
**Нюансы:**
- `swm_get_min` на пустом окне возвращает `INT_MAX`
- `window_size` должен быть > 0, иначе `swm_create` вернёт NULL
- Значения — `int`, отрицательные допустимы
## 3. API
### Структура
- **`SlidingWindowMin`** — непрозрачная структура (opaque pointer). Содержит кольцевой буфер значений, deque индексов, позицию и размер окна.
### Функции
- **`swm_create(window_size)`** — выделяет SlidingWindowMin, инициализирует пустой deque и кольцевой буфер размера `window_size`. Возвращает NULL при ошибке выделения памяти.
- **`swm_destroy(swm)`** — освобождает все ресурсы структуры.
- **`swm_add(swm, val)`** — добавляет значение в окно. Автоматически вытесняет индексы, вышедшие за границу окна, и удаляет из deque значения, которые больше текущего (поддерживает монотонный минимум).
- **`swm_get_min(swm)`** — возвращает минимум текущего окна за O(1). Для пустого окна — `INT_MAX`.
### Алгоритм
Использует монотонный deque: в голове deque всегда индекс минимального элемента текущего окна. При добавлении из хвоста удаляются элементы ≥ нового — они никогда не станут минимумом, пока новый элемент в окне.
Тест: `tests/test_swm_min.c`

155
lib/tcp_io_doc.md

@ -0,0 +1,155 @@
# tcp_io — Управление TCP-соединением на uasync + ll_queue
## 1. Назначение
Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`.
Одна структура `struct tcp_conn` = одно TCP-соединение.
Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах.
**Ключевые свойства:**
- Чтение, запись, ошибки и connect — через `uasync` (epoll/kqueue/poll).
- Два независимых `ll_queue`: `read_queue` (входящие данные) и `write_queue` (исходящие).
- Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы.
- Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди.
- Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке.
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены.
## 2. Как пользоваться
### Создание соединения
```c
struct tcp_conn* tc = tcp_conn_create(
ua, sock,
1500, // entry_data_size — размер буфера для одного recv
8192, // write_chunk_size — размер буфера для одной send-операции
32, // read_high_water — порог приостановки EPOLLIN
8, // read_low_water — порог возобновления EPOLLIN
0, // rcvbuf_size — размер буфера сокета (0 = не менять)
on_fin_cb, on_error_cb, arg);
```
После создания нужно установить callback на read_queue и включить отложенную обработку:
```c
queue_set_callback(tc->read_queue, on_read_cb, my_conn);
queue_set_waiter_defer(tc->read_queue, 1);
```
### Чтение данных (в on_read_cb)
```c
static void on_read_cb(struct ll_queue* q, void* arg) {
struct my_conn* c = (struct my_conn*)arg;
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// обработать e->dgram (e->len байт)
queue_entry_free(e);
queue_resume_callback(q); // обязательно!
}
```
### Запись данных
Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов):
```c
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
uint8_t* buf = memory_pool_alloc(tc->data_pool);
memcpy(buf, data, len);
e->dgram = buf; e->len = (uint16_t)len;
queue_data_put(tc->write_queue, e);
```
При заполнении (порог 32) нужно ждать через `queue_waiter_wait`.
### Graceful shutdown
```c
tcp_conn_push_fin(tc); // shutdown(SHUT_WR) после отправки всех данных → on_fin_sent
tcp_conn_push_close(tc); // close сокета после отправки всех данных → on_closed
```
Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.).
Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1).
### Уничтожение
```c
void tcp_conn_destroy(struct tcp_conn* tc);
```
Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`.
### Обработка ошибок
`on_error(tc, err, arg)` вызывается после закрытия сокета. **Реализация on_error обязана вызвать tcp_conn_destroy.** После возврата из on_error `tc` недействителен.
```c
static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
struct my_conn* c = (struct my_conn*)arg;
// почистить свои ресурсы
tcp_conn_destroy(tc); // tc после этого использовать нельзя
socket_close_wrapper(other_sock);
u_free(c);
}
```
### on_flushed (одноразовый)
```c
tcp_conn_set_flushed(tc, my_flushed_cb);
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится
```
### Приостановка чтения
```c
tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter
```
### Ограничения и многопоточность
- **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока.
- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены.
- **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди.
- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги).
## 3. API
### Структуры
| Поле | Назначение |
|------|-----------|
| `struct tcp_conn` | Всё состояние TCP-соединения: сокет, очереди, пулы, коллбэки, флаги |
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке |
| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен |
### Коллбэки (поля struct tcp_conn)
| Поле | Когда вызывается |
|------|-----------------|
| `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа |
| `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом |
| `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен |
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
| `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных |
### Graceful shutdown
| Функция | Описание |
|---------|----------|
| `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 |
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 |
### Прочее
| Функция | Описание |
|---------|----------|
| `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue |
| `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога |

107
lib/timeout_heap_doc.md

@ -0,0 +1,107 @@
# timeout_heap — Min-heap для управления таймерами
## 1. Назначение
timeout_heap — легковесная реализация min-heap (двоичная куча с минимумом в корне) для хранения таймеров в порядке возрастания времени срабатывания. Используется модулем `u_async` как внутренняя структура `UASYNC::timeout_heap`.
Ключевая особенность — **ленивое удаление (lazy deletion)**: при отмене таймера элемент не удаляется из кучи, а помечается флагом `deleted = 1`, его `expiration` обнуляется, и он всплывает в корень (bubble-up). Реальное удаление происходит при `peek`/`pop` — корень с `deleted == 1` выкидывается, а данные освобождаются через пользовательский `free_callback`. Это даёт O(log n) на отмену без полного удаления из середины кучи.
## 2. Как пользоваться
Модуль используется только внутри `u_async.c`, пользователь напрямую с ним не работает. Приводится для понимания внутреннего устройства.
### Создание и уничтожение
```c
TimeoutHeap* h = timeout_heap_create(16); // начальная ёмкость 16
timeout_heap_set_free_callback(h, ua, timeout_node_free_callback);
// ... использование ...
timeout_heap_destroy(h); // уничтожает все элементы через free_callback
```
### Вставка и извлечение
```c
size_t index;
timeout_heap_push(h, expiration_ms, my_data, &index);
// index обновляется при перемещениях элемента в куче
TimeoutEntry entry;
if (timeout_heap_peek(h, &entry) == 0) {
// entry.expiration — ближайшее время срабатывания
// entry.data — пользовательские данные
}
if (timeout_heap_pop(h, &entry) == 0) {
// элемент удалён из кучи, данные нужно освободить вручную или через free_callback
}
```
### Отмена
```c
// O(n) — линейный поиск по expiration+data:
timeout_heap_cancel(h, expiration, data);
// O(log n) — удаление по индексу (для внешнего отслеживания позиции):
timeout_heap_cancel_at(h, index, data);
```
### Ленивое удаление: как это работает
1. `cancel` / `cancel_at` ставит `deleted = 1`, `expiration = 0`, вызывает `bubble_up` — элемент всплывает в корень.
2. При следующем `peek`/`pop`: если корень `deleted`, он удаляется из кучи (`remove_root`), данные освобождаются через `free_callback`. Повторяется, пока корень не окажется активным.
3. O(log n) как на cancel, так и на последующее удаление.
## 3. API
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `timeout_heap_create(capacity)` | Создать кучу с начальной ёмкостью. Расширяется автоматически при заполнении. |
| `timeout_heap_destroy(h)` | Уничтожить кучу. Все данные (включая помеченные deleted) освобождаются через `free_callback` или `free()`. |
| `timeout_heap_set_free_callback(h, user_data, cb)` | Установить callback для освобождения данных при удалении deleted-узлов. Если NULL — данные не освобождаются. |
### Основные операции
| Функция | Описание |
|---------|----------|
| `timeout_heap_push(h, exp, data, &index)` | Вставить элемент с временем срабатывания `exp`. `index_ptr` (опционально) отслеживает позицию элемента в куче — обновляется при всех перемещениях. O(log n). |
| `timeout_heap_peek(h, &entry)` | Посмотреть ближайший **неудалённый** элемент без удаления. Попутно вычищает deleted-корни через `free_callback`. O(log n) в худшем случае. |
| `timeout_heap_pop(h, &entry)` | Извлечь ближайший неудалённый элемент. Аналогично чистит deleted. O(log n). |
### Отмена
| Функция | Описание |
|---------|----------|
| `timeout_heap_cancel(h, exp, data)` | Отменить по совпадению `expiration + data`. Линейный поиск — O(n). Для массового использования предпочитать `cancel_at`. |
| `timeout_heap_cancel_at(h, index, data)` | Отменить по индексу (с проверкой data). O(log n) — ленивое удаление с bubble-up. |
### Статистика
| Функция | Описание |
|---------|----------|
| `timeout_heap_get_size(h)` | Текущий размер кучи (включая deleted-элементы). |
| `timeout_heap_get_freed_count(h)` | Количество узлов, освобождённых через `free_callback` (не реализовано в текущей версии — возвращает 0). |
### Структуры
```c
typedef struct {
TimeoutTime expiration; // время срабатывания (ключ сортировки)
void *data; // пользовательские данные
size_t *index_ptr; // указатель на внешнюю переменную с индексом (обновляется при перемещениях)
int deleted; // 0 = активен, 1 = помечен на удаление
} TimeoutEntry;
struct TimeoutHeap {
TimeoutEntry *heap; // динамический массив
size_t size; // текущее количество элементов
size_t capacity; // выделенная ёмкость
void* user_data; // аргумент для free_callback
void (*free_callback)(void*, void*); // освобождение данных deleted-узлов
};
```
Индексация в коде: внешний API использует 0-based индексы в массиве `heap[]`. Внутренние операции (`bubble_up`, `heapify_down`) оперируют 1-based индексами через макросы `PARENT(i)`, `LEFT_CHILD(i)`, `RIGHT_CHILD(i)` для упрощения арифметики кучи.

174
lib/u_async_doc.md

@ -0,0 +1,174 @@
# u_async — Центральный цикл событий (event loop)
## 1. Назначение
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр `struct UASYNC` и запускает `uasync_mainloop()` / `uasync_poll()`. Модуль объединяет:
- **Сокеты** — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
- **Таймеры** — однократные таймеры с точностью 0.1 мс, реализованные на базе `timeout_heap` (min-heap) и `memory_pool`.
- **Немедленное выполнение** — FIFO-очередь `uasync_call_soon` для отложенного запуска callback в ближайшей итерации цикла.
- **Межпоточную связь** — `uasync_post` позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
## 2. Как пользоваться
### Правила
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
- **Закрытие из callback.** Можно вызывать `uasync_destroy` или `uasync_remove_socket` прямо из callback сокета/таймера. Модуль использует локальные копии указателей перед вызовом callback и генерационные счётчики (gen) для защиты от stale epoll-событий.
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
### Типовой сценарий
```c
// 1. Создать uasync
struct UASYNC* ua = uasync_create();
if (!ua) { /* ошибка */ }
// 2. Зарегистрировать сокет
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
// или для socket_t:
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, my_ctx);
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
// 4. Отменить таймер (обязательно обнулить дескриптор!)
uasync_cancel_timeout(ua, t_id);
t_id = NULL;
// 5. Отложенное выполнение в следующей итерации
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
// 6. Динамически включить/отключить мониторинг записи на сокете
uasync_set_socket_write(ua, sock_id, 1); // включить
uasync_set_socket_write(ua, sock_id, 0); // отключить
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
uasync_mainloop(ua);
// 8. Или один шаг с таймаутом (timebase, -1 = бесконечно)
uasync_poll(ua, 500); // ждать до 50 мс или первого события
// 9. Завершение
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
```
### Таймеры: правильный паттерн
```c
void* reconnect_timer = NULL; // дескриптор всегда обнуляем
// При запуске:
if (!reconnect_timer) {
reconnect_timer = uasync_set_timeout(ua, 5000, ctx, reconnect_cb, "reconnect");
}
// В callback или при отмене:
void reconnect_cb(void* arg) {
reconnect_timer = NULL; // сработал — обнулили
// ...
}
// При ручной отмене:
if (reconnect_timer) {
uasync_cancel_timeout(ua, reconnect_timer);
reconnect_timer = NULL;
}
```
### Межпоточное взаимодействие (uasync_post)
```c
// Из другого потока:
void notify_main(void* arg) {
UTUN_INSTANCE* u = arg;
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
}
void thread_func(UTUN_INSTANCE* u) {
// …
uasync_post(u->ua, notify_main, u);
}
// Для гарантии видимости памяти из другого потока перед uasync_post:
uasync_memsync(u->ua);
```
### Получение времени
```c
uint64_t now_tb = get_time_tb(); // timebase 0.1 мс (монотонные часы)
uint64_t now_us = get_time_us(); // микросекунды (для burst-измерений)
```
## 3. API
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `uasync_create()` | Создать экземпляр: аллоцирует структуру, `socket_array`, `timeout_heap`, `memory_pool`, epoll/poll, wakeup pipe/сокет. |
| `uasync_destroy(ua, close_fds)` | Уничтожить экземпляр. При `close_fds=1` закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
| `uasync_stop(ua)` | Установить флаг `stop = 1` — на следующей итерации `uasync_mainloop` выйдет. |
| `uasync_mainloop(ua)` | Бесконечный цикл `while(!stop) uasync_poll(ua, -1)`. |
| `uasync_poll(ua, timeout_tb)` | Одна итерация: обработать сокеты + таймеры. `timeout_tb` = максимальное ожидание в timebase; `-1` = ждать следующего таймера (или бесконечно, если нет таймеров). |
### Таймеры (timebase = 0.1 мс)
| Функция | Описание |
|---------|----------|
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Запланировать однократный таймер. Возвращает дескриптор `void*` для отмены. `name` — до 15 символов, используется в логах. |
| `uasync_cancel_timeout(ua, t_id)` | Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
### Немедленное выполнение (FIFO)
| Функция | Описание |
|---------|----------|
| `uasync_call_soon(ua, arg, cb)` | Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же `timeout_pool`. |
| `uasync_call_soon_cancel(ua, t_id)` | Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
### Сокеты
| Функция | Описание |
|---------|----------|
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg)` | Добавить fd (pipe, file). `r_cb/w_cb/e_cb` могут быть NULL. Возвращает дескриптор `void*`. |
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg)` | Добавить `socket_t` (кросс-платформенный сокет). |
| `uasync_remove_socket(ua, s_id)` | Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти дескриптор сокета по fd (возвращает актуальный указатель даже после realloc). |
### Межпоточное взаимодействие
| Функция | Описание |
|---------|----------|
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Запланировать callback в главном потоке. Выделяет `posted_task`, добавляет в связный список под мьютексом, будит главный поток через `uasync_wakeup`. |
| `uasync_memsync(ua)` | **Потокобезопасно.** Барьер памяти (lock/unlock `posted_lock`) для гарантии видимости данных, записанных из другого потока перед `uasync_post`. |
| `uasync_wakeup(ua)` | Разбудить `poll`/`epoll_wait` записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
| `uasync_get_wakeup_fd(ua)` | Получить write-fd wakeup pipe для использования в `signalfd` или кастомных механизмах. |
### Время
| Функция | Описание |
|---------|----------|
| `get_time_tb()` | Монотонное время в timebase (0.1 мс). `clock_gettime(CLOCK_MONOTONIC)` на POSIX, `QueryPerformanceCounter` на Windows. |
| `get_time_us()` | Монотонное время в микросекундах (для burst-измерений производительности). |
### Диагностика
| Функция | Описание |
|---------|----------|
| `uasync_get_stats(ua, ...)` | Получить счётчики аллокаций/освобождений таймеров и сокетов. |
| `uasync_print_resources(ua, prefix)` | Вывести в лог все активные таймеры (имя, оставшееся время) и сокеты. Вызывается также в `uasync_destroy` перед очисткой. |
### Внутренние структуры
- **`struct timeout_node`** — узел таймера: name, arg, callback, expiration_ms, heap_index. Используется и для таймеров в heap, и для FIFO-очереди immediate (через поле `next`). Выделяется из `timeout_pool`.
- **`struct socket_node`** — узел сокета: fd/sock, тип (FD/SOCK), колбэки, user_data, флаги active/enable_read/enable_write, gen (защита от stale epoll-событий после переиспользования fd).
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.

98
src/config_parser_doc.md

@ -0,0 +1,98 @@
# Config Parser (config_parser.c/h)
## 1. Назначение
INI-style парсер конфигурационного файла uTun. Читает текстовый конфиг, разбирает его на структуры и возвращает заполненный `struct utun_config` — корневое представление всех настроек.
Поддерживает два режима входа:
- `parse_config(filename)` — чтение из файла
- `parse_config_from_buf(buf, len, filename)` — чтение из буфера (для chatgui/in-memory использования)
Формат: стандартные `.ini`-секции `[section]`, строки `key=value`, комментарии `#`. Именованные секции через двоеточие: `[server:main]`, `[client:peer1]`, `[network:mynet]`.
## 2. Как пользоваться
```c
struct utun_config *cfg = parse_config("/etc/utun/config.ini");
if (!cfg) { /* ошибка — смотри логи DEBUG_CATEGORY_CONFIG */ }
// Работа с конфигом...
const char *tun_name = cfg->global.tun_ifname;
struct CFG_SERVER *srv = cfg->servers;
while (srv) {
ip_str_t addr = sockaddr_storage_to_str(&srv->ip);
srv = srv->next;
}
// Освобождение
free_config(cfg);
```
**Нюансы:**
- `parse_config` возвращает `NULL` при ошибках открытия/парсинга — проверяй возврат.
- Все строковые поля обрезаются по размерам буфера, переполнение логируется.
- Ключи клиента `link` требуют, чтобы сервер (`[server:имя]`) был объявлен выше по файлу. Иначе ошибка.
- `tun_enabled` по умолчанию `1`; если задан `tun_ifname` или `tun_ip` — генерируется имя `tun0` при отсутствии. Если нет ни одного TUN-параметра — считается выключенным (режим без root).
- IPv6-адреса в `[server:...] addr` поддерживают префиксы `temporary_ipv6:` и `permanent_ipv6:`.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|------------|
| `struct utun_config` | Корневой контейнер: `global` + связные списки `servers`, `clients`, `route_subnets`, `my_subnets`, `networks` |
| `struct global_config` | Глобальные настройки (ключи, TUN, таймауты, firewall, NAT, SOCKS/HTTP-прокси, NTP, debug, allowed_keys...) |
| `struct CFG_SERVER` | Локальный серверный линк: `addr` (ip:port), `netif_index`, `so_mark`, `fib`, `type`, `transport` (udp/tcp), `mtu`, `only_local` |
| `struct CFG_CLIENT` | Удалённый пир: `peer_public_key_hex`, `keepalive`, связный список `links` |
| `struct CFG_CLIENT_LINK` | Привязка клиента к локальному серверу: `local_srv` (указатель на `CFG_SERVER`), `remote_addr` |
| `struct CFG_ROUTE_ENTRY` | Маршрут: `IP` + `netmask` |
| `struct CFG_NETWORK` | Именованная сеть: `id` (56-bit), `pubkey_hex`, `signing_key_hex` |
| `struct CFG_FIREWALL_RULE` | Правило фаервола: `ip` (host order), `port`, `bypass` |
| `struct CFG_ALLOWED_KEY` | Разрешённый публичный ключ (32 байта binary) для контрольного сервера |
| `struct CFG_CONTROL_ALLOW` | Разрешённая подсеть для control-сервера |
| `struct CFG_NTP_SERVER` | NTP-сервер: имя хоста |
### Секции конфига
| Секция | Обработчик | Ключевые поля |
|--------|-----------|---------------|
| `[global]` | `parse_global()` | `my_node_name`, `my_private_key`, `my_public_key`, `my_node_id`, `tun_ifname`, `tun_ip`, `mtu`, `keepalive_*`, `bbr_max_cwnd`, `debug_level`, `log_file`, `db_path`, `db_sync_*`, `enable_timestamp`, `enable_colors`, `tun_test_mode` |
| `[server:name]` | `parse_server()` | `addr`, `so_mark`, `fib`, `netif`, `type` (public/nat/private/local), `transport` (udp/tcp), `mtu`, `only_local` |
| `[client:name]` | `parse_client()` | `link` (формат: `server_name:ip:port`), `peer_public_key`, `keepalive` |
| `[network:name]` | `parse_network()` | `id` (hex), `pubkey`, `signing_key` |
| `[routing]` | inline | `route_subnet`, `my_subnet` |
| `[firewall]` | `parse_firewall_rule()` | `allow` (IP:port или `all` для bypass) |
| `[control]` | `parse_control()` | `ip`, `port`, `allow` (подсеть) |
| `[allowed_keys]` | inline | `allow_all`, `key` (64 hex) |
| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (proto:ip:in_port:ext_port) |
| `[tcp_proxy_client]` | `parse_tcp_proxy_client()` | `enabled`, `tun_name`, `tun_ip`, `mtu`, `via_node`, `socks_*`, `http_proxy_*`, `forward` (port -> ip:port) |
| `[tcp_proxy_server]` | inline | `tcp_recv_buf` |
| `[msg_transport]` | `parse_msg_transport()` | `port` |
| `[debug]` | inline | `категория=уровень` (до 16 категорий) |
| `[ntp]` | inline | `enabled`, `server`, `interval` |
| `[gui]` | игнорируется | (зарезервировано) |
### Функции
| Функция | Назначение |
|---------|------------|
| `parse_config(filename)` | Открывает файл, парсит через `parse_config_internal`, возвращает `utun_config*`. `NULL` при ошибке. |
| `parse_config_from_buf(buf, len, filename)` | Парсит конфиг из буфера (in-memory). `filename` используется только для сообщений об ошибках. |
| `free_config(config)` | Рекурсивно освобождает все связные списки и сам `utun_config`. Безопасен для `NULL`. |
| `print_config(config)` | Выводит всё содержимое конфига через `DEBUG_INFO(DEBUG_CATEGORY_CONFIG, ...)`. Полезно для проверки после парсинга. |
| `update_config_keys(filename, priv_key, pub_key)` | **Устарела.** Дописывает ключи в конец файла. Используй `config_ensure_keys_and_node_id()` из `config_updater.h`. |
### Внутренние хелперы (static)
| Функция | Назначение |
|---------|------------|
| `trim(str)` | Обрезает пробелы слева и справа (in-place) |
| `parse_key_value(line, ...)` | Разбирает строку `key=value`, удаляет комментарии после `#` |
| `parse_ip_with_netmask(str, ip, netmask)` | Парсит `1.2.3.4/24` или `::1/64` в `struct IP` |
| `parse_sockaddr(addr, port, sockaddr)` | Преобразует адрес+порт в `sockaddr_storage` через `getaddrinfo` |
| `parse_address_and_port(str, sockaddr)` | Разбирает строку `ip:port` → вызов `parse_sockaddr` |
| `get_netif_index(ifname)` | Получает индекс сетевого интерфейса через `if_nametoindex` |
| `parse_firewall_rule(rule_str, global)` | Парсит `allow=IP[:port]` или `all` |
| `parse_control_allow(rule_str, global)` | Парсит `allow=IP[/CIDR]` |
| `hex_to_binary(hex_str, binary, len)` | Конвертит hex-строку в бинарный массив |

67
src/config_updater_doc.md

@ -0,0 +1,67 @@
# Config Updater (config_updater.c/h)
## 1. Назначение
Утилита автоматической инициализации и обновления ключей в конфигурационном файле uTun.
Основная задача — гарантировать, что в конфиге всегда присутствуют корректные и согласованные:
- `my_private_key` (X25519, 64 hex символа)
- `my_public_key` (производный от private, 64 hex)
- `my_node_id` (SHA256-идентификатор узла, 16 hex символов, вычисляется из private key)
Если чего-то нет или значение неконсистентно — модуль генерирует недостающее через `secure_channel.h` и атомарно перезаписывает конфиг, сохраняя всё остальное содержимое файла нетронутым.
## 2. Как пользоваться
```c
#include "config_updater.h"
int ret = config_ensure_keys_and_node_id("/etc/utun/config.ini");
if (ret == 0) {
// Конфиг готов: ключи и node_id гарантированно на месте
} else {
// Ошибка — смотри DEBUG_*(DEBUG_CATEGORY_CONFIG)
}
```
**Типовой сценарий (первый запуск):**
1. Файла нет → создаётся минимальный `[global]` с `my_node_name`, сгенерированными ключами и `my_node_id`.
2. Файл есть, но нет `my_private_key` → генерируется новая X25519-пара, записываются все три поля.
3. Файл есть с `my_private_key`, но нет `my_public_key` или `my_node_id` → вычисляются из private key, дописываются.
4. Все поля есть, но `my_public_key` не соответствует `my_private_key` или `my_node_id` не совпадает → перезаписываются на корректные.
**Нюансы:**
- Вычисление pubkey: `sc_compute_public_key_from_private(priv_bin, pub_bin)`.
- Вычисление node_id: `sc_derive_node_id(priv_bin)`.
- Генерация новой пары ключей: `sc_generate_keypair(&mykeys)`.
- Файл читается целиком в память, модифицируется через `insert_or_replace_option`, затем пишется обратно. Это атомарно: либо файл полностью перезаписан, либо остался нетронутым (при ошибке).
- Редактирование in-place через `memmove`: строки не сдвигают остальной конфиг, только вставляются/заменяются нужные опции внутри секции `[global]`.
- Для нового файла создаётся минимальный `[global]` с именем узла из `my_node_name` (или `"utun"` если не задано).
## 3. API
### Функции
| Функция | Назначение |
|---------|------------|
| `config_ensure_keys_and_node_id(filename)` | **Основная функция.** Читает конфиг, проверяет/генерирует ключи и node_id, записывает обратно. Возвращает 0 при успехе, -1 при ошибке. |
| `bytes_to_hex(bytes, len, hex_str, hex_len)` | Вспомогательная: конвертация бинарного массива в hex-строку. Безопасна для нулевых указателей и маленьких буферов. |
### Внутренние функции (static)
| Функция | Назначение |
|---------|------------|
| `is_valid_priv_key(key)` | Проверяет, что строка — 64 hex-символа |
| `is_valid_pub_key(key)` | То же для публичного ключа |
| `is_valid_node_id(node_id)` | Проверяет `node_id != 0` |
| `write_mem_to_file(filename, buffer, size)` | Пишет буфер в файл (одной операцией) |
| `find_section_global(buf, buf_len)` | Ищет позицию после `[global]` в текстовом буфере |
| `find_option(buf, buf_len, option)` | Ищет строку `option=...` в буфере |
| `insert_or_replace_option(buf, buf_len, buf_capacity, option, value)` | Заменяет значение опции при наличии, либо вставляет новую строку после `[global]`. Работает с `memmove`/`memcpy`, перевыделяет буфер при необходимости. |
### Зависимости
- `secure_channel.h` — `sc_generate_keypair()`, `sc_compute_public_key_from_private()`, `sc_derive_node_id()`
- `config_parser.h` — `parse_config_from_buf()`, `free_config()`, структуры `utun_config`/`global_config`
- `lib/mem.h` — `u_malloc`, `u_realloc`, `u_free`
- `lib/debug_config.h` — `DEBUG_*`

145
src/conn_mgr_doc.md

@ -0,0 +1,145 @@
# conn_mgr — Менеджер установки соединений
## 1. Назначение
Трёхфазный асинхронный менеджер подключения к удалённым нодам. Отвечает за установку ETCP-соединения с выбором оптимального способа:
1. **DIRECT** — прямое INIT-рукопожатие со всеми известными IPv4-адресами цели (с проверкой NAT-совместимости)
2. **REVERSE** — если у нас прямой IP, а цель за NAT: отправляем наши адреса через BGP, цель подключается к нам
3. **INDIRECT** — обмениваемся через BGP списками кандидатов-посредников, выбираем общих по минимальной сумме RTT, трафик идёт через `etcp_router`
Также выполняет фоновые задачи: периодический ping всех BGP-нод, поддержание списка лучших посредников, idle-таймаут для неактивных соединений, локальное сканирование сети.
Типы соединений (итоговый результат):
- `CONN_TYPE_DIRECT` (1) — прямое соединение
- `CONN_TYPE_REVERSE` (2) — цель подключилась к нам
- `CONN_TYPE_INDIRECT` (3) — через посредника
## 2. Как пользоваться
### Инициализация
```c
struct CONN_MGR* mgr = conn_mgr_init(instance);
// Регистрирует обработчик в etcp_router для ETCP_RT_ID_CONN_MGR (0x11)
// Запускает фоновые таймеры: bg_ping, candidate_ping
```
### Подключение
```c
static void my_connect_cb(int result, uint64_t node_id, void* arg) {
if (result == CONN_MGR_OK) { /* подключено */ }
else if (result == CONN_MGR_ERR_TIMEOUT) { /* таймаут */ }
else if (result == CONN_MGR_ERR_UNREACHABLE) { /* недостижима */ }
}
conn_mgr_connect_node(mgr, target_node_id, idle_timeout_ms, my_connect_cb, arg);
```
### Отправка данных
```c
struct ll_entry* entry = queue_entry_new(0);
entry->dgram = data; entry->len = len;
conn_mgr_send(mgr, node_id, entry); // entry освобождается внутри
```
### Alien-ноды
Для нод, которые сами подключились к нам, используется `conn_mgr_add_alien_node()` — не запускает фазы подключения, сразу устанавливает `CONNECTED`.
### Кандидаты-посредники
Внешний код (обычно `route_connectivity`) вызывает `conn_mgr_update_best_candidates()` для заполнения списка лучших посредников (топ-3 по RTT). Список используется фазой INDIRECT.
### Нюансы
- Повторный `connect_node` для уже подключённой ноды вернёт `CONN_MGR_ERR_ALREADY_CONNECTED`
- Для ноды в состоянии CONNECTING новый вызов добавит ещё один callback в список ожидания
- Для INDIRECT-соединений `conn_mgr_send` использует round-robin по выбранным посредникам
- Пока существует активный ETCP-линк к ноде, немедленно возвращает CONNECTED
## 3. API
### Корневые структуры
| Структура | Описание |
|-----------|----------|
| `CONN_MGR` | Корневая структура менеджера. Хранит массив `entries[]`, топ-3 `best_candidates`, таймеры (`bg_ping_timer`, `candidate_ping_timer`), списки `reverse_pending`/`exchange_pending`, `next_request_id` |
| `CONN_MGR_ENTRY` | Запись о подключении к одной ноде: `state`, `conn_type`, `alien`, `idle_timeout_ms`, список коллбэков `cb_list`, массив `intermediaries[]`, состояния `local_scan_state`/`main_connect_state`, текущая `main.phase` |
| `CONN_MGR_CANDIDATE` | Пара `{node_id, rtt}` — запись о кандидате-посреднике (packed) |
| `cm_cb_node` | Узел связного списка коллбэков для асинхронного результата подключения |
| `cm_reverse_pending` | Запись об ожидании REVERSE-подключения (связный список) |
| `cm_exchange_pending` | Запись об ожидании INDIRECT-обмена, включает кешированный ответ и таймер probe-повторов |
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `conn_mgr_init(instance)` | Выделяет CONN_MGR, регистрирует `ETCP_RT_ID_CONN_MGR` в маршрутизаторе, запускает bg_ping и candidate_ping таймеры |
| `conn_mgr_destroy(mgr)` | Отменяет таймеры, освобождает все entries, exchange_pending, reverse_pending |
### Подключение / отключение
| Функция | Описание |
|---------|----------|
| `conn_mgr_connect_node(mgr, id, timeout, cb, arg)` | Запускает 3-фазное подключение. Если уже подключена — сразу callback с OK. Если в процессе — добавляет callback в список |
| `conn_mgr_disconnect_node(mgr, id)` | Шлёт DISCONNECT пакет, сбрасывает `conn_mgr_type` в `NODEINFO_Q`, удаляет entry |
| `conn_mgr_send(mgr, id, entry)` | Отправляет данные подключённой ноде через `etcp_router`; для INDIRECT — round-robin по посредникам; обновляет `last_traffic_tb` |
| `conn_mgr_set_idle_timeout(mgr, id, ms)` | Устанавливает/снимает idle-таймер для ноды. При неактивности дольше `ms` — автоотключение |
| `conn_mgr_get_status(mgr, id, &state, &type)` | Возвращает текущее состояние и тип соединения |
### Alien-ноды
| Функция | Описание |
|---------|----------|
| `conn_mgr_add_alien_node(mgr, data, len)` | Регистрирует чужую ноду (сама подключилась к нам): копирует `TOPO_NODE`, добавляет в группу, помечает `alien=1` |
### Кандидаты-посредники
| Функция | Описание |
|---------|----------|
| `conn_mgr_update_best_candidates(mgr, id, rtt)` | Вставляет/обновляет кандидата в отсортированном топ-3 по RTT. Худшие кандидаты вытесняются |
### Протокольные подкоманды (ETCP_RT_ID_CONN_MGR = 0x11)
| Константа | Описание |
|-----------|----------|
| `CONN_MGR_SUBCMD_DIRECT_REQ` (0x01) | Фаза 2: "я за NAT, подключись ко мне" — наши адреса |
| `CONN_MGR_SUBCMD_DIRECT_RESP` (0x02) | Ответ на DIRECT_REQ (зарезервирован) |
| `CONN_MGR_SUBCMD_INTERM_EXCHANGE_REQ` (0x03) | Фаза 3 инициатор: наш топ-4 кандидатов |
| `CONN_MGR_SUBCMD_INTERM_EXCHANGE_RESP` (0x04) | Фаза 3 ответ: свои + чужие кандидаты с RTT с обеих сторон |
| `CONN_MGR_SUBCMD_INTERM_SELECTED` (0x05) | Фаза 3 финал: выбранные посредники |
| `CONN_MGR_SUBCMD_DISCONNECT` (0x06) | Уведомление о разрыве |
### Пакеты протокола (все packed)
| Структура | Поля |
|-----------|------|
| `CONN_MGR_DIRECT_REQ` | `cmd, subcmd, request_id, addr_count` + массив адресов `{type, ip[4], port, sock_id}` |
| `CONN_MGR_DIRECT_RESP` | `cmd, subcmd, request_id, accepted` |
| `CONN_MGR_INTERM_EXCHANGE_REQ` | `cmd, subcmd, request_id, candidate_count, candidates[4]` |
| `CONN_MGR_INTERM_EXCHANGE_RESP` | `cmd, subcmd, request_id, my_count, your_count, my_candidates[4], your_candidates[4]` |
| `CONN_MGR_INTERM_SELECTED` | `cmd, subcmd, request_id, count, selected[3]` |
| `CONN_MGR_DISCONNECT` | `cmd, subcmd, node_id` |
### Коды возврата
| Код | Значение |
|-----|----------|
| `CONN_MGR_OK` (0) | Успех |
| `CONN_MGR_ERR_NOT_FOUND` (-1) | Нода не найдена в BGP |
| `CONN_MGR_ERR_NO_ADDRESSES` (-2) | Нет адресов для подключения |
| `CONN_MGR_ERR_TIMEOUT` (-3) | Таймаут |
| `CONN_MGR_ERR_UNREACHABLE` (-4) | Недостижима (все фазы) |
| `CONN_MGR_ERR_ALREADY_CONNECTED` (-6) | Уже подключены |
| `CONN_MGR_ERR_INTERNAL` (-7) | Внутренняя ошибка |
### Ключевые константы
| Константа | Значение | Смысл |
|-----------|----------|-------|
| `CONN_MGR_MAX_CANDIDATES` | 3 | Макс. число лучших посредников в списке |
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Макс. число выбранных посредников для одного соединения |
| `CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS` | 5000 | Таймаут фазы DIRECT |
| `CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS` | 15000 | Таймаут фазы REVERSE |
| `CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS` | 15000 | Таймаут фазы INDIRECT |
| `CONN_MGR_CANDIDATE_STALE_TB` | 300000 | Порог устаревания кандидата (~30 сек) |
| `CONN_MGR_CANDIDATE_PING_TB` | 20000 | Интервал ping кандидатов (~2 сек) |
| `CONN_MGR_BG_PING_INTERVAL_TB` | 1000 | Интервал фонового ping нод (~100 мс) |
| `CONN_MGR_BG_PING_CYCLE_MIN_TB` | 100000 | Мин. длительность цикла обхода всех нод (~10 сек) |
| `CONN_MGR_IDLE_CHECK_INTERVAL_TB` | 10000 | Интервал проверки idle (~1 сек) |
| `CONN_MGR_LOCAL_SCAN_ATTEMPTS` | 3 | Попыток локального сканирования |

49
src/control_server_doc.md

@ -0,0 +1,49 @@
# Control Server (control_server)
## 1. Назначение
TCP-сервер для мониторинга ETCP-соединений. Принимает подключения от GUI-клиента `etcpmon`, отдаёт по запросу метрики соединений/каналов/TUN/роутера, список узлов BGP-топологии, позволяет менять уровни отладки на лету. Уведомляет подписанных клиентов об изменениях узлов через `control_server_notify_node_change()`.
## 2. Как пользоваться
1. Вызвать `control_server_init()` — создаёт слушающий TCP-сокет на адресе из конфига `control_sock`, регистрирует accept-колбэк в uasync.
2. Периодически вызывать `control_server_process_updates()` для обработки накопившихся данных клиентов.
3. При изменении/удалении узла BGP-топологии вызывать `control_server_notify_node_change()` / `control_server_notify_node_removed()` — сервер сам разошлёт уведомления подписанным клиентам.
4. При остановке вызвать `control_server_shutdown()`.
**Нюансы:**
- Доступ по IP контролируется через `control_allow` в конфиге (белый список); без правил доступ запрещён.
- Клиенты отключаются по idle-таймауту (300 секунд без команд).
- Лог пишется в `control_server.log` (полные дампы RX/TX).
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `struct control_server` | Состояние сервера: `listen_fd`, связный список `clients`, uasync-контекст, счётчик клиентов. |
| `struct control_client` | Состояние клиента: `fd`, буфер приёма (`recv_buffer`), `output_queue` для асинхронной отправки, `selected_peer_id`, флаг подписки на узлы. |
### Функции
| Функция | Назначение |
|---------|-----------|
| `control_server_init(server, ua, instance, bind_addr, max_clients)` | Создать слушающий сокет, зарегистрировать в uasync. Возвращает 0 / -1. |
| `control_server_shutdown(server)` | Закрыть всех клиентов, освободить слушающий сокет, закрыть лог-файл. |
| `control_server_process_updates(server)` | Обработать накопившиеся данные всех клиентов (вызывать периодически). |
| `control_server_get_client_count(server)` | Вернуть количество подключённых клиентов. |
| `control_server_notify_node_change(server, node)` | Разослать подписанным клиентам сериализованную информацию об узле. |
| `control_server_notify_node_removed(server, node_id)` | Разослать подписанным клиентам уведомление об удалении узла. |
### Поддерживаемые команды ETCPMON
| Команда | Ответ | Описание |
|---------|-------|----------|
| `CMD_LIST_CONN` | `RSP_CONN_LIST` | Список активных ETCP-соединений. |
| `CMD_SELECT_CONN` | — | Выбрать соединение по `peer_node_id` для метрик. |
| `CMD_GET_METRICS` | `RSP_METRICS` | Полные метрики: ETCP, TUN, router, все каналы с BBR. |
| `CMD_LIST_SOCKETS` | `RSP_SOCKET_LIST` | Список локальных сокетов с NAT-типом и NAT-адресом. |
| `CMD_ACTION` | `RSP_ACTION_RESULT` | `"nat"` — запрос NAT-проверки; `"nodes"` — дамп BGP-узлов текстом. |
| `CMD_GET_DEBUG_CONFIG` | `RSP_DEBUG_CONFIG` | Текущие уровни отладки (глобальный + по категориям). |
| `CMD_SET_DEBUG_CONFIG` | — | Установить уровни отладки. |
| `CMD_SUBSCRIBE_NODES` | `RSP_NODE_INFO` (список) + `RSP_NODE_INFO_END` | Подписка на изменения узлов + текущий список. |
| `CMD_DISCONNECT` | — | Отключение клиента. |

35
src/crc32_doc.md

@ -0,0 +1,35 @@
# crc32
## 1. Назначение
Модуль вычисления контрольной суммы **CRC-32 (IEEE 802.3)** с полиномом `0xEDB88320`. Используется в подсистемах `secure_channel` и `stcp` для контроля целостности данных при передаче — перед шифрованием к данным добавляется CRC32, при расшифровке CRC32 пересчитывается и сверяется.
## 2. Как пользоваться
**Одноразовое вычисление (внутренне инициализирует таблицу при первом вызове):**
```c
// Исходное значение 0xFFFFFFFF, результат XOR-ится с ~0
uint32_t crc = crc32_calc(data, data_len);
```
**Инкрементальное вычисление (например, при получении данных фрагментами):**
```c
uint32_t crc = 0xFFFFFFFF;
while (chunk) {
crc = crc32_update(crc, chunk_data, chunk_len);
}
crc ^= 0xFFFFFFFF; // финальный XOR
```
**С произвольным начальным значением (без финального XOR):**
```c
uint32_t crc = crc32_calc_ex(data, len, initial_crc);
```
## 3. API
| Функция | Описание |
|---------|----------|
| `crc32_init()` | Принудительная инициализация lookup-таблицы (256 × uint32_t). Автоматически вызывается при первом использовании, ручной вызов не обязателен. |
| `crc32_calc(data, len)` | CRC32 «в один проход». Авто-инит, начальное значение `0xFFFFFFFF`, финальный `~crc`. При `data == NULL` или `len == 0` возвращает `0xFFFFFFFF`. |
| `crc32_ex(data, len, initial_crc)` | CRC32 с заданным начальным значением, **без** финального XOR. Подходит для инкрементального использования и цепочек вызовов. |
| `crc32_update(crc, data, len)` | Синоним `crc32_ex(data, len, crc)` — инкрементальное обновление. |

239
src/db_sync_doc.md

@ -0,0 +1,239 @@
# db_sync — Distributed content-addressed table with SQLite + P2P sync
## 1. Назначение
Децентрализованная реплицируемая таблица JSON-записей, синхронизируемая между всеми узлами сети через ETCP. Каждый узел хранит полную копию данных каждого инстанса. Модуль поддерживает несколько независимых инстансов (таблиц), каждый идентифицируется хешем `SHA256(name || id_be)[0:8]`.
**Ключевые свойства:**
- **Content-addressed**: целостность цепочки гарантируется `chain_hash` — каскадным SHA256, где `chain_hash[n] = SHA256(chain_hash[n-1] || id || timestamp || author || author_signature)`.
- **Ed25519-подписи**: каждая запись обязательно подписана автором (`Ed25519(timestamp || json_data)`). Записи без подписи или с неверной подписью отвергаются.
- **Multi-instance**: несколько независимых таблиц внутри одного процесса (напр. `("chats", 1)` и `("chats", 2)`).
- **Ordered**: записи упорядочены по `(timestamp, author_signature)`. Первичный ключ — та же пара, определяющая уникальность (дубликаты по тому же автору в ту же миллисекунду невозможны).
- **Append-only**: записи не редактируются и не удаляются явно, только TTL-очистка собственных неотправленных записей.
- **Sync protocol**: 8 типов сообщений (INIT_SYNC → INIT_RESP → REFINE → SEND_DATA → SYNC_DONE, плюс PUSH/ACK_PUSH/ERROR). Используется бинарный поиск расхождений по chain_hash.
- **Push**: новые записи немедленно рассылаются (PUSH) всем синхронизированным пирам.
## 2. Как пользоваться
### Типовой сценарий
```
1. В конфиге: db_sync_enabled = 1, db_sync_ttl = 86400
2. db_sync_init(inst) — вызывается автоматически при старте utun_instance
3. struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, "chats", 1);
→ Создаёт таблицу SQLite, верифицирует цепочку, запускает TTL-таймер.
→ Если есть активные ETCP-соединения — автоматически запускает синхронизацию.
4. Вставка записи:
uint64_t ts = db_sync_next_timestamp(si); // монотонно возрастающий timestamp (ms)
uint8_t sig[64];
Ed25519_sign(my_privkey, ts || my_node_id || json, sig);
db_sync_insert_signed(si, json_str, json_len, sig, 64, ts);
→ Подпись ОБЯЗАТЕЛЬНА (sig=NULL → ошибка). Запись автоматически push-ится всем synced пирам.
5. Чтение:
db_sync_select(si, offset, limit, my_callback, ctx);
6. db_sync_instance_remove(si) — деактивировать инстанс (таблица БД не удаляется).
7. db_sync_destroy(inst) — вызывается автоматически при завершении.
```
### Ключевые концепции
#### Multi-instance
Каждый экземпляр `DB_SYNC_INSTANCE` идентифицируется 64-битным хешем `hash = SHA256(name || htobe64(id))[0:8]`. Этот хеш используется как routing key в синхронизационных сообщениях. На приёмной стороне по хешу находится нужный инстанс.
#### Chain hash
Каскадный хеш цепочки записей:
```
chain_hash[0] = SHA256(0x00...00 || id_0 || ts_0 || author_0 || sig_0)
chain_hash[n] = SHA256(chain_hash[n-1] || id_n || ts_n || author_n || sig_n)
```
Для протокола синхронизации используются первые 8 байт (`chain_hash8`). Сравнивая эти хеши на разных позициях, узлы находят точку расхождения.
#### Sync protocol (8 message types)
| Message | Direction | Описание |
|---------|-----------|----------|
| `DB_MSG_INIT_SYNC (0x01)` | A→B | Инициатор шлёт своё количество записей (`my_count`) |
| `DB_MSG_INIT_RESP (0x02)` | B→A | Truncation point `tp = min(counts)-1`, `chain_hash8[tp]`, + до 16 sparse-хешей на позициях `tp-2^k` |
| `DB_MSG_REFINE (0x03)` | A→B, B→A | Бинарный поиск: до 16 равномерно распределённых хешей в диапазоне расхождения |
| `DB_MSG_SEND_DATA (0x04)` | A→B, B→A | Пакетная передача записей (до 32 за раз). Wire: `[id:8][ts:8][author:8][dlen:4][data][sig_len:1=64][sig:64]` |
| `DB_MSG_PUSH (0x05)` | A→B | Рассылка одной новой записи всем synced-пирам |
| `DB_MSG_ACK_PUSH (0x06)` | B→A | Подтверждение получения PUSH; обновляет delivery_chain и флаг WAS_SENT |
| `DB_MSG_SYNC_DONE (0x07)` | B→A | Финальное подтверждение: peer_count + last_chain_hash8 |
| `DB_MSG_ERROR (0x08)` | B→A | Ошибка. Коды: `0x01` — instance not found, `0x02` — instance disabled |
**Алгоритм синхронизации:**
1. `INIT_SYNC`: A → B: `my_count_a`; B вычисляет `tp = min(count_a, count_b) - 1`.
2. `INIT_RESP`: B → A: `tp`, `chain_hash8_b[tp]`, + sparse-хеши на позициях `tp-1, tp-2, tp-4, tp-8, ..., tp-32768`.
3. Если `hash8_b[tp] == hash8_a[tp]` — цепочки совпадают до `tp`. Более длинная сторона шлёт «хвост» (записи после `tp`).
4. Если sparse-хеши показывают расхождение: A определяет диапазон `[ds, de]` где хеши не совпадают. Если `de-ds ≤ 1` — пустой `REFINE` (запрос данных). Иначе — `REFINE` с до 16 хешами, равномерно распределёнными в диапазоне.
5. `REFINE`: B ищет первую позицию, где хеши разошлись, шлёт `SEND_DATA` начиная с этой позиции.
6. `SEND_DATA`: A вставляет полученные записи (с верификацией Ed25519 подписи), пересчитывает каскадный chain_hash, шлёт `SYNC_DONE`.
7. `SYNC_DONE`: если counts/hashes не совпали — реинициируется sync.
#### Защита от подделок
- Ed25519-подпись автора проверяется для КАЖДОЙ вставляемой записи (и локальной, и от пиров).
- Публичный ключ автора ищется: (1) в своих ключах (если self), (2) в `topo_node_sqlite`, (3) в `peer_ed25519_pubkey` активного ETCP-соединения.
- Если подпись невалидна — запись отвергается с логом "discarding as forgery".
#### Peer management
- При поднятии ETCP-соединения для каждого инстанса добавляется `SI_PEER` и запускается синхронизация.
- При разрыве соединения `sync_state` пира сбрасывается в 0.
- `peer_check` таймер (каждые 5с) перебирает `topo_group->senders_list` и запускает синхронизацию для пиров с `sync_state == 0`.
- `PUSH` рассылается только пирам в состоянии `sync_state >= 1`.
#### TTL cleanup
- Периодический таймер (каждые 3600с).
- Удаляет записи authored by self, где НЕ установлен флаг `DB_REC_FLAG_WAS_SENT` (0x01) и `timestamp < now - db_sync_ttl`.
- Чужие записи и свои отправленные не удаляются.
## 3. API
### Глобальный жизненный цикл
```c
int db_sync_init(struct UTUN_INSTANCE* inst);
void db_sync_destroy(struct UTUN_INSTANCE* inst);
```
- `db_sync_init` — инициализирует DB_SYNC, открывает SQLite по пути `<db_path>/sync`, биндит ETCP service `0x20`, вешает коллбэки на существующие и новые соединения, стартует `peer_check` таймер. Если `db_sync_enabled = 0` — создаёт структуру в disabled-режиме.
- `db_sync_destroy` — отменяет все таймеры, анбиндит ETCP service, снимает коллбэки со всех соединений, закрывает SQLite, освобождает память.
### Управление инстансами
```c
struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* name, uint64_t id);
void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si);
```
- `db_sync_instance_add` — создаёт/регистрирует инстанс. Проверяет валидность имени (`[a-zA-Z0-9_]`, макс 48 символов). Вычисляет hash, создаёт SQLite-таблицу `db_sync_<name>_<id>`, верифицирует цепочку хешей, запускает TTL-таймер. Если уже есть активные соединения — автоматически инициирует sync.
- `db_sync_instance_remove` — деактивирует инстанс: `enabled=0`, отменяет TTL-таймер, освобождает peers, удаляет из массива. Таблица БД **не удаляется**.
### Операции с данными
```c
int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, size_t len,
const uint8_t* sig, size_t sig_len, uint64_t ts);
uint32_t db_sync_count(struct DB_SYNC_INSTANCE* si);
uint64_t db_sync_get_last_timestamp(struct DB_SYNC_INSTANCE* si);
uint64_t db_sync_next_timestamp(struct DB_SYNC_INSTANCE* si);
```
- `db_sync_insert_signed` — вставляет подписанную запись. `sig` ОБЯЗАТЕЛЬНО 64 байта (Ed25519). Проверяет подпись, проверяет дубликат по `(timestamp, author_signature)`, вычисляет chain_hash, вставляет, каскадно пересчитывает хеши последующих записей, рассылает PUSH всем synced-пирам. Возвращает 0 (успех), 1 (уже существует), -1 (ошибка), -2 (неверная подпись).
- `db_sync_count` — количество записей в локальной БД.
- `db_sync_get_last_timestamp` — последний выданный `db_sync_next_timestamp`.
- `db_sync_next_timestamp` — возвращает монотонно возрастающий timestamp в миллисекундах. Гарантирует `last_timestamp_ms < returned`.
### Чтение (select)
```c
typedef void (*db_sync_select_cb)(void* arg, uint64_t id, uint64_t timestamp,
const char* data, size_t data_len, uint64_t author,
const uint8_t* author_sig, size_t sig_len,
int delivered_peers, const char* delivery_chain);
int db_sync_select(struct DB_SYNC_INSTANCE* si, uint32_t offset, uint32_t limit,
db_sync_select_cb cb, void* arg);
```
- `db_sync_select` — итератор по записям, упорядоченным `ORDER BY timestamp, author_signature`. `limit=0` — без ограничения. Для каждой записи вызывает `cb` с полями: id, timestamp, data (JSON), author (node_id), author_sig (64 байта), delivered_peers (счётчик доставок), delivery_chain (hex-идентификаторы пиров через запятую). Возвращает количество переданных в callback записей.
### Callback на вставку
```c
typedef void (*db_sync_insert_cb)(struct DB_SYNC_INSTANCE* si,
uint64_t record_timestamp,
const char* json_data, size_t len,
uint64_t author_node_id, void* arg);
void db_sync_set_insert_cb(struct DB_SYNC_INSTANCE* si, db_sync_insert_cb cb, void* arg);
```
- `db_sync_set_insert_cb` — устанавливает callback, вызываемый после успешной вставки записи (локальной или от пира). `author_node_id = self` для локальных вставок, `= peer_node_id` для записей от пиров.
### Внутренние структуры
```c
struct DB_SYNC {
struct UTUN_INSTANCE* inst; // обратная ссылка на инстанс
sqlite3* db; // SQLite handle
uint64_t last_connected_tb; // время последнего подключения (timebase)
struct DB_SYNC_INSTANCE* instances; // динамический массив инстансов
int instance_count, instance_capacity;
void* peer_check_timer; // handle uasync-таймера
uint8_t enabled; // глобальный флаг из конфига
};
struct DB_SYNC_INSTANCE {
struct DB_SYNC* db_sync; // обратная ссылка
uint64_t hash; // routing key = SHA256(name||id_be)[0:8]
char table_name[64]; // "db_sync_<name>_<id>"
uint64_t next_id; // монотонно возрастающий id записей
uint64_t last_timestamp_ms; // последний выданный timestamp (ms)
uint8_t enabled; // per-instance enable/disable
void* ttl_timer; // handle TTL-таймера
struct SI_PEER* peers; // динамический массив пиров
int peer_count, peer_capacity;
db_sync_insert_cb on_insert; // callback на вставку
void* on_insert_arg;
};
struct SI_PEER {
uint64_t node_id; // идентификатор пира
uint32_t synced_pos; // позиция, до которой синхронизированы
uint8_t sync_state; // 0=idle, 1=syncing, 2=synced
};
```
### Конфигурация
| Параметр | Файл конфига | По умолчанию | Описание |
|----------|-------------|--------------|----------|
| `db_sync_enabled` | `global` | `0` | Включить модуль синхронизации |
| `db_sync_ttl` | `global` | `86400` | TTL неотправленных собственных записей (секунды) |
| `db_path` | `global` | — | Путь к БД; файл создаётся как `<db_path>/sync` |
### Константы синхронизации
| Константа | Значение | Описание |
|-----------|---------|----------|
| `ETCP_RT_ID_DB_SYNC` | `0x20` | ETCP service ID |
| `DB_REFINE_HASHES` | `16` | Макс. количество хешей в REFINE |
| `DB_SEND_DATA_MAX` | `32` | Макс. записей в одном SEND_DATA |
| `DB_SIG_SIZE` | `64` | Размер Ed25519 подписи |
| `DB_SYNC_PEER_CHECK_INTERVAL` | `5` | Интервал проверки пиров (секунды) |
| `DB_SYNC_TTL_INTERVAL` | `3600` | Интервал TTL-очистки (секунды) |
### Зависимости
- **etcp_api.h / etcp.h** — P2P-обмен сообщениями, коллбэки соединений
- **topo_group.h / topo_node_sqlite.h** — топология, discovery пиров, Ed25519 pubkeys
- **secure_channel.h** — SHA256 (`sc_sha256_*`), Ed25519 verify (`sc_ed25519_verify`)
- **lib/u_async.h** — асинхронный event loop, таймеры (`uasync_set_timeout`, `uasync_cancel_timeout`)
- **lib/sqlite3.h** — SQLite3 (WAL, synchronous=NORMAL, auto-checkpoint 10000)
- **lib/mem.h** — `u_malloc`, `u_calloc`, `u_realloc`, `u_free`
- **lib/debug_config.h** — `DEBUG_INFO`, `DEBUG_WARN`, `DEBUG_ERROR` (категория `DEBUG_CATEGORY_DEBUG`)
- **lib/sha256.h** — `SC_SHA256_CTX` (если не USE_OPENSSL)
- **lib/platform_compat.h** — `htobe64`, `get_time_tb`, `utun_gettimeofday`
- **openssl/evp.h** — низкоуровневый Ed25519 в `sc_ed25519_verify`
### Внутренние функции
| Функция | Назначение |
|---------|-----------|
| `db_sqlite_open/db_sqlite_close` | Открытие/закрытие SQLite с WAL-режимом |
| `db_sha256/db_hash64/db_chain_hash_compute` | SHA256 и вычисление chain_hash |
| `db_instance_find/db_instance_alloc` | Поиск/выделение инстанса по hash |
| `si_peer_find/si_peer_add` | Поиск/добавление пира в инстансе |
| `db_count` | `SELECT COUNT(*)` из таблицы инстанса |
| `db_chain_hash_at/db_chain_hash8_at` | Чтение chain_hash/chain_hash8 на позиции |
| `db_prev_chain_hash` | Поиск chain_hash записи, предшествующей данной |
| `db_cascade_from` | Пересчёт chain_hash начиная с позиции `from_pos` |
| `db_get_ed25519_pubkey` | Получение Ed25519 pubkey пира |
| `db_verify_author_sig` | Проверка Ed25519 подписи автора записи |
| `db_record_insert` | Полный цикл вставки: проверка подписи, проверка дубликата, вставка, каскад chain_hash |
| `db_sync_send/db_sync_send_hash` | Отправка сообщения пиру через ETCP |
| `si_delivery_update` | Обновление delivery_chain при доставке |
| `si_parse_record` | Разбор одной записи из wire-формата |
| `si_send_data_batch` | Формирование и отправка SEND_DATA пакета |
| `db_sync_recv_cb` | Центральный callback приёма ETCP-сообщений, маршрутизация по instance_hash |
| `db_handle_init_sync/_resp/_refine/_send_data/_push/_ack_push/_sync_done/_error` | Обработчики каждого типа сообщения |
| `db_sync_initiate_sync` | Отправка INIT_SYNC пиру |
| `db_verify_chain` | Полная верификация цепочки chain_hash (при старте инстанса) |
| `db_sync_peer_check_cb` | Таймер: периодический поиск активных пиров и запуск sync |
| `db_sync_instance_ttl_cb` | Таймер: TTL-очистка неотправленных собственных записей |

86
src/dummynet_doc.md

@ -0,0 +1,86 @@
# Dummynet — UDP Network Emulator
## 1. Назначение
Эмулятор сетевых условий для тестирования: задержка (фиксированная + случайная), ограничение пропускной способности (token bucket) и случайные потери пакетов. Два режима работы: **FULL** (автономный UDP-прокси между портами) и **INLINE** (встраиваемый фильтр в `send_hook` ETCP_LINK без создания своего сокета).
## 2. Как пользоваться
### Режим FULL (автономный UDP-прокси)
1. `dummynet_create(ua, bind_ip, listen_port)` — создаёт контекст, открывает UDP-сокет на `listen_port`.
2. `dummynet_set_direction(dn, DUMMYNET_FORWARD, ...)` и `DUMMYNET_BACKWARD` — настраивает параметры для каждого направления.
3. Пакеты от `listen_port-1` идут в направлении FORWARD, от `listen_port+1` — в BACKWARD.
```
[port-1] → [dummynet: delay→loss→shaper] → [dest_forward]
[port+1] → [dummynet: delay→loss→shaper] → [dest_backward]
```
### Режим INLINE (send_hook в ETCP_LINK)
1. `dummynet_filter_create(ua)` — создаёт фильтр (нет своего сокета).
2. `dummynet_filter_set_loss(df, permille)` / `dummynet_filter_set_delay(df, ms, jitter_ms)` — настройка.
3. `dummynet_filter_attach(df, link)` — встраивается в `link->send_hook`, перехватывая все вызовы `socket_sendto`.
4. Дополнительно: `dummynet_filter_block_addr/unblock_addr` — блокировка отправки на конкретные адреса.
### Механизмы эмуляции
| Механизм | Реализация |
|----------|-----------|
| **Delay** (fixed + random) | При приёме пакета создаётся uasync-таймер (`dummynet_delay_callback`). После срабатывания пакет кладётся в ll_queue направления |
| **Bandwidth (token bucket)** | Shaper-таймер забирает из очереди по одному пакету с интервалом `len × 8 / bw_kbps` (timebase 0.1ms). Гарантирует не более `bandwidth_kbps` |
| **Loss (permille)** | При приёме: `rand() % 1000 < loss_permille` → дроп. В INLINE-режиме — в хуке отправки |
### Статистика
Структура `dummynet_stats`: `recv`, `sent`, `dropped` (переполнение очереди), `lost` (random loss), `queue_size` (текущий), `queue_max` (пиковый).
Дополнительные отладочные счётчики: `delay_timer_set/fire`, `shaper_timer_set/fire`.
### Ключевые нюансы
- В FULL-режиме порты `listen_port-1`, `listen_port`, `listen_port+1` должны быть свободны. Направление определяется по порту источника.
- Шейпер использует burst allowance (`SHAPER_BURST_TB = 10 timebase units`), допуская небольшие всплески.
- При переполнении очереди (> `max_queue_pkts`) пакет дропается, увеличивается `dropped`.
- INLINE-фильтр в режиме блокировки адреса не дропает пакет, а возвращает `len` (пакет «успешно отправлен», но фактически никуда не ушёл).
## 3. API
### FULL-режим
| Функция | Назначение |
|---------|------------|
| `dummynet_create(ua, bind_ip, listen_port)` | Создать контекст, открыть UDP-сокет, зарегистрировать в uasync |
| `dummynet_destroy(dn)` | Закрыть сокет, отменить таймеры, очистить очереди, освободить память |
| `dummynet_set_direction(dn, dir, delay_fixed_ms, delay_random_ms, bw_kbps, max_q, loss_permille, dest_ip, dest_port)` | Настроить параметры направления |
| `dummynet_get_stats(dn, dir)` | Получить статистику направления |
| `dummynet_reset_stats(dn, dir)` | Сбросить статистику (-1 = все направления) |
| `dummynet_get_socket(dn)` | Вернуть fd сокета |
| `dummynet_get_listen_port(dn)` | Вернуть listen_port |
| `dummynet_get_uasync(dn)` | Вернуть указатель на UASYNC |
| `dummynet_get_queue_size(dn, dir)` | Вернуть текущий размер очереди направления |
| `dummynet_get_debug_counters(dn, dir, ...)` | Вернуть отладочные счётчики таймеров |
### INLINE-режим (dummynet_filter)
| Функция | Назначение |
|---------|------------|
| `dummynet_filter_create(ua)` | Создать фильтр |
| `dummynet_filter_set_loss(df, permille)` | Установить вероятность потерь |
| `dummynet_filter_set_delay(df, delay_ms, jitter_ms)` | Установить задержку (NOTE: в текущей реализации delay не применяется в хуке) |
| `dummynet_filter_attach(df, link)` | Прикрепить хук к ETCP_LINK |
| `dummynet_filter_detach(df)` | Открепить хук |
| `dummynet_filter_destroy(df)` | Уничтожить фильтр |
| `dummynet_filter_get_stats(df)` | Получить статистику |
| `dummynet_filter_block_addr(df, ip, port)` | Заблокировать адрес (до 4 адресов) |
| `dummynet_filter_unblock_addr(df, ip, port)` | Разблокировать адрес |
### Структуры
| Структура | Назначение |
|-----------|------------|
| `dummynet_stats` | Статистика: `recv`, `sent`, `dropped`, `lost`, `queue_size`, `queue_max` |
| `dummynet_dir` | Внутренняя: параметры направления, очередь, таймеры шейпера, отладочные счётчики |
| `dummynet` | Контекст FULL-режима: ua, сокет, два `dummynet_dir`, пул пакетов |
| `dummynet_filter` | Контекст INLINE-режима: ua, loss_permille, delay, link, статистика, список блокировок |
### Зависимости
`u_async.h`, `ll_queue.h`, `memory_pool.h`, `socket_compat.h`, `etcp_connections.h` (для ETCP_LINK), `platform_compat.h`

47
src/eim_nat_doc.md

@ -0,0 +1,47 @@
# EIM NAT (Endpoint-Independent Mapping NAT)
## 1. Назначение
Трансляция сетевых адресов с endpoint-independent поведением: для каждого кортежа `(внутренний IP, внутренний порт, протокол)` выделяется уникальный внешний порт на шлюзе, не зависящий от внешнего адресата. Поддерживает TCP, UDP и ICMP Echo. Инкрементальный пересчёт контрольных сумм.
## 2. Как пользоваться
```c
struct eim_nat_ctx nat;
eim_nat_init_ctx(&nat, &global_cfg); // таблица 64K записей
eim_nat_egress(&nat, pkt, len, node_id, conn); // src IP/port -> внешние
eim_nat_ingress(&nat, pkt, len, &entry); // dst IP/port -> внутренние
eim_nat_add_forward(&nat, proto, ip, port, ext_port); // статический проброс
eim_nat_destroy_ctx(&nat);
```
**Таблица:** 64K записей, индексируется по внешнему порту (внешний порт == индекс в таблице).
**eim_nat_egress:** выделяет внешний порт (аллоцирует из циклического пула `next_port`), заменяет src IP на шлюзовой и src порт на внешний, корректирует IP- и транспортную контрольные суммы. Фрагментированные пакеты пропускаются без обработки.
**eim_nat_ingress:** ищет запись по внешнему dst-порту в таблице, заменяет dst IP на внутренний и dst порт на внутренний, корректирует контрольные суммы. Возвращает `out_entry` — указатель на запись (содержит `src_node_id` и `src_conn` для маршрутизации ответа).
**eim_nat_add_forward:** добавляет статическую запись (`EIM_NAT_ENTRY_STATIC`), которая не вытесняется динамическими.
**Контрольные суммы:** пересчитываются инкрементально через `csum_update_n()` — старые значения вычитаются из ~csum, новые прибавляются. Это работает для TCP pseudo-header checksum и UDP.
**Конфиг:**
```ini
[nat]
enabled = 1
tun_ip = 10.0.0.1
port_start = 10000
port_end = 20000
[nat_forward]
rule = tcp 192.168.1.2:80 :8080
```
## 3. API
| Функция/Структура | Описание |
|---|---|
| `struct eim_nat_entry` | Запись: internal_ip, internal_port, proto, state (FREE/ACTIVE/STATIC), src_node_id, src_conn, last_seen |
| `struct eim_nat_ctx` | Контекст: gateway_ip, port_start/end, next_port (циклический аллокатор), table[64K], initialized |
| `eim_nat_init_ctx(ctx, cfg)` | Инициализация таблицы, загрузка статических пробросов из конфига |
| `eim_nat_destroy_ctx(ctx)` | Освобождение таблицы, обнуление |
| `eim_nat_egress(ctx, ip_data, ip_len, src_node_id, src_conn)` | Трансляция исходящего пакета: src IP/port → шлюзовые. Создаёт динамическую запись при первом проходе |
| `eim_nat_ingress(ctx, ip_data, ip_len, out_entry)` | Обратная трансляция входящего пакета: dst IP/port → внутренние. Заполняет out_entry для маршрутизации |
| `eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port)` | Добавление статического проброса (runtime API) |

121
src/etcp_api_doc.md

@ -0,0 +1,121 @@
# ETCP API
## 1. Назначение
Модуль предоставляет публичный API для отправки и приёма пакетов через ETCP-соединения. Это входная точка для всех вышележащих подсистем (роутер, маршрутизация, NAT, TCP-прокси, транспорт сообщений).
**Ключевая идея:** приёмный коллбэк `etcp_int_recv` диспетчеризует входящие пакеты по первому байту кодограммы (`cmd`). Любой модуль может зарегистрировать обработчик на свой `cmd` через `etcp_bind` и слать данные через `etcp_send`.
Формат кодограммы (сырой пакет внутри ETCP): `<cmd 1 байт> <данные N байт>`.
Модуль **тонкий**: отправка — просто `queue_data_put` в очередь normalizer'а, приём — выбор обработчика по ID из массива `ETCP_BINDINGS`.
## 2. Как пользоваться
### Типовой сценарий: модуль хочет принимать пакеты своего типа
```c
// Регистрируем коллбэк при инициализации модуля
etcp_bind(instance, ETCP_ID_DATA, my_data_recv_callback);
// Коллбэк вызывается для каждого входящего пакета с cmd=ETCP_ID_DATA
void my_data_recv_callback(struct ETCP_CONN* conn, struct ll_entry* entry) {
// entry->dgram[0] = cmd (уже совпал с ETCP_ID_DATA)
// entry->dgram[1..len-1] = данные
process_data(conn, entry->dgram + 1, entry->len - 1);
queue_dgram_free(entry);
queue_entry_free(entry);
}
```
### Отправка пакета
```c
struct ll_entry* e = queue_entry_new_from_pool(inst->data_pool);
memcpy(e->dgram, &my_cmd, 1);
memcpy(e->dgram + 1, payload, payload_len);
e->len = 1 + payload_len;
etcp_send(conn, e); // ownership entry передаётся
```
### Коллбэки состояния соединения
```c
// Срабатывает после INIT-handshake, можно слать данные
etcp_conn_set_ready_cbk(conn, on_conn_ready, my_data);
// Срабатывает при поднятии/падении соединения
etcp_conn_set_up_cbk(conn, on_conn_up, my_data);
etcp_conn_set_down_cbk(conn, on_conn_down, my_data);
// На уровне instance — при создании нового входящего соединения
etcp_set_new_conn_cbk(instance, on_new_conn, my_data);
```
**Важно:** коллбэки `set_*` заменяют всю цепочку одним обработчиком; `add_*`/`remove_*` добавляют/убирают обработчик в связном списке из нескольких коллбэков.
### Сервисные ID (для etcp_router)
Используются `ETCP_RT_ID_*`, отличаются от сырых `ETCP_ID_*` (пакет оборачивается в транспортный ID `ETCP_ID_SVC_ROUTE=0x03`):
- `ETCP_RT_ID_DATA` (0x00) — маршрутизация данных
- `ETCP_RT_ID_NAT` (0x02) — NAT-трафик между узлами
- `ETCP_RT_ID_SVC_ROUTE` (0x03) — транспорт роутера
- `ETCP_RT_ID_TCP_PROXY` (0x04) — TCP-прокси
- `ETCP_RT_ID_UDP_PROXY` (0x05) — UDP-прокси
- `ETCP_RT_ID_ICMP_PROXY` (0x06) — ICMP-прокси
- `ETCP_RT_ID_MSG_TRANSPORT` (0x10) — локальный IPC-транспорт сообщений
- `ETCP_RT_ID_CONN_MGR` (0x11) — управление соединениями
- `ETCP_RT_ID_NTP_TIME` (0x12) — синхронизация времени
### Фоновые соединения (etcp_connect)
```c
etcp_connect(instance, node, on_connect, arg,
ETCP_CONNECT_EARLY | ETCP_CONNECT_BGP_READY);
```
Флаги: `ETCP_CONNECT_EARLY` — раннее подключение, `ETCP_CONNECT_LATE` — отложенное, `ETCP_CONNECT_BGP_READY` — после BGP-синхронизации.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `ETCP_BINDINGS` | Per-instance массив `callbacks[256]` + коллбэк приёма метрик (`on_metrics_rcvd`) |
### Типы коллбэков
| Тип | Сигнатура | Назначение |
|-----|-----------|-----------|
| `etcp_recv_fn` | `void(conn, entry)` | Приём пакета — **обязан** освободить entry/dgram |
| `etcp_metrics_fn` | `void(user_ptr, conn, csv, csv_len, sig)` | Приём метрик (CSV + Ed25519-подпись) |
| `etcp_cbk_fn` | `void(conn, arg)` | События ready/up/down/new_conn |
| `etcp_connect_callback_t` | `void(arg, conn, type)` | Результат фонового подключения |
### Функции
| Функция | Назначение |
|---------|-----------|
| `etcp_bind(inst, id, fn)` | Подписаться на пакеты с cmd=`id` |
| `etcp_unbind(inst, id)` | Отписаться |
| `etcp_send(conn, entry)` | Отправить пакет (забирает ownership entry) |
| `etcp_int_recv(queue, arg)` | **Внутренняя:** диспетчер из очереди normalizer'а, вызывает bound-коллбэк по cmd |
| `etcp_conn_set_ready_cbk(conn, fn, arg)` | Установить одиночный коллбэк готовности (заменяет существующие) |
| `etcp_conn_set_up_cbk(conn, fn, arg)` | Установить коллбэк поднятия соединения |
| `etcp_conn_set_down_cbk(conn, fn, arg)` | Установить коллбэк падения соединения |
| `etcp_conn_add_ready_cbk/remove_ready_cbk` | Добавить/убрать из цепочки ready-коллбэков |
| `etcp_conn_add_up_cbk/remove_up_cbk` | Аналогично для up |
| `etcp_conn_add_down_cbk/remove_down_cbk` | Аналогично для down |
| `etcp_set_new_conn_cbk(inst, fn, arg)` | Установить коллбэк на входящее соединение |
| `etcp_add_new_conn_cbk/remove_new_conn_cbk` | Добавить/убрать из цепочки new_conn |
| `etcp_set_routing_exchange_state(conn, state)` | Установить состояние обмена маршрутами; при state≥3 вызывает `bgp_ready_cbk` |
| `etcp_connect(inst, node, cb, arg, flags)` | Фоновое подключение к узлу |
### Командные ID (сырые, первый байт кодограммы)
| Константа | Значение | Назначение |
|-----------|----------|-----------|
| `ETCP_ID_DATA` | 0x00 | Данные для передачи адресату |
| `ETCP_ID_ROUTE_ENTRY` | 0x01 | Элемент роутинг-таблицы (BGP) |
| `ETCP_ID_SVC_ROUTE` | 0x03 | Транспорт роутера (etcp_router) |

95
src/etcp_bbr_doc.md

@ -0,0 +1,95 @@
# BBR Congestion Control (etcp_bbr)
## 1. Назначение
Реализация алгоритма BBRv3 (Bottleneck Bandwidth and Round-trip propagation time) для контроля перегрузки на каждом ETCP-линке независимо. Вычисляет оптимальный cwnd и pacing rate на основе измерений bandwidth и RTT, а не потерь пакетов (в отличие от loss-based алгоритмов). Моделирует состояние канала через четыре фазы: STARTUP (экспоненциальный поиск bandwidth), DRAIN (слив лишнего inflight), PROBE_BW (циклическое зондирование), PROBE_RTT (периодический замер минимального RTT).
## 2. Как пользоваться
### Инициализация
```c
struct bbr bbr_state;
bbr_init(&bbr_state); // устанавливает STARTUP, сбрасывает все окна
```
Сразу после init нужно установить `init_cwnd` через etcp/kernel, BBR начинает со STARTUP.
### Вызов на каждом ACK
```c
struct bbr_rate_sample rs = {
.delivered = packets_delivered_this_ack,
.interval_us = us_since_last_ack,
.rtt_us = latest_rtt_us,
.acked_sacked = bytes_acked,
.lost = packets_lost_since_last_ack,
.is_app_limited = is_output_queue_below_target,
.tx_in_flight = current_inflight_packets,
.prior_delivered = cumulative_delivered_before,
};
uint32_t cwnd = current_cwnd, pacing_rate = current_pacing;
bbr_main(&bbr_state, &rs, &cwnd, &pacing_rate, MSS, inflight_bytes, is_cwnd_limited);
// применять cwnd, pacing_rate к линку
```
### Нотификация о потерях и старте передачи
```c
bbr_note_loss(&bbr_state); // вызывать при обнаружении потери
bbr_tx_start(&bbr_state); // вызывать при возобновлении отправки после idle
```
### Интеграция с ETCP_LINK
BBR создаётся в `etcp_link_new()` как `link->bbr`. `bbr_main()` вызывается в `etcp_handle_ack()` при обработке каждого ACK-пакета. Результаты (cwnd, pacing_rate) записываются в `link->inflight_lim_bytes` и `link->bbr_pacing_rate`. Pacing rate подаётся в traffic shaper линка. `link_id` задаётся в `link->local_link_id` для читаемых логов.
### Тестирование
Поле `now_tb` позволяет подменять реальное время (`get_time_tb()`) на заданное — при `now_tb == 0` используется `get_time_tb()`, иначе управляемое значение. `on_cwnd_update` — callback для отслеживания изменений cwnd в тестах.
### Фиксированная точка
- `BBR_SCALE = 8` — масштаб для gain-коэффициентов (2.77 = 2.77 * 256)
- `BW_SCALE = 24` — масштаб для bandwidth-значений (bytes/sec * mss)
- Все внутренние вычисления в фиксированной точке, преобразование в реальные единицы через `bbr_rate_bytes_per_sec()`
## 3. API
### Структуры
**`struct bbr_rate_sample`** — сэмпл доставки для одного ACK
| Поле | Назначение |
|------|-----------|
| `delivered` | сколько пакетов доставлено этим ACK |
| `interval_us` | время с предыдущего ACK, мкс |
| `rtt_us` | последний измеренный RTT |
| `acked_sacked` | байт подтверждено (+SACK) |
| `prior_delivered` | кумулятивно доставлено до этого ACK (для детекта round) |
| `tx_in_flight` | inflight на момент отправки подтверждённых пакетов |
| `lost` | число потерянных пакетов с последнего ACK |
| `is_app_limited` | приложение не забивает канал (выходная очередь ниже порога) |
**`struct bbr`** — состояние BBR на линк (подробности в etcp_bbr.h:55-108)
### Функции
**`void bbr_init(struct bbr* bbr)`** — инициализация: STARTUP, сброс окон, `min_rtt_us = ~0U`, `bw_lo = ~0U`, `inflight_hi/lo = ~0U`.
**`void bbr_main(struct bbr* bbr, const struct bbr_rate_sample* rs, uint32_t* cwnd_out, uint32_t* pacing_rate_out, uint32_t mss, uint32_t inflight_bytes, int is_cwnd_limited)`** — главный вызов на каждый ACK. Обновляет delivered, детектит начало раунда, вычисляет sample_bw, обновляет модель (congestion signals, ack aggregation, full_bw, drain, cycle, min_rtt), применяет gains, вычисляет cwnd и pacing rate.
**`void bbr_note_loss(struct bbr* bbr)`** — помечает потерю в текущем раунде/цикле.
**`void bbr_tx_start(struct bbr* bbr)`** — отмечает возобновление передачи после idle, сбрасывает `ack_epoch`.
### Режимы и циклы
| Режим | Описание |
|-------|---------|
| `BBR_STARTUP` (0) | Экспоненциальный рост cwnd (gain 2.77 pace, 2.0 cwnd), пока не найден bottleneck |
| `BBR_DRAIN` (1) | Слив избыточного inflight (gain 1000/2885 ≈ 0.35 pace) |
| `BBR_PROBE_BW` (2) | Основной рабочий режим, циклическое зондирование |
| `BBR_PROBE_RTT` (3) | Периодический сброс cwnd до 4*MSS для замера min_rtt |
Циклы внутри `BBR_PROBE_BW`:
- `BBR_BW_PROBE_UP` (1.25× pace) — зондирование большей полосы
- `BBR_BW_PROBE_DOWN` (0.75× pace) — слив после зонда
- `BBR_BW_PROBE_CRUISE` (1.0× pace) — крейсерский режим
- `BBR_BW_PROBE_REFILL` (1.0× pace) — восполнение перед новым зондом
### Зависимости
- `lib/u_async.h` — `get_time_tb()` для реального времени
- `lib/debug_config.h` — `DEBUG_DEBUG(DEBUG_CATEGORY_BBR, ...)` для отладки

71
src/etcp_connect_doc.md

@ -0,0 +1,71 @@
# ETCP Connect (etcp_connect)
## 1. Назначение
Управление исходящими ETCP-соединениями. Предоставляет асинхронный API `etcp_connect()` для установки соединения с удалённым узлом: создаёт ETCP_CONN, настраивает криптографию, поднимает UDP-линки и опционально TCP/STCP-транспорт, отслеживает прогресс установки с таймерами. Доставляет коллбэки о фазах готовности (EARLY, LATE, BGP_READY).
## 2. Как пользоваться
```c
// Запуск подключения к узлу
etcp_connect(inst, node, my_connect_cb, my_arg, ETCP_CONNECT_EARLY | ETCP_CONNECT_LATE);
```
Коллбэк вызывается на разных фазах:
```c
void my_connect_cb(void* arg, struct ETCP_CONN* conn, int type) {
if (!conn) {
// type == 0 — таймаут/ошибка подключения
return;
}
if (type & ETCP_CONNECT_EARLY)
// первый линк готов (можно начинать обмен)
if (type & ETCP_CONNECT_LATE)
// settle-фаза завершена (все линки проверены, неудачные удалены)
if (type & ETCP_CONNECT_BGP_READY)
// BGP-синхронизация завершена (routing_exchange_active >= 3)
}
```
**Жизненный цикл:**
1. `etcp_connect()` — создаёт ETCP_CONN, crypto_ctx, UDP-линки и TCP/STCP
2. `connect_ready_cb()` — первый линк стал ready → EARLY-коллбэк → запуск settle-таймера
3. `connect_settle_timeout_cb()` — settle (min_rtt × 8, clamp 500..2000ms) → удаление неудачных линков → LATE-коллбэк
4. `connect_bgp_ready_cb()` — BGP завершён → BGP_READY-коллбэк
5. При таймауте (`connect_initial_timeout_cb`) — type=0 (conn=NULL), все ресурсы освобождаются
**Нюансы:**
- Если соединение уже установлено — коллбэки вызываются немедленно
- Если соединение в процессе — коллбэк добавляется в список ожидающих (поддержка нескольких подписчиков)
- `etcp_connect_cancel_for_conn()` — только для deferred cleanup (фаза 2 закрытия), НЕ из mainloop
- При таймауте `etcp_connect()` может доставить type=0 с conn=NULL — нужно обрабатывать
## 3. API
### Структуры
**ETCP_CONNECT** — контекст одного исходящего соединения:
- `conn` — целевой ETCP_CONN
- `tcp_link` — STCP-транспорт (опционально)
- `cb_list` — цепочка коллбэков (несколько подписчиков)
- `initial_timer`, `settle_timer` — таймеры контроля прогресса
- `min_rtt` — минимальный RTT среди готовых линков (для расчёта settle-времени)
- `early_delivered` — флаг однократного вызова EARLY
- `tcp_ready` — TCP-линк поднят
- `done` — соединение завершено (ошибка/успех), дальнейшая обработка игнорируется
### Константы коллбэков (etcp_api.h)
| Константа | Описание |
|---|---|
| `ETCP_CONNECT_EARLY` (1) | Первый линк готов |
| `ETCP_CONNECT_LATE` (2) | Settle-фаза завершена |
| `ETCP_CONNECT_BGP_READY` (4) | BGP-синхронизация завершена |
### Функции
| Функция | Описание |
|---|---|
| `etcp_connect(inst, node, cb, arg, flags)` | Запустить асинхронное подключение к узлу. Возвращает 0 при успехе |
| `etcp_connect_cancel_for_conn(inst, conn)` | Отменить ожидание для conn. Только из etcp_connection_free_resources (фаза 2) |
### Пути завершения
1. **Таймаут установки** (`connect_initial_timeout_cb`): stcp_link_close → etcp_connection_close → connect_cancel → type=0
2. **Settle-таймаут** (`connect_settle_timeout_cb`): удаление неудачных линков/stcp → LATE → connect_cancel
3. **Штатное закрытие** (`utun_instance_destroy`): фаза 1 (detach) → фаза 2 (deferred: `etcp_connect_cancel_for_conn`)

198
src/etcp_connections_doc.md

@ -0,0 +1,198 @@
# ETCP Connections
## 1. Назначение
Подмодуль ETCP, обслуживающий UDP/TCP-сокеты для передачи кодограмм и управляющий одним ETCP-соединением (`ETCP_CONN`) через несколько каналов связи (`ETCP_LINK`, мультилинк с failover).
Отвечает за:
- Создание сокетов из конфига (серверных для входящих и клиентских для исходящих)
- Приём сырых пакетов, дешифровку, диспетчеризацию (INIT/PING/PONG/KEEPALIVE/данные)
- INIT handshake: обмен node_id, ключами pubkey (X25519 + Ed25519), MTU, keepalive-параметрами
- Keepalive с адаптивным периодом (200мс → 10с → 200мс при трафике)
- Управление inflight-лимитами, burst-измерения bandwidth, BBR congestion control per-link
- Детекцию NAT (DIRECT/EIM/strict), обновление nat_addr при смене адреса
## 2. Как пользоваться
### Инициализация сокетов и соединений
Двухфазная инициализация:
```c
// Фаза 1: создаём listen-сокеты (до topo_group_init)
init_sockets(instance);
// Фаза 2: создаём клиентские соединения из [client] секций конфига
init_connections(instance);
```
Обе функции берут конфиг из `instance->config`: серверы из `[server]`, клиенты из `[client]`.
### Входящий пакет: полный путь
```
socket_recvfrom() → etcp_connections_read_callback_socket()
├─ Есть линк и session_ready → sc_decrypt() по session_key
│ ├─ KEEPALIVE → обновить period, выйти
│ ├─ INIT_RESPONSE → handle_init_response_client()
│ └─ Данные → etcp_conn_input(pkt) [в ETCP-стек]
│
└─ Нет линка/сессии → попытка INIT-дешифровки
├─ Извлечь соль + obfuscated pubkey из хвоста пакета
├─ sc_obfuscate_pubkey() → восстановить pubkey пира
├─ sc_set_peer_public_key() → вычислить session_key
├─ sc_decrypt() → расшифровать
├─ PING → handle_ping(): ответить PONG, вернуть данные коллбэку
├─ PONG → handle_pong(): найти PING_CONTEXT, вызвать cb
└─ INIT_REQUEST → обработать handshake, отправить INIT_RESPONSE
```
### Отправка зашифрованного пакета
```c
struct ETCP_DGRAM* dgram = memory_pool_alloc(inst->pkt_pool);
dgram->link = link;
dgram->data[0] = ETCP_KEEPALIVE;
dgram->data_len = 1;
dgram->noencrypt_len = 0; // для INIT — SC_PUBKEY_ENC_SIZE
etcp_encrypt_send(dgram);
memory_pool_free(inst->pkt_pool, dgram);
```
`etcp_encrypt_send` делает:
1. `sc_encrypt(timestamp(2) + flag_up(1) + data, ...)` → зашифрованный буфер
2. Копирует `noencrypt_len` байт из хвоста данных в конец буфера (для pubkey при INIT)
3. `etcp_udp_send()` → `socket_sendto()` (или `link->send_hook` если установлен)
### PING/PONG (one-shot пробники)
```c
etcp_send_ping(instance, peer_pubkey_bin, &addr, timeout_ms,
my_ping_callback, user_arg,
user_data, user_data_len);
// Коллбэк:
void my_ping_callback(int success, uint16_t rtt, void* arg, uint64_t nonce,
const uint8_t* resp_data, size_t resp_data_len);
```
Пинг-пакеты идут мимо ETCP_CONN/LINK — через временный `sc_context_t`, pubkey обфусцируется в хвосте.
### Адаптивный keepalive
- Период `ka_period_ms`: старт = `keepalive_interval` из конфига, минимальный 200мс
- Если был трафик (`pkt_sent_since_keepalive=1`): период сбрасывается к `keepalive_interval`
- Без трафика: период растёт ×1.05 до `KA_PERIOD_MAX_MS` (10с)
- Таймаут = `period × KA_TIMEOUT_MULT` (×10). При превышении → линк падает
- Сервер не шлёт keepalive при `recv_keepalive=0` (линк уже мёртв), клиент шлёт всегда
## 3. API
### Ключевые структуры
**ETCP_SOCKET** — один слушающий/клиентский сокет из конфига:
- `fd` — UDP (или TCP через `stcp_link`)
- `links[]` — сортированный массив активных линков (по `ip_port_hash`), бинарный поиск
- `interface_addr` — IP интерфейса (автоопределённый или из конфига)
- `nat_addr`, `nat_type` — детектированный NAT-адрес и тип
- `type` — `CFG_SERVER_TYPE_PUBLIC/NAT/PRIVATE`
- `sock_id` — уникальный ID (0-255) для сопоставления при NAT-матчинге
**ETCP_LINK** — одно динамическое соединение (один путь между двумя узлами):
- Принадлежит ровно одному `ETCP_CONN` (родитель) и одному `ETCP_SOCKET` (сокет)
- `remote_addr` — адрес пира; поиск по `ip_port_hash` (CRC32 от sockaddr)
- `mtu`, `mtu_local`, `mtu_remote` — согласованный MTU (min)
- `is_server` — 0=клиент инициирует, 1=сервер принимает
- `initialized=1` — handshake завершён, соединение готово
- `link_status` — итоговый статус: `recv_keepalive && remote_keepalive`
- `link_state`: 0=init, 1=handshake, 2=reconnect, 3=connected
- `init_timer` / `keepalive_timer` / `shaper_timer` — таймеры uasync
- `remote_ed25519_pubkey` — Ed25519-ключ пира из INIT handshake
- `bbr` — состояние BBR congestion control per-link
- Поля burst-измерений bandwidth (sender + receiver)
- `send_hook` — перехватчик отправки (для net_emulator)
- Статистика: encrypt/decrypt/send/recv errors, total_encrypted/decrypted, retransmissions
**ETCP_DGRAM** — незашифрованная кодограмма:
- `link` — куда отправлять / откуда получено
- `data_len` — общий размер данных (без timestamp)
- `noencrypt_len` — байты с конца, не подлежащие шифрованию (SC_PUBKEY_ENC_SIZE для INIT)
- `timestamp`, `flag_up` — заголовок, шифруется вместе с data
**ETCP_INIT_REQUEST_PKT** (62 байта) — формат INIT-запроса в зашифрованном payload:
- `code` (0x02/0x04), `node_id[8]`, `session_id[4]`, `mtu[2]`, `keepalive[2]`, `recovery[2]`
- `link_id`, `socket_id`, `only_local`, `type`
- `src_ipv4[4]`, `src_port[2]` — для NAT_DIRECT-детекции
- `collision` — флаг разрешения коллизий
- `ed25519_pubkey[32]`
**ETCP_INIT_RESPONSE_PKT** (57 байт) — ответ сервера:
- `code` (0x03/0x05), `node_id[8]`, `session_id[4]`, `mtu[2]`
- `link_id`, `remote_socket_id`, `only_local`, `type`
- `peer_ipv4[4]`, `peer_port[2]` — внешний NAT-адрес клиента
- `ed25519_pubkey[32]`
### Типы кодограмм
| Константа | Значение | Назначение |
|-----------|----------|-----------|
| `ETCP_INIT_REQUEST` | 0x02 | INIT-запрос со сбросом ETCP-соединения |
| `ETCP_INIT_RESPONSE` | 0x03 | INIT-ответ со сбросом |
| `ETCP_INIT_REQUEST_NOINIT` | 0x04 | INIT-запрос без сброса |
| `ETCP_INIT_RESPONSE_NOINIT` | 0x05 | INIT-ответ без сброса |
| `ETCP_PING` | 0x06 | One-shot пробник |
| `ETCP_PONG` | 0x07 | Ответ на пробник |
| `ETCP_KEEPALIVE` | 0x08 | keepalive-пакет |
### NAT-типы
| Константа | Значение | Назначение |
|-----------|----------|-----------|
| `NAT_TYPE_UNKNOWN` | 0 | Не определён |
| `NAT_TYPE_EIM` | 1 | Endpoint-Independent Mapping |
| `NAT_TYPE_STRICT` | 2 | Address/Restricted или Symmetric |
| `NAT_TYPE_DIRECT` | 3 | Реальный публичный IP (нет NAT) |
| `NAT_VERIFIED_*` (4-7) | — | Верифицированные (опубликованные в nodeinfo) |
### Основные функции
| Функция | Назначение |
|---------|-----------|
| `init_sockets(inst)` | Создать listen-сокеты из `[server]` конфига (до `topo_group_init`) |
| `init_connections(inst)` | Создать клиентские соединения из `[client]` + сокеты если не созданы |
| `etcp_socket_add(inst, srv)` | Создать UDP-сокет из `CFG_SERVER`: bind, non-block, send/recv buffers 4MB |
| `etcp_socket_remove(e_sock)` | Закрыть сокет, удалить все линки, освободить память |
| `etcp_link_new(etcp, conn, addr, is_server)` | Создать ETCP_LINK: выделить local_link_id, BBR, вставить в сокет и ETCP_CONN |
| `etcp_link_close(link)` | Удалить линк: таймеры, удаление из списков сокета и ETCP_CONN |
| `etcp_link_update_inflight_lim(link, new_lim)` | Обновить лимит inflight (clamped к [8K, max_inflight]) |
| `etcp_encrypt_send(dgram)` | Зашифровать dgram (SC_CCM) + UDP-отправка через `etcp_udp_send` |
| `etcp_udp_send(link, fd, buf, len, addr, len)` | Отправка: если `send_hook` — через него, иначе `socket_sendto` |
| `etcp_send_ping(inst, pubkey, addr, timeout, cb, arg, data, len)` | One-shot PING |
| `etcp_send_ping_to_socket(inst, sock, pubkey, addr, ...)` | PING через конкретный сокет |
| `etcp_link_find_by_addr(e_sock, addr)` | Найти линк по адресу (бинарный поиск в `links[]`) |
| `etcp_link_find_by_remote_id(conn, remote_link_id)` | Найти линк по ID пира |
| `etcp_find_free_local_link_id(etcp)` | Найти свободный local_link_id (0-255, битовая карта) |
| `etcp_link_enter_init(link)` | Начать INIT handshake (link_state=1) |
| `etcp_link_enter_reinit(link)` | Начать переподключение (link_state=2, без сброса ETCP) |
### Burst-измерения
| Функция | Назначение |
|---------|-----------|
| `etcp_link_burst_start(link)` | Начать burst-замер bandwidth |
| `etcp_link_burst_check(link)` | Проверить условия для старта burst |
| `etcp_link_burst_finish(link)` | Завершить burst, установить таймер ожидания ответа |
### Константы
| Константа | Значение | Назначение |
|-----------|----------|-----------|
| `INFLIGHT_LIM_MIN` | 8192 | Минимальный inflight (8K) |
| `INFLIGHT_LIM_MAX` | 1048576 | Максимальный inflight (1M) |
| `PACKET_DATA_SIZE` | 1600 | Размер буфера пакета |
| `PACKET_DATA_MAX_MTU` | 1600 | Максимальный MTU |
| `KA_PERIOD_MIN_MS` | 200 | Мин. период keepalive |
| `KA_PERIOD_MAX_MS` | 10000 | Макс. период keepalive |
| `KA_TIMEOUT_MULT` | 10 | Множитель таймаута = period×10 |
| `ACK_REZERV` | 100 | Резерв байт под ACK |
| `INIT_TIMEOUT_INITIAL` | 500ms | Начальный таймаут INIT |
| `INIT_TIMEOUT_MAX` | 50000ms | Максимальный таймаут INIT |

39
src/etcp_debug_doc.md

@ -0,0 +1,39 @@
# ETCP Packet Section Dump (etcp_debug)
## 1. Назначение
Вывод в лог человекочитаемой сводки по ETCP-пакету: направление (SEND/RECV), timestamp, и содержимое каждой секции — ACK (last_delivered, count, rx_dup), CH_TS (возвращаемый и принимаемый channel timestamp), PAYLOAD (размер и seq). Используется для отладки трафика на уровне отдельных пакетов.
## 2. Как пользоваться
```c
#include "etcp_debug.h"
// В обработчике отправки/приёма пакета:
etcp_dump_pkt_sections(pkt, link, 1); // is_send=1 — отправка
etcp_dump_pkt_sections(pkt, link, 0); // is_send=0 — приём
```
Вывод идёт через `DEBUG_INFO(DEBUG_CATEGORY_DUMP, ...)`. Для включения:
```
debug = dump=info
```
### Вспомогательная функция
```c
IP_STR ip = ip_to_string(0x0A000001);
// ip.a == "10.0.0.1"
```
## 3. API
**`IP_STR ip_to_string(uint32_t ip)`** — конвертирует IP (network byte order) в строку `a.b.c.d`. Возвращает структуру с буфером 16 байт.
**`void etcp_dump_pkt_sections(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int is_send)`** — парсит `pkt->data` побайтово и выводит сводку секций: `ETCP_SECTION_ACK` (last_delivered, count, rx_dup_count), `ETCP_SECTION_TIMESTAMP` (ret_ts/recv_ts), `ETCP_SECTION_PAYLOAD` (size, seq). Неизвестные секции прерывают парсинг. Формат вывода:
```
ETCP PKT: SEND ts=12345 ack=100/5 dup=0 ch_ts=200/150 payload=1200(seq=101)
```
### Зависимости
- `etcp_connections.h` — `struct ETCP_DGRAM`, `struct ETCP_LINK`
- `etcp.h` — константы `ETCP_SECTION_*`
- `lib/debug_config.h` — `DEBUG_INFO(DEBUG_CATEGORY_DUMP, ...)`

289
src/etcp_doc.md

@ -0,0 +1,289 @@
# etcp
## 1. Назначение
Ядро протокола ETCP — TCP-подобный надёжный транспорт поверх UDP с шифрованием (AES-CCM + X25519), multi-link (несколько каналов между двумя узлами), фрагментацией/сборкой пакетов, ретрансмиссией с экспоненциальным backoff, BBR congestion control и burst-измерением пропускной способности.
Модуль управляет **одним ETCP-соединением** (struct `ETCP_CONN`) между локальным и удалённым узлом. Соединение может иметь несколько линков (struct `ETCP_LINK`), работающих через разные сокеты / сетевые пути.
Этапы жизни соединения:
```
создание (pending, state=0) → первый линк проинициализирован → ready (state=1) → close (state=2, detach) → ref_count=0 → полное освобождение ресурсов
```
## 2. Как пользоваться
### 2.1. Типовой сценарий
1. **Создание:** `etcp_connection_create(instance, name)` — создаёт `ETCP_CONN` в состоянии `pending` (state=0), размещает в очереди `instance->connections` с индексом peer_node_id=0.
2. **Добавление линков:** `etcp_link_new(etcp, socket, remote_addr, is_server)` — создаёт `ETCP_LINK`, добавляет в список `etcp->links`.
3. **Обмен ключами / INIT:** через `etcp_connections.c` происходит обмен INIT_REQUEST/INIT_RESPONSE, установка `peer_node_id`, инициализация `secure_channel` (X25519 + AES-CCM).
4. **Готовность:** `etcp_conn_ready()` → `etcp_conn_queue_set_ready()` — переиндексирует соединение в очереди `instance->connections` с реальным `peer_node_id`, переводит `state=1`, запускает callback'и `ready_cbks`, таймер метрик.
5. **Отправка данных:** `etcp_int_send(etcp, data, len)` — аллоцирует память из `data_pool`, создаёт `ETCP_FRAGMENT`, помещает в `input_queue`.
6. **Приём данных:** потребитель читает из `etcp->output_queue` — там лежат собранные `ETCP_FRAGMENT` с полезной нагрузкой.
7. **Закрытие:** `etcp_connection_close(etcp)` — 2-фазное: Phase 1 — detach от внешнего мира (линки, таймеры, роутинг), Phase 2 — отложенное освобождение ресурсов (очереди, пулы памяти, callback'и, struct) когда ref_count=0.
### 2.2. Ключевые концепции
#### Очереди данных (data flow)
```
┌──────────────┐
et cp_int_send ──► │ input_queue │ входная очередь (ETCP_FRAGMENT из io_pool)
└──────┬───────┘
│ input_queue_cb: перенос во inflight с присвоением seq
┌──────▼───────┐
│ input_send_q │ очередь на отправку (INFLIGHT_PACKET из inflight_pool)
└──────┬───────┘
│ etcp_request_pkt: формирует ETCP_DGRAM с секциями ACK+PAYLOAD
┌──────▼───────┐
│input_wait_ack│ ожидание ACK (INFLIGHT_PACKET, index по seq)
└──────┬───────┘
│ etcp_ack_recv: ACK получен → освобождение inflight
[удалён]
```
```
Приём:
┌──────────┐
etcp_conn_input ──► │ recv_q │ очередь сборки (ETCP_FRAGMENT, index по seq)
└────┬─────┘
│ etcp_output_try_assembly: поиск непрерывной последовательности
┌────▼──────┐
│output_queue│ собранные пакеты для потребителя
└───────────┘
```
- **`input_queue`** (ETCP_FRAGMENT) — пакеты от приложения к отправке. Callback `input_queue_cb` переносит их в `input_send_q`, создавая `INFLIGHT_PACKET` с seq.
- **`input_send_q`** (INFLIGHT_PACKET, hash-индекс по seq) — ожидающие отправки (новые или ретрансмиссии). Callback `input_send_q_cb` вызывает `etcp_conn_process_send_queue`.
- **`input_wait_ack`** (INFLIGHT_PACKET, hash-индекс по seq) — отправленные пакеты, ожидающие ACK. Callback `wait_ack_cb` запускает retrans-таймер.
- **`recv_q`** (ETCP_FRAGMENT, hash-индекс по seq) — принятые фрагменты, ждущие сборки.
- **`output_queue`** — собранные непрерывные пакеты для потребителя.
- **`ack_q`** (ACK_PACKET, hash-индекс по seq) — неотправленные ACK подтверждения, ожидающие piggyback.
- **`transit_queues`** — транзитные очереди для роутинга (хеш по `src_node_id:dst_node_id`).
#### Ретрансмиссия с экспоненциальным backoff
- Таймаут ретрансмиссии: `timeout = (rtt_avg_10 * K1 + jitter * K2) / 16`, где `K1=32`, `K2=32`.
- Минимальный timeout: 50 tb (5ms), максимальный: 10000 tb (1000ms).
- Пакеты в `input_wait_ack` упорядочены по времени отправки (FIFO). Проверка (`ack_timeout_check`) идёт с головы: как только найден пакет с неистекшим таймаутом — взводится таймер до его истечения, дальше не сканируем.
- Ретрансмит: перенос из `input_wait_ack` в `input_send_q`, инкремент `send_count`, добавление в `send_hist`.
- При ретрансмите старый линк (`last_link`) получает `total_retransmissions++` и вычитание из `inflight_bytes/packets`.
#### RTT/Jitter/Bandwidth метрики
- **RTT:** измеряется через секцию `ETCP_SECTION_TIMESTAMP` — результирующий `rtt_last` на соединение берётся как среднее по всем линкам (rtt_sum/cnt), `rtt_avg_10` — максимум RTT по линкам.
- **Jitter:** per-link экспоненциальное сглаживание разности RTT: `jitter += (|new_rtt - prev_rtt| * 65536 - jitter) / 32`. На соединении — среднее по линкам.
- **TT (transmission time):** время доставки отправленных пакетов, вычисляется из timestamp-секции.
- **RT (recv time):** время доставки принятых пакетов.
- **Bandwidth:** per-link, обновляется из BBR pacing rate и burst-измерений.
#### Burst bandwidth measurement
- Отправитель формирует пачку из `BURST_PACKET_COUNT` (12) пакетов, каждый содержит секцию `MEAS_TS` (type=0x07) с burst_id, flags, порядковым номером и µs-таймстемпом.
- Флаги: `IS_FIRST`, `IS_LAST`, `IS_FILLER` (если нет данных — пакет заполняется FILLER-секцией).
- Пропускаются первые `BURST_SKIP_COUNT` (3) пакета для исключения переходных процессов при замере inter-packet gap.
- Приёмник собирает времена прихода, вычисляет min/avg gap между пакетами, отправляет ответ `MEAS_RESP` (type=0x08) с gap_avg, gap_min, pkt_count.
- Bandwidth вычисляется как: `BW = pkt_size * 8000 / gap_min` (Kbps), BDP = `BW * 1000 / 8 * min_rtt_sec`.
- Минимальный интервал между burst: 500ms. Таймаут ожидания ответа: 2s.
- Ответ может быть piggyback'нут в обычный пакет (вне активного burst).
#### Channel timestamp handling
- Секция `ETCP_SECTION_TIMESTAMP` (type=0x06, 5 байт): добавляется когда есть новые данные о времени приёма от пира.
- Содержит: ret_ts (текущее время пира на момент получения нашего пакета) и dt (разница local_time - timestamp пакета).
- Позволяет вычислять RTT, jitter, tt, rt на всех линках, а также `recv_dt_avg_rx/tx` — экспоненциально сглаженные оценки односторонних задержек.
#### 2-phase deferred cleanup
- **Phase 1** (синхронно в `etcp_connection_close`): detach от внешнего мира — закрытие линков, отмена таймеров, остановка метрик, удаление из роутинга, удаление из очереди `instance->connections`, установка `state=2`.
- **Phase 2** (отложено через `uasync_call_soon`): освобождение очередей, memory_pool'ов, callback-цепочек, struct. Выполняется когда `ref_count == 0`. Если есть внешние ссылки — ждёт `etcp_conn_ref_free()`.
- `ref_count` нужен для безопасного доступа к conn из асинхронных контекстов (коллбэки, роутинг, BGP).
#### Backpressure (пороговое ожидание)
- `input_queue` имеет порог 0 (ждёт полного освобождения) — новые пакеты добавляются только когда очередь пуста.
- `etcp_conn_process_send_queue` пробует протолкнуть данные из `input_queue` → `input_send_q` когда `wait_ack_bytes <= optimal_inflight`.
- При ACK вызывается `input_queue_try_resume` — если `wait_ack_bytes <= optimal_inflight` и input_send_q пуста, возобновляет `input_queue_cb`.
#### Normalizer (фрагментация/сборка)
- `struct PKTNORM` — per-connection фрагментатор/дефрагментатор. Разбивает большие пакеты на части ≤ `frag_size` (MTU - overhead).
- При reinit/reset несобранные фрагменты возвращаются в normalizer для повторной обработки (`etcp_return_inflight_to_normalizer`).
### 2.3. State machine
| Поле | Значения | Описание |
|------|----------|----------|
| `state` | 0=pending, 1=ready, 2=deleted | Внешнее состояние conn |
| `initialized` | 0/1 | Хотя бы один линк прошёл обмен ключами |
| `reset_done` | 0/1 | 0=reinit разрешён, 1=соединение стабильно |
| `tx_state` | DATA_WAIT(1)/LINK_WAIT(2) | DATA_WAIT=можно отправлять, LINK_WAIT=все линки busy |
| `links_up` | 0/1 | Хотя бы один линк в статусе up |
| `got_initial_pkt` | 0/1 | Получен ли первый пакет (seq=1) после reset |
| `routing_exchange_active` | 0-4 | Статус BGP-обмена |
## 3. API
### 3.1. Основные структуры
#### `struct ETCP_CONN` (`etcp.h:140`)
Главная управляющая структура соединения. Содержит:
- **Состояние:** `state`, `ref_count`, `initialized`, `reset_done`, `links_up`, `tx_state`.
- **Очереди:** `input_queue`, `input_send_q`, `input_wait_ack`, `recv_q`, `output_queue`, `ack_q`, `transit_queues`.
- **Пулы памяти:** `inflight_pool` (для INFLIGHT_PACKET), `io_pool` (для ETCP_FRAGMENT).
- **Линки:** связный список `links`, `last_rr_link` (round-robin для load balancer).
- **Крипто:** `crypto_ctx` (secure_channel), `peer_ed25519_pubkey`.
- **Нормалайзер:** `normalizer` (PKTNORM) — фрагментация/сборка.
- **Метрики:** `rtt_last/avg_10/avg_100`, `jitter`, `tt_last`, окна (unacked_bytes, max_inflight, optimal_inflight).
- **Таймеры:** `retrans_timer`, `ack_resp_timer`.
- **Callback-цепочки:** `ready_cbks`, `up_cbks`, `down_cbks`, `bgp_ready_cbk`.
- **Счётчики:** ACK hit/miss, дубликаты, ретрансмиссии, `debug[8]` для live watch.
- **Метрики качества:** `metrics` (гистограммы RTT/потерь с 10-мин снимками, тотальные счётчики).
- **Идентификация:** `log_name` (формат `XXXX→YYYY [name]`), `name` (из конфига), `session_id`.
#### `struct INFLIGHT_PACKET` (`etcp.h:108`)
Пакет в состоянии inflight (отправлен или ждёт отправки). Поля:
- `seq` — sequence number (ID по протоколу).
- `state` — `WAIT_SEND` (в input_send_q) или `WAIT_ACK` (в input_wait_ack).
- `last_link` — последний линк, через который отправлен (для retrans/loss detection).
- `last_timestamp` — время последней отправки (для retrans timeout).
- `send_count` — количество попыток отправки.
- `retrans_req_count` — количество запросов ретрансмиссии.
- `send_hist[8]` — номера линков через которые передавался (кольцевой буфер, send_count — head).
- `delivered_at_send`, `inflight_at_send`, `is_app_limited` — снэпшоты для BBR rate estimation.
#### `struct ETCP_FRAGMENT` (`etcp.h:123`)
Фрагмент данных в очередях ввода/вывода. Поля:
- `seq` — sequence number.
- `timestamp` — timestamp пакета (от удалённой стороны при приёме).
- `ll` — `ll_entry` с dgram (указатель на data_pool) и len.
#### `struct ACK_PACKET` (`etcp.h:129`)
Неотправленное подтверждение приёма. Поля:
- `seq` — sequence number подтверждаемого пакета.
- `pkt_timestamp` — timestamp подтверждаемого пакета (часы удалённой стороны).
- `recv_timestamp` — локальное время приёма (для вычисления задержки ACK).
#### `struct etcp_metrics` (`etcp.h:70`)
Гистограммы и счётчики качества соединения:
- **Рабочие гистограммы:** `rtt_hist[12]`, `loss_hist[11]` — обновляются в реальном времени.
- **Финальные снимки:** `rtt_hist_final[12]`, `loss_hist_final[11]` — копируются раз в 10 мин.
- **Счётчики окна:** `work_samples/sent/rcvd/bytes_sent/bytes_rcvd/lost` — сбрасываются при снимке.
- **Тотальные:** `total_sent/rcvd/bytes_sent/bytes_rcvd/lost` — кумулятивные счётчики.
- **RTT-бакеты:** 0-1, 1-2, 2-3, 3-5, 5-8, 8-13, 13-21, 21-34, 34-55, 55-90, 90-150, 150+ ms (в 0.1ms).
- **Loss-бакеты:** 0%, 1%, ..., 10%+.
- Интервал снимка: 10 мин. Минимум пакетов для обновления: 200.
### 3.2. Основные функции
#### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `etcp_connection_create(instance, name)` | Создаёт ETCP_CONN, очереди, пулы, нормалайзер, помещает в `instance->connections` с key=0 (pending). |
| `etcp_connection_close(etcp)` | Phase 1: detach (линки, таймеры, роутинг). Phase 2: deferred free когда ref_count=0. |
| `etcp_conn_ref_take(conn)` | Увеличивает ref_count. Возвращает -1 если conn удалён. |
| `etcp_conn_ref_free(conn)` | Уменьшает ref_count. Если 0 и state==2 — планирует Phase 2. |
| `etcp_conn_reset(etcp)` | Сброс состояния после reinit: seq, метрики, очистка очередей, возврат данных в нормалайзер. |
| `etcp_conn_reinit(etcp)` | Полный reinit: сброс флагов, сброс NAT, сброс роутера, вызов `etcp_conn_reset`. |
| `etcp_links_reset(etcp)` | Сброс флага initialized у всех линков (после сбоя). |
| `etcp_conn_ready(conn)` | Вызывается когда первый линк проинициализирован. Устанавливает `initialized=1`, `reset_done=1`, запускает `conn_queue_set_ready`. |
| `etcp_conn_queue_set_ready(conn)` | Переиндексирует соединение в `instance->connections` с реальным `peer_node_id`, state=1, запускает ready/up callback'и, стартует таймер метрик. |
| `etcp_conn_set_peer_node_id(conn, id)` | Устанавливает peer_node_id. Если state=1 — переиндексирует в очереди. |
| `etcp_conn_on_inflight_lim_changed(etcp)` | Пересчитывает `optimal_inflight` как сумму `inflight_lim_bytes` всех линков. |
| `etcp_update_mtu(etcp)` | Пересчитывает MTU соединения как минимум MTU всех линков. Обновляет `frag_size` нормалайзера. |
#### Отправка
| Функция | Описание |
|---------|----------|
| `etcp_int_send(etcp, data, len)` | Отправка данных через ETCP: аллокация из data_pool, создание ETCP_FRAGMENT, помещение в input_queue. |
| `etcp_request_pkt(etcp)` | Формирует ETCP_DGRAM для отправки: выбирает линк через load balancer, берёт INFLIGHT_PACKET из input_send_q, добавляет ACK-секцию, опциональные секции (MEAS_TS, MEAS_RESP, TIMESTAMP, FILLER), payload. Перемещает inflight в wait_ack. |
| `etcp_encrypt_send(dgram)` | (etcp_connections.c) Шифрует ETCP_DGRAM (AES-CCM), добавляет nonce/tag/crc32/pubkey, отправляет через udp_send. |
#### Приём
| Функция | Описание |
|---------|----------|
| `etcp_conn_input(pkt)` | Обработка расшифрованного пакета: разбор секций (ACK, TIMESTAMP, PAYLOAD, MEAS_TS, MEAS_RESP, FILLER, METRICS), вызов `etcp_ack_recv`, добавление данных в `recv_q`/`ack_q`. |
| `etcp_ack_recv(etcp, seq, ts, dts)` | Обработка ACK: поиск пакета в `input_wait_ack`/`input_send_q`, вычитание inflight с линка, BBR rate estimation, освобождение памяти. |
| `etcp_output_try_assembly(etcp)` | Сборка выходной очереди: ищет непрерывную последовательность от `last_delivered_id+1` в `recv_q`, переносит в `output_queue`. |
| `etcp_stats(etcp)` | Вывод статистики в debug-лог: размеры очередей, RTT, счётчики, ID. |
#### Callback'и / События
| Функция | Описание |
|---------|----------|
| `etcp_on_link_down(etcp)` | Вызывается при падении линка. Проверяет все линки, если все down — вызывает `etcp_on_down`. |
| `etcp_on_up(etcp)` | Запуск цепочки `up_cbks`. |
| `etcp_on_down(etcp)` | Запуск цепочки `down_cbks`. |
#### Метрики качества
| Функция | Описание |
|---------|----------|
| `etcp_metrics_init(m)` | Обнуление структуры метрик. |
| `etcp_metrics_add_rtt(etcp, rtt_tb)` | Добавление RTT-замера в гистограмму. |
| `etcp_metrics_add_sent(etcp, len)` | Учёт отправленного пакета (working + total). |
| `etcp_metrics_add_rcvd(etcp, len)` | Учёт принятого пакета. |
| `etcp_metrics_add_loss(etcp, count)` | Учёт потерь. |
| `etcp_metrics_start_timer(etcp)` | Запуск 10-минутного таймера снимков. |
| `etcp_metrics_stop_timer(etcp)` | Остановка таймера метрик. |
### 3.3. Константы
| Константа | Значение | Описание |
|-----------|----------|----------|
| `MAX_INFLIGHT_BYTES` | 65536 | Начальное окно |
| `RETRANS_K1` | 32 | Множитель RTT для retrans timeout |
| `RETRANS_K2` | 32 | Множитель jitter для retrans timeout |
| `ACK_DELAY_TB` | 20 | Задержка отправки ACK (2ms) |
| `RTT_HISTORY_SIZE` | 10 | Размер истории RTT для jitter |
| `INFLIGHT_INITIAL_HASH_SIZE` | 1024 | Начальный размер хеш-таблиц |
| `MAX_INFLIGHT_SIZE` | 16384 | Максимум элементов в recv_q (защита от атак) |
| `ASM_BUF_MAX_SIZE` | 65536 | Максимальный размер буфера сборки |
| `BURST_PACKET_COUNT` | 12 | Пакетов в burst |
| `BURST_SKIP_COUNT` | 3 | Пропускаемых пакетов при замере gap |
| `MIN_BURST_INTERVAL_TB` | 5000 | Мин. интервал между burst (500ms) |
| `BURST_RESP_TIMEOUT_TB` | 20000 | Таймаут ответа на burst (2s) |
| `ETCP_METRICS_RTT_BUCKETS` | 12 | Число RTT-бакетов в гистограмме |
| `ETCP_METRICS_LOSS_BUCKETS` | 11 | Число loss-бакетов |
| `ETCP_METRICS_INTERVAL_TB` | 6000000 | Интервал снимков (10 мин) |
| `ETCP_METRICS_MIN_PACKETS` | 200 | Мин. пакетов для снимка |
### 3.4. Секции пакета
| Секция | Type | Размер | Описание |
|--------|------|--------|----------|
| `PAYLOAD` | 0x00 | 5 + data | Данные: type(1) + seq(4) + payload |
| `ACK` | 0x01 | 8 + N*8 | ACK: type(1) + count(1) + last_delivered_id(4) + rx_dup_count(2) + N×{seq(4) + recv_ts(2) + delay(2)} |
| `TIMESTAMP` | 0x06 | 5 | Channel timestamp: type(1) + ret_ts(2) + dt(2) |
| `MEAS_TS` | 0x07 | 9 | Burst measurement: type(1) + burst_id(2) + flags(1) + seq(1) + ts_us(2) + pkt_sz(2) |
| `MEAS_RESP` | 0x08 | 13 | Burst response: type(1) + burst_id(2) + valid(1) + gap_avg(4) + gap_min(4) + pkt_count(1) |
| `FILLER` | 0x09 | 3 + N | Filler padding: type(1) + len(2) + zeros(N) |
| `METRICS` | 0x0A | 2 + 64 + N | Metrics CSV: type(1) + csv_len(2) + ed25519_sig(64) + csv_data(N) |
### 3.5. Зависимости
| Модуль | Как используется |
|--------|-----------------|
| `etcp_connections.h` | ETCP_LINK, ETCP_DGRAM, ETCP_SOCKET, etcp_link_new/close, etcp_encrypt_send |
| `etcp_loadbalancer.h` | etcp_loadbalancer_select_link, etcp_loadbalancer_send |
| `etcp_router.h` | etcp_router_transit_queues_destroy, etcp_router_pause_retrans_for_node |
| `etcp_debug.h` | etcp_dump_pkt_sections |
| `etcp_connect.h` | etcp_connect_cancel_for_conn |
| `pkt_normalizer.h` | PKTNORM (pn_init, pn_deinit, pn_reset) |
| `secure_channel.h` | sc_context_t для шифрования |
| `routing.h` | routing_del_conn |
| `topo_group.h` | route_ping_cancel_for_conn |
| `../lib/ll_queue.h` | Все очереди |
| `../lib/u_async.h` | uasync_set_timeout, uasync_call_soon |
| `../lib/memory_pool.h` | memory_pool_alloc/free/init/destroy |
| `../lib/mem.h` | u_malloc, u_calloc, u_realloc, u_free, u_strdup |
| `../lib/debug_config.h` | DEBUG_ERROR/WARN/INFO/DEBUG/TRACE |

59
src/etcp_dump_doc.md

@ -0,0 +1,59 @@
# ETCP Connection State Dump (etcp_dump)
## 1. Назначение
Диагностический дамп полного состояния ETCP-соединения в stdout. Охватывает: общие параметры, очереди, пулы памяти, normalizer, все линки с детализацией (BBR, shaper, burst, ошибки, таймеры, NAT). Используется для отладки проблем с соединениями — удобно вызывать из control-сервера или по сигналу.
## 2. Как пользоваться
```c
#include "etcp_dump.h"
// Дамп одного соединения:
etcp_dump_conn_state(conn);
// Дамп всех соединений инстанса:
etcp_dump_all_conns(instance);
```
Вывод идёт в `stdout` через `fprintf`, формат структурированный с отступами. Пример фрагмента:
```
=== ETCP CONN STATE [myconn] ===
GENERAL: ... peer=0x... initialized=1 links_up=2 tx_state=0 ...
IDS: next_tx=1050 last_rx=1048 last_del=1040 rx_ack_till=1045
RTT: last=15 avg10=14 avg100=16 jitter=2
STATS: bytes_sent=123456 retrans=3 reinit=0 reset=0 ack_pkts=200
INFLIGHT: unacked=8192 optimal=65536
ACK_DEBUG: hit_inf=500 hit_sndq=10 miss=0 link_wait=0 rx_dup=0 tx_dup=0
TIMERS: retrans=ACTIVE ack_resp=free
--- QUEUES ---
input 5 pkts / 4096 bytes
input_send_q 2 pkts / 2048 bytes
...
--- NORMALIZER ---
frag=1400 data_ptr=0/0 flush=free pending=free ...
--- POOLS ---
inflight alloc=100 reuse=50
...
--- LINK 0 ---
BASIC: id=0/0 is_server=0 link_state=3 link_status=1 ...
BBR: bytes=4096 pkts=2 lim=65536 mode=2 cycle=2 pacing=12500000
RTT: last=12 jitter=3 min_rtt_us=5000 bw_lo=... bw_hi=... inflight_lo=... inflight_hi=...
...
--- LINK 1 ---
...
```
## 3. API
**`void etcp_dump_conn_state(struct ETCP_CONN* conn)`** — полный дамп одного соединения. Выводит секции: GENERAL, IDS, RTT, STATS, INFLIGHT, ACK_DEBUG, DEBUG, TIMERS, QUEUES (6 очередей), NORMALIZER (pkt_normalizer + его очереди), POOLS (inflight_pool, io_pool, instance->data/ack/pkt_pool), LINKS (в цикле по связанному списку link->next, для каждого: BASIC, ADDR, KA, TIMERS, BBR, RTT, TT/RT, MTU, ERRORS, NAT, LAST_RECV, HANDSHAKE, BURST, SHAPER).
**`void etcp_dump_all_conns(struct UTUN_INSTANCE* instance)`** — итерация по `instance->connections` (через `ll_entry`), вызывает `etcp_dump_conn_state` для каждого активного соединения.
### Зависимости
- `etcp.h` — `struct ETCP_CONN`, поля состояния, очереди, normalizer
- `etcp_connections.h` — `struct ETCP_LINK`
- `pkt_normalizer.h` — `struct PKTNORM`
- `lib/ll_queue.h` — `queue_entry_count()`, `queue_total_bytes()`
- `lib/memory_pool.h` — `memory_pool_get_stats()`
- `lib/socket_compat.h` — `AF_INET`, `sockaddr_in`, `ntohs`
- `utun_instance.h` — `struct UTUN_INSTANCE`

51
src/etcp_loadbalancer_doc.md

@ -0,0 +1,51 @@
# ETCP Load Balancer (etcp_loadbalancer)
## 1. Назначение
Балансировка исходящего трафика ETCP между несколькими линками (UDP-сокетами) внутри одного ETCP_CONN. Выбирает оптимальный линк по минимальному `inflight_bytes`, при равенстве — round-robin. Для каждого линка работает traffic shaper — ограничение полосы пропускания (bandwidth pacing) с таймерной задержкой и burst-режимом.
## 2. Как пользоваться
Типовой сценарий: перед отправкой пакета вызывается `etcp_loadbalancer_send(dgram)`. Функция сама выбирает линк (если он ещё не задан), отправляет и обновляет shaper-нагрузку линка.
```c
// Отправка пакета через loadbalancer
struct ETCP_DGRAM* dgram = memory_pool_alloc(inst->pkt_pool);
// ... заполнение dgram ...
etcp_loadbalancer_send(dgram); // сам выберет линк, зашифрует, отправит, освободит dgram
```
Если все линки заняты (shaper-таймер или inflight превышает лимит), пакет дропается. При освобождении линка shaper-таймер вызывает `loadbalancer_link_ready()`, который нотифицирует `ETCP_CONN->link_ready_for_send_fn`.
```c
// Проверка статуса связи
if (etcp_loadbalancer_get_link_status(etcp)) {
// есть хотя бы один живой линк
}
```
**Нюансы:**
- Алгоритм выбора: min `inflight_bytes`, среди равных — round-robin (поле `last_rr_link` в ETCP_CONN)
- Shaper работает через `shaper_load_time_tb` / `shaper_sub_nanotime` — виртуальное время передачи, сравнивается с `now_tb + SHAPER_BURST_DELAY_TB` (1ms)
- Burst-пакеты (`link->burst_active`) обходят shaper
- При безлимитном bandwidth (`link->bandwidth == 0`) shaper пропускается
- `etcp_loadbalancer_send()` освобождает dgram через `memory_pool_free()` — после вызова dgram недействителен
## 3. API
### Структуры
Поля ETCP_LINK, используемые балансировщиком:
- `initialized` — линк готов к работе
- `link_status` — 1 = линк жив
- `inflight_bytes` — байт в полёте (минимизируемый критерий)
- `burst_active` — 1 = burst bypass всех ограничений
- `shaper_timer` — активный таймер задержки shaper'а
- `bandwidth` — Kbps, 0 = безлимит
- `shaper_load_time_tb`, `shaper_sub_nanotime` — аккумулированное время передачи (0.1ms timebase)
### Функции
| Функция | Описание |
|---|---|
| `etcp_loadbalancer_select_link(etcp)` | Выбрать линк по min inflight_bytes с round-robin. NULL если все заняты |
| `etcp_loadbalancer_send(dgram)` | Выбрать линк → зашифровать/отправить → обновить shaper → освободить dgram |
| `loadbalancer_link_can_send(link)` | 1 если линк не заблокирован shaper'ом и burst не активен |
| `loadbalancer_link_ready(link)` | Уведомить ETCP_CONN о готовности линка (вызывает `link_ready_for_send_fn`) |
| `etcp_loadbalancer_get_link_status(etcp)` | 1 = есть живой линк, 0 = все недоступны |

131
src/etcp_router_doc.md

@ -0,0 +1,131 @@
# ETCP Router (etcp_router)
## 1. Назначение
Сервисный слой маршрутизации поверх ETCP — упрощённый TCP с восстановлением порядка доставки, дедупликацией и контролем inflight. Мультиплексирует до 256 сервисов (`svc_id`) на одном ETCP-соединении. Поддерживает transit (многошаговую маршрутизацию через промежуточные узлы), ретрансмиты, minRTT-измерения и подписи Ed25519.
## 2. Как пользоваться
### Инициализация
```c
// В utun_instance_init():
etcp_router_init(inst);
// Зарегистрировать обработчик сервиса (svc_id 0..255)
etcp_router_bind(inst, svc_id, my_service_callback);
```
### Отправка
```c
// Простой способ: сформировать ll_entry с svc_id в первом байте
struct ll_entry* e = queue_entry_new(0);
e->dgram = u_malloc(1 + payload_len);
e->dgram[0] = svc_id;
memcpy(e->dgram + 1, payload, payload_len);
e->len = 1 + payload_len;
etcp_route_send(inst, dst_node_id, e, force);
// Или через существующий ROUTER_CONN:
struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, remote_node_id, svc_id);
etcp_router_conn_send(rconn, data, len);
```
### Приём
```c
void my_service_callback(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry) { /* соединение закрыто */ return; }
uint8_t svc_id = entry->dgram[0];
uint64_t from = *(uint64_t*)(entry->dgram + 1); // remote_node_id
// ... данные начиная с entry->dgram[9] ...
queue_dgram_free(entry);
queue_entry_free(entry);
}
```
### Backpressure
```c
// На send_q (inflight переполнен):
struct queue_waiter_handle h;
etcp_router_on_send_ready(inst, node_id, svc_id, &h, my_ready_cb, my_arg);
// Когда send_q освободится — вызовется my_ready_cb
// На normalizer->input (сетевая очередь):
etcp_router_waiter_register(inst, node_id, &h, my_ready_cb, my_arg);
```
### Подписанные сообщения
```c
etcp_router_conn_send_signed(rconn, data, len); // добавляет Ed25519-подпись
```
## 3. API
### Ключевые структуры
**SVC_ROUTE_HDR** (25 байт) — заголовок пакета:
`cmd(1) + dst_node_id(8) + src_node_id(8) + seq(4) + svc_id(1) + flags(1) + timestamp(2)`
Флаги: `ROUTER_FLAG_START` (0x80), `ROUTER_FLAG_RST` (0x40), `ROUTER_FLAG_SIGNED` (0x08), `ROUTER_FLAG_CLOSE` (0x02). Sess_id в bits 5-4.
**ETCP_ROUTER_CONN** — состояние логического подключения (remote_node_id + svc_id):
- `tx_seq`, `rx_seq`, `tx_acked` — seq-нумерация для порядка и inflight
- `recv_q` — reorder-очередь с хеш-индексом по seq (восстановление порядка)
- `send_q` — очередь ожидания при полном inflight (backpressure)
- `inflight_q` — копии отправленных пакетов для ретрансмита (хеш по seq)
- `incoming_q` — FIFO между сетевым приёмом и recv_q (защита от гонок)
- `rtt`, `rtt_jitter`, `minrtt` — измерения задержки
- `inflight_limit` — текущий лимит пакетов в полёте (minrtt_probe снижает до 4)
- `no_ack_count` — последовательные ретрансмиты без ACK (при 17 — закрытие)
- `start_sent`, `peer_sync_done` — синхронизация после (пере)подключения
- `closed` — флаг закрытия (игнорирование таймеров)
**ROUTER_INFLIGHT** — запись в inflight_q: `seq, last_sent_tb, send_count, payload*`
**TRANSIT_QUEUE** — per (src,dst) пара на промежуточном узле:
- `q` — FIFO транзитных пакетов
- `waiter` — backpressure на send_input_q next_hop'а
- `conn` — ETCP_CONN следующего шага
### Внутренняя архитектура
```
Приём: сеть → incoming_q (FIFO) → router_incoming_q_cb → recv_q (хеш по seq)
↓
router_try_assembly → deliver (rx_seq++)
Отправка: router_enqueue_send → send_q (FIFO, backpressure)
router_drain_send_q → router_send_one → etcp_send + inflight_q (копия)
↓
router_track_inflight_state + retrans_schedule
ACK: периодический (10ms) + idle (500ms) → router_send_ack(rx_seq)
```
### Константы
| Константа | Значение | Описание |
|---|---|---|
| `ROUTER_MAX_INFLIGHT` | 256 | Макс. пакетов в полёте |
| `ROUTER_ACK_INTERVAL_TB` | 100 | ACK интервал 10ms (0.1ms timebase) |
| `ROUTER_ACK_IDLE_TB` | 5000 | Idle таймаут 500ms |
| `ROUTER_RETRANS_TIMEOUT_TB` | 3000 | Таймаут ретрансмита 300ms |
| `ROUTER_NO_ACK_MAX_RETRANS` | 17 | Макс. ретрансмитов без ACK (≈5s) |
| `ROUTER_MAX_SEND_Q_PACKETS` | 64 | Порог backpressure send_q |
### Функции
| Функция | Описание |
|---|---|
| `etcp_router_init(inst)` | Инициализация: bind ETCP_ID_SVC_ROUTE, создание router_conns |
| `etcp_router_destroy(inst)` | Деинициализация: unbind + close_all + free |
| `etcp_router_bind(inst, svc_id, cb)` | Зарегистрировать обработчик сервиса |
| `etcp_router_unbind(inst, svc_id)` | Удалить обработчик сервиса |
| `etcp_route_send(inst, dst, entry, force)` | Отправить пакет (loopback/transit/direct) |
| `etcp_router_conn_get(inst, remote, svc_id)` | Найти или создать ROUTER_CONN |
| `etcp_router_conn_send(rconn, data, len)` | Отправить данные с авто-seq и inflight-контролем |
| `etcp_router_conn_send_signed(rconn, data, len)` | Отправить с Ed25519-подписью |
| `etcp_router_conn_close(rconn)` | Закрыть seq-подключение (CLOSE + уведомление сервиса) |
| `etcp_router_conn_close_async(rconn, cb, arg)` | Асинхронное закрытие с коллбэком |
| `etcp_router_conn_close_all_for_node(inst, node_id)` | Закрыть все conn к узлу |
| `etcp_router_conn_restart(inst, remote, svc_id)` | Сброс состояния (перезапуск удалённой стороны) |
| `etcp_router_pause_retrans_for_node(inst, node_id)` | Сбросить ретрансмиты без закрытия (для conn reinit) |
| `router_set_max_inflight(rconn, new_max)` | Установить рабочий max_inflight |
| `etcp_router_input_q_count(inst, node_id)` | Размер normalizer->input очереди узла |
| `etcp_router_waiter_register/cancel(inst, node_id, h, cb, arg)` | Backpressure на normalizer->input |
| `etcp_router_on_send_ready/cancel_send_ready(inst, node, svc, h, cb, arg)` | Backpressure на send_q |
| `etcp_router_transit_queues_destroy(conn)` | Удалить все транзитные очереди ETCP_CONN |

28
src/firewall_doc.md

@ -0,0 +1,28 @@
# Firewall rule engine
## 1. Назначение
Фильтр пакетов по IP/порту для входящего/исходящего трафика. Правила загружаются из конфига, сортируются по IP и порту, затем проверка идёт бинарным поиском. Используется для разрешения/запрета прохождения пакетов через туннель.
## 2. Как пользоваться
```c
struct firewall_ctx fw;
fw_init(&fw);
fw_load_rules(&fw, &global_cfg);
if (fw_check(&fw, ip_host, port)) { /* пакет разрешён */ }
fw_free(&fw);
```
Правила задаются в конфиге в секции `[firewall]`:
- `rule = ip port` — разрешить IP:port (port=0 — любой порт)
- `bypass_all = 1` — пропускать всё независимо от правил
Сортировка — по IP, затем по порту. При проверке: бинарный поиск по IP, затем линейный скан соседних записей с тем же IP для сверки порта. Правило с `port=0` означает любой порт для этого IP.
## 3. API
| Функция/Структура | Описание |
|---|---|
| `struct firewall_ctx` | Набор правил (`rules`), их количество (`count`), флаг `bypass_all` |
| `fw_init(ctx)` | Обнуление контекста |
| `fw_load_rules(ctx, cfg)` | Копирование правил из конфига, сортировка qsort, установка bypass_all |
| `fw_check(ctx, ip, port)` | Возвращает 1 если пакет разрешён (bypass_all=1 или IP+port есть в правилах), 0 — запрещён |
| `fw_free(ctx)` | Освобождение памяти, обнуление полей |

72
src/lwip_tcp/lwip_pbuf_doc.md

@ -0,0 +1,72 @@
# lwip_pbuf
## 1. Назначение
Упрощённая реализация pbuf (packet buffer) для встроенного lwIP TCP стека — только PBUF_RAM.
В отличие от оригинального lwIP (где pbuf может быть RAM/ROM/Pool/Queue), здесь каждый pbuf — это единый блок `u_malloc`, содержащий: `struct pbuf` + зарезервированное место под заголовки (layer-зависимый offset) + полезные данные (payload). Поддерживаются цепочки pbuf через `next`, ref-counted управление памятью, сдвиг payload pointer через `pbuf_header`, цепочечное копирование данных.
Используется модулями `lwip_tcp`, `tcp_proxy_server`, `tcp_proxy_client` для формирования и разбора TCP-сегментов.
## 2. Как пользоваться
Типовой сценарий:
```c
// Создать pbuf с запасом под заголовки
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, data_len); // offset = 54 байта
// Записать заголовки (сдвиг payload назад в зарезервированную область)
pbuf_header(p, -54); // payload теперь в начале блока
memcpy(p->payload, &tcp_header, 54);
pbuf_header(p, 54); // вернуть payload на данные
// Заполнить данные
memcpy(p->payload, user_data, data_len);
// Прикрепить ещё сегментов
struct pbuf *tail = pbuf_alloc(PBUF_RAW, more_data_len);
memcpy(tail->payload, more_data, more_data_len);
pbuf_chain(p, tail); // p->tot_len = data_len + more_data_len
// Отправить… (потом освободить)
pbuf_free(p); // уменьшает ref, освобождает при ref==0
```
Ключевые концепции:
- **PBUF_RAM-only**: один `u_malloc` на pbuf (структура + зарезервированный offset + данные). Нет пулов, нет ROM-буферов.
- **Ref-counted**: `pbuf_ref()` +1, `pbuf_free()` -1, освобождение при 0. `pbuf_free()` рекурсивно проходит цепочку.
- **pbuf_header(increment)**: сдвиг `payload`-указателя. Положительный increment — сужение данных (потребление заголовка). Отрицательный — расширение в зарезервированную область (создание заголовка). Проверяет границы через доступный offset (разницу между `payload` и `(uint8_t*)p + sizeof(struct pbuf)`).
- **pbuf_header_force()**: то же, но без проверки границ при положительном increment (форсированное переопределение границ).
- **Цепочка (chain)**: связный список через `p->next`. `tot_len` — суммарная длина всей цепочки, `len` — длина текущего сегмента. `pbuf_chain()` добавляет в конец, `pbuf_dechain()` отрезает первый от остальных и корректирует `tot_len`.
- **pbuf_copy_partial()/pbuf_take_at()**: работают с цепочкой прозрачно — пропускают сегменты по смещению, копируют чанками.
## 3. API
### Структуры
| Структура | Описание |
|-----------|----------|
| `struct pbuf` | Элемент пакетного буфера: `next` (цепочка), `payload` (указатель на данные), `len` (длина сегмента), `tot_len` (суммарная длина цепочки), `ref` (счётчик ссылок), `flags`, `type_internal` |
| `PBUF_FLAG_TCP_FIN` | Флаг (0x01) — маркер FIN-сегмента TCP в поле `flags` |
### Перечисления
| Enum | Значения | Описание |
|------|----------|----------|
| `enum pbuf_layer` | `PBUF_TRANSPORT=54`, `PBUF_IP=34`, `PBUF_LINK=14`, `PBUF_RAW_TX=54`, `PBUF_RAW=0` | Зарезервированный offset под заголовки перед payload |
### Функции
| Функция | Описание |
|---------|----------|
| `pbuf_alloc(layer, length)` | Выделить pbuf с зарезервированным offset (layer) и данными длины length. Возвращает NULL при ошибке. |
| `pbuf_free(p)` | Рекурсивно пройти цепочку, уменьшить ref, освободить `u_free` при ref==0. |
| `pbuf_ref(p)` | Инкремент счётчика ссылок. |
| `pbuf_header(p, increment)` | Сдвинуть `payload` на increment байт. +increment = освободить место под заголовок, -increment = добавить заголовок (негативный сдвиг). Проверяет границы выделенной памяти. Возвращает 0 / -1. |
| `pbuf_header_force(p, increment)` | То же, но без проверки границ при положительном increment (форсирование). |
| `pbuf_chain(head, tail)` | Присоединить цепочку tail в конец head (head->tot_len не обновляется автоматически — caller делает это сам). |
| `pbuf_dechain(p)` | Отсоединить первый pbuf от цепочки: обнулить `next`, скорректировать `tot_len = len`. Возвращает остаток цепочки. |
| `pbuf_clen(p)` | Количество сегментов в цепочке. |
| `pbuf_copy_partial(p, buf, len, offset)` | Скопировать до len байт из цепочки p начиная со смещения offset в линейный буфер buf. Возвращает реально скопированное количество. |
| `pbuf_take(p, data, len)` | Записать len байт из data в цепочку p (начиная с offset 0). Обёртка над `pbuf_take_at`. |
| `pbuf_take_at(p, data, len, offset)` | Записать len байт из data в цепочку p начиная со смещения offset. Возвращает 0 при успехе, -1 если данных недостаточно. |
| `pbuf_get_contiguous(p, buf, len, offset)` | Скопировать len байт из цепочки в buf (как `pbuf_copy_partial`, но возвращает buf при успехе или NULL). |
### Зависимости
- `../../lib/mem.h` — `u_malloc`/`u_free` (wrappers с leak tracking)
- `string.h` — `memcpy`

461
src/lwip_tcp/lwip_tcp_doc.md

@ -0,0 +1,461 @@
# lwip_tcp
## 1. Назначение
Встроенный lwIP TCP стек, адаптированный для uTun. Полноценная реализация RFC 793 (TCP) с минимальным набором опций (только MSS), context-based архитектурой, single-threaded исполнением и интеграцией с event loop uasync. Не использует глобальных переменных lwIP — весь контекст изолирован в `struct lwip_tcp_ctx` (один экземпляр на TCP-прокси).
Модуль состоит из 4 файлов:
- `lwip_tcp.c` — ядро TCP: жизненный цикл, таймеры, выделение PCB, управление состояниями
- `lwip_tcp_out.c` — отправка сегментов: `tcp_write`, `tcp_output`, `tcp_enqueue_flags`, ретрансмиссии, RST, ACK, zero-window probe
- `lwip_tcp_in.c` — приём сегментов: `lwip_tcp_input` → разбор заголовка, маршрутизация по PCB, `tcp_process`/`tcp_receive`, OOSEQ, ACK-обработка
- `lwip_pbuf.c` — упрощённый packet buffer (PBUF_RAM), используемый как хранилище данных сегментов
- `lwip_tcp_opts.h` — жёстко заданные константы конфигурации
- `lwip_tcp_priv.h` — внутренний API: wire-формат TCP заголовка, структура сегмента, макросы событий, функции для `_in` и `_out`
Ключевые свойства:
- **Context-based:** весь TCP-стек инкапсулирован в `struct lwip_tcp_ctx`, никаких глобальных переменных
- **Single-threaded:** все операции в одном потоке, управляемом через `uasync`
- **uasync-driven:** таймер `tcp_tmr_cb` каждые `tmr_interval_ms` (по умолчанию 62.5ms = TCP_TMR_INTERVAL/4, ускоритель) вызывает `tcp_fasttmr` и каждый нечётный вызов — `tcp_slowtmr`
- **Output callback:** `ctx->output(pbuf, src_ip, dst_ip)` — вызывается когда TCP хочет отправить IP-пакет. Пользователь сам реализует инкапсуляцию в IP-заголовок и передачу
- **Memory pools:** пулы для `tcp_pcb`, `tcp_pcb_listen`, `tcp_seg` (выделение за O(1) из предварительно аллоцированной памяти)
- **Trace ring buffer:** 384 записи для отладки поведения TCP (поддерживает дамп)
## 2. Как пользоваться
### Типовой сценарий работы
```
lwip_tcp_init(ua, output_fn, arg) — инициализация контекста
└→ tcp_new(ctx) — создание TCP PCB
└→ tcp_bind(pcb, ip, port) — бинд локального адреса/порта
└→ tcp_listen(pcb) — перевод в LISTEN (сервер)
└→ tcp_connect(pcb, ip, port, cb) — исходящее соединение (клиент)
lwip_tcp_input(ctx, pbuf, src_ip, dst_ip) — подача входящего IP-пакета
→ разбор TCP-заголовка, диспетчеризация по активным PCB
tcp_write(pcb, data, len, flags) — отправка данных
→ данные попадают в pcb->unsent (очередь на отправку)
tcp_output(pcb) — отправка unsent сегментов через output callback
→ соблюдение окна перегрузки (cwnd) и окна приёмника (snd_wnd)
→ алгоритм Nagle (можно отключить через tcp_nagle_disable)
tcp_close(pcb) / tcp_abort(pcb) — закрытие соединения
lwip_tcp_destroy(ctx) — полная очистка контекста
```
### Таймеры
Единый таймер `tcp_tmr_cb` (через `uasync_set_timeout`) вызывается каждые `tmr_interval_ms`:
- **Fast timer** (`tcp_fasttmr`): каждый вызов. Delayed ACK (250ms), close-pending retry, повторная подача refused_data
- **Slow timer** (`tcp_slowtmr`): каждый нечётный вызов (каждые 2×tmr_interval_ms = 125ms по умолчанию). Ретрансмиссия (RTO, backoff), persist timer (zero-window probe), таймауты FIN_WAIT_2/SYN_RCVD/LAST_ACK, очистка TIME_WAIT, OOSEQ timeout, poll-события
`lwip_tcp_set_timer(ctx, interval_ms, rto_min_ms, rto_max_ms)` позволяет настроить интервал и лимиты RTO.
### Ключевые концепции
**4 списка PCB в контексте:**
- `bound_pcbs` — привязанные к порту, но без активного соединения (CLOSED после bind)
- `listen_pcbs` — слушающие (LISTEN), тип `tcp_pcb_listen` (облегчённая структура)
- `active_pcbs` — активные соединения (SYN_SENT..LAST_ACK)
- `tw_pcbs` — TIME_WAIT (ожидание 2×MSL)
**10 TCP состояний (enum tcp_state):**
```
CLOSED → LISTEN (bind + listen)
CLOSED → SYN_SENT → SYN_RCVD → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
→ CLOSE_WAIT → LAST_ACK → CLOSED
→ CLOSING → TIME_WAIT → CLOSED
```
**Send buffer и очереди сегментов:**
- `snd_buf` — максимальный объём данных, который можно принять от приложения через `tcp_write` (16×MSS = 23360 байт)
- `unsent` — очередь сегментов, готовых к отправке (не отправлены)
- `unacked` — отправленные, но ещё не подтверждённые сегменты
- `ooseq` — out-of-order сегменты, ожидающие сборки
- `snd_queuelen` — количество pbuf'ов в unsent (лимит: 64 = 4×SND_BUF/MSS)
**RTT estimation (Van Jacobson):**
- `rttest` — время отправки сегмента для текущего замера RTT
- `sa`, `sv` — сглаженное среднее RTT и вариация (scaled, /8 и /4)
- `rto` — retransmission timeout (в единицах slow timer = 250ms)
- `rtime` — счётчик тиков с момента последней отправки
**Алгоритм Nagle:**
- Включён по умолчанию (`TF_NODELAY` не установлен)
- Объединяет маленькие сегменты, если есть неподтверждённые данные
- Отключается: `tcp_nagle_disable(pcb)` → устанавливает `TF_NODELAY`
**Congestion control (NewReno-like):**
- `cwnd` — congestion window, инициализируется 1 сегментом (slow start)
- `ssthresh` — порог slow start/congestion avoidance
- `dupacks` — счётчик дублирующихся ACK (fast retransmit при 3)
- `TF_INFR` — флаг «в процессе fast recovery»
**TCP опции:** поддерживается только MSS (Maximum Segment Size). Window Scale, Timestamps, SACK отключены.
### Приём данных из сети
Внешний код получает IP-пакет (после IP-заголовка), формирует `struct pbuf*` с TCP-сегментом и вызывает:
```c
lwip_tcp_input(ctx, p, src_ip, dst_ip);
```
Функция разбирает TCP-заголовок, находит PCB по (src_ip, src_port, dst_ip, dst_port) и передаёт управление `tcp_process()` / `tcp_receive()`. При получении данных вызывается callback `pcb->recv(arg, pcb, pbuf, err)`.
### Отправка данных в сеть
TCP вызывает `ctx->output(arg, pbuf, src_ip, dst_ip)` когда нужно отправить IP-пакет. Пользователь реализует:
1. Предварительное выделение места под IP-заголовок (pbuf_header)
2. Заполнение IP-заголовка
3. Расчёт IP checksum (`tcp_ip_checksum`)
4. Отправку через транспортный уровень (ETCP, raw socket и т.п.)
### Memory pools
Контекст создаёт 3 memory pool'а:
- `pcb_pool` — `sizeof(tcp_pcb)` элементов для активных соединений
- `pcb_listen_pool` — `sizeof(tcp_pcb_listen)` для слушающих PCB
- `seg_pool` — `sizeof(tcp_seg)` для сегментов
При нехватке PCB в пуле (`tcp_alloc`) последовательно пытается освободить память:
1. Обработать pending close (`tcp_handle_closepend`)
2. Убить самый старый TIME_WAIT PCB (`tcp_kill_timewait`)
3. Убить LAST_ACK PCB (`tcp_kill_state`)
4. Убить CLOSING PCB (`tcp_kill_state`)
5. Убить наименее приоритетный активный PCB (`tcp_kill_prio`)
### Callbacks (на PCB)
| Callback | Тип | Когда вызывается |
|----------|-----|-----------------|
| `recv` | `tcp_recv_fn(arg, pcb, pbuf, err)` | Получены данные (pbuf ≠ NULL) или соединение закрыто (pbuf == NULL, err == LERR_OK) |
| `sent` | `tcp_sent_fn(arg, pcb, len)` | Данные подтверждены, освободилось место в snd_buf |
| `connected` | `tcp_connected_fn(arg, pcb, err)` | Завершено трёхстороннее рукопожатие (клиент) |
| `accept` | `tcp_accept_fn(arg, new_pcb, err)` | Входящее соединение (на listen_pcb) |
| `err` | `tcp_err_fn(arg, err)` | Асинхронная ошибка (RST, abort) |
| `poll` | `tcp_poll_fn(arg, pcb)` | Периодический опрос (интервал задаётся при `tcp_poll`) |
### Диагностика
- **Trace ring buffer**: `lwip_tcp_trace_record()` записывает кольцевые события (384 записи). `lwip_tcp_trace_dump()` выводит в консоль. Типы событий: 'S' (send), 'A' (ack), 'T' (timeout), 'R' (rexmit), 'F' (fast rexmit), 'K' (kill), 'X' (segment), 'D' (drop), 'L' (loss), 'P' (probe), 'M' (tmr), 'C' (close)
- **Stats**: счётчики ошибок выделения памяти (`pbuf_fails`, `seg_fails`, `pcb_fails`, `write_fails`, `enq_fails`, `rst_fails`, `ack_fails`, `probe_fails`, `split_fails`). `lwip_tcp_stats_dump()` / `lwip_tcp_stats_clear()`
- **Глобальный hook**: `g_tcp_trace` — функция обратного вызова для внешнего мониторинга (используется в тестах)
## 3. API
### Жизненный цикл контекста
| Функция | Описание |
|---------|----------|
| `lwip_tcp_init(ua, output, output_arg)` | Создать TCP-контекст: пулы памяти (pcb, pcb_listen, seg), запустить таймер. Возвращает `lwip_tcp_ctx*` или NULL при ошибке |
| `lwip_tcp_destroy(ctx)` | Уничтожить контекст: остановить таймер, освободить все PCB (через abort/close), уничтожить пулы |
| `lwip_tcp_set_timer(ctx, interval_ms, rto_min_ms, rto_max_ms)` | Перенастроить интервал таймера (мс) и границы RTO. rto_max_ms=0 — без ограничения |
### Входной трафик
| Функция | Описание |
|---------|----------|
| `lwip_tcp_input(ctx, p, src_ip, dst_ip)` | Подать входящий IP-пакет (после IP-заголовка). pbuf освобождается внутри. src_ip/dst_ip — host byte order |
### Управление PCB
| Функция | Описание |
|---------|----------|
| `tcp_new(ctx)` | Создать новый TCP PCB с приоритетом NORMAL (64) |
| `tcp_alloc(ctx, prio)` | Создать PCB с заданным приоритетом (1..127). При нехватке памяти агрессивно освобождает TIME_WAIT/LAST_ACK/CLOSING/низкоприоритетные PCB |
| `tcp_bind(pcb, ip, port)` | Привязать локальный адрес/порт. port=0 — автоназначение из диапазона [0xC000..0xFFFF]. Переводит в bound_pcbs |
| `tcp_listen(pcb)` | Перевести CLOSED→LISTEN. Старый PCB освобождается, создаётся `tcp_pcb_listen` |
| `tcp_connect(pcb, ip, port, connected)` | Инициировать исходящее соединение. Отправляет SYN, переводит в SYN_SENT. Вызывает `connected` при успехе |
| `tcp_close(pcb)` | Активное закрытие: устанавливает TF_RXCLOSED, отправляет FIN (SYN_RCVD/ESTABLISHED→FIN_WAIT_1, CLOSE_WAIT→LAST_ACK). LISTEN: освобождает PCB и потомков |
| `tcp_shutdown(pcb, shut_rx, shut_tx)` | Частичное закрытие. shut_rx: перестать принимать, shut_tx: отправить FIN |
| `tcp_abort(pcb)` | Принудительный разрыв: отправляет RST, освобождает все очереди, вызывает errf |
| `tcp_abandon(pcb, reset)` | Подобен abort, но reset=0 не отправляет RST |
| `tcp_recved(pcb, len)` | Уведомить TCP что приложение обработало len байт, увеличить rcv_wnd. При достаточном росте окна отправляет ACK |
### Отправка данных
| Функция | Описание |
|---------|----------|
| `tcp_write(pcb, data, len, apiflags)` | Записать данные в send buffer. apiflags: `TCP_WRITE_FLAG_COPY` (копировать данные), `TCP_WRITE_FLAG_MORE` (будут ещё данные). При ошибке памяти выставляет TF_NAGLEMEMERR |
| `tcp_output(pcb)` | Отправить все готовые сегменты из unsent. Соблюдает cwnd и snd_wnd. При установленном TF_ACK_NOW сначала отправляет ACK. Копирует данные из input_pcb (если в процессе обработки входа) |
| `tcp_send_empty_ack(pcb)` | Отправить чистый ACK (без данных) |
| `tcp_send_fin(pcb)` | Добавить FIN к последнему unsent-сегменту или создать отдельный сегмент |
| `tcp_enqueue_flags(pcb, flags)` | Поставить в очередь сегмент с флагами (SYN/FIN) и MSS-опцией |
### Ретрансмиссия
| Функция | Описание |
|---------|----------|
| `tcp_rexmit_rto_prepare(pcb)` | Подготовка к RTO-ретрансмиссии: переместить все unacked→unsent. Выставляет TF_RTO |
| `tcp_rexmit_rto_commit(pcb)` | Завершить RTO-ретрансмиссию: увеличить nrtx, записать trace, вызвать tcp_output |
| `tcp_rexmit_rto(pcb)` | Полный RTO-цикл: prepare + commit |
| `tcp_rexmit(pcb)` | Ретрансмиссия самого старого unacked сегмента (вставить обратно в unsent по порядку seqno) |
| `tcp_rexmit_fast(pcb)` | Fast retransmit (при 3 dupacks): переотправить первый unacked, уменьшить ssthresh/сwnd, выставить TF_INFR |
| `tcp_rexmit_seg(pcb, seg)` | Ретрансмит конкретного сегмента (из ooseq/unacked) |
| `tcp_zero_window_probe(pcb)` | Отправить 1-байтовый probe при нулевом окне приёмника (persist timer) |
| `tcp_split_unsent_seg(pcb, split)` | Разделить первый unsent сегмент на две части (для zero-window probe) |
### Внутренние (объявлены в priv.h)
| Функция | Описание |
|---------|----------|
| `tcp_fasttmr(ctx)` | Fast timer: обработка delayed ACK, close-pending, refused_data |
| `tcp_slowtmr(ctx)` | Slow timer: ретрансмиссия (RTO/backoff), persist timer, таймауты FIN_WAIT_2/SYN_RCVD/LAST_ACK, OOSEQ cleanup, poll, TIME_WAIT cleanup |
| `tcp_txnow(ctx)` | Экстренная отправка для PCB с флагом TF_NAGLEMEMERR |
| `tcp_rst(pcb, seqno, ackno, local_ip, remote_ip, local_port, remote_port)` | Отправить RST-сегмент |
| `tcp_process_refused_data(pcb)` | Повторно подать refused_data в callback recv |
| `tcp_pcb_purge(pcb)` | Очистить все очереди сегментов (unsent, unacked, ooseq, refused_data) |
| `tcp_pcb_remove(pcblist, pcb)` | Удалить PCB из списка, очистить, перевести в CLOSED |
| `tcp_seg_copy(seg)` | Копия сегмента с ref на pbuf (stub — используйте `tcp_seg_copy_with_ctx`) |
| `tcp_seg_copy_with_ctx(ctx, seg)` | Копия сегмента с явным контекстом для доступа к seg_pool |
| `tcp_segs_free(seg)` / `tcp_seg_free(seg)` | Освобождение цепочки/одного сегмента (stub — используйте локальные версии) |
| `tcp_free_ooseq(pcb)` | Очистить OOSEQ-очередь |
| `tcp_free(pcb)` | Освободить PCB в соответствующий пул (pcb_pool или pcb_listen_pool) |
| `tcp_next_iss(pcb)` | Сгенерировать Initial Sequence Number |
| `tcp_checksum(data, len)` | Расчёт TCP checksum |
| `tcp_ip_checksum(iph)` | Расчёт IP header checksum |
### Callback setters
| Функция | Описание |
|---------|----------|
| `tcp_arg(pcb, arg)` | Установить пользовательский аргумент для callbacks |
| `tcp_recv(pcb, recv)` | Установить callback приёма данных |
| `tcp_sent(pcb, sent)` | Установить callback подтверждения отправки |
| `tcp_err(pcb, errf)` | Установить callback асинхронной ошибки |
| `tcp_accept(pcb, accept)` | Установить callback входящего соединения (только для LISTEN) |
| `tcp_poll(pcb, poll, interval)` | Установить периодический опрос (интервал в единицах slow timer ~500ms) |
### Макросы-хелперы
| Макрос | Описание |
|--------|---------|
| `tcp_mss(pcb)` | Текущий MSS соединения |
| `tcp_sndbuf(pcb)` | Доступное место в send buffer |
| `tcp_sndqueuelen(pcb)` | Количество pbuf в очереди отправки |
| `tcp_nagle_disable(pcb)` / `tcp_nagle_enable(pcb)` | Отключить/включить алгоритм Nagle |
| `tcp_nagle_disabled(pcb)` | Проверить, отключён ли Nagle |
| `tcp_set_flags(pcb, f)` / `tcp_clear_flags(pcb, f)` / `tcp_is_flag_set(pcb, f)` | Операции с флагами PCB |
### Trace и диагностика
| Функция | Описание |
|---------|----------|
| `lwip_tcp_trace_record(ctx, event, seq, len, wnd, rto, rtime, state)` | Записать событие в ring buffer |
| `lwip_tcp_trace_dump(ctx)` | Вывести последние 50 событий trace в stdout |
| `lwip_tcp_trace_clear(ctx)` | Очистить trace buffer |
| `lwip_tcp_stats_dump(ctx)` | Вывести счётчики ошибок в stdout |
| `lwip_tcp_stats_clear(ctx)` | Сбросить счётчики ошибок |
| `tcp_debug_state_str(s)` | Получить строковое имя состояния |
## 4. Константы (lwip_tcp_opts.h)
### Размеры и буферы
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TCP_MSS` | 1460 | Maximum Segment Size (MTU 1500 − IP(20) − TCP(20)) |
| `TCP_SND_BUF` | `16 * TCP_MSS` = 23360 | Send buffer (макс. данные от приложения) |
| `TCP_WND` | `8 * TCP_MSS` = 11680 | Receive window (объявляемый размер окна) |
| `TCP_SND_QUEUELEN` | `4 * TCP_SND_BUF / TCP_MSS` = 64 | Макс. количество pbuf в unsent |
| `TCP_SNDLOWAT` | `min(2*MSS, SND_BUF/2)` = 2920 | Нижняя граница snd_buf для callback sent |
| `INITIAL_MSS` | 536 | Начальный MSS (RFC 879) до согласования опций |
### Таймеры и таймауты
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TCP_TMR_INTERVAL` | 250 ms | Базовый интервал таймера |
| `TCP_FAST_INTERVAL` | 250 ms | Fast timer interval |
| `TCP_SLOW_INTERVAL` | 500 ms (2×base) | Slow timer interval |
| `TCP_RTO_MIN_MS` | 3000 ms | Начальный RTO |
| `TCP_FIN_WAIT_TIMEOUT` | 20000 ms | Таймаут FIN_WAIT_2 (если установлен TF_RXCLOSED) |
| `TCP_SYN_RCVD_TIMEOUT` | 20000 ms | Таймаут SYN_RCVD |
| `TCP_MSL` | 60000 ms | Maximum Segment Lifetime (TIME_WAIT = 2×MSL = 120s) |
| `TCP_OOSEQ_TIMEOUT` | 6 × RTO | Таймаут очистки OOSEQ-очереди |
### Ретрансмиссия
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TCP_MAXRTX` | 12 | Макс. количество ретрансмиссий (после — разрыв) |
| `TCP_SYNMAXRTX` | 6 | Макс. SYN-ретрансмиссий |
| `tcp_backoff[]` | `{1,2,3,4,5,6,7,7,7,7,7,7,7}` | Коэффициенты backoff для RTO (2^n, capped at 7) |
| `tcp_persist_backoff[]` | `{3,6,12,24,48,96,120}` | Backoff для persist timer (секунды, затем 2 минуты) |
### Keepalive (отключён)
| Константа | Значение |
|-----------|----------|
| `TCP_KEEPIDLE_DEFAULT` | 7200000 ms (2 часа) |
| `TCP_KEEPINTVL_DEFAULT` | 75000 ms |
| `TCP_KEEPCNT_DEFAULT` | 9 |
### Порты
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TCP_LOCAL_PORT_RANGE_START` | 0xC000 (49152) | Начало диапазона эфемерных портов |
| `TCP_LOCAL_PORT_RANGE_END` | 0xFFFF (65535) | Конец диапазона |
### Приоритеты
| Константа | Значение |
|-----------|----------|
| `TCP_PRIO_MIN` | 1 |
| `TCP_PRIO_NORMAL` | 64 |
| `TCP_PRIO_MAX` | 127 |
При нехватке PCB в пуле, `tcp_alloc` убивает наименее приоритетные активные соединения. Низкий приоритет = ближе к `TCP_PRIO_MAX`.
### Отключённые фичи
| Константа | Значение | Комментарий |
|-----------|----------|-------------|
| `TCP_WND_SCALE` | 0 | Window scale option (не нужен в пределах малых окон) |
| `TCP_TIMESTAMPS` | 0 | TCP timestamps option |
| `TCP_SACK_OUT` | 0 | Selective ACK |
| `TCP_QUEUE_OOSEQ` | 1 | OOSEQ включён |
| `TCP_OVERSIZE` | 0 | Без pre-allocation размера pbuf |
| `TCP_LISTEN_BACKLOG` | 0 | Без backlog (ждём пока приложение заберёт) |
| `TCP_KEEPALIVE` | 0 | Keepalive отключён |
| `TCP_CHECKSUM_ON_COPY` | 0 | Без checksum-on-copy оптимизации |
| `TCP_CALCULATE_EFF_SEND_MSS` | 0 | Без расчёта эффективного MSS |
## 5. Структуры данных
### `struct lwip_tcp_ctx`
Основной контекст TCP-стека (один на инстанс TCP-прокси):
| Поле | Тип | Назначение |
|------|-----|-----------|
| `ua` | `UASYNC*` | Event loop для таймера |
| `output` | `tcp_output_fn` | Callback отправки IP-пакета |
| `output_arg` | `void*` | Аргумент для output callback |
| `timer` | `void*` | Handle таймера uasync |
| `bound_pcbs` | `tcp_pcb*` | Список PCB в CLOSED после bind |
| `listen_pcbs` | `tcp_pcb*` | Список слушающих PCB (LISTEN) |
| `active_pcbs` | `tcp_pcb*` | Список активных соединений |
| `tw_pcbs` | `tcp_pcb*` | Список TIME_WAIT |
| `pcb_pool` | `memory_pool*` | Пул для `tcp_pcb` |
| `pcb_listen_pool` | `memory_pool*` | Пул для `tcp_pcb_listen` |
| `seg_pool` | `memory_pool*` | Пул для `tcp_seg` |
| `tmr_interval_ms` | `uint16_t` | Интервал таймера (по умолчанию 62.5ms) |
| `rto_min_ms` | `uint16_t` | Минимальный RTO (по умолчанию 3000) |
| `rto_max_ms` | `uint16_t` | Максимальный RTO (0=без ограничений) |
| `ticks` | `uint32_t` | Счётчик slow timer тиков |
| `slowtmr_ctr` | `uint8_t` | Флаг защиты от двойной обработки PCB |
| `tmr_phase` | `uint8_t` | Фаза (чёт-нечет) для чередования slow/fast |
| `iss_seed` | `uint16_t` | Сид для генерации ISN |
| `ip_id` | `uint16_t` | Счётчик IP ID (для IP-заголовка) |
| `port_seed` | `uint16_t` | Сид для автоназначения портов |
| `trace_id` | `char` | Идентификатор контекста для trace ('A'/'B') |
| `trace` | `tcp_trace_buf` | Кольцевой буфер отладки (384 записи) |
| `stats` | struct | Счётчики ошибок (pbuf, seg, pcb, write, enq, rst, ack, probe, split fails) |
### `struct tcp_pcb`
Protocol Control Block для активного соединения:
| Группа | Поля | Назначение |
|--------|------|-----------|
| **Идентификация** | `local_ip/port`, `remote_ip/port`, `ttl`, `tos` | Сетевые адреса |
| **Состояние** | `state`, `prio`, `flags` | TCP состояние, приоритет, флаги (TF_*) |
| **Таймеры** | `polltmr/intervall`, `last_timer`, `tmr`, `rtime` | Poll-опрос, защита от двойного вызова, timestamp, RTO-счётчик |
| **Приём** | `rcv_nxt`, `rcv_wnd`, `rcv_ann_wnd`, `rcv_ann_right_edge` | Следующий ожидаемый seq, окно приёма, объявленное окно и правый край |
| **RTT** | `rttest`, `rtseq`, `sa`, `sv`, `rto`, `nrtx` | Замер RTT, сглаженное среднее/вариация, timeout, счётчик ретрансмиссий |
| **Duplicate ACK** | `dupacks`, `lastack` | Счётчик дублирующих ACK, последний ACK'нутый seq |
| **Congestion** | `cwnd`, `ssthresh`, `rto_end`, `bytes_acked` | Congestion window, порог slow start, конец RTO-окна, подтверждённые байты |
| **Отправка** | `snd_nxt`, `snd_wl1/2`, `snd_lbb`, `snd_wnd/max` | Следующий seqno, окно отправки, last byte buffered, окно приёмника |
| **Буфер** | `snd_buf`, `snd_queuelen` | Доступно для записи, длина очереди |
| **Сегменты** | `unsent`, `unacked`, `ooseq` | Очереди исходящих/подтверждённых/out-of-order сегментов |
| **Данные** | `refused_data` | Отложенные данные (когда приложение не готово) |
| **Callbacks** | `sent`, `recv`, `connected`, `pollfn`, `errf` | Функции обратного вызова |
| **Keepalive** | `keep_idle` | Интервал idle до первого keepalive probe (отключён) |
| **Persist** | `persist_cnt/backoff/probe` | Счётчик, backoff множитель, счётчик probe |
| **Размер** | `mss` | Согласованный MSS (начальный 536) |
### `struct tcp_pcb_listen`
Упрощённый PCB для LISTEN (не содержит полей данных/сегментов/таймеров):
| Поле | Тип | Назначение |
|------|-----|-----------|
| `next` | `tcp_pcb*` | Указатель на следующий в списке |
| `ctx` | `lwip_tcp_ctx*` | Контекст |
| `callback_arg` | `void*` | Пользовательский аргумент |
| `state` | `tcp_state` | Всегда LISTEN |
| `prio` | `uint8_t` | Приоритет |
| `local_port` | `uint16_t` | Локальный порт |
| `local_ip` | `uint32_t` | Локальный IP (0 = ANY) |
| `accept` | `tcp_accept_fn` | Callback входящего соединения |
### `struct tcp_seg`
Узел очереди сегментов:
| Поле | Тип | Назначение |
|------|-----|-----------|
| `next` | `tcp_seg*` | Следующий сегмент в списке |
| `p` | `pbuf*` | Пакетный буфер с TCP-заголовком и данными |
| `len` | `uint16_t` | Длина данных в сегменте |
| `flags` | `uint8_t` | Флаги опций (TF_SEG_OPTS_MSS) |
| `tcphdr` | `tcp_hdr*` | Указатель на TCP-заголовок внутри pbuf |
Макрос `TCP_TCPLEN(seg)` = `seg->len + (FIN или SYN ? 1 : 0)` — занимаемое место в sequence space.
### `struct tcp_hdr`
Wire-формат TCP-заголовка (20 байт, packed):
| Поле | Размер | Описание |
|------|--------|----------|
| `src` | `uint16_t` | Source port |
| `dest` | `uint16_t` | Destination port |
| `seqno` | `uint32_t` | Sequence number (network byte order) |
| `ackno` | `uint32_t` | Acknowledgement number |
| `_hdrlen_rsvd_flags` | `uint16_t` | Data offset (4 бита), reserved (6 бит), flags (6 бит) |
| `wnd` | `uint16_t` | Window size |
| `chksum` | `uint16_t` | Checksum |
| `urgp` | `uint16_t` | Urgent pointer |
Макросы для доступа: `TCPH_HDRLEN(hdr)`, `TCPH_FLAGS(hdr)`, `TCPH_HDRLEN_SET(hdr, len)`, `TCPH_FLAGS_SET(hdr, flags)`, `TCPH_SET_FLAG/UNSET_FLAG(hdr, flags)`.
Флаги: `TCP_FIN`(0x01), `TCP_SYN`(0x02), `TCP_RST`(0x04), `TCP_PSH`(0x08), `TCP_ACK`(0x10), `TCP_URG`(0x20), `TCP_ECE`(0x40), `TCP_CWR`(0x80).
### `struct tcp_trace_entry` / `struct tcp_trace_buf`
Кольцевой буфер отладки: 384 записи, каждая содержит ticks, event type, seq, len, wnd, rto, rtime, ctx_id, state.
## 6. Диагностические события трассировки (trace event types)
| Код | Значение | Где записывается |
|-----|----------|-----------------|
| `'S'` | Отправка сегмента (seq, len, wnd) | `tcp_output_segment()` в out.c |
| `'X'` | Отправка non-data сегмента (seq, len, cwnd) | `tcp_output_segment()` в out.c |
| `'A'` | Обработка ACK (ack, acked, cwnd) | `tcp_receive()` в in.c |
| `'D'` | Drop (seq, len) — дубликат или старый сегмент | `tcp_receive()` в in.c |
| `'r'` | Состояние RTO-счётчика (rtime, rto, state) | `tcp_receive()` в in.c |
| `'T'` | RTO timeout (rto, rtime) | `tcp_slowtmr()` в tcp.c |
| `'R'` | RTO retransmit commit (seq, len, rto) | `tcp_rexmit_rto_commit()` в out.c |
| `'F'` | Fast retransmit (seq, len, ssthresh) | `tcp_rexmit_fast()` в out.c |
| `'C'` | Close (state) | `tcp_close_shutdown()` в out.c/tcp.c |
| `'L'` | Loss simulation (drop len) | сетевой эмулятор |
| `'M'` | Timer tick | `tcp_tmr_cb()` в tcp.c |
| `'K'` | Kill PCB в slowtmr (state, nrtx, port) | `tcp_slowtmr()` в tcp.c |
| `'Y'` | Kill PCB в tcp_kill_* (seq, nrtx) | `tcp_kill_prio/state/timewait()` в tcp.c |
| `'Z'` | Abandon PCB (state, reset_flag, port) | `tcp_abandon()` в tcp.c, in.c |
| `'E'` | Error (err_code) | обработка ошибок в in.c |
| `'P'` | Zero-window probe (rto) | `tcp_zero_window_probe()` в out.c |
## 7. Зависимости
- `lib/mem.h` — `u_malloc`/`u_calloc`/`u_free`/`u_strdup` (для выделения `lwip_tcp_ctx`)
- `lib/debug_config.h` — `DEBUG_ERROR`, `DEBUG_WARN`, `DEBUG_INFO`, `DEBUG_DEBUG`, `DEBUG_TRACE`, категории `DEBUG_CATEGORY_ALL`/`DEBUG_CATEGORY_GENERAL`
- `lib/memory_pool.h` — `memory_pool_init/destroy/alloc/free/is_freed` (пулы PCB, сегментов)
- `lib/u_async.h` — `uasync_set_timeout`/`uasync_cancel_timeout`, `get_time_tb()`
- `lwip_pbuf.h` — `pbuf_alloc/free/ref/header/chain/dechain/clen/copy_partial/take/get_contiguous`
- `<string.h>` — `memcpy`, `memset`
- `<arpa/inet.h>` / `<winsock2.h>` — `ntohl`/`ntohs`/`htonl`/`htons` (сетевой порядок байт)

104
src/lwip_tcp/lwip_tcp_in_doc.md

@ -0,0 +1,104 @@
# lwip_tcp_in
## 1. Назначение
Обработка входящих TCP-сегментов — адаптированный код из lwIP 2.1.x `tcp_in.c` для uTun. Все `#if`-блоки убраны, глобальный IP lwIP заменён явными параметрами `src_ip`/`dst_ip`. Контекст задаётся через `lwip_tcp_ctx`, однопоточное исполнение.
Модуль реализует:
- Демультиплексирование входящего сегмента к PCB (по quad: src_ip, dst_ip, sport, dport)
- Полный TCP state machine на 11 состояний (SYN_SENT→SYN_RCVD→ESTABLISHED→...→TIME_WAIT)
- Поддержку simultaneous open (SYN_SENT + SYN+ACK), RST, SYN flood в ESTABLISHED
- Обработку ACK: window update, dupack detection/fast retransmit, congestion control (slow start / congestion avoidance), RTT estimation
- Реассемблинг данных: вставка не-по-порядку сегментов в сортированный OOSEQ-список, слияние при заполнении пробелов
- Congestion control: Reno вариант (slow start, congestion avoidance, fast retransmit, fast recovery через TF_INFR), cwnd при dupack >3 инкрементируется, при ack>lastack — выход из fast recovery (сброс cwnd в ssthresh)
- Защиту от вышедших за rcv_wnd данных (обрезание seg.len, обрезание FIN/SYN)
- Обработку delayed close: отложенное закрытие PCB после отправки всех данных
## 2. Как пользоваться
Внутренний модуль, главная точка входа — `lwip_tcp_input()` (вызывается из стека lwIP при получении IP-пакета с протоколом TCP). Передаётся контекст `lwip_tcp_ctx`, pbuf, src_ip, dst_ip.
**Ключевые концепции:**
- `lwip_tcp_input()` (стр.120): парсинг TCP-заголовка, демультиплексирование по active_pcbs→tw_pcbs→listen_pcbs, вызов `tcp_process()`, отправка RST если нет получателя. Продвигает найденный PCB в начало списка (LRU).
- `tcp_process()` (стр.452): полный state machine — разбор комбинаций флагов (RST, SYN, ACK, FIN) в соответствии с состоянием PCB. Для каждого состояния своя логика: SYN_SENT ждёт SYN+ACK с корректным ackno, SYN_RCVD ждёт ACK (и обрабатывает retransmit SYN), ESTABLISHED/CLOSE_WAIT вызывают `tcp_receive()`, FIN_WAIT_1/2, CLOSING, LAST_ACK обрабатывают закрытие.
- `tcp_receive()` (стр.703): основная логика ACK и данных:
- **Window update** (стр.710–720): обновление `snd_wnd`, трекинг `snd_wl1`/`snd_wl2` для защиты от старых ACK
- **Dupack detection** (стр.723–738): dupack — это ACK с ackno == lastack, tcplen==0, и snd_wnd не изменился. При dupack: инкремент счётчика, fast retransmit при >=3, инкремент cwnd при >3 (inflating)
- **ACK новых данных** (стр.739–806): обновление `lastack`, освобождение подтверждённых сегментов из unacked/unsent, congestion control (slow start при cwnd < ssthresh: cwnd += min(acked, 2*MSS) с учётом TF_RTO, congestion avoidance при cwnd >= ssthresh: cwnd += MSS когда bytes_acked >= cwnd), обновление snd_buf, выход из TF_RTO
- **RTT estimation** (стр.798–807): по методу Van Jacobson (sa/sv/rto), только если `rttest` установлен (был измеряемый сегмент) и ackno > rtseq
- **Reassembly** (стр.810–1001): если seqno in-window, но не in-order — создание/вставка в OOSEQ-список с автоматическим слиянием перекрывающихся сегментов и удалением полностью покрытых. Если seqno == rcv_nxt — приём данных, продвижение rcv_nxt, вытаскивание из ooseq примыкающих сегментов (стр.899–922)
- **PU-ACK** (стр.924/997/1000/1005): отправка ACK (delayed или immediately) в ответ на данные или out-of-window пакеты
- `tcp_listen_input()` (стр.373): обработка входящего SYN для listen PCB — создание нового PCB, установка ISS, регистрация в active_pcbs, разбор опций (MSS), отправка SYN+ACK.
- `tcp_timewait_input()` (стр.428): обработка пакетов для TIME_WAIT PCB — ответ RST на SYN в окне, продление таймера на FIN, ACK на данные.
- `tcp_parseopt()` (стр.1030): разбор TCP-опций — только MSS (TCP_OPT_MSS), ограничение до `TCP_MSS` (1460).
- **OOSEQ** (стр.647–672, 925–994): сортированный список out-of-order сегментов. `tcp_oos_insert_segment()` вставляет новый сегмент, сливая/обрезая соседние при перекрытии. FIN-флаг переносится на поглощающий сегмент. При вставке в середину списка предшествующий сегмент может обрезаться справа.
- **Congestion control** (Reno): `cwnd` инициализируется как `min(4*MSS, max(2*MSS, 4380))`. Slow start: cwnd += min(acked, 2*MSS). Congestion avoidance: cwnd += MSS когда bytes_acked >= cwnd. Fast retransmit при 3 dupack. Fast recovery: TF_INFR включён, cwnd продолжает расти на MSS за dupack >3; выход при первом новом ACK (сброс cwnd = ssthresh).
## 3. API
### Внешние (вызываются извне модуля)
| Функция | Назначение |
|---|---|
| `lwip_tcp_input(ctx, p, src_ip, dst_ip)` | Главная точка входа: демультиплексирование + state machine + reassembly |
| `tcp_trigger_input_pcb_close()` | Установка флага TF_CLOSED для отложенного закрытия извне |
### Внутренние (статические, только в этом файле)
| Функция | Назначение |
|---|---|
| `tcp_process(pcb)` | State machine: обработка RST/SYN, переходы SYN_SENT→ESTABLISHED, SYN_RCVD→ESTABLISHED, FIN-переходы |
| `tcp_receive(pcb)` | ACK-обработка (window update, dupack, congestion control, RTT), приём/реассемблинг данных |
| `tcp_listen_input(lpcb)` | SYN на listen PCB: создание нового PCB, SYN+ACK |
| `tcp_timewait_input(pcb)` | Обработка пакетов в TIME_WAIT |
| `tcp_parseopt(pcb)` | Разбор TCP-опций (MSS) |
| `tcp_oos_insert_segment(cseg, seg)` | Вставка сегмента в OOSEQ с обрезанием/слиянием |
| `tcp_free_acked_segments(pcb, seg_list)` | Освобождение полностью подтверждённых сегментов из списка, накопление `recv_acked` |
| `tcp_input_delayed_close(pcb)` | Отложенное закрытие PCB при TF_CLOSED |
| `tcp_get_next_optbyte()` | Последовательное чтение байтов TCP-опций (из tcphdr+опции или второго pbuf) |
### Локальные хелперы
| Функция | Назначение |
|---|---|
| `pbuf_cat_local(h, t)` | Конкатенация двух pbuf — используется при слиянии OOSEQ с recv_data |
| `tcp_seg_copy_local(pcb, seg)` | Копирование tcp_seg через memcpy + pbuf_ref (из seg_pool) |
| `tcp_seg_free_local(pcb, seg)` | Освобождение tcp_seg: pbuf_free + возврат в seg_pool |
| `tcp_segs_free_local(pcb, seg)` | Освобождение цепочки tcp_seg |
| `pbuf_realloc_local(p, new_len)` | Урезание pbuf-цепочки справа до нужной длины |
### Локальные static-переменные (состояние обработки одного пакета)
Используются для передачи данных между `lwip_tcp_input()` и вызываемыми функциями:
- `inseg` — текущий обрабатываемый сегмент
- `tcphdr`, `tcphdr_optlen`, `tcphdr_opt1len`, `tcphdr_opt2`, `tcp_optidx` — заголовок и опции
- `seqno`, `ackno` — номера последовательности (host byte order)
- `sport`, `dport` — порты (host byte order)
- `tcplen` — длина данных + SYN/FIN
- `flags` — флаги TCP-заголовка
- `recv_flags` — накопленные флаги обработки (TF_RESET, TF_CLOSED, TF_GOT_FIN)
- `recv_data` — собранные данные для передачи приложению через TCP_EVENT_RECV
- `recv_acked` — сумма подтверждённых байт для TCP_EVENT_SENT
- `tcp_input_pcb` — текущий PCB (для доступа из OOSEQ-функций)
- `tcp_src_ip`, `tcp_dst_ip` — IP-адреса пакета
## 4. Зависимости
- `lwip_tcp_priv.h` — заголовок TCP, макросы флагов/последовательностей, tcp_seg, события, таймеры
- `lwip_tcp.h` — PCB, контекст ctx, типы ошибок (LERR_*), состояния
- `lwip_tcp_opts.h` — TCP_MSS, TCP_WND, TCP_TMR_INTERVAL и прочие константы
- `lwip_pbuf.h` — pbuf API
- `lib/mem.h` — u_malloc/u_free
- `lib/debug_config.h` — DEBUG_ERROR/DEBUG_INFO
- `lib/memory_pool.h` — memory_pool_alloc/free для seg_pool
- Внешние функции из `lwip_tcp.c`: `tcp_update_rcv_ann_wnd()`, `tcp_abandon()`
- Внешние из `lwip_tcp_out.c`: `tcp_send_empty_ack()`, `tcp_enqueue_flags()`, `tcp_rst()`, `tcp_rexmit()`, `tcp_rexmit_rto()`, `tcp_rexmit_fast()`, `tcp_ack_now()`, `tcp_output()`, `tcp_alloc()`, `tcp_free()`, `tcp_pcb_purge()`, `tcp_pcb_remove()`, `tcp_process_refused_data()`, `tcp_abort()`
- `lwip_tcp_trace_record()` — запись в кольцевой буфер трассировки

2
src/lwip_tcp/lwip_tcp_opts.h

@ -30,12 +30,10 @@ extern "C" {
#define TCP_KEEPCNT_DEFAULT 9
#define TCP_WND_SCALE 0 // disabled
#define TCP_TIMESTAMPS 0 // disabled
#define TCP_SACK_OUT 0 // disabled
#define TCP_QUEUE_OOSEQ 1
#define TCP_OVERSIZE 0
#define TCP_LISTEN_BACKLOG 0
#define TCP_KEEPALIVE 0
#define TCP_CHECKSUM_ON_COPY 0
#define TCP_CALCULATE_EFF_SEND_MSS 0

167
src/lwip_tcp/lwip_tcp_out_doc.md

@ -0,0 +1,167 @@
# lwip_tcp_out
## 1. Назначение
Исходящая сторона lwIP TCP стека: сегментация данных в MSS-чанки, планирование передачи, контроль перегрузки, ретрансмиссия и отправка управляющих сегментов (SYN, FIN, RST, ACK, zero-window probe). Адаптирован из lwIP для uTun — убраны timestamps, SACK, WND_SCALE, checksum-on-copy.
## 2. Как пользоваться
Внутренний модуль — вызывается из `lwip_tcp.c` (таймеры, API), `lwip_tcp_in.c` (fast retransmit, SACK-обработка).
### Ключевые концепции
#### tcp_write() — сегментация и постановка в unsent
Пишет данные в выходной буфер PCB. Фазы:
1. **Дописывание в хвост last_unsent**: если последний unsent сегмент < MSS, добавляем цепочку pbuf к нему (copy/zero-copy, extend).
2. **Новые сегменты**: оставшиеся данные режутся на MSS-чанки, каждому создаётся `struct tcp_seg` через `tcp_create_segment`.
3. **Commit**: конкатенация к хвосту `pcb->unsent`, обновление `snd_lbb`, `snd_buf`, `snd_queuelen`. PSH-флаг ставится на последний сегмент, если не задан `TCP_WRITE_FLAG_MORE`.
4. Лимит `snd_queuelen` ≤ `TCP_SND_QUEUELEN` (4 * SND_BUF/MSS = 64). При превышении — LERR_MEM.
5. MSS адаптивен: если `snd_wnd_max` даёт меньшее окно, MSS снижается до `min(MSS, snd_wnd_max/2)`.
#### tcp_output() — планирование и отправка
Ограничения:
- **cwnd**: `min(cwnd, snd_wnd)` — окно перегрузки
- **Нейгл**: `tcp_do_output_nagle()` — запрещает отправку маленьких сегментов если есть unacked данные и сегмент < MSS (отключается `TF_NODELAY` или при `TF_INFR`).
- **Zero-window persist**: если всё ограничено snd_wnd (а не cwnd) и unacked пуст — взводится `persist_backoff` для последующей отправки zero-window probe.
- **Re-entrancy guard**: `tcp_input_pcb` — если output вызван из input того же PCB, сразу return.
- Отправленные data-сегменты (TCP_TCPLEN > 0) перемещаются из `unsent` в `unacked` с сортировкой по seqno.
- Пустые сегменты (например, чистый ACK) освобождаются после отправки.
- Обновляется `snd_nxt`.
#### tcp_enqueue_flags() — управляющие сегменты
Создаёт сегмент с заданными флагами (SYN, FIN, RST), ставит MSS-опцию для SYN. Инкрементирует `snd_lbb` для SYN/FIN (потребление номера последовательности).
#### tcp_send_fin() — завершение соединения
Если есть unsent данные — вешает FIN на хвостовой сегмент. Иначе создаёт новый FIN-сегмент через `tcp_enqueue_flags`.
#### tcp_split_unsent_seg() — дробление unsent
Отрезает хвост сегмента (например, для retransmit при потере). Копирует remainder в новый pbuf, создаёт новый `tcp_seg`, переносит флаги (PSH/FIN) на остаток. Вставляет после оригинального сегмента.
#### Ретрансмиссия
| Функция | Назначение |
|---------|-----------|
| `tcp_rexmit_rto_prepare()` | Переносит весь `unacked` обратно в `unsent`, ставит `TF_RTO`, сбрасывает `rttest` |
| `tcp_rexmit_rto_commit()` | Инкрементирует `nrtx`, дёргает `tcp_output()` |
| `tcp_rexmit_rto()` | prepare + commit — полный RTO retransmit |
| `tcp_rexmit()` | Перемещает 1-й unacked сегмент обратно в unsent (быстрая ретрансмиссия). Инкрементирует `nrtx`, сбрасывает `rttest` |
| `tcp_rexmit_fast()` | При 3 dupack: ретрансмитит 1-й unacked через `tcp_rexmit()`, затем `ssthresh = max(cwnd/2, 2*MSS)`, `cwnd = ssthresh + 3*MSS`, ставит `TF_INFR` |
#### tcp_zero_window_probe() — зонд при нулевом окне
Отправляет 1 байт данных из первого unsent сегмента (или FIN если сегмент уже пустой FIN). Инкрементирует `persist_probe`, `snd_nxt` сдвигается на 1.
#### Управляющие сегменты
| Функция | Назначение |
|---------|-----------|
| `tcp_rst()` | RST+ACK сегмент с указанными seqno/ackno, IP/портами |
| `tcp_send_empty_ack()` | Пустой ACK с `rcv_ann_wnd` — подтверждение приёма |
| `tcp_keepalive()` | Заглушка (keepalive отключён) |
#### Контрольные суммы
- `tcp_ip_checksum()` — IP-заголовок (20 байт, one's complement)
- `tcp_checksum()` — данные произвольной длины
- `tcp_pseudo_checksum()` — TCP псевдозаголовок (src_ip + dst_ip + proto + tcp_len + данные pbuf)
#### RTT tracking
`rttest`/`rtseq` взводятся в `tcp_output_segment()` при отправке данных если `rttest == 0` — запоминается время (`ticks`) и seqno. Сбрасывается при ретрансмиссии. Используется в `lwip_tcp_in.c` для обновления SRTT/RTO.
#### Выходной конвейер
Каждый сегмент, проходящий через `tcp_output_segment()`:
1. Заполняет ackno = `rcv_nxt`, wnd = `rcv_ann_wnd`
2. Записывает MSS-опцию если есть
3. Взводит `rttest`/`rtseq` если нужно
4. Сдвигает pbuf payload на TCP-заголовок
5. Вычисляет контрольную сумму TCP (псевдозаголовок)
6. Добавляет IP-заголовок (IPv4, vhl=0x45, ttl=64, вычисляет IP checksum)
7. Вызывает `ctx->output()` — коллбэк в tcp_proxy_client для отправки IP-пакета через ETCP
8. Снимает IP-заголовок обратно с pbuf
Проверка `tcp_output_segment_busy()` блокирует отправку если pbuf.ref != 1 (ещё читается в input).
#### Логирование и трассировка
- `lwip_tcp_trace_record()` — запись в ring-буфер событий: 'S'=send, 'X'=cwnd-limited, 'R'=RTO, 'F'=fast_rtx, 'P'=probe, 'C'=FIN
- `g_tcp_trace` — хук для внешнего наблюдателя (используется в тестах)
- `DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, ...)` — детали отправки
- Статистика: `stats.write_fails`, `stats.enq_fails`, `stats.rst_fails`, `stats.ack_fails`, `stats.probe_fails`, `stats.split_fails`, `stats.seg_fails`, `stats.pbuf_fails`
## 3. API
### Публичные (объявлены в lwip_tcp.h / lwip_tcp_priv.h)
```c
err_t tcp_write(struct tcp_pcb *pcb, const void *data, uint16_t len, uint8_t apiflags);
```
Сегментация данных в unsent очередь. `apiflags`: `TCP_WRITE_FLAG_COPY` (копировать данные, иначе zero-copy), `TCP_WRITE_FLAG_MORE` (не ставить PSH на последний сегмент). Возвращает `LERR_OK` или `LERR_MEM`.
```c
err_t tcp_output(struct tcp_pcb *pcb);
```
Отправка unsent сегментов с учётом cwnd/snd_wnd и Нейгла. Перемещает отправленные в unacked.
```c
err_t tcp_enqueue_flags(struct tcp_pcb *pcb, uint8_t flags);
```
Постановка управляющего сегмента (SYN/FIN/RST) в unsent очередь. Для SYN добавляет MSS-опцию.
```c
err_t tcp_send_fin(struct tcp_pcb *pcb);
```
Добавляет FIN к последнему unsent сегменту или создаёт новый.
```c
void tcp_rst(struct tcp_pcb *pcb, uint32_t seqno, uint32_t ackno,
uint32_t local_ip, uint32_t remote_ip,
uint16_t local_port, uint16_t remote_port);
```
Отправка RST+ACK.
```c
err_t tcp_send_empty_ack(struct tcp_pcb *pcb);
```
Пустой ACK с текущим `rcv_ann_wnd`.
```c
err_t tcp_rexmit_rto_prepare(struct tcp_pcb *pcb);
void tcp_rexmit_rto_commit(struct tcp_pcb *pcb);
void tcp_rexmit_rto(struct tcp_pcb *pcb);
```
RTO ретрансмиссия: prepare (unacked→unsent), commit (nrtx++, tcp_output). `tcp_rexmit_rto()` — одно действие.
```c
err_t tcp_rexmit(struct tcp_pcb *pcb);
```
Перемещает первый unacked сегмент в unsent.
```c
void tcp_rexmit_fast(struct tcp_pcb *pcb);
```
Быстрая ретрансмиссия при 3 dupack: ssthresh/2, cwnd=ssthresh+3*MSS, tcp_rexmit.
```c
err_t tcp_zero_window_probe(struct tcp_pcb *pcb);
```
Отправка 1-байтового зонда из первого unsent сегмента.
```c
err_t tcp_keepalive(struct tcp_pcb *pcb);
```
Заглушка (keepalive отключён в opts).
```c
err_t tcp_split_unsent_seg(struct tcp_pcb *pcb, uint16_t split);
```
Дробление первого unsent сегмента на части по split байт.
```c
uint16_t tcp_ip_checksum(const struct ip_hdr *iph);
uint16_t tcp_checksum(const void *data, uint16_t len);
```
Вычисление контрольных сумм.
## 4. Зависимости
- `lwip_tcp.h` — типы PCB, pbuf, err_t, коллбэки, enum state
- `lwip_tcp_priv.h` — `struct tcp_seg`, макросы TCPH_*, SEQ, Nagle
- `lwip_tcp_opts.h` — TCP_MSS, TCP_SND_QUEUELEN, TCP_WND etc.
- `lwip_pbuf.h` — `pbuf_alloc`, `pbuf_free`, `pbuf_header`, `pbuf_copy_partial`, `pbuf_take`, `pbuf_clen`, `pbuf_cat`
- `lib/mem.h` — u_malloc/u_free
- `lib/memory_pool.h` — `memory_pool_alloc`/`memory_pool_free` для seg_pool
- `lib/debug_config.h` — DEBUG_ERROR, DEBUG_TRACE, DEBUG_CATEGORY_ALL, DEBUG_CATEGORY_TRAFFIC

69
src/msg_transport_doc.md

@ -0,0 +1,69 @@
# Msg Transport (msg_transport)
## 1. Назначение
TCP-сервер для обмена сообщениями между локальными приложениями (например, chatgui) и удалёнными пирами через ETCP. Принимает TCP-соединения от локальных клиентов, привязывает (BIND) их к конкретному `peer_node_id` и пересылает данные туда-обратно через `etcp_router`. Поддерживает подписанные сообщения (Ed25519).
## 2. Как пользоваться
1. Вызвать `msg_transport_init()` — создаёт слушающий TCP-сокет, регистрирует callback `on_etcp_recv` в `etcp_router_bind()`.
2. Локальное приложение (chatgui) подключается по TCP и шлёт фреймы протокола.
3. При остановке вызвать `msg_transport_shutdown()`.
**Протокол фрейма:**
```
[size:2][type:1][id:8][options:1][data...]
```
- `size` — полный размер (включая поле size), native endian, до 8192 байт.
- `type` — команда (`MSG_CTL_*`, `MSG_DATA_*`, `MSG_RSP_*`).
- `id` — `peer_node_id` (uint64_t).
- `options` — битовые флаги (`MSG_OPT_SIGNED = 0x01`).
**Нюансы:**
- Приватный ключ (`GET_PRIVKEY`) отдаётся только подключениям с loopback (127.0.0.1 / ::1).
- Каждый `peer_id` может быть забинден только одним клиентом одновременно.
- Сообщения от пира без привязанного клиента молча дропаются (счётчик `drops_no_client`).
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `struct msg_transport` | Состояние сервера: `listen_fd`, связный список `clients`, `svc_id`, счётчики. |
| `struct msg_client` | Состояние TCP-клиента: `fd`, буфер приёма, `bound_peer_id`, флаг `is_localhost`. |
| `struct msg_header` | Заголовок фрейма: `size`, `type`, `id`, `options` (12 байт, packed). |
| `struct msg_transport_counters` | Счётчики: accepted, closed, bind/unbind, msg_sent/recv, errors, drops. |
### Функции
| Функция | Назначение |
|---------|-----------|
| `msg_transport_init(out, instance, ua, bind_addr, max_clients, svc_id)` | Создать сервер, привязаться к порту, зарегистрировать `svc_id` в `etcp_router_bind`. |
| `msg_transport_shutdown(t)` | Закрыть всех клиентов, освободить сокет. |
| `msg_transport_get_counters(t, c)` | Скопировать счётчики в переданную структуру. |
| `msg_build_header(hdr, payload_size, msg_type, id, options)` | Заполнить заголовок фрейма (inline). |
| `msg_validate_header(hdr)` | Проверить корректность заголовка (inline). |
### Типы сообщений
| Тип | Направление | Описание |
|-----|-------------|----------|
| `MSG_CTL_BIND` (0x01) | GUI→utun | Привязать TCP-соединение к пиру `id`. Ответ: `RSP_BIND_OK`. |
| `MSG_CTL_UNBIND` (0x02) | GUI→utun | Отвязать соединение. |
| `MSG_CTL_GET_PUBKEY` (0x03) | GUI→utun | Запросить свой публичный ключ. Ответ: `RSP_PUBKEY` (32 байта). |
| `MSG_CTL_GET_PRIVKEY` (0x04) | GUI→utun | Запросить приватный ключ (только localhost). Ответ: `RSP_PRIVKEY` (32 байта). |
| `MSG_CTL_GET_PEER_INFO` (0x05) | GUI→utun | Запросить ключи пира `id`. Ответ: `RSP_PEER_INFO` (pubkey + ed25519_pubkey = 64 байта). |
| `MSG_DATA_SEND` (0x10) | GUI→utun | Отправить данные пиру `id` (через `etcp_router_conn_send` / `send_signed`). |
| `MSG_DATA_RECV` (0x11) | utun→GUI | Получены данные от пира `id`. |
| `MSG_RSP_ERROR` (0xFF) | utun→GUI | Ошибка (1 байт код + текст). |
### Коды ошибок
| Код | Константа | Описание |
|-----|-----------|----------|
| 0x01 | `MSG_ERR_INVALID_MSG` | Некорректный заголовок или тип. |
| 0x02 | `MSG_ERR_NOT_BOUND` | Нет привязки к пиру. |
| 0x03 | `MSG_ERR_NO_ROUTE` | Нет маршрута до пира. |
| 0x04 | `MSG_ERR_SEND_FAILED` | Ошибка отправки. |
| 0x06 | `MSG_ERR_ID_TAKEN` | `peer_id` уже занят другим клиентом. |
| 0x07 | `MSG_ERR_ACCESS_DENIED` | Доступ запрещён (например, `GET_PRIVKEY` не с loopback). |
| 0x08 | `MSG_ERR_PEER_NOT_FOUND` | Пир не найден в BGP-топологии. |

44
src/nat_transport_doc.md

@ -0,0 +1,44 @@
# NAT Transport Layer (nat_transport)
## 1. Назначение
Транспортный слой NAT поверх ETCP. Обеспечивает проброс IP-трафика между клиентом и провайдером NAT через ETCP-роутер (канал `ETCP_RT_ID_NAT`). Клиент захватывает пакеты из TUN-интерфейса и отправляет провайдеру; провайдер выпускает их в интернет через свой NAT-движок, а обратные пакеты возвращает клиенту.
## 2. Как пользоваться
### Конфигурация
- **Провайдер** (`nat_via_node_id = 0`): включает `eim_nat`. В конфиге задаётся `nat_tun_ifname`, `nat_tun_ip` и параметры NAT (пул портов, шлюз).
- **Клиент** (`nat_via_node_id = X`): eim_nat не инициализируется, all трафик с NAT TUN уходит указанному узлу-провайдеру.
### Поток данных
1. **Клиент → Провайдер**: пакет из `nat_tun->output_queue` → callback `nat_transport_client_tun_out_cb` → упаковка в `NAT_SVC_HDR` (1 байт `ETCP_RT_ID_NAT` + 8 байт src_node_id + IP-пакет) → `etcp_route_send()`.
2. **Провайдер получает**: `nat_transport_etcp_recv_cb` (зарегистрирован через `etcp_router_bind`) → `eim_nat_egress()` (source NAT) → `tun_write()` в NAT TUN.
3. **Провайдер → Клиент (ответ)**: ответ из интернета попадает в `nat_tun->output_queue` → `nat_transport_provider_tun_out_cb` → `eim_nat_ingress()` (поиск исходного клиента) → упаковка в NAT_SVC_HDR → `etcp_route_send()` обратно клиенту.
4. **Клиент получает ответ**: `nat_transport_etcp_recv_cb` → `tun_write()` в NAT TUN клиента.
### Ключевые нюансы
- Используется отдельный TUN-интерфейс (`nat_tun`), не основной `tun`. Для него нужен маршрут по умолчанию в системе.
- Callback'и работают через очередь `output_queue` TUN-интерфейса. По одному пакету за вызов.
- Все пакеты на ETCP-канале предваряются 9-байтным заголовком `NAT_SVC_HDR`.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|------------|
| `nat_transport_ctx` | Контекст: `self_node_id`, `nat_via_node_id` (0=провайдер, иначе id провайдера), `nat_tun`, флаг `initialized` |
### Функции
| Функция | Назначение |
|---------|------------|
| `nat_transport_init(inst)` | Инициализация: создаёт NAT TUN, настраивает eim_nat (если провайдер), биндит `ETCP_RT_ID_NAT` через etcp_router, вешает callback'и на TUN output_queue |
| `nat_transport_destroy(inst)` | Деинициализация: анбиндит ETCP-канал, закрывает TUN, разрушает eim_nat |
### Внутренние коллбэки
| Коллбэк | Срабатывание |
|---------|-------------|
| `nat_transport_client_tun_out_cb` | Пакет из TUN output_queue на клиенте — упаковывает и шлёт провайдеру |
| `nat_transport_provider_tun_out_cb` | Пакет из TUN output_queue на провайдере — делает ingress NAT lookup, шлёт обратно клиенту |
| `nat_transport_etcp_recv_cb` | Получен пакет из канала `ETCP_RT_ID_NAT` — клиент пишет в TUN, провайдер делает egress NAT |
### Зависимости
`eim_nat.h`, `etcp_router.h`, `etcp_api.h`, `tun_if.h`, `utun_instance.h`, `ll_queue.h`

25
src/ntp_node_time_doc.md

@ -0,0 +1,25 @@
# NTP Node Time
## 1. Назначение
Синхронизация времени между узлами uTun через ETCP-канал. Позволяет вычислять смещение часов относительно каждого соседа, обнаруживать дрейф и, если узел сам не синхронизирован с NTP, корректировать свои часы по времени синхронизированного соседа. Используется для точной координации событий между узлами (db_sync, timestamp-ы сообщений в чате).
## 2. Как пользоваться
`ntp_node_time_init()` регистрирует callback на новые ETCP-соединения и callback на приём данных через `etcp_router` (service ID `ETCP_RT_ID_NTP_TIME = 0x12`). При установке нового соединения автоматически вызывается `ntp_node_on_conn_ready` — отправка `TIME_SYNC` соседу. При получении `TIME_SYNC` вычисляется per-peer offset, проверяется дрейф, и если мы не синхронизированы а сосед синхронизирован — корректируем свои часы и рассылаем коррекцию всем остальным соседям.
`ntp_node_sync_peers()` — широковещательная рассылка `TIME_SYNC` всем активным соседям. Вызывается автоматически после успешной NTP-синхронизации из `ntp_time`.
**Нюансы:**
- До 32 соседей (`NTP_NODE_MAX_PEERS`), при переполнении новый не добавляется
- Дрейф >2с — WARN, >10с — ERROR. Смещение не применяется автоматически к глобальному offset_us, кроме случая когда свой узел не синхронизирован
- Offset считается как `t4 - t_sender_us` (одностороннее смещение, без учёта RTT — предполагается что задержка ETCP мала)
## 3. API
**Структуры:**
- `struct NTP_NODE_PEER` — пара `node_id` + `offset_us` для одного соседа
- `struct NTP_NODE_TIME` — фиксированный массив `peers[32]` + счётчик `peer_count`
**Функции:**
- `ntp_node_time_init(inst)` — регистрирует bind в `etcp_router` (RT_ID 0x12) и callback на новые соединения
- `ntp_node_time_destroy(inst)` — снимает регистрацию bind и callback, обнуляет peer_count
- `ntp_node_sync_peers(inst)` — отправляет `TIME_SYNC` всем активным/initialized соседям (вызывается после NTP-синхронизации)

27
src/ntp_time_doc.md

@ -0,0 +1,27 @@
# NTP Time
## 1. Назначение
SNTP-клиент (RFC 5905) для синхронизации системного времени с внешними NTP-серверами. Модуль вычисляет смещение `offset_us = local_time - ntp_time` (положительное = локальные часы спешат) и позволяет везде в коде использовать `ntp_time_get_us()` вместо `gettimeofday()` для получения скорректированного времени. Без коррекции времени невозможна корректная работа node-to-node синхронизации (`ntp_node_time`) и оценка RTT/jitter в ETCP.
## 2. Как пользоваться
`ntp_time_init()` читает список NTP-серверов из конфига (`global.ntp_servers`) и запускает периодический опрос через uasync-таймер. Опрос — по одному серверу за цикл с round-robin, при неудаче перебирает остальные. Интервал ресинка задаётся через `ntp_resync_interval` (по умолчанию 3600с). Первый запрос — сразу после init. При успешной синхронизации вызывается `ntp_node_sync_peers()` для рассылки времени соседним узлам.
Для тестов: `ntp_time_set_test_addr()` задаёт прямой адрес NTP-сервера (IP:port), минуя DNS-резолвинг.
**Нюансы:**
- Если `ntp_enabled=0` или серверов нет, модуль не опрашивает, `ntp_time_get_us()` возвращает локальное время
- Если синхронизации не было, то же самое — возвращается `gettimeofday()` без коррекции
- SNTP (simplified NTP) — использует только один запрос-ответ, без сложной фильтрации
## 3. API
**Структуры:**
- `struct NTP_TIME` — состояние синхронизации: флаг synced, offset_us, last_sync_tb, список серверов, текущий индекс и таймер
**Функции:**
- `ntp_time_init(instance)` — читает конфиг, выделяет массив серверов, запускает первый sync-таймер
- `ntp_time_destroy(instance)` — отменяет таймер, освобождает память серверов
- `ntp_time_set_test_addr(ntp, ip, port)` — задать NTP-сервер напрямую (без DNS) для тестов
- `ntp_time_get_us(instance)` — скорректированное время в микросекундах (если synced), иначе `gettimeofday()`
- `ntp_time_get_seconds(instance)` — скорректированное время в секундах (time_t)
- `ntp_time_is_synced(instance)` — true если была успешная синхронизация

59
src/packet_dump_doc.md

@ -0,0 +1,59 @@
# Packet Dump (декодирование IP-пакетов)
## 1. Назначение
Вспомогательный модуль для диагностики: парсит сырой IPv4-пакет и формирует человекочитаемую строку с IP-адресами, протоколом, портами, флагами и hex-дампом первых 32 байт payload. Используется только в отладочных логах (категории `TUN`, `ROUTING`) для трассировки проходящих через TUN пакетов.
## 2. Как пользоваться
```c
#include "packet_dump.h"
// packet_data указывает на начало IPv4-заголовка
const uint8_t* packet_data = ...;
size_t len = ...;
char* str = dump_ip_packet_to_buffer(packet_data, len);
DEBUG_DEBUG(DEBUG_CATEGORY_TUN, "[TUN->] %s", str);
```
**Важно:** функция возвращает указатель на статический буфер (1024 байта). Не thread-safe. Каждый вызов перезаписывает предыдущий результат.
### Примеры вывода
```
IPv4 TCP 192.168.1.5:443 -> 10.0.0.2:54321 (1420 bytes) [ACK,PSH] seq=123456 ack=789012 win=64240 data: 1703012345...
IPv4 UDP 10.0.0.1:53 -> 192.168.1.3:45678 (76 bytes) len=56 data: 4500003c...
IPv4 ICMP 10.0.0.1 -> 8.8.8.8 (84 bytes) echo_request seq=1 id=1234 data: 08004d2c...
IPv4 ICMP 8.8.8.8 -> 10.0.0.1 (84 bytes) echo_reply seq=1 id=1234 data: 0000552c...
```
## 3. API
### Функции
**`dump_ip_packet_to_buffer(data, len)`** — парсит IPv4-заголовок и заголовок транспортного уровня, возвращает строку вида `IPv4 PROTO SRC:PORT -> DST:PORT (SIZE bytes) DETAILS data: HEX...`.
Параметры:
- `data` — указатель на начало IPv4-пакета
- `len` — полная длина данных
- Возвращает `static char[1024]`
Обрабатываемые протоколы:
- **TCP (6):** порты, seq/ack, флаги (FIN, SYN, RST, PSH, ACK, URG, ECE, CWR), window
- **UDP (17):** порты, длина
- **ICMP (1):** тип/code, echo_request/echo_reply с seq и id
Для нераспознанных протоколов выводит `src_ip:0 -> dst_ip:0` и 32 байта payload (может выйти за границы пакета — диагностический код, безопасностью жертвует в пользу информативности).
Hex-дамп: 32 байта транспортного payload (начиная с `data[ihl]`), 4 группы по 8 байт.
**`tcp_flags_to_str(flags)`** (static) — преобразует байт TCP-флагов в строку вида `SYN,ACK` или `none`.
### Где используется
- `tun_if.c` — логирование пакетов из/в TUN-интерфейс (категория `TUN`, уровень `DEBUG`)
- `tun_windows.c` — то же для Windows
- `routing.c` — логирование маршрутизируемых пакетов (категория `ROUTING`, уровень `DEBUG`)
### Зависимости
- `platform_compat.h` — порядок байт (не используется в текущей реализации, IP-адреса читаются напрямую как LE)
- Стандартная библиотека: `<stdio.h>`, `<string.h>`

95
src/pkt_normalizer_doc.md

@ -0,0 +1,95 @@
# Packet Normalizer (фрагментация/дефрагментация)
## 1. Назначение
Модуль адаптирует произвольные IP-пакеты к передаче через ETCP-канал с ограниченным MTU. Состоит из двух половин:
- **Packer** (упаковщик) — разбивает входящие пакеты на фрагменты размером `frag_size` и помещает их в `etcp->input_queue`. Фрагменты собираются в общий буфер пачками для экономии накладных расходов, сброс по таймеру (call_soon) при пустой входной очереди.
- **Unpacker** (распаковщик) — собирает фрагменты из `etcp->output_queue` обратно в исходные пакеты по заголовку длины (2 байта LE), помещает результат в `pn->output` (который читает ETCP router/клиент).
Каждое ETCP-соединение имеет свой экземпляр `PKTNORM` (создаётся в `etcp.c:247`).
## 2. Как пользоваться
### Создание и привязка
`pn_init()` вызывается автоматически при создании `ETCP_CONN`. Клиентский код получает нормализатор через `etcp->normalizer` и использует:
- **Отправка:** положить пакет в `pn->input` (очередь `pn_input`). Packer сам разобьёт и отправит.
- **Приём:** читать собранные пакеты из `pn->output` (очередь `pn_output`). На эту очередь ETCP вешает свой callback `etcp_int_recv`.
```c
// Создание (внутри etcp_connection_create):
etcp->normalizer = pn_init(etcp);
etcp->send_input_q = etcp->normalizer->input; // единая точка входа для отправки
// Отправка пакета (пример из тестов):
struct ll_entry* entry = ll_alloc_lldgram(len);
memcpy(entry->dgram, data, len);
entry->len = len;
queue_data_put(pn->input, entry);
// Приём (через callback на pn->output):
// queue_set_callback(pn->output, etcp_int_recv, etcp);
```
### Формат фрагментов
Каждый фрагмент, уходящий в ETCP, содержит конкатенацию пакетов, каждый предварён 2-байтовым LE-заголовком длины:
```
[packet1_len:2 LE][packet1_data][packet2_len:2 LE][packet2_data]...
```
### Сброс состояния
При разрыве/переподключении вызывать `pn_reset()`. При ошибке сборки (некорректный заголовок) вызывается `etcp_conn_reinit()` — полный перезапуск соединения.
## 3. API
### Структуры
**`struct PKTNORM`** — основная структура пары packer/unpacker.
| Поле | Описание |
|------|----------|
| `input` | Входная очередь packer (`pn_input`) — сюда кладут пакеты для отправки |
| `output` | Выходная очередь unpacker (`pn_output`) — отсюда читают собранные пакеты |
| `frag_size` | Максимальный размер одного фрагмента ETCP (`mtu - ACK_REZERV - UDP_HDR - UDP_SC_HDR`) |
| `data` / `data_ptr` / `data_size` | Буфер-накопитель packer (из `data_pool`) |
| `recvpart` | Частично собранный пакет unpacker |
| `flush_timer` | Таймер сброса накопленного буфера (call_soon) |
| `input_waiter_handle` | Backpressure-ожидание на `etcp->input_queue` |
| `tx_wait_time` | Задержка сброса (по умолчанию 10, не используется — сброс через call_soon) |
| `alloc_errors` / `logic_errors` | Счётчики ошибок |
| `in_total_pkts` / `in_total_bytes` | Статистика входящих (в packer) |
| `out_total_pkts` / `out_total_bytes` | Статистика исходящих (из unpacker) |
### Функции
**`pn_init(etcp)`** — создаёт пару PKTNORM. Выделяет `input`/`output` очереди с порогом 0 (backpressure: ждать полного опустошения). Вешает callback-и: `packer_cb` на input, `pn_unpacker_cb` на `etcp->output_queue`, `etcp_int_recv` на output. Возвращает NULL при ошибке.
**`pn_deinit(pn)`** — освобождает все ресурсы: очереди, буферы, таймеры, waiter-ы. Вызывает `routing_del_conn()` (deprecated, no-op). Снимает callback с `etcp->output_queue` для предотвращения use-after-free.
**`pn_reset(pn)`** — сбрасывает состояние packer и unpacker (буферы, pending, recvpart, таймер). Используется при переподключении.
**`pn_unpacker_reset_state(pn)`** — сбрасывает только unpacker (recvpart).
**`pn_packer_send(pn, data, len)`** — копирует данные в `ll_alloc_lldgram` и кладёт в `pn->input`. Используется только в юнит-тестах.
### Внутренние callback-и
**`packer_cb`** — callback на `pn->input`. Ждёт освобождения `etcp->input_queue` через `queue_waiter_wait`, затем вызывает `etcp_input_ready_cb`.
**`etcp_input_ready_cb`** — основной цикл packer. Извлекает пакет из `pn->input`, записывает 2-байтовый заголовок длины, копирует данные в буфер `pn->data`. При заполнении буфера (или если осталось <3 байт) вызывает `pn_send_to_etcp`. Если входная очередь пуста — ставит `flush_timer` через `uasync_call_soon` для сброса хвоста.
**`pn_flush_cb`** — таймерный callback, вызывает `pn_send_to_etcp` при простое.
**`pn_send_to_etcp(pn)`** — выделяет `ETCP_FRAGMENT` из `io_pool`, передаёт в него накопленный буфер `pn->data`, отправляет в `etcp->input_queue`. Буфер после этого обнуляется (передан во владение фрагмента).
**`pn_unpacker_cb`** — callback на `etcp->output_queue`. В цикле извлекает фрагменты, парсит 2-байтовые заголовки длины, копирует данные в `pn->recvpart`. При полной сборке пакета помещает его в `pn->output`. При ошибке вызывает `etcp_conn_reinit`.
**`pn_buf_renew(pn)`** — выделяет новый буфер из `data_pool` если `pn->data == NULL`. При нехватке места (<3 байт) вызывает `pn_send_to_etcp` для сброса текущего буфера.
### Зависимости
- `etcp.h` / `etcp_api.h` — структуры `ETCP_CONN`, `ETCP_FRAGMENT`, `etcp_int_recv`
- `routing.h` — `routing_add_conn`/`routing_del_conn` (deprecated, no-op)
- `utun_instance.h` — `UTUN_INSTANCE` (data_pool, ua)
- `ll_queue.h` — очереди, waiter, threshold
- `u_async.h` — `uasync_call_soon` для flush-таймера
- `mem.h` / `memory_pool` — аллокация буферов и фрагментов

77
src/proxy/icmp_proxy_doc.md

@ -0,0 +1,77 @@
# ICMP Proxy (icmp_proxy)
## 1. Назначение
Проксирование ICMP Echo (ping) между клиентским и exit-узлом по протоколу ETCP. Клиент
принимает ICMP Echo Request с TUN-интерфейса, упаковывает запрос и отправляет через
`etcp_route_send` на exit. Exit-узел через нативный `SOCK_RAW`-сокет отправляет ICMP Echo
во внешнюю сеть, сопоставляет ответы по `echo_id`/`echo_seq` и возвращает клиенту. Клиент
восстанавливает IP/ICMP Echo Reply и доставляет в TUN.
Один экземпляр контекста на процесс (глобальный `g_icmp_ctx`). Режим определяется флагом
`inst->tcp_proxy_server.enabled`. Для тестов предусмотрен loopback-режим без raw-сокета.
## 2. Как пользоваться
### Инициализация
```c
icmp_proxy_init(inst, ua); // exit создаёт SOCK_RAW, клиент — нет
```
При инициализации регистрируется коллбэк `icmp_proxy_recv_cb` в `etcp_router` для сервиса
`ETCP_RT_ID_ICMP_PROXY`. На exit создаётся один `SOCK_RAW(IPPROTO_ICMP)` на весь модуль.
### Клиент: отправка ping-запроса
```c
icmp_proxy_send_to_exit(inst, exit_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, len);
```
Вызывается из обработчика TUN-трафика. Запрос упаковывается и отправляется через `etcp_route_send`.
Ответ приходит асинхронно в `icmp_proxy_recv_cb` (REPLY), где вызывается `icmp_proxy_deliver_reply`
для сборки IP/ICMP Echo Reply и записи в TUN.
### Тестовый loopback
```c
icmp_proxy_set_test_loopback(inst, 1); // exit отвечает эмуляцией без raw-сокета
```
В этом режиме exit не отправляет реальный ICMP Echo, а сразу возвращает REPLY клиенту — полезно
для тестирования прохождения пакетов через ETCP без необходимости в SOCK_RAW и внешней сети.
### Очистка
```c
icmp_proxy_destroy(inst); // закрывает raw-сокет, удаляет pending-запросы, unbind
```
### Нюансы
- Запросы истекают через 5 секунд (`ICMP_TIMEOUT_TB`). Таймер `expire_timer` запускается
при добавлении первого запроса и перезапускается после каждой очистки.
- Неопознанные echo-ответы (не найденные в pending) логируются как `DEBUG_WARN` и игнорируются.
- Контрольная сумма ICMP пересчитывается на exit (при отправке) и на клиенте (при сборке ответа).
- Поле `orig_src_ip` в протоколе нужно для восстановления исходного dst_ip на клиенте.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `struct icmp_request` | Состояние активного echo-запроса на exit: client_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, время отправки. Связный список. |
| `struct icmp_proxy_ctx` | Глобальный контекст: is_exit, inst, ua, raw_sock, raw_read_id, список pending, таймаут запросов, флаг test_loopback, таймер очистки. |
### Формат пакета
| Смещение | Размер | Поле |
|----------|--------|------|
| 0 | 1 | `svc_id` = `ETCP_RT_ID_ICMP_PROXY` |
| 1 | 1 | `subcmd` = REQUEST (0x01) / REPLY (0x02) |
| 2 | 8 | `sender_node_id` |
| 10 | 4 | `dst_ip` (IP цели для ping) |
| 14 | 4 | `orig_src_ip` (IP исходного отправителя для обратной доставки) |
| 18 | 2 | `echo_id` (icmp_id) |
| 20 | 2 | `echo_seq` (icmp_seq) |
| 22 | N | `payload` (данные ICMP echo) |
### Функции
| Функция | Назначение |
|---------|-----------|
| `icmp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, на exit создаёт SOCK_RAW и регистрирует его в uasync. Регистрирует коллбэк в etcp_router. |
| `icmp_proxy_destroy(inst)` | Закрывает raw-сокет, отменяет таймер, освобождает pending, отвязывает коллбэк, освобождает память. |
| `icmp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет REQUEST → `exit_handle_request`, REPLY → `client_handle_reply`. |
| `icmp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает ICMP Echo Request и отправляет через etcp_route_send на exit. |
| `icmp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/ICMP Echo Reply-пакет и помещает в TUN input_queue. |
| `icmp_proxy_set_test_loopback(inst, enabled)` | Включает/выключает тестовый loopback-режим на exit (эмуляция echo-ответа без raw-сокета). |

132
src/proxy/socks_proxy_doc.md

@ -0,0 +1,132 @@
# socks_proxy
## 1. Назначение
Модуль реализует клиентскую сторону SOCKS5 и HTTP CONNECT прокси. Принимает TCP-соединения от локальных приложений (curl, браузер), выполняет handshake (SOCKS5 или HTTP CONNECT), определяет целевой IP:port (с разрешением доменных имён через DNS) и мультиплексирует трафик через ETCP к exit node. Взаимодействует с `tcp_proxy_server` на стороне exit node.
Файлы:
- `src/proxy/socks_proxy.c` — реализация
- `src/proxy/socks_proxy.h` — интерфейс
## 2. Как пользоваться
### Инициализация
```c
struct socks_proxy_conn* conns = NULL; // связный список активных соединений
int conn_count = 0; // счётчик соединений
uint32_t next_stream_id = 0; // монотонный генератор stream_id
struct listen_ctx* listen = socks_proxy_init_listen(
ua, "127.0.0.1:1080", &conns, &conn_count, &next_stream_id,
inst, via_node_id, is_http);
```
`is_http=0` — SOCKS5, `is_http=1` — HTTP CONNECT/прокси.
### Приём ETCP-данных от exit node
Внешний dispatcher (`tcp_proxy_client`) вызывает `socks_proxy_handle_etcp()` при получении ETCP-пакетов с `svc_id=ETCP_RT_ID_TCP_PROXY`:
```c
int handled = socks_proxy_handle_etcp(&conns, &conn_count, stream_id, subcmd, data, data_len);
// handled=1 — найден и обработан, 0 — stream_id не найден
```
Поддерживаемые subcmd: `DATA`, `CLOSE`, `ERROR`, `FIN`.
### Поиск соединения
```c
struct socks_proxy_conn* c = socks_proxy_find_conn(conns, stream_id);
// обход связного списка, O(N)
```
### Завершение
```c
socks_proxy_conn_free_all(&conns, &conn_count); // все соединения
socks_proxy_close_listen(ua, listen, &sock_out); // слушатель
```
### Ключевые нюансы
- **Бэкпрессур (backpressure):** при неудаче `etcp_route_send()` данные сохраняются в `tx_buf/tx_len`, и регистрируется waiter через `etcp_router_on_send_ready()`. Когда ETCP готов к отправке, `tx_waiter_cb` повторяет попытку и возобновляет `read_queue`.
- **Несколько соединений:** управляются через связный список `struct socks_proxy_conn*` + счётчик. Каждое соединение — отдельный TCP-сокет (`tcp_conn`) и независимый ETCP stream.
- **stream_id** генерируется монотонно (`++next_stream_id`), передаётся в заголовке ETCP-пакетов для мультиплексирования.
## 3. API
### Структуры
**`struct socks_proxy_conn`** — одно прокси-соединение:
- `tc` — TCP-соединение с клиентом (`tcp_conn`)
- `stream_id` — уникальный идентификатор потока в ETCP
- `dest_ip[4]`, `dest_port` — целевой адрес (куда хочет подключиться клиент)
- `state` — текущее состояние (SOCKS_STATE_* или HTTP_STATE_*)
- `is_http` — флаг: 0=SOCKS5, 1=HTTP
- `rem_closed`, `fin_remote`, `close_sent`, `close_pending` — флаги состояния закрытия
- `buf[1024]`, `buf_len` — буфер накопления данных для парсинга handshake
- `tx_buf`, `tx_len`, `tx_waiter` — бэкпрессур: отложенные данные и waiter handle
- `bytes_to_client`, `bytes_from_exit` — счётчики переданных байт (диагностика)
- `head`, `count` — указатели на голову списка и счётчик (для самоудаления)
**`struct listen_ctx`** (непрозрачный) — контекст слушающего сокета:
- `listen_sock`, `socket_id` — сокет и uasync handle
- `conns`, `conn_count`, `next_stream_id` — общий список соединений
### Основные функции
| Функция | Описание |
|---------|----------|
| `socks_proxy_init_listen(ua, addr, conns, count, next_sid, inst, via_node, is_http)` | Создаёт слушающий TCP-сокет на `addr` (IP:PORT). Возвращает `listen_ctx*`. |
| `socks_proxy_close_listen(ua, ctx, sock_out)` | Закрывает слушающий сокет, освобождает ctx. |
| `socks_proxy_handle_etcp(head, count, stream_id, subcmd, data, len)` | Диспетчер ETCP-сообщений от exit node. Обрабатывает DATA (→write_queue клиенту), CLOSE/ERROR (→push_close), FIN (→push_fin). |
| `socks_proxy_find_conn(head, stream_id)` | Поиск соединения по stream_id в связном списке. |
| `socks_proxy_conn_free(c)` | Освобождает соединение: отправляет отложенный CLOSE/ERROR в ETCP, удаляет из списка, освобождает `tcp_conn`, buffer, waiter. |
| `socks_proxy_conn_free_all(head, count)` | Освобождает все соединения в списке. |
### Внутренние функции
| Функция | Описание |
|---------|----------|
| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет: `[TCP_PROXY_HDR_SIZE=6] svc_id(1)+subcmd(1)+stream_id(4)` + payload, отправляет через `etcp_route_send`. |
| `send_connect(c)` | Отправляет CONNECT (dest_ip[4]+dest_port[2]) на exit node. |
| `send_data(c, data, len)` | Отправляет DATA (релей пользовательского трафика). |
| `send_close(c)`, `send_error(c)`, `send_fin(c)` | Отправляют управляющие subcmd. |
| `write_to_client(c, data, len)` | Запись данных в `tc->write_queue` (отправка клиенту через TCP). |
| `process_socks_greeting(c)` | Парсинг SOCKS5 GREETING: ver=5, выбор метода 0x00 (no auth), ответ 05 00. |
| `process_socks_request(c)` | Парсинг SOCKS5 CONNECT REQUEST (atyp=1/3/4, IPv4/IPv6/domain). DNS-резолвинг для доменов. Ответ 05 00 00 01 + dummy addr. |
| `process_http_request(c)` | Парсинг HTTP: CONNECT host:port (туннель) или GET/POST с абсолютным URL (режим HTTP-прокси с переписыванием URL в относительный). |
| `on_accept_cb(sock, arg)` | Callback accept: создаёт `socks_proxy_conn`, `tcp_conn_create`, вешает read callback. |
| `on_read_cb(q, arg)` | Callback чтения из TCP: handshake-парсинг или релей данных через ETCP с бэкпрессуром. |
| `tx_waiter_cb(q, arg)` | Callback бэкпрессура: повторная отправка отложенных данных при готовности ETCP. |
| `on_fin_cb`, `on_error_cb`, `on_closed_cb` | Callback'и жизненного цикла `tcp_conn`: FIN→удалённый FIN, ошибка→send_error+free, закрыт→send_close+free. |
## 4. Протокол
### SOCKS5 flow (is_http=0)
```
Клиент → uTun: [ver=5][nmethods][methods...]
uTun → Клиент: [05][00] (GREETING: no auth)
Клиент → uTun: [05][01][00][atyp][addr...][port] (CONNECT REQUEST)
uTun → Клиент: [05][00][00][01][0.0.0.0][0] (ответ ок)
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port]
Клиент ↔ uTun ↔ exit: RELAY данных через ETCP DATA
```
### HTTP CONNECT flow (is_http=1)
```
Клиент → uTun: CONNECT host:port HTTP/1.1\r\n\r\n
uTun → Клиент: HTTP/1.1 200 Connection Established\r\n\r\n
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port]
Клиент ↔ uTun ↔ exit: RELAY данных через ETCP DATA
```
### HTTP-прокси (GET/POST, is_http=1)
```
Клиент → uTun: GET http://host:port/path HTTP/1.1\r\nHost: ...\r\n\r\n
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port] + TCP_PROXY DATA[GET /path HTTP/1.1\r\n...]
Клиент ↔ uTun ↔ exit: RELAY данных через ETCP DATA
```
URL переписывается в относительный (`/path`), остальные заголовки передаются как есть.
## 5. Зависимости
- `tcp_proxy_server.h` — константы subcmd (`TCP_PROXY_SUBCMD_*`) и размер заголовка (`TCP_PROXY_HDR_SIZE=6`)
- `etcp.h` / `etcp_api.h` — `etcp_route_send()`, `ETCP_RT_ID_TCP_PROXY`
- `etcp_router.h` — `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()`, `etcp_router_waiter_cancel()`
- `tcp_io.h` / `tcp_conn` — TCP-соединения (`tcp_conn_create`, `tcp_conn_push_close`, `tcp_conn_push_fin`, write/read очереди)
- `u_async.h` — event loop, `uasync_add_socket_t`
- `ll_queue.h` — очереди (write_queue, read_queue), waiter механизм
- `memory_pool.h` — аллокация из пулов (`entry_pool`, `data_pool`)
- `utun_instance.h` — `struct UTUN_INSTANCE`

204
src/proxy/tcp_proxy_client_doc.md

@ -0,0 +1,204 @@
# tcp_proxy_client
## 1. Назначение
Клиентский оркестратор TCP прокси. Создаёт TUN-интерфейс, запускает встроенный lwIP TCP стек, перехватывает исходящие TCP-соединения со стороны клиента и туннелирует их через ETCP к удалённому exit-узлу (tcp_proxy_server). Поддерживает три режима перехвата трафика одновременно:
1. **Прозрачное проксирование через TUN** — весь TCP-трафик с TUN-интерфейса попадает в lwIP, который управляет реальными TCP-сессиями. Исходящие соединения прозрачно проксируются на exit-узел. UDP (через `udp_proxy`) и ICMP Echo (через `icmp_proxy`) также пробрасываются.
2. **SOCKS5 proxy listener** — встроенный SOCKS5-сервер, принимающий локальные подключения и туннелирующий их через ETCP.
3. **HTTP CONNECT proxy listener** — встроенный HTTP CONNECT-прокси (работает через тот же механизм SOCKS, что и SOCKS5).
Модуль полностью оркестрирует весь TCP-прокси-пайплайн на стороне клиента: захват трафика → lwIP TCP стек → ETCP-туннелирование → приём ответных данных → отдача в lwIP → запись IP-пакетов обратно в TUN.
## 2. Как пользоваться
### Типовой сценарий
```
tcp_proxy_client_create(inst, ua, tun_name, tun_ip, mtu, test_mode,
mappings, mapping_count, via_node_id,
socks_enabled, socks_addr, http_proxy_enabled, http_proxy_addr)
→ TUN init (tun_init_nat) — создание TUN-интерфейса с NAT
→ lwIP init (lwip_tcp_init) — запуск встроенного TCP стека
→ Настройка port mappings — прослушивание локальных портов в lwIP
→ SOCKS5/HTTP listen — запуск локальных прокси-слушателей
→ ETCP router bind — регистрация коллбэка tcp_proxy_client_router_recv_cb
→ UDP/ICMP proxy init
```
### Ключевые концепции
**TUN interface** через `tun_init_nat()`. IP-пакеты из output_queue TUN направляются в `tcp_proxy_client_tun_input()` — главную точку входа inbound-трафика.
**Встроенный lwIP TCP стек** (`lwip_tcp_init()`) с output-коллбэком `tcp_proxy_client_output_cb`, который пишет готовые IP-пакеты обратно в TUN (в сторону ОС клиента).
**Inbound TUN трафик** (`tcp_proxy_client_tun_input()`): парсинг IP-пакета → по протоколу:
- TCP → вызов `lwip_tcp_input()` — пакет попадает в lwIP, где срабатывает `accept_cb` для новых соединений или `recv_cb` для существующих
- UDP → `udp_proxy_send_to_exit()` — проброс UDP в exit
- ICMP (type 8/Echo) → `icmp_proxy_send_to_exit()` — проброс ping в exit
**Connection accept** (`tcp_proxy_client_accept_cb`): при новом входящем TCP-соединении в lwIP:
1. Выделяется `struct tcp_proxy_client_conn`
2. Определяется destination: поиск по port mapping config `mappings[]` (local_port → remote_ip:remote_port), иначе `local_ip:local_port` самого pcb
3. Назначается `stream_id` (инкремент `next_stream_id`)
4. Регистрируются lwIP коллбэки: `recv`, `sent`, `err`, `poll`
5. Создаётся очередь `to_lwip` для данных от exit
6. Отправляется ETCP CONNECT с dest_ip+dest_port через `etcp_route_send(force=1)`
**Client-side lwIP** — чтение данных (`tcp_proxy_client_recv_cb`):
- При получении данных от lwIP (клиент что-то отправляет): копируем pbuf → `tcp_proxy_client_send_data()` → ETCP DATA
- При backpressure (очередь нормализатора переполнена): сохраняем в `tx_buf`, регистрируем waiter через `etcp_router_waiter_register()`. При освобождении очереди `tcp_proxy_client_tx_waiter_cb` досылает данные и вызывает `tcp_recved()` для window update
- При p==NULL (FIN от lwIP): выставляется `fin_local`, отправляется ETCP FIN. Если обе стороны закрылись — ETCP CLOSE
**Exit-side data** — приём ответных данных (`tcp_proxy_client_handle_data()`):
- Данные от exit помещаются в очередь `to_lwip`
- `tcp_proxy_client_feed_from_transport()`: пока есть место в `tcp_sndbuf(pcb)`, читает из `to_lwip` и пишет через `tcp_write()` в lwIP. После записи вызывает `tcp_output()` для отправки
- Trigger на feed: при получении DATA, при `sent_cb` (lwIP освободил send buffer)
**Full lifecycle:**
1. CONNECT — установка соединения с exit
2. DATA ↔ DATA — двунаправленный обмен данными
3. FIN (от клиента или exit) — half-close: `tcp_shutdown(pcb, 0, 1)` (закрытие только send-стороны)
4. CLOSE — полное закрытие после обоих FIN, очистка `tcp_proxy_client_conn_free()`
5. ERROR — аварийное закрытие (ошибка lwIP или exit), немедленная очистка
**Per-port mapping конфигурация** (`tcp_proxy_client_mapping_config`):
```c
struct tcp_proxy_client_mapping_config {
uint16_t local_port; // локальный порт для прослушивания
char remote_ip[64]; // IP назначения на exit-узле
uint16_t remote_port; // порт назначения на exit-узле
};
```
При старте для каждого mapping создаётся listen pcb в lwIP. Также работает динамический listen для любых исходящих портов (`tcp_proxy_client_ensure_outbound_listen()`).
**SOCKS5/HTTP proxy listeners** — используют `socks_proxy_init_listen()` (общий код с SOCKS, флаг `is_http` различает режимы). SOCKS/HTTP обрабатываются приоритетно в `router_recv_cb` — им не нужен TUN/lwIP.
### Протокол обмена с exit-узлом
Формат сообщения: `svc_id(1) + subcmd(1) + stream_id(4) + [data]` (заголовок `TCP_PROXY_HDR_SIZE = 6` байт).
| Subcmd | Код | Направление | Описание |
|--------|-----|-------------|----------|
| CONNECT | 0x01 | client→exit | Установка TCP-соединения с destination |
| DATA | 0x03 | ↔ | Данные TCP-потока |
| CLOSE | 0x04 | ↔ | Полное закрытие потока |
| ERROR | 0x05 | ↔ | Ошибка, немедленная очистка |
| FIN | 0x06 | ↔ | Half-close (shutdown write) |
Специальное сообщение: `svc_id(1) + peer_id(8)` (9 байт) — CLOSE_ALL: очистка всех соединений к указанному peer.
### Механизмы надёжности
- **Backpressure в нормализатор**: `etcp_router_waiter_register()` — если `etcp_route_send()` не может отправить (очередь переполнена), данные буферизуются в `tx_buf` и досылаются при освобождении
- **Повтор CLOSE/ERROR**: если отправка CLOSE/ERROR не удалась, выставляется `close_pending=1`. В `poll_cb` повторяется попытка
- **Очередь to_lwip**: буферизует данные от exit до готовности lwIP send buffer, обеспечивая window-based flow control
## 3. API
### Структуры
#### `struct tcp_proxy_client_conn` — одно проксируемое TCP-соединение
| Поле | Тип | Назначение |
|------|-----|------------|
| `next` | `*` | Связный список всех соединений |
| `proxy` | `*tcp_proxy_client` | Обратный указатель на родительский прокси |
| `pcb` | `*tcp_pcb` | lwIP protocol control block для этого соединения |
| `stream_id` | `uint32_t` | Уникальный идентификатор потока в протоколе ETCP |
| `to_lwip` | `*ll_queue` | Очередь данных от exit → lwIP (flow control) |
| `fin_local` | `uint8_t` | Локальная сторона (lwIP) отправила FIN |
| `fin_remote` | `uint8_t` | Удалённая сторона (exit) отправила FIN |
| `rem_closed` | `uint8_t` | Exit прислал CLOSE — прекращаем обработку |
| `error` | `uint8_t` | Ошибка на любом конце — немедленная очистка |
| `close_sent` | `uint8_t` | CLOSE/ERROR отправлен на exit |
| `close_pending` | `uint8_t` | CLOSE/ERROR не доставлен, ждём повтора в poll |
| `tx_buf` | `*uint8_t` | Буфер при backpressure (lwIP→ETCP send fail) |
| `tx_len` | `uint16_t` | Длина данных в tx_buf |
| `tx_waiter` | `queue_waiter_handle` | Дескриптор waiter для backpressure |
| `dest_ip` | `uint8_t[4]` | IP назначения на exit-узле |
| `dest_port` | `uint16_t` | Порт назначения на exit-узле (network byte order) |
#### `struct tcp_proxy_client` — состояние прокси-клиента
| Поле | Тип | Назначение |
|------|-----|------------|
| `inst` | `*UTUN_INSTANCE` | Родительский инстанс uTun |
| `ua` | `*UASYNC` | Асинхронный event loop |
| `tun` | `*tun_if` | TUN-интерфейс для захвата/инжекции трафика |
| `conns` | `*tcp_proxy_client_conn` | Голова связного списка lwIP-соединений |
| `conn_count` | `int` | Количество активных lwIP-соединений |
| `entry_pool` | `*memory_pool` | Пул для `struct ll_entry` (данные to_lwip) |
| `next_stream_id` | `uint32_t` | Счётчик stream_id (начинается с 1) |
| `via_node_id` | `uint64_t` | Node ID exit-узла для маршрутизации ETCP |
| `lwip` | `*lwip_tcp_ctx` | Контекст встроенного lwIP TCP стека |
| `mappings` | `*mapping_config` | Указатель на массив port mapping конфигурации |
| `mapping_count` | `int` | Количество mapping записей |
| `socks_enabled` | `int` | Флаг включения SOCKS5 прокси |
| `socks_conns` | `*socks_proxy_conn` | Список активных SOCKS5-соединений |
| `socks_conn_count` | `int` | Количество SOCKS5-соединений |
| `socks_listen` | `*listen_ctx` | Слушающий сокет SOCKS5 |
| `http_proxy_enabled` | `int` | Флаг включения HTTP CONNECT прокси |
| `http_conns` | `*socks_proxy_conn` | Список активных HTTP CONNECT соединений |
| `http_conn_count` | `int` | Количество HTTP CONNECT соединений |
| `http_listen` | `*listen_ctx` | Слушающий сокет HTTP CONNECT |
### Функции
#### `tcp_proxy_client_create()`
Создаёт и инициализирует весь прокси-клиент. Параметры: инстанс uTun, uasync, имя/IP/MTU TUN-интерфейса, test_mode, массив port mapping, via_node_id, флаги/адреса SOCKS и HTTP прокси. Возвращает NULL при ошибке.
#### `tcp_proxy_client_destroy()`
Полная очистка: отвязка от ETCP router, остановка UDP/ICMP прокси, закрытие SOCKS/HTTP listeners, освобождение всех соединений (lwIP abort + очистка очередей to_lwip + tx_buf + waiter cancel), уничтожение lwIP стека, закрытие TUN, освобождение memory pool.
#### `tcp_proxy_client_router_recv_cb()`
Главный ETCP-коллбэк (зарегистрирован через `etcp_router_bind` на `ETCP_RT_ID_TCP_PROXY=0x04`). Обрабатывает входящие сообщения от exit-узла:
- **DATA** → `tcp_proxy_client_handle_data()` — данные в очередь to_lwip и feed в lwIP
- **CLOSE** → `tcp_proxy_client_handle_close()` — пометка rem_closed, закрытие lwIP pcb, освобождение соединения
- **ERROR** → `tcp_proxy_client_handle_error()` — аварийное закрытие lwIP pcb, освобождение соединения
- **FIN** → `tcp_proxy_client_handle_fin()` — half-close через tcp_shutdown(pcb, 0, 1)
- **CLOSE_ALL** (9-байтовый пакет без conn) → очистка всех соединений к peer
- Сначала пробует SOCKS/HTTP обработчики (приоритет — могут работать без TUN)
### Внутренние функции
#### lwIP коллбэки
- **`tcp_proxy_client_output_cb()`** — output lwIP: запись IP-пакетов в TUN (в сторону ОС клиента)
- **`tcp_proxy_client_accept_cb()`** — новый входящий TCP: создание `tcp_proxy_client_conn`, определение destination из mapping конфига, отправка ETCP CONNECT
- **`tcp_proxy_client_recv_cb()`** — данные от клиента (lwIP→ETCP): копирование pbuf → ETCP DATA с backpressure
- **`tcp_proxy_client_sent_cb()`** — lwIP освободил send buffer: вызов feed_from_transport для передачи накопленных данных
- **`tcp_proxy_client_err_cb()`** — ошибка lwIP: pcb уже уничтожен стеком, отправка ETCP ERROR
- **`tcp_proxy_client_poll_cb()`** — периодический poll (2 интервала): очистка при error, повтор close_pending
#### Отправка сообщений в ETCP
- **`tcp_proxy_client_send_msg()`** — базовая функция: сборка пакета `svc_id+subcmd+stream_id+data`, отправка через `etcp_route_send()`
- **`tcp_proxy_client_send_connect()`** — CONNECT с dest_ip+dest_port (6 байт данных), force=1
- **`tcp_proxy_client_send_data()`** — DATA с payload, без force (backpressure через waiter)
- **`tcp_proxy_client_send_close()`** — CLOSE, force=1, при ошибке выставляет close_pending
- **`tcp_proxy_client_send_error()`** — ERROR, force=1, при ошибке выставляет close_pending
- **`tcp_proxy_client_send_fin()`** — FIN, force=1
#### Обработка трафика
- **`tcp_proxy_client_tun_input()`** — коллбэк очереди output_queue TUN. Парсит IP-пакеты: TCP → lwip_tcp_input (с динамическим listen на целевой порт), UDP/ICMP → соответствующие прокси
- **`tcp_proxy_client_handle_non_tcp()`** — перенаправление UDP в `udp_proxy_send_to_exit()` и ICMP Echo в `icmp_proxy_send_to_exit()`
- **`tcp_proxy_client_feed_from_transport()`** — чтение данных из очереди `to_lwip` и запись в lwIP через `tcp_write()` пока есть место в send buffer. После feed вызывает `tcp_output()`
- **`tcp_proxy_client_tx_waiter_cb()`** — коллбэк backpressure waiter: досылка `tx_buf` при освобождении normalizer очереди, вызов `tcp_recved()` для window update
- **`tcp_proxy_client_ensure_outbound_listen()`** — динамическое создание listen pcb в lwIP для заданного порта (если ещё не создан), нужно для прозрачного проксирования любых исходящих TCP-соединений
#### Управление памятью и соединениями
- **`tcp_proxy_client_entry_from_data()`** — создание `ll_entry` из пула `entry_pool` с копированием данных
- **`tcp_proxy_client_find_conn()`** — линейный поиск соединения по `stream_id`
- **`tcp_proxy_client_conn_free()`** — полная очистка одного соединения: досылка pending CLOSE/ERROR, удаление из списка, очистка очереди to_lwip, освобождение tx_buf, cancel waiter, free памяти
### Зависимости
- `tun_if.h` — TUN-интерфейс (инициализация, запись, закрытие)
- `lwip_tcp/lwip_tcp.h` — встроенный lwIP TCP стек (инициализация, input, управление pcb)
- `etcp.h` / `etcp_api.h` / `etcp_router.h` — ETCP-маршрутизация и отправка данных
- `tcp_proxy_server.h` — общие константы протокола (TCP_PROXY_SUBCMD_*, TCP_PROXY_HDR_SIZE)
- `socks_proxy.h` — SOCKS5/HTTP CONNECT прокси (init_listen, handle_etcp, conn_free_all)
- `udp_proxy.h` / `icmp_proxy.h` — проброс UDP и ICMP
- `config_parser.h` — `tcp_proxy_client_mapping_config` (local_port → remote_ip:remote_port)
- `utun_instance.h` — `struct UTUN_INSTANCE` с полем `tcp_proxy_client`
- `lib/u_async.h` — асинхронный event loop
- `lib/ll_queue.h` — lock-free очереди (to_lwip, output_queue TUN)
- `lib/memory_pool.h` — пул для `struct ll_entry`
- `lib/mem.h` — обёртки malloc/free с учётом утечек
- `lib/debug_config.h` — логирование

108
src/proxy/tcp_proxy_server_doc.md

@ -0,0 +1,108 @@
# tcp_proxy_server
## 1. Назначение
TCP прокси-сервер на exit node. Принимает CONNECT-запросы от клиента через ETCP, создаёт OS TCP сокет к реальному адресату, релеит данные в обе стороны: клиент↔ETCP↔сервер↔OS TCP↔destination. Поддерживает backpressure (пауза чтения из OS сокета при заполнении ETCP очереди), graceful CLOSE, экспоненциальный ретрай управляющих сообщений, диагностические таймеры.
Работает в паре с `tcp_proxy_client` (на ingress node) и `lwip_tcp` (lwIP-стек для обработки TCP-сессий).
При старте сервера автоматически инициализируются `udp_proxy` и `icmp_proxy`, если клиентский TCP прокси не активен (чтобы exit node обслуживал все виды трафика).
## 2. Как пользоваться
### Включение
В конфиге: `tcp_proxy_server_enabled = yes` — сервер регистрирует обработчик `ETCP_RT_ID_TCP_PROXY` через `etcp_router_bind()` и готов принимать команды от клиентов.
### Типовой сценарий
1. **CONNECT (0x01):** клиент шлёт `dest_ip(4) + dest_port(2)`, сервер создаёт `tcp_proxy_server_conn`, открывает OS TCP сокет, делает `connect()` к destination.
2. **DATA (0x03):** клиент шлёт данные → сервер пишет в OS write_queue через pool-аллокацию.
3. **Чтение из destination:** данные с OS сокета попадают в `read_queue`, оттуда `read_queue_drain_cb` релеит их клиенту через ETCP.
4. **CLOSE (0x04):** клиент закрыл соединение → сервер паузит чтение, сливает read_queue, шлёт shutdown(SHUT_WR) в OS сокет.
5. **FIN (0x06):** клиент подтверждает получение FIN от destination → сервер push-ит FIN в OS сокет (завершение полного закрытия).
6. **ERROR (0x05):** ошибка на клиенте → сервер форсированно закрывает соединение.
7. **Очистка:** по `on_closed_cb` (OS сокет закрыт полностью) → `tcp_proxy_server_conn_free()` освобождает все ресурсы.
### Ключевые нюансы
- **Backpressure:** если `etcp_route_send()` вернула ошибку — данные буферизуются в `tx_buf`, регистрируется `pause_waiter` (ожидание освобождения ETCP канала), запускается fallback `retry_timer` (500ms, force=1). При возобновлении — сначала делается повтор tx_buf, потом продолжается дрейн очереди.
- **FIN handling:** FIN от destination не отправляется клиенту пока есть pending writes в OS сокет (ждём `on_flushed_cb`) или pending reads (tx_buf / read_queue не пусты — флаг `dst_fin_deferred`). При опустошении очередей — FIN отправляется немедленно.
- **Graceful CLOSE:** при получении CLOSE от клиента чтение из OS сокета паузится через `tcp_conn_pause_read()`, read_queue сливается (данные отбрасываются), push-ится close в OS сокет.
- **Ретрай CLOSE/ERROR:** при неудачной отправке `send_msg()` — экспоненциальный backoff (50→100→200→…→5000 tb = 5ms→500ms) через `close_timer` + `close_retry_cb`.
- **Диагностический таймер (1s):** логирует TCP буферы ОС, размеры read/write очередей, free counts entry/data pool-ов.
- **CLOSE_ALL:** специальный 9-байтовый пакет без валидного conn удаляет все соединения для заданного peer.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `tcp_proxy_server` | Корневой контекст: enabled, связный список conns, conn_count, ссылка на UTUN_INSTANCE |
| `tcp_proxy_server_conn` | Состояние одного прокси-соединения: stream_id, peer_node_id, dest_ip:port, tcp_conn, буферы/счётчики/таймеры для backpressure и закрытия |
### Функции инициализации/завершения
| Функция | Описание |
|---------|----------|
| `tcp_proxy_server_init(inst)` | Регистрирует ETCP биндинг на `ETCP_RT_ID_TCP_PROXY`, авто-инициализирует UDP/ICMP прокси если tcp_proxy_client не активен |
| `tcp_proxy_server_destroy(inst)` | Освобождает все соединения, уничтожает UDP/ICMP прокси |
### Обработчики команд (вызываются из `tcp_proxy_server_recv_cb`)
| Функция | Описание |
|---------|----------|
| `tcp_proxy_server_recv_cb(conn, entry)` | Главный ETCP-коллбэк: диспетчеризует subcmd → handle_connect/data/close/error/fin, обрабатывает CLOSE_ALL |
| `tcp_proxy_server_handle_connect(inst,entry,sid,src)` | Создаёт OS TCP сокет, `connect()` к destination, аллоцирует `tcp_proxy_server_conn`, регистрирует diag_timer и коллбэки tcp_conn |
| `tcp_proxy_server_handle_data(inst,conn,entry,sid)` | Копирует данные клиента в write_queue OS сокета через pool-аллокацию |
| `tcp_proxy_server_handle_close(inst, sid)` | Паузит чтение, сливает read_queue, отправляет shutdown в OS сокет, освобождает соединение если не connected |
| `tcp_proxy_server_handle_error(inst, sid)` | Делегирует в `handle_close` (обрабатывается идентично CLOSE) |
| `tcp_proxy_server_handle_fin(inst, sid)` | Push-ит FIN в write_queue OS сокета (клиент подтвердил получение FIN от destination) |
### Управление соединениями
| Функция | Описание |
|---------|----------|
| `tcp_proxy_server_find_conn(ctx, sid)` | Линейный поиск соединения по stream_id в связном списке |
| `tcp_proxy_server_conn_free(rc)` | Полное освобождение: защита от double-free, отмена таймеров, отправка pending close/error если надо, удаление из списка, `tcp_conn_destroy()`, u_free |
### Внутренние callback-и (статические)
| Функция | Описание |
|---------|----------|
| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет (svc_id + subcmd + sid + data) и отправляет через `etcp_route_send()` |
| `send_close(rc)` | Отправляет CLOSE клиенту, при неудаче запускает экспоненциальный ретрай через `close_timer` |
| `send_error(rc)` | Аналогично `send_close`, но шлёт ERROR |
| `send_fin(rc)` | Отправляет FIN клиенту (без ретрая) |
| `on_fin_cb(tc, arg)` | OS сокет получил FIN → машина состояний: если локальный FIN тоже → close; если write pending → ждать flush; если read pending → отложить FIN |
| `on_flushed_cb(tc, arg)` | Write очередь OS сокета опустела → отправить FIN клиенту или закрыть если оба FIN получены |
| `on_error_cb(tc, err, arg)` | Ошибка OS сокета → отправить ERROR клиенту, освободить соединение |
| `on_closed_cb(tc, arg)` | OS сокет полностью закрыт → освободить соединение |
| `read_queue_drain_cb(q, arg)` | Дрейн read_queue OS сокета → отправка данных клиенту через ETCP; backpressure: буферизация в tx_buf + pause_waiter + retry_timer |
| `pause_resume_cb(q, arg)` | ETCP канал освободился → возобновить дрейн read_queue |
| `retry_timer_cb(arg)` | Fallback-таймер 500ms: повтор tx_buf с force=1, при успехе возобновляет read_queue |
| `close_retry_cb(arg)` | Экспоненциальный backoff для повторной отправки CLOSE/ERROR (50→5000 tb) |
| `diag_timer_cb(arg)` | 1-секундный таймер: логирует TCP буферы ОС, размеры очередей, free-counts pool-ов |
### Константы
| Макрос | Значение | Описание |
|--------|----------|----------|
| `TCP_PROXY_HDR_SIZE` | 6 | svc_id(1) + subcmd(1) + stream_id(4) |
| `TCP_PROXY_CONNECT_HDR_SIZE` | 12 | HDR_SIZE + dest_ip(4) + dest_port(2) |
| `TCP_PROXY_SUBCMD_CONNECT` | 0x01 | Запрос на создание TCP соединения к destination |
| `TCP_PROXY_SUBCMD_DATA` | 0x03 | Данные для отправки в destination |
| `TCP_PROXY_SUBCMD_CLOSE` | 0x04 | Клиент закрывает прокси-соединение |
| `TCP_PROXY_SUBCMD_ERROR` | 0x05 | Ошибка на стороне клиента |
| `TCP_PROXY_SUBCMD_FIN` | 0x06 | Клиент подтвердил FIN от destination |
### Зависимости
| Модуль | Использование |
|--------|--------------|
| `tcp_io.h` | `tcp_conn` — абстракция OS TCP сокета с read/write очередями и memory pool-ами |
| `ll_queue.h` | Очереди read_queue/write_queue OS сокета, pause_waiter для backpressure |
| `etcp_router.h` | `etcp_router_bind()`, `etcp_route_send()`, `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()` |
| `u_async.h` | Таймеры (`uasync_set_timeout`, `uasync_cancel_timeout`) |
| `udp_proxy.h` | Авто-инициализация при старте сервера |
| `icmp_proxy.h` | Авто-инициализация при старте сервера |
| `config_parser.h` | Флаг `tcp_proxy_server_enabled` |
| `mem.h` | `u_malloc`/`u_free`/`u_calloc` для tx_buf и структур |

73
src/proxy/udp_proxy_doc.md

@ -0,0 +1,73 @@
# UDP Proxy (udp_proxy)
## 1. Назначение
Проксирование UDP-датаграмм между клиентским и exit-узлом по протоколу ETCP. Клиентский узел
принимает UDP-пакеты с TUN-интерфейса, инкапсулирует их и отправляет через `etcp_route_send`
на exit-узел. Exit-узел создаёт нативные UDP-сокеты для каждого потока (5-tuple) и ретранслирует
датаграммы во внешнюю сеть. Ответы проходят обратный путь — от exit к клиенту и затем в TUN.
Один экземпляр контекста на процесс (глобальный `g_udp_ctx`), режим определяется флагом
`inst->tcp_proxy_server.enabled`: включён → `is_exit=1`, иначе `is_exit=0`.
## 2. Как пользоваться
### Инициализация
```c
udp_proxy_init(inst, ua); // exit или клиент — определяется автоматически
```
При инициализации регистрируется коллбэк `udp_proxy_recv_cb` в `etcp_router` для сервиса
`ETCP_RT_ID_UDP_PROXY`.
### Клиент: отправка датаграммы в exit
```c
udp_proxy_send_to_exit(inst, exit_node_id, src_ip, src_port, dst_ip, dst_port, payload, len);
```
Вызывается из обработчика TUN-трафика. Пакет упаковывается в формат ETCP и отправляется
через `etcp_route_send`. Ответ приходит асинхронно в `udp_proxy_recv_cb`, который вызывает
`udp_proxy_deliver_reply` для записи IP/UDP-пакета обратно в TUN.
### Exit: приём датаграмм
Exit автоматически принимает REQUEST-ы через `etcp_router`. Для каждого нового потока
(уникальный `client_node_id + src_ip:port + dst_ip:port`) создаётся нативный UDP-сокет,
который регистрируется в `uasync` на чтение. При получении ответа от внешнего хоста
ответ упаковывается и отправляется обратно клиенту.
### Очистка
```c
udp_proxy_destroy(inst); // закрывает все flows, отменяет таймер, unbind из etcp_router
```
### Нюансы
- Потоки истекают через 60 секунд неактивности (`UDP_FLOW_TIMEOUT_TB`). Таймер `expire_timer`
запускается при создании первого потока, перезапускается после каждой очистки.
- UDP-сокеты на exit привязываются к `INADDR_ANY:0` (bind), трафик отправляется через `sendto`.
- Ответы в TUN на клиенте доставляются через `inst->tcp_proxy_client->tun->input_queue`.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|-----------|
| `struct udp_flow` | Состояние одного UDP-потока на exit: 5-tuple, нативный сокет, uasync read_id, времена создания и последней активности. Связный список. |
| `struct udp_proxy_ctx` | Глобальный контекст: флаг is_exit, inst, ua, список flows, таймаут истечения, таймер очистки. |
### Формат пакета
| Смещение | Размер | Поле |
|----------|--------|------|
| 0 | 1 | `svc_id` = `ETCP_RT_ID_UDP_PROXY` |
| 1 | 1 | `subcmd` = `UDP_PROXY_SUBCMD_DATA` (0x01) |
| 2 | 8 | `sender_node_id` |
| 10 | 4 | `src_ip` (клиент→exit: IP отправителя; exit→клиент: dst_ip) |
| 14 | 2 | `src_port` |
| 16 | 4 | `dst_ip` (клиент→exit: IP назначения; exit→клиент: src_ip) |
| 20 | 2 | `dst_port` |
| 22 | N | `payload` (UDP-датаграмма) |
### Функции
| Функция | Назначение |
|---------|-----------|
| `udp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, регистрирует коллбэк в etcp_router. Возвращает 0 при успехе. |
| `udp_proxy_destroy(inst)` | Закрывает все потоки, сокеты, отменяет таймеры, отвязывает коллбэк, освобождает память. |
| `udp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет в exit_handle_data (exit) или client_handle_reply (клиент). |
| `udp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает UDP-датаграмму и отправляет через etcp_route_send на exit. |
| `udp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/UDP-пакет и помещает в TUN input_queue для доставки приложению. |

50
src/route6_lib_doc.md

@ -0,0 +1,50 @@
# route6_lib — библиотека IPv6-маршрутизации на radix tree
## 1. Назначение
Локальная таблица маршрутизации IPv6 для поиска longest-prefix match по заданному адресу.
Используется при пробросе трафика в TUN-интерфейс — по destination IP определяем узел-владелец
подсети и отправляем пакет ему через ETCP.
Индексируется по `node_id` через `ll_queue`-хеш — это позволяет эффективно удалять все маршруты
узла при его withdraw из BGP-топологии.
## 2. Как пользоваться
```c
struct ROUTE6_TABLE *rt = route6_table_create(ua);
// При получении BGP-апдейта с новыми подсетями узла:
route6_insert(rt, nodeq);
// При withdraw узла:
route6_delete(rt, nodeq);
// При пробросе IPv6-пакета:
const struct ROUTE6_DATA *rd = route6_lookup(rt, dst_addr);
if (rd) forward_to_etcp(rd->v_node_info, pkt);
```
Ключевой нюанс: `make_key6` формирует 17-байтовый ключ в формате sockaddr-like
(первый байт = длина = 17, остальные 16 — адрес). Radix tree сравнивает ключи с отступом 1 байт
(RADIX_OFF), пропуская заголовок длины.
## 3. API
### Структуры
| Структура | Назначение |
|-----------|------------|
| `ROUTE6_DATA` | Запись маршрута: ключ+маска (17 байт sockaddr-like), `prefix_length`, `node_id` (для хеша), `v_node_info` (указатель на узел-владелец), `rn_nodes[2]` (scratch для radix) |
| `ROUTE6_TABLE` | Таблица: `rnh` (radix tree head), `queue` (ll_queue с хеш-индексом по `node_id`) |
### Функции
| Функция | Назначение |
|---------|------------|
| `route6_table_create(ua)` | Создать таблицу: `rn_inithead` + `queue_new` с хеш-индексом по `node_id` |
| `route6_table_destroy(table)` | Удалить все маршруты (radix+queue), освободить таблицу |
| `route6_insert(table, node)` | Вставить все `v6_subnets` узла в radix tree и queue (с хеш-индексом) |
| `route6_delete(table, node)` | Удалить из radix tree все маршруты с заданным `node_id` (поиск через хеш) |
| `route6_lookup(table, addr)` | Longest-prefix match по 16-байтовому адресу, возвращает `ROUTE6_DATA*` или NULL |
### Зависимости
- `lib/radix.h` — radix tree (longest-match lookup)
- `lib/ll_queue.h` — lock-free очередь с хеш-индексом для поиска по `node_id`
- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_SUBNET6`

64
src/route_connectivity_doc.md

@ -0,0 +1,64 @@
# route_connectivity — зондирование прямой связности с удалённым узлом
## 1. Назначение
Проверяет прямую IP-достижимость всех адресов удалённого узла через `etcp_send_ping_to_socket()`.
Для каждого адреса перебирает локальные сокеты-кандидаты (до 8), с каждого делает серию из 3 пингов
(CONN_PROBE_COUNT=3, CONN_PROBE_TIMEOUT_MS=1000). Результаты (reachable/unreachable, min_rtt)
сохраняются в `TOPO_CONNECTIVITY` узла и используются `conn_mgr` для выбора оптимального пути.
В отличие от `route_ping` (пинг через BGP-посредника), здесь пинги идут напрямую с локальных сокетов
на адреса удалённого узла — без промежуточных узлов.
## 2. Как пользоваться
```c
// Запустить зондирование узла (вызывается conn_mgr при обнаружении нового узла):
route_connectivity_probe_node(instance, nodeq);
// Отменить зондирование (при удалении узла / withdraw):
route_connectivity_cancel_node(instance, nodeq);
// Отменить все зондирования (при destroy):
route_connectivity_cancel_all(instance);
```
Результаты доступны через `nodeq->connectivity`:
- `probe_status` — `PROBE_STATUS_NONE` / `PROBE_STATUS_IN_PROGRESS` / `PROBE_STATUS_DONE`
- `interface_status/nat_status/real_status` — `PROBE_RESULT_UNKNOWN` / `REACHABLE` / `UNREACHABLE`
- `interface_min_rtt/nat_min_rtt/real_min_rtt` — минимальный RTT по всем сокетам (0.1ms)
- `pending_count` — счётчик незавершённых серий (когда 0 → PROBE_STATUS_DONE)
Ключевой нюанс: зондирование пропускает собственный узел (`nq->node->node_id == instance->node_id`).
Если зондирование уже в процессе, повторный вызов игнорируется.
## 3. API
### Внутренние структуры
| Структура | Назначение |
|-----------|------------|
| `conn_probe_ctx` | Контекст одной серии проб: `addr_type` (INTERFACE/NAT/REAL), `target_addr`, `candidate_sockets[8]`, `best_across_sockets` (min RTT по всем опробованным сокетам), `min_rtt` (в текущей серии), счётчики |
### Подбор сокетов-кандидатов (`conn_match_candidate_sockets`)
Для каждого адреса узла подбирает локальные сокеты в порядке предпочтения:
1. **INTERFACE-адрес**: PRIVATE/LOCAL сокеты в той же /24 подсети
2. **NAT/REAL-адрес**: DIRECT > EIM > PUBLIC/UNKNOWN сокеты
3. Fallback: остальные подходящие сокеты, затем все IPv4
Для IPv6: предпочитает сокеты в той же /64 подсети.
### Функции
| Функция | Назначение |
|---------|------------|
| `route_connectivity_probe_node(instance, nq)` | Перебрать все v4/v6 адреса узла, для каждого подобрать сокеты-кандидаты, запустить серии пингов по 3. Заполняет `nq->connectivity` |
| `route_connectivity_cancel_node(instance, nq)` | Сбросить `probe_status=NONE`, `pending_count=0`, занулить `nq` в ctx (чтобы callback не обновлял статистику) |
| `route_connectivity_cancel_all(instance)` | Сбросить состояние всех узлов в default-группе |
### Логика завершения
- Серия из 3 пингов с одного сокета: если все 3 потеряны → переход к следующему сокету
- Если хоть 1 пинг успешен → серия считается успешной для данного адреса, обновляется `*_status=REACHABLE` и `*_min_rtt`
- Когда `pending_count` доходит до 0 → `probe_status = PROBE_STATUS_DONE`
### Зависимости
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг)
- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6`
- `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all)
- `lib/u_async.h` — get_time_tb() для отметок времени

68
src/route_lib_doc.md

@ -0,0 +1,68 @@
# route_lib – библиотека таблицы IPv4-маршрутизации
## 1. Назначение
Ведёт таблицу IPv4-маршрутов в памяти: динамический массив `ROUTE_ENTRY`, отсортированный по network-адресу с поддержкой вставки/удаления и LPM-поиска (Longest Prefix Match). Используется модулем `routing` для принятия решения, куда слать пакет — какому узлу через ETCP или локально в TUN.
Таблица не содержит логики синхронизации — наполнение маршрутами идёт через BGP (`topo_node`).
## 2. Как пользоваться
```c
struct ROUTE_TABLE* rt = route_table_create();
// Вставка подсетей узла (из BGP):
route_insert(rt, topo_nodeq);
// Поиск маршрута (LPM):
struct ROUTE_ENTRY* e = route_lookup(rt, dest_ip);
if (e && e->v_node_info && e->v_node_info->hop_count > 0) {
// Отправить через ETCP узлу e->v_node_info->node->node_id
} else {
// Локальный маршрут — отдать в TUN
}
// Удаление маршрутов узла:
route_delete(rt, topo_nodeq);
route_table_destroy(rt);
```
**Ключевые нюансы:**
- Адреса в `ROUTE_ENTRY.network` хранятся в **big-endian** (сетевой порядок)
- Записи отсортированы по `network` ASC, при равенстве — по `prefix_length` DESC (более специфичные раньше)
- Алгоритм LPM: бинарный поиск с fallback-проверкой соседней записи
- `v_node_info == NULL` или `hop_count == 0` означает **локальный маршрут**
- Пересечение маршрутов запрещено — `route_insert` проверяет `check_route_overlap_in_table`
- Ёмкость массива динамически расширяется (×2) при переполнении
## 3. API
### Структуры
| Структура | Поля | Описание |
|-----------|------|----------|
| `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_NODEQ*) | Запись маршрута. `v_node_info==NULL` → локальный |
| `ROUTE_TABLE` | `entries`, `count`, `capacity`, `dynamic_subnets`, `local_subnets`, `stats` | Таблица маршрутизации. Динамический массив + статистика |
### Функции
| Функция | Описание |
|---------|----------|
| `route_table_create()` | Выделяет `ROUTE_TABLE`, `entries` на `INITIAL_CAPACITY=100`, счётчики в 0 |
| `route_table_destroy(table)` | Освобождает `entries`, `dynamic_subnets`, `local_subnets`, саму таблицу |
| `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_NODEQ`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости |
| `route_delete(table, node)` | Удаляет все записи, где `v_node_info == node`, со сдвигом массива |
| `route_lookup(table, dest_ip)` | LPM-поиск. Возвращает `ROUTE_ENTRY*` или NULL. Инкрементит `stats` (hits/misses) |
| `route_table_print(table)` | Печатает содержимое таблицы в лог |
| `parse_subnet(str, network, prefix_length)` | Парсит `"a.b.c.d/n"` → network (BE) + prefix_length. Возвращает 0/-1 |
| `is_local_subnet(ip)` | Проверяет, является ли IP локальным/мультикастовым/зарезервированным (0=true). RFC1918 + 0.0.0.0/8 + 127.0.0.0/8 + АPIPA + multicast + broadcast |
| `route_add_local_subnet(table, network, prefix_length)` | **Объявлена в .h, но не реализована** |
### Внутренние функции
| Функция | Описание |
|---------|----------|
| `prefix_to_mask(prefix)` | Преобразует длину префикса в маску: `/24` → `0xFFFFFF00` |
| `routes_overlap(n1, p1, n2, p2)` | Проверяет, совпадают ли две подсети (одинаковые network+prefix) |
| `check_route_overlap_in_table(net, pre, entries, count)` | Проверяет overlap со всеми существующими записями |
| `binary_search_insert_pos(entries, count, net, pre)` | Находит позицию для вставки с сохранением сортировки |
| `binary_search_lpm(table, dest_ip)` | Бинарный LPM-поиск с fallback на соседнюю запись |

62
src/route_ping_doc.md

@ -0,0 +1,62 @@
# route_ping — удалённый пинг через BGP (NAT-детекция, проверка живости)
## 1. Назначение
Механизм запроса удалённого пинга через BGP-канал. Узел A отправляет узлу B TOPO_PING_REQ
с IP:port цели, узел B выполняет серию пингов (`etcp_send_ping_to_socket`) и возвращает
TOPO_PING_RESP с результатами (count_ok, avg_rtt).
Используется:
- `route_connectivity` — для зондирования связности (прямые пинги, без BGP)
- `topo_group` — для NAT-детекции через промежуточный узел (third-party ping)
- Общий механизм проверки живости адресов
## 2. Как пользоваться
```c
// Отправить запрос пинга адреса target_ip:target_port через промежуточный узел to_conn
int ret = route_ping_send_req_addr(group, to_conn,
target_ip, target_port,
count=2, interval_ms=100, timeout_ms=500,
wait_timeout_ms=2000, callback, arg, pubkey);
```
Промежуточный узел (to_conn) получает запрос → выполняет серию `etcp_send_ping_to_socket()`
→ собирает статистику → отправляет ответ.
Ключевой нюанс: `wait_timeout_ms` задаёт таймаут ожидания ответа на локальной стороне.
Если ответ не приходит вовремя, `route_ping_pending_timeout` вызывает callback с `success=0`.
## 3. API
### Протокольные структуры (packed, wire-format)
| Структура | Поля |
|-----------|------|
| `TOPO_PING_REQ` | `cmd=ETCP_ID_TOPO_ENTRY`, `subcmd=0x07`, `request_id`, `count`, `interval_ms`, `timeout_ms`, `target_ipv4`, `target_port`, `pubkey` (опционально) |
| `TOPO_PING_RESP` | `cmd`, `subcmd=0x08`, `request_id`, `count_sent`, `count_ok`, `avg_rtt` (0.1ms) |
### Внутренние структуры
| Структура | Назначение |
|-----------|------------|
| `route_ping_series_ctx` | Контекст серии пингов (на отвечающей стороне): `reply_conn`, `request_id`, `target_addr`, `local_sock`, счётчики, статистика |
| `route_ping_pending` | Ожидающий запрос (на запрашивающей стороне): `request_id`, `callback`, `timeout_timer`, `via_conn` |
### Функции
| Функция | Назначение |
|---------|------------|
| `route_ping_send_req_addr(group, to_conn, target_ip, target_port, count, interval, timeout, wait_timeout, cb, arg, pubkey)` | Отправить TOPO_PING_REQ через BGP, зарегистрировать callback |
| `route_ping_handle_req(group, from_conn, data, len)` | Обработать входящий PING_REQ: найти локальный IPv4-сокет, запустить серию `etcp_send_ping_to_socket()`, по завершении отправить PING_RESP |
| `route_ping_handle_resp(group, from_conn, data, len)` | Обработать PING_RESP: найти pending по `request_id`, отменить таймер, вызвать callback |
| `route_ping_destroy_pending(group)` | Очистить весь список pending (при уничтожении BGP) |
| `route_ping_cancel_for_conn(group, conn)` | Отменить pending запросы, связанные с конкретным `ETCP_CONN` (при разрыве) |
### Callback
```c
typedef void (*route_ping_callback_t)(int success, uint16_t avg_rtt,
uint8_t count_sent, uint8_t count_ok, void* arg);
```
Вызывается при получении ответа или по таймауту (`success=0`).
### Зависимости
- `src/topo_group.h` — BGP-канал (список `ping_pending`, `next_ping_req_id`)
- `src/etcp_api.h` — `etcp_send()` для отправки через BGP
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` для собственно пинга
- `src/topo_node.h` — `topo_node_find_by_id()`

44
src/routing_doc.md

@ -0,0 +1,44 @@
# routing – централизованный модуль маршрутизации
## 1. Назначение
Связывает TUN-интерфейс и ETCP-транспорт в единый маршрутизатор. Принимает IP-пакеты из TUN, по IP-адресу назначения ищет маршрут в таблице (`route_lib`) и отправляет пакет через ETCP нужному узлу. В обратную сторону принимает пакеты из ETCP (callback `routing_pkt_from_etcp_cb`) и отдаёт в TUN локально.
IPv6-пакеты на данный момент дропаются на этапе `extract_dst_ip` (возвращает 0).
Модуль НЕ реализует таблицу маршрутизации сам — он пользуется `route_lib` (`instance->rt`) и BGP-обменом (`topo_node` / `topo_group`) для наполнения таблицы.
## 2. Как пользоваться
**Порядок инициализации в `utun_instance.c`:**
1. `routing_create()` — создаёт `ROUTE_TABLE` в `instance->rt`
2. `routing_bind()` — биндит коллбек `routing_pkt_from_etcp_cb` через `etcp_router_bind(ETCP_RT_ID_DATA)` — принимать пакеты из ETCP
3. `routing_set_tun()` — вешает коллбек `routing_pkt_from_tun_cb` на `tun->output_queue` — забирать пакеты из TUN
**Основной поток данных:**
- **TUN → сеть:** пакет попадает в `tun->output_queue` → `routing_pkt_from_tun_cb` → `route_pkt` → `etcp_route_send`
- **сеть → TUN:** пакет приходит через ETCP → `etcp_router` вызывает `routing_pkt_from_etcp_cb` → `route_pkt` → `queue_data_put(tun->input_queue)`
**Ключевые нюансы:**
- `routing_add_conn` / `routing_del_conn` — **deprecated** (no-op), маршруты теперь управляются через BGP и таблицу `route_lib`
- Все не-IPv4 пакеты (IPv6, unknown) дропаются в `extract_dst_ip`
- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) дропаются без логирования
- Пакеты без маршрута дропаются с инкрементом `instance->dropped_packets`
## 3. API
| Функция | Описание |
|---------|----------|
| `routing_create(instance)` | Создаёт `ROUTE_TABLE` в `instance->rt`. -1 при ошибке |
| `routing_bind(instance)` | Биндит обработчик входящих ETCP-пакетов через `etcp_router_bind` |
| `routing_destroy(instance)` | Отвязывает ETCP-обработчик, уничтожает `ROUTE_TABLE` |
| `routing_set_tun(instance)` | Вешает callback на `tun->output_queue` для перехвата пакетов из TUN |
| `routing_add_conn(etcp)` | **Deprecated**, ничего не делает |
| `routing_del_conn(etcp)` | **Deprecated**, ничего не делает |
| `route_pkt(instance, entry, src_node_id)` | Ядро маршрутизации: извлекает dst IP → `route_lookup` → `etcp_route_send` или `queue_data_put(tun->input_queue)` для локального |
### Внутренние функции
| Функция | Описание |
|---------|----------|
| `extract_dst_ip(data, len)` | Извлекает dst IPv4 из заголовка пакета (offset 16, 4 байта). Возвращает 0 для не-IPv4 |
| `routing_pkt_from_etcp_cb(conn, pkt)` | Callback: приём пакета из ETCP-роутера, вызывает `route_pkt` |
| `routing_pkt_from_tun_cb(q, arg)` | Callback: приём пакета из TUN output_queue, вызывает `route_pkt` |

352
src/secure_channel_doc.md

@ -0,0 +1,352 @@
# secure_channel
## 1. Назначение
Модуль реализует криптографический слой uTun: защищённый канал между двумя узлами на основе **X25519** (Elliptic Curve Diffie-Hellman) для обмена сессионным ключом и **AES-128-CCM** (Counter with CBC-MAC) для аутентифицированного шифрования трафика. Дополнительно предоставляет Ed25519 для цифровых подписей (аутентификация данных, происходящих от конкретного узла) и стриминговое шифрование (AES-128-CTR) для больших потоков данных, где нужна только конфиденциальность без аутентификации.
Это **один из важнейших модулей** проекта — каждый ETCP-пакет, проходящий между узлами, шифруется и аутентифицируется через этот канал.
**Ключевые криптографические операции:**
1. **X25519 ECDH** — получение общего секрета из своего приватного и чужого публичного ключа
2. **AES-128-CCM** — аутентифицированное шифрование (confidentiality + integrity): nonce(13) + ciphertext + tag(16)
3. **SHA256-based обфускация pubkey** — скрытие публичного ключа при передаче в INIT-пакетах (XOR с SHA256(salt || peer_pubkey))
4. **Ed25519** — цифровые подписи данных (one-shot и стриминговая через SHA-512 аккумулятор)
5. **AES-128-CTR** — стриминговое шифрование без аутентификации (bulk data)
6. **CRC32** — дополнительный контроль целостности расшифрованных данных
---
## 2. Как пользоваться
### 2.1. Типовой сценарий: генерация → обмен → шифрование
```c
// ── Шаг 1: Загрузка или генерация ключей ──
struct SC_MYKEYS my_keys;
// Вариант A: сгенерировать новую пару
sc_generate_keypair(&my_keys);
// Вариант B: загрузить из конфига (hex-строки)
sc_init_local_keys(&my_keys, pubkey_hex, privkey_hex);
// ── Шаг 2: Инициализация контекста ──
sc_context_t ctx;
sc_init_ctx(&ctx, &my_keys);
// Теперь ctx.session_ready == 0 — ждём установки ключа пира
// ── Шаг 3: Установка публичного ключа пира (после получения из сети) ──
uint8_t peer_pubkey[SC_PUBKEY_SIZE];
// ... получить peer_pubkey из INIT-пакета / конфига ...
sc_set_peer_public_key(&ctx, peer_pubkey, SC_PEER_PUBKEY_BIN); // или SC_PEER_PUBKEY_HEX
// Внутри: X25519 ECDH → shared_secret → SHA256(shared_secret || "uTun-v3-session") → session_key
// Теперь ctx.session_ready == 1
// ── Шаг 4: Шифрование / дешифрование ──
uint8_t ciphertext[1500];
size_t ct_len = sizeof(ciphertext);
// Шифрование: nonce(13) || AES-CCM(plaintext+crc32) || tag(16)
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ct_len);
// Дешифрование: проверка тега (аутентификация) → проверка CRC32 → возврат plaintext
uint8_t decrypted[1500];
size_t pt_len = sizeof(decrypted);
sc_decrypt(&ctx, ciphertext, ct_len, decrypted, &pt_len);
```
### 2.2. Обфускация pubkey (INIT-пакеты)
При отправке INIT-пакета публичный ключ не передаётся в открытом виде. Вместо этого он обфусцируется через XOR с SHA256-хэшем от соли и публичного ключа пира:
```c
uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]; // 8 случайных байт
random_bytes(salt, sizeof(salt));
uint8_t obfuscated[SC_PUBKEY_SIZE]; // 32 байта
sc_obfuscate_pubkey(salt, peer_pubkey, my_pubkey, obfuscated);
// Передаётся в пакете: salt(8) || obfuscated_pubkey(32) = 40 байт (SC_PUBKEY_ENC_SIZE)
```
**Формат пакета с обфусцированным pubkey:**
- Заголовок и данные шифруются AES-CCM
- Блок `salt(8) + obfuscated_pubkey(32)` добавляется **после зашифрованных данных, без шифрования**
**Деобфускация на приёмной стороне:**
```c
// Получатель знает salt из пакета и свой публичный ключ
sc_obfuscate_pubkey(salt, my_pubkey, received_obfuscated, decrypted_pubkey);
// XOR обратим: obfuscated XOR SHA256(salt||my_pubkey) = исходный pubkey отправителя
```
**Важный нюанс реализации:** в текущей версии `sc_obfuscate_pubkey()` вычисляет два SHA256-хэша (`SHA256(salt||peer_pubkey)` и `SHA256(peer_pubkey||salt)`), но XOR'ит публичный ключ только с первым. Второй хэш вычисляется, но не используется (возможно, задел на будущее).
### 2.3. Стриминговое шифрование (AES-128-CTR)
Для больших объёмов данных, где не нужна посылочная аутентификация:
```c
struct sc_stream_state stream;
sc_stream_init(&ctx, &stream, stream_id); // stream_id — произвольный uint32_t
sc_stream_xor(&stream, data_chunk1, len1); // XOR in-place (шифрует/дешифрует)
sc_stream_xor(&stream, data_chunk2, len2);
sc_stream_cleanup(&stream);
```
Nonce (12 байт) для CTR выводится из `SHA256(session_key || stream_id_LE || "uTun3-stream")`.
### 2.4. Ed25519 подписи
**One-shot:**
```c
uint8_t sig[SC_SIGN_SIZE]; // 64 байта
sc_ed25519_sign(privkey, msg, msg_len, sig);
int ok = sc_ed25519_verify(pubkey, msg, msg_len, sig); // SC_OK при успехе
```
**Стриминговая (потоковая) подпись:** SHA-512 аккумулятор + one-shot Ed25519 в финале. Подходит для данных любого размера (память O(1)):
```c
struct sc_stream_sign_state state;
// Подпись
sc_stream_sign_init(&ctx, &state);
sc_stream_sign_update(&state, chunk1, len1);
sc_stream_sign_update(&state, chunk2, len2);
sc_stream_sign_final(&state, sig, &sig_len); // автоматический cleanup
// Верификация
sc_stream_sign_verify_init(&state, ed25519_pubkey);
sc_stream_sign_update(&state, chunk1, len1);
sc_stream_sign_update(&state, chunk2, len2);
int result = sc_stream_sign_verify(&state, sig, sig_len); // SC_OK или SC_ERR_AUTH_FAILED
```
**Деривация Ed25519-ключа из X25519:** `sc_derive_ed25519_pubkey(x25519_privkey, ed25519_pubkey)` — SHA-512 от X25519-приватного ключа даёт Ed25519-приватный ключ (32 байта), из которого извлекается Ed25519-публичный ключ. Это позволяет одному ключу обслуживать и ECDH, и подписи.
### 2.5. Деривация node_id
```c
uint64_t node_id = sc_derive_node_id(private_key);
// SHA256(private_key) → первые 8 байт, старший бит обнулён (0x7FFFFFFFFFFFFFFF)
```
### 2.6. SHA256-based транскодирование
```c
sc_sha_transcode(key, key_len, data, data_len);
// data[i] ^= SHA256(key)[i % 32] — простой XOR с SHA256-хэшем ключа
```
---
## 3. API
### 3.1. Константы
| Константа | Значение | Описание |
|---|---|---|
| `SC_PRIVKEY_SIZE` | 32 | Размер приватного ключа X25519 (байт) |
| `SC_PUBKEY_SIZE` | 32 | Размер публичного ключа X25519 (байт) |
| `SC_HASH_SIZE` | 32 | Размер SHA256-хэша (байт) |
| `SC_NONCE_SIZE` | **13** | Размер nonce для AES-128-CCM. **Ровно 13 байт — требование CCM!** |
| `SC_SHARED_SECRET_SIZE` | 32 | Размер общего секрета X25519 (= SC_HASH_SIZE) |
| `SC_SESSION_KEY_SIZE` | 16 | Размер сессионного ключа AES-128 (байт) |
| `SC_TAG_SIZE` | 16 | Размер аутентификационного тега CCM (байт) |
| `SC_CRC32_SIZE` | 4 | Размер CRC32 в зашифрованном пакете (байт) |
| `SC_PUBKEY_ENC_SALT_SIZE` | 8 | Размер соли для обфускации pubkey (байт) |
| `SC_PUBKEY_ENC_SIZE` | 40 | Полный размер обфусцированного блока: salt(8) + obfuscated(32) |
| `SC_STREAM_NONCE_SIZE` | 12 | Размер nonce для AES-128-CTR (байт) |
| `SC_SIGN_SIZE` | 64 | Размер Ed25519 подписи (байт) |
| `SC_OK` | 0 | Успех |
| `SC_ERR_INVALID_ARG` | -1 | Неверные аргументы |
| `SC_ERR_CRYPTO` | -2 | Криптографическая ошибка (OpenSSL) |
| `SC_ERR_NOT_INITIALIZED` | -3 | Контекст/сессия не инициализирована |
| `SC_ERR_AUTH_FAILED` | -4 | Ошибка аутентификации (неверный тег/подпись) |
| `SC_ERR_CRC_FAILED` | -5 | Ошибка CRC32 (повреждение данных) |
| `SC_PEER_PUBKEY_BIN` | 0 | Флаг: ключ в бинарном формате |
| `SC_PEER_PUBKEY_HEX` | 1 | Флаг: ключ в hex-строке |
### 3.2. Структуры
**`struct SC_MYKEYS`** — локальная пара ключей узла:
- `uint8_t private_key[32]` — приватный ключ X25519
- `uint8_t public_key[32]` — публичный ключ X25519
**`struct secure_channel` (sc_context_t)** — контекст защищённого канала между двумя конкретными узлами:
- `pk` — указатель на `SC_MYKEYS` (локальные ключи)
- `peer_public_key[32]` — публичный ключ пира (заполняется после ECDH)
- `session_key[16]` — производный сессионный ключ AES-128
- `initialized` — флаг: `sc_init_ctx()` вызван
- `peer_key_set` — флаг: `sc_set_peer_public_key()` вызван успешно
- `session_ready` — флаг: можно шифровать/дешифровать
- `tx_counter` — счётчик отправленных пакетов (используется в nonce)
- `rx_counter` — счётчик принятых пакетов
**`struct sc_stream_state`** — состояние стримингового шифра (AES-128-CTR):
- `ectx` — `EVP_CIPHER_CTX*` (opaque)
- `initialized` — флаг инициализации
**`struct sc_stream_sign_state`** — состояние стриминговой подписи (Ed25519 + SHA-512):
- `md_ctx` — `EVP_MD_CTX*` (SHA-512 аккумулятор, opaque)
- `pkey` — `EVP_PKEY*` (Ed25519 ключ, opaque)
- `initialized` — флаг инициализации
- `is_sign` — 1 = режим подписи, 0 = режим верификации
### 3.3. Функции
#### Инициализация ключей и контекста
| Функция | Описание |
|---|---|
| `sc_init_ctx(ctx, mykeys)` | Инициализирует контекст канала, привязывает локальные ключи. **Не делает ECDH** — только обнуляет состояние. |
| `sc_generate_keypair(keys)` | Генерирует новую пару X25519 через `EVP_PKEY_keygen`. Случайный приватный ключ. |
| `sc_init_local_keys(mykeys, pub_hex, priv_hex)` | Загружает ключи из hex-строк (обычно из конфига). Валидирует публичный ключ (не all-zero). |
| `sc_set_peer_public_key(ctx, peer_pubkey, mode)` | **Ключевая операция:** выполняет X25519 ECDH, выводит session_key, устанавливает `session_ready=1`. mode: `SC_PEER_PUBKEY_BIN` или `SC_PEER_PUBKEY_HEX`. |
| `sc_compute_public_key_from_private(privkey, pubkey_out)` | Вычисляет публичный ключ из приватного (X25519). |
**Вывод session_key:** `SHA256(X25519_shared_secret || "uTun-v3-session") → truncate to 16 bytes`
#### Шифрование / дешифрование
| Функция | Описание |
|---|---|
| `sc_encrypt(ctx, plaintext, pt_len, ciphertext, &ct_len)` | AES-128-CCM шифрование. Формат вывода: `nonce(13) \|\| AES-CCM(plaintext + CRC32(plaintext)) \|\| tag(16)`. Инкрементирует `tx_counter`. |
| `sc_decrypt(ctx, ciphertext, ct_len, plaintext, &pt_len)` | AES-128-CCM дешифрование. Проверяет CCM-тег → проверяет CRC32. Инкрементирует `rx_counter` при успехе. |
**Формат nonce:** `SHA256(random_seed(8) || tx_counter(8) || tv_sec(4) || tv_usec(4))` — truncated to 13 bytes. Использует OpenSSL SHA256 (не sha256.h).
**Проверки при дешифровании:**
1. `ciphertext_len >= SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE` (минимум 13+16+4 = 33 байта)
2. CCM-тег: несовпадение → `SC_ERR_AUTH_FAILED` (пакет подделан или повреждён)
3. CRC32: несовпадение → `SC_ERR_CRC_FAILED` (повреждение данных после расшифровки)
#### Обфускация pubkey
| Функция | Описание |
|---|---|
| `sc_obfuscate_pubkey(salt, peer_pubkey, pubkey, output)` | `output[i] = pubkey[i] ^ SHA256(salt \|\| peer_pubkey)[i]` для i < 32. XOR обратим — повторный вызов с теми же salt/peer_pubkey восстанавливает исходный pubkey. |
#### Стриминговое шифрование (AES-128-CTR)
| Функция | Описание |
|---|---|
| `sc_stream_init(ctx, state, stream_id)` | Инициализирует CTR-контекст. Nonce из `SHA256(session_key \|\| stream_id_LE \|\| "uTun3-stream")`. |
| `sc_stream_xor(state, data, len)` | XOR данных с keystream in-place. Нулевая длина — no-op. |
| `sc_stream_cleanup(state)` | Освобождает `EVP_CIPHER_CTX`. |
#### Ed25519 подписи (one-shot)
| Функция | Описание |
|---|---|
| `sc_ed25519_sign(privkey[32], msg, msg_len, sig_out[64])` | Подпись сообщения. `privkey` — 32-байтный Ed25519-приватный ключ (НЕ X25519). |
| `sc_ed25519_verify(pubkey[32], msg, msg_len, sig[64])` | Проверка подписи. Возвращает `SC_OK` или `SC_ERR_AUTH_FAILED`. |
#### Ed25519 подписи (стриминг)
| Функция | Описание |
|---|---|
| `sc_stream_sign_init(ctx, state)` | Инициализирует подпись. Извлекает Ed25519-ключ из X25519 через `SHA512(x25519_privkey)`. |
| `sc_stream_sign_verify_init(state, ed25519_pubkey)` | Инициализирует верификацию с переданным Ed25519 pubkey. |
| `sc_stream_sign_update(state, data, len)` | SHA-512 update (инкрементальный). Нулевая длина — no-op. |
| `sc_stream_sign_final(state, sig_out, &sig_len)` | Завершает SHA-512 → one-shot Ed25519 подпись хэша → **автоматический cleanup**. `sig_len` всегда 64. |
| `sc_stream_sign_verify(state, sig, sig_len)` | Завершает SHA-512 → one-shot Ed25519 проверка → **автоматический cleanup**. Возвращает `SC_OK` или `SC_ERR_AUTH_FAILED`. |
| `sc_stream_sign_cleanup(state)` | Ручная очистка (если не вызван final/verify). |
**Потоковая схема:** Ed25519 внутри делает SHA-512 от данных, поэтому для стриминга нужно было бы грузить все данные в память. Вместо этого: `SHA-512(данные)` делается инкрементально через `EVP_DigestUpdate`, затем 64-байтный хэш подписывается one-shot `EVP_DigestSign`. Результат: `Ed25519(SHA-512(данные))` — консистентно с обеих сторон. Память O(1).
#### Деривация
| Функция | Описание |
|---|---|
| `sc_derive_ed25519_pubkey(x25519_privkey, ed25519_pubkey_out)` | Из X25519-приватного ключа (32 байта) выводит Ed25519-публичный ключ (32 байта) через `SHA512(x25519_privkey)`. |
| `sc_derive_node_id(private_key)` | `SHA256(private_key)` → первые 8 байт как uint64_t, старший бит обнулён. Уникальный идентификатор узла. |
#### Утилиты
| Функция | Описание |
|---|---|
| `sc_sha_transcode(key, key_len, data, data_len)` | `data[i] ^= SHA256(key)[i % 32]`. Простой XOR с хэшем ключа. |
### 3.4. Зависимости
| Зависимость | Использование |
|---|---|
| **OpenSSL EVP** (`openssl/evp.h`) | X25519 keygen/derive (`EVP_PKEY`), AES-128-CCM, AES-128-CTR, Ed25519 sign/verify, SHA-512 |
| **OpenSSL SHA** (`openssl/sha.h`) | `SHA256_Init/Update/Final` для построения nonce в `sc_build_nonce()` |
| **`../lib/sha256.h`** (`sc_sha256_*`) | SHA256 для вывода session_key, stream nonce, obfuscate pubkey, sha_transcode, node_id |
| **`../lib/debug_config.h`** | `DEBUG_ERROR`, `DEBUG_WARN`, `DEBUG_INFO`, `DEBUG_DEBUG` (категория `DEBUG_CATEGORY_CRYPTO`) |
| **`../lib/platform_compat.h`** | `random_bytes()` (для соли и random seed), `utun_gettimeofday()` (для nonce) |
| **`crc32.h`** (`crc32_calc`) | Контроль целостности в шифрованных пакетах |
### 3.5. Отсутствующие функции
- **`sc_rekey()`** упоминается в AGENTS.md, но **не реализована** в текущей версии. Ротация сессионного ключа не поддерживается — после ECDH ключ фиксирован на всё время сессии.
---
## 4. Формат зашифрованного пакета
```
┌──────────────┬─────────────────────────────────────┬──────────────┐
│ nonce (13) │ AES-CCM(plaintext || CRC32(pt)) │ tag (16) │
│ 13 bytes │ pt_len + 4 bytes │ 16 bytes │
└──────────────┴─────────────────────────────────────┴──────────────┘
```
- **nonce:** 13 байт, уникальный для каждого пакета (SHA256 от seed + counter + времени)
- **ciphertext:** AES-128-CCM in-place шифрование `plaintext || CRC32(plaintext)`
- **tag:** 16 байт, аутентификационный тег CCM (обеспечивает integrity + authenticity)
**Порядок проверки при дешифровании:** CCM-тег → если ОК, извлечение plaintext → CRC32 проверка.
### Формат INIT-пакета
```
┌───────────────────────────────────────────────────────┬──────────────────────────────────────┐
│ AES-CCM(header(3) || data) + auth tag │ salt(8) || obfuscated_pubkey(32) │
│ Зашифрованная часть (включает auth tag) │ Незашифрованная часть (SC_PUBKEY_ENC_SIZE=40) │
└───────────────────────────────────────────────────────┴──────────────────────────────────────┘
```
- Зашифрованная часть: ETCP-заголовок (timestamp uint16 + flag_up uint8) + данные, всё зашифровано AES-128-CCM с auth tag
- Незашифрованная часть: `salt(8) + pubkey XOR SHA256(salt || expected_peer_pubkey)`
- Получатель восстанавливает pubkey: `obfuscated XOR SHA256(salt || my_pubkey) = sender_pubkey` (XOR обратим)
---
## 5. Состояния и flow
```
┌─────────────┐ sc_init_ctx() ┌──────────────┐
│ Не создан │ ──────────────────────→│ initialized │
└─────────────┘ │ session_ready=0│
└──────┬───────┘
│ sc_set_peer_public_key()
│ (X25519 ECDH → session_key)
↓
┌──────────────┐
│ session_ready=1│
│ tx_counter=0 │
│ rx_counter=0 │
└──────┬───────┘
│ sc_encrypt() / sc_decrypt()
↓
tx_counter++, rx_counter++
```
- `session_ready` становится 1 только после успешного ECDH через `sc_set_peer_public_key()`
- Контекст привязан к конкретному пиру (peer_public_key фиксирован)
- Для смены пира нужен новый контекст
- Счётчики не синхронизируются между сторонами — каждая сторона считает свои tx/rx независимо
---
## 6. Примечания по безопасности
- **Nonce должен быть уникальным** для каждого шифрования с одним ключом. Здесь nonce строится из `SHA256(random_seed || counter || время)`. При случайном seed и монотонном counter'е коллизии крайне маловероятны.
- **CRC32 — не криптографическая защита.** Он дополнительно ловит битовые ошибки после расшифровки (belt-and-suspenders), но основная защита целостности — CCM-тег.
- **Обфускация pubkey — не шифрование.** Это XOR с детерминированным хэшем, обратима для любого, кто знает salt и второй pubkey. Защищает от пассивного наблюдения (не видно публичный ключ в открытом виде), но не от активной подмены.
- **OpenSSL EVP API** используется для всех криптоопераций (реализации FIPS-сертифицированы при использовании FIPS-провайдера).

55
src/stcp_client_doc.md

@ -0,0 +1,55 @@
# STCP Client
## 1. Назначение
TCP-клиент протокола STCP (Streaming TCP). Устанавливает TCP-соединение с STCP-сервером, выполняет X25519-handshake с передачей обфусцированных публичных ключей, согласует сессионные ключи через ECDH, после чего переходит в режим обмена данными (DATA). Каждое сообщение шифруется потоковым XOR-шифром и проверяется CRC32. Является нижним уровнем STCP-стека — поверх него работает `stcp_link`.
## 2. Как пользоваться
### Типовой сценарий (клиент):
```c
stcp_ready_cb on_ready = (stcp_ready_cb)my_ready_handler;
stcp_close_cb on_close = (stcp_close_cb)my_close_handler;
struct stcp_client *cli = stcp_client_connect(
ua, addr, port, &my_keys, peer_pubkey_bin,
on_ready, ready_ctx, on_close, close_ctx
);
// После handshake — on_ready.
// Соединение доступно через stcp_client_get_conn(cli).
// Tx: пакеты в conn->tx_queue → tx_cb шифрует и отправляет.
// Rx: пакеты попадают в conn->rx_queue.
// Завершение: stcp_client_destroy(cli).
```
### Ключевые нюансы
- **Handshake**: клиент посылает salt(8) + обфусцированный pubkey(XOR с SHA256(salt||server_pubkey), 32) + padding_size(2) + CRC32(4), зашифрованные stream_xor, плюс padding (8+ байт). Сервер присылает аналогичный ответ со статусом. После проверки CRC — переход в `STCP_STATE_DATA`.
- **Стрим-шифр**: используется `sc_stream_xor` с начальным nonce = `SHA256(session_key)`. Два стрима: `STCP_STREAM_CLIENT_SEND` (клиент→сервер) и `STCP_STREAM_SERVER_SEND` (сервер→клиент) — инициализируются в `client_derive_session`.
- **Фрейминг в DATA**: каждое сообщение — `len(2 LE)` + данные + `CRC32(4)`. CRC считается от данных, затем данные+CRC шифруются stream_xor.
- **Очереди**: `tx_queue` через callback `client_tx_queue_cb` шифрует каждое сообщение и отправляет. `rx_queue` получает расшифрованные сообщения для вышележащего слоя.
- **Асинхронность**: все операции через `UASYNC` — сокет в неблокирующем режиме. Поддержка частичной отправки (send_offset) и EAGAIN/WOULDBLOCK.
- **Обработка ошибок**: CRC mismatch, ошибки шифрования, сетевые ошибки — все ведут к `client_do_close` с кодом ошибки.
## 3. API
### Структура `struct stcp_client`
Обёртка над `struct stcp_conn`. Хранит `peer_pubkey`, callback `ready_cb`, ссылку на `UASYNC`.
### `stcp_client_connect()`
Создаёт TCP-сокет, подключается (асинхронно через `EINPROGRESS`), регистрирует write-коллбэк `client_connect_write_cb`. При успешном соединении: проверяет `SO_ERROR`, выводит сессионные ключи через ECDH (`client_derive_session`), отправляет handshake (`client_send_handshake`).
### `stcp_client_destroy()`
Закрывает соединение через `client_do_close`, освобождает `stcp_conn`, освобождает память.
### `stcp_client_get_conn()`
Возвращает внутренний `struct stcp_conn *` для доступа к сокету и очередям.
### Коллбэки
- **`stcp_ready_cb`**: вызывается при успешном handshake (переход в `STCP_STATE_DATA`).
- **`stcp_close_cb`**: вызывается при закрытии или ошибке соединения, получает код ошибки.
### Внутренние функции
- **`client_derive_session()`** — вычисляет session_key через ECDH, инициализирует stream_send/stream_recv.
- **`client_send_handshake()`** — формирует и отправляет handshake-пакет (salt + обфусцированный pubkey + зашифрованный padding+CRC + padding).
- **`process_server_response()`** — деобфусцирует pubkey сервера, расшифровывает и проверяет CRC ответа сервера.
- **`stcp_conn_process_recv()`** — конечный автомат разбора входящих данных: в HS-состоянии ждёт полный ответ, в DATA — разбирает фреймы (len + data + CRC).
- **`encrypt_and_crc()` / `decrypt_and_check()`** — шифрование/расшифровка фрейма с CRC32 (stream_xor + проверка целостности).

52
src/stcp_doc.md

@ -0,0 +1,52 @@
# STCP (Secure TCP) — общая часть соединения
## 1. Назначение
STCP — потоковый TCP-протокол с X25519-ключеобменом и потоковым AES-CTR шифрованием (stream cipher). Обеспечивает безопасное TCP-соединение: клиент подключается, сервер принимает, выполняется ECDH-рукопожатие с обфускацией публичных ключей, после чего трафик шифруется AES-CTR с контрольными суммами CRC32.
Модуль `stcp.c/stcp.h` содержит общие структуры, константы и базовый жизненный цикл соединения `struct stcp_conn`, используемый как клиентской (`stcp_client`), так и серверной (`stcp_server`) сторонами.
## 2. Как пользоваться
Типовая схема: вышестоящий код создаёт `struct stcp_conn` (сервер через `stcp_server_create`, клиент — через `stcp_client_connect`), устанавливает очереди приёма/передачи (`stcp_conn_set_tx_queue`, `stcp_conn_set_rx_queue`), коллбэк закрытия (`stcp_conn_set_on_close`). После рукопожатия (`STCP_STATE_DATA`) сообщения передаются через `rx_queue`/`tx_queue`: отправка — через ll_queue с коллбэком `tx_cb`, приём — данные раскладываются в `rx_queue`.
Потоковое шифрование (`sc_stream_state`) не требует буферизации целых сообщений — XOR применяется побайтово к потоку, поэтому порядок отправки/приёма критичен. Для каждого направления создаётся отдельный stream (`stream_send`, `stream_recv`).
Ключевые нюансы:
- Размер сообщения — `uint16_t` в префиксе, макс. 65535 байт.
- Каждое сообщение шифруется: 2 байта длины + данные + 4 байта CRC32.
- CRC32 проверяется после расшифровки для детектирования повреждений.
- При разрыве соединения или ошибке вызывается `on_close`.
## 3. API
### Состояния
- `STCP_STATE_INIT` — начальное (на клиенте)
- `STCP_STATE_HS_SERVER_WAIT` — сервер ждёт рукопожатия от клиента
- `STCP_STATE_DATA` — активный обмен данными (шифрованный)
- `STCP_STATE_CLOSED` / `STCP_STATE_ERROR` — завершение
### stcp_conn — структура соединения
| Поле | Описание |
|------|----------|
| `sock` | TCP-сокет |
| `ua` | UASYNC event loop |
| `state` | Текущее состояние (enum stcp_state) |
| `is_server` | 1 = серверная сторона |
| `session_key[SC_SESSION_KEY_SIZE]` | Ключ X25519 ECDH (AES-128) |
| `stream_send` / `stream_recv` | Состояния потокового AES-CTR для отправки и приёма |
| `my_keys` | Локальные ключи X25519 |
| `peer_pubkey` | Публичный ключ пира (после рукопожатия) |
| `rx_queue` / `tx_queue` | ll_queue для приёма/отправки сообщений |
| `tx_cb` | Коллбэк очереди tx_queue |
| `recv_buf` | Буфер приёма TCP (динамический, до 128KB) |
| `send_buf` | Буфер отправки (для досыла при EAGAIN) |
| `hs_expected_len` / `hs_key_processed` | Состояние рукопожатия |
| `on_ready` | Вызывается после завершения рукопожатия |
| `on_close` | Вызывается при закрытии соединения |
### Функции
- `stcp_conn_set_tx_queue(c, q)` — установить очередь отправки, привязывает `tx_cb` как коллбэк
- `stcp_conn_set_rx_queue(c, q)` — установить очередь приёма (в неё кладутся расшифрованные сообщения)
- `stcp_conn_set_on_close(c, cb, arg)` — установить коллбэк закрытия (err=0 — норма, иначе код ошибки)
- `stcp_conn_free(c)` — освободить сокет, буферы, очистить stream-ы. Если `allocated=1` — освободить и саму структуру

73
src/stcp_link_doc.md

@ -0,0 +1,73 @@
# STCP Link
## 1. Назначение
Прослойка-мост между STCP-транспортом (TCP с потоковым шифрованием) и ETCP-уровнем (маршрутизация, балансировка, управление соединениями). Инкапсулирует создание STCP-сервера или клиента, настройку очередей приёма/передачи и диспетчеризацию входящих пакетов в `api_bindings` экземпляра `UTUN_INSTANCE`. По сути — STCP как транспортный линк для ETCP.
## 2. Как пользоваться
### Сервер (приём входящих соединений):
```c
struct stcp_link_config cfg = {
.ua = ua, .my_keys = &my_keys, .inst = utun_instance,
.peer_pubkey = NULL, .remote_addr = NULL,
};
struct stcp_server *srv = stcp_server_listen(&cfg, port, on_link_cb, NULL);
// При входящем соединении вызывается on_link_cb со stcp_link.
// Остановка: stcp_link_server_destroy(srv).
```
### Клиент (исходящее соединение):
```c
struct stcp_link_config cfg = {
.ua = ua, .my_keys = &my_keys, .inst = utun_instance,
.peer_pubkey = server_pubkey, .peer_pubkey_mode = 0,
.remote_addr = (struct sockaddr_storage *)&addr, .remote_port = port,
};
struct stcp_link *link = stcp_link_connect(&cfg);
// По готовности: stcp_link_is_ready(link) == 1, или через stcp_link_set_on_ready.
// Отправка: stcp_link_send(link, data, len).
// Закрытие: stcp_link_close(link).
```
### Ключевые нюансы
- **ETCP_CONN**: каждый `stcp_link` содержит встроенный `struct ETCP_CONN` (`etcp_conn`), который используется для диспетчеризации через `inst->api_bindings`. Поле `transport_link` служит обратным указателем на `stcp_link`.
- **Диспетчеризация RX**: входящие пакеты из `rx_queue` обрабатываются коллбэком `link_rx_cb`, который по первому байту (`id`) демультиплексирует пакет в соответствующий обработчик `api_bindings.callbacks[id]`. Если конкретный обработчик не найден — пакет уходит в `callbacks[0]`.
- **Очередь TX**: `tx_queue` настраивается с `queue_set_waiter_defer(1)` — отправка через пороговое ожидание (backpressure). Очередь связывается с `stcp_conn` через `stcp_conn_set_tx_queue`.
- **Разница сервер/клиент**: сервер при accept создаёт `stcp_link` в коллбэке `server_accept_cb`, клиент — при ready в `client_ready_cb`. Настройка очередей идентична.
- **Закрытие**: `stcp_link_close` корректно очищает `transit_queues` (очереди ETCP-трафика в пути), `rx_queue`, `tx_queue`, уничтожает `stcp_conn` (сервер) или `stcp_client` (клиент).
## 3. API
### Структура `struct stcp_link_config`
Конфигурация для создания линка. Содержит:
- `ua` — экземпляр UASYNC (один на поток)
- `my_keys` — ключи этой стороны для ECDH
- `inst` — UTUN_INSTANCE для диспетчеризации через api_bindings
- `peer_pubkey` / `peer_pubkey_mode` — публичный ключ пира (binary или hex)
- `remote_addr` / `remote_port` — адрес пира (только для клиента)
### Структура `struct stcp_link` (opaque)
Содержит ссылки на `stcp_client`/`stcp_conn`, очереди RX/TX, встроенный `ETCP_CONN`, флаг `ready`, коллбэки on_ready/on_close.
### `stcp_server_listen()`
Оборачивает `stcp_server_create()`, передавая `server_accept_cb` в качестве коллбэка при входящем соединении. При accept создаёт `stcp_link`, настраивает очереди и вызывает пользовательский `on_link`.
### `stcp_link_connect()`
Создаёт `stcp_link`, подготавливает pubkey пира (конвертация hex→bin при необходимости), вызывает `stcp_client_connect()`. При успешном handshake (коллбэк `client_ready_cb`) настраивает очереди RX/TX и вызывает `on_ready_cb`.
### `stcp_link_close()`
Полная очистка: освобождает `transit_queues` (с отменой waiter'ов), `rx_queue`, `tx_queue`, уничтожает `stcp_conn` или `stcp_client`.
### `stcp_link_send()`
Отправляет сырые данные через STCP-линк: создаёт `ll_entry` с данными, кладёт в `conn->tx_queue`. Шифруется и отправляется коллбэком `tx_cb` из stcp_client/stcp_server.
### `stcp_link_is_ready()`
Возвращает 1 если линк готов к передаче данных (handshake завершён).
### `stcp_link_get_etcp_conn()`
Возвращает `struct ETCP_CONN *` для передачи в ETCP-уровень (маршрутизатор, балансировщик и т.д.).
### Коллбэки
- **`stcp_link_set_on_ready()`** — вызывается когда линк переходит в состояние готовности.
- **`stcp_link_set_on_close()`** — вызывается при закрытии линка.
- **`stcp_server_on_link_cb`** — коллбэк сервера при новом входящем соединении.

44
src/stcp_server_doc.md

@ -0,0 +1,44 @@
# STCP Server — приём и рукопожатие соединений
## 1. Назначение
STCP Server — серверная сторона протокола STCP. Создаёт слушающий TCP-сокет на указанном порту, принимает входящие соединения, выполняет серверную часть X25519-рукопожатия и поднимает зашифрованный канал AES-CTR для обмена сообщениями.
Использует общую структуру `struct stcp_conn` из `stcp.h`.
## 2. Как пользоваться
```c
struct stcp_server *srv = stcp_server_create(ua, port, &my_keys,
on_connect_cb, conn_arg, // вызывается после успешного рукопожатия
on_close_cb, close_arg); // вызывается при закрытии/ошибке
// ... use ...
stcp_server_destroy(srv);
```
После завершения рукопожатия соединение переходит в `STCP_STATE_DATA`, вызывается `connect_cb`. Вышестоящий код может слать сообщения через `tx_queue` — они будут зашифрованы с CRC32 и отправлены. Входящие расшифрованные сообщения попадают в `rx_queue`.
Ключевые нюансы:
- Сервер принимает клиентский pubkey+salt (обфусцированный), извлекает pubkey через `sc_obfuscate_pubkey`, выполняет ECDH.
- Отправляет ответ: статус OK + padding + CRC32, всё зашифровано AES-CTR.
- После рукопожатая трафик шифруется потоковым AES-CTR (XOR), для каждого направления — свой stream.
- Рукопожатие обрабатывается в два этапа: `process_client_handshake` (извлечение ключа, дешифровка) → `finish_client_handshake` (отправка ответа, переход в DATA).
## 3. API
### stcp_server — внутренняя структура
Слушающий сокет, ключи сервера, коллбэки `connect_cb` / `close_cb`. Не экспортируется, создаётся только через `stcp_server_create`.
### Функции
- `stcp_server_create(ua, port, keys, connect_cb, arg, close_cb, close_arg)` — создать сервер: открыть TCP-сокет, bind к порту (IPV4, INADDR_ANY), listen(16), зарегистрировать accept-коллбэк. При приёме соединения создаёт `struct stcp_conn` в состоянии `STCP_STATE_HS_SERVER_WAIT`.
- `stcp_server_destroy(srv)` — закрыть слушающий сокет, освободить память.
### Протокол рукопожатия (серверная сторона)
1. Сервер читает `SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT` байт (pubkey+salt + зашифрованные padding_size + CRC32).
2. Деобфусцирует клиентский pubkey (`sc_obfuscate_pubkey`, salt XOR с SHA256(salt||server_pubkey)).
3. Выполняет ECDH (`sc_init_ctx` + `sc_set_peer_public_key`) → получает `session_key`, инициализирует `stream_send`/`stream_recv`.
4. Расшифровывает padding_size, проверяет CRC32, ожидает padding-байты.
5. Формирует ответ: salt2(8) + обфусцированный серверный pubkey(32) + status=OK(1) + padding_size(2) + CRC32(4) + padding(8). Всё кроме salt2 зашифровано `sc_stream_xor`.
6. Переходит в `STCP_STATE_DATA`, вызывает `on_ready` (== `connect_cb`).

305
src/topo_group_doc.md

@ -0,0 +1,305 @@
# topo_group
## 1. Назначение
BGP-подобный обмен топологией узлов между пирами uTun. Каждый узел хранит полную таблицу известных узлов и при подключении нового пира синхронизирует её с ним. При изменении информации об узле или обнаружении его недоступности — изменения распространяются всем пирам (broadcast NODEINFO/WITHDRAW).
Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной семантикой:
- **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция, проверка связности
- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite (membership)
Группа по умолчанию (UTUN) создаётся автоматически при `topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для chatgui). Узлы разных групп изолированы: peer из utun-группы не видит узлы чат-группы и наоборот.
Помимо обмена топологией, модуль выполняет:
- **NAT-детекцию** (NAT_INFO / NAT_CHECK_REQ): клиент запрашивает проверку своего NAT у сервера; сервер через третий узел пингует клиента и сообщает тип NAT (EIM/STRICT)
- **Поиск оптимального маршрута** до узла (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей (ETCP соединений)
- **Зондирование связности** — для новых узлов запускает `route_connectivity_probe_node()`
- **Персистентность в SQLite** — синхронизация узлов и membership в БД (таблицы nodes, node_addresses, peers_*)
## 2. Как пользоваться
### 2.1. Инициализация (при старте utun)
```c
struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт контейнер + utun-группу по умолчанию
```
Здесь же:
- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — регистрирует приёмник пакетов топологии
- `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)` — подписывается на события on_up/on_down всех новых ETCP-соединений
- Открывается SQLite (если указан `db_path` в конфиге)
### 2.2. Новое ETCP-соединение
При ETCP on_up автоматически вызывается `topo_group_new_conn(group, conn)`:
1. Добавляет conn в `senders_list` (через `topo_group_add_to_senders`)
2. Запускает NAT check для всех линков всех соединений
3. Отправляет `TOPO_SUBCMD_REQUEST_TABLE` — запрос полной таблицы
При приёме `TOPO_SUBCMD_REQUEST_TABLE` peer отправляет:
- NODEINFO c локальным узлом (local_node)
- Full table sync — все известные узлы, кроме тех, чей путь уже включает этого пира (защита от петель)
- `TOPO_SUBCMD_TABLE_COMPLETE` — метка завершения начальной синхронизации
### 2.3. NODEINFO (обновление/создание узла)
`topo_group_process_nodeinfo(group, from, data, len)`:
1. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS`
2. Проверка `hop_count < MAX_HOPS(16)`
3. Проверка версии узла — если версия не новее (stale), только обновляет пути (hop_list) и выходит
4. Десериализация wire-формата → обновление/создание `TOPO_NODEQ`
5. Добавление пути (`topo_group_add_path`) через текущее соединение
6. Вставка в `routing` (`route_insert`) — только для UTUN-групп
7. Сохранение в SQLite (если есть)
8. Callback `node_updated_cb` (для chatgui member_sync)
9. Broadcast всем пирам, кроме hop_list (защита от петель)
10. Запуск зондирования связности для новых узлов с адресами
### 2.4. WITHDRAW (удаление узла)
`topo_group_process_withdraw(group, sender, data, len)`:
1. Поиск узла по `node_id`
2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление всех путей, содержащих wd_source в hop_list
3. Если путей не осталось (ret == 1): удаление из routing, уведомление control_server, broadcast withdraw дальше
4. Если группа CHAT — удаление member из SQLite
### 2.5. Отключение соединения
При ETCP on_down вызывается `topo_group_remove_conn(group, conn)`:
1. Отмена pending ping-запросов для этого conn
2. Для всех узлов с путями через этот conn — `topo_group_remove_path`. Если узел стал полностью недоступен (нет путей) — удаляется из routing и nodes, с broadcast withdraw
3. Удаление conn из `senders_list`
### 2.6. Поиск пути до узла
`topo_group_find_conn_for_node(group, node_id)` — перебирает `path`'ы узла, выбирает с минимальным `hop_count`. Предпочитает live-пути (links_up > 0). Если live-путей нет, fallback на любой путь с минимальным hop_count.
Интенсивно используется `etcp_router`:
- При маршрутизации пакетов к удалённому узлу (`src/etcp_router.c:262`)
- При поиске next-hop для multihop соединений
### 2.7. NAT-детекция
1. При новом соединении вызывается `topo_group_start_link_nat_check` для каждого линка
2. Сервер ищет «третий узел» через `topo_group_find_third_node` — произвольный conn кроме проверяемого
3. Отправляет `PING_REQ` через третий узел на IP:port линка проверяемого клиента
4. По результату пинга: `NAT_TYPE_EIM` (если успех) или `NAT_TYPE_STRICT`
5. Результат сохраняется в `link->nat_type` и отправляется клиенту через `topo_group_send_nat_info`
6. Клиент обновляет свой `local_node` через `topo_group_handle_nat_info` и broadcast-ит изменения
### 2.8. Широковещательная рассылка
- **NODEINFO**: при поступлении новой/обновлённой информации об узле — отправляется всем соседям, кроме тех, чей node_id уже есть в hop_list (защита от петель)
- **WITHDRAW**: при удалении узла — отправляется всем соседям, кроме отправителя
### 2.9. Изоляция групп
При приёме NODEINFO проверяется флаг `TOPO_FLAG_SEND_SUBNETS`:
- UTUN-группа ожидает флаг = 1 (с подсетями)
- CHAT-группа ожидает флаг = 0 (без подсетей)
- При несовпадении отправляется `TOPO_SUBCMD_ERR_GROUP_MISMATCH` и пакет игнорируется
### 2.10. Alien-узлы
Узлы, помеченные `alien=1`, не участвуют в обмене маршрутами: при `topo_group_new_conn` для alien-пира обмен пропускается.
## 3. API
### 3.1. Структуры
| Структура | Назначение |
|---|---|
| `TOPO_GROUPS` | Контейнер всех групп топологии экземпляра. Хранит `group_list` (ll_queue), memory_pool'ы для сериализации, sqlite3, callback `node_updated_cb` |
| `TOPO_GROUP` | Одна группа топологии. `group_id`, `group_type`, `nodes` (ll_queue узлов), `senders_list` (ll_queue conn'ов), `local_node` (TOPO_NODEQ) |
| `TOPO_GROUP_CONN_ITEM` | Элемент списка senders: conn + ll_entry — связка с senders_list |
| `TOPOMSG_NODEINFO_PKT` | Wire-формат пакета NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть |
| `TOPOMSG_WITHDRAW_PKT` | Wire-формат WITHDRAW: node_id (удаляемый) + wd_source (инициатор) |
| `TOPOMSG_NAT_INFO` | Wire-формат NAT_INFO: socket_id, IP, port (network byte order), nat_type |
| `TOPOMSG_NAT_CHECK_REQ` | Запрос проверки NAT от клиента к серверу: socket_id + interface IP:port |
| `TOPOMSG_TABLE_REQ` | Wire-формат запроса/завершения таблицы: cmd + subcmd |
| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire-формат ошибки несоответствия типа группы: expected_type + received_flags |
| `nat_check_arg` | Контекст callback NAT-проверки: link + nat_ip + nat_port |
### 3.2. Константы
| Константа | Значение | Назначение |
|---|---|---|
| `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID для пакетов топологии |
| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле + подсети |
| `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы |
| `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен |
| `TOPO_SUBCMD_NAT_INFO` | 0x09 | Информация о типе NAT клиента |
| `TOPO_SUBCMD_NAT_CHECK_REQ` | 0x0A | Запрос проверки NAT для сокета |
| `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации |
| `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы |
| `MAX_HOPS` | 16 | Максимальная длина hop-листа |
| `BGP_NODES_HASH_SIZE` | 256 | Размер хеш-таблицы nodes |
| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id по умолчанию для utun-группы |
### 3.3. Функции инициализации/завершения
| Функция | Назначение |
|---|---|
| `topo_groups_init(instance)` | Создаёт TOPO_GROUPS, memory_pool'ы, открывает SQLite, создаёт utun-группу по умолчанию, регистрирует ETCP-коллбэки |
| `topo_groups_destroy(instance)` | Освобождает все группы, memory_pool'ы, закрывает SQLite; снимает ETCP-регистрацию |
| `topo_group_create(instance, group_id, group_type)` | Создаёт TOPO_GROUP: nodes (hash queue), senders_list, local_node; вызывается из topo_groups_init / topo_groups_create_group |
| `topo_group_destroy(group)` | Освобождает senders_list, local_node, nodes очереди; узлы должны быть удалены заранее |
| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создаёт новую группу и добавляет в group_list; для CHAT устанавливает channel_id |
| `topo_groups_find(g, group_id)` | Поиск группы по group_id через хеш-индекс group_list |
| `topo_groups_get_default(g)` | Возвращает utun-группу по умолчанию (group_id=TOPO_GROUP_UTUN) |
### 3.4. Функции жизненного цикла соединения
| Функция | Назначение |
|---|---|
| `topo_group_new_conn(group, conn)` | Добавляет conn в senders_list, запускает NAT check для всех линков, отправляет REQUEST_TABLE; alien-пиры пропускаются |
| `topo_group_remove_conn(group, conn)` | Отменяет NAT check и ping pending, удаляет все пути через conn, удаляет недоступные узлы, broadcast withdraw, убирает conn из senders_list |
| `topo_group_etcp_conn_cbk(conn, arg)` | Callback при создании нового ETCP-соединения: подписывается на on_up/on_down через etcp_conn_add_up_cbk/down_cbk |
| `topo_group_on_conn_up(conn, arg)` | При on_up: вызывает topo_group_new_conn |
| `topo_group_on_conn_down(conn, arg)` | При on_down: вызывает topo_group_remove_conn |
### 3.5. Функции обработки пакетов
| Функция | Назначение |
|---|---|
| `topo_group_receive_cbk(from_conn, entry)` | Точка входа приёма пакетов топологии: диспетчеризация по subcmd (NODEINFO/WITHDRAW/REQUEST_TABLE/PING_REQ/PING_RESP/NAT_INFO/NAT_CHECK_REQ/TABLE_COMPLETE/ERR_GROUP_MISMATCH) |
| `topo_group_process_nodeinfo(group, from, data, len)` | Обработка NODEINFO: проверка группы/версии/hops, десериализация, обновление/создание TOPO_NODEQ, пути, routing, SQLite, broadcast, зондирование связности |
| `topo_group_process_withdraw(group, sender, data, len)` | Обработка WITHDRAW: удаление путей с wd_source, если узел недоступен — удаление из routing, broadcast |
| `topo_group_handle_request_table(group, conn)` | Обработка REQUEST_TABLE: отправляет NODEINFO (local_node) + full table sync + TABLE_COMPLETE |
| `topo_group_handle_nat_info(group, from_conn, data, len)` | Обработка NAT_INFO: обновляет nat_type в local_node (TOPO_SOCKMETA4), ETCP_SOCKET, при изменениях broadcast NODEINFO |
| `topo_group_handle_nat_check_req(group, from_conn, data, len)` | Обработка NAT_CHECK_REQ от клиента: находит линк и запускает NAT-проверку |
### 3.6. Функции поиска
| Функция | Назначение |
|---|---|
| `topo_group_find_conn_for_node(group, node_id)` | Поиск оптимального ETCP_CONN до узла: min hop_count, предпочтение live-путям |
| `topo_group_find_third_node(group, exclude)` | Поиск произвольного conn'а (не exclude) для использования как посредника при NAT-детекции; исключает private/local conn'ы |
### 3.7. Функции broadcast
| Функция | Назначение |
|---|---|
| `topo_group_send_nodeinfo(group, node, conn)` | Сериализует TOPO_NODEQ и отправляет NODEINFO одному conn |
| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW всем пирам (without exclude) |
| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Рассылает WITHDRAW всем conn в senders_list, кроме exclude |
| `topo_group_send_full_table(group, conn)` | Отправляет все известные узлы (кроме тех, чей путь включает target) указанному conn |
| `topo_group_send_table_request(group, conn)` | Отправляет запрос полной таблицы (TOPO_SUBCMD_REQUEST_TABLE) |
| `topo_group_send_table_complete(group, conn)` | Отправляет метку завершения синхронизации (TOPO_SUBCMD_TABLE_COMPLETE) |
| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку несоответствия типа группы |
### 3.8. Функции NAT
| Функция | Назначение |
|---|---|
| `topo_group_send_nat_info(conn, socket_id, nat_ip, nat_port, nat_type)` | Отправляет клиенту информацию о его NAT (результат проверки) |
| `topo_group_send_nat_check_req(conn, socket_id)` | Отправляет серверу запрос на проверку NAT для указанного socket_id |
| `topo_group_start_link_nat_check(group, link)` | Запускает NAT-проверку для одного линка через третий узел; проверяет is_local_subnet, пропускает private/local линки |
| `topo_group_request_nat_check_all(group)` | Запускает NAT check для всех линков всех соединений |
| `topo_group_set_nat_check_local(group, allow)` | Разрешить/запретить NAT check для локальных подсетей (127.x, 10.x, 192.168.x, 172.16-31.x) |
### 3.9. Функции путей
| Функция | Назначение |
|---|---|
| `topo_group_add_path(nq, conn, hop_list, hop_count)` | Создаёт TOPO_NODEPATH для узла (conn + hop_list) и добавляет в paths |
| `topo_group_remove_path(nq, conn)` | Удаляет путь через указанный conn; возвращает 1 если путей не осталось (unreachable) |
| `topo_group_remove_path_by_hop(nq, wd_source)` | Удаляет все пути, содержащие wd_source в hop_list (используется в WITHDRAW) |
| `topo_group_add_to_senders(group, conn)` | Добавляет conn в senders_list (с проверкой дубликатов) |
| `topo_group_should_send_to(nq, target_id)` | Проверяет, нужно ли отправлять NODEINFO узла указанному пиру (target_id не в hop_list) |
### 3.10. Callbacks/вспомогательные
| Функция | Назначение |
|---|---|
| `topo_groups_set_node_updated_cb(groups, fn)` | Устанавливает callback при обновлении узла (используется chatgui для member_sync) |
| `topo_groups_set_sqlite_db(g, db)` | Устанавливает внешнюю БД SQLite |
| `nat_link_check_cb(success, avg_rtt, count_sent, count_ok, arg)` | Callback завершения NAT-проверки: устанавливает nat_type линка и отправляет результат клиенту |
| `nodeinfo_dump_log(data, len)` | Логирует краткую сводку NODEINFO (node_id, ver, счётчики, hops) |
| `nodeinfo_format(data, len)` | Форматирует полный NODEINFO с именем, подсетями в читаемую строку (аллокация через u_malloc) |
| `group_subcmd_name(subcmd)` | Возвращает строковое имя subcmd для логов |
### 3.11. Проверка соответствия типа группы
| Функция | Назначение |
|---|---|
| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку GROUP_MISMATCH пиру, если его NODEINFO содержит неверный flags (разные типы групп) |
## 4. Архитектурные связи
### 4.1. Внешние зависимости
| Модуль | Как используется |
|---|---|
| `topo_node.c/h` | Модель данных узла: TOPO_NODE, TOPO_NODEQ, ser/deserialize, find_by_id, update_my_nodeinfo, free_lists |
| `topo_node_sqlite.h` | Персистентность в SQLite: topo_node_sqlite_node_put, member_put, member_del, init |
| `route_lib.h` | Таблица маршрутов: route_insert, route_delete |
| `route_ping.h` | NAT-детекция и проверка связности: route_ping_send_req_addr, handle_req, handle_resp, cancel_for_conn |
| `route_connectivity.h` | Зондирование связности узла: route_connectivity_probe_node, cancel_node, cancel_all |
| `etcp_api.h` | Регистрация приёмника: etcp_bind, etcp_add_new_conn_cbk, etcp_unbind |
| `etcp_connections.h` | Управление соединениями: etcp_conn_add_up_cbk, etcp_conn_add_down_cbk, ETCP_CONN, ETCP_LINK |
| `etcp.h` | Отправка пакетов: etcp_send |
| `etcp_router.h` | Закрытие виртуальных каналов: etcp_router_conn_close_all_for_node |
| `control_server.h` | Уведомления мониторинга: control_server_notify_node_change, control_server_notify_node_removed |
| `conn_mgr.h` | Менеджер соединений: использует topo_group_find_conn_for_node для поиска путей |
| `utun_instance.h` | UTUN_INSTANCE: конфиг, node_id, ключи, rt, topo_groups |
| `secure_channel.h` | Ключи: sc_derive_ed25519_pubkey |
| `memory_pool.h` | Память для сериализации: memory_pool_init / destroy |
| `debug_config.h` | Логирование: DEBUG_INFO/WARN/ERROR/DEBUG с категорией BGP |
| `mem.h` | Аллокация: u_malloc, u_free, u_calloc |
### 4.2. Кто вызывает topo_group
| Вызывающий | Что вызывает | Для чего |
|---|---|---|
| `utun_instance.c` | `topo_groups_init`, `topo_groups_destroy` | Инициализация/завершение при старте/остановке utun |
| `etcp_connections.c` | `topo_group_send_nodeinfo` | При изменении конфигурации сокетов — broadcast обновлённого local_node |
| `etcp_router.c` | `topo_group_find_conn_for_node` | Поиск next-hop для маршрутов и multihop-соединений (~15 вызовов) |
| `conn_mgr.c` | `topo_group_find_conn_for_node` | Поиск пути для установки прямого/обратного/indirect соединения |
| Тесты | `topo_groups_get_default`, `topo_group_find_conn_for_node`, `topo_node_find_by_id`, `topo_group_set_nat_check_local` | 9 тестовых файлов |
### 4.3. ETCP-коллбэки (верхний уровень)
Точка входа для пакетов топологии — `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` в `topo_groups_init`. Единственный обработчик на весь ETCP ID 0x01. Диспетчеризация внутри по subcmd (byte data[1]).
Коллбэки on_up/on_down новых соединений регистрируются через `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)`, которая при создании каждого нового ETCP_CONN подписывается на его события.
## 5. Форматы пакетов (wire)
### 5.1. NODEINFO
```
cmd(1) | subcmd(1) | TOPOMSG_NODE (82+ байта) | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [tranzit[]] | [hop_list[]]
```
### 5.2. WITHDRAW
```
cmd(1) | subcmd(1) | node_id(8) | wd_source(8) = 18 байт
```
### 5.3. REQUEST_TABLE / TABLE_COMPLETE
```
cmd(1) | subcmd(1) = 2 байта
```
### 5.4. NAT_INFO
```
cmd(1) | subcmd(1) | socket_id(1) | nat_ip(4) | nat_port(2) | nat_type(1) = 10 байт
```
### 5.5. NAT_CHECK_REQ
```
cmd(1) | subcmd(1) | socket_id(1) | interface_ip(4) | interface_port(2) = 9 байт
```
### 5.6. ERR_GROUP_MISMATCH
```
cmd(1) | subcmd(1) | expected_type(1) | received_flags(1) = 4 байта
```
## 6. Примечания
- **Хранилище памяти**: В отличие от STCP, модуль не имеет отдельной БД. Использует внешний SQLite3 pointer (`topo_groups->topo_sqlite_db`), который может быть открыт самим модулем (если указан `db_path`) или передан из chatgui через `topo_groups_set_sqlite_db`.
- **Версионирование NODEINFO**: Версия (ver, uint8_t 1-255) сравнивается через циклическую арифметику `(int8_t)(cur_ver - new_ver) >= 0`. Версия 0 не используется (нет данных).
- **Защита от петель**: NODEINFO рассылается только тем пирам, чей node_id отсутствует в hop_list. WITHDRAW broadcast всем кроме отправителя.
- **NAT check для локальных сетей**: По умолчанию запрещён (`allow_nat_check_local=0`), можно включить через `topo_group_set_nat_check_local(group, 1)` для тестов.
- **Aliens**: Узлы с `alien=1` хранятся в nodes, имеют пути, но не инициируют обмен маршрутами при подключении (topo_group_new_conn пропускает).
- **Обновление при NAT-детекции**: При получении NAT_INFO, если изменился nat_type сокета с не-EIM на EIM (или наоборот) — обновляется `local_node`, увеличивается версия и broadcast-ится всем пирам.

170
src/topo_node_doc.md

@ -0,0 +1,170 @@
# topo_node
## 1. Назначение
Модель данных узла топологии — структуры данных для представления узла в памяти, сериализации/десериализации в бинарный wire-формат для BGP-обмена, учёт ссылок (refcounting) и диагностический дамп.
Модуль определяет два слоя представлений:
- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) для удобной модификации.
- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети без выравнивания.
Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только данные и их упаковка/распаковка.
Узлы персистентно хранятся в SQLite через модуль `topo_node_sqlite`. Память для элементов списков (адреса, сокет-мета, подсети) выделяется из memory pool'ов из `topo_groups`.
## 2. Как пользоваться
### Типовые сценарии
**Создание/обновление информации о себе:**
```c
topo_group_update_my_nodeinfo(instance, group);
```
Строит `TOPO_NODEQ` (в `group->local_node`) из конфига: собирает список сокетов, адресов (interface/NAT/real), подсетей; при изменениях инкрементирует `ver` и помечает `dirty=1`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети не включаются.
**Сериализация для отправки по BGP:**
```c
uint8_t buf[2048];
int len = topo_node_serialize(group, nq, buf, sizeof(buf));
```
Запаковывает `TOPO_NODEQ` → бинарный буфер: заголовок `TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если `TOPO_FLAG_SEND_SUBNETS`), транзитные узлы и hop-лист.
**Десериализация при получении NODEINFO:**
```c
struct TOPO_NODE *ni = NULL;
struct TOPO_NODESUBNETS *subnets = NULL;
struct TOPOMSG_TRANZIT *tranzit = NULL;
uint8_t tranzit_count = 0;
uint64_t *hop_list = NULL;
uint8_t hop_count = 0;
topo_node_deserialize(group, data, data_len, &ni, &subnets, &tranzit, &tranzit_count, &hop_list, &hop_count);
```
Восстанавливает `TOPO_NODE`, подсети, транзит и hop-лист из бинарного буфера. Элементы списков аллоцируются из memory pool'ов. Выделенная память должна быть освобождена:
- `topo_node_unref(ni)` — освобождает `TOPO_NODE` и `node_name`
- `topo_node_free_lists(group, nq)` — освобождает списки обратно в пулы, а также `subnets`, `tranzit_data`, `hop_list`
**Поиск узла по ID:**
```c
struct TOPO_NODEQ *nq = topo_node_find_by_id(group, node_id);
```
Использует хеш-индекс очереди `group->nodes` (ключ — `node_id`, 8 байт).
**Дамп всех узлов:**
```c
topo_node_dump_all(group); // в лог
topo_node_format_all(group, buf, size); // в строку
```
### Ключевые концепции
- **TOPO_NODE** — базовые данные узла: pubkey (X25519), ed25519_pubkey, имя, версия, групповой ID. Разделяется между группами через счётчик ссылок.
- **TOPO_NODEQ** — запись в очереди узлов группы. Содержит `TOPO_NODE*` (общий), плюс динамические пути, подсети, транзитные узлы, hop-лист, связность, conn_mgr. Все поля аллоцированы отдельно и освобождаются через `topo_node_free_lists()`.
- **TOPO_NODEPATH** — путь до узла через конкретное ETCP-соединение. Хранится в `paths`-очереди типа `ll_queue`. После самой структуры идёт массив `uint64_t hop[hop_count]` (variable-length).
- **Счётчик ссылок:** `topo_node_ref()`/`topo_node_unref()`. При падении счётчика до 0 освобождается `node_name` и сам `TOPO_NODE`. Сами списки адресов и sock_meta освобождаются отдельно через `free_lists()`.
- **Адреса бывают трёх типов:** `TOPO_ADDR_INTERFACE` (LAN-адрес), `TOPO_ADDR_NAT` (после детекции NAT), `TOPO_ADDR_REAL` (подтверждённый прямой адрес). Для каждого сокета может быть до 3 адресов в списке.
- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): статус зондирования (NONE/IN_PROGRESS/DONE), результаты для interface/nat/real адресов, минимальные RTT, время зондирования.
- **TOPO_FLAG_SEND_SUBNETS** — флаг в `TOPO_NODE.flags`: если установлен, подсети сериализуются и включаются в wire-формат. Для chat-групп сбрасывается в 0.
- **Wire-протокол:** заголовок `TOPOMSG_NODE` (75 байт packed) + динамическая часть: `node_name` (0-255 байт), `SOCKMETA4*N`, `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, транзит, hop-лист. Размер динамической части вычисляется функцией `topo_node_dyn_size()`.
## 3. API
### Основные структуры
| Структура | Назначение |
|-----------|-----------|
| `struct TOPO_NODE` | Идентичность узла: pubkeys (X25519+Ed25519), имя, версия, group_id, node_id, списки v4/v6 сокет-метаданных и адресов. Счётчик ссылок. |
| `struct TOPO_NODEQ` | Запись в очереди группы. Содержит `TOPO_NODE*`, подсети, транзитные данные, hop-лист, очередь `paths` (TOPO_NODEPATH), связность, conn_mgr. `best_socket` — указатель на лучший ETCP-сокет для связи. |
| `struct TOPO_NODEPATH` | Путь до узла через ETCP-соединение с hop_count. Variable-length: после структуры идёт `uint64_t hop[]`. |
| `struct TOPO_NODESUBNETS` | Списки v4 и v6 подсетей, анонсируемых узлом. Отдельный malloc, может быть NULL. |
| `struct TOPO_CONNECTIVITY` | Локальное состояние проверки связности: статусы зондирования, RTT для interface/nat/real адресов. |
| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата (75 байт). Содержит счётчики элементов динамической части. |
| `struct TOPOMSG_ADDR4/6` | Packed wire-формат одного адреса (addr, port, type, socket_id, protocol). |
| `struct TOPOMSG_SOCKMETA4/6` | Packed wire-формат метаданных сокета (id, config_type, nat_type). |
| `struct TOPOMSG_SUBNET4/6` | Packed wire-формат подсети (addr, prefix_length). |
| `struct TOPOMSG_TRANZIT` | Packed wire-формат транзитного узла (node_id, rtt, link_q). |
### Вспомогательные типы в памяти (связанные списки)
| Тип | Назначение |
|-----|-----------|
| `struct TOPO_SOCKMETA4` | Элемент списка v4 сокет-метаданных (next*, id, config_type, nat_type). |
| `struct TOPO_ADDR4` | Элемент списка v4 адресов (next*, addr[4], port, type, socket_id, protocol). |
| `struct TOPO_SOCKMETA6` | Элемент списка v6 сокет-метаданных. |
| `struct TOPO_ADDR6` | Элемент списка v6 адресов. |
| `struct TOPO_SUBNET4` | Элемент списка v4 подсетей (next*, addr[4], prefix_length). |
| `struct TOPO_SUBNET6` | Элемент списка v6 подсетей. |
Все элементы списков аллоцируются из memory pool'ов и освобождаются через `memory_pool_free()`.
### Константы
| Константа | Значение | Назначение |
|-----------|---------|-----------|
| `TOPO_ADDR_INTERFACE` | 0 | Интерфейсный (LAN) адрес сокета |
| `TOPO_ADDR_NAT` | 1 | NAT-адрес после детекции |
| `TOPO_ADDR_REAL` | 2 | Подтверждённый прямой адрес |
| `TOPO_PROTO_UDP` | 0x01 | UDP-транспорт |
| `TOPO_PROTO_TCP` | 0x02 | TCP-транспорт |
| `PROBE_STATUS_NONE` | 0 | Зондирование не запускалось |
| `PROBE_STATUS_IN_PROGRESS` | 1 | Зондирование идёт |
| `PROBE_STATUS_DONE` | 2 | Зондирование завершено |
| `PROBE_RESULT_UNKNOWN` | 0 | Результат неизвестен |
| `PROBE_RESULT_REACHABLE` | 1 | Адрес достижим |
| `PROBE_RESULT_UNREACHABLE` | 2 | Адрес недостижим |
| `CONN_TYPE_DIRECT` | 1 | Прямое соединение |
| `CONN_TYPE_REVERSE` | 2 | Обратное соединение |
| `CONN_TYPE_INDIRECT` | 3 | Соединение через посредника |
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг: отправлять подсети |
| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID группы uTun по умолчанию |
### Функции
**Управление памятью и refcounting:**
- `topo_node_ref(ni)` — увеличить счётчик ссылок на `TOPO_NODE`.
- `topo_node_unref(ni)` — уменьшить счётчик; при 0 освободить `node_name` и `TOPO_NODE` (но не списки — их надо предварительно освободить через `topo_node_free_lists`).
- `topo_node_free_lists(group, nq)` — освободить все списки адресов/sock_meta обратно в memory pool'ы, освободить `subnets`, `tranzit_data`, `hop_list`, сделать `unref` на `node`, обнулить указатели.
**Сериализация:**
- `topo_node_dyn_size(msg)` — вычислить размер динамической части wire-формата по полям-счётчикам заголовка `TOPOMSG_NODE`. Не учитывает сам заголовок.
- `topo_node_serialize(group, nq, out, out_max)` — сериализовать `TOPO_NODEQ` в бинарный буфер. Возвращает количество записанных байт или -1 при ошибке (переполнение или невалидные аргументы). Формат: заголовок `TOPOMSG_NODE`, затем name, v4 sock_meta, v4 addrs, v6 sock_meta, v6 addrs, подсети (опционально), tranzit, hop_list.
**Десериализация:**
- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_tranzit, &out_tranzit_count, &out_hop_list, &out_hop_count)` — восстановить `TOPO_NODE`, подсети, транзит и hop-лист из бинарного wire-формата. Аллоцирует `TOPO_NODE` через `u_calloc` (ref_count=1), элементы списков — из memory pool'ов, `subnets`, `tranzit_data`, `hop_list` — отдельными `u_malloc`. Возвращает 0 при успехе, -1 при несоответствии размера или ошибке аллокации. Если подсети есть в wire-формате но флаг `TOPO_FLAG_SEND_SUBNETS` не установлен, они пропускаются.
**Поиск:**
- `topo_node_find_by_id(group, node_id)` — найти `TOPO_NODEQ` в очереди группы по `node_id` через хеш-индекс. Возвращает NULL если не найден.
**Обновление информации о себе:**
- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `group->local_node` (`TOPO_NODEQ`) из текущих данных инстанса (сокеты, NAT, адреса, подсети, ключи). Сравнивает количество элементов с предыдущим значением; если есть изменения — освобождает старый `local_node`, создаёт новый, инкрементирует `ver`, помечает `dirty=1`. Обновляет таблицу маршрутов (`route_delete`/`route_insert`). Для chat-групп подсети не включаются. Возвращает количество v4 подсетей.
**Диагностика:**
- `topo_node_dump_all(group)` — дамп всех узлов группы в лог (DEBUG_CATEGORY_BGP). Для каждого узла выводит: ID, имя, версию, pubkeys, v4/v6 сокеты и адреса, подсети, транзитные узлы, пути (с hop-ами), статус связности, conn_mgr. Пропускает узлы с `node==NULL`.
- `topo_node_format_all(group, buf, buf_size)` — аналогичный дамп в строковый буфер. Возвращает количество записанных байт. При переполнении добавляет `[TRUNCATED]`.
**Inline-аксессоры (из .h):**
- `topo_v4_sock_meta(ni)` / `topo_v4_addrs(ni)` / `topo_v6_sock_meta(ni)` / `topo_v6_addrs(ni)` — получить списки из `TOPO_NODE`.
- `topo_v4_subnets(r)` / `topo_v6_subnets(r)` — получить списки подсетей из `TOPO_NODESUBNETS*` (NULL-safe).
- `topo_list_count(head)` — посчитать количество элементов в любом из списков (next — первое поле).
### Зависимости
| Модуль | Использование |
|--------|--------------|
| `ll_queue.h` | `TOPO_NODEQ` — запись в очереди, хеш-индекс по `node_id`, `paths` — вложенная очередь |
| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) для массивов pubkey/ed25519 |
| `mem.h` | `u_malloc`, `u_calloc`, `u_free`, `u_strdup` |
| `memory_pool.h` | `memory_pool_alloc`/`memory_pool_free` для элементов списков (пулы из `topo_groups`) |
| `debug_config.h` | `DEBUG_INFO`, `DEBUG_WARN`, `DEBUG_ERROR`, `log_dump` |
| `topo_group.h` | `struct TOPO_GROUP` (аргумент большинства функций) |
| `etcp.h` | `struct ETCP_CONN`, `struct ETCP_SOCKET` |
| `config_parser.h` | `struct CFG_ROUTE_ENTRY`, `struct CFG_SERVER` (при построении local_node) |
| `route_lib.h` | `route_insert()`, `route_delete()` (при обновлении local_node) |
| `etcp_debug.h` | `ip_to_str()` (для дампа адресов и подсетей) |
| `utun_instance.h` | `struct UTUN_INSTANCE` (аргумент `topo_group_update_my_nodeinfo`) |

39
src/topo_node_sqlite_doc.md

@ -0,0 +1,39 @@
# topo_node_sqlite — SQLite-персистентность топологии
## 1. Назначение
Обеспечивает сохранение и восстановление топологии P2P-сети (ноды, адреса, каналы, участники каналов) в SQLite-базе. База используется как основной источник данных для chatgui и восстановления состояния между запусками utun.
В отличие от LMDB-слоя (`topo_node_lmdb`), который хранит данные в бинарном формате BGP-сообщений, SQLite-слой работает с нормализованными таблицами и используется для клиентских целей — чтение данных chatgui, получение списка участников канала, проверка online-статуса.
## 2. Как пользоваться
1. Получить готовый `sqlite3* db` (открывается извне, обычно в `chat_sync.c`)
2. Вызвать `topo_node_sqlite_init(db)` для создания таблиц
3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()`
4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_<channel_id>`)
5. Добавлять участников в канал через `topo_node_sqlite_member_put()`
6. Читать участников канала через `topo_node_sqlite_channel_peers_all()` — возвращает бинарный буфер со всеми участниками (node_id, ключи, подписи, адреса)
**Нюансы:**
- Имена таблиц `peers_<channel_id>` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_`
- `topo_node_sqlite_channel_peers_all()` формирует компактный бинарный буфер с числом записей в начале (uint16_t)
- Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта
- Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN
## 3. API
| Функция | Описание |
|---------|----------|
| `topo_node_sqlite_init(db)` | Создаёт таблицы `nodes`, `node_addresses`, `channels` (если не существуют) |
| `topo_node_sqlite_node_put(db, nq)` | Сохраняет ноду и её адреса (IPv4/IPv6) в транзакции; старые адреса удаляются |
| `topo_node_sqlite_channel_put(...)` | Сохраняет канал в `channels` + создаёт таблицу `peers_<ch_id>` |
| `topo_node_sqlite_member_put(...)` | Добавляет/обновляет участника в `peers_<ch_id>` (INSERT OR REPLACE) |
| `topo_node_sqlite_member_get_join(db, ch_id, node_id, ...)` | Читает join_sig и join_ts участника канала |
| `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_<ch_id>` |
| `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) |
| `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) |
| `topo_node_sqlite_channel_peers_all(...)` | Сериализует всех участников канала в бинарный буфер (count + [node_id\|x25519\|ed25519\|flags\|sigs\|name\|addrs]) |
| `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды |
| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online |
| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) |
**Структуры:** внешние — `sqlite3*`, `struct TOPO_NODEQ`, `struct TOPO_NODE`; собственных структур нет.

54
src/tun_if_doc.md

@ -0,0 +1,54 @@
# TUN Interface (tun_if)
## 1. Назначение
Модуль создания и управления виртуальным сетевым интерфейсом (TUN) — точкой входа/выхода всего VPN-трафика. Через TUN-интерфейс ОС отправляет IP-пакеты в uTun и получает обработанные пакеты обратно. Реализует кроссплатформенную абстракцию над платформенными механизмами: Linux (`/dev/net/tun`), FreeBSD (`/dev/tun` + TUNSIFHEAD), Windows (WinTun API).
Модель данных — две очереди:
- **output_queue** — пакеты, прочитанные из TUN (ОС → uTun → routing)
- **input_queue** — пакеты для записи в TUN (routing → uTun → ОС), с callback `tun_input_queue_callback`
## 2. Как пользоваться
```c
// Инициализация из конфига (основной интерфейс VPN)
tun = tun_init(ua, config);
// или для NAT TUN с явными параметрами
nat_tun = tun_init_nat(ua, "tun_nat", "10.0.1.1", 1500, 0);
// Чтение: очередь output_queue наполняется автоматически (через uasync/epoll),
// обработчик читает через queue_set_callback
struct ll_queue* out = tun_get_output_queue(tun);
// Запись: пакет кладётся в input_queue, callback пишет в TUN
tun_write(tun, buf, len); // прямой вызов, не через очередь
// Завершение
tun_close(tun);
```
**Root-права:** требуются на всех платформах для создания TUN-устройств и настройки IP.
**Тестовый режим** (`test_mode=1`): вместо реального TUN используется `socketpair(AF_UNIX, SOCK_DGRAM)`. Вторая сторона пары доступна через `tun_get_test_fd()`.
**Платформенные различия:**
- **Linux:** `open("/dev/net/tun")` + `ioctl(TUNSETIFF)` с `IFF_TUN | IFF_NO_PI`, неблокирующий режим через `fcntl(O_NONBLOCK)`
- **FreeBSD:** `open("/dev/tun")` (cloning device), имя присваивается ядром, `ioctl(TUNSIFHEAD)` для multi-af (4-байтный заголовок AF), `ioctl(TUNSIFMODE)` для POINTOPOINT
- **Windows:** WinTun API через `wintun.dll`, кольцевой буфер 4 MiB, отдельный read-поток (`tun_read_thread_proc`), пакеты передаются в main thread через `uasync_post → tun_packet_handler`
## 3. API
### Структуры
- **`struct tun_if`** — хендл интерфейса: `fd` (Linux/BSD файловый дескриптор), `platform_handle`/`adapter_handle` (Windows WinTun), `ifname`, `ifindex`, очереди `input_queue`/`output_queue`, пул `pool` для ll_entry, счётчики статистики (`bytes_read/written`, `packets_read/written`, `read_errors`, `write_errors`)
- **`struct tun_packet_data`** — обёртка для передачи пакета из read-потока (Windows) в main thread: указатели на `tun_if` и `ll_entry`
### Функции
- **`tun_init(ua, config)`** / **`tun_init_nat(ua, ifname, ip, mtu, test_mode)`** — создают TUN-интерфейс: платформенный init, настройка IP/MTU, поднятие интерфейса, создание очередей, регистрация в uasync (Linux/BSD) или запуск read-потока (Windows). `tun_init_nat` — вариант для отдельного NAT TUN с явными параметрами вместо конфига
- **`tun_close(tun)`** — остановка read-потока (Windows), удаление из uasync, слив и освобождение обеих очередей, уничтожение пула, платформенная очистка (`close(fd)` на Linux, `SIOCIFDESTROY` на FreeBSD, `WintunEndSession`+`WintunCloseAdapter` на Windows)
- **`tun_write(tun, buf, len)`** — прямая запись в TUN (в тестовом режиме — в output_queue, иначе `tun_platform_write`). Пакет имеет префиксный байт, отбрасываемый при записи
- **`tun_inject_packet(tun, buf, len)`** — инжекция пакета в output_queue (для тестов и Windows-нотификаций)
- **`tun_read_packet(tun, buf, len)`** — тестовый helper: чтение одного пакета из input_queue
- **`tun_packet_handler(arg)`** — callback для main thread (Windows): распаковывает `tun_packet_data`, кладёт `ll_entry` в output_queue
- **Платформенные функции** (`tun_linux.c` / `tun_freebsd.c` / `tun_windows.c`):
- `tun_platform_init()` — создание устройства, настройка IP/MTU, поднятие, получение `ifindex`, `O_NONBLOCK`
- `tun_platform_cleanup()` — закрытие fd/хендлов
- `tun_platform_read()` — чтение из TUN (FreeBSD: `readv` с отделением 4-байтного AF-заголовка)
- `tun_platform_write()` — запись в TUN (FreeBSD: `writev` с AF-заголовком; Windows: `WintunAllocateSendPacket` + `WintunSendPacket`)
- `tun_platform_get_poll_fd()` — fd для uasync (Windows возвращает -1, используется read-поток)

49
src/tun_route_doc.md

@ -0,0 +1,49 @@
# TUN Route (tun_route)
## 1. Назначение
Модуль управления маршрутами в системной таблице маршрутизации. Добавляет маршруты к заданным подсетям через TUN-интерфейс, чтобы исходящий трафик на эти подсети направлялся в виртуальный интерфейс uTun. Удаляет их при завершении работы. Кроссплатформенный: использует разные механизмы ОС с fallback-стратегией.
## 2. Как пользоваться
```c
// Добавить один маршрут
tun_route_add(ifindex, "tun0", 0x0A000000, 8); // 10.0.0.0/8
// Добавить все маршруты из конфига
tun_route_add_all(ifindex, "tun0", cfg.routes);
// Удалить все маршруты из конфига
tun_route_del_all(ifindex, "tun0", cfg.routes);
// Удалить все маршруты через интерфейс (очистка)
tun_route_flush("tun0");
```
**Network передаётся в host byte order** (внутри конвертируется в network byte order). `tun_route_add_all`/`tun_route_del_all` проходят по связному списку `CFG_ROUTE_ENTRY` и вызывают `tun_route_add`/`tun_route_del` для каждого элемента.
**Root-права:** требуются для изменения системной таблицы маршрутизации.
## 3. API
### Функции
- **`tun_route_add(ifindex, ifname, network, prefix_len)`** — добавляет маршрут к подсети `network/prefix_len` через интерфейс `ifindex`. Пытается использовать нативный механизм платформы, при неудаче — fallback на shell-команду
- **`tun_route_del(ifindex, ifname, network, prefix_len)`** — удаляет аналогичный маршрут
- **`tun_route_add_all(ifindex, ifname, routes)`** — массовое добавление маршрутов из связного списка `CFG_ROUTE_ENTRY`. Возвращает количество успешно добавленных
- **`tun_route_del_all(ifindex, ifname, routes)`** — массовое удаление. Возвращает количество удалённых
- **`tun_route_flush(ifname)`** — удаление всех маршрутов, привязанных к интерфейсу `ifname`, без привязки к конфигу
### Платформенные механизмы
**Linux:**
1. Netlink socket (`AF_NETLINK`, `NETLINK_ROUTE`): сообщения `RTM_NEWROUTE`/`RTM_DELROUTE` с атрибутами `RTA_DST` (адрес сети) и `RTA_OIF` (индекс интерфейса). Ожидание ACK от ядра через `poll` с таймаутом 1с
2. Fallback: `ip route add/del <net>/<prefix> dev <ifname>` через `system()`
3. `tun_route_flush`: `ip route flush dev <ifname>`, при неудаче — парсинг `ip route show` и удаление по одному
**Windows:**
1. `netsh interface ip add/delete route` через `system()` (первичный метод)
2. Fallback: IP Helper API — `CreateIpForwardEntry`/`DeleteIpForwardEntry` с заполнением `MIB_IPFORWARDROW`
3. `tun_route_flush`: `GetIpForwardTable` → перебор записей по `dwForwardIfIndex` → `DeleteIpForwardEntry`
**BSD/macOS:**
1. Routing socket (`PF_ROUTE`, `SOCK_RAW`): сообщения `RTM_ADD`/`RTM_DELETE`, адреса RTA_DST + RTA_NETMASK + RTA_IFP (sockaddr_dl)
2. Fallback: `route add/delete -net <net>/<prefix> -interface <ifname>` через `system()`
3. `tun_route_flush`: `route -n show -interface <ifname>` → парсинг awk → `route delete`

50
src/utun_doc.md

@ -0,0 +1,50 @@
# utun.c — Главная точка входа
## 1. Назначение
Точка входа процесса uTun. Парсинг CLI, запуск в режиме демона, создание и жизненный цикл корневого инстанса `UTUN_INSTANCE`, главный цикл обработки событий `mainloop()`, обработка сигналов (graceful shutdown/reload).
## 2. Как пользоваться
```bash
utun -f -c myconfig.conf -p /var/run/utun.pid -l /var/log/utun.log -d "etcp=debug"
```
- `-f` — foreground (без демонизации)
- `-c` — путь к конфигу (по умолчанию `utun.conf`)
- `-p` — PID-файл (по умолчанию `/var/run/utun.pid`, Windows — нет)
- `-l` — лог-файл (по умолчанию `utun.log`)
- `-d` — отладочная конфигурация (формат: `категория=уровень,...`)
- `-h` — справка
**Порядок запуска:**
1. Парсинг аргументов → инициализация системы отладки
2. `uasync_create()` → `utun_instance_create()` → `utun_instance_init()`
3. Демонизация (`fork`+`setsid`) если не `-f`
4. Запись PID-файла
5. `mainloop`: `uasync_poll(ua, 100)` в цикле пока `instance->running`
6. По `SIGTERM`/`SIGINT`: `g_shutdown=1` → выход из цикла → `utun_instance_destroy()` → `uasync_destroy()` → `u_report_unfreed_blocks()`
7. По `SIGHUP`: `g_reload=1` → `utun_instance_reload()` (селективный или полный)
## 3. API
### Функции
| Функция | Описание |
|---------|----------|
| `parse_args(argc, argv, args)` | Разбор CLI-аргументов через `getopt_long`, заполняет `cmd_args_t` |
| `print_usage(progname)` | Вывод справки по использованию |
| `write_pidfile(pidfile)` | Запись PID текущего процесса в файл |
| `remove_pidfile(pidfile)` | Удаление PID-файла при завершении |
| `daemonize()` | Демонизация: `fork()`→родитель выходит, `setsid()`, перенаправление fd 0/1/2 в `/dev/null` |
| `open_logfile(logfile)` | Открытие лог-файла с line-buffering |
| `signal_handler(sig)` | Обработчик: `SIGINT`/`SIGTERM`→shutdown, `SIGHUP`→reload, вызов `uasync_wakeup()` |
| `main(argc, argv)` | Главная функция: инициализация, демонизация, mainloop, cleanup |
### Структуры
| Структура | Описание |
|-----------|----------|
| `cmd_args_t` | Аргументы командной строки: config_file, pid_file, log_file, debug_config, foreground, help |
### Глобальные переменные
| Переменная | Описание |
|------------|----------|
| `g_shutdown` (sig_atomic_t) | Флаг завершения, выставляется из signal handler |
| `g_reload` (sig_atomic_t) | Флаг перезагрузки конфига по SIGHUP |
| `main_ua` | Указатель на главный UASYNC (для signal handler) |

116
src/utun_instance_doc.md

@ -0,0 +1,116 @@
# utun_instance — Корневой инстанс (центральный оркестратор)
## 1. Назначение
`UTUN_INSTANCE` — **центральная структура всего процесса uTun**. Владеет всеми подсистемами: TUN, таблица маршрутов, BGP-группы, ETCP-сокеты/соединения, control-сервер, message transport, firewall, NAT, TCP-прокси, memory pools, NTP, синхронизация БД, менеджер соединений.
Это **единственная точка входа для инициализации и завершения** всех компонентов. Порядок инициализации и destroy строго определён — от низкоуровневых подсистем к высокоуровневым и обратно.
## 2. Как пользоваться
### Создание
```c
struct UASYNC* ua = uasync_create();
struct UTUN_INSTANCE* inst = utun_instance_create(ua, "utun.conf"); // из файла
// или
struct UTUN_INSTANCE* inst = utun_instance_create_from_config(ua, config); // из готовой структуры
// или
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config_text); // из строки (тесты)
```
### Инициализация (оркестрация)
```c
utun_instance_init(inst); // запускает всю цепочку инициализации
```
`utun_instance_init()` **не вызывает** `utun_instance_create()` — create только аллоцирует память и загружает конфиг. `init` запускает все подсистемы в строгом порядке:
1. `db_sync_init` — распределённая БД с репликацией
2. `routing_set_tun` — привязка TUN к таблице маршрутов
3. `nat_transport_init` — NAT (если включён в конфиге)
4. `init_connections` — ETCP-сокеты, клиентские подключения, handshake
5. `control_server_init` — сервер мониторинга (etcpmon)
6. `msg_transport_init` — IPC-транспорт сообщений (чат, команды)
7. `ntp_time_init` + `ntp_node_time_init` — синхронизация времени
### Основной цикл
```c
inst->running = 1;
while (inst->running) {
uasync_poll(inst->ua, 100);
}
```
### Остановка и destroy
```c
utun_instance_stop(inst); // running=0, wakeup
utun_instance_destroy(inst); // полный cleanup в обратном порядке
```
`destroy` гарантирует порядок: NTP→msg_transport→control→ETCP sockets→db_sync→connections→pings→TUN→tcp_proxy→routing→NAT→etcp_router→conn_mgr→BGP→fw→stcp→networks→config→pools. **uasync не уничтожается** — вызывающий код должен сам вызвать `uasync_destroy()`.
### Reload (SIGHUP)
```c
inst = utun_instance_reload(inst, ua, "utun.conf");
```
Сравнивает старый и новый конфиг:
- Если изменились ключи/node_id/tun_ifname → **полный перезапуск** (destroy + create + init)
- Иначе — **селективное обновление**: только изменённые/добавленные/удалённые сокеты, клиенты, линки. Неизменённые не трогаются (без close, без сброса таймеров).
## 3. API
### Структура `UTUN_INSTANCE` — ключевые поля
| Поле | Тип | Назначение |
|------|-----|------------|
| `name` | `char[16]` | Имя инстанса из конфига |
| `config` | `struct utun_config*` | Полный разобранный конфиг |
| `node_id` | `uint64_t` | Идентификатор узла (из конфига или derived из privkey) |
| `my_keys` | `struct SC_MYKEYS` | Пара ключей X25519 (pub/priv) |
| `my_ed25519_pubkey` / `_privkey` | `uint8_t[32]` | Ed25519 ключи (для подписи) |
| `ua` | `struct UASYNC*` | Главный event loop (один на поток) |
| `running` | `int` | Флаг работы главного цикла |
| `tun` | `struct tun_if*` | TUN-интерфейс |
| `route_subnets` | `struct CFG_ROUTE_ENTRY*` | Маршруты для добавления/удаления системных роутов |
| `rt` | `struct ROUTE_TABLE*` | Таблица маршрутов |
| `topo_groups` | `struct TOPO_GROUPS*` | BGP-подобный обмен маршрутами |
| `connections` | `struct ll_queue*` | Очередь всех ETCP-соединений (индекс по peer_node_id) |
| `etcp_sockets` | `struct ETCP_SOCKET*` | Связный список UDP-сокетов |
| `stcp_server` | `struct stcp_server*` | STCP TCP-сервер |
| `data_pool` | `struct memory_pool*` | Пул для данных пакетов (payload) |
| `pkt_pool` | `struct memory_pool*` | Пул для `struct ETCP_DGRAM` |
| `ack_pool` | `struct memory_pool*` | Пул для `struct ACK_PACKET` |
| `control_srv` | `struct control_server*` | Сервер мониторинга (etcpmon backend) |
| `msg_t` | `struct msg_transport*` | IPC-транспорт сообщений |
| `fw` | `struct firewall_ctx` | Контекст файрвола |
| `nat` / `nat_tr` | `struct eim_nat_ctx` / `nat_transport_ctx` | EIM NAT + транспорт |
| `tcp_proxy_client` | `struct tcp_proxy_client*` | TCP-прокси клиент (опционально) |
| `tcp_proxy_server` | `struct tcp_proxy_server` | TCP-прокси сервер (exit node) |
| `router_bindings` / `router_conns` | | Привязки и соединения ETCP-роутера |
| `conn_mgr` | `struct CONN_MGR*` | Менеджер соединений (может быть NULL) |
| `db_sync` | `struct DB_SYNC*` | Распределённая синхронизация БД (может быть NULL) |
| `networks` | `struct ll_queue*` | Очередь сетей (NETWORK_ENTRY, индекс по 56-bit id) |
| `pending_connects` | `struct ETCP_CONNECT*` | Ожидающие фоновые подключения |
| `ntp` / `ntp_node` | `struct NTP_TIME` / `NTP_NODE_TIME` | Синхронизация времени |
| `stats_dir` | `char[512]` | Путь `<config_dir>/stats/` для файлов метрик |
| `next_socket_id` | `uint8_t` | Счётчик уникальных ID сокетов (0–255) |
| `socket_init_status` | `int` | Статус инициализации сокетов: 0=OK, 1=частично, -1=ошибка |
| `routed_packets` / `dropped_packets` | `uint64_t` | Счётчики маршрутизированных/отброшенных пакетов |
### Основные функции
| Функция | Описание |
|---------|----------|
| `utun_instance_create(ua, config_file)` | Загрузка конфига, аллокация инстанса, вызов `instance_init_common()`. Возвращает готовый к `init` инстанс или NULL |
| `utun_instance_create_from_config(ua, config)` | Создание из уже разобранного конфига (владение передаётся инстансу) |
| `utun_instance_create_from_str(ua, config_text)` | Создание из строки конфига (временный файл → parse → delete). Для тестов |
| `utun_instance_init(instance)` | **Основной оркестратор инициализации**. Запускает все подсистемы в правильном порядке, устанавливает `running=1` |
| `utun_instance_destroy(instance)` | **Полный cleanup** всех ресурсов в обратном порядке зависимостей |
| `utun_instance_stop(instance)` | Установка `running=0` + `uasync_wakeup()` для выхода из mainloop |
| `utun_instance_reload(instance, ua, config_file)` | Перезагрузка конфига по SIGHUP: полная при изменении ключей/TUN, иначе селективная |
| `utun_instance_set_tun_init_enabled(enabled)` | Глобальный флаг: включать инициализацию TUN или нет (для тестов без TUN) |
| `utun_instance_diagnose_leaks(instance, phase)` | Диагностика: подсчёт ETCP-сокетов/соединений/линков, проверка утечек пулов и TUN |
### Внутренние (static)
| Функция | Описание |
|---------|----------|
| `instance_init_common(instance, ua, config)` | Общая инициализация для всех create-функций: ключи, node_id, networks, memory pools, routing, TUN, сокеты, BGP, conn_mgr, firewall, etcp_router, routing_bind, tcp_proxy_server/client |
| `local_sockaddr_equal(a, b)` | Сравнение двух sockaddr_storage (IPv4/IPv6) — используется в reload |

50
tools/bping/bping_doc.md

@ -0,0 +1,50 @@
# bping — Bandwidth Ping
## 1. Назначение
Утилита измерения пропускной способности и потерь канала через burst-ы ICMP Echo.
В отличие от обычного ping, отправляет пачками (burst) — до 10,000 пингов за раз на максимальной скорости
с последующим сбором ответов в течение 5 секунд. Позволяет оценить пропускную способность
и packet loss под нагрузкой.
## 2. Как пользоваться
```
bping [-s размер] [-p посылок] [-b пачек] [-i интервал] host
```
| Опция | По умолчанию | Описание |
|-------|-------------|----------|
| `-s N` или `-s MIN:MAX` | 56 | Размер данных (payload) ICMP в байтах. Если диапазон — случайный размер в нём |
| `-p N` | 10 | Количество пингов в одной пачке (макс. 10000) |
| `-b N` | 0 (∞) | Число пачек. 0 — бесконечно (Ctrl+C для остановки) |
| `-i N` | 1.0 | Интервал между пачками в секундах (можно 0.001, 0.05 и т.д.) |
Требует **root** или **cap_net_raw** (raw sockets).
**Сборка:** `make` в `tools/bping/`, установка: `make install`.
**Примеры:**
```bash
sudo bping 8.8.8.8
bping -s 1472 -p 200 -i 0.05 1.1.1.1
bping -s 100:600 -p 500 -i 0.01 192.168.1.1
```
**Вывод:** per-packet RTT (размер, IP, ICMP seq, TTL, время) + per-burst сводка:
```
--- 200 transmitted, 195 received, 2% loss ---
```
## 3. Ключевые детали
- **SOCK_RAW + IPPROTO_ICMP** — raw сокет, требует привилегий
- **Без libpcap** — sendto/recvfrom напрямую, без сторонних библиотек
- ICMP Echo Request, идентификатор = `getpid() & 0xFFFF`, последовательные seq
- Данные заполняются `0x61` ('a'), как в стандартном ping
- Контрольная сумма ICMP — `in_cksum()`, своя реализация (RFC 1071)
- Приём: `select()` с таймаутом 1с в цикле до 5 секунд на пачку
- Ответы сопоставляются по ICMP ID + seq через `seq_list[]` (массив, не хеш-таблица)
- Межburst-интервал через `nanosleep()`, поддержка долей секунды
- Отдельный Makefile, не включён в autotools-сборку проекта
- SIGINT (Ctrl+C) — печать «⛔ bping остановлен» и выход

216
tools/chatgui/chatgui_doc.md

@ -0,0 +1,216 @@
# chatgui
## 1. Назначение
Десктопный GUI-чат (Telegram-подобный) со встроенным P2P uTun-узлом. Приложение объединяет полнофункциональный интерфейс обмена сообщениями и децентрализованный транспортный слой в едином процессе. Рассчитан на работу без центральных серверов — вся маршрутизация, синхронизация и доставка сообщений идут напрямую между узлами по ETCP-протоколу.
## 2. Архитектура
### 2.1. Уровни приложения
```
┌──────────────────────────────────────┐
│ GUI (Qt 6/5 Widgets, C++20) │ поток GUI
│ ├─ MainWindow (QSplitter) │
│ ├─ ChannelList / MessageList │
│ ├─ MessageDelegate (бабблы+реакции) │
│ ├─ EmojiPanel (статика+анимация) │
│ └─ LottieIcon / AnimTimer │
├──────────────────────────────────────┤
│ GuiBridge (Qt signals ↔ uasync) │ очередь событий
│ gui_bridge_post / uasync_post │
├──────────────────────────────────────┤
│ Транспорт (C, поток uasync) │ поток uasync
│ ├─ UtunNode (std::thread + UTUN_INSTANCE)│
│ ├─ ChatCore (отправка, БД, invite) │
│ ├─ ChatSync (P2P синхронизация) │
│ ├─ MemberSync (мемберы через Merkle)│
│ └─ MerkleSync (Merkle-дерево, 5 ур.)│
├──────────────────────────────────────┤
│ libutun (C, статическая библиотека) │ uasync
│ ├─ uasync (event loop, таймеры) │
│ ├─ ETCP (надежная доставка, ACK) │
│ ├─ SecureChannel (X25519+AES-CCM) │
│ ├─ conn_mgr (управление подключ.) │
│ ├─ topo_node/topo_group (BGP) │
│ └─ ntp_time (синхронизация времени) │
├──────────────────────────────────────┤
│ SQLite3 (WAL mode) │ чтение+запись
│ ├─ DbManager (C++, read-only для GUI)│
│ └─ chat_core (C, write из uasync) │
└──────────────────────────────────────┘
```
### 2.2. Потоковая модель
Приложение работает в двух потоках:
- **Поток GUI** — QApplication, все виджеты, отрисовка, QTimer для анимаций (AnimTimer 30fps). Только *чтение* из SQLite через DbManager.
- **Поток uasync** — UTUN_INSTANCE в выделенном std::thread. Вся сеть, криптография, ETCP, синхронизация, *запись* в SQLite. Один uasync-экземпляр на поток (правило u_async).
**Мост между потоками (gui_bridge):**
- **uasync → GUI:** `gui_bridge_post(event_type, data, len)` — сериализованные события в Qt main event loop через `QMetaObject::invokeMethod` с `Qt::QueuedConnection`. 10 типов событий: MSG_RECEIVED, CONNECT_RESULT, NEW_PEER, CHANNEL_UPDATED, MEMBERS_CHANGED, MY_NODE_ID, AUTO_CONNECT_STATUS, CHANNEL_PEERS_ONLINE, DB_READY, STATUS_REFRESH.
- **GUI → uasync:** `gui_bridge_post_uasync_fn(fn, arg)` — выполнение функции в uasync-потоке через `uasync_post`. Используется для отправки сообщений, создания каналов, invite-подключений.
Всё взаимодействие с БД (INSERT/UPDATE/DELETE) выполняется только из uasync-потока. GUI читает через DbManager (read-only), синхронизация не требуется — SQLite в WAL-режиме обеспечивает одновременное чтение.
## 3. Ключевые компоненты
### 3.1. GUI-слой
**MainWindow** (`src/mainwindow.h`) — главное окно 900×600. Горизонтальный QSplitter из трёх панелей:
- **ChannelList** — список каналов (QListView + QStandardItemModel), контекстное меню (invite, join, create group, settings).
- **MessageList** — список сообщений канала (ChatView + QStandardItemModel), поле ввода InputBar, EmojiPanel.
- **AccountList** — список участников канала (MemberListModel), онлайн-статус, имена.
Системный трей: QSystemTrayIcon + QMenu (показать/скрыть/выход). Закрытие окна сворачивает в трей.
**ChatView** (`src/chatview.h`) — QListView с фоновым изображением и drag-to-select. При движении >5px от точки нажатия стартует выделение сообщений. Ctrl+C копирует: для одного — выделенный текст, для нескольких — `[time] author: text`. Сигнал `hoveredIndexChanged` используется для активации анимаций эмодзи в MessageList.
**MessageList / MessageDelegate** — баббл-интерфейс сообщений:
- Аватары (32×32, цвет зависит от node_id), бабблы со скруглёнными углами (r=10px) и хвостиком.
- Цитаты: цветная полоса + мини-аватар + автор + текст.
- Реакции (эмодзи) в status bar под бабблом.
- Inline-анимированные эмодзи: при hover над сообщением — перебор кадров из LottieIcon, отрисовка поверх текста через QTextLayout.
- Растягивающийся InputBar (QTextEdit, Enter — отправка, Shift+Enter — перевод строки).
**EmojiPanel** (`src/emojipanel.h`) — QTabWidget с категориями (смайлики, жесты, символы, анимации). Каждая категория — QGridLayout из EmojiButton (QPushButton без Q_OBJECT, hover-анимация через per-widget QTimer). Статические эмодзи — Unicode символы. Анимированные — TGS-файлы, рендерятся в EmojiButton::paintEvent.
**LottieIcon** (`src/lottieicon.h`) — загрузка и рендер TGS-анимаций:
1. Gzip-декомпрессия через zlib (`inflateInit2` с `16+MAX_WBITS`)
2. Парсинг Lottie JSON через rlottie C API (`lottie_animation_from_data`)
3. Рендер кадра: `lottie_animation_render` → ARGB32 буфер → QImage
5 встроенных TGS-анимаций (вшиты в бинарник через chatgui.qrc): sparkles, white_flag, greeting, stop_hand, robot.
**AnimTimer** (`src/animtimer.h`) — синглтон-таймер 30fps с refcount-механизмом:
- `activate()` / `deactivate()` увеличивают/уменьшают счётчик ссылок.
- Срабатывает только когда есть активные анимации в видимых сообщениях.
- Сигнал `ticked` обновляет кадры всех зарегистрированных LottieIcon.
**Invite-ссылки** (`src/invite_link.h`) — кодирование/декодирование приглашений в формат `utun://...` (base64, содержит channel_id, pubkey, список адресов). QR-коды через zxing-cpp.
### 3.2. Транспортный слой
**UtunNode** (`transport/utun_node.h`) — C++ обёртка над UTUN_INSTANCE:
- Запускает выделенный std::thread с `runLoop()` — создаёт UTUN_INSTANCE, вызывает `utun_instance_start()`, входит в uasync event loop.
- Приём сообщений: ETCP callback → `QByteArray` → сигнал `messageReceived(uint64_t src, QByteArray data)`.
- Отправка: `send(dstNodeId, data)` через ETCP поверх экземпляра uTun.
- Конфигурация: NodeConfig (INI-файл), генерация X25519/Ed25519 ключей при первом запуске.
- Включает отладку NTP-синхронизации для временных меток сообщений.
**NodeConfig** (`transport/node_config.h`) — управление INI-конфигом uTun-узла:
- Генерация ключей (X25519 + Ed25519), сохранение/загрузка.
- Секции `[server]` и `[client]` для подключения к другим узлам.
- `[gui]` секция: путь к БД, настройки отладки.
- ConfigUpdater — утилита для программного изменения конфиг-файлов (append/replace).
**ChatCore** (`transport/chat_core.h`) — центральный API чата в потоке uasync:
- `chat_core_submit_message` — отправка сообщения: сохранение в локальную БД (таблица `msg_<channel_id>`), рассылка онлайн-пирам через etcp_send.
- `chat_core_create_channel` — создание канала: генерация X25519/Ed25519 ключей канала, подпись Ed25519, запись в `channels`, членство владельца.
- `chat_core_connect_from_invite` — подключение к пиру из invite-ссылки.
- `chat_core_connect_auto` — авто-подключение к узлу из SQLite (topo_node), коллбэк с результатом.
- Работа с БД: цепочка хешей (chain hash = SHA256(prev_hash || timestamp || data_hash)), список каналов, список пиров.
**ChatSync** (`transport/chat_sync.h`) — P2P синхронизация каналов:
- ETCP-сервисы: `0x30` (chat_sync — синхронизация каналов), `0x31` (member_sync — синхронизация участников).
- Протокол сообщений: INIT_SYNC → INIT_RESP → SEND_DATA ↔ PUSH+ACK_PUSH → SYNC_DONE.
- Batch-загрузка: до 32 сообщений за запрос, sparse-синхронизация (до 16 диапазонов).
- Таймаут пира: 5 секунд.
- Auto-connect: параллельное подключение до 10 узлов, останавливается при 3 успешных. Переключение группы при смене канала.
**MemberSync** (`transport/member_sync.h`) — синхронизация участников канала на основе Merkle-деревьев:
- Данные: таблицы `peers_<channel_id>`, `nodes`, `node_addresses`.
- Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || addrs).
- Автоматический пересчёт дерева при добавлении/удалении/обновлении мембера.
- Онлайн-статус: `member_sync_set_online(node_id, 0/1)`.
**MerkleSync** (`transport/merkle_sync.h`) — универсальный протокол синхронизации на Merkle-деревьях:
- 5-уровневое префиксное дерево над 64-битными ключами (node_id). 32 бакета на уровень (по 5 бит).
- SHA256-хеши бакетов. Два пира обмениваются хешами, находят различающиеся бакеты, передают только их.
- Wire-протокол: MSG_HASHES → MSG_REQUEST → MSG_BATCH, рекурсивно до совпадения хешей.
- Таймаут 10s, 3 ретрая. Фоновая проверка консистентности (`merkle_sync_bg_check`).
- Не зависит от типа данных — потребитель предоставляет три коллбэка (update_bucket_hash, get_items, apply_items). member_sync адаптирует это под мемберов каналов.
### 3.3. База данных
**DbManager** (`db/db_manager.h`) — C++/Qt обёртка над SQLite3 (read-only для GUI-потока):
- **Схема:** `channels` (каналы с ключами X25519/Ed25519, подписи), `msg_<channel_id>` (per-channel таблицы сообщений с chain_hash), `peers_<channel_id>` (участники), `nodes` (узлы), `node_addresses`, `accounts`, `ui_state`, `merkle_tree_hash`.
- Чтение: `getChannels()`, `getMessages()`, `getChannelMembers()`, `loadNodeInfo()`, `getAccounts()`.
- Методы для GUI: список каналов, сообщения с пагинацией, участники, онлайн-статус.
- Sync-операции (курсоры): `syncListChannels()`, `syncCount()`, `syncChainHashAt()`, `syncCursorOpen/Next/Close()`.
- Проверка целостности: `integrityCheckQuick()` (PRAGMA quick_check), `integrityCheckFull()` (PRAGMA integrity_check), `integrityOptimize()` (PRAGMA optimize).
## 4. Сборка
**Система сборки:** CMake 3.16+
**Стандарты:** C++20 (GUI), C99 (транспорт/libutun)
**Зависимости:**
- Qt 6 (Widgets + Network) или Qt 5.15+
- rlottie (анимированные эмодзи)
- zlib (gzip-декомпрессия TGS)
- OpenSSL (X25519, Ed25519, SHA256, AES-CCM)
- zxing-cpp (QR-коды, встроен как submodule)
**Структура сборки:**
- `libutun/` — статическая библиотека uTun: все исходники из `src/` (кроме utun.c) + lib/ (uasync, ll_queue, memory_pool, etc) + транспорт (chat_sync, member_sync, merkle_sync)
- `chatgui` — финальный бинарник: GUI-исходники + транспортные C++/C файлы + SQLite3 amalgamation
- Платформенный TUN: `tun_linux.c` / `tun_freebsd.c` / `tun_windows.c`
```bash
mkdir -p build && cd build
cmake .. && make -j4
./chatgui
```
## 5. Файловая структура
```
tools/chatgui/
├── CMakeLists.txt # проект CMake
├── build.sh / build.bat # скрипты сборки
├── src/ # GUI-слой (Qt C++)
│ ├── main.cpp # точка входа, QApplication
│ ├── mainwindow.cpp # главное окно, трей
│ ├── channellist.cpp # список каналов
│ ├── chatview.cpp # QListView с фоном и drag-to-select
│ ├── messagelist.cpp # модель сообщений, инициализация анимаций
│ ├── messagedelegate.cpp # отрисовка бабблов, цитат, реакций, эмодзи
│ ├── inputbar.cpp # поле ввода + кнопка эмодзи
│ ├── emojipanel.cpp # QTabWidget с категориями эмодзи
│ ├── emojitabbar.cpp # кастомный QTabBar
│ ├── emoji.cpp # данные эмодзи, builtinEmojiSet()
│ ├── lottieicon.cpp # загрузка TGS, рендер через rlottie
│ ├── animtimer.cpp # синглтон-таймер 30fps с refcount
│ ├── accountlist.cpp # список участников канала
│ ├── memberlistmodel.cpp # модель для AccountList
│ ├── invite_link.cpp # кодирование/декодирование utun:// ссылок
│ ├── invitedialog.cpp # диалог приглашения
│ ├── joindialog.cpp # диалог входа по invite-ссылке
│ ├── settingsdialog.cpp # окно настроек
│ ├── networksettingspage.cpp # страница сетевых настроек
│ ├── databasesettingspage.cpp # настройки БД
│ ├── statuspage.cpp # страница статуса
│ ├── creategroupdialog.cpp # создание группы
│ └── qrcode_utils.cpp # генерация QR-кодов
├── transport/ # транспортный слой (C/C++)
│ ├── utun_node.cpp # C++ обёртка UTUN_INSTANCE в std::thread
│ ├── node_config.cpp # INI-конфиг узла (ключи, серверы, клиенты)
│ ├── config_updater.cpp # утилита редактирования INI-файлов
│ ├── gui_bridge_impl.cpp # мост uasync↔Qt (10 типов событий)
│ ├── chat_core.c # центральный API чата (отправка, БД, invite)
│ ├── chat_sync.c # P2P синхронизация каналов (ETCP 0x30)
│ ├── member_sync.c # синхронизация мемберов через Merkle
│ └── merkle_sync.c # универсальный Merkle-протокол синхронизации
├── db/ # база данных
│ ├── db_manager.h/cpp # C++/Qt обёртка SQLite3 (read-only для GUI)
│ └── sqlite3.c/h # SQLite3 amalgamation (из ../../lib/)
├── libutun/ # сборка статической библиотеки uTun
│ └── CMakeLists.txt # uasync + utun = все .c из lib/ и src/
├── resources/ # ресурсы
│ ├── chatgui.qrc # Qt resource file (TGS вшиты в бинарник)
│ └── animations/ # TGS-анимации (5 шт.)
└── doc/ # документация
├── desc.txt # описание схемы БД
└── db_schema.md # детальная схема таблиц SQLite
```

120
tools/etcpmon/etcpmon_doc.md

@ -0,0 +1,120 @@
# etcpmon
## 1. Назначение
Windows GUI монитор для ETCP соединений uTun в реальном времени (100ms refresh). Подключается по TCP к control_server внутри uTun и отображает метрики соединений, линков, TUN-интерфейса, роутера и дебаг-уровней. Позволяет менять debug levels на лету через GUI.
## 2. Как пользоваться
### Типовой сценарий
1. В конфиге uTun (`utun.conf`): `control_ip=127.0.0.1`, `control_port=9090`
2. Запустить uTun → он поднимает TCP control server на указанном порту
3. Запустить `etcpmon.exe` (Windows, MinGW/MSYS2 сборка)
4. Ввести IP:порт сервера (по умолчанию `192.168.40.1:9091`), нажать Connect
5. Выбрать соединение из списка Connections — начнут поступать метрики
### Что отображает
**ETCP Connection:**
- RTT (last / avg10 / avg100, в 0.1ms), jitter, bytes sent, retransmissions, ACKs, unacked bytes, optimal inflight, links count
- RX/TX дубликаты, таймеры (retrans/ack_resp), WaitAck состояние (suspended/cb/timer)
- ID Sequence: next_tx_id, last_rx_id, last_delivered_id, rx_ack_till
- Счётчики ошибок: reinit, reset, pkt_format_errors
**TUN / Routing:**
- Bytes/packets read/written, ошибки чтения/записи
- Routed/dropped пакеты, TUN очереди (InQ/OutQ packets/bytes)
- Таблица маршрутов: total, local, learned, BGP senders, BGP nodes
**Links:**
- Статус (UP/DOWN), encrypt/decrypt/send/recv ошибки
- Total encrypted/decrypted bytes, bandwidth (Kbps), NAT changes
- RTT (last/avg10), transmit time, keepalive counters (sent/recv)
- Inflight bytes/packets/limit, таймеры (init/keepalive/shaper)
**BBR per link:**
- mode (STARTUP/DRAIN/PROBE_BW/PROBE_RTT), cycle idx, full_bw/loss_in_round флаги
- pacing_rate (bytes/sec), min_rtt (us), bw_hi/bw_lo (bytes/sec), inflight_hi/lo, pacing_gain
**Router Congestion (агрегированные):**
- total_conns, total_inflight, total_send_q, total_recv_q
- pkts sent/recv, send err, ACK sent/recv, dup_dropped, oob_dropped, stale_ack, sign_fail
- RTT max/avg, jitter max, minRTT min
**Графики BBR (scrolling canvas, 860 сэмплов, 1px=1sample):**
- Inflight Bytes, Cwnd (inflight_lim), minRTT, bwHi, Pacing Rate, bwLo, inflight Hi, BBR Mode
- 8 каналов с индивидуальными галочками включения
- Вертикальная линия курсора с показом значений в блоках под графиком
**Очереди (Queues & Errors):**
- InQ, InSend, WaitAck, AckQ, RecvQ, OutQ — bytes/packets
- Нормализатор: input/output pkts/bytes, alloc/logic errors, frag_size, data_ptr/size, cumulative totals
- ACK Debug: hit_inf, hit_sndq, miss, link_wait
- Системные ресурсы: active_timeouts, busy_memory_blocks
**Debug Levels (правая колонка):**
- Глобальный уровень: NONE/ERROR/WARN/INFO/DEBUG/TRACE (радиокнопки)
- Per-category уровни с метками категорий
- Radio-кнопки отправляют CMD_SET_DEBUG_CONFIG на сервер при клике
**Action:**
- Текстовое поле + кнопка Send — отправка произвольной команды (CMD_ACTION)
- Результат открывается в отдельном окне с моноширинным шрифтом Consolas
### Сборка
```cmd
cd tools\etcpmon
build.bat # через MSYS2 UCRT64 (рекомендуется)
```
```bash
bash build.sh # Unix/MinGW
make # MinGW Makefile
```
Серверная часть (control_server) встроена в utun, собирается стандартным `./configure && make`.
### Горячие клавиши
- Tab в поле Server/Port — переключение между полями
- Enter в поле Server/Port — Connect/Disconnect
- Enter в поле Action — отправка команды
## 3. API / Структура
### Компоненты
| Файл | Назначение |
|------|-----------|
| `etcpmon_main.c` | WinMain: инициализация GUI, создание окна, message loop |
| `etcpmon_client.h/c` | TCP клиент (Winsock2): connect, send/recv, парсинг протокола, history, pending requests с коллбэками |
| `etcpmon_gui.h/c` | WinAPI GUI: окно 1000×1400, все контролы (~200 edit-полей, списки, кнопки, radio, graph), тултипы |
| `etcpmon_graph.h/c` | Real-time график: 8 каналов, off-screen буфер, пересчёт min/max, курсор, обновление channel values |
| `etcpmon_protocol.h` | Бинарный протокол, общие структуры, shared между клиентом и сервером |
### Протокол (бинарный, TCP)
Формат: `[size:2][type:1][seq_id:1][payload...]`
**Клиент → Сервер:**
- `0x01` CMD_LIST_CONN — запрос списка соединений (без payload)
- `0x02` CMD_SELECT_CONN — выбор соединения: `peer_node_id` (uint64_t)
- `0x03` CMD_GET_METRICS — запрос метрик для выбранного соединения
- `0x04` CMD_DISCONNECT — отключение
- `0x05` CMD_LIST_SOCKETS — запрос списка локальных сокетов
- `0x06` CMD_ACTION — текстовая команда (32 байта)
- `0x07` CMD_GET_DEBUG_CONFIG — запрос текущих debug levels
- `0x08` CMD_SET_DEBUG_CONFIG — установка debug levels: `[global_level:1][cat_count:1][levels:N]`
- `0x09` CMD_SUBSCRIBE_NODES — подписка на изменения узлов (topo)
**Сервер → Клиент:**
- `0x81` RSP_CONN_LIST: `count(1) + N × {peer_node_id(8) + name(32)}`
- `0x82` RSP_METRICS: `etcp_metrics + tun_metrics + router_metrics + N × link_metrics`
- `0x83` RSP_SOCKET_LIST: `count(1) + N × socket_info`
- `0x84` RSP_DEBUG_CONFIG: `global_level(1) + cat_count(1) + names_csv(NUL-term) + levels[N]`
- `0x85` RSP_NODE_INFO — один узел (raw BGP_NODEINFO_PACKET)
- `0x86` RSP_NODE_INFO_END — конец списка узлов
- `0x88` RSP_NODE_REMOVED — узел удалён: `node_id(8)`
- `0x89` RSP_ACTION_RESULT — результат команды: `code(1) + text(NUL-term)`
- `0xFF` RSP_ERROR: `error_code(1) + message(NUL-term)`

29
tools/proxy/proxy_doc.md

@ -0,0 +1,29 @@
# proxy (тестовый инструмент)
## 1. Назначение
UDP прокси с симуляцией сетевых условий — вносит потери пакетов и задержки для тестирования поведения ETCP в нестабильной сети.
## 2. Как пользоваться
```
udp_proxy --listen [IP:]PORT --target IP:PORT [опции]
```
| Опция | Значение |
|---|---|
| `--listen [IP:]PORT` | Адрес прослушивания (IP опционально, по умолчанию 0.0.0.0) |
| `--target IP:PORT` | Куда пробрасывать трафик |
| `--loss-forward N` | Потери в прямом направлении, % (0–100, по умолчанию 0) |
| `--loss-back N` | Потери в обратном направлении, % |
| `--delay-forward A:B` | Задержка в прямом направлении, диапазон ms |
| `--delay-back A:B` | Задержка в обратном направлении, диапазон ms |
| `--help` | Справка |
Типовой сценарий: `udp_proxy --listen 0.0.0.0:12345 --target 8.8.8.8:53 --loss-forward 5 --delay-forward 10:50` — вставка между клиентом и сервером, наблюдение реакции ETCP на потери/задержки.
## 3. Ключевые детали
- Использует `u_async` как событийный цикл (epoll/poll)
- Per-client flow tracking: каждый уникальный клиент (IP:port) получает выделенный backend-сокет, поиск по хешу через линейный связный список (`udp_flow_t`)
- Раздельные настройки потерь и задержек для forward (клиент→сервер) и backward (сервер→клиент) направлений
- Задержка — случайная в заданном диапазоне (ms), потери — случайные с заданной вероятностью (`rand() % 100 < loss`)
- Сборка: `make` в `tools/proxy/`, линкуется с `libuasync.a` и `libpthread`
- Скрипт `proxy.sh` — быстрый запуск с преднастроенными адресами для локального тестирования
Loading…
Cancel
Save