You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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 БД), 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.

Алгоритм управления перегрузкой, работающий на уровне 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.