57 KiB
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 БД), msg_transport (IPC), 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 в epolluasync_call_soon(ua, arg, cb)— отложенный вызов на следующей итерации (FIFO). Используется для 2-phase cleanupuasync_post(ua, cb, arg)— межпоточный вызов (thread-safe). Пробуждает epoll через wakeup pipeuasync_memsync(ua)— memory barrier для синхронизации памяти между потоками
Правила:
- Никаких sleep/usleep — только таймеры
- Нельзя использовать из нескольких потоков один UASYNC
uasync_post— единственный способ вызвать код uTun из другого потока
3.2 ll_queue — lock-free очередь
Основа всего межмодульного взаимодействия. Критические правила:
-
Callback правило:
queue_set_callback(q, cb, arg)→ внутри cb обязательно:queue_data_get(q)→ обработка →queue_resume_callback(q). Без resume очередь навсегда блокируется. -
Backpressure:
queue_set_threshold(q, max_packets, max_bytes)+queue_waiter_wait(q, &handle, cb, arg). Не забиваем очередь — добавляем только когда waiter сработал. -
Поиск по индексу:
queue_data_put_with_index(q, entry)— entry должен иметь ключ по смещениюindex_offsetразмеромindex_size. FNV-1a хеш. -
Память:
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 добавляет:
- End-to-end seq: не зависит от per-hop ETCP seq
- Transit forwarding: пакет может пройти через несколько промежуточных узлов
- Per-service изоляция: разные сервисы имеют независимые seq и inflight control
- Inflight control: свой congestion window (max_inflight, default 256)
- 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, msg_transport, 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. msg_transport_init() // IPC транспорт
7. running = 1
8. ntp_time_init() // NTP синхронизация
9. ntp_node_time_init() // межузловое время
12.2 Завершение (строго обратный порядок)
utun_instance_destroy(instance):
1. running = 0
2. Отмена NTP таймеров
3. msg_transport_destroy() // сначала IPC
4. 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.