etcp_conn_queue_set_ready() делает ранний выход по state!=0, поэтому при
реините уже готового соединения (peer restart / duplicate INIT / collision)
reinit_pending оставался в 1 навсегда. Добавленные в 841b3042 guard'ы
etcp_conn_input/etcp_output_try_assembly дропали все входящие пакеты.
Снятие reinit_pending + REINIT перенесено в etcp_conn_ready.
Новый модуль src/radio (radio/radio_audio/radio_headless) — push-to-talk
радио поверх topo-групп, доставка аудио в канал, headless-режим и тесты.
Интеграция в chatgui (ptt_hotkey_manager, radio_audio_engine, радионастройки,
вызовы) и chatgui-android (RadioAudioEngine, RadioOverlayService, UI).
Все ETCP_ID_*/ETCP_RT_ID_* сведены в etcp_api.h (единственное место
определения) с реестром и предупреждением про коллизии в пределах одного
пространства имён. Убраны локальные define из topo_group.h,
topo_group_invite.h, db_sync.h, chat_sync.h, chat_join.h; хардкод 0x31 в
member_sync.c заменён на ETCP_RT_ID_MEMBER_SYNC.
Коллизия: chatgui-десктоп биндил мёртвый ETCP_RT_ID_CHAT 0x11 поверх
ETCP_RT_ID_CONN_MGR 0x11 — затирался conn_mgr_router_recv_handler, из-за
чего INDIRECT-обмен conn_mgr не работал и голос звонка не устанавливался.
Удалён мёртвый код recvCallback/messageReceived/send в utun_node.{cpp,h}.
При REQUEST_TABLE для ещё не созданной группы (CHAT-канал не загружен) пир
дропал запрос, и проситель навсегда не узнавал таблицу — односторонняя
видимость (звонок android→linux падал «peer offline»). Теперь запрос
запоминается в pending_table_reqs[32] и выполняется при создании группы
(topo_group_fulfill_table_reqs → handle_request_table).
Попутно: test_chat_join_e2e — критерий успеха через CHAT_EVT_CONNECT_RESULT
(JOIN_READY) вместо senders_list; doxygen-комментарии в etcp_api.h.
recv_conn хранил заимствованный указатель на физический conn без очистки
при его освобождении. Добавлен etcp_router_conn_destroyed(), вызываемый из
etcp_connection_close фазы 1; RST теперь отправляется через живой conn,
доставивший пакет, а не через устаревший recv_conn.
ip_is_public() вынесен в lib/platform_compat (единый классификатор IPv4 с ntohl).
auto_socket_reconcile_config: тип сокета выставляется по фактическому адресу.
sock_collect_views: has_priv/has_inet/is_eim — по классификации адреса и NAT-детекции.
is_local_subnet упрощён до !ip_is_public(). Тест test_sock_match обновлён.
ncd_deliver_up_cb (uasync_call_soon) держал сырой указатель handle h,
который мог быть освобождён node_conn_direct_close/force_close раньше
выполнения отложенного callback'а. Сохраняем токен call_soon в handle
и отменяем его при close через uasync_call_soon_cancel.
Часы colo2 отстают на ~52с (ntpd выключен), что ломало reality auth
(diff < -30с = time_window). Теперь клиент и сервер передают
ntp_time_get_seconds() в reality_client_hello_build_at /
reality_server_hello_build_at вместо сырого системного времени.
reinit должен происходить только при смене reset_id пира
(etcp_conn_apply_peer_reset_id). gop-проверка 'dirty vs clean' на догоняющем
TCP-линке давала ложный reinit и рвала соединение (JOIN_INFO_RESP терялся).
Удалён peer_got_initial_pkt из stcp_link (стал не нужен).
- sock_match: единая классификация сокетов (DIRECT/NAT/LOCAL) и адресов (PUB/PRIV/UNREACH)
- eim приравнен к direct; на приватный адрес — только с приватного интерфейса;
оба EIM за одним NAT (одинаковый публичный IP) — только через LAN
- topo_node: LOCAL-сокеты рекламируются (INTERFACE), private скрыт,
NAT-адрес не рекламируется для strict и при оффлайне
- ntp: флаг reachable по циклам + пере-анонс nodeinfo при смене доступности
- cm_direct_add_links / ncd_create_links / conn_match_candidate_sockets на sock_match
- test_sock_match: unit-тесты классификации и сопоставления
- topo_node: сокет рекламирует INTERFACE (до NAT) + NAT (после NAT, EIM);
strict/direct отдельным адресом не передаются
- topo_node_sqlite: addr_type_classify различает NAT_VERIFIED_*; добавлен
ADDR_TYPE_NAT (тип неизвестен), fallback больше не даёт DIRECT по IP
- chat_member: бейдж NAT-типа по приоритету (public>nat_eim>nat>nat_strict>local)
вместо MIN(addr_type)
- GUI (desktop+android): адреса мембера с socket_id и ролью (local/public/nat/nat_eim/nat_strict)
- категория connection теперь только жизненный цикл соединения; INIT/PING/keepalive
переклассифицированы в ETCP/SOCKET/KEEPALIVE/REALITY/ROUTING, удалены дубли
- settingsdialog: устранён дубль my_node_name в конфиге (двухпроходная запись + только при изменении)
- storagesettingspage: канонические ключи storage_total_size/storage_unit_size с суффиксом M/G
- audiodevicesettingspage: compressor_max_gain_db min 0→1
- etcp_connections: %s-баг в логе TCP link (addr → sockaddr_storage_to_str)
- docs/sample: [chatserver], актуализированы ключи хранилища
- node_conn_direct_close: immediate conn cleanup when links are down
(graceful fin_wait on a down conn left a lingering entry, so re-open
in Phase3 stuck in 'pending' with no timer and scan never continued)
- tcp_link_close_cb: skip etcp_link_close if conn was already closed
synchronously during etcp_on_link_down cascade (fixes use-after-free)