Files
utun2/ARCHITECTURE.md
T
Evgeny 852ce401e7 Добавлен group_id в etcp_router и topo_group, удалён msg_transport
- etcp_router: SVC_ROUTE_HDR (+8B), ETCP_ROUTER_CONN/TRANSIT_QUEUE хеши расширены под group_id
- Все caller'ы (proxy, routing, conn_mgr, nat, chatgui, tests) обновлены
- topo_group: глобальный реестр TOPO_NODE по node_id, ref_count=0 при создании
- BGP-обмен активируется для всех групп (а не только дефолтной)
- Протокол: group_id добавлен в WITHDRAW/TABLE_REQ/TABLE_COMPLETE/ERR_GROUP_MISMATCH
- Per-connection коллбэки заменены на instance-level conn_status
- msg_transport полностью удалён из проекта
57/57 тестов
2026-07-22 15:51:03 +03:00

1085 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# uTun — Архитектура системы
Документ для разработчиков. Описывает внутреннее устройство, дизайн-решения, взаимодействие модулей и поток данных.
---
## 1. Обзор и дизайн-решения
uTun — защищённый VPN-туннель с собственным надёжным транспортным протоколом ETCP. Ключевые архитектурные решения:
| Решение | Обоснование |
|---------|-------------|
| **Однопоточный async** | Нет гонок данных, нет оверхеда на мьютексы, детерминированное поведение. Один `UASYNC` на поток |
| **Очередь-ориентированный дизайн** | Все взаимодействия между модулями — через lock-free очереди `ll_queue`. Callback-и + backpressure |
| **2 уровня надёжности** | ETCP (per-connection, RTT-based retrans) + etcp_router (per-service, 300ms retrans/17 попыток, transit forwarding) |
| **Memory pools** | Предварительно выделенные пулы для hot-path объектов (пакеты, ACK, payload). Нет malloc в критическом пути |
| **Детерминированный node_id** | `SHA256(privkey) → 63-bit` — глобально уникальный идентификатор без централизованного распределения |
| **2-phase cleanup** | Отложенное освобождение ресурсов через `uasync_call_soon` — защита от UAF при вызове close из callback |
### Область применения
- Site-to-site и client-to-site VPN
- Mesh-сети с автоматическим обнаружением топологии (BGP)
- Exit-прокси: SOCKS5, HTTP CONNECT, TCP/UDP/ICMP туннелирование
- P2P чат с синхронизацией (chatgui)
- Тестирование сетевых протоколов (dummynet — эмуляция потерь/задержек)
### Ограничения
- Требует root для TUN и raw socket (ICMP proxy)
- Однопоточная модель: CPU-bound операции блокируют event loop
- Нет аппаратного ускорения криптографии (OpenSSL софтварный)
- Максимум 16 hop-ов в BGP (защита от петель)
- Максимум 65535 фрагментов на пакет (нормализатор)
---
## 2. Многослойный протокольный стек
```
┌─────────────────────────────────────────────────────────────────┐
│ Layer 4: Приложения │
│ routing (IP-форвардинг), proxy (SOCKS5/TCP/UDP/ICMP), chat, │
│ db_sync (P2P БД), NTP, NAT │
├─────────────────────────────────────────────────────────────────┤
│ Layer 3: etcp_router — сервисная маршрутизация │
│ seq-нумерация, дедупликация, реордеринг, ретрансмиссия, │
│ inflight control, transit forwarding (multi-hop), minRTT │
│ SVC_ROUTE_HDR: [cmd|dst|src|seq|svc_id|flags|timestamp] (25B) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2.5: LoadBalancer — multi-link балансировка │
│ выбор линка (min inflight, round-robin), traffic shaper │
│ (token bucket per-link), burst measurement │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: ETCP — надёжный транспорт │
│ inflight queues (send_q / wait_ack), ACK processing, │
│ retransmission (K1*rtt + K2*jitter), BBR congestion control, │
│ фрагментация (pkt_normalizer), keepalive, INIT handshake │
├─────────────────────────────────────────────────────────────────┤
│ Layer 1: secure_channel — криптография │
│ X25519 ECDH → AES-128-CCM (AEAD), Ed25519 signatures, │
│ AES-128-CTR (streaming), pubkey obfuscation (INIT/PING) │
│ Формат: nonce[13] || AES-CCM(plaintext || CRC32) || tag[16] │
├─────────────────────────────────────────────────────────────────┤
│ Layer 0: Транспорт │
│ UDP (основной), TCP/STCP (fallback), TUN device, │
│ кроссплатформенные абстракции (socket_compat, platform_compat) │
└─────────────────────────────────────────────────────────────────┘
```
### Почему два уровня надёжности?
**ETCP** обеспечивает надёжную доставку между двумя непосредственно соединёнными узлами (per-connection). Этого достаточно для прямого линка, но недостаточно когда трафик идёт через промежуточные узлы (transit).
**etcp_router** добавляет end-to-end надёжность поверх ETCP для multi-hop сценариев:
- Свой sequence number — не зависит от ETCP seq (который per-hop)
- Transit forwarding: промежуточный узел получает `SVC_ROUTE_HDR`, видит что `dst_node_id != local_node_id` и пересылает дальше
- Собственный retransmission таймер (300ms, 17 попыток) — не ждёт ETCP retrans (который срабатывает на каждом hop-е отдельно)
- Per-service изоляция: `(remote_node_id, svc_id)` кортеж — разные сервисы имеют независимые seq
---
## 3. Фундамент: однопоточный async (lib/)
### 3.1 UASYNC — событийный цикл
Центральный планировщик. Один инстанс на поток.
```
uasync_mainloop():
while (!stop):
timeout = timeout_heap_peek() // ближайший таймер
epoll_wait/poll(timeout) // ждём события на сокетах
process expired timers // сработавшие таймеры
process ready sockets // сработавшие сокеты
process call_soon queue // отложенные callback (FIFO)
process posted tasks // межпоточные callback
```
**Ключевые API:**
- `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` — таймер в 0.1ms единицах. Возвращает handle для отмены
- `uasync_add_socket(ua, fd, read_cb, write_cb, error_cb, arg)` — регистрация fd в epoll
- `uasync_call_soon(ua, arg, cb)` — отложенный вызов на следующей итерации (FIFO). Используется для 2-phase cleanup
- `uasync_post(ua, cb, arg)` — межпоточный вызов (thread-safe). Пробуждает epoll через wakeup pipe
- `uasync_memsync(ua)` — memory barrier для синхронизации памяти между потоками
**Правила:**
- Никаких sleep/usleep — только таймеры
- Нельзя использовать из нескольких потоков один UASYNC
- `uasync_post` — единственный способ вызвать код uTun из другого потока
### 3.2 ll_queue — lock-free очередь
Основа всего межмодульного взаимодействия. **Критические правила:**
1. **Callback правило:** `queue_set_callback(q, cb, arg)` → внутри cb обязательно: `queue_data_get(q)` → обработка → `queue_resume_callback(q)`. Без resume очередь навсегда блокируется.
2. **Backpressure:** `queue_set_threshold(q, max_packets, max_bytes)` + `queue_waiter_wait(q, &handle, cb, arg)`. Не забиваем очередь — добавляем только когда waiter сработал.
3. **Поиск по индексу:** `queue_data_put_with_index(q, entry)` — entry должен иметь ключ по смещению `index_offset` размером `index_size`. FNV-1a хеш.
4. **Память:** `queue_entry_new_from_pool(pool)` / `queue_entry_free(entry)` для entry. `queue_dgram_free(entry)` отдельно для dgram.
### 3.3 Memory pools
Три пула в UTUN_INSTANCE для hot-path:
| Пул | Размер блока | Назначение |
|-----|-------------|-----------|
| `data_pool` | `PACKET_DATA_SIZE` | Payload данные (фрагменты, recv_q, output_queue) |
| `pkt_pool` | `sizeof(ETCP_DGRAM) + PACKET_DATA_SIZE` | Исходящие wire-пакеты |
| `ack_pool` | `sizeof(ACK_PACKET)` | Записи ACK в ack_q |
Пулы кэшируют до 64 освобождённых блоков (singly-linked free list). При исчерпании — fallback на malloc. Канарейки для детекции переполнения.
### 3.4 Кроссплатформенность
| Абстракция | Linux | FreeBSD | Windows |
|-----------|-------|---------|---------|
| Event loop | epoll | poll | poll |
| TUN | /dev/net/tun (ioctl) | /dev/tun (ioctl) | Wintun DLL |
| Роутинг | netlink (RTM_NEWROUTE) | routing socket | CreateIpForwardEntry |
| Сокеты | socket_t = int | socket_t = int | socket_t = SOCKET |
| Random | /dev/urandom | /dev/urandom | BCryptGenRandom |
---
## 4. Сквозной путь данных
### 4.1 Отправка: TUN → сеть
```
Ядро пишет IP-пакет в /dev/tun
│
▼ (epoll: fd готов к чтению)
┌──────────────────────────────────────────────┐
│ tun_read_callback(fd, tun) │ tun_if.c
│ tun_platform_read() → сырой IP-пакет │
│ ll_entry = pool_alloc(tun->pool) │ префикс [0x00] (cmd byte)
│ queue_data_put(tun->output_queue, entry) │
└──────────────────────┬───────────────────────┘
│ queue callback
▼
┌──────────────────────────────────────────────┐
│ routing_pkt_from_tun_cb(q, instance) │ routing.c:207
│ queue_data_get() → entry │
│ route_pkt(instance, entry, SELF_NODE_ID) │
│ queue_resume_callback() │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ route_pkt(instance, entry, src_node_id) │ routing.c:53
│ extract_dst_ip(entry) → dst_ip │ парсинг IPv4 заголовка
│ route_lookup(rt, dst_ip) → ROUTE_ENTRY │ longest prefix match
│ if v_node_info == NULL (локальный): │
│ → queue_data_put(tun->input_queue) │ доставка себе
│ else (удалённый узел): │
│ → etcp_route_send(inst, node_id, entry) │ отправка через ETCP
└──────────────────────┬───────────────────────┘
│ если удалённый
▼
┌──────────────────────────────────────────────┐
│ etcp_route_send(inst, dst, entry) │ etcp_router.c
│ rconn = etcp_router_conn_get(dst, svc_id) │ per-(dst,svc) соединение
│ if rconn->inflight >= inflight_limit: │
│ → queue_data_put(rconn->send_q) │ backpressure
│ return │
│ router_send_one(rconn, data, len): │
│ SVC_ROUTE_HDR hdr = {cmd, dst, src, │ 25 байт
│ tx_seq++, svc_id, flags, timestamp} │
│ ll_entry = [hdr || payload] │
│ entry_copy = copy for inflight_q (retrans)│
│ etcp_send(conn, entry) │
│ start retrans_timer (300ms) │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ etcp_send(conn, entry) │ etcp_api.c
│ queue_data_put(conn->send_input_q, entry) │ == normalizer->input
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ pkt_normalizer — packer │ pkt_normalizer.c
│ waiter на etcp->input_queue │ backpressure: ждём пустую очередь
│ etcp_input_ready_cb(): │
│ pop from pn->input │
│ write [total_len:2 LE][data] in pn->data │ буфер аккумуляции
│ when buffer >= frag_size: │
│ pn_send_to_etcp() → ETCP_FRAGMENT │
│ queue_data_put(etcp->input_queue) │
│ pn_flush_cb(): flush partial buffer │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ etcp — inflight processing │ etcp.c
│ input_queue_cb(): │
│ pop ETCP_FRAGMENT from input_queue │
│ wrap in INFLIGHT_PACKET (inflight_pool) │
│ assign next_tx_id++ │
│ queue_data_put_with_index(input_send_q) │ хеш-индекс по seq
│ │
│ input_send_q_cb → process_send_queue(): │
│ loop: │
│ etcp_request_pkt(etcp): │
│ select link (loadbalancer) │
│ pop from input_send_q │
│ move to input_wait_ack │
│ build wire packet: │
│ [ACK section (0x01)] │ mandatory, always first
│ [optional: MEAS_RESP, TIMESTAMP, │
│ MEAS_TS, FILLER] │
│ [PAYLOAD (0x00): seq(4B) + data] │ always last
│ etcp_loadbalancer_send(dgram) │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ LoadBalancer → secure_channel → UDP │
│ select_link(): min inflight_bytes, RR ties │
│ check shaper (token bucket) │
│ etcp_encrypt_send(dgram): │
│ sc_encrypt(header(3) + data) │ AES-128-CCM
│ append CRC32 │
│ socket_sendto(sock, ...) │
│ update shaper_load_time_tb │
└──────────────────────────────────────────────┘
```
### 4.2 Приём: сеть → TUN
```
UDP socket recvfrom()
│
▼
┌──────────────────────────────────────────────┐
│ etcp_connections_read_callback_socket() │ etcp_connections.c
│ sc_decrypt() → проверка CRC32 │
│ if INIT_REQUEST: process handshake │
│ if INIT_RESPONSE: complete handshake │
│ if KEEPALIVE/PING/PONG: handle │
│ if data (ETCP sections): │
│ → etcp_conn_input(pkt) │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ etcp_conn_input(pkt) │ etcp.c
│ for each section in pkt: │
│ ACK (0x01): │
│ parse cumulative rx_ack_till │ всё до этого seq собрано
│ for each individual ACK: │
│ find in input_wait_ack by seq │
│ remove, update BBR (rate_sample) │
│ free data_pool memory │
│ advance tx state if window opens │
│ TIMESTAMP (0x06): │
│ compute RTT, update jitter │
│ PAYLOAD (0x00): │
│ extract seq from data[1..4] │
│ create ACK entry in ack_q │ будет piggybacked в ответ
│ if seq == last_delivered_id + 1: │
│ move directly to output_queue │
│ else: │
│ store in recv_q (hash by seq) │ OOSEQ буфер
│ etcp_output_try_assembly(): │
│ scan recv_q for contiguous seq │
│ move to output_queue │
└──────────────────────┬───────────────────────┘
│ output_queue callback
▼
┌──────────────────────────────────────────────┐
│ pkt_normalizer — unpacker │ pkt_normalizer.c
│ pn_unpacker_cb(): │
│ pop ETCP_FRAGMENT from output_queue │
│ read [total_len:2 LE] header │
│ accumulate in recvpart buffer │
│ when recvpart complete: │
│ queue_data_put(pn->output, packet) │
└──────────────────────┬───────────────────────┘
│ pn->output callback
▼
┌──────────────────────────────────────────────┐
│ etcp_int_recv(queue, conn) │ etcp_api.c
│ read cmd = data[0] │
│ dispatch: bindings->callbacks[cmd](conn, e)│
│ cmd=0x03 (ETCP_ID_SVC_ROUTE): │
│ → etcp_router_recv_cb() │
│ cmd=0x00 (ETCP_ID_DATA): │
│ → direct routing handler (legacy) │
└──────────────────────┬───────────────────────┘
│ для cmd=0x03 (SVC_ROUTE)
▼
┌──────────────────────────────────────────────┐
│ etcp_router_recv_cb(conn, entry) │ etcp_router.c
│ parse SVC_ROUTE_HDR: │
│ dst_node_id, src_node_id, seq, svc_id │
│ │
│ if dst_node_id != local_node_id: │
│ router_forward_transit(): │ мы — промежуточный узел
│ find/create TRANSIT_QUEUE(src,dst) │
│ queue_data_put(transit_q, entry) │
│ drain transit_q → next hop via │
│ waiter on send_input_q │ backpressure
│ return │
│ │
│ if payload_len == 0: │ это ACK/CLOSE/RST
│ router_handle_ack(rconn, hdr): │
│ tx_acked = hdr->seq │ продвигаем окно
│ drain send_q │ освободившиеся слоты
│ return │
│ │
│ router_handle_data_packet(rconn, ...): │
│ check bounds, dedup (recv_q lookup) │
│ queue_data_put(rconn->incoming_q) │
│ if hdr->seq == rconn->rx_seq + 1: │ следующий ожидаемый
│ router_try_assembly(rconn): │
│ scan recv_q for contiguous seq │
│ deliver to service callback │
└──────────────────────┬───────────────────────┘
│ service callback (ETCP_RT_ID_DATA)
▼
┌──────────────────────────────────────────────┐
│ routing_pkt_from_etcp_cb(conn, pkt) │ routing.c:180
│ route_pkt(instance, pkt, conn->peer_node) │
│ → route_lookup() → если локальный: │
│ → queue_data_put(tun->input_queue) │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ tun_input_queue_callback(q, tun) │ tun_if.c:88
│ queue_data_get() → pkt │
│ tun_platform_write(pkt->dgram+1, len) │ пишем в /dev/tun
│ queue_dgram_free + queue_entry_free │
│ queue_resume_callback() │
└──────────────────────────────────────────────┘
│
▼
Ядро читает IP-пакет из /dev/tun → доставка приложению
```
### 4.3 Retransmission (два уровня)
**ETCP retrans** (`ack_timeout_check`):
```
Таймер сканирует input_wait_ack с головы.
Для каждого INFLIGHT_PACKET:
if now - last_timestamp > timeout:
timeout = RTT * K1 + jitter * K2
min 50, max 10000 (0.1ms единицы)
→ переместить обратно в input_send_q (переотправка)
```
**Router retrans** (`router_retrans_timer_cb`):
```
300ms начальный таймаут
Экспоненциальный backoff (×2 каждые 3 попытки)
Максимум 17 попыток (суммарно ~5 секунд)
Копии пакетов хранятся в inflight_q (hash по seq)
При успешном ACK: удаление из inflight_q
```
---
## 5. ETCP — надёжный транспорт (Layer 2)
### 5.1 Основные структуры
**ETCP_CONN** — одно логическое соединение с удалённым узлом:
- Идентичность: `peer_node_id` (uint64_t), `session_id` (uint32_t)
- Криптография: `crypto_ctx` (sc_context_t — сессионные ключи)
- Фрагментация: `normalizer` (PKTNORM*)
- Линки: связный список `ETCP_LINK*`
- Счётчик seq: `next_tx_id` (монотонный), `last_rx_id`, `last_delivered_id`
- RTT: `rtt_last`, `rtt_avg_10`, `rtt_avg_100`, `jitter`, `rtt_history[10]`
- Состояние: 0=pending, 1=ready, 2=deleted (2-phase cleanup)
**ETCP_LINK** — один UDP-путь:
- Статус: `recv_keepalive` (принимаем ли пакеты), `remote_keepalive` (видит ли нас пир), `link_status`
- BBR: `bbr` (struct bbr*), `delivered_bytes`, `pacing_rate`, `bandwidth`
- Flow control: `inflight_bytes`, `inflight_packets`, `inflight_lim_bytes`
- Shaper: `shaper_load_time_tb`, `shaper_sub_nanotime`, `shaper_timer`
- NAT: `nat_type`, `nat_ip`, `nat_port`
**Wire-формат пакета:**
```
[encrypted:]
[timestamp:2] [flag_up:1] // ETCP_ENCRYPTED_HDR = 3 байта
[section_0] [section_1] ... [section_N] // типизированные секции
[appended after encryption:]
[CRC32:4]
Sections:
ACK (0x01): [count:1][rx_ack_till:4][rx_dup:2][per-ACK: seq(4)+ts(2)+delay(2)]
PAYLOAD (0x00): [seq:4 BE][data...] (always last)
TIMESTAMP (0x06): [cur_ts:2][txrx_offset:2] (RTT measurement)
MEAS_TS (0x07): [burst_id:2][flags:1][seq:1][ts_us:2][sz:2] (burst)
MEAS_RESP (0x08): [burst_id:2][valid:1][gap_avg:4][gap_min:4][pkt_cnt:1]
FILLER (0x09): [len:2][zeros...] (burst padding)
METRICS (0x0A): [csv_len:2][Ed25519_sig:64][csv...]
```
### 5.2 INIT handshake
Клиент знает pubkey сервера из конфига. Сервер не знает клиента заранее.
```
Client Server
│ │
│ 1. ECDH: priv_C × pub_S → session_key │
│ 2. salt = random(8) │
│ 3. obf_pub = XOR(pub_C, │
│ SHA256(salt || pub_S)) │
│ 4. encrypt INIT_REQUEST │
│ (AES-CCM, session_key) │
│ │
│──── INIT_REQ + [salt||obf_pub] ──────→│
│ │ 1. deobf: XOR(obf_pub,
│ │ SHA256(salt || pub_S))
│ │ → pub_C recovered
│ │ 2. ECDH: priv_S × pub_C
│ │ → same session_key
│ │ 3. decrypt INIT_REQUEST
│ │ (проверка: клиент знает pub_S)
│ │ 4. create ETCP_CONN
│ │ 5. encrypt INIT_RESPONSE
│←────── INIT_RESP + [salt||obf_pub] ───│
│ │
│ decrypt RESPONSE, complete │
│ link_state = connected │
│ │
│←══════ keepalive/данные ═════════════→│
```
**Ключевое свойство обфускации:** `XOR(XOR(pub_C, SHA256(salt||pub_S)), SHA256(salt||pub_S)) = pub_C`. Обе стороны знают `pub_S` и `salt`, поэтому обе могут восстановить `pub_C`. Пассивный наблюдатель видит только случайный XOR.
### 5.3 BBR congestion control (per-link)
Алгоритм управления перегрузкой, работающий на уровне ETCP_LINK.
**Режимы:**
```
STARTUP → экспоненциальный рост до обнаружения BDP
↓ (найден BDP)
DRAIN → слив избыточного inflight
↓
PROBE_BW → циклическое зондирование bandwidth:
CRUISE (без изменений) → REFILL (увеличение) →
UP (зондирование вверх) → DOWN (слив) → CRUISE...
↓ (периодически)
PROBE_RTT → зондирование минимального RTT (inflight=4)
```
**Оценка bandwidth:** два окна — `bw_hi` (максимальный фильтр, окно 10) и `bw_lo` (минимум из окна 6). Используется fixed-point арифметика (BBR_SCALE=8, BW_SCALE=24).
**Pacing rate:** `pacing_rate = bw * pacing_gain`. Пакеты отправляются не чаще чем `pkt_size / pacing_rate`.
**Burst measurement:** Когда inflight достигает лимита и >500ms с последнего burst, отправляется 12 пакетов подряд (bypass shaper). Получатель измеряет inter-packet gaps и возвращает min/avg gap. Отправитель вычисляет: `BW = pkt_size * 8000 / gap_min_us`.
---
## 6. etcp_router — сервисная маршрутизация (Layer 3)
### 6.1 Зачем нужен поверх ETCP
ETCP обеспечивает надёжность только на одном hop-е. etcp_router добавляет:
1. **End-to-end seq:** не зависит от per-hop ETCP seq
2. **Transit forwarding:** пакет может пройти через несколько промежуточных узлов
3. **Per-service изоляция:** разные сервисы имеют независимые seq и inflight control
4. **Inflight control:** свой congestion window (max_inflight, default 256)
5. **minRTT probing:** периодически снижает inflight до 4 для измерения минимального RTT
### 6.2 Transit forwarding
```
Узел A → Узел B (промежуточный) → Узел C (цель)
A: etcp_route_send(inst, C_node_id, data)
→ topo_group_find_conn_for_node(C_node_id) → conn_AB
→ router_send_one(rconn_AC): SVC_ROUTE_HDR {dst=C, src=A, seq=5}
B: etcp_router_recv_cb():
dst_node_id == C → не нам
router_forward_transit():
TRANSIT_QUEUE tq(src=A, dst=C) → queue_data_put
drain через conn_BC → etcp_send(conn_BC, entry)
(SVC_ROUTE_HDR не модифицируется, только пересылается)
C: etcp_router_recv_cb():
dst_node_id == C → нам
router_handle_data_packet()
→ incoming_q → recv_q (reorder) → service callback
```
Backpressure на транзите: если `conn_BC->send_input_q` заполнена, transit queue ждёт освобождения через waiter.
### 6.3 Inflight control и retrans
```
Отправка:
tx_seq - tx_acked >= inflight_limit → блокировка → send_q
tx_seq - tx_acked < inflight_limit → router_send_one() → inflight_q + retrans_timer
ACK:
получаем SVC_ROUTE_HDR с seq=N, payload_len=0
→ tx_acked = N
→ удаляем все inflight_q записи с seq <= N
→ drain send_q (освободившиеся слоты)
Retrans:
300ms → 600ms → 1200ms → ... → максимум 17 попыток
router_retransmit_one(): копия из inflight_q → etcp_send()
если 17 попыток без ACK → закрытие rconn
```
---
## 7. BGP топология
### 7.1 Модель данных
- **TOPO_NODE:** pubkey, ed25519_pubkey, имя, версия, флаги
- **TOPO_NODEQ:** узел в группе + пути (TOPO_NODEPATH), подсети, connectivity, transit
- **TOPO_NODEPATH:** путь к узлу (next_hop_node_id, hop_count, hop_list до 16)
- **TOPO_GROUP:** группа узлов (UTUN — VPN с подсетями, CHAT — чат без подсетей)
### 7.2 Поток синхронизации
```
Новое ETCP соединение установлено
│
▼
topo_group_new_conn(group, conn):
1. topo_group_add_to_senders() // добавить в список BGP-пиров
2. topo_group_send_table_request(conn) // запросить полную таблицу
│
▼ (удалённая сторона получает REQUEST_TABLE)
topo_group_handle_request_table():
1. topo_group_send_nodeinfo(local_node) // отправить информацию о себе
2. topo_group_send_full_table() // все известные узлы
├── for each node in group->nodes:
│ if peer NOT in node's hop_list:
│ topo_group_send_nodeinfo(node, conn)
3. topo_group_send_table_complete()
```
### 7.3 Обработка NODEINFO
```
topo_group_process_nodeinfo(group, from_conn, data, len):
1. Проверка типа группы (UTUN/CHAT)
2. Проверка hop_count < MAX_HOPS (16)
3. Проверка версии:
if existing_node.version >= new_node.version:
только обновить пути (множественные пути к узлу)
return
4. Полное обновление:
- десериализация TOPO_NODE (адреса, подсети, hop_list)
- создание/обновление TOPO_NODEQ в group->nodes (hash queue)
- добавление пути: from_conn->peer_node_id в hop_list
- ROUTE_INSERT(instance->rt, node) ← ПОДСЕТИ В ТАБЛИЦУ МАРШРУТИЗАЦИИ!
- персистентность в SQLite
- уведомление control_server + chatgui
5. BROADCAST соседям:
for each sender in senders_list:
if sender->peer_node_id not in node->hop_list:
topo_group_send_nodeinfo(node, sender->conn)
```
### 7.4 WITHDRAW (удаление узла)
```
topo_group_remove_conn(group, conn):
for each node reachable ONLY through this conn:
topo_group_process_withdraw(group, node):
route_delete(instance->rt, node) ← удаление из таблицы маршрутов
broadcast WITHDRAW соседям
удаление из group->nodes
```
---
## 8. conn_mgr — трёхфазное установление соединений
### 8.1 Диаграмма фаз
```
conn_mgr_connect_node(mgr, target_node_id)
│
├── Фаза 1: DIRECT ─────────────────────────┐
│ Для каждого адреса target × наш сокет: │
│ etcp_connection_create() │
│ etcp_link_new() → INIT │
│ таймер 5000ms │
│ │
│ ✓ Успех → CONN_MGR_OK (DIRECT) │
│ ✗ Таймаут: │
│ if (у нас прямой IP) && (!у target): │
│ → Фаза 2 (REVERSE) │
│ else: │
│ → Фаза 3 (INDIRECT) │
│ │
├── Фаза 2: REVERSE ─────────────────────────┤
│ etcp_route_send(DIRECT_REQ) → через BGP │
│ { наши адреса для подключения } │
│ таймер 15000ms │
│ │
│ Target получает DIRECT_REQ: │
│ etcp_connection_create() │
│ etcp_link_new(REVERSE) → INIT к нам │
│ │
│ ✓ Успех → CONN_MGR_OK (REVERSE) │
│ ✗ Таймаут → Фаза 3 (INDIRECT) │
│ │
└── Фаза 3: INDIRECT ────────────────────────┤
etcp_route_send(EXCHANGE_REQ) → BGP │
{ наши top-4 кандидата + RTT } │
таймер 15000ms │
│
Target: probe наши кандидаты │
→ EXCHANGE_RESP {свои кандидаты + RTT} │
│
Мы: probe кандидаты target │
cm_compute_intermediaries(): │
union списков │
total_rtt = our_rtt + their_rtt │
sort by total_rtt, select top 3 │
→ INTERM_SELECTED │
entry->conn_type = INDIRECT │
CONN_MGR_OK │
```
### 8.2 Фоновые процессы
- **bg_ping_timer:** каждые ~100ms пингует очередной BGP-узел (RTT измерение), полный цикл ≥10s
- **candidate_ping_timer:** каждые ~2s обновляет RTT для best_candidates[3]
- **idle_timer:** каждые ~1s проверяет неактивные соединения, дисконнект по idle_timeout
### 8.3 NAT compatibility check
При выборе адресов для DIRECT фазы проверяется совместимость NAT типов:
- Если у нас EIM NAT: можем соединиться с кем угодно
- Если у нас Symmetric NAT и target за NAT: DIRECT невозможен (→ REVERSE/INDIRECT)
- NAT тип определяется через сравнение src_ipv4 (что мы о себе сообщаем) и наблюдаемого адреса при INIT
---
## 9. Криптография
### 9.1 Генерация ключей и идентичности
```
X25519 keypair:
EVP_PKEY_keygen(EVP_PKEY_X25519)
→ private_key[32], public_key[32]
Node ID (63-bit):
SHA256(private_key) → hash[32]
node_id = *((uint64_t*)hash) & 0x7FFFFFFFFFFFFFFF
Ed25519 keypair (производный от X25519):
SHA512(private_key) → hash[64]
ed25519_privkey = hash[0..32]
ed25519_pubkey = EVP_PKEY_get_raw_public_key(ed25519_privkey)
```
### 9.2 Сессионный ключ
```
X25519 ECDH:
shared_secret = EVP_PKEY_derive(my_priv, peer_pub) // 32 байта
Session key (AES-128):
SHA256(shared_secret || "uTun-v3-session") → hash[32]
session_key = hash[0..16]
```
### 9.3 Формат зашифрованного пакета
```
Шифрование:
nonce = sc_build_nonce(tx_counter) // 13 байт: SHA256(urandom||counter||time)
AES-128-CCM(nonce, session_key):
encrypt: header[3] + plaintext[N] + CRC32[4]
tag = CCM auth tag[16]
Пакет:
[nonce:13] [ciphertext: N+7] [tag:16] [+ pubkey_block:40 для INIT]
Расшифровка:
извлечь nonce[13] из начала, tag[16] из конца
AES-128-CCM decrypt
проверить CRC32
проверить CCM auth tag (EVP_DecryptFinal_ex)
```
### 9.4 Streaming cipher (STCP)
```
AES-128-CTR:
iv = SHA256(session_key || stream_id_LE || "uTun3-stream")[0..16]
EVP_aes_128_ctr(session_key, iv)
XOR in-place (конфиденциальность без аутентификации)
STCP добавляет свой CRC32 на уровне фреймов
```
### 9.5 Ed25519 подписи
Используются для:
- Метрик (METRICS section 0x0A) — подписанные CSV
- Сообщений чата (MSG_OPT_SIGNED флаг)
- db_sync записей (JSON + Ed25519 sig)
Стриминговая подпись: `SHA512(SHA512(data))` — обе стороны аккумулируют SHA-512, затем one-shot Ed25519 над финальным хешем.
---
## 10. Прокси-система
### 10.1 Архитектура клиента (tcp_proxy_client)
```
Приложение (браузер и т.д.)
│ подключается к SOCKS5 localhost:1080
▼
┌──────────────────┐
│ socks_proxy │ SOCKS5/HTTP CONNECT парсинг
│ (src/proxy/) │ → резолвинг destination
└────────┬─────────┘
│ ETCP CONNECT → tcp_proxy_server на exit node
▼
┌──────────────────┐
│ lwip_tcp │ Встроенный TCP стек
│ (src/lwip_tcp/) │ Завершает TCP на клиенте
│ │ tcp_write() → сегменты
└────────┬─────────┘
│ lwIP output callback → TUN write
▼
┌──────────────────┐
│ TUN (nat_tun) │ Локальный виртуальный интерфейс
│ │ Перехватывает ВЕСЬ исходящий TCP
└────────┬─────────┘
│ для не-TCP: udp_proxy / icmp_proxy
│ для TCP: lwip_tcp_input() → tcp_process()
▼
ETCP туннель → exit node
```
**Ключевая особенность:** клиент использует встроенный lwIP TCP стек для терминации TCP-соединений локально. Это позволяет:
- Не гонять TCP ACK через туннель (нет TCP-over-TCP проблемы)
- Прозрачно обрабатывать потери на пути клиент↔exit (ETCP надёжен)
- Поддерживать SOCKS5/HTTP CONNECT без модификации приложений
### 10.2 Архитектура сервера (tcp_proxy_server)
```
ETCP туннель от клиента
│ CONNECT subcmd: target_ip:port
▼
┌──────────────────┐
│ tcp_proxy_server │ Exit node
│ │ Создаёт OS TCP сокет к реальному адресату
│ │ Релеит данные: ETCP ↔ OS TCP
│ │ Backpressure: tx_buf + pause_waiter + retry_timer
│ │ FIN handling: отложенный FIN до опустошения read_queue
└────────┬─────────┘
│ OS TCP connect()
▼
Целевой сервер в интернете
```
### 10.3 lwIP TCP стек
Встроенный в uTun TCP/IP стек (адаптированный lwIP):
- **Context-based:** один `lwip_tcp_ctx` на tcp_proxy_client
- **Таймеры:** `tcp_fasttmr()` (delayed ACK) и `tcp_slowtmr()` (retrans, keepalive, TIME_WAIT) — каждые 250ms
- **4 списка PCB:** bound, listen, active, time_wait
- **10 TCP состояний** (RFC 793): CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → FIN_WAIT_1/2 → TIME_WAIT
- **Reno congestion control:** slow start, congestion avoidance, fast retransmit (3 dupack), fast recovery
- **Memory pools:** pcb_pool, pcb_listen_pool, seg_pool
- **Trace ring buffer:** 384 записи внутренних событий для отладки
- **Конфигурация:** MSS=1460, RTO_MIN=3000ms, MSL=60s, TCP_WND=8*MSS
---
## 11. Модель потоков
### 11.1 Основной процесс uTun (однопоточный)
```
Поток 1 (главный):
uasync_mainloop()
├── Сокеты:
│ ├── UDP listen (все [server] сокеты)
│ ├── TCP listen (control_server, STCP)
│ └── TUN fd (read/write через uasync callback)
├── Таймеры:
│ ├── ETCP: retrans, ack_response, keepalive
│ ├── etcp_router: retrans, ACK, no_route_retry, minRTT_probe
│ ├── BBR: shaper per-link
│ ├── conn_mgr: bg_ping, candidate_ping, idle, connect_timeout
│ ├── Нормализатор: flush_timer
│ ├── Прокси: диагностика (1s), retry (500ms)
│ ├── NTP: resync
│ └── db_sync: TTL cleanup
├── call_soon (FIFO):
│ └── Отложенные callback (2-phase cleanup, resume)
└── post (межпоточные):
└── Из chatgui/GUI потока
НЕТ: sleep, usleep, pthread_mutex_lock (в главном потоке)
ВСЕ: асинхронно через uasync
```
### 11.2 chatgui (двухпоточный)
```
Поток 1 (Qt main thread):
- Рендеринг UI (QPainter)
- Обработка событий (клики, ввод)
- Чтение из SQLite (read-only)
- Отправка команд в uTun через GuiBridge (Qt signals)
Поток 2 (uTun networking):
- uasync_mainloop()
- ETCP соединения
- ChatSync (P2P синхронизация каналов)
- Запись в SQLite (thread-safe write)
- GuiBridge: получение команд через uasync_post + wakeup pipe
```
**Правила межпоточного взаимодействия:**
- Из GUI → uTun: `uasync_post(ua, callback, arg)` — thread-safe
- Из uTun → GUI: `emit Qt signal` — Qt thread-safe через queued connection
- Память: `uasync_memsync(ua)` перед post для видимости изменений
- БД: chat_core (uTun поток) пишет, DbManager (GUI поток) читает
### 11.3 Windows TUN (дополнительный поток)
На Windows TUN чтение выполняется в отдельном потоке `tun_read_thread_proc` (Wintun API требует dedicated thread). Результаты передаются в главный поток через `uasync_post`.
---
## 12. Жизненный цикл UTUN_INSTANCE
### 12.1 Инициализация
```
utun_instance_create(ua, config_path):
config_ensure_keys_and_node_id() // авто-генерация ключей при первом запуске
parse_config(config_path) // парсинг INI в utun_config
debug_set_output_file() // открытие лог-файла
debug_set_level() per config // настройка уровней отладки
instance_init_common(instance, ua, config):
1. Копирование name, node_id из конфига
2. sc_init_local_keys() // загрузка X25519 ключей
3. sc_derive_ed25519_pubkey() // деривация Ed25519 ключей
4. queue_new() для networks // хеш-очередь сетей
5. queue_new() для connections // хеш-очередь ETCP соединений
6. memory_pool_init() ×3 // data_pool, pkt_pool, ack_pool
7. routing_create(instance) // таблица маршрутов
8. tun_init() // TUN интерфейс
9. init_sockets(instance) // UDP сокеты из [server]
10. topo_groups_init(instance) // BGP группы + SQLite
11. topo_group_update_my_nodeinfo() // локальный узел
12. conn_mgr_init(instance) // менеджер соединений
13. fw_init() + fw_load_rules() // firewall
14. etcp_router_init(instance) // сервисный роутер
15. routing_bind(instance) // привязка data handler
16. tcp_proxy_server_init() // exit node прокси
17. tcp_proxy_client_create() // клиентский прокси (если включён)
utun_instance_init(instance):
1. db_sync_init(instance) // распределённая БД
2. routing_set_tun(instance) // подключение TUN к роутингу
3. nat_transport_init(instance) // NAT транспорт
4. init_connections(instance) // ETCP соединения из [client]
5. control_server_init() // мониторинг
6. running = 1
8. ntp_time_init() // NTP синхронизация
9. ntp_node_time_init() // межузловое время
```
### 12.2 Завершение (строго обратный порядок)
```
utun_instance_destroy(instance):
1. running = 0
2. Отмена NTP таймеров
3. control_server_destroy() // потом мониторинг
5. Закрытие всех UDP сокетов // etcp_socket_remove() для каждого
6. db_sync_destroy()
7. etcp_connection_close() для всех соединений в connections
8. uasync_poll() до опустошения deferred callbacks
9. Отмена pending_pings
10. tun_route_del_all() + tun_close() // TUN
11. tcp_proxy_client_destroy()
12. tcp_proxy_server_destroy()
13. routing_destroy()
14. nat_transport_destroy()
15. etcp_router_destroy()
16. conn_mgr_destroy()
17. topo_groups_destroy()
18. fw_free()
19. stcp_link_server_destroy()
20. networks queue очистка
21. free_config() // освобождение конфига
22. memory_pool_destroy() ×3 // pkt_pool, ack_pool, data_pool (порядок важен!)
23. u_free(instance)
```
### 12.3 2-phase cleanup ETCP_CONN
```
etcp_connection_close(etcp):
Phase 1:
- очистка таймеров
- уничтожение линков (etcp_link_free)
- уничтожение нормализатора
- удаление из instance->connections
- state = 2 (deleted)
Phase 2 (if ref_count == 0):
- uasync_call_soon(etcp_connection_free_deferred)
- на следующей итерации event loop:
- освобождение всех очередей
- освобождение crypto_ctx
- u_free(etcp)
Зачем 2 фазы:
- close может быть вызван из callback того же ETCP_CONN
- нельзя освобождать память пока мы ещё в стеке вызова callback
- call_soon гарантирует что стек полностью размотается перед free
```
### 12.4 Reload
```
utun_instance_reload(instance, new_config_path):
- парсинг нового конфига
- сравнение [server] секций: удалить удалённые, добавить новые
- сравнение [client] секций: закрыть ненужные соединения, открыть новые
- обновление firewall, NAT, keepalive параметров
- замена instance->config → free_config(old)
```
---
## Приложение A: Полный список очередей в системе
| Модуль | Очередь | Тип | Назначение |
|--------|---------|-----|-----------|
| tun_if | `tun->output_queue` | FIFO | TUN → routing |
| tun_if | `tun->input_queue` | FIFO | routing → TUN |
| routing | `routing_pkt` (через etcp_router) | — | IP-пакеты на маршрутизацию |
| pkt_normalizer | `pn->input` | FIFO | Пакеты на фрагментацию |
| pkt_normalizer | `pn->output` | FIFO | Собранные пакеты |
| etcp | `send_input_q` | FIFO | Вход от нормализатора |
| etcp | `input_queue` | FIFO | Фрагменты → inflight |
| etcp | `input_send_q` | hash(seq) | Готовые к отправке |
| etcp | `input_wait_ack` | hash(seq) | Отправленные, ждут ACK |
| etcp | `recv_q` | hash(seq) | Принятые не по порядку |
| etcp | `output_queue` | FIFO | Собранные данные |
| etcp | `ack_q` | hash(seq) | Неотправленные ACK |
| etcp | `transit_queues` | hash(src,dst) | Транзитные пакеты |
| etcp_router | `rconn->incoming_q` | FIFO | Входящие router-пакеты |
| etcp_router | `rconn->recv_q` | hash(seq) | Реордеринг router-пакетов |
| etcp_router | `rconn->send_q` | FIFO | Заблокированные на отправку |
| etcp_router | `rconn->inflight_q` | hash(seq) | Копии для ретрансмиссии |
| topo_group | `group->nodes` | hash(node_id) | Все известные узлы |
| topo_group | `group->senders_list` | linked list | BGP-пиры |
| utun_instance | `instance->connections` | hash(peer_node_id) | Все ETCP_CONN |
| utun_instance | `instance->networks` | hash(network_id) | Доверенные сети |
| conn_mgr | `mgr->entries[]` | dynamic array | Состояния подключений |
## Приложение B: Основные таймауты и константы
| Константа | Значение | Где используется |
|-----------|---------|-----------------|
| ETCP retrans min | 5ms (50 tb) | etcp ack_timeout_check |
| ETCP retrans max | 1000ms (10000 tb) | etcp ack_timeout_check |
| ETCP delayed ACK | 2ms (20 tb) | etcp ack_response_timer |
| Router retrans init | 300ms | etcp_router retrans_timer |
| Router retrans max tries | 17 (~5s) | etcp_router |
| Router minRTT probe interval | 10s | etcp_router |
| Keepalive interval (active) | 200ms | etcp_connections |
| Keepalive interval (idle) | 10s | etcp_connections |
| INIT retrans init | 500ms | etcp_connections |
| INIT retrans max | 3200ms | etcp_connections |
| conn_mgr DIRECT timeout | 5s | conn_mgr |
| conn_mgr REVERSE/INDIRECT timeout | 15s | conn_mgr |
| Normalizer frag_size | mtu - ACK_RESERVE - UDP_HDR - SC_HDR | ~1553 bytes |
| BBR burst packets | 12 | etcp burst measurement |
| BBR max cwnd | 1MB (configurable) | etcp_bbr |
| lwIP TCP_RTO_MIN | 3000ms | lwip_tcp |
| lwIP TCP_MSL | 60s | lwip_tcp |
| UDP proxy flow expiry | 60s | udp_proxy |
| ICMP proxy request timeout | 5s | icmp_proxy |
| db_sync record batch | 32 | db_sync |
| BGP max hops | 16 | topo_group |
| BGP MAX_ADDR_TYPES | 16 | topo_node |
## Приложение C: Ключевые callback-цепочки
### C.1 Новое ETCP соединение
```
etcp_connection_create()
→ new_conn_cbks chain (topo_group_etcp_conn_cbk, ...)
→ topo_group: up/down callbacks on connection
→ etcp_conn_queue_set_ready()
→ ready_cbks chain
etcp_on_up()
→ up_cbks chain (topo_group_on_conn_up, conn_mgr notification, ...)
→ topo_group_on_conn_up: BGP full table sync
```
### C.2 Получение данных (снизу вверх)
```
UDP recv → sc_decrypt
→ etcp_conn_input: parse sections, ACK processing, store in recv_q
→ etcp_output_try_assembly: output_queue
→ pn_unpacker_cb: reassemble
→ etcp_int_recv: dispatch by cmd byte
→ cmd=0x03: etcp_router_recv_cb
→ router_handle_data_packet
→ incoming_q callback
→ router_try_assembly
→ service callback (routing_pkt_from_etcp_cb, proxy, chat, ...)
```
### C.3 Отправка данных (сверху вниз)
```
service (routing, proxy, chat)
→ etcp_route_send / etcp_router_conn_send
→ router_send_one: SVC_ROUTE_HDR + inflight_q
→ etcp_send: send_input_q
→ pkt_normalizer packer: fragment
→ etcp->input_queue
→ input_queue_cb: INFLIGHT_PACKET
→ input_send_q
→ process_send_queue: etcp_request_pkt
→ loadbalancer_select_link
→ etcp_encrypt_send: sc_encrypt
→ socket_sendto
```
---
*Документ основан на анализе исходного кода uTun. Для каждого модуля доступен отдельный `_doc.md` файл с детальным описанием API.*