From aee44943faa95c9daf7da72ed95d53ac3e419196 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 16 Aug 2026 09:19:09 +0300 Subject: [PATCH] =?UTF-8?q?topo:=20invite=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=20topo=5Fgroup=5Finvite,=20conn=5Fmgr=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD=20=D0=B4=D0=BE=203-?= =?UTF-8?q?=D1=84=D0=B0=D0=B7=D0=BD=D0=BE=D0=B3=D0=BE=20open;=20member=5Fs?= =?UTF-8?q?ync:=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=87=D1=91=D1=82=20merkle-=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D0=B0=20=D0=BF=D1=80=D0=B8=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - topo_group_invite.c/h: invite/join через DIRECT + INVITE_INFO_REQ/RESP (из conn_mgr) - conn_mgr: conn_mgr_open_invite → conn_mgr_open, убран invite-режим (-480 строк) - topo_group_connect: подключение через node_conn_direct вместо conn_mgr - member_sync: _rebuild_all_trees сносит merkle_tree_hash канала и собирает заново из peers (чистка осиротевших листов) - member_sync: хеш блока A/B (update_sig/adm_tags) в док-комментариях - обновлены doc-файлы и тесты --- AGENTS.md | 18 +- db_a/chats.db | Bin 4096 -> 53248 bytes db_a/chats.db-shm | Bin 32768 -> 0 bytes db_a/chats.db-wal | Bin 94792 -> 0 bytes db_b/chats.db | Bin 4096 -> 53248 bytes db_b/chats.db-shm | Bin 32768 -> 0 bytes db_b/chats.db-wal | Bin 94792 -> 0 bytes src/Makefile.am | 2 + src/chat/member_sync.c | 7 + src/chat/member_sync.h | 24 +- src/chat/member_sync_doc.md | 149 ++++++ src/chat/merkle_sync.h | 10 +- src/chat/merkle_sync_doc.md | 158 ++++++ src/chat/merkle_tz.md | 6 + src/media_delivery/media_delivery.c | 4 +- src/media_delivery/media_download.c | 4 +- src/media_delivery/sync.md | 2 +- src/routing_layer/conn_mgr.h | 42 +- src/routing_layer/conn_mgr_core.c | 480 +----------------- src/routing_layer/conn_mgr_priv.h | 59 +-- src/routing_layer/route6_lib_doc.md | 2 +- src/routing_layer/route_connectivity_doc.md | 2 +- src/routing_layer/route_lib_doc.md | 4 +- src/routing_layer/topo_group_connect.c | 55 +- src/routing_layer/topo_group_doc.md | 396 ++++++++------- src/routing_layer/topo_group_invite.c | 535 ++++++++++++++++++++ src/routing_layer/topo_group_invite.h | 73 +++ src/routing_layer/topo_node.h | 4 +- src/routing_layer/topo_node_doc.md | 212 ++++---- src/routing_layer/topo_node_sqlite_doc.md | 2 +- src/routing_layer/topo_recovery.c | 145 +++--- src/routing_layer/topo_recovery.h | 24 +- src/transport_layer/node_conn_direct.c | 160 +++--- src/utun_instance.c | 6 +- tests/test_conn_mgr.c | 6 +- tests/test_conn_mgr_already_connected.c | 8 +- tests/test_invite_group_create.c | 21 +- 37 files changed, 1502 insertions(+), 1118 deletions(-) delete mode 100644 db_a/chats.db-shm delete mode 100644 db_a/chats.db-wal delete mode 100644 db_b/chats.db-shm delete mode 100644 db_b/chats.db-wal create mode 100644 src/chat/member_sync_doc.md create mode 100644 src/chat/merkle_sync_doc.md create mode 100644 src/routing_layer/topo_group_invite.c create mode 100644 src/routing_layer/topo_group_invite.h diff --git a/AGENTS.md b/AGENTS.md index 24676a91..94088946 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,12 +220,13 @@ debug - всё что нужно для понимание сути происх info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться. -### Debug Categories (27 категорий) +### Debug Categories (28 категорий) ``` NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6, TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13, SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20, -KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26 +KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26, +MEMBER_SYNC=27 ``` ### Настройка отладки @@ -313,11 +314,12 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26 - `route6_lib.c/h` - IPv6 routing - `route_ping.c/h` - Route ping probing (NAT check, liveness) - `route_connectivity.c/h` - Route connectivity checks -- `topo_group.c/h` - BGP-like route exchange between nodes -- `topo_group_connect.c/h` - BGP group connections -- `topo_node.c/h` - Node management (peer management) +- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT) +- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл) +- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP) +- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry) - `topo_node_sqlite.c/h` - SQLite persistence for nodes -- `topo_recovery.c/h` - Topology recovery +- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения - `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND) - `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding) - `nat_detection.c/h` - NAT type detection @@ -329,8 +331,8 @@ KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26 - `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик - `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей - `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок -- `member_sync.c/h` - Списки участников каналов в актуальном состоянии у всех. Добавил/удалил участника — изменение разойдётся по всем узлам. Работает поверх merkle_sync -- `merkle_sync.c/h` - Быстрое сравнение и синхронизация данных между двумя узлами. Обмениваются контрольными суммами и передают только различия. Универсальный движок: не привязан к конкретному типу данных +- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись +- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`) - `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core) - `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core) - `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core) diff --git a/db_a/chats.db b/db_a/chats.db index 4a3492ca9ee32d110ce3484c72386f9ff8651a2c..cb585069c92040bad76388899ae5964434ef0450 100644 GIT binary patch literal 53248 zcmeI)%WoP-9Ki7fKY$pTl8d{X6yF!BQ@> z_D9D9#}{82H;io7_*w|VFtX}dQct}V)Xt(lP_LQT{;b`sap%`RODn$_#eBv1|oq-HW2;0?cbwqQg z(NO8miQ_xRc6Zbt9luY1WIe9$G&Tq3`-< z$)iHTFPRo7#sCw6V;wBEzO}ZVh^o5up{NCPv*@*+*lBJ*-?4%#Biu==F4~;l-Powz zw~xkqlfkvwXl)8zjLNUmkKL2-f*@U;mc3-Y==29Es$#}Ry$?`-P*^Uv%vnP_No&`V z$Jf+k&j%C!O)giqDw(&Rg%iT}W4UvehSh^06;aQy!==lDbq^e0jo4VQ|ecl`|OyZTN#@kbUf0%6DVia#OG1L+; zts`~5bXa}fe3o4<-@cvM4QG9{xO(#SSPr`~b+`}(oUG!h#A+}t*HupQhgK$6u2eEV z%dpH?f5djPiLzn|Y5_^CA9`hrFCO)sPHVfVN_c8DJL}QljB=%ByKZN=SftY816icg zCZk}o#-vu?U>f@FXc#7@()0)p({$a>7oRZMbH5Aorb7DeK&p4m!BLR+V%A_b+n)O- zd}Fzo+&vf%4|V#Cps009ILKmY**5I_I{1Q0+V65#%yp@0Aa2q1s}0tg_000IagfWZ6< zaQ{F5eM}1xKmY**5I_I{1Q0*~0R#}>{+}^`00IagfB*srAbfFx w&+%k$&vt^}|7WiMYp4es0tg_000IagfB*srAb;zWBOLvQM-O#*ZH0av#M*8l(j diff --git a/db_a/chats.db-shm b/db_a/chats.db-shm deleted file mode 100644 index 520b5cb362d363bc9c8455ce6e2ce8c4de5cd358..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*KQ2U36bIltV}`*DnqMyx?=^P}iltkLlOiAKuv+1G$aIS3glcZAy88w zr^pF`ngTg9Q3%u&$SI*hpr%01#T5cI1-5G*+o=hJz?=esvI4thPwwCZ_S+!@0!0N5 lTK<1V2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0Rj~SJ^__5D4GBO diff --git a/db_a/chats.db-wal b/db_a/chats.db-wal deleted file mode 100644 index d84b99130c6cef4dfc21a52df584224ab3871d47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94792 zcmeI*e{36P9mjFoN$j{uXg3L&3I(nUQ)4v@GAK}{B9io0tU6BXI#Mzuj*ER+PI2rs zK3D13C{3Z-UkC~D;}0q{G}KLFz#sg{22;C%V1J-?XehuYpg_gML=nIKLGV21dz`zQ zQ?C(hk^4TivG2!ov0ruE_@2-Ac|LeLz7~1+Z{HS)?25?c%EhNIf3g1yJr}i$y-FY2w(J_}eAhc#CY`tyaXX zdp_TKtGlttU0?difY=io_*LXBcZn}72q1s}0tg_000IagfB*vjbAh9=cxvCi=&WPT zm95%|viP89)T|SAt5OWFc1`5TU{7CLsgVlG)DqGkH*R5Xzq8j7B?+;c8j zCEGOSZE<$r-B{pUz8%`IW|y>$C6g=YhxNR6G@qFq%g<^@^jU3ex-gZ=i8D;?Nz_m`ls@ysvF41&x~hfYo|;aiLyW zRQq4s+THCw;Fc$^&d=8@$J-xnofa=T`8mINz2MN*CrhiB&c)(B1!cFE1eOupoc{0tg_000Iag zfB*t-lt87s(SM(8*?+Iby7Tql_jvvHtvl{jgK)>a+MXsa;3gRucW_&dJ19Tn_o%!; zYL8}r{xc6%$P2ttZ$^9{1Q0*~0R#|0009ILKmY**-c*4=Uf|`y-+XKBXBU3nCNB{8 zJz}>G!MA=>;|uikKitd6nYaThUOx?73V3GpYh`=FObcgdibf_`5%!NxP{-0dFlut zfB*srAbge{LR5buAxFE1cgSP(z} z0R#|0009ILKwvuwEXHrtg?85hs57*#(o73b4{FKZ`pik<#!iYD| zg5BucAJ0yWYq_a{mYdFIM>I9>f+m;HzGAiFSQUr)2wum01oAU}LgfXnJajFo9lUgb zyufyPE9S8vfB*srAby6$-mv37(3Ki4!1tXn zOl)OOjjhHnA5I_I{1Q0*~0R#|0009I#R3MNS=<6Ok zeEIpEnKpR=_p*WKA?pyfmKW&zN+g-Q8p;bKulC*7q0&4p1Q0*~0R#|0;7t}-tMw*Q z>2&mSd)!_=Ip%g*l!lA?TiZDmTuTP}`hxq!^y8XW>FbvC$}gE*K|ieLwWImW?4(#`jc@>yUo$*mSm_4c9C7 zsqd~)7!9t=U%TC?E_ngF zY}E|rBhY8W&)%xx>y|f~_4T39huWS#8jGj)?TgO3KlR=#Q1trOtxA#k2$+vRj?$18 zZ!}5y-;>G<3_kz(`FAa>UmB1bcgZg-2q1s}0tg_000IagfB*srAke`A@*?C8bzR_J zUrYS^_T!ax>IgdcU!13g00IagfB*srAb;OgN!etdEL`kig+2>Ldf zyTPU{*AeXK{cejo0vzd?H)Krp54YW^t2r`=~g_O(o#d)Dcie&>TX~ zeQ`CXa0^Ay{bLd`?x58~W86W;9Sr^tqH2y*E!0S3cg?=PBH#T(1x}8=w0*}Nl%MhY zR9@iF@yYd9c3*sqI)XO8h_D+01Q0*~0R#|0009ILKmdWx5D4T2UVZRyA76X?`t>$> z0r#?j_w?oX1Y6Dv^!E<6%nS4m^`7nwUY-R42q1s}0v#oAU!U7Ueb5`7L$9{V9{R@6aBg?FuTx(1cAqrMb<6E1Prs)>o|+ikY;ZPpHe*!X8z3)0UVw22 z8?)yHy8eA5(lJk=H%_4zjzcV%ju>z=UFlAw4vN_ylPza|Y)INApD1|&@&X;37wDJ2 zt*E@f!ti5{+;Q!}GI@cH`W=oZg8%{uAb;AUKGM*sl?5I_I{1Q0*~0R#|0prZr=d4b;#{^FgV`r(&-e*ppq+(~&S|N}SEXBF378utn^KiV2 zW#>{QeW?FH)nosK{vG`rdhE5AO1*b>*@<`8U@4baLf;jL-JPBJ&3xtoYmlEmYq-7? z`{U!Gw%W8y#`8@>Tub7uMaI=NAs7@5|uIbV07C3K{uM&ku?b zLRVCmRa0g3j-~2?zO6p8&Ag^M8M&wOXxuxfnx^^I%je3bnfYlWG#iY2&OlMfk?;Cv z$)iHTFPRo7#sCw6V;wBEzqPg>i>kWxfv5#^^XRpn*lq4S+qHr#Biw1LF4~;l-Q29* zvyUd->EPOGw6=sUM&;M($L>jZL6ELa%YHInbo#v%RWXyJ{(Gq3FRYeZ=DeYuq_u0w z<7;ZN=Yt9VI+rV3mCTz@!wKQ`Pwk=XyN!!|Fkhil}GU;mT#fdIyfLMrZQS= z>n}8KT8ueb&XpgimKA2UY#JpIZROHvIzu{~K5tHzrt!)%<8@WvA0}J39L1YX47CKz z>PVe09af(=pJrFfw{B(j!dV|JuAY1~k)xhW9WF%yr>l4>u^LRv+bXB|Lo1UjS1OsG zWmsmcKVrMtL|L%}wSXkn552O*7mxaGr?t~mB|Nd3o!imijB=%ByKZN=SftY816icg zrlVlG#-vu?U>f@FcoZh4()0)p({$a>7oRZMbH5Aorb7DeP^x#$;ZczHV%A_b+n)P6 zd}Fzo+&h?z4t4sRps009ILKmY**5I_I{1Q0+V65#%yp@0Aa2q1s}0tg_000IagfWYDl zaR0yfeM}1xKmY**5I_I{1Q0*~0R#}>{+}^`00IagfB*srAb;zWBOLvQM-O#*ZH0av#M*8l(j diff --git a/db_b/chats.db-shm b/db_b/chats.db-shm deleted file mode 100644 index 15fd3a9fe77fd0158d0932f44ec0517fac950db7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*J4ysW6a~;4zoU*ierFPw;6hx%fQhk-F%~1i*cF%=YcLBC1XCBG?@1}92ExEQ z7mC|7O?TB81PBlyK!5-N0t5&UAV7cs0RjXF5FkK+Km~zM0e~we diff --git a/db_b/chats.db-wal b/db_b/chats.db-wal deleted file mode 100644 index 0cb201b151ec9357d28e238593b3ef5986c28b06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94792 zcmeI*Z)_WP9mjFoN$j|BXjvH^Ek##lQge#eEh}ZJroxi!+C_Dp)^oI`2%RqWbvebc zllUB^1-7*t6Ay|IuxVAb(J?l$!5;94C;XYTjy>Sd)JF)a@@JzG;!lBrCg1^x-|u|q z+})gdjbMx1_o7kgs;mm=@mDZa2EfB*srAb7k+M zlx<9x&Dug)e9+Tt=0e@96vL}sV|i_)pecos(X6I~jvcHRb7n@gR3Dy*Cei}~(ML_^ zoO5Q$GW27XIJ@s|EO0L04rNfWO3KPIo-1hkwY+jDKYnl|KcyVdrj(J%!o+w^oZ+CB zD`b>LYq!;Cc4Ab?O%#;eWHy^orpwjhtloNGv0AasimlHWwHYPUt~g`ZUNfbj9TDei zo>QFCu&rvvd61!fm1@asw(}nQN>gWd&AQ)d8~T>EHD}grV{Tra_w7xkpkA|1n5}mg zXX=$%Z~xm{JG-3+-16krW5;Ty?d}h^R>g}>e$H=RFZk`1r_LVu{j0kC%8_zy8xbu^nf7Ygsz+X2Y^9KI)YpNsY>NlM_0&&BF00IagfB*sr zAb7tXy4@)LWN!TMq$ZN*cWXyP2ETPX zWg_S1?VUvbpm#o%Is)nlGD_Pz0{NM~+sg~w^5nCBy71}Cmo~_s#>S8Yo%?giGeZCY z1Q0*~0R#|0009ILKmdV2ULdQzvN83o({HuO3-tKdWF5kq^8($ex7@sdxWj?~0tg_0 z00IagfB*sryjud5?neK8vSt6hH`bl6|GvlVzi-`f?==W_+(L(?9?In)~{)Nfu7Xon|T3omjwX?5I_I{1Q0*~0R*mNf!Ur!`c6@T-_x=S z-ikQdl}=8O03y65$?ZSn#MACRm=So8P-@zhpd zUO=p{AbfRZ;U})zh3_68uEBRVtKaKGieRR zBkK^>oEJ#+oeSgz#3~B{2q1s}0tg_000IaE1x_RqX-$;ff2eiYy{}i)*Kyx_)sFi> zS8BKb-*-Zlyg%GzkcoYg{3Rx1%hwHtRa8^0tg_000IagfB*srAkd)# zfxN&Y4{iDWN4~f9V{P&R&SeAFL)IazEicgbL?oHK9LftMFZUhoP-&hP0tg_000Iag z@E!}Cs`Vz)sv7;=cBhw5j=5bHrQxFf*0PTW*OGz0zTiGF{kY;*`Z^`O^2>OxpzYW4 z%Ax%D!IAuwazL9>#_rL^4k&}-(7npAno$(5wA^duHS$V@mC4-r;Ym#%Y0R14QSvr4 zTDY=&`|jPtd-VDG^sISY>I$xbD{W zjZ9_>%3!@>Et~Vz;>@5bpGSSZ^v;i0?@Y$i6KXW#^z_#j%9d^FM%}JD>w1t@9}cd| zU%Q>CE_nf~Y}Rz*DbF%>+3_I4{y7AC>Bo-4MnG%pL+KdD0=w4|Q=lbQwP4Wv10tg_000IagfB*srAb6$yDspT(!Q@G|NZ4pP)E?g|KdC~1Q0*~0R#|0009ILKmY**Rs;fd1Y0(L?SZZ1 zFa4@b9YNnpb0^re<~o86z29z8M<6z`Ab}cp!>%pWZXfsiN?5tj5`?oAH=IU@@kJfdB#sAb>ze2^{TndgxCk zoF4kRrHdJtY}30<1$yYct+I!{F*Kah9q#Lt7ros}M!9Y}{p9Mcsd##9+iHWed1upy zy?g!S1;`69?qFl~yg=8#Z$vugDRjpvRKjtH8N(I>ZmN~;M0&57{V~~c_Q!^#Rq~0F z7a%Xtv3Y@%{B6a{3%u~zJN|JxwdW*xfsXnejwgcv0tg_000IagfB*srAb`MX0`d|- zzjs|=d2?(>_b)#%#JGd2aj_i%1Q0*~0R#|0009ILKmdV`5(wl4{(AoWjbnG+aFe`P z5Z!nsBL1)-fB*srAb. - * Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || addrs). + * Хеш мембера = SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig). * * ── Использование ── * @@ -20,12 +20,15 @@ * } * * // добавление/обновление мембера (из chat_core_create_channel, cs_handle_welcome) - * member_sync_put(inst, ch_id, node_id, x25519, ed25519, join_sig, join_ts, addrs, ac); + * member_sync_put(inst, ch_id, node_id, x25519, ed25519, + * join_sig, join_ts, update_sig, update_ts, userinfo, + * adm_tags, adm_tags_sig, storage); * // дерево автоматически пересчитано * * // онлайн-статус (из cs_on_conn_up / cs_on_conn_down) - * member_sync_set_online(inst, node_id, 1); // online - * member_sync_set_online(inst, node_id, 0); // offline + * // только локальная запись; возвращает 1 если изменилось — тогда рассылка: + * if (member_sync_set_online(inst, node_id, 1)) + * merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1); * * // отмена синхронизации (коллбэк НЕ вызывается) * member_sync_cancel(inst, peer, ch_id); @@ -99,7 +102,8 @@ int member_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id); /* - * Добавить/обновить мембера и пересчитать дерево. + * Добавить/обновить мембера (локальная запись, доверенная) и пересчитать дерево. + * Делегирует в member_sync_apply_record() с from_peer = inst->node_id. * * ch_id — ID канала * member_id — node_id участника @@ -108,13 +112,15 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c * join_sig — Ed25519 подпись join-сообщения (64 байта) * сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE) * join_ts — NTP-синхронизированное время подписи (unix epoch seconds) + * update_sig — подпись блока A (64 байта), update_ts — версия блока A * userinfo — метаданные узла (имя) в канале + * adm_tags — атрибуты владельца (JSON с ver/storage), adm_tags_sig — подпись канальным ключом + * storage — производная от adm_tags.storage * - * Пишет только в таблицу peers_ (INSERT OR REPLACE). - * nodes обновляется отдельно через topo_node_sqlite_node_update_verified(). - * Вызывает merkle_sync_recompute_path() для пересчёта дерева. + * Пишет блоки через topo_node_sqlite_member_block_put/member_owner_put, узел — + * через topo_node_sqlite_node_update_verified, затем merkle_sync_recompute_path(). * - * Возвращает 0 при успехе, -1 при ошибке. + * Возвращает маску MS_APPLY_* (0 = no-op) или -1 при ошибке. */ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, const uint8_t* x25519, diff --git a/src/chat/member_sync_doc.md b/src/chat/member_sync_doc.md new file mode 100644 index 00000000..34552cdf --- /dev/null +++ b/src/chat/member_sync_doc.md @@ -0,0 +1,149 @@ +# member_sync — синхронизация участников канала + +## 1. Назначение + +Тонкая прослойка над `merkle_sync`, адаптированная под участников (мемберов) чат-каналов. +Хранит мемберов в SQLite (`peers_`, `nodes`, `node_addresses`), пересчитывает Merkle-дерево +при каждом изменении и через merkle_sync рассылает дельту другим узлам. + +Данные мембера защищены двумя независимо подписанными блоками: +- **Блок A (мембер)** — идентичность и профиль: `node_id`, `x25519`, `ed25519`, `userinfo` (имя), + подпись `update_sig` **ключом самого мембера** (`ed25519`); `ver = update_ts` внутри эпохи `join_ts`. +- **Блок B (владелец)** — атрибуты, управляемые владельцем канала: `adm_tags` (JSON с `ver`, + `storage`), подпись `adm_tags_sig` **канальным ключом** (`ed25519` канала); `ver = adm_tags.ver`. + +Оба блока обновляются независимо — только если их `ver` увеличился. Устаревший блок помечается +stale и автору отправляется свежая запись (send-back). + +## 2. Как пользоваться + +```c +// ── разово (из chat_sync_init) ── +member_sync_init(inst); // merkle_sync_init(0x31) + verify/purge + rebuild + подписка + +// ── подписка на BGP-события канала (BGP → member_sync → node_props_changed) ── +member_sync_subscribe_group(inst, group); // при создании канала + +// ── запуск синхронизации (из cs_on_conn_up) ── +member_sync_start(inst, peer, ch_id, on_done, ch); + +// ── коллбэк завершения ── +static void on_done(uint64_t peer, const char* ch_id, int result, void* arg) { + if (result == MT_OK) ch->synced = CS_SYNC_DONE; +} + +// ── добавление/обновление мембера (локальное, без верификации подписей) ── +member_sync_put(inst, ch_id, node_id, x25519, ed25519, + join_sig, join_ts, update_sig, update_ts, userinfo, + adm_tags, adm_tags_sig, storage); + +// ── онлайн-статус: только локальная запись, возвращает 1 если изменилось ── +if (member_sync_set_online(inst, node_id, 1)) + merkle_sync_push_update(inst, ch_id, node_id, 0x01, (uint8_t[]){1}, 1); // рассылает caller + +// ── отмена (коллбэк не вызовется) ── +member_sync_cancel(inst, peer, ch_id); + +// ── количество мемберов ── +int n = member_sync_count(inst, ch_id); + +// ── разово (из chat_sync_destroy) ── +member_sync_destroy(inst); +``` + +## 3. Модель данных + +- **Таблицы**: `peers_` (per-channel мемберы), `nodes`/`node_addresses` (глобальная + идентичность и адреса, обновляются отдельно через `topo_node_sqlite_*`). +- **Хеш мембера** (участвует в Merkle-дереве): + `SHA256(node_id || x25519 || ed25519 || join_sig || join_ts || update_sig || update_ts || adm_tags_len || adm_tags || adm_tags_sig)`. + Поля `userinfo` (имя) и `source` в хеш **не** входят. +- **Колонка `source`**: `source != 0` — плейсхолдер, созданный topo_group (не верифицирован); + такие записи **исключаются** из хеша и сериализации. `source == 0` — обычный верифицированный + меркл-рекорд. + +### Wire-формат мембера (get_items / apply_items / _serialize_member) + +``` +[count:2][node_id:8][x25519:32][ed25519:32][flags:1] + (flags&HAS_JOIN → [join_sig:64][join_ts:8]) + [update_sig:64][update_ts:8] + [userinfo_len:1][userinfo] + [adm_tags_len:1][adm_tags][adm_tags_sig:64] +``` + +`flags = PEERS_FLAG_HAS_JOIN` — `join_sig`/`join_ts` присутствуют (join-запись). + +## 4. apply_record — по-блочное сравнение и применение + +`member_sync_apply_record(inst, ch_id, from_peer, &rec)` — единая точка записи мембера +(и локальная `put`, и приёмная). Возвращает битовую маску `MS_APPLY_CHANGED | MS_APPLY_STALE`. + +1. **Идентичность** (только для приёмных записей): `node_id == sc_derive_node_id_from_pubkey(x25519)` + и `ed25519` совпадает с доверенным из `nodes`. Несовпадение → forgery, блок A отклоняется. +2. **Блок A** — проверка `update_sig` (или `join_sig` для join-only). Локальная запись + (`from_peer == inst->node_id`) доверенная, без верификации. Сравнение `ver`: + - нет локальной записи → changed; + - `join_ts > local_join_ts` → changed (**re-join / смена ключа**); + - `join_ts < local_join_ts` → stale (старая эпоха); + - иначе по `update_ts`. +3. **Блок B** — проверка `adm_tags_sig` канальным ключом, `adm_storage = (storage=="yes")`. + Сравнение `adm_tags.ver` с локальным. +4. **Запись**: changed-блоки пишутся (`member_block_put` / `member_owner_put`), узел обновляется + через `node_update_verified`, пересчитывается классификация `nodeinfo_updated` (из + `node_addresses`, полученных по BGP), затем `merkle_sync_recompute_path`. +5. **Реальная сверка**: если `recompute_path` вернул 0 (данные идентичны) — `changed` сбрасывается + в 0 (no-op), чтобы не рассылать. + +На приёме (`_member_apply_items`) каждый `MS_APPLY_CHANGED` генерирует `node_props_changed` и +включается в relay (`merkle_sync_broadcast`); `MS_APPLY_STALE` вызывает `member_sync_send_to` +(отправить нашу свежую запись автору). + +## 5. Подписки и коллбэки + +- **`member_sync_subscribe_group(inst, group)`** — подписывает на BGP node-события chat-группы + (`topo_group_add_node_cbk`). При событии BGP (`_ms_on_bgp_node`) рассылается `node_props_changed` + с текущим `adm_tags`. REMOVE **не удаляет** мембера. +- **`member_sync_add_props_cbk/remove_props_cbk`** — многоподписочный `node_props_changed_fn` + (изменились `adm_tags` любого узла, включая себя). +- **`member_sync_set_node_updated_cb`** — одиночный `member_sync_node_updated_fn` (online-статус + узла изменился по инициативе удалённого пира, вызывается из uasync-потока). + +## 6. Верификация и удаление + +Единственный механизм удаления мембера — битая запись: +- `member_sync_verify_local_record` — проверяет привязку `node_id = derive(x25519)`, подписи + `update_sig`/`join_sig`/`adm_tags_sig`. Отсутствие подписи = «не верифицировано» → **не удаляем**. +- `member_sync_verify_and_purge` / `_all` — прогоняют записи канала (всех `peers_%`), битые удаляют + через `topo_node_sqlite_member_del` + `recompute_path`. +- Вызываются при `member_sync_init` и при старте синхронизации канала. Отдельного + `member_sync_del` нет — conn_down ≠ удаление мембера. + +## 7. API + +| Функция | Назначение | +|---|---| +| `member_sync_init(inst)` | `merkle_sync_init(0x31, &g_member_ops, inst)` + verify/purge + rebuild всех деревьев + подписка на существующие chat-группы | +| `member_sync_destroy(inst)` | Отписка от групп, `merkle_sync_destroy` | +| `member_sync_subscribe_group(inst, group)` | Подписка на BGP-события CHAT-группы | +| `member_sync_start(inst, peer, ch_id, done_cb, arg)` | `verify_and_purge` + `merkle_sync_start` | +| `member_sync_cancel(inst, peer, ch_id)` | `merkle_sync_cancel` | +| `member_sync_put(...)` | Локальная запись мембера (trusted) → `apply_record` | +| `member_sync_apply_record(inst, ch_id, from_peer, &rec)` | Приём/применение рекорда (по-блочно) | +| `member_sync_send_to(inst, ch_id, member_id, target_peer)` | Send-back нашей свежей записи конкретному пиру | +| `member_sync_broadcast_one(inst, ch_id, member_id)` | Сериализовать мембера и разослать всем (relay после локального изменения) | +| `member_sync_set_online(inst, node_id, online)` | Записать `nodes.online`. Возвращает 1 (изменилось) / 0 | +| `member_sync_count(inst, ch_id)` | `COUNT(*)` из `peers_` | +| `member_sync_build_join_msg(...)` / `build_update_msg(...)` | Канонические сообщения для подписи/верификации join/update (112 байт и 64+8+userinfo) | +| `member_sync_get_hash(inst, ch_id, level, prefix64)` | Хеш бакета (для тестов) | +| `member_sync_add/remove_props_cbk`, `set_node_updated_cb` | Подписки на изменения | + +## 8. Нюансы + +- **Онлайн-статус не участвует в Merkle-дереве**: не хешируется и не версионируется. `set_online` + только пишет в БД и возвращает 1/0; рассылку `MSG_ITEM_UPDATE` выполняет **caller** (chat_sync). +- **`member_sync_put` = локальный `apply_record`** с `from_peer = inst->node_id` — без верификации + подписей (свои данные доверенные). +- **`source`-плейсхолдеры** topo_group пропускаются (`src != 0`), в дереве не участвуют. +- **Re-join / смена ключа** распознаётся по `join_ts` (перекрывает сравнение `update_ts`). +- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`. diff --git a/src/chat/merkle_sync.h b/src/chat/merkle_sync.h index b9222e4b..a63524be 100644 --- a/src/chat/merkle_sync.h +++ b/src/chat/merkle_sync.h @@ -20,12 +20,13 @@ struct UTUN_INSTANCE; * бакеты и передают только их содержимое. * * merkle_sync не знает, что такое "элемент" — потребитель (member_sync) - * предоставляет три коллбэка, описывающих модель данных: + * предоставляет четыре коллбэка, описывающих модель данных: * * update_bucket_hash — перечислить элементы в диапазоне ключей, * вычислить хеш каждого и подать в SHA256-контекст * get_items — сериализовать элементы бакета в wire-формат * apply_items — десериализовать и сохранить полученные элементы + * apply_update — применить лёгкое обновление (MSG_ITEM_UPDATE) * * Потребитель (chat_sync/chat_core) работает только с member_sync * и не видит merkle_sync напрямую: @@ -120,7 +121,7 @@ struct merkle_sync_data_ops { * *len — [in] размер буфера, [out] записанный размер * * Wire-формат для мемберов: - * [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][addr_cnt:1][addrs:var]... + * [count:2][node_id:8][x25519:32][ed25519:32][flags:1]([join_sig:64][join_ts:8])[update_sig:64][update_ts:8][userinfo_len:1][userinfo:var][adm_tags_len:1][adm_tags:var][adm_tags_sig:64]... * * Возвращает 0 при успехе, <0 при ошибке, -2 если буфер мал. */ @@ -161,7 +162,7 @@ struct merkle_sync_data_ops { * * peer — node_id пира * ns — namespace (channel_id) - * result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = исчерпаны ретраи + * result — MT_OK (0) = успех, MT_ERR_TIMEOUT (-1) = зарезервировано (в протоколе не возникает) * arg — пользовательский контекст * * При отмене через merkle_sync_cancel() коллбэк НЕ вызывается. @@ -199,7 +200,8 @@ void merkle_sync_destroy(struct UTUN_INSTANCE* inst); /* * Запустить синхронизацию namespace ns с пиром peer. - * Отправляет MSG_HASHES(level=1) и запускает таймаут 10s. + * Отправляет MSG_HASHES(level=0). Таймаута нет — сессия завершается при + * совпадении корневого хеша либо удаляется вручную (cancel/destroy). * * Если сессия для (peer, ns) уже существует — перезапускает её * с новым коллбэком (старый коллбэк теряется без вызова). diff --git a/src/chat/merkle_sync_doc.md b/src/chat/merkle_sync_doc.md new file mode 100644 index 00000000..52a297f6 --- /dev/null +++ b/src/chat/merkle_sync_doc.md @@ -0,0 +1,158 @@ +# merkle_sync — синхронизация данных на Merkle-деревьях + +## 1. Назначение + +Универсальный движок синхронизации между двумя узлами. Группирует элементы в префиксное +Merkle-дерево (5 уровней × 32 бакета, SHA256). Два пира обмениваются хешами уровней, находят +различающиеся бакеты и передают **только их содержимое** — вместо полной пересылки всех данных. + +merkle_sync **не знает**, что такое «элемент». Потребитель (member_sync) предоставляет набор +коллбэков `merkle_sync_data_ops`, описывающих модель данных (как перечислить элементы бакета +и посчитать хеш, как сериализовать элементы в wire-формат, как применить полученные). + +Работает поверх надёжного транспорта (ETCP), поэтому **таймаутов и ретраев в протоколе нет**: +сессия завершается при совпадении корневого хеша, либо удаляется вручную (`cancel`/`destroy`). + +## 2. Как пользоваться + +Напрямую merkle_sync обычно не используют — его потребляет member_sync. Типовой сценарий +потребителя выглядит так: + +```c +// ── разово: инициализация (из member_sync_init) ── +merkle_sync_init(inst, 0x31, &ops, data_ctx); // svc_id — ID ETCP-сервиса + +// ── запустил синхронизацию — забыл ── +merkle_sync_start(inst, peer, ns, on_done, my_arg); + +// ── коллбэк завершения ── +static void on_done(uint64_t peer, const char* ns, int result, void* arg) { + if (result == MT_OK) /* хеши сошлись, данные идентичны */; + if (result == MT_ERR_TIMEOUT) /* (зарезервировано, в протоколе не возникает) */; +} + +// ── изменил данные — дерево само пересчиталось ── +int changed = merkle_sync_recompute_path(inst, ns, key); // 1 = изменилось, 0 = no-op + +// ── рассылка дельты остальным пирам (relay) ── +merkle_sync_broadcast(inst, ns, from_peer, data, len); +merkle_sync_push_update(inst, ns, key, type, data, len); // лёгкое обновление (online и т.п.) + +// ── отменил — коллбэк не вызовется ── +merkle_sync_cancel(inst, peer, ns); + +// ── разово: завершение ── +merkle_sync_destroy(inst); +``` + +## 3. Модель данных (коллбэки `merkle_sync_data_ops`) + +| Коллбэк | Что делает | +|---|---| +| `update_bucket_hash(ctx, ns, level, prefix64, sha_ctx)` | Перечислить элементы в префиксном диапазоне, вычислить хеш каждого и подать в `sha_ctx` через `EVP_DigestUpdate(sha_ctx, item_hash, 32)`. Возвращает число элементов (0 = бакет пуст, -1 = ошибка) | +| `get_items(ctx, ns, level, prefix, pbytes, buf, len)` | Сериализовать элементы бакета в wire-формат. Возвращает 0 / -2 (буфер мал) / <0 (ошибка) | +| `apply_items(ctx, ns, from_peer, data, len)` | Десериализовать и сохранить полученные элементы. Должна пересчитать дерево через `merkle_sync_recompute_path` для каждого изменённого ключа | +| `apply_update(ctx, ns, key, type, data, len)` | Применить лёгкое обновление (не влияет на Merkle-хеш) — MSG_ITEM_UPDATE | + +`ctx` — прозрачный контекст, передаваемый в `merkle_sync_init` и возвращаемый первым аргументом +в каждый коллбэк. `ns` (namespace) — строка-имя домена данных, обычно `channel_id`. + +## 4. Формат дерева + +5-уровневое префиксное дерево над 64-битными ключами. Каждый уровень берёт 5 старших бит ключа: +уровень 1 — биты 59-63, уровень 5 — биты 39-63. Каждый узел разбивается на 32 бакета (5 бит = 32). + +``` +level 1 [0..31] корень: 32 бакета по 5 бит +level 2 [0..31]...[0..31] каждый — ещё 32 бакета +... +level 5 [0..31].........[0..31] листья: 32^5 = 33M бакетов макс +``` + +- Хеш бакета = `SHA256(хеш_элемента_1 || ... || хеш_элемента_N)`. +- Бакет считается **терминальным** (leaf) на уровне 5 или если в нём `< 8` элементов. +- Хеши бакетов хранятся в таблице `merkle_tree_hash(namespace, level, prefix64, hash, member_count)`. + +Вспомогательные функции префикса: `merkle_sync_level_prefix(key, level)` (выделить `level*5` +старших бит) и `merkle_sync_prefix_bytes(level)` (байт на хранение префикса: уровень 1 → 1 байт, +уровень 5 → 4 байта). + +## 5. Wire-протокол + +Каждое сообщение: `[svc_id:1][ns_len:1][ns:var][type:1][payload:var]`. + +| Тип | #define | Назначение | +|---|---|---| +| 0x01 | MSG_HASHES | Сравнение бакетов: bitmap + хеши, либо данные (is_data) | +| 0x02 | MSG_REQUEST | Запрос содержимого различающихся бакетов | +| 0x03 | MSG_BATCH | Передача данных/подхешей запрошенных бакетов | +| 0x04 | MSG_ITEM_UPDATE | Лёгкое обновление (online-статус), вне дерева | + +Форматы payload: +- **MSG_HASHES**: `[level:1][prefix_bytes:1][prefix:pb][is_data:1]` + (`[bitmap:4][hash:32]*` | `[dlen:2][data]`) +- **MSG_REQUEST**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1} × count ]` +- **MSG_BATCH**: `[count:1][ {level:1, prefix_bytes:1, prefix:pb, is_terminal:1, [data | bitmap+hashes]} × count ]` +- **MSG_ITEM_UPDATE**: `[key:8][type:1][data]` + +### Алгоритм обмена + +``` +A → MSG_HASHES(level=0, bitmap+хеши всех 32 бакетов уровня 1) → B +B сравнивает со своим деревом, находит различающиеся бакеты (differs) +B → MSG_REQUEST(level=2, prefix=X, is_terminal) → A +A → MSG_BATCH(данные бакета или подхеши) → B +B сохраняет, пересчитывает хеши +Рекурсивно для подбакетов, пока хеши не совпадут. +differs==0 и level==0 → корневой хеш совпал → сессия SYNCED. +``` + +## 6. Сессии и out-очередь (backpressure) + +`struct ms_session` — сессия на пару (peer, ns): `sess_state` (`SESS_SYNCING`/`SESS_SYNCED`), +`pending[128]` (ожидающие бакеты: `MS_PEND_WAITING`/`MS_PEND_RESOLVED`, с родительским индексом +и счётчиком подбакетов), `done_cb`/`cb_arg`, `out_q` + `waiter`. + +**По-пировая очередь рассылки** `out_q`: рассылка (`broadcast`/`push_update`/`send_to`) не шлёт +немедленно, а кладёт каждый элемент в `out_q` подходящей сессии. Дренаж через `ms_outq_drain_cb`: +`queue_data_get(out_q)` → `etcp_send(conn, entry)`. Перед отправкой проверяется занятость +`conn->send_input_q`; при превышении порога — `queue_waiter_wait(send_input_q, &s->waiter, ...)` +приостанавливает дренаж до освобождения (backpressure, без дропов). + +Отправка: +- `_send_msg` — синхронно одному пиру (служебные HASHES/REQUEST/BATCH — по прямому соединению). +- `_broadcast_to_session` — кладёт в `out_q` конкретной сессии (обёртка в MSG_HASHES is_data=1). + +## 7. WAL-таймер + +Отдельный редкий таймер `MS_WAL_INTERVAL_MS = 60000` (60 с) — `sqlite3_wal_checkpoint_v2(..., +PASSIVE)`. Локальная операция без сети. На Android (`UTUN_HAVE_STANDBY`) при спячке откладывается +через `standby_wait`. Периодической сверки деревьев (bg-таймера) нет — пересчёт событийный. + +## 8. API + +| Функция | Назначение | +|---|---| +| `merkle_sync_init(inst, svc_id, ops, data_ctx)` | Создать контекст, `_ensure_table`, `etcp_bind(svc_id, _recv_cb)`, WAL-таймер. `inst->msync = ms` | +| `merkle_sync_destroy(inst)` | Отвязать сервис, отменить WAL-таймер, удалить все сессии (коллбэки не вызываются) | +| `merkle_sync_start(inst, peer, ns, done_cb, arg)` | Создать/перезапустить сессию, отправить MSG_HASHES(level=0). Перезапуск теряет старый коллбэк без вызова | +| `merkle_sync_cancel(inst, peer, ns)` | Удалить сессию, `done_cb` не вызывается | +| `merkle_sync_recompute_path(inst, ns, key)` | Пересчитать все 5 уровней для ключа. Возвращает 1 (изменилось) / 0 (no-op) / -1 | +| `merkle_sync_push_update(inst, ns, key, type, data, len)` | MSG_ITEM_UPDATE всем **SYNCED**-сессиям ns | +| `merkle_sync_broadcast(inst, ns, from_peer, data, len)` | Relay: всем сессиям ns, кроме `from_peer` | +| `merkle_sync_send_to(inst, ns, peer, data, len)` | Данные одному пиру (send-back при stale) | +| `merkle_sync_bg_check(inst, ns)` | Проверить один level-5 бакет (сравнение с БД), при расхождении — пересчёт пути вверх. 1/0/-1 | +| `merkle_sync_get_hash(inst, ns, level, prefix64)` | Прочитать хеш бакета (статический буфер, нули если нет) | +| `merkle_sync_level_prefix(key, level)` / `merkle_sync_prefix_bytes(level)` | Арифметика префикса | + +## 9. Нюансы + +- **svc_id** задаётся при init (member_sync использует `0x31`); единственный приёмник `_recv_cb`. +- **Нет таймаута сессии**: `MT_ERR_TIMEOUT` зарезервирован, но не возникает — сессия живёт до + совпадения корневого хеша или явной отмены. +- **Перезапуск**: `merkle_sync_start` на существующую пару (peer, ns) перезаписывает `done_cb` + (старый коллбэк теряется без вызова). +- **Сессия создаётся и на приёме**: при неожиданном MSG_HASHES от неизвестного пира сессия + создаётся автоматически (symmetrical start). +- **Результат recompute** важен для подавления no-op рассылки: `changed==0` означает, что данные + фактически не изменились. +- Категория логов — `DEBUG_CATEGORY_MEMBER_SYNC`. diff --git a/src/chat/merkle_tz.md b/src/chat/merkle_tz.md index 6f2054ea..e53f27c2 100644 --- a/src/chat/merkle_tz.md +++ b/src/chat/merkle_tz.md @@ -1,5 +1,11 @@ # ТЗ: событийная синхронизация чата (db_sync + merkle_sync) вместо периодического опроса +> **Статус: реализовано.** Это историческое ТЗ. Актуальное описание модулей — в +> `merkle_sync_doc.md` и `member_sync_doc.md` (рядом с исходниками). Ключевые пункты +> реализованы: событийный peer_check (`db_sync_has_pending_work`/`resume`), per-session +> `out_q` с backpressure и отдельный WAL-таймер в merkle_sync, централизованный путь +> «запись → пересчёт → рассылка» в member_sync (коммиты `d62c370`, `d0784a3`, `a4e73ac`, `4e06db7`). + ## 1. Цель После завершения первичной репликации синхронизация должна перейти в **событийный режим**: любое изменение базы немедленно рассылается пирам (push), без периодического таймера-опроса. diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c index b149e163..8a4bdd34 100644 --- a/src/media_delivery/media_delivery.c +++ b/src/media_delivery/media_delivery.c @@ -868,7 +868,7 @@ static void md_handle_block_req(struct media_delivery_ctx* md, uint64_t from_nod if (grp && grp->conn_mgr) { DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: no direct conn to 0x%016llx — connecting via conn_mgr", MD_ID, (unsigned long long)from_node); - conn_mgr_open_invite(grp->instance, grp->group_id, NULL, from_node, NULL, NULL, NULL); + conn_mgr_open(grp->instance, grp->group_id, from_node, NULL, NULL, NULL); } } @@ -1049,7 +1049,7 @@ static void md_super_connect(struct media_delivery_ctx* md, uint64_t peer_node_i DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: connecting to supernode 0x%016llx", MD_ID, (unsigned long long)peer_node_id); - conn_mgr_open_invite(grp->instance, grp->group_id, NULL, peer_node_id, md_super_conn_cb, md, NULL); + conn_mgr_open(grp->instance, grp->group_id, peer_node_id, md_super_conn_cb, md, NULL); } /* ── supernode start/stop ── */ diff --git a/src/media_delivery/media_download.c b/src/media_delivery/media_download.c index c213e7b9..8c193be7 100644 --- a/src/media_delivery/media_download.c +++ b/src/media_delivery/media_download.c @@ -352,8 +352,8 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst, DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: peer[%d] route: grp=%p conn_mgr=%p", MDL_ID, pi, (void*)grp, grp ? (void*)grp->conn_mgr : NULL); if (grp && grp->conn_mgr) { - int cm_rc = conn_mgr_open_invite(inst, grp->group_id, NULL, dl->peers[pi].node_id, md_dl_conn_cb, dl, NULL); - if (cm_rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: conn_mgr_open_invite failed rc=%d for 0x%016llx", MDL_ID, cm_rc, (unsigned long long)dl->peers[pi].node_id); + int cm_rc = conn_mgr_open(inst, grp->group_id, dl->peers[pi].node_id, md_dl_conn_cb, dl, NULL); + if (cm_rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: conn_mgr_open failed rc=%d for 0x%016llx", MDL_ID, cm_rc, (unsigned long long)dl->peers[pi].node_id); } else for (int bi = 0; bi < dl->peers[pi].num_blocks; bi++) { DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "%s: direct BLOCK_REQ: peer-local bi=%d peer_num_blocks=%d", diff --git a/src/media_delivery/sync.md b/src/media_delivery/sync.md index b08a4f02..73183629 100644 --- a/src/media_delivery/sync.md +++ b/src/media_delivery/sync.md @@ -34,7 +34,7 @@ last_recv_id INTEGER — максимальный id, подтве Механизмы обнаружения: -- **При старте** (`md_super_start`, стр. 730): суперузел ищет в БД все узлы с `node_type=4` в своей chat-группе и инициирует прямое подключение к каждому через `conn_mgr_open_invite`. +- **При старте** (`md_super_start`, стр. 730): суперузел ищет в БД все узлы с `node_type=4` в своей chat-группе и инициирует прямое подключение к каждому через `conn_mgr_open`. - **При появлении нового узла в BGP** (`md_on_bgp_node`, стр. 790): проверяет `node_type=4` в `peers_` — если да, вызывает `md_super_connect`. - **При изменении свойств** (`md_on_props_changed`, стр. 825): если `adm_tags` изменились на `supernode=yes` — подключается; если на `no` — удаляет пира. diff --git a/src/routing_layer/conn_mgr.h b/src/routing_layer/conn_mgr.h index 0033e38f..29250d1d 100644 --- a/src/routing_layer/conn_mgr.h +++ b/src/routing_layer/conn_mgr.h @@ -28,14 +28,12 @@ struct TOPO_GROUP; * существует группа. Внутри: кеш entries (по одной на целевой узел), список * handle'ов (по одному на каждый вызов open), фоновые ping/idle/мониторинг. * - * === Единая точка входа: conn_mgr_open_invite === + * === Единая точка входа: conn_mgr_open === * - * ni != NULL — invite-режим: DIRECT + INVITE_INFO_REQ/RESP, группа создаётся. - * Коллбэк: CONN_EVENT_JOIN (успех) или CONN_EVENT_TIMEOUT (ошибка). - * ni == NULL — обычное 3-фазное подключение (DIRECT→REVERSE→INDIRECT). - * Коллбэк: CONN_EVENT_UP (успех) или CONN_EVENT_TIMEOUT (провал). + * 3-фазное подключение (DIRECT→REVERSE→INDIRECT). + * Коллбэк: CONN_EVENT_UP (успех) или CONN_EVENT_TIMEOUT (провал). * - * === 3 фазы (когда ni == NULL) === + * === 3 фазы === * * 1. DIRECT: node_conn_direct на все адреса, NAT-фильтрация, LAN broadcast ping. * 2. REVERSE: у нас прямой IP — шлём DIRECT_REQ цели через BGP. @@ -51,22 +49,20 @@ struct TOPO_GROUP; * * Несколько handle на один ETCP_CONN. conn_mgr_close удаляет ОДИН handle. * Последний handle: DIRECT/REVERSE — DISCONNECT + NCD CLOSE. - * Invite — только NCD CLOSE. */ enum conn_mgr_event { CONN_EVENT_UP = 0, /* связь есть. handle жив, можно отправлять данные */ CONN_EVENT_DOWN = 1, /* временный обрыв. handle жив, само восстановится. НЕ закрывать */ CONN_EVENT_TIMEOUT = 2, /* подключение не удалось. handle жив, закройте сами */ - CONN_EVENT_JOIN = 3, /* invite принят, группа создана. handle жив */ }; /** * Единый коллбэк для всех операций. * @param h handle (NULL при ошибках до создания handle) - * @param node_id целевой узел (inviter при invite, target при обычном open) + * @param node_id целевой узел * @param group_id группа - * @param event UP / DOWN / TIMEOUT / JOIN + * @param event UP / DOWN / TIMEOUT */ typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, @@ -76,31 +72,23 @@ typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h, struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group); void conn_mgr_destroy(struct CONN_MGR* mgr); -void conn_mgr_direct_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); /* ═══════ единая точка входа ═══════ */ /** - * Единственная публичная функция подключения. + * Единственная публичная функция подключения — 3-фазное (DIRECT→REVERSE→INDIRECT). + * Используется медиа-доставкой. Пиры/группы подключаются через node_conn_direct, + * invite — через topo_group_invite. * - * ni != NULL → invite: DIRECT + INVITE_INFO_REQ/RESP, группа создаётся, - * коллбэк CONN_EVENT_JOIN при успехе. - * ni == NULL → обычное 3-фазное подключение, коллбэк CONN_EVENT_UP при успехе. - * - * Ошибка/таймаут в обоих случаях: CONN_EVENT_TIMEOUT. + * Ошибка/таймаут: CONN_EVENT_TIMEOUT. * node_id и group_id ВСЕГДА валидны в коллбэке (даже при TIMEOUT). - * - * @param group_id группа (channel_id из invite-ссылки) - * @param ni inviter (caller-owned, копируется). NULL — поиск по node_id. - * @param node_id inviter'а (нужен когда ni == NULL) */ -int conn_mgr_open_invite(struct UTUN_INSTANCE* inst, - uint64_t group_id, - struct TOPO_NODE* ni, - uint64_t node_id, - conn_mgr_cb_t cb, void* cb_arg, - struct CONN_MGR_HANDLE** out_handle); +int conn_mgr_open(struct UTUN_INSTANCE* inst, + uint64_t group_id, + uint64_t node_id, + conn_mgr_cb_t cb, void* cb_arg, + struct CONN_MGR_HANDLE** out_handle); /** Закрыть handle. Последний handle рвёт соединение. */ void conn_mgr_close(struct CONN_MGR_HANDLE* h); diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 33de4c26..24628bba 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -21,9 +21,7 @@ #include "topo_node.h" #include "topo_node_sqlite.h" #include "topo_group.h" -#include "../transport_layer/etcp_connect.h" #include "route_ping.h" -#include "../chat/chat_core.h" #ifdef UTUN_HAVE_STANDBY #include "standby.h" #endif @@ -31,9 +29,6 @@ /* ═══════ forward-декларации ═══════ */ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); -static void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len); -static void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len); -static void cm_invite_cancel(struct cm_invite_pending* inv); /* ═══════ утилиты ═══════ */ @@ -46,13 +41,6 @@ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint6 return h; } -static struct CONN_MGR_HANDLE* cm_invite_handle_new(conn_mgr_cb_t cb, void* cb_arg, uint64_t node_id, uint64_t group_id) { - struct CONN_MGR_HANDLE* h = u_calloc(1, sizeof(*h)); - if (!h) return NULL; - h->node_id = node_id; h->group_id = group_id; h->cb = cb; h->cb_arg = cb_arg; - return h; -} - static void cm_deliver_up_cb(void* arg) { struct CONN_MGR_HANDLE* h = (struct CONN_MGR_HANDLE*)arg; h->deliver_up_token = NULL; @@ -301,8 +289,6 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void } } -/* NCD → invite: UP (отправляем INVITE_INFO_REQ для проверки членства), - * TIMEOUT/DOWN (фейлим invite с CONN_EVENT_TIMEOUT). */ /* ═══════ init / destroy ═══════ */ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { @@ -324,8 +310,8 @@ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { void conn_mgr_destroy(struct CONN_MGR* mgr) { if (!mgr) return; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p invite=%p exch=%p rev=%p", - mgr, mgr->entries, mgr->invite_list, mgr->exchange_pending, mgr->reverse_pending); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 0 enter mgr=%p entries=%p exch=%p rev=%p", + mgr, mgr->entries, mgr->exchange_pending, mgr->reverse_pending); if (mgr->bg_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->bg_ping_timer); mgr->bg_ping_timer = NULL; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 1 bg_ping done"); if (mgr->candidate_ping_timer) { uasync_cancel_timeout(mgr->instance->ua, mgr->candidate_ping_timer); mgr->candidate_ping_timer = NULL; } @@ -340,9 +326,6 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) { int clean = 0; while (e) { struct ll_entry* next = e->next; struct CONN_MGR_ENTRY* entry = (struct CONN_MGR_ENTRY*)e; cm_entry_cleanup(entry); clean++; e = next; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 3 entries cleaned (%d)", clean); - int inv_clean = 0; - while (mgr->invite_list) { cm_invite_cancel(mgr->invite_list); inv_clean++; } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[CM_DESTROY] 4 invite_list cleaned (%d)", inv_clean); while (mgr->exchange_pending) { struct cm_exchange_pending* ep = mgr->exchange_pending; mgr->exchange_pending = ep->next; if (ep->timer) uasync_cancel_timeout(mgr->instance->ua, ep->timer); @@ -358,8 +341,8 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) { /* ═══════ публичное API ═══════ */ -int conn_mgr_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, - conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle) { +int cm_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, + conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle) { if (out_handle) *out_handle = NULL; if (!mgr) { if (cb) cb(NULL, node_id, 0, CONN_EVENT_TIMEOUT, cb_arg); return -1; } struct TOPO_GROUP* group = mgr->group; @@ -749,394 +732,14 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry } } -void conn_mgr_direct_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { - if (!entry || !entry->dgram || entry->len < 3) return; - uint8_t* d = entry->dgram + 1; size_t len = entry->len - 1; uint8_t sub = d[1]; - struct CONN_MGR* mgr = conn->instance->conn_mgr; - switch (sub) { - case CM_SUBCMD_INVITE_INFO_REQ: if (mgr && len >= sizeof(struct CM_INVITE_REQ)) cm_handle_invite_info_req(mgr, conn, d, len); break; - case CM_SUBCMD_INVITE_INFO_RESP: if (mgr) cm_handle_invite_info_resp(mgr, conn, d, len); break; - } -} - -/* ═══════ invite обработчики ═══════ */ - -/* Формирует полный INVITE_INFO_RESP: если сервер участник канала — возвращает - * is_member=1 + ВСЕ поля канала (name, owner, ключи, ch_sig, join/update_sig). - * Если не участник — is_member=0. Попутно создаёт инфраструктуру группы через - * chat_core_ensure_channel_ready, чтобы группа была готова к маршрутизации сразу. */ -void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len) { - if (len < sizeof(struct CM_INVITE_REQ)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_REQ too short (%zu)", len); return; } - struct CM_INVITE_REQ* req = (struct CM_INVITE_REQ*)data; - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ from=%016llx group=%016llx", - (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id); - - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id); - - /* данные узла-ответчика для ed_pubkey и node_name */ - struct TOPO_GROUP* grp = topo_groups_find(mgr->instance->topo_groups, req->group_id); - struct TOPO_GROUP* sg = grp ? grp : mgr->group; - struct TOPO_NODE* ln = sg->local_node ? topo_node_registry_find(sg->instance->topo_groups, sg->local_node->node_id) : NULL; - const char* nm = ln ? ln->node_name : NULL; size_t nl = nm ? strlen(nm) : 0; if (nl > 255) nl = 255; - - /* ── ищем канал в SQLite ── */ - sqlite3* db = mgr->instance->topo_sqlite_db; - char ch_name[128] = ""; uint64_t owner = 0; - uint8_t ch_x25519[32] = {0}, ch_ed_pub[32] = {0}, ch_sig[64] = {0}; - int have_channel = db && topo_node_sqlite_channel_get(db, ch_id, ch_name, (int)sizeof(ch_name), - &owner, ch_x25519, ch_ed_pub, ch_sig) == 0; - - if (!have_channel) { - size_t rs = CM_INVITE_RESP_HDR_SIZE + nl; - struct CM_INVITE_RESP* resp = u_calloc(1, rs); if (!resp) return; - resp->cmd = ETCP_RT_ID_CONN_MGR; resp->subcmd = CM_SUBCMD_INVITE_INFO_RESP; - resp->group_id = req->group_id; resp->is_member = 0; - memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE); - resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); - struct ll_entry* qe = queue_entry_new(0); - if (qe) { size_t new_len = rs + 1; uint8_t* buf = u_malloc(new_len); - if (buf) { buf[0]=ETCP_RT_ID_CONN_MGR; buf[1]=ETCP_RT_ID_CONN_MGR; buf[2]=CM_SUBCMD_INVITE_INFO_RESP; - memcpy(buf + 3, (uint8_t*)resp + 2, rs - 2); qe->dgram = buf; qe->len = (uint16_t)new_len; etcp_send(conn, qe); } - else { queue_entry_free(qe); } - } - u_free(resp); - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: group=%016llx NOT in DB — responding is_member=NO to 0x%016llx", - (unsigned long long)req->group_id, (unsigned long long)conn->peer_node_id); - return; - } - - /* ── участник: генерируем join_sig / update_sig ── */ - uint64_t myid = mgr->instance->node_id; - uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0; - { - if (db && topo_node_sqlite_member_get_join(db, ch_id, myid, my_join_sig, &my_join_ts) != 0) { - uint8_t join_msg[256]; size_t mlen = 0; - memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32; - memcpy(join_msg + mlen, ch_ed_pub, 32); mlen += 32; - memcpy(join_msg + mlen, &myid, 8); mlen += 8; - memcpy(join_msg + mlen, mgr->instance->my_keys.public_key, 32); mlen += 32; - my_join_ts = (uint64_t)ntp_time_get_seconds(mgr->instance); - memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8; - if (sc_ed25519_sign(mgr->instance->my_ed25519_privkey, join_msg, mlen, my_join_sig) != SC_OK) - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: join_sig sign FAILED myid=%016llx ch=%s", (unsigned long long)myid, ch_id); - else - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig NEW myid=%016llx ts=%llu ch=%s", - (unsigned long long)myid, (unsigned long long)my_join_ts, ch_id); - } else { - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig REUSED myid=%016llx ts=%llu ch=%s", - (unsigned long long)myid, (unsigned long long)my_join_ts, ch_id); - } - } - - uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts = 0; - { - char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", - mgr->instance->name[0] ? mgr->instance->name : ""); - uint8_t umsg[256]; size_t ulen = 0; - memcpy(umsg + ulen, my_join_sig, 64); ulen += 64; - my_update_ts = (uint64_t)ntp_time_get_seconds(mgr->instance); - memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8; - size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1; - if (sc_ed25519_sign(mgr->instance->my_ed25519_privkey, umsg, ulen, my_update_sig) != SC_OK) - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: update_sig sign FAILED myid=%016llx", (unsigned long long)myid); - } - - /* ── создаём инфраструктуру группы СРАЗУ (topo_group, db_sync, msg-таблицу) ── */ - chat_core_ensure_channel_ready(ch_id); - - /* ── сериализуем ответ ── */ - size_t ch_nl = strlen(ch_name); if (ch_nl > 255) ch_nl = 255; - uint8_t inv_flags = (my_join_sig[0] || my_join_ts) ? PEERS_FLAG_HAS_JOIN : 0; - char userinfo[256]; size_t userinfo_len = (size_t)snprintf(userinfo, sizeof(userinfo), - "{\"name\":\"%s\"}", mgr->instance->name[0] ? mgr->instance->name : ""); - - size_t ch_info_len = 1 + ch_nl + 8 + 32 + 32 + 64 + 1 - + (inv_flags & PEERS_FLAG_HAS_JOIN ? 72 : 0) - + 64 + 8 + 1 + userinfo_len; - size_t rs = CM_INVITE_RESP_HDR_SIZE + nl + ch_info_len; - uint8_t* rb = u_malloc(rs); if (!rb) return; - memset(rb, 0, rs); - - { struct CM_INVITE_RESP* resp = (struct CM_INVITE_RESP*)rb; - resp->cmd = ETCP_RT_ID_CONN_MGR; resp->subcmd = CM_SUBCMD_INVITE_INFO_RESP; - resp->group_id = req->group_id; resp->is_member = 1; - memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE); - resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); } - - uint8_t* ch = rb + CM_INVITE_RESP_HDR_SIZE + nl; - *ch++ = (uint8_t)ch_nl; memcpy(ch, ch_name, ch_nl); ch += ch_nl; - memcpy(ch, &owner, 8); ch += 8; - memcpy(ch, ch_x25519, 32); ch += 32; - memcpy(ch, ch_ed_pub, 32); ch += 32; - memcpy(ch, ch_sig, 64); ch += 64; - *ch++ = inv_flags; - if (inv_flags & PEERS_FLAG_HAS_JOIN) { memcpy(ch, my_join_sig, 64); ch += 64; memcpy(ch, &my_join_ts, 8); ch += 8; } - memcpy(ch, my_update_sig, 64); ch += 64; - memcpy(ch, &my_update_ts, 8); ch += 8; - *ch++ = (uint8_t)userinfo_len; memcpy(ch, userinfo, userinfo_len); - - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "invite: membership CHECK OK from=0x%016llx group=%016llx ch_name=%.*s owner=%016llx " - "ch_x25519=%016llx... ch_ed=%016llx... inv_flags=%u join_ts=%llu upd_ts=%llu my_name=%s → is_member=YES full_ch_info (%zu+%zu=%zuB)", - (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id, - (int)ch_nl, ch_name, (unsigned long long)owner, - *(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed_pub, - (unsigned)inv_flags, (unsigned long long)my_join_ts, (unsigned long long)my_update_ts, - mgr->instance->name[0] ? mgr->instance->name : "?", - CM_INVITE_RESP_HDR_SIZE + nl, ch_info_len, rs); - - { struct ll_entry* qe = queue_entry_new(0); - if (qe) { size_t dgram_len = rs + 1; uint8_t* dgram = u_malloc(dgram_len); - if (dgram) { dgram[0]=ETCP_RT_ID_CONN_MGR; dgram[1]=ETCP_RT_ID_CONN_MGR; dgram[2]=CM_SUBCMD_INVITE_INFO_RESP; - memcpy(dgram + 3, rb + 2, rs - 2); qe->dgram = dgram; qe->len = (uint16_t)dgram_len; etcp_send(conn, qe); } - else { queue_entry_free(qe); } - } - } - u_free(rb); -} - -void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len) { - if (len < CM_INVITE_RESP_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP too short (%zu < %zu)", len, CM_INVITE_RESP_HDR_SIZE); return; } - struct CM_INVITE_RESP* resp = (struct CM_INVITE_RESP*)data; - uint8_t nnl = resp->node_name_len; - if (len < CM_INVITE_RESP_HDR_SIZE + (size_t)nnl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP truncated (need %zu, got %zu)", CM_INVITE_RESP_HDR_SIZE + (size_t)nnl, len); return; } - - /* ── ищем ожидающий invite ── */ - struct cm_invite_pending* inv = NULL; - { struct TOPO_GROUP* grp = topo_groups_find(mgr->instance->topo_groups, resp->group_id); - if (grp && grp->conn_mgr) - for (struct cm_invite_pending* p = grp->conn_mgr->invite_list; p; p = p->next) - if (p->state == CM_INVITE_WAIT_INFO && (!conn->peer_node_id || p->node_id == conn->peer_node_id)) { inv = p; break; } - } - if (!inv) { for (struct cm_invite_pending* p = mgr->invite_list; p; p = p->next) if (p->state == CM_INVITE_WAIT_INFO) { inv = p; break; } } - if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP — no pending invite in WAIT_INFO state"); return; } - - if (!resp->is_member) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id); - cm_invite_fail(inv); return; - } - - /* ── сохраняем Ed25519 pubkey ответчика для NODEINFO-верификации ── */ - if (inv->temp_nq) { - struct TOPO_NODE* ni2 = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->temp_nq->node_id); - if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE); - } - - /* ── парсим channel_info (новое) ── */ - const uint8_t* ch = data + CM_INVITE_RESP_HDR_SIZE + nnl; - size_t ch_remain = len - (CM_INVITE_RESP_HDR_SIZE + nnl); - if (ch_remain < CM_INVITE_RESP_CH_MIN_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP channel info too short (%zu < %zu)", ch_remain, CM_INVITE_RESP_CH_MIN_SIZE); cm_invite_fail(inv); return; } - - uint8_t c_nl = *ch++; ch_remain--; - if (ch_remain < (size_t)c_nl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_name truncated"); cm_invite_fail(inv); return; } - char ch_name[128]; size_t cn = c_nl > 127 ? 127 : c_nl; memcpy(ch_name, ch, cn); ch_name[cn] = '\0'; ch += c_nl; ch_remain -= c_nl; - - uint64_t ch_owner; memcpy(&ch_owner, ch, 8); ch += 8; ch_remain -= 8; - const uint8_t* ch_x25519 = ch; ch += 32; ch_remain -= 32; - const uint8_t* ch_ed_pub = ch; ch += 32; ch_remain -= 32; - const uint8_t* ch_sig = ch; ch += 64; ch_remain -= 64; - - if (ch_remain < 1) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at inv_flags"); cm_invite_fail(inv); return; } - uint8_t inv_flags = *ch++; ch_remain--; - - const uint8_t* join_sig = NULL; uint64_t join_ts = 0; - if (inv_flags & PEERS_FLAG_HAS_JOIN) { - if (ch_remain < 72) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at join_sig"); cm_invite_fail(inv); return; } - join_sig = ch; ch += 64; memcpy(&join_ts, ch, 8); ch += 8; ch_remain -= 72; - } - - if (ch_remain < 73) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at update"); cm_invite_fail(inv); return; } - const uint8_t* upd_sig = ch; ch += 64; uint64_t upd_ts; memcpy(&upd_ts, ch, 8); ch += 8; ch_remain -= 72; - - uint8_t ui_len = *ch++; ch_remain--; - char ch_userinfo[256] = ""; - if (ui_len > 0 && ch_remain >= ui_len) { size_t ul = ui_len > 255 ? 255 : ui_len; memcpy(ch_userinfo, ch, ul); ch_userinfo[ul] = '\0'; } - - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)resp->group_id); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "invite: INFO_RESP from=0x%016llx group=%016llx node_name=%.*s " - "ch_name=%s ch_owner=%016llx ch_x25519=%016llx... ch_ed=%016llx... " - "inv_flags=%u join_ts=%llu upd_ts=%llu userinfo=%s", - (unsigned long long)conn->peer_node_id, (unsigned long long)resp->group_id, - (int)nnl, nnl ? (const char*)resp->node_name : "?", - ch_name, (unsigned long long)ch_owner, - *(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed_pub, - (unsigned)inv_flags, (unsigned long long)join_ts, (unsigned long long)upd_ts, - ch_userinfo[0] ? ch_userinfo : "?"); - - /* ── верифицируем ch_sig ── */ - { - uint8_t vmsg[1024]; size_t vlen = 0; - vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1; - vlen += (size_t)snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_name) + 1; - memcpy(vmsg + vlen, &ch_owner, 8); vlen += 8; - memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32; - memcpy(vmsg + vlen, ch_ed_pub, 32); vlen += 32; - if (sc_ed25519_verify(ch_ed_pub, vmsg, vlen, ch_sig) != SC_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY FAIL — rejecting group=%016llx from=0x%016llx", - (unsigned long long)resp->group_id, (unsigned long long)conn->peer_node_id); - cm_invite_fail(inv); return; - } - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY OK"); - } - - /* ── сохраняем канал в БД и создаём ПОЛНОЦЕННУЮ группу (topo_group + db_sync + msg-таблицу) ── */ - { - sqlite3* ldb = inv->mgr->instance->topo_sqlite_db; - if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner, - ch_x25519, NULL, ch_ed_pub, NULL, ch_sig); - } - chat_core_ensure_channel_ready(ch_id); - - /* ── верифицируем update_sig приглашающего ── */ - { - const uint8_t* inv_ed = conn->peer_ed25519_pubkey; - uint8_t ivmsg[256]; size_t ilen = 0; - if (join_sig && (inv_flags & PEERS_FLAG_HAS_JOIN)) { memcpy(ivmsg + ilen, join_sig, 64); ilen += 64; } - else { memset(ivmsg + ilen, 0, 64); ilen += 64; } - memcpy(ivmsg + ilen, &upd_ts, 8); ilen += 8; - { const char* nm2 = ch_userinfo[0] ? ch_userinfo : ""; - size_t nl2 = strlen(nm2); memcpy(ivmsg + ilen, nm2, nl2 + 1); ilen += nl2 + 1; } - if (sc_ed25519_verify(inv_ed, ivmsg, ilen, upd_sig) != SC_OK) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY FAIL node=0x%016llx", (unsigned long long)conn->peer_node_id); - } else { - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY OK"); - } - } - - if (inv->overall_timer) { uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL; } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" ch=%s — full channel ready", - (unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id); - if (inv->cb) inv->cb(inv->handle, inv->node_id, inv->mgr->group->group_id, CONN_EVENT_JOIN, inv->cb_arg); - if (inv->invite_conn) { - /* переиспользуем существующий conn через ncd: он добавляет недостающие линки - * (ncd_create_links с дедупом по etcp_link_find_by_addr) */ - struct TOPO_NODE* ni = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->node_id); - struct CONN_MGR_ENTRY* entry = cm_ensure_entry(inv->mgr, inv->node_id); - if (ni && entry) { - entry->state = CONN_MGR_STATE_CONNECTED; - entry->conn_type = CONN_TYPE_DIRECT; - if (inv->handle && !inv->handle->entry) { - inv->handle->entry = entry; - inv->handle->next = entry->handles; - entry->handles = inv->handle; - } - node_conn_direct_open_node(inv->mgr->instance, inv->node_id, - cm_ncd_callback, entry, &entry->ncd_handle, ni, NULL); - } else { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] cannot promote conn: ni=%p entry=%p node=0x%016llx", - (void*)ni, (void*)entry, (unsigned long long)inv->node_id); - } - inv->invite_conn = NULL; - } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] conn transferred to ncd, calling cancel"); - inv->temp_nq = NULL; - cm_invite_cancel(inv); -} - -void cm_invite_overall_timeout(void* arg) { - struct cm_invite_pending* inv = (struct cm_invite_pending*)arg; - if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: overall timer IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; } - inv->overall_timer = NULL; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: overall TIMEOUT to 0x%016llx — no response within %ds", - (unsigned long long)inv->node_id, CM_INVITE_DEFAULT_TIMEOUT_MS/1000); - cm_invite_fail(inv); -} - -/* Cleanup invite resources WITHOUT calling external callbacks. - * Safe to call during teardown/destroy. */ -static void cm_invite_free_deferred(void* arg) { - struct cm_invite_pending* inv = (struct cm_invite_pending*)arg; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_FREE] freeing inv=%p node=0x%016llx", inv, (unsigned long long)inv->node_id); - u_free(inv); -} - -static void cm_invite_cancel(struct cm_invite_pending* inv) { - if (!inv) return; - inv->cancelled = 1; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv=%p node=0x%016llx mgr=%p conn=%p", - inv, (unsigned long long)inv->node_id, inv->mgr, (void*)inv->invite_conn); - if (!inv->mgr) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr is NULL!"); goto inv_cancel_free; } - if (!inv->mgr->instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr->instance is NULL!"); goto inv_cancel_free; } - if (inv->overall_timer) { - uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] overall_timer cancelled"); - } - /* гасим висящий etcp_connect (если ещё идёт) — иначе его settle-таймер дёрнет - * cm_invite_etcp_cb с освобождённым inv (use-after-free) */ - etcp_connect_cancel_by_node(inv->mgr->instance, inv->node_id); - if (inv->temp_nq) { - queue_remove_data(inv->mgr->group->nodes, &inv->temp_nq->ll); - topo_nodeq_free_group_fields(inv->mgr->instance->topo_groups, inv->temp_nq); - queue_entry_free(&inv->temp_nq->ll); inv->temp_nq = NULL; - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] temp_nq removed"); - } - { struct cm_invite_pending** pp = &inv->mgr->invite_list; - while (*pp) { if (*pp == inv) { *pp = inv->next; break; } pp = &(*pp)->next; } - } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] unlinked from invite_list"); -inv_cancel_free: - if (inv->mgr && inv->mgr->instance && inv->mgr->instance->ua) - { DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] posting deferred free inv=%p", inv); - uasync_post(inv->mgr->instance->ua, cm_invite_free_deferred, inv); } - else - { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] no ua — immediate free inv=%p", inv); - u_free(inv); } -} - -/* Cancel invite AND notify caller. For normal operation (timeout, error, invite failure). - * Not safe during teardown — use cm_invite_cancel for destroy paths. */ -void cm_invite_fail(struct cm_invite_pending* inv) { - if (!inv) return; - if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "cm_invite_fail: IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; } - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "cm_invite_fail: node=0x%016llx state=%d conn=%p", - (unsigned long long)inv->node_id, (int)inv->state, - (void*)inv->invite_conn); - conn_mgr_cb_t cb = inv->cb; - struct CONN_MGR_HANDLE* handle = inv->handle; - uint64_t node_id = inv->node_id; - uint64_t group_id = inv->mgr->group->group_id; - void* cb_arg = inv->cb_arg; - cm_invite_cancel(inv); - if (cb) cb(handle, node_id, group_id, CONN_EVENT_TIMEOUT, cb_arg); - if (handle) u_free(handle); -} - -void cm_invite_cleanup(struct cm_invite_pending* inv) { if (inv) u_free(inv); } - -/* ── etcp_connect callback for invite ── */ - -static void cm_invite_etcp_cb(void* arg, struct ETCP_CONN* conn, int event) { - struct cm_invite_pending* inv = (struct cm_invite_pending*)arg; - if (!inv || inv->cancelled) return; - if (event != ETCP_CONNECT_LATE && event != ETCP_CONNECT_EARLY) return; - if (inv->state != CM_INVITE_CONNECTING) return; - if (!conn) { cm_invite_fail(inv); return; } - - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: etcp_connect %s for 0x%016llx — sending membership check", - event == ETCP_CONNECT_LATE ? "LATE" : "EARLY", (unsigned long long)inv->node_id); - inv->invite_conn = conn; - inv->state = CM_INVITE_WAIT_INFO; - { uint8_t buf[11]; - buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ; - memcpy(buf + 3, &inv->mgr->group->group_id, 8); - struct ll_entry* qe = queue_entry_new(0); - if (!qe) { cm_invite_fail(inv); return; } - qe->dgram = u_malloc(sizeof(buf)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; } - memcpy(qe->dgram, buf, sizeof(buf)); qe->len = sizeof(buf); etcp_send(conn, qe); - } -} - -int conn_mgr_open_invite(struct UTUN_INSTANCE* inst, - uint64_t group_id, - struct TOPO_NODE* ni, - uint64_t node_id, - conn_mgr_cb_t cb, void* cb_arg, - struct CONN_MGR_HANDLE** out_handle) { +int conn_mgr_open(struct UTUN_INSTANCE* inst, + uint64_t group_id, + uint64_t node_id, + conn_mgr_cb_t cb, void* cb_arg, + struct CONN_MGR_HANDLE** out_handle) { if (out_handle) *out_handle = NULL; if (!inst || !inst->topo_groups || !cb) return -1; - /* 1. Найти или создать группу → mgr */ struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); if (!group) { char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); @@ -1146,68 +749,5 @@ int conn_mgr_open_invite(struct UTUN_INSTANCE* inst, struct CONN_MGR* mgr = group->conn_mgr; if (!mgr) return -1; - /* 2. ni == NULL → обычное 3-фазное подключение */ - if (!ni) return conn_mgr_open(mgr, node_id, 0, cb, cb_arg, out_handle); - - /* 3. ni != NULL → invite-режим */ - uint64_t nid = ni->node_id; - if (!nid || nid == inst->node_id) return -1; - if (!ni->v4_addrs && !ni->v6_addrs) return -1; - if (!ni->public_key[0]) return -1; - - /* 4. Копировать ni в group_ni */ - struct TOPO_GROUPS* groups = inst->topo_groups; - struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1; - gni->node_id=nid; gni->ver=1; gni->group_id=group_id; memcpy(gni->public_key,ni->public_key,SC_PUBKEY_SIZE); - for (const struct TOPO_ADDR4* src = ni->v4_addrs; src; src = src->next) { - struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue; - memset(a4,0,sizeof(*a4)); memcpy(a4->addr,src->addr,4); a4->port=src->port; - a4->type=TOPO_ADDR_NAT; a4->protocol=src->protocol; a4->next=gni->v4_addrs; gni->v4_addrs=a4; - } - for (const struct TOPO_ADDR6* src6 = ni->v6_addrs; src6; src6 = src6->next) { - struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue; - memset(a6,0,sizeof(*a6)); memcpy(a6->addr,src6->addr,16); a6->port=src6->port; - a6->type=TOPO_ADDR_NAT; a6->protocol=src6->protocol; a6->next=gni->v6_addrs; gni->v6_addrs=a6; - } - if (!gni->v4_addrs && !gni->v6_addrs) { u_free(gni); return -1; } - gni = topo_node_registry_store(groups, gni); if (!gni) return -1; - - /* 5. TOPO_GROUP_NODE + invite_pending */ - struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE)); - if (!qe) { topo_node_registry_unref(groups,gni->node_id); return -1; } - struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)qe; - memset((uint8_t*)nq+sizeof(struct ll_entry),0,sizeof(*nq)-sizeof(struct ll_entry)); nq->node_id=nid; - - struct cm_invite_pending* inv = u_calloc(1, sizeof(*inv)); - if (!inv) { queue_entry_free(qe); topo_node_registry_unref(groups,gni->node_id); return -1; } - inv->mgr=mgr; inv->node_id=nid; inv->state=CM_INVITE_CONNECTING; - inv->temp_nq=nq; inv->next=mgr->invite_list; mgr->invite_list=inv; - inv->cb=cb; inv->cb_arg=cb_arg; - - struct CONN_MGR_HANDLE* h = cm_invite_handle_new(NULL, NULL, nid, group_id); - if (!h) { cm_invite_fail(inv); return -1; } inv->handle=h; if (out_handle) *out_handle=h; - queue_data_put_with_index(group->nodes, &nq->ll); - - /* 6. лог адресов ДО старта — виден даже при неудаче etcp_connect */ - { - char addrs[512] = ""; int aoff = 0; - for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next) - aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)", - a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port, - a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : "?"); - for (const struct TOPO_ADDR6* a6 = gni->v6_addrs; a6; a6 = a6->next) - aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %s:%d(%s)", - ip_to_str(a6->addr, AF_INET6).str, (int)a6->port, - a6->protocol == TOPO_PROTO_UDP ? "UDP" : a6->protocol == TOPO_PROTO_TCP ? "TCP" : "?"); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%ds", - (unsigned long long)nid, addrs, CM_INVITE_DEFAULT_TIMEOUT_MS/1000); - } - - /* 7. etcp_connect — запускает UDP links + TCP STCP link через единый API */ - if (etcp_connect(inst, nq, cm_invite_etcp_cb, inv, ETCP_CONNECT_EARLY | ETCP_CONNECT_LATE) != 0) { - cm_invite_fail(inv); if (out_handle) *out_handle=NULL; return -1; - } - - inv->overall_timer = uasync_set_timeout(inst->ua, CM_INVITE_DEFAULT_TIMEOUT_MS*10, inv, cm_invite_overall_timeout, "conn_mgr_invite"); - return 0; + return cm_open(mgr, node_id, 0, cb, cb_arg, out_handle); } diff --git a/src/routing_layer/conn_mgr_priv.h b/src/routing_layer/conn_mgr_priv.h index 4eff64c1..fd7570bb 100644 --- a/src/routing_layer/conn_mgr_priv.h +++ b/src/routing_layer/conn_mgr_priv.h @@ -31,7 +31,6 @@ struct stcp_link; #define CONN_MGR_CONNECT_DIRECT_TIMEOUT_MS 2000 #define CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS 15000 #define CONN_MGR_INTERM_EXCHANGE_TIMEOUT_MS 15000 -#define CM_INVITE_DEFAULT_TIMEOUT_MS 2000 #define CACHE_MS 20000 /* время жизни RTT-замера (через cm_is_rtt_fresh) */ #define CONN_MGR_MAX_INTERMEDIARIES 3 @@ -63,8 +62,6 @@ enum { CM_SUBCMD_BRIDGE_RESPONSE = 0x11, CM_SUBCMD_BRIDGE_CLOSE = 0x12, CM_SUBCMD_BRIDGE_CLOSED = 0x13, - CM_SUBCMD_INVITE_INFO_REQ = 0x0A, - CM_SUBCMD_INVITE_INFO_RESP = 0x0B, }; struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed)); @@ -78,49 +75,12 @@ struct CM_INTERM_SEL { uint8_t cmd, subcmd; uint32_t request_id; uint8_t coun struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; }; #pragma pack(pop) -#pragma pack(push, 1) -struct CM_INVITE_REQ { uint8_t cmd, subcmd; uint64_t group_id; }; -#pragma pack(pop) - -/* - * INVITE_INFO_RESP wire format (devel — без обратной совместимости): - * - * [0..1] cmd, subcmd - * [2..9] group_id - * [10] is_member (1=участник, 0=не в канале) - * [11..13] reserved - * [14..45] ed25519_pubkey (Ed25519 pubkey узла-ответчика, для NODEINFO-верификации) - * [46] node_name_len - * [47..N] node_name (node_name_len байт) - * - * Если is_member == 1 — далее полная информация о канале: - * [N+1] ch_name_len - * [N+2..] ch_name (ch_name_len байт) - * +8 owner (node_id создателя) - * +32 ch_x25519 (X25519 pubkey канала) - * +32 ch_ed_pub (Ed25519 pubkey канала) - * +64 ch_sig (Ed25519-подпись канала) - * +1 inv_flags (PEERS_FLAG_HAS_JOIN) - * [if PEERS_FLAG_HAS_JOIN: +64 join_sig + 8 join_ts] - * +64 update_sig - * +8 update_ts - * +1 userinfo_len - * +var userinfo (userinfo_len байт, JSON с именем узла) - */ -#pragma pack(push, 1) -struct CM_INVITE_RESP { uint8_t cmd, subcmd; uint64_t group_id; uint8_t is_member; uint8_t reserved[3]; uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; uint8_t node_name_len; uint8_t node_name[0]; }; -#pragma pack(pop) - -#define CM_INVITE_RESP_HDR_SIZE (sizeof(struct CM_INVITE_RESP)) /* 47 байт, без node_name */ -#define CM_INVITE_RESP_CH_MIN_SIZE (1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1) /* 211 байт, без учёта join и имён */ - #define CM_DIRECT_REQ_H_SIZE (sizeof(struct CM_DIRECT_REQ)) #define CM_DIRECT_RESP_HDR_SIZE (sizeof(struct CM_DIRECT_RESP)) #define CM_EXCHANGE_REQ_SIZE (sizeof(struct CM_EXCHANGE_REQ)) #define CM_EXCHANGE_RESP_SIZE (sizeof(struct CM_EXCHANGE_RESP)) #define CM_INTERM_SEL_SIZE (sizeof(struct CM_INTERM_SEL)) #define CM_DISCONNECT_SIZE (sizeof(struct CM_DISCONNECT)) -#define CM_INVITE_REQ_SIZE (sizeof(struct CM_INVITE_REQ)) /* ═══════ внутренние структуры ═══════ */ @@ -132,17 +92,6 @@ struct cm_exchange_pending { struct CM_EXCHANGE_RESP cached; uint8_t received, probed; }; -enum CM_INVITE_STATE { CM_INVITE_CONNECTING = 0, CM_INVITE_WAIT_INFO = 1, }; - -struct cm_invite_pending { - struct cm_invite_pending* next; struct CONN_MGR* mgr; - uint64_t node_id; uint8_t state; - struct ETCP_CONN* invite_conn; - uint8_t cancelled:1; void* overall_timer; - struct CONN_MGR_HANDLE* handle; struct TOPO_GROUP_NODE* temp_nq; - conn_mgr_cb_t cb; void* cb_arg; -}; - struct CONN_MGR_HANDLE { uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg; struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next; @@ -171,15 +120,14 @@ struct CONN_MGR { uint32_t next_request_id, direct_timeout_ms; uint8_t initialized; struct cm_reverse_pending* reverse_pending; struct cm_exchange_pending* exchange_pending; - struct cm_invite_pending* invite_list; uint8_t _unused[0]; }; /* ═══════ декларации внутренних функций ═══════ */ /* core — публичные (в priv.h, не в conn_mgr.h) */ -int conn_mgr_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, - conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle); +int cm_open(struct CONN_MGR* mgr, uint64_t node_id, uint32_t idle_timeout_ms, + conn_mgr_cb_t cb, void* cb_arg, struct CONN_MGR_HANDLE** out_handle); /* core */ struct CONN_MGR_ENTRY* cm_find_entry(struct CONN_MGR* mgr, uint64_t node_id); @@ -198,8 +146,6 @@ void cm_handle_direct_req(struct ETCP_CONN* conn, const uint8_t* data, size_t le void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id); void cm_reverse_init_cb(struct ETCP_CONN* conn, int event, void* arg); void cm_reverse_timeout_cb(void* arg); -void cm_invite_fail(struct cm_invite_pending* inv); -void cm_invite_cleanup(struct cm_invite_pending* inv); int cm_nat_compatible(struct ETCP_SOCKET* our, uint8_t tcfg, uint8_t tnat); void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s); void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s); @@ -228,6 +174,5 @@ void cm_bg_ping_timer_cb(void* arg); void cm_candidate_ping_timer_cb(void* arg); uint64_t cm_get_node_max_probe_time(struct TOPO_GROUP_NODE* nq); int cm_is_rtt_fresh(struct TOPO_GROUP_NODE* nq, uint64_t now_tb); -void cm_invite_overall_timeout(void* arg); #endif diff --git a/src/routing_layer/route6_lib_doc.md b/src/routing_layer/route6_lib_doc.md index d905f031..30562a7e 100644 --- a/src/routing_layer/route6_lib_doc.md +++ b/src/routing_layer/route6_lib_doc.md @@ -47,4 +47,4 @@ if (rd) forward_to_etcp(rd->v_node_info, pkt); ### Зависимости - `lib/radix.h` — radix tree (longest-match lookup) - `lib/ll_queue.h` — lock-free очередь с хеш-индексом для поиска по `node_id` -- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_SUBNET6` +- `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_SUBNET6` diff --git a/src/routing_layer/route_connectivity_doc.md b/src/routing_layer/route_connectivity_doc.md index 629e5d82..ca292186 100644 --- a/src/routing_layer/route_connectivity_doc.md +++ b/src/routing_layer/route_connectivity_doc.md @@ -59,6 +59,6 @@ route_connectivity_cancel_all(instance); ### Зависимости - `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг) -- `src/topo_node.h` — `TOPO_NODEQ`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6` +- `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6` - `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all) - `lib/u_async.h` — get_time_tb() для отметок времени diff --git a/src/routing_layer/route_lib_doc.md b/src/routing_layer/route_lib_doc.md index 8c51ae99..00751cdb 100644 --- a/src/routing_layer/route_lib_doc.md +++ b/src/routing_layer/route_lib_doc.md @@ -40,7 +40,7 @@ route_table_destroy(rt); | Структура | Поля | Описание | |-----------|------|----------| -| `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_NODEQ*) | Запись маршрута. `v_node_info==NULL` → локальный | +| `ROUTE_ENTRY` | `network` (uint32_t, BE), `prefix_length` (uint8_t), `v_node_info` (TOPO_GROUP_NODE*) | Запись маршрута. `v_node_info==NULL` → локальный | | `ROUTE_TABLE` | `entries`, `count`, `capacity`, `dynamic_subnets`, `local_subnets`, `stats` | Таблица маршрутизации. Динамический массив + статистика | ### Функции @@ -49,7 +49,7 @@ route_table_destroy(rt); |---------|----------| | `route_table_create()` | Выделяет `ROUTE_TABLE`, `entries` на `INITIAL_CAPACITY=100`, счётчики в 0 | | `route_table_destroy(table)` | Освобождает `entries`, `dynamic_subnets`, `local_subnets`, саму таблицу | -| `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_NODEQ`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости | +| `route_insert(table, node)` | Вставляет все v4-подсети узла (`TOPO_GROUP_NODE`) с сортировкой. Проверяет overlap. Расширяет capacity при необходимости | | `route_delete(table, node)` | Удаляет все записи, где `v_node_info == node`, со сдвигом массива | | `route_lookup(table, dest_ip)` | LPM-поиск. Возвращает `ROUTE_ENTRY*` или NULL. Инкрементит `stats` (hits/misses) | | `route_table_print(table)` | Печатает содержимое таблицы в лог | diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 6c56cc71..dbaae888 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/src/routing_layer/topo_group_connect.c @@ -1,7 +1,7 @@ /* * topo_group_connect.c — авто-подключение к узлам группы (бесконечный цикл до успеха) * - * Phase 1: одновременный запуск conn_mgr_open_invite для пиров с connected=1. + * Phase 1: одновременный запуск node_conn_direct_open для пиров с connected=1. * Таймаут = TGC_DIRECT_TIMEOUT_MS. Если хоть один подключился → done. * * Phase 2: последовательный перебор (supernode, затем public/EIM адреса). @@ -26,7 +26,7 @@ #include "topo_group.h" #include "topo_node.h" #include "topo_node_sqlite.h" -#include "conn_mgr.h" +#include "../transport_layer/node_conn_direct.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/u_async.h" @@ -61,9 +61,9 @@ struct TOPO_GROUP_CONNECT { int tried_super; uint64_t* candidate_ids; int candidate_count; - struct CONN_MGR_HANDLE* handles[TGC_MAX_HANDLES]; + struct NODE_CONN_DIRECT* handles[TGC_MAX_HANDLES]; int handle_count; - struct CONN_MGR_HANDLE* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */ + struct NODE_CONN_DIRECT* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */ }; static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc); @@ -72,8 +72,7 @@ static void tgc_phase1_timeout(void* arg); static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc); static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label); -static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, - enum conn_mgr_event event, void* arg); +static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg); /* ─── нотификация GUI о списке узлов в процессе подключения ─── */ @@ -131,8 +130,8 @@ int topo_group_connect_init(struct TOPO_GROUP* group) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase 1 launching %d connects ch=%s grp=%016llx", TGC_ID, count, group->channel_id, (unsigned long long)group->group_id); for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count); - conn_mgr_open_invite(group->instance, group->group_id, NULL, ids[i], tgc_callback, gc, - &gc->handles[gc->handle_count++]); + node_conn_direct_open(group->instance, ids[i], tgc_callback, gc, + &gc->handles[gc->handle_count++], NULL); } tgc_notify_connecting(gc, ids, gc->handle_count); u_free(ids); @@ -156,8 +155,8 @@ void topo_group_connect_destroy(struct TOPO_GROUP* group) { #ifdef UTUN_HAVE_STANDBY if (gc->pause_wait) { standby_wait_cancel(gc->pause_wait); gc->pause_wait = NULL; } #endif - for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); - if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } + for (int i = 0; i < gc->handle_count; i++) node_conn_direct_close(gc->handles[i]); + if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; } u_free(gc->candidate_ids); u_free(gc); } @@ -275,17 +274,17 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn * В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить * залипшее состояние) и ставит паузу перед следующей попыткой. */ -static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, - enum conn_mgr_event event, void* arg) { +static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) { struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; - (void)group_id; + struct ETCP_CONN* conn = node_conn_direct_get_conn(h); + uint64_t node_id = conn ? conn->peer_node_id : 0; - int ok = (event == CONN_EVENT_UP); + int ok = (event == NCD_EVENT_UP); if (ok) { struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); if (nq) nq->handle = h; - } else if (event == CONN_EVENT_TIMEOUT) { + } else if (event == NCD_EVENT_TIMEOUT) { struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); if (nq) nq->handle = NULL; } @@ -304,7 +303,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g tgc_notify_connecting(gc, NULL, 0); } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); - if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } + if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; } tgc_start_pause(gc, (void(*)(void*))tgc_phase2_try_next, "tgc_p2_try"); } break; @@ -315,7 +314,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g tgc_notify_connecting(gc, NULL, 0); } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); - if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } + if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; } tgc_start_pause(gc, (void(*)(void*))tgc_phase3_try_next, "tgc_p3_try"); } break; @@ -369,8 +368,8 @@ static void tgc_phase1_timeout(void* arg) { for (int i = 0; i < count; i++) { int connected = 0; for (int j = 0; j < gc->handle_count; j++) { - struct CONN_MGR_HANDLE* h = gc->handles[j]; - if (h && conn_mgr_get_conn(h) != NULL) { connected = 1; break; } + struct NODE_CONN_DIRECT* h = gc->handles[j]; + if (h && node_conn_direct_get_conn(h) != NULL) { connected = 1; break; } } if (!connected) topo_node_sqlite_set_connected(db, gc->group->channel_id, ids[i], 0); } @@ -378,11 +377,11 @@ static void tgc_phase1_timeout(void* arg) { int remaining = 0; for (int i = 0; i < gc->handle_count; i++) { - struct CONN_MGR_HANDLE* h = gc->handles[i]; - if (h && conn_mgr_get_conn(h) != NULL) { + struct NODE_CONN_DIRECT* h = gc->handles[i]; + if (h && node_conn_direct_get_conn(h) != NULL) { gc->handles[remaining++] = h; } else { - if (h) conn_mgr_close(h); + if (h) node_conn_direct_close(h); } } gc->handle_count = remaining; @@ -435,7 +434,7 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { uint64_t nid = gc->candidate_ids[gc->cursor++]; DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 trying 0x%016llx (%d/%d) super_round=%d", TGC_ID, (unsigned long long)nid, gc->cursor, gc->candidate_count, gc->tried_super); - if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; + if (node_conn_direct_open(gc->group->instance, nid, tgc_callback, gc, &gc->cur_handle, NULL) < 0) continue; tgc_notify_connecting(gc, &nid, 1); return; } @@ -469,7 +468,7 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { uint64_t nid = gc->candidate_ids[gc->cursor++]; DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 trying 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)nid, gc->cursor, gc->candidate_count); - if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; + if (node_conn_direct_open(gc->group->instance, nid, tgc_callback, gc, &gc->cur_handle, NULL) < 0) continue; tgc_notify_connecting(gc, &nid, 1); return; } @@ -490,8 +489,8 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) { gc->pause_timer = NULL; gc->pause_wait = NULL; if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } - for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); - if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } + for (int i = 0; i < gc->handle_count; i++) node_conn_direct_close(gc->handles[i]); + if (gc->cur_handle) { node_conn_direct_close(gc->cur_handle); gc->cur_handle = NULL; } gc->handle_count = 0; gc->pending = 0; gc->connected_count = 0; gc->cursor = 0; gc->tried_super = 0; u_free(gc->candidate_ids); gc->candidate_ids = NULL; gc->candidate_count = 0; @@ -513,8 +512,8 @@ static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) { TGC_ID, count, gc->group->channel_id); for (int i = 0; i < count && gc->handle_count < TGC_MAX_HANDLES; i++) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: Phase1 connect to 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)ids[i], i + 1, count); - conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, ids[i], tgc_callback, gc, - &gc->handles[gc->handle_count++]); + node_conn_direct_open(gc->group->instance, ids[i], tgc_callback, gc, + &gc->handles[gc->handle_count++], NULL); } tgc_notify_connecting(gc, ids, gc->handle_count); u_free(ids); diff --git a/src/routing_layer/topo_group_doc.md b/src/routing_layer/topo_group_doc.md index bd2e2f37..73f9f462 100644 --- a/src/routing_layer/topo_group_doc.md +++ b/src/routing_layer/topo_group_doc.md @@ -2,106 +2,112 @@ ## 1. Назначение -BGP-подобный обмен топологией узлов между пирами uTun. Каждый узел хранит полную таблицу известных узлов и при подключении нового пира синхронизирует её с ним. При изменении информации об узле или обнаружении его недоступности — изменения распространяются всем пирам (broadcast NODEINFO/WITHDRAW). +BGP-подобный обмен топологией узлов между пирами uTun через ETCP. Каждый узел хранит таблицу +известных узлов и при подключении нового пира синхронизирует её с ним. Изменения информации об +узле и обнаружение недоступности распространяются всем пирам (NODEINFO/WITHDRAW). -Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной семантикой: -- **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция, проверка связности -- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite (membership) +Модуль поддерживает **изолированные группы** — разные «пространства имён» узлов с разной +семантикой: +- **TOPO_GROUP_TYPE_UTUN (1)** — VPN-сеть: обмен маршрутами (подсети) между узлами, NAT-детекция, + проверка связности. +- **TOPO_GROUP_TYPE_CHAT (2)** — чат-группа: только узлы, без подсетей, персистентность в SQLite. -Группа по умолчанию (UTUN) создаётся автоматически при `topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для chatgui). Узлы разных групп изолированы: peer из utun-группы не видит узлы чат-группы и наоборот. +Группа по умолчанию (UTUN, `group_id = TOPO_GROUP_UTUN`) создаётся автоматически при +`topo_groups_init()`. Дополнительные группы создаются через `topo_groups_create_group()` (для +chatgui). Узлы разных групп изолированы, маршрутизация пакетов внутри группы ведётся по +`group_id` из wire-заголовков. Помимо обмена топологией, модуль выполняет: -- **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях конфигурации сокетов собирает информацию о себе (адреса, подсети, ключи) -- **NAT-детекцию** — выделена в отдельный самодостаточный модуль `nat_detection.h` (свой ETCP_ID 0x02, своя обработка пакетов); topo_group предоставляет делегат `topo_group_set_nat_check_local` для тестовых нужд -- **Поиск оптимального маршрута** до узла (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей (ETCP соединений) -- **Зондирование связности** — для новых узлов запускает `route_connectivity_probe_node()` -- **Персистентность в SQLite** — синхронизация узлов и membership в БД (таблицы nodes, node_addresses, peers_*) +- **Сборку local_node** (`topo_group_update_my_nodeinfo`) — при старте и при изменениях сокетов/active-mode. +- **NAT-детекцию** — выделена в отдельный модуль `nat_detection.h` (свой ETCP_ID 0x02). +- **Поиск оптимального маршрута** (`topo_group_find_conn_for_node`) — по min hop_count среди live-путей. +- **Зондирование связности** — `route_connectivity_probe_node()` для новых узлов. +- **Авто-подключение к узлам CHAT-группы** — `topo_group_connect` (бесконечный цикл до успеха). +- **Восстановление каскадно отвалившихся узлов** — `topo_recovery` (после разрыва соединения). +- **Invite/join** — `topo_group_invite` (прямое подключение + проверка членства). +- **Персистентность в SQLite** — `topo_node_sqlite` (таблицы nodes, node_addresses, peers_*). +- **Per-group broadcast** — `broadcast` (дёшевая рассылка данных с TTL внутри группы). ## 2. Как пользоваться ### 2.1. Инициализация (при старте utun) ```c -struct TOPO_GROUPS* g = topo_groups_init(instance); // создаёт контейнер + utun-группу по умолчанию +struct TOPO_GROUPS* g = topo_groups_init(instance); // контейнер + utun-группа по умолчанию ``` Здесь же: -- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — регистрирует приёмник пакетов топологии -- `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)` — подписывается на события on_up/on_down всех новых ETCP-соединений -- Открывается SQLite (если указан `db_path` в конфиге), БД сохраняется в `instance->topo_sqlite_db` -- NAT-детекция инициализируется отдельно в `utun_instance.c` через `nat_detection_create(instance)` +- Открывается SQLite (`{db_path}/chats.db`), если задан `db_path`; БД — `instance->topo_sqlite_db`. +- `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` — приёмник пакетов топологии. +- `etcp_add_conn_status_cbk(instance, topo_group_conn_status, g)` — события UP/DOWN/DELETE всех соединений. +- `etcp_add_socket_cbk(...)` — изменения адресов/статуса сокетов. +- `utun_add_activity_cbk(...)` — смена active/standby режима. ### 2.2. Новое ETCP-соединение -При ETCP on_up автоматически вызывается `topo_group_new_conn(group, conn)`: -1. Добавляет conn в `senders_list` (через `topo_group_add_to_senders`) -2. Отправляет `TOPO_SUBCMD_REQUEST_TABLE` — запрос полной таблицы +При ETCP UP (`topo_group_conn_status` → `topo_group_new_conn` для каждой группы): +1. Дедупликация (тот же conn стреляет UP дважды — UDP- и TCP-линк). +2. `topo_recovery_cancel_for_node` — узел появился в сети, отменяем его recovery. +3. Обновление peer-узла: `conn_presence/conn_up |= NCONN_DIRECT`. +4. `topo_group_add_to_senders` + `topo_group_send_table_request` (запрос полной таблицы). +5. `topo_group_connect_on_up` (авто-подключение CHAT). -При приёме `TOPO_SUBCMD_REQUEST_TABLE` peer отправляет: -- NODEINFO c локальным узлом (local_node) -- Full table sync — все известные узлы (с hop_count > 0), кроме тех, чей путь уже включает этого пира (защита от петель) -- `TOPO_SUBCMD_TABLE_COMPLETE` — метка завершения начальной синхронизации; при получении вызывает `etcp_set_routing_exchange_state(from_conn, 3)` +На `REQUEST_TABLE` пир отвечает: NODEINFO о себе (local_node) + full table sync (все узлы, кроме +тех, чей путь уже включает этого пира — защита от петель) + `TABLE_COMPLETE`. При получении +`TABLE_COMPLETE` вызывается `etcp_set_routing_exchange_state(from_conn, 3)`. ### 2.3. NODEINFO (обновление/создание узла) `topo_group_process_nodeinfo(group, from, data, len)`: -1. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS`; при несовпадении — отправка `TOPO_SUBCMD_ERR_GROUP_MISMATCH` -2. Проверка `hop_count < MAX_HOPS(16)` -3. Проверка версии узла — если версия не новее (stale), только обновляет пути (hop_list) и выходит -4. Десериализация wire-формата → обновление/создание `TOPO_NODEQ` (через `topo_node_deserialize`) -5. При создании нового узла: инициализация `connectivity` (PROBE_STATUS_NONE, PROBE_RESULT_UNKNOWN) -6. Добавление пути (`topo_group_add_path`) через текущее соединение, с очисткой старых путей содержащих peer_node_id (`topo_group_remove_path_by_hop`) -7. Вставка в `routing` (`route_insert`) — только для UTUN-групп -8. Сохранение в SQLite (если есть): `topo_node_sqlite_node_put` + для CHAT групп — `topo_node_sqlite_member_put` -9. Уведомление control_server (`control_server_notify_node_change`) -10. Callback `node_updated_cb` (для chatgui member_sync) — вызывается и при создании, и при обновлении -11. Broadcast всем пирам, кроме hop_list (защита от петель) -12. Запуск зондирования связности для новых узлов с адресами (`route_connectivity_probe_node`) +1. Проверка `group_id` пакета (`ni->group_id == group->group_id`). +2. Проверка соответствия типа группы по флагу `TOPO_FLAG_SEND_SUBNETS` (CHAT → 0, UTUN → 1); + при несовпадении — `TOPO_SUBCMD_ERR_GROUP_MISMATCH`. +3. Проверка `hop_count < MAX_HOPS`; пропуск собственного `node_id`. +4. Проверка версии: если `(int8_t)(last_ver - new_ver) >= 0` (stale) — только обновляется путь. +5. Десериализация wire-формата → `TOPO_NODE` (глобальная идентичность) + подсети + hop_list. +6. Верификация идентичности: `node_id == sc_derive_node_id_from_pubkey(public_key)` и + Ed25519 self-signature (`x25519_self_sig`) над каноническим сообщением. +7. Сохранение в глобальный реестр `node_registry` (дедупликация по node_id, ref_count). +8. Обновление/создание `TOPO_GROUP_NODE` (per-group): paths, last_ver, subnets. +9. `topo_group_remove_path_by_hop` + `topo_group_add_path` (через текущий conn). +10. `conn_presence/conn_up |= NCONN_BGP` (+ `NCONN_DIRECT` если прямой). +11. `route_insert` — только для UTUN-групп. +12. SQLite: `topo_node_sqlite_member_exists` → `addrs_put`; для CHAT — `member_placeholder_put` + (source=1, вне merkle-хеша); `nodeinfo_updated`. +13. `control_server_notify_node_change`. +14. Callback `node_updated_cb` (legacy, в текущем коде потребителей нет). +15. Fire BGP node event callbacks (`TOPO_NODE_EVENT_NEW`/`UPDATE`) — используют member_sync, media_delivery. +16. `topo_fire_nodeinfo_cbk` — глобальные nodeinfo-подписчики. +17. Broadcast NODEINFO всем пирам, кроме самого узла и тех, чей node_id есть в hop_list (антипетля). +18. `route_connectivity_probe_node` для новых/изменённых узлов с адресами. ### 2.4. WITHDRAW (удаление узла) `topo_group_process_withdraw(group, sender, data, len)`: -1. Поиск узла по `node_id` -2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление всех путей, содержащих wd_source в hop_list -3. Если путей не осталось (ret == 1): удаление из routing, уведомление control_server, установка `dirty = 1`, отмена connectivity probe (`route_connectivity_cancel_node`), освобождение paths, `topo_node_free_lists`, закрытие виртуальных каналов (`etcp_router_conn_close_all_for_node`), broadcast withdraw дальше -4. Если группа CHAT — удаление member из SQLite через `topo_node_sqlite_member_del` +1. Поиск `TOPO_GROUP_NODE` по `node_id`. +2. `topo_group_remove_path_by_hop(nq, wd_source)` — удаление путей, содержащих wd_source в hop_list. +3. Если путей не осталось: сброс `conn_presence/conn_up`, `route_delete` (UTUN), уведомление + control_server, отмена connectivity probe, освобождение paths и group-полей, удаление из + `nodes`, `etcp_router_conn_close_all_for_node`, broadcast withdraw (кроме отправителя), + fire `TOPO_NODE_EVENT_REMOVE`. +4. **BGP не удаляет member-запись из SQLite** — удаление из `peers_*` только через битую подпись + при верификации (member_sync). ### 2.5. Отключение соединения -При ETCP on_down вызывается `topo_group_remove_conn(group, conn)`: -1. Проверка наличия conn в senders_list (если уже удалён — выход) -2. Для всех узлов с путями через этот conn — `topo_group_remove_path`. Если узел стал полностью недоступен (нет путей) — удаляется из routing и nodes, устанавливается `dirty = 1`, отменяется connectivity probe, закрываются виртуальные каналы, broadcast withdraw -3. Удаление conn из `senders_list` +При ETCP DOWN/DELETE (`topo_group_remove_conn`): +1. Проверка наличия conn в senders_list (если нет — выход). +2. Для каждого узла с путями через conn: вычисляется next_hop из hop_list, `remove_path`. Если + узел стал недоступен — `topo_recovery_add_node` (для каскадных, кроме самого пира), полная + очистка (как в WITHDRAW) + broadcast withdraw. +3. Удаление conn из `senders_list`. +4. Если были каскадные узлы — `topo_recovery_start`. +5. `topo_group_connect_on_down`. ### 2.6. Поиск пути до узла -`topo_group_find_conn_for_node(group, node_id)` — перебирает `path`'ы узла, выбирает с минимальным `hop_count`. Предпочитает live-пути (links_up > 0). Если live-путей нет, fallback на любой путь с минимальным hop_count. - -Интенсивно используется `etcp_router`: -- При маршрутизации пакетов к удалённому узлу -- При поиске next-hop для multihop соединений - -### 2.7. NAT-детекция - -NAT-детекция выделена в отдельный самодостаточный модуль `nat_detection.h`: -- Свой ETCP ID: `ETCP_ID_NAT_DETECTION` (0x02) -- Сам подписывается на conn_up/conn_down, сам принимает свои пакеты -- При поднятии линка (INIT handshake) вызывается `nat_detection_link_ready()` -- Ищет третий узел напрямую в `instance->connections`, обновляет `ETCP_SOCKET` и broadcast-ит NODEINFO через `topo_group_update_my_nodeinfo()` - -Из topo_group в NAT-детекцию остался только делегат: -- `topo_group_set_nat_check_local(group, allow)` → вызывает `nat_detection_set_allow_local(instance->nat_det, allow)` - -### 2.8. Широковещательная рассылка - -- **NODEINFO**: при поступлении новой/обновлённой информации об узле — отправляется всем соседям, кроме тех, чей node_id уже есть в hop_list (защита от петель) -- **WITHDRAW**: при удалении узла — отправляется всем соседям, кроме отправителя - -### 2.9. Изоляция групп - -При приёме NODEINFO проверяется флаг `TOPO_FLAG_SEND_SUBNETS`: -- UTUN-группа ожидает флаг = 1 (с подсетями) -- CHAT-группа ожидает флаг = 0 (без подсетей) -- При несовпадении отправляется `TOPO_SUBCMD_ERR_GROUP_MISMATCH` и пакет игнорируется +`topo_group_find_conn_for_node(group, node_id)` — перебирает paths узла, выбирает min `hop_count`, +предпочитая live-пути (`conn->links_up > 0`); fallback на любой путь. Интенсивно используется +`etcp_router` и `conn_mgr` (next-hop для маршрутов и multihop-соединений). ## 3. API @@ -109,130 +115,119 @@ NAT-детекция выделена в отдельный самодостат | Структура | Назначение | |---|---| -| `TOPO_GROUPS` | Контейнер всех групп топологии экземпляра. Хранит `group_list` (ll_queue), memory_pool'ы для сериализации (v4_sock_meta, v4_addr, v6_sock_meta, v6_addr, v4_subnet, v6_subnet), callback `node_updated_cb` | -| `TOPO_GROUP` | Одна группа топологии. `group_id`, `group_type`, `nodes` (ll_queue узлов), `senders_list` (ll_queue conn'ов), `local_node` (TOPO_NODEQ), `ed25519_public_key` (производный из X25519), `channel_id[64]` (для CHAT-групп) | -| `TOPO_GROUP_CONN_ITEM` | Элемент списка senders: conn + ll_entry — связка с senders_list | -| `TOPO_NODE` | Идентичность узла: pubkeys, имя, версия (ver), списки адресов (v4/v6 sock_meta, addrs). Разделяется между группами через ref_count | -| `TOPO_NODEQ` | Запись в очереди группы: указатель на TOPO_NODE, пути (paths), hop_list, tranzit_data, subnets, connectivity, conn_mgr (тип + intermediaries), best_socket, dirty, last_ver | -| `TOPO_NODEPATH` | Путь до узла через конкретное ETCP_CONN: conn + hop_count | -| `TOPO_NODESUBNETS` | Подсети узла: списки TOPO_SUBNET4 / TOPO_SUBNET6 | -| `TOPO_CONNECTIVITY` | Состояние связности с узлом: probe_status, pending_count, rtt-метрики, статусы (interface/nat/real), probe_list | -| `TOPOMSG_NODEINFO_PKT` | Wire-формат пакета NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть | -| `TOPOMSG_WITHDRAW_PKT` | Wire-формат WITHDRAW: node_id (удаляемый) + wd_source (инициатор) | -| `TOPOMSG_TABLE_REQ` | Wire-формат запроса/завершения таблицы: cmd + subcmd | -| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire-формат ошибки несоответствия типа группы: expected_type + received_flags | -| `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль): ping_pending, next_ping_req_id, allow_nat_check_local | +| `TOPO_GROUPS` | Контейнер всех групп. `group_list` (hash), `node_registry` (глобальный реестр TOPO_NODE*, hash), memory_pool'ы (v4/v6 sock_meta, addr, subnet), `node_updated_cb` (legacy) | +| `TOPO_GROUP` | Одна группа. `group_id`, `group_type`, `nodes` (queue TOPO_GROUP_NODE), `senders_list` (queue conn'ов), `local_node`, `ed25519_public_key`, `channel_id` (CHAT), `conn_mgr`, `recovery_list`, `connect`, `broadcast`, `node_cbks` | +| `TOPO_NODE` | Глобальная идентичность узла (в `node_registry`): pubkeys, `x25519_self_sig`, name, v4/v6 sock_meta+addrs, `client_type`, `client_activity`, `ver`, `group_ref_count` | +| `TOPO_GROUP_NODE` | Per-group запись узла: `node_id`, `subnets`, `paths`, `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) | +| `TOPO_NODEPATH` | Путь до узла через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` | +| `TOPO_NODESUBNETS` | Подсети узла: списки `TOPO_SUBNET4`/`TOPO_SUBNET6` | +| `TOPO_CONNECTIVITY` | Связность: probe_status, rtt-метрики, статусы (interface/nat/real), `probe_deferred_wait` (standby) | +| `TOPOMSG_NODEINFO_PKT` | Wire NODEINFO: cmd + subcmd + TOPOMSG_NODE + переменная часть | +| `TOPOMSG_WITHDRAW_PKT` | Wire WITHDRAW: `group_id`, `node_id` (удаляемый), `wd_source` (инициатор) | +| `TOPOMSG_TABLE_REQ` | Wire запроса/завершения таблицы: `group_id` | +| `TOPOMSG_ERR_GROUP_MISMATCH` | Wire ошибки типа группы: `group_id`, `expected_type`, `received_flags` | +| `NAT_DETECTION` | Контекст NAT-детекции (отдельный модуль) | ### 3.2. Константы | Константа | Значение | Назначение | |---|---|---| -| `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID для пакетов топологии | -| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле + подсети | +| `ETCP_ID_TOPO_ENTRY` | 0x01 | ETCP ID пакетов топологии | +| `TOPO_SUBCMD_NODEINFO` | 0x04 | Полная информация об узле | | `TOPO_SUBCMD_REQUEST_TABLE` | 0x05 | Запрос полной таблицы | | `TOPO_SUBCMD_WITHDRAW` | 0x06 | Узел стал недоступен | | `TOPO_SUBCMD_TABLE_COMPLETE` | 0x0B | Завершение начальной синхронизации | | `TOPO_SUBCMD_ERR_GROUP_MISMATCH` | 0x0C | Ошибка несоответствия типа группы | | `MAX_HOPS` | 16 | Максимальная длина hop-листа | -| `BGP_NODES_HASH_SIZE` | 256 | Размер хеш-таблицы nodes | -| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id по умолчанию для utun-группы | -| `TOPO_GROUP_TYPE_UTUN` | 1 | Тип группы: VPN-сеть (подсети, маршруты) | -| `TOPO_GROUP_TYPE_CHAT` | 2 | Тип группы: чат (без подсетей) | +| `BGP_NODES_HASH_SIZE` / `TOPO_NODE_REGISTRY_HASH_SIZE` | 256 | Хеш-таблицы nodes / node_registry | +| `TOPO_GROUP_UTUN` | 0x8000000000000000ULL | group_id utun-группы по умолчанию | +| `TOPO_GROUP_TYPE_UTUN` / `TOPO_GROUP_TYPE_CHAT` | 1 / 2 | Тип группы | | `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг NODEINFO: узел передаёт подсети | -| `PROBE_STATUS_NONE` | 0 | Связность: не проверялась | -| `PROBE_STATUS_IN_PROGRESS` | 1 | Связность: проверка идёт | -| `PROBE_STATUS_DONE` | 2 | Связность: проверка завершена | -| `PROBE_RESULT_UNKNOWN` | 0 | Результат проверки: неизвестно | -| `PROBE_RESULT_REACHABLE` | 1 | Результат проверки: доступен | -| `PROBE_RESULT_UNREACHABLE` | 2 | Результат проверки: недоступен | +| `NCONN_DIRECT` / `NCONN_INDIRECT` / `NCONN_BGP` | 1/2/4 | Биты наличия соединений (presence/up) | +| `CONN_TYPE_DIRECT` / `REVERSE` / `INDIRECT` / `NONE` | 1/2/3/0 | Стратегия conn_mgr | +| `CLIENT_TYPE_SERVER` / `DESKTOP` / `MOBILE` | 0/1/2 | Тип клиента | +| `CLIENT_ACTIVITY_STANDBY` / `ACTIVE` | 0/1 | Режим (standby/active) | +| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы | +| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы | +| `TOPO_NODE_EVENT_NEW/UPDATE/REMOVE` | 0/1/2 | События BGP node callbacks | +| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников для INDIRECT | -### 3.3. Функции инициализации/завершения +### 3.3. Инициализация/завершение | Функция | Назначение | |---|---| -| `topo_groups_init(instance)` | Создаёт TOPO_GROUPS, memory_pool'ы, открывает SQLite (если db_path в конфиге), создаёт utun-группу по умолчанию, регистрирует ETCP-коллбэки | -| `topo_groups_destroy(instance)` | Освобождает все группы, memory_pool'ы, закрывает SQLite, отменяет connectivity probes, снимает ETCP-регистрацию | -| `topo_group_create(instance, group_id, group_type)` | Создаёт TOPO_GROUP: nodes (hash queue), senders_list, local_node (через update_my_nodeinfo), ed25519_pubkey; вызывается из topo_groups_init / topo_groups_create_group | -| `topo_group_destroy(group)` | Освобождает senders_list, local_node (free_lists), nodes очереди | -| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создаёт новую группу и добавляет в group_list; для CHAT устанавливает channel_id | -| `topo_groups_find(g, group_id)` | Поиск группы по group_id через хеш-индекс group_list | -| `topo_groups_get_default(g)` | Возвращает utun-группу по умолчанию (group_id=TOPO_GROUP_UTUN) | +| `topo_groups_init(instance)` | Контейнер + node_registry + пулы + SQLite + utun-группа + ETCP/activity коллбэки | +| `topo_groups_destroy(instance)` | Отвязка коллбэков, destroy групп, node_registry, пулов, закрытие SQLite | +| `topo_group_create(instance, group_id, group_type)` | Создать TOPO_GROUP: nodes/senders/local_node/conn_mgr/broadcast | +| `topo_group_destroy(group)` | recovery_cancel_all, connect_destroy, broadcast_destroy, conn_mgr_destroy, очистка nodes/local_node | +| `topo_groups_create_group(g, group_id, group_type, channel_id)` | Создать группу + добавить в group_list + new_conn для существующих соединений + connect_init (CHAT) | +| `topo_groups_find(g, group_id)` / `topo_groups_get_default(g)` | Поиск группы / utun-группа | +| `topo_group_on_activity_change(instance, active, arg)` | Обновить local_node и broadcast его по всем группам/пирам | -### 3.4. Функции жизненного цикла соединения +### 3.4. Жизненный цикл соединения | Функция | Назначение | |---|---| -| `topo_group_new_conn(group, conn)` | Добавляет conn в senders_list, отправляет REQUEST_TABLE | -| `topo_group_remove_conn(group, conn)` | Удаляет все пути через conn, удаляет недоступные узлы (route_delete, cancel_connectivity, etcp_router_conn_close_all_for_node, broadcast withdraw), убирает conn из senders_list | -| `topo_group_etcp_conn_cbk(conn, arg)` | Callback при создании нового ETCP-соединения: подписывается на on_up/on_down через etcp_conn_add_up_cbk/down_cbk | -| `topo_group_on_conn_up(conn, arg)` | При on_up: вызывает topo_group_new_conn | -| `topo_group_on_conn_down(conn, arg)` | При on_down: вызывает topo_group_remove_conn | +| `topo_group_new_conn(group, conn)` | Дедуп, отмена recovery, обновить peer, add_to_senders, request_table, connect_on_up | +| `topo_group_remove_conn(group, conn)` | Удалить пути, удалить недоступные узлы (recovery/broadcast), убрать conn, connect_on_down | +| `topo_group_conn_status(conn, status, arg)` | Callback status: UP→new_conn, DOWN/DELETE→remove_conn для каждой группы | -### 3.5. Функции обработки пакетов +### 3.5. Обработка пакетов | Функция | Назначение | |---|---| -| `topo_group_receive_cbk(from_conn, entry)` | Точка входа приёма пакетов топологии: работает только с default-группой, диспетчеризация по subcmd (NODEINFO/WITHDRAW/REQUEST_TABLE/TABLE_COMPLETE/ERR_GROUP_MISMATCH) | -| `topo_group_process_nodeinfo(group, from, data, len)` | Обработка NODEINFO: проверка группы/версии/hops, десериализация, обновление/создание TOPO_NODEQ, пути, routing, SQLite, broadcast, зондирование связности, node_updated_cb | -| `topo_group_process_withdraw(group, sender, data, len)` | Обработка WITHDRAW: удаление путей с wd_source, если узел недоступен — полная очистка (routing, connectivity, paths, etcp_router), broadcast | -| `topo_group_handle_request_table(group, conn)` | Обработка REQUEST_TABLE: отправляет NODEINFO (local_node) + full table sync + add_to_senders + TABLE_COMPLETE | +| `topo_group_receive_cbk(from_conn, entry)` | Точка входа: извлекает `group_id`, находит группу, диспетчеризация по subcmd | +| `topo_group_process_nodeinfo(group, from, data, len)` | Полный цикл NODEINFO (см. 2.3) | +| `topo_group_process_withdraw(group, sender, data, len)` | Удаление путей/узла по WITHDRAW (см. 2.4) | +| `topo_group_handle_request_table(group, conn)` | local_node + full table + add_to_senders + TABLE_COMPLETE | -### 3.6. Функции поиска +### 3.6. Поиск и пути | Функция | Назначение | |---|---| -| `topo_group_find_conn_for_node(group, node_id)` | Поиск оптимального ETCP_CONN до узла: min hop_count, предпочтение live-путям | -| `topo_node_find_by_id(group, node_id)` | Поиск TOPO_NODEQ в nodes по node_id через хеш-индекс | -| `topo_groups_find(g, group_id)` | Поиск группы по group_id | +| `topo_group_find_conn_for_node(group, node_id)` | Оптимальный ETCP_CONN (min hop_count, предпочтение live) | +| `topo_node_find_by_id(group, node_id)` | Поиск TOPO_GROUP_NODE по node_id (hash) | +| `topo_group_add_path(nq, conn, hop_list, hop_count, cumulative_rtt)` | Создать TOPO_NODEPATH + inline hop_list | +| `topo_group_remove_path(nq, conn)` | Удалить путь; возвращает 1 если путей не осталось | +| `topo_group_remove_path_by_hop(nq, wd_source)` | Удалить пути, содержащие wd_source в hop_list | +| `topo_group_should_send_to(nq, target_id)` | Проверка антипетли (target_id не во всех путях) | -### 3.7. Функции broadcast +### 3.7. Broadcast | Функция | Назначение | |---|---| -| `topo_group_send_nodeinfo(group, node, conn)` | Сериализует TOPO_NODEQ и отправляет NODEINFO одному conn | -| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW всем пирам (без exclude) | -| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Рассылает WITHDRAW всем conn в senders_list, кроме exclude | -| `topo_group_send_full_table(group, conn)` | Отправляет все известные узлы (с hop_count > 0), кроме тех, чей путь включает target, указанному conn | -| `topo_group_send_table_request(group, conn)` | Отправляет запрос полной таблицы (TOPO_SUBCMD_REQUEST_TABLE) | -| `topo_group_send_table_complete(group, conn)` | Отправляет метку завершения синхронизации (TOPO_SUBCMD_TABLE_COMPLETE) | -| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку несоответствия типа группы | +| `topo_group_send_nodeinfo(group, node, conn, cumulative_rtt)` | Сериализовать и отправить NODEINFO одному conn | +| `topo_group_send_withdraw(group, node_id)` | Broadcast WITHDRAW (wd_source = self) | +| `topo_group_broadcast_withdraw(group, node_id, wd_source, exclude)` | Broadcast WITHDRAW всем, кроме exclude | +| `topo_group_send_full_table(group, conn)` | Все известные узлы (антипетля) указанному conn | +| `topo_group_send_table_request/complete(group, conn)` | REQUEST_TABLE / TABLE_COMPLETE | +| `topo_group_send_err_group_mismatch(group, conn, ...)` | ERR_GROUP_MISMATCH | -### 3.8. Функции NAT (делегаты в nat_detection) +### 3.8. Коллбэки (подписки) | Функция | Назначение | |---|---| -| `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local(instance->nat_det, allow)` — разрешить/запретить NAT check для локальных подсетей (тесты) | -| `nat_detection_create(inst)` | Создаёт модуль NAT-детекции: etcp_bind на 0x02, подписка на conn_up/conn_down | -| `nat_detection_destroy(nd)` | Уничтожает модуль NAT-детекции | -| `nat_detection_link_ready(nd, link)` | Запускает NAT-чек для линка после INIT handshake | -| `nat_detection_cancel_for_conn(nd, conn)` | Отменяет все pending NAT-пинги для соединения | -| `nat_detection_request_check_all(nd)` | Принудительная перепроверка NAT для всех линков | +| `topo_group_add_node_cbk/remove_node_cbk(group, fn, arg)` | BGP node events (NEW/UPDATE/REMOVE) — consumer: member_sync, media_delivery | +| `utun_add_nodeinfo_cbk/remove_nodeinfo_cbk(instance, fn, arg)` | Глобальная подписка на изменения nodeinfo (per-instance) | +| `topo_fire_nodeinfo_cbk(instance, group, node)` | Вызвать глобальные nodeinfo-подписчики | +| `topo_groups_set_node_updated_cb(groups, fn)` | Legacy callback (в текущем коде потребителей нет) | -### 3.9. Функции путей +### 3.9. NAT (делегаты в nat_detection) | Функция | Назначение | |---|---| -| `topo_group_add_path(nq, conn, hop_list, hop_count)` | Создаёт TOPO_NODEPATH для узла (conn + hop_list) и добавляет в paths | -| `topo_group_remove_path(nq, conn)` | Удаляет путь через указанный conn; возвращает 1 если путей не осталось (unreachable) | -| `topo_group_remove_path_by_hop(nq, wd_source)` | Удаляет все пути, содержащие wd_source в hop_list (используется в WITHDRAW) | -| `topo_group_add_to_senders(group, conn)` | Добавляет conn в senders_list (с проверкой дубликатов) | -| `topo_group_should_send_to(nq, target_id)` | Проверяет, нужно ли отправлять NODEINFO узла указанному пиру (target_id не во всех путях) | +| `topo_group_set_nat_check_local(group, allow)` | Делегат в `nat_detection_set_allow_local` | -### 3.10. Callbacks/вспомогательные +### 3.10. Связанные модули -| Функция | Назначение | -|---|---| -| `topo_groups_set_node_updated_cb(groups, fn)` | Устанавливает callback при создании/обновлении узла (используется chatgui для member_sync) | -| `topo_group_update_my_nodeinfo(instance, group)` | Собирает информацию о локальном узле из конфига и ключей: адреса сокетов, подсети, pubkeys, имя; обновляет group->local_node; вызывается при старте и при изменениях сокетов | -| `nodeinfo_dump_log(data, len)` | Логирует краткую сводку NODEINFO (node_id, ver, счётчики, hops) | -| `nodeinfo_format(data, len)` | Форматирует полный NODEINFO с именем, подсетями в читаемую строку (аллокация через u_malloc) | -| `group_subcmd_name(subcmd)` | Возвращает строковое имя subcmd для логов | - -### 3.11. Проверка соответствия типа группы - -| Функция | Назначение | +| Модуль | Назначение | |---|---| -| `topo_group_send_err_group_mismatch(group, conn, expected_type, received_flags)` | Отправляет ошибку GROUP_MISMATCH пиру, если его NODEINFO содержит неверный flags (разные типы групп) | +| `topo_group_connect` | Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл, standby-aware) | +| `topo_group_invite` | Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP, ETCP_ID 0x13) | +| `topo_recovery` | Восстановление каскадно отвалившихся узлов после разрыва соединения | +| `broadcast` | Per-group рассылка данных с TTL (ETCP_ID_BROADCAST), `broadcast_init/сброс` | +| `topo_node` | Модель данных: TOPO_NODE/TOPO_GROUP_NODE, ser/deserialize, node_registry, sig-msg | +| `topo_node_sqlite` | Персистентность: nodes, node_addresses, peers_*, member block/owner/placeholder | ## 4. Архитектурные связи @@ -240,74 +235,81 @@ NAT-детекция выделена в отдельный самодостат | Модуль | Как используется | |---|---| -| `topo_node.c/h` | Модель данных узла: TOPO_NODE, TOPO_NODEQ, ser/deserialize, find_by_id, update_my_nodeinfo, free_lists, dump_all, format_all | -| `topo_node_sqlite.h` | Персистентность в SQLite: topo_node_sqlite_node_put, member_put, member_del, init | -| `nat_detection.h` | NAT-детекция: отдельный модуль с собственным ETCP_ID 0x02; делегат set_nat_check_local | -| `route_lib.h` | Таблица маршрутов: route_insert, route_delete | -| `route_connectivity.h` | Зондирование связности узла: route_connectivity_probe_node, cancel_node, cancel_all | -| `etcp_api.h` | Регистрация приёмника: etcp_bind, etcp_add_new_conn_cbk, etcp_unbind | -| `etcp_connections.h` | Управление соединениями: etcp_conn_add_up_cbk, etcp_conn_add_down_cbk, ETCP_CONN, ETCP_LINK | -| `etcp.h` | Отправка пакетов: etcp_send, etcp_set_routing_exchange_state | -| `etcp_debug.h` | Логирование ETCP: log_name | -| `etcp_router.h` | Закрытие виртуальных каналов: etcp_router_conn_close_all_for_node | -| `control_server.h` | Уведомления мониторинга: control_server_notify_node_change, control_server_notify_node_removed | -| `conn_mgr.h` | Менеджер соединений: использует topo_group_find_conn_for_node для поиска путей | -| `utun_instance.h` | UTUN_INSTANCE: конфиг, node_id, ключи, rt, topo_groups, nat_det, topo_sqlite_db, control_srv | -| `secure_channel.h` | Ключи: sc_derive_ed25519_pubkey | -| `memory_pool.h` | Память для сериализации: memory_pool_init / destroy | -| `debug_config.h` | Логирование: DEBUG_INFO/WARN/ERROR/DEBUG с категорией BGP | -| `mem.h` | Аллокация: u_malloc, u_free, u_calloc | -| `platform_compat.h` | Кроссплатформенная совместимость | +| `topo_node.c/h` | Модель узла, ser/deserialize, node_registry, update_my_nodeinfo, sign_self | +| `topo_node_sqlite.h` | Персистентность (node_put, member_placeholder_put, addrs_put, nodeinfo_updated) | +| `nat_detection.h` | NAT-детекция (отдельный модуль, ETCP_ID 0x02) | +| `route_lib.h` | route_insert / route_delete | +| `route_connectivity.h` | probe_node / cancel_node / cancel_all | +| `etcp_api.h` | etcp_bind, etcp_add_conn_status_cbk, etcp_add_socket_cbk, etcp_unbind | +| `etcp_connections.h` | ETCP_CONN, conn->peer_node_id, links_up | +| `etcp.h` | etcp_send, etcp_set_routing_exchange_state | +| `etcp_router.h` | etcp_router_conn_close_all_for_node | +| `control_server.h` | notify_node_change / notify_node_removed | +| `conn_mgr.h` | conn_mgr_init/destroy (per-group), поиск путей | +| `secure_channel.h` | sc_derive_ed25519_pubkey, sc_derive_node_id_from_pubkey, sc_ed25519_verify | +| `broadcast.h` | broadcast_init/destroy (per-group) | +| `memory_pool.h` | Пулы для сериализации | +| `debug_config.h` | Логирование, категория BGP | +| `mem.h` | u_malloc/u_free/u_calloc | ### 4.2. Кто вызывает topo_group | Вызывающий | Что вызывает | Для чего | |---|---|---| -| `utun_instance.c` | `topo_groups_init`, `topo_groups_destroy` | Инициализация/завершение при старте/остановке utun | -| `etcp_connections.c` | `topo_group_update_my_nodeinfo` | При изменении конфигурации сокетов — обновление local_node | -| `etcp_router.c` | `topo_group_find_conn_for_node` | Поиск next-hop для маршрутов и multihop-соединений (~15 вызовов) | -| `conn_mgr.c` | `topo_group_find_conn_for_node` | Поиск пути для установки прямого/обратного/indirect соединения | -| `nat_detection.c` | `topo_group_update_my_nodeinfo` | При обновлении NAT-типа сокета — обновление local_node и broadcast | -| Тесты | `topo_groups_get_default`, `topo_group_find_conn_for_node`, `topo_node_find_by_id`, `topo_group_set_nat_check_local` | 9 тестовых файлов | - -### 4.3. ETCP-коллбэки (верхний уровень) - -Точка входа для пакетов топологии — `etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk)` в `topo_groups_init`. Единственный обработчик на весь ETCP ID 0x01. Диспетчеризация внутри по subcmd (byte data[1]). Обработка всегда идёт через default-группу (TOPO_GROUP_UTUN). - -Коллбэки on_up/on_down новых соединений регистрируются через `etcp_add_new_conn_cbk(instance, topo_group_etcp_conn_cbk, NULL)`, которая при создании каждого нового ETCP_CONN подписывается на его события. +| `utun_instance.c` | `topo_groups_init/destroy` | Инициализация/завершение | +| `etcp_connections.c` | `topo_group_send_nodeinfo` | Переотправка local_node при смене сокетов | +| `etcp_router.c` | `topo_group_find_conn_for_node` | next-hop для маршрутов | +| `conn_mgr_*.c` | `topo_group_find_conn_for_node`, `topo_fire_nodeinfo_cbk` | Пути для соединений | +| `nat_detection.c` | `topo_group_send_nodeinfo`, `topo_group_update_my_nodeinfo` | Обновление local_node после NAT-чека | +| `member_sync.c` | `topo_group_add_node_cbk` | BGP → member_sync → node_props_changed | +| `media_delivery.c` | `topo_group_add_node_cbk` | События узлов для медиа | +| `route_connectivity.c` | `topo_fire_nodeinfo_cbk` | Уведомление об изменении узла | ## 5. Форматы пакетов (wire) ### 5.1. NODEINFO ``` -cmd(1) | subcmd(1) | TOPOMSG_NODE (82+ байта) | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [tranzit[]] | [hop_list[]] +cmd(1) | subcmd(1) | TOPOMSG_NODE | [node_name] | [v4_sock_meta[]] | [v4_addrs[]] | [v6_sock_meta[]] | [v6_addrs[]] | [v4_subnets[]] | [v6_subnets[]] | [hop_list[]] ``` +`TOPOMSG_NODE` содержит `group_id`, `node_id`, `ver`, pubkeys, `x25519_self_sig(64)`, счётчики, +`hop_count`, `client_type`, `client_activity`, `cumulative_rtt`. ### 5.2. WITHDRAW ``` -cmd(1) | subcmd(1) | node_id(8) | wd_source(8) = 18 байт +cmd(1) | subcmd(1) | group_id(8) | node_id(8) | wd_source(8) = 26 байт ``` ### 5.3. REQUEST_TABLE / TABLE_COMPLETE ``` -cmd(1) | subcmd(1) = 2 байта +cmd(1) | subcmd(1) | group_id(8) = 10 байт ``` ### 5.4. ERR_GROUP_MISMATCH ``` -cmd(1) | subcmd(1) | expected_type(1) | received_flags(1) = 4 байта +cmd(1) | subcmd(1) | group_id(8) | expected_type(1) | received_flags(1) = 12 байт ``` ## 6. Примечания -- **Хранилище SQLite**: Открывается в `topo_groups_init` если в конфиге задан `db_path`. БД хранится как `instance->topo_sqlite_db` (sqlite3*), используется во всех группах. Файл: `{db_path}/chats.db`. Режим: WAL, foreign_keys=ON, synchronous=NORMAL. -- **Версионирование NODEINFO**: Версия (ver, uint8_t 1-255) сравнивается через циклическую арифметику `(int8_t)(cur_ver - new_ver) >= 0`. Версия 0 не используется (нет данных). Если версия stale — обновляются только пути без замены данных узла. -- **Защита от петель**: NODEINFO рассылается только тем пирам, чей node_id отсутствует во всех путях (hop_list). WITHDRAW broadcast всем кроме отправителя. -- **Связность узлов**: Для новых/обновлённых узлов с адресами автоматически запускается зондирование (`route_connectivity_probe_node`). При удалении узла (withdraw/remove_conn) пробы отменяются (`route_connectivity_cancel_node`). -- **Виртуальные каналы**: При удалении узла закрываются все виртуальные ETCP-каналы через `etcp_router_conn_close_all_for_node`. -- **Dirty-флаг**: При withdraw/remove_conn узлы помечаются `dirty = 1` перед удалением. -- **NAT check для локальных сетей**: По умолчанию запрещён (`allow_nat_check_local=0`), можно включить через `topo_group_set_nat_check_local(group, 1)` для тестов (делегирует в `nat_detection_set_allow_local`). -- **Conn_mgr поля**: `conn_mgr_type`, `conn_mgr_intermediaries[]`, `conn_mgr_intermediariy_count` — используются менеджером соединений для стратегии установки связи с узлом (direct/reverse/indirect). -- **Обновление local_node при NAT-детекции**: Модуль `nat_detection` после определения типа NAT вызывает `topo_group_update_my_nodeinfo()` для обновления local_node с новыми данными сокетов — это автоматически увеличивает версию, и NODEINFO broadcast-ится всем пирам через механизм изменения конфигурации сокетов. - -(End of file - total 302 lines) +- **Глобальный реестр идентичности**: `TOPO_NODE` (pubkeys, адреса, имя) хранится один раз в + `node_registry` и разделяется между группами через `group_ref_count`. Per-group данные — в + `TOPO_GROUP_NODE` (paths, subnets, connectivity, conn_presence/up). +- **Версионирование NODEINFO**: `ver` (uint8_t) сравнивается циклически `(int8_t)(cur - new) >= 0`; + stale-версия только обновляет путь, не заменяя данные узла. +- **Верификация идентичности**: `node_id == derive(x25519)` + Ed25519 self-signature над каноническим + сообщением (`topo_node_build_sig_msg`). Несовпадение → пакет отвергается как forgery. +- **Защита от петель**: NODEINFO рассылается пирам, чей node_id отсутствует в hop_list всех путей; + WITHDRAW broadcast всем, кроме отправителя. +- **Hop-лист хранится в пути**: `TOPO_NODEPATH` содержит inline `hop_list[hop_count]` сразу после + структуры; `next_hop` для recovery вычисляется из него. +- **Связность**: для новых узлов с адресами запускается `route_connectivity_probe_node`; при + удалении — `route_connectivity_cancel_node`. В standby пробы откладываются (`PROBE_STATUS_DEFERRED`). +- **Виртуальные каналы**: при удалении узла закрываются `etcp_router_conn_close_all_for_node`. +- **CHAT-группа пишет плейсхолдер** `source=1` (pubkeys из BGP, без подписей) — вне merkle-хеша; + полная запись приходит позже через merkle-путь (member_sync). +- **BGP не удаляет member из SQLite**: работает только со своей RAM-таблицей; удаление из `peers_*` + — только битая подпись (member_sync). +- **NAT check для локальных сетей**: по умолчанию запрещён (`allow_nat_check_local=0`), включается + для тестов через `topo_group_set_nat_check_local`. +- **Обновление local_node при NAT/active-изменениях**: `nat_detection` и `topo_group_on_activity_change` + вызывают `topo_group_update_my_nodeinfo` → новая версия → NODEINFO broadcast всем пирам. diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c new file mode 100644 index 00000000..d5d9b2a8 --- /dev/null +++ b/src/routing_layer/topo_group_invite.c @@ -0,0 +1,535 @@ +/** + * @file topo_group_invite.c + * @brief Invite/join к каналу через прямое (ncd) подключение + проверка членства. + * + * Перенесено из conn_mgr (INVITE_INFO_REQ/RESP) — conn_mgr теперь только медиа. + */ +#include +#include +#include +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/memory_pool.h" +#include "../lib/u_async.h" +#include "utun_instance.h" +#include "etcp_api.h" +#include "etcp.h" +#include "etcp_connections.h" +#include "topo_node.h" +#include "topo_node_sqlite.h" +#include "topo_group.h" +#include "topo_group_invite.h" +#include "../transport_layer/node_conn_direct.h" +#include "ntp_time.h" +#include "../chat/chat_core.h" + +#define TGI_ID "topo_group_invite" +#define TGI_TIMEOUT_MS 2000 + +/* wire-формат INVITE_INFO_RESP (devel — без обратной совместимости): + * + * [0..1] cmd, subcmd + * [2..9] group_id + * [10] is_member + * [11..13] reserved + * [14..45] ed25519_pubkey + * [46] node_name_len + * [47..N] node_name + * + * Если is_member == 1 — далее полная информация о канале: + * [N+1] ch_name_len + * [N+2..] ch_name + * +8 owner + * +32 ch_x25519 + * +32 ch_ed_pub + * +64 ch_sig + * +1 inv_flags + * [if PEERS_FLAG_HAS_JOIN: +64 join_sig + 8 join_ts] + * +64 update_sig + * +8 update_ts + * +1 userinfo_len + * +var userinfo + */ +#pragma pack(push, 1) +struct TGI_INVITE_REQ { uint8_t cmd, subcmd; uint64_t group_id; }; +struct TGI_INVITE_RESP { uint8_t cmd, subcmd; uint64_t group_id; uint8_t is_member; uint8_t reserved[3]; uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; uint8_t node_name_len; uint8_t node_name[0]; }; +#pragma pack(pop) + +#define TGI_INVITE_RESP_HDR_SIZE (sizeof(struct TGI_INVITE_RESP)) +#define TGI_INVITE_RESP_CH_MIN_SIZE (1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1) + +enum { TGI_CONNECTING = 0, TGI_WAIT_INFO = 1 }; + +struct tgi_pending { + struct tgi_pending* next; + struct UTUN_INSTANCE* inst; + uint64_t node_id, group_id; + uint8_t state; + uint8_t cancelled; + void* overall_timer; + struct NODE_CONN_DIRECT* ncd_handle; + struct TOPO_GROUP_NODE* temp_nq; + tgi_cb_t cb; void* cb_arg; +}; + +/* глобальный список pending-invite (invite — редкая одиночная операция) */ +static struct tgi_pending* g_tgi_list = NULL; + +static void tgi_cancel(struct tgi_pending* p); +static void tgi_fail(struct tgi_pending* p); +static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len); +static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len); +static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg); + +static void tgi_unlink(struct tgi_pending* p) { + struct tgi_pending** pp = &g_tgi_list; + while (*pp) { if (*pp == p) { *pp = p->next; return; } pp = &(*pp)->next; } +} + +static struct tgi_pending* tgi_find_waiting(uint64_t node_id) { + for (struct tgi_pending* p = g_tgi_list; p; p = p->next) + if (p->state == TGI_WAIT_INFO && p->node_id == node_id) return p; + return NULL; +} + +/* ═══════════ приём пакетов ═══════════ */ + +void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { + if (!conn || !entry || entry->len < 2) { + if (entry) { if (entry->dgram) queue_dgram_free(entry); queue_entry_free(entry); } + return; + } + uint8_t* d = entry->dgram; + if (d[0] != ETCP_RT_ID_GROUP_INVITE) { queue_dgram_free(entry); queue_entry_free(entry); return; } + uint8_t sub = d[1]; + if (sub == TGI_SUBCMD_INFO_REQ) tgi_handle_info_req(conn, d, entry->len); + else if (sub == TGI_SUBCMD_INFO_RESP) tgi_handle_info_resp(conn, d, entry->len); + queue_dgram_free(entry); queue_entry_free(entry); +} + +/* Инвайтера сторона: INVITE_INFO_REQ → проверка членства → INVITE_INFO_RESP */ +static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { + if (len < sizeof(struct TGI_INVITE_REQ)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ too short (%zu)", len); return; } + struct TGI_INVITE_REQ* req = (struct TGI_INVITE_REQ*)data; + struct UTUN_INSTANCE* inst = conn->instance; + if (!inst || !inst->topo_groups) return; + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ from=%016llx group=%016llx", + (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id); + + char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id); + + struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, req->group_id); + struct TOPO_GROUP* sg = grp ? grp : topo_groups_get_default(inst->topo_groups); + struct TOPO_NODE* ln = sg && sg->local_node ? topo_node_registry_find(inst->topo_groups, sg->local_node->node_id) : NULL; + const char* nm = ln ? ln->node_name : NULL; size_t nl = nm ? strlen(nm) : 0; if (nl > 255) nl = 255; + + sqlite3* db = inst->topo_sqlite_db; + char ch_name[128] = ""; uint64_t owner = 0; + uint8_t ch_x25519[32] = {0}, ch_ed_pub[32] = {0}, ch_sig[64] = {0}; + int have_channel = db && topo_node_sqlite_channel_get(db, ch_id, ch_name, (int)sizeof(ch_name), + &owner, ch_x25519, ch_ed_pub, ch_sig) == 0; + + if (!have_channel) { + size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl; + uint8_t* rb = u_calloc(1, rs); if (!rb) return; + struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb; + resp->cmd = ETCP_RT_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP; + resp->group_id = req->group_id; resp->is_member = 0; + if (sg) memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE); + resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); + struct ll_entry* qe = queue_entry_new(0); + if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; etcp_send(conn, qe); } else u_free(rb); + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: group=%016llx NOT in DB — responding is_member=NO to 0x%016llx", + (unsigned long long)req->group_id, (unsigned long long)conn->peer_node_id); + return; + } + + /* ── участник: генерируем join_sig / update_sig ── */ + uint64_t myid = inst->node_id; + uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0; + { + if (db && topo_node_sqlite_member_get_join(db, ch_id, myid, my_join_sig, &my_join_ts) != 0) { + uint8_t join_msg[256]; size_t mlen = 0; + memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32; + memcpy(join_msg + mlen, ch_ed_pub, 32); mlen += 32; + memcpy(join_msg + mlen, &myid, 8); mlen += 8; + memcpy(join_msg + mlen, inst->my_keys.public_key, 32); mlen += 32; + my_join_ts = (uint64_t)ntp_time_get_seconds(inst); + memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8; + if (sc_ed25519_sign(inst->my_ed25519_privkey, join_msg, mlen, my_join_sig) != SC_OK) + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: join_sig sign FAILED myid=%016llx ch=%s", (unsigned long long)myid, ch_id); + else + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig NEW myid=%016llx ts=%llu ch=%s", + (unsigned long long)myid, (unsigned long long)my_join_ts, ch_id); + } + } + + uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts = 0; + { + char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", + inst->name[0] ? inst->name : ""); + uint8_t umsg[256]; size_t ulen = 0; + memcpy(umsg + ulen, my_join_sig, 64); ulen += 64; + my_update_ts = (uint64_t)ntp_time_get_seconds(inst); + memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8; + size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1; + if (sc_ed25519_sign(inst->my_ed25519_privkey, umsg, ulen, my_update_sig) != SC_OK) + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: update_sig sign FAILED myid=%016llx", (unsigned long long)myid); + } + + /* ── создаём инфраструктуру группы СРАЗУ ── */ + chat_core_ensure_channel_ready(ch_id); + + /* ── сериализуем ответ ── */ + size_t ch_nl = strlen(ch_name); if (ch_nl > 255) ch_nl = 255; + uint8_t inv_flags = (my_join_sig[0] || my_join_ts) ? PEERS_FLAG_HAS_JOIN : 0; + char userinfo[256]; size_t userinfo_len = (size_t)snprintf(userinfo, sizeof(userinfo), + "{\"name\":\"%s\"}", inst->name[0] ? inst->name : ""); + + size_t ch_info_len = 1 + ch_nl + 8 + 32 + 32 + 64 + 1 + + (inv_flags & PEERS_FLAG_HAS_JOIN ? 72 : 0) + + 64 + 8 + 1 + userinfo_len; + size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl + ch_info_len; + uint8_t* rb = u_malloc(rs); if (!rb) return; + memset(rb, 0, rs); + + { struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb; + resp->cmd = ETCP_RT_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP; + resp->group_id = req->group_id; resp->is_member = 1; + if (sg) memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE); + resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); } + + uint8_t* ch = rb + TGI_INVITE_RESP_HDR_SIZE + nl; + *ch++ = (uint8_t)ch_nl; memcpy(ch, ch_name, ch_nl); ch += ch_nl; + memcpy(ch, &owner, 8); ch += 8; + memcpy(ch, ch_x25519, 32); ch += 32; + memcpy(ch, ch_ed_pub, 32); ch += 32; + memcpy(ch, ch_sig, 64); ch += 64; + *ch++ = inv_flags; + if (inv_flags & PEERS_FLAG_HAS_JOIN) { memcpy(ch, my_join_sig, 64); ch += 64; memcpy(ch, &my_join_ts, 8); ch += 8; } + memcpy(ch, my_update_sig, 64); ch += 64; + memcpy(ch, &my_update_ts, 8); ch += 8; + *ch++ = (uint8_t)userinfo_len; memcpy(ch, userinfo, userinfo_len); + + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "invite: membership CHECK OK from=0x%016llx group=%016llx ch_name=%.*s owner=%016llx inv_flags=%u → is_member=YES (%zuB)", + (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id, + (int)ch_nl, ch_name, (unsigned long long)owner, (unsigned)inv_flags, rs); + + { struct ll_entry* qe = queue_entry_new(0); + if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; etcp_send(conn, qe); } else u_free(rb); + } +} + +/* Джойнер: INVITE_INFO_RESP → верификация ch_sig → создать канал → TGI_EVENT_JOIN */ +static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { + if (len < TGI_INVITE_RESP_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP too short (%zu < %zu)", len, TGI_INVITE_RESP_HDR_SIZE); return; } + struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)data; + uint8_t nnl = resp->node_name_len; + if (len < TGI_INVITE_RESP_HDR_SIZE + (size_t)nnl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP truncated"); return; } + + struct tgi_pending* inv = tgi_find_waiting(conn->peer_node_id); + if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP — no pending invite in WAIT_INFO state"); return; } + + if (!resp->is_member) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id); + tgi_fail(inv); return; + } + + /* ── сохраняем Ed25519 pubkey ответчика для NODEINFO-верификации ── */ + if (inv->temp_nq) { + struct TOPO_NODE* ni2 = topo_node_registry_find(inv->inst->topo_groups, inv->temp_nq->node_id); + if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE); + } + + /* ── парсим channel_info ── */ + const uint8_t* ch = data + TGI_INVITE_RESP_HDR_SIZE + nnl; + size_t ch_remain = len - (TGI_INVITE_RESP_HDR_SIZE + nnl); + if (ch_remain < TGI_INVITE_RESP_CH_MIN_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP channel info too short"); tgi_fail(inv); return; } + + uint8_t c_nl = *ch++; ch_remain--; + if (ch_remain < (size_t)c_nl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_name truncated"); tgi_fail(inv); return; } + char ch_name[128]; size_t cn = c_nl > 127 ? 127 : c_nl; memcpy(ch_name, ch, cn); ch_name[cn] = '\0'; ch += c_nl; ch_remain -= c_nl; + + uint64_t ch_owner; memcpy(&ch_owner, ch, 8); ch += 8; ch_remain -= 8; + const uint8_t* ch_x25519 = ch; ch += 32; ch_remain -= 32; + const uint8_t* ch_ed_pub = ch; ch += 32; ch_remain -= 32; + const uint8_t* ch_sig = ch; ch += 64; ch_remain -= 64; + + if (ch_remain < 1) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at inv_flags"); tgi_fail(inv); return; } + uint8_t inv_flags = *ch++; ch_remain--; + + const uint8_t* join_sig = NULL; uint64_t join_ts = 0; + if (inv_flags & PEERS_FLAG_HAS_JOIN) { + if (ch_remain < 72) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at join_sig"); tgi_fail(inv); return; } + join_sig = ch; ch += 64; memcpy(&join_ts, ch, 8); ch += 8; ch_remain -= 72; + } + + if (ch_remain < 73) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at update"); tgi_fail(inv); return; } + const uint8_t* upd_sig = ch; ch += 64; uint64_t upd_ts; memcpy(&upd_ts, ch, 8); ch += 8; ch_remain -= 72; + + uint8_t ui_len = *ch++; ch_remain--; + char ch_userinfo[256] = ""; + if (ui_len > 0 && ch_remain >= ui_len) { size_t ul = ui_len > 255 ? 255 : ui_len; memcpy(ch_userinfo, ch, ul); ch_userinfo[ul] = '\0'; } + + char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)resp->group_id); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, + "invite: INFO_RESP from=0x%016llx group=%016llx ch_name=%s ch_owner=%016llx inv_flags=%u join_ts=%llu upd_ts=%llu userinfo=%s", + (unsigned long long)conn->peer_node_id, (unsigned long long)resp->group_id, + ch_name, (unsigned long long)ch_owner, (unsigned)inv_flags, + (unsigned long long)join_ts, (unsigned long long)upd_ts, + ch_userinfo[0] ? ch_userinfo : "?"); + + /* ── верифицируем ch_sig ── */ + { + uint8_t vmsg[1024]; size_t vlen = 0; + vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1; + vlen += (size_t)snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_name) + 1; + memcpy(vmsg + vlen, &ch_owner, 8); vlen += 8; + memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32; + memcpy(vmsg + vlen, ch_ed_pub, 32); vlen += 32; + if (sc_ed25519_verify(ch_ed_pub, vmsg, vlen, ch_sig) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY FAIL — rejecting group=%016llx from=0x%016llx", + (unsigned long long)resp->group_id, (unsigned long long)conn->peer_node_id); + tgi_fail(inv); return; + } + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY OK"); + } + + /* ── сохраняем канал и создаём группу ── */ + { sqlite3* ldb = inv->inst->topo_sqlite_db; + if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner, + ch_x25519, NULL, ch_ed_pub, NULL, ch_sig); + } + chat_core_ensure_channel_ready(ch_id); + + /* ── верифицируем update_sig приглашающего ── */ + { + const uint8_t* inv_ed = conn->peer_ed25519_pubkey; + uint8_t ivmsg[256]; size_t ilen = 0; + if (join_sig && (inv_flags & PEERS_FLAG_HAS_JOIN)) { memcpy(ivmsg + ilen, join_sig, 64); ilen += 64; } + else { memset(ivmsg + ilen, 0, 64); ilen += 64; } + memcpy(ivmsg + ilen, &upd_ts, 8); ilen += 8; + { const char* nm2 = ch_userinfo[0] ? ch_userinfo : ""; + size_t nl2 = strlen(nm2); memcpy(ivmsg + ilen, nm2, nl2 + 1); ilen += nl2 + 1; } + if (sc_ed25519_verify(inv_ed, ivmsg, ilen, upd_sig) != SC_OK) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY FAIL node=0x%016llx", (unsigned long long)conn->peer_node_id); + } else { + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY OK"); + } + } + + if (inv->overall_timer) { uasync_cancel_timeout(inv->inst->ua, inv->overall_timer); inv->overall_timer = NULL; } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" ch=%s — full channel ready", + (unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id); + + /* ── успех: переводим ncd-handle во владение узла, доставляем JOIN ── */ + { + uint64_t node_id = inv->node_id, group_id = inv->group_id; + tgi_cb_t cb = inv->cb; void* cb_arg = inv->cb_arg; + if (inv->ncd_handle) node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL); + struct TOPO_GROUP* g = topo_groups_find(inv->inst->topo_groups, group_id); + struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, node_id) : NULL; + if (nq) nq->handle = inv->ncd_handle; + inv->ncd_handle = NULL; /* не закрываем — соединение переходит узлу */ + inv->temp_nq = NULL; /* не удаляем узел из группы */ + tgi_cancel(inv); + if (cb) cb(node_id, group_id, TGI_EVENT_JOIN, cb_arg); + } +} + +/* ═══════════ ncd-коллбэк (джойнер) ═══════════ */ + +static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) { + struct tgi_pending* p = (struct tgi_pending*)arg; + if (!p || p->cancelled) return; + if (ev == NCD_EVENT_UP) { + if (p->state != TGI_CONNECTING) return; + struct ETCP_CONN* conn = node_conn_direct_get_conn(h); + if (!conn) { tgi_fail(p); return; } + p->state = TGI_WAIT_INFO; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: connected to 0x%016llx — sending membership check", (unsigned long long)p->node_id); + struct TGI_INVITE_REQ req; + req.cmd = ETCP_RT_ID_GROUP_INVITE; req.subcmd = TGI_SUBCMD_INFO_REQ; req.group_id = p->group_id; + struct ll_entry* qe = queue_entry_new(0); + if (!qe) { tgi_fail(p); return; } + qe->dgram = u_malloc(sizeof(req)); + if (!qe->dgram) { queue_entry_free(qe); tgi_fail(p); return; } + memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req); + if (etcp_send(conn, qe) != 0) { u_free(qe->dgram); queue_entry_free(qe); tgi_fail(p); } + } else if (ev == NCD_EVENT_TIMEOUT) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ncd TIMEOUT connecting to 0x%016llx", (unsigned long long)p->node_id); + tgi_fail(p); + } +} + +static void tgi_overall_timeout(void* arg) { + struct tgi_pending* p = (struct tgi_pending*)arg; + if (p->cancelled) return; + p->overall_timer = NULL; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: overall TIMEOUT to 0x%016llx — no response within %dms", + (unsigned long long)p->node_id, TGI_TIMEOUT_MS); + tgi_fail(p); +} + +/* ═══════════ отмена / fail ═══════════ */ + +static void tgi_cancel(struct tgi_pending* p) { + if (!p) return; + p->cancelled = 1; + if (p->overall_timer) { uasync_cancel_timeout(p->inst->ua, p->overall_timer); p->overall_timer = NULL; } + if (p->ncd_handle) { node_conn_direct_force_close(p->ncd_handle); p->ncd_handle = NULL; } + if (p->temp_nq) { + struct TOPO_GROUP* g = topo_groups_find(p->inst->topo_groups, p->group_id); + if (g) { queue_remove_data(g->nodes, &p->temp_nq->ll); topo_nodeq_free_group_fields(p->inst->topo_groups, p->temp_nq); queue_entry_free(&p->temp_nq->ll); } + p->temp_nq = NULL; + } + tgi_unlink(p); + u_free(p); +} + +static void tgi_fail(struct tgi_pending* p) { + if (!p || p->cancelled) return; + uint64_t node_id = p->node_id, group_id = p->group_id; + tgi_cb_t cb = p->cb; void* cb_arg = p->cb_arg; + tgi_cancel(p); + if (cb) cb(node_id, group_id, TGI_EVENT_TIMEOUT, cb_arg); +} + +void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst) { + struct tgi_pending* p = g_tgi_list; + while (p) { + struct tgi_pending* next = p->next; + if (!inst || p->inst == inst) tgi_cancel(p); + p = next; + } +} + +/* ═══════════ инициализация ═══════════ */ + +void topo_group_invite_init(struct UTUN_INSTANCE* inst) { + if (!inst) return; + etcp_bind(inst, ETCP_RT_ID_GROUP_INVITE, topo_group_invite_recv_handler); +} + +/* ═══════════ входная точка джойнера ═══════════ */ + +int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, + struct TOPO_NODE* ni, uint64_t node_id, + tgi_cb_t cb, void* cb_arg) { + if (!inst || !inst->topo_groups || !cb || !ni) return -1; + + /* 1. Найти или создать группу */ + struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); + if (!group) { + char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); + group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch); + if (!group) return -1; + } + + uint64_t nid = node_id; + if (!nid || nid == inst->node_id) return -1; + if (!ni->v4_addrs && !ni->v6_addrs) return -1; + if (!ni->public_key[0]) return -1; + + /* 2. Копировать ni в group_ni */ + struct TOPO_GROUPS* groups = inst->topo_groups; + struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1; + gni->node_id = nid; gni->ver = 1; gni->group_id = group_id; memcpy(gni->public_key, ni->public_key, SC_PUBKEY_SIZE); + for (const struct TOPO_ADDR4* src = ni->v4_addrs; src; src = src->next) { + struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue; + memset(a4, 0, sizeof(*a4)); memcpy(a4->addr, src->addr, 4); a4->port = src->port; + a4->type = TOPO_ADDR_NAT; a4->protocol = src->protocol; a4->next = gni->v4_addrs; gni->v4_addrs = a4; + } + for (const struct TOPO_ADDR6* src6 = ni->v6_addrs; src6; src6 = src6->next) { + struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue; + memset(a6, 0, sizeof(*a6)); memcpy(a6->addr, src6->addr, 16); a6->port = src6->port; + a6->type = TOPO_ADDR_NAT; a6->protocol = src6->protocol; a6->next = gni->v6_addrs; gni->v6_addrs = a6; + } + if (!gni->v4_addrs && !gni->v6_addrs) { u_free(gni); return -1; } + gni = topo_node_registry_store(groups, gni); if (!gni) return -1; + + /* 3. TOPO_GROUP_NODE + pending */ + struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE)); + if (!qe) { topo_node_registry_unref(groups, gni->node_id); return -1; } + struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)qe; + memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry)); nq->node_id = nid; + + struct tgi_pending* p = u_calloc(1, sizeof(*p)); + if (!p) { queue_entry_free(qe); topo_node_registry_unref(groups, gni->node_id); return -1; } + p->inst = inst; p->node_id = nid; p->group_id = group_id; p->state = TGI_CONNECTING; + p->temp_nq = nq; p->cb = cb; p->cb_arg = cb_arg; + p->next = g_tgi_list; g_tgi_list = p; + + queue_data_put_with_index(group->nodes, &nq->ll); + + /* 4. прямое подключение (ncd) */ + if (node_conn_direct_open_node(inst, nid, tgi_ncd_cb, p, &p->ncd_handle, gni, NULL) == NCD_ERR) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ncd open failed for 0x%016llx", (unsigned long long)nid); + tgi_fail(p); return -1; + } + + /* 5. лог адресов */ + { + char addrs[512] = ""; int aoff = 0; + for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next) + aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)", + a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port, + a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : "?"); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%dms", + (unsigned long long)nid, addrs, TGI_TIMEOUT_MS); + } + + p->overall_timer = uasync_set_timeout(inst->ua, TGI_TIMEOUT_MS * 10, p, tgi_overall_timeout, "tgi_overall"); + return 0; +} + +/* ═══════════ инвайтера сторона (direct, без INVITE_INFO) ═══════════ */ + +struct tgi_conn_ctx { struct UTUN_INSTANCE* inst; uint64_t node_id, group_id; }; + +static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) { + struct tgi_conn_ctx* c = (struct tgi_conn_ctx*)arg; + if (ev == NCD_EVENT_UP) { + struct TOPO_GROUP* g = topo_groups_find(c->inst->topo_groups, c->group_id); + struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, c->node_id) : NULL; + if (nq) nq->handle = h; + node_conn_direct_set_callback(h, NULL, NULL); + u_free(c); + } else if (ev == NCD_EVENT_TIMEOUT) { + u_free(c); + } +} + +int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id, + struct TOPO_NODE* ni, uint64_t node_id) { + if (!inst || !inst->topo_groups) return -1; + struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); + if (!group) { + char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); + group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch); + if (!group) return -1; + } + if (!node_id || node_id == inst->node_id) return -1; + + struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); + if (!nq) { + struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE)); + if (!qe) return -1; + nq = (struct TOPO_GROUP_NODE*)qe; + memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry)); + nq->node_id = node_id; + queue_data_put_with_index(group->nodes, &nq->ll); + } + + struct tgi_conn_ctx* c = u_calloc(1, sizeof(*c)); + if (!c) return -1; + c->inst = inst; c->node_id = node_id; c->group_id = group_id; + + struct NODE_CONN_DIRECT* h = NULL; + int r = ni ? node_conn_direct_open_node(inst, node_id, tgi_to_channel_cb, c, &h, ni, NULL) + : node_conn_direct_open(inst, node_id, tgi_to_channel_cb, c, &h, NULL); + if (r == NCD_ERR || !h) { u_free(c); return -1; } + return 0; +} diff --git a/src/routing_layer/topo_group_invite.h b/src/routing_layer/topo_group_invite.h new file mode 100644 index 00000000..13738671 --- /dev/null +++ b/src/routing_layer/topo_group_invite.h @@ -0,0 +1,73 @@ +/** + * @file topo_group_invite.h + * @brief Invite/join к каналу через прямое (ncd) подключение. + * + * Модуль содержит всю логику работы с invite-ссылками: прямое подключение к + * приглашающему узлу через node_conn_direct (ncd) и проверку членства + * (INVITE_INFO_REQ/RESP) с бутстрапом криптографической идентичности канала. + * + * Поток джойнера (topo_group_invite_join): + * 1. Создаёт/находит группу, сохраняет TOPO_NODE приглашающего. + * 2. Открывает прямое соединение (ncd) и шлёт INVITE_INFO_REQ. + * 3. Получает INVITE_INFO_RESP — проверяет ch_sig, сохраняет канал в БД, + * поднимает инфраструктуру группы (chat_core_ensure_channel_ready). + * 4. Доставляет TGI_EVENT_JOIN. + * + * Инвайтера сторона: tgi_handle_info_req отвечает на INVITE_INFO_REQ — + * проверяет членство в БД и возвращает полную информацию о канале. + * + * conn_mgr при этом используется ТОЛЬКО для медиа — invite больше от него + * не зависит. + */ +#ifndef TOPO_GROUP_INVITE_H +#define TOPO_GROUP_INVITE_H + +#include +#include "topo_node.h" +#include "secure_channel.h" + +struct UTUN_INSTANCE; +struct ETCP_CONN; +struct ll_entry; +struct NODE_CONN_DIRECT; + +/* service id протокола invite (etcp_bind) */ +#define ETCP_RT_ID_GROUP_INVITE 0x13 + +#define TGI_SUBCMD_INFO_REQ 0x01 +#define TGI_SUBCMD_INFO_RESP 0x02 + +/* события коллбэка джойнера */ +#define TGI_EVENT_JOIN 0 +#define TGI_EVENT_TIMEOUT 1 + +typedef void (*tgi_cb_t)(uint64_t node_id, uint64_t group_id, int event, void* arg); + +/** Идемпотентная инициализация: биндит обработчик ETCP_RT_ID_GROUP_INVITE. */ +void topo_group_invite_init(struct UTUN_INSTANCE* inst); + +/** + * Джойнер: прямое подключение (ncd) к приглашающему + проверка членства. + * ni — TOPO_NODE приглашающего (pubkey + адреса), не владеем. + * При успехе вызывается cb(TGI_EVENT_JOIN), при провале cb(TGI_EVENT_TIMEOUT). + */ +int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, + struct TOPO_NODE* ni, uint64_t node_id, + tgi_cb_t cb, void* cb_arg); + +/** + * Инвайтера сторона: прямое подключение (ncd) к целевому узлу для последующей + * отправки CHANNEL_INVITE (через chat_sync, по ETCP_CONN_STATUS_UP). + * Без INVITE_INFO-проверки членства. ni может быть NULL — тогда адреса грузятся + * из БД (node_conn_direct_open). Handle сохраняется в nq->handle. + */ +int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id, + struct TOPO_NODE* ni, uint64_t node_id); + +/** Отменяет все pending-invite (teardown). */ +void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst); + +/** Обработчик приёма пакетов invite (etcp_bind). */ +void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); + +#endif diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index 91e4a4f6..986278a5 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -33,7 +33,7 @@ extern "C" { struct TOPO_GROUP; struct TOPO_GROUPS; struct ETCP_SOCKET; -struct CONN_MGR_HANDLE; +struct NODE_CONN_DIRECT; struct ETCP_CONN; struct UTUN_INSTANCE; @@ -176,7 +176,7 @@ struct TOPO_GROUP_NODE { struct TOPO_CONNECTIVITY connectivity; // результаты ping-проб (interface/nat/real) uint8_t conn_presence; // NODE_CONN_* — какие подключения есть в принципе uint8_t conn_up; // NODE_CONN_* — какие из них подняты - struct CONN_MGR_HANDLE* handle; // активный хендл соединения к узлу + struct NODE_CONN_DIRECT* handle; // активный ncd-handle прямого соединения к узлу }; // API — глобальный реестр TOPO_NODE diff --git a/src/routing_layer/topo_node_doc.md b/src/routing_layer/topo_node_doc.md index f75cf75a..c347909e 100644 --- a/src/routing_layer/topo_node_doc.md +++ b/src/routing_layer/topo_node_doc.md @@ -2,76 +2,88 @@ ## 1. Назначение -Модель данных узла топологии — структуры данных для представления узла в памяти, сериализации/десериализации в бинарный wire-формат для BGP-обмена, учёт ссылок (refcounting) и диагностический дамп. +Модель данных узла топологии — структуры для представления узла в памяти, сериализации/ +десериализации в бинарный wire-формат для BGP-обмена, глобальный реестр идентичностей +(refcounting) и диагностический дамп. -Модуль определяет два слоя представлений: -- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) для удобной модификации. -- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети без выравнивания. +Модуль определяет два слоя: +- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) и две + структуры узла — `TOPO_NODE` (глобальная идентичность) и `TOPO_GROUP_NODE` (per-group данные). +- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети. -Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только данные и их упаковка/распаковка. +Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только +данные и их упаковка/распаковка. Персистентность — в `topo_node_sqlite`. -Узлы персистентно хранятся в SQLite через модуль `topo_node_sqlite`. Память для элементов списков (адреса, сокет-мета, подсети) выделяется из memory pool'ов из `topo_groups`. +Два уровня представления узла: +- **TOPO_NODE** — глобальная идентичность (pubkeys, имя, адреса, self-signature). Хранится один + раз в глобальном реестре `TOPO_GROUPS->node_registry` (hash по node_id), разделяется между + группами через `group_ref_count`. +- **TOPO_GROUP_NODE** — узел в контексте группы: `node_id`, `paths`, `subnets`, `last_ver`, + `connectivity`, `conn_presence/conn_up`, `handle`. Хранится в `TOPO_GROUP->nodes`. ## 2. Как пользоваться ### Типовые сценарии -**Создание/обновление информации о себе:** +**Сборка информации о себе:** ```c topo_group_update_my_nodeinfo(instance, group); ``` -Строит `TOPO_NODEQ` (в `group->local_node`) из конфига: собирает список сокетов, адресов (interface/NAT/real), подсетей; при изменениях инкрементирует `ver` и помечает `dirty=1`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети не включаются. +Строит `group->local_node` (`TOPO_GROUP_NODE`) из конфига: сокеты, адреса (interface/NAT/real), +подсети; при изменениях инкрементирует `ver`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети +не включаются. **Сериализация для отправки по BGP:** ```c -uint8_t buf[2048]; -int len = topo_node_serialize(group, nq, buf, sizeof(buf)); +struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id); +uint8_t buf[4096]; +int len = topo_node_serialize(ni, nq, buf, sizeof(buf), cumulative_rtt); ``` -Запаковывает `TOPO_NODEQ` → бинарный буфер: заголовок `TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если `TOPO_FLAG_SEND_SUBNETS`), транзитные узлы и hop-лист. +Запаковывает `TOPO_NODE` + per-group данные (`TOPO_GROUP_NODE`) в буфер: заголовок +`TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если +`TOPO_FLAG_SEND_SUBNETS`) и hop-лист (через `topo_node_best_hop_list`). **Десериализация при получении NODEINFO:** ```c -struct TOPO_NODE *ni = NULL; -struct TOPO_NODESUBNETS *subnets = NULL; -struct TOPOMSG_TRANZIT *tranzit = NULL; -uint8_t tranzit_count = 0; -uint64_t *hop_list = NULL; -uint8_t hop_count = 0; -topo_node_deserialize(group, data, data_len, &ni, &subnets, &tranzit, &tranzit_count, &hop_list, &hop_count); +struct TOPO_NODE* ni = NULL; +struct TOPO_NODESUBNETS* subnets = NULL; +uint64_t* hop_list = NULL; uint8_t hop_count = 0; +uint16_t cumulative_rtt = 0; +topo_node_deserialize(group, data, data_len, &ni, &subnets, &hop_list, &hop_count, &cumulative_rtt); ``` -Восстанавливает `TOPO_NODE`, подсети, транзит и hop-лист из бинарного буфера. Элементы списков аллоцируются из memory pool'ов. Выделенная память должна быть освобождена: -- `topo_node_unref(ni)` — освобождает `TOPO_NODE` и `node_name` -- `topo_node_free_lists(group, nq)` — освобождает списки обратно в пулы, а также `subnets`, `tranzit_data`, `hop_list` +Восстанавливает `TOPO_NODE` (через `u_calloc`), подсети и hop-лист. Освобождение: +- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). +- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (`subnets`, `paths`-элементы). **Поиск узла по ID:** ```c -struct TOPO_NODEQ *nq = topo_node_find_by_id(group, node_id); +struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); // per-group +struct TOPO_NODE* ni = topo_node_registry_find(groups, node_id); // глобальная идентичность ``` -Использует хеш-индекс очереди `group->nodes` (ключ — `node_id`, 8 байт). **Дамп всех узлов:** ```c -topo_node_dump_all(group); // в лог -topo_node_format_all(group, buf, size); // в строку +topo_node_dump_all(group); // в лог +topo_node_format_all(group, buf, size); // в строку ``` ### Ключевые концепции -- **TOPO_NODE** — базовые данные узла: pubkey (X25519), ed25519_pubkey, имя, версия, групповой ID. Разделяется между группами через счётчик ссылок. - -- **TOPO_NODEQ** — запись в очереди узлов группы. Содержит `TOPO_NODE*` (общий), плюс динамические пути, подсети, транзитные узлы, hop-лист, связность, conn_mgr. Все поля аллоцированы отдельно и освобождаются через `topo_node_free_lists()`. - -- **TOPO_NODEPATH** — путь до узла через конкретное ETCP-соединение. Хранится в `paths`-очереди типа `ll_queue`. После самой структуры идёт массив `uint64_t hop[hop_count]` (variable-length). - -- **Счётчик ссылок:** `topo_node_ref()`/`topo_node_unref()`. При падении счётчика до 0 освобождается `node_name` и сам `TOPO_NODE`. Сами списки адресов и sock_meta освобождаются отдельно через `free_lists()`. - -- **Адреса бывают трёх типов:** `TOPO_ADDR_INTERFACE` (LAN-адрес), `TOPO_ADDR_NAT` (после детекции NAT), `TOPO_ADDR_REAL` (подтверждённый прямой адрес). Для каждого сокета может быть до 3 адресов в списке. - -- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): статус зондирования (NONE/IN_PROGRESS/DONE), результаты для interface/nat/real адресов, минимальные RTT, время зондирования. - -- **TOPO_FLAG_SEND_SUBNETS** — флаг в `TOPO_NODE.flags`: если установлен, подсети сериализуются и включаются в wire-формат. Для chat-групп сбрасывается в 0. - -- **Wire-протокол:** заголовок `TOPOMSG_NODE` (75 байт packed) + динамическая часть: `node_name` (0-255 байт), `SOCKMETA4*N`, `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, транзит, hop-лист. Размер динамической части вычисляется функцией `topo_node_dyn_size()`. +- **Глобальный реестр**: `TOPO_NODE` хранится один раз в `node_registry`; `topo_node_registry_store` + дедуплицирует по `node_id` (возвращает существующий при совпадении). `registry_ref/unref` + управляют `group_ref_count`; при 0 — `topo_node_destroy`. +- **Путь**: `TOPO_NODEPATH = {conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]` + сразу после структуры. Полный hop-лист для отправки — `topo_node_best_hop_list`. +- **Self-signature**: `x25519_self_sig` — Ed25519-подпись над каноническим сообщением + (`topo_node_build_sig_msg`: pubkey || имя || client_type || client_activity || адреса). + Ставится `topo_node_sign_self`, проверяется в `topo_group_process_nodeinfo`. +- **Адреса трёх типов**: `TOPO_ADDR_INTERFACE` (LAN), `TOPO_ADDR_NAT` (после детекции), + `TOPO_ADDR_REAL` (подтверждённый прямой). Для каждого сокета — до 3 адресов. +- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): probe_status + (NONE/IN_PROGRESS/DONE/DEFERRED), RTT и статусы interface/nat/real, `probe_deferred_wait`. +- **TOPO_FLAG_SEND_SUBNETS** — если установлен, подсети сериализуются; для chat-групп сброшен. +- **Wire-протокол**: заголовок `TOPOMSG_NODE` (packed) + динамика: `node_name`, `SOCKMETA4*N`, + `ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, hop-лист. Размер динамики — `topo_node_dyn_size()`. ## 3. API @@ -79,92 +91,88 @@ topo_node_format_all(group, buf, size); // в строку | Структура | Назначение | |-----------|-----------| -| `struct TOPO_NODE` | Идентичность узла: pubkeys (X25519+Ed25519), имя, версия, group_id, node_id, списки v4/v6 сокет-метаданных и адресов. Счётчик ссылок. | -| `struct TOPO_NODEQ` | Запись в очереди группы. Содержит `TOPO_NODE*`, подсети, транзитные данные, hop-лист, очередь `paths` (TOPO_NODEPATH), связность, conn_mgr. `best_socket` — указатель на лучший ETCP-сокет для связи. | -| `struct TOPO_NODEPATH` | Путь до узла через ETCP-соединение с hop_count. Variable-length: после структуры идёт `uint64_t hop[]`. | -| `struct TOPO_NODESUBNETS` | Списки v4 и v6 подсетей, анонсируемых узлом. Отдельный malloc, может быть NULL. | -| `struct TOPO_CONNECTIVITY` | Локальное состояние проверки связности: статусы зондирования, RTT для interface/nat/real адресов. | -| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата (75 байт). Содержит счётчики элементов динамической части. | -| `struct TOPOMSG_ADDR4/6` | Packed wire-формат одного адреса (addr, port, type, socket_id, protocol). | -| `struct TOPOMSG_SOCKMETA4/6` | Packed wire-формат метаданных сокета (id, config_type, nat_type). | -| `struct TOPOMSG_SUBNET4/6` | Packed wire-формат подсети (addr, prefix_length). | -| `struct TOPOMSG_TRANZIT` | Packed wire-формат транзитного узла (node_id, rtt, link_q). | +| `struct TOPO_NODE` | Глобальная идентичность узла: `flags`, `group_id`, `node_id`, `ver`, `public_key`, `ed25519_public_key`, `x25519_self_sig`, `client_type`, `client_activity`, `node_name`, списки v4/v6 sock_meta и addrs, `group_ref_count` | +| `struct TOPO_GROUP_NODE` | Per-group запись: `node_id`, `subnets`, `paths` (queue TOPO_NODEPATH), `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) | +| `struct TOPO_NODEPATH` | Путь через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[]` | +| `struct TOPO_NODESUBNETS` | v4/v6 подсети узла (отдельный malloc, может быть NULL) | +| `struct TOPO_CONNECTIVITY` | Связность: probe_status, RTT, статусы interface/nat/real, probe-тайминги | +| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата: счётчики, `hop_count`, `client_type/activity`, `cumulative_rtt` | +| `struct TOPOMSG_ADDR4/6` | Packed wire одного адреса (addr, port, type, socket_id, protocol) | +| `struct TOPOMSG_SOCKMETA4/6` | Packed wire сокет-метаданных (id, config_type, nat_type) | +| `struct TOPOMSG_SUBNET4/6` | Packed wire подсети (addr, prefix_length) | ### Вспомогательные типы в памяти (связанные списки) | Тип | Назначение | |-----|-----------| -| `struct TOPO_SOCKMETA4` | Элемент списка v4 сокет-метаданных (next*, id, config_type, nat_type). | -| `struct TOPO_ADDR4` | Элемент списка v4 адресов (next*, addr[4], port, type, socket_id, protocol). | -| `struct TOPO_SOCKMETA6` | Элемент списка v6 сокет-метаданных. | -| `struct TOPO_ADDR6` | Элемент списка v6 адресов. | -| `struct TOPO_SUBNET4` | Элемент списка v4 подсетей (next*, addr[4], prefix_length). | -| `struct TOPO_SUBNET6` | Элемент списка v6 подсетей. | +| `struct TOPO_SOCKMETA4/6` | Элемент списка сокет-метаданных (next*, id, config_type, nat_type) | +| `struct TOPO_ADDR4/6` | Элемент списка адресов (next*, addr, port, type, socket_id, protocol) | +| `struct TOPO_SUBNET4/6` | Элемент списка подсетей (next*, addr, prefix_length) | -Все элементы списков аллоцируются из memory pool'ов и освобождаются через `memory_pool_free()`. +Элементы списков аллоцируются из memory pool'ов (`topo_groups`) через `memory_pool_alloc/free`. ### Константы | Константа | Значение | Назначение | |-----------|---------|-----------| -| `TOPO_ADDR_INTERFACE` | 0 | Интерфейсный (LAN) адрес сокета | -| `TOPO_ADDR_NAT` | 1 | NAT-адрес после детекции | -| `TOPO_ADDR_REAL` | 2 | Подтверждённый прямой адрес | -| `TOPO_PROTO_UDP` | 0x01 | UDP-транспорт | -| `TOPO_PROTO_TCP` | 0x02 | TCP-транспорт | -| `PROBE_STATUS_NONE` | 0 | Зондирование не запускалось | -| `PROBE_STATUS_IN_PROGRESS` | 1 | Зондирование идёт | -| `PROBE_STATUS_DONE` | 2 | Зондирование завершено | -| `PROBE_RESULT_UNKNOWN` | 0 | Результат неизвестен | -| `PROBE_RESULT_REACHABLE` | 1 | Адрес достижим | -| `PROBE_RESULT_UNREACHABLE` | 2 | Адрес недостижим | -| `CONN_TYPE_DIRECT` | 1 | Прямое соединение | -| `CONN_TYPE_REVERSE` | 2 | Обратное соединение | -| `CONN_TYPE_INDIRECT` | 3 | Соединение через посредника | -| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Флаг: отправлять подсети | -| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID группы uTun по умолчанию | +| `TOPO_ADDR_INTERFACE` / `TOPO_ADDR_NAT` / `TOPO_ADDR_REAL` | 0/1/2 | Тип адреса | +| `TOPO_PROTO_UDP` / `TOPO_PROTO_TCP` | 0x01/0x02 | Транспорт | +| `ADDR_TYPE_NETIF/DIRECT/NAT_EIM/NAT_STRICT` | 0/1/2/3 | Классификация адреса | +| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы | +| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы | +| `CONN_TYPE_NONE/DIRECT/REVERSE/INDIRECT` | 0/1/2/3 | Стратегия соединения | +| `CLIENT_TYPE_SERVER/DESKTOP/MOBILE` | 0/1/2 | Тип клиента | +| `CLIENT_ACTIVITY_STANDBY/ACTIVE` | 0/1 | Режим | +| `NCONN_DIRECT/INDIRECT/BGP` | 1/2/4 | Биты наличия соединений | +| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Отправлять подсети | +| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID utun-группы по умолчанию | +| `TOPO_GROUP_TYPE_UTUN/CHAT` | 1/2 | Тип группы | +| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников | ### Функции -**Управление памятью и refcounting:** -- `topo_node_ref(ni)` — увеличить счётчик ссылок на `TOPO_NODE`. -- `topo_node_unref(ni)` — уменьшить счётчик; при 0 освободить `node_name` и `TOPO_NODE` (но не списки — их надо предварительно освободить через `topo_node_free_lists`). -- `topo_node_free_lists(group, nq)` — освободить все списки адресов/sock_meta обратно в memory pool'ы, освободить `subnets`, `tranzit_data`, `hop_list`, сделать `unref` на `node`, обнулить указатели. +**Реестр и refcounting:** +- `topo_node_registry_find(groups, node_id)` — найти `TOPO_NODE` в глобальном реестре. +- `topo_node_registry_store(groups, ni)` — сохранить с дедупликацией (возвращает существующий при совпадении). +- `topo_node_registry_ref/unref(groups, node_id)` — инкремент/декремент `group_ref_count`. +- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя). + +**Per-group:** +- `topo_node_find_by_id(group, node_id)` — найти `TOPO_GROUP_NODE` по node_id (hash). +- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (subnets, paths) + unref глобального TOPO_NODE. **Сериализация:** -- `topo_node_dyn_size(msg)` — вычислить размер динамической части wire-формата по полям-счётчикам заголовка `TOPOMSG_NODE`. Не учитывает сам заголовок. -- `topo_node_serialize(group, nq, out, out_max)` — сериализовать `TOPO_NODEQ` в бинарный буфер. Возвращает количество записанных байт или -1 при ошибке (переполнение или невалидные аргументы). Формат: заголовок `TOPOMSG_NODE`, затем name, v4 sock_meta, v4 addrs, v6 sock_meta, v6 addrs, подсети (опционально), tranzit, hop_list. +- `topo_node_dyn_size(msg)` — размер динамической части по счётчикам заголовка. +- `topo_node_serialize(ni, nq, out, out_max, cumulative_rtt)` — сериализовать `TOPO_NODE` + per-group в буфер. +- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_hop_list, &out_hop_count, &out_cumulative_rtt)` — восстановить из wire-формата. -**Десериализация:** -- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_tranzit, &out_tranzit_count, &out_hop_list, &out_hop_count)` — восстановить `TOPO_NODE`, подсети, транзит и hop-лист из бинарного wire-формата. Аллоцирует `TOPO_NODE` через `u_calloc` (ref_count=1), элементы списков — из memory pool'ов, `subnets`, `tranzit_data`, `hop_list` — отдельными `u_malloc`. Возвращает 0 при успехе, -1 при несоответствии размера или ошибке аллокации. Если подсети есть в wire-формате но флаг `TOPO_FLAG_SEND_SUBNETS` не установлен, они пропускаются. +**Пути:** +- `topo_node_best_hop_list(nq, &out_count, &out_rtt)` — лучший hop-лист (по min hop_count). -**Поиск:** -- `topo_node_find_by_id(group, node_id)` — найти `TOPO_NODEQ` в очереди группы по `node_id` через хеш-индекс. Возвращает NULL если не найден. +**Подпись:** +- `topo_node_build_sig_msg(ni, buf, buf_size)` — каноническое сообщение для Ed25519-подписи. +- `topo_node_sign_self(instance, ni)` — подписать свой NODEINFO (`x25519_self_sig`). -**Обновление информации о себе:** -- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `group->local_node` (`TOPO_NODEQ`) из текущих данных инстанса (сокеты, NAT, адреса, подсети, ключи). Сравнивает количество элементов с предыдущим значением; если есть изменения — освобождает старый `local_node`, создаёт новый, инкрементирует `ver`, помечает `dirty=1`. Обновляет таблицу маршрутов (`route_delete`/`route_insert`). Для chat-групп подсети не включаются. Возвращает количество v4 подсетей. +**Обновление о себе:** +- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `local_node`. +- `topo_node_update_my_addresses(instance)` — обновить адреса. +- `topo_node_on_socket_changed(sock, event, arg)` — обработчик изменений сокетов. **Диагностика:** -- `topo_node_dump_all(group)` — дамп всех узлов группы в лог (DEBUG_CATEGORY_BGP). Для каждого узла выводит: ID, имя, версию, pubkeys, v4/v6 сокеты и адреса, подсети, транзитные узлы, пути (с hop-ами), статус связности, conn_mgr. Пропускает узлы с `node==NULL`. -- `topo_node_format_all(group, buf, buf_size)` — аналогичный дамп в строковый буфер. Возвращает количество записанных байт. При переполнении добавляет `[TRUNCATED]`. +- `topo_node_dump_all(group)` / `topo_node_format_all(group, buf, size)` — дамп узлов. -**Inline-аксессоры (из .h):** -- `topo_v4_sock_meta(ni)` / `topo_v4_addrs(ni)` / `topo_v6_sock_meta(ni)` / `topo_v6_addrs(ni)` — получить списки из `TOPO_NODE`. -- `topo_v4_subnets(r)` / `topo_v6_subnets(r)` — получить списки подсетей из `TOPO_NODESUBNETS*` (NULL-safe). -- `topo_list_count(head)` — посчитать количество элементов в любом из списков (next — первое поле). +**Inline-аксессоры (.h):** +- `topo_v4_sock_meta/addr`, `topo_v6_sock_meta/addr`, `topo_v4_subnets/topo_v6_subnets`, `topo_list_count`. ### Зависимости | Модуль | Использование | |--------|--------------| -| `ll_queue.h` | `TOPO_NODEQ` — запись в очереди, хеш-индекс по `node_id`, `paths` — вложенная очередь | -| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) для массивов pubkey/ed25519 | -| `mem.h` | `u_malloc`, `u_calloc`, `u_free`, `u_strdup` | -| `memory_pool.h` | `memory_pool_alloc`/`memory_pool_free` для элементов списков (пулы из `topo_groups`) | -| `debug_config.h` | `DEBUG_INFO`, `DEBUG_WARN`, `DEBUG_ERROR`, `log_dump` | -| `topo_group.h` | `struct TOPO_GROUP` (аргумент большинства функций) | -| `etcp.h` | `struct ETCP_CONN`, `struct ETCP_SOCKET` | -| `config_parser.h` | `struct CFG_ROUTE_ENTRY`, `struct CFG_SERVER` (при построении local_node) | -| `route_lib.h` | `route_insert()`, `route_delete()` (при обновлении local_node) | -| `etcp_debug.h` | `ip_to_str()` (для дампа адресов и подсетей) | -| `utun_instance.h` | `struct UTUN_INSTANCE` (аргумент `topo_group_update_my_nodeinfo`) | +| `ll_queue.h` | `TOPO_GROUP_NODE` — запись в очереди (hash по node_id), `paths` — вложенная очередь | +| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) | +| `mem.h` | `u_malloc`, `u_calloc`, `u_free` | +| `memory_pool.h` | Элементы списков из пулов `topo_groups` | +| `debug_config.h` | Логирование (BGP) | +| `topo_group.h` | `struct TOPO_GROUP`, `TOPO_GROUPS` | +| `route_lib.h` | `route_insert/route_delete` | +| `utun_instance.h` | `struct UTUN_INSTANCE` | diff --git a/src/routing_layer/topo_node_sqlite_doc.md b/src/routing_layer/topo_node_sqlite_doc.md index 6dacbf70..04ae8db9 100644 --- a/src/routing_layer/topo_node_sqlite_doc.md +++ b/src/routing_layer/topo_node_sqlite_doc.md @@ -36,4 +36,4 @@ | `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online | | `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) | -**Структуры:** внешние — `sqlite3*`, `struct TOPO_NODEQ`, `struct TOPO_NODE`; собственных структур нет. +**Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет. diff --git a/src/routing_layer/topo_recovery.c b/src/routing_layer/topo_recovery.c index fe176d0b..da3daa72 100644 --- a/src/routing_layer/topo_recovery.c +++ b/src/routing_layer/topo_recovery.c @@ -1,13 +1,13 @@ /** * @file topo_recovery.c - * @brief Реализация восстановления каскадных узлов. + * @brief Реализация восстановления каскадных узлов (прямые подключения, ncd). * * Цепочка: add_node (группировка по next_hop) → start (запуск всех ctx) → try_next (цикл) * try_next: сперва is_next_hop-узел группы, затем остальные по мин. RTT * ├─ recovery_callback (ok/fail): удалить узел → try_next - * └─ timeout_callback (2с): conn_mgr_cancel_callback → удалить → try_next - * Завершение: оба списка пусты → освобождение ctx - * Отмена: cancel_for_node (on_up) → cancel_callback + освобождение ctx + * └─ timeout_callback (2с): закрыть handle → удалить → try_next + * Завершение: список пуст → освобождение ctx + * Отмена: cancel_for_node (on_up) → закрыть handle + освобождение ctx */ #include #include @@ -19,9 +19,8 @@ #include "topo_node.h" #include "topo_group.h" #include "topo_recovery.h" -#include "conn_mgr.h" -#include "config_parser.h" -#include "../transport_layer/etcp_connections.h" +#include "../transport_layer/node_conn_direct.h" +#include "../transport_layer/etcp.h" #define RECOVERY_CAPACITY_INIT 16 @@ -30,37 +29,18 @@ static struct TOPO_RECOVERY_CTX* topo_recovery_ctx_alloc(struct TOPO_GROUP* grou struct TOPO_RECOVERY_CTX* ctx = u_calloc(1, sizeof(struct TOPO_RECOVERY_CTX)); if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "recovery: alloc ctx failed"); return NULL; } ctx->instance = group->instance; ctx->group = group; ctx->next_hop_id = next_hop_id; - ctx->capacity[0] = RECOVERY_CAPACITY_INIT; ctx->capacity[1] = RECOVERY_CAPACITY_INIT; - ctx->nodes[0] = u_calloc(ctx->capacity[0], sizeof(struct TOPO_RECOVERY_NODE)); - ctx->nodes[1] = u_calloc(ctx->capacity[1], sizeof(struct TOPO_RECOVERY_NODE)); - if (!ctx->nodes[0] || !ctx->nodes[1]) { u_free(ctx->nodes[0]); u_free(ctx->nodes[1]); u_free(ctx); return NULL; } + ctx->capacity = RECOVERY_CAPACITY_INIT; + ctx->nodes = u_calloc(ctx->capacity, sizeof(struct TOPO_RECOVERY_NODE)); + if (!ctx->nodes) { u_free(ctx); return NULL; } return ctx; } static void topo_recovery_ctx_free(struct TOPO_RECOVERY_CTX* ctx) { if (!ctx) return; - u_free(ctx->nodes[0]); u_free(ctx->nodes[1]); + u_free(ctx->nodes); u_free(ctx); } -/* Аналог cm_has_direct_ip: есть ли адреса с прямым доступом (PUBLIC/UNKNOWN/EIM/DIRECT) */ -static int node_has_direct_ip(struct TOPO_GROUP* group, const struct TOPO_GROUP_NODE* nq) { - if (!nq) return 0; - struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id); - if (!ni) return 0; - for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - if (a->type != TOPO_ADDR_NAT && a->type != TOPO_ADDR_INTERFACE) continue; - for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) { - if (m->id != a->socket_id) continue; - if (m->config_type == CFG_SERVER_TYPE_PUBLIC || m->config_type == CFG_SERVER_TYPE_UNKNOWN - || m->nat_type == NAT_VERIFIED_EIM || m->nat_type == NAT_VERIFIED_DIRECT) - return 1; - } - } - return 0; -} - - /* Ищет незапущенный (started==0) контекст по next_hop_id */ static struct TOPO_RECOVERY_CTX* topo_recovery_find_by_next_hop(struct TOPO_GROUP* group, uint64_t next_hop_id) { struct TOPO_RECOVERY_CTX* ctx = group->recovery_list; @@ -76,16 +56,16 @@ static void topo_recovery_ctx_remove(struct TOPO_GROUP* group, struct TOPO_RECOV static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx); -/* Коллбэк conn_mgr: подключение удалось или провалилось */ -static void topo_recovery_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, - enum conn_mgr_event event, void* arg) { - struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; (void)h; (void)group_id; +/* Коллбэк ncd: подключение удалось или провалилось */ +static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) { + struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; + struct ETCP_CONN* conn = node_conn_direct_get_conn(h); + uint64_t node_id = conn ? conn->peer_node_id : 0; if (node_id != ctx->current_node_id) return; if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } - int list = ctx->current_list; - for (size_t i = 0; i < ctx->count[list]; i++) - if (ctx->nodes[list][i].node_id == node_id) { ctx->nodes[list][i] = ctx->nodes[list][ctx->count[list] - 1]; ctx->count[list]--; break; } - if (event == CONN_EVENT_UP) { + for (size_t i = 0; i < ctx->count; i++) + if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; } + if (event == NCD_EVENT_UP) { struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id); if (nq) nq->handle = h; DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); @@ -96,58 +76,53 @@ static void topo_recovery_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, topo_recovery_try_next(ctx); } -/* Таймаут 2с: отменяет conn_mgr-коллбэк, удаляет узел, переходит к следующему */ +/* Таймаут 2с: закрывает handle, удаляет узел, переходит к следующему */ static void topo_recovery_timeout_cb(void* arg) { struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; ctx->connect_timer = NULL; uint64_t node_id = ctx->current_node_id; DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx timeout connecting to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); if (ctx->current_handle) { - conn_mgr_close(ctx->current_handle); + node_conn_direct_close(ctx->current_handle); ctx->current_handle = NULL; } - int list = ctx->current_list; - for (size_t i = 0; i < ctx->count[list]; i++) - if (ctx->nodes[list][i].node_id == node_id) { ctx->nodes[list][i] = ctx->nodes[list][ctx->count[list] - 1]; ctx->count[list]--; break; } + for (size_t i = 0; i < ctx->count; i++) + if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; } ctx->current_node_id = 0; topo_recovery_try_next(ctx); } /* Выбирает узел: сперва is_next_hop с мин. RTT, за ним — остальные по мин. RTT */ -static struct TOPO_RECOVERY_NODE* topo_recovery_find_best(struct TOPO_RECOVERY_CTX* ctx, int list) { +static struct TOPO_RECOVERY_NODE* topo_recovery_find_best(struct TOPO_RECOVERY_CTX* ctx) { struct TOPO_RECOVERY_NODE* best = NULL; - for (size_t i = 0; i < ctx->count[list]; i++) - if (ctx->nodes[list][i].is_next_hop && (!best || ctx->nodes[list][i].min_rtt < best->min_rtt)) - best = &ctx->nodes[list][i]; + for (size_t i = 0; i < ctx->count; i++) + if (ctx->nodes[i].is_next_hop && (!best || ctx->nodes[i].min_rtt < best->min_rtt)) + best = &ctx->nodes[i]; if (best) return best; - for (size_t i = 0; i < ctx->count[list]; i++) - if (!best || ctx->nodes[list][i].min_rtt < best->min_rtt) - best = &ctx->nodes[list][i]; + for (size_t i = 0; i < ctx->count; i++) + if (!best || ctx->nodes[i].min_rtt < best->min_rtt) + best = &ctx->nodes[i]; return best; } static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx) { while (1) { - if (ctx->count[ctx->current_list] == 0) { - ctx->current_list++; - if (ctx->current_list > 1) { - DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx all nodes exhausted, no reconnections", (unsigned long long)ctx->next_hop_id); - topo_recovery_ctx_remove(ctx->group, ctx); - topo_recovery_ctx_free(ctx); - return; - } - continue; + if (ctx->count == 0) { + DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx all nodes exhausted, no reconnections", (unsigned long long)ctx->next_hop_id); + topo_recovery_ctx_remove(ctx->group, ctx); + topo_recovery_ctx_free(ctx); + return; } - struct TOPO_RECOVERY_NODE* best = topo_recovery_find_best(ctx, ctx->current_list); + struct TOPO_RECOVERY_NODE* best = topo_recovery_find_best(ctx); uint64_t node_id = best->node_id; - DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx trying %016llx rtt=%u list=%s %s", + DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx trying %016llx rtt=%u %s", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id, best->min_rtt, - ctx->current_list == 0 ? "direct" : "indirect", best->is_next_hop ? "[next_hop]" : ""); + best->is_next_hop ? "[next_hop]" : ""); ctx->current_node_id = node_id; - if (conn_mgr_open_invite(ctx->instance, ctx->group->group_id, NULL, node_id, - topo_recovery_callback, ctx, &ctx->current_handle) == -1) { - DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: conn_mgr_open_invite %016llx failed", (unsigned long long)node_id); - *best = ctx->nodes[ctx->current_list][ctx->count[ctx->current_list] - 1]; ctx->count[ctx->current_list]--; + if (node_conn_direct_open(ctx->instance, node_id, topo_recovery_callback, ctx, + &ctx->current_handle, NULL) == NCD_ERR) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: node_conn_direct_open %016llx failed", (unsigned long long)node_id); + *best = ctx->nodes[ctx->count - 1]; ctx->count--; ctx->current_node_id = 0; continue; } @@ -176,24 +151,23 @@ void topo_recovery_add_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq group->recovery_list = ctx; } - int list = node_has_direct_ip(group, nq) ? 0 : 1; uint16_t rtt = topo_get_chain_rtt(nq); - if (ctx->count[list] >= ctx->capacity[list]) { - size_t new_cap = ctx->capacity[list] * 2; - struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes[list], new_cap * sizeof(struct TOPO_RECOVERY_NODE)); + if (ctx->count >= ctx->capacity) { + size_t new_cap = ctx->capacity * 2; + struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes, new_cap * sizeof(struct TOPO_RECOVERY_NODE)); if (!new_nodes) return; - ctx->nodes[list] = new_nodes; ctx->capacity[list] = new_cap; + ctx->nodes = new_nodes; ctx->capacity = new_cap; } - ctx->nodes[list][ctx->count[list]].node_id = node_id; - ctx->nodes[list][ctx->count[list]].min_rtt = rtt; - ctx->nodes[list][ctx->count[list]].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0); - ctx->count[list]++; + ctx->nodes[ctx->count].node_id = node_id; + ctx->nodes[ctx->count].min_rtt = rtt; + ctx->nodes[ctx->count].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0); + ctx->count++; - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "recovery: next=%016llx add node %016llx list=%s rtt=%u is_nh=%d count=%zu", + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "recovery: next=%016llx add node %016llx rtt=%u is_nh=%d count=%zu", (unsigned long long)next_hop, (unsigned long long)node_id, - list == 0 ? "direct" : "indirect", rtt, node_id == next_hop ? 1 : 0, ctx->count[list]); + rtt, node_id == next_hop ? 1 : 0, ctx->count); } void topo_recovery_start(struct TOPO_GROUP* group) { @@ -203,13 +177,13 @@ void topo_recovery_start(struct TOPO_GROUP* group) { while (ctx) { struct TOPO_RECOVERY_CTX* next_ctx = ctx->next; if (!ctx->started) { - if (ctx->count[0] + ctx->count[1] == 0) { + if (ctx->count == 0) { topo_recovery_ctx_remove(group, ctx); topo_recovery_ctx_free(ctx); } else { - ctx->started = 1; ctx->current_list = 0; - DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: start next=%016llx direct=%zu indirect=%zu", - (unsigned long long)ctx->next_hop_id, ctx->count[0], ctx->count[1]); + ctx->started = 1; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: start next=%016llx count=%zu", + (unsigned long long)ctx->next_hop_id, ctx->count); topo_recovery_try_next(ctx); started_ctxs++; } @@ -227,14 +201,13 @@ void topo_recovery_cancel_for_node(struct TOPO_GROUP* group, uint64_t node_id) { while (*pp) { struct TOPO_RECOVERY_CTX* ctx = *pp; int found = 0; - for (int list = 0; list < 2 && !found; list++) - for (size_t i = 0; i < ctx->count[list] && !found; i++) - if (ctx->nodes[list][i].node_id == node_id) found = 1; + for (size_t i = 0; i < ctx->count && !found; i++) + if (ctx->nodes[i].node_id == node_id) found = 1; if (found) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: cancel next=%016llx (node %016llx came up)", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); if (ctx->current_node_id != 0 && ctx->current_handle) - conn_mgr_close(ctx->current_handle); + node_conn_direct_close(ctx->current_handle); if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } *pp = ctx->next; topo_recovery_ctx_free(ctx); @@ -250,7 +223,7 @@ void topo_recovery_cancel_all(struct TOPO_GROUP* group) { while (ctx) { struct TOPO_RECOVERY_CTX* next = ctx->next; if (ctx->current_node_id != 0 && ctx->current_handle) - conn_mgr_close(ctx->current_handle); + node_conn_direct_close(ctx->current_handle); if (ctx->connect_timer) { uasync_cancel_timeout(group->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } topo_recovery_ctx_free(ctx); ctx = next; diff --git a/src/routing_layer/topo_recovery.h b/src/routing_layer/topo_recovery.h index 9cf50e83..7a503e6b 100644 --- a/src/routing_layer/topo_recovery.h +++ b/src/routing_layer/topo_recovery.h @@ -3,13 +3,13 @@ * @brief Восстановление каскадно отвалившихся узлов после разрыва ETCP-соединения. * * Когда рвётся соединение, узлы достижимые только через него становятся недоступны. - * Модуль собирает их перед удалением из BGP-таблицы и пробует переподключиться: + * Модуль собирает их перед удалением из BGP-таблицы и пробует переподключиться + * напрямую (ncd): * 1. add_node — из hop_list вычисляет next_hop относительно failed_peer, * группирует узлы по next_hop в отдельные recovery-контексты - * 2. Классификация на direct (есть прямые IP) / indirect (нет) - * 3. start — для каждого контекста запускает асинхронный перебор: + * 2. start — для каждого контекста запускает асинхронный перебор: * сперва пробует next_hop-узел (is_next_hop=1), затем остальные по мин. RTT - * 4. Таймаут 2с на каждую попытку conn_mgr_connect_node + * 3. Таймаут 2с на каждую попытку node_conn_direct_open * * Группировка: <мы> -> -> * Каждый next_hop со своим subtree — отдельный recovery-контекст. @@ -30,6 +30,7 @@ extern "C" { struct TOPO_GROUP; struct TOPO_GROUP_NODE; +struct NODE_CONN_DIRECT; #define TOPO_RECOVERY_CONNECT_TIMEOUT_MS 2000 @@ -44,14 +45,13 @@ struct TOPO_RECOVERY_CTX { struct UTUN_INSTANCE* instance; struct TOPO_GROUP* group; uint64_t next_hop_id; /* ключ группировки: node_id next-hop'а от failed_peer */ - struct TOPO_RECOVERY_NODE* nodes[2]; /* [0]=с прямыми IP, [1]=без прямых IP */ - size_t count[2]; /* текущее количество в каждом списке */ - size_t capacity[2]; /* выделенная ёмкость */ - int current_list; /* 0=direct, 1=indirect */ - uint64_t current_node_id; /* node_id в текущей попытке, 0=нет активной */ - struct CONN_MGR_HANDLE* current_handle; /* handle текущей попытки conn_mgr_connect_node */ - void* connect_timer; /* внешний таймер 2с (uasync) */ - uint8_t started; /* 0=сбор узлов (add_node), 1=перебор запущен */ + struct TOPO_RECOVERY_NODE* nodes; /* кандидаты на восстановление (прямые) */ + size_t count; /* текущее количество */ + size_t capacity; /* выделенная ёмкость */ + uint64_t current_node_id; /* node_id в текущей попытке, 0=нет активной */ + struct NODE_CONN_DIRECT* current_handle; /* handle текущей попытки node_conn_direct_open */ + void* connect_timer; /* внешний таймер 2с (uasync) */ + uint8_t started; /* 0=сбор узлов (add_node), 1=перебор запущен */ }; /** diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 3f118653..bec3262c 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -125,6 +125,29 @@ static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t no * с новым ключом, старый conn больше не нужен) */ +/* Создаёт один UDP-линк на use_sock → sa, с вытеснением stale-линка и дедупом. */ +static int ncd_add_udp_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* use_sock, struct sockaddr_storage* sa) { + struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, sa, 0); + if (stale && stale->etcp == conn) return 0; + if (stale && stale->etcp != conn + && memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE)) + { + if (!stale->etcp->links_up || !stale->etcp->initialized) { + struct ETCP_CONN* old = stale->etcp; + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale link node=0x%016llx(old pk=%016llx) for new node=0x%016llx(pk=%016llx)", + (unsigned long long)old->peer_node_id, *(const uint64_t*)old->crypto_ctx.peer_public_key, + (unsigned long long)conn->peer_node_id, *(const uint64_t*)conn->crypto_ctx.peer_public_key); + etcp_link_close(stale); + etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED); + } else { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: port already used by live conn 0x%016llx for node 0x%016llx", + (unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id); + return 0; + } + } + return etcp_link_new(conn, use_sock, sa, 0) ? 1 : 0; +} + static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, struct ETCP_SOCKET* specific_sock) { struct ETCP_CONN* conn = entry->conn; @@ -133,107 +156,72 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, uint64_t nid = entry->node_id; /* ---- IPv4 ---- */ - { - struct ETCP_SOCKET* socks[64]; int sock_count = 0; - if (specific_sock) { - if (specific_sock->local_addr.ss_family == AF_INET) - socks[sock_count++] = specific_sock; - } else { - for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) - if (s->local_addr.ss_family == AF_INET && s->type != CFG_SERVER_TYPE_PRIVATE) - socks[sock_count++] = s; + for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { + v4_addrs++; + if (a->port == 0 || !(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v4_skip++; continue; } + int zero = 1; for (int j = 0; j < 4; j++) if (a->addr[j] != 0) { zero = 0; break; } + if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } + struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; + memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port); + struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); + + if (a->protocol & TOPO_PROTO_TCP) { + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { + if (!s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue; + struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0); + if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } + } } - if (sock_count == 0) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 sock_count=0 node=0x%016llx", (unsigned long long)nid); - - if (sock_count > 0) { - int rr = 0; - for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { - v4_addrs++; - if (a->port == 0 || !(a->protocol & TOPO_PROTO_UDP)) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v4_skip++; continue; } - int zero = 1; for (int j = 0; j < 4; j++) if (a->addr[j] != 0) { zero = 0; break; } - if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v4 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } - struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; - memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port); - struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); - struct ETCP_SOCKET* use_sock = socks[rr++ % sock_count]; - { - struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa, 0); - if (stale && stale->etcp == conn) continue; - if (stale && stale->etcp != conn - && memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE)) - { - if (!stale->etcp->links_up || !stale->etcp->initialized) { - struct ETCP_CONN* old = stale->etcp; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale link node=0x%016llx(old pk=%016llx) for new node=0x%016llx(pk=%016llx)", - (unsigned long long)old->peer_node_id, *(const uint64_t*)old->crypto_ctx.peer_public_key, - (unsigned long long)conn->peer_node_id, *(const uint64_t*)conn->crypto_ctx.peer_public_key); - etcp_link_close(stale); - etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED); - } else { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: %d.%d.%d.%d:%d already used by live conn 0x%016llx for node 0x%016llx", - a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port, - (unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id); - continue; - } - } + if (a->protocol & TOPO_PROTO_UDP) { + if (specific_sock && !specific_sock->is_tcp && specific_sock->local_addr.ss_family == AF_INET) { + link_count += ncd_add_udp_link(conn, specific_sock, &sa); + } else if (!specific_sock) { + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { + if (s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue; + link_count += ncd_add_udp_link(conn, s, &sa); } - if (etcp_link_new(conn, use_sock, &sa, 0)) link_count++; } } } /* ---- IPv6 ---- */ - { - struct ETCP_SOCKET* socks[64]; int sock_count = 0; - if (specific_sock) { - if (specific_sock->local_addr.ss_family == AF_INET6) - socks[sock_count++] = specific_sock; - } else { - for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) - if (s->local_addr.ss_family == AF_INET6 && s->type != CFG_SERVER_TYPE_PRIVATE) - socks[sock_count++] = s; - } + for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) { + v6_addrs++; + if (a->port == 0 || !(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v6_skip++; continue; } + int zero = 1; for (int j = 0; j < 16; j++) if (a->addr[j] != 0) { zero = 0; break; } + if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } + int is_ll = (a->addr[0] == 0xfe && (a->addr[1] & 0xc0) == 0x80); + struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; + memcpy(&sin6.sin6_addr, a->addr, 16); sin6.sin6_port = htons(a->port); - if (sock_count == 0) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 sock_count=0 node=0x%016llx", (unsigned long long)nid); - - if (sock_count > 0) { - int rr = 0; - for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) { - v6_addrs++; - if (a->port == 0 || !(a->protocol & TOPO_PROTO_UDP)) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v6_skip++; continue; } - int zero = 1; for (int j = 0; j < 16; j++) if (a->addr[j] != 0) { zero = 0; break; } - if (zero) { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[ncd] v6 skip zero-addr node=0x%016llx", (unsigned long long)nid); continue; } - struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; - memcpy(&sin6.sin6_addr, a->addr, 16); sin6.sin6_port = htons(a->port); - struct ETCP_SOCKET* use_sock = socks[rr++ % sock_count]; - if ((a->addr[0] == 0xfe && (a->addr[1] & 0xc0) == 0x80)) sin6.sin6_scope_id = use_sock->netif_index; + if (a->protocol & TOPO_PROTO_TCP) { + if (is_ll) { struct ETCP_SOCKET* sv = inst->etcp_sockets; + while (sv) { if (sv->local_addr.ss_family == AF_INET6 && sv->netif_index) { sin6.sin6_scope_id = sv->netif_index; break; } sv = sv->next; } } + struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { + if (!s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue; + struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0); + if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } + } + } + if (a->protocol & TOPO_PROTO_UDP) { + if (specific_sock && !specific_sock->is_tcp && specific_sock->local_addr.ss_family == AF_INET6) { + if (is_ll) sin6.sin6_scope_id = specific_sock->netif_index; struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); - { - struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa, 0); - if (stale && stale->etcp == conn) continue; - if (stale && stale->etcp != conn - && memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE)) - { - if (!stale->etcp->links_up || !stale->etcp->initialized) { - struct ETCP_CONN* old = stale->etcp; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[ncd] evict stale v6 link node=0x%016llx(old) -> 0x%016llx(new)", - (unsigned long long)old->peer_node_id, (unsigned long long)conn->peer_node_id); - etcp_link_close(stale); - etcp_cbk_fire(old, ETCP_CBK_EVENT_NODE_CHANGED); - } else { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "[ncd] addr conflict: v6 port=%d already used by live conn 0x%016llx for node 0x%016llx", - (int)a->port, (unsigned long long)stale->etcp->peer_node_id, (unsigned long long)conn->peer_node_id); - continue; - } - } + link_count += ncd_add_udp_link(conn, specific_sock, &sa); + } else if (!specific_sock) { + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { + if (s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue; + if (is_ll) sin6.sin6_scope_id = s->netif_index; + struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); + link_count += ncd_add_udp_link(conn, s, &sa); } - if (etcp_link_new(conn, use_sock, &sa, 0)) link_count++; } } } if (link_count == 0) { - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] create_links: node=0x%016llx v4=%d/skp=%d v6=%d/skp=%d → %d UDP links (TCP handled by cm_direct_add_links)", + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] create_links: node=0x%016llx v4=%d/skp=%d v6=%d/skp=%d → %d links (UDP+TCP)", (unsigned long long)nid, v4_addrs, v4_skip, v6_addrs, v6_skip, link_count); } return link_count; diff --git a/src/utun_instance.c b/src/utun_instance.c index 363f3e85..6ec9deb9 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -9,6 +9,7 @@ #include "route_lib.h" #include "routing.h" #include "topo_group.h" +#include "topo_group_invite.h" #include "nat_detection.h" #include "etcp_connections.h" #include "etcp.h" @@ -216,7 +217,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "BGP module initialized"); etcp_router_bind(instance, ETCP_RT_ID_CONN_MGR, conn_mgr_router_recv_handler); - etcp_bind(instance, ETCP_RT_ID_CONN_MGR, conn_mgr_direct_recv_handler); + topo_group_invite_init(instance); struct TOPO_GROUP* g = topo_groups_get_default(instance->topo_groups); if (instance->rt && g && g->local_node) { topo_group_update_my_nodeinfo(instance, g); @@ -466,7 +467,8 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { /* Phase F: BGP */ if (instance->topo_groups) { etcp_router_unbind(instance, ETCP_RT_ID_CONN_MGR); - etcp_unbind(instance, ETCP_RT_ID_CONN_MGR); + etcp_unbind(instance, ETCP_RT_ID_GROUP_INVITE); + topo_group_invite_cancel_all(instance); topo_groups_destroy(instance); } broadcast_destroy_instance(instance); diff --git a/tests/test_conn_mgr.c b/tests/test_conn_mgr.c index a6822845..badf9531 100644 --- a/tests/test_conn_mgr.c +++ b/tests/test_conn_mgr.c @@ -68,8 +68,8 @@ static void ccb(struct CONN_MGR_HANDLE* h, uint64_t id, uint64_t gid, static void test1(void* arg) { (void)arg; if (result) return; if (!topo_node_find_by_id(topo_groups_get_default(g_a->topo_groups), g_nid_b)) { uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test1, "t1"); return; } - fprintf(stderr, "Test 1: direct — conn_mgr_open_invite(B)\n"); fflush(stderr); - conn_mgr_open_invite(g_a, TOPO_GROUP_UTUN, NULL, g_nid_b, ccb, NULL, NULL); + fprintf(stderr, "Test 1: direct — conn_mgr_open(B)\n"); fflush(stderr); + conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_nid_b, ccb, NULL, NULL); uasync_set_timeout(ua, 50, NULL, (timeout_callback_t)test2, "t2a"); } static void test2(void* arg) { @@ -145,7 +145,7 @@ static void test3(void* arg) { (void)arg; if (result) return; fprintf(stderr, "Test 2: DB node (B2) — direct connect\n"); fflush(stderr); cdone2 = 0; cevent2 = 0; - conn_mgr_open_invite(g_a2, TOPO_GROUP_UTUN, NULL, g_nid_b2, ccb2, NULL, NULL); + conn_mgr_open(g_a2, TOPO_GROUP_UTUN, g_nid_b2, ccb2, NULL, NULL); uasync_set_timeout(ua2, 20, NULL, (timeout_callback_t)test3_check, "t3a"); } static void test3_check(void* arg) { diff --git a/tests/test_conn_mgr_already_connected.c b/tests/test_conn_mgr_already_connected.c index c783d15f..c6f2a93b 100644 --- a/tests/test_conn_mgr_already_connected.c +++ b/tests/test_conn_mgr_already_connected.c @@ -1,8 +1,8 @@ /** * @file test_conn_mgr_already_connected.c - * @brief conn_mgr: подключение к уже-соединённому узлу через conn_mgr_open_invite + * @brief conn_mgr: подключение к уже-соединённому узлу через conn_mgr_open * - * Проверяет, что conn_mgr_open_invite находит существующее ETCP-соединение + * Проверяет, что conn_mgr_open находит существующее ETCP-соединение * и сразу вызывает callback с CONN_EVENT_UP. */ #include @@ -70,8 +70,8 @@ static void test_find_conn(void* arg) { static void test_mgr_connect(void* arg) { (void)arg; cb_fired = 0; cb_ok = 0; - fprintf(stderr, "Test 2: conn_mgr_open_invite(B)\n"); fflush(stderr); - conn_mgr_open_invite(g_a, TOPO_GROUP_UTUN, NULL, g_b->node_id, conn_cb, NULL, NULL); + fprintf(stderr, "Test 2: conn_mgr_open(B)\n"); fflush(stderr); + conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_b->node_id, conn_cb, NULL, NULL); uasync_call_soon(ua, NULL, test_check); } diff --git a/tests/test_invite_group_create.c b/tests/test_invite_group_create.c index 76e69108..2eef1eea 100644 --- a/tests/test_invite_group_create.c +++ b/tests/test_invite_group_create.c @@ -1,6 +1,6 @@ /** * @file test_invite_group_create.c - * @brief Full invite flow: conn_mgr_open_invite → B is member → CONN_EVENT_JOIN + * @brief Full invite flow: topo_group_invite_join → B is member → TGI_EVENT_JOIN */ #include #include @@ -22,7 +22,7 @@ #include "../src/routing_layer/topo_group.h" #include "../src/routing_layer/topo_node.h" #include "../src/routing_layer/topo_node_sqlite.h" -#include "../src/routing_layer/conn_mgr.h" +#include "../src/routing_layer/topo_group_invite.h" #define TIMEOUT_TB 50000 #define POLL_MS 20 @@ -48,12 +48,11 @@ static char* ls(const char* p, const char* k) { static int result = 0; -static void ccb(struct CONN_MGR_HANDLE* h, uint64_t nid, uint64_t gid, - enum conn_mgr_event ev, void* arg) { - (void)h; (void)gid; (void)arg; - fprintf(stderr, "CB: ev=%d node=0x%llx\n", (int)ev, (unsigned long long)nid); fflush(stderr); - if (ev == CONN_EVENT_JOIN) result = 1; - if (ev == CONN_EVENT_TIMEOUT) result = 2; +static void ccb(uint64_t nid, uint64_t gid, int ev, void* arg) { + (void)gid; (void)arg; + fprintf(stderr, "CB: ev=%d node=0x%llx\n", ev, (unsigned long long)nid); fflush(stderr); + if (ev == TGI_EVENT_JOIN) result = 1; + if (ev == TGI_EVENT_TIMEOUT) result = 2; } static void to_cb(void* arg) { (void)arg; fprintf(stderr, "TIMEOUT\n"); fflush(stderr); result = 2; } @@ -141,11 +140,11 @@ int main(void) { ni->v4_addrs = a4; uint64_t gid = 0x7777777700000001ULL; - fprintf(stderr, "=== conn_mgr_open_invite gid=0x%016llX nid=0x%016llX ===\n", + fprintf(stderr, "=== topo_group_invite_join gid=0x%016llX nid=0x%016llX ===\n", (unsigned long long)gid, (unsigned long long)nid_b); fflush(stderr); - int r = conn_mgr_open_invite(a, gid, ni, nid_b, ccb, NULL, NULL); - fprintf(stderr, "conn_mgr_open_invite => %d\n", r); fflush(stderr); + int r = topo_group_invite_join(a, gid, ni, nid_b, ccb, NULL); + fprintf(stderr, "topo_group_invite_join => %d\n", r); fflush(stderr); void* tt = uasync_set_timeout(ua, TIMEOUT_TB, NULL, to_cb, "to"); int el = 0;