72 changed files with 8908 additions and 2 deletions
@ -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> |
||||||
|
``` |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
Задача сделать полное архитектурное описание проекта. |
||||||
|
Как делаем |
||||||
|
|
||||||
|
1. определяем список проектов. продумываем последовательность разбора проектов - зависимости - что от чего зависит, что лучше разбирать в начале. |
||||||
|
|
||||||
|
Далее по каждому проекту: |
||||||
|
|
||||||
|
2. разбиваешь весь проект на логические модули. |
||||||
|
поручаешь субагентам (параллельно запускаешь до 8 потоков), балансируй нагрузку на агентов (по объему кода): если модули короткие - можно 2-3 простых одному субагенту. |
||||||
|
- проанализировать логику работы модуля |
||||||
|
- понять как этим пользоваться с позиции пользователя (ты - архитектор и пользователь - типовой сценарий использования модуля с точки зрения удобства и лёгкости использования). |
||||||
|
какие есть потенциальные проблемные места, можно ли пользоваться в многопоточной архитектуре, можно ли в асинхронной архитектуре, можно ли делать close из callback (если есть коллбэки и модулю асинхронный). какие есть еще нюансы и ограничения. |
||||||
|
- создать (или обновить если есть) описание работы модуля - файл рядом с исходниками называется [module_name]_doc.md |
||||||
|
описание должно быть с точки зрения пользователя - в первую очередь как пользоваться и какие нюансы. понятным языком (если используется терминология специфичная для модуля - то должна быть понятна или пояснения). |
||||||
|
Схема описания: |
||||||
|
1. назначение модуля, зачем нужен, что делает |
||||||
|
2. как пользоваться, лучше типовой сценарий (с нюансами если есть), |
||||||
|
3. кратко пояснения по апи и нюансы если есть |
||||||
|
|
||||||
|
далее читаешь полностью все эти описания и формируешь итоговое описание проекта. |
||||||
|
Продумай какая структура документации лучше подходит, разбей на главы. |
||||||
|
И сделай общее описние проекта (user manual). |
||||||
|
|
||||||
|
твоя задача на основе сформированной документации разобраться как работает utun. ключевое слово =- хорошо. хорошо понять все модули, их взаимодействое, возможности, облести применимости и |
||||||
|
ограничения. понять архитектурно как работает проект собранный из этих модулей. и только после этого написать документацию. |
||||||
@ -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 = нет) |
||||||
@ -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` |
||||||
@ -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). Только для отладки. | |
||||||
@ -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) | |
||||||
@ -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)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). | |
||||||
@ -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 | |
||||||
@ -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) масок в поддеревьях. |
||||||
@ -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)` — неподдерживаемый тип поля. |
||||||
@ -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) — размер выходного дайджеста в байтах. |
||||||
@ -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` | |
||||||
@ -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` |
||||||
@ -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 и отменить ожидание низкого порога | |
||||||
@ -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)` для упрощения арифметики кучи. |
||||||
@ -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`. |
||||||
@ -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-строку в бинарный массив | |
||||||
@ -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_*` |
||||||
@ -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 | Попыток локального сканирования | |
||||||
@ -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` | — | Отключение клиента. | |
||||||
@ -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)` — инкрементальное обновление. | |
||||||
@ -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-очистка неотправленных собственных записей | |
||||||
@ -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` |
||||||
@ -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) | |
||||||
@ -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) | |
||||||
@ -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, ...)` для отладки |
||||||
@ -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`) |
||||||
@ -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 | |
||||||
@ -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, ...)` |
||||||
@ -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 | |
||||||
@ -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` |
||||||
@ -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 = все недоступны | |
||||||
@ -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 | |
||||||
@ -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)` | Освобождение памяти, обнуление полей | |
||||||
@ -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` |
||||||
@ -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` (сетевой порядок байт) |
||||||
@ -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()` — запись в кольцевой буфер трассировки |
||||||
@ -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 |
||||||
@ -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-топологии. | |
||||||
@ -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` |
||||||
@ -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-синхронизации) |
||||||
@ -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 если была успешная синхронизация |
||||||
@ -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>` |
||||||
@ -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` — аллокация буферов и фрагментов |
||||||
@ -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-сокета). | |
||||||
@ -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` |
||||||
@ -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` — логирование |
||||||
@ -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 и структур | |
||||||
@ -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 для доставки приложению. | |
||||||
@ -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` |
||||||
@ -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() для отметок времени |
||||||
@ -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 на соседнюю запись | |
||||||
@ -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()` |
||||||
@ -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` | |
||||||
@ -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-провайдера). |
||||||
@ -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 + проверка целостности). |
||||||
@ -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` — освободить и саму структуру |
||||||
@ -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`** — коллбэк сервера при новом входящем соединении. |
||||||
@ -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`). |
||||||
@ -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-ится всем пирам. |
||||||
@ -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`) | |
||||||
@ -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`; собственных структур нет. |
||||||
@ -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-поток) |
||||||
@ -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` |
||||||
@ -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) | |
||||||
@ -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 | |
||||||
@ -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 остановлен» и выход |
||||||
@ -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 |
||||||
|
``` |
||||||
@ -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)` |
||||||
@ -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…
Reference in new issue