Compare commits
526
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9be8b134 | ||
|
|
cfa0419576 | ||
|
|
f8a2ecaabd | ||
|
|
a198cfb9e3 | ||
|
|
83bf76dd41 | ||
|
|
36c096c4e9 | ||
|
|
29d54ed20d | ||
|
|
e59ef240e8 | ||
|
|
27dd9e7b60 | ||
|
|
8185b83eba | ||
|
|
76afc56be8 | ||
|
|
5420a07ffc | ||
|
|
f00d67ca3c | ||
|
|
90dff0b2f2 | ||
|
|
83734c920d | ||
|
|
e1dcee3c86 | ||
|
|
d5114f36b4 | ||
|
|
68a225baec | ||
|
|
ef404da44c | ||
|
|
dbda8a24d7 | ||
|
|
bb462bad69 | ||
|
|
deee734e6c | ||
|
|
8ff4a0bf46 | ||
|
|
d2703b2145 | ||
|
|
9b72a0dadf | ||
|
|
5e3701bb2f | ||
|
|
4bab1f7ea4 | ||
|
|
9d9c82fb03 | ||
|
|
99c1ee3394 | ||
|
|
d88e39fb4e | ||
|
|
f63d640c95 | ||
|
|
86b7f9cf44 | ||
|
|
f331d177ca | ||
|
|
28bbd62d74 | ||
|
|
c1717d4047 | ||
|
|
4bfdb1dcc2 | ||
|
|
6e3be1aaf3 | ||
|
|
eea2b22c4e | ||
|
|
81fc4291db | ||
|
|
1a212489fd | ||
|
|
0d616ce1e7 | ||
|
|
7ff8e1f15a | ||
|
|
40e2fe2e27 | ||
|
|
359a9f3dfc | ||
|
|
68a7695d81 | ||
|
|
6f3d634c77 | ||
|
|
4177ab3536 | ||
|
|
80558f6686 | ||
|
|
da5cf6fc99 | ||
|
|
e6e866a41a | ||
|
|
e1675f6910 | ||
|
|
bb13824ad8 | ||
|
|
857a4cee2f | ||
|
|
a6444725da | ||
|
|
4a00da31da | ||
|
|
af50fde9d9 | ||
|
|
f1b62f9112 | ||
|
|
0b4997c5d5 | ||
|
|
b8dd2b8cc5 | ||
|
|
6cfd88afcf | ||
|
|
8e7fb1f31b | ||
|
|
00a85a1468 | ||
|
|
182fb97970 | ||
|
|
e6a812042f | ||
|
|
2f3cf0933c | ||
|
|
80451c43be | ||
|
|
007f7c8ae8 | ||
|
|
f541ab428b | ||
|
|
7ebfddfc4b | ||
|
|
56d513ed85 | ||
|
|
d1562bdd0e | ||
|
|
145dd1e5ab | ||
|
|
ab5c077600 | ||
|
|
6b413d67a6 | ||
|
|
4363a424d0 | ||
|
|
086eefed55 | ||
|
|
5e8ff95c99 | ||
|
|
21d8b3844e | ||
|
|
a7ffaf497e | ||
|
|
9ce0abe5a9 | ||
|
|
483e89cd79 | ||
|
|
4d9e624f8e | ||
|
|
bafda5ae3a | ||
|
|
09e620e12a | ||
|
|
3bd9b964fa | ||
|
|
a258b77240 | ||
|
|
936bacbe4b | ||
|
|
1227027c2f | ||
|
|
d8089f8d12 | ||
|
|
4862a52684 | ||
|
|
e40d8de1b2 | ||
|
|
dbf6bb759c | ||
|
|
be0bd224fa | ||
|
|
6bf6d99b9c | ||
|
|
7caebc5a11 | ||
|
|
6c13755a5c | ||
|
|
c6f28571b7 | ||
|
|
76ba7aae6a | ||
|
|
066b895055 | ||
|
|
a297d2d649 | ||
|
|
f09d76e3fd | ||
|
|
7737391ded | ||
|
|
dc08256025 | ||
|
|
72d7405a97 | ||
|
|
73096e8e3e | ||
|
|
51c667edd8 | ||
|
|
2edf09fde1 | ||
|
|
6ab2526966 | ||
|
|
c1b0772071 | ||
|
|
159237a70c | ||
|
|
c8051619c1 | ||
|
|
2b74d2bf4f | ||
|
|
35bd19b6c0 | ||
|
|
7e166c650c | ||
|
|
055ffa27b2 | ||
|
|
cd3cf38f06 | ||
|
|
12b0ab437e | ||
|
|
a9bbb470c2 | ||
|
|
6894c66f3a | ||
|
|
a2f9b76d10 | ||
|
|
3d40951e66 | ||
|
|
4350b54769 | ||
|
|
92d52d25a8 | ||
|
|
9e0949434e | ||
|
|
aa7a9b74f4 | ||
|
|
12556344b0 | ||
|
|
2e3ab00ce6 | ||
|
|
f92c78a2e0 | ||
|
|
7c7c316c3f | ||
|
|
94aaf3c8e3 | ||
|
|
c282d03f25 | ||
|
|
f7db49fc97 | ||
|
|
8cc97d2ffe | ||
|
|
f9994e11b5 | ||
|
|
1e0281d4d8 | ||
|
|
42a58881ca | ||
|
|
19c3cc0434 | ||
|
|
81e2a9aa4a | ||
|
|
575a3788e3 | ||
|
|
461417beac | ||
|
|
26b5da6835 | ||
|
|
99608de811 | ||
|
|
5c4207c145 | ||
|
|
a42dc38874 | ||
|
|
83194b19c7 | ||
|
|
14c5ddf864 | ||
|
|
f10a6f70c5 | ||
|
|
e2ee5f2397 | ||
|
|
324b289968 | ||
|
|
5914e26579 | ||
|
|
2894e06014 | ||
|
|
d0edacb7b9 | ||
|
|
1ce285c09b | ||
|
|
8dcd2fac3b | ||
|
|
bb2ef05157 | ||
|
|
c4eddfe50e | ||
|
|
f0629ea679 | ||
|
|
4da9d85545 | ||
|
|
d5444fb9bc | ||
|
|
50da7fe457 | ||
|
|
99be040488 | ||
|
|
902ffa05b4 | ||
|
|
051b13e161 | ||
|
|
04ace670ed | ||
|
|
99dd48480f | ||
|
|
acd9dcd1a8 | ||
|
|
790978458c | ||
|
|
f9a3830416 | ||
|
|
5f51149622 | ||
|
|
0688ffc26d | ||
|
|
ec1fc52da1 | ||
|
|
5e80a9cc6c | ||
|
|
f003aaa92d | ||
|
|
bf03348c64 | ||
|
|
0445c38c31 | ||
|
|
30a69712fc | ||
|
|
27cf6ae365 | ||
|
|
1ad8ff73ad | ||
|
|
0bc0d6cc62 | ||
|
|
23cec465bf | ||
|
|
a4abe30b18 | ||
|
|
34d1dcb7c8 | ||
|
|
93bf846fc4 | ||
|
|
c87cc6e4a2 | ||
|
|
1fbd1e466b | ||
|
|
b4d5da1df2 | ||
|
|
ff74ac8295 | ||
|
|
dcd6cd93c4 | ||
|
|
d439ee7ca7 | ||
|
|
39ee5b392b | ||
|
|
acde6bda9d | ||
|
|
8167e0e5f5 | ||
|
|
5a32d08ffd | ||
|
|
b98b87ea12 | ||
|
|
05d3fa30d9 | ||
|
|
516ed04897 | ||
|
|
1d295c7d3a | ||
|
|
1c6a7e7bf9 | ||
|
|
f1f4976222 | ||
|
|
14515ca446 | ||
|
|
8dd03c2b5c | ||
|
|
412b9bc713 | ||
|
|
ff2860ffbc | ||
|
|
c7a530092d | ||
|
|
deac10e3f6 | ||
|
|
42c4195f2e | ||
|
|
ce3d942efe | ||
|
|
a22a27b7a9 | ||
|
|
7d57b484d8 | ||
|
|
44acb5aecf | ||
|
|
5d11f90cd4 | ||
|
|
4824f3523d | ||
|
|
5a7823e6ef | ||
|
|
5792db996c | ||
|
|
16a33ac9f7 | ||
|
|
3afd31e561 | ||
|
|
2715faffff | ||
|
|
9459b43211 | ||
|
|
964f6cb045 | ||
|
|
bed99abbe9 | ||
|
|
d6eecd6f1c | ||
|
|
ecdd767934 | ||
|
|
1cf2cc10dd | ||
|
|
39f9d6a0e9 | ||
|
|
e84c66a3d3 | ||
|
|
e195894048 | ||
|
|
ee437b9473 | ||
|
|
299c613760 | ||
|
|
74b63cc315 | ||
|
|
032f8e83b2 | ||
|
|
c7d7789a58 | ||
|
|
fb995db489 | ||
|
|
942736d604 | ||
|
|
b8021efed8 | ||
|
|
1260884485 | ||
|
|
52d9077bb5 | ||
|
|
519a2ee242 | ||
|
|
6d6ec7e725 | ||
|
|
d0a168f89e | ||
|
|
5ccd47dd17 | ||
|
|
2f6ff9303e | ||
|
|
d9aede4138 | ||
|
|
0860b1e06d | ||
|
|
10927b1d08 | ||
|
|
3a8cc8a326 | ||
|
|
bf7464a4d9 | ||
|
|
e51badebcb | ||
|
|
d5d103148f | ||
|
|
d5721563fc | ||
|
|
274554f7a5 | ||
|
|
8fe1011da2 | ||
|
|
21a4dd7a70 | ||
|
|
33912355d6 | ||
|
|
5178f7abc6 | ||
|
|
d0ee0341c5 | ||
|
|
a2861b0570 | ||
|
|
a51c080407 | ||
|
|
2ea4fb94ab | ||
|
|
986193055e | ||
|
|
a43d478495 | ||
|
|
bf5a128a05 | ||
|
|
477d8f995b | ||
|
|
03d69cb772 | ||
|
|
f5e4c78b8c | ||
|
|
425e1a527b | ||
|
|
f1e3c2c08a | ||
|
|
bd9f1bb0c9 | ||
|
|
1fb52d3f5b | ||
|
|
4614577d19 | ||
|
|
f37221107d | ||
|
|
42e7b9b1b6 | ||
|
|
7753a0f11e | ||
|
|
4bfec7257f | ||
|
|
2cad81050d | ||
|
|
18f10b23b3 | ||
|
|
a3eeebbc52 | ||
|
|
85c92e35a8 | ||
|
|
be95e93a36 | ||
|
|
7c09bfca7f | ||
|
|
77a737fd71 | ||
|
|
9a9517deab | ||
|
|
b73d6e91e2 | ||
|
|
0987d9dd04 | ||
|
|
ccbd214c97 | ||
|
|
6414cb15ef | ||
|
|
8c3fcfd1c8 | ||
|
|
1b7d314d85 | ||
|
|
ea5a0b7d5a | ||
|
|
6fc964031c | ||
|
|
4bf47ca357 | ||
|
|
ea4a92125b | ||
|
|
5586528555 | ||
|
|
e8d9d683dc | ||
|
|
cc01457957 | ||
|
|
5b8aba9c8b | ||
|
|
f5d67d5240 | ||
|
|
35ef504423 | ||
|
|
a87d3116af | ||
|
|
c5f743a09d | ||
|
|
b167a27e85 | ||
|
|
f00a5bd306 | ||
|
|
608caf01c1 | ||
|
|
e801fce2c6 | ||
|
|
e89948f5ed | ||
|
|
3c7b59ef02 | ||
|
|
54fb4185f1 | ||
|
|
ac264c254c | ||
|
|
5403647aee | ||
|
|
3a007fbb29 | ||
|
|
92e9281623 | ||
|
|
e0f28bf851 | ||
|
|
9dbef48eb3 | ||
|
|
e84e17c396 | ||
|
|
9cc921f615 | ||
|
|
0d5e9332e3 | ||
|
|
b1e94eff34 | ||
|
|
bbe43b9842 | ||
|
|
3bba05d354 | ||
|
|
d43716933f | ||
|
|
3c18e594d1 | ||
|
|
23d426fd0c | ||
|
|
f221fa5a89 | ||
|
|
6e33675503 | ||
|
|
76398d204a | ||
|
|
d2b195473d | ||
|
|
caf37e9541 | ||
|
|
316180c276 | ||
|
|
5f085b2086 | ||
|
|
466a56b499 | ||
|
|
0989eff73d | ||
|
|
187aa02b49 | ||
|
|
0aaf6dfea0 | ||
|
|
9c40b489cf | ||
|
|
ce798a193e | ||
|
|
ddba151aa5 | ||
|
|
2fddf839af | ||
|
|
7bb43c42d6 | ||
|
|
b442d131c9 | ||
|
|
783646a967 | ||
|
|
b74ff2633d | ||
|
|
b97169de19 | ||
|
|
72f48d1f14 | ||
|
|
227104e224 | ||
|
|
d57a729b92 | ||
|
|
3ca9926c48 | ||
|
|
4c9a32d865 | ||
|
|
5e436975c6 | ||
|
|
a4a9ae7b0a | ||
|
|
34a31f63be | ||
|
|
75485bf7bf | ||
|
|
432017cc22 | ||
|
|
147648ffd0 | ||
|
|
1b3c4552fb | ||
|
|
cc510fc74c | ||
|
|
abe3d4bc89 | ||
|
|
7bc2ac5a46 | ||
|
|
b68e0ac3db | ||
|
|
3c44c3d7bc | ||
|
|
e21cd26511 | ||
|
|
7e413db1e1 | ||
|
|
b39a861a57 | ||
|
|
b349d7b7b2 | ||
|
|
c3ac337ef0 | ||
|
|
4aae1f6151 | ||
|
|
259dfa35d9 | ||
|
|
7e0a0905e5 | ||
|
|
34e3d9bba0 | ||
|
|
599d8ad421 | ||
|
|
4035794631 | ||
|
|
5f406c2a41 | ||
|
|
94d6dd05c8 | ||
|
|
67ae4ecf11 | ||
|
|
d0f122f4d8 | ||
|
|
08b6b2e2a1 | ||
|
|
5298fe9f6a | ||
|
|
aefa8ff680 | ||
|
|
0f23076f48 | ||
|
|
92a535e009 | ||
|
|
519d6e4eaf | ||
|
|
716db3a820 | ||
|
|
3d6f855d34 | ||
|
|
229e992695 | ||
|
|
a79c724110 | ||
|
|
6b1298aa62 | ||
|
|
0364abb00e | ||
|
|
f8d532b04d | ||
|
|
4dba742149 | ||
|
|
88eb0f2108 | ||
|
|
ade0e93f7f | ||
|
|
addff8547e | ||
|
|
1c18bdc9ab | ||
|
|
f3008b7aba | ||
|
|
c2f4f048d7 | ||
|
|
3c38072b9b | ||
|
|
4cfd0ec89f | ||
|
|
08c42e62ef | ||
|
|
dd153264e1 | ||
|
|
6f87ea1426 | ||
|
|
6fec5d6d98 | ||
|
|
9b9a9f2e00 | ||
|
|
27a7b7bf25 | ||
|
|
c9ba018a0d | ||
|
|
df5f6263bd | ||
|
|
0b5a892a88 | ||
|
|
076016f34a | ||
|
|
8417b1955c | ||
|
|
67322582f6 | ||
|
|
439361cb52 | ||
|
|
6e350760d1 | ||
|
|
672f04b833 | ||
|
|
f5cab5a14e | ||
|
|
f5f6ff50af | ||
|
|
62f4850968 | ||
|
|
082d9d3960 | ||
|
|
05a5b6c2a7 | ||
|
|
fdcf92be04 | ||
|
|
11aa760d47 | ||
|
|
bc27352710 | ||
|
|
0a00bd1c90 | ||
|
|
e2ee0d81dd | ||
|
|
6afc57569a | ||
|
|
ccb3821e2f | ||
|
|
29f9a97552 | ||
|
|
f37ac2c637 | ||
|
|
0ef0553598 | ||
|
|
f1c88ad375 | ||
|
|
17c92e1745 | ||
|
|
34e346463d | ||
|
|
ef11d3b677 | ||
|
|
2689931826 | ||
|
|
133ea8d120 | ||
|
|
75ff7c4ce3 | ||
|
|
86a0f041c7 | ||
|
|
c1f07ec6ae | ||
|
|
50d5486a9f | ||
|
|
036cf43918 | ||
|
|
fc5b3911f3 | ||
|
|
008f499367 | ||
|
|
8169322d56 | ||
|
|
d84ab69465 | ||
|
|
91bb95a981 | ||
|
|
03f4cd7939 | ||
|
|
71d3b017e6 | ||
|
|
8230eb4de7 | ||
|
|
bc149497f3 | ||
|
|
0bcdb9930a | ||
|
|
f2283eff6b | ||
|
|
a011c57237 | ||
|
|
9e2a1e5208 | ||
|
|
d6d8ffc4a8 | ||
|
|
14b01964b1 | ||
|
|
7fb9b43e9e | ||
|
|
63c0b9a655 | ||
|
|
00502bf4d4 | ||
|
|
d3bc77922b | ||
|
|
b4b11d6086 | ||
|
|
b47a82e3ac | ||
|
|
04dc53672a | ||
|
|
964a938437 | ||
|
|
e8843d9499 | ||
|
|
ecba318dec | ||
|
|
1aec478787 | ||
|
|
8f97cf8070 | ||
|
|
bd0260040c | ||
|
|
3b24e7e207 | ||
|
|
993e22085a | ||
|
|
00e0421e56 | ||
|
|
fabeb770a7 | ||
|
|
47addfeffc | ||
|
|
02e1526115 | ||
|
|
c81cc8b9ca | ||
|
|
99389f4bd3 | ||
|
|
b09a020855 | ||
|
|
090e7db598 | ||
|
|
5215163a37 | ||
|
|
1c1ab24a27 | ||
|
|
353752165c | ||
|
|
b1a3d0c6cd | ||
|
|
16333b989d | ||
|
|
d2bf40e11a | ||
|
|
d0a401e166 | ||
|
|
4edf72ea14 | ||
|
|
9b4974f02d | ||
|
|
f15d53947e | ||
|
|
68129c0151 | ||
|
|
a3a8b5b1be | ||
|
|
55d2f437a0 | ||
|
|
108dfae82b | ||
|
|
3a703d1093 | ||
|
|
433a091db5 | ||
|
|
21ebc832e1 | ||
|
|
a3f3b2fff2 | ||
|
|
093e537fed | ||
|
|
6a14be05f5 | ||
|
|
fd6155aa4a | ||
|
|
f47e80571d | ||
|
|
e02c9f7727 | ||
|
|
05ace58589 | ||
|
|
92c514ce18 | ||
|
|
6267973d62 | ||
|
|
cecbc682a0 | ||
|
|
07cd150b47 | ||
|
|
94d1e245a5 | ||
|
|
02265644ce | ||
|
|
0835c3d3cc | ||
|
|
34959a9657 | ||
|
|
90fab54a4a | ||
|
|
439011ce60 | ||
|
|
ec70908733 | ||
|
|
f123a1bdeb | ||
|
|
3bb8f76b7d | ||
|
|
6b54b63986 | ||
|
|
c08c24140c | ||
|
|
d4efdba44e | ||
|
|
41057ee9d5 | ||
|
|
87724433b1 | ||
|
|
9117813c4d | ||
|
|
15fafc42c9 | ||
|
|
16a5e628fd | ||
|
|
32d80110cb | ||
|
|
b3a223521c | ||
|
|
8cba8ef3e6 | ||
|
|
113f0259ea | ||
|
|
889dfc411a | ||
|
|
ad62eee10c | ||
|
|
d68ab90f23 |
No files matched your search
@@ -359,7 +359,6 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
|
||||
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
|
||||
- `etcp_bbr.c/h` - BBR congestion control for ETCP
|
||||
- `etcp_connect.c/h` - ETCP outbound connections
|
||||
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
|
||||
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
|
||||
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||||
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
|
||||
название «Зашифрованное вложение» вместо manifest.
|
||||
нельзя открыть или сохранить как готовое вложение. До расшифровки цитата показывает
|
||||
название «Зашифрованное вложение», после — обычный preview исходного вложения.
|
||||
Список чатов сохраняет общее название зашифрованного вложения вместо manifest.
|
||||
|
||||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||||
@@ -38,6 +39,13 @@ encrypted_file_body:variable
|
||||
```
|
||||
|
||||
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
|
||||
MIME — непустая NUL-строка длиной1..31 байт: ASCII33..126 без двойной кавычки
|
||||
и обратной косой черты. Общий `encrypted_file_validate_type` применяется в send,
|
||||
write и read; это проверка представления поля, без полного разбора MIME-грамматики.
|
||||
Send отклоняет неверное значение до создания задачи/ключа/worker, write — до файловых
|
||||
операций. Read проверяет расшифрованное аутентифицированное поле и при ошибке сохраняет
|
||||
прежние info/type. Сырые неверные строки в диагностику не выводятся.
|
||||
|
||||
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
|
||||
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
|
||||
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
|
||||
@@ -45,8 +53,26 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
|
||||
|
||||
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
|
||||
все tags файла, точный размер и hash тела. Результат пишется во временный файл
|
||||
и публикуется после успешной проверки и durable rename. Все длительные файловые
|
||||
операции и криптография выполняются в media_async.
|
||||
и публикуется после успешной проверки и durable rename. Подготовка файлов
|
||||
и криптография выполняются в media_async worker.
|
||||
|
||||
`encrypted_file_write/read` возвращают закрытый проверенный `file_io_temp`,
|
||||
не изменяя конечное назначение. Задача владеет им до publish/discard; запись идёт
|
||||
в эксклюзивно созданный соседний файл с правами 0600 на POSIX. Одинаковые файлы,
|
||||
включая `/./`, hardlink и конечную ссылку назначения, отклоняются до подготовки;
|
||||
публикация повторно проверяет идентичность. При ошибке read не выдаёт новые метаданные.
|
||||
|
||||
Режим `FILE_IO_DURABLE` действует весь срок temp: flush/sync открытого файла/close,
|
||||
затем rename и сохранение каталога. После успешного rename ошибка directory sync
|
||||
не откатывает установленный результат; temp уже отсоединён от владельца.
|
||||
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
|
||||
какие артефакты задача действительно создала или опубликовала.
|
||||
|
||||
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
|
||||
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
|
||||
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
|
||||
Конечные файлы и имена других форматов этот проход сохраняет; активные temp другого
|
||||
instance того же процесса остаются защищены общим реестром.
|
||||
|
||||
## Хранение и разрешения
|
||||
|
||||
@@ -69,7 +95,8 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
|
||||
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
|
||||
Сетевая классификация `peers.storage` для этого не используется.
|
||||
|
||||
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
|
||||
В шапке desktop-клиента кнопка «Запросы расшифровки» видна при наличии запросов без решения
|
||||
и показывает их число.
|
||||
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
|
||||
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
|
||||
и подтверждённая доставка ключа; отказ также сохраняется.
|
||||
@@ -110,9 +137,18 @@ backpressure и повтором через 5 секунд; restart продол
|
||||
|
||||
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
|
||||
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
|
||||
лишние байты и отсутствие частичного plaintext после отказа.
|
||||
лишние байты, alias обоих API, сохранность существующего назначения/метаданных,
|
||||
23 сценария отказов I/O с контролем fd/temp, включая ошибку после rename.
|
||||
Для MIME проверяются все256 значений первого байта, NULL и границы поля,
|
||||
восемь ранних отказов writer с сохранением файлов/output, три допустимых roundtrip
|
||||
и семь аутентифицированных некорректных wire-полей с сохранением info/type/target.
|
||||
`test_media_primitives` проверяет оба режима temp и границу durable-публикации;
|
||||
`test_attachment_send` проверяет восемь отказов MIME до ключей/worker/файлов,
|
||||
затем останавливает сервис после настоящего crypto rename до completion.
|
||||
|
||||
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
|
||||
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
|
||||
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
|
||||
восстановление удалённого plaintext без нового согласия и удаление канала.
|
||||
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
|
||||
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
|
||||
File diff suppressed because it is too large.
Load diff
+226
-67
@@ -1,66 +1,169 @@
|
||||
Формат кодограммы:
|
||||
------------------
|
||||
пакеты идут по следующему пути:
|
||||
пакет кем-то помещается в входную очередь (ETCP input_queue).
|
||||
Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null):
|
||||
- либо wait ETCP input_queue (если нечего передавать)
|
||||
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия".
|
||||
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку.
|
||||
как формируется пакет:
|
||||
1. выбираем канал передачи (etcp_loadbalancer_select_link)
|
||||
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем
|
||||
3. если пакет найден или есть неподтверждённые ACK:
|
||||
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало
|
||||
2. в конец добавляем секцию 0x00 с данными пакета
|
||||
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен
|
||||
Функция прикрепления опциональных секций:
|
||||
- запрашивает выбор канала передачи.
|
||||
- добавляет накопившиеся ACK
|
||||
Кодограмма сервиса через etcp_send попадает в normalizer; его фрагменты поступают в
|
||||
input_queue, затем в indexed input_send_q с назначенным seq.
|
||||
Очереди, ACK/RTO, изменение окна и готовность линка вызывают etcp_conn_wake_sender.
|
||||
Один call_soon обслуживает накопленные события без рекурсивного входа и задержки таймером.
|
||||
При OOM выделения сетевого пакета очередь сохраняется; retry назначается через 10ms.
|
||||
close/reset отменяет ожидающий запуск. Не подтверждённая сессия не передаёт DATA/ACK;
|
||||
HELLO/CHALLENGE/CONFIRM и keepalive передаются своим независимым путём.
|
||||
|
||||
Формирование пакета:
|
||||
1. Просмотреть input_send_q без извлечения: первый готовый повтор, иначе первый готовый новый DATA.
|
||||
Заблокированная запись пропускается; порядок остальных записей не меняется. Поиск не резервирует окно,
|
||||
не выдаёт номер попытки и не меняет историю/RR. Если кандидата нет, можно подгрузить один новый фрагмент
|
||||
и повторить просмотр. Общий бюджет учитывает DATA-байты send_q+wait_ack; приёмное окно по till сохраняется.
|
||||
После callback подгрузки проверяются reset_count/close/сессия, включая удаление прежнего burst-линка.
|
||||
2. Выбрать инициализированный UP-линк с готовым shaper и местом для целого DATA:
|
||||
inflight_bytes + длина DATA <= inflight_lim_bytes. Проверка выполняется без переполнения.
|
||||
Среди допустимых выбирается минимум inflight_bytes, при равенстве — round-robin.
|
||||
Для повтора обход начинается после last_link самого фрагмента: сначала неиспользованные допустимые пути.
|
||||
История — битовая карта256 локальных слотов,32 байта на логический пакет; wire-формат не меняется.
|
||||
Пока остался неиспользованный путь, занятый по cwnd/pacing, этот пакет ждёт, а send_q просматривается дальше.
|
||||
Когда все допустимые пути использованы, разрешён следующий обход. Очистка истории и новый бит фиксируются
|
||||
только после принятия send; отказ send не расходует обход. Перед удалением линка очищаются его курсор/бит
|
||||
во всех логических записях до освобождения слота и callbacks, которые могут повторно выдать тот же local_link_id.
|
||||
При наличии незастрявшего initialized+UP пути застрявшие исключены из повторов, даже если альтернатива занята.
|
||||
Если застряли все, доступны все UP-пути. Прежний путь разрешён лишь при отсутствии других допустимых;
|
||||
иначе ждём альтернативу. DOWN последней альтернативы пробуждает отправителя.
|
||||
3. Наступивший по сроку или срочный ACK обслуживает тот же линк, который принял DATA,
|
||||
без ограничения DATA-cwnd/pacing. Готовый DATA другого линка уступает этому ACK;
|
||||
DATA того же линка забирает ACK piggyback. Если DATA не помещается, он остаётся в очереди.
|
||||
Кумулятивный till можно отправить по любому рабочему пути. ACK-only не увеличивает нагрузку DATA-shaper;
|
||||
физическая очередь канала остаётся общей, её накопление ограничивают DATA-cwnd/pacing BBR.
|
||||
4. Выделить ETCP_DGRAM до удаления DATA из очереди и изменения счётчиков.
|
||||
Выделить физическую попытку с новым attempt_no для этого seq и зарезервировать окно выбранного линка.
|
||||
Только успешное принятие транспортом фиксирует время, last_link, историю/RR и перенос DATA в input_wait_ack.
|
||||
Ошибка send снимает резерв; логический DATA остаётся готовым без сетевого loss sample.
|
||||
Пакет содержит ACK, опциональные timestamp/измерительные секции и последнюю секцию PAYLOAD.
|
||||
5. В активной ограниченной burst-серии pacing обходится; DATA всё равно должен
|
||||
помещаться в окно, иначе серия отправляет FILLER. ACK/FILLER окно DATA не занимают.
|
||||
Если готовый повтор относится к другому пути, серия идёт FILLER: новые DATA его не обгоняют.
|
||||
При приёме пакета (от etcp_connections):
|
||||
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
|
||||
- ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки
|
||||
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
|
||||
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки.
|
||||
- ack (0x01): пара seq+attempt_no подтверждает физическую попытку только принявшего ACK линка;
|
||||
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
|
||||
- timestamp (0x06): обновляем RTT и EWMA |ΔRTT| пути; recv_dt_avg хранит поправки времени TX/RX в Q16.
|
||||
У conn rtt_last — среднее RTT линков, rtt_avg_10 — их максимум (имя историческое).
|
||||
BBR использует отдельные DATA RTT/jitter по ACK физических попыток; RTO также учитывает rtt_last своего линка.
|
||||
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
|
||||
Только после этого ставим ACK с seq+attempt_no в ack_q принявшего линка; OOM до сохранения не подтверждается.
|
||||
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
|
||||
|
||||
пересканирование inflight:
|
||||
- запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет.
|
||||
- если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется
|
||||
- очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете
|
||||
- по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди
|
||||
Таймер попыток каждого линка (etcp_tx.c):
|
||||
- RTO = clamp(ceil(1.2*RTT_for_RTO + 1.5*data_jitter_us + 3000), 10000, 1000000) мкс,
|
||||
RTT_for_RTO=max(data_rtt_us,100*rtt_last): DATA EWMA и последний timestamp/keepalive RTT своего линка.
|
||||
Целочисленный расчёт использует uint64 и округляет вверх до целой микросекунды.
|
||||
RTT использует собственный DATA ACK минус receiver_delay; EWMA RTT=1/8, jitter=1/4 |RTT-EWMA|.
|
||||
Первый DATA RTT задаёт jitter=RTT/2. Timestamp-RTT задаёт нижнюю границу срока, чтобы после роста задержки
|
||||
попытки доживали до ACK и DATA-измерения продолжались; BBR RTT/min_rtt по этому сигналу не обновляются.
|
||||
Без обоих измерений RTO=1с.
|
||||
- FIFO содержит физические попытки в порядке принятия транспортом. Таймер назначается по старейшей;
|
||||
при изменении RTO проверяется новый срок. Timestamp handler пересчитывает таймер сразу после обновления RTT,
|
||||
включая сокращение без новых DATA/ACK. Допускается оставить уже запланированную более раннюю проверку.
|
||||
uasync округляет задержку вверх до миллисекунды; callback может выполниться позже расчётного срока.
|
||||
- Истечение снимает только собственный физический flight и сообщает loss собственному BBR.
|
||||
Если логический seq ещё не доставлен и других активных копий нет, он возвращается в input_send_q.
|
||||
Уже доставленный seq не возвращается, даже если его физическая копия истекла позже.
|
||||
|
||||
Обычный ACK-only ждёт received_at_us старейшей записи своего пути + 2000мкс.
|
||||
Piggyback на готовом DATA передаёт ACK сразу. Готовый ACK может включать более свежие записи:
|
||||
срок проверяется по голове пакета, а не отдельно для каждой записи внутри него.
|
||||
Среди готовых обычных ACK выбирается самый старый срок; срочный ответ на ACK_REQUEST имеет приоритет.
|
||||
Короткая ограниченная измерительная burst-серия завершается до перехода к ACK другого пути.
|
||||
Продвижение логической границы также задаёт срок 2мс от первого неподтверждённого продвижения;
|
||||
следующие DATA его не отодвигают. Любой принятый пакет с актуальным till снимает этот запрос.
|
||||
Один ack_resp_timer соединения обслуживает минимум сроков очередей рабочих путей и логической границы.
|
||||
После piggyback/частичной отправки срок пересчитывается по оставшимся головам. Новый RX между
|
||||
timer и отложенным sender не задерживает старые ACK: готовность проверяется по абсолютному времени.
|
||||
При DOWN очередь пути не создаёт таймер; после UP её старый наступивший срок действует сразу.
|
||||
OOM ACK-таймера логируется и включает существующий срочный ответ, сохраняя received_at_us для RTT.
|
||||
Reset/close отменяют таймер и очищают сроки; DATA/RTO/BBR и wire-формат этим механизмом не меняются.
|
||||
|
||||
inflight - две очереди (ll_queue):
|
||||
- input_send_q: список пакетов ожидающих отправку
|
||||
- input_wait_ack: список неподтвержденных пакетов ожидающих ack
|
||||
При отправке пакет перемещается из input_send_q в input_wait_ack.
|
||||
При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q.
|
||||
При принятии отправки транспортом пакет перемещается из input_send_q в input_wait_ack.
|
||||
При необходимости повтора пакет перемещается из input_wait_ack обратно в input_send_q.
|
||||
доп. свойства пакетов (struct INFLIGHT_PACKET):
|
||||
- указатель на ETC_LINK последней отправки (last_link)
|
||||
- timestamp последней отправки (last_timestamp)
|
||||
- число отправок (send_count)
|
||||
- send_hist[8] - история через какие линки отправлялся (индекс линка)
|
||||
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
|
||||
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
|
||||
- send_hist[8] — local_link_id последних попыток, только диагностика
|
||||
- last_attempt_id — внутренний составной ключ (seq<<16)|attempt_no последней принятой отправки
|
||||
- attempt_count — выданные номера попыток данного seq, общие для всех каналов, включая неудачные send
|
||||
- active_attempts — число ещё учитываемых физических копий
|
||||
- Payload принадлежит только логической записи. Попытка ETCP_TX_ATTEMPT хранит seq и неизменные снимки, без payload.
|
||||
inflight_bytes/inflight_packets линка равны сумме DATA-длин/числу записей в его attempts, включая резерв send.
|
||||
ACK/till может уже удалить логический объект, пока другая физическая копия продолжает занимать своё окно.
|
||||
Повторный ACK неизвестной/истёкшей попытки не вычитает flight и не создаёт BBR sample.
|
||||
ACK известного ID с другим seq отклоняется целиком; неизвестный ID допускает только логическое подтверждение seq.
|
||||
Удаление линка обнуляет last_link в обеих очередях до освобождения его памяти.
|
||||
Общий optimal_inflight равен сумме лимитов всех линков, с насыщением UINT32_MAX;
|
||||
это дополнительный backpressure подачи новых фрагментов, не замена индивидуальных окон.
|
||||
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
|
||||
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
|
||||
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
|
||||
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
|
||||
input_queue даже если подтверждённые записи уже удалены по selective ACK.
|
||||
При заполненном sequence-окне собственный таймер запрашивает актуальный till секцией ACK_REQUEST.
|
||||
Получатель отвечает через запрашивающий путь, даже без ожидающих DATA/ACK-элементов.
|
||||
При продвижении frontier, reset/close таймер отменяется. Это восстанавливает поток при потере последнего till.
|
||||
Память физических попыток ограничена: 16384 на линк, 32768 на соединение; исчерпание блокирует DATA до освобождения.
|
||||
Очередь ACK каждого линка ограничена 16384 записями; переполнение логируется, DATA остаётся сохранённым для повтора/till.
|
||||
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
|
||||
optimal_inflight расчитывается из RTT и bandwidth линка.
|
||||
|
||||
bandwidth по каждому линку адаптивно подстраиваем:
|
||||
Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты).
|
||||
BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec).
|
||||
Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT.
|
||||
Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы.
|
||||
BBR каждого линка получает ACK/loss samples и возвращает cwnd и pacing rate.
|
||||
Скорость — доставленные байты за max(интервал отправки, интервал доставки), по снимкам при отправке.
|
||||
Интервал между соседними ACK не используется: ACK одной пачки могут обрабатываться в один tick.
|
||||
ACK с подтверждениями также возвращает точку приёма своего пути: накопленные DATA-байты и монотонное время
|
||||
последнего увеличения счётчика. В счётчик входят допустимые сохранённые/уже доставленные DATA, включая копии;
|
||||
OOM до сохранения и отвергнутые DATA его не меняют. Считаются байты полезной нагрузки секции, без заголовков.
|
||||
При отправке попытка запоминает известную точку. ACK действующей попытки продвигает точку пути;
|
||||
повторный/чужой ID её не меняет, переставленные ACK не откатывают. Противоречивая пара логируется и игнорируется.
|
||||
При положительных дельтах interval=max(send_us,ceil(delivered*delta_rx_us/delta_rx_bytes)).
|
||||
Приёмный интервал заменяет ACK clock: обратная очередь больше не завышает BW сжатием ACK
|
||||
и не занижает его растяжением. Send-интервал сохраняет защиту от сжатия приёмного времени.
|
||||
Без достоверной приёмной базы/положительных дельт используется max(send_us,ack_us).
|
||||
Счётчики и времена — uint64, часы двух сторон не сравниваются; нужны только дельты одних часов.
|
||||
Если интервал не представим в uint32, sample не используется для BW вместо насыщения, завышающего скорость.
|
||||
После пустого физического flight база начинается заново, чтобы простой не занижал скорость следующей передачи.
|
||||
Reset потока обнуляет точки приёма; исчерпание локального 64-битного счётчика закрывает соединение с ERROR.
|
||||
RTT/min_rtt, формула RTO, политика окна и дедлайн ACK не корректируются приёмной точкой.
|
||||
Внутренняя полоса — байты/мкс Q16; pacing — байты/с, BDP — байты, без повторного множителя MSS.
|
||||
Минимальное окно BBR — четыре DATA-фрагмента normalizer; максимальное — bbr_max_cwnd (по умолчанию 1048576).
|
||||
8192 — нижняя допустимая граница настройки bbr_max_cwnd; транспорт не повышает до неё текущее окно.
|
||||
PROBE_RTT осушает flight до max(4*MSS, 0.5*BDP), выдерживает >=200мс и раунд ACK, затем восстанавливает окно.
|
||||
RTT-фильтры обновляются при обработке ACK. Короткий минимум принимает меньший достоверный RTT
|
||||
либо текущее измерение после истечения 5с; RTT=0 означает отсутствие измерения.
|
||||
Долгий min_rtt принимает короткий минимум, если он не больше прежнего, либо истекли его 10с.
|
||||
Вместе со значением копируется метка короткого фильтра: повторное использование кеша не задаёт текущее время.
|
||||
Завершение PROBE_RTT возобновляет короткое окно. Окна истекают строго после своей границы;
|
||||
hold завершается при достижении срока и завершённом ACK-раунде. Метки RTT и срок hold — полные uint64 TB (100мкс).
|
||||
MSS — размер DATA-фрагмента normalizer без заголовков; начальное окно UDP и STCP равно 4*MSS с верхним лимитом конфига.
|
||||
В PROBE_BW действует дополнительный целевой бюджет очереди 20мс: всё окно, включая резервы ACK и PROBE_UP,
|
||||
ограничивается max(4*MSS, BDP + BW*20мс) по min_rtt. STARTUP/DRAIN не ограничивают окно этой формулой,
|
||||
но целый раунд RTT выше min_rtt+бюджет завершает STARTUP и переводит его в DRAIN.
|
||||
Shaper списывает только DATA-байты: единицы совпадают с delivery sample и pacing BBR.
|
||||
Это цель по оценкам BBR; минимум окна, изменение полосы и очереди обратного пути могут давать большую задержку.
|
||||
Отказ из-за нехватки окна сохраняет cwnd_limited до следующего ACK: вычитание ACK
|
||||
не должно скрывать этот факт от BBR. app_limited учитывает ожидающие очереди и
|
||||
pending/частичный буфер normalizer; ограничение окном не означает отсутствие данных.
|
||||
Потеря для BBR учитывается при RTO, фактическая повторная передача — при новой попытке.
|
||||
Три RTO физических DATA-попыток без ACK-прогресса ставят локальный флаг tx_stalled, в том числе в одном вызове таймера.
|
||||
До установки флага действующий ACK своего пути сбрасывает серию RTO. После установки сброс возможен только по ACK
|
||||
попытки, принятой транспортом после момента установки флага; старый/чужой/уже снятый attempt не доказывает восстановление.
|
||||
tx_stalled ограничивает выбор повторов при наличии альтернативы; UP, новые DATA, ACK/keepalive и формула RTO сохраняются.
|
||||
Reset линка снимает флаг и серию RTO. Новых очередей, копий payload и таймеров детектор не создаёт.
|
||||
При уменьшении окна уже отправленные пакеты остаются в учёте; новые DATA ждут ACK/RTO.
|
||||
Измерительные секции burst 0x07/0x08 и FILLER поддерживаются реализацией.
|
||||
|
||||
|
||||
**** Формат кодограмм для etcp.c/h ****
|
||||
Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
|
||||
1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах:
|
||||
[Timestamp high][Timestamp low][flags/flag_up]
|
||||
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется
|
||||
[Timestamp:2][flags/flag_up:1]
|
||||
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое), при повторе обновляется.
|
||||
Копируется из памяти в порядке байтов хоста; на используемых little-endian платформах передаётся LE.
|
||||
- flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны
|
||||
|
||||
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
|
||||
@@ -72,14 +175,19 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
|
||||
а) Подтверждения (ACK) - заголовок 0x01:
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][attempt_no(2 LE)][delay_us(4 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 255, дополнительно ограничено MTU и оставшимся местом)
|
||||
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
|
||||
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
|
||||
- seq: uint32_t — номер подтверждаемого пакета
|
||||
- recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс)
|
||||
- dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT)
|
||||
Каждая запись = 8 байт.
|
||||
- attempt_no: uint16_t — номер физической попытки данного seq, 1..65535; возвращается только тем же путём
|
||||
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
|
||||
При count>0 после всех записей обязательно добавляется [rx_data_bytes(8 LE)][rx_data_time_us(8 LE)].
|
||||
Это точка приёма всего пути на момент формирования ACK, а не отдельной подтверждаемой попытки.
|
||||
Время — монотонные микросекунды получателя при последнем положительном приросте DATA-байтов; 0/0 — точки нет.
|
||||
Каждая запись = 10 байт, база ACK = 8 байт; полный размер = 8+10*count+(count>0 ? 16 : 0).
|
||||
count=0 допустим для передачи till, точки приёма не содержит. До обработки проверяется вся длина ACK.
|
||||
ACK_REQUEST: [0x0a], одна секция длиной 1 байт, запрос повторной отправки актуального till.
|
||||
в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные):
|
||||
[0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)]
|
||||
- RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time)
|
||||
@@ -87,12 +195,20 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
- RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp
|
||||
(относительное смещение между локальным и удалённым временем)
|
||||
Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки)
|
||||
и если прошло не более ~100ms (dt < 1000000 таймбаз).
|
||||
и если прошло менее 100с (dt < 1000000 таймбаз). Результат rtt_last задаёт нижнюю границу RTT для RTO;
|
||||
DATA RTT и min_rtt BBR обновляются только ACK действующих физических попыток.
|
||||
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
|
||||
[0x00] [ID 4 байта LE] [данные...]
|
||||
- ID: uint32_t, 32-битный циклический номер пакета
|
||||
[0x00] [ID 4 байта LE] [attempt_no 2 байта LE] [данные...]
|
||||
- ID: uint32_t, номер фрагмента 1..UINT32_MAX в пределах одной эпохи потока.
|
||||
- attempt_no: uint16_t, номер 1..65535, общий для всех каналов одного ID.
|
||||
Номер выдаётся при создании попытки и не возвращается при отказе send; исходный seq при повторе сохраняется.
|
||||
После номера65535 следующая попытка аварийно меняет эпоху через REINIT; счётчик не оборачивается.
|
||||
Перед выдачей seq=0 после UINT32_MAX поток также меняет эпоху: пара seq/attempt_no не повторяется внутри эпохи.
|
||||
Смена эпохи отбрасывает прежний поток, владельцы восстанавливают свои операции по REINIT, как при фатальном OOM.
|
||||
Старые DATA/ACK отклоняются заголовком сессии до разбора. Заголовок PAYLOAD занимает7 байт.
|
||||
Внутренние хеш-очереди сохраняют64-битный ключ (seq<<16)|attempt_no; он не передаётся по сети целиком.
|
||||
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
|
||||
всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний.
|
||||
Порядок: ACK, возможный ACK_REQUEST, измерительные/временные секции и FILLER, затем payload последним.
|
||||
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
|
||||
|
||||
цель протокола:
|
||||
@@ -103,20 +219,25 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
|
||||
- RTT последнего пакета и время отправки последнего пакета
|
||||
- общее количество отправленных/принятых байт
|
||||
- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
|
||||
- число фактических повторных отправок; потери для BBR учитываются отдельно при истечении физических попыток.
|
||||
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
|
||||
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
|
||||
для каждого канала ограничение полосы с тестированием отклика:
|
||||
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков)
|
||||
Ограничение реализуем следующим образом:
|
||||
- в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности.
|
||||
Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время)
|
||||
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи");
|
||||
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения.
|
||||
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client.
|
||||
Окно и pacing проверяются до извлечения DATA из очереди; без допустимого UP-линка фрагмент остаётся в очереди.
|
||||
У каждого линка собственный shaper по точной bbr_pacing_rate в байтах/с:
|
||||
- После успешного send списываются только payload_len DATA-байтов, без заголовков и ACK.
|
||||
- Основной счётчик времени имеет единицу 0.1 мс; sub-разряды — 0.1 нс, модуль 1000000.
|
||||
Прибавка равна ceil(payload_len * 10000000000 / bbr_pacing_rate), вычисляется целочисленно в uint64.
|
||||
- Накопленный кредит после простоя ограничивается 1 мс. При смене ставки пересчитывается оставшийся долг;
|
||||
таймер снимается и при необходимости назначается заново. При нулевой ставке pacing не ограничивает отправку.
|
||||
- Положительный долг должен быть строго меньше 1 мс. Таймер ждёт первого допустимого TB:
|
||||
shaper_load_time_tb - SHAPER_BURST_DELAY_TB + 1; дробь меньше TB не меняет эту строгую границу.
|
||||
uasync округляет срок вверх до миллисекунды; callback повторно проверяет готовность перед уведомлением.
|
||||
- ACK/control и измерительная burst-серия не списывают DATA-pacing. Окно DATA проверяется и в burst.
|
||||
Конструктор исходящего UDP-линка запускает INIT сразу; TCP использует отдельный STCP connect/handshake.
|
||||
процесс установки соединения (рукопожатие):
|
||||
- клиент отправляет init запрос и выставляет таймаут
|
||||
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
|
||||
- по таймауту повторяет INIT: начальный интервал 500 TB = 50 мс, максимум 50000 TB = 5 с;
|
||||
каждые 10 отправок интервал увеличивается на interval/4 + 1 TB. TB здесь равен 0.1 мс.
|
||||
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
|
||||
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
|
||||
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
|
||||
@@ -157,18 +278,18 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
- mtu: uint16_t локальный MTU
|
||||
- keepalive: uint16_t интервал keepalive в мс
|
||||
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
|
||||
- link_id: uint8_t (0-255), назначается отправителем
|
||||
- link_id: uint8_t (1-255), назначается отправителем; ноль запрещён
|
||||
- socket_id: uint8_t, уникальный ID сокета отправителя
|
||||
- only_local: uint8_t, флаг "только локальные подключения"
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- type: uint8_t, конфигурационный тип сокета CFG_SERVER_TYPE_*
|
||||
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
|
||||
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
|
||||
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
|
||||
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
|
||||
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
|
||||
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
|
||||
- salt + obfuscated_pubkey: ключ отправителя XOR SHA256(salt || публичный ключ получателя).
|
||||
Этот блок не шифруется AES, передаётся в конце пакета
|
||||
в noencrypt_len байтах (не попадает под AES-шифрование)
|
||||
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
|
||||
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
|
||||
@@ -182,7 +303,7 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
- node_id: 64 бита, big-endian
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
|
||||
- mtu: uint16_t локальный MTU сервера
|
||||
- link_id: uint8_t идентификатор канала сервера
|
||||
- link_id: uint8_t идентификатор канала сервера, 1..255
|
||||
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
|
||||
- only_local: uint8_t флаг сервера
|
||||
- type: uint8_t тип сокета сервера
|
||||
@@ -211,16 +332,34 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
|
||||
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
|
||||
|
||||
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
|
||||
До изменения транспорта INIT_REQUEST проверяет полное 67-байтовое тело и соответствие node_id
|
||||
аутентифицированному X25519-ключу. Оба направления INIT отклоняют link_id=0 и MTU меньше
|
||||
ETCP_MIN_UDP_MTU(family): 191 байт для IPv4, 211 для IPv6. MTU больше 1500 ограничивается сверху.
|
||||
Получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
|
||||
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
|
||||
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
|
||||
приём кодограмм из UDP выглядит так:
|
||||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||||
Принятый INIT обновляет recv_keepalive, время последнего RX и входной remote_keepalive=flag_up
|
||||
до переиспользования буфера для ответа. INIT с flag_up=0 снимает прежний UP UDP-линка:
|
||||
получение запроса доказывает только входящую достижимость, обратный путь ещё не подтверждён.
|
||||
Ответ и keepalive отправляются и при DOWN, чтобы холодное подключение и восстановление
|
||||
могли завершить двустороннюю проверку. При DOWN одного линка соединение остаётся UP,
|
||||
если есть другой работоспособный линк. Повтор неизменного состояния не создаёт нового DOWN/UP.
|
||||
INIT_RESPONSE должен содержать полное обязательное тело (64 байта без timestamp/flag_up)
|
||||
и node_id, соответствующий аутентифицированному ключу пира. Усечённый ответ или чужой node_id
|
||||
отклоняются до изменения живости, эпохи и параметров линка. Дополнительный padding разрешён.
|
||||
Входящий conn с известным ключом индексируется по node_id до callback создания линка.
|
||||
Отмена входящего линка из callback не закрывает conn, если у него появился другой линк или NCD-владелец.
|
||||
Close немедленно отключает линк и ресурсы, но память удерживается active_users до выхода внутренних стеков RX/переходов/callbacks.
|
||||
Приём UDP: uasync -> etcp_connections_read_callback_socket -> etcp_process_packet ->
|
||||
etcp_packet_decrypted (проверки и снятие SESSION_DATA) -> etcp_conn_input -> normalizer/output_queue.
|
||||
|
||||
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
|
||||
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
|
||||
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
|
||||
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
|
||||
Обычные keepalive периодически шлют обе стороны; полезный TX сбрасывает адаптивный период к базовому.
|
||||
Сон одной из сторон останавливает обычный таймер. UDP-анонс сна подтверждается флагом SLEEP_ACK,
|
||||
повторяется до ACK/исчерпания попыток; TCP отдельного подтверждения сна не требует.
|
||||
RX watchdog работает отдельно в normal и standby и не продлевается собственными отправками.
|
||||
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
|
||||
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
|
||||
|
||||
@@ -234,7 +373,8 @@ Padding keepalive, PING/PONG и STCP-handshake:
|
||||
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
|
||||
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
|
||||
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
|
||||
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
|
||||
padding_size=0 сразу завершает handshake. При ETCP_CONN клиент читает актуальный диапазон при отправке;
|
||||
без ETCP_CONN сохраняет выбранную длину при запуске. Сервер выбирает длину независимо по пиру/instance.
|
||||
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
|
||||
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
|
||||
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
|
||||
@@ -280,3 +420,22 @@ only after STCP handshake. It completes the same session confirmation before INI
|
||||
REINIT notifies stream consumers without tearing down physical ownership. The
|
||||
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
|
||||
its previous control messages may have been discarded by the stream reset.
|
||||
|
||||
Отказы памяти и прерывание потока:
|
||||
- OOM до сохранения RX DATA в recv_q не создаёт ACK; отправитель повторяет DATA.
|
||||
- ACK означает принятие в текущий транспортный поток, а успешный etcp_send —
|
||||
передачу владения транспорту. Оба не гарантируют завершение операции сервиса
|
||||
после прерывания сессии. Надёжность операции через REINIT обеспечивает её владелец.
|
||||
- OOM после принятия данных в normalizer или при подготовке TX inflight вызывает
|
||||
fatal REINIT: новая локальная эпоха, блокировка DATA, отбрасывание старых очередей,
|
||||
частичных кодограмм и физических попыток. Старые payload не воспроизводятся.
|
||||
Физические линки сохраняются; очистка очередей/reset не выделяет память.
|
||||
- Активный обработчик освобождает собственный извлечённый фрагмент ровно один раз;
|
||||
после reset/close из callback он прекращает разбор старой кодограммы/пакета.
|
||||
- Согласование новой эпохи выполняется после очистки. OOM control-пакета оставляет
|
||||
повтор активным, без продления исходного срока 10с. Отказ обязательного session-
|
||||
таймера или генерации локальной эпохи закрывает соединение с диагностикой.
|
||||
Если невозможно выделить даже обязательный deferred-close token внутри callback,
|
||||
процесс завершается через abort: соединение не оставляется в зависшем состоянии.
|
||||
- После подтверждения эпох владельцы получают REINIT и восстанавливают операции
|
||||
своего протокола; при невозможности согласования соединение закрывается.
|
||||
+125
-305
@@ -1,329 +1,149 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
|
||||
# Архитектура ETCP Router
|
||||
|
||||
# ETCP Router Architecture
|
||||
Маршрутизатор связывает сервис с удалённым узлом через группу, не привязывая логическую
|
||||
сессию к одному физическому ETCP. Ключ канала — `(group_id, remote_node_id, svc_id)`.
|
||||
Wire-формат и сервисный API подробно описаны в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md),
|
||||
объявления — в [etcp_router.h](../src/routing_layer/etcp_router.h).
|
||||
|
||||
## Структуры данных
|
||||
## Отправка и приём
|
||||
|
||||
```
|
||||
ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ll_entry (хеш-индекс: remote_node_id[8] + svc_id[1]) │
|
||||
│ remote_node_id: u64 │
|
||||
│ svc_id: u8 │
|
||||
│ last_dgram_ts: u16 (timestamp последней датаграммы) │
|
||||
│ │
|
||||
│ tx_seq: u32 следующий seq для отправки │
|
||||
│ rx_seq: u32 ожидаемый seq для сборки │
|
||||
│ tx_acked: u32 сколько наших пакетов подтвердил remote │
|
||||
│ last_sent_ack_seq: u32 последний отправленный ACK │
|
||||
│ │
|
||||
│ recv_q: ll_queue* очередь reorder (хеш по seq[4]) │
|
||||
│ send_q: ll_queue* очередь при переполнении inflight │
|
||||
│ │
|
||||
│ ack_timer: void* периодический 100ms │
|
||||
│ idle_ack_timer: void* idle таймаут 500ms │
|
||||
│ send_waiter: queue_waiter_handle waiter на send_input_q │
|
||||
│ watchdog_timer: void* проверка инварианта 500ms │
|
||||
│ send_blocked: u8 inflight полон │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
SVC_ROUTE_HDR (22 байта)
|
||||
┌──────┬────────────────┬────────────────┬───────┬────────┬──────────┐
|
||||
│ cmd │ dst_node_id │ src_node_id │ seq │ svc_id │ payload │
|
||||
│ u8 │ u64 │ u64 │ u32 │ u8 │ ... │
|
||||
└──────┴────────────────┴────────────────┴───────┴────────┴──────────┘
|
||||
|
||||
seq в data-пакетах = tx_seq (счетчик отправленных)
|
||||
seq в ACK-пакетах = rx_seq (ожидаемый следующий)
|
||||
CLOSE-флаг = 0x80000000 в seq, RST-флаг = 0x40000000
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Producer["Сервис: payload + mode"] --> Send["send_q: plaintext, seq, mode"]
|
||||
Send --> Session{"peer_reset_id подтверждён?"}
|
||||
Session -- нет --> Hello["HELLO / challenge / confirm"]
|
||||
Hello --> Session
|
||||
Session -- да --> Route["Текущий маршрут: conn_mgr → BGP, либо прямой глобальный ETCP"]
|
||||
Route --> Wait["Waiter физической send_input_q"]
|
||||
Wait --> Encode["route_crypto_encode: encrypt → sign"]
|
||||
Encode --> Inflight["inflight_q: неизменяемое тело без visited-list"]
|
||||
Inflight --> Path["Добавить source ID"]
|
||||
Path --> ETCP["etcp_send / normalizer"]
|
||||
Inflight -- RTO 300 ms --> Wait
|
||||
ETCP --> Network["Сеть / транзитные узлы"]
|
||||
Network --> Check["Проверить visited-list"]
|
||||
Check --> Destination{"dst=self?"}
|
||||
Destination -- нет --> Transit["Добавить self один раз, BGP next hop / transit FIFO"]
|
||||
Transit --> Network
|
||||
Destination -- да --> Decode["Crypto policy + verify/decrypt + проверка эпох"]
|
||||
Decode --> Incoming["incoming_q: plaintext"]
|
||||
Incoming --> Reorder["recv_q: reorder по seq"]
|
||||
Reorder --> Ready{"receive_ready разрешает?"}
|
||||
Ready -- нет --> Busy["Сохранить пакет, ACK RECEIVE_BUSY"]
|
||||
Busy --> Resume["resume_receive: отложенное продолжение"]
|
||||
Resume --> Reorder
|
||||
Ready -- да --> Callback["Callback сервиса получает entry"]
|
||||
Callback --> Ack["rx_seq++ → ACK текущей границы"]
|
||||
Ack --> Release["Отправитель снимает подтверждённый inflight"]
|
||||
```
|
||||
|
||||
## Send Path (отправка данных)
|
||||
Каждая кодограмма имеет самостоятельный seq; это не байтовый поток.
|
||||
send_q ограничена 64 пакетами при обычной отправке, `force` обходит этот порог.
|
||||
Inflight по умолчанию — 256, при minRTT probe — 4. Получатель ограничивает reorder
|
||||
окном max_inflight и суммарным размером очередей max_inflight+1 записей.
|
||||
|
||||
```
|
||||
etcp_route_send(inst, dst, entry)
|
||||
etcp_router_conn_send(rconn, data, len)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ rconn = find/get │ поиск по (dst, svc_id)
|
||||
│ авто-создание если │ или etcp_router_conn_get()
|
||||
│ ещё нет │
|
||||
└────────┬────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
|
||||
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
|
||||
│ return -1 │
|
||||
└──────┬───────────────────────┘
|
||||
│ копия в send_q + router_send_kick()
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_send_kick(rconn) │
|
||||
│ closed/no_route → return │
|
||||
│ send_q пуст → return │
|
||||
│ inflight >= limit → send_blocked=1, ret │
|
||||
│ waiter уже ждёт → return │
|
||||
│ нет conn → set_no_route │
|
||||
│ queue_waiter_wait(send_input_q, │
|
||||
│ &send_waiter, drain_cb) │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│ send_input_q опустел (round-robin: FIFO waiter-ов)
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
|
||||
│ e = send_q.pop() │
|
||||
│ router_send_one_flags(e) │
|
||||
│ → tx_seq++ │
|
||||
│ → etcp_send(conn, entry) │
|
||||
│ если send_q ещё непуст: │
|
||||
│ inflight полон → blocked=1│
|
||||
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
|
||||
│ если пуст → send_blocked=0 │
|
||||
└──────────────────────────────┘
|
||||
Все события выполняются в uasync-потоке. Штатный normalizer откладывает physical waiters,
|
||||
а callbacks производителей send_q могут выполняться синхронно при освобождении места.
|
||||
Поэтому код отправки повторно проверяет состояние/эпохи после callback.
|
||||
|
||||
Повторный запуск drain:
|
||||
• приход ACK → router_handle_ack → kick
|
||||
• маршрут появился → router_no_route_retry_cb → kick
|
||||
• рост inflight-лимита → router_update_inflight_limit → kick
|
||||
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
|
||||
## Эпохи логического канала
|
||||
|
||||
Полный сетевой пакет:
|
||||
|
||||
`[57-byte SVC_ROUTE_HDR][payload][signature?][visited_ids:8*N][count:1]`
|
||||
|
||||
Числа в заголовке — native endian. flags — отдельный байт; биты seq не заняты флагами.
|
||||
Заголовок включает group/src/dst/service, seq/timestamp и поля reset_id/peer_reset_id/challenge.
|
||||
visited-list содержит 1..64 ID, находится вне подписи/шифрования и проверяется на каждом переходе.
|
||||
|
||||
Первичное согласование двух сторон имеет вид:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Узел A, local=a
|
||||
participant B as Узел B, local=b
|
||||
A->>B: HELLO(reset=a, peer=0)
|
||||
B->>A: CHALLENGE(reset=b, peer=a, cB)
|
||||
A->>B: CONFIRM(reset=a, peer=b, cB)
|
||||
A->>B: CHALLENGE(reset=a, peer=b, cA)
|
||||
B->>A: CONFIRM(reset=b, peer=a, cA)
|
||||
Note over A,B: Каждый узел принимает peer ID только после ответа на свой pending challenge
|
||||
```
|
||||
|
||||
## Receive Path (приём данных)
|
||||
Получение HELLO/CHALLENGE с неизвестной эпохой само по себе не сбрасывает работающий канал.
|
||||
Переставленные/старые CONFIRM не совпадают с pending ID/challenge и игнорируются.
|
||||
Новые challenge ограничены интервалом 300 ms; первичный HELLO повторяется без отдельного timeout.
|
||||
|
||||
```
|
||||
etcp_router_recv_cb(conn, entry)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ dst == node_id? │
|
||||
└──┬───────────────┬───┘
|
||||
│ YES │ NO (транзит)
|
||||
▼ ▼
|
||||
┌────────────────────┐ ┌──────────────────────────┐
|
||||
│ Мы — целевая нода │ │ Транзит: найти next hop │
|
||||
└────────┬───────────┘ │ route_bgp_find_conn_for_ │
|
||||
│ │ node(bgp, hdr.dst) │
|
||||
▼ │ etcp_send(next, entry) │
|
||||
┌────────────────────┐ └──────────────────────────┘
|
||||
│ pl_len == 0? │
|
||||
└──┬───────────┬─────┘
|
||||
│ YES │ NO (данные)
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────────────────────┐
|
||||
│ CLOSE/ │ │ rconn = find/get │
|
||||
│ RST? │ │ (авто-создание если нет) │
|
||||
└──┬──┬───┘ └───────────┬─────────────┘
|
||||
│ │ │
|
||||
│ │ NO (ACK) ▼
|
||||
│ ▼ ┌───────────────────────────────────────┐
|
||||
│ ┌──────────┐│ Проверка границ seq: │
|
||||
│ │ tx_acked ││ d = seq - rx_seq │
|
||||
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
|
||||
│ │ (только ││ │
|
||||
│ │ вперёд) ││ Дубликат? │
|
||||
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
|
||||
│ └──────────┘│ seq уже в recv_q? │
|
||||
│ └──────┬─────────────┬──────────────────┘
|
||||
│ │ OK │ out-of-bounds / dup
|
||||
│ ▼ ▼
|
||||
│ ┌──────────────┐ ┌─────────┐
|
||||
│ │ recv_q.put() │ │ DROP │
|
||||
│ │ хеш по seq │ └─────────┘
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ seq == rx_seq? │
|
||||
│ └──┬───────────────┬───┘
|
||||
│ │ YES │ NO
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ try_assembly │ │
|
||||
│ │ while next в │ │
|
||||
│ │ recv_q: │ │
|
||||
│ │ deliver() │ │
|
||||
│ │ rx_seq++ │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┴──┐
|
||||
│ │ router_deliver() │
|
||||
│ │ вызывает callback │
|
||||
│ │ сервиса (svc_id) │
|
||||
│ │ entry: [svc_id|payload] │
|
||||
│ └──────────────┬──────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────────────────────┐
|
||||
│ router_schedule_ack(rconn) │
|
||||
│ запускает ack_timer (100ms) если │
|
||||
│ ещё не запущен │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
| Событие | Что сохраняется | Что меняется |
|
||||
| --- | --- | --- |
|
||||
| Смена ETCP/next hop | Логические эпохи, seq, очереди | Физический waiter/маршрут |
|
||||
| Первое подтверждение peer | Накопленная send_q | Заполняется peer_reset_id, начинается отправка |
|
||||
| Подтверждённый рестарт peer | Собственный reset_id | Очереди, seq, RTT/метрики; уведомление сервиса |
|
||||
| Явный conn_restart | Ключ group/peer/service и адрес rconn | Новый reset_id, пустое состояние; затем callback и HELLO |
|
||||
| conn_close | Только данные, переданные во владение callback | Запись снимается с реестра, ресурсы освобождаются |
|
||||
|
||||
```
|
||||
CLOSE / RST ветка:
|
||||
┌────────────────────────────────────────┐
|
||||
│ pl_len == 0 && seq & CLOSE/RST │
|
||||
│ → router_close_and_notify(rconn) │
|
||||
│ ├─ router_send_close_to_service() │
|
||||
│ │ (callback с NULL conn) │
|
||||
│ ├─ отправить CLOSE удалённой стороне│
|
||||
│ ├─ отмена всех таймеров │
|
||||
│ ├─ очистка recv_q + send_q │
|
||||
│ └─ удаление из inst->router_conns │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
Сервис возобновляет прикладную синхронизацию после сброса; старые гарантии доставки через reset не продолжаются.
|
||||
Готовность router означает наличие подтверждённого peer ID. Это отдельное состояние
|
||||
от физического UP и READY BGP-группы.
|
||||
|
||||
## ACK Mechanism
|
||||
## Подтверждение и пауза потребителя
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_schedule_ack(rconn) │
|
||||
│ (вызывается при получении данных) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
┌────────────────▼────────────────────────┐
|
||||
│ если idle_ack_timer активен — отменить │
|
||||
│ если ack_timer не активен — запустить │
|
||||
│ ROUTER_ACK_INTERVAL_TB = 1000 (100ms) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼ (через 100ms)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_ack_timer_cb() │
|
||||
│ │
|
||||
│ if rx_seq != last_sent_ack_seq: │
|
||||
│ router_send_ack(rconn) │
|
||||
│ last_sent_ack_seq = rx_seq │
|
||||
│ перезапустить ack_timer │
|
||||
│ else: │
|
||||
│ запустить idle_ack_timer (500ms) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼ (через 500ms)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_idle_ack_timer_cb() │
|
||||
│ │
|
||||
│ if rx_seq != last_sent_ack_seq: │
|
||||
│ router_send_ack(rconn) (досылка) │
|
||||
│ last_sent_ack_seq = rx_seq │
|
||||
└─────────────────────────────────────────┘
|
||||
DATA проходят crypto policy и verify/decrypt до изменения reorder/rx_seq.
|
||||
Сервис получает `[svc:1][src:8][dst:8][rx_flags:1][group:8][payload]`, всего 26 байт заголовка.
|
||||
Его callback владеет entry, ACK продвигается после успешной передачи callback.
|
||||
|
||||
router_send_ack(rconn):
|
||||
hdr.seq = rx_seq
|
||||
hdr.svc_id = svc_id
|
||||
pl_len = 0 → это ACK-пакет
|
||||
etcp_send(conn, entry)
|
||||
```
|
||||
ACK подтверждает только диапазон до tx_sent, в модульном порядке uint32.
|
||||
При продвижении снимаются существующие inflight-записи и один раз учитываются исходные payload bytes.
|
||||
|
||||
```
|
||||
Обработка входящего ACK:
|
||||
┌────────────────────────────────────────┐
|
||||
│ pl_len == 0 && нет CLOSE/RST флагов │
|
||||
│ │
|
||||
│ if seq >= tx_acked: // только вперёд │
|
||||
│ tx_acked = seq // обновить │
|
||||
│ else: │
|
||||
│ DEBUG_WARN "stale ACK" │
|
||||
│ │
|
||||
│ if send_blocked: │
|
||||
│ send_blocked = 0 │
|
||||
│ router_send_kick(rconn) │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
RECEIVE_BUSY не подтверждает отложенные DATA, но поддерживает живость ожидающего диска/буфера сервиса.
|
||||
Даже при прежнем seq допустимый BUSY обновляет отсчёт RTO и останавливает новую отправку/проход ретрансмитов.
|
||||
Обычный повторный ACK без продвижения этого не делает. Снятие BUSY возобновляет send_q.
|
||||
|
||||
## Inflight Control
|
||||
receive_ready — предикат без I/O и изменения/закрытия роутера. При 0 пакет сохраняется в recv_q;
|
||||
resume_receive планирует продолжение. Управляющий CLOSE не проходит через этот предикат.
|
||||
|
||||
```
|
||||
ROUTER_MAX_INFLIGHT = 256
|
||||
| Механизм | Интервал/условие |
|
||||
| --- | --- |
|
||||
| ACK | Не чаще 10 ms; текущая граница повторяется при дубликате DATA после потери ACK |
|
||||
| RECEIVE_BUSY | Около 100 ms, пока сервис остановлен |
|
||||
| Idle ACK | 500 ms, только если rx_seq ещё не был отправлен в ACK |
|
||||
| Handshake/RTO | 300 ms |
|
||||
| Нет маршрута после handshake | Повторный поиск через 20 ms |
|
||||
| Транзит при backpressure | Waiter + повтор выбора маршрута через 20 ms |
|
||||
| Watchdog send_q | 500 ms, пока очередь непуста |
|
||||
| Нет прогресса/BUSY | Закрытие после 17 циклов, для сервиса CALL — после 67 |
|
||||
|
||||
Отправка возможна когда: tx_seq - tx_acked < 256
|
||||
Отправка заблокирована: tx_seq - tx_acked >= 256
|
||||
Транзитные очереди принадлежат физическому ETCP, сгруппированы по group/src/dst и не имеют своих seq.
|
||||
При смене маршрута пакет переносится на актуальный next hop без ожидания освобождения старой очереди.
|
||||
Отсутствие пути приводит к потере транзитной копии; её восстанавливает end-to-end inflight источника.
|
||||
|
||||
При блокировке:
|
||||
данные → send_q (FIFO, без дропов)
|
||||
send_blocked = 1
|
||||
## Владение и локальная доставка
|
||||
|
||||
Разблокировка:
|
||||
приход ACK от remote → tx_acked обновляется
|
||||
→ router_send_kick() → waiter → drain_cb разгребает send_q
|
||||
или маршрут появился / inflight-лимит вырос → kick
|
||||
страховка: router_send_watchdog_cb (500ms) → force kick
|
||||
| Объект | Владение |
|
||||
| --- | --- |
|
||||
| Входной entry etcp_route_send | Всегда поглощается, включая ошибку |
|
||||
| Буфер etcp_router_conn_send | Заимствован на время копирования |
|
||||
| ETCP_ROUTER_CONN из conn_get | Заимствован до close/destroy |
|
||||
| Сервисный entry | Передаётся callback; освободить dgram и entry |
|
||||
| Физический conn в callback | Заимствован, может обозначать транзитного пира |
|
||||
| Синтетический conn | Только на время callback, instance/peer=self; без сетевого send/ref_take |
|
||||
| Send-ready handle/arg | У производителя до callback/отмены; перед освобождением отменить |
|
||||
|
||||
send_q полностью разобран → send_blocked = 0
|
||||
```
|
||||
Self-доставка через etcp_route_send синхронна и не создаёт канал. Она обходит сетевые crypto policy
|
||||
и receive_ready, выдаёт rx_flags=0; режим проверяется на допустимые биты.
|
||||
Payload должен быть непустым и помещаться вместе с 26-байтным заголовком в uint16 длину.
|
||||
Нет обработчика, переполнение размера или отказ памяти возвращают -1.
|
||||
Отдельный синтетический conn каждого вызова сохраняет контекст при вложенных доставках разных ядер.
|
||||
|
||||
## Полная схема потоков
|
||||
Закрываемый канал удаляется из реестра до callback. Callback может закрывать группу/другие каналы
|
||||
или открыть замену с тем же ключом; старая запись не возвращается повторным поиском.
|
||||
Память канала и send_q освобождается отложенно за пределами текущего callback.
|
||||
До массовой остановки сервисы отменяют producers/waiters; уничтожение ядра из callback останавливаемого сервиса запрещено.
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Сервис │ (routing, proxy, NAT, ...)
|
||||
│ (svc_id)│
|
||||
└────┬─────┘
|
||||
│
|
||||
etcp_route_send() │ callback(svc_id, entry)
|
||||
▼ ▲
|
||||
┌─────────┐ send ┌─────────────────────────────────┐ recv ┌─────────┐
|
||||
│ TUN │─────────▶│ ETCP ROUTER │─────────▶│ TUN │
|
||||
│ (выход) │ │ │ │ (вход) │
|
||||
└─────────┘ │ ┌──────────┐ ┌──────────┐ │ └─────────┘
|
||||
│ │ send_q │ │ recv_q │ │
|
||||
│ │ (FIFO) │ │ (хеш seq)│ │
|
||||
│ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ Inflight / Assembly │ │
|
||||
│ │ tx_seq - tx_acked │ │
|
||||
│ │ rx_seq → deliver │ │
|
||||
│ └───────────┬─────────────┘ │
|
||||
│ │ │
|
||||
└──────────────┼──────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ BGP │
|
||||
│ (нахождение│
|
||||
│ next hop) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ ETCP conn │──▶ сеть
|
||||
└─────────────┘
|
||||
close_group удаляет логические каналы и pending transit указанной группы.
|
||||
destroy снимает wire callback, закрывает все логические каналы и очищает транзитные очереди,
|
||||
таймеры и waiters на всех живых ETCP. Уничтожать сам ETCP для очистки роутера не требуется.
|
||||
|
||||
──▶ send (исходящие данные)
|
||||
──▶ recv (входящие данные, callback сервису)
|
||||
──▶ транзит (через BGP к следующему hop)
|
||||
```
|
||||
|
||||
## Таймеры (один rconn)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ack_timer ──────── 100ms ─────▶ router_ack_timer_cb│
|
||||
│ (периодический) │
|
||||
│ │
|
||||
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
|
||||
│ (досылка последнего ACK) │
|
||||
│ │
|
||||
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
|
||||
│ (страховка инварианта drain, force kick) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Флаги seq
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ CLOSE: 0x80000000 нормальное закрытие conn │
|
||||
│ RST: 0x40000000 conn не найден (reset) │
|
||||
│ │
|
||||
│ CLOSE/RST-пакеты: seq=0 | флаг, pl_len=0 │
|
||||
│ Data-пакеты: seq=tx_seq, pl_len>0 │
|
||||
│ ACK-пакеты: seq=rx_seq, pl_len=0 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
Метрики delivery считают первые подтверждения исходного payload, без заголовков/crypto/ретрансмитов.
|
||||
RTT — 0.1 ms, snapshot API выдаёт микросекунды; jitter — unsigned 16.16 EWMA,
|
||||
монитор публикует удвоенный EWMA. Очереди, метрики и параметры minRTT сбрасываются вместе с логической сессией.
|
||||
@@ -0,0 +1,530 @@
|
||||
Аудит media_async / media_delivery — 2026-10-24
|
||||
Статус: alias encrypted_file, граница voice_file, переанонс SQLite и расхождение MIME write/read исправлены.
|
||||
Сверка двух каталогов завершена: все11 заголовков, sync.md, _desc.txt и encrypted_attachments.md.
|
||||
Общий service_lifecycle.md проверен в media-части; разделы других сервисов не считаются закрытыми.
|
||||
Ниже сохранена хронология воспроизведений и исправлений; прежние отметки незавершённости относятся к этапам.
|
||||
Последние проверки MIME и итоговая архитектурная сверка — в конце.
|
||||
|
||||
Подтверждённый дефект: исходник уничтожается через другое имя того же файла
|
||||
|
||||
encrypted_file_write, src/media_async/encrypted_file.c:79-87, сравнивает только строки
|
||||
source/destination. Разные строки могут обозначать один inode. fopen(destination,"wb")
|
||||
усекает уже открытый source. Криптография затем успешно обрабатывает пустой файл.
|
||||
Контейнер корректен для нулевого содержимого, но исходные данные потеряны; возврат равен 0.
|
||||
|
||||
Воспроизведено на настоящих функциях из src/libutun.a:
|
||||
- source и source с компонентом /./: исходные30 байт заменены контейнером152 байта,
|
||||
plaintext_size=0, rc=0, SHA256 исходника изменился;
|
||||
- source и hardlink: тот же результат;
|
||||
- encrypted_file_read тоже пропускает оба вида alias: контейнер182 байта заменяется
|
||||
plaintext30 байт, rc=0, исходный ciphertext больше не существует в прежнем виде.
|
||||
|
||||
У read такая же проверка strcmp в строке118 и разрушающий fopen в строке142.
|
||||
В текущем групповом pipeline вызывающий выбирает отдельный .part; воспроизведение
|
||||
доказывает дефект файлового API, а не наблюдавшуюся потерю реального пользовательского вложения.
|
||||
Ошибку нельзя замаскировать описанием одинаковых файлов как успешного no-op:
|
||||
шифрование и расшифровка должны создавать другое содержимое.
|
||||
|
||||
Диагностика:
|
||||
- /tmp/media-encrypted-alias-repro.c — четыре проверки, сверяются inode, размеры и SHA256;
|
||||
- /tmp/media-encrypted-alias-repro.mk — использует цели/библиотеки/флаги tests/Makefile;
|
||||
- /tmp/media-encrypted-alias-repro.log — полный журнал с media=debug;
|
||||
- /tmp/media-encrypted-alias-lvVS77/ — сохранённые входы и результаты опыта.
|
||||
Проектные .c/.h для воспроизведения не менялись.
|
||||
|
||||
Согласованная архитектура исправления
|
||||
|
||||
Одной проверки file_io_paths перед fopen(wb) недостаточно для полного исправления
|
||||
контракта владения. Сейчас encrypted_file удаляет destination при ошибке, а
|
||||
ak_file_done повторно удаляет заранее вычисленный .part даже при раннем отказе.
|
||||
Нужно убрать открытие произвольного назначения с усечением и очистку по одному имени.
|
||||
|
||||
Граница модулей
|
||||
|
||||
- lib/file_io: UTF-8/regular-file проверки, идентичность, эксклюзивный temp,
|
||||
flush/sync/close, atomic rename, владение temp и защита от orphan cleanup.
|
||||
- encrypted_file: создание/проверка контейнера в собственном temp; без публикации
|
||||
конечного имени, SQLite и состояния async-задачи.
|
||||
- chat_attachment: задача владеет temp, выбирает конечное имя и выполняет одну
|
||||
публикацию; completion меняет состояние БД/UI и освобождает только свои ресурсы.
|
||||
Подготовка, sync и rename остаются в worker, как сейчас; новые файловые I/O в uasync не добавляются.
|
||||
|
||||
Предлагаемый контракт write/read
|
||||
|
||||
1. Функция получает source, конечный target_path и указатель на обнулённый file_io_temp.
|
||||
target_path задаёт место будущего результата; он не открывается с усечением.
|
||||
До начала работы file_io_paths(source,target_path) обязан вернуть 0.
|
||||
Совпадение строки, /./, hardlink и конечная ссылка назначения отклоняются без изменений.
|
||||
2. Источник открывается общим file_io_open_read. Только после успешного открытия
|
||||
создаётся уникальный temp рядом с target_path через file_io_temp_open.
|
||||
На POSIX он имеет права 0600; создание эксклюзивное, владелец сразу учтён в общем реестре.
|
||||
3. Контейнер пишется/проверяется через уже открытый FILE этого temp. Нет второго .part,
|
||||
второго открытия выхода по пути или дополнительного слоя публикации внутри codec.
|
||||
4. Полностью проверяются данные, закрытие входа и durable завершение выхода.
|
||||
При ошибке удаляется только созданный этой операцией temp; существующие source/target сохраняются.
|
||||
При успехе возвращается закрытый, синхронизированный temp, который ещё не опубликован.
|
||||
Результат info/type у read присваивается только после полного успешного завершения подготовки.
|
||||
5. Владелец file_io_temp размещается прямо в ak_file и имеет стабильный адрес до done;
|
||||
копировать его структуру нельзя: реестр хранит адрес владельца.
|
||||
Функции не принимают уже активный output и не уничтожают его при отказе аргументов.
|
||||
|
||||
Единая политика durability
|
||||
|
||||
В file_io_temp_open добавить flags, в file_io_temp хранить выбранный режим на весь
|
||||
срок операции. FILE_IO_DURABLE включает обе части: finish и publish.
|
||||
Обычные пользователи передают 0 и сохраняют свою семантику; отдельные флаги в каждом
|
||||
finish/publish не нужны. Это предотвращает случайное сохранение файла без сохранения имени.
|
||||
|
||||
Durable finish: fflush -> fsync/_commit того же открытого fd -> fclose. Ошибка любой
|
||||
фазы сохраняет первый код ошибки; close выполняется и при отказе sync. Открывать путь
|
||||
заново ради fsync не нужно. После отказа temp остаётся у владельца до discard.
|
||||
|
||||
Publish повторно проверяет идентичность source/target и принимает только закрытый temp.
|
||||
На POSIX: rename и fsync каталога назначения; соседний temp находится в том же каталоге.
|
||||
На Windows: общий wide API и MOVEFILE_WRITE_THROUGH. Родительский каталог вычисляется
|
||||
без обрезания: имя без '/' означает '.', '/file' означает '/'.
|
||||
Политика sync принадлежит file_io, а не формату зашифрованного контейнера.
|
||||
|
||||
Точка фиксации — успешный rename. До неё ошибка сохраняет старый target.
|
||||
После rename ошибка fsync каталога означает: результат уже установлен, долговечность
|
||||
имени не подтверждена. Нельзя обещать откат, удалять новый файл вслепую или восстанавливать
|
||||
старое содержимое. В этот момент temp уже отсоединён от реестра и его имя в owner пусто.
|
||||
Этот переход должен быть явно описан в file_io.h и проверен тестом.
|
||||
Длительный fsync выполняется после снятия блокировки реестра временных файлов.
|
||||
|
||||
Владение задачи и отмена
|
||||
|
||||
ak_file хранит file_io_temp вместо temporary[...]. Worker после успешной подготовки
|
||||
один раз вызывает file_io_temp_publish с тем же target и источником.
|
||||
До publish поле temporary непустое; после успешного rename оно пустое даже при
|
||||
ошибке directory sync. По этому переходу задача фиксирует факт своей публикации.
|
||||
Completion всегда может вызвать discard: после rename он ничего не удаляет.
|
||||
Cleanup при отмене/удалении канала учитывает отдельно действительно созданные/
|
||||
заменённые артефакты. Вычисленный путь не является доказательством владения файлом.
|
||||
При отказе до создания выхода нельзя удалять существующий target или его alias.
|
||||
|
||||
Отдельных async owners, refcount и фоновых потоков не требуется. Ключ по-прежнему сохраняется
|
||||
до шифрования; ready получателя/публикация сообщения подтверждаются только после
|
||||
успешного worker и проверки актуальности канала в completion. Авторская строка ключа
|
||||
сохраняется до worker по прежним правилам. Общей транзакции ФС/SQLite нет: после rename
|
||||
путь остаётся известен attachment_keys для последующей очистки/повторной операции.
|
||||
|
||||
Кеш и восстановление после crash
|
||||
|
||||
Активный temp защищён существующим file_io-реестром, в том числе после finish.
|
||||
В chat_msg убрать специальное сохранение вычисленного cipher_path.part; конечный
|
||||
cipher_path по-прежнему защищается attachment_keys, временный файл — своим owner.
|
||||
После crash реестра нет: такие temp допустимо удалить.
|
||||
|
||||
Нельзя ограничиться нынешним orphan walk дерева media: plaintext-temp находится
|
||||
в decrypted, а ak_recover_preparations знает только фиксированный .part.
|
||||
В init/очистке после завершения workers нужно удалять оставшиеся временные файлы
|
||||
строгого собственного формата <UUID>.uenc.tmp.<32hex> / <UUID>.file.tmp.<32hex>
|
||||
в управляемых каталогах media/decrypted. Использовать file_io_remove_orphan,
|
||||
чтобы сохранить активные temp другого instance того же процесса. Конечные файлы,
|
||||
ключи и данные иных форматов такой проход не удаляет; ошибка обхода логируется.
|
||||
|
||||
Область изменений
|
||||
|
||||
Основные файлы: lib/file_io.c/h, media_async/encrypted_file.c/h,
|
||||
chat/chat_attachment.c, chat/chat_msg.c, test_encrypted_file/test_media_primitives
|
||||
и сетевые проверки зашифрованных вложений. Остальные вызовы file_io_temp_open
|
||||
в media_async/media_delivery/video переводятся на явные flags=0.
|
||||
DM не вызывает encrypted_file_write/read: он использует media_crypto streams
|
||||
и общие sync/publish helper-функции из encrypted_file.h. При правке общего слоя
|
||||
необходимо сохранить их durable контракт; перенос общих операций в file_io должен
|
||||
быть прямым, без совместимых обёрток и без изменения DM state machine.
|
||||
Формат контейнера, nonce, протокол ключей и схема БД для этого фикса не меняются.
|
||||
|
||||
Регрессии и критерии готовности
|
||||
|
||||
- Обе операции: одинаковая строка, /./, hardlink, symlink, отсутствующий/необычный source;
|
||||
source и прежний target не меняются, alias не удаляется, чужой temp не освобождается.
|
||||
- Обычные файлы размером 0/1/32767/32768/32769 байт, большой файл, UTF-8 пути;
|
||||
успешная подготовка не меняет target до publish. Read не публикует info/type при ошибке.
|
||||
- Внедрение ошибок open/fdopen/read/write/seek/flush/fsync/input-close/output-close/
|
||||
rename/directory-sync/RNG; отсутствие утечек fd/памяти/регистраций temp.
|
||||
- Подмена target на hardlink между подготовкой и publish: повторная проверка
|
||||
отклоняет публикацию, исходник сохраняется, удаляется только собственный temp.
|
||||
- Отказ directory sync после rename: новый файл цел, temp-owner отсоединён,
|
||||
ошибка не представляется как откат; повторная операция корректна.
|
||||
- Orphan cleanup во время work, после finish, после publish; crash/restart
|
||||
в обоих деревьях, чужие имена сохраняются. Отмена, stop и удаление канала
|
||||
до work, после подготовки и после rename: один done, нет публикации сообщения/ready.
|
||||
- Профильные 20 тестов, ASAN/UBSan/LSan и полный набор. Изменение file_io требует
|
||||
также проверки его прежних пользователей: copy, video и media blocks/download.
|
||||
|
||||
Этот проект реализован. Успешные 20 тестов ниже относятся к исходной реализации;
|
||||
проверки исправленного кода приведены в конце.
|
||||
|
||||
Что проверено и что осталось
|
||||
|
||||
Прочитаны все шесть пар .c/.h media_async, целиком file_transfer.c/h, заголовки
|
||||
media_delivery/download/index/proto, sync.md и doc/encrypted_attachments.md.
|
||||
Полная повторная сверка остальных реализаций media_delivery и исправление контрактов
|
||||
ещё не выполнены. Прежние [V] в общем checklist относятся к предыдущим аудитам;
|
||||
новые незавершённые пункты отмечены [~].
|
||||
|
||||
Linux: ./build.sh --no-gui -j4 — PASS;20/20 профильных тестов — PASS:
|
||||
media_primitives, attachment, attachment_send, encrypted_file, encrypted_attachment,
|
||||
media_async, media_index, media_index_regressions, media_delivery_sql,
|
||||
media_delivery_download, media_download_timeout, media_query, media_blocks,
|
||||
media_replication, media_delivery_integration, media_delivery_full,
|
||||
media_delivery_chat, dm_media, dm_worker, services.
|
||||
Логи: /tmp/media-audit-build.log, /tmp/media-doc-audit-results.log,
|
||||
/tmp/media-doc-audit-logs/. Штатный test_encrypted_file не покрывает alias.
|
||||
Старые сбои encrypted_attachment/media_delivery_chat в этом прогоне не воспроизвелись;
|
||||
один PASS не закрывает задачи о нестабильности.
|
||||
На исходном этапе полный check.sh и новые sanitizer-прогоны не запускались.
|
||||
|
||||
Результаты исправления — 2026-10-24
|
||||
|
||||
- file_io_temp получил единый FILE_IO_DURABLE: sync открытого fd при finish,
|
||||
write-through rename на Windows и directory sync после rename на POSIX.
|
||||
После rename temporary пуст, включая отказ directory sync; discard не удаляет результат.
|
||||
Общие sync/publish функции перенесены в file_io; DM вызывает их напрямую.
|
||||
- Write/read готовят отдельный закрытый temp. Source/target не открываются с усечением;
|
||||
совпадающая строка, /./, hardlink и конечная ссылка отклоняются до создания выхода.
|
||||
Read выдаёт info/type только после успешного закрытия/сохранения проверенного результата.
|
||||
- ak_file хранит один output, учитывает подготовленный plaintext snapshot и выполненный rename.
|
||||
Worker публикует один раз; completion очищает только собственные артефакты.
|
||||
Кеш использует реестр владельцев; recovery очищает строгие temp в media и decrypted.
|
||||
Построение путей и SQL recovery используют одну каноническую media_base.
|
||||
- test_encrypted_file: прежние шесть размеров, восемь alias-сценариев обоих API,
|
||||
активный output, alias перед publish и 23 отказа I/O с контролем fd/temp/метаданных.
|
||||
До rename прежний target сохраняется; после отказа directory sync новый файл установлен.
|
||||
- test_media_primitives: durable и обычный режим, sync failure до rename, failure после
|
||||
rename, относительный/корневой каталог, защита активного temp в worker и после close.
|
||||
- test_attachment_send детерминированно ждёт настоящего crypto rename без обработки
|
||||
completion, затем останавливает сервис: нет сообщения/ключа/оставшихся файлов/temp.
|
||||
- test_encrypted_attachment при каждом startup/restart проверяет оба дерева:
|
||||
stale temp удалены; открытый/закрытый активные owners, конечные файлы и чужие имена сохранены.
|
||||
- Профильный Linux-набор: 21/21 PASS, включая video/no_ffmpeg, DM, services и media delivery.
|
||||
Основной make check: 185/185 PASS. TUN в отдельном network namespace с тем же бинарником:
|
||||
proxy9/9, burst1/1, load8/8 PASS; 134217728 проверенных байт.
|
||||
- ASAN/UBSan/LSan: 21/21 PASS с detect_leaks=1. Все встроенные зависимости собраны
|
||||
с sanitizer-флагами в /tmp/utun-media-fix-sanitizers; обычные объекты проекта не смешивались.
|
||||
Windows/FreeBSD build/runtime не проверялись.
|
||||
|
||||
Логи: /tmp/media-fix-profile-results.log, /tmp/media-fix-full-check.log,
|
||||
/tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log,
|
||||
/tmp/media-fix-cancel-after-rename.log, /tmp/media-fix-encrypted-file.log.
|
||||
Проверка реализации не выявила противоречий владения в перечисленных сценариях.
|
||||
Частичные отметки остальных файлов аудита сохраняются; этот фикс не означает полного
|
||||
завершения проверки media_async/media_delivery или закрытия старых нестабильных тестов.
|
||||
|
||||
Продолжение аудита — 2026-10-24: граница длительности voice_file
|
||||
|
||||
voice_file.h обещает максимум24ч; attachment_validate (attachment.c:35) отвергает
|
||||
duration_ms >86400000. Однако voice_file.c:16 проверяет samples/channels/rate >86400:
|
||||
целочисленное деление отбрасывает долю секунды. Записи с полными кадрами на24ч+20..980мс
|
||||
проходят проверку аргументов. Затем voice_file.c:37 вычисляет точную длительность
|
||||
в миллисекундах по числу20мс кадров, уже выходящую за предел метаданных.
|
||||
attachment_send.c вызывает voice_file_encode в worker, а attachment_validate —
|
||||
при публикации результата. При успешном полном кодировании такой результат будет
|
||||
отклонён после подготовки файла. Это дефект границы записей длиннее суток;
|
||||
сбой обычных коротких записей или потеря данных этим опытом не установлены.
|
||||
|
||||
Диагностическое воспроизведение на настоящем voice_file_encode из src/libutun.a:
|
||||
- /tmp/media-voice-duration-repro.c и .mk; сборка через tests/Makefile и его библиотеки/флаги.
|
||||
- Linux64: виртуальный read-only PCM достаточного размера, без чтения или заполнения.
|
||||
Linker wrapper opus_codec_encoder_create фиксирует достижение кодировщика
|
||||
и возвращает NULL до чтения PCM/открытия output. Реальная проверка аргументов не подменена.
|
||||
- Частоты8000/12000/16000/24000/48000, mono/stereo; для каждой пары проверены
|
||||
24ч-20мс, ровно24ч, +19мс хвоста, +20мс, +980мс и +1000мс: всего60 случаев.
|
||||
- Ровно24ч и +19мс хвоста проходят, так как хвост короче полного кадра отбрасывается;
|
||||
+1000мс отвергается до encoder. На +20/+980мс во всех10 сочетаниях codec достигнут,
|
||||
тогда как attachment_validate соответствующей длительности возвращает-1:
|
||||
duration_guard_mismatches=20, confirmed=1.
|
||||
- Все60 вызовов возвращают-1: для допустивших encoder это намеренная инъекция отказа,
|
||||
для +1000мс — штатная валидация. Опыт доказывает ошибку допуска к encoder,
|
||||
но не является24-часовым кодированием или сквозной проверкой отправки такого файла.
|
||||
- Полный журнал с media=info/debug=info: /tmp/media-voice-duration-repro.log;
|
||||
сборка: /tmp/media-voice-duration-build.log. Проектные .c/.h не менялись.
|
||||
|
||||
Проект простого исправления:
|
||||
1. Задать один ATTACHMENT_DURATION_MAX_MS=86400000 в attachment.h; использовать
|
||||
в валидаторе voice/video и проверке video completion вместо отдельных литералов.
|
||||
2. После проверки rate/channels считать frame_samples=(rate/50)*channels,
|
||||
frames=count/frame_samples. До создания encoder/открытия файла отклонять
|
||||
frames<25 либо frames>ATTACHMENT_DURATION_MAX_MS/20 (4320000).
|
||||
Проверяется длительность записываемых полных кадров, хвост <20мс по-прежнему отбрасывается.
|
||||
3. Только после ограничения frames считать samples=frames*frame_samples
|
||||
и duration_ms=frames*20. Размер samples не превосходит count, длительность
|
||||
помещается в uint32_t; большие count не требуют потенциально переполняющего умножения на20.
|
||||
4. Сообщение раннего отказа различает неверные параметры и недопустимую длительность;
|
||||
для длительности выводит count/rate/channels/frames и разрешённые границы.
|
||||
Дополнительный helper/API или изменение pipeline/формата не нужны.
|
||||
5. В test_attachment добавить проверку границ500мс/24ч, хвоста <20мс,
|
||||
превышения на полный кадр, разных rate/channels и раннего отказа без изменения файла/метаданных.
|
||||
Верхние границы проверять диагностической точкой до encoder без реального кодирования суток;
|
||||
сохранить существующий настоящий Opus roundtrip короткой записи.
|
||||
|
||||
На этапе подтверждения аудит был остановлен по AGENTS.md:10, без изменения рабочего кода.
|
||||
Пользователь согласовал исправление; результаты приведены ниже.
|
||||
|
||||
Результаты исправления voice_file — 2026-10-24
|
||||
|
||||
- ATTACHMENT_DURATION_MAX_MS в attachment.h задаёт один предел86400000мс:
|
||||
его используют валидатор voice/video, video completion и кодировщик голоса.
|
||||
- voice_file считает frames=count/frame и проверяет25..4320000 до создания Opus
|
||||
и открытия выходного файла. Метаданные при раннем отказе не изменяются.
|
||||
Хвост <20мс сохраняет прежнее поведение: отбрасывается, включая хвост после ровно24ч.
|
||||
- Проверка архитектуры: frame всегда положителен и не больше1920 для разрешённых rate/channels;
|
||||
деление count/frame не переполняется. После ограничения frames произведение frames*frame
|
||||
не превосходит исходного count; frames*20 не превосходит86400000 и помещается в uint32_t.
|
||||
Подготовка, публикация, владение PCM/файлом и формат контейнера не меняются.
|
||||
- Лог раннего отказа содержит count/rate/channels/frames и разрешённые границы;
|
||||
debug принятой записи показывает число кадров, длительность и отброшенный хвост.
|
||||
- test_attachment: 120 граничных вызовов для пяти частот и mono/stereo.
|
||||
Все70 недопустимых длительностей отвергнуты до encoder; все50 допустимых
|
||||
дошли до диагностического encoder с инъекцией отказа. Во всех120 случаях
|
||||
побайтно сохранены исходные metadata и содержимое существующего output.
|
||||
Linux64 использует виртуальный read-only PCM; суточное кодирование не выполняется.
|
||||
- Настоящий Opus encode/decode: прежняя1с запись и десять записей на500мс с хвостом
|
||||
почти20мс, все rate/channels, все три preset. Проверены header, декодирование,
|
||||
число20мс кадров, EOF и валидность метаданных результата.
|
||||
- validate/encode/decode voice/video принимают86400000мс и отвергают86400001мс.
|
||||
- ./build.sh --no-gui -j4 — PASS. В изменённых C-файлах/тесте предупреждений сборки нет;
|
||||
общие прежние предупреждения остальных модулей остаются в полном логе.
|
||||
- Профильные тесты10/10 PASS: attachment, attachment_send, encrypted_file,
|
||||
encrypted_attachment, media_async, dm_media, dm_worker, services, video, video_no_ffmpeg.
|
||||
Первый запуск attachment внутри sandbox выдал запреты создания stream fd;
|
||||
весь профильный набор, включая attachment, успешно повторён вне sandbox.
|
||||
|
||||
Логи: /tmp/media-voice-fix-build.log, /tmp/media-voice-fix-profile-results.log,
|
||||
/tmp/media-voice-fix-profile-logs/test_attachment.log и остальные логи этого каталога.
|
||||
Новый полный make check и ASAN/UBSan/LSan не запускались; прежние результаты encrypted_file
|
||||
выше не относятся к этому изменению. Windows/FreeBSD не проверялись.
|
||||
voice_file.h и attachment.h сохраняют [~]: завершение этого фикса не закрывает
|
||||
оставшуюся сверку контрактов всего аудита. Повторная проверка media_delivery ещё не завершена.
|
||||
|
||||
Продолжение аудита — 2026-10-24: переанонс записей SQLite
|
||||
|
||||
Полностью прочитаны media_index.c/h, media_download.c/h, media_delivery.c/h,
|
||||
file_transfer.c/h, media_delivery_proto.h и sync.md. Повторно сверены media_crypto.c/h.
|
||||
В media_crypto.h уточнены native байты nonce, границы CCM/буферов, пустой поток,
|
||||
момент выдачи размеров/хеша и отсутствие close/fsync/rename; неверный аргумент не обнуляет выходы.
|
||||
В file_transfer.h уточнены lookup/авторизация, 16 операций, размер1..INT64_MAX,
|
||||
усечение своего временного назначения даже при отказе запуска, таймаут и отмена/владение callback.
|
||||
Изменения только описаний сохранены коммитом5dbca4a2.
|
||||
|
||||
Подтверждённый дефект
|
||||
|
||||
media_delivery_announce_local_blocks (media_delivery.c:1610) читает media_id/block_id
|
||||
через sqlite3_column_blob, но проверяет только ненулевой указатель. Затем
|
||||
media_delivery_send_have_block копирует по16 байт в пакет и подписывает объявление.
|
||||
media_files имеет BLOB NOT NULL без CHECK длины: такие строки допустимы текущей схемой.
|
||||
Обычные media_index writers пишут правильные длины; опыт моделирует повреждённые
|
||||
локальные данные, а не доказанный путь внесения повреждения удалённым участником.
|
||||
|
||||
Настоящие SQLite, схема, обработчик, выбор группы/суперузла и Ed25519; перехвачена
|
||||
только транспортная отправка в существующем tests/media_delivery_fixture.h:
|
||||
- Контрольные UUID16/16, chunk0, chat_id111: один корректный HAVE_BLOCK.
|
||||
- media_id длиной1 байт: один HAVE_BLOCK с16-байтным media_id, из них15 байт вне BLOB.
|
||||
В журнале вместо ожидаемого единственного7a видны посторонние ненулевые байты памяти.
|
||||
- block_id длиной1 байт: аналогичное чтение15 лишних байт в сетевой block_id.
|
||||
- media_id или block_id длиной17 байт: отправка с молчаливым усечением до16.
|
||||
- chunk=-1: wire_chunk=4294967295; chunk=4294967296: wire_chunk=0.
|
||||
Причина — sqlite3_column_int с последующим приведением к uint32_t без диапазона.
|
||||
- chat_id111junk: strtoull без endptr принимает префикс111 и направляет объявление в группу111.
|
||||
- Итого8 случаев,7 повреждённых строк из7 отправлены; invalid_records_sent=7, confirmed=1.
|
||||
Ошибки соответствующих полей в модуле не логируются. Пакеты сохранены только стендом,
|
||||
реальные удалённые узлы в опыте не участвовали. Ошибка ASAN или падение не воспроизводились.
|
||||
|
||||
Артефакты: /tmp/media-announcement-audit.c, /tmp/media-announcement-audit.mk,
|
||||
/tmp/media-announcement-audit-build.log, /tmp/media-announcement-audit.log.
|
||||
Сборка через tests/Makefile с настоящими src/libutun.a/libuasync.a и штатными флагами;
|
||||
проектные .c для диагностики не менялись. Очистка собственного стенда исправлена и повторно проверена.
|
||||
Отдельно по коду видно: результаты bind/завершающего step/finalize игнорируются;
|
||||
SQL-ошибку окончания обхода нельзя выдавать как обычное «done». Инъекция SQL-ошибок здесь не выполнялась.
|
||||
|
||||
Проект простого исправления
|
||||
|
||||
1. Проверять строку на границе чтения SQLite до доступа как к фиксированным структурам:
|
||||
media_id/block_id имеют тип BLOB и ровно16 байт; проверить полученные указатели.
|
||||
Скопировать прошедшие проверку UUID в локальные массивы до вложенных SQL/отправок.
|
||||
2. chunk читать как int64: тип INTEGER, 0..UINT32_MAX, только затем cast.
|
||||
chat_id — TEXT, длина1..20 без embedded NUL, каноническое ненулевое uint64 decimal.
|
||||
Полный разбор должен отвергать суффиксы, знаки, пробелы, leading zero и переполнение;
|
||||
строка канала должна сохранять свой смысл при переводе в router group_id.
|
||||
3. Плохую строку пропускать с WARN, указав rowid/типы/длины/некорректное поле,
|
||||
без чтения за BLOB и без анонса. Корректные соседние строки продолжать обрабатывать.
|
||||
Никакого общего выделяемого снимка или отката уже отправленных корректных анонсов не требуется.
|
||||
4. Проверять bind, terminal step и finalize; SQL-ошибки логировать с фазой/кодами
|
||||
и количеством обработанных строк. Не превращать их в обычный успешный финальный лог.
|
||||
Сохранить существующий void API, поштучные анонсы и wire-формат.
|
||||
5. Регрессии в штатной media-цели: valid row, длины UUID0/1/15/16/17 и неверные SQL-типы,
|
||||
chunk=-1/0/UINT32_MAX/UINT32_MAX+1, неверные/граничные chat_id, embedded NUL;
|
||||
смесь корректных/плохих записей и ошибки SQL. Перехват HAVE_BLOCK обязан доказать,
|
||||
что наружу ушли только исходные16 байт проверенных UUID и правильный номер группы/блока.
|
||||
|
||||
Аудит остановлен по AGENTS.md:10 после подтверждения дефекта и подготовки проекта исправления.
|
||||
Новые полные build/check/sanitizer-прогоны не запускались: изменения в репозитории только документальные.
|
||||
media_crypto.h и file_transfer.h можно отметить [V]; media_delivery.h/proto.h остаются [~]:
|
||||
нужно исправление этого пути и завершение сверки описаний. Остальные прежние отметки не закрывают
|
||||
весь аудит. Остались контракты attachment/attachment_send/voice, связанные encrypted/lifecycle документы
|
||||
и устаревший _desc.txt (RTT-подбор, direct-only репликация и её интервалы расходятся с текущим кодом).
|
||||
|
||||
Результаты исправления SQLite-переанонса — 2026-10-24
|
||||
|
||||
- Проверка остаётся внутри существующего обхода media_files. UUID допускаются только
|
||||
как BLOB ровно16 байт; chunk — INTEGER в0..UINT32_MAX, читается через int64.
|
||||
Исходные SQL-типы получены до преобразований. Проверенные UUID копируются
|
||||
в локальные16-байтные массивы до вложенных запросов/отправок.
|
||||
- chat_id имеет тип TEXT, длину1..20; разбор по объявленной длине отвергает embedded NUL,
|
||||
знаки/пробелы/суффиксы, ноль/leading zero и переполнение. Допускается весь ненулевой uint64,
|
||||
включая UINT64_MAX. Условие gid<=(UINT64_MAX-digit)/10 до операции исключает переполнение.
|
||||
- Непроверенные фиксированные memcpy и сужающие cast устранены. Некорректная строка
|
||||
пропускается с WARN: id/поле, типы/длины, исходный chunk. Хорошие соседние строки продолжают обрабатываться.
|
||||
- prepare/bind/завершающий step/finalize проверяются. Отказ SQL логирует фазу/коды
|
||||
и неполный обход; обычный complete-лог не выдаётся. Счётчики rows/eligible/skipped
|
||||
описывают обработку, а не гарантированную доставку. Уже корректно отправленные пакеты не откатываются.
|
||||
- Архитектура сохраняет существующие void API, wire-формат, источник ролей и единичные анонсы;
|
||||
нет общего снимка/новых ресурсов, транзакций или состояния. Statement закрывается и при ошибке bind/step.
|
||||
- test_media_delivery_sql: 36 случаев полей,32 недопустимых строки не отправляются,
|
||||
четыре допустимые сохраняют точные UUID/group/chunk/timestamp. Проверены границы
|
||||
UINT32_MAX/UINT64_MAX, все заявленные длины UUID и неверные SQL-типы, embedded NUL.
|
||||
В смеси valid/invalid/valid отправлены только два корректных анонса.
|
||||
- Linux: пять внедрений prepare/bind/first-step/step после первой строки/finalize.
|
||||
До отказа сохранены0/0/0/1/2 корректных отправки, statement не остаётся активным;
|
||||
проверяется ERROR с фазой и отсутствие ложного complete. После каждого отказа
|
||||
обычный повтор успешно отправляет обе строки с исходными UUID и номерами блоков.
|
||||
После захвата файлового лога тест явно восстанавливает console_enabled.
|
||||
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
|
||||
Профиль12/12 PASS: delivery_sql, query, blocks, replication, index/index_regressions,
|
||||
delivery_download, download_timeout, delivery_integration/full/chat, services.
|
||||
После изменения только вывода диагностики SQL-тест повторно прошёл отдельно.
|
||||
- ASAN/UBSan/LSan4/4 PASS: delivery_sql/query/blocks/replication, detect_leaks=1.
|
||||
Свежая копия /tmp/utun-media-announcement-sanitizers содержит текущий код;
|
||||
CPPFLAGS=-fsanitize=address,undefined применён и к встроенным зависимостям.
|
||||
Обычные объекты проекта не смешивались с sanitizer-объектами.
|
||||
Первый configure внутри sandbox отклонён LeakSanitizer из-за ptrace;
|
||||
сборка и тесты успешно выполнены вне sandbox.
|
||||
|
||||
Логи: /tmp/media-announcement-fix-build.log, /tmp/media-announcement-fix-profile-results.log,
|
||||
/tmp/media-announcement-fix-sql-final.log, /tmp/media-announcement-sanitizer-build.log,
|
||||
/tmp/media-announcement-sanitizer-tests-build.log, /tmp/media-announcement-sanitizer-results.log.
|
||||
Полный make check и Windows/FreeBSD не запускались. Завершение фикса не означает завершения
|
||||
сверки оставшихся заголовков и документов двух каталогов.
|
||||
|
||||
Продолжение сверки описаний — 2026-10-24: MIME контейнера
|
||||
|
||||
Сверены и уточнены attachment.h, attachment_send.h, voice_file.h, media_delivery_proto.h
|
||||
и комментарий неиспользуемого HAVE_BLOCK timeout в media_delivery.h:
|
||||
- attachment: UTF-8 и только ASCII C0/DEL, границы полей по kind, состояние выходов
|
||||
при ошибке encode/decode, content_type не является валидатором kind.
|
||||
- voice_file: собственное перезаписываемое назначение, поздняя ошибка close может
|
||||
изменить info; native контейнер и отсутствие fsync/атомарной публикации.
|
||||
- attachment_send: владение начинается при вызове trampoline, при отказе самого post
|
||||
остаётся у caller; PCM/compressor, контекст освобождения, DM generation, канальные
|
||||
request_id/reply, внутренний UUID подготовки и события подготовки вместо обещания доставки.
|
||||
- proto: вне SYNC числа native-endian; SERVE_REG/LEAVE только вспомогательный учёт,
|
||||
загрузчик их не посылает. SERVE_ACK не отправляется; приём SERVE_ACK/HAVE_BLOCK_ACK игнорируется,
|
||||
ожидания ACK/его таймера нет. Не менялись команды, wire или runtime-поведение.
|
||||
Коммиты описаний:08692fa4 и4e4c60ee. Из общего service_lifecycle повторно сверена
|
||||
только media-секция111..196 по текущему порядку start/quiesce/destroy и прочитанным модулям;
|
||||
за пределами media документ не считается полностью проверенным.
|
||||
|
||||
Подтверждённый дефект MIME
|
||||
|
||||
encrypted_file_write проверяет только ненулевой type и strlen(type)<32, затем шифрует строку.
|
||||
encrypted_file_read требует непустую NUL-терминированную строку в32-байтном поле,
|
||||
ASCII33..126 без '"' и '\\'. Поэтому write может успешно подготовить непригодный для read контейнер.
|
||||
chat_attachment_send (chat_attachment.c:198..225) имеет такую же слабую входную проверку:
|
||||
до сохранения ключа/старта worker проверяется лишь наличие указателя и длина.
|
||||
|
||||
Реальный roundtrip, без подмены криптографии или файловых функций:
|
||||
- Контроль application/octet-stream: write=0/read=0, hash расшифрованного temp равен исходному.
|
||||
- Пустой MIME, перевод строки, двойная кавычка, обратная косая черта и не-ASCII UTF-8:
|
||||
все пять write=0, после publish и вычисления точного внешнего hash соответствующий read=-1.
|
||||
Ключ, author, UUID и контейнер при чтении те же. Ошибка — invalid metadata/type,
|
||||
а не аутентификация или порча контейнера в опыте.
|
||||
- При ошибке read прежние info/type сохранены, temp очищен; SHA256 исходника неизменен во всех6 случаях.
|
||||
Это подтверждённый дефект принятия аргумента в API. Сквозная отправка/открытие карточки GUI не моделировалась.
|
||||
- /tmp/media-content-type-audit.c и .mk, сборка через tests/Makefile и настоящие библиотеки;
|
||||
/tmp/media-content-type-audit-build.log, /tmp/media-content-type-audit.log.
|
||||
Артефакты: /tmp/media-content-type-audit-FdtMC3/. Лог содержит MIME как hex, без управляющих символов в тексте.
|
||||
|
||||
Проект простого исправления
|
||||
|
||||
1. Экспортировать encrypted_file_validate_type в encrypted_file.h: 0/-1, bounded strnlen(type,32),
|
||||
непустая строка длиной1..31, ASCII33..126 кроме двойной кавычки и обратной косой черты.
|
||||
Это ограничение существующего контейнера, не новый полноценный MIME parser и не правило для всех сообщений чата.
|
||||
Диагностика показывает длину/позицию/hex ошибочного байта, не выводит сырую непроверенную строку.
|
||||
2. Вызывать один валидатор в chat_attachment_send до выделения задачи/генерации ключа/записи в БД,
|
||||
в encrypted_file_write до открытия source/temp и в read для расшифрованного32-байтного поля.
|
||||
Существующие проверки длины расшифрованного блока сохраняются; правило MIME не дублируется в трёх местах.
|
||||
3. Не менять формат, nonce, crypto, DB state machine или файловое владение. Reader остаётся строгим;
|
||||
writer не должен выдавать успех для метаданных, которые этот же reader не принимает.
|
||||
4. Регрессии: NULL/пустая строка/31/32 байта, каждый запрещённый символ, ASCII-границы
|
||||
и валидные значения; writer на раннем отказе сохраняет source/target/output.
|
||||
Для reader плохие metadata шифруются тестом с известным ключом и пересчитанным внешним hash:
|
||||
отказ не теряет прежние info/type и не оставляет temp. Проверить send-отказ без строки ключа/worker/файлов.
|
||||
|
||||
На этапе воспроизведения аудит был остановлен по AGENTS.md:10. Затем пользователь разрешил продолжать исправления.
|
||||
|
||||
Реализация и проверка MIME-фикса — 2026-10-24
|
||||
|
||||
- encrypted_file_validate_type использует bounded strnlen(type,32) и одно правило для send/write/read.
|
||||
NULL, пустая/нетерминированная строка и недопустимый байт отклоняются с WARN: длина/позиция/hex.
|
||||
Успешная проверка имеет DEBUG-лог; сырые непроверенные строки в лог не попадают.
|
||||
- send проверяет type до выделения задачи, генерации/сохранения ключа и старта worker.
|
||||
write отказывает до файловых операций и изменения output. read проверяет уже аутентифицированное
|
||||
поле после проверки длины metadata и сохраняет атомарное присваивание info/type только при успехе.
|
||||
Валидатор не выполняет I/O и разрешён в uasync; файловые write/read остаются worker-only.
|
||||
- Правило сохраняет действующий набор reader, включая короткое значение "a"; это не полный MIME parser.
|
||||
Не добавлены новый wire, изменение nonce/crypto, состояние БД или второй владелец файлов.
|
||||
Код:90964e7a/f4cc6920, регрессии:aace975a.
|
||||
- test_encrypted_file: все256 значений первого байта,92 допустимых; NULL отдельно.
|
||||
Восемь ранних отказов сохраняют output побайтно, source/cipher/target по SHA256.
|
||||
Три настоящих roundtrip проверяют длины1/24/31, metadata, SHA256 plaintext и сохранность target до publish.
|
||||
Семь плохих wire-полей зашифрованы настоящим CCM с пересчитанным внешним SHA256:
|
||||
read отказывает, сохраняя info/type/старый target/source; temp и fd не остаются.
|
||||
- test_attachment_send: восемь отказов до ключей/worker, число файлов и исходный байт сохранены.
|
||||
Сохранён прежний сценарий реального encrypted worker, отмены после публикации и recovery.
|
||||
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
|
||||
Профиль10/10 PASS: encrypted_file, attachment_send, encrypted_attachment, attachment, media_async,
|
||||
services, media_delivery_chat/full, dm_media, dm_worker.
|
||||
- ASAN/UBSan/LSan4/4 PASS: encrypted_file, attachment_send, encrypted_attachment, services.
|
||||
detect_leaks=1, halt_on_error=1; встроенные зависимости также инструментированы через CPPFLAGS.
|
||||
Отдельная копия /tmp/utun-media-mime-sanitizers исключает смешение обычных/sanitizer-объектов.
|
||||
Первый build остановился из-за нехватки /tmp; очищены воспроизводимые объекты прежних копий,
|
||||
затем повторная сборка и весь прогон завершились успешно. Исходники и журналы сохранены.
|
||||
|
||||
Логи: /tmp/media-mime-fix-build.log, /tmp/media-mime-fix-profile-results.log,
|
||||
/tmp/media-mime-fix-profile-logs/, /tmp/media-mime-sanitizer-build-retry.log,
|
||||
/tmp/media-mime-sanitizer-tests-build.log, /tmp/media-mime-sanitizer-results.log,
|
||||
/tmp/media-mime-sanitizer-test_*.log. Полный check и Windows/FreeBSD для MIME-фикса не запускались.
|
||||
encrypted_file.h теперь согласован с реализацией. Последующая сверка общих описаний завершена ниже.
|
||||
|
||||
Завершение аудита media_async/media_delivery — 2026-10-24
|
||||
|
||||
- Все11 публичных заголовков двух каталогов сверены по полным функциям и структурам реализаций:
|
||||
media_async/crypto, attachment/send, encrypted_file, voice_file, media_index/download/delivery/proto,
|
||||
file_transfer. Уточнены назначения, входы/выходы, владение и потоки, ошибки и wire-ограничения.
|
||||
- _desc.txt заменён актуальным описанием подготовки, поиска/загрузки, relay, каталога и репликации.
|
||||
Убраны прежние предположения о RTT-выборе, только прямых соединениях, часовых повторах и ACK-окне.
|
||||
Отражены фактические QUERY/CM/transfer UUID, пределы выдач, per-group RX-курсор и атомарный приём страницы.
|
||||
sync.md уточняет отсутствие ожидания HAVE ACK и проверки SQLite при локальном переанонсе.
|
||||
- encrypted_attachments.md целиком сопоставлен с chat_attachment.c и источниками desktop/Android UI:
|
||||
inputbar, MessageList/attachment_key_ui, JNI snapshot, ChatRepository, MessageBubble/Reply/ChannelList.
|
||||
Уточнены MIME, видимость desktop-кнопки при наличии pending requests и preview цитаты после расшифровки.
|
||||
Контейнер не выдаётся UI как готовый файл; путь/метаданные заменяются после готовности plaintext.
|
||||
Это сверка исходников, без нового интерактивного GUI/Android runtime-прогона.
|
||||
- media-раздел service_lifecycle.md сверён по текущему start/quiesce/join/done/destroy и владельцам
|
||||
worker/temp/CM/relay. Дописано, что ранний MIME-валидатор допустим в uasync и не выполняет I/O.
|
||||
Общий документ сохраняет частичную отметку: голос, звонки и другие немедийные владельцы вне этой проверки.
|
||||
- Архитектура исправлений остаётся простой: формат/валидация контейнера принадлежат encrypted_file,
|
||||
temp/durability — file_io, публикация и БД/UI — задаче chat_attachment; один владелец на ресурс.
|
||||
Media_index регистрирует подготовленное, delivery проверяет передачу и ведёт каталог своей группы.
|
||||
Синхронные приём/проверка/сборка delivery в uasync и отсутствие общей транзакции ФС/SQLite
|
||||
явно описаны как действующие границы. File_transfer оставляет ключи/авторизацию/проверку caller.
|
||||
- Все четыре воспроизведённых дефекта исправлены и имеют регрессии. Неразобранных подтверждённых
|
||||
дефектов в рамках этого аудита не осталось. Последние изменения после MIME-прогона — только описания;
|
||||
git diff --check проходит. Для MIME действуют профиль10/10 и ASAN/UBSan/LSan4/4 выше;
|
||||
прежние полные185/186 и sanitizer21/21 не приписываются последующим исправлениям.
|
||||
|
||||
Коммиты общей сверки:91fed902 (delivery),8d175049 (encrypted/UI/lifecycle).
|
||||
Чек-лист: все media-заголовки и encrypted_attachments.md — [V], общий service_lifecycle.md — [~].
|
||||
Windows/FreeBSD, новый полный check и интерактивные GUI/Android проверки не выполнялись.
|
||||
@@ -107,3 +107,40 @@ IP совпали — трансляция IP не обнаружена; отл
|
||||
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
|
||||
Через SOCKS внешний IP прокси
|
||||
не записывается как собственный адрес.
|
||||
|
||||
Управляющий обмен и ресурсы
|
||||
--------------------------
|
||||
|
||||
Команды идут через сервис NAT_DETECTION ETCP-router с подписью и шифрованием.
|
||||
Используются узлы группы с рабочим маршрутом через READY-соседа; это не требует
|
||||
прямого UDP-соединения A с C. CHAT-допуск требует полной неудалённой записи участника.
|
||||
Источник и группа берутся из проверенного заголовка router, а не из next hop.
|
||||
|
||||
Версия управляющего формата — 4. Основное сообщение занимает 117 байт:
|
||||
10 однобайтовых полей команды/сокетов/этапа/результата, шесть uint64
|
||||
(owner, generation, round, token, observer, helper), uint16 timeout_ms
|
||||
и три адреса по 19 байт (target, primary, observer_address).
|
||||
RECHECK занимает 23 байта: cmd, command, version, socket_id и адрес 19 байт.
|
||||
Многобайтовые числа кодируются вручную в big-endian; адреса — route_ping_addr_encode.
|
||||
Это размеры сообщения после служебного заголовка router. Приём требует точного
|
||||
размера; старые generation/round/token и чужие делегирования результата отклоняются.
|
||||
|
||||
Детектор работает с шагом 100 мс. Помощник отправляет до трёх проб на этап;
|
||||
таймаут одной пробы — не меньше секунды и не меньше четырёх RTT наблюдателя.
|
||||
Общий бюджет обхода — 30 секунд; незавершённый обход может продолжиться после паузы.
|
||||
Полный результат перепроверяется через 30 минут, неполный — через 30 секунд.
|
||||
Повтор по неудаче подключения ограничен одним запросом за пять минут на endpoint.
|
||||
Сохранённый успешный ETCP-профиль не истекает по таймеру; изменение endpoint его сбрасывает.
|
||||
|
||||
История исходящих хранит до 256 IP для каждого локального endpoint (IP:порт).
|
||||
Она сохраняется в SQLite. Переполнение или ошибка загрузки/записи помечают историю
|
||||
неполной: UNKNOWN сохраняется до достоверного измерения, OPEN по незнакомому входу
|
||||
с неполной историей не подтверждается. Кеш наблюдений ограничен 128 записями,
|
||||
одновременно выполняется не более 64 задач помощи.
|
||||
|
||||
Детектор владеет таймерами, подписками и изолированными probe_only UDP-сокетами.
|
||||
Эти сокеты не публикуются как рабочие адреса. Их отдельное удаление снимает ссылку
|
||||
в состоянии родителя, отменяет зависимые пробы и допускает обычный последующий teardown.
|
||||
Ошибка регистрации любой подписки или начального таймера отменяет весь запуск.
|
||||
Ядро сохраняет контекст в instance->nat_det; останавливает его до ETCP/router
|
||||
и обнуляет поле после destroy. Уничтожать ядро из callback детектора нельзя.
|
||||
+24
-2
@@ -6,6 +6,14 @@
|
||||
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
|
||||
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
|
||||
|
||||
В NODEINFO секции подсетей присутствуют только при `TOPO_FLAG_SEND_SUBNETS`.
|
||||
Без флага оба счётчика подсетей равны нулю; противоречащий этому заголовок отклоняется.
|
||||
Parser проверяет точную длину пакета и проходит все присутствующие секции независимо
|
||||
от запрошенных `out_*`: например, `out_subnets=NULL` отключает создание объектов подсетей,
|
||||
но сохраняет проверку префиксов и правильное положение следующего hop-list.
|
||||
Выходы выдаются только после полного успеха; при ошибке они обнулены, частичные объекты
|
||||
освобождены. Подпись и допустимость пути проверяются после структурного разбора.
|
||||
|
||||
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
|
||||
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
|
||||
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
|
||||
@@ -13,6 +21,12 @@
|
||||
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
|
||||
только timestamp.
|
||||
|
||||
Локальный узел группы готовится до удаления прежнего `local_node`: сравниваются сами адреса
|
||||
и префиксы подсетей, включая замену при прежнем количестве. При отказе памяти, подписи или
|
||||
конфликте IPv4-маршрутов прежние узел и маршруты сохраняются. Подсети CHAT не публикуются.
|
||||
`topo_group_update_my_nodeinfo` возвращает число IPv4-подсетей, для CHAT — 0, при ошибке — -1;
|
||||
рассылку подготовленного NODEINFO выполняет вызывающий.
|
||||
|
||||
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
|
||||
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
|
||||
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
|
||||
@@ -50,6 +64,8 @@ snapshot загрузка использует его. Локальная пуб
|
||||
Уникальность определяется endpoint и видом адреса,
|
||||
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
|
||||
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
|
||||
Такая загрузка требует хотя бы одного адреса; отказ SQL/памяти освобождает все частичные списки
|
||||
и возвращает NULL. Наличие валидного snapshot с пустым списком адресов, напротив, допустимо.
|
||||
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
|
||||
старые профили сбрасываются, сериализованные snapshots инвалидируются.
|
||||
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
|
||||
@@ -62,6 +78,12 @@ Timestamp 0 обозначает bootstrap-сведения без подпис
|
||||
|
||||
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
|
||||
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
|
||||
Путь подготавливается до изменения общего реестра и групповых подсетей: отказ
|
||||
памяти сохраняет прежнюю запись, ссылки и пути. После commit IPv4-таблица
|
||||
перестраивается для этого узла, включая удаление старых подсетей при пустом
|
||||
новом наборе. Конфликт подсетей или OOM установки маршрутов логируется,
|
||||
новый набор не устанавливается, прежние маршруты удаляются; ошибка запрещает
|
||||
READY сессии. Принятая подписанная запись и пути при этом могут сохраниться.
|
||||
Линки дополняются как при UP, так и до установления соединения; повторная
|
||||
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
|
||||
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
|
||||
@@ -78,8 +100,8 @@ Bootstrap-адреса не перекрывают подписанную зап
|
||||
удерживает один handle на клиента, независимо от количества его адресов.
|
||||
|
||||
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
|
||||
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
|
||||
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
|
||||
Само событие транспортного UP не включает соединение в UTUN-группу автоматически.
|
||||
Входящий JOIN остаётся явным запросом
|
||||
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
|
||||
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
|
||||
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Proxy: исправления и проверка архитектуры
|
||||
Дата: 2026-10-24. Статус: реализовано и проверено на Linux.
|
||||
Ниже сохранён согласованный проект; фактические изменения и результаты — в конце.
|
||||
|
||||
Подтверждённые факты до исправлений
|
||||
|
||||
1. udp_proxy.c не проверяет bind/sendto, результат socket_set_nonblocking,
|
||||
часть выделений/регистраций/таймеров и отправку ответа через router.
|
||||
Неудачный запуск flow может оставлять ресурс без рабочего таймера очистки.
|
||||
2. server_init и client_create оба запускают общие UDP/ICMP-контексты.
|
||||
client_destroy сохраняет их по server.enabled, server_destroy уничтожает всегда.
|
||||
Простой rollback через destroy одного модуля не выражает владение общими ресурсами.
|
||||
3. ICMP init публикует контекст и сообщает успех без raw socket. При этом тот же
|
||||
контекст нужен для клиентских REPLY: отсутствие возможности exit не означает,
|
||||
что клиентская сторона ICMP не может работать через другой exit.
|
||||
4. В диагностическом прогоне tcp_io error_cb при HUP отменяет read_waiter,
|
||||
включает чтение и напрямую вызывает read_cb вопреки read_paused.
|
||||
Для fd=15 порог чтения 4; журнал фиксирует read_paused=1 и рост rq до 53.
|
||||
Это нарушение ограничения очереди, а не только избыточная диагностика.
|
||||
5. EPIPE/ENOTCONN в HTTPS CONNECT воспроизведены. Системная трасса четырёх TLS-потоков:
|
||||
recv(destination)=0 -> поздняя TLS-запись 24 байта от клиента -> send(destination)=24
|
||||
-> SO_ERROR=EPIPE либо shutdown(SHUT_WR)=ENOTCONN. Повторный shutdown не обнаружен.
|
||||
Определение содержимого зашифрованной TLS-записи по трассе невозможно.
|
||||
Сам по себе этот сценарий не доказывает дефект half-close в proxy.
|
||||
6. После удаления exit-потока приходят поздние WINDOW: диспетчер пишет unhandled,
|
||||
хотя это известная команда уже завершённого потока. Создавать поток по WINDOW нельзя.
|
||||
7. enabled=yes в серверных тестовых конфигах не поддержан парсером. Сейчас сервер
|
||||
включается при обработке любого параметра секции, включая неизвестный.
|
||||
|
||||
Предлагаемая архитектура
|
||||
|
||||
Владелец общих UDP/ICMP-контекстов — UTUN_INSTANCE. Их жизненный цикл координируется
|
||||
в utun_instance.c после router_init и до router_destroy. TCP client/server управляют
|
||||
только своими listeners, TCP-потоками и bindings. Общих счётчиков ссылок не требуется:
|
||||
при существующем lifecycle они живут до остановки instance.
|
||||
|
||||
Контексты нужны при включённом exit или клиентском TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
server.enabled обозначает действующий TCP exit. Роль при запуске ICMP exit должна
|
||||
передаваться явно, а не определяться случайным порядком вызовов init.
|
||||
При отказе создания клиента rollback освобождает только его ресурсы и собственный binding.
|
||||
Повторное создание клиента/сервера поверх активного состояния отклоняется.
|
||||
Результат client_create публикуется в inst->tcp_proxy_client только после успеха.
|
||||
|
||||
Порядок остановки: клиент и exit-потоки -> общие UDP/ICMP -> router -> drain deferred free.
|
||||
При destroy всего instance остановку proxy перенести перед остановкой UTUN-группы,
|
||||
разборкой topo_groups и ETCP-сокетов/соединений. Сейчас proxy уничтожается в phase M,
|
||||
после topo_groups и транспорта: для нового rollback нельзя сохранять этот поздний порядок.
|
||||
Общий instance_init_common уже вызывает instance_destroy при отказе создания;
|
||||
нужно использовать этот путь, не добавляя параллельную систему частичной очистки.
|
||||
При create_from_config сохранить существующее правило: отказ возвращает config вызывающему коду.
|
||||
Ресурсы закрываются в одном uasync-потоке; stop/destroy не вызывается из активного
|
||||
callback уничтожаемого владельца. Таймеры/waiters отменяются до освобождения памяти.
|
||||
|
||||
Инициализация и ICMP
|
||||
|
||||
Разделить создание общего ICMP-контекста/регистрацию REPLY и запуск raw exit socket.
|
||||
Это две разные операции: при отказе raw нельзя уничтожать работающую клиентскую сторону.
|
||||
Например, icmp_proxy_init создаёт общий контекст, отдельный enable_exit открывает
|
||||
raw socket и регистрирует чтение. Новый raw socket публикуется только после полного успеха.
|
||||
Обе операции возвращают ошибки непосредственно своему вызывающему коду.
|
||||
|
||||
Рекомендуемая политика: EPERM/EACCES/отсутствие поддержки raw явно отключают только ICMP exit,
|
||||
с WARN о причине и итогом запуска tcp=on/udp=on/icmp_exit=off. TCP/UDP продолжают работать.
|
||||
Ошибки выделения/регистрации обязательных ресурсов прерывают запуск proxy с полной очисткой.
|
||||
Если продукт требует обязательного ICMP, отсутствие raw должно быть фатальным на уровне
|
||||
конфигурации; это отдельная политика, а не скрытый fallback внутри init.
|
||||
В production виртуальные echo-ответы недопустимы. Тестовый loopback выбирается явно
|
||||
и должен работать одинаково с правами raw и без них.
|
||||
|
||||
Заданные SOCKS/HTTP listeners обязательны: enabled с пустым/неверным адресом либо
|
||||
неудачный bind/listen проваливают create. Нельзя вернуть готовый клиент без выбранного входа.
|
||||
Проверки router bind оставляем, но при живом inst нынешний bind не выделяет память
|
||||
и возвращает 0; не нужно изображать реальные OOM-пути там, где их нет.
|
||||
|
||||
UDP и ограниченные ресурсы
|
||||
|
||||
Новый flow сначала полностью создаётся локально: socket -> nonblocking -> bind ->
|
||||
read registration -> обеспечение expire timer. Только затем добавляется в список.
|
||||
Любой отказ снимает уже созданные регистрации/таймеры и закрывает именно новый сокет.
|
||||
Если общий таймер не удаётся продлить, оставшиеся flows закрываются с диагностикой:
|
||||
состояние без гарантированной очистки не сохраняется.
|
||||
|
||||
sendto проверяется по точному размеру. Для нулевой датаграммы n=0 означает успех.
|
||||
EINTR допускает повтор операции; EAGAIN/ENOBUFS означает явное отбрасывание датаграммы
|
||||
с диагностикой. Не добавляем неограниченную очередь и TCP-подобные повторы UDP.
|
||||
Остальные ошибки также логируются с peer, tuple, длиной и errno; packet-specific ошибка
|
||||
не должна автоматически уничтожать рабочий flow. Невалидный/сломанный сокет удаляется.
|
||||
Проверяются ошибки router send, выделения ответов и доставки в TUN.
|
||||
ICMP request тоже не публикуется без рабочего механизма истечения срока ожидания.
|
||||
|
||||
TCP HUP, FIN и границы ответственности
|
||||
|
||||
FIN относится только к одному направлению и следует после его DATA. Ошибка реальной
|
||||
записи остаётся ошибкой даже после FIN противоположной стороны; ENOTCONN не превращается
|
||||
в успешный on_fin_sent. Существующая регрессия test_fin_shutdown_error сохраняет этот контракт.
|
||||
|
||||
HUP — повод обработать завершение сокета, но не разрешение обойти high_water или явную pause_read.
|
||||
При достижении high_water чтение останавливается до low_water. Если level-triggered HUP
|
||||
не даёт event loop уснуть, регистрацию сокета нужно временно снять и восстановить по
|
||||
queue waiter; простой return с оставленным HUP в epoll/poll создаёт busy loop.
|
||||
Нужен один механизм восстановления регистрации в tcp_io, учитывающий connected,
|
||||
fin_remote, write_buf, write_queue и уничтожение владельца. Состояние fd/пулы сохраняются
|
||||
до окончательного destroy; запланированная запись не должна зависеть от паузы чтения.
|
||||
Повторное добавление использует новую generation uasync, старые события игнорируются.
|
||||
Явную паузу владельца следует отличать от паузы по high_water; HUP не снимает ни одну.
|
||||
|
||||
Это наиболее чувствительный этап: перед реализацией проверить переходы регистрации
|
||||
при HUP с ожидающей записью, low_water, обеих FIN и destroy из callback.
|
||||
Не менять глобальную семантику всех регистраций uasync ради одного tcp_io-сценария.
|
||||
|
||||
Поздние WINDOW/FIN/CLOSE/ERROR для отсутствующего потока игнорируются как события
|
||||
завершённого sid с debug-диагностикой. DATA не воскрешает поток, неверное окно живого
|
||||
потока по-прежнему является ошибкой. HTTP не обязан ждать FIN для ответа с известной длиной.
|
||||
Проверить синхронный loopback: состояние перед отправкой control резервируется заранее;
|
||||
после callback с возможным destroy нельзя обращаться к владельцу без проверки его жизни.
|
||||
|
||||
HTTPS-тест разделить на нормальное двустороннее TLS shutdown и явный abrupt close.
|
||||
В первом случае ожидать отсутствие EPIPE/ENOTCONN; во втором — корректную ошибку,
|
||||
сохранность уже доставленного ответа и освобождение ресурсов. Proxy не разбирает TLS.
|
||||
Сначала проверить эту гипотезу управляемым тестовым сервером, затем менять fixture.
|
||||
|
||||
Проверка после реализации
|
||||
|
||||
- Fault injection: socket/nonblocking/bind/read registration/expire timer/sendto/OOM.
|
||||
Проверять отсутствие fd/памяти/таймеров/handlers после каждого отказа и успешный повторный запуск.
|
||||
- client only, exit only, обе роли на одном instance, два независимых instance;
|
||||
отказ raw сохраняет ICMP REPLY клиента, остановка одного TCP-модуля не ломает общие контексты.
|
||||
- UDP payload=0, краткая/ошибочная отправка, transient error и следующий успешный пакет.
|
||||
- HUP при полном proxy window и остановленном consumer: очередь остаётся ограниченной,
|
||||
event loop не крутится, после снятия блокировки все байты и FIN доходят в правильном порядке.
|
||||
- pending write при HUP, manual pause, low_water/re-register, destroy в callback и stale events.
|
||||
- FIN в обе стороны при backpressure, RST до/после ответа, нормальный/оборванный TLS,
|
||||
поздний WINDOW, синхронный loopback и HTTP keep-alive со сменой sid.
|
||||
- Текущие proxy9, test_tcp_io, тесты uasync/queue, затем ASAN/UBSan и полный check.sh.
|
||||
Windows/FreeBSD требуют отдельной проверки poll/select; Linux PASS её не заменяет.
|
||||
|
||||
Доказательства исходного анализа
|
||||
|
||||
Диагностический test_socks_http_proxy под gdb: PASS, 16 byte-exact загрузок.
|
||||
Включены существующие proxy/socket debug через gdb, проектные файлы не менялись.
|
||||
Полный журнал: /tmp/proxy-design-debug.log; события lifecycle: /tmp/proxy-design-lifecycle.log.
|
||||
Системные вызовы TLS: /tmp/proxy-design-close.744427,
|
||||
сводка четырёх соединений: /tmp/proxy-design-close-summary.txt.
|
||||
Эти журналы относятся к реализации до исправлений.
|
||||
|
||||
Реализация и проверка архитектуры
|
||||
|
||||
1. UTUN_INSTANCE единолично создаёт и уничтожает общие UDP/ICMP-контексты. TCP-модули
|
||||
владеют только своими bindings/listeners/потоками. Их rollback не затрагивает общие
|
||||
контексты. Занятой binding не перезаписывается, повторный init TCP отклоняется.
|
||||
Proxy закрывается и deferred free обрабатываются до остановки групп/транспорта.
|
||||
2. Все заданные listeners и TUN mappings обязательны. Частичный отказ возвращает NULL
|
||||
и освобождает созданные ресурсы; create_from_config сохраняет config у вызывающего.
|
||||
3. ICMP init создаёт контекст/binding, enable_exit атомарно создаёт raw socket/регистрацию.
|
||||
Отсутствие прав/поддержки отключает только ICMP exit с явным WARN. Клиентские REPLY
|
||||
сохраняются; остальные ошибки запуска фатальны. Test loopback включается явно.
|
||||
4. UDP flow публикуется после всех обязательных шагов. Отказы создания/продления таймера
|
||||
не оставляют бессрочных flows. Проверяются socket/nonblocking/bind/registration,
|
||||
sendto, ответные выделения/router/TUN. EINTR повторяется, нулевая датаграмма допустима;
|
||||
ошибки пакета логируются без очереди повторов, сломанный сокет закрывается.
|
||||
5. TCP HUP сохраняет high/low-water и ручную остановку чтения. Без доступного I/O
|
||||
регистрация временно снимается; low_water восстанавливает её с новой generation.
|
||||
fd/очереди сохраняются; запись обслуживается независимо. Ошибка восстановления
|
||||
регистрации корректно вызывает on_error, включая destroy владельца из callback.
|
||||
6. Поздний WINDOW отсутствующего sid отбрасывается с DEBUG. Формат протокола и машина
|
||||
CONNECTED/WINDOW/FIN сохранены. Реальные ошибки записи/SHUT_WR не превращаются в успех.
|
||||
HTTPS fixture теперь завершает TLS двусторонне; abrupt close — отдельный режим.
|
||||
7. Неподдержанный enabled в серверных конфигурациях профильных и TUN-тестов заменён
|
||||
поддержанным connect_timeout. Контракты заголовков и документы модулей обновлены.
|
||||
|
||||
Проверено:
|
||||
- test_proxy_lifecycle: 25/25 изолированных сценариев, контроль fd/памяти/регистраций/таймеров.
|
||||
Покрыты socket/nonblock/bind/registration/timer/OOM, отправка/EINTR/нулевой UDP,
|
||||
частичный отказ instance, повторный запуск, ICMP REPLY после EPERM, HUP re-register,
|
||||
ожидающая запись и destroy из error callback. /tmp/proxy-fix-lifecycle2.log.
|
||||
- test_tcp_io: 19/19, HUP high_water=4 на epoll/poll, парковка без busy loop,
|
||||
точная доставка данных и FIN после low_water, ручная пауза и независимая запись.
|
||||
- SOCKS/HTTP/HTTPS: оба режима по 16 byte-exact загрузок. Штатный TLS shutdown без
|
||||
EPIPE/ENOTCONN; abrupt сохраняет ошибки TCP без повреждения доставленных ответов.
|
||||
/tmp/proxy-fix-check1/test_socks_http_proxy.log, /tmp/proxy-fix-tls-abrupt.log.
|
||||
- Основной make check: 185/185 PASS. /tmp/proxy-fix-full-check2.log.
|
||||
Первый check.sh остановлен общим лимитом 141с; local_transport_packets отдельно PASS.
|
||||
Повтор завершил основной набор, но TUN-этапу помешал чужой процесс на UDP 15002.
|
||||
- Те же TUN fixtures и идентичный src/utun выполнены в отдельном network namespace:
|
||||
proxy9/9, burst1/1, load8/8 PASS; load проверил134217728 байт.
|
||||
/tmp/proxy-fix-network-check.log; копия fixtures/логов: /tmp/utun-proxy-fix-network/.
|
||||
- Настоящий raw ICMP echo до127.0.0.1 через два ETCP-instance и явный loopback PASS.
|
||||
UTUN_TEST_ICMP_RAW=1 требует raw socket и не переключается на виртуальный ответ.
|
||||
/tmp/proxy-fix-icmp-raw.log, /tmp/proxy-fix-icmp-loopback.log.
|
||||
- ASAN/UBSan/LSan: 16/16 PASS с detect_leaks=1, включая оба TLS-режима, lifecycle,
|
||||
tcp_io/flush, пакеты, regressions, HTTP, TCP/UDP/ICMP, services, uasync/queue и STCP.
|
||||
Настоящий raw ICMP дополнительно PASS под теми же санитайзерами с root.
|
||||
Все встроенные зависимости собраны с sanitizer-флагами в отдельной копии текущих
|
||||
исходников /tmp/utun-proxy-fix-sanitizers; обычные объекты проекта не менялись.
|
||||
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-sanitizer-icmp-raw.log.
|
||||
|
||||
В проверенных Linux-сценариях противоречий владения/rollback/half-close не выявлено.
|
||||
Сборка и runtime Windows/FreeBSD не проверялись; Linux epoll/poll не заменяют эту проверку.
|
||||
+185
-46
@@ -1,61 +1,200 @@
|
||||
# Proxy: потоки TCP и датаграммы
|
||||
# Proxy: TCP, UDP и ICMP
|
||||
|
||||
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
|
||||
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
|
||||
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
|
||||
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
|
||||
Клиент принимает IPv4 из отдельного TUN через lwIP либо TCP от локальных SOCKS5/HTTP
|
||||
listeners. Exit открывает обычный IPv4 TCP/UDP или raw ICMP socket. Вся передача proxy
|
||||
идёт через `etcp_route_send` в `TOPO_GROUP_UTUN`, mode=0. Протокол не имеет слоя
|
||||
совместимости с прежними форматами.
|
||||
|
||||
## TCP
|
||||
## Сообщения для router и приёмные метаданные
|
||||
|
||||
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
|
||||
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
|
||||
Это форматы **сервисного API**, а не полные ETCP wire packets. На отправке первый байт —
|
||||
svc_id, остальное — payload router. Router callback получает такой заголовок:
|
||||
|
||||
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
|
||||
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
|
||||
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
|
||||
- DATA (3): от 1 до 4096 байт.
|
||||
- CLOSE (4), ERROR (5): прекращение потока.
|
||||
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
|
||||
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
|
||||
| Смещение | Размер | Поле |
|
||||
|---|---|---|
|
||||
| 0 | 1 | svc_id |
|
||||
| 1 | 8 | src_node_id |
|
||||
| 9 | 8 | dst_node_id |
|
||||
| 17 | 1 | rx_flags |
|
||||
| 18 | 8 | group_id |
|
||||
| 26 | N | service payload |
|
||||
|
||||
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
|
||||
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
|
||||
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
|
||||
не является подтверждением потребления данных конечным TCP.
|
||||
Смещения определены `ROUTER_SVC_*_OFF` в `src/routing_layer/etcp_router.h`.
|
||||
Заголовок callback имеет размер 26 байт. Сервис освобождает полученные entry/dgram
|
||||
при любом исходе. На отправке router принимает владение entry/dgram даже при ошибке;
|
||||
0 означает принятие сообщения router, а не доставку конечному TCP/UDP.
|
||||
|
||||
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
|
||||
или backpressure роутера. Закрытие одного направления не прекращает другое.
|
||||
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
|
||||
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
|
||||
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
|
||||
TCP-клиент проверяет src_node_id==via_node_id и group_id==TOPO_GROUP_UTUN, включая
|
||||
пустые уведомления. UDP/ICMP допускают REQUEST при включённом exit, REPLY — только при
|
||||
наличии клиента и совпадении src_node_id с его via_node_id; отдельной проверки group
|
||||
в их callback нет. IP, TCP/UDP-порты и ICMP id/seq в proxy API передаются в network
|
||||
byte order. TCP stream_id и WINDOW credit копируются как host-order uint32_t:
|
||||
формат не нормализован между архитектурами с разным byte order.
|
||||
|
||||
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
|
||||
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
|
||||
накопленные HTTP headers/body передаются частями размером DATA.
|
||||
## TCP и управление окном
|
||||
|
||||
Определения — `src/proxy/proxy_protocol.h`.
|
||||
На отправке: `svc:1, cmd:1, stream_id:4, data` (заголовок 6 байт).
|
||||
На приёме: router header(26), cmd(1), stream_id(4), data (заголовок 31 байт).
|
||||
Клиент отправляет в `ETCP_RT_ID_TCP_PROXY_SERVER=0x04`, exit отвечает в
|
||||
`ETCP_RT_ID_TCP_PROXY_CLIENT=0x08`. На exit поток определяется парой peer_node_id/stream_id;
|
||||
повторный CONNECT этой пары отклоняется с ERROR.
|
||||
|
||||
| Команда | Код | Содержимое после stream_id |
|
||||
|---|---|---|
|
||||
| CONNECT | 1 | IPv4:4, порт:2 |
|
||||
| CONNECTED | 2 | Пусто; exit завершил реальный TCP connect |
|
||||
| DATA | 3 | 1..4096 байт |
|
||||
| CLOSE | 4 | Пусто; прекращение потока с дренажом принятой записи на exit |
|
||||
| ERROR | 5 | Пусто; отмена, на exit немедленно уничтожает TCP и очереди |
|
||||
| FIN | 6 | Пусто; конец одного направления после всех его DATA |
|
||||
| WINDOW | 7 | uint32_t дополнительного кредита, ровно 4 байта |
|
||||
|
||||
Каждое направление начинает с окна 65536 байт. proxy_flow init оставляет ready=0.
|
||||
DATA ждёт CONNECTED; SOCKS success и HTTP CONNECT 200 также отправляются только после него.
|
||||
Локальный TUN TCP handshake выполняет lwIP и сам по себе connect назначения не подтверждает.
|
||||
При отправке DATA списывается tx_credit, при приёме — rx_credit.
|
||||
WINDOW допускает только ненулевой кредит без увеличения tx_credit сверх 65536.
|
||||
|
||||
Кредит возвращается после потребления локальным TCP: на OS-сокетах после опустошения
|
||||
write_queue **и write_buf**, в lwIP после принятия байтов ограниченным send-buffer
|
||||
через tcp_write. Это не подтверждение чтения удалённым приложением.
|
||||
Для HTTP учитывается число исходных upstream байтов независимо от преобразования
|
||||
headers/chunks/trailers; кредит возвращается после доставки соответствующего результата в OS TCP.
|
||||
|
||||
Backpressure router и окно потока независимы. При ошибке DATA остаётся в очереди/буфере
|
||||
владельца, регистрируется send-ready waiter и safety-net на 500 мс.
|
||||
У TCP-клиента с lwIP timer повторяет DATA с force=1, у exit и SOCKS/HTTP — обычный force=0.
|
||||
Force обходит лимит send_q router, но никогда не окно proxy.
|
||||
При отсутствии CONNECTED/кредита (-2) отправка ждёт ready/WINDOW, а не только router waiter.
|
||||
|
||||
OS TCP сохраняет ограничение read_high_water и явную остановку чтения при HUP.
|
||||
Если HUP не оставляет работы для чтения/записи, tcp_io временно снимает регистрацию fd;
|
||||
low_water восстанавливает её с новой generation. Сокет и очереди сохраняются, запись
|
||||
обслуживается независимо от чтения. Ошибка повторной регистрации закрывает соединение.
|
||||
|
||||
## Half-close и удаление потока
|
||||
|
||||
Владелец выставляет fin_pending только после отправки всех исходящих DATA.
|
||||
proxy_flow не видит очередей владельца и сам это условие не проверяет.
|
||||
FIN не должен ждать записи противоположного направления: обе половины независимы.
|
||||
Приём FIN ставит FIN-маркер после ранее принятых DATA в OS write_queue; в lwIP FIN
|
||||
откладывается до дренажа to_lwip и успешного tcp_shutdown.
|
||||
|
||||
Окончательное закрытие сырого TCP наступает после обоих FIN и необходимых локальных
|
||||
очередей/shutdown. Дополнительный обмен CLOSE при штатном half-close не требуется.
|
||||
На exit CLOSE паузит чтение, отбрасывает непрочитанный read_queue и закрывает сокет
|
||||
после принятой write_queue; ERROR уничтожает поток сразу. На клиенте CLOSE/ERROR
|
||||
прекращают поток; HTTP вне туннеля дополнительно проверяет завершённость ответа.
|
||||
|
||||
Общий retry повторяет pending CONNECTED → WINDOW → FIN через 5000 tb (500 мс),
|
||||
используя force=1. CONNECT/CLOSE/ERROR в эту очередь не входят.
|
||||
Клиентский close_pending может повторяться в poll/free пока поток жив; сервер после
|
||||
ошибки TCP удаляет поток и делает попытку ERROR. Гарантированного повторения CLOSE/ERROR
|
||||
после освобождения владельца нет.
|
||||
Поздний WINDOW отсутствующего потока отбрасывается с debug-диагностикой, без создания потока.
|
||||
|
||||
Router close/restart уведомляет сервис entry длиной 26 байт без payload.
|
||||
Proxy называет это CLOSE_ALL: exit удаляет потоки указанного peer, клиент — все свои
|
||||
TUN/SOCKS/HTTP потоки к настроенному exit. Это не самостоятельная подкоманда или
|
||||
9-байтовый пакет на проводе.
|
||||
|
||||
## SOCKS5 и HTTP
|
||||
|
||||
SOCKS реализует CONNECT к IPv4 или DNS-имени, метод аутентификации 0 (no auth).
|
||||
IPv6 отклоняется с REP=8; BIND и UDP ASSOCIATE не реализованы.
|
||||
DNS выполняется асинхронно на ingress, выбирается первая A-запись.
|
||||
Совмещённые и разделённые greeting/request/data сохраняют непрочитанный хвост.
|
||||
Ошибка DNS/connect до успеха даёт SOCKS REP=4 либо HTTP 502.
|
||||
|
||||
HTTP/1.0–1.1 forward proxy требует absolute http:// URL; upstream получает origin-form,
|
||||
Host из URL и Via. Обычный запрос отправляется как HTTP/1.1 с Connection: close,
|
||||
кроме Upgrade. Frontend HTTP/1.1 может сохранять keep-alive: каждый запрос получает
|
||||
новый stream_id/upstream, pipelining обслуживается последовательно, включая разные цели.
|
||||
Новый stream начинается после доставки полного ответа и отправки CLOSE/ERROR старого.
|
||||
|
||||
Codec проверяет headers до пересылки, поддерживает Content-Length, chunked
|
||||
с extensions/trailers, промежуточные ответы, HEAD/204/304 и тело до EOF.
|
||||
Hop-by-hop и proxy authentication поля удаляются, framing/Connection строятся заново.
|
||||
HTTP/1.0 получает декодированное chunked тело и закрытие. CONNECT ждёт полного блока
|
||||
headers и реального CONNECTED; согласованный 101 переводит соединение в сырой туннель.
|
||||
Ранний final response прекращает upload и требует закрытия клиента.
|
||||
Ошибка до final headers может дать собственный HTTP error; после них закрывает оборванный ответ.
|
||||
|
||||
Лимиты: headers/trailers 32 KiB, start line и chunk/trailer line 8 KiB, 512 header fields.
|
||||
CL+TE, разные Content-Length, obs-fold, управляющие байты и неверные порты отклоняются.
|
||||
Поддержан только transfer coding chunked; HTTP IPv6-назначения отклоняются с 502.
|
||||
Headers имеют deadline 30 с, DNS/connect — 15 с, передача/idle/дренаж закрытия — 60 с.
|
||||
Deadline headers не продлевается каждым байтом. Установленные CONNECT/Upgrade-туннели
|
||||
используют TCP lifecycle без HTTP deadline. Детали codec — в `src/proxy/http_proxy.h`.
|
||||
|
||||
## UDP и ICMP
|
||||
|
||||
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
|
||||
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
|
||||
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
|
||||
Контексты принадлежат конкретному UTUN_INSTANCE. REQUEST=1, REPLY=2.
|
||||
На отправке UDP: `svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`.
|
||||
ICMP: `svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`.
|
||||
Заголовки отправки — 14 байт, приёма — 39. Отдельного sender_node_id в payload нет.
|
||||
|
||||
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
|
||||
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
|
||||
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
|
||||
UDP exit создаёт сокет на tuple client_node_id/src_ip/src_port/dst_ip/dst_port,
|
||||
проверяет источник ответа, меняет src/dst местами в REPLY. Удаляет неактивные flows
|
||||
при очередной периодической проверке с порогом >60 с. Клиент восстанавливает IPv4 checksum,
|
||||
UDP checksum=0 (допустимо для IPv4).
|
||||
Flow публикуется только после socket/nonblocking/bind/read registration и создания
|
||||
таймера очистки. Ошибка продления общего таймера закрывает оставшиеся flows.
|
||||
sendto проверяется по точной длине, включая успешный нулевой payload; EINTR повторяется.
|
||||
EAGAIN/ENOBUFS и ошибки отдельного пакета логируются без очереди повторов; сломанный
|
||||
сокет удаляется. Ошибки выделения ответа, router send и доставки в TUN диагностируются.
|
||||
|
||||
## Проверки и ограничения
|
||||
ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет
|
||||
wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента.
|
||||
Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum.
|
||||
Нечётный хвост checksum обрабатывается без чтения за буфером.
|
||||
`icmp_proxy_init` создаёт общий контекст и binding; `icmp_proxy_enable_exit` отдельно
|
||||
открывает raw socket. EPERM/EACCES/отсутствие поддержки raw отключают только ICMP exit
|
||||
с WARN; клиентские REPLY сохраняются. Прочие ошибки запуска прерывают создание instance.
|
||||
Тестовый `test_loopback` включается явно, закрывает raw socket и даёт виртуальные ответы
|
||||
независимо от прав процесса. Production его не включает.
|
||||
Pending raw request создаётся только с рабочим таймером истечения срока ожидания.
|
||||
|
||||
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
|
||||
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
|
||||
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
|
||||
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
|
||||
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
|
||||
Send API принимает payload 0..65507, TUN delivery — 0..65506 из-за однобайтового
|
||||
префикса в uint16_t длине entry. UDP send требует локальный UDP-контекст, ICMP send
|
||||
его не требует. Доставка ответа требует клиентский TUN; своего окна/повтора датаграмм нет.
|
||||
|
||||
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
|
||||
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
|
||||
Windows/FreeBSD в этом прогоне не проверялись.
|
||||
## Жизненный цикл, диагностика и проверки
|
||||
|
||||
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
|
||||
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
|
||||
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
|
||||
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
|
||||
Все операции и callbacks — в uasync владельца. Proxy init запрашивает binding
|
||||
после router init; client/server имеют разные service IDs. Общие UDP/ICMP создаёт
|
||||
и уничтожает UTUN_INSTANCE; TCP-модули освобождают только собственные ресурсы.
|
||||
Контексты нужны для включённого exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
Заданные listeners, TUN mappings и bindings обязательны: ошибка откатывает создание.
|
||||
Повторный init активного модуля и попытка занять чужой binding отклоняются.
|
||||
При уничтожении instance TCP-клиент/сервер и общие контексты закрываются до остановки
|
||||
групп и транспорта; ua обрабатывает отложенные free. Точные контракты — в .h.
|
||||
|
||||
Категория proxy: info — подключения/закрытия/транзакции, debug — окна/паузы/повторы,
|
||||
trace — трафик и секундная диагностика exit TCP-очередей/пулов/OS buffers.
|
||||
Для поиска проблемы также полезны socket и etcp_route.
|
||||
Регрессионный тест включает proxy=debug через UTUN_TEST_DEBUG=1.
|
||||
|
||||
| Тест | Что проверяет |
|
||||
|---|---|
|
||||
| test_http_proxy | HTTP codec, разбиения headers/chunks, framing, лимиты, source credit, Upgrade |
|
||||
| test_proxy_packets | IPv4/UDP/ICMP, checksum и payload длиной 0..1501 |
|
||||
| test_proxy_regressions | Handshake/CONNECTED, HTTP pipeline/EOF/deadlines, окна/FIN, peer+sid, reentrancy, TUN options/fragment validation, per-instance UDP/ICMP |
|
||||
| test_proxy_lifecycle | 25 сценариев отказов/rollback, fd/память/таймеры, владение bindings, ICMP без raw, HUP re-register и destroy из callback |
|
||||
| test_tcp_proxy_client | Create/destroy с тестовым TUN, без утечек |
|
||||
| test_tcp_proxy_server | TCP echo через tcp_conn; сам по себе не проверяет протокол exit |
|
||||
| test_socks_http_proxy | Реальные SOCKS IPv4/domain, HTTP и HTTPS CONNECT, byte-exact загрузки; штатный TLS shutdown либо UTUN_TEST_TLS_ABRUPT=1 |
|
||||
| test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP: явный test_loopback, UTUN_TEST_ICMP_RAW=1 требует настоящий raw socket |
|
||||
| test_tcp_io | 19 сценариев, включая HUP high/low-water на epoll/poll, ручную паузу и независимую запись |
|
||||
| test_tcp_io_flush | Callback после последнего синхронного send |
|
||||
|
||||
Проверка 2026-10-24: профильные тесты проходят на Linux. ICMP проверен и через явный
|
||||
test_loopback, и настоящим raw echo до 127.0.0.1 с правами root. Штатный TLS shutdown проходит без EPIPE/ENOTCONN;
|
||||
аварийный сохраняет реальные ошибки записи, все 16 загрузок остаются byte-exact.
|
||||
Тестовые конфиги используют поддержанный connect_timeout. Итоги полного прогона
|
||||
и sanitizer-проверок — в [proxy_fix_plan.txt](proxy_fix_plan.txt).
|
||||
Windows/FreeBSD runtime в этом окружении не проверены.
|
||||
Создание proxy из конфигурации в utun_instance.c исключено под _WIN32.
|
||||
IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет.
|
||||
Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.
|
||||
@@ -71,6 +71,16 @@ ConnectivityManager до запроса запуска и передаёт ко
|
||||
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
|
||||
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
|
||||
|
||||
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
|
||||
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
|
||||
Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
Ошибка обязательного listener, TUN mapping или регистрации откатывает создание instance.
|
||||
Недоступность raw ICMP по правам или поддержке отключает только ICMP exit с WARN;
|
||||
клиентские ICMP REPLY и TCP/UDP продолжают работать.
|
||||
`utun_instance_destroy()` закрывает proxy и обрабатывает отложенные free до остановки
|
||||
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
|
||||
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
|
||||
|
||||
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
|
||||
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
|
||||
`:memory:` данные сохраняются только до уничтожения ядра.
|
||||
@@ -113,7 +123,8 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
|
||||
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
|
||||
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
|
||||
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
|
||||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
|
||||
Подготовленные блоки отменённых задач не регистрируются; учёт назначений подготовки
|
||||
сохраняется для последующей очистки. Это синхронное ожидание: длительное
|
||||
транскодирование или распознавание речи может задержать stop; принудительного
|
||||
прерывания этих библиотек нет.
|
||||
|
||||
@@ -125,6 +136,67 @@ completion. Отказ запуска/неверные аргументы даю
|
||||
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
|
||||
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
|
||||
|
||||
Групповой crypto worker хранит один `file_io_temp` прямо в задаче вложения.
|
||||
Общий валидатор MIME не выполняет I/O: send вызывает его в uasync до создания
|
||||
задачи/ключа, файловый codec повторяет проверку в worker до открытия файлов.
|
||||
`encrypted_file_write/read` только готовят закрытый durable temp; worker публикует
|
||||
его одним rename, completion подтверждает результат в БД/UI. Отмена до completion
|
||||
не публикует сообщение; cleanup учитывает реально созданные/заменённые файлы.
|
||||
Ошибка синхронизации каталога после rename означает установленное имя без подтверждённой
|
||||
durability; отложенный discard уже не удаляет результат. Общей транзакции ФС/БД нет.
|
||||
До запуска workers и после их завершения recovery очищает собственные stale temp
|
||||
в `media/decrypted`; общий file_io-реестр сохраняет активные temp всех instance.
|
||||
|
||||
`media_delivery_init()` вызывается после подготовки chat core и media_index, пока
|
||||
общая БД и группы живы; подписка на роли обязательна. Репликация хранит отдельный
|
||||
контекст и CM handle для каждой пары «CHAT-группа–пир». DOWN отменяет текущий RPC,
|
||||
сохраняя handle для восстановления CM; TIMEOUT закрывает его перед повторным открытием.
|
||||
Отдельный ETCP DOWN не означает потерю всех путей CM. До free контекста отменяются
|
||||
его waiter, pump, watchdog и собственный handle; destroy отменяет также сверку ролей.
|
||||
Обычный stop сохраняет каталог, его эпоху и RX-курсоры. Потеря локальной роли удаляет
|
||||
чужие объявления вместе с RX-курсорами только этой группы одной транзакцией;
|
||||
удаление канала удаляет также собственные объявления. Страница репликации и её
|
||||
RX-курсор фиксируются одним самостоятельным commit; вложенная внешняя транзакция
|
||||
не считается сохранённым прогрессом. Wire-контракт находится в `media_delivery_proto.h`.
|
||||
|
||||
Перед файловой подготовкой назначение записывается в `media_preparations`. Успешный
|
||||
`media_index_commit()` регистрирует весь файл и снимает эту запись в одном savepoint;
|
||||
после ошибки/отмены путь остаётся известен, даже если worker уже опубликовал копию.
|
||||
Это гарантия записи БД; файловые изменения и внешний SQL COMMIT ей не охватываются.
|
||||
При обычной ошибке откатывается только регистрация; SQLite может сама отменить всю
|
||||
транзакцию при тяжёлой ошибке. Общая база берётся из `ui_state.media_base` и повторно
|
||||
используется после stop/start. Загрузчик не меняет существующую базу.
|
||||
|
||||
Входящая загрузка и исходящая подготовка резервируют нормализованный путь в общем
|
||||
media_delivery до начала записи. Отмена снимает загрузку с индекса, закрывает CM handles
|
||||
и таймеры, освобождает резервацию; выполняющийся обработчик удерживает сам контекст.
|
||||
Поэтому отмена и повторный start из progress callback допустимы, а done старой загрузки
|
||||
вызывается после возврата из progress. Каждая попытка блока имеет отдельные UUID и temp:
|
||||
поздние пакеты старого держателя/попытки не изменяют новый файл.
|
||||
|
||||
Producer и relay-выдачи независимо удерживают файл блока. После отмены producer
|
||||
непроверенный источник завершается с ошибкой; проверенный может обслужить оставшихся
|
||||
читателей. Последний владелец удаляет temp. При удалении канала отменяются и producer,
|
||||
и все его выдачи; перед освобождением выдачи отменяются её waiter, pump и таймер.
|
||||
DONE выдачи следует только после проверки блока и принятия всех его данных router.
|
||||
|
||||
После проверки всех блоков загрузчик собирает отдельный temp, проверяет операции
|
||||
чтения/записи/close, полный размер и SHA-256, затем атомарно заменяет имя назначения.
|
||||
Ошибка до публикации сохраняет прежний файл. Затем выполняется регистрация в SQLite:
|
||||
при её ошибке опубликованный файл остаётся учтён в media_preparations для очистки.
|
||||
Проверка и сборка блоков пока синхронны в uasync; контракт workers относится к исходящей подготовке.
|
||||
|
||||
Локальное удаление канала ждёт его исходящие video/register задачи. Последний
|
||||
completion продолжает удаление; новые исходящие медиа канала в это время отклоняются,
|
||||
подготовленные результаты ожидающих отправок не публикуются как новые сообщения.
|
||||
После завершения задач отменяются загрузки/раздача и удаляются учтённые файлы, включая
|
||||
назначения неуспешных подготовок. Общие файлы других каналов сохраняются. Очистка файлов
|
||||
выполняется вне внешней транзакции: SQL-откат не восстанавливает удалённый файл.
|
||||
При ошибке очистки записи оставшихся файлов сохраняются для повторного вызова;
|
||||
удаление метаданных канала и неполная файловая очистка диагностируются отдельно.
|
||||
Низкоуровневый `media_index_delete_channel()` сам workers не останавливает.
|
||||
Точные ограничения путей и контракт повторной регистрации находятся в `media_index.h`.
|
||||
|
||||
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
|
||||
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
|
||||
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
|
||||
|
||||
+564
-8
@@ -4,6 +4,437 @@
|
||||
|
||||
## Текущие задачи
|
||||
|
||||
[ ] **member_files: cleanup удаляет чужой temp при отказе создания загрузки** — воспроизведено 24.10.2026 на a198cfb9.
|
||||
test_member_files_network падает в фазе23: локальный O_EXCL open возвращает MF_EXISTS,
|
||||
затем TRANSFER_FINISH без проверки владения вызывает mf_disk_local_remove(t->temp).
|
||||
Прежнее назначение37байт сохраняется, заранее созданный чужой .part исчезает.
|
||||
Проверены общий check.sh и отдельный повтор: одинаковый FAIL preservation case=0, target=1, foreign_temp=0;
|
||||
отсутствие .part дополнительно проверено в обоих оставшихся тестовых каталогах.
|
||||
Нужен учёт успешного создания собственного temp, включая отмену до PREPARE и отказ max_bytes;
|
||||
проверять/удалять можно только созданный этой передачей файл. Код этого модуля в ETCP-ветке не менялся.
|
||||
Логи: /tmp/etcp-solo-native-full-check.log, /tmp/etcp-solo-member-network-repeat.log;
|
||||
подробный первый лог: /tmp/etcp-solo-member-network-suite-failed.log.
|
||||
|
||||
[+] **media_async/media_delivery: завершить сверку контрактов и исправлений** — завершено 24.10.2026.
|
||||
Проверены все11 заголовков двух каталогов по полным реализациям; исправлены alias файлов,
|
||||
граница voice duration, переанонс SQLite и расхождение MIME. Результаты каждого фикса приведены ниже.
|
||||
Устаревший _desc.txt заменён описанием текущей архитектуры; уточнены sync.md, encrypted_attachments.md
|
||||
и media-раздел service_lifecycle.md. Общий lifecycle за пределами media остаётся частично проверенным.
|
||||
Итог, границы и доказательства: [media_async_audit_findings.txt](media_async_audit_findings.txt).
|
||||
|
||||
[+] **encrypted_file: одинаково проверять MIME при записи и чтении контейнера** — исправлено 24.10.2026.
|
||||
Прежний write проверял только strlen(type)<32 и принимал пустое значение/недопустимые символы,
|
||||
тогда как read их отвергает. Пять реальных write/read с теми же ключом/UUID/подписанным hash:
|
||||
write=0, read=-1; контрольный application/octet-stream проходит полный roundtrip.
|
||||
Прежний chat_attachment_send также проверял только длину и мог принять такой MIME до записи ключа/запуска worker.
|
||||
Воспроизведение: /tmp/media-content-type-audit.c и .mk, /tmp/media-content-type-audit.log;
|
||||
исходники/контейнеры: /tmp/media-content-type-audit-FdtMC3/. Исходный файл не меняется.
|
||||
Реализован один bounded encrypted_file_validate_type для send/write/read; 1..31 байт,
|
||||
ASCII33..126 без двойной кавычки/обратной косой черты — действующие ограничения read.
|
||||
Отказ до создания ключа/файлов; формат, криптография и файловое владение сохранены.
|
||||
Регрессии: все256 значений байта,92 допустимых;8 ранних отказов writer/send,3 roundtrip,
|
||||
7 отказов чтения аутентифицированных плохих метаданных. Проверены прежние выходы, файлы, fd/temp и отсутствие ключей/worker.
|
||||
./build.sh --no-gui -j4 PASS; профиль10/10 и ASAN/UBSan/LSan4/4 PASS, detect_leaks=1.
|
||||
Логи: /tmp/media-mime-fix-build.log, /tmp/media-mime-fix-profile-results.log,
|
||||
/tmp/media-mime-sanitizer-results.log. Полный check и Windows/FreeBSD для этого фикса не запускались.
|
||||
Подробности: [media_async_audit_findings.txt](media_async_audit_findings.txt). Сверка описаний завершена.
|
||||
|
||||
[+] **media_delivery: проверять записи SQLite перед переанонсом локальных блоков** — исправлено 24.10.2026.
|
||||
Прежний announce_local_blocks не проверял длины media_id/block_id, диапазон chunk и полный chat_id.
|
||||
Реальный обработчик отправил все7 повреждённых записей: UUID длиной1/17 байт, chunk=-1/2^32, chat_id=111junk.
|
||||
При UUID длиной1 байт остальные15 байт сетевого поля взяты за границей BLOB;
|
||||
chunk=-1 превращается в4294967295, 2^32 — в0, 111junk — в группу111.
|
||||
Стенд использует настоящие SQLite/Ed25519; отправка перехвачена, пакеты в реальную сеть не уходят.
|
||||
Воспроизведение: /tmp/media-announcement-audit.c и .mk; лог: /tmp/media-announcement-audit.log.
|
||||
Реализованы проверки типов/точных длин UUID, chunk через int64 и канонического ненулевого decimal chat_id
|
||||
до копирования/кастов/отправки. Плохие строки пропускаются с WARN; prepare/bind/step/finalize проверяются.
|
||||
UUID копируются до вложенных SQL. Сохранены void API, поштучные анонсы и формат wire.
|
||||
test_media_delivery_sql: 36 случаев полей (32 отказа/4 корректных), смесь2/3 и5 SQL faults с recovery.
|
||||
Проверяются точные wire UUID/group/chunk, отсутствие оставшихся statements и ERROR вместо успешного лога при отказе SQL.
|
||||
Сборка PASS; профильные12/12 и ASAN/UBSan/LSan4/4 PASS, detect_leaks=1, все встроенные зависимости инструментированы.
|
||||
Логи: /tmp/media-announcement-fix-profile-results.log, /tmp/media-announcement-fix-sql-final.log,
|
||||
/tmp/media-announcement-sanitizer-results.log. Полный check и Windows/FreeBSD не запускались.
|
||||
Архитектура, границы воспроизведения и регрессии: [media_async_audit_findings.txt](media_async_audit_findings.txt).
|
||||
Последующая сверка описаний двух каталогов завершена; итог указан в начале списка.
|
||||
|
||||
[+] **media_async voice_file: согласовать верхнюю границу длительности с метаданными** — исправлено 24.10.2026.
|
||||
Прежняя проверка samples/channels/rate >86400 пропускала полные кадры 24ч+20..980мс,
|
||||
хотя attachment_validate отклоняет duration_ms >86400000. В штатном pipeline это означает
|
||||
лишнюю подготовку файла перед отказом публикации; дефект относится к граничным записям длиннее суток.
|
||||
Диагностический вызов реального voice_file_encode с остановкой на создании Opus:
|
||||
60 случаев, все пять частот и 1/2 канала; 20 подтверждённых расхождений проверки.
|
||||
/tmp/media-voice-duration-repro.c, /tmp/media-voice-duration-repro.mk, /tmp/media-voice-duration-repro.log.
|
||||
Реализован общий предел миллисекунд в attachment.h; voice_file проверяет 25..4320000 полных кадров
|
||||
до создания encoder/открытия output. Валидатор и video completion используют тот же предел.
|
||||
test_attachment: 120 граничных случаев; 70 ранних отказов, прежний файл и metadata сохранены.
|
||||
Настоящий Opus encode/decode: 11 записей, включая 500мс с хвостом для всех пяти частот и 1/2 канала.
|
||||
Границы метаданных voice/video 86400000/86400001мс проверены через validate/encode/decode.
|
||||
./build.sh --no-gui -j4 PASS; профильные тесты 10/10 PASS, включая DM, encrypted attachments и video.
|
||||
Логи: /tmp/media-voice-fix-build.log, /tmp/media-voice-fix-profile-results.log, /tmp/media-voice-fix-profile-logs/.
|
||||
Подробности и ограничения воспроизведения: [media_async_audit_findings.txt](media_async_audit_findings.txt).
|
||||
На этом этапе новые полный check и sanitizer-прогоны не выполнялись; последующие проверки и итог аудита указаны выше.
|
||||
|
||||
[+] **media_async encrypted_file: не уничтожать source через alias назначения** — исправлено 24.10.2026.
|
||||
write/read отвергают только одинаковые строки; /./ и hardlink обходят проверку одного файла.
|
||||
write возвращает0, заменяя исходные30 байт контейнером пустого файла; read возвращает0,
|
||||
заменяя входной ciphertext182 байта plaintext30 байт. Проверены реальные функции, inode и SHA256.
|
||||
Реализован один file_io_temp в ak_file; codec готовит закрытый проверенный temp,
|
||||
worker публикует его одним rename. Target не открывается с усечением; alias проверяется до work и перед publish.
|
||||
Durability задаётся на весь срок temp; ошибка directory sync после rename не считается откатом.
|
||||
Cleanup удаляет только собственные артефакты; crash recovery учитывает temp в media и decrypted.
|
||||
DM не вызывает codec: сохранить его контракт общих sync/publish helpers при изменениях file_io.
|
||||
Детали, проверка архитектуры исправления и незавершённый аудит: [media_async_audit_findings.txt](media_async_audit_findings.txt).
|
||||
Воспроизведение: /tmp/media-encrypted-alias-repro.c, /tmp/media-encrypted-alias-repro.log.
|
||||
Добавлены восемь alias-сценариев, 23 отказа I/O с контролем fd/temp/метаданных,
|
||||
отмена настоящего worker после rename до completion, startup/restart обоих деревьев.
|
||||
Профильные21/21 и основной make check185/185 PASS; proxy9/9, burst1/1, load8/8 PASS
|
||||
в отдельном network namespace,134217728 проверенных байт. ASAN/UBSan/LSan21/21 PASS, detect_leaks=1.
|
||||
Логи: /tmp/media-fix-full-check.log, /tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log.
|
||||
Последующий аудит остальных контрактов media_async/media_delivery завершён; Windows/FreeBSD не проверялись.
|
||||
|
||||
[+] **Proxy: исправить HUP/backpressure, владение и rollback** — завершено 24.10.2026.
|
||||
Согласованный проект, причины и результаты: [doc/proxy_fix_plan.txt](proxy_fix_plan.txt).
|
||||
HUP больше не обходит read_high_water/ручную паузу; idle HUP снимается с poll,
|
||||
low_water восстанавливает регистрацию с новой generation, запись остаётся независимой.
|
||||
Общими UDP/ICMP владеет instance; TCP-модули освобождают только свои ресурсы.
|
||||
Proxy закрывается до групп/транспорта. Listeners/mappings обязательны, частичный запуск откатывается.
|
||||
Raw ICMP запускается отдельно: EPERM сохраняет клиентские REPLY и TCP/UDP; остальные ошибки запуска фатальны.
|
||||
UDP проверяет создание flow, таймеры и точную отправку; нулевой payload и EINTR обработаны.
|
||||
Поздний WINDOW завершённого sid — DEBUG drop. Неверный enabled убран из серверных тестовых конфигов.
|
||||
Штатный TLS shutdown проходит без EPIPE/ENOTCONN; abrupt сохраняет реальные ошибки TCP.
|
||||
Оба TLS-режима: 16 byte-exact загрузок. Настоящий raw ICMP и явный loopback PASS.
|
||||
test_proxy_lifecycle: 25/25 сценариев отказов без утечек fd/памяти; test_tcp_io: 19/19, epoll/poll HUP.
|
||||
Основной make check: 185/185 PASS. Общий check.sh сначала не уложился в141с,
|
||||
затем TUN-этапу помешал занятый другим процессом UDP15002. TUN-тесты повторены
|
||||
с тем же бинарником в отдельном network namespace: proxy9/9, burst1/1, load8/8 PASS,
|
||||
134217728 проверенных байт. ASAN/UBSan/LSan: 16/16 и отдельный raw ICMP PASS, detect_leaks=1.
|
||||
Логи: /tmp/proxy-fix-full-check2.log, /tmp/proxy-fix-network-check.log,
|
||||
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-icmp-raw.log.
|
||||
Windows/FreeBSD runtime не проверялся.
|
||||
|
||||
[ ] **ETCP: полезная утилизация нескольких каналов и доставка при зависании части линков** — общая задача открыта.
|
||||
После push исследован одиночный дуплекс2Мбит/с на изолированной базе a198cfb9.
|
||||
Точные7763 ACK показывают задержку обратной очереди: пример DATA15.909мс, hold0.029мс,
|
||||
обратный ACK32.219мс; DOWN продолжает ждать flight7644 при цели5915байт.
|
||||
Контрольный20-секундный замер1.726/1.658Мбит/с, retries=0; простой ACK-резерв/округление/уменьшение бюджета
|
||||
не дают устойчивого прохождения. Предел окна DOWN сокращает фазу до6..22мс, но короткий FAIL1.656 остаётся.
|
||||
Прототипы не перенесены в рабочий ETCP/BBR. Добавлены сценарий duplex-slow и UTUN_MULTIPATH_SAMPLE_MS=1000..60000
|
||||
для длительных замеров; прежние пороги сохранены. Далее — причины остановки DATA в CRUISE/REFILL:
|
||||
физическое окно, общий admission budget и pacer; подробные данные/команды — doc/etcp_multipath_plan.txt.
|
||||
24.10.2026: исправлены RTT. min_rtt наследует метку короткого минимума, вместо омоложения кеша каждым ACK;
|
||||
прежний минимум20мс заменяется после10с своего возраста (прежде15с). Метки/hold PROBE_RTT используют полные uint64 TB;
|
||||
проверены 32-битная граница, нулевые младшие32бита срока, длительный idle, hold>=200мс и ACK-раунд.
|
||||
Исправлено замораживание DATA-измерений при скачке RTT: RTO берёт max(DATA EWMA, timestamp-RTT своего линка).
|
||||
Формула1.2*RTT+1.5*DATA_jitter+3мс и clamp10мс..1с сохранены; BBR обновляют только действующие DATA ACK.
|
||||
Новый профиль rtt-change:20->100->20мс, проверяет обновление RTT, снятие stalled, старение/снижение minRTT и доставку.
|
||||
DATA-измерения возобновились примерно за203мс, после перехода повторов нет; minRTT103.794мс, после возврата23.941мс.
|
||||
До фикса стенд дал2035 повторов и остановку DATA ACK-модели; отдельная регрессия настоящего retrans-таймера также FAIL.
|
||||
Пересчёт attempts-таймера вызывается и при сокращении timestamp-RTT без новых DATA/ACK: регрессия123->27мс
|
||||
до фикса сохраняла96мс лишнего ожидания. Пересчёт идёт до внешнего RTT callback, который может закрыть conn/линк;
|
||||
OOM таймера также безопасно закрывает conn из RX. Негативный ASAN-тест ловит UAF при обратном порядке.
|
||||
BBR24/24; test_etcp_windows63 сценария в обоих режимах часов PASS; четыре ASAN/UBSan-теста и ASAN rtt-change без ошибок/утечек.
|
||||
Check.sh186/186, proxy9/9, burst1/1, load8/8 PASS,134217728 проверенных байт,189с после последней правки.
|
||||
Полная матрица: первые6 скоростных профилей, все9 отказов и rtt-change PASS; остался1 FAIL2Мбит/с:1.662<1.700.
|
||||
Одиночный8Мбит/с7.076/7.019, агрегат8.737/8.717 из10. Далее —2Мбит/с, STCP backpressure, смена RTT нескольких путей/BW и повторный отказ.
|
||||
Логи/CSV /tmp/etcp-rtt-complete-* и /tmp/etcp-rtt-close-*;8652 среза проверены, отдельные332 RTT: /tmp/etcp-rtt-complete-rtt-change.csv/.html.
|
||||
Детали: doc/etcp_multipath_plan.txt. Предыдущий этап — исправление BW:
|
||||
24.10.2026: исправлена недооценка BW растянутыми ACK: достоверный RX clock заменяет ACK clock,
|
||||
interval=max(send_us,ceil(delivered*delta_rx_us/delta_rx_bytes)); без RX-базы/прогресса max(send_us,ack_us).
|
||||
Сохранены send-граница, проверка пары/переполнения/idle и логические ACK; wire/RTT/RTO/окно не менялись.
|
||||
DEBUG ACK показывает исходные интервалы и выбранные часы. До фикса точная регрессия FAIL, после PASS.
|
||||
test_etcp_windows: 61 сценарий с обоими режимами часов PASS; BBR 20/20, восемь ASAN/UBSan-тестов без ошибок/утечек.
|
||||
Check.sh 186/186, proxy 9/9, burst 1/1, load 8/8 PASS, 134217728 проверенных байт, 201с.
|
||||
Полная последовательная матрица: все 7 скоростных и 9 отказных профилей PASS; дуплексный агрегат 8.768/8.789 из10.
|
||||
Три прогона рабочего кода: одиночный 8Мбит/с даёт 7.045..7.081 в обе стороны, агрегат проходит каждый раз.
|
||||
Ранних ACK-пакетов 0; steady retries=0; ACK на одиночном 8Мбит/с пути 0.170..0.180Мбит/с.
|
||||
Остаётся нестабильность одиночного дуплекса 2Мбит/с: один повтор дал 1.672<1.700Мбит/с.
|
||||
Далее — этот недобор, STCP backpressure, отдельные проверки смены RTT/BW и повторного отказа.
|
||||
Обнаруженный на этом этапе дефект возраста min_rtt исправлен и проверен следующим этапом выше.
|
||||
Логи/CSV: /tmp/etcp-rx-clock-*; 8372 среза проверены, HTML /tmp/etcp-rx-clock-matrix.html.
|
||||
Подробности и предыдущие этапы: doc/etcp_multipath_plan.txt.
|
||||
Предыдущий этап — диагностика до исправления ACK:
|
||||
Продолжение 24.10.2026: диагностика срезов 100 мс (настройка 10..1000 мс), 104 поля; CSV и лог буферизуются до drain.
|
||||
Видны очереди, callbacks/deferred waiter, DATA/ACK, flight/cwnd, BBR/RTT/RTO, долг/готовность pacer и канал эмулятора.
|
||||
tools/etcp_diag_report.py проверяет CSV и строит автономные интерактивные графики с исходными срезами.
|
||||
Дуплекс: 598 срезов; two-stuck/ASAN: 5208/5196 срезов, без ошибок sanitizer/утечек; CHECK сохраняет накопленный лог.
|
||||
Итоговый check.sh 184/184, proxy 9/9, burst 1/1, load 8/8 PASS, 166 с; JS smoke-проверка графиков/фильтров PASS.
|
||||
На одиночном 8 Мбит/с пути ACK занимают 0.347/0.390 Мбит/с, среднее ожидание ACK-only 230/271 мкс вместо окна 2 мс.
|
||||
В срезах side=1 path=1 DATA готовы, а BBR задаёт около 6.3 Мбит/с; PROBE_RTT временно уменьшает окно до 32.9 тыс. байт.
|
||||
Дедлайн обходит посторонний wake sender. Дальше разбирать абсолютные deadlines ACK; простого !timer недостаточно.
|
||||
Алгоритмы протокола на этом этапе не менялись; полный дуплекс всё ещё не проходит скоростные пороги (5 нарушений, 8.513/8.337).
|
||||
Логи/CSV: /tmp/etcp-duplex-diag-*; просмотр: /tmp/etcp-duplex-diag-view.html; детали — doc/etcp_multipath_plan.txt.
|
||||
24.10.2026: реализованы сканирование send_q с приоритетом готовых повторов и ограниченная подгрузка новых DATA.
|
||||
Заблокированный пакет пропускается. История путей пакета (32 байта) исключает новый обход до перебора допустимых;
|
||||
история/RR фиксируются после принятого send, удаление линка очищает слот до callbacks/повторной выдачи ID.
|
||||
После 3 RTO без ACK-прогресса tx_stalled исключает путь из повторов при наличии незастрявшего UP-пути.
|
||||
Все застрявшие пути допускаются, новые DATA сохраняют прежний выбор; действующий ACK отправки после флага снимает его.
|
||||
Старый/чужой ACK не восстанавливает путь; reset очищает состояние. Новых очередей/таймеров/копий нет, RTO сохранён.
|
||||
Windows: 51 сценарий PASS с управляемыми/реальными часами; восемь ASAN/UBSan-тестов без ошибок/утечек.
|
||||
Контроль занятого на 1 мс рабочего пути: latency 377.012 вместо 736.623 мс, ready_wait 2.318 мс, профиль PASS.
|
||||
Отрицательный контроль задержки готового повтора 200 мс остаётся FAIL по очереди и общей задержке.
|
||||
Полная последовательная multipath-матрица: все 9 профилей отказов PASS, первые 6 скоростных профилей PASS.
|
||||
Two-stuck: latency 379.997 мс при пределе 462.219 мс; ещё 3/3 повтора — 374.981..378.557 мс.
|
||||
Load-drop/load-hold: паузы 39.234/39.355 мс, steady goodput 7.221/7.468 из 8 Мбит/с.
|
||||
Max latency под насыщенной нагрузкой 161.287/86.306 мс записана; отдельным порогом она здесь не ограничена.
|
||||
Two-stuck/load-drop также проходят ASAN/UBSan с detect_leaks=1 без ошибок/утечек.
|
||||
Остаётся дуплекс: 8.412/8.435 из 10 Мбит/с, все 7 нарушений матрицы — его скоростные пороги.
|
||||
Далее — дуплекс, STCP backpressure, отдельные проверки смены RTT/BW и повторного отказа.
|
||||
Полный check.sh: 184/184, proxy 9/9, burst 1/1, load 8/8 PASS, 134217728 проверенных байт, 197 с со сборкой.
|
||||
Длинная multipath-матрица запускается отдельно; её общий результат остаётся FAIL из-за дуплекса.
|
||||
Логи: /tmp/etcp-send-scan-*.log; подробности — doc/etcp_multipath_plan.txt.
|
||||
Предыдущая диагностика:
|
||||
24.10.2026: тесты отдельно проверяют обработку RTO (опоздание<=20мс), ожидание готового повтора (<=100мс)
|
||||
и доставку сверх заданной задержки канала (<=100мс). TX учитывается после принятия send.
|
||||
Быстрые профили сохраняют общий предел100мс; длинные учитывают по одному RTO отказавших путей плюс100мс.
|
||||
Остальные8 профилей PASS; two-stuck нестабилен: обычный прогон дал738.317мс при пределе459.332мс,
|
||||
история фрагмента324 —1->2->1->2->0. Готовый повтор не застревает: лишнее ожидание создаёт ещё один круг RTO.
|
||||
Контроль с занятостью рабочего пути всего1мс воспроизводит736.623мс latency и534.548мс паузы;
|
||||
точный лог показывает свободное окно рабочего пути и ожидание pacer при выборе2->1.
|
||||
Контроль задержки уже готового повтора на200мс проваливает новую проверку очереди, а не маскируется бюджетом RTO.
|
||||
Slow-drop/two-stuck/load-drop проходят ASAN/UBSan с detect_leaks=1 без ошибок/утечек.
|
||||
Полный check.sh184/184, proxy9/9, burst1/1, load8/8 PASS,134217728 байт,161с; multipath запускается отдельно.
|
||||
Перед изменением транспорта отдельно проработать выбор путей без DATA-прогресса, ограниченные пробы
|
||||
и восстановление, сохраняя физический UP и защиту от поздних ACK. Ранние копии исключены; дуплекс не менялся.
|
||||
Логи: /tmp/etcp-recovery-stages-*.log/.csv; подробности — doc/etcp_multipath_plan.txt.
|
||||
Предыдущий этап:
|
||||
24.10.2026: повтор ждёт другого initialized+UP канала, даже когда прежний уже готов.
|
||||
При отсутствии таких альтернатив разрешён прежний; DOWN последней альтернативы будит sender.
|
||||
Новых состояний/ресурсов/таймеров нет. ACK/control и burst-FILLER не блокируются ожиданием DATA.
|
||||
Регрессия до исправления воспроизводит возврат на прежний путь; после — windows44 сценария
|
||||
с управляемыми/реальными часами PASS, восемь ASAN/UBSan-тестов без ошибок/утечек.
|
||||
Полный check.sh184/184, proxy9/9, burst1/1, load8/8 PASS,134217728 проверенных байт,161с.
|
||||
Профили отказов:7/9 PASS. load-drop/load-hold проходят каждый3/3, паузы39.796..44.982мс,
|
||||
steady goodput7.189..7.476 из8Мбит/с оставшегося канала. Пороги сохранены.
|
||||
Slow-drop остаётся FAIL: пауза238.569мс, latency264.727мс; two-stuck219.338/380.177мс.
|
||||
В них4 нарушения порогов; доставка/порядок/ресурсы проходят. Полная скоростная матрица не повторялась.
|
||||
Трейс показал ожидание первого RTO на медленном пути и повторный выбор прежнего пути под нагрузкой;
|
||||
ожидание в хвосте send_q в этих опытах не объясняет основную паузу (максимум2.6/3.5мс).
|
||||
Ранние копии до RTO исключены пользователем24.10.2026. При UP повтор ждёт RTO своего пути;
|
||||
закрытие линка по-прежнему возвращает недоставленные DATA без ожидания RTO.
|
||||
При RTT200мс RTO уже243мс без jitter: порог паузы100мс этого ожидания не учитывает.
|
||||
Пороги тестов не менялись; дальше отдельно оценивать ожидание RTO, готовность повтора и доставку.
|
||||
Остаются здоровье/пробы каналов, STCP backpressure и дуплекс; спекулятивных ранних копий в плане нет.
|
||||
Логи: /tmp/etcp-retry-alternative-*.log/.csv; диагностический observer — /tmp/etcp-recovery-probe.c/.mk.
|
||||
Предыдущий контроль после исправления pacer: таймер ждёт первого разрешённого TB, а не погашения всего долга;
|
||||
кредит1мс, BBR/cwnd/min_rtt, RTO и wire-формат сохранены. Нового состояния и ресурсов нет.
|
||||
До исправления регрессия получает10TB вместо1TB; после него18 сочетаний долга/дроби проходят.
|
||||
Проверены INT_MAX, ранний/поздний callback, ограниченный burst, OOM повторного таймера,
|
||||
rate change и reset/close из настоящего uasync callback. Windows42 сценария PASS с обоими режимами часов;
|
||||
восемь профильных тестов ASAN/UBSan с detect_leaks=1 без ошибок/утечек.
|
||||
check.sh184/184, proxy9/9, burst1/1, load8/8 PASS (134217728 байт),162с.
|
||||
Полная последовательная матрица c7a53009 с прежними порогами: FAIL,14 нарушений —
|
||||
8 скоростных проверок дуплекса и6 проверок пауз/задержек slow-drop,two-stuck,load-drop/load-hold.
|
||||
Equal14.438/16, bandwidth9.052/10, RTT20/200:14.299/16Мбит/с; первые6 скоростных профилей PASS.
|
||||
Дуплекс8.394/8.458 из10Мбит/с остаётся FAIL. Drop-forward PASS: пауза58.001мс, задержка83.656мс.
|
||||
Паузы slow-drop/two-stuck/load-drop/load-hold:239.421/201.381/213.644/208.899мс;
|
||||
задержки slow-drop/two-stuck:264.044/482.693мс. Доставка, порядок и контроль ресурсов проходят.
|
||||
Это один прогон; сравнение с исходными15 нарушениями не доказывает устойчивое улучшение всей матрицы.
|
||||
Далее отдельно разбирать ограничения BBR/cwnd и PROBE_RTT, здоровье линков и STCP backpressure.
|
||||
Результаты/план: doc/etcp_multipath_plan.txt; /tmp/etcp-pacer-ready-matrix.log/.csv,
|
||||
/tmp/etcp-pacer-ready-{windows-final,real-time,full-check}.log и /tmp/etcp-pacer-ready-asan-test_*.log.
|
||||
Реализованы начальный pacing от 4*MSS/RTT, точные DATA B/s и ограниченный кредит shaper,
|
||||
разделение логического seq/payload и физических attempt_id, ACK/RTT/RTO каждого пути,
|
||||
ротация повторов A→B→C→A при UP-каналах, rollback отказавшего send и запрос потерянного till.
|
||||
Wire attempt_no сокращён до2 байт, общий для всех каналов одного seq; внутренний ключ — (seq<<16)|attempt_no.
|
||||
DATA header7 вместо13 байт, ACK entry10 вместо16. Номер1..65535; исчерпание номера/seq меняет эпоху через REINIT.
|
||||
Normalizer не менялся. Пара seq/номер не повторяется в одной эпохе; старые ACK отсекаются до разбора.
|
||||
Проверка компактного формата: windows38 сценариев, session/BBR/transport/init_liveness PASS;
|
||||
те же5 тестов ASAN/UBSan с detect_leaks=1 без ошибок/утечек. check.sh184/184, proxy9/9, burst1/1, load8/8 PASS.
|
||||
bandwidth2+8 RTT20/20:8.715Мбит/с, retries=0, профиль PASS. Логи: /tmp/etcp-attempt16-*.log/.csv.
|
||||
RTO=clamp(ceil(1.2*RTT+1.5*jitter+3мс),10мс,1с), округление до мкс. Пределы: попытки 16384/линк,
|
||||
32768/соединение, ACK 16384/линк. Без RTT начальный RTO=1с.
|
||||
STARTUP завершается после целого раунда устойчивой очереди выше бюджета, без искусственного изменения BW.
|
||||
BBR20/20, windows26 сценариев PASS; восемь тестов ASAN/UBSan с detect_leaks=1 PASS; основной набор183/183 PASS.
|
||||
check.sh завершён успешно; дополнительные tcp_proxy_full/burst/load PASS (8 соединений,134217728 проверенных байт).
|
||||
Полная штатная multipath-матрица до смены формулы RTO: FAIL, 20 нарушений. Есть недобор RTT20/200мс и bidirectional;
|
||||
slow-drop даёт паузу401мс, два зависших пути676мс, DROP/HOLD под нагрузкой229–243мс.
|
||||
После смены RTO один полный прогон даёт 15 нарушений: slow-drop244мс, two-stuck278мс,
|
||||
load-drop/load-hold96/83мс; установившиеся интервалы исправных каналов без повторов.
|
||||
windows31 (20 наборов проверки реального RTO), BBR20/20, session/transport PASS;
|
||||
windows/session/transport ASAN/UBSan с detect_leaks=1 PASS. Логи: /tmp/etcp-rto-margin-*.log.
|
||||
Jitter-контроль RTT20/200мс: при случайной добавке 0..10/30мс новая формула даёт отдельные лишние повторы,
|
||||
старая в контрольном прогоне — без повторов. При jitter30мс скорость снижена у обеих формул;
|
||||
коэффициенты не объявлены оптимальными. Логи: /tmp/etcp-rto-jitter-review.log, /tmp/etcp-rto-jitter-old.log.
|
||||
Последний check.sh после смены RTO: 182/183, FAIL только test_bgp_route_exchange (повтор отдельно с debug PASS).
|
||||
test_encrypted_attachment в этом прогоне PASS; воспроизведённый ранее дефект media остаётся открытым.
|
||||
Логи: /tmp/etcp-rto-margin-full-check.log, /tmp/etcp-rto-margin-bgp-repeat.log.
|
||||
Три прогона скоростей: 8+8 RTT20/20 — 13.868..14.083Мбит/с; RTT20/200 — 13.567..13.951;
|
||||
2+8+2 RTT20/80/200 — 10.566..10.749 из12; 2+8 одновременно в обе стороны — 8.080..8.379 из10 на направление.
|
||||
DATA-повторов в измеряемых исправных интервалах нет. Подробные результаты: /tmp/etcp-multilink-quality-20261023.csv/.txt.
|
||||
BGP-падение из-за INIT/remote_keepalive исправлено; отдельная задача ниже содержит итог проверки.
|
||||
Дуплекс проверен18 последовательными опытами в /tmp/etcp-duplex-native/, исходники проекта не менялись.
|
||||
ACK compression завышает BW:170502 байта приняты за186.36мс, sender sample использует150.73мс.
|
||||
Нативный прототип counter/time в ACK сокращает пики очереди до23.1мс вместо52.9мс, но все скоростные пороги не проходит.
|
||||
Коэффициент282/256+3мс и запас10мс при busy-флаге не дали устойчивого выигрыша, пики очереди выросли до35.5/33.7мс.
|
||||
В штатном агрегате ACK занимает0.515..0.559Мбит/с на направление (5.15..5.59% полосы), большая часть — ACK-only.
|
||||
Реализована только коррекция BW по точке приёма:16 байт добавляются при count>0, пустой ACK остаётся8-байтовым.
|
||||
Перестановка/дубли ACK, независимые часы, простой/reset, переполнения и границы MTU/формата проверены.
|
||||
Windows35 сценариев и ещё4 профильных теста PASS, те же5 тестов ASAN/UBSan без ошибок/утечек;
|
||||
check.sh184/184, proxy9/9, burst1/1, load8/8 PASS (/tmp/etcp-bw-correction-full-check.log).
|
||||
Отдельный дуплекс2+8 RTT20/80:8.223/8.182Мбит/с, пики очереди агрегата22.8мс, retries=0;
|
||||
профиль остаётся FAIL по5 прежним порогам скорости. bandwidth2+8 RTT20/20:8.844Мбит/с, PASS.
|
||||
RTT20/200,8+8:13.453Мбит/с, очередь<=24мс, retries=0, FAIL по3 прежним порогам скорости.
|
||||
Логи: /tmp/etcp-bw-duplex-final.log/.csv, /tmp/etcp-bw-{bandwidth,rtt}.log/.csv, /tmp/etcp-bw-asan-*.log.
|
||||
Объединение ACK в пределах прежнего дедлайна2мс отложено; окно/RTT/RTO и политика ACK не менялись.
|
||||
Дуплексный запас и busy-флаг не добавлялись и не считаются подтверждённым исправлением.
|
||||
One-way->duplex->one-way:3/3 PASS; ASAN/UBSan не нашли ошибок/утечек, скоростной профиль остаётся FAIL.
|
||||
Подробности/уточнённый план: doc/etcp_multipath_plan.txt; результаты: /tmp/etcp-duplex-native/results.json,
|
||||
/tmp/etcp-duplex-native/summary.csv и ack_bandwidth.csv. Политика окна прототипов в репозиторий не переносилась.
|
||||
Здоровье/пробы каналов и STCP backpressure пока не реализованы; ранние копии исключены, пороги тестов сохранены.
|
||||
OOM после принятия данных исправлен аварийной сменой эпохи с сохранением физических линков.
|
||||
Старый поток отбрасывается без воспроизведения; после согласования владельцы получают REINIT.
|
||||
Очистка reset не выделяет память; убрана реконструкция inflight, новых OOM-очередей/таймеров нет.
|
||||
Проверены отказы RX/TX, flush/waiter/session-таймеров, RNG, reset/close из callback и ноль выделений при reset.
|
||||
windows31 сценарий, queue_regressions77/77 и шифрованное восстановление обеих сторон PASS;
|
||||
десять тестов ASAN/UBSan с detect_leaks=1 PASS. Далее — здоровье каналов; ранние повторы исключены.
|
||||
После OOM-правок полный check.sh: 181/183; FAIL BGP route exchange и encrypted attachment.
|
||||
BGP отдельно с диагностикой PASS; encrypted attachment повторно FAIL из-за удаления активного temp кешем (задача ниже).
|
||||
Отдельно proxy9/9, burst1/1 и load8/8 PASS (134217728 проверенных байт); /tmp/etcp-oom-reinit-proxy-check.log.
|
||||
План и подробные результаты: [doc/etcp_multipath_plan.txt](etcp_multipath_plan.txt).
|
||||
Логи: /tmp/etcp-attempts-matrix.log, /tmp/etcp-attempts-asan-test_*.log,
|
||||
/tmp/etcp-oom-reinit-asan-test_*.log, /tmp/etcp-abort-encrypted-recovery.log.
|
||||
|
||||
[+] **Crypto: повторное освобождение ключа после отказа инициализации потоковой подписи** — исправлено 24.10.2026:
|
||||
Оба init создают ключ и SHA-512-контекст локально, заполняя state только после полного успеха.
|
||||
Перед первым использованием state={0}; отказ оставляет его пустым. Повторный init активного state
|
||||
отклоняется без потери ресурсов и накопленного хеша. Cleanup обнуляет все поля и допускает повторы.
|
||||
Final проверяет capacity до финализации; недостаточный буфер позволяет повторить вызов с сохранённым хешем.
|
||||
Контракты и обработка ошибок отражены в .h и примере документации; формат подписи прежний.
|
||||
Регрессия до исправления падает на непустом state после отказа EVP_MD_CTX_new.
|
||||
Проверены 6 отказов init, 8 отказов final/verify, 4 сочетания повторного init, capacity0/63/64,
|
||||
повторная очистка/использование и совпадение с независимой one-shot подписью SHA512 сообщения.
|
||||
Тесты не перехватывают free. Восемь профильных тестов ASAN/UBSan с detect_leaks=1 PASS;
|
||||
check.sh184/184, proxy9/9, burst1/1, load8/8 PASS. Пример документации компилируется с -Werror.
|
||||
Логи: /tmp/sc-sign-regression-before.log, /tmp/sc-sign-fix-full-check.log, /tmp/sc-sign-fix-asan-*.log.
|
||||
|
||||
[+] **Media: очистка кеша удаляла временный файл активной загрузки** — исправлено 24.10.2026:
|
||||
file_io хранит общий защищённый mutex реестр владельцев temp по фактическим нормализованным путям.
|
||||
Владение сохраняется от создания до publish/discard, включая интервал после finish; очистка кеша
|
||||
проверяет владение и удаляет сироту под тем же mutex. Угадывание суффиксов не используется.
|
||||
Регрессии проверяют открытый/закрытый temp, барьеры создания/finish и очистку после реального BLOCK_CHUNK.
|
||||
Полный check.sh: 184/184, proxy9/9, burst1/1, load8/8 PASS; пять профильных тестов ASAN/UBSan PASS,
|
||||
test_encrypted_attachment дополнительно повторён пять раз без отказов.
|
||||
Логи: /tmp/media-orphan-full-check.log, /tmp/media-orphan-asan-*.log, /tmp/media-orphan-repeat-*.log.
|
||||
|
||||
[+] **ETCP: входящий INIT сохранял устаревший признак обратной достижимости** — исправлено 23.10.2026:
|
||||
Принятый INIT обновляет RX и remote_keepalive до переиспользования буфера для ответа; flag_up=0 снимает ложный UP.
|
||||
INIT/INIT_RESPONSE и обычный RX публикуют переходы живости с защитой от close/удаления линка из callback.
|
||||
При DOWN handshake продолжает ответ/keepalive; исправный второй линк сохраняет UP соединения.
|
||||
INIT_RESPONSE проверяет длину тела (>=64 байт без timestamp/flag_up) и node_id до изменения состояния.
|
||||
Раньше проверка общей длины принимала усечённое тело 61..63 байта. Новых таймеров/очередей/полей нет.
|
||||
test_etcp_init_liveness: 130 отказов без изменения link/conn и передачи владения pkt, 12 допустимых ответов;
|
||||
оба направления односторонней потери, оба порядка ключей, восстановление без смены эпохи, резервный линк,
|
||||
close и удаление link из INIT/keepalive callback PASS, также с обычными часами.
|
||||
Семь профильных тестов ASAN/UBSan с detect_leaks=1 PASS; дополненный тест удаления link повторён отдельно.
|
||||
BGP route exchange: 100/100 PASS (A<B:48, A>B:52; последние пять с debug). Основной набор: 184/184 PASS.
|
||||
Логи: /tmp/etcp-init-boundary-before.log, /tmp/etcp-init-final-{liveness,realtime,asan-liveness}.log,
|
||||
/tmp/etcp-init-fix-asan-test_*.log, /tmp/etcp-init-bgp-stable-*.log, /tmp/etcp-init-fix-full-check.log.
|
||||
|
||||
[+] **ETCP/BBR: бюджет очереди в установившемся режиме** — реализовано 23.10.2026 по выбору пользователя:
|
||||
В PROBE_BW окно ограничено max(4*MSS, BDP + BW*20мс), с насыщением арифметики и учётом ACK/PROBE_UP резервов
|
||||
внутри общего предела. Ограничение применяется также на fast path и до callback. 0 отключает политику.
|
||||
STARTUP/DRAIN не ограничены этим бюджетом; это мягкая цель модели, а не гарантия физической очереди <=20мс.
|
||||
До политики bandwidth давал очередь 47.1–47.9мс >40мс при корректных единицах и сложении резервов;
|
||||
после политики профиль проходит по скорости и очереди, equal также PASS. Разные RTT и отказы не закрыты.
|
||||
BBR 18/18, windows 16 сценариев PASS; BBR/windows/session/transport/STCP traffic/NCD ASAN/UBSan
|
||||
с detect_leaks=1: 6/6 PASS. Полный check.sh: 180/183, FAIL NAT detection/fault и encrypted_attachment;
|
||||
связь падений с BBR не установлена, отдельная открытая задача сохраняется.
|
||||
Логи: /tmp/etcp-queue-budget-fastpath.log, /tmp/etcp-queue-budget-windows.log,
|
||||
/tmp/etcp-queue-limited-asan-results.log, /tmp/etcp-queue-limited-asan-test_*.log,
|
||||
/tmp/etcp-queue-limited-full-check.log. Политика описана в etcp_bbr.h, etcp_bbr_doc.md и doc/etcp_protocol.txt.
|
||||
|
||||
[+] **ETCP/BBR: единое окно DATA и корректное осушение PROBE_RTT** — исправлено 23.10.2026:
|
||||
BBR использует MSS=normalizer->frag_size, а не wire-MTU/размер STCP-кадра. Начальное окно UDP/STCP равно 4*MSS;
|
||||
MTU пересчитывается после включения нового линка в список. init_cwnd инициализируется четырьмя фрагментами.
|
||||
Транспорт применяет положительное окно с верхним ограничением bbr_max_cwnd, не поднимая его до 8192.
|
||||
INFLIGHT_LIM_MIN=8192 остаётся нижней допустимой границей настройки максимального окна; нулевое окно отклоняется с логом.
|
||||
PROBE_RTT осушает до max(4*MSS, 0.5*BDP), ждёт >=200мс и раунд ACK, затем восстанавливает предыдущее окно.
|
||||
Отправленные при осушении DATA помечаются app_limited. Ограничения после потерь больше не уменьшают окно ниже 4*MSS.
|
||||
BBR 15/15, отправитель 16 сценариев; ETCP session/transport, STCP traffic и NCD PASS.
|
||||
Те же шесть тестов ASAN/UBSan с detect_leaks=1: 6/6 PASS, ошибок и утечек не обнаружено.
|
||||
Общий check.sh: 181/183, FAIL test_nat_detection_faults/test_nat_detection (contact_history/UNKNOWN);
|
||||
test_encrypted_attachment в этом прогоне PASS. NAT-падения и отдельный повтор отмечены следующей открытой задачей.
|
||||
Логи: /tmp/etcp-probe-test_*.log, /tmp/etcp-probe-asan-results.log, /tmp/etcp-probe-asan-test_*.log;
|
||||
полный прогон: /tmp/etcp-probe-full-check.log.
|
||||
исходные нарушения: /tmp/etcp-probe-contract-{bbr,windows}-before.log, /tmp/etcp-probe-lossfloor-before.log.
|
||||
|
||||
[+] **ETCP/BBR: байтовые единицы, delivery clock и независимость ACK от локального DATA-pacer** — исправлено 23.10.2026:
|
||||
Устранено повторное умножение на MSS при расчёте pacing/BDP: доставка 1MB/s давала 570240000B/s при MSS=576,
|
||||
теперь cruise pacing=990000B/s независимо от MSS. Q16 байт/мкс сохраняет многогигабитные оценки;
|
||||
выходы pacing/BDP/cwnd и рост inflight_hi насыщаются вместо переполнения.
|
||||
PROBE_UP растит inflight_hi на MSS за подтверждённое окно; резерв равен 2*MSS, Reno rounds считаются в пакетах.
|
||||
Счётчики ACK aggregation расширены до 32 бит, подтверждения свыше 64KiB больше не усекаются.
|
||||
Delivery sample использует снимки delivered/time/send-time пакета и max(send_elapsed, ack_elapsed);
|
||||
несколько ACK в одном tick больше не создают ложный пик скорости. Суммарный delivered растёт только на новые ACKed байты.
|
||||
ACK без DATA обходит локальный DATA-cwnd и DATA-pacer и не начисляет их нагрузку: низкая исходящая DATA-скорость
|
||||
получателя не тормозит обратные подтверждения. Физическая очередь канала остаётся общей для DATA/ACK.
|
||||
dummynet измеряет максимальное время очереди/сериализации отдельно от заданной propagation delay;
|
||||
multipath проверяет его предел base_RTT+20ms наряду с полезной утилизацией, FILLER/повторы не засчитываются.
|
||||
BBR 14/14, window/sender 14 сценариев PASS. ASAN/UBSan с detect_leaks=1: BBR/windows/calibration/session/transport/STCP/NCD 7/7;
|
||||
после последнего исправления переполнения BBR/windows повторены отдельно. Основная multipath-задача остаётся открытой.
|
||||
Общий check.sh перед последним исправлением переполнения: 181/183; падения вынесены отдельной задачей ниже.
|
||||
Логи: /tmp/etcp-recovery-overflow-{before,after,windows}.log, /tmp/etcp-recovery-asan-results.log,
|
||||
/tmp/etcp-recovery-asan-overflow-{bbr,windows}.log, /tmp/etcp-multipath-recovery-check.log.
|
||||
|
||||
[ ] **Разобрать падения NAT detection/fault и encrypted attachment в общем прогоне** — обнаружено 23.10.2026:
|
||||
check.sh: test_nat_detection_faults завершился assert в run_alias (ожидался inbound OPEN, получен UNKNOWN/contact_history);
|
||||
отдельный повтор тоже упал, уже в run_case mode=5: mapping DEPENDENT корректен, inbound UNKNOWN вместо ожидаемого.
|
||||
test_encrypted_attachment достиг phase=3/12001 heartbeat и упал; отдельный повтор завершился phase=10/560 heartbeat PASS.
|
||||
После исправления окна BBR общий прогон снова 181/183: encrypted_attachment PASS, но FAIL оба NAT-теста.
|
||||
test_nat_detection_faults упал в run_case mode=1 (UNKNOWN/contact_history); test_nat_detection не дождался профиля
|
||||
за 18с на socket=udp. Его отдельный повтор тоже упал, уже на socket=second, с той же причиной contact_history/UNKNOWN.
|
||||
После бюджета очереди полный check.sh: 180/183, повторились оба NAT-падения и encrypted_attachment.
|
||||
Связь с изменениями BBR не установлена. Разобрать фикстуры/таймеры по логам, не ослаблять ожидания.
|
||||
Логи: tests/logs/test_nat_detection_faults.log, tests/logs/test_encrypted_attachment.log,
|
||||
/tmp/etcp-recovery-nat-retry.log, /tmp/etcp-recovery-attachment-retry.log,
|
||||
/tmp/etcp-probe-full-check.log, /tmp/etcp-probe-nat-retry.log, /tmp/etcp-queue-limited-full-check.log.
|
||||
|
||||
[+] **ETCP: единый установщик прямого транспорта через NCD** — исправлено 22.10.2026:
|
||||
Удалён неиспользуемый рабочим кодом etcp_connect с EARLY/LATE, отдельными линками и таймерами;
|
||||
три тестовых файла переведены на NCD handles с начала подключения. Сохранены проверки независимых владельцев,
|
||||
отмены отложенного UP, TIMEOUT, выхода из UTUN с сохранением транспорта и shutdown с pending LEAVE.
|
||||
Состояние реестра, физический UP, готовность ETCP-сессии и READY группы разделены в контрактах.
|
||||
Начальный отправитель не создаёт преждевременный UP. Merkle-фикстура ждёт готовности обоих потоков.
|
||||
|
||||
[+] **ETCP: индивидуальные окна DATA, ACK и корректный учёт повторов** — исправлено 22.10.2026:
|
||||
Выбор линка проверяет место для целого DATA, ACK не блокируется DATA-cwnd; burst без места использует FILLER.
|
||||
WAIT_ACK занимает окно, RTO освобождает его до постановки в WAIT_SEND; поздний ACK не вычитает байты повторно.
|
||||
BBR получает факт ограничения окном до вычитания ACK; app_limited учитывает подготовленные данные и normalizer.
|
||||
Один call_soon объединяет пробуждения без таймерной задержки/рекурсии; OOM retry через 10ms, reset/close отменяет token.
|
||||
Невозможность запланировать отправку закрывает транспорт отложенно, сохраняя вызывающий стек.
|
||||
Проверяются размер max_cwnd, переполнение суммы окон; ошибки [global] теперь отклоняют конфигурацию.
|
||||
Новый test_etcp_windows: 12 сценариев настоящих очередей/отправителя, сверка счётчиков и внедрение отказов.
|
||||
STCP-тест проверяет каждый байт двустороннего 1MiB трафика при окнах 8KiB и независимых тестовых командах.
|
||||
ASAN/UBSan с detect_leaks=1: windows/session/transport/connect/stcp_traffic/SIGPIPE/NCD, 7/7;
|
||||
результаты /tmp/etcp-asan-complete-results.log, логи /tmp/etcp-asan-complete-logs/.
|
||||
Финальный check.sh: 182/182, TCP proxy 9/9, burst/load PASS (210s);
|
||||
полный лог /tmp/etcp-full-check-complete.log. Ветка fix/etcp-connect-link-windows.
|
||||
|
||||
[+] **STCP: запись в закрытый TCP-сокет завершала библиотечный процесс SIGPIPE** — исправлено 22.10.2026:
|
||||
В общем прогоне STCP traffic завершался сигналом 13 после передачи данных, при teardown;
|
||||
воспроизведено первым же повтором ASAN. Оба send-пути STCP используют socket_send с MSG_NOSIGNAL
|
||||
либо SO_NOSIGPIPE; обработчики сигналов процесса не меняются. Ошибка EPIPE обрабатывается штатным путём.
|
||||
test_stcp_sigpipe проверяет raw/buffered send в дочерних процессах с SIGPIPE=SIG_DFL.
|
||||
20 повторов STCP traffic под ASAN/UBSan с detect_leaks=1 прошли без сигналов/ошибок/утечек;
|
||||
логи /tmp/etcp-asan-traffic-fixed-*.log, детерминированная проверка включена в общий Linux-набор.
|
||||
|
||||
[ ] **Аудит .h ETCP/STCP: оставшиеся расхождения контрактов** — проверено 22.10.2026:
|
||||
stcp.h: do_close уже очищает оба sc_stream и планирует free для allocated/free_on_close; комментарий утверждает обратное.
|
||||
Разделить контракты finish (drain/запрет новых данных/до 1с) и do_close (немедленное закрытие/потеря очередей/callback).
|
||||
Описать владение внешними rx_queue/tx_queue, преусловия размера frame и callback/reentrancy/lifetime.
|
||||
stcp_client.h: ping автоматически освобождается после завершения; NULL на ошибке запуска не вызывает ping callback.
|
||||
RTT пинга — uint16_t в 0.1мс, от отправки STCP handshake, без TCP/SOCKS/REALITY dial; явно описать срок out_cli/handle.
|
||||
inst также нужен для NTP-времени и профиля REALITY, а не только padding автономного STCP.
|
||||
stcp_link.h/stcp_server.h: дополнить параметры, возвраты, время жизни callbacks/inst и различие handles
|
||||
серверного адаптера stcp_server_listen и базового stcp_server_create (одно имя opaque-типа, разные реализации).
|
||||
etcp.h: ref_count удерживает объект/phase-2, но close уже освобождает normalizer и линки в phase-1;
|
||||
reset_done не блокирует reinit (в реализации только запись/диагностика); last_rx_id не обновляется при RX.
|
||||
etcp_connection_ready объявлена без реализации/вызовов; рабочая функция — etcp_conn_ready.
|
||||
etcp_connections.h проверен частично: у data_len исключены все 3 байта timestamp/flag, не только timestamp;
|
||||
send_hook уступает SOCKS UDP; etcp_send_tcp_ping при отказе запуска возвращает отрицательный код без callback.
|
||||
etcp_session.h и etcp_padding.h сверены с реализацией без подтверждённых смысловых расхождений.
|
||||
Подробности: /tmp/protocol-header-audit.txt. Продолжить протоколы перед routing/BGP; media/file_transfer отложен.
|
||||
|
||||
[+] **Android: приём рации через SCO после отпускания PTT** — исправлено 22.10.2026:
|
||||
Общий MediaAudioController сохраняет разговорный тракт для SCO-only выхода, даже без подписчиков микрофона.
|
||||
AudioTrack рации снова использует USAGE_VOICE_COMMUNICATION: Android учитывает playback как разговорную активность.
|
||||
@@ -25,23 +456,148 @@
|
||||
ma_copy_file проверяет существование/идентичность, сохраняет один regular-файл при ./ или hardlink;
|
||||
ссылки назначения отклоняются, копирование идёт через соседний temp с проверенными записью/close/publish.
|
||||
Общие примитивы lib/file_io используются также video, сохраняя публикацию видео только в completion.
|
||||
ma_uuid возвращает ошибку RNG и обнуляет выход; media_index не записывает результат при отказе media/block UUID.
|
||||
ma_uuid возвращает ошибку RNG и обнуляет выход; media_index не регистрирует блоки при отказе media/block UUID.
|
||||
ma_file_size и переменные вызывающего кода — int64_t; проверяются смещения, число блоков и размеры выделений.
|
||||
Обход передаёт ошибки, пропускает ссылки/reparse points, проверяет длину/глубину; Windows использует wide API.
|
||||
Контракты .h и service_lifecycle описывают синхронные отказы, лимит 16 до completion и отсутствие отката work при отмене.
|
||||
Неверные аргументы дают -1, closing/destroy — CANCELLED; повторный вход в destroy диагностируется и отклоняется.
|
||||
Новый test_media_primitives проверяет сохранность при I/O/fdopen ошибках, RNG/seek без записей в БД,
|
||||
Новый test_media_primitives проверяет сохранность при I/O/fdopen ошибках, RNG/seek без записей блоков в БД,
|
||||
sparse-размеры на границах 2/4 ГиБ, ошибки обхода, ссылки/глубину и ownership задач с внедрением ошибок.
|
||||
Все шесть тестов media_primitives/video/media_index/attachment_send/services/dm_worker прошли ASAN/UBSan с detect_leaks=1.
|
||||
Общий check.sh: 176/176 и proxy/burst/load. Windows runtime/сборка не проверялись: кросс-компилятора в окружении нет.
|
||||
Логи: /tmp/media-primitives-full-check-final.log, /tmp/media-primitives-final.log, /tmp/media-asan-test_*.log.
|
||||
|
||||
[ ] **media_index_commit: не обрезать длинный location** — статическая проверка 22.10.2026:
|
||||
Относительный dest_path записывается через snprintf в location[512] без проверки длины;
|
||||
register_async допускает dest_path до 1023 байт, а media_base может не сокращать его до 511 байт.
|
||||
При таком пути в БД попадёт обрезанное имя. Проверить сценарий на реальном вложенном пути,
|
||||
затем отклонять непредставимый location до INSERT либо согласовать размеры со всеми читателями.
|
||||
Найдено при сверке вызывающего кода media_async; воспроизведение и исправление отложены.
|
||||
[+] **media_index: пути без обрезания и единая media_base** — исправлено 22.10.2026:
|
||||
Общие location/path проверяют границу базы, компоненты и пределы UTF-8 путей (1024/512 с NUL).
|
||||
Путь в 550 байт полностью сохраняется, читается и удаляется. Проверки предшествуют файловой подготовке.
|
||||
Выдача использует UTF-8 file_io; загрузчик проверяет общую базу, stop/start сохраняет выбранную базу БД.
|
||||
Писатели, backfill, выдача и удаление используют один формат; контракты находятся в media_index.h.
|
||||
|
||||
[+] **media_index_commit: атомарная регистрация и проверяемые повторы** — исправлено 22.10.2026:
|
||||
Единый commit регистрирует весь авторский/скачанный файл через SAVEPOINT; key=NULL означает скачанный файл.
|
||||
Проверяются геометрия/массивы, bind/step/finalize и управление транзакцией.
|
||||
Ошибка второго блока откатывает оба пути регистрации; обычная ошибка сохраняет внешнюю транзакцию.
|
||||
SQLite может сама откатить её целиком — это обнаруживается и диагностируется.
|
||||
Совпадающий повтор сохраняет node_sign и timestamp; противоречащие метаданные дают ошибку без замены старых.
|
||||
Файловые изменения worker не входят в SQL-атомарность.
|
||||
|
||||
[+] **Удаление канала: дождаться исходящих медиа и учесть незавершённые файлы** — исправлено 22.10.2026:
|
||||
Канал ждёт свои video/register completions; новые исходящие задачи отклоняются до завершения удаления.
|
||||
Последний completion продолжает удаление. Подготовленные ожидающие отправки не становятся новыми сообщениями.
|
||||
Контекст остаётся у владельца во время синхронного MSG_SUBMITTED; повторный вход обработчика проверен.
|
||||
До worker путь учитывается в media_preparations; успешный commit снимает запись в той же транзакции.
|
||||
После ошибки хеширования/отмены опубликованная копия остаётся известна для очистки канала.
|
||||
Проверены две ожидающие задачи, повтор удаления, создание того же channel_id и удаление из обработчика события.
|
||||
|
||||
[+] **media_index_delete_channel: ошибки и повторяемая частичная очистка** — исправлено 22.10.2026:
|
||||
Полный снимок и проверка путей предшествуют удалению; внешняя транзакция отклоняется.
|
||||
Общие файлы других каналов сохраняются; ENOENT считается успехом.
|
||||
SQL/файловые ошибки возвращают -1, записи неочищенных путей сохраняются для повтора.
|
||||
Проверен отказ DELETE после unlink: повтор завершает очистку без восстановления файла.
|
||||
Очистка включает media_preparations; ошибки очистки и удаление метаданных канала сообщаются раздельно.
|
||||
|
||||
[+] **media_index_init: ошибки всех индексов доходят до запуска чата** — исправлено 22.10.2026:
|
||||
Проверяется каждый CREATE INDEX. Ошибка прерывает chat_core_init с освобождением собственных ресурсов;
|
||||
общая БД ядра сохраняется. Проверены запрет каждого индекса и успешный повтор старта после снятия запрета.
|
||||
Дополнительно исправлены дубликат start (отказ без callback) и доступ к dl после синхронного завершения QUERY.
|
||||
Тесты используют реальную схему БД и проверяют новый контракт владения.
|
||||
Итог: check.sh — 177/177, proxy/burst/load PASS, 159с.
|
||||
Девять тестов index_regressions/index/delivery_download/download_timeout/services/delivery_full/delivery_chat/attachment_send/video
|
||||
прошли ASAN/UBSan с detect_leaks=1. Windows runtime/сборка не проверялись: кросс-компилятора нет.
|
||||
Логи: /tmp/media-index-full-check-final.log, /tmp/media-index-final-test_media_index_regressions.log,
|
||||
/tmp/media-index-asan-test_*.log. Новый тест: tests/test_media_index_regressions.c.
|
||||
|
||||
[+] **media_delivery QUERY/QUERY_RESP: проверять длины до чтения записей** — исправлено 22.10.2026:
|
||||
Проверяются точный размер, счётчики и все записи до SQL/изменений состояния. Ответ связан с media_id,
|
||||
UUID попытки, группой и end-to-end отправителем router; дубликаты, истёкшие и старые ответы игнорируются.
|
||||
test_media_query защищает точную границу payload под ASAN и проверяет все усечения, чужие ответы,
|
||||
повреждение последней записи без изменения первой, отмену и повторный start того же media_id.
|
||||
|
||||
[+] **media_delivery QUERY: устранить потерю блоков и держателей из-за разных лимитов** — исправлено 22.10.2026:
|
||||
Одна пачка до 32 block_id — один полный ответ, до четырёх кандидатов на каждый блок (максимум 1604 байта).
|
||||
Нулевой результат и ошибка БД различаются. Неполная пачка не принимается; держатели других пачек и текущие CM сохраняются.
|
||||
QUERY имеет отдельный дедлайн и курсор источников каждого блока; чанки не продлевают поиск остальных блоков.
|
||||
Источники своей CHAT-группы, автор последний; HAVE_BLOCK не зависит от курсора поиска.
|
||||
Peer-слоты переиспользуются после освобождения; SLOT/SEARCH/RETRY различаются, OVERLOADED соблюдает retry_after.
|
||||
Отменяется поздний CM UP, события обрабатываются после open. Устранено двойное освобождение при отказе router.
|
||||
Проверены 65 блоков/260 записей, старт 4100 блоков, сборка с 13 держателями при 10 слотах,
|
||||
настоящий watchdog при входящих чанках, SQL-ошибка второго блока, RNG/send failures и отсутствие повторного done_cb.
|
||||
Сетевой integration проверяет содержимое QUERY_RESP вместо безусловного smoke PASS.
|
||||
Восемь тестов query/delivery_download/download_timeout/delivery_sql/delivery_integration/delivery_chat/delivery_full/services
|
||||
прошли ASAN/UBSan с detect_leaks=1. Итоговый check.sh: 178/178, proxy/burst/load PASS, 172с.
|
||||
Логи: /tmp/media-query-regressions.log, /tmp/media-query-full-check-final.log, /tmp/media-query-asan-final-test_*.log.
|
||||
Windows runtime/сборка не проверялись: кросс-компилятора нет. Контракты — в соответствующих .h.
|
||||
|
||||
[+] **media_delivery QUERY_RESP: устранить неверное поле chunk** — исправлено 22.10.2026:
|
||||
Неиспользуемые chunk/rtt удалены из нового формата QUERY_RESP. Блок однозначно задаётся block_id,
|
||||
а его номер загрузчик берёт из сохранённых метаданных; позиция в выборочной пачке не считается номером блока.
|
||||
|
||||
[+] **media_delivery CHUNK: прогресс только после проверенного приёма и записи** — исправлено 22.10.2026:
|
||||
Реальные router callbacks/SQLite, корректные подпись блока и ключ автора. CHUNK с data_len=1 без payload
|
||||
оставляет файл пустым, но устанавливает bytes_received=1, RECV и обновляет last_progress_tb.
|
||||
То же при внедрённом fwrite=0/ENOSPC. Текущий unit-тест допускает 456 байт при expected_size=100.
|
||||
Проверять полный payload, геометрию/непрерывность, текущего держателя и попытку до файловых операций;
|
||||
не считать дубли и неудачную запись прогрессом. Нужна привязка пакетов к отдельной попытке передачи блока.
|
||||
Стенд /tmp/media-block-audit.c; логи /tmp/media-block-audit-truncated.log, /tmp/media-block-audit-write.log.
|
||||
Реализованы строгие размеры/непрерывность, tuple группы/держателя/блока/UUID и отдельный temp каждой попытки.
|
||||
Прогресс публикуется после проверенного close. Регрессии: tests/test_media_blocks.c, включая реальные I/O-сбои.
|
||||
|
||||
[+] **media_delivery DONE: проверка ключа и конечный результат ошибки хеша** — исправлено 22.10.2026:
|
||||
Для настоящей Ed25519 пары с pubkey[0]=0 корректно подписанный блок отвергается: наличие ключа определяется
|
||||
первым байтом. При отсутствии ключа проверка заменяется сравнением wire block_sig с метаданными.
|
||||
Отдельно: при верной подписи блока и несовпадающем хеше файла state=DONE, validated=0/1, active=1;
|
||||
три проверки watchdog возвращают 0, done не вызывается. Итоговая ошибка должна завершать загрузку,
|
||||
а публикация назначения и регистрация допустимы только после проверенных чтения/записи/close/SHA256.
|
||||
Обязателен ключ ровно 32 байта; проверяется подпись метаданных по фактическим байтам и ожидаемый размер.
|
||||
Сборка идёт в собственный temp, SHA-256 проверяется всегда, затем publish и регистрация SQLite.
|
||||
Ошибка до publish сохраняет старое назначение; ошибка SQLite сохраняет подготовленный опубликованный файл.
|
||||
Регрессии с настоящим pubkey[0]=00, неверной подписью/хешем, сбоями I/O/rename/SQLite: tests/test_media_blocks.c.
|
||||
|
||||
[+] **media_delivery progress_cb: отмена освобождает dl внутри обработчика DONE** — исправлено 22.10.2026:
|
||||
progress_cb вызывает media_download_cancel, md_dl_finish освобождает dl; media_download_handle_done затем
|
||||
вызывает md_dl_send_have_block с освобождённым указателем. ASAN heap-use-after-free, чтение 8 байт.
|
||||
Обработчик явно удерживает контекст. Terminal снимает загрузку с индекса и отменяет её timers/CM/waiters;
|
||||
освобождение и done выполняются после выхода последнего удерживающего обработчика, ровно один раз.
|
||||
После progress проверяется terminal: отменённая загрузка не продолжает финализацию. Один перенос callback
|
||||
в конец функции/отложенный финализатор не заменяет владение. Relay-получатели отдельно удерживают файл блока.
|
||||
Проверить также отмену с повторным start того же media_id, отмену на последнем progress и поздние пакеты.
|
||||
Лог /tmp/media-block-audit-cancel-asan.log; стенд вне репозитория /tmp/media-block-audit.c.
|
||||
Реализовано явное удержание контекста обработчиком; отдельные ссылки producer/выдач удерживают файл блока.
|
||||
Единый TX pump готовых/растущих источников соблюдает backpressure для DATA и DONE; remote CANCEL привязан к UUID.
|
||||
Проверены отмена/повторный start из последнего progress и старые relay-читатели при успехе/отмене новой загрузки.
|
||||
Полный ./check.sh: 179/179, proxy/burst/load PASS (/tmp/media-block-full-check-final.log).
|
||||
Десять целевых наборов ASAN+UBSan с detect_leaks=1: PASS (/tmp/media-block-asan-test_*.log).
|
||||
Windows runtime/сборка не проверялись. Синхронные проверка и сборка блоков остаются в uasync.
|
||||
|
||||
[+] **media_delivery: исправить репликацию суперузлов** — исправлено 22.10.2026. Воспроизведения прежней реализации:
|
||||
SUPER_REPL с num_entries=1 и одним заголовком: ASAN use-after-poison при чтении id в media_delivery.c:622.
|
||||
ACK от известного пира в другой router-группе без ожидающей пачки: inflight 0→255, tx_cursor=77, запускается timer.
|
||||
HELLO двух пустых узлов: после 20 доставок остаётся следующий HELLO, обмен сам не завершается.
|
||||
Для 9 записей sender ставит 4 одинаковые пачки первых 4 записей. По таймауту 20000→40000 при inflight=4
|
||||
отправляет 0 повторов, хотя timer снова вооружён. На потерю ACK эта схема не восстанавливается.
|
||||
При id=4294967297 первая запись остаётся 64-битной, но seq максимума пачки усечён до 4.
|
||||
Ошибка INSERT второй строки оставляет первую применённой, сохраняет RX cursor=2 и отправляет ACK=2: теряются данные.
|
||||
Группа из тела HELLO переопределяет router-группу (111→333); ответ включает записи групп 111 и 222.
|
||||
Старая пачка уменьшает сохранённый RX cursor 2→1. После supernode stop/start чужие записи удалены,
|
||||
но cursor=2 сохранён и рекламируется в HELLO. Принятая запись не будит выдачу третьему готовому супер-пиру.
|
||||
Уточнение прежней записи: смешения направлений курсоров в БД нет — md_ss_set вызывается только при приёме REPL;
|
||||
ACK обновляет peer_last_recv_id в памяти. Исправлять нужно подтверждение ожидающей пачки и жизненный цикл курсоров.
|
||||
Стенд вне репозитория /tmp/media-super-audit.c; обычные логи /tmp/media-super-audit-{ack,hello,pages,timeout,wide,sql,group,purge,replay,forward}.log.
|
||||
ASAN: /tmp/media-super-audit-truncated-asan.log, exit=1 ожидаем из-за подтверждённой ошибки, не успешный regression.
|
||||
Реализованы receiver-driven SYNC_REQ/SYNC_RESP и CHANGED вместо HELLO/ACK/inflight: одна страница с UUID на (group,peer).
|
||||
Группа/источник только из router, актуальные подписанные роли обоих участников; CM владеет прямым/непрямым путём.
|
||||
Страницы до 32 строк, little endian, 64-битные ID; high-water из sqlite_sequence, сохранённая эпоха каталога.
|
||||
Весь RX packet и курсор фиксируются собственной транзакцией после полной проверки, включая ошибки commit/rollback SQLite.
|
||||
Идемпотентный максимум (status,timestamp) выдаёт новый локальный ID только при изменении; собственные блоки сеются из media_files.
|
||||
Таймаут повторяет прежний запрос, CHANGED/dirty и периодическая сверка не теряют обновление около последней страницы.
|
||||
Репликация соблюдает backpressure; ограниченный слот ответа и watchdog восстанавливаются после reset router send_q.
|
||||
Потеря роли/удаление канала очищают строки вместе с RX-курсорами только своей группы; обычный stop сохраняет БД.
|
||||
test_media_replication: 10000 строк, id>2^32, усечения/дубли/чужие ответы, SQL read/write/commit faults,
|
||||
потеря запросов/ответов/CHANGED, DOWN/UP, новая эпоха, две группы, смена роли, три узла, заполнение/reset очередей.
|
||||
SQL-тест использует настоящую схему; сетевые full/chat/integration — CHAT-группы и подписанные роли member_sync.
|
||||
Финальный ./check.sh: 180/180, proxy 9/9, burst/load PASS (/tmp/media-repl-full-check-final.log).
|
||||
11 целевых наборов ASAN+UBSan с detect_leaks=1: PASS (/tmp/media-repl-asan-final-test_*.log).
|
||||
Контракты обновлены в media_delivery.h/media_delivery_proto.h, sync.md и doc/service_lifecycle.md. Windows не проверялся.
|
||||
|
||||
[+] **test_stream_service: убрать зависимость bootstrap-проверки от гонки TCP-handshake** — исправлено 22.10.2026:
|
||||
Падение на строке 409: REINIT=0 при probes=1/subflows=1/failures=0 и доставленной первой пачке.
|
||||
|
||||
+174
-23
@@ -6,6 +6,28 @@
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
static SRWLOCK temp_mutex = SRWLOCK_INIT;
|
||||
#define TEMP_LOCK() AcquireSRWLockExclusive(&temp_mutex)
|
||||
#define TEMP_UNLOCK() ReleaseSRWLockExclusive(&temp_mutex)
|
||||
#else
|
||||
#include <pthread.h>
|
||||
static pthread_mutex_t temp_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define TEMP_LOCK() pthread_mutex_lock(&temp_mutex)
|
||||
#define TEMP_UNLOCK() pthread_mutex_unlock(&temp_mutex)
|
||||
#endif
|
||||
|
||||
/* Реестр заимствует стабильный адрес владельца, не выделяет память и общий для workers/instances. */
|
||||
static struct file_io_temp *owned_temps;
|
||||
|
||||
static void file_io_temp_untrack(struct file_io_temp *output) {
|
||||
struct file_io_temp **p = &owned_temps;
|
||||
while (*p && *p != output) p = &(*p)->next_owned;
|
||||
if (*p) *p = output->next_owned;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary owner missing from registry path=%s", output->temporary);
|
||||
output->next_owned = NULL;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
|
||||
@@ -83,6 +105,22 @@ FILE *file_io_open_read(const char *path) {
|
||||
return file;
|
||||
}
|
||||
|
||||
FILE *file_io_open_update(const char *path) {
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(path, 0, &info) || info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update requires regular file path=%s", path ? path : "(null)"); return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return NULL;
|
||||
FILE *file = _wfopen(wide, L"r+b");
|
||||
#else
|
||||
FILE *file = fopen(path, "r+b");
|
||||
#endif
|
||||
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update open failed path=%s errno=%d", path, errno);
|
||||
return file;
|
||||
}
|
||||
|
||||
int file_io_paths(const char *source, const char *destination) {
|
||||
struct file_io_info input, output;
|
||||
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
|
||||
@@ -97,28 +135,111 @@ int file_io_paths(const char *source, const char *destination) {
|
||||
return !memcmp(input.identity, output.identity, sizeof(input.identity));
|
||||
}
|
||||
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
|
||||
/* Синхронизировать именно открытый файл; повторного открытия имени нет. */
|
||||
static int file_io_sync_descriptor(FILE *file, const char *path) {
|
||||
#ifdef _WIN32
|
||||
int result = _commit(_fileno(file));
|
||||
#else
|
||||
int result = fsync(fileno(file));
|
||||
#endif
|
||||
if (result) {
|
||||
int error = errno ? errno : EIO;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync failed path=%s errno=%d", path, error);
|
||||
errno = error;
|
||||
}
|
||||
return result ? -1 : 0;
|
||||
}
|
||||
|
||||
int file_io_sync_file(const char *path) {
|
||||
FILE *file = file_io_open_update(path);
|
||||
if (!file) return -1;
|
||||
int result = file_io_sync_descriptor(file, path);
|
||||
if (fclose(file)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync close failed path=%s errno=%d", path, errno); result = -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int file_io_sync_directory(const char *path) {
|
||||
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync invalid path"); return -1;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
char parent[FILE_IO_PATH_MAX];
|
||||
memcpy(parent, path, strlen(path) + 1);
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (!slash) memcpy(parent, ".", 2);
|
||||
else if (slash == parent) slash[1] = 0;
|
||||
else *slash = 0;
|
||||
int fd = open(parent, O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory open failed path=%s errno=%d", parent, errno); return -1; }
|
||||
int error = fsync(fd) ? (errno ? errno : EIO) : 0;
|
||||
if (close(fd) && !error) error = errno ? errno : EIO;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync/close failed path=%s errno=%d", parent, error); return -1; }
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Только rename; владелец temp снимает регистрацию до длительного directory sync. */
|
||||
static int file_io_rename(const char *source, const char *destination, unsigned flags) {
|
||||
#ifdef _WIN32
|
||||
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(source, from) || file_io_wide(destination, to)) return -1;
|
||||
DWORD options = MOVEFILE_REPLACE_EXISTING | ((flags & FILE_IO_DURABLE) ? MOVEFILE_WRITE_THROUGH : 0);
|
||||
if (!MoveFileExW(from, to, options)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s winerr=%lu", source, destination, GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
(void)flags;
|
||||
if (rename(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s errno=%d", source, destination, errno); return -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_publish(const char *source, const char *destination) {
|
||||
if (file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid source/destination"); return -1;
|
||||
}
|
||||
if (file_io_rename(source, destination, FILE_IO_DURABLE)) return -1;
|
||||
if (file_io_sync_directory(destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file installed but directory durability unconfirmed destination=%s", destination); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file durably installed destination=%s", destination);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags) {
|
||||
if (!output || output->file || output->temporary[0] || !destination || !*destination
|
||||
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|
||||
|| (flags & ~FILE_IO_DURABLE) || strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
|
||||
}
|
||||
output->flags = flags;
|
||||
for (unsigned attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t random[16]; char suffix[33];
|
||||
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
|
||||
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
|
||||
snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix);
|
||||
char temporary[FILE_IO_PATH_MAX];
|
||||
snprintf(temporary, sizeof(temporary), "%s.tmp.%s", destination, suffix);
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; }
|
||||
if (file_io_wide(temporary, wide)) return -1;
|
||||
TEMP_LOCK();
|
||||
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
#else
|
||||
int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
TEMP_LOCK();
|
||||
int fd = open(temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
#endif
|
||||
if (fd < 0) {
|
||||
output->temporary[0] = 0;
|
||||
if (errno == EEXIST) continue;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1;
|
||||
int error = errno; TEMP_UNLOCK();
|
||||
if (error == EEXIST) continue;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, error); return -1;
|
||||
}
|
||||
memcpy(output->temporary, temporary, strlen(temporary) + 1);
|
||||
output->next_owned = owned_temps; owned_temps = output;
|
||||
TEMP_UNLOCK(); /* Файл виден очистке только вместе с зарегистрированным владельцем. */
|
||||
#ifdef _WIN32
|
||||
output->file = _fdopen(fd, "wb+");
|
||||
if (!output->file) _close(fd);
|
||||
@@ -127,7 +248,7 @@ int file_io_temp_open(struct file_io_temp *output, const char *destination) {
|
||||
if (!output->file) close(fd);
|
||||
#endif
|
||||
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s durable=%u", output->temporary, flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
|
||||
@@ -136,9 +257,12 @@ int file_io_temp_open(struct file_io_temp *output, const char *destination) {
|
||||
int file_io_temp_finish(struct file_io_temp *output) {
|
||||
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
|
||||
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
|
||||
if (!error && (output->flags & FILE_IO_DURABLE) && file_io_sync_descriptor(output->file, output->temporary))
|
||||
error = errno ? errno : EIO;
|
||||
if (fclose(output->file) && !error) error = errno ? errno : EIO;
|
||||
output->file = NULL;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/sync/close failed path=%s errno=%d", output->temporary, error); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary finished path=%s durable=%u", output->temporary, output->flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -151,30 +275,57 @@ void file_io_temp_discard(struct file_io_temp *output) {
|
||||
if (!output->temporary[0]) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
int error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide);
|
||||
int valid = file_io_wide(output->temporary, wide) == 0;
|
||||
TEMP_LOCK();
|
||||
int error = valid ? _wremove(wide) : -1;
|
||||
#else
|
||||
TEMP_LOCK();
|
||||
int error = remove(output->temporary);
|
||||
#endif
|
||||
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
|
||||
file_io_temp_untrack(output);
|
||||
output->temporary[0] = 0;
|
||||
TEMP_UNLOCK();
|
||||
}
|
||||
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
|
||||
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
|
||||
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1;
|
||||
}
|
||||
#else
|
||||
if (rename(output->temporary, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1;
|
||||
}
|
||||
#endif
|
||||
TEMP_LOCK();
|
||||
if (file_io_rename(output->temporary, destination, output->flags)) { TEMP_UNLOCK(); return -1; }
|
||||
file_io_temp_untrack(output);
|
||||
output->temporary[0] = 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
|
||||
TEMP_UNLOCK();
|
||||
if ((output->flags & FILE_IO_DURABLE) && file_io_sync_directory(destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary installed but directory durability unconfirmed destination=%s", destination); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s durable=%u", destination, output->flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_remove_orphan(const char *path) {
|
||||
if (!path || !*path) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file orphan removal without path"); return -1; }
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
#endif
|
||||
TEMP_LOCK();
|
||||
for (struct file_io_temp *owner = owned_temps; owner; owner = owner->next_owned) {
|
||||
if (!strcmp(owner->temporary, path)) {
|
||||
TEMP_UNLOCK();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file orphan cleanup keeps active temporary path=%s", path);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int result = _wremove(wide);
|
||||
#else
|
||||
int result = remove(path);
|
||||
#endif
|
||||
int error = errno; TEMP_UNLOCK();
|
||||
if (!result) return 1;
|
||||
if (error == ENOENT) return 0;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "file orphan removal failed path=%s errno=%d", path, error);
|
||||
return -1;
|
||||
}
|
||||
+27
-6
@@ -1,8 +1,11 @@
|
||||
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
|
||||
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
|
||||
/* file_io — синхронные файловые операции: UTF-8 пути, идентичность и публикация через соседний temp.
|
||||
* inspect различает найденный/отсутствующий файл и ошибку; paths сравнивает идентичность файлов.
|
||||
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
|
||||
* Активный temp автоматически защищён от remove_orphan во всех потоках до publish/discard, включая время после finish.
|
||||
* Пока temp активен, структуру нельзя копировать/перемещать; с одним temp работает один владелец.
|
||||
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
|
||||
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
|
||||
* FILE_IO_DURABLE сохраняет данные при finish и имя при publish. POSIX temp имеет права 0600.
|
||||
* После успешного rename temp отсоединён даже при ошибке синхронизации каталога: отката публикации нет. */
|
||||
#ifndef FILE_IO_H
|
||||
#define FILE_IO_H
|
||||
#include <stdio.h>
|
||||
@@ -10,6 +13,7 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#define FILE_IO_PATH_MAX 4096
|
||||
#define FILE_IO_DURABLE 1u
|
||||
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
|
||||
struct file_io_info {
|
||||
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
|
||||
@@ -19,6 +23,8 @@ struct file_io_info {
|
||||
struct file_io_temp {
|
||||
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
|
||||
FILE *file; // открытый wb+; NULL после finish
|
||||
struct file_io_temp *next_owned; // внутренний реестр живых temp; изменяет только file_io под блокировкой
|
||||
unsigned flags; // режим, заданный open: 0 либо FILE_IO_DURABLE на весь срок операции
|
||||
};
|
||||
|
||||
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
|
||||
@@ -29,14 +35,29 @@ int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
|
||||
#endif
|
||||
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
|
||||
FILE *file_io_open_read(const char *path);
|
||||
// Открыть существующий regular-файл для чтения/записи без усечения; конечные ссылки запрещены. UTF-8, NULL при ошибке.
|
||||
FILE *file_io_open_update(const char *path);
|
||||
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
|
||||
int file_io_paths(const char *source, const char *destination);
|
||||
// Сохранить существующий закрытый regular-файл без конечной ссылки; UTF-8, 0/-1.
|
||||
int file_io_sync_file(const char *path);
|
||||
// Сохранить каталог, содержащий path; без разделителя используется '.', для '/file' — '/'. UTF-8, 0/-1.
|
||||
int file_io_sync_directory(const char *path);
|
||||
// Переместить отдельный готовый файл с durable заменой destination; caller владеет source. 0/-1.
|
||||
// Ошибка после rename не откатывает замену; сохраняется каталог назначения. Файлы/каталоги не подменяются извне.
|
||||
int file_io_publish(const char *source, const char *destination);
|
||||
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination);
|
||||
// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output.
|
||||
// flags=0 — flush/close и atomic rename; FILE_IO_DURABLE — также sync файла и каталога назначения.
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags);
|
||||
// Проверить flush/[sync]/close, обнулить file при любом результате; temp остаётся собственностью output.
|
||||
int file_io_temp_finish(struct file_io_temp *output);
|
||||
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
|
||||
void file_io_temp_discard(struct file_io_temp *output);
|
||||
// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются.
|
||||
// Повторно проверить paths и заменить destination закрытым соседним temp; одинаковые файлы отклоняются. 0/-1.
|
||||
// При ошибке до rename temporary сохраняется; после rename он пуст, в том числе при отказе directory sync.
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
|
||||
/* Удалить уже признанный сиротой файл, атомарно проверив владение file_io_temp во всех потоках.
|
||||
* 1 — удалён, 0 — активный temp или уже отсутствует, -1 — ошибка (логируется).
|
||||
* path должен иметь то же нормализованное написание, что и путь при temp_open; рабочий каталог не меняется. */
|
||||
int file_io_remove_orphan(const char *path);
|
||||
#endif
|
||||
+26
-14
@@ -129,15 +129,7 @@ struct ll_entry* ll_alloc_lldgram(uint16_t len) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
void queue_free(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, tail=%p, count=%d",
|
||||
q, q->head, q->tail, q->count);
|
||||
|
||||
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
|
||||
// Это упрощает архитектуру и предотвращает double-u_free
|
||||
|
||||
static void queue_cancel_notifications(struct ll_queue* q) {
|
||||
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
|
||||
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
|
||||
struct queue_waiter* w = q->waiter_head;
|
||||
@@ -150,11 +142,6 @@ void queue_free(struct ll_queue* q) {
|
||||
q->waiter_head = NULL;
|
||||
q->waiter_tail = NULL;
|
||||
|
||||
// Освободить хеш-таблицу
|
||||
if (q->hash_table) {
|
||||
u_free(q->hash_table);
|
||||
}
|
||||
|
||||
// Отменить отложенное возобновление
|
||||
if (q->resume_timeout_id) {
|
||||
uasync_call_soon_cancel(q->ua, q->resume_timeout_id);
|
||||
@@ -170,6 +157,31 @@ void queue_free(struct ll_queue* q) {
|
||||
q->on_get_arg = NULL;
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
q->callback_suspended = 1;
|
||||
}
|
||||
|
||||
void queue_clear(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
#ifdef QUEUE_THREAD_CHECK
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "[%s] discard queue: packets=%d bytes=%zu", q->name, q->count, q->total_bytes);
|
||||
queue_cancel_notifications(q);
|
||||
struct ll_entry* entry = q->head;
|
||||
q->head = q->tail = NULL; q->count = 0; q->total_bytes = 0;
|
||||
if (q->hash_table) memset(q->hash_table, 0, q->hash_size * sizeof(*q->hash_table));
|
||||
while (entry) {
|
||||
struct ll_entry* next = entry->next;
|
||||
entry->owner = NULL; entry->next = entry->prev = entry->hash_next = NULL;
|
||||
queue_dgram_free(entry); queue_entry_free(entry); entry = next;
|
||||
}
|
||||
}
|
||||
|
||||
void queue_free(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, count=%d", (void*)q, (void*)q->head, q->count);
|
||||
queue_cancel_notifications(q);
|
||||
if (q->hash_table) u_free(q->hash_table);
|
||||
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
|
||||
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
|
||||
|
||||
|
||||
@@ -205,6 +205,12 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
|
||||
*/
|
||||
void queue_free(struct ll_queue* q);
|
||||
|
||||
/* Аварийный сброс: освободить все entry/dgram, отменить waiters и отложенные уведомления без их вызова.
|
||||
* Не выделяет память (кроме действий пользовательского dgram_free_fn). Очередь/индекс/обычный callback/пороги сохраняются.
|
||||
* empty/on_get обработчики снимаются, callback приостановлен; владельцы заново ставят ожидания после восстановления.
|
||||
* Вызывать в потоке ua; NULL допустим. Для продолжения автозабора нужен queue_resume_callback(). */
|
||||
void queue_clear(struct ll_queue* q);
|
||||
|
||||
// ==================== Конфигурация ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "socket_compat.h"
|
||||
#include "debug_config.h"
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
// ==================== Windows Implementation ====================
|
||||
@@ -211,6 +212,27 @@ int socket_set_mark(socket_t sock, int mark) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Ошибки send остаются у вызывающего; закрытый пир не завершает процесс сигналом. */
|
||||
ssize_t socket_send(socket_t sock, const void* buf, size_t len) {
|
||||
#ifdef _WIN32
|
||||
if (len > INT_MAX) { WSASetLastError(WSAEMSGSIZE); return SOCKET_ERROR_CODE; }
|
||||
return send(sock, (const char*)buf, (int)len, 0);
|
||||
#elif defined(MSG_NOSIGNAL)
|
||||
return send(sock, buf, len, MSG_NOSIGNAL);
|
||||
#elif defined(SO_NOSIGPIPE)
|
||||
int enabled = 1;
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled)) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "cannot disable SIGPIPE for socket=%d: %s", (int)sock, strerror(errno));
|
||||
return SOCKET_ERROR_CODE;
|
||||
}
|
||||
return send(sock, buf, len, 0);
|
||||
#else
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "safe TCP send is unsupported on this platform");
|
||||
errno = ENOTSUP;
|
||||
return SOCKET_ERROR_CODE;
|
||||
#endif
|
||||
}
|
||||
|
||||
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
|
||||
const struct sockaddr* dest, socklen_t dest_len) {
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -75,6 +75,11 @@ int socket_bind_to_device(socket_t sock, const char* ifname);
|
||||
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_set_mark(socket_t sock, int mark);
|
||||
|
||||
/* Один TCP send без SIGPIPE и изменения обработчиков процесса. Буфер заимствован;
|
||||
* возвращает число байт либо SOCKET_ERROR_CODE, включая EAGAIN. Ошибку логирует владелец.
|
||||
* MSG_NOSIGNAL либо SO_NOSIGPIPE; без поддержки возвращает ENOTSUP. Windows: len <= INT_MAX. */
|
||||
ssize_t socket_send(socket_t sock, const void* buf, size_t len);
|
||||
|
||||
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
|
||||
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
|
||||
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
|
||||
|
||||
+62
-47
@@ -24,6 +24,38 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err);
|
||||
static void connect_timeout_cb(void* arg);
|
||||
static const uint8_t tcp_fin_sentinel;
|
||||
|
||||
// Сохраняем fd при HUP, но не оставляем level-triggered событие в poll без работы.
|
||||
// Очередь восстанавливает регистрацию с новым generation; write_buf обслуживается независимо от чтения.
|
||||
static int sync_socket_monitoring(struct tcp_conn* tc) {
|
||||
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) return -1;
|
||||
int read = !tc->read_paused && !tc->read_stopped && !tc->fin_remote;
|
||||
int write = !tc->connected || tc->write_buf != NULL;
|
||||
if (tc->hup_received && !read && !write) {
|
||||
if (tc->socket_id) {
|
||||
uasync_remove_socket_t(tc->ua, tc->sock); tc->socket_id = NULL;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: park HUP fd=%d rq=%d paused=%u stopped=%u fin=%u",
|
||||
(int)tc->sock, tc->read_queue->count, tc->read_paused, tc->read_stopped, tc->fin_remote);
|
||||
}
|
||||
tc->write_monitor = 0;
|
||||
return 0;
|
||||
}
|
||||
if (!tc->socket_id) {
|
||||
tc->socket_id = uasync_add_socket_t(tc->ua, tc->sock, read_cb, write_cb, error_cb, "tcp_io", tc);
|
||||
if (!tc->socket_id) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot restore socket registration fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: restore HUP fd=%d read=%d write=%d", (int)tc->sock, read, write);
|
||||
}
|
||||
if (uasync_set_socket_read(tc->ua, tc->socket_id, read) != ERR_OK ||
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, write) != ERR_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: monitoring update failed fd=%d read=%d write=%d", (int)tc->sock, read, write);
|
||||
tcp_conn_handle_error(tc, EIO); return -1;
|
||||
}
|
||||
tc->write_monitor = write;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct tcp_conn* tcp_conn_create(
|
||||
struct UASYNC* ua, socket_t sock,
|
||||
size_t entry_data_size, size_t write_chunk_size,
|
||||
@@ -201,7 +233,7 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
|
||||
static void read_cb(socket_t sock, void* arg) {
|
||||
(void)sock;
|
||||
struct tcp_conn* tc = (struct tcp_conn*)arg;
|
||||
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue) return;
|
||||
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue || tc->read_paused || tc->read_stopped || tc->fin_remote) return;
|
||||
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
uint8_t* buf = memory_pool_alloc(tc->data_pool);
|
||||
@@ -209,8 +241,7 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: read_cb alloc failed fd=%d", (int)tc->sock);
|
||||
if (e) queue_entry_free(e);
|
||||
if (buf) memory_pool_free(tc->data_pool, buf);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
tc->read_paused = 1;
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,9 +253,12 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
queue_data_put(rq, e);
|
||||
// tc мог быть уничтожен через callback chain — read_queue обнулён
|
||||
if (rq && tc->read_queue && tc->read_queue->count >= tc->read_high_water && !tc->read_paused) {
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
tc->read_paused = 1;
|
||||
queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc);
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot wait for read queue fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
}
|
||||
}
|
||||
} else if (n == 0) {
|
||||
memory_pool_free(tc->data_pool, buf);
|
||||
@@ -232,8 +266,8 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
if (tc->fin_remote) return;
|
||||
tc->fin_remote = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
if (tc->read_queue->count == 0) {
|
||||
if (tc->on_fin) tc->on_fin(tc, tc->arg);
|
||||
} else {
|
||||
@@ -251,9 +285,16 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
static void resume_read_cb(struct ll_queue* q, void* arg) {
|
||||
(void)q;
|
||||
struct tcp_conn* tc = (struct tcp_conn*)arg;
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
|
||||
if (!tc || tc->sock == SOCKET_INVALID || tc->destroyed || tc->read_stopped || tc->fin_remote) return;
|
||||
if (tc->read_queue->count > tc->read_low_water) {
|
||||
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot renew read waiter fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
}
|
||||
return;
|
||||
}
|
||||
tc->read_paused = 0;
|
||||
sync_socket_monitoring(tc);
|
||||
}
|
||||
|
||||
static void fin_deferred_cb(struct ll_queue* q, void* arg) {
|
||||
@@ -313,8 +354,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) {
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
|
||||
tc->write_monitor = 0;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT OFF fd=%d (queue empty)", (int)tc->sock);
|
||||
queue_resume_callback(q);
|
||||
notify_flushed(tc);
|
||||
@@ -370,8 +410,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
tc->write_offset = 0;
|
||||
memory_pool_free(tc->data_pool, e->dgram);
|
||||
queue_entry_free(e);
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
|
||||
tc->write_monitor = 1;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (partial send, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
|
||||
} else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
|
||||
tc->write_buf = memory_pool_alloc(tc->data_pool);
|
||||
@@ -387,8 +426,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
tc->write_offset = 0;
|
||||
memory_pool_free(tc->data_pool, e->dgram);
|
||||
queue_entry_free(e);
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
|
||||
tc->write_monitor = 1;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (EAGAIN, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: send error fd=%d errno=%d", (int)tc->sock, errno);
|
||||
@@ -427,10 +465,7 @@ static void write_cb(socket_t sock, void* arg) {
|
||||
if (flush_write_buf(tc)) return;
|
||||
if (!tc->write_buf) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: write_cb flushed fd=%d wmon=%d (resume WQ cb)", (int)tc->sock, tc->write_monitor);
|
||||
if (tc->write_queue->waiter_defer) {
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
|
||||
tc->write_monitor = 0;
|
||||
}
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
queue_resume_callback(tc->write_queue);
|
||||
notify_flushed(tc);
|
||||
}
|
||||
@@ -446,33 +481,13 @@ static void error_cb(socket_t sock, void* arg) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
int so_err = 0; socklen_t so_len = sizeof(so_err);
|
||||
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&so_err, &so_len) == 0 && so_err == 0) {
|
||||
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
|
||||
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
|
||||
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
|
||||
if (tc->fin_remote && tc->fin_local) {
|
||||
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
|
||||
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
|
||||
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
|
||||
tc->socket_id = NULL; tc->write_monitor = 0;
|
||||
return;
|
||||
}
|
||||
if (tc->fin_remote) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
|
||||
(int)tc->sock);
|
||||
tcp_conn_push_close(tc);
|
||||
return;
|
||||
}
|
||||
// Грациозное закрытие peer (FIN, EPOLLHUP) при приостановленном чтении: FIN пришёл не как
|
||||
// EPOLLIN (чтение отключено backpressure-ом), а как HUP. Возобновляем чтение, чтобы
|
||||
// read_cb дослил остаток и прочитал FIN (recv()==0 → fin_remote → отложенный on_fin).
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d fin_r=%d fin_l=%d read_paused=%d rq=%d — resume read",
|
||||
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused,
|
||||
tc->read_queue ? tc->read_queue->count : 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
|
||||
tc->read_paused = 0;
|
||||
read_cb(tc->sock, tc);
|
||||
if (!tc->hup_received)
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: HUP fd=%d fin_r=%u fin_l=%u paused=%u stopped=%u rq=%d",
|
||||
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused, tc->read_stopped, tc->read_queue->count);
|
||||
tc->hup_received = 1;
|
||||
if (tc->write_buf && flush_write_buf(tc)) return;
|
||||
if (!tc->fin_remote && !tc->read_paused && !tc->read_stopped) read_cb(tc->sock, tc);
|
||||
if (!tc->destroyed && !tc->error && !tc->closed) sync_socket_monitoring(tc);
|
||||
return;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: async error fd=%d connected=%d err=%d so_err=%d fin_r=%d fin_l=%d closed=%d wmon=%d",
|
||||
@@ -516,9 +531,9 @@ void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_con
|
||||
|
||||
void tcp_conn_pause_read(struct tcp_conn* tc) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
tc->read_paused = 1;
|
||||
tc->read_stopped = 1; tc->read_paused = 1;
|
||||
sync_socket_monitoring(tc);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
|
||||
+6
-1
@@ -27,6 +27,9 @@
|
||||
// Для записи выделить entry из tc->entry_pool и dgram из tc->data_pool, заполнить len и dgram_pool;
|
||||
// затем queue_data_put(tc->write_queue, e). Длина должна помещаться в буфер и uint16_t.
|
||||
// После принятого FIN новые данные не добавлять; закрывать сокет через push_close/destroy.
|
||||
// HUP не снимает backpressure/явную pause_read: регистрация без доступной работы временно удаляется,
|
||||
// low_water восстанавливает её с новым поколением uasync. FD/очереди сохраняются до решения владельца.
|
||||
// Ошибка восстановления/ожидания очереди вызывает on_error, как остальные ошибки TCP.
|
||||
|
||||
#ifndef TCP_IO_H
|
||||
#define TCP_IO_H
|
||||
@@ -52,7 +55,9 @@ struct tcp_conn {
|
||||
int read_high_water; // Число входных entry для приостановки чтения
|
||||
int read_low_water; // Порог очереди для возобновления чтения
|
||||
int rcvbuf_size; // Запрошенный SO_RCVBUF; <=0 — не настраивать буферы
|
||||
uint8_t read_paused; // Чтение отключено backpressure или pause_read
|
||||
uint8_t read_paused; // Чтение приостановлено; low_water снимает только автоматическую паузу
|
||||
uint8_t read_stopped; // Владелец запретил чтение; low_water/HUP не снимает запрет
|
||||
uint8_t hup_received; // Получен HUP с SO_ERROR=0; idle registration снимается до готовности очереди
|
||||
|
||||
uint8_t write_monitor; // 1 = EPOLLOUT активен
|
||||
uint8_t connected;
|
||||
|
||||
+4
-2
@@ -79,6 +79,8 @@ utun_CORE_SOURCES = \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_tx.c \
|
||||
transport_layer/etcp_tx.h \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
transport_layer/etcp_debug.c \
|
||||
transport_layer/etcp_dump.c \
|
||||
@@ -99,7 +101,6 @@ utun_CORE_SOURCES = \
|
||||
transport_layer/pkt_normalizer.c \
|
||||
transport_layer/packet_dump.c \
|
||||
transport_layer/etcp_api.c \
|
||||
transport_layer/etcp_connect.c \
|
||||
transport_layer/node_conn_direct.c \
|
||||
transport_layer/socket_monitor.c \
|
||||
transport_layer/auto_socket.c \
|
||||
@@ -266,6 +267,8 @@ libutun_a_SOURCES = \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_tx.c \
|
||||
transport_layer/etcp_tx.h \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
transport_layer/etcp_debug.c \
|
||||
transport_layer/etcp_dump.c \
|
||||
@@ -286,7 +289,6 @@ libutun_a_SOURCES = \
|
||||
transport_layer/pkt_normalizer.c \
|
||||
transport_layer/packet_dump.c \
|
||||
transport_layer/etcp_api.c \
|
||||
transport_layer/etcp_connect.c \
|
||||
transport_layer/node_conn_direct.c \
|
||||
transport_layer/socket_monitor.c \
|
||||
transport_layer/auto_socket.c \
|
||||
|
||||
+66
-18
@@ -23,11 +23,13 @@ enum { AK_REQUEST = 1, AK_REQUEST_ACK, AK_GRANT, AK_DENY, AK_RESULT_ACK };
|
||||
|
||||
struct ak_file {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32], source[1024], cipher[1024], plain[1024], temporary[1040], type[32];
|
||||
char channel[32], source[1024], cipher[1024], plain[1024], type[32];
|
||||
struct file_io_temp output; // Единственный собственный temp; стабильный адрес до completion.
|
||||
uint8_t id[16], key[32], message[32], hash[32], reply[32];
|
||||
struct attachment_info info;
|
||||
uint64_t author, request;
|
||||
int encrypt, error;
|
||||
int plain_prepared, published; // Только реально подготовленный snapshot и выполненный rename дают право очистки.
|
||||
};
|
||||
|
||||
/* Все ошибки SQLite содержат конкретную операцию. */
|
||||
@@ -94,10 +96,8 @@ invalid:
|
||||
|
||||
/* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */
|
||||
static int ak_paths(struct ak_file* job) {
|
||||
const char* slash = strrchr(CC(job->inst)->db_path, '/');
|
||||
char base[512], hex[33]; ak_hex(job->id, hex);
|
||||
if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path);
|
||||
else snprintf(base, sizeof(base), ".");
|
||||
if (media_index_get_base(CC(job->inst)->db, base)) return -1;
|
||||
int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex);
|
||||
int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex);
|
||||
if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) {
|
||||
@@ -121,12 +121,18 @@ static int ak_parent(const char* path) {
|
||||
static void ak_file_work(void* arg) {
|
||||
struct ak_file* job = arg; job->error = -1;
|
||||
if (ak_parent(job->plain) || ak_parent(job->cipher)) return;
|
||||
snprintf(job->temporary, sizeof(job->temporary), "%s.part", job->encrypt ? job->cipher : job->plain);
|
||||
if (job->encrypt) {
|
||||
if (ma_copy_file(job->source, job->plain) || encrypted_file_sync(job->plain) || encrypted_file_sync_directory(job->plain) ||
|
||||
encrypted_file_write(job->plain, job->temporary, job->key, job->author, job->id, &job->info, job->type)) return;
|
||||
} else if (encrypted_file_read(job->cipher, job->temporary, job->key, job->author, job->id, job->hash, &job->info, job->type)) return;
|
||||
if (encrypted_file_publish(job->temporary, job->encrypt ? job->cipher : job->plain)) return;
|
||||
if (ma_copy_file(job->source, job->plain)) return;
|
||||
job->plain_prepared = 1;
|
||||
if (file_io_sync_file(job->plain) || file_io_sync_directory(job->plain) ||
|
||||
encrypted_file_write(job->plain, job->cipher, &job->output, job->key, job->author, job->id, &job->info, job->type)) return;
|
||||
} else if (encrypted_file_read(job->cipher, job->plain, &job->output, job->key, job->author, job->id,
|
||||
job->hash, &job->info, job->type)) return;
|
||||
int error = file_io_temp_publish(&job->output, job->encrypt ? job->plain : job->cipher, job->encrypt ? job->cipher : job->plain);
|
||||
job->published = !job->output.temporary[0]; // Rename уже выполнен и при последующем отказе directory sync.
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: output publication encrypt=%d installed=%d error=%d",
|
||||
job->encrypt, job->published, error);
|
||||
if (error) return;
|
||||
job->error = 0;
|
||||
}
|
||||
|
||||
@@ -174,11 +180,12 @@ static void ak_file_done(void* arg, int error) {
|
||||
if (job->encrypt) chat_event_submitted(inst, job->request, 0);
|
||||
}
|
||||
if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed");
|
||||
if ((!success || !valid) && job->temporary[0] && remove(job->temporary) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary cleanup failed");
|
||||
file_io_temp_discard(&job->output);
|
||||
if (!valid) {
|
||||
if (remove(job->plain) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
|
||||
if (job->encrypt && remove(job->cipher) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
|
||||
if (((job->encrypt && job->plain_prepared) || (!job->encrypt && job->published)) && remove(job->plain) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
|
||||
if (job->encrypt && job->published && remove(job->cipher) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
|
||||
}
|
||||
if (valid) {
|
||||
if (success && !job->encrypt) chat_media_cache_enforce(inst);
|
||||
@@ -191,7 +198,7 @@ static void ak_file_done(void* arg, int error) {
|
||||
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
|
||||
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request) {
|
||||
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !inst->media_async || !ak_channel(channel) ||
|
||||
!source || strlen(source) >= 1024 || !type || strlen(type) >= 32 || attachment_validate(info)) {
|
||||
!source || strlen(source) >= 1024 || encrypted_file_validate_type(type) || attachment_validate(info)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid send arguments/state"); return -1;
|
||||
}
|
||||
struct ak_file* job = u_calloc(1, sizeof(*job));
|
||||
@@ -544,9 +551,52 @@ static void ak_tick(void* arg) {
|
||||
if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed");
|
||||
}
|
||||
|
||||
struct ak_temp_cleanup { const char* root; const char* extension; unsigned removed; };
|
||||
|
||||
/* Только <channel>/<UUID>.<uenc|file>.tmp.<UUID>; чужие имена и активные owners сохраняются. */
|
||||
static void ak_remove_temp(void* arg, const char* path, int64_t size) {
|
||||
(void)size;
|
||||
struct ak_temp_cleanup* cleanup = arg;
|
||||
size_t prefix = strlen(cleanup->root);
|
||||
if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return;
|
||||
const char* relative = path + prefix + 1;
|
||||
const char* slash = strchr(relative, '/');
|
||||
if (!slash || slash - relative >= 20) return;
|
||||
char channel[20]; memcpy(channel, relative, (size_t)(slash - relative)); channel[slash - relative] = 0;
|
||||
if (!ak_channel(channel)) return;
|
||||
const char* name = slash + 1;
|
||||
const char* hex = "0123456789abcdef";
|
||||
if (strlen(name) != 74 || strspn(name, hex) != 32 || memcmp(name + 32, cleanup->extension, 5) ||
|
||||
memcmp(name + 37, ".tmp.", 5) || strspn(name + 42, hex) != 32) return;
|
||||
int removed = file_io_remove_orphan(path);
|
||||
if (removed > 0) cleanup->removed++;
|
||||
else if (removed < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery failed path=%s", path);
|
||||
}
|
||||
|
||||
/* И при crash до публикации, и при прерванной расшифровке temp не является готовым вложением. */
|
||||
static void ak_recover_temps(struct UTUN_INSTANCE* inst) {
|
||||
char base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(CC(inst)->db, base)) return;
|
||||
const char* trees[] = {"media", "decrypted"};
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
char root[MEDIA_BASE_MAX + 16]; snprintf(root, sizeof(root), "%s/%s", base, trees[i]);
|
||||
struct file_io_info info;
|
||||
int status = file_io_inspect(root, 0, &info);
|
||||
if (status == 1) continue;
|
||||
if (status < 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect temporary tree path=%s", root); continue; }
|
||||
struct ak_temp_cleanup cleanup = {.root = root, .extension = i ? ".file" : ".uenc"};
|
||||
if (ma_dir_walk(root, ak_remove_temp, &cleanup))
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery incomplete path=%s removed=%u", root, cleanup.removed);
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery path=%s removed=%u", root, cleanup.removed);
|
||||
}
|
||||
}
|
||||
|
||||
/* После crash/отмены убрать только собственные подготовки, не имеющие сообщения.
|
||||
* Вызывается до запуска workers либо после их полного завершения. */
|
||||
static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
|
||||
ak_recover_temps(inst);
|
||||
char base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(CC(inst)->db, base)) return;
|
||||
char** channels = NULL; int count = 0, columns = 0;
|
||||
if (sqlite3_get_table(CC(inst)->db, "SELECT channel_id FROM channels", &channels, &count, &columns, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: recovery channel scan failed: %s", sqlite3_errmsg(CC(inst)->db)); return;
|
||||
@@ -566,16 +616,14 @@ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int field = 0; field < fields; field++) {
|
||||
const char* path = paths[(row + 1) * fields + field];
|
||||
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path);
|
||||
if ((remove(path) && errno != ENOENT) || (remove(temporary) && errno != ENOENT)) {
|
||||
if (remove(path) && errno != ENOENT) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unfinished file removal failed path=%s: %s", path, strerror(errno)); failed = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_free_table(paths);
|
||||
if (rows && !failed) {
|
||||
const char* slash = strrchr(CC(inst)->db_path, '/');
|
||||
unsigned prefix = slash ? (unsigned)(slash - CC(inst)->db_path) : 1;
|
||||
unsigned prefix = (unsigned)strlen(base);
|
||||
char cleanup[768];
|
||||
snprintf(cleanup, sizeof(cleanup), "DELETE FROM media_files WHERE chat_id='%s' AND location IN"
|
||||
"(SELECT substr(a.cipher_path,%u) FROM attachment_keys a WHERE %s)", channel, prefix + 2, condition);
|
||||
|
||||
@@ -28,6 +28,7 @@ void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
|
||||
/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули.
|
||||
* Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки.
|
||||
* -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */
|
||||
/* type проверяется encrypted_file_validate_type до выделения задачи, генерации UUID/ключа и записи в БД. */
|
||||
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
|
||||
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
|
||||
|
||||
|
||||
+9
-17
@@ -319,20 +319,6 @@ void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
/* ─── локальное удаление канала ─── */
|
||||
|
||||
static const char* cc_ui_state_get(struct chat_core_ctx* cc, const char* key) {
|
||||
static char val[512];
|
||||
val[0] = '\0';
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char* v = (const char*)sqlite3_column_text(st, 0);
|
||||
if (v) snprintf(val, sizeof(val), "%s", v);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
@@ -341,6 +327,7 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
return;
|
||||
}
|
||||
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — empty ch_id", CC_ID); return; }
|
||||
if (chat_media_defer_channel_delete(inst, ch_id)) return;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel BEGIN ch=%s", CC_ID, ch_id);
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
@@ -368,10 +355,14 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
chat_sync_on_channel_deleted(inst, ch_id);
|
||||
|
||||
/* 6. удалить медиафайлы канала на диске + строки media_files */
|
||||
media_index_delete_channel(cc->db, ch_id, cc_ui_state_get(cc, "media_base"));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
int media_error = media_index_get_base(cc->db, media_base) || media_index_delete_channel(cc->db, ch_id, media_base);
|
||||
if (media_error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "channel=%s removed with incomplete media cleanup; retry required", ch_id);
|
||||
|
||||
/* 7. удалить таблицы и метаданные канала */
|
||||
topo_node_sqlite_channel_delete(cc->db, ch_id);
|
||||
if (topo_node_sqlite_channel_delete(cc->db, ch_id)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel metadata deletion failed ch=%s", ch_id); return;
|
||||
}
|
||||
|
||||
/* 8. уведомить GUI */
|
||||
{
|
||||
@@ -380,7 +371,8 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
chat_event_post(inst, CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx media_cleanup=%s",
|
||||
CC_ID, ch_id, (unsigned long long)gid, media_error ? "incomplete" : "complete");
|
||||
}
|
||||
|
||||
void chat_core_delete_channel_trampoline(void* arg) {
|
||||
|
||||
+18
-9
@@ -107,6 +107,11 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
if (!cc->shared_db) sqlite3_close(cc->db);
|
||||
inst->chat_core = NULL; u_free(cc); return -1;
|
||||
}
|
||||
if (strlen(db_path) >= sizeof(cc->db_path) || media_index_init(cc->db)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media schema/path initialization failed");
|
||||
if (!cc->shared_db) sqlite3_close(cc->db);
|
||||
inst->chat_core = NULL; u_free(cc); return -1;
|
||||
}
|
||||
cc->inst = inst;
|
||||
cc->my_node_id = inst->node_id;
|
||||
|
||||
@@ -141,9 +146,6 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
|
||||
chat_member_init(inst);
|
||||
|
||||
/* create media_files table */
|
||||
media_index_init(cc->db);
|
||||
|
||||
db_exec(cc,
|
||||
"CREATE TABLE IF NOT EXISTS local_identity ("
|
||||
" id INTEGER PRIMARY KEY CHECK (id = 1),"
|
||||
@@ -172,13 +174,20 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
);
|
||||
/* store media_base for block_req handler */
|
||||
{
|
||||
char mbase[512];
|
||||
char mbase[MEDIA_BASE_MAX];
|
||||
const char* ls = strrchr(db_path, '/');
|
||||
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", (int)(ls - db_path), db_path);
|
||||
else snprintf(mbase, sizeof(mbase), "%s", db_path);
|
||||
char sql[1024]; snprintf(sql, sizeof(sql),
|
||||
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
|
||||
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
|
||||
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", ls == db_path ? 1 : (int)(ls - db_path), db_path);
|
||||
else snprintf(mbase, sizeof(mbase), ".");
|
||||
sqlite3_stmt* stored = NULL;
|
||||
int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &stored, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(stored);
|
||||
int base_error = rc == SQLITE_ROW ? media_index_get_base(cc->db, mbase)
|
||||
: rc == SQLITE_DONE ? media_index_set_base(cc->db, mbase) : -1;
|
||||
int finish = stored ? sqlite3_finalize(stored) : SQLITE_OK;
|
||||
if (base_error || finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media_base initialization failed rc=%d: %s", rc, sqlite3_errmsg(cc->db));
|
||||
chat_core_destroy(inst); return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,10 @@ struct chat_delete_channel_req {
|
||||
|
||||
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
|
||||
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
|
||||
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
|
||||
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют.
|
||||
* Исходящие video/register задачи сначала завершаются; удаление продолжит последний completion.
|
||||
* Новые исходящие медиа этого канала в это время отклоняются. Событие CHANNEL_DELETED приходит после удаления метаданных;
|
||||
* ошибка файловой очистки логируется отдельно, записи неочищенных файлов сохраняются для повтора. */
|
||||
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_delete_channel_trampoline(void* arg);
|
||||
|
||||
@@ -79,6 +79,7 @@ struct ms_props_cbk; /* member_sync.c */
|
||||
struct media_index_result;
|
||||
struct ms_apply_cbk; /* member_sync.c */
|
||||
struct group_avatar;
|
||||
struct media_submit_ctx;
|
||||
|
||||
struct chat_core_ctx {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -92,6 +93,7 @@ struct chat_core_ctx {
|
||||
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
|
||||
struct group_avatar* group_avatar;
|
||||
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
|
||||
struct media_submit_ctx* media_submissions; /* исходящие задачи до последнего callback; удаление канала ждёт их */
|
||||
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
|
||||
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
|
||||
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
|
||||
@@ -125,6 +127,8 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32],
|
||||
int chat_msg_parse_media_body(const char* body, struct media_index_result* result);
|
||||
/* Учесть также расшифрованные производные файлы и применить общую квоту кеша. */
|
||||
void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
|
||||
/* Пометить текущие исходящие задачи канала; 1 — удаление продолжит последний completion, 0 — задач нет. */
|
||||
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel);
|
||||
|
||||
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
|
||||
|
||||
|
||||
+106
-90
@@ -157,6 +157,9 @@ void chat_core_submit_trampoline(void* arg) {
|
||||
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
|
||||
|
||||
struct media_submit_ctx {
|
||||
struct media_submit_ctx* next;
|
||||
struct media_destination* destination; // Общая с загрузчиком резервация пути до завершения workers.
|
||||
int delete_pending;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel_id[64];
|
||||
char content_type[32];
|
||||
@@ -173,8 +176,34 @@ struct media_submit_ctx {
|
||||
|
||||
/* Завершение владеющей асинхронной отправки, включая все ошибки подготовки/регистрации. */
|
||||
static void media_submit_finish(struct media_submit_ctx* ctx, int success) {
|
||||
struct UTUN_INSTANCE* inst = ctx->inst;
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
char channel[64]; snprintf(channel, sizeof(channel), "%s", ctx->channel_id);
|
||||
/* Синхронный обработчик события может запросить удаление; ctx пока остаётся в списке владельца. */
|
||||
chat_event_submitted(ctx->inst, ctx->request_id, success);
|
||||
int delete_pending = ctx->delete_pending;
|
||||
struct media_submit_ctx** slot = &cc->media_submissions;
|
||||
while (*slot && *slot != ctx) slot = &(*slot)->next;
|
||||
if (*slot) *slot = ctx->next;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submission missing from owner ch=%s", channel);
|
||||
media_delivery_release(inst, ctx->destination);
|
||||
u_free(ctx);
|
||||
if (delete_pending) {
|
||||
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next)
|
||||
if (!strcmp(pending->channel_id, channel)) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media submissions drained; continuing channel deletion ch=%s", channel);
|
||||
chat_core_delete_channel(inst, channel);
|
||||
}
|
||||
}
|
||||
|
||||
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel) {
|
||||
struct chat_core_ctx* cc = CC(inst); int pending = 0;
|
||||
for (struct media_submit_ctx* ctx = cc->media_submissions; ctx; ctx = ctx->next) {
|
||||
if (strcmp(ctx->channel_id, channel)) continue;
|
||||
ctx->delete_pending = 1; pending++;
|
||||
}
|
||||
if (pending) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "channel deletion waiting for outgoing media ch=%s tasks=%d", channel, pending);
|
||||
return pending != 0;
|
||||
}
|
||||
|
||||
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
|
||||
@@ -187,6 +216,10 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
|
||||
media_submit_finish(mctx, 0);
|
||||
return;
|
||||
}
|
||||
if (mctx->delete_pending) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "prepared media retained for pending deletion ch=%s", mctx->channel_id);
|
||||
media_submit_finish(mctx, 0); return;
|
||||
}
|
||||
|
||||
struct DB_SYNC_INSTANCE* si = si_find(mctx->inst, mctx->channel_id);
|
||||
if (!si) {
|
||||
@@ -326,14 +359,8 @@ static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
|
||||
CC_ID, vi->width, vi->height, (long long)vi->duration_ms);
|
||||
}
|
||||
|
||||
char media_base[1024];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash)
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) { media_submit_finish(mctx, 0); return; }
|
||||
|
||||
media_index_register_async(
|
||||
mctx->inst->media_async, mctx->inst->ua, cc->db,
|
||||
@@ -351,6 +378,23 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
if (req) chat_event_submitted(inst, req->request_id, 0);
|
||||
return;
|
||||
}
|
||||
char media_base[MEDIA_BASE_MAX], location[MEDIA_PATH_MAX], destination[MEDIA_PATH_MAX];
|
||||
if (media_index_get_base(cc->db, media_base) || media_index_location(req->media_dest, media_base, location)
|
||||
|| media_index_path(media_base, location, destination)) {
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next) {
|
||||
if ((!strcmp(pending->channel_id, req->channel_id) && pending->delete_pending)
|
||||
|| !strcmp(pending->media_dest, destination)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media submit rejected: channel deleting/destination busy ch=%s path=%s",
|
||||
req->channel_id, req->media_dest);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
}
|
||||
if (!si_find(inst, req->channel_id)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit without channel ch=%s", req->channel_id);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
char reply_hex[65];
|
||||
if (msg_reply_hex(cc, req->channel_id, req->reply_to, reply_hex) < 0) {
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
@@ -382,28 +426,25 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
return;
|
||||
}
|
||||
|
||||
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
|
||||
char media_base[1024];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash)
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
|
||||
/* copy fields for async callback (req is freed by trampoline immediately) */
|
||||
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
|
||||
if (!mctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media context allocation failed", CC_ID);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
mctx->destination = media_delivery_reserve(inst, destination);
|
||||
if (!mctx->destination) { u_free(mctx); chat_event_submitted(inst, req->request_id, 0); return; }
|
||||
if (media_index_track_preparation(cc->db, req->channel_id, destination, media_base)) {
|
||||
media_delivery_release(inst, mctx->destination); u_free(mctx);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
mctx->inst = inst;
|
||||
mctx->request_id = req->request_id;
|
||||
mctx->next = cc->media_submissions; cc->media_submissions = mctx;
|
||||
memcpy(mctx->reply_hex, reply_hex, strlen(reply_hex) + 1);
|
||||
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
|
||||
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
|
||||
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
|
||||
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", destination);
|
||||
mctx->timestamp = req->timestamp;
|
||||
mctx->duration_ms = req->duration_ms;
|
||||
mctx->width = req->width;
|
||||
@@ -414,7 +455,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
}
|
||||
|
||||
if (req->media_video) {
|
||||
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
|
||||
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, destination, on_video_prepared, mctx) < 0)
|
||||
on_video_prepared(mctx, -1, NULL);
|
||||
return;
|
||||
}
|
||||
@@ -423,7 +464,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
inst->media_async, inst->ua, cc->db,
|
||||
cc->my_node_id, inst->my_ed25519_privkey,
|
||||
req->channel_id,
|
||||
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
|
||||
req->media_src, destination, req->media_copy ? 1 : 0,
|
||||
media_base,
|
||||
on_media_registered, mctx);
|
||||
}
|
||||
@@ -876,16 +917,10 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
char final_name[256];
|
||||
media_final_name(&result, content_type, base_filename, final_name, sizeof(final_name));
|
||||
|
||||
char dest[1024], media_base[512];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) {
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
} else {
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
}
|
||||
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
char dest[MEDIA_PATH_MAX], media_base[MEDIA_BASE_MAX], relative[MEDIA_PATH_MAX];
|
||||
int length = snprintf(relative, sizeof(relative), "media/%s/%s", ch_id, final_name);
|
||||
if (length < 0 || length >= (int)sizeof(relative) || media_index_get_base(cc->db, media_base)
|
||||
|| media_index_path(media_base, relative, dest)) { media_index_result_free(&result); return -1; }
|
||||
|
||||
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { media_index_result_free(&result); return -1; }
|
||||
@@ -902,9 +937,13 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s ct=%s file=%s blocks=%d size=%lld dest=%s",
|
||||
CC_ID, ch_id, content_type ? content_type : "?", final_name, nb, (long long)fsize, dest);
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
media_download_start(inst, gid, &result, dest, media_base, author_node_id, md_download_done_cb, ctx, md_download_progress_cb, ctx);
|
||||
int started = media_download_start(inst, gid, &result, dest, media_base, author_node_id,
|
||||
md_download_done_cb, ctx, md_download_progress_cb, ctx);
|
||||
media_index_result_free(&result);
|
||||
return 0;
|
||||
if (started < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media download rejected ch=%s path=%s", ch_id, dest); u_free(ctx);
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
static int md_auto_download(struct UTUN_INSTANCE* inst,
|
||||
@@ -1102,10 +1141,8 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized || !cc->db) return;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
int total = 0;
|
||||
sqlite3_stmt* cs = NULL;
|
||||
@@ -1164,37 +1201,9 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int registered = 0;
|
||||
for (int n = 0; n < result.num_blocks; n++) {
|
||||
int exists = 0;
|
||||
sqlite3_stmt* ex = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_blob(ex, 1, result.block_ids + n * 16, 16, SQLITE_STATIC);
|
||||
exists = (sqlite3_step(ex) == SQLITE_ROW);
|
||||
sqlite3_finalize(ex);
|
||||
}
|
||||
if (exists) continue;
|
||||
|
||||
char rel[512];
|
||||
size_t bl = strlen(media_base);
|
||||
if (strncmp(path, media_base, bl) == 0 && path[bl] == '/') snprintf(rel, sizeof(rel), "%s", path + bl + 1);
|
||||
else snprintf(rel, sizeof(rel), "%s", path);
|
||||
|
||||
int64_t chunk_size = (n == result.num_blocks - 1) ? result.file_size - (int64_t)n * result.block_size : result.block_size;
|
||||
int64_t offset = (int64_t)n * result.block_size;
|
||||
|
||||
if (media_index_register_downloaded(cc->db, result.media_id, result.block_ids + n * 16,
|
||||
result.content_hash, ch_id, rel, cc->my_node_id,
|
||||
result.file_size, chunk_size, n, offset) == 0) {
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
|
||||
if (registered > 0) {
|
||||
total += registered;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: backfill registered %d blocks ch=%s file=%s",
|
||||
CC_ID, registered, ch_id, final_name);
|
||||
}
|
||||
int registered = media_index_commit(cc->db, &result, cc->my_node_id, NULL, ch_id, path, media_base);
|
||||
if (!registered) total += result.num_blocks;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media backfill registration failed ch=%s file=%s", ch_id, path);
|
||||
|
||||
u_free(body);
|
||||
media_index_result_free(&result);
|
||||
@@ -1336,10 +1345,8 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
|
||||
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
|
||||
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct bf_list list = {0};
|
||||
bf_collect(cc, media_base, NULL, &list);
|
||||
@@ -1406,10 +1413,8 @@ void chat_media_autodownload_backfill_channel(struct UTUN_INSTANCE* inst, const
|
||||
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
|
||||
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct bf_list list = {0};
|
||||
bf_collect(cc, media_base, ch_id, &list);
|
||||
@@ -1449,12 +1454,6 @@ struct media_msg_file {
|
||||
|
||||
typedef void (*media_msg_file_cb)(const struct media_msg_file* f, void* arg);
|
||||
|
||||
static void chat_media_base(struct chat_core_ctx* cc, char* out, size_t out_sz) {
|
||||
const char* ls = strrchr(cc->db_path, '/');
|
||||
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - cc->db_path), cc->db_path);
|
||||
else snprintf(out, out_sz, "%s", cc->db_path);
|
||||
}
|
||||
|
||||
/* Итератор по всем медиасообщениям всех каналов (без фильтра по node_id/времени). */
|
||||
static void chat_media_foreach_media_message(struct chat_core_ctx* cc, const char* media_base, media_msg_file_cb cb, void* arg) {
|
||||
if (!cc->db || !cb) return;
|
||||
@@ -1599,7 +1598,8 @@ static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
|
||||
|
||||
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
|
||||
static uint64_t chat_media_cache_scan_sum(struct chat_core_ctx* cc) {
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return cc->media_cache_bytes;
|
||||
struct cache_file_list files = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
|
||||
uint64_t sum = 0;
|
||||
@@ -1642,12 +1642,13 @@ static void orphan_walk_cb(void* arg, const char* full_path, int64_t size) {
|
||||
char* found = bsearch(&rel, ow->rels, (size_t)ow->rel_count, sizeof(char*), cache_str_cmp);
|
||||
if (found) return;
|
||||
|
||||
if (remove(full_path) == 0) {
|
||||
int removed = file_io_remove_orphan(full_path);
|
||||
if (removed > 0) {
|
||||
ow->removed++;
|
||||
ow->removed_bytes += (uint64_t)(size > 0 ? size : 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan removed: %s (%lld bytes)",
|
||||
CC_ID, rel, (long long)size);
|
||||
} else {
|
||||
} else if (removed < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache orphan remove failed: %s", CC_ID, full_path);
|
||||
}
|
||||
}
|
||||
@@ -1656,7 +1657,8 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized || !cc->db || !inst) return;
|
||||
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
|
||||
|
||||
struct file_io_info directory;
|
||||
@@ -1666,7 +1668,22 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
struct cache_rel_list rels = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
|
||||
/* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */
|
||||
sqlite3_stmt* preparations = NULL;
|
||||
int prep_rc = sqlite3_prepare_v2(cc->db, "SELECT location FROM media_preparations UNION SELECT location FROM media_files",
|
||||
-1, &preparations, NULL);
|
||||
if (prep_rc == SQLITE_OK) {
|
||||
while ((prep_rc = sqlite3_step(preparations)) == SQLITE_ROW) {
|
||||
const char* location = (const char*)sqlite3_column_text(preparations, 0);
|
||||
if (location) cache_rel_add(&rels, location);
|
||||
}
|
||||
}
|
||||
int prep_finish = preparations ? sqlite3_finalize(preparations) : SQLITE_OK;
|
||||
if (prep_rc != SQLITE_DONE || prep_finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media orphan scan cannot protect indexed/preparing files rc=%d", prep_rc);
|
||||
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
|
||||
u_free(rels.items); return;
|
||||
}
|
||||
/* Конечные контейнеры принадлежат attachment_keys; активные temp защищены file_io-реестром. */
|
||||
if (cc->attachment_initialized) {
|
||||
sqlite3_stmt* active = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT cipher_path FROM attachment_keys", -1, &active, NULL) == SQLITE_OK) {
|
||||
@@ -1676,8 +1693,6 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
size_t prefix = strlen(media_base);
|
||||
if (path && !strncmp(path, media_base, prefix) && path[prefix] == '/') {
|
||||
cache_rel_add(&rels, path + prefix + 1);
|
||||
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path + prefix + 1);
|
||||
cache_rel_add(&rels, temporary);
|
||||
}
|
||||
}
|
||||
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted cache: active scan failed rc=%d", rc);
|
||||
@@ -1714,7 +1729,8 @@ void chat_media_cache_enforce(struct UTUN_INSTANCE* inst) {
|
||||
return;
|
||||
}
|
||||
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct cache_file_list files = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
|
||||
|
||||
@@ -278,7 +278,11 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
|
||||
if (conn->input_queue) off += snprintf(buf + off, sizeof(buf) - off, "input_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_queue), queue_entry_count(conn->input_queue));
|
||||
if (conn->input_send_q) off += snprintf(buf + off, sizeof(buf) - off, "send_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_send_q), queue_entry_count(conn->input_send_q));
|
||||
if (conn->input_wait_ack) off += snprintf(buf + off, sizeof(buf) - off, "wait_ack: %uB/%dp\n", (unsigned)queue_total_bytes(conn->input_wait_ack), queue_entry_count(conn->input_wait_ack));
|
||||
if (conn->ack_q) off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->ack_q), queue_entry_count(conn->ack_q));
|
||||
unsigned ack_bytes = 0, ack_packets = 0;
|
||||
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
|
||||
ack_bytes += (unsigned)queue_total_bytes(link->ack_q); ack_packets += queue_entry_count(link->ack_q);
|
||||
}
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%up ", ack_bytes, ack_packets);
|
||||
if (conn->recv_q) off += snprintf(buf + off, sizeof(buf) - off, "recv_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->recv_q), queue_entry_count(conn->recv_q));
|
||||
if (conn->output_queue) off += snprintf(buf + off, sizeof(buf) - off, "out_q: %uB/%dp\n", (unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue));
|
||||
|
||||
|
||||
+17
-2
@@ -10,6 +10,8 @@
|
||||
#include <ctype.h>
|
||||
#include "../lib/platform_compat.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include "transport_layer/etcp_connections.h"
|
||||
#ifndef _WIN32
|
||||
#include <netdb.h>
|
||||
#include <ifaddrs.h>
|
||||
@@ -379,7 +381,15 @@ static int parse_global(const char *key, const char *value, struct global_config
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "bbr_max_cwnd") == 0) {
|
||||
global->bbr_max_cwnd = atoi(value);
|
||||
char* end;
|
||||
errno = 0;
|
||||
long parsed = strtol(value, &end, 10);
|
||||
if (errno || end == value || *end || parsed < INFLIGHT_LIM_MIN || parsed > INT_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid bbr_max_cwnd '%s'; expected %u..%d bytes",
|
||||
filename, line_num, value, INFLIGHT_LIM_MIN, INT_MAX);
|
||||
return -1;
|
||||
}
|
||||
global->bbr_max_cwnd = (int)parsed;
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "auto_sockets") == 0) {
|
||||
@@ -1121,7 +1131,12 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
continue;
|
||||
}
|
||||
switch (cur_section) {
|
||||
case SECTION_GLOBAL: parse_global(key, value, &cfg->global, filename, line_num); break;
|
||||
case SECTION_GLOBAL:
|
||||
if (parse_global(key, value, &cfg->global, filename, line_num) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid global option %s", filename, line_num, key);
|
||||
goto error;
|
||||
}
|
||||
break;
|
||||
case SECTION_SERVER: if (cur_server) parse_server(key, value, cur_server, filename, line_num); break;
|
||||
case SECTION_CLIENT: if (cur_client) parse_client(key, value, cur_client, cfg->servers, filename, line_num); break;
|
||||
case SECTION_ROUTING: if (strcmp(key, "route_subnet") == 0) add_route_entry(&cfg->route_subnets, value); else if (strcmp(key, "my_subnet") == 0) add_route_entry(&cfg->my_subnets, value); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown routing option '%s'", filename, line_num, key); break;
|
||||
|
||||
+2
-2
@@ -168,7 +168,7 @@ struct global_config {
|
||||
int keepalive_timeout; // keepalive timeout in ms (default: 2000)
|
||||
int keepalive_interval; // keepalive interval in ms (default: 200)
|
||||
int keepalive_adaptive; // 1 = adaptive period (default), 0 = fixed interval
|
||||
int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 1048576)
|
||||
int bbr_max_cwnd; // лимит DATA-окна одного линка, 8192..INT_MAX байтов; по умолчанию 1048576
|
||||
|
||||
// Firewall configuration
|
||||
struct CFG_FIREWALL_RULE *firewall_rules;
|
||||
@@ -267,7 +267,7 @@ struct utun_config {
|
||||
};
|
||||
|
||||
/* Непустой путь; результат освобождать free_config. NULL при ошибке открытия, чтения,
|
||||
* закрытия, памяти или обязательной проверки формата. Часть ошибочных опций только
|
||||
* закрытия, памяти или обязательной проверки формата, включая ошибки [global]. Часть опций других секций только
|
||||
* логируется и пропускается; ненулевой результат не гарантирует корректность всех полей. */
|
||||
struct utun_config* parse_config(const char *filename);
|
||||
/* buf — len байтов без завершающего NUL (NULL допустим при len=0), буфер не меняется.
|
||||
|
||||
@@ -1140,8 +1140,11 @@ static void send_metrics(struct control_server* server, struct control_client* c
|
||||
rsp->etcp.input_send_q_packets = (uint32_t)queue_entry_count(conn->input_send_q);
|
||||
rsp->etcp.input_wait_ack_bytes = (uint32_t)queue_total_bytes(conn->input_wait_ack);
|
||||
rsp->etcp.input_wait_ack_packets = (uint32_t)queue_entry_count(conn->input_wait_ack);
|
||||
rsp->etcp.ack_q_bytes = (uint32_t)queue_total_bytes(conn->ack_q);
|
||||
rsp->etcp.ack_q_packets = (uint32_t)queue_entry_count(conn->ack_q);
|
||||
rsp->etcp.ack_q_bytes = rsp->etcp.ack_q_packets = 0;
|
||||
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
|
||||
rsp->etcp.ack_q_bytes += (uint32_t)queue_total_bytes(link->ack_q);
|
||||
rsp->etcp.ack_q_packets += (uint32_t)queue_entry_count(link->ack_q);
|
||||
}
|
||||
rsp->etcp.recv_q_bytes = (uint32_t)queue_total_bytes(conn->recv_q);
|
||||
rsp->etcp.recv_q_packets = (uint32_t)queue_entry_count(conn->recv_q);
|
||||
rsp->etcp.output_queue_bytes = (uint32_t)queue_total_bytes(conn->output_queue);
|
||||
@@ -1153,7 +1156,9 @@ static void send_metrics(struct control_server* server, struct control_client* c
|
||||
rsp->etcp.pkt_format_errors = (conn->links && conn->links->conn) ? (uint32_t)conn->links->conn->pkt_format_errors : 0;
|
||||
|
||||
/* Timer flags */
|
||||
rsp->etcp.retrans_timer_active = (conn->retrans_timer != NULL) ? 1 : 0;
|
||||
rsp->etcp.retrans_timer_active = 0;
|
||||
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
|
||||
if (link->retrans_timer) rsp->etcp.retrans_timer_active = 1;
|
||||
rsp->etcp.ack_resp_timer_active = (conn->ack_resp_timer != NULL) ? 1 : 0;
|
||||
|
||||
/* Connection IDs */
|
||||
@@ -1276,7 +1281,7 @@ static void send_metrics(struct control_server* server, struct control_client* c
|
||||
rtt_cnt++;
|
||||
}
|
||||
{
|
||||
uint16_t j = (uint16_t)(rc->rtt_jitter / 32768);
|
||||
uint32_t j = rc->rtt_jitter / 32768;
|
||||
if (j > rsp->router.jitter) rsp->router.jitter = j;
|
||||
}
|
||||
if (rc->minrtt_window_count > 0) {
|
||||
|
||||
+5
-5
@@ -154,7 +154,7 @@ static void dm_cleanup_pump(struct dm_media_state* media);
|
||||
static void dm_cleanup_work(void* arg) {
|
||||
struct dm_cleanup_job* job = arg;
|
||||
job->error = dm_file_remove(job->path);
|
||||
if (!job->error) job->error = encrypted_file_sync_directory(job->path);
|
||||
if (!job->error) job->error = file_io_sync_directory(job->path);
|
||||
}
|
||||
|
||||
static void dm_cleanup_done(void* arg, int error) {
|
||||
@@ -488,7 +488,7 @@ static void dm_file_work(void* arg) {
|
||||
struct dm_file_job* job = arg;
|
||||
job->error = -1;
|
||||
if (job->work == DM_FILE_DELETE) {
|
||||
if (!dm_file_remove(job->cipher) && !dm_file_remove(job->temp)) job->error = encrypted_file_sync_directory(job->cipher);
|
||||
if (!dm_file_remove(job->cipher) && !dm_file_remove(job->temp)) job->error = file_io_sync_directory(job->cipher);
|
||||
return;
|
||||
}
|
||||
if (job->work == DM_FILE_PREPARE) {
|
||||
@@ -530,9 +530,9 @@ done:
|
||||
if (out && fclose(out)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_media: worker output close failed"); job->error = -1; }
|
||||
if (!job->error) {
|
||||
if (job->work == DM_FILE_PREPARE) {
|
||||
job->error = encrypted_file_publish(job->temp, job->cipher);
|
||||
if (!job->error) job->error = encrypted_file_sync_directory(job->plain);
|
||||
} else job->error = encrypted_file_publish(job->work == DM_FILE_CUSTODY ? job->cipher : job->temp, job->output);
|
||||
job->error = file_io_publish(job->temp, job->cipher);
|
||||
if (!job->error) job->error = file_io_sync_directory(job->plain);
|
||||
} else job->error = file_io_publish(job->work == DM_FILE_CUSTODY ? job->cipher : job->temp, job->output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "attachment.h"
|
||||
|
||||
/* Имя должно быть корректным UTF-8 basename без управляющих символов. */
|
||||
/* Корректный UTF-8 basename без ASCII C0/DEL и разделителей пути. */
|
||||
static int valid_name(const char* name, size_t n) {
|
||||
if (!n || n > 255 || (n == 1 && name[0] == '.') || (n == 2 && !memcmp(name, "..", 2))) return 0;
|
||||
for (size_t i = 0; i < n;) {
|
||||
@@ -32,7 +32,7 @@ int attachment_validate(const struct attachment_info* info) {
|
||||
if (!info || !valid_name(info->name, strnlen(info->name, sizeof(info->name)))) goto invalid;
|
||||
size_t wave = strnlen(info->waveform, sizeof(info->waveform));
|
||||
if (info->kind == ATTACHMENT_VOICE) {
|
||||
if (!info->duration_ms || info->duration_ms > 86400000 || info->width || info->height || wave != 100) goto invalid;
|
||||
if (!info->duration_ms || info->duration_ms > ATTACHMENT_DURATION_MAX_MS || info->width || info->height || wave != 100) goto invalid;
|
||||
if (strspn(info->waveform, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") != wave) goto invalid;
|
||||
} else {
|
||||
if (wave) goto invalid;
|
||||
@@ -40,7 +40,7 @@ int attachment_validate(const struct attachment_info* info) {
|
||||
if (info->duration_ms || info->width || info->height) goto invalid;
|
||||
} else if (info->kind == ATTACHMENT_VIDEO) {
|
||||
if (!info->width || !info->height || info->width > 16384 || info->height > 16384) goto invalid;
|
||||
if (!info->duration_ms || info->duration_ms > 86400000) goto invalid;
|
||||
if (!info->duration_ms || info->duration_ms > ATTACHMENT_DURATION_MAX_MS) goto invalid;
|
||||
} else goto invalid;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* attachment — общие метаданные file/voice/video, без хранения файла, адресата и транспорта.
|
||||
* validate/decode возвращают 0 / -1, encode — число записанных байтов / -1.
|
||||
* Формат: kind:1, name_len:2, duration_ms:4, width:2, height:2, UTF-8 basename, waveform:100 только для voice.
|
||||
* Числа big-endian; имя 1..255 байт без разделителей пути, лишние wire-байты запрещены.
|
||||
* Caller владеет struct/буферами; DM шифрует эти метаданные, GUI получает разобранные поля. */
|
||||
* Числа big-endian; имя — корректный UTF-8 basename длиной 1..255 байт, без '/'/'\\', ASCII C0/DEL и '.'/'..'.
|
||||
* Лишние wire-байты и NUL внутри переданного имени запрещены. Caller владеет struct/буферами;
|
||||
* вход и выход не пересекаются. DM шифрует эти метаданные, GUI получает разобранные поля. */
|
||||
#ifndef ATTACHMENT_H
|
||||
#define ATTACHMENT_H
|
||||
#include <stdint.h>
|
||||
@@ -14,16 +15,23 @@ extern "C" {
|
||||
enum attachment_kind { ATTACHMENT_FILE = 1, ATTACHMENT_VOICE, ATTACHMENT_VIDEO };
|
||||
#define ATTACHMENT_WAVE_SIZE 100
|
||||
#define ATTACHMENT_WIRE_MAX (11 + 255 + ATTACHMENT_WAVE_SIZE)
|
||||
#define ATTACHMENT_DURATION_MAX_MS 86400000u /* Общий максимум длительности voice/video: 24ч. */
|
||||
struct attachment_info {
|
||||
enum attachment_kind kind;
|
||||
char name[256];
|
||||
uint32_t duration_ms;
|
||||
uint16_t width, height;
|
||||
enum attachment_kind kind; /* Только FILE/VOICE/VIDEO. */
|
||||
char name[256]; /* UTF-8 basename с завершающим NUL. */
|
||||
uint32_t duration_ms; /* VOICE/VIDEO: 1..ATTACHMENT_DURATION_MAX_MS; FILE: 0. */
|
||||
uint16_t width, height; /* VIDEO: 1..16384; FILE/VOICE: 0. */
|
||||
char waveform[ATTACHMENT_WAVE_SIZE + 1]; /* 100 уровней из A-Za-z0-9+/; только voice. */
|
||||
};
|
||||
/* Проверить все поля и их согласованность с kind; info не меняется, NULL даёт -1. */
|
||||
int attachment_validate(const struct attachment_info* info);
|
||||
/* Записать полные проверенные метаданные в out ёмкостью capacity; ошибка не меняет out.
|
||||
* Вернуть точную длину wire или -1. out обязателен, ATTACHMENT_WIRE_MAX достаточно для любого kind. */
|
||||
int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t capacity);
|
||||
/* Разобрать ровно length байт и проверить метаданные; data/info обязательны. 0/-1.
|
||||
* При ошибке info может быть изменена: результат используется только при возврате 0. */
|
||||
int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info);
|
||||
/* Заимствованная постоянная строка: VOICE=audio/opus, VIDEO=video/mp4, иначе application/octet-stream. Не валидатор kind. */
|
||||
const char* attachment_content_type(enum attachment_kind kind);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -133,7 +133,7 @@ static void attachment_publish(struct attachment_send_req* req, int error) {
|
||||
|
||||
static void attachment_video_done(void* arg, int error, const struct video_info* info) {
|
||||
struct attachment_send_req* req = arg;
|
||||
if (!error && info && info->is_video && info->duration_ms > 0 && info->duration_ms <= 86400000 &&
|
||||
if (!error && info && info->is_video && info->duration_ms > 0 && info->duration_ms <= ATTACHMENT_DURATION_MAX_MS &&
|
||||
info->width > 0 && info->width <= 16384 && info->height > 0 && info->height <= 16384) {
|
||||
req->info.duration_ms = (uint32_t)info->duration_ms;
|
||||
req->info.width = (uint16_t)info->width; req->info.height = (uint16_t)info->height;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
/* attachment_send — подготовить голос/видео и отправить в зафиксированный канал или DM. Вызывать trampoline
|
||||
* через uasync_post. Request и PCM/compressor переходят во владение задачи;
|
||||
* pcm_release освобождает pcm_owner, NULL означает PCM из u_malloc.
|
||||
/* attachment_send — подготовить вложение и передать его публикации в зафиксированный канал или DM.
|
||||
* Trampoline вызывается в uasync (обычно через uasync_post), забирает req и PCM/compressor, включая отказ запуска.
|
||||
* Если сам uasync_post не принят, request и его ресурсы остаются у вызывающего.
|
||||
* pcm_release освобождает pcm_owner, NULL означает PCM из u_malloc; release может быть в worker
|
||||
* или в uasync при отказе/отмене без work. Нельзя использовать GUI-объекты, требующие освобождения в UI-потоке.
|
||||
* Qt/Android используют общий результат подготовки, доставка остаётся в chat/dm.
|
||||
* req выделяется u_calloc; caller заполняет target/is_dm, info и source либо PCM. inst должен жить до done.
|
||||
* req выделяется u_calloc; caller заполняет target/is_dm, info и source либо PCM/compressor, строки NUL-терминированы.
|
||||
* inst/config/media_async/uasync живут до завершения задачи. После вызова req нельзя читать или освобождать.
|
||||
* Исходник не изменять во время подготовки; temporary_source разрешает удалить только собственный временный source.
|
||||
* Подготовка выполняется в media_async worker, публикация и attachment-события — в uasync.
|
||||
* output/id/error заполняет задача; temporary_source разрешает удалить принадлежащий приложению source. */
|
||||
* Attachment-события 0/1/2 означают начало/готовность/ошибку подготовки, не подтверждение доставки сообщения.
|
||||
* Подготовленный output затем забирает chat/dm; собственные пути и PCM освобождаются также при отмене. */
|
||||
#ifndef ATTACHMENT_SEND_H
|
||||
#define ATTACHMENT_SEND_H
|
||||
#include "attachment.h"
|
||||
@@ -18,22 +23,23 @@ struct attachment_send_req {
|
||||
char target[64]; /* Десятичный channel_id либо conv_id без префикса dm:, зафиксированный до подготовки. */
|
||||
int is_dm;
|
||||
int encrypted; /* Отдельный случайный ключ группового вложения с ручной выдачей. */
|
||||
char content_type[32]; /* Необязательный исходный MIME (например image/png). */
|
||||
uint64_t dm_generation; /* Захватывается до подготовки и сохраняется во всех её стадиях. */
|
||||
unsigned dm_paths_held;
|
||||
char content_type[32]; /* Исходный MIME только для encrypted channel; пусто → MIME по kind. */
|
||||
uint64_t dm_generation; /* Поколение беседы до подготовки; 0 → захват в trampoline. Удалённое поколение не публикуется. */
|
||||
unsigned dm_paths_held; /* Внутренние holds output/source; caller оставляет 0. */
|
||||
uint8_t reply_to[32]; /* Оригинал канального ответа, зафиксированный до подготовки. */
|
||||
uint64_t request_id; /* CHAT_EVT_MSG_SUBMITTED после публикации либо ошибки. */
|
||||
struct attachment_info info;
|
||||
uint8_t id[16];
|
||||
char source[1024], output[1024];
|
||||
int transcode, temporary_source;
|
||||
uint64_t request_id; /* Только канал: CHAT_EVT_MSG_SUBMITTED после публикации либо ошибки. Для DM — 0. */
|
||||
struct attachment_info info; /* kind обязателен; пустое name заменяется UUID.opus/mp4; для copy нужны готовые метаданные. */
|
||||
uint8_t id[16]; /* Внутренний UUID подготовки для attachment-событий; не UUID конечного media_index/DM. */
|
||||
char source[1024], output[1024]; /* source задаёт caller; output — внутреннее случайное назначение задачи. */
|
||||
int transcode, temporary_source; /* transcode только VIDEO: отдельный FFmpeg worker; иначе файл копируется. */
|
||||
const int16_t* pcm;
|
||||
size_t pcm_count; /* Для voice: int16, 48 кГц моно, число отсчётов. */
|
||||
void* pcm_owner;
|
||||
void (*pcm_release)(void* owner);
|
||||
struct audio_compressor* compressor;
|
||||
int preset, error;
|
||||
void* pcm_owner; /* Аргумент собственного pcm_release. */
|
||||
void (*pcm_release)(void* owner); /* NULL → u_free(pcm); задача вызывает не более одного раза. */
|
||||
struct audio_compressor* compressor; /* Альтернатива PCM: задача flush/читает output/destroy, внешние обращения прекращены. */
|
||||
int preset, error; /* preset: 0..2 для voice; error заполняет worker. */
|
||||
};
|
||||
/* arg — собственный struct attachment_send_req; завершается событиями chat/dm и освобождением req. NULL — no-op. */
|
||||
void attachment_send_trampoline(void* arg);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -10,118 +10,103 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
/* Сделать создание/удаление имён durable на POSIX. */
|
||||
int encrypted_file_sync_directory(const char* path) {
|
||||
#ifndef _WIN32
|
||||
char parent[1024];
|
||||
snprintf(parent, sizeof(parent), "%s", path);
|
||||
char* slash = strrchr(parent, '/');
|
||||
if (!slash) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: path without parent: %s", path); return -1; }
|
||||
*slash = '\0';
|
||||
int fd = open(parent, O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory open failed: %s: %s", parent, strerror(errno)); return -1; }
|
||||
int rc = fsync(fd);
|
||||
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory sync failed: %s: %s", parent, strerror(errno));
|
||||
close(fd);
|
||||
return rc ? -1 : 0;
|
||||
#else
|
||||
(void)path;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Windows MoveFileEx обеспечивает замену и write-through существующего артефакта. */
|
||||
int encrypted_file_publish(const char* temp, const char* path) {
|
||||
#ifdef _WIN32
|
||||
if (!MoveFileExA(temp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s winerr=%lu", temp, path, GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
if (rename(temp, path) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s: %s", temp, path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
return encrypted_file_sync_directory(path);
|
||||
}
|
||||
|
||||
#define EF_HEADER 66
|
||||
#define EF_META (32 + ATTACHMENT_WIRE_MAX)
|
||||
|
||||
/* Гарантировать сохранение файла перед публикацией или подтверждением. */
|
||||
int encrypted_file_sync(const char* path) {
|
||||
FILE* f = fopen(path, "rb+");
|
||||
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync open failed path=%s: %s", path, strerror(errno)); return -1; }
|
||||
#ifdef _WIN32
|
||||
int rc = _commit(_fileno(f));
|
||||
#else
|
||||
int rc = fsync(fileno(f));
|
||||
#endif
|
||||
if (fclose(f)) rc = -1;
|
||||
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync failed path=%s: %s", path, strerror(errno));
|
||||
return rc ? -1 : 0;
|
||||
/* Одно ограничение поля для отправителя и аутентифицированных метаданных reader. */
|
||||
int encrypted_file_validate_type(const char* type) {
|
||||
size_t length = type ? strnlen(type, 32) : 0;
|
||||
if (!length || length == 32) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: invalid content type length=%zu present=%d allowed=1..31", length, !!type);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
unsigned byte = (unsigned char)type[i];
|
||||
if (byte < 33 || byte > 126 || byte == '"' || byte == '\\') {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: invalid content type byte index=%zu value=0x%02x length=%zu", i, byte, length);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "encrypted file: content type validated length=%zu", length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Записать контейнер; частичный destination удаляется при любой ошибке. */
|
||||
int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32],
|
||||
/* Подготовить контейнер в эксклюзивном temp; конечный target не открывается для записи. */
|
||||
int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32],
|
||||
uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type) {
|
||||
if (!output || output->file || output->temporary[0]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: write requires inactive output owner"); return -1;
|
||||
}
|
||||
if (encrypted_file_validate_type(type)) return -1;
|
||||
FILE* in = NULL; FILE* out = NULL;
|
||||
int rc = -1;
|
||||
uint8_t header[EF_HEADER] = {0}, meta[EF_META] = {0}, cipher[EF_META + 16], nonce[13], hash[32];
|
||||
size_t length = 0;
|
||||
uint64_t size = 0;
|
||||
if (!source || !destination || !strcmp(source, destination) || !key || !author || !id ||
|
||||
!type || strlen(type) >= 32 || attachment_validate(info)) goto done;
|
||||
const char* reason = "invalid arguments";
|
||||
if (!source || !target || !key || !author || !id || attachment_validate(info)) goto done;
|
||||
reason = "source/target alias or invalid file type";
|
||||
if (file_io_paths(source, target)) goto done;
|
||||
reason = "cannot open source";
|
||||
in = file_io_open_read(source);
|
||||
if (!in) goto done;
|
||||
reason = "metadata encoding/encryption failed";
|
||||
int n = attachment_encode(info, meta + 32, ATTACHMENT_WIRE_MAX);
|
||||
if (n < 0) goto done;
|
||||
memcpy(meta, type, strlen(type));
|
||||
media_cipher_nonce(author, id, UINT64_MAX, nonce);
|
||||
if (media_ccm_encrypt(key, nonce, meta, (size_t)n + 32, cipher, &length)) goto done;
|
||||
in = fopen(source, "rb"); out = fopen(destination, "wb");
|
||||
if (!in || !out) goto done;
|
||||
reason = "cannot create private output";
|
||||
if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done;
|
||||
out = output->file;
|
||||
reason = "container write/encryption failed";
|
||||
memcpy(header, "UTENC001", 8); memcpy(header + 8, id, 16);
|
||||
header[32] = (uint8_t)(length >> 8); header[33] = (uint8_t)length;
|
||||
if (fwrite(header, 1, sizeof(header), out) != sizeof(header) || fwrite(cipher, 1, length, out) != length ||
|
||||
media_encrypt_stream(key, author, id, in, out, &size, hash)) goto done;
|
||||
for (unsigned i = 0; i < 8; i++) header[24 + i] = (uint8_t)(size >> (56 - i * 8));
|
||||
memcpy(header + 34, hash, 32);
|
||||
if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header) || fflush(out)) goto done;
|
||||
reason = "cannot rewrite container header";
|
||||
if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header)) goto done;
|
||||
reason = "source close or output durable finish failed";
|
||||
rc = 0;
|
||||
done:
|
||||
OPENSSL_cleanse(meta, sizeof(meta));
|
||||
if (in && fclose(in)) rc = -1;
|
||||
if (out && fclose(out)) rc = -1;
|
||||
if (!rc) rc = encrypted_file_sync(destination);
|
||||
if (!rc) rc = file_io_temp_finish(output);
|
||||
if (rc) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation failed source=%s: %s", source ? source : "?", strerror(errno));
|
||||
if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: partial cleanup failed");
|
||||
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu", (unsigned long long)author,
|
||||
(unsigned long long)size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation rejected source=%s target=%s reason=%s",
|
||||
source ? source : "?", target ? target : "?", reason);
|
||||
file_io_temp_discard(output);
|
||||
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu temp=%s",
|
||||
(unsigned long long)author, (unsigned long long)size, output->temporary);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Проверить подписанный hash, метаданные и все порции до разрешения публикации. */
|
||||
int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author,
|
||||
int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author,
|
||||
const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]) {
|
||||
if (!output || output->file || output->temporary[0]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: read requires inactive output owner"); return -1;
|
||||
}
|
||||
FILE* in = NULL; FILE* out = NULL;
|
||||
struct attachment_info result;
|
||||
char result_type[32];
|
||||
int rc = -1;
|
||||
uint8_t actual[32], header[EF_HEADER], meta[EF_META], cipher[EF_META + 16], nonce[13];
|
||||
size_t length = 0;
|
||||
uint64_t size = 0;
|
||||
const char* reason = "invalid arguments";
|
||||
if (!source || !destination || !strcmp(source, destination) || !key || !author || !id || !hash || !info || !type) goto done;
|
||||
if (!source || !target || !key || !author || !id || !hash || !info || !type) goto done;
|
||||
reason = "source/target alias or invalid file type";
|
||||
if (file_io_paths(source, target)) goto done;
|
||||
reason = "cannot hash ciphertext";
|
||||
if (ma_sha256_file(source, actual)) goto done;
|
||||
reason = "ciphertext does not match signed manifest hash";
|
||||
if (CRYPTO_memcmp(actual, hash, 32)) goto done;
|
||||
reason = "cannot open ciphertext or truncated header";
|
||||
in = fopen(source, "rb");
|
||||
in = file_io_open_read(source);
|
||||
if (!in || fread(header, 1, sizeof(header), in) != sizeof(header)) goto done;
|
||||
reason = "unsupported container format or mismatched UUID";
|
||||
if (memcmp(header, "UTENC001", 8) || memcmp(header + 8, id, 16)) goto done;
|
||||
@@ -133,15 +118,14 @@ int encrypted_file_read(const char* source, const char* destination, const uint8
|
||||
reason = "metadata authentication failed";
|
||||
if (media_ccm_decrypt(key, nonce, cipher, encrypted_size, meta, &length)) goto done;
|
||||
reason = "invalid attachment metadata";
|
||||
if (length < 32 ||
|
||||
!memchr(meta, 0, 32) || !meta[0] || attachment_decode(meta + 32, length - 32, info)) goto done;
|
||||
if (length < 32 || attachment_decode(meta + 32, length - 32, &result)) goto done;
|
||||
reason = "invalid content type";
|
||||
for (unsigned i = 0; i < 32 && meta[i]; i++) if (meta[i] < 33 || meta[i] > 126 || meta[i] == '"' || meta[i] == '\\') goto done;
|
||||
memcpy(type, meta, 32);
|
||||
if (encrypted_file_validate_type((const char*)meta)) goto done;
|
||||
memcpy(result_type, meta, 32);
|
||||
for (unsigned i = 0; i < 8; i++) size = (size << 8) | header[24 + i];
|
||||
out = fopen(destination, "wb");
|
||||
reason = "cannot open temporary plaintext";
|
||||
if (!out) goto done;
|
||||
if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done;
|
||||
out = output->file;
|
||||
reason = "file authentication, size or body hash failed";
|
||||
if (media_decrypt_stream(key, author, id, size, header + 34, in, out)) goto done;
|
||||
reason = "close or durable file sync failed";
|
||||
@@ -149,12 +133,15 @@ int encrypted_file_read(const char* source, const char* destination, const uint8
|
||||
done:
|
||||
OPENSSL_cleanse(meta, sizeof(meta));
|
||||
if (in && fclose(in)) rc = -1;
|
||||
if (out && fclose(out)) rc = -1;
|
||||
if (!rc) rc = encrypted_file_sync(destination);
|
||||
if (!rc) rc = file_io_temp_finish(output);
|
||||
if (rc) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s reason=%s", source ? source : "?", reason);
|
||||
if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected output cleanup failed");
|
||||
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu", (unsigned long long)author,
|
||||
(unsigned long long)size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s target=%s reason=%s",
|
||||
source ? source : "?", target ? target : "?", reason);
|
||||
file_io_temp_discard(output);
|
||||
} else {
|
||||
*info = result; memcpy(type, result_type, sizeof(result_type));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu temp=%s",
|
||||
(unsigned long long)author, (unsigned long long)size, output->temporary);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -1,23 +1,33 @@
|
||||
/* Контейнер группового вложения: публичный заголовок, зашифрованные метаданные и
|
||||
* порции AES-256-CCM. Полный SHA256 контейнера берётся из подписанного сообщения.
|
||||
* Все функции файловые, вызываются только в media_async worker. Выход decrypt —
|
||||
* временный файл; публиковать его можно только после успеха и durable сохранения. */
|
||||
* Write/read вызывать только в media_async worker: они готовят отдельный durable temp,
|
||||
* конечное имя публикует владелец через file_io_temp_publish. Source не изменяется. */
|
||||
#ifndef ENCRYPTED_FILE_H
|
||||
#define ENCRYPTED_FILE_H
|
||||
#include "attachment.h"
|
||||
#include "../../lib/file_io.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#define ENCRYPTED_ATTACHMENT_TYPE "application/x-utun-encrypted"
|
||||
int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32],
|
||||
/* Ограничение MIME существующего контейнера: NUL-строка длиной 1..31 байт,
|
||||
* ASCII 33..126 без двойной кавычки и обратной косой черты. NULL/нет NUL в первых 32 байтах → -1.
|
||||
* 0 — подходит, -1 — ошибка с диагностикой без вывода сырой строки. Без I/O, допустим вызов в uasync.
|
||||
* Это проверка представления поля контейнера, а не полный разбор MIME-грамматики. */
|
||||
int encrypted_file_validate_type(const char* type);
|
||||
/* source/target — UTF-8 пути разных файлов; target отсутствует либо regular без конечной ссылки.
|
||||
* output заранее обнулён и имеет стабильный адрес до publish/discard; активный output отклоняется без очистки.
|
||||
* 0 — output содержит закрытый проверенный temp рядом с target, target ещё не изменён.
|
||||
* -1 — ошибка; source/target сохранены, собственный temp передан discard (ошибки удаления логируются).
|
||||
* Ключ — 32 байта, UUID — 16, author!=0. type должен пройти encrypted_file_validate_type;
|
||||
* неверный type отвергается до файловых операций и не меняет output/source/target.
|
||||
* Пути и файлы не подменяются во время вызова; публикация повторно проверяет их идентичность. */
|
||||
int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32],
|
||||
uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type);
|
||||
int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author,
|
||||
/* Проверить подписанный hash всего контейнера, UUID, метаданные и все tags. Контракт output — как у write.
|
||||
* info/type[32] присваиваются только при успехе; при ошибке прежние значения сохраняются. */
|
||||
int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author,
|
||||
const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]);
|
||||
/* Flush/fsync и close проверяет владелец; этот примитив сохраняет уже закрытый файл. */
|
||||
int encrypted_file_sync(const char* path);
|
||||
/* Durable rename (write-through на Windows) и сохранение каталога на POSIX. */
|
||||
int encrypted_file_publish(const char* temporary, const char* path);
|
||||
int encrypted_file_sync_directory(const char* path);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -164,7 +164,7 @@ int ma_copy_file(const char* source, const char* destination) {
|
||||
int status = file_io_paths(source, destination);
|
||||
uint64_t total = 0;
|
||||
if (status == 0) phase = "create temporary";
|
||||
if (status == 0 && file_io_temp_open(&output, destination)) status = -1;
|
||||
if (status == 0 && file_io_temp_open(&output, destination, 0)) status = -1;
|
||||
if (status == 0) {
|
||||
uint8_t buffer[65536]; size_t count;
|
||||
while (1) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* Общие AES-256-CCM примитивы. Файлы обрабатываются порциями с независимыми tags.
|
||||
* nonce определяется author/UUID/part; UUID новый для каждой подготовки. Домен nonce
|
||||
* utun_dm_media сохранён для существующего формата DM. Повторы используют готовый ciphertext.
|
||||
* Файловые функции вызываются в worker; caller открывает файлы и публикует результат
|
||||
* только после успешной проверки и durable сохранения. 0 = успех, -1 = ошибка. */
|
||||
/* media_crypto — AES-256-CCM сообщений и файлов без БД, транспорта и владения файлами.
|
||||
* Ключ 32 байта, nonce 13 байт, tag 16 байт; AAD нет. UUID новый для каждой подготовки
|
||||
* с данным ключом/автором; сетевые повторы используют уже готовый ciphertext.
|
||||
* FILE-функции вызываются в worker и работают с текущих позиций до EOF источника.
|
||||
* Caller открывает отдельные src/dst, закрывает их и публикует результат только после
|
||||
* успешной проверки и durable сохранения. Здесь есть fflush(dst), но нет close/fsync/rename.
|
||||
* Ключи, буферы и FILE заимствованы на время вызова. int-функции: 0 = успех, -1 = ошибка. */
|
||||
#ifndef MEDIA_CRYPTO_H
|
||||
#define MEDIA_CRYPTO_H
|
||||
#include <stdint.h>
|
||||
@@ -16,16 +18,31 @@ extern "C" {
|
||||
#define MEDIA_CIPHER_TAG_SIZE 16
|
||||
#define MEDIA_CIPHER_PLAIN_MAX 65535u
|
||||
#define MEDIA_CIPHER_CHUNK_SIZE 32768u
|
||||
/* Первые 13 байт SHA256("utun_dm_media"[13] || author:uint64 || id:16 || part:uint64).
|
||||
* Числа в native byte order текущего формата; id/nonce обязательны, проверок аргументов нет.
|
||||
* part — номер порции с нуля; разным plaintext с тем же ключом нужен новый id. */
|
||||
void media_cipher_nonce(uint64_t author, const uint8_t id[16], uint64_t part, uint8_t nonce[MEDIA_CIPHER_NONCE_SIZE]);
|
||||
/* out вмещает plain_len+16; расшифровка проверяет tag до возврата plaintext. */
|
||||
/* plain_len 0..65535, plain=NULL допустим только при нулевой длине; out вмещает plain_len+16.
|
||||
* out/key/nonce/out_len обязательны; успех даёт ciphertext||tag и его длину.
|
||||
* После проверки аргументов *out_len=0 до успеха; при неверных аргументах выходы не меняются. */
|
||||
int media_ccm_encrypt(const uint8_t key[32], const uint8_t nonce[13], const uint8_t* plain,
|
||||
size_t plain_len, uint8_t* out, size_t* out_len);
|
||||
/* cipher_len 16..65551, out вмещает cipher_len-16 байт; все указатели обязательны.
|
||||
* Только rc=0 разрешает использовать plaintext. При ошибке содержимое out не определено;
|
||||
* после проверки аргументов *out_len=0 до успешной проверки tag. При неверных аргументах выходы не меняются. */
|
||||
int media_ccm_decrypt(const uint8_t key[32], const uint8_t nonce[13], const uint8_t* cipher,
|
||||
size_t cipher_len, uint8_t* out, size_t* out_len);
|
||||
/* Поток plaintext → ciphertext; возвращает размер оригинала и SHA256 ciphertext. */
|
||||
/* Порции по 32768 plaintext-байт, каждая с отдельным tag; пустой файл тоже даёт один tag.
|
||||
* author!=0, все указатели обязательны, src!=dst; файлы должны обозначать разные данные.
|
||||
* При корректных аргументах plain_size/hash обнуляются и выдаются только при успехе:
|
||||
* фактический размер plaintext и SHA256 всего ciphertext, без внешнего заголовка контейнера.
|
||||
* Ошибка может оставить частичный dst; выходные FILE не закрываются. */
|
||||
int media_encrypt_stream(const uint8_t key[32], uint64_t author, const uint8_t id[16],
|
||||
FILE* src, FILE* dst, uint64_t* plain_size, uint8_t hash[32]);
|
||||
/* Проверяет каждый tag, точный размер и полный hash; dst должен быть временным. */
|
||||
/* Те же требования к аргументам/файлам; plain_size/hash берутся из проверенного manifest.
|
||||
* Проверяет каждый tag, точный размер (включая EOF) и полный hash ciphertext.
|
||||
* dst должен быть собственным временным файлом: до общей проверки в него уже записываются порции.
|
||||
* Частичный/непроверенный dst при ошибке отбрасывает вызывающий. */
|
||||
int media_decrypt_stream(const uint8_t key[32], uint64_t author, const uint8_t id[16],
|
||||
uint64_t plain_size, const uint8_t hash[32], FILE* src, FILE* dst);
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -12,8 +12,16 @@ int voice_file_encode(const int16_t* pcm, size_t count, int rate, int channels,
|
||||
static const char levels[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
if (!pcm || !info || !path || (rate != 8000 && rate != 12000 && rate != 16000 && rate != 24000 && rate != 48000) ||
|
||||
(channels != 1 && channels != 2) || preset < 0 || preset > 2) goto invalid;
|
||||
size_t frame = (size_t)(rate / 50) * channels, samples = count / frame * frame;
|
||||
if (samples < (size_t)rate * channels / 2 || samples / channels / rate > 86400) goto invalid;
|
||||
size_t frame = (size_t)(rate / 50) * channels, frames = count / frame;
|
||||
if (frames < 25 || frames > ATTACHMENT_DURATION_MAX_MS / 20) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA,
|
||||
"voice_file: invalid duration count=%zu rate=%d channels=%d frames=%zu allowed_frames=25..%u",
|
||||
count, rate, channels, frames, ATTACHMENT_DURATION_MAX_MS / 20);
|
||||
return -1;
|
||||
}
|
||||
size_t samples = frames * frame;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "voice_file: encode count=%zu rate=%d channels=%d frames=%zu duration=%ums discarded=%zu",
|
||||
count, rate, channels, frames, (uint32_t)(frames * 20), count - samples);
|
||||
opus_codec_encoder_t* encoder = opus_codec_encoder_create(rate, channels);
|
||||
if (!encoder) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice_file: encoder creation failed"); return -1; }
|
||||
opus_codec_encoder_bitrate_set(encoder, bitrates[preset]);
|
||||
@@ -34,7 +42,7 @@ int voice_file_encode(const int16_t* pcm, size_t count, int rate, int channels,
|
||||
}
|
||||
uint16_t end = 0;
|
||||
if (fwrite(&end, 2, 1, file) != 1 || fflush(file)) goto done;
|
||||
info->kind = ATTACHMENT_VOICE; info->duration_ms = (uint32_t)(samples / frame * 20);
|
||||
info->kind = ATTACHMENT_VOICE; info->duration_ms = (uint32_t)(frames * 20);
|
||||
info->width = info->height = 0;
|
||||
for (size_t i = 0; i < 100; i++) {
|
||||
size_t first = i * samples / 100, last = (i + 1) * samples / 100;
|
||||
@@ -57,6 +65,6 @@ done:
|
||||
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "voice_file: prepared path=%s duration=%ums rate=%d channels=%d", path, info->duration_ms, rate, channels);
|
||||
return error;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice_file: invalid/short recording count=%zu rate=%d channels=%d preset=%d", count, rate, channels, preset);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice_file: invalid parameters count=%zu rate=%d channels=%d preset=%d", count, rate, channels, preset);
|
||||
return -1;
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
/* voice_file — кодирование PCM в общий контейнер Opus голосовых сообщений Qt/Android. Вызывать только в worker.
|
||||
* PCM принадлежит вызывающему; encode заполняет duration/waveform по реально
|
||||
* записанным кадрам. При ошибке неполный файл удаляется; возврат 0 / -1.
|
||||
* PCM принадлежит вызывающему; encode заполняет duration/waveform по реально записанным кадрам. Возврат 0 / -1.
|
||||
* path — собственный файл вызывающего: fopen(wb) перезаписывает прежнее содержимое;
|
||||
* после ошибки записи/close файл удаляется (отказ удаления логируется). fsync/атомарной публикации здесь нет.
|
||||
* count — число interleaved int16 отсчётов; rate — 8/12/16/24/48 kHz, channels — 1/2, preset — 0..2.
|
||||
* Пишутся полные кадры по 20мс (хвост отбрасывается), минимум 500мс, максимум 24ч.
|
||||
* name задаёт caller; encode устанавливает kind=VOICE и обнуляет width/height. */
|
||||
* Пишутся полные кадры по 20мс (хвост отбрасывается), минимум 500мс, максимум ATTACHMENT_DURATION_MAX_MS.
|
||||
* Недопустимая длительность отвергается до создания encoder, открытия path и изменения info.
|
||||
* name задаёт caller; encode устанавливает kind=VOICE и обнуляет width/height.
|
||||
* info используется только при rc=0: при поздней ошибке close часть полей уже может быть изменена.
|
||||
* Формат native-endian: magic:uint32=0x5355504f, rate:uint32, channels:uint16, samples_per_channel:uint16;
|
||||
* затем (packet_len:uint16, Opus bytes) для каждого кадра и нулевая длина в конце. */
|
||||
#ifndef VOICE_FILE_H
|
||||
#define VOICE_FILE_H
|
||||
#include "attachment.h"
|
||||
|
||||
@@ -1,47 +1,67 @@
|
||||
Архитектура распространения медиа
|
||||
|
||||
1. src (создатель медиа) - подписывает, разделяет на блоки, создает UUID, отправляет в сообщении информацию о всех блоках для возможности скачивания и валидации (проверки сумм блоков и подписи файла).
|
||||
2. req узел - кто хочет получить медиа.
|
||||
Актуальный протокол репликации и передачи блоков описан в sync.md и media_delivery_proto.h.
|
||||
Контракты API и владения — в media_index.h, media_download.h, media_delivery.h и file_transfer.h.
|
||||
|
||||
Подготовка и регистрация
|
||||
|
||||
основаня идея распространения контента
|
||||
- каждый узел выбирает удобный (по пигну) на старте. выбирает следующий если таймаут (2 сек)
|
||||
и устанавливает к нему прямое подключение. уведомляет суперузел что он обслуживает этот узел.
|
||||
Суперузлы хранят в памяти список узлов (ll_queue) которые они обслуживают. Если соединение рвётся - узел автоматически выпадает из списка обслуживаемых.
|
||||
media_index готовит неизменяемый файл в media_async worker: при необходимости копирует,
|
||||
вычисляет SHA256 всего содержимого, создаёт UUID файла/блоков и Ed25519-подписи байтов блоков.
|
||||
Размер обычного блока выбирается в пределах 10..25 MiB; последний блок может быть короче.
|
||||
В uasync completion регистрирует результат в SQLite. Подписанное сообщение чата содержит
|
||||
UUID, геометрию, SHA256 и подписи блоков, необходимые получателю для проверки.
|
||||
|
||||
- req-узел когда принял полный блок - уведомляет суперузел об этом.
|
||||
Назначение заранее учитывается в media_preparations. Файловые операции и SQLite не образуют
|
||||
общую транзакцию; неуспешная подготовка остаётся учтённой для удаления канала.
|
||||
Зашифрованное групповое вложение проходит тот же pipeline как непрозрачный контейнер;
|
||||
ключи и расшифрованные копии обслуживает chat_attachment, отдельно от каталога держателей.
|
||||
|
||||
- суперузлы между собой реплицируют таблицу с узлами для скачивания.
|
||||
Также если подключен автор контекнта то суперузел автору контента отправляет обновления о доступности его блоков.
|
||||
Поиск и загрузка
|
||||
|
||||
На всех узлах есть таблица с узлами для скачивания блоков.
|
||||
ID primary/autincrement | UUID блока | id группы | id ноды | timestamp когда добавлено-обновлено
|
||||
Суперузлы в нее сохраняют весь контент групп в которых они состоят. обычные узлы сохраняют только свой контент.
|
||||
Если нет суперузлов или они недоступны - то роль суперузла выполняет автор контента.
|
||||
media_download получает метаданные файла и ищет держателей через QUERY пачками до32 блоков.
|
||||
Источники поиска — до9 суперузлов из таблицы своей CHAT-группы в порядке node_id, затем автор.
|
||||
Это отдельный поиск кандидатов; полномочия репликации проверяются по подписанным adm_tags.
|
||||
На блок сохраняется до4 держателей, на загрузку — до10 peer slots. Сортировки по RTT здесь нет.
|
||||
Один QUERY имеет собственные UUID/дедлайн, по умолчанию2с; трафик блоков не продлевает его.
|
||||
Каждый CM handle принадлежит своей операции; CM может получить прямой, обратный или непрямой путь.
|
||||
|
||||
BLOCK_REQ создаёт новый transfer_id. Ответы принимаются только от текущего держателя
|
||||
с совпавшими group/media/block/chunk/UUID. Записывается непрерывный префикс, дубли не дают прогресса.
|
||||
Простой по умолчанию2с переключает попытку; бюджет ошибок по умолчанию3 на блок.
|
||||
OVERLOADED задаёт отдельное ожидание, RELAY_FULL предлагает читателей конкретного блока.
|
||||
|
||||
Полученные байты проверяются по ожидаемой Ed25519-подписи автора. Все блоки собираются
|
||||
в соседний temp с обязательной проверкой полного SHA256, затем выполняются atomic rename
|
||||
и регистрация скачанного файла в SQLite. Приём, проверка, сборка и выдача выполняются в uasync.
|
||||
Этот путь не обещает durable fsync, используемый отдельно encrypted_file/file_io_temp.
|
||||
|
||||
Выдача и relay
|
||||
|
||||
Репликация между суперузлами.
|
||||
Растущий temp представлен media_block_source: producer и каждая выдача удерживают свою ссылку.
|
||||
Доступен только успешно записанный префикс; проверенный temp живёт до последнего читателя.
|
||||
У каждого stream_ctx собственные offset/UUID, waiter, отложенный pump и таймер.
|
||||
До5 relay-читателей на растущий блок; до3 выдач индексированных файлов одновременно,
|
||||
также действует настраиваемый лимит на файл. DATA/DONE соблюдают router backpressure.
|
||||
При отмене/удалении снимаются callbacks, waiter, pump и timer до освобождения владельца.
|
||||
|
||||
Каждый суперузел хранит указатель (id) по какой элемент синхронизировано с каждым другим суперузлом.
|
||||
отправляет злементы и ждёт подтверждения (не более 4 в очереди). переповтор - 10 сек без ответа.
|
||||
Суперузлы мониторят появление других суперузлов в bgp таблице. когда узел появился пробуют установить прямое подключение (без посредников и без обратного подключения - это сделает другой суперузел сам).
|
||||
Если не получается 10 сек - отключаемся и следующая попытка через час.
|
||||
то есть как только видят повяление - пробуют установить прямую связь каждый с каждым.
|
||||
Прямое подключение - это независимый автономный процесс.
|
||||
Для этого нужен коллбэк из bgp - появился новый узел и обновился узел. Его надо добавить если нет с возможностью цепочки подписчиков (как коллбэки статусов в etcp).
|
||||
Каталог и репликация
|
||||
|
||||
при появлении суперузлата также начинаем репликацию таблицы блоков - отдельный асинхронный процесс.
|
||||
для каждого суперузла свой контекст (таймаут, состояние протокола). отправляем обновление и ждём подтверждение с таймаутом (2 сек). таумаут постепенно увеличиваем до 10 минут если нет подтверждений и сбрасываем если появилось.
|
||||
block_availability хранит объявления PROCESSING/COMPLETE по (group,block,node).
|
||||
Merge выбирает максимум (status,timestamp); дубликат не создаёт новый локальный ID.
|
||||
Собственные блоки переанонсируются из media_files с проверкой типов/границ полей SQLite.
|
||||
HAVE_BLOCK_ACK отправляется ответом на объявление, но инициатор его не ждёт и игнорирует при приёме.
|
||||
SERVE_REG/LEAVE обслуживают вспомогательный список; основной загрузчик их не посылает,
|
||||
SERVE_ACK не отправляется. Учёт обслуживаемых узлов не определяет репликацию или загрузку.
|
||||
|
||||
Контекст репликации создаётся на пару (group,peer), если обе стороны — суперузлы этой группы
|
||||
по подписанным adm_tags. Каждый контекст имеет свой CM handle. Роли перечитываются
|
||||
при init/member_sync и каждые30с; потеря роли очищает только соответствующую группу.
|
||||
Приём хранит source_epoch/last_recv_id. Страница до32 записей и новый RX-курсор фиксируются
|
||||
одной транзакцией. SYNC использует little endian; остальные числовые wire-поля — native endian.
|
||||
Один RPC повторяется по дедлайнам2..30с, watchdog работает каждые200мс; CHANGED — подсказка
|
||||
о новых данных, потеря которой восстанавливается периодическим запросом каждые30с.
|
||||
Обычный stop/start сохраняет каталог, эпоху и RX-курсоры; удаление канала удаляет его данные.
|
||||
|
||||
Алгоритм.
|
||||
при получении сообщения (в зависимости от настроек кеширования) узлы получившие сообщение с медиа хотят получить вложение. Порядок действий:
|
||||
- выбирают суперузел с минимальным rtt и отправляют ему запрос - хочу скачать медиа. дай список узлов с которых качаем.
|
||||
В запросе - UUID запрашиваемых блоков (обязательно один файл можно несколько блоков для него), id src узла.
|
||||
запрос направляется в суперузел изи автору контента - если нет суперузлов или они недоступны. таймаут 2 сек.
|
||||
далее выбираем лучшие узлы (не более 10 штук по пингу) и пробуем к ним установить подключение (прямое или через промежуточный узел).
|
||||
перебираем пытаясь подключиться, пока не наберем нужное количество (по количеству блоков, но не более 10 штук)
|
||||
к кому подключились - запрашиваем блоки также с выбором следующего при таймауте или неудаче.
|
||||
отправляем им запрос и пытаемся скачать.
|
||||
Отдельный file_transfer передаёт неизменяемые файлы между явно заданными узлами для DM.
|
||||
Он использует подписанные router-пакеты и свои CM handles, а авторизацию, ключи, проверку
|
||||
содержимого, временный путь и окончательную публикацию предоставляет вызывающий модуль.
|
||||
@@ -3,7 +3,11 @@
|
||||
* и возвращает путь; получатель проверяет содержимое в своём done.
|
||||
* Каждая операция владеет CM handle до завершения/отмены, включая DOWN/TIMEOUT.
|
||||
* Все API/callbacks в uasync; destroy вызывается вне callbacks, до удаления групп.
|
||||
* Файл назначения принадлежит вызывающему; при ошибке его нужно отбросить.
|
||||
* До 16 входящих/исходящих операций суммарно. DATA до 8192 байт; router-пакеты подписаны,
|
||||
* uint64 wire-поля имеют native byte order. Простой 30с завершает операцию ошибкой;
|
||||
* при восстановлении пути/простое приём повторяет REQUEST с непрерывного полученного offset.
|
||||
* Собственное временное назначение передаёт вызывающий: файл открывается с усечением,
|
||||
* в том числе до возможного отказа запуска. При ошибке/cancel файл не удаляется и отбрасывается caller.
|
||||
*/
|
||||
#ifndef FILE_TRANSFER_H
|
||||
#define FILE_TRANSFER_H
|
||||
@@ -15,21 +19,33 @@ struct UTUN_INSTANCE;
|
||||
struct file_transfer;
|
||||
struct file_transfer_op;
|
||||
|
||||
/* Вызывается для каждого REQUEST, включая повторы: проверить права group/peer/object/size.
|
||||
* 0 — записан NUL-терминированный путь (<cap байт) неизменяемого файла; иначе отказ.
|
||||
* Файл сохранять доступным до завершения выдачи; перед отзывом отменить операции объекта. */
|
||||
typedef int (*file_transfer_lookup_fn)(void* arg, uint64_t group, uint64_t peer,
|
||||
const uint8_t object[16], uint64_t size, char* path, size_t cap);
|
||||
typedef void (*file_transfer_done_fn)(void* arg, int error);
|
||||
|
||||
/* 0 в done означает точный приём size байт и закрытие файла, а не custody ACK.
|
||||
* Возврат NULL означает отказ запуска, done не вызывается.
|
||||
* cancel синхронно вызывает done(-2); ошибка fclose заменяет результат на -1, в том числе при отмене.
|
||||
* Handle перестаёт существовать перед вызовом done, его нужно обнулить в callback. */
|
||||
/* Один ft на instance; inst/его ua/router/группы живут дольше ft. lookup обязателен, arg заимствован.
|
||||
* Создаёт router binding; возвращает owner либо NULL при ошибке. */
|
||||
struct file_transfer* file_transfer_create(struct UTUN_INSTANCE* inst, file_transfer_lookup_fn lookup, void* arg);
|
||||
/* Снимает binding и синхронно завершает активные pull как cancel; NULL допустим. */
|
||||
void file_transfer_destroy(struct file_transfer* ft);
|
||||
|
||||
/* 0 в done означает точный приём size байт и закрытие файла, а не custody ACK.
|
||||
* group/peer ненулевые, object указывает на 16 байт, size=1..INT64_MAX; done обязателен.
|
||||
* destination — собственный временный путь; строка не сохраняется, файл открывается до возврата.
|
||||
* Возвращает handle операции; done/arg заимствованы до завершения/отмены.
|
||||
* Возврат NULL означает отказ запуска, done не вызывается.
|
||||
* Ошибка приёма/таймаут даёт done(-1); успешный приём не включает fsync или проверку хеша.
|
||||
* Handle перестаёт существовать перед вызовом done, его нужно обнулить в callback. */
|
||||
struct file_transfer_op* file_transfer_pull(struct file_transfer* ft, uint64_t group, uint64_t peer,
|
||||
const uint8_t object[16], uint64_t size, const char* destination,
|
||||
file_transfer_done_fn done, void* arg);
|
||||
/* Синхронный done(-2), ошибка fclose заменяет результат на -1. NULL допустим; прежний handle больше не использовать. */
|
||||
void file_transfer_cancel(struct file_transfer_op* op);
|
||||
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются синхронно с -2, при ошибке fclose — с -1. */
|
||||
/* Отозвать все операции объекта во всех группах (TTL/delete); ft=NULL допустим, object обязателен.
|
||||
* callbacks приёма вызываются синхронно с -2, при ошибке fclose — с -1. */
|
||||
void file_transfer_cancel_object(struct file_transfer* ft, const uint8_t object[16]);
|
||||
|
||||
#endif
|
||||
+1116
-1111
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,9 @@
|
||||
* После регистрации файла через media_index автор вызывает announce_media; получатель загружает его через media_download.
|
||||
* Суперузлы обмениваются сведениями о наличии блоков; держатели выдают подписанные блоки, загрузчик собирает файл.
|
||||
* Чат-сервис вызывает init и затем destroy; bind — другое имя init, вызывать оба последовательно нельзя.
|
||||
* Все операции — uasync; долгие файловые/crypto-задачи выполняет media_async. Для файлов DM используется dm_media/file_transfer. */
|
||||
* Все операции — uasync; приём/проверка/сборка/выдача блоков синхронные. Подготовку исходящих файлов выполняет media_async.
|
||||
* Готовые файлы и растущие temp имеют единый TX pump: свой offset/UUID/waiter на получателя, DONE после всего проверенного хвоста.
|
||||
* До трёх выдач индексированных файлов и до пяти relay-читателей на растущий блок. Для DM используется dm_media/file_transfer. */
|
||||
#ifndef MEDIA_DELIVERY_H
|
||||
#define MEDIA_DELIVERY_H
|
||||
|
||||
@@ -13,34 +15,38 @@ extern "C" {
|
||||
#include <stdint.h>
|
||||
#include "../lib/ll_queue.h"
|
||||
#include "../lib/sqlite3.h"
|
||||
#include "../lib/file_io.h"
|
||||
#include "media_delivery_proto.h"
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
// ── константы ──
|
||||
|
||||
#define MEDIA_MAX_REPL_INFLIGHT 4
|
||||
#define MEDIA_REPL_TIMEOUT_TB 20000 // начальный таймаут 2s (0.1ms)
|
||||
#define MEDIA_REPL_TIMEOUT_MAX_TB 6000000 // макс 10 min
|
||||
#define MEDIA_MAX_DOWNLOADS 10
|
||||
#define MEDIA_REPL_TIMEOUT_MAX_TB 300000 // предел повторов 30s
|
||||
#define MEDIA_REPL_POLL_TB 300000 // периодическая сверка 30s, включая потерю CHANGED
|
||||
#define MEDIA_REPL_WATCHDOG_TB 2000 // контроль дедлайна/очереди/CM каждые 200ms
|
||||
#define MEDIA_QUERY_TIMEOUT_TB 20000 // таймаут QUERY 2s
|
||||
#define MEDIA_HAVE_BLOCK_TIMEOUT_TB 20000 // таймаут HAVE_BLOCK_ACK 2s
|
||||
#define MEDIA_RECONNECT_COOLDOWN_TB 360000000 // 1 час (0.1ms)
|
||||
#define MEDIA_HELLO_TIMEOUT_TB 20000 // таймаут SUPER_HELLO 2s
|
||||
#define MEDIA_HAVE_BLOCK_TIMEOUT_TB 20000 // объявлен 2s; текущий HAVE_BLOCK не ждёт ACK и не ставит этот таймер
|
||||
#define MEDIA_DL_STALL_TIMEOUT_TB 20000 // таймаут простоя загрузки 2s (0.1ms)
|
||||
#define MEDIA_DL_MAX_ATTEMPTS 3 // попыток на блок до ошибки
|
||||
|
||||
// ── структуры ──
|
||||
|
||||
struct media_super_peer {
|
||||
struct ll_entry ll; // индекс по peer_node_id (8 байт)
|
||||
uint64_t peer_node_id;
|
||||
uint64_t group_id; // CHAT-группа, в которой установлена связь с пиром
|
||||
uint64_t peer_last_recv_id; // что пир говорит он получил от нас (из SUPER_HELLO / SUPER_ACK)
|
||||
uint32_t timeout_tb; // текущий таймаут (растёт при ошибках)
|
||||
uint8_t inflight_count; // пакетов в полёте (макс 4)
|
||||
uint8_t connected; // 1 = прямое подключение установлено
|
||||
uint8_t hello_done; // 1 = SUPER_HELLO обмен завершён, можно реплицировать
|
||||
void* timeout_timer; // таймер ретрансмита
|
||||
uint64_t peer_node_id; // Эти два поля подряд — 16-байтный ключ очереди.
|
||||
uint64_t group_id; // CHAT-группа, отдельное владение даже при том же peer_node_id.
|
||||
struct media_pkt_sync_req request; // Неизменяемый запрос до успешного commit/отмены.
|
||||
struct media_pkt_sync_req response; // Один ожидающий выдачи запрос пира; ответы вычисляются из БД.
|
||||
uint64_t deadline_tb; // Дедлайн запроса, idle-сверки или повторного открытия CM.
|
||||
uint32_t timeout_tb; // Задержка повторов 2..30s, сбрасывается после commit.
|
||||
uint8_t connected; // Рабочий путь по событию CM, включая непрямой.
|
||||
uint8_t request_pending, request_send, response_pending;
|
||||
uint8_t dirty; // CHANGED во время запроса: после commit проверить ещё раз.
|
||||
uint8_t changed; // Наш CHANGED ожидает места в router send_q.
|
||||
void* timeout_timer; // Watchdog восстанавливает и потерянный waiter при reset router.
|
||||
void* pump; // Отменяемая отложенная отправка; waiter не вызывает её рекурсивно.
|
||||
struct queue_waiter_handle waiter;
|
||||
struct CONN_MGR_HANDLE* connection; // собственное CM-владение; восстановлением пути управляет CM
|
||||
struct media_delivery_ctx* md; // обратная ссылка
|
||||
};
|
||||
@@ -54,32 +60,10 @@ struct md_file_load {
|
||||
uint64_t downloader_ids[10]; // кто сейчас качает (для редиректа)
|
||||
};
|
||||
|
||||
/*
|
||||
* block_availability: жизненный цикл записей и гарантия уникальности id
|
||||
*
|
||||
* Каждое событие создаёт новую запись с новым id (AUTOINCREMENT).
|
||||
* Старая запись удаляется — id НЕ переиспользуется.
|
||||
* Это критично для репликации: курсор super_sync.last_recv_id
|
||||
* всегда монотонно растёт и не пропускает события.
|
||||
*
|
||||
* BLOCK_PROCESSING (subcmd 0x10):
|
||||
* md_ba_insert(... , status=0) — чистый INSERT, новый id=X.
|
||||
*
|
||||
* HAVE_BLOCK (subcmd 0x06):
|
||||
* md_ba_complete_block():
|
||||
* 1. DELETE FROM block_availability WHERE block_uuid=? AND node_id=? AND status=0
|
||||
* (удаляет processing-запись id=X, если была)
|
||||
* 2. Если completed уже есть → no-op (возврат 0)
|
||||
* 3. INSERT ... status=1 — новый id=Y.
|
||||
*
|
||||
* SUPER_REPL (subcmd 0x08):
|
||||
* Принимающая сторона:
|
||||
* - status=0: INSERT OR IGNORE (дубликаты при репликации — норма)
|
||||
* - status=1: md_ba_complete_block() — та же логика что HAVE_BLOCK
|
||||
*
|
||||
* Итог: processing id=X удаляется, completed вставляется с новым id=Y>X.
|
||||
* Репликация через md_ba_get_since(id > last_recv_id) всегда видит Y.
|
||||
*/
|
||||
/* Каталог уникален по (group,block,holder). Объединение выбирает максимум (status,timestamp).
|
||||
* Только реальное изменение получает новый AUTOINCREMENT ID; повторы не меняют sqlite_sequence.
|
||||
* ID источника не становится локальным ID. Страница и RX-курсор сохраняются атомарно.
|
||||
* Верхняя граница выдачи — sqlite_sequence, включая удалённые ID и промежутки других групп. */
|
||||
|
||||
struct media_served_node {
|
||||
struct ll_entry ll; // индекс по node_id (8 байт)
|
||||
@@ -90,61 +74,64 @@ struct media_served_node {
|
||||
|
||||
#define MD_MAX_RELAY_DOWNSTREAM 5
|
||||
|
||||
struct relay_downstream {
|
||||
uint64_t node_id;
|
||||
uint64_t group_id;
|
||||
uint64_t sent_offset; // сколько байт уже отправлено этому пиру
|
||||
struct queue_waiter_handle waiter;
|
||||
};
|
||||
|
||||
struct relay_block_ctx {
|
||||
struct ll_entry ll; // индекс по block_id (16 байт)
|
||||
/* Источник байтов блока: producer загрузки и каждая выдача удерживают отдельную ссылку.
|
||||
* Индекс заимствует источник; последний владелец удаляет собственный temp.
|
||||
* available — только непрерывный, успешно закрытый префикс; verified разрешает отправить DONE. */
|
||||
struct media_block_source {
|
||||
uint8_t block_id[16];
|
||||
uint8_t media_id[16];
|
||||
uint64_t group_id;
|
||||
uint32_t chunk;
|
||||
uint64_t file_offset; // всего байт получено от апстрима (размер .chunk_N)
|
||||
char chunk_file[2048]; // путь к .chunk_N файлу для relay-чтения
|
||||
struct relay_downstream downstream[MD_MAX_RELAY_DOWNSTREAM];
|
||||
int downstream_count;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint32_t size;
|
||||
uint32_t available;
|
||||
uint64_t base; // Начало блока в индексированном файле; для temp — 0.
|
||||
unsigned refs;
|
||||
uint8_t indexed, owned, verified, failed;
|
||||
struct ll_entry* entry;
|
||||
struct media_delivery_ctx* md;
|
||||
struct file_io_temp storage; // Путь temp либо индексированного файла; открытых FILE не хранит.
|
||||
};
|
||||
|
||||
struct stream_ctx;
|
||||
struct relay_fwd_ctx;
|
||||
struct media_destination;
|
||||
struct media_delivery_ctx {
|
||||
struct stream_ctx* streams;
|
||||
struct relay_fwd_ctx* forwards;
|
||||
struct media_destination* destinations; // Активные писатели нормализованных путей, входящие и исходящие.
|
||||
uint64_t self_node_id;
|
||||
int initialized;
|
||||
uint8_t is_supernode; // из adm_tags в peers_*
|
||||
uint8_t active_streams; // текущее число исходящих стримов (admission)
|
||||
uint8_t is_supernode; // Диагностическое «есть хотя бы одна группа с нашей ролью supernode».
|
||||
uint8_t source_epoch[16]; // Сохранённое поколение каталога; перезапуск сервиса его не меняет.
|
||||
void* reconcile_timer; // Периодическое перечитывание ролей и восстановление CM после ошибок.
|
||||
uint8_t active_streams; // Выдачи индексированных файлов; растущий relay ограничен отдельно на блок.
|
||||
uint8_t max_downloads_per_file; // лимит параллельных загрузок с одного файла (source node), default 3
|
||||
uint8_t stream_completed; // 1 = хотя бы один стрим завершился (для тестов)
|
||||
uint8_t streams_started; // монотонный счётчик запущенных стримов (для тестов)
|
||||
uint8_t streams_started; // Диагностический счётчик запусков, по модулю 256.
|
||||
int dl_stall_timeout_tb; // таймаут простоя загрузки (по умолч. MEDIA_DL_STALL_TIMEOUT_TB)
|
||||
int query_timeout_tb; // Дедлайн одной попытки QUERY, независимо от входящих чанков; 0 — 2с.
|
||||
int dl_max_attempts; // попыток на блок (по умолч. MEDIA_DL_MAX_ATTEMPTS)
|
||||
sqlite3* db; // = inst->topo_sqlite_db (кешируем для удобства)
|
||||
struct ll_queue* served_nodes; // media_served_node — только суперузел
|
||||
struct ll_queue* super_peers; // media_super_peer — только суперузел
|
||||
struct ll_queue* downloads; // media_download — активные загрузки
|
||||
struct ll_queue* relay_blocks; // relay_block_ctx — relay в процессе загрузки
|
||||
struct ll_queue* block_sources; // media_block_source, заимствованный индекс по block_id.
|
||||
struct ll_queue* file_loads; // md_file_load — per-file download count (source node)
|
||||
struct UTUN_INSTANCE* inst;
|
||||
};
|
||||
|
||||
// ── Жизненный цикл и роль узла ──
|
||||
|
||||
// Создать таблицы/контекст и подключить router и подписки; 0/-1. Общая БД и группы должны быть готовы.
|
||||
// Создать таблицы/контекст и подключить router и подписки; 0/-1. Общие БД, media_index и chat_core уже готовы.
|
||||
int media_delivery_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Отменить загрузки/выдачи/репликацию и снять подписки; вызывать до удаления групп и общей БД.
|
||||
// Отменить загрузки/выдачи/репликацию и снять подписки; workers исходящих писателей уже завершены, группы/БД ещё живы.
|
||||
void media_delivery_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Вызывает init целиком; использовать вместо init, а не как следующий этап. 0/-1.
|
||||
int media_delivery_bind(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Включить/выключить локальную работу суперузла и обмен доступностью блоков.
|
||||
void media_delivery_set_supernode(struct UTUN_INSTANCE* inst, int is_supernode);
|
||||
// Перечитать актуальные роли из peers_* и согласовать владение CM по каждой CHAT-группе; только uasync.
|
||||
// Обычно вызывается автоматически по member_sync и периодически; не изменяет подписанные роли в БД.
|
||||
void media_delivery_refresh_roles(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// ── Внутренний учёт выдач ──
|
||||
|
||||
@@ -154,7 +141,8 @@ void media_delivery_stream_done(struct UTUN_INSTANCE* inst);
|
||||
// ── Анонсы доступности блоков ──
|
||||
|
||||
/* Автор анонсирует свои блоки суперузлам CHAT-группы (HAVE_BLOCK), чтобы первый
|
||||
* скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа. */
|
||||
* скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа.
|
||||
* Собственные зарегистрированные блоки добавляются в локальный каталог и без удалённых суперузлов. */
|
||||
void media_delivery_announce_media(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
const uint8_t* media_id, const uint8_t* block_ids,
|
||||
int num_blocks);
|
||||
@@ -166,22 +154,33 @@ void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_i
|
||||
|
||||
/* Переанонсировать суперузлам ВСЕ локальные блоки (media_files WHERE node_id=self).
|
||||
* Вызывается при старте после синхронизации, чтобы суперузел восстановил свой
|
||||
* block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */
|
||||
* block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле.
|
||||
* Некорректные SQL-типы/длины UUID, chunk вне uint32 и неканонический ID канала пропускаются с WARN.
|
||||
* Ошибка SQL завершает обход с ERROR; уже отправленные корректные анонсы сохраняются.
|
||||
* Анонсы best-effort, void не подтверждает доставку; UUID копируются до вложенных SQL/отправок. */
|
||||
void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// ── Отмена работы канала ──
|
||||
|
||||
/* Отменить всё активное, что связано с каналом (локальное удаление канала):
|
||||
* загрузки с group_id, их relay-контексты и super_peers этой группы. */
|
||||
* загрузки с group_id, их relay-контексты и super_peers этой группы.
|
||||
* Каталог группы и её RX-курсоры удаляются одной транзакцией; ошибка очистки логируется. */
|
||||
void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id);
|
||||
|
||||
// ── Внутренние операции загрузчика: relay ──
|
||||
struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id);
|
||||
/* Зарезервировать путь в общей базе перед любой подготовкой файла; NULL — занят/ошибка.
|
||||
* Владелец освобождает handle после завершения всех записей, включая ошибку/отмену. Только uasync. */
|
||||
struct media_destination* media_delivery_reserve(struct UTUN_INSTANCE* inst, const char* path);
|
||||
void media_delivery_release(struct UTUN_INSTANCE* inst, struct media_destination* destination);
|
||||
|
||||
struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md,
|
||||
const uint8_t* block_id, const uint8_t* media_id, uint32_t chunk);
|
||||
|
||||
void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id);
|
||||
/* Создать уникальный temp блока с одной producer-ссылкой; NULL — ошибка.
|
||||
* publish будит выдачи после записи/проверки: 0 <= available <= size, verified разрешён только для полного проверенного блока.
|
||||
* abort отсоединяет индекс и будит выдачи для отправки ошибки; недоступный получатель освобождается по таймауту.
|
||||
* release снимает ссылку; проверенный temp живёт до завершения последней выдачи. Только uasync. */
|
||||
struct media_block_source* md_block_create(struct media_delivery_ctx* md, uint64_t group_id,
|
||||
const uint8_t media_id[16], const uint8_t block_id[16], uint32_t chunk, uint32_t size, const char* destination);
|
||||
void md_block_publish(struct media_block_source* source, uint32_t available, int verified);
|
||||
void md_block_abort(struct media_block_source* source);
|
||||
void md_block_release(struct media_block_source* source);
|
||||
|
||||
// ── Внутренний учёт нагрузки на файл ──
|
||||
struct md_file_load* md_file_load_find(struct media_delivery_ctx* md, const uint8_t* media_id);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* media_delivery_proto — wire-структуры и подкоманды доставки блоков медиа CHAT-группы.
|
||||
* Описывает запросы держателей, чанки, подтверждения наличия и репликацию между суперузлами.
|
||||
* Это формат payload сервиса router, без группового маршрутизационного заголовка; обработчики — media_delivery/download. */
|
||||
* Это формат payload сервиса router, без группового маршрутизационного заголовка; обработчики — media_delivery/download.
|
||||
* Числа SYNC кодируются little endian; остальные packed-поля — native byte order текущего формата.
|
||||
* SERVE_REG/LEAVE меняют только вспомогательный served_nodes; текущий загрузчик их не посылает.
|
||||
* SERVE_ACK сейчас не отправляется, входящие SERVE_ACK/HAVE_BLOCK_ACK игнорируются; ожидания ACK и его retry нет. */
|
||||
#ifndef MEDIA_DELIVERY_PROTO_H
|
||||
#define MEDIA_DELIVERY_PROTO_H
|
||||
|
||||
@@ -13,20 +16,20 @@ extern "C" {
|
||||
/* ── подкоманды (первый байт данных после etcp_router) ── */
|
||||
|
||||
enum {
|
||||
MEDIA_SUBCMD_SERVE_REG = 0x01, // req-узел→суперузел: обслуживай меня
|
||||
MEDIA_SUBCMD_SERVE_ACK = 0x02, // суперузел→req-узел: подтверждение
|
||||
MEDIA_SUBCMD_SERVE_LEAVE = 0x03, // req-узел→суперузел: отключение
|
||||
MEDIA_SUBCMD_SERVE_REG = 0x01, // вспомогательный учёт узла в served_nodes
|
||||
MEDIA_SUBCMD_SERVE_ACK = 0x02, // объявлен, но не отправляется; приём игнорируется
|
||||
MEDIA_SUBCMD_SERVE_LEAVE = 0x03, // удалить вспомогательную запись served_nodes
|
||||
MEDIA_SUBCMD_QUERY = 0x04, // req-узел→суперузел: кто имеет блоки?
|
||||
MEDIA_SUBCMD_QUERY_RESP = 0x05, // суперузел→req-узел: список узлов
|
||||
MEDIA_SUBCMD_HAVE_BLOCK = 0x06, // узел→суперузел: у меня есть блок
|
||||
MEDIA_SUBCMD_HAVE_BLOCK_ACK = 0x07, // суперузел→узел: подтверждение приёма
|
||||
MEDIA_SUBCMD_SUPER_REPL = 0x08, // суперузел↔суперузел: репликация записей
|
||||
MEDIA_SUBCMD_SUPER_ACK = 0x09, // подтверждение репликации
|
||||
MEDIA_SUBCMD_SYNC_REQ = 0x08, // получатель→суперузел: страница после курсора
|
||||
MEDIA_SUBCMD_SYNC_RESP = 0x09, // суперузел→получатель: страница каталога
|
||||
MEDIA_SUBCMD_BLOCK_REQ = 0x0A, // req-узел→блок-холдер: дай блок
|
||||
MEDIA_SUBCMD_BLOCK_CHUNK = 0x0B, // блок-холдер→req-узел: чанк данных (1KB)
|
||||
MEDIA_SUBCMD_BLOCK_DONE = 0x0C, // блок-холдер→req-узел: блок передан полностью
|
||||
MEDIA_SUBCMD_CANCEL = 0x0D, // req-узел→блок-холдер: отмена передачи блока
|
||||
MEDIA_SUBCMD_SUPER_HELLO = 0x0E, // суперузел↔суперузел: рукопожатие при подключении
|
||||
MEDIA_SUBCMD_CHANGED = 0x0E, // суперузел→суперузел: каталог мог измениться (один байт)
|
||||
MEDIA_SUBCMD_BLOCK_OVERLOADED = 0x0F, // автор→req-узел: перегружен, попробуй позже
|
||||
MEDIA_SUBCMD_BLOCK_PROCESSING = 0x10, // узел→суперузел: начал качать блок (status=processing)
|
||||
MEDIA_SUBCMD_BLOCK_RELAY_FULL = 0x11, // relay-узел→req-узел: перегружен, вот список кому транслирую
|
||||
@@ -34,7 +37,34 @@ enum {
|
||||
|
||||
/* ── ответные статусы ── */
|
||||
|
||||
#define MEDIA_ERR_NOT_SUPERNODE 0xFE
|
||||
#define MEDIA_ERR_NOT_SUPERNODE 0xFE /* Объявлен, в текущих ответах не используется. */
|
||||
|
||||
/* Один QUERY — одна полная пачка результатов, включая блоки без держателей.
|
||||
* Группу и end-to-end отправителя задаёт router. Пределы общие для обеих сторон. */
|
||||
#define MEDIA_QUERY_MAX_BLOCKS 32
|
||||
#define MEDIA_QUERY_MAX_HOLDERS 4
|
||||
#define MEDIA_QUERY_OK 0
|
||||
#define MEDIA_QUERY_DB_ERROR 1
|
||||
|
||||
/* Каждая попытка BLOCK_REQ получает новый UUID; все ответы/CANCEL повторяют его.
|
||||
* Группа и отправитель берутся только из router. Байты идут непрерывно от offset=0.
|
||||
* DONE подтверждает конец передачи; подпись берётся из метаданных сообщения, а не из ответа держателя. */
|
||||
#define MEDIA_BLOCK_DATA_MAX 1024
|
||||
#define MEDIA_BLOCK_SIZE_MAX (25U * 1024U * 1024U)
|
||||
#define MEDIA_BLOCK_OK 0
|
||||
#define MEDIA_BLOCK_UNAVAILABLE 1
|
||||
|
||||
/* Репликация положительных объявлений, не снимок удалённой файловой системы.
|
||||
* Группа/источник — только router; обе стороны должны быть суперузлами этой CHAT-группы.
|
||||
* Все числа SYNC — little endian. ID ограничены INT64_MAX; epoch/UUID — непрозрачные 16 байт.
|
||||
* Новый запрос имеет новый UUID; timeout повторяет тот же запрос. Ответ принимается только
|
||||
* для текущего UUID. Эпоха сохраняется в БД источника и меняется при пересоздании каталога.
|
||||
* При несовпадении ожидаемой эпохи источник читает с нуля. OK применяется вместе с RX-курсором
|
||||
* одной транзакцией. CHANGED — подсказка; периодический SYNC восстанавливает потерянные подсказки. */
|
||||
#define MEDIA_SYNC_MAX_ENTRIES 32
|
||||
#define MEDIA_SYNC_OK 0
|
||||
#define MEDIA_SYNC_DB_ERROR 1
|
||||
#define MEDIA_SYNC_CURSOR_INVALID 2
|
||||
|
||||
/* ── packed-структуры пакетов ── */
|
||||
|
||||
@@ -58,28 +88,30 @@ struct media_pkt_serve_leave {
|
||||
|
||||
struct media_pkt_query {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_QUERY
|
||||
uint64_t group_id;
|
||||
uint8_t media_id[16];
|
||||
uint16_t num_blocks;
|
||||
uint8_t request_id[16]; // Новый случайный UUID для каждой попытки; не средство аутентификации.
|
||||
uint16_t num_blocks; // 1..MEDIA_QUERY_MAX_BLOCKS
|
||||
// далее: block_ids[num_blocks * 16]
|
||||
};
|
||||
|
||||
#define MEDIA_QUERY_HDR_SIZE (sizeof(struct media_pkt_query)) // 1+8+16+2=27
|
||||
#define MEDIA_QUERY_HDR_SIZE (sizeof(struct media_pkt_query)) // 35
|
||||
|
||||
struct media_pkt_query_resp_entry {
|
||||
uint64_t node_id;
|
||||
uint16_t rtt; // cumulative_rtt из topo_node суперузла (0.1ms)
|
||||
uint8_t block_id[16];
|
||||
uint32_t chunk;
|
||||
uint8_t num_holders; // 0..MEDIA_QUERY_MAX_HOLDERS; ноль — держатели не найдены.
|
||||
uint64_t holders[MEDIA_QUERY_MAX_HOLDERS]; // Уникальные ненулевые узлы; неиспользуемые слоты нулевые.
|
||||
};
|
||||
|
||||
struct media_pkt_query_resp {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_QUERY_RESP
|
||||
uint16_t num_entries;
|
||||
// далее: entries[num_entries * sizeof(media_pkt_query_resp_entry)]
|
||||
uint8_t media_id[16];
|
||||
uint8_t request_id[16]; // Точное эхо QUERY, включая ответ с ошибкой.
|
||||
uint8_t status; // MEDIA_QUERY_OK / MEDIA_QUERY_DB_ERROR
|
||||
uint16_t num_blocks; // OK: столько же, сколько в QUERY; ошибка: 0.
|
||||
// Далее результаты всех блоков в порядке запроса; частичных успешных ответов нет.
|
||||
};
|
||||
|
||||
#define MEDIA_QUERY_RESP_HDR_SIZE (sizeof(struct media_pkt_query_resp)) // 1+2=3
|
||||
#define MEDIA_QUERY_RESP_HDR_SIZE (sizeof(struct media_pkt_query_resp)) // 36; максимум с записями — 1604 байта.
|
||||
|
||||
struct media_pkt_have_block {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_HAVE_BLOCK
|
||||
@@ -97,30 +129,46 @@ struct media_pkt_have_block_ack {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_HAVE_BLOCK_ACK
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t status; // 0=OK, 1=dup/already
|
||||
uint8_t status; // 0=сохранено изменение, 1=повтор/старое объявление, 2=ошибка БД.
|
||||
};
|
||||
|
||||
struct media_pkt_super_repl {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_SUPER_REPL
|
||||
uint32_t seq; // макс id в пачке
|
||||
uint16_t num_entries;
|
||||
// далее: записи raw block_availability
|
||||
struct media_pkt_sync_req {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_SYNC_REQ
|
||||
uint8_t request_id[16]; // UUID текущего запроса страницы.
|
||||
uint8_t source_epoch[16]; // Ожидаемая эпоха источника; нули для первого запроса.
|
||||
uint64_t after_id; // Уже атомарно сохранённый RX-курсор.
|
||||
};
|
||||
|
||||
#define MEDIA_SUPER_REPL_HDR_SIZE (sizeof(struct media_pkt_super_repl)) // 1+4+2=7
|
||||
|
||||
struct media_pkt_super_ack {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_SUPER_ACK
|
||||
uint32_t ack_seq; // подтверждённый max id
|
||||
struct media_pkt_sync_entry {
|
||||
uint64_t id; // ID строки у источника, строго возрастают в странице.
|
||||
uint8_t block_id[16];
|
||||
uint64_t node_id; // Ненулевой держатель; уникален с block_id внутри группы.
|
||||
uint32_t chunk; // Неизменяемый номер блока.
|
||||
int64_t timestamp; // Unix-время объявления, неотрицательное.
|
||||
uint8_t status; // 0=PROCESSING, 1=COMPLETE; COMPLETE имеет приоритет.
|
||||
};
|
||||
|
||||
struct media_pkt_sync_resp {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_SYNC_RESP
|
||||
uint8_t request_id[16]; // Точное эхо UUID запроса.
|
||||
uint8_t status; // MEDIA_SYNC_*; ошибка не продвигает курсор, записей нет.
|
||||
uint8_t source_epoch[16];
|
||||
uint64_t from_id; // after_id запроса при совпадении эпох, иначе 0.
|
||||
uint64_t next_id; // При more — последний ID страницы; иначе верхняя граница снимка.
|
||||
uint8_t more; // 0/1; 1 требует полной страницы из 32 записей.
|
||||
uint16_t num_entries; // 0..32; далее media_pkt_sync_entry[num_entries].
|
||||
};
|
||||
|
||||
#define MEDIA_SYNC_REQ_SIZE sizeof(struct media_pkt_sync_req) // 41
|
||||
#define MEDIA_SYNC_RESP_HDR_SIZE sizeof(struct media_pkt_sync_resp) // 53
|
||||
#define MEDIA_SYNC_PACKET_MAX (MEDIA_SYNC_RESP_HDR_SIZE + MEDIA_SYNC_MAX_ENTRIES * sizeof(struct media_pkt_sync_entry)) // 1493
|
||||
|
||||
struct media_pkt_block_req {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_BLOCK_REQ
|
||||
uint64_t group_id; // CHAT-группа для маршрутизации и прямого коннекта
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16]; // UUID попытки; одинаков только при дублировании запроса.
|
||||
uint32_t chunk;
|
||||
uint64_t offset; // смещение в блоке (обычно 0)
|
||||
};
|
||||
|
||||
#define MEDIA_BLOCK_REQ_SIZE (sizeof(struct media_pkt_block_req))
|
||||
@@ -129,21 +177,23 @@ struct media_pkt_block_chunk {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_BLOCK_CHUNK
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16];
|
||||
uint32_t chunk;
|
||||
uint32_t offset; // смещение в блоке
|
||||
uint16_t data_len; // 1..1024
|
||||
// далее: data[data_len]
|
||||
};
|
||||
|
||||
#define MEDIA_BLOCK_CHUNK_HDR_SIZE (sizeof(struct media_pkt_block_chunk)) // 1+16+16+4+4+2=43
|
||||
#define MEDIA_BLOCK_CHUNK_HDR_SIZE (sizeof(struct media_pkt_block_chunk)) // 59
|
||||
|
||||
struct media_pkt_block_done {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_BLOCK_DONE
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16];
|
||||
uint32_t chunk;
|
||||
uint32_t total_size; // полный размер блока
|
||||
uint8_t block_sig[64]; // Ed25519 подпись блока
|
||||
uint8_t status; // MEDIA_BLOCK_OK / MEDIA_BLOCK_UNAVAILABLE; ошибка не подтверждает байты.
|
||||
};
|
||||
|
||||
#define MEDIA_BLOCK_DONE_SIZE (sizeof(struct media_pkt_block_done))
|
||||
@@ -152,19 +202,15 @@ struct media_pkt_cancel {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_CANCEL
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16];
|
||||
uint32_t chunk;
|
||||
};
|
||||
|
||||
struct media_pkt_super_hello {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_SUPER_HELLO
|
||||
uint64_t last_recv_id; // ID в МОЕЙ базе до которого пир подтвердил приём
|
||||
uint64_t group_id; // CHAT-группа, в которой установлена связь
|
||||
};
|
||||
|
||||
struct media_pkt_block_overloaded {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_BLOCK_OVERLOADED
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16];
|
||||
uint32_t chunk;
|
||||
uint32_t retry_after_ms; // рекомендованная задержка перед повтором (ms), 0 = default 2000
|
||||
};
|
||||
@@ -173,12 +219,13 @@ struct media_pkt_block_relay_full {
|
||||
uint8_t subcmd; // MEDIA_SUBCMD_BLOCK_RELAY_FULL
|
||||
uint8_t media_id[16];
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16];
|
||||
uint32_t chunk;
|
||||
uint16_t num_relay_nodes; // 1..5
|
||||
// далее: relay_node_ids[num_relay_nodes * 8]
|
||||
};
|
||||
|
||||
#define MEDIA_RELAY_FULL_HDR_SIZE (sizeof(struct media_pkt_block_relay_full)) // 1+16+16+4+2=39
|
||||
#define MEDIA_RELAY_FULL_HDR_SIZE (sizeof(struct media_pkt_block_relay_full)) // 55
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
|
||||
+578
-653
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,14 @@
|
||||
/* media_download — загрузка канального файла по media_index_result: QUERY держателей → блоки → проверка/сборка.
|
||||
* Состояние хранит inst->md. При открытии подключения через conn_mgr загрузчик получает CM handle.
|
||||
* Найденный ETCP_CONN используется без нового handle; при отсутствии conn_mgr запрос отправляется best-effort без handle.
|
||||
* Старт копирует метаданные result; callbacks и обработчики — uasync-поток, файловая проверка — media_async.
|
||||
* Failover/таймер простоя ограничивают повторы; done сообщает результат, а не гарантирует доставку другим участникам. */
|
||||
* Старт копирует метаданные result; callbacks, обработчики и файловые операции выполняются в uasync-потоке.
|
||||
* QUERY: одна пачка до 32 блоков в полёте, отдельный UUID/дедлайн, источники только своей группы и автор.
|
||||
* До четырёх кандидатов на блок; до десяти используемых узлов, слоты переиспользуются после освобождения.
|
||||
* Повторы ограничены источниками и бюджетом провалов блока; OVERLOADED соблюдает retry_after.
|
||||
* Попытка имеет UUID и отдельный temp; принимается только непрерывный префикс от текущего узла своей группы.
|
||||
* Прогресс обновляется после успешного close; DONE проверяет размер и подпись по сохранённому ключу автора.
|
||||
* Проверенный temp независимо удерживают relay-выдачи. Итоговый файл публикуется после полного SHA-256.
|
||||
* done сообщает результат, а не гарантирует доставку другим участникам. */
|
||||
#ifndef MEDIA_DOWNLOAD_H
|
||||
#define MEDIA_DOWNLOAD_H
|
||||
|
||||
@@ -14,23 +20,28 @@ extern "C" {
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include "../lib/ll_queue.h"
|
||||
#include "media_delivery_proto.h"
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
struct media_index_result;
|
||||
struct CONN_MGR_HANDLE;
|
||||
struct media_block_source;
|
||||
struct media_destination;
|
||||
|
||||
/* максимальное число узлов-держателей одного блока (кандидатов на фейловер) */
|
||||
#define MD_MAX_HOLDERS_PER_BLOCK 4
|
||||
/* максимальное число уникальных узлов-держателей в загрузке */
|
||||
#define MD_MAX_HOLDERS_PER_BLOCK MEDIA_QUERY_MAX_HOLDERS
|
||||
/* Максимальное число одновременно используемых держателей; свободные слоты переиспользуются. */
|
||||
#define MD_MAX_PEERS 10
|
||||
|
||||
enum md_wait_reason { MD_WAIT_NONE, MD_WAIT_SEARCH, MD_WAIT_SLOT, MD_WAIT_RETRY };
|
||||
|
||||
/* ── состояние блока в конечном автомате загрузки ── */
|
||||
enum md_block_state {
|
||||
MD_BLK_IDLE = 0, /* ещё не запрошен */
|
||||
MD_BLK_CONN, /* conn_mgr_open в процессе, BLOCK_REQ ещё не отправлен */
|
||||
MD_BLK_REQ, /* BLOCK_REQ отправлен, ждём первый чанк */
|
||||
MD_BLK_RECV, /* чанки идут */
|
||||
MD_BLK_WAIT, /* перегружен/нет свободного держателя — ретрай позже */
|
||||
MD_BLK_WAIT, /* Причина в wait_reason: поиск, занятые слоты или retry_at_tb. */
|
||||
MD_BLK_DONE, /* BLOCK_DONE + сигнатура валидна */
|
||||
MD_BLK_FAILED /* попытки исчерпаны */
|
||||
};
|
||||
@@ -38,14 +49,20 @@ enum md_block_state {
|
||||
/* ── состояние одного блока ── */
|
||||
struct media_download_block {
|
||||
uint8_t block_id[16];
|
||||
uint8_t transfer_id[16]; /* Текущая попытка, меняется до отправки BLOCK_REQ. */
|
||||
struct media_block_source* source; // Собственная producer-ссылка на файл этой попытки.
|
||||
uint64_t holders[MD_MAX_HOLDERS_PER_BLOCK]; /* узлы, у кого есть блок */
|
||||
int num_holders;
|
||||
int holder_idx; /* текущий держатель (-1 = нет) */
|
||||
int attempts; /* 0..max (фейловеры/провалы блока) */
|
||||
uint32_t bytes_received; /* max(offset+len) полученных байт */
|
||||
uint32_t bytes_received; /* Непрерывный успешно записанный и закрытый префикс. */
|
||||
uint32_t expected_size; /* реальный размер блока (последний короче) */
|
||||
uint8_t state; /* enum md_block_state */
|
||||
uint64_t last_progress_tb; /* get_time_tb() последнего чанка/прогресса */
|
||||
uint64_t retry_at_tb; /* Не запрашивать блок до этого срока после OVERLOADED. */
|
||||
int query_next; /* Следующий источник поиска; QUERY не сбрасывает attempts. */
|
||||
uint8_t author_tried; /* Прямой fallback автора уже запускался. */
|
||||
uint8_t wait_reason; /* enum md_wait_reason; нехватка слота не расходует attempts. */
|
||||
};
|
||||
|
||||
/* ── соединение с узлом-держателем ── */
|
||||
@@ -53,11 +70,15 @@ struct media_download_peer {
|
||||
uint64_t node_id;
|
||||
uint8_t connected;
|
||||
struct CONN_MGR_HANDLE* cm_handle; /* Один запрос на держателя; закрывается после DOWN/TIMEOUT или завершения загрузки. */
|
||||
uint8_t pending_event; /* CM event + 1; обработка отложена, чтобы open не вызывал рекурсивный pump. */
|
||||
};
|
||||
|
||||
/* ── внутреннее состояние загрузки (доступно для тестов) ── */
|
||||
struct media_download {
|
||||
struct ll_entry ll;
|
||||
struct ll_entry* entry; /* Владеющая запись; после удаления из индекса живёт до последнего refs. */
|
||||
unsigned refs; /* Активная загрузка и обработчики, вызывающие пользовательские callbacks. */
|
||||
struct media_destination* destination; // Исключительное право записи dest_path до terminal.
|
||||
uint8_t author_pubkey[32]; /* Точный ключ автора из БД, скопирован до начала загрузки. */
|
||||
uint8_t media_id[16];
|
||||
uint64_t group_id;
|
||||
char dest_path[1024];
|
||||
@@ -75,10 +96,15 @@ struct media_download {
|
||||
int err;
|
||||
uint64_t super_nodes[10];
|
||||
int super_count;
|
||||
int super_current;
|
||||
uint64_t announce_node; /* Адресат HAVE_BLOCK, независимый от текущего QUERY. */
|
||||
uint8_t query_id[16]; /* UUID ожидающей попытки; поздние ответы игнорируются. */
|
||||
uint64_t query_node;
|
||||
uint64_t query_deadline_tb; /* 0 — нет ожидающего QUERY; чанки не меняют дедлайн. */
|
||||
int query_count;
|
||||
int query_blocks[MEDIA_QUERY_MAX_BLOCKS]; /* Индексы пачки в blocks, в порядке QUERY. */
|
||||
void* pump_timer; /* Отложенная обработка CM; отменяется до освобождения dl. */
|
||||
uint64_t author_node_id; /* fallback, когда у суперноды нет инфо */
|
||||
void* watchdog_timer; /* таймер простоя (2с) */
|
||||
uint64_t last_activity_tb; /* любая входящая активность (chunk/done/query_resp) */
|
||||
void* watchdog_timer; /* Ближайший QUERY/простой блока/retry_at; отменяется до done_cb. */
|
||||
int num_peers;
|
||||
struct media_download_peer peers[MD_MAX_PEERS];
|
||||
void (*done_cb)(void* arg, int err);
|
||||
@@ -88,6 +114,16 @@ struct media_download {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
};
|
||||
|
||||
/* Запустить непустой файл с корректной геометрией и массивами block_ids/block_sigs; блок до MEDIA_BLOCK_SIZE_MAX.
|
||||
* author_node_id должен иметь ровно 32 байта Ed25519-ключа в nodes; значение первого байта не используется как признак наличия.
|
||||
* dest_path <1024, media_base <512 байт (UTF-8); назначение принадлежит общей базе БД, смена базы отклоняется.
|
||||
* Проверки идут до создания каталогов. 0 — принят: done_cb ровно один раз, в том числе синхронно при отсутствии суперузлов;
|
||||
* -1 — отказ без done_cb, включая уже активный media_id. Аргументы callbacks заимствованы до done_cb.
|
||||
* Успех включает атомарную регистрацию блоков опубликованного файла в SQLite; общей транзакции ФС/БД нет.
|
||||
* Ошибка регистрации не удаляет опубликованный файл;
|
||||
* путь остаётся в media_preparations для удаления канала. Остальные поля result копируются до возврата.
|
||||
* progress_cb может отменить загрузку и запустить новую с тем же media_id; done_cb старой загрузки
|
||||
* вызывается после возврата из progress_cb, когда её последний обработчик освободил контекст. */
|
||||
int media_download_start(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
const struct media_index_result* result,
|
||||
const char* dest_path, const char* media_base,
|
||||
@@ -95,21 +131,28 @@ int media_download_start(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
void (*done_cb)(void* arg, int err), void* done_arg,
|
||||
void (*progress_cb)(void* arg, int blocks_done, int num_blocks), void* progress_arg);
|
||||
|
||||
/* Отменить всю загрузку media_id: block_id/chunk не используются. Отдельный CANCEL отправляется для каждой текущей попытки.
|
||||
* 0 — отменена, -1 — не найдена. done_cb(-2) после последнего удерживающего обработчика; повторной отмены/done нет. */
|
||||
int media_download_cancel(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* media_id, const uint8_t* block_id, uint32_t chunk);
|
||||
|
||||
/* ── handlers for incoming data, called from md_etcp_recv dispatch ── */
|
||||
/* Полный ответ пачки. group_id/from_node — из router; несовпадающие/истёкшие попытки игнорируются.
|
||||
* Повреждённый пакет не меняет состояние; ошибка поиска продолжает обход источников.
|
||||
* Может завершить загрузку и вызвать done_cb; после возврата прежний dl может быть освобождён. */
|
||||
void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* data, size_t len);
|
||||
uint64_t group_id, uint64_t from_node, const uint8_t* data, size_t len);
|
||||
/* group_id/from_node берутся из router. Длина/состояние/блок/chunk/UUID проверяются до записи и прогресса.
|
||||
* Повреждение текущей попытки меняет держателя, локальная ошибка завершает загрузку.
|
||||
* Чужие и устаревшие пакеты игнорируются; обработчики могут завершить загрузку синхронно. */
|
||||
void media_download_handle_chunk(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* data, size_t len);
|
||||
uint64_t group_id, uint64_t from_node, const uint8_t* data, size_t len);
|
||||
void media_download_handle_done(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* data, size_t len);
|
||||
uint64_t group_id, uint64_t from_node, const uint8_t* data, size_t len);
|
||||
void media_download_handle_overloaded(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* data, size_t len);
|
||||
uint64_t group_id, uint64_t from_node, const uint8_t* data, size_t len);
|
||||
void media_download_handle_relay_full(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* media_id, const uint8_t* block_id,
|
||||
uint32_t chunk, const uint64_t* node_ids, int num_nodes);
|
||||
uint64_t group_id, uint64_t from_node, const uint8_t* data, size_t len);
|
||||
|
||||
/* ── conn_mgr callback (non-static for test visibility) ── */
|
||||
struct CONN_MGR_HANDLE;
|
||||
@@ -118,9 +161,9 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
|
||||
enum conn_mgr_event event, void* arg);
|
||||
|
||||
/* ── тестовые хуки (для детерминированных юнит-тестов с «фейковым» временем) ──
|
||||
* md_dl_check_stall — проверка простоя: возвращает 0 (активна) / -1 (провал, нужно finish).
|
||||
* md_dl_check_stall — обработать QUERY/простой/ожидание, запустить готовые блоки; 0 / -1 (нужен finish).
|
||||
* md_dl_failover_block — переход блока к следующему держателю: 0, включая DONE/FAILED и ожидание новых держателей;
|
||||
* -1 — исчерпаны попытки или не удалось начать запрос (лимит peers, ошибка conn_mgr_open).
|
||||
* -1 — исчерпаны попытки или локальная ошибка; занятые peer-слоты возвращают 0.
|
||||
* md_dl_dump — дамп состояния загрузки в лог (DEBUG_CATEGORY_MEDIA). */
|
||||
int md_dl_check_stall(struct media_download* dl, uint64_t now_tb);
|
||||
int md_dl_failover_block(struct media_download* dl, int bi, uint64_t now_tb);
|
||||
|
||||
+314
-113
@@ -19,6 +19,140 @@
|
||||
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_TARGET 30
|
||||
|
||||
/* Нормализация только синтаксиса: ссылки каталогов здесь не разрешаются. */
|
||||
static int mi_clean_path(const char* input, char* output, size_t capacity) {
|
||||
if (!input || !*input || strlen(input) >= capacity) goto invalid;
|
||||
size_t used = 0, pos = 0;
|
||||
char source[MEDIA_PATH_MAX];
|
||||
if (capacity > sizeof(source)) goto invalid;
|
||||
memcpy(source, input, strlen(input) + 1);
|
||||
#ifdef _WIN32
|
||||
for (size_t i = 0; source[i]; i++) if (source[i] == '\\') source[i] = '/';
|
||||
if (source[0] && source[1] == ':') {
|
||||
if (source[2] != '/' || !((source[0] >= 'A' && source[0] <= 'Z') || (source[0] >= 'a' && source[0] <= 'z')))
|
||||
goto invalid;
|
||||
output[used++] = source[0]; output[used++] = ':'; output[used++] = '/'; pos = 3;
|
||||
} else if (source[0] == '/' && source[1] == '/') {
|
||||
output[used++] = '/'; output[used++] = '/'; pos = 2;
|
||||
} else
|
||||
#endif
|
||||
if (source[0] == '/') { output[used++] = '/'; pos = 1; }
|
||||
while (source[pos]) {
|
||||
if (source[pos] == '/') { pos++; continue; }
|
||||
size_t start = pos;
|
||||
while (source[pos] && source[pos] != '/') pos++;
|
||||
size_t length = pos - start;
|
||||
if (length == 1 && source[start] == '.') continue;
|
||||
if (length == 2 && source[start] == '.' && source[start + 1] == '.') goto invalid;
|
||||
for (size_t i = start; i < pos; i++) if (source[i] == ':') goto invalid;
|
||||
if (used && output[used - 1] != '/') output[used++] = '/';
|
||||
if (used + length >= capacity) goto invalid;
|
||||
memcpy(output + used, source + start, length); used += length;
|
||||
}
|
||||
if (!used) output[used++] = '.';
|
||||
output[used] = 0;
|
||||
return 0;
|
||||
invalid:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid/oversized path=%s limit=%zu", input ? input : "(null)", capacity);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* location намеренно строго канонический: одинаковое имя должно иметь одно представление в БД. */
|
||||
static int mi_location_valid(const char* location) {
|
||||
char clean[MEDIA_PATH_MAX];
|
||||
if (location && *location && location[0] != '/' && !strchr(location, '\\') && !strchr(location, ':')
|
||||
&& !mi_clean_path(location, clean, sizeof(clean)) && strcmp(clean, ".") && !strcmp(location, clean)) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid location=%s", location ? location : "(null)"); return -1;
|
||||
}
|
||||
|
||||
int media_index_path(const char* base, const char* location, char path[MEDIA_PATH_MAX]) {
|
||||
char clean[MEDIA_BASE_MAX], full[MEDIA_PATH_MAX];
|
||||
if (!path) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index path without output"); return -1; }
|
||||
if (mi_clean_path(base, clean, sizeof(clean)) || mi_location_valid(location)) return -1;
|
||||
int size = !strcmp(clean, ".") ? snprintf(full, sizeof(full), "%s", location)
|
||||
: snprintf(full, sizeof(full), "%s%s%s", clean, clean[strlen(clean) - 1] == '/' ? "" : "/", location);
|
||||
if (size >= MEDIA_PATH_MAX || size < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index joined path too long base=%s location=%s", clean, location); return -1;
|
||||
}
|
||||
memcpy(path, full, (size_t)size + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int media_index_location(const char* path, const char* base, char location[MEDIA_PATH_MAX]) {
|
||||
char full[MEDIA_PATH_MAX], root[MEDIA_BASE_MAX];
|
||||
if (!location) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index location without output"); return -1; }
|
||||
if (mi_clean_path(path, full, sizeof(full)) || mi_clean_path(base, root, sizeof(root))) return -1;
|
||||
size_t length = strlen(root);
|
||||
const char* relative = NULL;
|
||||
if (!strcmp(root, ".") && full[0] != '/' && !strchr(full, ':')) relative = full;
|
||||
else if (!strncmp(full, root, length)) {
|
||||
if (root[length - 1] == '/') relative = full + length;
|
||||
else if (full[length] == '/') relative = full + length + 1;
|
||||
}
|
||||
if (!relative || mi_location_valid(relative)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index path outside base path=%s base=%s", full, root); return -1;
|
||||
}
|
||||
memcpy(location, relative, strlen(relative) + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Единая диагностика SQL, включая ошибки управления savepoint. */
|
||||
static int mi_exec(sqlite3* db, const char* sql) {
|
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
||||
if (rc == SQLITE_OK) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index SQL failed rc=%d sql=%s: %s", rc, sql, sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
|
||||
int media_index_get_base(sqlite3* db, char base[MEDIA_BASE_MAX]) {
|
||||
sqlite3_stmt* st = NULL;
|
||||
int rc = db && base ? sqlite3_prepare_v2(db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &st, NULL) : SQLITE_MISUSE;
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
const char* value = rc == SQLITE_ROW ? (const char*)sqlite3_column_text(st, 0) : NULL;
|
||||
int valid = value && sqlite3_column_bytes(st, 0) == (int)strlen(value) && !mi_clean_path(value, base, MEDIA_BASE_MAX);
|
||||
if (st && sqlite3_finalize(st) != SQLITE_OK) valid = 0;
|
||||
if (valid) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index cannot read media_base rc=%d: %s", rc, db ? sqlite3_errmsg(db) : "no DB");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int media_index_set_base(sqlite3* db, const char* base) {
|
||||
char clean[MEDIA_BASE_MAX]; sqlite3_stmt* st = NULL;
|
||||
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index set base without DB"); return -1; }
|
||||
if (mi_clean_path(base, clean, sizeof(clean))) return -1;
|
||||
if (mi_exec(db, "CREATE TABLE IF NOT EXISTS ui_state(key TEXT PRIMARY KEY,value TEXT)")) return -1;
|
||||
int rc = sqlite3_prepare_v2(db, "INSERT INTO ui_state(key,value) VALUES('media_base',?) ON CONFLICT(key) DO NOTHING", -1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, clean, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
int finish = st ? sqlite3_finalize(st) : SQLITE_OK;
|
||||
if (rc != SQLITE_DONE || finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index set base SQL failure requested=%s rc=%d: %s", clean, rc, sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
char stored[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(db, stored)) return -1;
|
||||
if (strcmp(stored, clean)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index base change rejected requested=%s stored=%s", clean, stored); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int media_index_track_preparation(sqlite3* db, const char* chat_id, const char* path, const char* base) {
|
||||
char location[MEDIA_PATH_MAX]; sqlite3_stmt* st = NULL;
|
||||
if (!db || !chat_id || !*chat_id || strlen(chat_id) >= 64 || media_index_location(path, base, location)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid preparation path/channel"); return -1;
|
||||
}
|
||||
int rc = sqlite3_prepare_v2(db, "INSERT INTO media_preparations(chat_id,location) VALUES(?,?) ON CONFLICT DO NOTHING",
|
||||
-1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, location, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
int finish = st ? sqlite3_finalize(st) : SQLITE_OK;
|
||||
if (rc == SQLITE_DONE && finish == SQLITE_OK) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media_index preparation tracked chat=%s path=%s", chat_id, location); return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index track preparation chat=%s rc=%d: %s", chat_id, rc, sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
|
||||
static int mi_insert(sqlite3* db,
|
||||
const uint8_t* media_id, const uint8_t* block_id,
|
||||
const uint8_t* content_hash,
|
||||
@@ -27,40 +161,48 @@ static int mi_insert(sqlite3* db,
|
||||
int64_t timestamp, int64_t file_size,
|
||||
int64_t chunk_size, int chunk, int64_t offset) {
|
||||
const char* sql =
|
||||
"INSERT OR IGNORE INTO media_files(media_id,block_id,content_hash,chat_id,location,"
|
||||
"INSERT INTO media_files(media_id,block_id,content_hash,chat_id,location,"
|
||||
"node_id,node_sign,timestamp,file_size,chunk_size,chunk,offset)"
|
||||
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||
" VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12) ON CONFLICT(block_id) DO UPDATE SET block_id=excluded.block_id"
|
||||
" WHERE media_files.media_id=excluded.media_id AND media_files.content_hash=excluded.content_hash"
|
||||
" AND media_files.chat_id=excluded.chat_id AND media_files.location=excluded.location"
|
||||
" AND media_files.node_id=excluded.node_id AND media_files.file_size=excluded.file_size"
|
||||
" AND media_files.chunk_size=excluded.chunk_size AND media_files.chunk=excluded.chunk AND media_files.offset=excluded.offset";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: insert prep fail: %s", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
sqlite3_bind_blob(stmt, 1, media_id, 16, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(stmt, 2, block_id, 16, SQLITE_STATIC);
|
||||
sqlite3_bind_blob(stmt, 3, content_hash, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 4, chat_id, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 5, location, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)node_id);
|
||||
sqlite3_bind_blob(stmt, 7, node_sign, 64, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 8, timestamp);
|
||||
sqlite3_bind_int64(stmt, 9, file_size);
|
||||
sqlite3_bind_int64(stmt, 10, chunk_size);
|
||||
sqlite3_bind_int(stmt, 11, chunk);
|
||||
sqlite3_bind_int64(stmt, 12, offset);
|
||||
int rc = SQLITE_OK;
|
||||
#define MI_BIND(expression) do { if (rc == SQLITE_OK) rc = (expression); } while (0)
|
||||
MI_BIND(sqlite3_bind_blob(stmt, 1, media_id, 16, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_blob(stmt, 2, block_id, 16, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_blob(stmt, 3, content_hash, 32, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_text(stmt, 4, chat_id, -1, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_text(stmt, 5, location, -1, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_int64(stmt, 6, (sqlite3_int64)node_id));
|
||||
MI_BIND(sqlite3_bind_blob(stmt, 7, node_sign, 64, SQLITE_STATIC));
|
||||
MI_BIND(sqlite3_bind_int64(stmt, 8, timestamp));
|
||||
MI_BIND(sqlite3_bind_int64(stmt, 9, file_size));
|
||||
MI_BIND(sqlite3_bind_int64(stmt, 10, chunk_size));
|
||||
MI_BIND(sqlite3_bind_int(stmt, 11, chunk));
|
||||
MI_BIND(sqlite3_bind_int64(stmt, 12, offset));
|
||||
#undef MI_BIND
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
|
||||
int changed = sqlite3_changes(db), finish = sqlite3_finalize(stmt);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: insert fail rc=%d: %s", rc, sqlite3_errmsg(db));
|
||||
if (rc != SQLITE_DONE || finish != SQLITE_OK || changed != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index %s chat=%s block=%02x%02x%02x%02x chunk=%d path=%s rc=%d: %s",
|
||||
rc == SQLITE_DONE && finish == SQLITE_OK && !changed ? "conflicting block metadata" : "insert failed",
|
||||
chat_id, block_id[0], block_id[1], block_id[2], block_id[3], chunk, location, rc, sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int media_index_init(sqlite3* db) {
|
||||
if (!db) return -1;
|
||||
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index init without DB"); return -1; }
|
||||
|
||||
const char* sql =
|
||||
"CREATE TABLE IF NOT EXISTS media_files ("
|
||||
@@ -86,21 +228,13 @@ int media_index_init(sqlite3* db) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_exec(db,
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_media_files_block_id ON media_files(block_id);",
|
||||
NULL, NULL, NULL);
|
||||
sqlite3_exec(db,
|
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_media_id ON media_files(media_id);",
|
||||
NULL, NULL, NULL);
|
||||
sqlite3_exec(db,
|
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_chat_id ON media_files(chat_id);",
|
||||
NULL, NULL, NULL);
|
||||
sqlite3_exec(db,
|
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_node_id ON media_files(node_id);",
|
||||
NULL, NULL, NULL);
|
||||
sqlite3_exec(db,
|
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_content_hash ON media_files(content_hash);",
|
||||
NULL, NULL, NULL);
|
||||
if (mi_exec(db, "CREATE TABLE IF NOT EXISTS media_preparations(chat_id TEXT NOT NULL,location TEXT NOT NULL,"
|
||||
"PRIMARY KEY(chat_id,location))")
|
||||
|| mi_exec(db, "CREATE UNIQUE INDEX IF NOT EXISTS idx_media_files_block_id ON media_files(block_id)")
|
||||
|| mi_exec(db, "CREATE INDEX IF NOT EXISTS idx_media_files_media_id ON media_files(media_id)")
|
||||
|| mi_exec(db, "CREATE INDEX IF NOT EXISTS idx_media_files_chat_id ON media_files(chat_id)")
|
||||
|| mi_exec(db, "CREATE INDEX IF NOT EXISTS idx_media_files_node_id ON media_files(node_id)")
|
||||
|| mi_exec(db, "CREATE INDEX IF NOT EXISTS idx_media_files_content_hash ON media_files(content_hash)")) return -1;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index: table+indices created");
|
||||
return 0;
|
||||
@@ -114,43 +248,85 @@ int media_calc_block_size(uint64_t file_size) {
|
||||
return (int)target;
|
||||
}
|
||||
|
||||
/* Удаление regular/link без перехода через конечную ссылку; каталоги не удаляются. */
|
||||
static int mi_remove(const char* path) {
|
||||
struct file_io_info info;
|
||||
int found = file_io_inspect(path, 0, &info);
|
||||
if (found == 1) return 0;
|
||||
if (found || (info.type != FILE_IO_REGULAR && info.type != FILE_IO_LINK)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index cannot remove non-file path=%s", path); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
int rc = _wremove(wide);
|
||||
#else
|
||||
int rc = remove(path);
|
||||
#endif
|
||||
if (!rc || errno == ENOENT) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index remove failed path=%s errno=%d: %s", path, errno, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base) {
|
||||
if (!db || !chat_id || !chat_id[0] || !media_base || !media_base[0]) return -1;
|
||||
|
||||
char base[512];
|
||||
size_t base_len = strlen(media_base);
|
||||
while (base_len > 0 && media_base[base_len - 1] == '/') base_len--;
|
||||
snprintf(base, sizeof(base), "%.*s", (int)base_len, media_base);
|
||||
|
||||
int files = 0, removed = 0;
|
||||
char checked_base[MEDIA_BASE_MAX];
|
||||
if (!db || !chat_id || !*chat_id || !sqlite3_get_autocommit(db) || mi_clean_path(media_base, checked_base, sizeof(checked_base))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index delete invalid args/external transaction"); return -1;
|
||||
}
|
||||
struct mi_delete_file { char location[MEDIA_PATH_MAX]; char path[MEDIA_PATH_MAX]; int shared; };
|
||||
struct mi_delete_file* files = NULL; uint32_t count = 0;
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(db,
|
||||
"SELECT DISTINCT location FROM media_files WHERE chat_id=? AND location!=''",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC);
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char* loc = (const char*)sqlite3_column_text(st, 0);
|
||||
if (!loc || !loc[0]) continue;
|
||||
files++;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof(full), "%s/%s", base, loc);
|
||||
if (remove(full) == 0) removed++;
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media_index: delete_channel remove failed: %s", full);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
const char* query = "WITH paths AS (SELECT chat_id,location FROM media_files UNION SELECT chat_id,location FROM media_preparations)"
|
||||
" SELECT location,EXISTS(SELECT 1 FROM paths other WHERE other.location=mine.location AND other.chat_id!=mine.chat_id)"
|
||||
" FROM paths mine WHERE chat_id=?";
|
||||
int rc = sqlite3_prepare_v2(db, query, -1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto failed;
|
||||
/* Полный снимок и проверка всех путей до первого необратимого удаления. */
|
||||
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
||||
const char* location = (const char*)sqlite3_column_text(st, 0);
|
||||
char path[MEDIA_PATH_MAX];
|
||||
if (!location || sqlite3_column_bytes(st, 0) != (int)strlen(location)
|
||||
|| media_index_path(media_base, location, path) || count >= UINT32_MAX / sizeof(*files) - 1) goto failed;
|
||||
struct mi_delete_file* grown = u_realloc(files, (count + 1) * sizeof(*files));
|
||||
if (!grown) goto failed;
|
||||
files = grown;
|
||||
snprintf(files[count].location, sizeof(files[count].location), "%s", location);
|
||||
memcpy(files[count].path, path, strlen(path) + 1);
|
||||
files[count].shared = sqlite3_column_int(st, 1); count++;
|
||||
}
|
||||
|
||||
int rows = 0;
|
||||
sqlite3_stmt* del = NULL;
|
||||
if (sqlite3_prepare_v2(db, "DELETE FROM media_files WHERE chat_id=?", -1, &del, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(del, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(del) == SQLITE_DONE) rows = sqlite3_changes(db);
|
||||
sqlite3_finalize(del);
|
||||
if (rc != SQLITE_DONE) goto failed;
|
||||
rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto failed;
|
||||
rc = sqlite3_prepare_v2(db, "DELETE FROM media_preparations WHERE chat_id=? AND location=?", -1, &st, NULL);
|
||||
if (rc != SQLITE_OK) goto failed;
|
||||
int errors = 0, rows = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (!files[i].shared && mi_remove(files[i].path)) { errors++; continue; }
|
||||
sqlite3_stmt* blocks = NULL;
|
||||
rc = sqlite3_prepare_v2(db, "DELETE FROM media_files WHERE chat_id=? AND location=?", -1, &blocks, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(blocks, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(blocks, 2, files[i].location, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(blocks);
|
||||
int finish = blocks ? sqlite3_finalize(blocks) : SQLITE_OK;
|
||||
if (rc != SQLITE_DONE || finish != SQLITE_OK) goto failed;
|
||||
rows += sqlite3_changes(db);
|
||||
rc = sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, files[i].location, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_DONE) goto failed;
|
||||
rc = sqlite3_reset(st);
|
||||
if (rc != SQLITE_OK) goto failed;
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index: delete_channel chat=%s files=%d removed=%d rows=%d",
|
||||
chat_id, files, removed, rows);
|
||||
return 0;
|
||||
rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto failed;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index delete chat=%s paths=%u rows=%d failures=%d", chat_id, count, rows, errors);
|
||||
u_free(files); return errors ? -1 : 0;
|
||||
failed:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index incomplete delete chat=%s rc=%d paths=%u: %s",
|
||||
chat_id, rc, count, sqlite3_errmsg(db));
|
||||
if (st) sqlite3_finalize(st);
|
||||
u_free(files); return -1;
|
||||
}
|
||||
|
||||
int media_index_generate_uuid(uint8_t uuid_out[16]) {
|
||||
@@ -163,54 +339,72 @@ void media_index_result_free(struct media_index_result* result) {
|
||||
if (result->block_sigs) { u_free(result->block_sigs); result->block_sigs = NULL; }
|
||||
}
|
||||
|
||||
/* Завершить только свой savepoint; SQLite могла уже отменить всю транзакцию. */
|
||||
static int mi_end_registration(sqlite3* db, int failed) {
|
||||
if (!failed && !mi_exec(db, "RELEASE media_registration")) return 0;
|
||||
if (!sqlite3_get_autocommit(db)) {
|
||||
if (!mi_exec(db, "ROLLBACK TO media_registration")) mi_exec(db, "RELEASE media_registration");
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media_index registration transaction no longer active");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Проверка геометрии без умножения непроверенных размеров и числа блоков. */
|
||||
static int mi_result_valid(const struct media_index_result* result) {
|
||||
if (result && result->file_size > 0 && result->block_size > 0 && result->num_blocks > 0 && result->block_ids
|
||||
&& (uint64_t)result->num_blocks <= UINT32_MAX / 64
|
||||
&& (result->file_size - 1) / result->block_size + 1 == result->num_blocks) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid geometry bytes=%lld block_size=%lld count=%d ids=%p",
|
||||
(long long)(result ? result->file_size : 0), (long long)(result ? result->block_size : 0),
|
||||
result ? result->num_blocks : 0, (void*)(result ? result->block_ids : NULL)); return -1;
|
||||
}
|
||||
|
||||
/* Общий атомарный путь для автора и держателя скачанного файла. */
|
||||
int media_index_commit(sqlite3* db, const struct media_index_result* result,
|
||||
uint64_t node_id, const uint8_t* ed25519_privkey,
|
||||
const char* chat_id, const char* dest_path, const char* media_base) { if (!db || !result || !chat_id || !dest_path || !media_base) return -1;
|
||||
|
||||
int nb = result->num_blocks;
|
||||
int64_t bs = result->block_size;
|
||||
int64_t fs = result->file_size;
|
||||
uint64_t node_id, const uint8_t* key, const char* chat_id, const char* dest_path, const char* media_base) {
|
||||
char location[MEDIA_PATH_MAX];
|
||||
if (!db || !chat_id || !*chat_id || strlen(chat_id) >= 64 || mi_result_valid(result)
|
||||
|| media_index_location(dest_path, media_base, location)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid registration arguments"); return -1;
|
||||
}
|
||||
if (mi_exec(db, "SAVEPOINT media_registration")) return -1;
|
||||
int64_t ts = (int64_t)time(NULL);
|
||||
|
||||
size_t base_len = strlen(media_base);
|
||||
if (base_len > 0 && media_base[base_len - 1] == '/') base_len--;
|
||||
const char* rel = dest_path;
|
||||
if (strncmp(rel, media_base, base_len) == 0 && rel[base_len] == '/') rel += base_len + 1;
|
||||
|
||||
for (int n = 0; n < nb; n++) {
|
||||
int64_t cs = (n == nb - 1) ? fs - n * bs : bs;
|
||||
int64_t off = n * bs;
|
||||
|
||||
char location[512];
|
||||
snprintf(location, sizeof(location), "%s", rel);
|
||||
|
||||
uint8_t sig_msg[92];
|
||||
size_t soff = 0;
|
||||
memcpy(sig_msg + soff, result->media_id, 16); soff += 16;
|
||||
memcpy(sig_msg + soff, result->content_hash, 32); soff += 32;
|
||||
memcpy(sig_msg + soff, &ts, 8); soff += 8;
|
||||
memcpy(sig_msg + soff, &fs, 8); soff += 8;
|
||||
memcpy(sig_msg + soff, &cs, 8); soff += 8;
|
||||
int32_t cn = (int32_t)n; memcpy(sig_msg + soff, &cn, 4); soff += 4;
|
||||
memcpy(sig_msg + soff, &off, 8); soff += 8;
|
||||
memcpy(sig_msg + soff, &node_id, 8); soff += 8;
|
||||
|
||||
uint8_t node_sign[64];
|
||||
if (ma_sign_block(ed25519_privkey, sig_msg, soff, node_sign) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: node_sign failed chunk=%d", n);
|
||||
return -1;
|
||||
int failed = 0;
|
||||
for (int n = 0; n < result->num_blocks; n++) {
|
||||
int64_t offset = (int64_t)n * result->block_size;
|
||||
int64_t size = n == result->num_blocks - 1 ? result->file_size - offset : result->block_size;
|
||||
uint8_t signature[64] = {0};
|
||||
if (key) {
|
||||
uint8_t message[92]; size_t used = 0;
|
||||
memcpy(message + used, result->media_id, 16); used += 16;
|
||||
memcpy(message + used, result->content_hash, 32); used += 32;
|
||||
memcpy(message + used, &ts, 8); used += 8;
|
||||
memcpy(message + used, &result->file_size, 8); used += 8;
|
||||
memcpy(message + used, &size, 8); used += 8;
|
||||
int32_t chunk = n; memcpy(message + used, &chunk, 4); used += 4;
|
||||
memcpy(message + used, &offset, 8); used += 8;
|
||||
memcpy(message + used, &node_id, 8); used += 8;
|
||||
if (ma_sign_block(key, message, used, signature)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index signing failed chat=%s chunk=%d", chat_id, n); failed = 1; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mi_insert(db, result->media_id, result->block_ids + n * 16,
|
||||
result->content_hash, chat_id, location,
|
||||
node_id, node_sign, ts, fs, cs, n, off) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: insert failed chunk=%d", n);
|
||||
return -1;
|
||||
if (mi_insert(db, result->media_id, result->block_ids + (size_t)n * 16, result->content_hash,
|
||||
chat_id, location, node_id, signature, ts, result->file_size, size, n, offset)) { failed = 1; break; }
|
||||
}
|
||||
if (!failed) {
|
||||
sqlite3_stmt* st = NULL;
|
||||
int rc = sqlite3_prepare_v2(db, "DELETE FROM media_preparations WHERE chat_id=? AND location=?", -1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, location, -1, SQLITE_STATIC);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
int finish = st ? sqlite3_finalize(st) : SQLITE_OK;
|
||||
if (rc != SQLITE_DONE || finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index clearing preparation failed chat=%s rc=%d: %s", chat_id, rc, sqlite3_errmsg(db));
|
||||
failed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index: committed %d chunks for chat=%s path=%s",
|
||||
nb, chat_id, rel);
|
||||
if (mi_end_registration(db, failed)) return -1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index registered chat=%s blocks=%d path=%s signed=%d",
|
||||
chat_id, result->num_blocks, location, key != NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -218,7 +412,11 @@ int media_index_register_downloaded(sqlite3* db, const uint8_t* media_id, const
|
||||
const uint8_t* content_hash, const char* chat_id,
|
||||
const char* location, uint64_t node_id,
|
||||
int64_t file_size, int64_t chunk_size, int chunk, int64_t offset) {
|
||||
if (!db || !media_id || !block_id || !content_hash || !chat_id || !location) return -1;
|
||||
if (!db || !media_id || !block_id || !content_hash || !chat_id || !*chat_id || strlen(chat_id) >= 64
|
||||
|| mi_location_valid(location) || file_size <= 0 || chunk_size <= 0 || chunk < 0
|
||||
|| offset < 0 || offset >= file_size || chunk_size > file_size - offset) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index invalid downloaded block"); return -1;
|
||||
}
|
||||
static const uint8_t zero_sign[64] = {0};
|
||||
return mi_insert(db, media_id, block_id, content_hash, chat_id, location,
|
||||
node_id, zero_sign, (int64_t)time(NULL), file_size, chunk_size, chunk, offset);
|
||||
@@ -344,12 +542,15 @@ void media_index_register_async(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!*src_path || !*dest_path || strlen(src_path) >= sizeof(((struct mi_reg_ctx*)0)->src_path)
|
||||
char checked_location[MEDIA_PATH_MAX];
|
||||
if (!*src_path || !*chat_id || media_index_location(dest_path, media_base, checked_location)
|
||||
|| strlen(src_path) >= sizeof(((struct mi_reg_ctx*)0)->src_path)
|
||||
|| strlen(dest_path) >= sizeof(((struct mi_reg_ctx*)0)->dest_path)
|
||||
|| strlen(chat_id) >= sizeof(((struct mi_reg_ctx*)0)->chat_id)
|
||||
|| strlen(media_base) >= sizeof(((struct mi_reg_ctx*)0)->media_base)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index register oversized/empty path"); cb(cb_arg, -1, NULL); return;
|
||||
}
|
||||
if (media_index_track_preparation(db, chat_id, dest_path, media_base)) { cb(cb_arg, -1, NULL); return; }
|
||||
struct mi_reg_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index context allocation failed"); cb(cb_arg, -1, NULL); return; }
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Порядок: init → register_async → callback с UUID, хешем и подписями блоков; затем можно анонсировать файл.
|
||||
* Register_async копирует/хеширует/подписывает файл в worker, затем commit и callback выполняются в uasync.
|
||||
* db/ma/ua заимствованы и должны жить до завершения; доступ к БД выполняется в потоке ядра.
|
||||
* Назначения учитываются в media_preparations до запуска worker; неуспешная подготовка оставляет их для удаления канала.
|
||||
* Перед delete_channel вызывающий должен дождаться всех своих workers/completions; функция их сама не отменяет.
|
||||
* Результат callback заимствован только до возврата. result_free освобождает block_ids/block_sigs, но не саму структуру. */
|
||||
#ifndef MEDIA_INDEX_H
|
||||
#define MEDIA_INDEX_H
|
||||
@@ -11,7 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
struct media_async;
|
||||
@@ -19,10 +21,25 @@ struct UASYNC;
|
||||
|
||||
#define MEDIA_ID_SIZE 16
|
||||
#define MEDIA_HASH_SIZE 32
|
||||
#define MEDIA_PATH_MAX 1024
|
||||
#define MEDIA_BASE_MAX 512
|
||||
|
||||
/* Пути UTF-8, пределы включают NUL. location — непустой относительный путь с '/'; '.'/'..' запрещены.
|
||||
* Преобразования нормализуют разделители/компоненты '.' полного пути, отклоняют '..' и обрезание.
|
||||
* Проверяется строковая принадлежность базе с тем же написанием, а не переходы через ссылки файловой системы. 0/-1. */
|
||||
int media_index_location(const char* path, const char* base, char location[MEDIA_PATH_MAX]);
|
||||
int media_index_path(const char* base, const char* location, char path[MEDIA_PATH_MAX]);
|
||||
/* Общая база БД в ui_state: set создаёт отсутствующую запись, существующая должна совпадать после нормализации.
|
||||
* get возвращает проверенную базу; 0/-1. Смена базы при работающем ядре не поддерживается. */
|
||||
int media_index_set_base(sqlite3* db, const char* base);
|
||||
int media_index_get_base(sqlite3* db, char base[MEDIA_BASE_MAX]);
|
||||
/* Учесть назначение до файловой подготовки. Ошибка подготовки оставляет запись для delete_channel;
|
||||
* успешная регистрация файла удаляет её в той же транзакции. Сам файл здесь не меняется. 0/-1. */
|
||||
int media_index_track_preparation(sqlite3* db, const char* chat_id, const char* path, const char* base);
|
||||
|
||||
struct media_index_result {
|
||||
uint8_t media_id[16];
|
||||
uint8_t content_hash[32];
|
||||
uint8_t media_id[16]; // случайный идентификатор целого файла
|
||||
uint8_t content_hash[32]; // SHA256 полного содержимого
|
||||
int64_t file_size; // полный размер файла, байты
|
||||
int64_t block_size; // размер обычного блока, байты; последний может быть короче
|
||||
int num_blocks; // число блоков и элементов block_ids/block_sigs
|
||||
@@ -32,28 +49,34 @@ struct media_index_result {
|
||||
|
||||
// ── Инициализация таблицы ──
|
||||
|
||||
// Создать таблицу media_files и её индексы, сохранив существующие записи; 0/-1.
|
||||
// Создать media_files, media_preparations и индексы, сохранив записи; ошибка любого DDL даёт -1, успех 0.
|
||||
int media_index_init(sqlite3* db);
|
||||
|
||||
// ── Регистрация файлов и блоков ──
|
||||
|
||||
/* Записать подготовленные блоки и подписанные сведения о держателе; 0/-1, result остаётся у вызывающего.
|
||||
/* Атомарно записать все блоки файла; 0/-1, result остаётся у вызывающего, block_sigs здесь не используются.
|
||||
* ed25519_privkey — 32 байта для авторской node_sign; NULL — скачанный файл, node_sign новой записи нулевая.
|
||||
* chat_id — десятичный ID канала; dest_path — готовый файл, media_base — база для относительного location.
|
||||
* Все блоки записываются через SAVEPOINT: ошибка откатывает регистрацию; внешнюю транзакцию завершает вызывающий.
|
||||
* SQLite может сама откатить всю транзакцию при тяжёлой ошибке. Уже выполненные файловые операции не откатываются.
|
||||
* Повтор с теми же метаданными/путём допустим, конфликт block_id даёт ошибку; прежняя подпись сохраняется.
|
||||
* Выполняется в uasync, сама функция не копирует/не хеширует файл и не анонсирует блоки. */
|
||||
int media_index_commit(
|
||||
sqlite3* db, const struct media_index_result* result,
|
||||
uint64_t node_id, const uint8_t* ed25519_privkey,
|
||||
const char* chat_id, const char* dest_path, const char* media_base);
|
||||
|
||||
/* Зарегистрировать скачанный блок в media_files (location — путь к собранному файлу),
|
||||
* чтобы узел мог отдавать его как держатель. node_sign = нули (при отдаче не проверяется). */
|
||||
/* Зарегистрировать один скачанный блок: location — канонический путь относительно общей базы; 0/-1.
|
||||
* file_size/chunk_size >0, chunk/offset >=0, блок целиком помещается в файл.
|
||||
* node_sign новой записи — нули (при отдаче не проверяется); существующая подпись при совпадающем повторе сохраняется.
|
||||
* Для целого файла использовать commit с ключом NULL; одиночные вызовы не образуют общую транзакцию. */
|
||||
int media_index_register_downloaded(sqlite3* db, const uint8_t* media_id, const uint8_t* block_id,
|
||||
const uint8_t* content_hash, const char* chat_id,
|
||||
const char* location, uint64_t node_id,
|
||||
int64_t file_size, int64_t chunk_size, int chunk, int64_t offset);
|
||||
|
||||
/* Подготовить и зарегистрировать файл; пути/ключ/chat_id копируются до возврата, cb_arg заимствован до callback.
|
||||
* При copy_file!=0 копирует src_path в dest_path; при 0 dest_path уже должен содержать готовый файл.
|
||||
* src/dest < MEDIA_PATH_MAX, media_base < MEDIA_BASE_MAX, chat_id <64 байт; dest проверяется до запуска worker.
|
||||
* Файлы и db/ma/ua должны жить до callback. Callback может быть синхронным при отказе запуска.
|
||||
* err=0 даёт result только на время callback; иначе result=NULL. Сохранить нужные поля/массивы отдельной копией. */
|
||||
void media_index_register_async(
|
||||
@@ -79,7 +102,9 @@ int media_calc_block_size(uint64_t file_size);
|
||||
// ── Локальное удаление канала ──
|
||||
|
||||
/* Удалить медиа канала (локальное удаление канала): стереть файлы на диске
|
||||
* (media_base + location) и строки media_files с chat_id. Возвращает 0/ошибка. */
|
||||
* (media_base + location) и строки media_files с chat_id. Вызывать вне внешней транзакции и после остановки writers.
|
||||
* Общий файл другого канала сохраняется; ENOENT — успех. При ошибке сохраняются записи для повторной очистки.
|
||||
* Удаление файлов необратимо: возможен частичный результат. 0 — всё очищено, -1 — ошибка/неполная очистка. */
|
||||
int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+155
-209
@@ -1,251 +1,197 @@
|
||||
# Репликация block_availability между суперузлами
|
||||
|
||||
## 1. Таблицы в SQLite
|
||||
## Каталог и курсоры
|
||||
|
||||
### `block_availability`
|
||||
Хранит информацию о том, какие узлы имеют какие блоки:
|
||||
Нормативные wire-структуры находятся в `media_delivery_proto.h`, состояние и владение
|
||||
ресурсами — в `media_delivery.h`. Все обработчики выполняются в uasync-потоке ядра.
|
||||
|
||||
```
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
block_uuid BLOB — UUID блока (16 байт)
|
||||
group_id INTEGER — id группы
|
||||
node_id INTEGER — id узла-владельца блока
|
||||
chunk INTEGER — номер чанка
|
||||
timestamp INTEGER — unix-время добавления/обновления
|
||||
```
|
||||
`block_availability` хранит положительные объявления PROCESSING/COMPLETE:
|
||||
`id AUTOINCREMENT, block_uuid[16], group_id, node_id, chunk, timestamp, status`.
|
||||
Уникальный ключ — `(group_id, block_uuid, node_id)`, индекс страниц — `(group_id,id)`.
|
||||
Это каталог кандидатов для загрузки, не подтверждённый снимок удалённой файловой системы.
|
||||
Протокола удалённых tombstone здесь нет; загрузчик проверяет полученные блоки и меняет
|
||||
держателя при недоступности. Пересоздание источника не удаляет ранее полученные объявления.
|
||||
|
||||
Индексы: `block_uuid+node_id` (unique), `id`, `node_id`.
|
||||
`super_sync` хранит **нашу позицию приёма от источника**:
|
||||
`(group_id, peer_node_id) PRIMARY KEY, source_epoch[16], last_recv_id`.
|
||||
Позиции отправки в БД и ACK нет. ID входящей строки не становится локальным ID.
|
||||
|
||||
### `super_sync`
|
||||
Хранит указатель, до какого `id` синхронизировано с каждым другим суперузлом:
|
||||
`media_replication_meta` хранит версию схемы и случайную эпоху локального каталога.
|
||||
Обычный stop/start сохраняет эпоху, каталог и RX-курсоры. При переходе со старой схемы
|
||||
или отсутствии одной из таблиц вспомогательный каталог пересоздаётся одной транзакцией
|
||||
с новой эпохой. `media_files` и данные чата при этом сохраняются.
|
||||
|
||||
```
|
||||
peer_node_id INTEGER PRIMARY KEY — id пира-суперузла
|
||||
last_recv_id INTEGER — максимальный id, подтверждённый пиром (что он принял от нас)
|
||||
```
|
||||
## Роли и соединения
|
||||
|
||||
Обе таблицы создаются в `media_delivery_create_tables()` (media_delivery.c:940-971).
|
||||
Пир индексируется по паре `(node_id,group_id)`. Обе стороны репликации должны быть
|
||||
актуальными суперузлами одной CHAT-группы: `adm_tags.supernode=yes`, без `deleted=yes`.
|
||||
Производный `node_type` и общий диагностический флаг `md.is_supernode` не дают полномочий.
|
||||
Группа и отправитель берутся только из router; группа не дублируется в SYNC payload.
|
||||
|
||||
На всех узлах есть `block_availability`. Суперузлы сохраняют в неё весь контент групп, в которых состоят. Обычные узлы — только свой контент. Если нет суперузлов или они недоступны, роль суперузла выполняет автор контента.
|
||||
Перечитывание ролей выполняется при init, событиях member_sync и каждые 30 секунд.
|
||||
Так обнаруживаются и группы, созданные после init. Chat core и media_index должны
|
||||
быть готовы до media_delivery_init; отказ подписки на роли завершает init ошибкой.
|
||||
|
||||
## 2. Обнаружение других суперузлов
|
||||
Каждый контекст владеет отдельным CM handle. CM может предоставить прямой или
|
||||
непрямой путь; handle сохраняется при DOWN, восстановлением занимается CM.
|
||||
После TIMEOUT свой handle закрывается, повтор открытия выполняется с задержкой.
|
||||
Raw ETCP DOWN и потеря BGP-маршрута не удаляют объявления или RX-курсоры.
|
||||
|
||||
Суперузлы определяются через свойство `supernode=yes` в `adm_tags` таблицы `peers_<channel>`.
|
||||
При потере локальной роли закрываются только peers этой группы; чужие объявления
|
||||
и все её RX-курсоры удаляются атомарно, собственные объявления сохраняются.
|
||||
Удаление канала отменяет его работу и удаляет весь его каталог вместе с курсорами.
|
||||
Обычный destroy сохраняет БД; до free отменяются CM callbacks, waiter, pump и таймеры.
|
||||
|
||||
Механизмы обнаружения:
|
||||
## Запросы страниц
|
||||
|
||||
- **При старте** (`md_super_start`, стр. 730): суперузел ищет в БД все узлы с `node_type=4` в своей chat-группе и инициирует прямое подключение к каждому через `conn_mgr_open`.
|
||||
- **При появлении нового узла в BGP** (`md_on_bgp_node`, стр. 790): проверяет `node_type=4` в `peers_<channel>` — если да, вызывает `md_super_connect`.
|
||||
- **При изменении свойств** (`md_on_props_changed`, стр. 825): если `adm_tags` изменились на `supernode=yes` — подключается; если на `no` — удаляет пира.
|
||||
| Команда | Подкоманда | Размер |
|
||||
|---------|------------|--------|
|
||||
| SYNC_REQ | 0x08 | 41 байт |
|
||||
| SYNC_RESP | 0x09 | 53 + N × 45 байт, N ≤ 32 |
|
||||
| CHANGED | 0x0E | 1 байт |
|
||||
|
||||
Как только суперузел видит появление другого суперузла в BGP-таблице — пробует установить прямую связь. Прямое подключение — независимый автономный процесс (без посредников и без обратного подключения — это сделает другой суперузел сам). Если не получается 10 сек — отключаемся и следующая попытка через час.
|
||||
Все числа SYNC кодируются little endian. UUID запроса и эпоха — непрозрачные 16 байт.
|
||||
ID строк/курсоры ограничены INT64_MAX. Максимум ответа — 1493 байта.
|
||||
|
||||
## 3. Протокол репликации
|
||||
После CM UP получатель создаёт запрос с новым UUID и сохранёнными эпохой/курсором
|
||||
источника. На пару «группа–пир» приходится один ожидающий запрос.
|
||||
При таймауте повторяются его прежние UUID и параметры, с задержками 2, 4, 8, 16, 30 секунд.
|
||||
Успешный commit сбрасывает задержку до 2 секунд. DOWN отменяет RPC; UP создаёт новый UUID.
|
||||
|
||||
### Handshake: SUPER_HELLO (subcmd 0x0E)
|
||||
Источник строит страницу внутри read transaction:
|
||||
|
||||
После установки прямого подключения инициатор отправляет `SUPER_HELLO` (`md_super_conn_cb`, стр. 484) с полем `last_recv_id` — до какого `id` он уже получил записи от этого пира.
|
||||
1. При совпадении эпох читает после запрошенного курсора, иначе после нуля.
|
||||
2. Фиксирует верхнюю границу H из `sqlite_sequence`, которая не уменьшается при DELETE.
|
||||
3. Выбирает только строки router-группы с `after < id ≤ H`, ORDER BY id, LIMIT 33.
|
||||
4. Передаёт первые 32. Если есть 33-я, `more=1`, `next_id` равен ID последней переданной.
|
||||
5. В последней странице `more=0`, `next_id=H`, даже при отсутствии строк.
|
||||
|
||||
При получении `SUPER_HELLO` (`md_handle_super_hello`, стр. 454):
|
||||
- Создаёт/обновляет `media_super_peer` в памяти
|
||||
- Устанавливает `connected=1`, `hello_done=1`
|
||||
- Отправляет ответный `SUPER_HELLO` со своим `last_recv_id`
|
||||
- Обнуляет `inflight_count` и запускает репликацию через `md_super_repl_send`
|
||||
Так курсор проходит удалённые ID и промежутки, принадлежащие другим группам.
|
||||
Повторный запрос может получить более свежий согласованный снимок: принимается только
|
||||
первый корректный ответ актуального UUID, следующие игнорируются.
|
||||
|
||||
### Репликация: SUPER_REPL (subcmd 0x08)
|
||||
Ошибка чтения/завершения snapshot даёт DB_ERROR без записей; получатель сохраняет RPC
|
||||
и повторяет его по исходному дедлайну. Если при совпавшей эпохе курсор выше H,
|
||||
CURSOR_INVALID сбрасывает только эту RX-пару, оставляя объявления; новый запрос с нуля
|
||||
будет создан после задержки 30 секунд. Эпоха источника от чужого курсора не меняется.
|
||||
|
||||
`md_super_repl_send` (стр. 228):
|
||||
- Запрашивает из `block_availability` записи с `id > peer->peer_last_recv_id`
|
||||
- Порциями по 4 записи (константа `MEDIA_MAX_REPL_INFLIGHT = 4`)
|
||||
- Каждая запись: 52 байта (id:8, block_uuid:16, group_id:8, node_id:8, chunk:4, timestamp:8)
|
||||
- Пакет: `subcmd` + `seq` (макс id в пачке) + `num_entries` + сырые строки
|
||||
- Инкрементит `inflight_count`
|
||||
## Атомарный приём и сходимость
|
||||
|
||||
### Подтверждение: SUPER_ACK (subcmd 0x09)
|
||||
До SQL проверяются весь пакет и его точная длина, группа/пир/UUID, эпоха, диапазон
|
||||
from/next, количество, порядок ID, статусы, дубли ключей и согласованность chunk.
|
||||
Неподходящий ответ не меняет запрос, курсор или дедлайн.
|
||||
|
||||
При получении `SUPER_ACK` (`md_handle_super_ack`, стр. 436):
|
||||
- Обновляется `peer_last_recv_id = ack_seq`
|
||||
- Декрементится `inflight_count`
|
||||
- Таймаут сбрасывается на начальное значение (`MEDIA_REPL_TIMEOUT_TB = 2с`)
|
||||
- Запускается следующая порция репликации (если есть записи)
|
||||
Все объявления и RX-эпоха/курсор применяются одной собственной транзакцией.
|
||||
Внешняя незавершённая транзакция приводит к отказу этой попытки: вложенный savepoint
|
||||
не гарантировал бы окончательный commit. При ошибке строки, курсора или commit
|
||||
прогресс не публикуется; учитывается и автоматический rollback SQLite.
|
||||
|
||||
### Приём репликации (`md_handle_super_repl`, стр. 404):
|
||||
- Разбирает каждую запись, вызывает `md_ba_insert` (INSERT OR REPLACE в `block_availability`)
|
||||
- Сохраняет `max_id` в `super_sync` через `md_ss_set(db, from_node, max_id)`
|
||||
- Отправляет `SUPER_ACK` с подтверждённым `max_id`
|
||||
Объединение выбирает максимум `(status,timestamp)`: COMPLETE выше PROCESSING,
|
||||
при равном статусе выигрывает более свежий timestamp. Chunk одного блока в группе
|
||||
неизменяем. Только реальное изменение выполняет REPLACE с новым локальным ID;
|
||||
повтор/старое объявление не меняет строку и sqlite_sequence. Это останавливает эхо
|
||||
при распространении одной записи через несколько суперузлов.
|
||||
|
||||
## 4. Таймауты и retransmit
|
||||
HAVE_BLOCK и BLOCK_PROCESSING проходят тот же merge; несовпадение router-группы
|
||||
с полем объявления отклоняется. ACK HAVE: 0 — изменение, 1 — no-op, 2 — ошибка БД.
|
||||
Инициатор HAVE_BLOCK не ожидает этот ACK, не ставит таймер и игнорирует его при приёме.
|
||||
Собственные зарегистрированные блоки добавляются непосредственно из media_files,
|
||||
в том числе на единственном суперузле; повторный анонс использует стабильное время.
|
||||
Переанонс проверяет типы SQLite, точные 16-байтные UUID, chunk в диапазоне uint32
|
||||
и полный канонический ненулевой decimal chat_id в диапазоне uint64 до копирования/отправки.
|
||||
Повреждённая строка пропускается с WARN; SQL-ошибка завершает обход с ERROR и счётчиками
|
||||
неполной обработки. Уже отправленные корректные объявления сохраняются.
|
||||
|
||||
- **Начальный таймаут:** 2 секунды (`MEDIA_REPL_TIMEOUT_TB`)
|
||||
- **Exponential backoff:** при отсутствии ACK таймаут удваивается (до 10 минут — `MEDIA_REPL_TIMEOUT_MAX_TB`)
|
||||
- **При успешном ACK:** таймаут сбрасывается на 2 секунды
|
||||
- **При неудачном подключении:** повтор через 1 час (`MEDIA_RECONNECT_COOLDOWN_TB`)
|
||||
- Механика: `md_repl_schedule_retrans` → `md_repl_retrans_cb` → `md_super_repl_send`
|
||||
После commit изменений отправляется CHANGED остальным peers этой группы.
|
||||
CHANGED во время RPC ставит dirty, и после commit выполняется ещё один запрос.
|
||||
`more=1` также сразу запускает следующую страницу. Потеря CHANGED восстанавливается
|
||||
периодическим запросом каждые 30 секунд; дубликаты не создают новые локальные ID.
|
||||
|
||||
## 5. Источники записей в block_availability
|
||||
## Очереди, диагностика и тесты
|
||||
|
||||
Записи попадают в таблицу из трёх источников:
|
||||
Отправка репликации использует force=0 и общий router backpressure.
|
||||
Контекст хранит один отложенный запрос ответа, один свой RPC и признак CHANGED;
|
||||
очередь ответов не растёт. Более поздний входящий запрос заменяет отложенный.
|
||||
Если это старый RPC, его ответ будет проигнорирован, а актуальный запрос повторится.
|
||||
|
||||
1. **HAVE_BLOCK** — когда req-узел принял полный блок, он уведомляет свой суперузел (`md_handle_have_block`, стр. 371)
|
||||
2. **SUPER_REPL** — репликация от другого суперузла
|
||||
3. **Локально** — при регистрации своего медиа через `media_index`
|
||||
Waiter только планирует отменяемый pump; один вызов отправляет максимум три пакета.
|
||||
Watchdog каждые 200ms проверяет дедлайны, повторяет неудачные отправки и восстанавливает
|
||||
пробуждение после reset router-очереди. Репликация не перезапускает общий MEDIA-канал,
|
||||
который также используется загрузками и выдачами блоков.
|
||||
|
||||
При получении HAVE_BLOCK суперузел немедленно реплицирует новые записи всем подключённым super-пирам (`md_handle_have_block`, стр. 398-400).
|
||||
Категория media на DEBUG показывает group/peer, UUID/epoch, from/next, count/more,
|
||||
результат merge и commit. Ошибки SQL показывают фазу и коды, таймауты — UUID и задержку.
|
||||
|
||||
## 6. Кто есть суперузел
|
||||
`test_media_replication` использует настоящие handlers/SQLite/router send_q с
|
||||
управляемыми CM и доставкой: страницы до 10000 записей, ID выше 2^32, усечения,
|
||||
дубли/чужие ответы, SQL/commit faults, retry, смена эпохи, две группы, смена роли,
|
||||
сходимость трёх узлов и backpressure/reset. `test_media_delivery_sql` проверяет
|
||||
настоящую схему, restart/reset и сохранность media_files. Full/chat/integration
|
||||
проверяют реальные транспорт, CHAT-членство и подписанные роли.
|
||||
SQL-тест также проверяет 36 вариантов полей локального переанонса, смесь корректных/
|
||||
повреждённых строк и пять SQL faults с последующим восстановлением и точными wire-полями.
|
||||
|
||||
- `node_type=4` в `peers_<channel>` + `adm_tags` содержит `supernode=yes`
|
||||
- `md->is_supernode` устанавливается при инициализации и может меняться через `media_delivery_set_supernode` / `md_on_props_changed`
|
||||
- Если узел перестаёт быть суперузлом — вызывается `md_super_stop`: удаляются все чужие блоки из `block_availability`, очищается очередь свер-пиров
|
||||
## Попытки передачи и relay
|
||||
|
||||
## 7. Обслуживаемые узлы (served_nodes)
|
||||
Актуальные структуры пакетов находятся в `media_delivery_proto.h`, владение загрузкой —
|
||||
в `media_download.h`, источником байтов и выдачами — в `media_delivery.h`.
|
||||
Реализация приёма, проверки, сборки и выдачи блоков работает синхронно в uasync.
|
||||
|
||||
- Обычный узел при старте загрузки отправляет суперузлу `SERVE_REG` (subcmd 0x01)
|
||||
- Суперузел сохраняет его в `served_nodes` (ll_queue с индексом по node_id)
|
||||
- Если соединение рвётся — узел автоматически выпадает из списка (`md_on_conn_status`)
|
||||
- Также автор контента получает от суперузла обновления о доступности его блоков
|
||||
Каждый BLOCK_REQ имеет новый случайный `transfer_id[16]`. BLOCK_CHUNK, BLOCK_DONE,
|
||||
BLOCK_OVERLOADED, BLOCK_RELAY_FULL и CANCEL повторяют его. Группа и end-to-end
|
||||
отправитель берутся из router. Получатель проверяет группу, текущего держателя,
|
||||
media_id, block_id, chunk, состояние REQ/RECV и UUID до файловых операций.
|
||||
Поздние ответы прежней попытки не меняют таймеры, файл и счётчики новой.
|
||||
|
||||
## 8. Очистка
|
||||
CHUNK содержит ровно объявленное число байтов, от 1 до 1024. Принимается только
|
||||
непрерывный префикс в пределах ожидаемого размера блока. Полный дубль игнорируется
|
||||
без обновления прогресса; разрыв, частичное перекрытие и повреждённая текущая попытка
|
||||
вызывают смену держателя. Ошибка локального чтения/записи/close завершает загрузку.
|
||||
Прогресс публикуется после успешного закрытия записанного файла.
|
||||
|
||||
- **При удалении узла из BGP** (`md_on_bgp_node`, TOPO_NODE_EVENT_REMOVE): удаляются все его записи из `block_availability`, свер-пир удаляется
|
||||
- **При разрыве соединения** (`md_on_conn_status`, ETCP_CONN_STATUS_DOWN/DELETE): свер-пир помечается как отключённый (`connected=0, hello_done=0, inflight_count=0`), таймеры отменяются; served_node удаляется
|
||||
- **При отключении режима суперузла** (`md_super_stop`): удаляются все чужие блоки (`WHERE node_id != self`), очищается очередь свер-пиров
|
||||
Каждая попытка создаёт уникальный temp и `media_block_source`. Producer загрузки
|
||||
удерживает одну ссылку, каждая выдача — ещё одну. Источник индексирован по block_id;
|
||||
группа, media_id и chunk дополнительно проверяются при выборе. Доступны только
|
||||
успешно записанные байты `available`. Источник готового файла использует тот же
|
||||
объект, но задаёт диапазон `base/size` и не владеет удалением файла.
|
||||
|
||||
## 9. Диаграмма потоков
|
||||
Один `stream_ctx` на получателя хранит свой UUID, offset, waiter, отложенный pump и
|
||||
таймер простоя. Единственный pump выдаёт готовые диапазоны и растущие temp.
|
||||
Waiter только планирует pump: синхронное срабатывание не создаёт рекурсивную отправку.
|
||||
DATA и DONE соблюдают backpressure router. DONE со статусом OK отправляется, только
|
||||
когда блок проверен и весь его хвост принят в очередь router.
|
||||
|
||||
```
|
||||
[Узел A: обычный] [Суперузел S1] [Суперузел S2]
|
||||
| | |
|
||||
|── HAVE_BLOCK ─────────────────>| |
|
||||
| |── md_ba_insert (локально) |
|
||||
| |── SUPER_REPL ────────────>|
|
||||
| | |── md_ba_insert (локально)
|
||||
| |<─────────── SUPER_ACK ────|
|
||||
| |── md_ss_set(S2, max_id) |
|
||||
| | |
|
||||
|── QUERY ──────────────────────>| |
|
||||
|<───── QUERY_RESP (node list) ──| |
|
||||
| | |
|
||||
|── BLOCK_REQ ─────────────────────────────────────────────>|
|
||||
|<───── BLOCK_CHUNK (stream) ───────────────────────────────|
|
||||
|<───── BLOCK_DONE ─────────────────────────────────────────|
|
||||
| | |
|
||||
|── HAVE_BLOCK ─────────────────>| |
|
||||
| |── SUPER_REPL ────────────>|
|
||||
```
|
||||
DONE не передаёт подпись: ожидаемая Ed25519-подпись взята из метаданных сообщения,
|
||||
ровно 32 байта ключа автора копируются из nodes при старте. Проверяются размер файла
|
||||
и подпись фактических байтов. Затем все блоки собираются в соседний temp; полный
|
||||
SHA-256 проверяется всегда. После проверенных файловых операций имя назначения
|
||||
заменяется атомарно, затем регистрируется в SQLite. Между ФС и SQLite общей транзакции
|
||||
нет: ошибка регистрации оставляет опубликованный файл в учёте подготовки для очистки.
|
||||
|
||||
## 10. Константы
|
||||
До трёх выдач индексированных файлов допускаются одновременно; действует также
|
||||
настраиваемый лимит на файл. У растущего блока до пяти relay-читателей. При перегрузке
|
||||
RELAY_FULL содержит уникальных получателей именно этого блока своей группы, без
|
||||
самого запросившего узла; если таких нет, возвращается OVERLOADED с задержкой.
|
||||
CANCEL закрывает только точную выдачу данного узла/группы/UUID, снимая её счётчики один раз.
|
||||
|
||||
| Константа | Значение | Описание |
|
||||
|-----------|----------|----------|
|
||||
| `MEDIA_MAX_REPL_INFLIGHT` | 4 | Макс пакетов в полёте на одного пира |
|
||||
| `MEDIA_REPL_TIMEOUT_TB` | 20000 | Начальный таймаут 2с (0.1ms units) |
|
||||
| `MEDIA_REPL_TIMEOUT_MAX_TB` | 6000000 | Макс таймаут 10 мин |
|
||||
| `MEDIA_RECONNECT_COOLDOWN_TB` | 360000000 | Повтор подключения через 1 час |
|
||||
| `MEDIA_HELLO_TIMEOUT_TB` | 20000 | Таймаут SUPER_HELLO 2с |
|
||||
После завершения producer проверенный temp остаётся до окончания последнего читателя.
|
||||
Отмена непроверенного producer помечает источник ошибочным и будит выдачи для ответа
|
||||
UNAVAILABLE; при недоступной очереди выдача освобождается по таймауту.
|
||||
Удаление канала отменяет его загрузки и выдачи. Waiter, pump и timer снимаются до free.
|
||||
|
||||
## 11. BLOCK_PROCESSING: регистрация блока в процессе загрузки
|
||||
Отмена из progress callback немедленно отсоединяет загрузку, но обработчик удерживает
|
||||
её память до возврата из callback. Только после снятия последней ссылки вызывается
|
||||
done. Повторный start того же media_id независим от старого контекста и его пакетов.
|
||||
Входящие и исходящие писатели используют общую резервацию нормализованного назначения.
|
||||
|
||||
Когда узел начинает скачивать блок, он отправляет суперузлу `BLOCK_PROCESSING` (subcmd 0x10).
|
||||
Суперузел вставляет запись в `block_availability` со `status=0` (processing) через `INSERT OR IGNORE`
|
||||
— не перезаписывает существующие completed-записи (status=1).
|
||||
| Структура | Размер заголовка/пакета, байт |
|
||||
|-----------|-------------------------------|
|
||||
| BLOCK_REQ / CANCEL | 53 |
|
||||
| BLOCK_CHUNK | 59 + data_len |
|
||||
| BLOCK_DONE | 58 |
|
||||
| BLOCK_OVERLOADED | 57 |
|
||||
| BLOCK_RELAY_FULL | 55 + num_relay_nodes × 8 |
|
||||
|
||||
Когда блок полностью скачан, узел отправляет `HAVE_BLOCK` (subcmd 0x06).
|
||||
Суперузел вызывает `INSERT OR REPLACE` со `status=1` — processing-запись заменяется на completed.
|
||||
|
||||
Записи с `status=0` реплицируются между суперузлами так же, как и completed (поле `status` включено
|
||||
в сериализацию `SUPER_REPL`, размер строки: 56 байт вместо 52).
|
||||
|
||||
### BLOCK_PROCESSING vs HAVE_BLOCK
|
||||
|
||||
| Операция | subcmd | SQL | Статус | id |
|
||||
|----------|--------|-----|--------|----|
|
||||
| Начал качать | 0x10 | INSERT (status=0) | processing | новый X |
|
||||
| Скачал | 0x06 | DELETE (status=0) + проверка + INSERT (status=1) | completed | новый Y>X |
|
||||
|
||||
**Гарантия:** каждый переход создаёт новую запись с новым id. Старая удаляется — id не переиспользуется.
|
||||
Если completed уже существует — HAVE_BLOCK игнорируется (no-op).
|
||||
Если processing пытается вставиться поверх completed — `SQLITE_CONSTRAINT`, логируется WARN.
|
||||
|
||||
### Схема block_availability (обновлённая)
|
||||
|
||||
```
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT — монотонный, не переиспользуется
|
||||
block_uuid BLOB — UUID блока (16 байт)
|
||||
group_id INTEGER — id группы
|
||||
node_id INTEGER — id узла-владельца блока
|
||||
chunk INTEGER — номер чанка
|
||||
timestamp INTEGER — unix-время добавления/обновления
|
||||
status INTEGER — 0=processing (качается), 1=completed (скачан)
|
||||
```
|
||||
|
||||
Уникальный индекс: `(block_uuid, node_id)` — предотвращает две completed-записи для одного блока.
|
||||
|
||||
## 12. Relay-стриминг: передача блока в процессе загрузки
|
||||
|
||||
### Принцип
|
||||
Если узел C хочет скачать блок, а узел B уже качает этот блок от узла A:
|
||||
1. C видит B в QUERY_RESP (благодаря BLOCK_PROCESSING, status=0)
|
||||
2. C отправляет BLOCK_REQ к B
|
||||
3. B принимает запрос, добавляет C в `relay_downstream` для этого блока
|
||||
4. B получает BLOCK_CHUNK от A → пишет в файл → форвардит C
|
||||
5. B получает BLOCK_DONE от A → форвардит C → удаляет relay-контекст
|
||||
|
||||
### Flow control
|
||||
- Relay-узел всегда отстаёт от апстрима: он не может слать быстрее, чем получает
|
||||
- Каждый downstream хранит `sent_offset` — сколько байт уже отправлено
|
||||
- При получении нового чанка: читается файл, отправляется отставание (до 1KB за раз)
|
||||
- При ошибке отправки — чанк пропускается, ретрай на следующем чанке или в BLOCK_DONE
|
||||
- Бэкпрессур через `etcp_router_on_send_ready` для асинхронной досылки
|
||||
|
||||
### Переполнение relay (RELAY_FULL)
|
||||
- Максимум 5 downstream-узлов на блок (`MD_MAX_RELAY_DOWNSTREAM`)
|
||||
- При переполнении B отправляет C пакет `RELAY_FULL` (subcmd 0x11)
|
||||
со списком node_id текущих downstream-узлов
|
||||
- C извлекает список и пробует запросить блок у одного из этих узлов
|
||||
|
||||
### Структуры данных
|
||||
|
||||
**relay_block_ctx** — контекст relay для одного блока (индекс по block_id в `md->relay_blocks`):
|
||||
- `block_id[16]`, `media_id[16]`, `chunk` — идентификаторы блока
|
||||
- `chunk_file[2048]` — путь к `.chunk_N` файлу (источник данных для форвардинга)
|
||||
- `file_offset` — всего получено байт от апстрима (размер файла)
|
||||
- `downstream[5]` — массив relay_downstream
|
||||
|
||||
**relay_downstream** — один пир, запросивший relay:
|
||||
- `node_id`, `group_id` — кому слать
|
||||
- `sent_offset` — сколько байт уже отправлено этому пиру
|
||||
- `waiter` — handle для бэкпрессур-досылки
|
||||
|
||||
### Диаграмма relay-потока
|
||||
|
||||
```
|
||||
[Узел A: источник] [Узел B: relay] [Узел C: req]
|
||||
| | |
|
||||
| |<── BLOCK_REQ ────────────|
|
||||
| |── relay downstream add |
|
||||
| |── catchup send (если есть)|
|
||||
| | |
|
||||
|── BLOCK_CHUNK ──────────>| |
|
||||
| |── fwrite (в файл) |
|
||||
| |── fread → BLOCK_CHUNK ──>|
|
||||
| | |
|
||||
|── BLOCK_DONE ───────────>| |
|
||||
| |── fwd BLOCK_DONE ───────>|
|
||||
| |── md_relay_remove |
|
||||
```
|
||||
|
||||
### Жизненный цикл relay_block_ctx
|
||||
|
||||
1. **Создание** — в `md_dl_start_block` при отправке первого BLOCK_REQ
|
||||
2. **Обновление file_offset** — в `media_download_handle_chunk` при каждом чанке
|
||||
3. **Форвардинг downstream** — в `media_download_handle_chunk` после записи
|
||||
4. **Завершение** — в `media_download_handle_done` после валидации: форвард BLOCK_DONE + удаление
|
||||
5. **Отмена** — при `media_download_cancel`: контекст удаляется вместе с `md->relay_blocks`
|
||||
|
||||
### Константы relay
|
||||
|
||||
| Константа | Значение | Описание |
|
||||
|-----------|----------|----------|
|
||||
| `MD_MAX_RELAY_DOWNSTREAM` | 5 | Макс число downstream-узлов на блок |
|
||||
| `MEDIA_RELAY_FULL_HDR_SIZE` | 39 | Размер заголовка RELAY_FULL (1+16+16+4+2) |
|
||||
Регрессии: `tests/test_media_blocks.c` проверяет I/O, реальные подписи с первым байтом
|
||||
ключа 00, отмену/повторный start, поздние пакеты, лимиты и две relay-очереди разной скорости.
|
||||
`tests/test_media_delivery_full.c` проверяет сетевую загрузку усечённого последнего блока.
|
||||
+13
-473
@@ -1,478 +1,18 @@
|
||||
## TCP Proxy Protocol — архитектура (v2, без CONNECTED)
|
||||
# Протокол proxy
|
||||
|
||||
### 1. Протокольный формат (общий для client и server)
|
||||
Актуальное описание TCP/UDP/ICMP, router-метаданных, окон, half-close и проверок:
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md).
|
||||
|
||||
```
|
||||
┌──────┬──────┬───────────────────────┬──────────────────────┐
|
||||
│svc_id│subcmd│ stream_id (4) │ data ... │
|
||||
│ 1B │ 1B │ │ │
|
||||
└──────┴──────┴───────────────────────┴──────────────────────┘
|
||||
HDR = 6 байт
|
||||
TCP ожидает реальный CONNECTED и использует WINDOW; FIN завершает одно направление.
|
||||
Прежние схемы без CONNECTED, с общим TCP service ID и 9-байтовым CLOSE_ALL не соответствуют
|
||||
реализации. Общие определения и контракт proxy_flow находятся в [proxy_protocol.h](proxy_protocol.h).
|
||||
|
||||
subcmd:
|
||||
0x01 CONNECT client→server : dest_ip(4)+dest_port(2)
|
||||
0x03 DATA ↔ bidirectional: payload
|
||||
0x04 CLOSE ↔ bidirectional: (нет данных)
|
||||
0x05 ERROR ↔ bidirectional: (нет данных)
|
||||
```
|
||||
Описание отдельных модулей:
|
||||
|
||||
---
|
||||
- [TCP client](tcp_proxy_client_doc.md) — TUN/lwIP, маршрутизация к exit, приём ответов.
|
||||
- [TCP server](tcp_proxy_server_doc.md) — OS connect, окна, очереди, FIN/CLOSE/ERROR.
|
||||
- [SOCKS и HTTP](socks_proxy_doc.md) — listeners, DNS, HTTP framing/keep-alive/туннели.
|
||||
- [UDP](udp_proxy_doc.md) — per-instance flows и восстановление ответа в TUN.
|
||||
- [ICMP](icmp_proxy_doc.md) — raw socket, сопоставление echo, тестовый fallback.
|
||||
|
||||
### 2. Диаграмма состояний `tcp_proxy_client_conn`
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ ALLOC + │ accept_cb: u_calloc, stream_id++
|
||||
│ CONNECT │ send CONNECT → exit
|
||||
│ отправлен │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ send успешен? │
|
||||
└───────────┬───────────┘
|
||||
Y │ N
|
||||
┌────────┘ └──────┐
|
||||
▼ ▼
|
||||
┌────────────┐ ┌──────────────┐
|
||||
│ ACTIVE │ │ FREE │
|
||||
│ traffic │ └──────────────┘
|
||||
└────┬──┬────┘
|
||||
│ │
|
||||
│ └─────────────────────────────────┐
|
||||
│ │
|
||||
▼ lwIP FIN (p=NULL) ▼ ERROR от lwIP/exit
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ TUN_CLOSED │ │ ERROR │
|
||||
│ tun_closed=1 │ │ error=1 │
|
||||
│ ↓ send_close │ │ send ERROR→ │
|
||||
│ после drain │ └──────┬───────┘
|
||||
│ tx_buf+lwIP │ │
|
||||
└──────┬───────┘ poll_cb: tcp_abort+free
|
||||
│
|
||||
│ recv CLOSE от exit
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ TUN+REM CLOSED │
|
||||
│ tun=1 rem=1 │
|
||||
│ ждём flush буферов│
|
||||
└────────┬──────────┘
|
||||
│ poll_cb: pending=0, unsent=NULL, unacked=NULL
|
||||
│ (или sndbuf==0)
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ CLEANUP │
|
||||
│ free(pc) │
|
||||
└──────────────┘
|
||||
|
||||
|
||||
ДОП. СОСТОЯНИЯ:
|
||||
┌─────────────────┐ send_data в ETCP вернул -1 (send_q полон)
|
||||
│ TX_BACKPRESSURE│ не tcp_recved() → окно lwIP=0 → recv_cb остановлен
|
||||
│ tx_buf != NULL │ etcp_router_on_send_ready() → ждём освобождения
|
||||
│ tx_waiter pend │ waiter_cb: retry send_data → tcp_recved → окно открыто
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐ send_close/send_error вернул -1
|
||||
│ CLOSE_PENDING │ → retry в poll_cb
|
||||
│ close_pending=1│
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Диаграмма состояний `tcp_proxy_server_conn`
|
||||
|
||||
```
|
||||
┌───────────────────┐
|
||||
│ CREATING │ recv CONNECT
|
||||
│ socket() создан │ → неблокирующий connect()
|
||||
│ sock = неблок. │
|
||||
└────────┬──────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ connect() результат? │
|
||||
└────┬──────────┬──────────┘
|
||||
EINPROGRESS│ │ошибка сразу
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│CONNECTING│ │ ERROR │ send ERROR→client
|
||||
│ждём write│ │ FREE │
|
||||
└────┬─────┘ └──────────────┘
|
||||
│ write_cb: SO_ERROR==0
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ CONNECTED │ connected=1
|
||||
│ flush pend │ → flush pending_buf в сокет
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
client→DATA │ │ ERROR (sock/pipe)
|
||||
(sock_send) │ ▼
|
||||
или pending_buf │ ┌──────────────┐
|
||||
│ │ │ ERROR │ send ERROR→client
|
||||
│ │ │ FREE │
|
||||
│ sock EOF (n=0) │ │
|
||||
│ │ └──────────────┘
|
||||
│ ▼
|
||||
│ ┌──────────────────┐
|
||||
│ │ SOCK_CLOSED │ sock_closed=1
|
||||
│ │ out_buf/pause? │ если очереди пусты → send_close
|
||||
│ │ ждём drain │ иначе ждём retry_cb/waiter_cb
|
||||
│ └────────┬─────────┘
|
||||
│ │ очереди опустошены → send_close
|
||||
│ │ если cli_closed → FREE
|
||||
│ │
|
||||
client→CLOSE │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────┐ │
|
||||
│ CLI_CLOSED │ │ cli_closed=1
|
||||
│ out_buf pend? │◄────────┘ если out_buf пуст → shutdown(SHUT_WR)
|
||||
│ ждём flush │ иначе ждём sock_retry_cb → shutdown
|
||||
└──────┬───────┘
|
||||
│
|
||||
└──────────────┐
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ cli && sock │ оба закрыты + очереди пусты
|
||||
│ очереди пусты │
|
||||
└────────┬─────────┘
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ FREE │ conn_free(rc)
|
||||
└──────────────┘
|
||||
|
||||
|
||||
ДОП. СОСТОЯНИЯ:
|
||||
┌─────────────────┐ send_msg в ETCP вернул -1
|
||||
│ PAUSED │ буферизуем в pause_buf, убираем read_id (пауза чтения)
|
||||
│ pause_buf>0 │ etcp_router_on_send_ready() → ждём освобождения
|
||||
│ pause_waiter │ waiter_cb: retry send_msg → resume read_id
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐ CLOSE/ERROR не доставлен через ETCP
|
||||
│ CLOSE_PENDING │ close_timer с backoff 50..5000 tb → ретрай
|
||||
│ close_pending=1│
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. DATA FLOW
|
||||
|
||||
```
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
APP → lwIP → recv_cb → ETCP → handle → socket → DEST
|
||||
APP ← lwIP ← tcp_write ← ETCP ← read_cb ← socket ← DEST
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
|
||||
CLIENT (узел A) SERVER (узел B)
|
||||
|
||||
APP (через TUN) DEST (реальный)
|
||||
│ │
|
||||
▼ TCP SYN/ACK (lwIP) ▼ TCP SYN/ACK (OS)
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ lwIP │ │ socket │
|
||||
│ TCP │ │ (неблок) │
|
||||
└────┬─────┘ └────┬─────┘
|
||||
│ recv_cb │ read_cb
|
||||
│ │
|
||||
┌────▼────────────┐ ┌────▼───────────────┐
|
||||
│ tcp_proxy │ ETCP │ tcp_proxy │
|
||||
│ _client │◄═══════════════════════►│ _server │
|
||||
│ │ │ │
|
||||
│ ┌────────────┐ │ │ ┌──────────────┐ │
|
||||
│ │ recv_cb │ │──CONNECT+DATA──► │ │handle_connect│ │
|
||||
│ │(lwIP→ETCP) │ │ │ │handle_data │ │
|
||||
│ │ │ │ backpressure: │ └──────┬───────┘ │
|
||||
│ │ send fail: │ │ waiter на send_q │ │ sock_send │
|
||||
│ │ tx_buf+ │ │ │ ┌──────▼───────┐ │
|
||||
│ │ waiter │ │ │ │ out_buf │ │
|
||||
│ │ окно lwIP=0│ │ │ │ pending_buf │ │
|
||||
│ └────────────┘ │ │ └─────────────┘ │
|
||||
│ │ │ │ send() │
|
||||
│ ┌────────────┐ │ │ ┌──────▼───────┐ │
|
||||
│ │handle_data │ │◄────DATA──────── │ │ read_cb │ │
|
||||
│ │handle_close│ │ │ │(sock→ETCP) │ │
|
||||
│ └─────┬──────┘ │ │ │ │ │
|
||||
│ │ │ │ │ send fail: │ │
|
||||
│ ┌─────▼──────┐ │ │ │ pause_buf+ │ │
|
||||
│ │ to_lwip │ │ │ │ waiter+ │ │
|
||||
│ │ (очередь) │ │ │ │ пауза read_id│ │
|
||||
│ └─────┬──────┘ │ │ └─────────────┘ │
|
||||
│ │tcp_write│ │ │
|
||||
│ ▼ │ └───────────────────┘
|
||||
│ lwIP→TUN→APP │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. BACKPRESSURE (lwIP→ETCP и socket→ETCP)
|
||||
|
||||
```
|
||||
══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Используется встроенный механизм ll_queue threshold waiter (на router send_q):
|
||||
etcp_router_on_send_ready(inst, group_id, node_id, svc_id, &waiter, cb, arg)
|
||||
→ внутри: queue_waiter_wait(rconn->send_q, &waiter)
|
||||
etcp_router_cancel_send_ready(inst, group_id, node_id, svc_id, &waiter)
|
||||
|
||||
CLIENT (lwIP → ETCP):
|
||||
recv_cb(buf, len):
|
||||
ret = send_data(buf, len)
|
||||
if ret == 0: tcp_recved(len)
|
||||
if ret < 0:
|
||||
tx_buf = copy(buf), tx_len = len
|
||||
НЕ tcp_recved() → окно lwIP = 0, recv_cb больше не вызывается
|
||||
etcp_router_on_send_ready(inst, group_id, via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &tx_waiter, tx_waiter_cb, pc)
|
||||
|
||||
tx_waiter callback:
|
||||
ret = send_data(tx_buf, tx_len)
|
||||
if ret == 0:
|
||||
tcp_recved(tx_len) → окно открыто, lwIP возобновляет recv_cb
|
||||
free(tx_buf)
|
||||
if tun_closed → send_close()
|
||||
// если ret < 0 → waiter остаётся, router вызовет снова
|
||||
|
||||
SERVER (socket → ETCP):
|
||||
read_cb(buf, n):
|
||||
ret = send_msg(DATA, buf, n)
|
||||
if ret == 0: ok
|
||||
if ret < 0:
|
||||
pause_buf = copy(buf), pause_len = n
|
||||
uasync_remove_socket_t(sock) → пауза чтения с сокета
|
||||
etcp_router_on_send_ready(inst, group_id, peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT, &pause_waiter, pause_waiter_cb, rc)
|
||||
|
||||
pause_waiter callback:
|
||||
ret = send_msg(DATA, pause_buf, pause_len)
|
||||
if ret == 0:
|
||||
free(pause_buf)
|
||||
uasync_add_socket_t(sock) → возобновляем чтение
|
||||
if sock_closed → send_close()
|
||||
// если ret < 0 → waiter остаётся
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Последовательность: HANDSHAKE (упрощённый, без CONNECTED)
|
||||
|
||||
```
|
||||
CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET
|
||||
─────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tcp_proxy_client_accept_cb() CONNECT
|
||||
├─ u_calloc(conn) ────────────────────────────→ tcp_proxy_server_handle_connect()
|
||||
├─ stream_id++ ├─ socket(AF_INET,SOCK_STREAM)
|
||||
├─ send_connect() ├─ socket_set_nonblocking()
|
||||
│ [dest_ip(4)+dest_port(2)] ├─ uasync_add_socket_t(read/write/error)
|
||||
├─ conn→conns list ├─ connect(неблок.) → EINPROGRESS
|
||||
│ └─ rc→conns list
|
||||
▼
|
||||
ACTIVE — данные идут СРАЗУ (без ожидания CONNECTED)
|
||||
│
|
||||
│ DATA ...connect завершён...
|
||||
├──────────────────────────────────────────────────→ tcp_proxy_server_sock_write_cb()
|
||||
│ ├─ connected=1
|
||||
│ DATA └─ flush pending_buf в сокет
|
||||
├──────────────────────────────────────────────────→
|
||||
│
|
||||
│ DATA ←─ tcp_proxy_server_sock_read_cb()
|
||||
│◄────────────────────────────────────────────── (socket recv → ETCP)
|
||||
│
|
||||
└──→ трафик идёт в обе стороны
|
||||
|
||||
При ошибке connect:
|
||||
...connect fail... → tcp_proxy_server_send_error() → client получает ERROR, error=1, cleanup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Последовательность: CLOSE (drain очередей → close в конце)
|
||||
|
||||
```
|
||||
CLIENT (lwIP) CLIENT PROXY ETCP SERVER PROXY SOCKET
|
||||
─────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
══ Вариант А: lwIP закрывает первым ══
|
||||
|
||||
tcp_proxy_client_recv_cb(p=NULL)
|
||||
├─ tun_closed=1
|
||||
│ если tx_buf пуст → send_close CLOSE
|
||||
│ если tx_buf pend → waiter досылает ──────────→ tcp_proxy_server_handle_close()
|
||||
│ потом send_close ├─ cli_closed=1
|
||||
└─ │ если out_buf пуст → shutdown(SHUT_WR)
|
||||
│ иначе ждём sock_retry_cb
|
||||
│
|
||||
...out_buf дослан...
|
||||
sock_retry_cb:
|
||||
shutdown(SHUT_WR)
|
||||
│
|
||||
...сокет дочитывает остаток...
|
||||
│
|
||||
tcp_proxy_server_sock_read_cb(n=0)
|
||||
├─ sock_closed=1
|
||||
│ если pause_buf пуст → send_close
|
||||
│ иначе ждём waiter_cb
|
||||
│
|
||||
CLOSE │
|
||||
tcp_proxy_client_handle_close() ◄────────────────────────┘
|
||||
└─ rem_closed=1
|
||||
│
|
||||
poll_cb: tun=1, rem=1, to_lwip пуст, unsent=NULL, unacked=NULL
|
||||
├─ tcp_close(pcb)
|
||||
└─ conn_free()
|
||||
|
||||
|
||||
══ Вариант Б: серверный сокет закрывается первым ══
|
||||
|
||||
tcp_proxy_server_sock_read_cb(n=0)
|
||||
├─ sock_closed=1
|
||||
│ если out_buf+pause_buf пусты → send_close
|
||||
│ иначе ждём retry/waiter
|
||||
CLOSE │
|
||||
tcp_proxy_client_handle_close() ◄────────────────────────┘
|
||||
└─ rem_closed=1
|
||||
│
|
||||
▼
|
||||
...приложение закрывает TCP...
|
||||
│
|
||||
tcp_proxy_client_recv_cb(p=NULL)
|
||||
├─ tun_closed=1
|
||||
│ если tx_buf пуст → send_close
|
||||
└─
|
||||
|
||||
poll_cb: оба закрыты, буферы пусты → cleanup
|
||||
|
||||
|
||||
══ Вариант В: Ошибка / аномалия ══
|
||||
|
||||
ERROR
|
||||
─────────────────────── (или ←) ──────────
|
||||
error=1
|
||||
│
|
||||
▼
|
||||
poll_cb: tcp_abort(pcb) → немедленная очистка
|
||||
или: rc→error=1 → conn_free
|
||||
|
||||
|
||||
══ Вариант Г: conn не найден / closed / error ══
|
||||
|
||||
Любой пришедший пакет (DATA/CLOSE/ERROR) для conn в закрытом состоянии:
|
||||
→ tcp_proxy_*_send_msg(ERROR, stream_id)
|
||||
→ удалённая сторона получает ERROR и немедленно закрывается
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Диспетчеризация в `tcp_proxy_client_etcp_recv_cb()`
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ tcp_proxy_client_etcp_recv_cb() │
|
||||
│ (единая точка входа для обоих направлений) │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ subcmd == CONNECT? │
|
||||
│ └── tcp_proxy_server_handle_connect() ←── всегда сервер │
|
||||
│ │
|
||||
│ stream_id в server.conns? (если server.enabled) │
|
||||
│ ├── DATA → tcp_proxy_server_handle_data() │
|
||||
│ ├── CLOSE → tcp_proxy_server_handle_close() │
|
||||
│ └── ERROR → rc->error=1; conn_free() │
|
||||
│ │
|
||||
│ stream_id в client.conns? (если client proxy != NULL) │
|
||||
│ ├── DATA → tcp_proxy_client_handle_data() │
|
||||
│ ├── CLOSE → tcp_proxy_client_handle_close() │
|
||||
│ └── ERROR → tcp_proxy_client_handle_error() │
|
||||
│ │
|
||||
│ Приоритет: │
|
||||
│ 1. CONNECT всегда интерпретируется сервером │
|
||||
│ 2. Если server.enabled И stream_id найден в server.conns → server │
|
||||
│ 3. Иначе если client proxy != NULL → client │
|
||||
│ 4. Иначе drop │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. INIT
|
||||
|
||||
```
|
||||
CLIENT SERVER
|
||||
─────────────────────────────────────────────────────────
|
||||
|
||||
utun_instance.c
|
||||
│
|
||||
├─ tcp_proxy_client_create() tcp_proxy_server_init()
|
||||
│ ├─ tun_init_nat() (TUN iface) ├─ memset(ctx,0)
|
||||
│ ├─ lwip_tcp_init() (TCP стек) ├─ ctx->enabled = config ...
|
||||
│ ├─ etcp_router_bind( ├─ etcp_router_bind(
|
||||
│ │ ETCP_ID_TCP_PROXY, │ ETCP_ID_TCP_PROXY,
|
||||
│ │ tcp_proxy_client_etcp_recv_cb) │ tcp_proxy_client_etcp_recv_cb)
|
||||
│ ├─ udp_proxy_init() ├─ udp_proxy_init()
|
||||
│ └─ icmp_proxy_init() └─ icmp_proxy_init() (если client не активен)
|
||||
│
|
||||
└─ ОБА слушают ETCP_ID_TCP_PROXY через один обработчик
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. Поля структур
|
||||
|
||||
```
|
||||
tcp_proxy_client_conn:
|
||||
stream_id — уникальный ID потока
|
||||
tun_closed — lwIP/FIN (сторона TUN)
|
||||
rem_closed — CLOSE от exit
|
||||
error — немедленная очистка
|
||||
close_sent — CLOSE/ERROR отправлен
|
||||
close_pending — отправка не удалась, ретрай
|
||||
to_lwip — очередь DATA от exit → lwIP (feed_from_transport)
|
||||
pcb — lwIP tcp_pcb
|
||||
tx_buf — буфер при backpressure lwIP→ETCP (send_data fail)
|
||||
tx_len — длина tx_buf
|
||||
tx_waiter — waiter на router send_q (etcp_router_on_send_ready)
|
||||
|
||||
tcp_proxy_server_conn:
|
||||
stream_id — уникальный ID потока
|
||||
cli_closed — клиент прислал CLOSE
|
||||
sock_closed — сокет получил EOF
|
||||
error — ошибка
|
||||
connected — сокет подключился (connect завершён)
|
||||
close_pending — CLOSE/ERROR не доставлен, ретрай
|
||||
pending_buf — буфер данных от клиента до завершения connect
|
||||
out_buf — буфер отправки в сокет (неблокирующий send, EAGAIN retry)
|
||||
out_off/out_len — offset/размер out_buf
|
||||
out_timer — таймер повтора send в сокет
|
||||
out_backoff — backoff для retry send в сокет
|
||||
pause_buf — буфер при backpressure socket→ETCP (send_msg fail)
|
||||
pause_len — длина pause_buf
|
||||
pause_waiter — waiter на router send_q (etcp_router_on_send_ready)
|
||||
close_timer — таймер повтора CLOSE/ERROR
|
||||
close_backoff — backoff для retry CLOSE/ERROR
|
||||
sock — OS сокет к адресату
|
||||
read_id — handle uasync для событий сокета
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. etcp_router waiter API
|
||||
|
||||
```
|
||||
// etcp_router.h
|
||||
void etcp_router_on_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn cb, void* arg);
|
||||
void etcp_router_cancel_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t node_id, uint8_t svc_id, struct queue_waiter_handle* h);
|
||||
|
||||
// Внутри:
|
||||
// rconn = router_conn_find(inst, group_id, node_id, svc_id)
|
||||
// queue_waiter_wait(rconn->send_q, h, cb, arg)
|
||||
// queue_waiter_cancel(rconn->send_q, h)
|
||||
//
|
||||
// send_q — очередь router conn при полном inflight
|
||||
// (threshold = ROUTER_MAX_SEND_Q_PACKETS - 1 в router_conn_reset)
|
||||
```
|
||||
Контракты функций, буферов и полей HTTP codec — в [http_proxy.h](http_proxy.h).
|
||||
+49
-35
@@ -1,7 +1,11 @@
|
||||
/* Потоковый HTTP/1.x codec локального proxy. Не владеет сокетами/DNS/ETCP.
|
||||
* Заголовки проверяются целиком до пересылки; тело передаётся ограниченными порциями.
|
||||
* emit получает исходное число байт для возврата ETCP credit после доставки результата.
|
||||
* Возврат feed: 0=нужно продолжение, 1=сообщение завершено, <0=HTTP error status. */
|
||||
* Для нового сообщения обнулить message/request, затем headers_feed → request/response_parse → body_feed.
|
||||
* parse меняет headers и сохраняет в fields указатели внутрь него: message нельзя копировать после parse.
|
||||
* emit вызывается синхронно, буфер заимствован только на время вызова; source_len нужен для возврата
|
||||
* ETCP credit после доставки результата. Ошибку feed/parse нельзя продолжать как успешное сообщение.
|
||||
* Возврат feed: 0=нужно продолжение, 1=заголовки/тело завершены, <0=отрицательный HTTP status.
|
||||
* HTTP_BODY_EOF завершает владелец по FIN, отдельной функции EOF у codec нет. */
|
||||
#ifndef HTTP_PROXY_H
|
||||
#define HTTP_PROXY_H
|
||||
|
||||
@@ -12,54 +16,64 @@
|
||||
#define HTTP_PROXY_HEADER_LIMIT 32768
|
||||
#define HTTP_PROXY_FIELDS_LIMIT 512
|
||||
|
||||
// Результат: >=0 = принято целиком, <0 = ошибка вывода (codec вернёт -500); частичный приём не поддержан.
|
||||
// source_len может отличаться от len; len=0/data=NULL допустимы при удалении исходных байтов.
|
||||
typedef int (*http_proxy_emit)(void* arg, const uint8_t* data, size_t len, size_t source_len);
|
||||
|
||||
struct http_proxy_field { char* name; char* value; };
|
||||
struct http_proxy_field { char* name; char* value; }; // Заимствованные NUL-строки внутри message.headers.
|
||||
// NONE — исходное нулевое состояние; EOF — тело до внешнего FIN; DONE — точная граница сообщения.
|
||||
enum http_proxy_body_state { HTTP_BODY_NONE, HTTP_BODY_LENGTH, HTTP_BODY_EOF, HTTP_BODY_CHUNK_SIZE,
|
||||
HTTP_BODY_CHUNK_DATA, HTTP_BODY_CHUNK_CRLF, HTTP_BODY_TRAILERS, HTTP_BODY_DONE };
|
||||
|
||||
struct http_proxy_message {
|
||||
char headers[HTTP_PROXY_HEADER_LIMIT + 1];
|
||||
size_t header_len;
|
||||
size_t first_line_len;
|
||||
struct http_proxy_field fields[HTTP_PROXY_FIELDS_LIMIT];
|
||||
unsigned field_count;
|
||||
uint8_t headers_done;
|
||||
uint8_t minor;
|
||||
uint8_t close;
|
||||
uint8_t chunked;
|
||||
uint8_t has_length;
|
||||
uint8_t upgrade;
|
||||
uint8_t decode_chunks;
|
||||
unsigned status;
|
||||
uint64_t length;
|
||||
enum http_proxy_body_state body;
|
||||
uint64_t remaining;
|
||||
char line[HTTP_PROXY_LINE_LIMIT + 1];
|
||||
size_t line_len;
|
||||
size_t trailer_len;
|
||||
unsigned crlf_pos;
|
||||
char headers[HTTP_PROXY_HEADER_LIMIT + 1]; // До parse — CRLF-блок, после — строки с NUL-разделителями.
|
||||
size_t header_len; // Число исходных байтов, включая CRLFCRLF.
|
||||
size_t first_line_len; // Длина стартовой строки с CRLF.
|
||||
struct http_proxy_field fields[HTTP_PROXY_FIELDS_LIMIT]; // Разобранные поля собственного headers.
|
||||
unsigned field_count; // Число заполненных fields.
|
||||
uint8_t headers_done; // headers_feed получил полный CRLFCRLF.
|
||||
uint8_t minor; // Версия исходного HTTP: 0 или 1.
|
||||
uint8_t close; // Запрос: Connection close; ответ: решение закрыть frontend.
|
||||
uint8_t chunked; // Единственный поддержанный Transfer-Encoding — chunked.
|
||||
uint8_t has_length; // Присутствует проверенный Content-Length (включая нулевой).
|
||||
uint8_t upgrade; // В Connection найден token upgrade.
|
||||
uint8_t decode_chunks; // Убрать framing chunked для ответа HTTP/1.0.
|
||||
unsigned status; // HTTP status ответа; у запроса 0.
|
||||
uint64_t length; // Content-Length; отсутствие различает has_length.
|
||||
enum http_proxy_body_state body; // Состояние потокового разбора тела.
|
||||
uint64_t remaining; // Остаток Content-Length или текущего chunk.
|
||||
char line[HTTP_PROXY_LINE_LIMIT + 1]; // Неполная строка размера chunk или trailer.
|
||||
size_t line_len; // Сколько байт накоплено в line.
|
||||
size_t trailer_len; // Суммарный размер trailers, лимит HEADER_LIMIT.
|
||||
unsigned crlf_pos; // 0/1: сколько байт CRLF после chunk уже принято.
|
||||
};
|
||||
|
||||
struct http_proxy_request {
|
||||
char host[256];
|
||||
char authority[264];
|
||||
char upgrade[HTTP_PROXY_LINE_LIMIT + 1];
|
||||
uint16_t port;
|
||||
uint8_t connect;
|
||||
uint8_t head;
|
||||
uint8_t close;
|
||||
uint8_t minor;
|
||||
char host[256]; // IPv4 literal или DNS-имя; IPv6 отклоняется с -502.
|
||||
char authority[264]; // Нормализованный host:port для upstream Host.
|
||||
char upgrade[HTTP_PROXY_LINE_LIMIT + 1]; // Предложенные protocol[/version], пусто без Upgrade.
|
||||
uint16_t port; // Host byte order; HTTP default=80, CONNECT требует порт.
|
||||
uint8_t connect; // Метод CONNECT: после успеха используется сырой туннель.
|
||||
uint8_t head; // Метод HEAD: у ответа нет тела.
|
||||
uint8_t close; // Закрыть frontend: HTTP/1.0 или Connection close.
|
||||
uint8_t minor; // Версия клиента, определяет сериализацию ответа.
|
||||
};
|
||||
|
||||
// Накапливает только заголовки; хвост входа остаётся у вызывающей стороны.
|
||||
// Накапливает заголовки из заимствованного data; *used = число принятых байтов, хвост остаётся вызывающему.
|
||||
// m первоначально обнулён; 1 = CRLFCRLF получен, 0 = ждать продолжения, <0 = -400/-414/-431.
|
||||
int http_proxy_headers_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used);
|
||||
// Проверяет запрос и отправляет реконструированные заголовки через emit (кроме CONNECT).
|
||||
// Однократно после headers_feed==1: проверяет запрос, заполняет обнулённый r и body в m.
|
||||
// Обычный запрос требует absolute http:// URL; emit получает origin-form HTTP/1.1 headers.
|
||||
// CONNECT проверяет authority и не вызывает emit. 0 = успех, <0 = отрицательный HTTP status.
|
||||
int http_proxy_request_parse(struct http_proxy_message* m, struct http_proxy_request* r, http_proxy_emit emit, void* arg);
|
||||
// Проверяет ответ, учитывает HEAD/Upgrade и формирует собственные connection headers.
|
||||
// Однократно после headers_feed==1; r — исходный запрос, close_client — решение закрыть frontend.
|
||||
// Проверяет HEAD/Upgrade, задаёт body, выводит headers в версии клиента. 0 = успех, <0 = -502/-500.
|
||||
// Для следующего промежуточного ответа владелец обнуляет m; r сохраняется до final response.
|
||||
int http_proxy_response_parse(struct http_proxy_message* m, const struct http_proxy_request* r,
|
||||
int close_client, http_proxy_emit emit, void* arg);
|
||||
// Определяет точную границу тела, фильтрует trailers; поддерживает произвольные TCP-разбиения.
|
||||
// После parse: передаёт тело из data через emit, *used = принятые байты до точной границы тела.
|
||||
// Фильтрует trailers; 1 = DONE, 0 = продолжить, <0 = отрицательный HTTP status.
|
||||
// Для HTTP_BODY_EOF возвращает 0, завершение по FIN проверяет владелец. Вывод не откатывается при ошибке.
|
||||
int http_proxy_body_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used,
|
||||
http_proxy_emit emit, void* arg);
|
||||
|
||||
|
||||
+81
-34
@@ -10,16 +10,15 @@
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/ll_queue.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <openssl/rand.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <netinet/ip_icmp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#else
|
||||
#define ICMP_MINLEN 8
|
||||
@@ -98,9 +97,22 @@ static int exit_send_echo(struct UTUN_INSTANCE* inst, uint64_t client_node_id,
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto dst=0x%08x id=0x%04x seq=%u len=%zu",
|
||||
dst_ip, echo_id, echo_seq, icmp_len);
|
||||
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_addr = {.s_addr = dst_ip}};
|
||||
ssize_t n = sendto(ctx->raw_sock, buf, icmp_len, 0, (struct sockaddr*)&addr, sizeof(addr));
|
||||
if (!ctx->expire_timer) {
|
||||
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->request_timeout_tb, ctx, req_expire_timer_cb, "icmp_expire");
|
||||
if (!ctx->expire_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: cannot reserve expiry timer peer=%016llx", (unsigned long long)client_node_id);
|
||||
u_free(buf); u_free(r); return -1;
|
||||
}
|
||||
}
|
||||
ssize_t n;
|
||||
do { n = sendto(ctx->raw_sock, buf, icmp_len, 0, (struct sockaddr*)&addr, sizeof(addr)); } while (n < 0 && errno == EINTR);
|
||||
u_free(buf);
|
||||
if (n < 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto failed: %s", strerror(errno)); u_free(r); return -1; }
|
||||
if (n != (ssize_t)icmp_len) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto failed dst=%08x sent=%zd expected=%zu error=%d",
|
||||
dst_ip, n, icmp_len, n < 0 ? socket_get_error() : 0);
|
||||
if (!ctx->pending) { uasync_cancel_timeout(ctx->ua, ctx->expire_timer); ctx->expire_timer = NULL; }
|
||||
u_free(r); return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto sent %zd bytes", n);
|
||||
|
||||
r->client_node_id = client_node_id; r->dst_ip = dst_ip; r->orig_src_ip = orig_src_ip;
|
||||
@@ -109,8 +121,6 @@ static int exit_send_echo(struct UTUN_INSTANCE* inst, uint64_t client_node_id,
|
||||
if (payload_len > 0) memcpy(r->payload, payload, r->payload_len);
|
||||
r->sent_tb = get_time_tb();
|
||||
r->next = ctx->pending; ctx->pending = r;
|
||||
if (!ctx->expire_timer)
|
||||
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->request_timeout_tb, ctx, req_expire_timer_cb, "icmp_expire");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -181,9 +191,17 @@ static void exit_handle_request(struct ETCP_CONN* conn, struct ll_entry* entry)
|
||||
uint8_t* payload = entry->dgram + ICMP_PROXY_RECV_HDR_SIZE;
|
||||
size_t payload_len = entry->len - ICMP_PROXY_RECV_HDR_SIZE;
|
||||
|
||||
if (ctx->raw_sock != SOCKET_INVALID) {
|
||||
if (ctx->test_loopback) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "icmp_proxy: explicit test echo peer=%016llx", (unsigned long long)client_node_id);
|
||||
} else if (ctx->raw_sock != SOCKET_INVALID) {
|
||||
exit_send_echo(inst, client_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, payload_len);
|
||||
} else if (ctx->test_loopback) {
|
||||
goto drop;
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: exit unavailable peer=%016llx dst=%08x",
|
||||
(unsigned long long)client_node_id, dst_ip);
|
||||
goto drop;
|
||||
}
|
||||
{
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: test loopback reply to 0x%08x", dst_ip);
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (e) {
|
||||
@@ -197,11 +215,10 @@ static void exit_handle_request(struct ETCP_CONN* conn, struct ll_entry* entry)
|
||||
memcpy(e->dgram + 12, &echo_seq, 2);
|
||||
if (payload_len > 0) memcpy(e->dgram + ICMP_PROXY_HDR_SIZE, payload, payload_len);
|
||||
e->len = ICMP_PROXY_HDR_SIZE + payload_len;
|
||||
etcp_route_send(inst, TOPO_GROUP_UTUN, client_node_id, e, 0, 0);
|
||||
} else queue_entry_free(e);
|
||||
}
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: no raw socket, dropping echo request to 0x%08x", dst_ip);
|
||||
if (etcp_route_send(inst, TOPO_GROUP_UTUN, client_node_id, e, 0, 0) < 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: test reply router send failed");
|
||||
} else { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: test reply allocation failed"); queue_entry_free(e); }
|
||||
} else DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: test reply entry allocation failed");
|
||||
}
|
||||
|
||||
drop:
|
||||
@@ -321,15 +338,27 @@ int icmp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
|
||||
void icmp_proxy_set_test_loopback(struct UTUN_INSTANCE* inst, int enabled) {
|
||||
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
|
||||
if (ctx) ctx->test_loopback = enabled;
|
||||
if (!ctx) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: test mode requires initialized context"); return; }
|
||||
if (ctx->pending) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: cannot change test mode with pending requests"); return; }
|
||||
if (enabled && ctx->raw_sock != SOCKET_INVALID) {
|
||||
uasync_remove_socket_t(ctx->ua, ctx->raw_sock); ctx->raw_read_id = NULL;
|
||||
socket_close_wrapper(ctx->raw_sock); ctx->raw_sock = SOCKET_INVALID;
|
||||
}
|
||||
ctx->test_loopback = enabled != 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: explicit test loopback=%d", ctx->test_loopback);
|
||||
}
|
||||
|
||||
static void req_expire_timer_cb(void* arg) {
|
||||
struct icmp_proxy_ctx* ctx = (struct icmp_proxy_ctx*)arg;
|
||||
req_expire(ctx);
|
||||
ctx->expire_timer = NULL;
|
||||
if (ctx->pending)
|
||||
if (ctx->pending) {
|
||||
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->request_timeout_tb, ctx, req_expire_timer_cb, "icmp_expire");
|
||||
if (!ctx->expire_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: expiry timer failed, dropping all pending requests");
|
||||
while (ctx->pending) { struct icmp_request* r = ctx->pending; ctx->pending = r->next; u_free(r); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void req_expire(struct icmp_proxy_ctx* ctx) {
|
||||
@@ -345,35 +374,53 @@ static void req_expire(struct icmp_proxy_ctx* ctx) {
|
||||
// Публичные init/destroy
|
||||
// ====================================================================
|
||||
int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
|
||||
if (!inst) return -1;
|
||||
if (inst->icmp_proxy) return 0;
|
||||
if (!inst || !ua) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: invalid init inst=%p ua=%p", (void*)inst, (void*)ua); return -EINVAL; }
|
||||
if (inst->icmp_proxy) {
|
||||
if (inst->icmp_proxy->ua == ua) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: init attempted with another event loop"); return -EINVAL;
|
||||
}
|
||||
if (inst->router_bindings.callbacks[ETCP_RT_ID_ICMP_PROXY]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: service already belongs to another owner"); return -EINVAL;
|
||||
}
|
||||
struct icmp_proxy_ctx* ctx = u_calloc(1, sizeof(struct icmp_proxy_ctx));
|
||||
if (!ctx) return -1;
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: context allocation failed"); return -ENOMEM; }
|
||||
ctx->inst = inst; ctx->ua = ua; ctx->raw_sock = SOCKET_INVALID;
|
||||
ctx->request_timeout_tb = ICMP_TIMEOUT_TB;
|
||||
ctx->is_exit = inst->tcp_proxy_server.enabled;
|
||||
ctx->test_loopback = 0;
|
||||
ctx->expire_timer = NULL;
|
||||
if (RAND_bytes((unsigned char*)&ctx->next_token, sizeof(ctx->next_token)) != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: token initialization failed"); u_free(ctx); return -1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: token initialization failed"); u_free(ctx); return -EIO;
|
||||
}
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_ICMP_PROXY, icmp_proxy_recv_cb) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: router binding failed"); u_free(ctx); return -EIO;
|
||||
}
|
||||
inst->icmp_proxy = ctx;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: shared context ready node=%016llx", (unsigned long long)inst->node_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ctx->is_exit) {
|
||||
ctx->raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
||||
if (ctx->raw_sock == SOCKET_INVALID)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw socket(SOCK_RAW) failed: %s", strerror(errno));
|
||||
else {
|
||||
socket_set_nonblocking(ctx->raw_sock);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw socket created fd=%d", ctx->raw_sock);
|
||||
ctx->raw_read_id = uasync_add_socket_t(ua, ctx->raw_sock, raw_read_cb, NULL, NULL, "icmp_raw", ctx);
|
||||
if (!ctx->raw_read_id) { socket_close_wrapper(ctx->raw_sock); ctx->raw_sock = SOCKET_INVALID; }
|
||||
}
|
||||
// Raw exit публикуется только после регистрации; отказ не затрагивает клиентский binding.
|
||||
int icmp_proxy_enable_exit(struct UTUN_INSTANCE* inst) {
|
||||
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
|
||||
if (!ctx || !inst->tcp_proxy_server.enabled) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: exit requires context/server"); return -EINVAL; }
|
||||
if (ctx->test_loopback || ctx->raw_sock != SOCKET_INVALID) return 0;
|
||||
socket_t sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
||||
if (sock == SOCKET_INVALID) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw socket unavailable error=%d (%s)", err, socket_strerror(err));
|
||||
return -(err ? err : EIO);
|
||||
}
|
||||
|
||||
etcp_router_bind(inst, ETCP_RT_ID_ICMP_PROXY, icmp_proxy_recv_cb);
|
||||
ctx->initialized = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy initialized (exit=%d raw_sock=%d)", ctx->is_exit, (int)ctx->raw_sock);
|
||||
if (socket_set_nonblocking(sock) < 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw nonblocking failed error=%d", err);
|
||||
socket_close_wrapper(sock); return -(err ? err : EIO);
|
||||
}
|
||||
void* read_id = uasync_add_socket_t(ctx->ua, sock, raw_read_cb, NULL, NULL, "icmp_raw", ctx);
|
||||
if (!read_id) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw read registration failed"); socket_close_wrapper(sock); return -ENOMEM;
|
||||
}
|
||||
ctx->raw_sock = sock; ctx->raw_read_id = read_id;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw exit ready fd=%d", (int)sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+42
-28
@@ -1,7 +1,9 @@
|
||||
/* IPv4 ICMP echo через UTUN-router: exit отправляет ping через raw socket, сопоставляет reply
|
||||
* по собственному wire id/seq и восстанавливает исходные id/seq клиента. Запросы ограничены таймаутом.
|
||||
* init/destroy и callbacks — в uasync; контекст принадлежит inst, роль берётся из tcp_proxy_server.enabled.
|
||||
* test_loopback возвращает виртуальные ответы без сетевой отправки и предназначен для тестов. */
|
||||
* по wire id/seq и IP цели, восстанавливая исходные id/seq клиента. Запросы ограничены таймаутом.
|
||||
* init/destroy и callbacks — в uasync; inst владеет контекстом в inst->icmp_proxy.
|
||||
* init регистрирует общий контекст; enable_exit отдельно открывает raw socket. recv_cb проверяет server.enabled для REQUEST
|
||||
* и via_node_id клиентского proxy для REPLY. IP/id/seq в API — network byte order.
|
||||
* test_loopback явно переключает тестовый exit на виртуальные ответы, закрывая raw socket без pending-запросов. */
|
||||
#ifndef ICMP_PROXY_H
|
||||
#define ICMP_PROXY_H
|
||||
|
||||
@@ -23,59 +25,71 @@ struct ETCP_CONN;
|
||||
#define ICMP_PROXY_SUBCMD_REQUEST 0x01 // client→exit: пингани dst_ip
|
||||
#define ICMP_PROXY_SUBCMD_REPLY 0x02 // exit→client: echo reply
|
||||
|
||||
// Заголовок сообщения на отправке (включая байт svc_id; src/dst node_id добавляет роутер):
|
||||
// Сообщение для etcp_route_send (не полный wire packet; метаданные приёма добавляет router):
|
||||
// svc_id(1) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2) + payload
|
||||
#define ICMP_PROXY_HDR_SIZE 14
|
||||
// Полный заголовок на приёме (после router_deliver):
|
||||
// svc_id(1) + src_node_id(8) + dst_node_id(8) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2)
|
||||
#define ICMP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 30
|
||||
// router header(26: svc/src/dst/flags/group) + subcmd(1) + dst_ip(4) + orig_src_ip(4) + icmp_id(2) + icmp_seq(2)
|
||||
#define ICMP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 39
|
||||
|
||||
// Активный ICMP echo запрос (сторона exit узла)
|
||||
struct icmp_request {
|
||||
struct icmp_request* next;
|
||||
uint64_t client_node_id;
|
||||
uint32_t dst_ip;
|
||||
struct icmp_request* next; // следующий ожидающий запрос exit
|
||||
uint64_t client_node_id; // ingress-узел для обратной доставки
|
||||
uint32_t dst_ip; // IP цели ping, network byte order
|
||||
uint32_t orig_src_ip; // IP исходного отправителя (чтобы вернуть reply правильному адресату)
|
||||
uint16_t echo_id;
|
||||
uint16_t echo_seq;
|
||||
uint16_t wire_id;
|
||||
uint16_t wire_seq;
|
||||
uint8_t payload[1500];
|
||||
size_t payload_len;
|
||||
uint64_t sent_tb;
|
||||
socket_t sock; // raw сокет (только если per-request)
|
||||
uint16_t echo_id; // исходный id приложения, network byte order
|
||||
uint16_t echo_seq; // исходный seq приложения, network byte order
|
||||
uint16_t wire_id; // заменённый id в raw ICMP, network byte order
|
||||
uint16_t wire_seq; // заменённый seq, уникален вместе с wire_id и dst_ip среди pending
|
||||
uint8_t payload[1500]; // сохранённый префикс запроса, не используется для сборки ответа
|
||||
size_t payload_len; // длина сохранённого префикса, не больше sizeof(payload)
|
||||
uint64_t sent_tb; // время отправки, tb = 0.1 мс
|
||||
socket_t sock; // не используется; запросы идут через общий ctx->raw_sock
|
||||
};
|
||||
|
||||
struct icmp_proxy_ctx {
|
||||
int initialized;
|
||||
int is_exit;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct UASYNC* ua;
|
||||
struct UTUN_INSTANCE* inst; // заимствованный instance-владелец
|
||||
struct UASYNC* ua; // заимствованный event loop
|
||||
socket_t raw_sock; // один SOCK_RAW для всего ICMP на exit
|
||||
void* raw_read_id;
|
||||
struct icmp_request* pending;
|
||||
uint32_t next_token;
|
||||
uint64_t request_timeout_tb;
|
||||
int test_loopback; // 1 = виртуальный loopback без raw сокета (только тесты)
|
||||
void* raw_read_id; // регистрация raw_sock в ua
|
||||
struct icmp_request* pending; // собственные запросы, ожидающие ответа
|
||||
uint32_t next_token; // генератор wire id/seq со случайным начальным значением
|
||||
uint64_t request_timeout_tb; // период проверки и порог ожидания, default=50000 (5 с)
|
||||
int test_loopback; // явный виртуальный exit, только для тестов
|
||||
void* expire_timer; // периодический таймер очистки истёкших запросов
|
||||
};
|
||||
|
||||
|
||||
// Создать контекст и bind ICMP service; повторный init не меняет существующий контекст и даёт 0.
|
||||
// inst/ua живут до destroy. 0 = контекст и binding готовы, <0 = ошибка без публикации нового контекста.
|
||||
// Контекстом владеет instance; TCP client/server его не уничтожают. Raw socket здесь не создаётся.
|
||||
int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua);
|
||||
// Запустить raw exit существующего контекста. 0 = готов (включая явный test_loopback), <0 = -errno.
|
||||
// Отказ сохраняет клиентскую сторону и прежнее состояние; политику отсутствия прав выбирает instance.
|
||||
int icmp_proxy_enable_exit(struct UTUN_INSTANCE* inst);
|
||||
// Снять binding, отменить таймер, закрыть raw socket, удалить pending и обнулить inst->icmp_proxy; NULL допустим.
|
||||
void icmp_proxy_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Единый etcp_router коллбэк
|
||||
// Router callback: conn->instance задаёт владельца; REQUEST требует server.enabled, REPLY — настроенного exit.
|
||||
// Принимает владение entry/dgram при любом исходе, в том числе при отклонении пакета.
|
||||
void icmp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
|
||||
// Сторона клиента: принять IP/ICMP echo request с TUN, отправить через etcp
|
||||
// Скопировать payload (0..65507) в REQUEST к exit в UTUN, force=0/mode=0; не требует локального контекста.
|
||||
// dst_ip — цель ping, orig_src_ip — адрес приложения; payload заимствован, при длине>0 не NULL.
|
||||
// 0 = принято router, <0 = ошибка; это не подтверждение raw send или получения echo reply.
|
||||
int icmp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id,
|
||||
uint32_t dst_ip, uint32_t orig_src_ip, uint16_t echo_id, uint16_t echo_seq,
|
||||
const uint8_t* payload, size_t payload_len);
|
||||
|
||||
// Собрать Echo Reply с IPv4 src_ip→dst_ip и исходными id/seq; пересчитать обе checksum.
|
||||
// Требует inst->tcp_proxy_client->tun, payload заимствован (0..65506); 0 = в tun input_queue, -1 = ошибка.
|
||||
int icmp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
uint32_t dst_ip, uint32_t src_ip, uint16_t echo_id, uint16_t echo_seq,
|
||||
const uint8_t* payload, size_t payload_len);
|
||||
|
||||
// Явно включить тестовый loopback, закрыв raw socket; при pending-запросах изменение отклоняется с WARN.
|
||||
// Без контекста ничего не делает; выключение loopback само по себе raw socket не открывает.
|
||||
void icmp_proxy_set_test_loopback(struct UTUN_INSTANCE* inst, int enabled);
|
||||
|
||||
|
||||
|
||||
+55
-65
@@ -1,77 +1,67 @@
|
||||
# ICMP Proxy (icmp_proxy)
|
||||
# ICMP proxy
|
||||
|
||||
## 1. Назначение
|
||||
Проксирование ICMP Echo (ping) между клиентским и exit-узлом по протоколу ETCP. Клиент
|
||||
принимает ICMP Echo Request с TUN-интерфейса, упаковывает запрос и отправляет через
|
||||
`etcp_route_send` на exit. Exit-узел через нативный `SOCK_RAW`-сокет отправляет ICMP Echo
|
||||
во внешнюю сеть, сопоставляет ответы по `echo_id`/`echo_seq` и возвращает клиенту. Клиент
|
||||
восстанавливает IP/ICMP Echo Reply и доставляет в TUN.
|
||||
`icmp_proxy.c/h` передаёт IPv4 Echo Request/Reply через `ETCP_RT_ID_ICMP_PROXY`
|
||||
в `TOPO_GROUP_UTUN`. Общий формат router описан в
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракты API — в
|
||||
[icmp_proxy.h](icmp_proxy.h). Другие типы ICMP модуль не проксирует.
|
||||
|
||||
Один экземпляр контекста на процесс (глобальный `g_icmp_ctx`). Режим определяется флагом
|
||||
`inst->tcp_proxy_server.enabled`. Для тестов предусмотрен loopback-режим без raw-сокета.
|
||||
## Контекст и raw socket
|
||||
|
||||
## 2. Как пользоваться
|
||||
Каждый `UTUN_INSTANCE` владеет отдельным `inst->icmp_proxy`, глобального контекста нет.
|
||||
`init(inst, ua)` задаёт таймаут 50000 tb (5 с), случайное начало `next_token` и
|
||||
регистрирует общий binding. Raw socket отдельно запускает `icmp_proxy_enable_exit(inst)`:
|
||||
один неблокирующий `AF_INET/SOCK_RAW/IPPROTO_ICMP` регистрируется в uasync до публикации.
|
||||
|
||||
### Инициализация
|
||||
```c
|
||||
icmp_proxy_init(inst, ua); // exit создаёт SOCK_RAW, клиент — нет
|
||||
```
|
||||
При инициализации регистрируется коллбэк `icmp_proxy_recv_cb` в `etcp_router` для сервиса
|
||||
`ETCP_RT_ID_ICMP_PROXY`. На exit создаётся один `SOCK_RAW(IPPROTO_ICMP)` на весь модуль.
|
||||
Повторный init сохраняет существующий контекст и возвращает 0. Ошибка создания
|
||||
контекста/случайного token/binding возвращает отрицательный код без публикации контекста.
|
||||
`enable_exit` возвращает -errno при отказе raw socket/nonblocking/регистрации, сохраняя
|
||||
общий контекст и приём клиентских REPLY. Instance трактует отсутствие прав/поддержки raw
|
||||
как отключение только ICMP exit с WARN; отказ обязательного ресурса прерывает создание instance.
|
||||
TCP client/server не создают и не уничтожают общий контекст.
|
||||
|
||||
### Клиент: отправка ping-запроса
|
||||
```c
|
||||
icmp_proxy_send_to_exit(inst, exit_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, len);
|
||||
```
|
||||
Вызывается из обработчика TUN-трафика. Запрос упаковывается и отправляется через `etcp_route_send`.
|
||||
Ответ приходит асинхронно в `icmp_proxy_recv_cb` (REPLY), где вызывается `icmp_proxy_deliver_reply`
|
||||
для сборки IP/ICMP Echo Reply и записи в TUN.
|
||||
`test_loopback` по умолчанию выключен. `icmp_proxy_set_test_loopback(inst, 1)` разрешает
|
||||
виртуальный REPLY явно, закрывая ранее открытый raw socket. Переключение при pending-запросах
|
||||
отклоняется с WARN; без контекста setter ничего не делает. Production fallback без сети отсутствует.
|
||||
|
||||
### Тестовый loopback
|
||||
```c
|
||||
icmp_proxy_set_test_loopback(inst, 1); // exit отвечает эмуляцией без raw-сокета
|
||||
```
|
||||
В этом режиме exit не отправляет реальный ICMP Echo, а сразу возвращает REPLY клиенту — полезно
|
||||
для тестирования прохождения пакетов через ETCP без необходимости в SOCK_RAW и внешней сети.
|
||||
## Запрос и сопоставление ответа
|
||||
|
||||
### Очистка
|
||||
```c
|
||||
icmp_proxy_destroy(inst); // закрывает raw-сокет, удаляет pending-запросы, unbind
|
||||
```
|
||||
REQUEST принимается при текущем `server.enabled`. Exit заменяет исходные echo id/seq
|
||||
32-битным возрастающим token: старшие 16 бит — wire_id, младшие — wire_seq (в сетевом порядке).
|
||||
При совпадении `wire_id/wire_seq/dst_ip` с pending выбирает следующий token.
|
||||
Сопоставление ответа использует эту тройку; исходные id/seq и адрес приложения хранятся
|
||||
в `icmp_request`. Разные клиенты с одинаковыми исходными id/seq не смешиваются.
|
||||
|
||||
### Нюансы
|
||||
- Запросы истекают через 5 секунд (`ICMP_TIMEOUT_TB`). Таймер `expire_timer` запускается
|
||||
при добавлении первого запроса и перезапускается после каждой очистки.
|
||||
- Неопознанные echo-ответы (не найденные в pending) логируются как `DEBUG_WARN` и игнорируются.
|
||||
- Контрольная сумма ICMP пересчитывается на exit (при отправке) и на клиенте (при сборке ответа).
|
||||
- Поле `orig_src_ip` в протоколе нужно для восстановления исходного dst_ip на клиенте.
|
||||
Raw reader проверяет IPv4 IHL/протокол, Echo Reply type/code и ICMP checksum, ищет pending
|
||||
по wire id/seq и адресу отправителя, удаляет найденный запрос и отправляет REPLY ingress-узлу.
|
||||
Данные ответа берутся из raw пакета. Сохранённый в request префикс payload до 1500 байт
|
||||
для формирования ответа не используется; он не ограничивает длину реального echo payload.
|
||||
|
||||
## 3. API
|
||||
Таймер резервируется до первого raw send, затем продлевается каждые 5 с, пока pending
|
||||
не пуст. Удаляет записи с возрастом **больше** 5 с на очередном обходе; удаление не происходит
|
||||
точно в момент достижения порога. `destroy` снимает binding, отменяет таймер, закрывает
|
||||
raw socket/регистрацию, освобождает pending и обнуляет `inst->icmp_proxy`.
|
||||
Все действия — в uasync; `inst` и `ua` заимствуются до destroy.
|
||||
Отказ таймера до отправки отклоняет запрос; отказ продления освобождает все pending с ERROR.
|
||||
Запрос без действующего таймера в pending не публикуется.
|
||||
|
||||
### Структуры
|
||||
| Структура | Назначение |
|
||||
|-----------|-----------|
|
||||
| `struct icmp_request` | Состояние активного echo-запроса на exit: client_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, время отправки. Связный список. |
|
||||
| `struct icmp_proxy_ctx` | Глобальный контекст: is_exit, inst, ua, raw_sock, raw_read_id, список pending, таймаут запросов, флаг test_loopback, таймер очистки. |
|
||||
## Сообщения и доставка
|
||||
|
||||
### Формат пакета
|
||||
| Смещение | Размер | Поле |
|
||||
|----------|--------|------|
|
||||
| 0 | 1 | `svc_id` = `ETCP_RT_ID_ICMP_PROXY` |
|
||||
| 1 | 1 | `subcmd` = REQUEST (0x01) / REPLY (0x02) |
|
||||
| 2 | 8 | `sender_node_id` |
|
||||
| 10 | 4 | `dst_ip` (IP цели для ping) |
|
||||
| 14 | 4 | `orig_src_ip` (IP исходного отправителя для обратной доставки) |
|
||||
| 18 | 2 | `echo_id` (icmp_id) |
|
||||
| 20 | 2 | `echo_seq` (icmp_seq) |
|
||||
| 22 | N | `payload` (данные ICMP echo) |
|
||||
Вход `etcp_route_send`:
|
||||
`svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`.
|
||||
REQUEST=1, REPLY=2, заголовок отправки — 14 байт. В REPLY `dst_ip` остаётся адресом
|
||||
цели ping, `orig_src_ip` — адресом приложения. Router callback получает
|
||||
26-байтовые метаданные перед cmd, итого заголовок приёма — 39 байт.
|
||||
Peer находится в метаданных router; отдельного sender ID в ICMP payload нет.
|
||||
IP/id/seq передаются в network byte order.
|
||||
|
||||
### Функции
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `icmp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, на exit создаёт SOCK_RAW и регистрирует его в uasync. Регистрирует коллбэк в etcp_router. |
|
||||
| `icmp_proxy_destroy(inst)` | Закрывает raw-сокет, отменяет таймер, освобождает pending, отвязывает коллбэк, освобождает память. |
|
||||
| `icmp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет REQUEST → `exit_handle_request`, REPLY → `client_handle_reply`. |
|
||||
| `icmp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает ICMP Echo Request и отправляет через etcp_route_send на exit. |
|
||||
| `icmp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/ICMP Echo Reply-пакет и помещает в TUN input_queue. |
|
||||
| `icmp_proxy_set_test_loopback(inst, enabled)` | Включает/выключает тестовый loopback-режим на exit (эмуляция echo-ответа без raw-сокета). |
|
||||
`send_to_exit` копирует payload 0..65507 и возвращает результат router
|
||||
(0 = принято в очередь, <0 = ошибка). Локальный ICMP-контекст для этой отправки не требуется.
|
||||
Используются `force=0, mode=0`, собственного повтора REQUEST/REPLY нет.
|
||||
|
||||
Клиент допускает REPLY только от `tcp_proxy_client->via_node_id`.
|
||||
`deliver_reply(inst, dst_ip, src_ip, ...)` требует клиентский TUN, допускает payload
|
||||
0..65506, собирает IPv4 `src_ip→dst_ip` с Echo Reply, TTL 64 и обеими checksum.
|
||||
Нечётный хвост checksum дополняется нулём без чтения за границей.
|
||||
Результат с префиксом версии 4 ставится в `tun->input_queue`:
|
||||
0 = поставлен в очередь, -1 = ошибка. Payload заимствован на время вызова.
|
||||
`recv_cb` принимает владение входным entry/dgram при любом исходе.
|
||||
+41
-23
@@ -1,6 +1,11 @@
|
||||
// Общий TCP proxy протокол. DATA ограничен окном каждого потока; WINDOW возвращает
|
||||
// кредит после потребления данных локальным TCP. FIN следует за всеми DATA своего
|
||||
// направления. CONNECTED подтверждает реальный connect exit → destination.
|
||||
/* Общий TCP proxy протокол и inline-машина окна; все вызовы выполняются в uasync владельца.
|
||||
* init задаёт окно, но ready устанавливает владелец после CONNECTED (на exit — flush).
|
||||
* Перед destroy владельца обязателен proxy_flow_destroy: retry хранит указатель на flow.
|
||||
* Отправка: svc(1), cmd(1), sid(4), payload; приём: router header(26), cmd(1), sid(4), payload.
|
||||
* sid и WINDOW uint32_t копируются в host byte order; IPv4/порт CONNECT — network byte order.
|
||||
* CONNECTED подтверждает реальный connect exit → destination. DATA ограничен окном потока.
|
||||
* FIN завершает одно направление; владелец ставит fin_pending только после всех своих DATA.
|
||||
* CLOSE/ERROR и CONNECT отправляет владелец напрямую: общий retry их не сохраняет. */
|
||||
#ifndef PROXY_PROTOCOL_H
|
||||
#define PROXY_PROTOCOL_H
|
||||
|
||||
@@ -22,30 +27,32 @@
|
||||
#define TCP_PROXY_SUBCMD_WINDOW 0x07
|
||||
#define TCP_PROXY_HDR_SIZE 6
|
||||
#define TCP_PROXY_CONNECT_HDR_SIZE 12
|
||||
#define TCP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 5)
|
||||
#define TCP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 5) // 31 байт с метаданными router, не wire header
|
||||
#define TCP_PROXY_CHUNK 4096
|
||||
#define TCP_PROXY_WINDOW 65536u
|
||||
|
||||
struct proxy_flow {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct UASYNC* ua;
|
||||
uint64_t peer;
|
||||
uint32_t sid;
|
||||
uint8_t svc;
|
||||
uint8_t ready;
|
||||
uint8_t connected_pending;
|
||||
uint8_t fin_pending;
|
||||
uint8_t fin_sent;
|
||||
uint8_t fin_received;
|
||||
uint32_t tx_credit;
|
||||
uint32_t rx_credit;
|
||||
uint32_t consumed;
|
||||
void* retry;
|
||||
void (*wake)(void* arg);
|
||||
void* arg;
|
||||
struct UTUN_INSTANCE* inst; // Заимствованный instance для отправки в TOPO_GROUP_UTUN, mode=0.
|
||||
struct UASYNC* ua; // Заимствованный event loop; живёт дольше flow и retry.
|
||||
uint64_t peer; // Конечный узел противоположной стороны потока.
|
||||
uint32_t sid; // ID потока; на exit уникален только вместе с peer.
|
||||
uint8_t svc; // Сервис получателя: TCP_PROXY_SERVER или TCP_PROXY_CLIENT.
|
||||
uint8_t ready; // Разрешены DATA и FIN; начальное значение 0.
|
||||
uint8_t connected_pending; // Exit должен поставить CONNECTED в router перед остальными control.
|
||||
uint8_t fin_pending; // Владелец уже передал все DATA и просит отправить FIN.
|
||||
uint8_t fin_sent; // FIN принят router; это не ACK конечного TCP.
|
||||
uint8_t fin_received; // Владелец принял FIN; последующие DATA отклоняются.
|
||||
uint32_t tx_credit; // Сколько DATA ещё разрешено отправить, изначально TCP_PROXY_WINDOW.
|
||||
uint32_t rx_credit; // Сколько DATA разрешено принять до следующего WINDOW.
|
||||
uint32_t consumed; // Потреблённые байты, кредит которых ещё не передан router.
|
||||
void* retry; // Отменяемый таймер control, 5000 tb = 500 мс.
|
||||
void (*wake)(void* arg); // Возобновление очередей владельца после timer flush; может освободить владельца.
|
||||
void* arg; // Заимствованный аргумент wake.
|
||||
};
|
||||
|
||||
// Сообщение целиком передаётся маршрутизатору, который освобождает entry при любом результате.
|
||||
// Копирует заимствованный data (len<=4096; при len>0 не NULL), отправляет в UTUN с mode=0.
|
||||
// 0 = принято router, <0 = ошибка; force обходит лимит send_q, не гарантирует доставку.
|
||||
// Созданный entry целиком передаётся router, который освобождает его при любом результате.
|
||||
static inline int proxy_send(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t svc, uint8_t cmd,
|
||||
uint32_t sid, const uint8_t* data, size_t len, int force) {
|
||||
if (len > TCP_PROXY_CHUNK) {
|
||||
@@ -66,6 +73,8 @@ static inline int proxy_send(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t
|
||||
return etcp_route_send(inst, TOPO_GROUP_UTUN, peer, e, force, 0);
|
||||
}
|
||||
|
||||
// Отправляет pending CONNECTED → WINDOW → FIN с force=1; неудачу повторяет через 500 мс.
|
||||
// Владелец сам дренирует DATA перед fin_pending; flush не проверяет его очереди.
|
||||
static inline void proxy_flow_flush(struct proxy_flow* f);
|
||||
|
||||
// Повторяем только недоставленные control-сообщения; DATA остаётся у владельца потока.
|
||||
@@ -76,6 +85,8 @@ static inline void proxy_flow_retry(void* arg) {
|
||||
if (f->wake) f->wake(f->arg);
|
||||
}
|
||||
|
||||
// Обнуляет неактивный f, задаёт peer/svc/sid и оба окна по 64 KiB; указатели заимствуются.
|
||||
// Перед повторным init нужно отменить прежний retry через destroy; ready остаётся 0.
|
||||
static inline void proxy_flow_init(struct proxy_flow* f, struct UTUN_INSTANCE* inst, struct UASYNC* ua,
|
||||
uint64_t peer, uint8_t svc, uint32_t sid, void (*wake)(void*), void* arg) {
|
||||
memset(f, 0, sizeof(*f));
|
||||
@@ -84,6 +95,7 @@ static inline void proxy_flow_init(struct proxy_flow* f, struct UTUN_INSTANCE* i
|
||||
f->wake = wake; f->arg = arg;
|
||||
}
|
||||
|
||||
// Отменяет только control retry; память flow, очереди и router waiter освобождает владелец.
|
||||
static inline void proxy_flow_destroy(struct proxy_flow* f) {
|
||||
if (f->retry) { uasync_cancel_timeout(f->ua, f->retry); f->retry = NULL; }
|
||||
}
|
||||
@@ -114,7 +126,8 @@ retry:
|
||||
if (!f->retry) f->retry = uasync_set_timeout(f->ua, 5000, f, proxy_flow_retry, "proxy_control");
|
||||
}
|
||||
|
||||
// 0 = отправлено, -2 = ждём окно/CONNECTED, -1 = router backpressure/ошибка выделения.
|
||||
// Копирует DATA; 0 = принято router, -2 = ждём окно/CONNECTED, -1 = неверный DATA/ошибка router или выделения.
|
||||
// При ошибке восстанавливает tx_credit; исходный буфер остаётся у владельца для повтора.
|
||||
static inline int proxy_flow_send(struct proxy_flow* f, const uint8_t* data, size_t len, int force) {
|
||||
if (!f->ready || len > f->tx_credit) return -2;
|
||||
if (!len || len > TCP_PROXY_CHUNK || f->fin_sent) {
|
||||
@@ -127,6 +140,8 @@ static inline int proxy_flow_send(struct proxy_flow* f, const uint8_t* data, siz
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Проверяет DATA len=1..4096, доступное rx_credit и отсутствие FIN; 0 списывает кредит, -1 = ошибка.
|
||||
// Проверку ready и доставку байтов локальному TCP выполняет вызывающая сторона.
|
||||
static inline int proxy_flow_receive(struct proxy_flow* f, size_t len) {
|
||||
if (!len || len > TCP_PROXY_CHUNK || len > f->rx_credit || f->fin_received) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: invalid receive sid=%08x len=%zu credit=%u fin=%u",
|
||||
@@ -137,6 +152,8 @@ static inline int proxy_flow_receive(struct proxy_flow* f, size_t len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// WINDOW: заимствованные 4 байта host-order credit; 0 добавляет кредит, -1 = неверный размер/кредит.
|
||||
// Не вызывает wake: владелец после успеха сам возобновляет отправку.
|
||||
static inline int proxy_flow_window(struct proxy_flow* f, const uint8_t* data, size_t len) {
|
||||
uint32_t credit = 0;
|
||||
if (len == 4) memcpy(&credit, data, 4);
|
||||
@@ -149,7 +166,8 @@ static inline int proxy_flow_window(struct proxy_flow* f, const uint8_t* data, s
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Вызывать только после передачи байтов локальному TCP, не после получения ETCP DATA.
|
||||
// Возвращает кредит через flush после передачи len байт локальному TCP (OS send/lwIP tcp_write).
|
||||
// Владелец гарантирует, что байты ранее приняты и не учтены повторно; проверки здесь нет.
|
||||
static inline void proxy_flow_consume(struct proxy_flow* f, uint32_t len) {
|
||||
f->consumed += len;
|
||||
proxy_flow_flush(f);
|
||||
|
||||
+41
-35
@@ -1,6 +1,9 @@
|
||||
/* Локальные SOCKS5 CONNECT и HTTP proxy/CONNECT: DNS, handshake и передача TCP через удалённый exit.
|
||||
* tcp_proxy_client владеет listener и списками соединений; их callbacks выполняются в uasync.
|
||||
* init_listen заимствует указатели на списки/счётчики до close_listen; закрытие listener не освобождает клиентов. */
|
||||
* init_listen заимствует inst/ua/списки/счётчики; они живут до закрытия listener И освобождения клиентов.
|
||||
* Сначала закрыть listener, затем conn_free_all; закрытие listener само по себе не освобождает клиентов.
|
||||
* DNS выполняется локально (первая A-запись); SOCKS поддерживает только CONNECT без аутентификации.
|
||||
* HTTP keep-alive использует новый stream для каждой транзакции; CONNECT/101 переходят в сырой туннель. */
|
||||
#ifndef SOCKS_PROXY_H
|
||||
#define SOCKS_PROXY_H
|
||||
|
||||
@@ -25,80 +28,83 @@ struct socks_http_state;
|
||||
enum {
|
||||
SOCKS_STATE_GREETING = 0, // ждём SOCKS5 greeting (ver + methods)
|
||||
SOCKS_STATE_REQUEST, // ждём SOCKS5 CONNECT request (== HTTP_STATE_REQUEST)
|
||||
SOCKS_STATE_CONNECTING, // DNS в процессе (== HTTP_STATE_CONNECTING)
|
||||
SOCKS_STATE_CONNECTING, // ждём DNS или CONNECTED от exit (== HTTP_STATE_CONNECTING)
|
||||
SOCKS_STATE_RELAY // релей данных (== HTTP_STATE_RELAY)
|
||||
};
|
||||
|
||||
// DNS/connect и сырой туннель используют общие состояния; HTTP framing хранится отдельно в http.
|
||||
enum {
|
||||
HTTP_STATE_REQUEST = SOCKS_STATE_REQUEST, // ждём заголовки очередного HTTP-запроса
|
||||
HTTP_STATE_CONNECTING = SOCKS_STATE_CONNECTING, // DNS в процессе
|
||||
HTTP_STATE_CONNECTING = SOCKS_STATE_CONNECTING, // DNS или реальный connect на exit
|
||||
HTTP_STATE_RELAY = SOCKS_STATE_RELAY // релей данных
|
||||
};
|
||||
|
||||
struct socks_proxy_conn {
|
||||
struct socks_proxy_conn* next;
|
||||
struct socks_proxy_conn* next; // следующий клиент в списке владельца
|
||||
struct socks_proxy_conn** head; // указатель на голову списка (для удаления)
|
||||
int* count; // указатель на счётчик списка
|
||||
struct tcp_conn* tc;
|
||||
struct UASYNC* ua;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t via_node_id;
|
||||
struct proxy_flow flow;
|
||||
uint32_t stream_id;
|
||||
struct tcp_conn* tc; // собственный OS TCP к локальному клиенту
|
||||
struct UASYNC* ua; // заимствованный event loop
|
||||
struct UTUN_INSTANCE* inst; // заимствованный instance
|
||||
uint64_t via_node_id; // настроенный exit
|
||||
struct proxy_flow flow; // окно и control retry текущего upstream
|
||||
uint32_t stream_id; // текущий upstream; HTTP между транзакциями может быть 0
|
||||
uint32_t* next_stream_id; // HTTP: новый поток для следующей транзакции
|
||||
struct socks_http_state* http;
|
||||
uint8_t dest_ip[4];
|
||||
uint16_t dest_port;
|
||||
uint8_t fin_remote;
|
||||
uint8_t rem_closed;
|
||||
uint8_t close_sent;
|
||||
uint8_t close_pending;
|
||||
uint8_t state;
|
||||
uint8_t freed;
|
||||
uint8_t close_queued;
|
||||
struct socks_http_state* http; // собственное HTTP-состояние; NULL для SOCKS
|
||||
uint8_t dest_ip[4]; // IPv4 назначения, network byte order
|
||||
uint16_t dest_port; // порт назначения, network byte order
|
||||
uint8_t fin_remote; // FIN от exit; FIN локального клиента хранится в tc->fin_remote
|
||||
uint8_t rem_closed; // прекращена обработка потока/клиента
|
||||
uint8_t close_sent; // CLOSE принят router
|
||||
uint8_t close_pending; // неудавшийся CLOSE; последняя попытка при free
|
||||
uint8_t state; // SOCKS_STATE_* / HTTP_STATE_*
|
||||
uint8_t freed; // освобождение уже началось
|
||||
uint8_t close_queued; // close-маркер уже поставлен в write_queue
|
||||
void* free_soon_id; // handle отложенного освобождения (uasync_call_soon)
|
||||
uint8_t buf[16384];
|
||||
uint16_t buf_len;
|
||||
uint8_t buf[16384]; // handshake или неразобранный хвост read block
|
||||
uint16_t buf_len; // число сохранённых байтов в buf
|
||||
uint8_t* tx_buf; // буфер при backpressure (retry в tx_waiter_cb)
|
||||
uint16_t tx_len;
|
||||
uint16_t tx_len; // остаток tx_buf; отправляется блоками до 4096 байт
|
||||
uint32_t bytes_to_client; // байт записано клиенту (curl) через write_queue
|
||||
uint32_t bytes_from_exit; // байт получено от exit node через ETCP DATA
|
||||
struct queue_waiter_handle tx_waiter;
|
||||
void* tx_retry_timer; // safety-net таймер backpressure (force=1)
|
||||
uint8_t fin_deferred; // FIN отложен до сброса tx_buf
|
||||
struct queue_waiter_handle tx_waiter; // ожидание router send_q, отменить до free
|
||||
void* tx_retry_timer; // safety-net 500 мс, повтор с обычным force=0
|
||||
uint8_t fin_deferred; // зарезервировано, текущая реализация не использует
|
||||
struct adns_query* dns_q; // активный DNS-запрос (NULL когда нет)
|
||||
uint8_t dns_kind; // DNSK_SOCKS / DNSK_HTTP_CONNECT / DNSK_HTTP_PROXY
|
||||
};
|
||||
|
||||
struct listen_ctx;
|
||||
|
||||
// Создать слушающий сокет для SOCKS/HTTP proxy. addr_str = "IP:PORT".
|
||||
// listen_ctx должен быть освобождён через socks_proxy_close_listen.
|
||||
// Возвращает listen_ctx при успехе, NULL при ошибке.
|
||||
// Создать IPv4 listener: addr_str = "IPv4:PORT", 0 разрешает выбор порта ОС.
|
||||
// conns/count/next_stream_id и inst/ua заимствуются; is_http=0 — SOCKS5, иначе HTTP.
|
||||
// Общий генератор sid нужен для всех списков одного tcp_proxy_client. Возврат: ctx или NULL при ошибке.
|
||||
// Не регистрирует router callback: входящие ответы должен передавать tcp_proxy_client.
|
||||
struct listen_ctx* socks_proxy_init_listen(struct UASYNC* ua, const char* addr_str,
|
||||
struct socks_proxy_conn** conns, int* count, uint32_t* next_stream_id,
|
||||
struct UTUN_INSTANCE* inst, uint64_t via_node_id, int is_http);
|
||||
|
||||
// Закрыть слушающий сокет и освободить listen_ctx.
|
||||
// Закрыть listener и освободить ctx в том же ua. NULL ctx допустим; активные клиенты сохраняются.
|
||||
// Если sock_out задан, в него записывается SOCKET_INVALID: сокет уже закрыт.
|
||||
void socks_proxy_close_listen(struct UASYNC* ua, struct listen_ctx* ctx, socket_t* sock_out);
|
||||
// Фактический порт активного listener (IP:0 выбирает ОС); 0 при ошибке.
|
||||
uint16_t socks_proxy_get_port(const struct listen_ctx* ctx);
|
||||
|
||||
// Найти соединение по stream_id
|
||||
// Найти текущий stream_id в head; заимствованный результат или NULL, список не меняется.
|
||||
struct socks_proxy_conn* socks_proxy_find_conn(struct socks_proxy_conn* head, uint32_t stream_id);
|
||||
|
||||
// Обработать ответ exit: CONNECTED/DATA/WINDOW_UPDATE/CLOSE/ERROR/FIN.
|
||||
// Возвращает 1 если обработано, 0 если stream_id не найден
|
||||
// Передать ответ exit: CONNECTED/DATA/WINDOW/CLOSE/ERROR/FIN; data заимствован на время вызова.
|
||||
// Проверку peer/group выполняет внешний dispatcher. Может закрыть/удалить соединение.
|
||||
// Возвращает 1 при найденном sid (в том числе неизвестная команда), 0 если sid не найден.
|
||||
int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count,
|
||||
uint32_t stream_id, uint8_t subcmd,
|
||||
const uint8_t* data, size_t data_len);
|
||||
|
||||
// Освободить одно соединение (удаляет из списка)
|
||||
// Освободить c и его TCP/DNS/HTTP/таймеры/waiter, удалить из списка; NULL допустим.
|
||||
// Активный HTTP upstream отменяется; в остальных случаях free не гарантирует уведомление exit.
|
||||
void socks_proxy_conn_free(struct socks_proxy_conn* c);
|
||||
|
||||
// Освободить все соединения в списке
|
||||
// Освободить все соединения; head/count — те же живые адреса, что переданы init_listen.
|
||||
void socks_proxy_conn_free_all(struct socks_proxy_conn** head, int* count);
|
||||
|
||||
|
||||
|
||||
@@ -22,10 +22,11 @@ struct listen_ctx* listen = socks_proxy_init_listen(
|
||||
`is_http=0` — SOCKS5, `is_http=1` — HTTP CONNECT/прокси.
|
||||
|
||||
### Приём ETCP-данных от exit node
|
||||
Внешний dispatcher (`tcp_proxy_client`) вызывает `socks_proxy_handle_etcp()` при получении ETCP-пакетов с `svc_id=ETCP_RT_ID_TCP_PROXY`:
|
||||
Внешний dispatcher (`tcp_proxy_client`) проверяет peer/group и вызывает `socks_proxy_handle_etcp()`
|
||||
при получении пакетов `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08). Запросы уходят в `ETCP_RT_ID_TCP_PROXY_SERVER` (0x04):
|
||||
```c
|
||||
int handled = socks_proxy_handle_etcp(&conns, &conn_count, stream_id, subcmd, data, data_len);
|
||||
// handled=1 — найден и обработан, 0 — stream_id не найден
|
||||
// handled=1 — stream_id найден (включая игнорирование команды), 0 — stream_id не найден
|
||||
```
|
||||
Поддерживаемые subcmd: `CONNECTED`, `DATA`, `WINDOW`, `CLOSE`, `ERROR`, `FIN`.
|
||||
|
||||
@@ -37,12 +38,21 @@ struct socks_proxy_conn* c = socks_proxy_find_conn(conns, stream_id);
|
||||
|
||||
### Завершение
|
||||
```c
|
||||
socks_proxy_conn_free_all(&conns, &conn_count); // все соединения
|
||||
socks_proxy_close_listen(ua, listen, &sock_out); // слушатель
|
||||
socks_proxy_conn_free_all(&conns, &conn_count); // все соединения
|
||||
```
|
||||
`inst/ua/conns/count/next_stream_id` заимствуются и должны жить до закрытия listener и всех клиентов.
|
||||
Закрытие listener сохраняет активных клиентов. Вызовы и callbacks выполняются в uasync владельца.
|
||||
Адрес listener — IPv4:port; порт 0 выбирает ОС, фактический порт возвращает `socks_proxy_get_port()`.
|
||||
При создании через `tcp_proxy_client_create` каждый включённый listener обязателен:
|
||||
пустой/неверный адрес или ошибка bind/listen откатывают создание всего клиента.
|
||||
|
||||
### Ключевые нюансы
|
||||
- **Бэкпрессур (backpressure):** при неудаче `etcp_route_send()` данные сохраняются в `tx_buf/tx_len`, и регистрируется waiter через `etcp_router_on_send_ready()`. Когда ETCP готов к отправке, `tx_waiter_cb` повторяет попытку и возобновляет `read_queue`.
|
||||
- **Backpressure:** DATA ждёт CONNECTED и кредита `proxy_flow`. Неотправленные байты сохраняются
|
||||
в `tx_buf/tx_len` и отправляются порциями до 4096. При доступном окне ставится router send-ready
|
||||
waiter; safety-net 500 мс возобновляет отправку с обычным force=0. Чтение tcp_io ограничено его очередью.
|
||||
HUP сохраняет high/low-water и ручную остановку чтения; временно снятая регистрация fd
|
||||
восстанавливается после освобождения очереди. Запись не зависит от паузы чтения.
|
||||
- **Несколько соединений:** управляются через связный список `struct socks_proxy_conn*` + счётчик. В HTTP keep-alive каждое сообщение получает новый upstream stream; клиентский TCP-сокет сохраняется. Pipelining обрабатывается последовательно, включая запросы к разным адресатам.
|
||||
- **stream_id** генерируется монотонно (`++next_stream_id`), передаётся в заголовке ETCP-пакетов для мультиплексирования.
|
||||
|
||||
@@ -71,31 +81,32 @@ socks_proxy_close_listen(ua, listen, &sock_out); // слушатель
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `socks_proxy_init_listen(ua, addr, conns, count, next_sid, inst, via_node, is_http)` | Создаёт слушающий TCP-сокет на `addr` (IP:PORT). Возвращает `listen_ctx*`. |
|
||||
| `socks_proxy_init_listen(ua, addr, conns, count, next_sid, inst, via_node, is_http)` | Создаёт IPv4 listener, возвращает собственный ctx или NULL. Не регистрирует router callback. |
|
||||
| `socks_proxy_close_listen(ua, ctx, sock_out)` | Закрывает слушающий сокет, освобождает ctx. |
|
||||
| `socks_proxy_get_port(ctx)` | Фактический порт listener, 0 при ошибке. |
|
||||
| `socks_proxy_handle_etcp(head, count, stream_id, subcmd, data, len)` | Диспетчер ETCP-сообщений от exit node. Обрабатывает DATA (→write_queue клиенту), CLOSE/ERROR (→push_close), FIN (→push_fin). |
|
||||
| `socks_proxy_find_conn(head, stream_id)` | Поиск соединения по stream_id в связном списке. |
|
||||
| `socks_proxy_conn_free(c)` | Освобождает соединение: отправляет отложенный CLOSE/ERROR в ETCP, удаляет из списка, освобождает `tcp_conn`, buffer, waiter. |
|
||||
| `socks_proxy_conn_free(c)` | Удаляет из списка, отменяет DNS/control retry/DATA timer/waiter, освобождает TCP/HTTP/buffers. Активный HTTP upstream отменяется; pending CLOSE/ERROR получает последнюю попытку. |
|
||||
| `socks_proxy_conn_free_all(head, count)` | Освобождает все соединения в списке. |
|
||||
|
||||
### Внутренние функции
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет: `[TCP_PROXY_HDR_SIZE=6] svc_id(1)+subcmd(1)+stream_id(4)` + payload, отправляет через `etcp_route_send`. |
|
||||
| `send_msg(inst, group_id, dst, subcmd, sid, data, len, force)` | Через общий proxy_send копирует payload и отправляет в TOPO_GROUP_UTUN; group_id аргумент игнорируется, mode=0. |
|
||||
| `send_connect(c)` | Отправляет CONNECT (dest_ip[4]+dest_port[2]) на exit node. |
|
||||
| `send_data(c, data, len)` | Отправляет DATA (релей пользовательского трафика). |
|
||||
| `send_close(c)`, `send_error(c)`, `send_fin(c)` | Отправляют управляющие subcmd. |
|
||||
| `send_close(c)` | Попытка CLOSE с force=1; pending повторяется при free. ERROR отправляется через send_msg, FIN — через proxy_flow_flush. |
|
||||
| `write_to_client(c, data, len)` | Запись данных в `tc->write_queue` (отправка клиенту через TCP). |
|
||||
| `process_socks_greeting(c)` | Парсинг SOCKS5 GREETING: ver=5, выбор метода 0x00 (no auth), ответ 05 00. |
|
||||
| `process_socks_request(c)` | Парсинг SOCKS5 CONNECT REQUEST (atyp=1/3/4, IPv4/IPv6/domain). DNS-резолвинг для доменов. Ответ 05 00 00 01 + dummy addr. |
|
||||
| `process_socks_request(c)` | CONNECT к IPv4 или DNS-имени; IPv6 отклоняется с REP=8. Успешный ответ отправляет socks_connected только после CONNECTED. |
|
||||
| `http_read(c)`, `http_receive(c)` | Потоковый разбор клиентского запроса и upstream ответа через HTTP codec. |
|
||||
| `http_finish(c)` | Завершение/отмена upstream, ожидание доставки ответа клиенту и переход к следующему запросу. |
|
||||
| `http_fail(c)`, `http_deadline(c)` | Ошибки и тайм-ауты: собственный HTTP error до final headers, закрытие оборванного сообщения после них. |
|
||||
| `on_accept_cb(sock, arg)` | Callback accept: создаёт `socks_proxy_conn`, `tcp_conn_create`, вешает read callback. |
|
||||
| `on_read_cb(q, arg)` | Callback чтения из TCP: handshake-парсинг или релей данных через ETCP с бэкпрессуром. |
|
||||
| `tx_waiter_cb(q, arg)` | Callback бэкпрессура: повторная отправка отложенных данных при готовности ETCP. |
|
||||
| `on_fin_cb`, `on_error_cb`, `on_closed_cb` | Callback'и жизненного цикла `tcp_conn`: FIN→удалённый FIN, ошибка→send_error+free, закрыт→send_close+free. |
|
||||
| `on_fin_cb`, `on_error_cb`, `on_closed_cb` | FIN ждёт исходящих DATA; ошибка отправляет ERROR, cleanup из TCP callbacks откладывается через call_soon. HTTP вне туннеля использует границы транзакций. |
|
||||
|
||||
## 4. Протокол
|
||||
|
||||
@@ -104,8 +115,9 @@ socks_proxy_close_listen(ua, listen, &sock_out); // слушатель
|
||||
Клиент → uTun: [ver=5][nmethods][methods...]
|
||||
uTun → Клиент: [05][00] (GREETING: no auth)
|
||||
Клиент → uTun: [05][01][00][atyp][addr...][port] (CONNECT REQUEST)
|
||||
uTun → Клиент: [05][00][00][01][0.0.0.0][0] (ответ ок)
|
||||
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port]
|
||||
exit → uTun (ETCP): CONNECTED (реальный connect завершён)
|
||||
uTun → Клиент: [05][00][00][01][0.0.0.0][0] (ответ ок)
|
||||
Клиент ↔ uTun ↔ exit: RELAY данных через ETCP DATA
|
||||
```
|
||||
|
||||
@@ -121,10 +133,12 @@ uTun → Клиент: HTTP/1.1 200 Connection Established\r\n\r\n
|
||||
### HTTP-прокси (GET/POST, is_http=1)
|
||||
```
|
||||
Клиент → uTun: GET http://host:port/path HTTP/1.1\r\nHost: ...\r\n\r\n
|
||||
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port] + TCP_PROXY DATA[GET /path HTTP/1.1\r\n...]
|
||||
uTun → exit (ETCP): TCP_PROXY CONNECT[dest_ip+port]
|
||||
exit → uTun (ETCP): CONNECTED
|
||||
uTun → exit (ETCP): TCP_PROXY DATA[GET /path HTTP/1.1\r\n...]
|
||||
exit → uTun: HTTP headers/body через ETCP DATA, затем FIN
|
||||
uTun → Клиент: проверенный и реконструированный HTTP response
|
||||
uTun → exit (ETCP): CLOSE завершённого stream
|
||||
uTun → exit (ETCP): CLOSE завершённого stream (после полного ответа, FIN нужен для тела до EOF)
|
||||
Клиент → uTun: следующий запрос → новый CONNECT с новым stream_id
|
||||
```
|
||||
URL преобразуется в origin-form без потери query; Host строится из absolute URL. Заголовки текущего соединения, включая поля из Connection, и Proxy-Authorization удаляются в обоих направлениях. Framing и Connection формируются заново, добавляется Via. Upstream обычного запроса получает Connection: close; frontend HTTP/1.1 сохраняет keep-alive для ответов с определённой длиной.
|
||||
@@ -139,12 +153,29 @@ ETCP credit возвращается за исходные принятые ба
|
||||
|
||||
ERROR на exit немедленно уничтожает сокет и отложенный upload; CLOSE сохраняет обычное завершение очереди.
|
||||
|
||||
SOCKS greeting принимает только предложенный метод 0 (no auth); иначе отправляется 05 FF.
|
||||
Реализован только CONNECT, без BIND/UDP ASSOCIATE. DNS выполняется на ingress через
|
||||
асинхронный A-resolver, выбирается первый адрес. Хвост совмещённых greeting/request/data сохраняется.
|
||||
Ошибка DNS/connect до установления потока даёт SOCKS REP=4 либо HTTP 502.
|
||||
|
||||
В сыром туннеле FIN локального клиента ждёт tx_buf и read_queue, но не write_queue
|
||||
противоположного направления. FIN от exit ставится в write_queue клиента после предыдущих DATA.
|
||||
После обоих FIN и фактического локального shutdown ставится close-маркер. WINDOW возвращает
|
||||
кредит после опустошения write_queue и write_buf. Общий retry повторяет CONNECTED/WINDOW/FIN,
|
||||
отдельной гарантированной очереди CLOSE/ERROR у listener нет.
|
||||
|
||||
## 5. Зависимости
|
||||
- `tcp_proxy_server.h` — константы subcmd (`TCP_PROXY_SUBCMD_*`) и размер заголовка (`TCP_PROXY_HDR_SIZE=6`)
|
||||
- `etcp.h` / `etcp_api.h` — `etcp_route_send()`, `ETCP_RT_ID_TCP_PROXY`
|
||||
- `proxy_protocol.h` — subcmd, размеры заголовков, proxy_flow и proxy_send
|
||||
- `etcp_api.h` — `ETCP_RT_ID_TCP_PROXY_SERVER/CLIENT`
|
||||
- `etcp_router.h` — `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()`
|
||||
- `tcp_io.h` / `tcp_conn` — TCP-соединения (`tcp_conn_create`, `tcp_conn_push_close`, `tcp_conn_push_fin`, write/read очереди)
|
||||
- `u_async.h` — event loop, `uasync_add_socket_t`
|
||||
- `ll_queue.h` — очереди (write_queue, read_queue), waiter механизм
|
||||
- `memory_pool.h` — аллокация из пулов (`entry_pool`, `data_pool`)
|
||||
- `utun_instance.h` — `struct UTUN_INSTANCE`
|
||||
|
||||
Нормативные размеры сообщений и порядок байтов — в [proxy_protocol.md](../../doc/proxy_protocol.md).
|
||||
Контракты чистого HTTP codec (обнуление состояния, emit/source_len, used и EOF) — в [http_proxy.h](http_proxy.h).
|
||||
`test_socks_http_proxy` проверяет 16 byte-exact загрузок и двусторонний TLS shutdown.
|
||||
`UTUN_TEST_TLS_ABRUPT=1` отдельно воспроизводит обрыв TLS: реальные EPIPE/ENOTCONN
|
||||
остаются ошибками TCP и диагностируются, уже доставленные ответы сохраняются.
|
||||
@@ -723,7 +723,8 @@ void tcp_proxy_client_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* en
|
||||
if (subcmd == TCP_PROXY_SUBCMD_ERROR) { tcp_proxy_client_handle_error(proxy, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
if (subcmd == TCP_PROXY_SUBCMD_FIN) { tcp_proxy_client_handle_fin(proxy, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
}
|
||||
if (subcmd == TCP_PROXY_SUBCMD_DATA || subcmd == TCP_PROXY_SUBCMD_CLOSE || subcmd == TCP_PROXY_SUBCMD_FIN || subcmd == TCP_PROXY_SUBCMD_ERROR) {
|
||||
if (subcmd == TCP_PROXY_SUBCMD_DATA || subcmd == TCP_PROXY_SUBCMD_CLOSE || subcmd == TCP_PROXY_SUBCMD_FIN ||
|
||||
subcmd == TCP_PROXY_SUBCMD_ERROR || subcmd == TCP_PROXY_SUBCMD_WINDOW) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "PROXY subcmd=%02x sid=%08x — client conn not found, silent drop", subcmd, stream_id);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return;
|
||||
}
|
||||
@@ -742,8 +743,18 @@ struct tcp_proxy_client* tcp_proxy_client_create(struct UTUN_INSTANCE* inst, str
|
||||
int http_proxy_enabled, const char* http_proxy_addr)
|
||||
{
|
||||
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "ua is NULL"); return NULL; }
|
||||
if (inst && (inst->tcp_proxy_client || inst->router_bindings.callbacks[ETCP_RT_ID_TCP_PROXY_CLIENT])) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: duplicate instance binding"); return NULL;
|
||||
}
|
||||
if ((socks_enabled && (!socks_addr || !socks_addr[0])) ||
|
||||
(http_proxy_enabled && (!http_proxy_addr || !http_proxy_addr[0])) || mapping_count < 0 || (mapping_count && !mappings)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: missing listener address or invalid mappings"); return NULL;
|
||||
}
|
||||
|
||||
int need_tun = tun_name && tun_name[0] && tun_ip && tun_ip[0];
|
||||
if ((tun_name && tun_name[0]) != (tun_ip && tun_ip[0])) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: TUN requires both name and IP"); return NULL;
|
||||
}
|
||||
if (!need_tun && !socks_enabled && !http_proxy_enabled) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "no proxy mode enabled"); return NULL;
|
||||
}
|
||||
@@ -776,10 +787,20 @@ struct tcp_proxy_client* tcp_proxy_client_create(struct UTUN_INSTANCE* inst, str
|
||||
int j; for (j = 0; j < mapping_count; j++) {
|
||||
uint16_t port_net = htons(mappings[j].local_port);
|
||||
struct tcp_pcb *lpcb = tcp_new(p->lwip);
|
||||
if (!lpcb) continue;
|
||||
tcp_bind(lpcb, INADDR_ANY, port_net);
|
||||
if (!lpcb) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: mapping PCB allocation failed port=%u", mappings[j].local_port);
|
||||
tcp_proxy_client_destroy(p); return NULL;
|
||||
}
|
||||
if (tcp_bind(lpcb, INADDR_ANY, port_net) != LERR_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: mapping bind failed port=%u", mappings[j].local_port);
|
||||
tcp_abort(lpcb); tcp_proxy_client_destroy(p); return NULL;
|
||||
}
|
||||
struct tcp_pcb *listen_pcb = tcp_listen(lpcb);
|
||||
if (listen_pcb) { tcp_arg(listen_pcb, p); tcp_accept(listen_pcb, tcp_proxy_client_accept_cb); }
|
||||
if (!listen_pcb) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: mapping listen failed port=%u", mappings[j].local_port);
|
||||
tcp_abort(lpcb); tcp_proxy_client_destroy(p); return NULL;
|
||||
}
|
||||
tcp_arg(listen_pcb, p); tcp_accept(listen_pcb, tcp_proxy_client_accept_cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,21 +808,28 @@ struct tcp_proxy_client* tcp_proxy_client_create(struct UTUN_INSTANCE* inst, str
|
||||
if (socks_enabled && socks_addr && socks_addr[0]) {
|
||||
p->socks_listen = socks_proxy_init_listen(ua, socks_addr, &p->socks_conns, &p->socks_conn_count,
|
||||
&p->next_stream_id, inst, via_node_id, 0);
|
||||
if (!p->socks_listen) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_client: SOCKS init failed on %s", socks_addr); }
|
||||
if (!p->socks_listen) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_client: SOCKS init failed on %s", socks_addr);
|
||||
tcp_proxy_client_destroy(p); return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (http_proxy_enabled && http_proxy_addr && http_proxy_addr[0]) {
|
||||
p->http_listen = socks_proxy_init_listen(ua, http_proxy_addr, &p->http_conns, &p->http_conn_count,
|
||||
&p->next_stream_id, inst, via_node_id, 1);
|
||||
if (!p->http_listen) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_client: HTTP proxy init failed on %s", http_proxy_addr); }
|
||||
if (!p->http_listen) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_client: HTTP proxy init failed on %s", http_proxy_addr);
|
||||
tcp_proxy_client_destroy(p); return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst) {
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_TCP_PROXY_CLIENT, tcp_proxy_client_router_recv_cb) != 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "TCP proxy client: etcp_router_bind failed");
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: etcp_router_bind failed");
|
||||
tcp_proxy_client_destroy(p); return NULL;
|
||||
} else {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy client: etcp_router bind registered for ID=0x%02x", ETCP_RT_ID_TCP_PROXY_CLIENT);
|
||||
if (need_tun) { udp_proxy_init(inst, ua); icmp_proxy_init(inst, ua); }
|
||||
p->router_bound = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,11 +841,9 @@ struct tcp_proxy_client* tcp_proxy_client_create(struct UTUN_INSTANCE* inst, str
|
||||
|
||||
void tcp_proxy_client_destroy(struct tcp_proxy_client* p) {
|
||||
if (!p) return;
|
||||
int total = p->conn_count + p->socks_conn_count + p->http_conn_count;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy client destroying: lwip=%d socks=%d http=%d",
|
||||
p->conn_count, p->socks_conn_count, p->http_conn_count);
|
||||
if (p->inst) etcp_router_unbind(p->inst, ETCP_RT_ID_TCP_PROXY_CLIENT);
|
||||
if (p->inst && !p->inst->tcp_proxy_server.enabled) { udp_proxy_destroy(p->inst); icmp_proxy_destroy(p->inst); }
|
||||
if (p->router_bound) { etcp_router_unbind(p->inst, ETCP_RT_ID_TCP_PROXY_CLIENT); p->router_bound = 0; }
|
||||
|
||||
if (p->socks_listen) socks_proxy_close_listen(p->ua, p->socks_listen, NULL);
|
||||
socks_proxy_conn_free_all(&p->socks_conns, &p->socks_conn_count);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* Клиент TCP-прокси: принимает IPv4 с отдельного TUN через lwIP либо TCP от SOCKS/HTTP listener.
|
||||
* Передаёт потоки через UTUN-router на via_node_id; proxy_flow обеспечивает окно и backpressure.
|
||||
* create/destroy и callbacks — в uasync. Владеет созданными TUN/lwIP/listener/потоками,
|
||||
* заимствует inst/ua/mappings до destroy. Без tun_name/tun_ip может работать только SOCKS/HTTP. */
|
||||
* заимствует inst/ua/mappings до destroy. Без пары непустых tun_name/tun_ip работает только SOCKS/HTTP.
|
||||
* Владелец сам записывает результат create в inst->tcp_proxy_client и обнуляет поле после destroy.
|
||||
* Клиент принимает TCP-ответы только от via_node_id в TOPO_GROUP_UTUN; UDP/ICMP требуют TUN. */
|
||||
#ifndef TCP_PROXY_CLIENT_H
|
||||
#define TCP_PROXY_CLIENT_H
|
||||
|
||||
@@ -32,12 +34,12 @@ struct listen_ctx;
|
||||
struct tcp_pcb;
|
||||
|
||||
struct tcp_proxy_client_conn {
|
||||
struct tcp_proxy_client_conn* next;
|
||||
struct tcp_proxy_client* proxy;
|
||||
struct tcp_pcb* pcb;
|
||||
struct tcp_proxy_client_conn* next; // следующий lwIP-поток клиента
|
||||
struct tcp_proxy_client* proxy; // заимствованный владелец
|
||||
struct tcp_pcb* pcb; // принадлежит lwIP; NULL после detach/err callback
|
||||
|
||||
struct proxy_flow flow;
|
||||
uint32_t stream_id;
|
||||
struct proxy_flow flow; // окно и retry CONNECTED/WINDOW/FIN
|
||||
uint32_t stream_id; // общий генератор с SOCKS/HTTP-потоками
|
||||
|
||||
struct ll_queue* to_lwip; // DATA от exit → lwIP (управление потоком)
|
||||
|
||||
@@ -47,54 +49,65 @@ struct tcp_proxy_client_conn {
|
||||
uint8_t rem_closed; // exit отправил CLOSE
|
||||
uint8_t error; // ошибка, немедленная очистка
|
||||
uint8_t close_sent; // отправили CLOSE/ERROR в exit
|
||||
uint8_t close_pending; // CLOSE/ERROR не доставлен, ждём повтора
|
||||
uint8_t close_pending; // CLOSE/ERROR не принят router, повтор в poll/free пока pc жив
|
||||
|
||||
struct ll_queue* tx_queue; // буфер lwIP→ETCP при backpressure (аналог server read_queue)
|
||||
struct queue_waiter_handle tx_waiter; // waiter на send_q (etcp_router_on_send_ready)
|
||||
void* tx_retry_timer; // retry safety-net (force=1), аналог server retry_timer
|
||||
void* tx_retry_timer; // safety-net 500 мс, повтор DATA с force=1 (окно proxy сохраняется)
|
||||
|
||||
uint32_t bytes_to_exit; // байт отправлено в exit (из lwIP/TUN)
|
||||
uint32_t bytes_to_exit; // байт принято router для exit (из lwIP/TUN)
|
||||
uint32_t bytes_from_exit; // байт получено от exit (в lwIP/TUN)
|
||||
uint32_t bp_count; // счётчик backpressure (send fail)
|
||||
|
||||
uint8_t dest_ip[4];
|
||||
uint16_t dest_port;
|
||||
uint8_t dest_ip[4]; // IPv4 назначения в сетевом порядке
|
||||
uint16_t dest_port; // порт назначения в сетевом порядке
|
||||
};
|
||||
|
||||
struct tcp_proxy_client {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct UASYNC* ua;
|
||||
struct tun_if* tun;
|
||||
struct tcp_proxy_client_conn* conns;
|
||||
int conn_count;
|
||||
struct memory_pool* entry_pool;
|
||||
uint32_t next_stream_id;
|
||||
uint64_t via_node_id;
|
||||
struct lwip_tcp_ctx* lwip;
|
||||
struct UTUN_INSTANCE* inst; // заимствован; NULL допустим для автономного TUN smoke test
|
||||
uint8_t router_bound; // собственный CLIENT binding установлен; rollback не снимает чужую регистрацию
|
||||
struct UASYNC* ua; // заимствованный event loop
|
||||
struct tun_if* tun; // собственный TUN или NULL для SOCKS/HTTP без TUN
|
||||
struct tcp_proxy_client_conn* conns; // список собственных lwIP-потоков
|
||||
int conn_count; // число lwIP-потоков, без SOCKS/HTTP
|
||||
struct memory_pool* entry_pool; // собственный пул ll_entry для очередей lwIP-потоков
|
||||
uint32_t next_stream_id; // общий счётчик TUN/SOCKS/HTTP, init=1, перед выдачей инкрементируется
|
||||
uint64_t via_node_id; // настроенный exit для всех потоков
|
||||
struct lwip_tcp_ctx* lwip; // собственный TCP-стек или NULL без TUN
|
||||
|
||||
struct tcp_proxy_client_mapping_config* mappings; // указатель на конфиг
|
||||
int mapping_count;
|
||||
int mapping_count; // число записей mappings, порты конфига в host byte order
|
||||
|
||||
// SOCKS proxy
|
||||
int socks_enabled;
|
||||
struct socks_proxy_conn* socks_conns;
|
||||
int socks_conn_count;
|
||||
struct listen_ctx* socks_listen;
|
||||
int socks_enabled; // SOCKS5 listener обязателен при создании
|
||||
struct socks_proxy_conn* socks_conns; // собственные SOCKS5-клиенты
|
||||
int socks_conn_count; // число SOCKS5-клиентов
|
||||
struct listen_ctx* socks_listen; // собственный listener или NULL при отключении/ошибке
|
||||
|
||||
// HTTP CONNECT proxy
|
||||
int http_proxy_enabled;
|
||||
struct socks_proxy_conn* http_conns;
|
||||
int http_conn_count;
|
||||
struct listen_ctx* http_listen;
|
||||
// HTTP forward proxy и CONNECT
|
||||
int http_proxy_enabled; // HTTP listener обязателен при создании
|
||||
struct socks_proxy_conn* http_conns; // собственные HTTP-клиенты
|
||||
int http_conn_count; // число HTTP-клиентов
|
||||
struct listen_ctx* http_listen; // собственный listener или NULL при отключении/ошибке
|
||||
};
|
||||
|
||||
// Создать клиент; ua обязателен. Непустые tun_name И tun_ip включают TUN/lwIP (mtu<=0 → 1500).
|
||||
// test_mode передаётся tun_init_nat: ненулевой использует тестовый TUN; mappings заимствуются до destroy.
|
||||
// SOCKS/HTTP создаются при enabled и непустом IPv4:port; via_node_id — общий exit.
|
||||
// Результат: собственный p или NULL при критической ошибке/отсутствии режима.
|
||||
// Все выбранные входы и CLIENT binding обязательны; отказ полностью освобождает новые ресурсы.
|
||||
// Общие UDP/ICMP создаёт и уничтожает instance отдельно; create их не затрагивает.
|
||||
struct tcp_proxy_client* tcp_proxy_client_create(struct UTUN_INSTANCE* inst, struct UASYNC* ua,
|
||||
const char* tun_name, const char* tun_ip, int mtu, int test_mode,
|
||||
struct tcp_proxy_client_mapping_config* mappings, int mapping_count,
|
||||
uint64_t via_node_id,
|
||||
int socks_enabled, const char* socks_addr,
|
||||
int http_proxy_enabled, const char* http_proxy_addr);
|
||||
// Остановить listeners, освободить потоки/TUN/lwIP и снять router binding; NULL допустим.
|
||||
// Не затрагивает общие UDP/ICMP и не обнуляет inst->tcp_proxy_client.
|
||||
void tcp_proxy_client_destroy(struct tcp_proxy_client* p);
|
||||
// Router callback: conn->instance задаёт владельца, entry содержит метаданные router и передаёт владение.
|
||||
// Освобождает entry/dgram при любом исходе; CLOSE_ALL = пустой payload router, а не TCP-подкоманда.
|
||||
void tcp_proxy_client_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
|
||||
|
||||
|
||||
@@ -1,204 +1,88 @@
|
||||
# tcp_proxy_client
|
||||
# TCP proxy client
|
||||
|
||||
## 1. Назначение
|
||||
`tcp_proxy_client.c/h` объединяет TUN/lwIP, SOCKS5 CONNECT и HTTP forward proxy/CONNECT,
|
||||
передавая потоки к общему exit `via_node_id`. Форматы и общие правила —
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракт API и полей —
|
||||
[tcp_proxy_client.h](tcp_proxy_client.h), локальные listeners —
|
||||
[socks_proxy_doc.md](socks_proxy_doc.md).
|
||||
|
||||
Клиентский оркестратор TCP прокси. Создаёт TUN-интерфейс, запускает встроенный lwIP TCP стек, перехватывает исходящие TCP-соединения со стороны клиента и туннелирует их через ETCP к удалённому exit-узлу (tcp_proxy_server). Поддерживает три режима перехвата трафика одновременно:
|
||||
## Создание и владение
|
||||
|
||||
1. **Прозрачное проксирование через TUN** — весь TCP-трафик с TUN-интерфейса попадает в lwIP, который управляет реальными TCP-сессиями. Исходящие соединения прозрачно проксируются на exit-узел. UDP (через `udp_proxy`) и ICMP Echo (через `icmp_proxy`) также пробрасываются.
|
||||
2. **SOCKS5 proxy listener** — встроенный SOCKS5-сервер, принимающий локальные подключения и туннелирующий их через ETCP.
|
||||
3. **HTTP CONNECT proxy listener** — встроенный HTTP CONNECT-прокси (работает через тот же механизм SOCKS, что и SOCKS5).
|
||||
`tcp_proxy_client_create` вызывается в uasync с живым `ua`. Для TUN нужны **оба**
|
||||
непустых `tun_name` и `tun_ip`; задание только одного — ошибка. `tun_init_nat` создаёт
|
||||
отдельный proxy TUN (test_mode выбирает тестовый интерфейс); MTU<=0 заменяется на 1500,
|
||||
MTU TUN передаётся в lwIP. Имя tun_init_nat не означает включение eim_nat на ingress.
|
||||
Без TUN необходим хотя бы флаг SOCKS или HTTP; эти listeners могут работать без lwIP.
|
||||
|
||||
Модуль полностью оркестрирует весь TCP-прокси-пайплайн на стороне клиента: захват трафика → lwIP TCP стек → ETCP-туннелирование → приём ответных данных → отдача в lwIP → запись IP-пакетов обратно в TUN.
|
||||
`inst/ua/mappings` заимствуются до destroy; результат принадлежит вызывающему коду.
|
||||
Создание само не заполняет `inst->tcp_proxy_client`: это делает владелец, например
|
||||
`utun_instance`. Для автономного TUN smoke test допускается inst=NULL, но для
|
||||
обмена через router нужен instance. Mappings содержат порты в host byte order,
|
||||
адрес назначения — IPv4-строку.
|
||||
|
||||
## 2. Как пользоваться
|
||||
Все заданные SOCKS/HTTP listeners, TUN mappings, TUN/lwIP/пулы и TCP router binding
|
||||
обязательны. Ошибка любого шага возвращает NULL и освобождает уже созданные ресурсы.
|
||||
Повторное создание клиента или занятой binding отклоняются. UDP/ICMP создаёт владелец
|
||||
instance после успешного создания TCP-модулей, если есть TUN либо включён exit.
|
||||
|
||||
### Типовой сценарий
|
||||
Клиент привязывается к `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08), сервер — к
|
||||
`ETCP_RT_ID_TCP_PROXY_SERVER` (0x04). Генератор stream_id общий для TUN/SOCKS/HTTP,
|
||||
изначально равен 1, перед выдачей инкрементируется. На exit ключ потока — пара peer/sid.
|
||||
|
||||
```
|
||||
tcp_proxy_client_create(inst, ua, tun_name, tun_ip, mtu, test_mode,
|
||||
mappings, mapping_count, via_node_id,
|
||||
socks_enabled, socks_addr, http_proxy_enabled, http_proxy_addr)
|
||||
→ TUN init (tun_init_nat) — создание TUN-интерфейса с NAT
|
||||
→ lwIP init (lwip_tcp_init) — запуск встроенного TCP стека
|
||||
→ Настройка port mappings — прослушивание локальных портов в lwIP
|
||||
→ SOCKS5/HTTP listen — запуск локальных прокси-слушателей
|
||||
→ ETCP router bind — регистрация коллбэка tcp_proxy_client_router_recv_cb
|
||||
→ UDP/ICMP proxy init
|
||||
```
|
||||
## TUN → exit
|
||||
|
||||
### Ключевые концепции
|
||||
Output queue TUN содержит однобайтовый префикс перед IP-пакетом. Обработчик проверяет
|
||||
IPv4 version/IHL/total length и отвергает фрагменты (MF/offset) с WARN.
|
||||
Для UDP и ICMP Echo type 8 учитывается IHL, затем вызывается соответствующий proxy.
|
||||
TCP без IP-заголовка передаётся в `lwip_tcp_input`; перед этим по порту назначения
|
||||
при необходимости создаётся динамический listen PCB.
|
||||
|
||||
**TUN interface** через `tun_init_nat()`. IP-пакеты из output_queue TUN направляются в `tcp_proxy_client_tun_input()` — главную точку входа inbound-трафика.
|
||||
При accept выбирается первый mapping с совпадающим локальным портом; если его нет —
|
||||
исходные `pcb->local_ip/local_port`. Создаются `tcp_proxy_client_conn`, `proxy_flow`
|
||||
и очереди `tx_queue` (lwIP→exit) и `to_lwip` (exit→lwIP), отправляется CONNECT.
|
||||
Локальный TCP handshake завершает lwIP; его успех не подтверждает connect назначения.
|
||||
DATA ожидает CONNECTED от exit.
|
||||
|
||||
**Встроенный lwIP TCP стек** (`lwip_tcp_init()`) с output-коллбэком `tcp_proxy_client_output_cb`, который пишет готовые IP-пакеты обратно в TUN (в сторону ОС клиента).
|
||||
Recv callback копирует pbuf в блоки до 4096 байт в tx_queue. Drain отправляет через
|
||||
`proxy_flow_send`; `tcp_recved` возвращает окно локальному клиенту только после
|
||||
успешного принятия DATA router. При -2 ожидаются CONNECTED/WINDOW; при другой ошибке
|
||||
регистрируется router send-ready waiter. Safety-net каждые 500 мс повторяет отправку
|
||||
с force=1, сохраняя ограничение окна proxy. Неотправленный entry возвращается в начало очереди.
|
||||
|
||||
**Inbound TUN трафик** (`tcp_proxy_client_tun_input()`): парсинг IP-пакета → по протоколу:
|
||||
- TCP → вызов `lwip_tcp_input()` — пакет попадает в lwIP, где срабатывает `accept_cb` для новых соединений или `recv_cb` для существующих
|
||||
- UDP → `udp_proxy_send_to_exit()` — проброс UDP в exit
|
||||
- ICMP (type 8/Echo) → `icmp_proxy_send_to_exit()` — проброс ping в exit
|
||||
## Exit → TUN
|
||||
|
||||
**Connection accept** (`tcp_proxy_client_accept_cb`): при новом входящем TCP-соединении в lwIP:
|
||||
1. Выделяется `struct tcp_proxy_client_conn`
|
||||
2. Определяется destination: поиск по port mapping config `mappings[]` (local_port → remote_ip:remote_port), иначе `local_ip:local_port` самого pcb
|
||||
3. Назначается `stream_id` (инкремент `next_stream_id`)
|
||||
4. Регистрируются lwIP коллбэки: `recv`, `sent`, `err`, `poll`
|
||||
5. Создаётся очередь `to_lwip` для данных от exit
|
||||
6. Отправляется ETCP CONNECT с dest_ip+dest_port через `etcp_route_send(force=1)`
|
||||
Router callback проверяет peer==via_node_id и group==TOPO_GROUP_UTUN, включая пустые
|
||||
уведомления router. Сначала предлагает stream SOCKS/HTTP спискам, затем ищет lwIP-поток.
|
||||
|
||||
**Client-side lwIP** — чтение данных (`tcp_proxy_client_recv_cb`):
|
||||
- При получении данных от lwIP (клиент что-то отправляет): копируем pbuf → `tcp_proxy_client_send_data()` → ETCP DATA
|
||||
- При backpressure (router send_q переполнен): сохраняем в `tx_buf`, регистрируем waiter через `etcp_router_on_send_ready()`. При освобождении очереди `tcp_proxy_client_tx_waiter_cb` досылает данные и вызывает `tcp_recved()` для window update
|
||||
- При p==NULL (FIN от lwIP): выставляется `fin_local`, отправляется ETCP FIN. Если обе стороны закрылись — ETCP CLOSE
|
||||
CONNECTED открывает `flow.ready`; WINDOW проверяет и увеличивает кредит, возобновляя
|
||||
очереди. DATA после проверки окна помещается в to_lwip. `feed_from_transport` пишет
|
||||
часть entry по доступному `tcp_sndbuf` с `TCP_WRITE_FLAG_COPY`, оставляя хвост в очереди.
|
||||
Кредит возвращается после успешного `tcp_write`, затем вызывается `tcp_output`.
|
||||
Дальнейший feed запускают DATA, WINDOW/wake, sent и poll callbacks.
|
||||
Выходные IPv4-пакеты lwIP записываются через `tun_write`.
|
||||
|
||||
**Exit-side data** — приём ответных данных (`tcp_proxy_client_handle_data()`):
|
||||
- Данные от exit помещаются в очередь `to_lwip`
|
||||
- `tcp_proxy_client_feed_from_transport()`: пока есть место в `tcp_sndbuf(pcb)`, читает из `to_lwip` и пишет через `tcp_write()` в lwIP. После записи вызывает `tcp_output()` для отправки
|
||||
- Trigger на feed: при получении DATA, при `sent_cb` (lwIP освободил send buffer)
|
||||
## Завершение
|
||||
|
||||
**Full lifecycle:**
|
||||
1. CONNECT — установка соединения с exit
|
||||
2. DATA ↔ DATA — двунаправленный обмен данными
|
||||
3. FIN (от клиента или exit) — half-close: `tcp_shutdown(pcb, 0, 1)` (закрытие только send-стороны)
|
||||
4. CLOSE — полное закрытие после обоих FIN, очистка `tcp_proxy_client_conn_free()`
|
||||
5. ERROR — аварийное закрытие (ошибка lwIP или exit), немедленная очистка
|
||||
Локальный FIN устанавливает fin_local. FIN к exit ставится только после опустошения
|
||||
tx_queue; общий control retry ждёт ready и повторяет неудавшуюся отправку.
|
||||
Удалённый FIN устанавливает fin_remote/flow.fin_received и fin_deferred.
|
||||
После дренажа to_lwip выполняется `tcp_shutdown(pcb, 0, 1)`; при неудаче попытка повторяется.
|
||||
|
||||
**Per-port mapping конфигурация** (`tcp_proxy_client_mapping_config`):
|
||||
```c
|
||||
struct tcp_proxy_client_mapping_config {
|
||||
uint16_t local_port; // локальный порт для прослушивания
|
||||
char remote_ip[64]; // IP назначения на exit-узле
|
||||
uint16_t remote_port; // порт назначения на exit-узле
|
||||
};
|
||||
```
|
||||
При старте для каждого mapping создаётся listen pcb в lwIP. Также работает динамический listen для любых исходящих портов (`tcp_proxy_client_ensure_outbound_listen()`).
|
||||
Когда обе половины завершены, PCB отсоединяется от callbacks: lwIP может самостоятельно
|
||||
освободить его по TIME_WAIT без err callback. Поток удаляется после отправленного FIN,
|
||||
полученного FIN, снятого fin_deferred и пустых tx_queue/to_lwip. Штатное завершение
|
||||
не требует дополнительного обмена CLOSE.
|
||||
|
||||
**SOCKS5/HTTP proxy listeners** — используют `socks_proxy_init_listen()` (общий код с SOCKS, флаг `is_http` различает режимы). SOCKS/HTTP обрабатываются приоритетно в `router_recv_cb` — им не нужен TUN/lwIP.
|
||||
Входящий CLOSE прекращает поток через `tcp_close` и очистку, ERROR вызывает abort.
|
||||
Ошибка lwIP обнуляет PCB, который уже освобождает стек; остальная очистка отменяет
|
||||
control retry, DATA timer, router waiter и освобождает обе очереди.
|
||||
CLOSE/ERROR при неудаче отправки могут повторяться через close_pending в poll/free
|
||||
пока поток жив; отдельной сохраняемой очереди этих команд нет.
|
||||
|
||||
### Протокол обмена с exit-узлом
|
||||
Пустое уведомление router (`ROUTER_SVC_HDR_SIZE=26`, без TCP command/sid) удаляет
|
||||
все lwIP/SOCKS/HTTP потоки клиента к настроенному exit. Это не 9-байтовый wire CLOSE_ALL.
|
||||
|
||||
Формат сообщения: `svc_id(1) + subcmd(1) + stream_id(4) + [data]` (заголовок `TCP_PROXY_HDR_SIZE = 6` байт).
|
||||
|
||||
| Subcmd | Код | Направление | Описание |
|
||||
|--------|-----|-------------|----------|
|
||||
| CONNECT | 0x01 | client→exit | Установка TCP-соединения с destination |
|
||||
| DATA | 0x03 | ↔ | Данные TCP-потока |
|
||||
| CLOSE | 0x04 | ↔ | Полное закрытие потока |
|
||||
| ERROR | 0x05 | ↔ | Ошибка, немедленная очистка |
|
||||
| FIN | 0x06 | ↔ | Half-close (shutdown write) |
|
||||
|
||||
Специальное сообщение: `svc_id(1) + peer_id(8)` (9 байт) — CLOSE_ALL: очистка всех соединений к указанному peer.
|
||||
|
||||
### Механизмы надёжности
|
||||
|
||||
- **Backpressure на router send_q**: `etcp_router_on_send_ready()` — если `etcp_route_send()` не может отправить (inflight полон), данные буферизуются в `tx_buf` и досылаются при освобождении
|
||||
- **Повтор CLOSE/ERROR**: если отправка CLOSE/ERROR не удалась, выставляется `close_pending=1`. В `poll_cb` повторяется попытка
|
||||
- **Очередь to_lwip**: буферизует данные от exit до готовности lwIP send buffer, обеспечивая window-based flow control
|
||||
|
||||
## 3. API
|
||||
|
||||
### Структуры
|
||||
|
||||
#### `struct tcp_proxy_client_conn` — одно проксируемое TCP-соединение
|
||||
| Поле | Тип | Назначение |
|
||||
|------|-----|------------|
|
||||
| `next` | `*` | Связный список всех соединений |
|
||||
| `proxy` | `*tcp_proxy_client` | Обратный указатель на родительский прокси |
|
||||
| `pcb` | `*tcp_pcb` | lwIP protocol control block для этого соединения |
|
||||
| `stream_id` | `uint32_t` | Уникальный идентификатор потока в протоколе ETCP |
|
||||
| `to_lwip` | `*ll_queue` | Очередь данных от exit → lwIP (flow control) |
|
||||
| `fin_local` | `uint8_t` | Локальная сторона (lwIP) отправила FIN |
|
||||
| `fin_remote` | `uint8_t` | Удалённая сторона (exit) отправила FIN |
|
||||
| `rem_closed` | `uint8_t` | Exit прислал CLOSE — прекращаем обработку |
|
||||
| `error` | `uint8_t` | Ошибка на любом конце — немедленная очистка |
|
||||
| `close_sent` | `uint8_t` | CLOSE/ERROR отправлен на exit |
|
||||
| `close_pending` | `uint8_t` | CLOSE/ERROR не доставлен, ждём повтора в poll |
|
||||
| `tx_buf` | `*uint8_t` | Буфер при backpressure (lwIP→ETCP send fail) |
|
||||
| `tx_len` | `uint16_t` | Длина данных в tx_buf |
|
||||
| `tx_waiter` | `queue_waiter_handle` | Дескриптор waiter для backpressure |
|
||||
| `dest_ip` | `uint8_t[4]` | IP назначения на exit-узле |
|
||||
| `dest_port` | `uint16_t` | Порт назначения на exit-узле (network byte order) |
|
||||
|
||||
#### `struct tcp_proxy_client` — состояние прокси-клиента
|
||||
| Поле | Тип | Назначение |
|
||||
|------|-----|------------|
|
||||
| `inst` | `*UTUN_INSTANCE` | Родительский инстанс uTun |
|
||||
| `ua` | `*UASYNC` | Асинхронный event loop |
|
||||
| `tun` | `*tun_if` | TUN-интерфейс для захвата/инжекции трафика |
|
||||
| `conns` | `*tcp_proxy_client_conn` | Голова связного списка lwIP-соединений |
|
||||
| `conn_count` | `int` | Количество активных lwIP-соединений |
|
||||
| `entry_pool` | `*memory_pool` | Пул для `struct ll_entry` (данные to_lwip) |
|
||||
| `next_stream_id` | `uint32_t` | Счётчик stream_id (начинается с 1) |
|
||||
| `via_node_id` | `uint64_t` | Node ID exit-узла для маршрутизации ETCP |
|
||||
| `lwip` | `*lwip_tcp_ctx` | Контекст встроенного lwIP TCP стека |
|
||||
| `mappings` | `*mapping_config` | Указатель на массив port mapping конфигурации |
|
||||
| `mapping_count` | `int` | Количество mapping записей |
|
||||
| `socks_enabled` | `int` | Флаг включения SOCKS5 прокси |
|
||||
| `socks_conns` | `*socks_proxy_conn` | Список активных SOCKS5-соединений |
|
||||
| `socks_conn_count` | `int` | Количество SOCKS5-соединений |
|
||||
| `socks_listen` | `*listen_ctx` | Слушающий сокет SOCKS5 |
|
||||
| `http_proxy_enabled` | `int` | Флаг включения HTTP CONNECT прокси |
|
||||
| `http_conns` | `*socks_proxy_conn` | Список активных HTTP CONNECT соединений |
|
||||
| `http_conn_count` | `int` | Количество HTTP CONNECT соединений |
|
||||
| `http_listen` | `*listen_ctx` | Слушающий сокет HTTP CONNECT |
|
||||
|
||||
### Функции
|
||||
|
||||
#### `tcp_proxy_client_create()`
|
||||
Создаёт и инициализирует весь прокси-клиент. Параметры: инстанс uTun, uasync, имя/IP/MTU TUN-интерфейса, test_mode, массив port mapping, via_node_id, флаги/адреса SOCKS и HTTP прокси. Возвращает NULL при ошибке.
|
||||
|
||||
#### `tcp_proxy_client_destroy()`
|
||||
Полная очистка: отвязка от ETCP router, остановка UDP/ICMP прокси, закрытие SOCKS/HTTP listeners, освобождение всех соединений (lwIP abort + очистка очередей to_lwip + tx_buf + waiter cancel), уничтожение lwIP стека, закрытие TUN, освобождение memory pool.
|
||||
|
||||
#### `tcp_proxy_client_router_recv_cb()`
|
||||
Главный ETCP-коллбэк (зарегистрирован через `etcp_router_bind` на `ETCP_RT_ID_TCP_PROXY=0x04`). Обрабатывает входящие сообщения от exit-узла:
|
||||
- **DATA** → `tcp_proxy_client_handle_data()` — данные в очередь to_lwip и feed в lwIP
|
||||
- **CLOSE** → `tcp_proxy_client_handle_close()` — пометка rem_closed, закрытие lwIP pcb, освобождение соединения
|
||||
- **ERROR** → `tcp_proxy_client_handle_error()` — аварийное закрытие lwIP pcb, освобождение соединения
|
||||
- **FIN** → `tcp_proxy_client_handle_fin()` — half-close через tcp_shutdown(pcb, 0, 1)
|
||||
- **CLOSE_ALL** (9-байтовый пакет без conn) → очистка всех соединений к peer
|
||||
- Сначала пробует SOCKS/HTTP обработчики (приоритет — могут работать без TUN)
|
||||
|
||||
### Внутренние функции
|
||||
|
||||
#### lwIP коллбэки
|
||||
- **`tcp_proxy_client_output_cb()`** — output lwIP: запись IP-пакетов в TUN (в сторону ОС клиента)
|
||||
- **`tcp_proxy_client_accept_cb()`** — новый входящий TCP: создание `tcp_proxy_client_conn`, определение destination из mapping конфига, отправка ETCP CONNECT
|
||||
- **`tcp_proxy_client_recv_cb()`** — данные от клиента (lwIP→ETCP): копирование pbuf → ETCP DATA с backpressure
|
||||
- **`tcp_proxy_client_sent_cb()`** — lwIP освободил send buffer: вызов feed_from_transport для передачи накопленных данных
|
||||
- **`tcp_proxy_client_err_cb()`** — ошибка lwIP: pcb уже уничтожен стеком, отправка ETCP ERROR
|
||||
- **`tcp_proxy_client_poll_cb()`** — периодический poll (2 интервала): очистка при error, повтор close_pending
|
||||
|
||||
#### Отправка сообщений в ETCP
|
||||
- **`tcp_proxy_client_send_msg()`** — базовая функция: сборка пакета `svc_id+subcmd+stream_id+data`, отправка через `etcp_route_send()`
|
||||
- **`tcp_proxy_client_send_connect()`** — CONNECT с dest_ip+dest_port (6 байт данных), force=1
|
||||
- **`tcp_proxy_client_send_data()`** — DATA с payload, без force (backpressure через waiter)
|
||||
- **`tcp_proxy_client_send_close()`** — CLOSE, force=1, при ошибке выставляет close_pending
|
||||
- **`tcp_proxy_client_send_error()`** — ERROR, force=1, при ошибке выставляет close_pending
|
||||
- **`tcp_proxy_client_send_fin()`** — FIN, force=1
|
||||
|
||||
#### Обработка трафика
|
||||
- **`tcp_proxy_client_tun_input()`** — коллбэк очереди output_queue TUN. Парсит IP-пакеты: TCP → lwip_tcp_input (с динамическим listen на целевой порт), UDP/ICMP → соответствующие прокси
|
||||
- **`tcp_proxy_client_handle_non_tcp()`** — перенаправление UDP в `udp_proxy_send_to_exit()` и ICMP Echo в `icmp_proxy_send_to_exit()`
|
||||
- **`tcp_proxy_client_feed_from_transport()`** — чтение данных из очереди `to_lwip` и запись в lwIP через `tcp_write()` пока есть место в send buffer. После feed вызывает `tcp_output()`
|
||||
- **`tcp_proxy_client_tx_waiter_cb()`** — коллбэк backpressure waiter: досылка `tx_buf` при освобождении router send_q, вызов `tcp_recved()` для window update
|
||||
- **`tcp_proxy_client_ensure_outbound_listen()`** — динамическое создание listen pcb в lwIP для заданного порта (если ещё не создан), нужно для прозрачного проксирования любых исходящих TCP-соединений
|
||||
|
||||
#### Управление памятью и соединениями
|
||||
- **`tcp_proxy_client_entry_from_data()`** — создание `ll_entry` из пула `entry_pool` с копированием данных
|
||||
- **`tcp_proxy_client_find_conn()`** — линейный поиск соединения по `stream_id`
|
||||
- **`tcp_proxy_client_conn_free()`** — полная очистка одного соединения: досылка pending CLOSE/ERROR, удаление из списка, очистка очереди to_lwip, освобождение tx_buf, cancel waiter, free памяти
|
||||
|
||||
### Зависимости
|
||||
- `tun_if.h` — TUN-интерфейс (инициализация, запись, закрытие)
|
||||
- `lwip_tcp/lwip_tcp.h` — встроенный lwIP TCP стек (инициализация, input, управление pcb)
|
||||
- `etcp.h` / `etcp_api.h` / `etcp_router.h` — ETCP-маршрутизация и отправка данных
|
||||
- `tcp_proxy_server.h` — общие константы протокола (TCP_PROXY_SUBCMD_*, TCP_PROXY_HDR_SIZE)
|
||||
- `socks_proxy.h` — SOCKS5/HTTP CONNECT прокси (init_listen, handle_etcp, conn_free_all)
|
||||
- `udp_proxy.h` / `icmp_proxy.h` — проброс UDP и ICMP
|
||||
- `config_parser.h` — `tcp_proxy_client_mapping_config` (local_port → remote_ip:remote_port)
|
||||
- `utun_instance.h` — `struct UTUN_INSTANCE` с полем `tcp_proxy_client`
|
||||
- `lib/u_async.h` — асинхронный event loop
|
||||
- `lib/ll_queue.h` — lock-free очереди (to_lwip, output_queue TUN)
|
||||
- `lib/memory_pool.h` — пул для `struct ll_entry`
|
||||
- `lib/mem.h` — обёртки malloc/free с учётом утечек
|
||||
- `lib/debug_config.h` — логирование
|
||||
`destroy` снимает только собственный TCP binding, закрывает listeners/клиентов,
|
||||
освобождает lwIP/TUN/пул. Общие UDP/ICMP принадлежат instance и сохраняются.
|
||||
Владелец затем сам обнуляет `inst->tcp_proxy_client`. Все callbacks и очистка выполняются
|
||||
в uasync; уничтожать клиента следует вне его активного callback.
|
||||
@@ -1,7 +1,5 @@
|
||||
// tcp_proxy_server.c — TCP прокси-сервер (exit node)
|
||||
#include "tcp_proxy_server.h"
|
||||
#include "udp_proxy.h"
|
||||
#include "icmp_proxy.h"
|
||||
#include "etcp.h"
|
||||
#include "etcp_api.h"
|
||||
#include "etcp_router.h"
|
||||
@@ -494,7 +492,8 @@ void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
if (subcmd == TCP_PROXY_SUBCMD_FIN) { tcp_proxy_server_handle_fin(inst, peer, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
}
|
||||
}
|
||||
if (subcmd == TCP_PROXY_SUBCMD_DATA || subcmd == TCP_PROXY_SUBCMD_CLOSE || subcmd == TCP_PROXY_SUBCMD_FIN || subcmd == TCP_PROXY_SUBCMD_ERROR) {
|
||||
if (subcmd == TCP_PROXY_SUBCMD_DATA || subcmd == TCP_PROXY_SUBCMD_CLOSE || subcmd == TCP_PROXY_SUBCMD_FIN ||
|
||||
subcmd == TCP_PROXY_SUBCMD_ERROR || subcmd == TCP_PROXY_SUBCMD_WINDOW) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "PROXY subcmd=%02x sid=%08x — server conn not found, silent drop", subcmd, stream_id);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return;
|
||||
}
|
||||
@@ -507,16 +506,18 @@ void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
// ====================================================================
|
||||
|
||||
int tcp_proxy_server_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst) return -1;
|
||||
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: init requires instance"); return -1; }
|
||||
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
|
||||
if (ctx->inst || ctx->conns) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: duplicate initialization"); return -1; }
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
if (!inst->config) return 0;
|
||||
ctx->enabled = inst->config->global.tcp_proxy_server_enabled;
|
||||
ctx->inst = inst;
|
||||
if (!ctx->enabled) return 0;
|
||||
etcp_router_bind(inst, ETCP_RT_ID_TCP_PROXY_SERVER, tcp_proxy_server_recv_cb);
|
||||
udp_proxy_init(inst, inst->ua);
|
||||
icmp_proxy_init(inst, inst->ua);
|
||||
if (!inst->config->global.tcp_proxy_server_enabled) return 0;
|
||||
if (inst->router_bindings.callbacks[ETCP_RT_ID_TCP_PROXY_SERVER] ||
|
||||
etcp_router_bind(inst, ETCP_RT_ID_TCP_PROXY_SERVER, tcp_proxy_server_recv_cb) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: service binding unavailable"); ctx->inst = NULL; return -1;
|
||||
}
|
||||
ctx->enabled = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server initialized node=%016llx", (unsigned long long)inst->node_id);
|
||||
return 0;
|
||||
}
|
||||
@@ -524,12 +525,12 @@ int tcp_proxy_server_init(struct UTUN_INSTANCE* inst) {
|
||||
void tcp_proxy_server_destroy(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst) return;
|
||||
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
|
||||
if (!ctx->inst) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server destroying: conn_count=%d", ctx->conn_count);
|
||||
struct tcp_proxy_server_conn* rc = ctx->conns;
|
||||
while (rc) { struct tcp_proxy_server_conn* next = rc->next; tcp_proxy_server_conn_free(rc); rc = next; }
|
||||
ctx->conns = NULL; ctx->enabled = 0;
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_TCP_PROXY_SERVER);
|
||||
udp_proxy_destroy(inst);
|
||||
icmp_proxy_destroy(inst);
|
||||
ctx->conns = NULL;
|
||||
if (ctx->enabled) etcp_router_unbind(inst, ETCP_RT_ID_TCP_PROXY_SERVER);
|
||||
ctx->enabled = 0; ctx->inst = NULL;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server destroyed");
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
/* Exit TCP-прокси: принимает CONNECT/DATA через UTUN-router и открывает обычный TCP к назначению.
|
||||
* Поток определяется парой peer_node_id/stream_id; proxy_flow согласует окна и half-close.
|
||||
* init учитывает tcp_proxy_server_enabled в конфиге; destroy закрывает потоки и общий UDP/ICMP-прокси.
|
||||
* Состояние принадлежит UTUN_INSTANCE, операции и callbacks выполняются в его uasync-потоке. */
|
||||
* init учитывает tcp_proxy_server_enabled в конфиге; destroy закрывает только TCP-потоки и свой binding.
|
||||
* Сначала init, затем приём через router callback, в конце destroy; повторный init живого сервера недопустим.
|
||||
* Состояние принадлежит UTUN_INSTANCE, операции и callbacks выполняются в его uasync-потоке.
|
||||
* CLOSE дренирует запись и прекращает чтение; ERROR немедленно уничтожает поток, FIN — half-close. */
|
||||
#ifndef TCP_PROXY_SERVER_H
|
||||
#define TCP_PROXY_SERVER_H
|
||||
|
||||
@@ -24,50 +26,70 @@ struct ETCP_CONN;
|
||||
#include "proxy_protocol.h"
|
||||
|
||||
struct tcp_proxy_server_conn {
|
||||
struct tcp_proxy_server_conn* next;
|
||||
struct tcp_proxy_server* ctx;
|
||||
struct tcp_conn* tc;
|
||||
struct proxy_flow flow;
|
||||
uint32_t stream_id;
|
||||
uint64_t peer_node_id;
|
||||
uint8_t dest_ip[4];
|
||||
uint16_t dest_port;
|
||||
struct tcp_proxy_server_conn* next; // следующий поток exit
|
||||
struct tcp_proxy_server* ctx; // заимствованный владелец в inst
|
||||
struct tcp_conn* tc; // собственный OS TCP к назначению
|
||||
struct proxy_flow flow; // окно, FIN и control retry
|
||||
uint32_t stream_id; // ID клиента; уникален в паре с peer_node_id
|
||||
uint64_t peer_node_id; // конечный ingress-узел, не транспортный сосед
|
||||
uint8_t dest_ip[4]; // IPv4 назначения в сетевом порядке
|
||||
uint16_t dest_port; // порт назначения в сетевом порядке
|
||||
|
||||
uint8_t close_queued;
|
||||
uint8_t close_queued; // close-маркер уже поставлен в write_queue TCP
|
||||
uint8_t cli_closed; // client sent CLOSE
|
||||
|
||||
void* diag_timer; // 1-секундный таймер диагностики
|
||||
uint8_t freed; // 1 = уже освобождён, защита от double-free
|
||||
uint8_t freed; // логически удалён; сама структура освобождается через call_soon
|
||||
|
||||
struct queue_waiter_handle pause_waiter;
|
||||
void* retry_timer; // fallback timer (500ms) for force=1 retry on stall
|
||||
uint32_t bytes_sent; // байт записано в destination (реальный TCP)
|
||||
uint32_t bytes_relayed; // байт отправлено клиенту через ETCP
|
||||
uint32_t drain_count; // счётчик вызовов read_queue_drain_cb
|
||||
struct queue_waiter_handle pause_waiter; // ожидание свободного места в router send_q
|
||||
void* retry_timer; // fallback 500 мс: возобновить read_queue, DATA отправляется с force=0
|
||||
uint32_t bytes_sent; // сумма входящих DATA, учитывается до проверки/записи в TCP
|
||||
uint32_t bytes_relayed; // байт принято router для клиента
|
||||
uint32_t drain_count; // число успешно отправленных DATA из read_queue
|
||||
uint32_t data_count; // счётчик входящих DATA от клиента
|
||||
struct UASYNC* ua;
|
||||
struct UASYNC* ua; // заимствованный loop для таймеров и отложенного free
|
||||
};
|
||||
|
||||
struct tcp_proxy_server {
|
||||
int enabled;
|
||||
int conn_count;
|
||||
struct tcp_proxy_server_conn* conns;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
int enabled; // exit обслуживает CONNECT/UDP/ICMP REQUEST
|
||||
int conn_count; // число потоков в conns
|
||||
struct tcp_proxy_server_conn* conns; // собственные потоки всех пиров
|
||||
struct UTUN_INSTANCE* inst; // заимствованный instance, содержащий сервер
|
||||
};
|
||||
|
||||
// Обнулить состояние и учесть config->global.tcp_proxy_server_enabled; NULL inst → -1, иначе 0.
|
||||
// Без конфига/при отключении не bind-ит сервис. При включении bind SERVER обязателен; отказ возвращает -1.
|
||||
// Общими UDP/ICMP владеет instance; init/destroy TCP-сервера их не затрагивают.
|
||||
int tcp_proxy_server_init(struct UTUN_INSTANCE* inst);
|
||||
// Удалить потоки, выключить сервер и снять свой binding; NULL inst и повторный destroy допустимы.
|
||||
// Дать ua обработать отложенные освобождения до уничтожения instance.
|
||||
void tcp_proxy_server_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Линейный поиск в ненулевом ctx по паре peer/sid; заимствованный указатель или NULL.
|
||||
struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server* ctx, uint64_t peer, uint32_t stream_id);
|
||||
// Немедленно удалить rc из списка, закрыть TCP и отменить таймеры/waiter; NULL допустим.
|
||||
// Память rc освобождается отложенно в ua; сама функция не отправляет CLOSE/ERROR пиру.
|
||||
void tcp_proxy_server_conn_free(struct tcp_proxy_server_conn* rc);
|
||||
|
||||
// Router callback: conn->instance — владелец, peer берётся из ROUTER_SVC_SRC_OFF entry.
|
||||
// Принимает владение entry/dgram при любом исходе. Пустой payload router удаляет потоки этого peer.
|
||||
void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
|
||||
// entry — полный приёмный пакет CONNECT с IPv4/портом; принимает владение entry/dgram.
|
||||
// inst должен быть включён, sid/src — уже разобранные поля. 0 = connect запущен/готов, -1 = ошибка.
|
||||
// CONNECTED отправляется только после реального connect; таймаут — из connect_timeout_ms (0 = отключён).
|
||||
int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry* entry,
|
||||
uint32_t stream_id, uint64_t src_node_id);
|
||||
// Полный entry DATA не короче RECV_HDR_SIZE; принимает владение entry/dgram. conn сейчас не используется.
|
||||
// Peer берётся из entry; проверяет окно и ставит данные в OS write_queue. 0 = обработано, -1 = отклонено.
|
||||
// Ошибка выделения в write_queue может закрыть поток и при возврате 0: это не подтверждение OS send.
|
||||
int tcp_proxy_server_handle_data(struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn, struct ll_entry* entry, uint32_t stream_id);
|
||||
// CLOSE: прекратить чтение, отбросить read_queue и закрыть TCP после записи уже принятой write_queue.
|
||||
void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
|
||||
// ERROR: немедленно удалить поток вместе с непрочитанными/незаписанными данными.
|
||||
void tcp_proxy_server_handle_error(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
|
||||
// FIN: поставить shutdown(SHUT_WR) после уже принятых DATA; обратное направление продолжает работать.
|
||||
// handle_close/error/fin допускают NULL inst и отсутствующую пару peer/sid.
|
||||
void tcp_proxy_server_handle_fin(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
|
||||
|
||||
|
||||
|
||||
@@ -1,109 +1,85 @@
|
||||
# tcp_proxy_server
|
||||
# TCP proxy server (exit)
|
||||
|
||||
## 1. Назначение
|
||||
`tcp_proxy_server.c/h` открывает обычный TCP к IPv4-назначению по CONNECT ingress-узла
|
||||
и передаёт данные через UTUN-router. Общий протокол —
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md), контракт API —
|
||||
[tcp_proxy_server.h](tcp_proxy_server.h).
|
||||
|
||||
TCP прокси-сервер на exit node. Принимает CONNECT-запросы от клиента через ETCP, создаёт OS TCP сокет к реальному адресату, релеит данные в обе стороны: клиент↔ETCP↔сервер↔OS TCP↔destination. Поддерживает backpressure (пауза чтения из OS сокета при заполнении ETCP очереди), graceful CLOSE, экспоненциальный ретрай управляющих сообщений, диагностические таймеры.
|
||||
## Инициализация и ресурсы
|
||||
|
||||
Работает в паре с `tcp_proxy_client` (на ingress node) и `lwip_tcp` (lwIP-стек для обработки TCP-сессий).
|
||||
Контекст встроен в `inst->tcp_proxy_server`. `init(inst)` обнуляет его, читает
|
||||
`config->global.tcp_proxy_server_enabled`, при включении регистрирует сервис
|
||||
`ETCP_RT_ID_TCP_PROXY_SERVER` (0x04).
|
||||
NULL inst даёт -1; отсутствие config или отключённый сервер дают 0 без binding.
|
||||
Занятый binding/ошибка bind возвращает -1; enabled выставляется только после успеха.
|
||||
Повторный init отклоняется. Общие UDP/ICMP создаёт и уничтожает instance независимо от TCP-модулей.
|
||||
|
||||
При старте сервера автоматически инициализируются `udp_proxy` и `icmp_proxy`, если клиентский TCP прокси не активен (чтобы exit node обслуживал все виды трафика).
|
||||
INI-парсер включает сервер при обработке параметра секции `[tcp_proxy_server]`.
|
||||
Поддержанные ключи: `tcp_recv_buf`, `fib`, `so_mark`, `netif`, `bind_ip`,
|
||||
`connect_timeout`. Ключ `enabled` в этой секции текущим парсером не поддерживается;
|
||||
для включения достаточно, например, `connect_timeout=2000`.
|
||||
Пустая секция без параметров сама по себе флаг не выставляет.
|
||||
|
||||
## 2. Как пользоваться
|
||||
Каждый поток принадлежит серверу, содержит собственный `tcp_conn` и `proxy_flow`.
|
||||
Поиск выполняется по паре `peer_node_id/stream_id`; одинаковые sid разных пиров допустимы.
|
||||
Повторный CONNECT существующей пары отклоняется с ERROR.
|
||||
|
||||
### Включение
|
||||
В конфиге: `tcp_proxy_server_enabled = yes` — сервер регистрирует обработчик `ETCP_RT_ID_TCP_PROXY` через `etcp_router_bind()` и готов принимать команды от клиентов.
|
||||
## Connect и обмен данными
|
||||
|
||||
### Типовой сценарий
|
||||
1. **CONNECT (0x01):** клиент шлёт `dest_ip(4) + dest_port(2)`, сервер создаёт `tcp_proxy_server_conn`, открывает OS TCP сокет, делает `connect()` к destination.
|
||||
2. **DATA (0x03):** клиент шлёт данные → сервер пишет в OS write_queue через pool-аллокацию.
|
||||
3. **Чтение из destination:** данные с OS сокета попадают в `read_queue`, оттуда `read_queue_drain_cb` релеит их клиенту через ETCP.
|
||||
4. **CLOSE (0x04):** клиент закрыл соединение → сервер паузит чтение, сливает read_queue, шлёт shutdown(SHUT_WR) в OS сокет.
|
||||
5. **FIN (0x06):** клиент подтверждает получение FIN от destination → сервер push-ит FIN в OS сокет (завершение полного закрытия).
|
||||
6. **ERROR (0x05):** ошибка на клиенте → сервер форсированно закрывает соединение.
|
||||
7. **Очистка:** по `on_closed_cb` (OS сокет закрыт полностью) → `tcp_proxy_server_conn_free()` освобождает все ресурсы.
|
||||
CONNECT содержит IPv4 и порт в сетевом порядке. Создаётся неблокирующий OS-сокет,
|
||||
применяются bind_ip, SO_MARK, FreeBSD FIB и привязка к интерфейсу там, где поддержаны,
|
||||
затем создаётся tcp_conn и вызывается connect. Эти настройки относятся к TCP-сокетам,
|
||||
UDP/ICMP их не наследуют. Таймаут connect — `connect_timeout` мс,
|
||||
по умолчанию 2000; 0 отключает таймаут. Асинхронная ошибка/таймаут приводит к ERROR и очистке.
|
||||
|
||||
### Ключевые нюансы
|
||||
- **Backpressure:** если `etcp_route_send()` вернула ошибку — данные буферизуются в `tx_buf`, регистрируется `pause_waiter` (ожидание освобождения ETCP канала), запускается fallback `retry_timer` (500ms, force=1). При возобновлении — сначала делается повтор tx_buf, потом продолжается дрейн очереди.
|
||||
- **FIN handling:** FIN от destination не отправляется клиенту пока есть pending writes в OS сокет (ждём `on_flushed_cb`) или pending reads (tx_buf / read_queue не пусты — флаг `dst_fin_deferred`). При опустошении очередей — FIN отправляется немедленно.
|
||||
- **Graceful CLOSE:** при получении CLOSE от клиента чтение из OS сокета паузится через `tcp_conn_pause_read()`, read_queue сливается (данные отбрасываются), push-ится close в OS сокет.
|
||||
- **Ретрай CLOSE/ERROR:** при неудачной отправке `send_msg()` — экспоненциальный backoff (50→100→200→…→5000 tb = 5ms→500ms) через `close_timer` + `close_retry_cb`.
|
||||
- **Диагностический таймер (1s):** логирует TCP буферы ОС, размеры read/write очередей, free counts entry/data pool-ов.
|
||||
- **Connect-таймаут:** `connect()` к destination асинхронный; если за `connect_timeout` мс (конфиг `[tcp_proxy_server]`, по умолчанию 2000, `0` = без таймаута) соединение не установилось — срабатывает `tcp_conn_handle_error(ETIMEDOUT)` → клиенту уходит ERROR, соединение освобождается (защита от «чёрной дыры» и утечки fd).
|
||||
- **CLOSE_ALL:** специальный 9-байтовый пакет без валидного conn удаляет все соединения для заданного peer.
|
||||
Реальный успех connect ставит pending CONNECTED. Только после принятия CONNECTED router
|
||||
flow становится ready. Сервер отправляет ответы в `ETCP_RT_ID_TCP_PROXY_CLIENT` (0x08).
|
||||
Все proxy send используют `TOPO_GROUP_UTUN` и mode=0.
|
||||
|
||||
## 3. API
|
||||
DATA клиента проверяется через proxy_flow_receive, копируется в pool-backed write_queue TCP.
|
||||
Кредит WINDOW возвращается после опустошения **и write_queue, и write_buf**,
|
||||
то есть после передачи принятой порции в OS TCP, а не при получении ETCP-пакета.
|
||||
Счётчик bytes_sent увеличивается до проверки/постановки в очередь и не является
|
||||
числом реально доставленных destination байтов.
|
||||
|
||||
### Структуры
|
||||
Данные destination берутся из read_queue и отправляются через proxy_flow_send (force=0).
|
||||
При блокировке entry возвращается в начало read_queue. Для ошибки router ставится
|
||||
send-ready waiter; отсутствие ready/кредита (-2) ждёт CONNECTED/WINDOW.
|
||||
Safety-net 500 мс возобновляет drain с обычным force=0, не обходит окно/очередь router.
|
||||
Чтение tcp_io ограничивается его очередью. Отдельных tx_buf/pause_buf у сервера нет.
|
||||
|
||||
| Структура | Назначение |
|
||||
|-----------|-----------|
|
||||
| `tcp_proxy_server` | Корневой контекст: enabled, связный список conns, conn_count, ссылка на UTUN_INSTANCE |
|
||||
| `tcp_proxy_server_conn` | Состояние одного прокси-соединения: stream_id, peer_node_id, dest_ip:port, tcp_conn, буферы/счётчики/таймеры для backpressure и закрытия |
|
||||
## FIN, CLOSE и ERROR
|
||||
|
||||
### Функции инициализации/завершения
|
||||
FIN destination отправляется клиенту только после опустошения read_queue.
|
||||
Ожидание write_queue противоположного направления для этого не требуется.
|
||||
FIN клиента отмечает flow.fin_received и ставит FIN-маркер в OS write_queue:
|
||||
shutdown(SHUT_WR) следует за всеми ранее принятыми DATA. Дальнейшие DATA после FIN отклоняются.
|
||||
Когда FIN отправлен, принят встречный FIN и OS TCP отправил локальный FIN,
|
||||
в write_queue ставится close-маркер.
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_proxy_server_init(inst)` | Регистрирует ETCP биндинг на `ETCP_RT_ID_TCP_PROXY`, авто-инициализирует UDP/ICMP прокси если tcp_proxy_client не активен |
|
||||
| `tcp_proxy_server_destroy(inst)` | Освобождает все соединения, уничтожает UDP/ICMP прокси |
|
||||
CLOSE клиента устанавливает cli_closed, паузит OS-чтение и отбрасывает read_queue.
|
||||
При установленном TCP close-маркер ждёт ранее принятые записи write_queue;
|
||||
ещё не подключённый поток освобождается сразу. ERROR немедленно уничтожает поток,
|
||||
не ожидая дренажа потенциально зависшего upload.
|
||||
|
||||
### Обработчики команд (вызываются из `tcp_proxy_server_recv_cb`)
|
||||
Общий proxy_flow повторяет только pending CONNECTED/WINDOW/FIN через 500 мс.
|
||||
CLOSE/ERROR не имеют экспоненциального backoff или собственной очереди server retry.
|
||||
OS error callback удаляет поток и делает отдельную попытку отправки ERROR;
|
||||
conn_free сам по себе не отправляет control-сообщение.
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_proxy_server_recv_cb(conn, entry)` | Главный ETCP-коллбэк: диспетчеризует subcmd → handle_connect/data/close/error/fin, обрабатывает CLOSE_ALL |
|
||||
| `tcp_proxy_server_handle_connect(inst,entry,sid,src)` | Создаёт OS TCP сокет, `connect()` к destination, аллоцирует `tcp_proxy_server_conn`, регистрирует diag_timer и коллбэки tcp_conn |
|
||||
| `tcp_proxy_server_handle_data(inst,conn,entry,sid)` | Копирует данные клиента в write_queue OS сокета через pool-аллокацию |
|
||||
| `tcp_proxy_server_handle_close(inst, sid)` | Паузит чтение, сливает read_queue, отправляет shutdown в OS сокет, освобождает соединение если не connected |
|
||||
| `tcp_proxy_server_handle_error(inst, sid)` | Делегирует в `handle_close` (обрабатывается идентично CLOSE) |
|
||||
| `tcp_proxy_server_handle_fin(inst, sid)` | Push-ит FIN в write_queue OS сокета (клиент подтвердил получение FIN от destination) |
|
||||
## Диспетчеризация и очистка
|
||||
|
||||
### Управление соединениями
|
||||
recv_cb принимает владение entry/dgram, CONNECT/DATA handlers также освобождают их.
|
||||
Peer берётся из метаданных router, а не из физического conn.
|
||||
Уведомление router без payload (26 байт) удаляет только потоки данного peer.
|
||||
Это локальное CLOSE_ALL/restart/close-событие канала, не отдельная 9-байтовая команда proxy.
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_proxy_server_find_conn(ctx, sid)` | Линейный поиск соединения по stream_id в связном списке |
|
||||
| `tcp_proxy_server_conn_free(rc)` | Полное освобождение: защита от double-free, отмена таймеров, отправка pending close/error если надо, удаление из списка, `tcp_conn_destroy()`, u_free |
|
||||
conn_free сразу помечает freed, удаляет поток из списка, отменяет waiter/control retry,
|
||||
DATA retry и диагностический таймер, уничтожает TCP. Память структуры освобождается
|
||||
через uasync_call_soon, чтобы текущая синхронная доставка могла проверить freed.
|
||||
Указатель нельзя использовать как живой поток после conn_free.
|
||||
|
||||
### Внутренние callback-и (статические)
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `send_msg(inst, dst, subcmd, sid, data, len, force)` | Формирует ETCP-пакет (svc_id + subcmd + sid + data) и отправляет через `etcp_route_send()` |
|
||||
| `send_close(rc)` | Отправляет CLOSE клиенту, при неудаче запускает экспоненциальный ретрай через `close_timer` |
|
||||
| `send_error(rc)` | Аналогично `send_close`, но шлёт ERROR |
|
||||
| `send_fin(rc)` | Отправляет FIN клиенту (без ретрая) |
|
||||
| `on_fin_cb(tc, arg)` | OS сокет получил FIN → машина состояний: если локальный FIN тоже → close; если write pending → ждать flush; если read pending → отложить FIN |
|
||||
| `on_flushed_cb(tc, arg)` | Write очередь OS сокета опустела → отправить FIN клиенту или закрыть если оба FIN получены |
|
||||
| `on_error_cb(tc, err, arg)` | Ошибка OS сокета → отправить ERROR клиенту, освободить соединение |
|
||||
| `on_closed_cb(tc, arg)` | OS сокет полностью закрыт → освободить соединение |
|
||||
| `read_queue_drain_cb(q, arg)` | Дрейн read_queue OS сокета → отправка данных клиенту через ETCP; backpressure: буферизация в tx_buf + pause_waiter + retry_timer |
|
||||
| `pause_resume_cb(q, arg)` | ETCP канал освободился → возобновить дрейн read_queue |
|
||||
| `retry_timer_cb(arg)` | Fallback-таймер 500ms: повтор tx_buf с force=1, при успехе возобновляет read_queue |
|
||||
| `close_retry_cb(arg)` | Экспоненциальный backoff для повторной отправки CLOSE/ERROR (50→5000 tb) |
|
||||
| `diag_timer_cb(arg)` | 1-секундный таймер: логирует TCP буферы ОС, размеры очередей, free-counts pool-ов |
|
||||
|
||||
### Константы
|
||||
|
||||
| Макрос | Значение | Описание |
|
||||
|--------|----------|----------|
|
||||
| `TCP_PROXY_HDR_SIZE` | 6 | svc_id(1) + subcmd(1) + stream_id(4) |
|
||||
| `TCP_PROXY_CONNECT_HDR_SIZE` | 12 | HDR_SIZE + dest_ip(4) + dest_port(2) |
|
||||
| `TCP_PROXY_SUBCMD_CONNECT` | 0x01 | Запрос на создание TCP соединения к destination |
|
||||
| `TCP_PROXY_SUBCMD_DATA` | 0x03 | Данные для отправки в destination |
|
||||
| `TCP_PROXY_SUBCMD_CLOSE` | 0x04 | Клиент закрывает прокси-соединение |
|
||||
| `TCP_PROXY_SUBCMD_ERROR` | 0x05 | Ошибка на стороне клиента |
|
||||
| `TCP_PROXY_SUBCMD_FIN` | 0x06 | Клиент подтвердил FIN от destination |
|
||||
|
||||
### Зависимости
|
||||
|
||||
| Модуль | Использование |
|
||||
|--------|--------------|
|
||||
| `tcp_io.h` | `tcp_conn` — абстракция OS TCP сокета с read/write очередями и memory pool-ами |
|
||||
| `ll_queue.h` | Очереди read_queue/write_queue OS сокета, pause_waiter для backpressure |
|
||||
| `etcp_router.h` | `etcp_router_bind()`, `etcp_route_send()`, `etcp_router_on_send_ready()`, `etcp_router_cancel_send_ready()` |
|
||||
| `u_async.h` | Таймеры (`uasync_set_timeout`, `uasync_cancel_timeout`) |
|
||||
| `udp_proxy.h` | Авто-инициализация при старте сервера |
|
||||
| `icmp_proxy.h` | Авто-инициализация при старте сервера |
|
||||
| `config_parser.h` | Флаг `tcp_proxy_server_enabled` |
|
||||
| `mem.h` | `u_malloc`/`u_free`/`u_calloc` для tx_buf и структур |
|
||||
destroy выключает сервер и снимает только его binding; общие UDP/ICMP-контексты сохраняются.
|
||||
Instance останавливает proxy до разборки UTUN-группы и транспорта; ua обрабатывает отложенные free.
|
||||
Все операции выполняются в uasync. Каждую секунду диагностический таймер пишет
|
||||
размеры TCP-очередей, наличие write_buf, кеши пулов и OS socket buffers на уровне proxy=trace.
|
||||
+97
-40
@@ -10,14 +10,13 @@
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/ll_queue.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
@@ -27,6 +26,19 @@
|
||||
static void flow_expire_timer_cb(void* arg);
|
||||
static void flow_expire(struct udp_proxy_ctx* ctx);
|
||||
|
||||
// Удаляет опубликованный или ещё создаваемый flow, закрывая все его ресурсы.
|
||||
static void flow_free(struct udp_flow* f) {
|
||||
struct udp_proxy_ctx* ctx = f->ctx;
|
||||
struct udp_flow** prev = &ctx->flows;
|
||||
while (*prev && *prev != f) prev = &(*prev)->next;
|
||||
if (*prev) { *prev = f->next; ctx->flow_count--; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "udp_proxy: free flow peer=%016llx fd=%d remaining=%d",
|
||||
(unsigned long long)f->client_node_id, (int)f->sock, ctx->flow_count);
|
||||
if (f->read_id) uasync_remove_socket_t(ctx->ua, f->sock);
|
||||
if (f->sock != SOCKET_INVALID) socket_close_wrapper(f->sock);
|
||||
u_free(f);
|
||||
}
|
||||
|
||||
static struct udp_flow* flow_find(struct udp_flow* head, uint64_t client_node_id,
|
||||
uint32_t src_ip, uint16_t src_port,
|
||||
uint32_t dst_ip, uint16_t dst_port) {
|
||||
@@ -44,7 +56,16 @@ static void flow_read_cb(socket_t sock, void* arg) {
|
||||
uint8_t buf[65507];
|
||||
struct sockaddr_in from; socklen_t flen = sizeof(from);
|
||||
ssize_t n = recvfrom(f->sock, buf, sizeof(buf), 0, (struct sockaddr*)&from, &flen);
|
||||
if (n < 0) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: recv failed errno=%d", errno); return; }
|
||||
if (n < 0) {
|
||||
int err = socket_get_error();
|
||||
if (err == EINTR || err == EAGAIN || err == EWOULDBLOCK) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "udp_proxy: read not ready fd=%d error=%d", (int)f->sock, err); return;
|
||||
}
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: recv failed peer=%016llx fd=%d error=%d (%s)",
|
||||
(unsigned long long)f->client_node_id, (int)f->sock, err, socket_strerror(err));
|
||||
if (err == EBADF || err == ENOTSOCK) flow_free(f);
|
||||
return;
|
||||
}
|
||||
if (from.sin_addr.s_addr != f->dst_ip || from.sin_port != f->dst_port) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: unexpected reply source"); return;
|
||||
}
|
||||
@@ -53,9 +74,9 @@ static void flow_read_cb(socket_t sock, void* arg) {
|
||||
|
||||
// Обернуть ответ: меняем src/dst для восстановления на стороне клиента
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) return;
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: reply entry allocation failed"); return; }
|
||||
e->dgram = u_malloc(UDP_PROXY_HDR_SIZE + n);
|
||||
if (!e->dgram) { queue_entry_free(e); return; }
|
||||
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: reply allocation failed bytes=%zd", n); queue_entry_free(e); return; }
|
||||
e->dgram[0] = ETCP_RT_ID_UDP_PROXY;
|
||||
e->dgram[1] = UDP_PROXY_SUBCMD_REPLY;
|
||||
// Ответ src = оригинальный dst_ip:dest_port
|
||||
@@ -67,7 +88,8 @@ static void flow_read_cb(socket_t sock, void* arg) {
|
||||
memcpy(e->dgram + UDP_PROXY_HDR_SIZE, buf, n);
|
||||
e->len = UDP_PROXY_HDR_SIZE + n;
|
||||
|
||||
etcp_route_send(ctx->inst, TOPO_GROUP_UTUN, f->client_node_id, e, 0, 0);
|
||||
if (etcp_route_send(ctx->inst, TOPO_GROUP_UTUN, f->client_node_id, e, 0, 0) < 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: reply router send failed bytes=%zd", n);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
@@ -76,7 +98,9 @@ static void flow_read_cb(socket_t sock, void* arg) {
|
||||
static void exit_handle_data(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
|
||||
struct udp_proxy_ctx* ctx = inst ? inst->udp_proxy : NULL;
|
||||
if (!inst || !ctx || !inst->tcp_proxy_server.enabled || entry->len < UDP_PROXY_RECV_HDR_SIZE) goto drop;
|
||||
if (!inst || !ctx || !inst->tcp_proxy_server.enabled || entry->len < UDP_PROXY_RECV_HDR_SIZE) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid request len=%u context=%p", entry->len, (void*)ctx); goto drop;
|
||||
}
|
||||
|
||||
uint64_t client_node_id; memcpy(&client_node_id, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
|
||||
uint32_t src_ip; memcpy(&src_ip, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, 4);
|
||||
@@ -85,31 +109,54 @@ static void exit_handle_data(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
uint16_t dst_port; memcpy(&dst_port, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 11, 2);
|
||||
uint8_t* payload = entry->dgram + UDP_PROXY_RECV_HDR_SIZE;
|
||||
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
|
||||
if (payload_len > 65507) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: oversized request len=%zu", payload_len); goto drop; }
|
||||
|
||||
struct udp_flow* f = flow_find(ctx->flows, client_node_id, src_ip, src_port, dst_ip, dst_port);
|
||||
if (!f) {
|
||||
f = u_calloc(1, sizeof(struct udp_flow));
|
||||
if (!f) goto drop;
|
||||
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: flow allocation failed"); goto drop; }
|
||||
f->ctx = ctx;
|
||||
f->sock = SOCKET_INVALID;
|
||||
f->client_node_id = client_node_id; f->src_ip = src_ip; f->src_port = src_port;
|
||||
f->dst_ip = dst_ip; f->dst_port = dst_port;
|
||||
f->ua = ctx->ua; f->created_tb = get_time_tb(); f->last_activity_tb = f->created_tb;
|
||||
|
||||
f->sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (f->sock == SOCKET_INVALID) { u_free(f); DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: socket failed"); goto drop; }
|
||||
socket_set_nonblocking(f->sock);
|
||||
if (f->sock == SOCKET_INVALID) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: socket failed error=%d", socket_get_error()); flow_free(f); goto drop;
|
||||
}
|
||||
if (socket_set_nonblocking(f->sock) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: nonblocking failed fd=%d error=%d", (int)f->sock, socket_get_error());
|
||||
flow_free(f); goto drop;
|
||||
}
|
||||
struct sockaddr_in bind_addr = {.sin_family = AF_INET, .sin_addr = {.s_addr = INADDR_ANY}, .sin_port = 0};
|
||||
bind(f->sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr));
|
||||
if (bind(f->sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr)) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: bind ANY:0 failed fd=%d error=%d", (int)f->sock, socket_get_error());
|
||||
flow_free(f); goto drop;
|
||||
}
|
||||
f->read_id = uasync_add_socket_t(ctx->ua, f->sock, flow_read_cb, NULL, NULL, "udp_flow", f);
|
||||
if (!f->read_id) { socket_close_wrapper(f->sock); u_free(f); goto drop; }
|
||||
f->next = ctx->flows; ctx->flows = f; ctx->flow_count++;
|
||||
if (!ctx->expire_timer)
|
||||
if (!f->read_id) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: read registration failed"); flow_free(f); goto drop; }
|
||||
if (!ctx->expire_timer) {
|
||||
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->flow_timeout_tb, ctx, flow_expire_timer_cb, "udp_expire");
|
||||
if (!ctx->expire_timer) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: expiry timer failed"); flow_free(f); goto drop; }
|
||||
}
|
||||
f->next = ctx->flows; ctx->flows = f; ctx->flow_count++;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "udp_proxy: flow ready peer=%016llx src=%08x:%u dst=%08x:%u fd=%d flows=%d",
|
||||
(unsigned long long)client_node_id, ntohl(src_ip), ntohs(src_port), ntohl(dst_ip), ntohs(dst_port),
|
||||
(int)f->sock, ctx->flow_count);
|
||||
}
|
||||
f->last_activity_tb = get_time_tb();
|
||||
|
||||
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_addr = {.s_addr = dst_ip}, .sin_port = dst_port};
|
||||
sendto(f->sock, payload, payload_len, 0, (struct sockaddr*)&addr, sizeof(addr));
|
||||
ssize_t sent;
|
||||
do { sent = sendto(f->sock, payload, payload_len, 0, (struct sockaddr*)&addr, sizeof(addr)); } while (sent < 0 && errno == EINTR);
|
||||
if (sent != (ssize_t)payload_len) {
|
||||
int err = sent < 0 ? socket_get_error() : 0;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: dropped datagram peer=%016llx src=%08x:%u dst=%08x:%u len=%zu sent=%zd error=%d",
|
||||
(unsigned long long)client_node_id, ntohl(src_ip), ntohs(src_port), ntohl(dst_ip), ntohs(dst_port),
|
||||
payload_len, sent, err);
|
||||
if (err == EBADF || err == ENOTSOCK) flow_free(f);
|
||||
} else DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "udp_proxy: sent peer=%016llx bytes=%zu", (unsigned long long)client_node_id, payload_len);
|
||||
|
||||
drop:
|
||||
queue_dgram_free(entry); queue_entry_free(entry);
|
||||
@@ -120,7 +167,7 @@ drop:
|
||||
// ====================================================================
|
||||
static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
if (entry->len < UDP_PROXY_RECV_HDR_SIZE) { queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
const uint8_t* d = entry->dgram;
|
||||
uint8_t* d = entry->dgram;
|
||||
uint32_t src_ip, dst_ip;
|
||||
uint16_t src_port, dst_port;
|
||||
// В сообщении: src_ip:src_port = оригинальный dst, dst_ip:dst_port = оригинальный src
|
||||
@@ -132,7 +179,8 @@ static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry)
|
||||
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
|
||||
|
||||
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
|
||||
udp_proxy_deliver_reply(inst, src_ip, src_port, dst_ip, dst_port, payload, payload_len);
|
||||
if (udp_proxy_deliver_reply(inst, src_ip, src_port, dst_ip, dst_port, payload, payload_len) < 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: TUN delivery failed bytes=%zu", payload_len);
|
||||
queue_dgram_free(entry); queue_entry_free(entry);
|
||||
}
|
||||
|
||||
@@ -164,11 +212,13 @@ int udp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id,
|
||||
uint32_t src_ip, uint16_t src_port,
|
||||
uint32_t dst_ip, uint16_t dst_port,
|
||||
const uint8_t* payload, size_t payload_len) {
|
||||
if (!inst || !inst->udp_proxy || payload_len > 65507) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid send"); return -1; }
|
||||
if (!inst || !inst->udp_proxy || payload_len > 65507 || (payload_len && !payload)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid send len=%zu", payload_len); return -1;
|
||||
}
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) return -1;
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: request entry allocation failed"); return -1; }
|
||||
e->dgram = u_malloc(UDP_PROXY_HDR_SIZE + payload_len);
|
||||
if (!e->dgram) { queue_entry_free(e); return -1; }
|
||||
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: request allocation failed"); queue_entry_free(e); return -1; }
|
||||
e->dgram[0] = ETCP_RT_ID_UDP_PROXY;
|
||||
e->dgram[1] = UDP_PROXY_SUBCMD_REQUEST;
|
||||
memcpy(e->dgram + 2, &src_ip, 4);
|
||||
@@ -187,12 +237,14 @@ int udp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
uint32_t src_ip, uint16_t src_port,
|
||||
uint32_t dst_ip, uint16_t dst_port,
|
||||
const uint8_t* payload, size_t payload_len) {
|
||||
if (!inst || !inst->tcp_proxy_client || !inst->tcp_proxy_client->tun || payload_len > 65506) return -1;
|
||||
if (!inst || !inst->tcp_proxy_client || !inst->tcp_proxy_client->tun || payload_len > 65506 || (payload_len && !payload)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid TUN delivery len=%zu", payload_len); return -1;
|
||||
}
|
||||
|
||||
// Собрать IP/UDP ответный пакет
|
||||
size_t ip_len = 20 + 8 + payload_len;
|
||||
uint8_t* pkt = u_malloc(ip_len);
|
||||
if (!pkt) return -1;
|
||||
if (!pkt) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: IP packet allocation failed"); return -1; }
|
||||
|
||||
memset(pkt, 0, 28); // IPv4 UDP checksum=0: проверка отключена явно
|
||||
pkt[0] = 0x45; pkt[8] = 64; pkt[9] = IPPROTO_UDP;
|
||||
@@ -212,7 +264,7 @@ int udp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
if (payload_len > 0) memcpy(pkt + 28, payload, payload_len);
|
||||
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) { u_free(pkt); return -1; }
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: TUN entry allocation failed"); u_free(pkt); return -1; }
|
||||
e->dgram = u_malloc(1 + ip_len);
|
||||
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: reply allocation failed"); u_free(pkt); queue_entry_free(e); return -1; }
|
||||
e->dgram[0] = 4; memcpy(e->dgram + 1, pkt, ip_len); e->len = 1 + ip_len;
|
||||
@@ -223,10 +275,15 @@ int udp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
|
||||
static void flow_expire_timer_cb(void* arg) {
|
||||
struct udp_proxy_ctx* ctx = (struct udp_proxy_ctx*)arg;
|
||||
flow_expire(ctx);
|
||||
ctx->expire_timer = NULL;
|
||||
if (ctx->flows)
|
||||
flow_expire(ctx);
|
||||
if (ctx->flows) {
|
||||
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->flow_timeout_tb, ctx, flow_expire_timer_cb, "udp_expire");
|
||||
if (!ctx->expire_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: cannot renew expiry timer, closing %d flows", ctx->flow_count);
|
||||
while (ctx->flows) flow_free(ctx->flows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void flow_expire(struct udp_proxy_ctx* ctx) {
|
||||
@@ -234,9 +291,7 @@ static void flow_expire(struct udp_proxy_ctx* ctx) {
|
||||
while (*prev) {
|
||||
struct udp_flow* f = *prev;
|
||||
if (now - f->last_activity_tb > ctx->flow_timeout_tb) {
|
||||
*prev = f->next; ctx->flow_count--;
|
||||
if (f->read_id) { uasync_remove_socket_t(f->ua, f->sock); f->read_id = NULL; }
|
||||
socket_close_wrapper(f->sock); u_free(f);
|
||||
flow_free(f);
|
||||
} else prev = &f->next;
|
||||
}
|
||||
}
|
||||
@@ -245,19 +300,24 @@ static void flow_expire(struct udp_proxy_ctx* ctx) {
|
||||
// Публичные init/destroy
|
||||
// ====================================================================
|
||||
int udp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
|
||||
if (!inst) return -1;
|
||||
if (inst->udp_proxy) return 0;
|
||||
if (!inst || !ua) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid init"); return -1; }
|
||||
if (inst->udp_proxy) {
|
||||
if (inst->udp_proxy->ua == ua) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: init attempted with another event loop"); return -1;
|
||||
}
|
||||
if (inst->router_bindings.callbacks[ETCP_RT_ID_UDP_PROXY]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: service already belongs to another owner"); return -1;
|
||||
}
|
||||
struct udp_proxy_ctx* ctx = u_calloc(1, sizeof(struct udp_proxy_ctx));
|
||||
if (!ctx) return -1;
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: context allocation failed"); return -1; }
|
||||
ctx->inst = inst; ctx->ua = ua;
|
||||
ctx->flow_timeout_tb = UDP_FLOW_TIMEOUT_TB;
|
||||
ctx->expire_timer = NULL;
|
||||
ctx->is_exit = inst->tcp_proxy_server.enabled;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_UDP_PROXY, udp_proxy_recv_cb) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: router binding failed"); u_free(ctx); return -1;
|
||||
}
|
||||
inst->udp_proxy = ctx;
|
||||
|
||||
etcp_router_bind(inst, ETCP_RT_ID_UDP_PROXY, udp_proxy_recv_cb);
|
||||
ctx->initialized = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "udp_proxy initialized (exit=%d)", ctx->is_exit);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "udp_proxy: shared context ready node=%016llx", (unsigned long long)inst->node_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -266,10 +326,7 @@ void udp_proxy_destroy(struct UTUN_INSTANCE* inst) {
|
||||
if (!ctx) return;
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_UDP_PROXY);
|
||||
if (ctx->expire_timer) { uasync_cancel_timeout(ctx->ua, ctx->expire_timer); ctx->expire_timer = NULL; }
|
||||
struct udp_flow* f = ctx->flows;
|
||||
while (f) { struct udp_flow* n = f->next;
|
||||
if (f->read_id) { uasync_remove_socket_t(ctx->ua, f->sock); f->read_id = NULL; }
|
||||
socket_close_wrapper(f->sock); u_free(f); f = n; }
|
||||
while (ctx->flows) flow_free(ctx->flows);
|
||||
u_free(ctx); inst->udp_proxy = NULL;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "udp_proxy destroyed");
|
||||
}
|
||||
+34
-26
@@ -1,6 +1,9 @@
|
||||
/* UDP из TUN через UTUN-router: клиент отправляет REQUEST, exit открывает сокет для кортежа адресов
|
||||
* и возвращает REPLY. Потоки exit удаляются по таймауту неактивности.
|
||||
* init/destroy и callbacks — в uasync; контекст принадлежит inst, роль берётся из tcp_proxy_server.enabled. */
|
||||
* init/destroy и callbacks — в uasync; inst владеет контекстом в inst->udp_proxy.
|
||||
* Приём REQUEST проверяет текущее server.enabled,
|
||||
* а REPLY — наличие tcp_proxy_client и совпадение peer с via_node_id. Один inst может быть и клиентом, и exit.
|
||||
* API принимает IP/порты в network byte order, payload заимствован только на время вызова. */
|
||||
#ifndef UDP_PROXY_H
|
||||
#define UDP_PROXY_H
|
||||
|
||||
@@ -22,55 +25,60 @@ struct ETCP_CONN;
|
||||
#define UDP_PROXY_SUBCMD_REQUEST 0x01
|
||||
#define UDP_PROXY_SUBCMD_REPLY 0x02
|
||||
|
||||
// Заголовок сообщения на отправке (включая байт svc_id; src/dst node_id добавляет роутер):
|
||||
// Сообщение для etcp_route_send (не полный wire packet; метаданные приёма добавляет router):
|
||||
// svc_id(1) + subcmd(1) + src_ip(4) + src_port(2) + dst_ip(4) + dst_port(2) + payload
|
||||
#define UDP_PROXY_HDR_SIZE 14
|
||||
// Полный заголовок на приёме (после router_deliver):
|
||||
// svc_id(1) + src_node_id(8) + dst_node_id(8) + subcmd(1) + src_ip(4) + src_port(2) + dst_ip(4) + dst_port(2)
|
||||
#define UDP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 30
|
||||
// router header(26: svc/src/dst/flags/group) + subcmd(1) + src_ip(4) + src_port(2) + dst_ip(4) + dst_port(2)
|
||||
#define UDP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 13) // 39
|
||||
|
||||
// Один UDP поток (сторона exit узла: сопоставляет client_node_id + кортеж адресов с OS сокетом)
|
||||
struct udp_flow {
|
||||
struct udp_flow* next;
|
||||
struct udp_proxy_ctx* ctx;
|
||||
uint64_t client_node_id;
|
||||
uint32_t src_ip;
|
||||
uint16_t src_port;
|
||||
uint32_t dst_ip;
|
||||
uint16_t dst_port;
|
||||
socket_t sock;
|
||||
void* read_id;
|
||||
struct UASYNC* ua;
|
||||
uint64_t created_tb;
|
||||
uint64_t last_activity_tb;
|
||||
struct udp_flow* next; // следующий flow exit
|
||||
struct udp_proxy_ctx* ctx; // заимствованный владелец
|
||||
uint64_t client_node_id; // конечный ingress-узел
|
||||
uint32_t src_ip; // исходный адрес приложения, network byte order
|
||||
uint16_t src_port; // исходный порт приложения, network byte order
|
||||
uint32_t dst_ip; // адрес назначения, network byte order
|
||||
uint16_t dst_port; // порт назначения, network byte order
|
||||
socket_t sock; // собственный неблокирующий UDP, bind ANY:0
|
||||
void* read_id; // регистрация сокета в ua
|
||||
struct UASYNC* ua; // заимствованный event loop
|
||||
uint64_t created_tb; // время создания (tb = 0.1 мс)
|
||||
uint64_t last_activity_tb; // последний REQUEST или ответ от ожидаемого dst_ip:dst_port
|
||||
};
|
||||
|
||||
struct udp_proxy_ctx {
|
||||
int initialized;
|
||||
int is_exit; // 1=exit узел (создаёт сокеты), 0=клиент (шлёт через etcp)
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct UASYNC* ua;
|
||||
struct udp_flow* flows;
|
||||
int flow_count;
|
||||
uint64_t flow_timeout_tb;
|
||||
struct UTUN_INSTANCE* inst; // заимствованный instance-владелец
|
||||
struct UASYNC* ua; // заимствованный event loop
|
||||
struct udp_flow* flows; // собственные flows exit
|
||||
int flow_count; // число flows
|
||||
uint64_t flow_timeout_tb; // период проверки и порог неактивности, default=600000 (60 с)
|
||||
void* expire_timer; // периодический таймер очистки истёкших потоков
|
||||
};
|
||||
|
||||
|
||||
// Создать per-instance контекст и bind UDP service; повторный init существующего контекста даёт 0.
|
||||
// inst/ua должны жить до destroy. 0 = контекст и binding готовы, -1 = ошибка без публикации нового контекста.
|
||||
// TCP client/server этот контекст не создают/уничтожают: им владеет instance.
|
||||
int udp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua);
|
||||
// Снять binding, отменить таймер, закрыть flows и обнулить inst->udp_proxy; NULL/нет контекста допустимы.
|
||||
void udp_proxy_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Exit узел: принимает REQUEST, создаёт сокет, шлёт данные
|
||||
// Сторона клиента: принимает REPLY, доставляет на TUN
|
||||
// Сторона клиента: принимает REPLY от настроенного exit, доставляет на TUN.
|
||||
// conn->instance задаёт владельца; callback принимает владение entry/dgram при любом исходе.
|
||||
void udp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
|
||||
// Сторона клиента: принять IP/UDP пакет с TUN, отправить через etcp_route_send
|
||||
// Скопировать payload (0..65507 байт) в REQUEST и отправить в UTUN, force=0/mode=0.
|
||||
// Требует inst->udp_proxy; при payload_len>0 payload не NULL. 0 = принято router, <0 = ошибка.
|
||||
int udp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id,
|
||||
uint32_t src_ip, uint16_t src_port,
|
||||
uint32_t dst_ip, uint16_t dst_port,
|
||||
const uint8_t* payload, size_t payload_len);
|
||||
|
||||
// Сторона клиента: доставить ответ на TUN (вызывается из recv_cb)
|
||||
// Собрать IPv4/UDP src→dst и поставить в tun->input_queue (префикс версии 4); UDP checksum=0.
|
||||
// Требует inst->tcp_proxy_client->tun; payload=0..65506. 0 = поставлено в очередь, -1 = ошибка.
|
||||
int udp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
|
||||
uint32_t src_ip, uint16_t src_port,
|
||||
uint32_t dst_ip, uint16_t dst_port,
|
||||
|
||||
+44
-63
@@ -1,73 +1,54 @@
|
||||
# UDP Proxy (udp_proxy)
|
||||
# UDP proxy
|
||||
|
||||
## 1. Назначение
|
||||
Проксирование UDP-датаграмм между клиентским и exit-узлом по протоколу ETCP. Клиентский узел
|
||||
принимает UDP-пакеты с TUN-интерфейса, инкапсулирует их и отправляет через `etcp_route_send`
|
||||
на exit-узел. Exit-узел создаёт нативные UDP-сокеты для каждого потока (5-tuple) и ретранслирует
|
||||
датаграммы во внешнюю сеть. Ответы проходят обратный путь — от exit к клиенту и затем в TUN.
|
||||
`udp_proxy.c/h` передаёт UDP между TUN клиента и обычными UDP-сокетами exit через
|
||||
`ETCP_RT_ID_UDP_PROXY` в `TOPO_GROUP_UTUN`. Общие форматы и ограничения:
|
||||
[doc/proxy_protocol.md](../../doc/proxy_protocol.md). Контракты API и полей — в
|
||||
[udp_proxy.h](udp_proxy.h).
|
||||
|
||||
Один экземпляр контекста на процесс (глобальный `g_udp_ctx`), режим определяется флагом
|
||||
`inst->tcp_proxy_server.enabled`: включён → `is_exit=1`, иначе `is_exit=0`.
|
||||
## Контекст и жизненный цикл
|
||||
|
||||
## 2. Как пользоваться
|
||||
Контекст принадлежит `inst->udp_proxy`, глобального контекста нет. `init(inst, ua)`
|
||||
создаёт его вместе с router binding, повторный init с тем же ua возвращает 0, сохраняя контекст.
|
||||
Контекст публикуется только после успеха; TCP client/server им не управляют. Диспетчер проверяет текущее
|
||||
`inst->tcp_proxy_server.enabled` для REQUEST. REPLY принимается только при наличии
|
||||
`tcp_proxy_client` и совпадении peer с его `via_node_id`. Один instance может совмещать роли.
|
||||
|
||||
### Инициализация
|
||||
```c
|
||||
udp_proxy_init(inst, ua); // exit или клиент — определяется автоматически
|
||||
```
|
||||
При инициализации регистрируется коллбэк `udp_proxy_recv_cb` в `etcp_router` для сервиса
|
||||
`ETCP_RT_ID_UDP_PROXY`.
|
||||
Exit индексирует flows линейным списком по
|
||||
`client_node_id + src_ip:src_port + dst_ip:dst_port`. У каждого flow собственный
|
||||
неблокирующий `AF_INET/SOCK_DGRAM`, привязанный к `INADDR_ANY:0`, и read callback в uasync.
|
||||
Ответ принимается только от исходного `dst_ip:dst_port`; в REPLY адреса и порты меняются местами.
|
||||
REQUEST и принятый ответ обновляют `last_activity_tb`.
|
||||
Новый flow публикуется после socket/nonblocking/bind/read registration и создания таймера.
|
||||
Отказ любого этапа освобождает новый flow целиком. Результат sendto проверяется по точной длине:
|
||||
нулевая датаграмма с sent=0 успешна, EINTR повторяется, остальные ошибки/короткий send
|
||||
логируются с peer/tuple/длиной/errno. Датаграмма отбрасывается без добавления очереди повторов;
|
||||
рабочий flow сохраняется, невалидный сокет удаляется.
|
||||
|
||||
### Клиент: отправка датаграммы в exit
|
||||
```c
|
||||
udp_proxy_send_to_exit(inst, exit_node_id, src_ip, src_port, dst_ip, dst_port, payload, len);
|
||||
```
|
||||
Вызывается из обработчика TUN-трафика. Пакет упаковывается в формат ETCP и отправляется
|
||||
через `etcp_route_send`. Ответ приходит асинхронно в `udp_proxy_recv_cb`, который вызывает
|
||||
`udp_proxy_deliver_reply` для записи IP/UDP-пакета обратно в TUN.
|
||||
Таймер запускается при первом flow и проверяет список каждые 600000 tb (60 с),
|
||||
пока flows остаются. Удаляет flow при неактивности **больше** 60 с; удаление происходит
|
||||
на очередном обходе, а не точно в момент достижения порога. `destroy` снимает binding,
|
||||
отменяет таймер, закрывает сокеты/регистрации и обнуляет `inst->udp_proxy`.
|
||||
Все операции выполняются в uasync; `inst` и `ua` заимствуются до destroy.
|
||||
Отказ продления таймера закрывает оставшиеся flows с ERROR, не оставляя их без механизма очистки.
|
||||
|
||||
### Exit: приём датаграмм
|
||||
Exit автоматически принимает REQUEST-ы через `etcp_router`. Для каждого нового потока
|
||||
(уникальный `client_node_id + src_ip:port + dst_ip:port`) создаётся нативный UDP-сокет,
|
||||
который регистрируется в `uasync` на чтение. При получении ответа от внешнего хоста
|
||||
ответ упаковывается и отправляется обратно клиенту.
|
||||
## Сообщения и доставка
|
||||
|
||||
### Очистка
|
||||
```c
|
||||
udp_proxy_destroy(inst); // закрывает все flows, отменяет таймер, unbind из etcp_router
|
||||
```
|
||||
На вход `etcp_route_send` передаётся:
|
||||
`svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`.
|
||||
REQUEST=1, REPLY=2. Заголовок отправки — 14 байт. Router callback получает 26 байт метаданных
|
||||
router перед подкомандой, поэтому заголовок приёма — 39 байт. Отдельного `sender_node_id`
|
||||
в UDP payload нет: peer берётся из метаданных router. Адреса и порты — network byte order.
|
||||
|
||||
### Нюансы
|
||||
- Потоки истекают через 60 секунд неактивности (`UDP_FLOW_TIMEOUT_TB`). Таймер `expire_timer`
|
||||
запускается при создании первого потока, перезапускается после каждой очистки.
|
||||
- UDP-сокеты на exit привязываются к `INADDR_ANY:0` (bind), трафик отправляется через `sendto`.
|
||||
- Ответы в TUN на клиенте доставляются через `inst->tcp_proxy_client->tun->input_queue`.
|
||||
`send_to_exit` требует созданный UDP-контекст, копирует заимствованный payload
|
||||
длиной 0..65507 и возвращает результат router: 0 означает принятие в очередь,
|
||||
отрицательное значение — ошибку. Собственного окна/повтора UDP у модуля нет;
|
||||
отправка использует `force=0, mode=0`.
|
||||
|
||||
## 3. API
|
||||
`deliver_reply` требует `inst->tcp_proxy_client->tun`, допускает payload 0..65506,
|
||||
собирает IPv4/UDP с TTL 64, пересчитывает IPv4 checksum и оставляет UDP checksum=0.
|
||||
Пакет получает однобайтовый префикс версии 4 и ставится в `tun->input_queue`.
|
||||
Возврат 0 означает постановку в очередь; -1 — отсутствие TUN, недопустимый размер
|
||||
или ошибку выделения. Входной payload остаётся у вызывающего кода.
|
||||
|
||||
### Структуры
|
||||
| Структура | Назначение |
|
||||
|-----------|-----------|
|
||||
| `struct udp_flow` | Состояние одного UDP-потока на exit: 5-tuple, нативный сокет, uasync read_id, времена создания и последней активности. Связный список. |
|
||||
| `struct udp_proxy_ctx` | Глобальный контекст: флаг is_exit, inst, ua, список flows, таймаут истечения, таймер очистки. |
|
||||
|
||||
### Формат пакета
|
||||
| Смещение | Размер | Поле |
|
||||
|----------|--------|------|
|
||||
| 0 | 1 | `svc_id` = `ETCP_RT_ID_UDP_PROXY` |
|
||||
| 1 | 1 | `subcmd` = `UDP_PROXY_SUBCMD_DATA` (0x01) |
|
||||
| 2 | 8 | `sender_node_id` |
|
||||
| 10 | 4 | `src_ip` (клиент→exit: IP отправителя; exit→клиент: dst_ip) |
|
||||
| 14 | 2 | `src_port` |
|
||||
| 16 | 4 | `dst_ip` (клиент→exit: IP назначения; exit→клиент: src_ip) |
|
||||
| 20 | 2 | `dst_port` |
|
||||
| 22 | N | `payload` (UDP-датаграмма) |
|
||||
|
||||
### Функции
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `udp_proxy_init(inst, ua)` | Выделяет контекст, определяет is_exit, регистрирует коллбэк в etcp_router. Возвращает 0 при успехе. |
|
||||
| `udp_proxy_destroy(inst)` | Закрывает все потоки, сокеты, отменяет таймеры, отвязывает коллбэк, освобождает память. |
|
||||
| `udp_proxy_recv_cb(conn, entry)` | Единый коллбэк etcp_router. По subcmd направляет в exit_handle_data (exit) или client_handle_reply (клиент). |
|
||||
| `udp_proxy_send_to_exit(inst, ...)` | Клиент: упаковывает UDP-датаграмму и отправляет через etcp_route_send на exit. |
|
||||
| `udp_proxy_deliver_reply(inst, ...)` | Клиент: собирает IP/UDP-пакет и помещает в TUN input_queue для доставки приложению. |
|
||||
`recv_cb` принимает владение entry/dgram при любом исходе. `init` возвращает -1 при
|
||||
неверных аргументах, другом ua, занятом binding или ошибке выделения/регистрации.
|
||||
+5
-3
@@ -90,10 +90,12 @@ int radio_init(struct TOPO_GROUP* group) {
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: alloc failed", RADIO_ID); return -1; }
|
||||
ctx->group = group;
|
||||
ctx->stream_id = (uint16_t)(get_time_tb() & 0xFFFF);
|
||||
group->radio = ctx;
|
||||
|
||||
ctx->talker_timer = uasync_set_timeout(group->instance->ua, RADIO_TALKER_TIMEOUT_TB, ctx, radio_talker_timer_cb, "radio_talker");
|
||||
if (!ctx->talker_timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: init talker_timer failed grp=%016llx", RADIO_ID, (unsigned long long)group->group_id);
|
||||
if (!ctx->talker_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: init talker_timer failed grp=%016llx", RADIO_ID, (unsigned long long)group->group_id);
|
||||
u_free(ctx); return -1;
|
||||
}
|
||||
group->radio = ctx;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: init ok grp=%016llx", RADIO_ID, (unsigned long long)group->group_id);
|
||||
return 0;
|
||||
|
||||
@@ -22,11 +22,12 @@ enum address_inbound {
|
||||
enum address_profile_source { ADDRESS_SOURCE_NONE, ADDRESS_SOURCE_ETCP, ADDRESS_SOURCE_STUN };
|
||||
|
||||
struct address_profile {
|
||||
uint8_t mapping;
|
||||
uint8_t inbound;
|
||||
uint8_t mapping; /* enum address_mapping: UNKNOWN, без трансляции, EIM или зависимый mapping. */
|
||||
uint8_t inbound; /* enum address_inbound: UNKNOWN, OPEN либо нужен исходящий контакт. */
|
||||
uint8_t source; /* ETCP — измерение рабочего endpoint; STUN — предварительный EIM? с неизвестным входом. */
|
||||
};
|
||||
|
||||
/* Постоянные строки для диагностики; неизвестное значение возвращает INVALID. */
|
||||
static inline const char *address_mapping_name(uint8_t value) {
|
||||
static const char *const names[] = {"UNKNOWN", "UNCHANGED", "EIM", "DEPENDENT"};
|
||||
return value <= ADDRESS_MAP_DEPENDENT ? names[value] : "INVALID";
|
||||
@@ -42,6 +43,8 @@ static inline const char *address_profile_source_name(uint8_t value) {
|
||||
return value <= ADDRESS_SOURCE_STUN ? names[value] : "INVALID";
|
||||
}
|
||||
|
||||
/* 1 — поля входят в enum и при source=NONE оба свойства UNKNOWN; 0 — иначе/NULL.
|
||||
* Проверяет представление, а не достоверность измерения или сочетания свойств. */
|
||||
static inline int address_profile_valid(const struct address_profile *profile) {
|
||||
return profile && profile->mapping <= ADDRESS_MAP_DEPENDENT && profile->inbound <= ADDRESS_IN_CONTACT &&
|
||||
profile->source <= ADDRESS_SOURCE_STUN &&
|
||||
|
||||
@@ -48,9 +48,10 @@ typedef void (*conn_mgr_cb_t)(struct CONN_MGR_HANDLE* h,
|
||||
|
||||
/* ═══════ lifecycle ═══════ */
|
||||
|
||||
/* Создать менеджер группы и его фоновые проверки; NULL при ошибке. */
|
||||
/* Создать менеджер группы с обоими фоновыми таймерами; NULL с очисткой ресурсов при ошибке. */
|
||||
struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group);
|
||||
/* Остановить проверки и освободить менеджер. Все клиентские handles закрыть ДО destroy. */
|
||||
/* Остановить проверки и освободить менеджер вне его callbacks.
|
||||
* Живые клиентские handles отсоединяются, callbacks отменяются; владельцы закрывают их сами. */
|
||||
void conn_mgr_destroy(struct CONN_MGR* mgr);
|
||||
/* Приём управляющего пакета от etcp_router; забирает entry. */
|
||||
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
@@ -60,9 +61,14 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
|
||||
/**
|
||||
* Параллельные пробы: DIRECT сразу, REVERSE через 0/500мс, INDIRECT через 0/1000мс от общего старта.
|
||||
* Посредник используется сразу после проверки; поздний прямой путь заменяет его без повторного UP.
|
||||
* Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен.
|
||||
* Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен, cb_arg заимствован.
|
||||
* Отсутствующая группа создаётся как CHAT с десятичным channel_id=group_id.
|
||||
* При отсутствии узла загружает запись из общей SQLite во временный групповой узел;
|
||||
* отказ запуска очищает приобретённые ссылки. Последний CM-владелец убирает такой
|
||||
* узел, если у него не появились BGP-пути.
|
||||
* Ошибка до запуска может возвращаться без callback. В callback h может быть NULL.
|
||||
* Успешно выданный handle закрывает вызывающий, в том числе после TIMEOUT.
|
||||
* out_handle необязателен: иначе вызывающий получает handle из callback.
|
||||
* При ошибке *out_handle=NULL. Успешный handle закрывает вызывающий, в том числе после TIMEOUT.
|
||||
*/
|
||||
int conn_mgr_open(struct UTUN_INSTANCE* inst,
|
||||
uint64_t group_id,
|
||||
@@ -73,7 +79,9 @@ int conn_mgr_open(struct UTUN_INSTANCE* inst,
|
||||
/** Закрыть свой handle и отменить его callbacks, в том числе отложенный UP.
|
||||
* Допустимо из callback (также закрытие другого handle). После close указатель больше не использовать.
|
||||
* Очистка внутренней записи CM отсоединяет handles, но владельцы всё равно обязаны вызвать close.
|
||||
* Последний handle освобождает ресурсы CM; чужие NCD-владельцы сохраняют транспорт. */
|
||||
* Последний handle снимает запись CM; чужие NCD-владельцы сохраняют транспорт.
|
||||
* Освобождение памяти закрытых объектов выполняют фоновые callbacks/destroy менеджера,
|
||||
* без новых выделений памяти в close. После destroy отсоединённый handle допускает только close. */
|
||||
void conn_mgr_close(struct CONN_MGR_HANDLE* h);
|
||||
|
||||
/* ═══════ доступ к соединению ═══════ */
|
||||
@@ -81,13 +89,13 @@ void conn_mgr_close(struct CONN_MGR_HANDLE* h);
|
||||
/** Заимствованный READY прямой ETCP_CONN; NULL для INDIRECT, ожидания или отсоединённого handle. */
|
||||
struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h);
|
||||
|
||||
/** Передать e->dgram через маршрутизатор CM. Освобождает entry при ненулевом h;
|
||||
* буфер dgram отдельно не освобождает. При h==NULL entry остаётся у вызывающего. */
|
||||
int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e);
|
||||
/* Данные сервиса отправляются через etcp_route_send/etcp_router_conn_send с его svc_id.
|
||||
* CM handle всё это время сохраняет потребность в доставке; политика сервиса задаёт шифрование. */
|
||||
|
||||
/* ═══════ мониторинг (вызывается извне) ═══════ */
|
||||
|
||||
/** Обновить RTT кандидата-посредника (≤3, сортировка по возрастанию). */
|
||||
/** Обновить кандидата (≤3, сортировка по RTT, затем node_id); self/0 и RTT=0/FFFF игнорируются.
|
||||
* При заполнении списка READY-кандидаты вытесняют недоступных независимо от прежнего RTT. */
|
||||
void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt);
|
||||
|
||||
/** Запланировать перепроверку путей после изменения маршрутов или READY групповых сессий. */
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
/* conn_mgr_core.c — init/destroy, open/close/handles, DIRECT, REVERSE, invite */
|
||||
/* conn_mgr_core.c — владение handles, жизненный цикл и DIRECT/REVERSE-пробы. */
|
||||
#include "conn_mgr_priv.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
#include "../lib/platform_compat.h"
|
||||
#include "../lib/socket_compat.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/u_async.h"
|
||||
@@ -35,6 +30,7 @@
|
||||
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
|
||||
static void cm_attempt_tick(void* arg);
|
||||
static void cm_start_attempt(struct CONN_MGR_ENTRY* entry, int recovery);
|
||||
static void cm_deliver_up_cb(void* arg);
|
||||
|
||||
static const char* cm_path_name(uint8_t type) {
|
||||
static const char* names[] = { "NONE", "DIRECT", "REVERSE", "INDIRECT" };
|
||||
@@ -49,7 +45,16 @@ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint6
|
||||
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: handle allocation failed"); return NULL; }
|
||||
h->node_id = entry->node_id; h->group_id = group_id; h->cb = cb; h->cb_arg = cb_arg;
|
||||
h->ua = entry->mgr->instance->ua;
|
||||
h->mgr = entry->mgr;
|
||||
if (entry->state == CONN_MGR_STATE_CONNECTED) {
|
||||
h->deliver_up_token = uasync_call_soon(h->ua, h, cm_deliver_up_cb);
|
||||
if (!h->deliver_up_token) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: initial UP scheduling failed peer=%016llx", (unsigned long long)entry->node_id);
|
||||
u_free(h); return NULL;
|
||||
}
|
||||
}
|
||||
h->entry = entry; h->next = entry->handles; entry->handles = h;
|
||||
h->owner_next = h->mgr->handles; h->mgr->handles = h;
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -60,7 +65,7 @@ static void cm_deliver_up_cb(void* arg) {
|
||||
h->cb(h, h->node_id, h->group_id, CONN_EVENT_UP, h->cb_arg);
|
||||
}
|
||||
|
||||
/* Закрытые handles освобождаются через call_soon, поэтому next переживает callbacks.
|
||||
/* Закрытые handles освобождаются фоновым GC, поэтому next переживает callbacks.
|
||||
* Рассылает событие ВСЕМ handle'ам entry. Одно соединение — много слушателей:
|
||||
* каждый conn_mgr_open создал свой handle, все получают UP/DOWN/TIMEOUT.
|
||||
* Все поля handle'а читаются до вызова коллбэка — коллбэк может закрыть
|
||||
@@ -149,8 +154,20 @@ struct CONN_MGR_ENTRY* cm_ensure_entry(struct CONN_MGR* mgr, uint64_t node_id) {
|
||||
return new_e;
|
||||
}
|
||||
|
||||
static void cm_entry_free_cb(void* arg) { queue_entry_free(arg); }
|
||||
static void cm_handle_free_cb(void* arg) { u_free(arg); }
|
||||
/* Объекты сняты с рабочих списков, но активный CM-стек ещё может читать state/next.
|
||||
* В начале фонового callback и при destroy этих стеков уже нет. Аллокации не нужны. */
|
||||
void cm_collect(struct CONN_MGR* mgr) {
|
||||
size_t handles = 0, entries = 0;
|
||||
while (mgr->retired_handles) {
|
||||
struct CONN_MGR_HANDLE* h = mgr->retired_handles;
|
||||
mgr->retired_handles = h->owner_next; u_free(h); handles++;
|
||||
}
|
||||
while (mgr->retired_entries) {
|
||||
struct CONN_MGR_ENTRY* entry = mgr->retired_entries;
|
||||
mgr->retired_entries = entry->retired_next; queue_entry_free(&entry->ll); entries++;
|
||||
}
|
||||
if (handles || entries) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: collected closed handles=%zu entries=%zu", handles, entries);
|
||||
}
|
||||
|
||||
void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
|
||||
if (!entry) return;
|
||||
@@ -171,7 +188,7 @@ void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
|
||||
cm_cleanup_db_node(entry);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: cleanup group=%016llx peer=%016llx",
|
||||
(unsigned long long)mgr->group->group_id, (unsigned long long)entry->node_id);
|
||||
uasync_call_soon(mgr->instance->ua, entry, cm_entry_free_cb);
|
||||
entry->retired_next = mgr->retired_entries; mgr->retired_entries = entry;
|
||||
}
|
||||
|
||||
void cm_clear_nodeinfo(struct CONN_MGR* mgr, uint64_t node_id) {
|
||||
@@ -496,6 +513,10 @@ struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) {
|
||||
if (!mgr->entries) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "entries alloc failed"); u_free(mgr); return NULL; }
|
||||
mgr->bg_ping_timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_BG_PING_INTERVAL_TB, mgr, cm_bg_ping_timer_cb, "conn_mgr_bg_ping");
|
||||
mgr->candidate_ping_timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_CANDIDATE_PING_TB, mgr, cm_candidate_ping_timer_cb, "conn_mgr_cand_ping");
|
||||
if (!mgr->bg_ping_timer || !mgr->candidate_ping_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: monitor timers unavailable group=%016llx", (unsigned long long)group->group_id);
|
||||
conn_mgr_destroy(mgr); return NULL;
|
||||
}
|
||||
mgr->bg_ping_cycle_start_tb = get_time_tb(); mgr->initialized = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: initialized for group %016llx", (unsigned long long)group->group_id);
|
||||
return mgr;
|
||||
@@ -514,6 +535,11 @@ void conn_mgr_destroy(struct CONN_MGR* mgr) {
|
||||
if (mgr->cand_tcp_pings[i]) stcp_client_destroy(mgr->cand_tcp_pings[i]);
|
||||
while (mgr->entries->head) cm_entry_cleanup((struct CONN_MGR_ENTRY*)mgr->entries->head);
|
||||
queue_free(mgr->entries);
|
||||
while (mgr->handles) {
|
||||
struct CONN_MGR_HANDLE* h = mgr->handles;
|
||||
mgr->handles = h->owner_next; h->mgr = NULL; h->owner_next = NULL;
|
||||
}
|
||||
cm_collect(mgr);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: destroyed group=%016llx", (unsigned long long)mgr->group->group_id);
|
||||
u_free(mgr);
|
||||
}
|
||||
@@ -539,7 +565,11 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id,
|
||||
if (!target && mgr->instance->topo_sqlite_db && mgr->instance->topo_groups) {
|
||||
struct TOPO_NODE* ni = topo_node_sqlite_node_load(mgr->instance->topo_sqlite_db, mgr->instance->topo_groups, node_id);
|
||||
if (ni) {
|
||||
ni = topo_node_registry_store(mgr->instance->topo_groups, ni);
|
||||
struct TOPO_NODE* stored = topo_node_registry_store(mgr->instance->topo_groups, ni);
|
||||
if (!stored) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: cannot register DB node=%016llx", (unsigned long long)node_id);
|
||||
topo_node_destroy(mgr->instance->topo_groups, ni); return -1;
|
||||
}
|
||||
struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE) - sizeof(struct ll_entry));
|
||||
if (qe) {
|
||||
target = (struct TOPO_GROUP_NODE*)qe;
|
||||
@@ -547,7 +577,10 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id,
|
||||
target->node_id = node_id; target->conn_mgr_type = CONN_TYPE_NONE;
|
||||
queue_data_put_with_index(group->nodes, &target->ll); loaded_from_db = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: node 0x%016llx loaded from DB", (unsigned long long)node_id);
|
||||
} else DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: failed to alloc TOPO_GROUP_NODE for DB node 0x%016llx", (unsigned long long)node_id);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: failed to alloc TOPO_GROUP_NODE for DB node 0x%016llx", (unsigned long long)node_id);
|
||||
topo_node_registry_unref(mgr->instance->topo_groups, node_id); return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "node 0x%016llx skipped — not in routing, no active connections", (unsigned long long)node_id); return -1; }
|
||||
@@ -555,28 +588,26 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id,
|
||||
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, node_id);
|
||||
|
||||
if (entry && entry->handles) {
|
||||
entry->db_loaded |= (uint8_t)loaded_from_db;
|
||||
struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg);
|
||||
if (!h) return -1; if (out_handle) *out_handle = h;
|
||||
if (entry->state == CONN_MGR_STATE_CONNECTED)
|
||||
h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: joined demand peer=%016llx state=%u retry=%llu",
|
||||
(unsigned long long)node_id, entry->state, (unsigned long long)entry->retry_tb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
entry = cm_ensure_entry(mgr, node_id);
|
||||
if (!entry) return -1;
|
||||
if (!entry) { if (loaded_from_db) topo_nodeq_remove_node(group, target); return -1; }
|
||||
entry->db_loaded |= (uint8_t)loaded_from_db;
|
||||
int already_up = entry->state == CONN_MGR_STATE_CONNECTED;
|
||||
if (!already_up) entry->state = CONN_MGR_STATE_CONNECTING;
|
||||
struct CONN_MGR_HANDLE* h = cm_handle_new(entry, gid, cb, cb_arg);
|
||||
if (!h) { cm_entry_cleanup(entry); return -1; } if (out_handle) *out_handle = h;
|
||||
// Первый владелец входящего пути начинает свои пробы, сохраняя рабочую доставку.
|
||||
if (entry->timer) { uasync_cancel_timeout(mgr->instance->ua, entry->timer); entry->timer = NULL; }
|
||||
if (already_up) h->deliver_up_token = uasync_call_soon(mgr->instance->ua, h, cm_deliver_up_cb);
|
||||
entry->last_traffic_tb = get_time_tb();
|
||||
entry->timeout_notified = 0;
|
||||
entry->retry_delay_ms = CONN_MGR_RETRY_MIN_MS;
|
||||
entry->db_loaded |= (uint8_t)loaded_from_db;
|
||||
cm_start_attempt(entry, 0);
|
||||
if (entry->state == CONN_MGR_STATE_DISCONNECTED) return 0;
|
||||
cm_arm_timer(entry);
|
||||
@@ -597,7 +628,12 @@ void conn_mgr_close(struct CONN_MGR_HANDLE* h) {
|
||||
if (!entry->handles) cm_entry_cleanup(entry);
|
||||
}
|
||||
// Не посылаем DISCONNECT: удалённые владельцы и групповой транспорт живут независимо.
|
||||
uasync_call_soon(h->ua, h, cm_handle_free_cb);
|
||||
struct CONN_MGR* mgr = h->mgr;
|
||||
if (!mgr) { u_free(h); return; } /* менеджер уже разрушен вне callbacks */
|
||||
struct CONN_MGR_HANDLE** owner = &mgr->handles;
|
||||
while (*owner && *owner != h) owner = &(*owner)->owner_next;
|
||||
if (*owner) *owner = h->owner_next;
|
||||
h->owner_next = mgr->retired_handles; mgr->retired_handles = h;
|
||||
}
|
||||
|
||||
struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h) {
|
||||
@@ -606,16 +642,6 @@ struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h) {
|
||||
return cm_direct_conn(h->entry->mgr, h->entry->node_id);
|
||||
}
|
||||
|
||||
int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e) {
|
||||
if (!h || !e) return -1;
|
||||
struct CONN_MGR_ENTRY* entry = h->entry;
|
||||
if (!entry || entry->state != CONN_MGR_STATE_CONNECTED) { queue_entry_free(e); return -1; }
|
||||
entry->last_traffic_tb = get_time_tb();
|
||||
struct ETCP_ROUTER_CONN* r = etcp_router_conn_get(entry->mgr->instance, entry->mgr->group->group_id, entry->node_id, ETCP_RT_ID_CONN_MGR);
|
||||
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: no route to 0x%016llx, dropping %u bytes", (unsigned long long)entry->node_id, (unsigned)e->len); queue_entry_free(e); return -1; }
|
||||
int ret = etcp_router_conn_send(r, e->dgram, e->len, 0); queue_entry_free(e); return ret;
|
||||
}
|
||||
|
||||
/* Локальные пробы уже созданы DIRECT через NCD. Проверяем все LAN-адреса,
|
||||
* не дублируя handshake и не создавая отдельные ping-контексты. */
|
||||
void cm_start_local_scan(struct CONN_MGR_ENTRY* entry) {
|
||||
@@ -666,6 +692,7 @@ void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry) {
|
||||
uint64_t now = get_time_tb();
|
||||
if (!entry->reverse_started) {
|
||||
entry->reverse_started = 1; entry->reverse_request_id = ++entry->mgr->next_request_id;
|
||||
if (!entry->reverse_request_id) entry->reverse_request_id = ++entry->mgr->next_request_id;
|
||||
entry->reverse_deadline_tb = now + CONN_MGR_CONNECT_REVERSE_TIMEOUT_MS * 10;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE start peer=%016llx elapsed=%llums",
|
||||
(unsigned long long)entry->node_id, (unsigned long long)((now - entry->start_tb) / 10));
|
||||
@@ -682,6 +709,10 @@ void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* dat
|
||||
return;
|
||||
}
|
||||
struct CM_DIRECT_REQ req; memcpy(&req, data, sizeof(req));
|
||||
if (!req.request_id || !src || src == mgr->instance->node_id) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid DIRECT_REQ peer=%016llx request=%u",
|
||||
(unsigned long long)src, req.request_id); return;
|
||||
}
|
||||
struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, src);
|
||||
if (!ni) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: REVERSE unknown peer=%016llx", (unsigned long long)src); return; }
|
||||
struct CONN_MGR_ENTRY* entry = cm_ensure_entry(mgr, src);
|
||||
@@ -702,12 +733,6 @@ void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* dat
|
||||
|
||||
/* ═══════ disconnect / recv ═══════ */
|
||||
|
||||
void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id) {
|
||||
struct CONN_MGR_ENTRY* entry = cm_find_entry(mgr, node_id); if (!entry) return;
|
||||
cm_clear_nodeinfo(mgr, node_id); cm_entry_cleanup(entry);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: remote disconnect from 0x%016llx", (unsigned long long)node_id);
|
||||
}
|
||||
|
||||
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 2) {
|
||||
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
|
||||
@@ -734,7 +759,6 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
|
||||
if (mgr) { struct CONN_MGR_ENTRY* e = cm_find_entry(mgr, src); if (e && e->state == CONN_MGR_STATE_CONNECTED) e->last_traffic_tb = get_time_tb(); }
|
||||
switch (sub) {
|
||||
case CM_SUBCMD_DIRECT_REQ: cm_handle_direct_req(src, mgr, d, len); break;
|
||||
case CM_SUBCMD_DIRECT_RESP: break;
|
||||
case CM_SUBCMD_INTERM_EXCHANGE_REQ:
|
||||
if (len == CM_EXCHANGE_REQ_SIZE) cm_handle_interm_exchange_req(src, mgr, (struct CM_EXCHANGE_REQ*)d);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed EXCHANGE_REQ peer=%016llx len=%zu", (unsigned long long)src, len);
|
||||
@@ -742,10 +766,6 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
|
||||
case CM_SUBCMD_INTERM_EXCHANGE_RESP: if (mgr) cm_handle_interm_exchange_resp(mgr, src, d, len); break;
|
||||
case CM_SUBCMD_INTERM_SELECTED: if (mgr) cm_handle_interm_selected(mgr, src, d, len); break;
|
||||
case CM_SUBCMD_INTERM_ACK: if (mgr) cm_handle_interm_ack(mgr, src, d, len); break;
|
||||
case CM_SUBCMD_DISCONNECT:
|
||||
if (len == CM_DISCONNECT_SIZE && ((struct CM_DISCONNECT*)d)->node_id == src) cm_handle_disconnect(mgr, src);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: malformed DISCONNECT peer=%016llx len=%zu", (unsigned long long)src, len);
|
||||
break;
|
||||
default: DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: unknown command=%u peer=%016llx", sub, (unsigned long long)src); break;
|
||||
}
|
||||
queue_dgram_free(entry); queue_entry_free(entry);
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
|
||||
Доступность определяется по опубликованным адресам и общей классификации
|
||||
`sock_match`: публичный IPv4, EIM/прямой NAT, пригодный глобальный IPv6.
|
||||
Приватные адреса EIM-интерфейса не считаются внешними.
|
||||
Приватные адреса EIM-интерфейса не считаются внешними. Для интерфейса с политикой
|
||||
PUBLIC объявленный LAN-listener также разрешает сначала попробовать DIRECT.
|
||||
|
||||
DIRECT использует NCD, который создаёт UDP/TCP-линки по известной записи узла,
|
||||
включая IPv4/IPv6, LAN и Reality-параметры. LOCAL — те же локальные handshake-пробы:
|
||||
@@ -45,11 +46,9 @@ REVERSE просит получателя открыть/дополнить NCD
|
||||
|
||||
DIRECT/REVERSE определяются по текущим живым линкам: наличие исходящего означает
|
||||
DIRECT, только входящие — REVERSE. Метка первого UP не используется как постоянный тип.
|
||||
Окно звонка показывает фактический путь router, включая BGP fallback до готовности CM:
|
||||
DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` содержит call_id,
|
||||
тип и next hop; публикуется при изменении. Проверка идёт раз в секунду общим таймером
|
||||
звонка, сразу при начале/принятии и при событиях CM. Завершение сбрасывает статус
|
||||
и отменяет таймер. Отображение одинаково в Qt и Android.
|
||||
`conn_mgr_route_conn()` возвращает заимствованный next hop подтверждённой доставки.
|
||||
Его `allow_probe` разрешает использовать ещё не подтверждённый выбор только для
|
||||
собственного управляющего обмена CM. ETCP-router может отдельно использовать BGP fallback.
|
||||
|
||||
Один timer на entry обслуживает ближайшее запланированное действие, повторы и
|
||||
общий предел ожидания. READY и изменение маршрутов перепроверяют пути через
|
||||
@@ -72,7 +71,8 @@ TIMEOUT и срок очередного цикла проб независим
|
||||
Если потеряны все пути, сервис получает DOWN. Затем новый цикл запускает DIRECT
|
||||
и LOCAL сразу, REVERSE через 0/500 мс; при отсутствии пригодного резерва INDIRECT
|
||||
запускается сразу независимо от NAT. Рабочий посредник сохраняется во время проб.
|
||||
Каждый цикл ограничен 15 секундами. Между неудачными циклами пауза растёт:
|
||||
Дедлайн цикла — через 15 секунд от запланированного INDIRECT: 15–16 секунд
|
||||
от начального старта, при recovery — 15 секунд. Между неудачными циклами пауза растёт:
|
||||
1 → 2 → 4 → 8 → 15 секунд, далее остаётся 15 секунд. Подтверждённый прямой READY
|
||||
останавливает поиск и сбрасывает паузу.
|
||||
|
||||
@@ -114,7 +114,35 @@ TIMEOUT и срок очередного цикла проб независим
|
||||
|
||||
Wire-структуры описаны в `conn_mgr_priv.h`. DIRECT_REQ содержит только request_id.
|
||||
INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{node_id, rtt}`.
|
||||
Совместимость с прежним адресным DIRECT_REQ не поддерживается.
|
||||
Структуры фиксированной длины, все числа native endian: DIRECT_REQ 6 байт,
|
||||
EXCHANGE_REQ 37, EXCHANGE_RESP 68, SELECTED/ACK 37. Массивы на проводе всегда
|
||||
содержат три слота; count указывает число действительных. RTT в единицах 0.1 мс.
|
||||
request_id ненулевой; ноль пропускается при переполнении счётчика.
|
||||
DIRECT_RESP и DISCONNECT удалены; неизвестные команды отклоняются с WARN.
|
||||
Удалённый узел не может командой закрыть локальную потребность сервиса.
|
||||
Добавление участников и получение описания канала — chat_join/member_sync и topo_group_invite.
|
||||
|
||||
## Фоновые проверки и освобождение
|
||||
|
||||
BG ping проверяет по одному узлу каждые 100 мс; полный цикл начинается не чаще
|
||||
одного раза в 60 секунд. Узлы с живым прямым линком пропускаются. Выбирается
|
||||
первый подходящий UDP endpoint IPv4/IPv6. Candidate ping работает каждые 2 секунды:
|
||||
обновляет до трёх кандидатов, пингует активные UDP/STCP-линки и запускает пробы
|
||||
route_connectivity для неактивных. Без активного линка устаревший замер удаляет
|
||||
кандидата через 30 секунд. Если у READY-транспорта нет текущего RTT, результат
|
||||
route_connectivity используется только в пределах 20 секунд после пробы.
|
||||
NAT-детектор откладывает эти пробы, standby переносит их в активную фазу.
|
||||
|
||||
Кандидаты упорядочены по RTT, затем node_id. При заполнении кеша кандидат с
|
||||
READY-сессией вытесняет недоступного независимо от его старого RTT. Проба
|
||||
недоступного узла не вытесняет READY-кандидата из полностью рабочего списка.
|
||||
Перед согласованием каждое плечо и RTT всё равно проверяются заново.
|
||||
|
||||
close/cleanup сразу снимают объект с рабочего списка и отменяют callbacks.
|
||||
Память закрытых handles/entries остаётся у менеджера до начала фонового callback
|
||||
или destroy: вложенные callbacks могут безопасно закрывать себя и следующий handle.
|
||||
Сборка не выделяет память и не зависит от успешного call_soon при закрытии.
|
||||
Destroy отсоединяет ещё открытые handles; они остаются у вызывающего до close.
|
||||
|
||||
## API и владение
|
||||
|
||||
@@ -132,6 +160,10 @@ INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{nod
|
||||
NCD-владение CM. Группа и другие NCD-владельцы сохраняют транспорт.
|
||||
- `conn_mgr_get_conn()` возвращает заимствованный прямой транспорт либо NULL
|
||||
при INDIRECT/отсоединённом handle. Данные следует отправлять через router.
|
||||
Сервис передаёт собственный svc_id и выбирает шифрование; общего conn_mgr_send нет.
|
||||
- Загрузка узла из SQLite временно удерживает запись реестра. Ошибки регистрации,
|
||||
группового узла, CM-entry или handle освобождают созданные объекты. Приобретение
|
||||
handle уже готового пути возвращает ошибку, если отложенный UP не удалось запланировать.
|
||||
|
||||
Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену
|
||||
пути с временем от старта, новые циклы и паузы; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR —
|
||||
@@ -144,3 +176,6 @@ INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{nod
|
||||
обрыв после начального срока, повторные циклы с реальными паузами 1/2/4с, смену адресов
|
||||
и локальных сокетов, потерю маршрута посредника, устаревший ACK, начальный TIMEOUT
|
||||
с последующим UP и закрытие последнего handle из TIMEOUT callback.
|
||||
`test_conn_mgr_handles` дополнительно проверяет READY-кандидата при заполненном
|
||||
недоступными узлами кеше, отказ начального UP и close без аллокаций; `test_topo_node_wire`
|
||||
проверяет четыре отказа bootstrap и ошибки обязательных CM-таймеров.
|
||||
@@ -17,6 +17,7 @@ void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry) {
|
||||
struct CONN_MGR* mgr = entry->mgr;
|
||||
if (!entry->indirect_started) {
|
||||
entry->indirect_started = 1; entry->exchange_request_id = ++mgr->next_request_id;
|
||||
if (!entry->exchange_request_id) entry->exchange_request_id = ++mgr->next_request_id;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: INDIRECT start peer=%016llx elapsed=%llums candidates=%u",
|
||||
(unsigned long long)entry->node_id, (unsigned long long)((get_time_tb() - entry->start_tb) / 10),
|
||||
mgr->best_candidate_count);
|
||||
@@ -185,7 +186,8 @@ void cm_handle_interm_ack(struct CONN_MGR* mgr, uint64_t src, const uint8_t* dat
|
||||
|
||||
void cm_handle_interm_exchange_req(uint64_t src, struct CONN_MGR* mgr, struct CM_EXCHANGE_REQ* req) {
|
||||
if (!mgr) return;
|
||||
if (req->candidate_count > CONN_MGR_MAX_CANDIDATES || !topo_node_registry_find(mgr->instance->topo_groups, src)) {
|
||||
if (!req->request_id || !src || src == mgr->instance->node_id || req->candidate_count > CONN_MGR_MAX_CANDIDATES ||
|
||||
!topo_node_registry_find(mgr->instance->topo_groups, src)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: invalid EXCHANGE_REQ peer=%016llx count=%u", (unsigned long long)src, req->candidate_count);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,23 +37,31 @@ int cm_is_rtt_fresh(struct TOPO_GROUP_NODE* nq, uint64_t now_tb) {
|
||||
* из route_connectivity после успешного probe. INDIRECT-фаза использует этот
|
||||
* список (best_candidates) для выбора посредника с минимальным суммарным RTT. */
|
||||
void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt) {
|
||||
if (!mgr || node_id == mgr->instance->node_id || !rtt) return;
|
||||
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) {
|
||||
if (mgr->best_candidates[i].node_id != node_id) continue;
|
||||
mgr->best_candidates[i].rtt = rtt;
|
||||
for (uint8_t j = i; j > 0 && mgr->best_candidates[j].rtt < mgr->best_candidates[j-1].rtt; j--)
|
||||
{ struct CONN_MGR_CANDIDATE t = mgr->best_candidates[j]; mgr->best_candidates[j]=mgr->best_candidates[j-1]; mgr->best_candidates[j-1]=t; }
|
||||
for (uint8_t j = i; j + 1 < mgr->best_candidate_count && mgr->best_candidates[j].rtt > mgr->best_candidates[j+1].rtt; j++)
|
||||
{ struct CONN_MGR_CANDIDATE t = mgr->best_candidates[j]; mgr->best_candidates[j]=mgr->best_candidates[j+1]; mgr->best_candidates[j+1]=t; }
|
||||
return;
|
||||
if (!mgr || !node_id || node_id == mgr->instance->node_id || !rtt || rtt == UINT16_MAX) return;
|
||||
uint8_t slot = 0;
|
||||
while (slot < mgr->best_candidate_count && mgr->best_candidates[slot].node_id != node_id) slot++;
|
||||
if (slot == mgr->best_candidate_count) {
|
||||
if (slot < CONN_MGR_MAX_CANDIDATES) mgr->best_candidate_count++;
|
||||
else {
|
||||
slot = CONN_MGR_MAX_CANDIDATES - 1;
|
||||
/* Быстрый, но недоступный кандидат не блокирует рабочий READY-путь. */
|
||||
int ready = cm_direct_conn(mgr, node_id) != NULL;
|
||||
int previous_ready = cm_direct_conn(mgr, mgr->best_candidates[slot].node_id) != NULL;
|
||||
for (int i = slot - 1; i >= 0 && previous_ready; i--)
|
||||
if (!cm_direct_conn(mgr, mgr->best_candidates[i].node_id)) { slot = (uint8_t)i; previous_ready = 0; }
|
||||
if (ready < previous_ready || (ready == previous_ready && (rtt > mgr->best_candidates[slot].rtt ||
|
||||
(rtt == mgr->best_candidates[slot].rtt && node_id >= mgr->best_candidates[slot].node_id)))) return;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: candidate admitted peer=%016llx rtt=%u replacing=%016llx",
|
||||
(unsigned long long)node_id, rtt, (unsigned long long)mgr->best_candidates[slot].node_id);
|
||||
}
|
||||
mgr->best_candidates[slot] = (struct CONN_MGR_CANDIDATE){node_id, rtt};
|
||||
for (uint8_t i = 0; i < mgr->best_candidate_count; i++) for (uint8_t j = i + 1; j < mgr->best_candidate_count; j++) {
|
||||
struct CONN_MGR_CANDIDATE *a = &mgr->best_candidates[i], *b = &mgr->best_candidates[j];
|
||||
if (b->rtt < a->rtt || (b->rtt == a->rtt && b->node_id < a->node_id)) {
|
||||
struct CONN_MGR_CANDIDATE tmp = *a; *a = *b; *b = tmp;
|
||||
}
|
||||
}
|
||||
if (mgr->best_candidate_count < CONN_MGR_MAX_CANDIDATES)
|
||||
{ mgr->best_candidates[mgr->best_candidate_count++]=(struct CONN_MGR_CANDIDATE){node_id,rtt}; }
|
||||
else if (rtt < mgr->best_candidates[CONN_MGR_MAX_CANDIDATES-1].rtt)
|
||||
{ mgr->best_candidates[CONN_MGR_MAX_CANDIDATES-1]=(struct CONN_MGR_CANDIDATE){node_id,rtt}; }
|
||||
else return;
|
||||
for (uint8_t i = mgr->best_candidate_count - 1; i > 0 && mgr->best_candidates[i].rtt < mgr->best_candidates[i-1].rtt; i--)
|
||||
{ struct CONN_MGR_CANDIDATE t = mgr->best_candidates[i]; mgr->best_candidates[i]=mgr->best_candidates[i-1]; mgr->best_candidates[i-1]=t; }
|
||||
}
|
||||
|
||||
/* Маршрут может вести через посредника. Для прямой пробы нужны transport peer и его живой линк. */
|
||||
@@ -105,12 +113,14 @@ static void cm_bg_ping_to_node(struct CONN_MGR* mgr, struct TOPO_NODE* ni) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Фоновый ping: один узел группы за тик (~100ms), полный цикл ≥ 10с.
|
||||
/* Фоновый ping: один узел группы за тик (~100ms), полный цикл ≥ 60с.
|
||||
* Пингует только если нет активного ETCP-соединения с узлом. Цель: поддерживать
|
||||
* свежие RTT-замеры для ВСЕХ узлов группы — без этого best_candidates протухают
|
||||
* и INDIRECT-фаза не может выбрать посредника. */
|
||||
void cm_bg_ping_timer_cb(void* arg) {
|
||||
struct CONN_MGR* mgr = (struct CONN_MGR*)arg;
|
||||
mgr->bg_ping_timer = NULL; mgr->bg_ping_wait = NULL;
|
||||
cm_collect(mgr);
|
||||
uint64_t nat_delay = nat_detection_probe_delay(mgr->instance->nat_det);
|
||||
if (nat_delay) {
|
||||
mgr->bg_ping_timer = uasync_set_timeout(mgr->instance->ua, nat_delay, mgr, cm_bg_ping_timer_cb, "cm_bg_nat_defer");
|
||||
@@ -120,7 +130,8 @@ void cm_bg_ping_timer_cb(void* arg) {
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
if (standby_get_sleep_tb() > 0) {
|
||||
mgr->bg_ping_wait = standby_wait(mgr, cm_bg_ping_timer_cb);
|
||||
return;
|
||||
if (mgr->bg_ping_wait) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: background standby wait failed; retrying by timer"); goto rearm;
|
||||
}
|
||||
mgr->bg_ping_wait = NULL;
|
||||
#endif
|
||||
@@ -133,7 +144,12 @@ void cm_bg_ping_timer_cb(void* arg) {
|
||||
if (mgr->bg_ping_cursor >= total) {
|
||||
mgr->bg_ping_cursor = 0;
|
||||
if ((now - mgr->bg_ping_cycle_start_tb) < CONN_MGR_BG_PING_CYCLE_MIN_TB)
|
||||
{ mgr->bg_ping_timer = uasync_set_timeout(mgr->instance->ua, (int)(CONN_MGR_BG_PING_CYCLE_MIN_TB - (now - mgr->bg_ping_cycle_start_tb)), mgr, cm_bg_ping_timer_cb, "conn_mgr_bg_ping"); return; }
|
||||
{
|
||||
mgr->bg_ping_timer = uasync_set_timeout(mgr->instance->ua,
|
||||
(int)(CONN_MGR_BG_PING_CYCLE_MIN_TB - (now - mgr->bg_ping_cycle_start_tb)), mgr, cm_bg_ping_timer_cb, "conn_mgr_bg_ping");
|
||||
if (!mgr->bg_ping_timer) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: background cycle timer allocation failed");
|
||||
return;
|
||||
}
|
||||
mgr->bg_ping_cycle_start_tb = now;
|
||||
}
|
||||
|
||||
@@ -152,6 +168,7 @@ void cm_bg_ping_timer_cb(void* arg) {
|
||||
}
|
||||
rearm:
|
||||
mgr->bg_ping_timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_BG_PING_INTERVAL_TB, mgr, cm_bg_ping_timer_cb, "conn_mgr_bg_ping");
|
||||
if (!mgr->bg_ping_timer) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: background timer allocation failed");
|
||||
}
|
||||
|
||||
/* ═══════ candidate_ping ═══════ */
|
||||
@@ -165,6 +182,8 @@ static int cm_has_active_conn(struct TOPO_GROUP* g, uint64_t nid) {
|
||||
* Вызывается раз в ~2с, самоперезапускается. */
|
||||
void cm_candidate_ping_timer_cb(void* arg) {
|
||||
struct CONN_MGR* mgr = (struct CONN_MGR*)arg; uint64_t now = get_time_tb();
|
||||
mgr->candidate_ping_timer = NULL; mgr->candidate_ping_wait = NULL;
|
||||
cm_collect(mgr);
|
||||
uint64_t nat_delay = nat_detection_probe_delay(mgr->instance->nat_det);
|
||||
if (nat_delay) {
|
||||
mgr->candidate_ping_timer = uasync_set_timeout(mgr->instance->ua, nat_delay, mgr, cm_candidate_ping_timer_cb, "cm_candidate_nat_defer");
|
||||
@@ -174,7 +193,8 @@ void cm_candidate_ping_timer_cb(void* arg) {
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
if (standby_get_sleep_tb() > 0) {
|
||||
mgr->candidate_ping_wait = standby_wait(mgr, cm_candidate_ping_timer_cb);
|
||||
return;
|
||||
if (mgr->candidate_ping_wait) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: candidate standby wait failed; retrying by timer"); goto rearm;
|
||||
}
|
||||
mgr->candidate_ping_wait = NULL;
|
||||
#endif
|
||||
@@ -220,5 +240,7 @@ void cm_candidate_ping_timer_cb(void* arg) {
|
||||
}
|
||||
} else route_connectivity_probe_node(mgr->instance, mgr->group, nq);
|
||||
}
|
||||
rearm:
|
||||
mgr->candidate_ping_timer = uasync_set_timeout(mgr->instance->ua, CONN_MGR_CANDIDATE_PING_TB, mgr, cm_candidate_ping_timer_cb, "conn_mgr_cand_ping");
|
||||
if (!mgr->candidate_ping_timer) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "conn_mgr: candidate timer allocation failed");
|
||||
}
|
||||
@@ -38,7 +38,7 @@ struct stcp_client;
|
||||
#define CONN_MGR_EXCHANGE_RETRANSMIT_MS 1000
|
||||
#define CONN_MGR_RETRY_MIN_MS 1000
|
||||
#define CONN_MGR_RETRY_MAX_MS 15000
|
||||
#define CACHE_MS 20000 /* время жизни RTT-замера ~20с (через cm_is_rtt_fresh, мс) */
|
||||
#define CACHE_MS 20000 /* свежесть RTT-пробы 20с; поля *_tb и RTT используют 0.1мс */
|
||||
#define CONN_MGR_MAX_INTERMEDIARIES 3
|
||||
|
||||
/* ═══════ состояния ═══════ */
|
||||
@@ -47,7 +47,6 @@ enum CONN_MGR_STATE {
|
||||
CONN_MGR_STATE_DISCONNECTED = 0,
|
||||
CONN_MGR_STATE_CONNECTING = 1,
|
||||
CONN_MGR_STATE_CONNECTED = 2,
|
||||
CONN_MGR_STATE_FAILED = 3,
|
||||
};
|
||||
|
||||
enum CM_TRY_STATE {
|
||||
@@ -61,68 +60,71 @@ enum CM_TRY_STATE {
|
||||
|
||||
enum {
|
||||
CM_SUBCMD_DIRECT_REQ = 0x01,
|
||||
CM_SUBCMD_DIRECT_RESP = 0x02,
|
||||
CM_SUBCMD_INTERM_EXCHANGE_REQ = 0x03,
|
||||
CM_SUBCMD_INTERM_EXCHANGE_RESP= 0x04,
|
||||
CM_SUBCMD_INTERM_SELECTED = 0x05,
|
||||
CM_SUBCMD_DISCONNECT = 0x06,
|
||||
CM_SUBCMD_INTERM_ACK = 0x07,
|
||||
};
|
||||
|
||||
/* RTT — 0.1мс; 0/FFFF не участвуют в согласовании. */
|
||||
struct CONN_MGR_CANDIDATE { uint64_t node_id; uint16_t rtt; } __attribute__((packed));
|
||||
|
||||
/* Полные фиксированные структуры, включая неиспользованные слоты массивов.
|
||||
* Native endian, cmd=ETCP_RT_ID_CONN_MGR; request_id ненулевой.
|
||||
* DIRECT_REQ=6B, EXCHANGE_REQ=37B, EXCHANGE_RESP=68B, SELECTED/ACK=37B. */
|
||||
#pragma pack(push, 1)
|
||||
struct CM_DIRECT_REQ { uint8_t cmd, subcmd; uint32_t request_id; };
|
||||
struct CM_DIRECT_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t accepted; };
|
||||
struct CM_EXCHANGE_REQ { uint8_t cmd, subcmd; uint32_t request_id; uint8_t candidate_count; struct CONN_MGR_CANDIDATE candidates[CONN_MGR_MAX_CANDIDATES]; };
|
||||
struct CM_EXCHANGE_RESP { uint8_t cmd, subcmd; uint32_t request_id; uint8_t my_count, your_count; struct CONN_MGR_CANDIDATE my_candidates[CONN_MGR_MAX_CANDIDATES], your_candidates[CONN_MGR_MAX_CANDIDATES]; };
|
||||
struct CM_INTERM_SEL { uint8_t cmd, subcmd; uint32_t request_id; uint8_t count; struct CONN_MGR_CANDIDATE selected[3]; };
|
||||
struct CM_DISCONNECT { uint8_t cmd, subcmd; uint64_t node_id; };
|
||||
#pragma pack(pop)
|
||||
|
||||
#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))
|
||||
|
||||
/* ═══════ внутренние структуры ═══════ */
|
||||
|
||||
struct CONN_MGR_HANDLE {
|
||||
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg;
|
||||
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg; /* исходная цель/группа, заимствованный callback */
|
||||
struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next;
|
||||
struct CONN_MGR* mgr; /* владелец памяти до destroy; NULL у отсоединённого после destroy handle */
|
||||
struct CONN_MGR_HANDLE* owner_next; /* список всех handles mgr либо список закрытых до безопасного GC */
|
||||
struct UASYNC* ua;
|
||||
uint8_t closed; /* close отменяет callbacks; память освобождается вне текущего стека */
|
||||
uint8_t closed; /* close отменяет callbacks; менеджер освобождает память вне текущего стека */
|
||||
void* deliver_up_token; /* токен отложенного cm_deliver_up_cb (uasync_call_soon), NULL если не постился/уже сработал */
|
||||
};
|
||||
|
||||
struct CONN_MGR_ENTRY {
|
||||
struct ll_entry ll; /* ll_queue entry — первый элемент */
|
||||
uint64_t node_id; /* хеш-ключ 8B, offset от data = 0 */
|
||||
uint8_t state, conn_type;
|
||||
uint64_t last_traffic_tb;
|
||||
struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle;
|
||||
uint64_t intermediaries[3]; uint8_t intermediariy_count;
|
||||
uint8_t local_scan_state, main_connect_state, db_loaded;
|
||||
uint8_t state, conn_type; /* CONN_MGR_STATE / CONN_TYPE; рабочая доставка независимо от проб */
|
||||
uint64_t last_traffic_tb; /* последняя принятая CM-команда при CONNECTED; не idle-дедлайн */
|
||||
struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle; /* владельцы запроса; отдельный общий прямой транспорт CM */
|
||||
uint64_t intermediaries[3]; uint8_t intermediariy_count; /* подтверждённый резерв, актуальность проверяется перед выбором */
|
||||
uint8_t local_scan_state, main_connect_state, db_loaded; /* CM_TRY / CM_TRY / временный узел из SQLite */
|
||||
uint8_t reverse_started, indirect_started, indirect_confirmed, direct_up, timeout_notified, ever_up;
|
||||
uint8_t attempt_active;
|
||||
uint16_t retry_delay_ms;
|
||||
void* timer; void* refresh_token;
|
||||
uint8_t attempt_active; /* исходящий цикл; не признак CONNECTED */
|
||||
uint16_t retry_delay_ms; /* пауза между циклами, 1000..15000мс */
|
||||
void* timer; void* refresh_token; /* ближайший шаг/срок; отложенный пересчёт после маршрутов */
|
||||
uint64_t start_tb, reverse_start_tb, indirect_start_tb, deadline_tb;
|
||||
uint64_t initial_deadline_tb, retry_tb, node_timestamp, peer_exchange_deadline_tb;
|
||||
uint64_t reverse_next_tb, reverse_deadline_tb, indirect_next_tb;
|
||||
uint32_t reverse_request_id, exchange_request_id, peer_exchange_request_id, peer_reverse_request_id;
|
||||
uint32_t reverse_request_id, exchange_request_id, peer_exchange_request_id, peer_reverse_request_id; /* независимые исходящие/входящие id */
|
||||
struct CM_INTERM_SEL selection; /* Исходящий выбор до подтверждения пира. */
|
||||
struct CONN_MGR* mgr;
|
||||
struct CONN_MGR_ENTRY* retired_next; /* уже снятая с entries запись до безопасного GC */
|
||||
};
|
||||
|
||||
struct CONN_MGR {
|
||||
struct UTUN_INSTANCE* instance; struct TOPO_GROUP* group;
|
||||
struct ll_queue* entries;
|
||||
struct CONN_MGR_HANDLE *handles, *retired_handles;
|
||||
struct CONN_MGR_ENTRY* retired_entries; /* сборка в фоновых callbacks/destroy не требует выделений памяти */
|
||||
void *bg_ping_timer, *candidate_ping_timer;
|
||||
void *bg_ping_wait, *candidate_ping_wait; /* standby_wait handle (Android) */
|
||||
size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb;
|
||||
size_t bg_ping_cursor; uint64_t bg_ping_cycle_start_tb; /* один узел за тик; полный цикл не чаще 60с */
|
||||
struct CONN_MGR_CANDIDATE best_candidates[CONN_MGR_MAX_CANDIDATES]; uint8_t best_candidate_count;
|
||||
struct stcp_client* cand_tcp_pings[CONN_MGR_MAX_CANDIDATES]; /* влётные candidate TCP-пинги (для отмены при destroy) */
|
||||
uint32_t next_request_id; uint8_t initialized;
|
||||
@@ -138,6 +140,7 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id,
|
||||
struct CONN_MGR_ENTRY* cm_find_entry(struct CONN_MGR* mgr, uint64_t node_id);
|
||||
struct CONN_MGR_ENTRY* cm_ensure_entry(struct CONN_MGR* mgr, uint64_t node_id);
|
||||
void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_collect(struct CONN_MGR* mgr); /* только вне вложенных CM-вызовов: начало фонового callback либо destroy */
|
||||
void cm_cleanup_db_node(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_deliver_event(struct CONN_MGR_ENTRY* entry, enum conn_mgr_event event);
|
||||
void cm_update_nodeinfo(struct CONN_MGR_ENTRY* entry);
|
||||
@@ -148,7 +151,6 @@ void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_start_local_scan(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_handle_direct_req(uint64_t src, struct CONN_MGR* mgr, const uint8_t* data, size_t len);
|
||||
void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id);
|
||||
void cm_finalize_indirect(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_refresh_path(struct CONN_MGR_ENTRY* entry);
|
||||
void cm_arm_timer(struct CONN_MGR_ENTRY* entry);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Подпись/шифрование — в route_crypto.c (encode при отправке, decode до изменения состояния приёма)
|
||||
//
|
||||
// Поток сервисной кодограммы:
|
||||
// in -> {send_q} -> [src: etcp_send] -> ... -> [dst: recv_q] -> {incoming_q} -> out
|
||||
// in -> {send_q} -> {inflight_q / etcp_send} -> ... -> {incoming_q} -> {recv_q} -> сервис
|
||||
// ack возвращается по обратному пути; (pause) — backpressure при заполнении очередей.
|
||||
|
||||
|
||||
@@ -239,18 +239,22 @@ static void transit_queue_drain_cb(struct ll_queue* q, void* arg) {
|
||||
// Удалить все транзитные очереди соединения (вызывается при закрытии ETCP_CONN).
|
||||
void etcp_router_transit_queues_destroy(struct ETCP_CONN* conn) {
|
||||
if (!conn || !conn->transit_queues) return;
|
||||
int pairs = 0, dropped = 0;
|
||||
struct ll_entry* entry;
|
||||
while ((entry = queue_data_get(conn->transit_queues)) != NULL) {
|
||||
struct TRANSIT_QUEUE* tq = (struct TRANSIT_QUEUE*)entry;
|
||||
if (tq->route_timer) uasync_cancel_timeout(conn->instance->ua, tq->route_timer);
|
||||
if (conn->send_input_q) queue_waiter_cancel(conn->send_input_q, &tq->waiter);
|
||||
struct ll_entry* e;
|
||||
while ((e = queue_data_get(tq->q)) != NULL) free_entry(e);
|
||||
while ((e = queue_data_get(tq->q)) != NULL) { free_entry(e); dropped++; }
|
||||
queue_free(tq->q);
|
||||
queue_entry_free(&tq->ll);
|
||||
pairs++;
|
||||
}
|
||||
queue_free(conn->transit_queues);
|
||||
conn->transit_queues = NULL;
|
||||
if (pairs) DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "transit queues released: peer=%016llx pairs=%d discarded=%d",
|
||||
(unsigned long long)conn->peer_node_id, pairs, dropped);
|
||||
}
|
||||
|
||||
// Сбросить recv_conn у всех ROUTER_CONN, ссылающихся на уничтожаемый ETCP_CONN.
|
||||
@@ -427,7 +431,6 @@ static void router_confirm_peer(struct ETCP_ROUTER_CONN* rconn, const struct SVC
|
||||
if (previous && router_conn_reset(rconn) != 0) { router_close_and_notify(rconn); return; }
|
||||
if (rconn->handshake_timer) { uasync_cancel_timeout(rconn->inst->ua, rconn->handshake_timer); rconn->handshake_timer = NULL; }
|
||||
rconn->peer_reset_id = peer_id;
|
||||
rconn->peer_sync_done = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE,
|
||||
"router session: group=%016llx peer=%016llx svc=%u local=%016llx remote=%016llx previous=%016llx",
|
||||
(unsigned long long)rconn->group_id, (unsigned long long)rconn->remote_node_id, rconn->svc_id,
|
||||
@@ -436,24 +439,18 @@ static void router_confirm_peer(struct ETCP_ROUTER_CONN* rconn, const struct SVC
|
||||
if (!rconn->closed) router_send_kick(rconn);
|
||||
}
|
||||
|
||||
// Синтетический loopback-conn (static) для доставки сервисного коллбэка при локальных событиях
|
||||
// (CLOSE, restart) без реального сетевого соединения.
|
||||
static struct ETCP_CONN* loopback_conn(struct UTUN_INSTANCE* inst) {
|
||||
static struct ETCP_CONN c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.instance = inst; c.peer_node_id = inst->node_id;
|
||||
return &c;
|
||||
}
|
||||
|
||||
// Доставить локальному сервису событие закрытия conn (пустая кодограмма ROUTER_SVC_HDR_SIZE).
|
||||
static void router_send_close_to_service(struct ETCP_ROUTER_CONN* rconn) {
|
||||
struct UTUN_INSTANCE* inst = rconn->inst;
|
||||
etcp_recv_fn cb = inst->router_bindings.callbacks[rconn->svc_id];
|
||||
if (!cb) return;
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) return;
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router close notification allocation failed svc=%u", rconn->svc_id); return; }
|
||||
e->dgram = u_malloc(ROUTER_SVC_HDR_SIZE);
|
||||
if (!e->dgram) { queue_entry_free(e); return; }
|
||||
if (!e->dgram) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router close notification payload allocation failed svc=%u", rconn->svc_id);
|
||||
queue_entry_free(e); return;
|
||||
}
|
||||
e->dgram[0] = rconn->svc_id;
|
||||
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &rconn->remote_node_id, 8);
|
||||
memcpy(e->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8);
|
||||
@@ -462,7 +459,8 @@ static void router_send_close_to_service(struct ETCP_ROUTER_CONN* rconn) {
|
||||
e->len = ROUTER_SVC_HDR_SIZE;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_close_svc: svc_id=%u remote=%016llx",
|
||||
rconn->svc_id, (unsigned long long)rconn->remote_node_id);
|
||||
cb(loopback_conn(rconn->inst), e);
|
||||
struct ETCP_CONN local = {.instance = inst, .peer_node_id = inst->node_id};
|
||||
cb(&local, e);
|
||||
}
|
||||
|
||||
// Финальный шаг закрытия: освобождает ll entry (вызывается через uasync_call_soon, уже вне очередей).
|
||||
@@ -526,7 +524,6 @@ static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) {
|
||||
queue_set_threshold(rconn->send_q, ROUTER_MAX_SEND_Q_PACKETS - 1, 0);
|
||||
queue_set_callback(rconn->incoming_q, router_incoming_q_cb, rconn);
|
||||
queue_set_threshold(rconn->incoming_q, 0, 0);
|
||||
rconn->incoming_data_ready = 0;
|
||||
rconn->receive_blocked = 0;
|
||||
rconn->peer_receive_blocked = 0;
|
||||
rconn->recv_conn = NULL;
|
||||
@@ -544,7 +541,7 @@ static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) {
|
||||
rconn->tx_seq = 0; rconn->tx_sent = 0; rconn->rx_seq = 0; rconn->tx_acked = 0;
|
||||
rconn->retrans_seq = 0; rconn->retrans_end = 0;
|
||||
rconn->last_sent_ack_seq = 0; rconn->last_ack_sent_tb = 0;
|
||||
rconn->send_blocked = 0; rconn->start_sent = 0; rconn->peer_sync_done = 0;
|
||||
rconn->send_blocked = 0; rconn->start_sent = 0;
|
||||
rconn->peer_reset_id = 0;
|
||||
rconn->no_route = 0; rconn->no_route_timer = NULL; rconn->no_ack_count = 0; rconn->closed = 0;
|
||||
rconn->c_pkts_sent = 0; rconn->c_pkts_send_err = 0; rconn->c_pkts_rcvd = 0;
|
||||
@@ -555,18 +552,17 @@ static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Закрыть conn: уведомить локальный сервис, отправить CLOSE удалённой стороне, очистить
|
||||
// очереди/таймеры, удалить из реестра router_conns и отложить освобождение ll через call_soon.
|
||||
// Снять канал с реестра, уведомить сервис/пира, очистить очереди/таймеры; само ll освободить через call_soon.
|
||||
static void router_close_and_notify(struct ETCP_ROUTER_CONN* rconn) {
|
||||
if (!rconn || rconn->closed) return;
|
||||
struct UTUN_INSTANCE* inst = rconn->inst;
|
||||
rconn->closed = 1;
|
||||
// Callback может закрыть группу или открыть новый канал с тем же ключом.
|
||||
if (inst->router_conns) queue_remove_data(inst->router_conns, &rconn->ll);
|
||||
router_send_close_to_service(rconn);
|
||||
// Отправляем CLOSE удалённой стороне + снимаем waiter отправки
|
||||
if (router_send_conn(rconn)) router_send_ctrl(rconn, ROUTER_FLAG_CLOSE);
|
||||
router_conn_free_queues(rconn);
|
||||
if (inst->router_conns)
|
||||
queue_remove_data(inst->router_conns, &rconn->ll);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_conn: closed group=%016llx remote=%016llx svc_id=%u",
|
||||
(unsigned long long)rconn->group_id, (unsigned long long)rconn->remote_node_id, rconn->svc_id);
|
||||
uasync_call_soon(inst->ua, rconn, router_close_finalize);
|
||||
@@ -803,7 +799,7 @@ static void router_retrans_timer_cb(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
// Собрать+закодировать пакет в начале отправки и положить в send_q.
|
||||
// Скопировать plaintext и mode в send_q, выделить seq; кодирование выполняется при отправке после handshake.
|
||||
static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t len, int force, int mode) {
|
||||
uint8_t required = rconn->inst->router_bindings.required_crypto[rconn->svc_id];
|
||||
if ((mode & required) != required) {
|
||||
@@ -1034,14 +1030,24 @@ static void router_idle_ack_timer_cb(void* arg) {
|
||||
|
||||
// Доставка сервисной кодограммы в едином формате [svc_id][src][dst][rx_flags][group_id][payload].
|
||||
// Для loopback (dst == self) src и dst оба = self, rx_flags = 0.
|
||||
static void router_deliver_loopback(struct UTUN_INSTANCE* inst, uint64_t group_id, uint8_t svc_id, struct ll_entry* entry) {
|
||||
etcp_recv_fn cb = inst->router_bindings.callbacks[svc_id];
|
||||
if (!cb) { queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
static int router_deliver_loopback(struct UTUN_INSTANCE* inst, uint64_t group_id, uint8_t svc_id, struct ll_entry* entry, int mode) {
|
||||
size_t payload_len = entry->len - 1;
|
||||
if (!payload_len || payload_len > UINT16_MAX - ROUTER_SVC_HDR_SIZE || (mode & ~(ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router loopback: invalid payload svc=%u bytes=%zu mode=%x", svc_id, payload_len, mode);
|
||||
free_entry(entry); return -1;
|
||||
}
|
||||
etcp_recv_fn cb = inst->router_bindings.callbacks[svc_id];
|
||||
if (!cb) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router loopback: no handler svc=%u group=%016llx", svc_id, (unsigned long long)group_id);
|
||||
free_entry(entry); return -1;
|
||||
}
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) { queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router loopback: entry allocation failed svc=%u", svc_id); free_entry(entry); return -1; }
|
||||
e->dgram = u_malloc(ROUTER_SVC_HDR_SIZE + payload_len);
|
||||
if (!e->dgram) { queue_entry_free(e); queue_dgram_free(entry); queue_entry_free(entry); return; }
|
||||
if (!e->dgram) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router loopback: payload allocation failed svc=%u bytes=%zu", svc_id, payload_len);
|
||||
queue_entry_free(e); free_entry(entry); return -1;
|
||||
}
|
||||
e->dgram[0] = svc_id;
|
||||
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &inst->node_id, 8);
|
||||
memcpy(e->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8);
|
||||
@@ -1050,11 +1056,12 @@ static void router_deliver_loopback(struct UTUN_INSTANCE* inst, uint64_t group_i
|
||||
if (payload_len > 0) memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF, entry->dgram + 1, payload_len);
|
||||
e->len = ROUTER_SVC_HDR_SIZE + payload_len;
|
||||
queue_dgram_free(entry); queue_entry_free(entry);
|
||||
cb(loopback_conn(inst), e);
|
||||
struct ETCP_CONN local = {.instance = inst, .peer_node_id = inst->node_id};
|
||||
cb(&local, e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Доставка: декодирует (подпись/шифрование) перед вызовом сервисного коллбэка.
|
||||
// wire = [SVC_ROUTE_HDR][payload][sig?] — финальный пакет, лежавший в recv_q.
|
||||
// Доставить уже проверенный plaintext из recv_q; криптографические проверки выполнены до постановки в очередь.
|
||||
static int router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn,
|
||||
const uint8_t* wire, size_t wire_len) {
|
||||
struct UTUN_INSTANCE* inst = rconn->inst;
|
||||
@@ -1091,6 +1098,8 @@ static int router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn
|
||||
|
||||
// Собрать доставку из recv_q: последовательно доставлять пакеты по возрастанию rx_seq, пока есть следующий.
|
||||
static void router_try_assembly(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn) {
|
||||
struct ETCP_CONN local = {.instance = rconn->inst, .peer_node_id = rconn->inst->node_id};
|
||||
if (!conn) conn = &local;
|
||||
uint32_t next = rconn->rx_seq;
|
||||
while (1) {
|
||||
struct ll_entry* entry = queue_find_data_by_index(rconn->recv_q, &next);
|
||||
@@ -1112,7 +1121,7 @@ static void router_try_assembly(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN
|
||||
rconn->receive_blocked = 0;
|
||||
queue_remove_data(rconn->recv_q, entry);
|
||||
uint64_t peer_id = rconn->peer_reset_id;
|
||||
int ret = router_deliver(rconn, conn ? conn : loopback_conn(rconn->inst), entry->dgram, entry->len);
|
||||
int ret = router_deliver(rconn, conn, entry->dgram, entry->len);
|
||||
if (ret != 0 || rconn->closed || rconn->peer_reset_id != peer_id) { free_entry(entry); return; }
|
||||
rconn->rx_seq = next + 1;
|
||||
next++;
|
||||
@@ -1134,8 +1143,6 @@ static void router_incoming_q_cb(struct ll_queue* q, void* arg) {
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
|
||||
uint32_t seq = *(uint32_t*)e->data;
|
||||
const uint8_t* wire = e->dgram;
|
||||
size_t wire_len = e->len;
|
||||
|
||||
if ((int32_t)(seq - rconn->rx_seq) < 0 || queue_find_data_by_index(rconn->recv_q, &seq)) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router_incoming_q: dup in recv_q seq=%u, dropping", seq);
|
||||
@@ -1143,7 +1150,10 @@ static void router_incoming_q_cb(struct ll_queue* q, void* arg) {
|
||||
}
|
||||
|
||||
struct ll_entry* rq = queue_entry_new(4);
|
||||
if (!rq) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
|
||||
if (!rq) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router reorder entry allocation failed svc=%u seq=%u", rconn->svc_id, seq);
|
||||
queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return;
|
||||
}
|
||||
*(uint32_t*)rq->data = seq;
|
||||
rq->dgram = e->dgram; rq->len = e->len; e->dgram = NULL;
|
||||
queue_entry_free(e);
|
||||
@@ -1186,7 +1196,8 @@ static void router_handle_ack(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CON
|
||||
uint16_t new_rtt = get_current_timestamp() - ack_ts;
|
||||
int d_rtt = (int)new_rtt - (int)rconn->rtt;
|
||||
if (d_rtt < 0) d_rtt = -d_rtt;
|
||||
rconn->rtt_jitter += (int32_t)(((int64_t)d_rtt * 65536 - rconn->rtt_jitter) / 32);
|
||||
int64_t jitter = rconn->rtt_jitter;
|
||||
rconn->rtt_jitter = (uint32_t)(jitter + ((int64_t)d_rtt * 65536 - jitter) / 32);
|
||||
rconn->rtt = new_rtt;
|
||||
}
|
||||
rconn->tx_acked = seq;
|
||||
@@ -1265,8 +1276,7 @@ static void router_forward_transit(struct UTUN_INSTANCE* inst, struct ll_entry*
|
||||
transit_queue_wait(tq);
|
||||
}
|
||||
|
||||
// Принять data-пакет: зафиксировать recv_conn, синхронизировать rx_seq при первом пакете сеанса,
|
||||
// отсеять out-of-bounds/дубликаты и положить пакет в incoming_q.
|
||||
// Принять данные согласованной сессии: зафиксировать recv_conn, проверить окно/дубликаты и положить в incoming_q.
|
||||
static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn,
|
||||
uint32_t seq, const uint8_t* wire, size_t wire_len) {
|
||||
rconn->recv_conn = conn;
|
||||
@@ -1300,10 +1310,13 @@ static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETC
|
||||
return;
|
||||
}
|
||||
struct ll_entry* qe = queue_entry_new(4);
|
||||
if (!qe) return;
|
||||
if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router incoming entry allocation failed svc=%u seq=%u", rconn->svc_id, seq); return; }
|
||||
*(uint32_t*)qe->data = seq;
|
||||
qe->dgram = u_malloc(wire_len);
|
||||
if (!qe->dgram) { queue_dgram_free(qe); queue_entry_free(qe); return; }
|
||||
if (!qe->dgram) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router incoming payload allocation failed svc=%u seq=%u bytes=%zu", rconn->svc_id, seq, wire_len);
|
||||
queue_dgram_free(qe); queue_entry_free(qe); return;
|
||||
}
|
||||
qe->len = wire_len; memcpy(qe->dgram, wire, wire_len);
|
||||
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router: incoming_q seq=%u rx_seq=%u from %016llx svc_id=%u",
|
||||
@@ -1431,16 +1444,21 @@ void etcp_router_destroy(struct UTUN_INSTANCE* inst) {
|
||||
queue_free(inst->router_conns);
|
||||
inst->router_conns = NULL;
|
||||
}
|
||||
for (struct ll_entry* entry = inst->connections ? inst->connections->head : NULL; entry; entry = entry->next) {
|
||||
struct ETCP_CONN* conn = ((struct conn_queue_entry*)entry->data)->conn;
|
||||
if (conn) etcp_router_transit_queues_destroy(conn);
|
||||
}
|
||||
memset(&inst->router_bindings, 0, sizeof(inst->router_bindings));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "etcp_router destroyed");
|
||||
}
|
||||
|
||||
// Зарегистрировать обработчик сервиса. callback=NULL — снять обработчик (эквивалент unbind без ошибки).
|
||||
// Зарегистрировать обработчик; callback=NULL снимает callback, readiness и crypto policy без ошибки отсутствия.
|
||||
int etcp_router_bind(struct UTUN_INSTANCE* inst, uint8_t svc_id, etcp_recv_fn callback) {
|
||||
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "NULL instance"); return -1; }
|
||||
if (!callback) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "unbind svc_id=%u", svc_id);
|
||||
inst->router_bindings.callbacks[svc_id] = NULL;
|
||||
inst->router_bindings.receive_ready[svc_id] = NULL;
|
||||
inst->router_bindings.required_crypto[svc_id] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -1486,7 +1504,7 @@ static void router_receive_pump(void* arg) {
|
||||
rconn->receive_pump = NULL;
|
||||
if (rconn->closed) return;
|
||||
// Физический recv_conn мог исчезнуть за время дискового I/O; сервис получает стабильный instance.
|
||||
router_try_assembly(rconn, loopback_conn(rconn->inst));
|
||||
router_try_assembly(rconn, NULL);
|
||||
if (!rconn->closed) router_schedule_ack(rconn);
|
||||
}
|
||||
|
||||
@@ -1513,8 +1531,7 @@ int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_
|
||||
|
||||
if (dst_node_id == inst->node_id) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "loopback svc_id=%u len=%zu", svc_id, payload_len);
|
||||
router_deliver_loopback(inst, group_id, svc_id, entry);
|
||||
return 0;
|
||||
return router_deliver_loopback(inst, group_id, svc_id, entry, mode);
|
||||
}
|
||||
|
||||
struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, dst_node_id, svc_id);
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
// Изменяемый список находится вне end-to-end подписи/шифрования. Получатель
|
||||
// проверяет его границы, уникальность, первый ID=src и последний ID=ETCP peer.
|
||||
// Это защита от ошибок маршрутизации, а не доказательство пути от недоверенного пира.
|
||||
// flags: bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE
|
||||
// flags: bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE, bit0=RECEIVE_BUSY (ACK)
|
||||
// reset_id/peer_reset_id — идентификаторы отправителя/получателя во всех пакетах.
|
||||
// START=HELLO, RST=CHALLENGE, START|RST=CONFIRM; challenge используется только в handshake.
|
||||
// ACK-пакет: тот же заголовок, seq=rx_seq, payload_len=0
|
||||
// ACK-пакет: тот же заголовок, seq=rx_seq, payload_len=0; RECEIVE_BUSY поддерживает ожидание без продвижения seq.
|
||||
// Локальная доставка через etcp_route_send выполняется синхронно без сетевого канала/crypto/receive_ready.
|
||||
#ifndef ETCP_ROUTER_H
|
||||
#define ETCP_ROUTER_H
|
||||
|
||||
@@ -48,7 +49,7 @@ struct SVC_ROUTE_HDR {
|
||||
uint64_t src_node_id;
|
||||
uint32_t seq; // data: tx_seq; ACK: rx_seq (ожидаемый seq)
|
||||
uint8_t svc_id; // идентификатор сервиса
|
||||
uint8_t flags; // bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE
|
||||
uint8_t flags; // ROUTER_FLAG_*; RECEIVE_BUSY допустим только в ACK без payload
|
||||
uint16_t timestamp; // часы отправителя (0.1ms), для RTT; ACK: эхо последнего data-пакета
|
||||
uint64_t reset_id; // случайный идентификатор локального экземпляра router_conn
|
||||
uint64_t peer_reset_id; // идентификатор экземпляра получателя (0 только в HELLO)
|
||||
@@ -109,7 +110,7 @@ struct ETCP_ROUTER_CONN {
|
||||
uint8_t svc_id; // = data[16]
|
||||
uint16_t last_dgram_ts; // последний timestamp датаграммы (rx или tx, timebase 0.1ms)
|
||||
uint16_t rtt; // измеренный RTT (0.1ms)
|
||||
int32_t rtt_jitter; // EMA jitter, fixed-point *65536 (как в ETCP)
|
||||
uint32_t rtt_jitter; // EWMA |ΔRTT|, беззнаковый fixed-point *65536; монитор показывает удвоенный EWMA
|
||||
uint16_t max_inflight; // настраиваемый рабочий максимум (default=ROUTER_MAX_INFLIGHT)
|
||||
uint16_t inflight_limit; // текущий эффективный лимит (=max_inflight или MINRTT_PROBE_MAX_INFLIGHT)
|
||||
uint16_t minrtt; // скользящее среднее minRTT (0.1ms), default=100 (10ms)
|
||||
@@ -128,12 +129,12 @@ struct ETCP_ROUTER_CONN {
|
||||
uint32_t tx_seq; // следующий seq для отправки
|
||||
uint32_t tx_sent; // граница фактически отправленных пакетов (ACK <= tx_sent)
|
||||
uint32_t rx_seq; // ожидаемый seq для сборки (next expected)
|
||||
uint32_t tx_acked; // сколько наших пакетов подтвердил remote (для inflight)
|
||||
uint32_t tx_acked; // следующий seq после подтверждённых пакетов, модульный uint32, не накопленный счётчик
|
||||
uint32_t last_sent_ack_seq; // последний отправленный ACK (= rx_seq на момент отправки)
|
||||
uint64_t last_ack_sent_tb; // время отправки последнего ACK (timebase 0.1ms)
|
||||
|
||||
struct UTUN_INSTANCE* inst;
|
||||
struct ETCP_CONN* recv_conn; // физический conn последнего принятого data-пакета (для deliver, не NULL)
|
||||
struct ETCP_CONN* recv_conn; // заимствованный транспорт последнего DATA; NULL после reset/уничтожения транспорта
|
||||
struct ll_queue* recv_q; // reorder очередь: hash по seq (4 байта, offset 0)
|
||||
void* ack_timer; // периодический таймер (10ms при consumer_ack)
|
||||
void* idle_ack_timer; // idle таймер (дослать последний ack)
|
||||
@@ -156,16 +157,14 @@ struct ETCP_ROUTER_CONN {
|
||||
uint64_t challenge_sent_tb; // время отправки запроса, 0.1 ms
|
||||
void* handshake_timer; // таймер повторов согласования
|
||||
uint8_t start_sent; // получен первый ACK данных
|
||||
uint8_t peer_sync_done; // 1 = завершено подтверждение peer ID
|
||||
|
||||
// Ретрансмиты: inflight очередь
|
||||
struct ll_queue* inflight_q; // хеш по seq (4 байта), непрерывный блок без SACK
|
||||
void* retrans_timer; // таймер проверки застоя ACK
|
||||
uint64_t last_ack_changed_tb; // когда tx_acked последний раз менялся (0.1ms)
|
||||
uint64_t last_ack_changed_tb; // отсчёт RTO: продвижение ACK, принятый RECEIVE_BUSY или начало повторного прохода (0.1ms)
|
||||
|
||||
// Очередь приёма входящего трафика (между сетью и recv_q)
|
||||
struct ll_queue* incoming_q; // FIFO очередь входящих пакетов
|
||||
uint8_t incoming_data_ready; // 1 = данные в incoming_q и callback был вызван
|
||||
uint8_t receive_blocked; // сервис временно не принимает следующую кодограмму
|
||||
uint8_t peer_receive_blocked; // последний ACK пира объявил паузу приёма
|
||||
void* receive_pump; // отложенное продолжение приёма
|
||||
@@ -178,7 +177,7 @@ struct ETCP_ROUTER_CONN {
|
||||
uint32_t c_retrans_done; // число выполненых ретрансмитов
|
||||
uint32_t c_dup_dropped; // дропнуто дубликатов seq
|
||||
uint32_t c_oob_dropped; // дропнуто out-of-bounds seq
|
||||
uint32_t c_stale_ack; // устаревших ACK
|
||||
uint32_t c_stale_ack; // ACK вне диапазона или без продвижения seq, включая допустимый повтор RECEIVE_BUSY
|
||||
uint32_t c_sign_fail; // signature verification failures
|
||||
struct route_tx_meter delivery; // первые подтверждения исходного payload, без wire overhead
|
||||
};
|
||||
@@ -193,7 +192,9 @@ struct ETCP_ROUTER_TX_STATS {
|
||||
int ready, no_route, receive_busy, probing;
|
||||
};
|
||||
|
||||
/* Только uasync. 0 — снимок, 1 — канал отсутствует/закрыт, -1 — неверные аргументы. Канал не создаёт. */
|
||||
/* Только uasync. 0 — снимок, 1 — канал отсутствует/закрыт (out обнулён), -1 — неверные аргументы/время.
|
||||
* now_us — монотонные микросекунды, не раньше последнего учтённого ACK. При -1 out не меняется; канал не создаётся.
|
||||
* ready означает подтверждённый peer_reset_id, отдельно от READY топологии или готовности физического транспорта. */
|
||||
int etcp_router_tx_stats(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t peer, uint8_t svc,
|
||||
uint64_t now_us, struct ETCP_ROUTER_TX_STATS *out);
|
||||
|
||||
@@ -225,7 +226,7 @@ struct ROUTER_INFLIGHT {
|
||||
struct ll_entry ll; // индекс по seq (4 байта, offset 0)
|
||||
uint32_t seq;
|
||||
uint64_t last_sent_tb; // время последней отправки (0.1ms)
|
||||
uint8_t send_count; // число переотправок
|
||||
uint8_t send_count; // успешные отправки, включая первую; диагностический циклический счётчик
|
||||
uint8_t* dgram; // неизменяемое тело [SVC_ROUTE_HDR][payload][sig?], без visited_ids/count
|
||||
size_t dgram_len;
|
||||
};
|
||||
@@ -241,24 +242,32 @@ struct ETCP_ROUTER_BINDINGS {
|
||||
// 3. Публичное API (для сервисов)
|
||||
// ====================================================================
|
||||
|
||||
// Инициализация: etcp_bind(ETCP_ID_SVC_ROUTE) + очистка bindings + создание router_conns
|
||||
// Первичная инициализация ядра: очистить bindings, создать router_conns, etcp_bind(ETCP_ID_SVC_ROUTE); 0/-1.
|
||||
// Вызывать один раз до запуска сервисов; после destroy можно инициализировать заново.
|
||||
int etcp_router_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Закрыть каналы, снять обработчик и освободить маршрутизатор.
|
||||
// Снять обработчик, закрыть каналы и очистить все транзитные очереди/таймеры на живых ETCP. Повторный destroy допустим.
|
||||
// До вызова остановить производителей/ожидания сервисов; из callback останавливаемого сервиса ядро не уничтожать.
|
||||
void etcp_router_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Зарегистрировать обработчик сервиса
|
||||
// Зарегистрировать/заменить callback; 0/-1. Замена сохраняет receive_ready/crypto policy.
|
||||
// callback=NULL снимает callback, receive_ready и policy, даже если сервиса не было; каналы не закрывает.
|
||||
// Сервис получает владение entry: [26-byte ROUTER_SVC header][payload]. Пустой payload — уведомление close/reset.
|
||||
// conn заимствован на время callback; при локальных событиях синтетический (instance, peer=self), отправлять через него нельзя.
|
||||
// End-to-end src/dst/group читать из заголовка через memcpy; conn может обозначать промежуточного пира.
|
||||
int etcp_router_bind(struct UTUN_INSTANCE* inst, uint8_t svc_id, etcp_recv_fn callback);
|
||||
// Снять обработчик; само по себе не закрывает логические каналы сервиса.
|
||||
int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id);
|
||||
|
||||
// Задать обязательные ROUTER_FLAG_ENCRYPTED/SIGNED для зарегистрированного сервиса, в обе стороны.
|
||||
// Проверка входящих данных — до состояния/очередей/receive_ready, только на конечном узле; управляющие пакеты без payload исключены.
|
||||
// Задать обязательные ROUTER_FLAG_ENCRYPTED/SIGNED для зарегистрированного сервиса, в обе стороны сетевого обмена.
|
||||
// Входящие данные проверяются до состояния/очередей/receive_ready, на конечном узле; header-only control исключён.
|
||||
// Доверенная синхронная доставка dst=self обходит crypto policy и даёт rx_flags=0.
|
||||
// Работает в uasync; unbind очищает требование. Возвращает 0 при успехе, -1 при неверном instance/сервисе/маске.
|
||||
int etcp_router_require_crypto(struct UTUN_INSTANCE* inst, uint8_t svc_id, uint8_t flags);
|
||||
|
||||
// Проверяется до доставки/ACK. 0 оставляет пакет в recv_q, сохраняя порядок; CLOSE не блокируется.
|
||||
// ready получает заимствованный проверенный payload и работает в uasync без I/O. NULL снимает ограничение.
|
||||
// ready получает заимствованный plaintext, работает в uasync без I/O и изменения состояния/закрытия роутера.
|
||||
// NULL снимает ограничение; для уже приостановленных каналов вызвать resume_receive отдельно. Локальная доставка не проверяется.
|
||||
void etcp_router_set_receive_ready(struct UTUN_INSTANCE* inst, uint8_t svc_id,
|
||||
int (*ready)(struct UTUN_INSTANCE*, uint64_t, uint64_t, const uint8_t*, size_t));
|
||||
// Продолжение отложено; канал не создаётся, waiter/arg сервиса не сохраняются.
|
||||
@@ -266,8 +275,11 @@ void etcp_router_resume_receive(struct UTUN_INSTANCE* inst, uint64_t group, uint
|
||||
|
||||
// Отправить сервисный пакет (авто-conn, seq, inflight-контроль через send_q).
|
||||
// Принимает владение entry во всех исходах, включая ошибку.
|
||||
// entry->dgram = svc_id[1] || payload; 0=принято в очередь, <0=ошибка, не подтверждение доставки.
|
||||
// force разрешает превысить лимит числа пакетов в send_q.
|
||||
// entry->dgram = svc_id[1] || непустой payload; mode должен содержать только допустимые биты.
|
||||
// Для удалённого узла 0=принято в send_q, не подтверждение доставки. Предел включает wire-header, crypto и полный visited-list.
|
||||
// Для dst=self callback вызывается синхронно, 0=доставлено callback, -1=размер/нет обработчика/ошибка памяти.
|
||||
// В self-ветке нет канала, inflight, receive_ready и crypto; rx_flags=0, payload до UINT16_MAX-ROUTER_SVC_HDR_SIZE байт.
|
||||
// force разрешает превысить лимит числа пакетов сетевой send_q; для self не используется.
|
||||
// mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (см. route_crypto.h), 0 = обычный пакет.
|
||||
int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force, int mode);
|
||||
|
||||
@@ -287,14 +299,17 @@ struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_
|
||||
|
||||
// Отправить данные с авто-seq и контролем inflight
|
||||
// data: payload без svc_id, mode: ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (0 = обычный)
|
||||
// Копирует data; 0=принято в очередь, <0=ошибка. Доставка подтверждается отдельно протоколом.
|
||||
// Копирует непустой data; 0=принято в очередь, <0=ошибка. Доставка подтверждается отдельно протоколом.
|
||||
// Это сетевой API rconn; для синхронной локальной доставки использовать etcp_route_send.
|
||||
int etcp_router_conn_send(struct ETCP_ROUTER_CONN* rconn,
|
||||
const uint8_t* data, size_t len, int mode);
|
||||
|
||||
// Закрыть канал и уведомить сервис. После вызова rconn использовать нельзя.
|
||||
// Снять канал с реестра, затем синхронно уведомить сервис и очистить ресурсы; память освобождается отложенно.
|
||||
// Callback может закрывать другие каналы/группу или открыть замену с тем же ключом. После close rconn использовать нельзя.
|
||||
void etcp_router_conn_close(struct ETCP_ROUTER_CONN* rconn);
|
||||
|
||||
// Установить рабочий max_inflight, пересчитывает inflight_limit и state machine
|
||||
// Установить max_inflight (минимум 4), пересчитать inflight_limit и state machine.
|
||||
// В minRTT probe лимит равен 4; reset сессии возвращает default=256.
|
||||
void etcp_router_set_max_inflight(struct ETCP_ROUTER_CONN* rconn, uint16_t new_max);
|
||||
|
||||
// Дождаться порога send_q. h должен быть обнулён и жить до callback/отмены.
|
||||
@@ -307,7 +322,7 @@ void etcp_router_on_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id, ui
|
||||
void etcp_router_cancel_send_ready(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, uint8_t svc_id,
|
||||
struct queue_waiter_handle* h);
|
||||
|
||||
// Backpressure: проверить есть ли место в send_q (без регистрации waiter)
|
||||
// Есть ли место в send_q существующего канала; 1/0. Канал не создаёт, его отсутствие даёт 0.
|
||||
int etcp_router_send_q_has_room(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, uint8_t svc_id);
|
||||
|
||||
// ====================================================================
|
||||
@@ -323,7 +338,8 @@ void etcp_router_close_group(struct UTUN_INSTANCE* inst, uint64_t group_id);
|
||||
// Закрыть логические каналы одного пира в группе.
|
||||
void etcp_router_conn_close_all_for_node(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id);
|
||||
|
||||
// Начать новую локальную сессию для конкретного peer+svc в группе и уведомить сервис.
|
||||
// Сбросить существующий peer+svc: новая локальная эпоха, пустые очереди/seq/метрики, затем уведомление сервиса и HELLO.
|
||||
// Отсутствующий канал не создаётся. Данные, отправленные сервисом из уведомления, относятся к новой сессии.
|
||||
void etcp_router_conn_restart(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id);
|
||||
|
||||
// Удалить все транзитные очереди соединения (вызывается при закрытии ETCP_CONN)
|
||||
|
||||
@@ -1,121 +1,192 @@
|
||||
# ETCP Router: протокол и проверка транспорта
|
||||
# ETCP Router: протокол и сервисный API
|
||||
|
||||
Логическое соединение определяется `(group_id, remote_node_id, svc_id)`. Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами; физический ETCP и транзитные узлы могут меняться. Формат изменён без обратной совместимости.
|
||||
Логический канал определяется тройкой `(group_id, remote_node_id, svc_id)`.
|
||||
Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами;
|
||||
физический ETCP и транзитные узлы могут меняться. Работа и callbacks выполняются в uasync-потоке ядра.
|
||||
|
||||
Объявления и точные константы — в `etcp_router.h`, схема потоков — в
|
||||
[архитектурном описании](../../doc/etcp_router_arch.md). Формат не имеет согласования версий;
|
||||
узлы используют один формат сборки, обратная совместимость не поддерживается.
|
||||
|
||||
## Wire-формат
|
||||
|
||||
Packed `SVC_ROUTE_HDR`, 57 байт:
|
||||
Packed `SVC_ROUTE_HDR`, 57 байт, числовые поля в native endian:
|
||||
|
||||
`cmd:1 | group:8 | dst:8 | src:8 | seq:4 | svc:1 | flags:1 | timestamp:2 | reset_id:8 | peer_reset_id:8 | challenge:8`
|
||||
|
||||
Числовое представление полей сохраняет существующий формат проекта (native endian). Отдельной версии/согласования формата нет; узлы должны обновляться совместно.
|
||||
|
||||
Полный пакет: `[SVC_ROUTE_HDR][payload][signature?][visited_ids:8*count][count:1]`.
|
||||
Источник начинает список своим ID; транзит проверяет отсутствие собственного ID и дописывает его.
|
||||
Правило действует для DATA, ACK и всех управляющих пакетов. Повторное посещение отбрасывается;
|
||||
также проверяются размер, уникальность ID, первый ID=src и последний ID=ETCP peer.
|
||||
Предел — 64 ID; на конечном узле полный список допустим, для следующего перехода места уже нет.
|
||||
|
||||
Источник начинает список собственным ID. Каждый транзитный узел проверяет список и добавляет
|
||||
свой ID один раз на приёме, перед ожиданием очереди. При повторной отправке источника список создаётся заново.
|
||||
Проверяются границы, 1..64 ID, отсутствие нулей/дубликатов/собственного ID, первый ID=src,
|
||||
последний ID=ETCP peer. Полный список из 64 ID допустим на конечном узле; следующий транзит уже невозможен.
|
||||
|
||||
Список находится вне end-to-end подписи/шифрования и снимается перед crypto decode.
|
||||
Это защита от временных петель при сходимости BGP, а не криптографическое подтверждение пути.
|
||||
Это защита от временных петель BGP, а не доказательство пути от недоверенного пира.
|
||||
Транзит не декодирует payload. При занятой физической очереди он сохраняет пакет в FIFO
|
||||
для `(group, src, dst)`; waiter и таймер 20 ms повторно выбирают путь.
|
||||
При исчезновении маршрута пакет отбрасывается, повтор доставки выполняет источник.
|
||||
|
||||
Маршруты строит BGP по правилам `topo_group.h`. Транзит использует текущий next hop.
|
||||
При backpressure пакет ждёт в очереди; waiter и таймер 20 ms повторно проверяют маршрут.
|
||||
Смена пути не требует освобождения старой физической очереди. ID добавляется один раз на приёме,
|
||||
а не при каждой попытке выгрузки. При исчезновении маршрута транзит отбрасывается; доставку повторяет источник.
|
||||
Отправитель сначала выбирает транспорт через conn_mgr, затем BGP группы; при отсутствии группы
|
||||
допускается прямой глобальный транспорт. Транзит требует маршрута зарегистрированной группы:
|
||||
одно прямое соединение не разрешает ретрансляцию для неизвестного group_id.
|
||||
Право обработки сервисных данных проверяет сам сервис.
|
||||
|
||||
`reset_id` — случайный ненулевой ID локального экземпляра соединения; `peer_reset_id` — ID получателя. DATA, ACK и CLOSE принимаются только при совпадении обоих ID с текущей парой. Смена физического транспорта пару не меняет.
|
||||
## Согласование эпох
|
||||
|
||||
| Пакет | Флаги | Payload | Поля |
|
||||
|---|---|---|---|
|
||||
| HELLO | START (0x80) | нет | собственный ID, peer=0, challenge=0 |
|
||||
| CHALLENGE | RST (0x40) | нет | оба ID, случайный ненулевой challenge |
|
||||
| CONFIRM | START|RST | нет | оба ID, эхо challenge |
|
||||
| DATA | 0 либо SIGNED/ENCRYPTED | непустой | seq, оба ID, challenge=0 |
|
||||
| ACK | 0 | нет | seq=следующий ожидаемый, оба ID, challenge=0 |
|
||||
| CLOSE | CLOSE (0x02) | нет | оба ID, challenge=0 |
|
||||
`reset_id` — случайный ненулевой ID локального экземпляра канала,
|
||||
`peer_reset_id` — подтверждённый ID получателя. DATA/ACK/CLOSE допускаются только для текущей пары.
|
||||
Смена физического транспорта пару не меняет.
|
||||
|
||||
Незнакомый ID сам по себе не сбрасывает рабочую сессию. Получатель выдаёт свежий challenge и меняет peer ID только после соответствующего CONFIRM. CHALLENGE не меняет рабочую сессию: отправляется CONFIRM и, если нужно, встречный challenge. Потерянные сообщения handshake восстанавливаются повторением HELLO через 300 ms; выдача новых challenge ограничена тем же интервалом.
|
||||
| Исходящий пакет | flags | Payload | Значение |
|
||||
| --- | --- | --- | --- |
|
||||
| HELLO | START, 0x80 | Нет | Свой ID, peer=0, challenge=0 |
|
||||
| CHALLENGE | RST, 0x40 | Нет | Оба ID и случайный ненулевой challenge |
|
||||
| CONFIRM | START\|RST | Нет | Оба ID и эхо challenge |
|
||||
| DATA | 0, SIGNED 0x08 и/или ENCRYPTED 0x04 | Непустой | seq, оба ID, challenge=0 |
|
||||
| ACK | 0 или RECEIVE_BUSY 0x01 | Нет | seq=rx_seq, оба ID, challenge=0 |
|
||||
| CLOSE | CLOSE, 0x02 | Нет | Оба ID, challenge=0 |
|
||||
|
||||
Первичное согласование сохраняет накопленные исходящие данные. Подтверждённый рестарт пира сбрасывает seq/очереди и уведомляет сервис; собственный ID сохраняется. Явный `etcp_router_conn_restart` создаёт новый собственный ID. Сброс выполняется до уведомления, поэтому данные, отправленные сервисом из callback, сохраняются. Сервис должен заново выполнить прикладную синхронизацию после уведомления; гарантии доставки не продолжаются через явный сброс сессии.
|
||||
Незнакомый ID не сбрасывает рабочую сессию: получатель сначала выдаёт challenge.
|
||||
CONFIRM должен совпасть с локальным ID, pending peer ID и challenge.
|
||||
CHALLENGE вызывает ответ CONFIRM и при необходимости встречную проверку, но сам по себе не меняет сессию.
|
||||
Выдача новых challenge ограничена интервалом 300 ms; пока первичный peer не подтверждён,
|
||||
HELLO повторяется каждые 300 ms без отдельного таймаута. При этом исходные данные остаются в send_q.
|
||||
На этом этапе отсутствие маршрута обрабатывается тем же HELLO-таймером.
|
||||
|
||||
Challenge защищает от случайных старых/переставленных управляющих пакетов, но не является end-to-end аутентификацией против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. Подпись/шифрование DATA включаются вызывающим сервисом через `ROUTE_CRYPTO_SIGN` / `ROUTE_CRYPTO_ENCRYPT`.
|
||||
Первичное согласование сохраняет накопленные исходящие данные.
|
||||
Подтверждённый рестарт пира очищает seq/очереди/метрики и уведомляет сервис, сохраняя собственный ID.
|
||||
Явный `etcp_router_conn_restart` выбирает новый собственный ID. Отсутствующий канал не создаётся.
|
||||
Сброс выполняется до уведомления, поэтому отправка из callback относится уже к новой сессии.
|
||||
Гарантии прежнего канала не продолжаются через его сброс; прикладную синхронизацию возобновляет сервис.
|
||||
|
||||
## Очереди и подтверждения
|
||||
Challenge защищает от старых/переставленных пакетов, но не аутентифицирует end-to-end управление
|
||||
против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений.
|
||||
DATA кодируются через route_crypto: сначала шифрование, затем подпись заголовка и payload.
|
||||
|
||||
- `send_q`: plaintext + seq + crypto mode, до 64 пакетов при обычной отправке. Кодирование происходит при отправке после handshake, с актуальной парой ID.
|
||||
- `tx_seq`: следующий выделяемый seq; `tx_sent`: граница фактически отправленного; `tx_acked`: граница подтверждённого.
|
||||
- `inflight_q`: неизменяемое тело с подписью, без списка посещённых узлов, обычно до 256 пакетов. Каждый ретрансмит начинает новый список; ошибка не теряет pending-пакет.
|
||||
- ACK принимается только в модульном интервале `(tx_acked, tx_sent]`. Дубликаты, старые и выходящие за границу ACK не продлевают ожидание прогресса. Удаляются существующие inflight-записи, а не перебираются все номера до произвольного ACK.
|
||||
- Новые DATA и ретрансмиты идут по одному через waiter физической `send_input_q`. При смене пути отменяется waiter на очереди, где он действительно был зарегистрирован, включая уже запланированный callback.
|
||||
- Каждые 300 ms без прогресса ACK планируется проход повторной отправки. Логический канал закрывается после 67 циклов без прогресса для `ETCP_RT_ID_CALL` (около 20 s), для остальных сервисов — после 17 циклов (около 5 s). Отсутствующий маршрут проверяется каждые 20 ms; очередь сохраняется. Пока первичный handshake не завершён, HELLO повторяется без отдельного таймаута.
|
||||
- Watchdog 500 ms восстанавливает отправку и переносит waiter при смене заблокированного пути.
|
||||
- Сброс физического ETCP сохраняет router inflight и seq. Восстановление самого ETCP-handshake остаётся обязанностью транспортного слоя.
|
||||
- Crypto decode/проверка подписи выполняются до помещения в reorder и изменения rx_seq. Повреждённый пакет не подтверждается как доставленный.
|
||||
- DATA выдаются сервису строго по seq; входящие дубликаты повторно проверяются после извлечения из очереди.
|
||||
- Wire-пакет ограничен `PKTNORM_MAX_DGRAM_SIZE` (16384), включая заголовок, crypto overhead и резерв полного списка переходов. Пустой payload запрещён: он используется для управления.
|
||||
## Очереди и seq
|
||||
|
||||
## Сервисный API
|
||||
| Состояние | Содержимое/смысл |
|
||||
| --- | --- |
|
||||
| send_q | Plaintext FIFO; в entry.data — выделенный seq и crypto mode |
|
||||
| tx_seq | Следующий выделяемый seq |
|
||||
| tx_sent | Граница действительно отправленных пакетов |
|
||||
| tx_acked | Граница подтверждённых пакетов, модульный uint32 |
|
||||
| inflight_q | Неизменяемое подписанное/зашифрованное тело без visited-list; исходная длина payload учитывается отдельно |
|
||||
| incoming_q | Проверенный plaintext между сетью и reorder |
|
||||
| recv_q | Reorder по seq; данные, ещё не выданные сервису |
|
||||
| rx_seq | Следующий ожидаемый seq, увеличивается после успешного callback |
|
||||
|
||||
`etcp_router_conn_send(rconn, data, len, mode)` копирует payload; 0 означает принятие в очередь, а не end-to-end доставку. При backpressure производитель ждёт через `etcp_router_on_send_ready`; отменяет ожидание через `etcp_router_cancel_send_ready`. Handle должен жить до callback/отмены. Режим `force` низкоуровневого `etcp_route_send` обходит порог очереди и не предназначен для непрерывного потока.
|
||||
Начальные seq равны 0. Сравнения выполняются по модульной разности uint32.
|
||||
Обычная отправка ограничена 64 пакетами send_q; `force` обходит этот порог, но не проверку размера/режима.
|
||||
Кодирование выполняется при реальной отправке после handshake, с текущими эпохами.
|
||||
После успешного etcp_send запись снимается с send_q; при отказе исходный пакет остаётся в очереди.
|
||||
|
||||
`etcp_router_bind` регистрирует callback, владеющий переданным ll_entry. Формат доставки — 26 байт заголовка:
|
||||
Обычный inflight — 256 пакетов, минимум настраиваемого max_inflight — 4.
|
||||
Во время minRTT probe действует лимит 4; сброс сессии возвращает default=256.
|
||||
Новые DATA и ретрансмиты проходят по одному через waiter физической send_input_q.
|
||||
При смене пути отменяется ожидание на очереди, где оно действительно зарегистрировано.
|
||||
Штатный normalizer откладывает физические waiters, чтобы избежать рекурсивного drain.
|
||||
|
||||
Wire-пакет ограничен `PKTNORM_MAX_DGRAM_SIZE` (16384), включая 57-байтный заголовок,
|
||||
crypto overhead и резерв полного 513-байтного visited-list. Пустой payload зарезервирован для управления.
|
||||
Входной размер проверяется до выделения/копирования payload, включая SIZE_MAX и неверные биты mode.
|
||||
|
||||
## ACK, backpressure и повторы
|
||||
|
||||
ACK принимается в модульном диапазоне `[tx_acked, tx_sent]`.
|
||||
Только продвижение seq освобождает существующие inflight-записи и учитывает первую доставку payload.
|
||||
Старые/выходящие за диапазон ACK не продлевают ожидание прогресса.
|
||||
|
||||
Исключение — допустимый RECEIVE_BUSY: даже при прежнем seq он подтверждает живость принимающего сервиса,
|
||||
обновляет отсчёт RTO, обнуляет no_ack_count, останавливает текущий повторный проход и новую отправку.
|
||||
Снятие BUSY возобновляет send_q, в том числе при прежнем seq.
|
||||
Busy ACK не подтверждает отложенные пакеты. c_stale_ack включает и допустимые ACK без продвижения.
|
||||
|
||||
receive_ready получает заимствованный проверенный plaintext перед callback и ACK.
|
||||
Это предикат без I/O и изменения/закрытия состояния роутера.
|
||||
При 0 пакет остаётся в recv_q, порядок сохраняется; принимающий узел публикует BUSY примерно каждые 100 ms.
|
||||
CLOSE обрабатывается отдельно и предикатом не блокируется.
|
||||
`etcp_router_resume_receive` планирует продолжение, не создаёт канал и не сохраняет аргументы сервиса.
|
||||
Снятие предиката через set_receive_ready(NULL) само по себе не планирует продолжение уже остановленных каналов.
|
||||
|
||||
ACK ограничен снизу интервалом 10 ms. При повторном DATA после потери ACK текущая граница отсылается снова.
|
||||
Idle-таймер 500 ms отправляет ACK только если rx_seq ещё отличается от last_sent_ack_seq;
|
||||
безусловного повтора каждого последнего ACK нет.
|
||||
|
||||
Каждые 300 ms без продвижения ACK или допустимого BUSY планируется повторный проход inflight.
|
||||
Канал закрывается после 17 циклов (около 5 s), для ETCP_RT_ID_CALL — 67 (около 20 s).
|
||||
Отсутствующий маршрут после согласования проверяется каждые 20 ms, очередь сохраняется.
|
||||
Watchdog 500 ms повторяет kick при непустой send_q и переносит waiter на актуальный путь.
|
||||
Сброс физического ETCP сохраняет router seq/inflight; восстановление физического handshake выполняет транспорт.
|
||||
|
||||
До изменения seq/reorder выполняются требования crypto policy и decode/verify.
|
||||
Повреждённая подпись/криптограмма не подтверждается как доставленная.
|
||||
Приём проверяет окно относительно max_inflight, а суммарный буфер incoming_q+recv_q ограничен max_inflight+1.
|
||||
|
||||
## Сервисный API и локальная доставка
|
||||
|
||||
`etcp_router_bind` регистрирует callback, получающий владение ll_entry:
|
||||
|
||||
`svc:1 | src:8 | dst:8 | rx_flags:1 | group:8 | payload...`
|
||||
|
||||
Читать невыравненные числовые поля нужно через `memcpy`. Заголовок без payload означает уведомление о закрытии/сбросе. После обработки освобождаются `queue_dgram_free(entry)` и `queue_entry_free(entry)`.
|
||||
Заголовок доставки — 26 байт. Числовые поля не выровнены, читать через memcpy.
|
||||
rx_flags содержит crypto-флаги исходного сетевого пакета. Пустой payload — уведомление close/reset.
|
||||
После обработки сервис освобождает queue_dgram_free(entry) и queue_entry_free(entry).
|
||||
|
||||
Callback может закрыть соединение. Освобождение send_q отложено за пределы callback производителя; отправка проверяет, не закрылся/перезапустился ли rconn. При уничтожении реестр удаляется ровно один раз для каждого соединения.
|
||||
conn заимствован на время callback и может обозначать промежуточного ETCP-пира.
|
||||
Для локальной доставки, закрытия и отложенного приёма он синтетический: instance и peer=self.
|
||||
Этот объект нельзя сохранять или использовать для сетевой отправки/ref_take;
|
||||
реальные end-to-end src/dst/group берутся из сервисного заголовка.
|
||||
Каждый синтетический callback имеет собственный контекст, включая вложенные вызовы разных ядер.
|
||||
|
||||
Замена ненулевого callback сохраняет readiness/policy. bind(NULL) и unbind снимают оба ограничения;
|
||||
unbind возвращает -1 при отсутствии обработчика, bind(NULL) в этом случае даёт 0.
|
||||
Снятие обработчика не закрывает существующие каналы.
|
||||
|
||||
`etcp_router_conn_send` копирует непустой payload в сетевую send_q.
|
||||
Возврат 0 означает принятие, не end-to-end доставку.
|
||||
Производитель ждёт порог через on_send_ready и отменяет handle через cancel_send_ready.
|
||||
Callback ожидания может выполниться синхронно; handle и arg должны жить до завершения/отмены.
|
||||
Отсутствующий канал не создаётся ни проверкой места, ни регистрацией ожидания.
|
||||
|
||||
`etcp_route_send` принимает entry с svc_id перед payload и освобождает его во всех исходах.
|
||||
Для удалённого узла создаёт канал и ставит копию в очередь.
|
||||
Для dst=self выполняет callback синхронно без канала, seq/inflight, receive_ready или crypto.
|
||||
Доверенная self-ветка обходит сетевую crypto policy и передаёт rx_flags=0; допустимые биты mode проверяются.
|
||||
Её payload — 1..65509 байт, чтобы 26-байтный заголовок и данные помещались в uint16 длину.
|
||||
Нет обработчика, неверный размер/режим или отказ памяти дают -1 вместо ложного успеха.
|
||||
|
||||
## Закрытие и метрики
|
||||
|
||||
Индивидуальное закрытие сначала снимает канал с реестра, затем уведомляет сервис.
|
||||
Callback может закрыть свою группу/другие каналы или создать замену с тем же ключом.
|
||||
Ресурсы/таймеры очищаются, память канала освобождается отложенно; старый rconn после close использовать нельзя.
|
||||
Сервисные producers/waiters должны быть отменены до массового close_group/destroy.
|
||||
|
||||
close_group удаляет логические каналы и ожидающий транзит указанной группы.
|
||||
destroy снимает wire-обработчик, закрывает все логические каналы и очищает все транзитные очереди,
|
||||
их waiters и route timers даже при остающемся живым ETCP. Повторный destroy допустим.
|
||||
|
||||
tx_stats не создаёт канал: 0 — снимок, 1 — отсутствует/закрыт с нулевым out, -1 — аргументы/время.
|
||||
queued/inflight/acked bytes измеряют исходный payload. Повторы не увеличивают acked-итог.
|
||||
ready означает подтверждённый peer_reset_id, не READY группы и не гарантированный физический UP.
|
||||
RTT хранится в 0.1 ms, в snapshot переводится в микросекунды.
|
||||
Jitter — беззнаковый 16.16 EWMA |ΔRTT|; монитор публикует удвоенный EWMA без сужения до uint16.
|
||||
История delivery и параметры RTT/probe сбрасываются при смене логической сессии.
|
||||
|
||||
## Проверки и диагностика
|
||||
|
||||
`test_bgp_paths` использует настоящий BGP и роутер в управляемой сети четырёх узлов:
|
||||
все 64 графа, разбиение/слияние, изменения при непустых очередях, REINIT, выбор кратчайшего пути
|
||||
с устойчивым разрешением равенства. Дополнительно проверяет подписанный зашифрованный транзит,
|
||||
петли DATA/ACK/control, повреждённые списки переходов, смену заблокированного next hop и очистку таймеров.
|
||||
Счётчики `UTUN_INSTANCE.router_loop_drops` и `router_path_errors` сопровождаются WARN с идентичностью пакета.
|
||||
- test_etcp_router_unit: 43 проверки seq/reorder, crypto policy/verify/decrypt, RECEIVE_BUSY, метрик,
|
||||
nested close/reopen, вложенной self-доставки, предельной длины, unsigned jitter и очистки pending transit.
|
||||
Fixture освобождает собственные registry-ссылки и проверяет отсутствие оставшихся u_malloc-объектов.
|
||||
- test_etcp_router_faults: 15 сценариев потерь, перестановок, дубликатов, duplex/multiplexing, handshake,
|
||||
старых эпох/ACK, wraparound, смены маршрута, закрытия из callbacks, timeout и десятисекундного разрыва звонка.
|
||||
- test_bgp_paths: реальные BGP/роутеры в управляемой сети четырёх узлов, все 64 графа,
|
||||
signed+encrypted transit, петли DATA/ACK/control, повреждённые пути, смена занятого next hop и cleanup.
|
||||
- test_etcp_router, test_etcp_router_tcp, test_etcp_router_reconnect: UDP/STCP, backpressure,
|
||||
reset транспорта и транзит с перезапуском промежуточного узла.
|
||||
|
||||
`tests/test_etcp_router_faults` перехватывает SVC_ROUTE между настоящими роутерами, очередями и UASYNC-таймерами. Физическая очередь тестовая: нижележащий ETCP не скрывает потери от роутера. Проверяются:
|
||||
|
||||
1. Потери DATA/ACK, перестановка, дубликаты, четыре потока (два направления × два сервиса), backpressure и ограниченность физической очереди.
|
||||
2. Потеря HELLO/CHALLENGE/CONFIRM в обоих направлениях.
|
||||
3. Старые, повторные, слишком большие ACK и ACK ещё не отправленного пакета.
|
||||
4. Сохранение inflight при сбросе транспорта.
|
||||
5. Старые DATA/ACK/CLOSE/HELLO/CONFIRM после смены сессии.
|
||||
6. Переполнение uint32 seq.
|
||||
7. Закрытие после смены пути при зарегистрированном и отложенном waiter.
|
||||
8. Закрытие из callback приёма.
|
||||
9. Смена маршрута при занятой старой очереди без новой отправки.
|
||||
10. Исчезновение и восстановление маршрута.
|
||||
11. Таймаут при непрерывных повторных ACK без прогресса.
|
||||
12. Одновременный рестарт обоих концов.
|
||||
13. Граничные размеры payload, crypto overhead, SIZE_MAX.
|
||||
14. Закрытие из callback производителя при освобождении send_q.
|
||||
|
||||
Успех передачи требует совпадения содержимого, порядка и количества, финальных ACK, пустых send/inflight. Завершение проверяет освобождение таймеров. Unit-набор включает 34 проверки, в том числе криптографию и повторную доставку после неверной подписи.
|
||||
|
||||
Сетевой `test_etcp_router` использует UDP/dummynet, backpressure и сброс с восстановлением ETCP-handshake. `test_etcp_router_reconnect` проверяет a–b–c и перезапуск b, по 500 пакетов до/после, с ожиданием последних ACK в каждой фазе и фиксированным seed.
|
||||
|
||||
Переменная `ROUTER_TEST_LOG=/tmp/router.log` включает подробный файл. Рабочая категория — `etcp_route`; DEBUG показывает handshake, пару эпох, ретрансмиты и смену состояния; поштучные ACK/DATA оставлены на TRACE. Ошибочные ветки не скрыты. В fault-тесте WARN/ERROR для намеренно неверных ACK/размеров и искусственного таймаута ожидаемы.
|
||||
|
||||
Предыдущий этап проверки роутера: 14 fault-сценариев и 34 unit-проверки прошли; fault-набор также прошёл ASan/UBSan/LeakSanitizer (инструментированы router, route_crypto и ll_queue, остальная библиотека обычная). UDP-тест: 440/440 доставлено и подтверждено. Транзитный: 1000/1000 доставлено и подтверждено. Это не доказательство отсутствия всех ошибок стека.
|
||||
|
||||
## Lifecycle транспорта и восстановление
|
||||
|
||||
ETCP имеет единую точку финального освобождения после DELETE, завершения callback и освобождения последней ссылки. Запрос закрытия идемпотентен и сразу запрещает новые send/ref_take.
|
||||
|
||||
NCD-entry удерживает одну ссылку ETCP. При DELETE запись сначала исключается из реестра, затем владельцы получают CLOSED; старые handles безопасно закрываются после создания нового соединения с тем же node_id. Закрытые callbacks/handles удаляются после внешней рассылки. Отложенный начальный UP отменяется при DOWN/close. При удалении сокета обход использует снимок ссылок ETCP, поскольку DOWN может удалить NCD-entry.
|
||||
|
||||
Recovery удерживает TOPO_NODE для каждого уникального кандидата и освобождает ссылку при любом завершении. При успехе BGP сначала получает собственный NCD-handle. `test_topo_recovery` удаляет B и проверяет самостоятельное подключение A↔C и доставку 1000 сообщений без тестового reconnect.
|
||||
|
||||
`etcp_session.c` согласует эпохи физического транспорта через HELLO/CHALLENGE/CONFIRM поверх аутентифицированного UDP/STCP. Каждый DATA/ACK содержит пару эпох; старые кадры отбрасываются до обработки. Сброс потока не генерирует ложный DOWN. REINIT повторно запускает обмен таблицами BGP. Формат и таймауты описаны в `doc/etcp_protocol.txt`.
|
||||
|
||||
Дополнительные проверки:
|
||||
- `test_etcp_lifecycle`: DELETE/refcount, повторное закрытие, закрытие из callback, FIN_WAIT, отмена начального UP, удаление сокета, владение recovery.
|
||||
- `test_etcp_session`: реальные crypto/ingress/таймеры; reset любой стороны, одновременный reset, потери всех видов control, повторный reset, отказ одного из двух линков, timeout, старые DATA/ACK/control, неверные размеры.
|
||||
- `test_etcp_router_tcp`: тот же контроль содержимого, порядка, дубликатов, backpressure и финальных ACK по STCP; обе стороны сбрасываются одновременно.
|
||||
|
||||
Диагностика по запросу: `SESSION_TEST_LOG=/tmp/session.log`, `LIFECYCLE_TEST_LOG=/tmp/lifecycle.log`, `ROUTER_TEST_LOG=/tmp/router.log`, `BGP_TEST_LOG=/tmp/bgp.log`. `ROUTER_TEST_RESET=client|server|both` выбирает сторону reset; `ROUTER_TEST_TCP=1` включает STCP. Ожидаемые ошибки отрицательных сценариев не скрываются.
|
||||
|
||||
Дополнительно исправлен монитор conn_mgr: наличие маршрута через посредника не означает прямого соединения. Фоновый PING выбирает транспорт по node_id конечного peer и живой линк именно этого транспорта. Проверка BGP-треугольника выдерживает полный период фоновых проб и требует отсутствия ошибок входящих пакетов до внесения потерь. На старом monitor эта проверка воспроизводимо падает; с исправлением проходит.
|
||||
|
||||
Итоговая проверка текущих исправлений: 16 регрессионных executable-тестов прошли (router unit/faults/lifecycle/session, UDP, TCP, reconnect, recovery, STCP traffic, NCD, три conn_mgr, normalizer/ETCP, BGP triangle). Семь наборов дополнительно прошли ASan/UBSan/LeakSanitizer: lifecycle, session, router faults, UDP router, TCP router, autonomous recovery, BGP triangle. Инструментированы ETCP core/API/connections/session, NCD, router, recovery, topo_group/connect, conn_mgr core/monitor и ll_queue; остальные библиотечные объекты обычные. Отрицательные тесты намеренно вызывают ошибки; после принудительного удаления транспорта возможен отказ приёма запоздалой UDP-датаграммы без существующего линка. Финальный BGP-прогон, включая фоновые пробы и cleanup таймеров, завершился без диагностических ошибок.
|
||||
ROUTER_TEST_LOG=/tmp/router.log включает подробный файл в fault/network-наборах.
|
||||
Категория etcp_route: DEBUG — эпохи/повторы/состояния, TRACE — отдельные DATA/ACK.
|
||||
Ожидаемые WARN/ERROR отрицательных сценариев не скрываются.
|
||||
@@ -160,7 +160,8 @@ int multicast_open(struct TOPO_GROUP* group, multicast_loss_fn on_loss, void* ar
|
||||
/* Отправить через открытый descriptor, принадлежащий inst; группа берётся из дескриптора.
|
||||
* recipients: count>0 ненулевых node_id; повторы удаляются, собственный ID допустим.
|
||||
* len может быть 0; при len>0 payload обязателен. Размер payload, списка и полного
|
||||
* резерва пройденного пути должен помещаться в 16 КиБ.
|
||||
* резерва пройденного пути должен помещаться в 16 КиБ. Лимиты count и полного
|
||||
* размера проверяются до удаления дублей из входного списка.
|
||||
* 0=принято к обработке; <0=вызов не принят, ничего не отправлено.
|
||||
* После принятия отдельные ветки могут потеряться; уведомления идут в on_loss.
|
||||
* Обе входные области копируются и после возврата больше не используются. */
|
||||
@@ -217,7 +218,9 @@ void multicast_close_group(struct UTUN_INSTANCE* inst, uint64_t group_id);
|
||||
* cmd:1 | group:8 | descriptor:2 | count:2 | source:8 | recipients:8*count |
|
||||
* payload | visited:8*hops | hops:1.
|
||||
* Получатели строго возрастают. Первый visited=source, последний=ETCP peer.
|
||||
* Транзит добавляет себя один раз; повторное посещение запрещено.
|
||||
* Приём разрешён от ACCEPT-сессии своей группы, до обязательного завершения
|
||||
* её таблицы. Каждый получатель добавляет себя до локальной доставки/транзита;
|
||||
* повторное посещение и вход с hops=64 отбрасываются целиком, включая локальную доставку.
|
||||
*
|
||||
* Wire OVERLOAD — payload отдельного router-сервиса:
|
||||
* descriptor:2 | src:8 | dst:8 | packets:4 | count:2 | prefix_len:1 |
|
||||
|
||||
@@ -1676,6 +1676,12 @@ void nat_detection_observed_by_peer(struct NAT_DETECTION *detector, struct ETCP_
|
||||
|
||||
void nat_detection_socket_removed(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket) {
|
||||
if (!detector || !socket || detector->stopping) return;
|
||||
for (struct nat_socket *state = detector->sockets; state; state = state->next) {
|
||||
if (state->helper_primary != socket) continue;
|
||||
state->helper_primary = NULL;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT isolated helper removed source=%s helper=%s generation=%016llx",
|
||||
state->socket->name, socket->name, (unsigned long long)socket->generation);
|
||||
}
|
||||
if (!socket->probe_only) { detector->publish_pending = 1; detector->publish_after = get_time_tb(); }
|
||||
struct nat_probe *probe = detector->probes;
|
||||
while (probe) {
|
||||
@@ -1717,16 +1723,21 @@ struct NAT_DETECTION *nat_detection_create(struct UTUN_INSTANCE *instance) {
|
||||
detector->instance = instance; detector->automatic = 1; detector->reservation_dirty = 1;
|
||||
if (etcp_router_bind(instance, ETCP_RT_ID_NAT_DETECTION, receive_control)) { u_free(detector); return NULL; }
|
||||
etcp_add_conn_status_cbk(instance, connection_status, detector);
|
||||
if (!instance->conn_status_cbks || instance->conn_status_cbks->fn != connection_status || instance->conn_status_cbks->arg != detector) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT connection subscription allocation failed"); goto fail;
|
||||
}
|
||||
etcp_add_socket_cbk(instance, socket_changed, detector, ETCP_SOCKET_EVENT_ADDR_CHANGED);
|
||||
if (!instance->socket_cbks || instance->socket_cbks->fn != socket_changed || instance->socket_cbks->arg != detector) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT socket subscription allocation failed"); goto fail;
|
||||
}
|
||||
detector->timer = uasync_set_timeout(instance->ua, NAT_TICK_TB, detector, detector_tick, "nat_detection");
|
||||
if (!detector->timer) {
|
||||
etcp_remove_conn_status_cbk(instance, connection_status, detector);
|
||||
etcp_remove_socket_cbk(instance, socket_changed, detector);
|
||||
etcp_router_unbind(instance, ETCP_RT_ID_NAT_DETECTION); u_free(detector);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector initial timer failed"); return NULL;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT detector initial timer failed"); goto fail;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT endpoint detector started: UDP mapping/inbound, TCP IP-only presence with port=0");
|
||||
return detector;
|
||||
fail:
|
||||
nat_detection_destroy(detector); return NULL;
|
||||
}
|
||||
|
||||
void nat_detection_destroy(struct NAT_DETECTION *detector) {
|
||||
|
||||
@@ -49,7 +49,7 @@ enum nat_check_reason {
|
||||
NAT_REASON_ADDRESS_CHANGED, /* Наблюдаемый адрес изменился; прежний профиль сброшен. */
|
||||
NAT_REASON_CONTACT_HISTORY, /* IP уже был знаком или история неполна: вход не подтверждён как незнакомый. */
|
||||
NAT_REASON_PROXY, /* Сокет работает через SOCKS: его собственный NAT не измеряется. */
|
||||
NAT_REASON_NO_LISTENER, /* Нет публичного TCP-listener для проверки входящих. */
|
||||
NAT_REASON_NO_LISTENER, /* Резерв причины в управляющем протоколе; активных TCP-проб сейчас нет. */
|
||||
NAT_REASON_CANCELLED, /* Проверка отменена. */
|
||||
NAT_REASON_INVALID_OBSERVATION, /* Наблюдение непригодно, в том числе из-за непубличного source IP. */
|
||||
NAT_REASON_DEPENDENT_MAPPING, /* Определён strict, входящая доступность осталась неизвестной. */
|
||||
@@ -74,10 +74,14 @@ struct nat_check_status {
|
||||
};
|
||||
|
||||
/* Создать детектор для instance с готовым маршрутизатором и группами; автоматические проверки включены.
|
||||
* Возвращает контекст или NULL при ошибке; instance должен существовать до destroy(). */
|
||||
* Возвращает контекст или NULL при ошибке, снимая частично установленные обработчики.
|
||||
* Ядро сохраняет результат в instance->nat_det до обработки событий: управляющий RX использует это поле.
|
||||
* instance должен существовать до destroy(); повторное создание при занятом router-binding запрещено. */
|
||||
struct NAT_DETECTION *nat_detection_create(struct UTUN_INSTANCE *instance);
|
||||
|
||||
/* Остановить проверки, снять обработчики и освободить detector; NULL допустим. Ядро и его рабочие сокеты не освобождаются. */
|
||||
/* Остановить проверки, снять обработчики и освободить detector; NULL допустим.
|
||||
* Вызывать до teardown ETCP/router и вне callbacks детектора; затем обнулить instance->nat_det.
|
||||
* Ядро и рабочие сокеты не освобождаются; внутренние probe_only-сокеты принадлежат детектору. */
|
||||
void nat_detection_destroy(struct NAT_DETECTION *detector);
|
||||
|
||||
/* Сообщить detector об изменении адресов/готовности групп; пересмотр кандидатов отложен и объединяет повторные вызовы. */
|
||||
@@ -109,7 +113,8 @@ void nat_detection_cancel_for_conn(struct NAT_DETECTION *detector, struct ETCP_C
|
||||
* reverse_done != 0 — обратное подключение тоже исчерпано; только тогда перепроверяется filtered (CONTACT). */
|
||||
void nat_detection_connection_failed(struct NAT_DETECTION *detector, struct ETCP_CONN *connection, int reverse_done);
|
||||
|
||||
/* Удалить состояние socket и отменить связанные задачи; вызвать до освобождения самого сокета. */
|
||||
/* Удалить состояние socket и отменить связанные задачи; вызвать до освобождения самого сокета.
|
||||
* Для внутреннего probe_only также снимает ссылку владельца: последующий destroy не удалит сокет повторно. */
|
||||
void nat_detection_socket_removed(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket);
|
||||
|
||||
/* Учесть исходящую отправку с socket на destination (IP:порт) до передачи пакета транспорту.
|
||||
|
||||
@@ -39,10 +39,17 @@ struct ROUTE6_TABLE {
|
||||
struct ll_queue *queue; // indexed by node_id for delete
|
||||
};
|
||||
|
||||
/* Создать таблицу в ua (его время жизни длиннее таблицы); NULL при ошибке. */
|
||||
struct ROUTE6_TABLE *route6_table_create(struct UASYNC *ua);
|
||||
/* Освободить таблицу и записи, сохраняя заимствованные узлы; NULL допустим. */
|
||||
void route6_table_destroy(struct ROUTE6_TABLE *table);
|
||||
/* Вставить IPv6-подсети node. Вложенные префиксы допустимы; дубликаты radix пропускает.
|
||||
* true означает завершённый обход, даже если ничего не добавлено. false — неверный вход/нет подсетей/OOM;
|
||||
* при OOM ранее вставленные подсети остаются. Адреса и /0..128 должен проверить вызывающий. */
|
||||
bool route6_insert(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node);
|
||||
/* Удалить все записи с node->node_id (не по равенству указателей); NULL table/node допустимы. */
|
||||
void route6_delete(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node);
|
||||
/* Найти наиболее длинный подходящий префикс; addr — ненулевой указатель на 16 сетевых байт. NULL — нет маршрута. */
|
||||
const struct ROUTE6_DATA *route6_lookup(struct ROUTE6_TABLE *table, const uint8_t addr[16]);
|
||||
|
||||
|
||||
|
||||
@@ -16,18 +16,23 @@ extern "C" {
|
||||
struct UTUN_INSTANCE;
|
||||
struct TOPO_GROUP;
|
||||
|
||||
// Запускает зондирование связности ко всем адресам удалённого узла
|
||||
/* Запустить асинхронные пробы известных адресов nq из registry; group — его группа.
|
||||
* UDP: до 8 сокетов, серия CONN_PROBE_COUNT с каждого до первого успешного сокета;
|
||||
* TCP: один STCP ping с REALITY-параметрами при наличии. RTT — 0.1 мс, UINT16_MAX неизвестен.
|
||||
* До 16 уникальных IP:port на семейство. Собственный/спящий узел и текущие пробы пропускаются;
|
||||
* NAT/standby могут отложить старт. Результат — в nq->connectivity, завершение уведомляет topo callback. */
|
||||
void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group,
|
||||
struct TOPO_GROUP_NODE* nq);
|
||||
|
||||
// Отменяет все pending пробы для узла (при удалении / withdraw)
|
||||
/* Отменить UDP/TCP-пробы и отложенный старт, освободить контексты; NULL допустим.
|
||||
* Сбрасывает probe_status/pending_count, сохранённые RTT/status остаются. */
|
||||
void route_connectivity_cancel_node(struct TOPO_GROUP_NODE* nq);
|
||||
|
||||
// Отменяет все pending пробы для всех узлов (при destroy)
|
||||
// Отменяет pending пробы всех узлов во всех группах instance (при destroy)
|
||||
void route_connectivity_cancel_all(struct UTUN_INSTANCE* instance);
|
||||
|
||||
// Устанавливает таймаут одного probe-пинга (по умолчанию CONN_PROBE_TIMEOUT_MS).
|
||||
// Используется тестами для ускорения probe-циклов.
|
||||
// Используется тестами для ускорения probe-циклов; настройка общая для процесса, 0 игнорируется.
|
||||
void route_connectivity_set_probe_timeout_ms(uint16_t ms);
|
||||
|
||||
|
||||
|
||||
@@ -1,64 +1,42 @@
|
||||
# route_connectivity — зондирование прямой связности с удалённым узлом
|
||||
# route_connectivity — прямые пробы адресов узла
|
||||
|
||||
## 1. Назначение
|
||||
Проверяет прямую IP-достижимость всех адресов удалённого узла через `etcp_send_ping_to_socket()`.
|
||||
Для каждого адреса перебирает локальные сокеты-кандидаты (до 8), с каждого делает серию из 3 пингов
|
||||
(CONN_PROBE_COUNT=3, CONN_PROBE_TIMEOUT_MS=1000). Результаты (reachable/unreachable, min_rtt)
|
||||
сохраняются в `TOPO_CONNECTIVITY` узла и используются `conn_mgr` для выбора оптимального пути.
|
||||
`route_connectivity_probe_node(instance, group, nq)` берёт общую запись узла из
|
||||
registry и запускает асинхронные пробы. Собственный узел, пир в SLEEP-фазе и уже
|
||||
идущие/отложенные пробы пропускаются. NAT-окно сохраняет незнакомые IP для проверки
|
||||
входящих; Android standby откладывает старт до активного интервала.
|
||||
|
||||
В отличие от `route_ping` (пинг через BGP-посредника), здесь пинги идут напрямую с локальных сокетов
|
||||
на адреса удалённого узла — без промежуточных узлов.
|
||||
Для каждого семейства перебираются до 16 уникальных IP:port. Нулевые адреса/порты
|
||||
пропускаются. Дедупликация выполняется по IP:port, без учёта типа адреса/протокола.
|
||||
|
||||
## 2. Как пользоваться
|
||||
```c
|
||||
// Запустить зондирование узла (вызывается conn_mgr при обнаружении нового узла):
|
||||
route_connectivity_probe_node(instance, nodeq);
|
||||
UDP проверяется с обычных локальных сокетов того же семейства (до 8).
|
||||
IPv4-кандидаты фильтруются через `sock_match` в порядке списка сокетов; предпочтений
|
||||
по /24 или рейтинга DIRECT/EIM здесь нет. Для IPv6 сначала идут сокеты той же /64,
|
||||
затем остальные IPv6. TCP-сокеты и `probe_only` для UDP исключены.
|
||||
С каждого UDP-сокета идёт последовательная серия из `CONN_PROBE_COUNT=3` ping;
|
||||
достаточно одного ответа в серии, чтобы завершить этот адрес успешно. Следующий
|
||||
сокет пробуется, если текущая серия не дала успеха. RTT — минимум успешной серии,
|
||||
а не обязательный минимум по всем доступным сокетам.
|
||||
|
||||
// Отменить зондирование (при удалении узла / withdraw):
|
||||
route_connectivity_cancel_node(instance, nodeq);
|
||||
TCP-адрес проверяется одним `etcp_send_tcp_ping` через STCP-клиент, независимо
|
||||
от набора UDP-сокетов. Используются REALITY-параметры из записи узла; адрес с
|
||||
REALITY-флагом без параметров пропускается. Проба не создаёт READY-сессию группы.
|
||||
Таймаут по умолчанию 1000 мс; `route_connectivity_set_probe_timeout_ms(ms)` меняет
|
||||
его для новых контекстов всего процесса, 0 игнорируется.
|
||||
|
||||
// Отменить все зондирования (при destroy):
|
||||
route_connectivity_cancel_all(instance);
|
||||
```
|
||||
Результаты находятся в `nq->connectivity`: `probe_status`, `pending_count`,
|
||||
`interface/nat/real_status`, соответствующие `*_min_rtt` и `*_probe_time`.
|
||||
RTT и время — timebase (0.1 мс), UINT16_MAX означает неизвестный RTT.
|
||||
INTERFACE обновляет также исторические поля REAL; отдельного третьего адреса
|
||||
для этого не требуется. Успех одного адреса не затирается неудачей другого.
|
||||
Завершение последней pending-пробы устанавливает DONE, обновляет БД и вызывает
|
||||
`topo_fire_nodeinfo_cbk`. Если пригодных адресов/сокетов нет, DONE означает только
|
||||
конец обхода: UNKNOWN не превращается автоматически в UNREACHABLE.
|
||||
|
||||
Результаты доступны через `nodeq->connectivity`:
|
||||
- `probe_status` — `PROBE_STATUS_NONE` / `PROBE_STATUS_IN_PROGRESS` / `PROBE_STATUS_DONE`
|
||||
- `interface_status/nat_status/real_status` — `PROBE_RESULT_UNKNOWN` / `REACHABLE` / `UNREACHABLE`
|
||||
- `interface_min_rtt/nat_min_rtt/real_min_rtt` — минимальный RTT по всем сокетам (0.1ms)
|
||||
- `pending_count` — счётчик незавершённых серий (когда 0 → PROBE_STATUS_DONE)
|
||||
`route_connectivity_cancel_node(nq)` отменяет все UDP/TCP-пробы и отложенный старт,
|
||||
освобождает контексты и сбрасывает probe_status/pending_count. Последние результаты
|
||||
RTT/status сохраняются. Отмена нужна до удаления узла, группы или рабочих сокетов.
|
||||
`route_connectivity_cancel_all(instance)` обходит все группы, не только UTUN.
|
||||
Все вызовы выполняются в потоке ядра; group/nq заимствованы до завершения или отмены.
|
||||
|
||||
Ключевой нюанс: зондирование пропускает собственный узел (`nq->node->node_id == instance->node_id`).
|
||||
Если зондирование уже в процессе, повторный вызов игнорируется.
|
||||
|
||||
## 3. API
|
||||
|
||||
### Внутренние структуры
|
||||
| Структура | Назначение |
|
||||
|-----------|------------|
|
||||
| `conn_probe_ctx` | Контекст одной серии проб: `addr_type` (INTERFACE/NAT/REAL), `target_addr`, `candidate_sockets[8]`, `best_across_sockets` (min RTT по всем опробованным сокетам), `min_rtt` (в текущей серии), счётчики |
|
||||
|
||||
### Подбор сокетов-кандидатов (`conn_match_candidate_sockets`)
|
||||
Для каждого адреса узла подбирает локальные сокеты в порядке предпочтения:
|
||||
1. **INTERFACE-адрес**: PRIVATE/LOCAL сокеты в той же /24 подсети
|
||||
2. **NAT/REAL-адрес**: DIRECT > EIM > PUBLIC/UNKNOWN сокеты
|
||||
3. Fallback: остальные подходящие сокеты, затем все IPv4
|
||||
|
||||
Для IPv6: предпочитает сокеты в той же /64 подсети.
|
||||
|
||||
### Функции
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `route_connectivity_probe_node(instance, nq)` | Перебрать все v4/v6 адреса узла, для каждого подобрать сокеты-кандидаты, запустить серии пингов по 3. Заполняет `nq->connectivity` |
|
||||
| `route_connectivity_cancel_node(instance, nq)` | Сбросить `probe_status=NONE`, `pending_count=0`, занулить `nq` в ctx (чтобы callback не обновлял статистику) |
|
||||
| `route_connectivity_cancel_all(instance)` | Сбросить состояние всех узлов в default-группе |
|
||||
|
||||
### Логика завершения
|
||||
- Серия из 3 пингов с одного сокета: если все 3 потеряны → переход к следующему сокету
|
||||
- Если хоть 1 пинг успешен → серия считается успешной для данного адреса, обновляется `*_status=REACHABLE` и `*_min_rtt`
|
||||
- Когда `pending_count` доходит до 0 → `probe_status = PROBE_STATUS_DONE`
|
||||
|
||||
### Зависимости
|
||||
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг)
|
||||
- `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() для отметок времени
|
||||
`route_ping` также выполняет прямые UDP-пробы, но для измерения наблюдаемого endpoint;
|
||||
выбор наблюдателей/помощников и вычисление NAT-профиля находятся в `nat_detection`.
|
||||
@@ -9,6 +9,9 @@
|
||||
// Порядок кодирования: encrypt-then-sign (подпись заголовка и payload, включая шифртекст).
|
||||
// Список пройденных узлов добавляет/снимает etcp_router снаружи этого формата.
|
||||
// При ошибке декодирования (плохая подпись / не удалось расшифровать) пакет дропается.
|
||||
// Все вызовы — в потоке ядра; inst и его registry/ключи должны быть готовы.
|
||||
// Вход заимствован и не изменяется; при успехе *out принадлежит вызывающему (u_free).
|
||||
// При ошибке out/out_len не устанавливаются. Буфер выделяется и при mode=0/отсутствии crypto-флагов.
|
||||
#ifndef ROUTE_CRYPTO_H
|
||||
#define ROUTE_CRYPTO_H
|
||||
|
||||
@@ -28,6 +31,7 @@ struct UTUN_INSTANCE;
|
||||
|
||||
// Кодировать пакет: base = [SVC_ROUTE_HDR][plaintext payload].
|
||||
// mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT.
|
||||
// peer_node_id — получатель ECDH; hdr.src_node_id должен быть локальным, исходные crypto-флаги очищены.
|
||||
// Возвращает новый буфер (u_malloc) с финальным пакетом. 0 = успех, <0 = ошибка.
|
||||
int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
|
||||
const uint8_t* base, size_t base_len, int mode,
|
||||
@@ -36,6 +40,7 @@ int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
|
||||
// Декодировать пакет: wire = [SVC_ROUTE_HDR][payload][sig?].
|
||||
// Проверяет подпись и/или расшифровывает. Через *rx_flags сообщает, что было на wire
|
||||
// (ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT). Возвращает [SVC_ROUTE_HDR][plaintext] с очищенными флагами.
|
||||
// Ключ отправителя берётся по hdr.src_node_id из registry. rx_flags обязателен; может быть заполнен и при ошибке.
|
||||
// 0 = успех, <0 = ошибка (пакет дропается).
|
||||
int route_crypto_decode(struct UTUN_INSTANCE* inst,
|
||||
const uint8_t* wire, size_t wire_len,
|
||||
|
||||
@@ -21,22 +21,12 @@ static uint32_t prefix_to_mask(uint8_t prefix) {
|
||||
}
|
||||
|
||||
static bool routes_overlap(uint32_t net1, uint8_t pre1, uint32_t net2, uint8_t pre2) {
|
||||
if (pre1 == pre2 && net1 == net2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool check_route_overlap_in_table(uint32_t network, uint8_t prefix,
|
||||
const struct ROUTE_ENTRY *entries, size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (routes_overlap(network, prefix, entries[i].network, entries[i].prefix_length)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
uint32_t mask = prefix_to_mask(pre1 < pre2 ? pre1 : pre2);
|
||||
return (net1 & mask) == (net2 & mask);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Поиск (LPM)
|
||||
// Поиск по непересекающимся диапазонам
|
||||
// ============================================================================
|
||||
|
||||
static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count,
|
||||
@@ -56,51 +46,19 @@ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count,
|
||||
return left;
|
||||
}
|
||||
|
||||
static struct ROUTE_ENTRY* binary_search_lpm(struct ROUTE_TABLE *table, uint32_t dest_ip) {
|
||||
if (table->count == 0) return NULL;
|
||||
|
||||
/* В отсортированной таблице без пересечений подходящий диапазон может быть только один. */
|
||||
static struct ROUTE_ENTRY* binary_search_route(struct ROUTE_TABLE *table, uint32_t dest_ip) {
|
||||
int left = 0;
|
||||
int right = (int)table->count - 1;
|
||||
struct ROUTE_ENTRY *best = NULL;
|
||||
uint8_t best_prefix = 0;
|
||||
|
||||
while (left <= right) {
|
||||
int mid = left + (right - left) / 2;
|
||||
struct ROUTE_ENTRY *entry = &table->entries[mid];
|
||||
uint32_t mask = prefix_to_mask(entry->prefix_length);
|
||||
|
||||
if ((dest_ip & mask) == (entry->network & mask)) {
|
||||
if (entry->prefix_length > best_prefix) {
|
||||
best_prefix = entry->prefix_length;
|
||||
best = entry;
|
||||
}
|
||||
if (mid + 1 < (int)table->count) {
|
||||
struct ROUTE_ENTRY *next = &table->entries[mid + 1];
|
||||
uint32_t next_mask = prefix_to_mask(next->prefix_length);
|
||||
if ((dest_ip & next_mask) == (next->network & next_mask)) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (dest_ip > entry->network) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
if ((dest_ip & mask) == entry->network) return entry;
|
||||
if (dest_ip > entry->network) left = mid + 1;
|
||||
else right = mid - 1;
|
||||
}
|
||||
|
||||
// fallback проверка предыдущей записи
|
||||
if (!best && left > 0) {
|
||||
struct ROUTE_ENTRY *entry = &table->entries[left - 1];
|
||||
uint32_t mask = prefix_to_mask(entry->prefix_length);
|
||||
if ((dest_ip & mask) == (entry->network & mask)) {
|
||||
best = entry;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -160,8 +118,25 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) {
|
||||
|
||||
for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) {
|
||||
uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network);
|
||||
if (check_route_overlap_in_table(network, s->prefix_length, table->entries, table->count)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "overlap detected for %s/%d", ip_to_string(network).a, s->prefix_length);
|
||||
if (s->prefix_length > 32) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: invalid IPv4 prefix",
|
||||
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length);
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < table->count; i++) {
|
||||
const struct ROUTE_ENTRY *e = &table->entries[i];
|
||||
if (!routes_overlap(network, s->prefix_length, e->network, e->prefix_length)) continue;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps node=%016llx subnet=%s/%u",
|
||||
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length,
|
||||
(unsigned long long)e->v_node_info->node_id, ip_to_string(e->network).a, e->prefix_length);
|
||||
return false;
|
||||
}
|
||||
for (const struct TOPO_SUBNET4 *previous = sub; previous != s; previous = previous->next) {
|
||||
uint32_t other; memcpy(&other, previous->addr, 4); other = ntohl(other);
|
||||
if (!routes_overlap(network, s->prefix_length, other, previous->prefix_length)) continue;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps own subnet=%s/%u",
|
||||
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length,
|
||||
ip_to_string(other).a, previous->prefix_length);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +151,7 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) {
|
||||
|
||||
for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) {
|
||||
uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network);
|
||||
network &= prefix_to_mask(s->prefix_length);
|
||||
int pos = binary_search_insert_pos(table->entries, table->count, network, s->prefix_length);
|
||||
if (pos < (int)table->count) memmove(&table->entries[pos + 1], &table->entries[pos], (table->count - pos) * sizeof(struct ROUTE_ENTRY));
|
||||
struct ROUTE_ENTRY* e = &table->entries[pos];
|
||||
@@ -210,7 +186,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip) {
|
||||
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_ROUTING, "dest_ip=%s", ip_to_string(dest_ip).a);
|
||||
|
||||
struct ROUTE_ENTRY *result = binary_search_lpm(table, dest_ip);
|
||||
struct ROUTE_ENTRY *result = binary_search_route(table, dest_ip);
|
||||
|
||||
if (result) {
|
||||
table->stats.routes_lookup_hits++;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* route_lib — локальная IPv4-таблица подсетей UTUN, отсортированный массив без пересечения маршрутов.
|
||||
* create → insert/add_local_subnet → lookup → delete/destroy; все операции в одном uasync-потоке.
|
||||
* create → insert → lookup → delete/destroy; все операции в одном uasync-потоке.
|
||||
* Пересечения и дубликаты запрещены, в том числе внутри одного набора подсетей; /0 занимает всю таблицу.
|
||||
* Вставка нормализует адреса по маске; при отказе таблица и её статистика не меняются.
|
||||
* network/dest_ip/parse_subnet — host order; только is_local_subnet принимает network order.
|
||||
* TOPO_GROUP_NODE заимствован: удалить его маршруты до освобождения узла.
|
||||
* lookup возвращает элемент массива, действительный до следующего изменения таблицы. */
|
||||
@@ -39,24 +41,24 @@ typedef enum {
|
||||
struct ROUTE_ENTRY {
|
||||
uint32_t network; // Сетевой адрес в host order
|
||||
uint8_t prefix_length; // Длина префикса подсети
|
||||
struct TOPO_GROUP_NODE* v_node_info; // узел владелец этих маршрутов. null если - локальный маршрут.
|
||||
struct TOPO_GROUP_NODE* v_node_info; // заимствованный владелец; локальный маршрут тоже содержит узел
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Таблица маршрутизации
|
||||
*
|
||||
* Структура представляет собой таблицу маршрутизации, содержащую записи маршрутов, подсети и статистику.
|
||||
* маршруты не должны пересекаться. т.е. адрес 192.168.1.1/30 нельзя добавить если есть 192.168.1.2/32 или 192.168.1.1/24
|
||||
* Подсети не пересекаются: например, 192.168.1.0/24 исключает 192.168.1.0/30 и 192.168.1.2/32.
|
||||
*/
|
||||
struct ROUTE_TABLE {
|
||||
struct ROUTE_ENTRY *entries; /**< Массив записей маршрутов */
|
||||
size_t count; /**< Текущее количество записей */
|
||||
size_t capacity; /**< Максимальная емкость таблицы */
|
||||
uint64_t my_node_id; /**< ID локального узла */
|
||||
uint32_t *dynamic_subnets; /**< Динамические подсети (массив пар: сеть, префикс) */
|
||||
size_t dynamic_subnet_count; /**< Количество динамических подсетей */
|
||||
uint32_t *local_subnets; /**< Локальные подсети (массив пар: сеть, префикс) */
|
||||
size_t local_subnet_count; /**< Количество локальных подсетей */
|
||||
uint32_t *dynamic_subnets; /**< Не используется текущей реализацией; destroy освобождает память */
|
||||
size_t dynamic_subnet_count; /**< Не используется текущей реализацией */
|
||||
uint32_t *local_subnets; /**< Не используется текущей реализацией; destroy освобождает память */
|
||||
size_t local_subnet_count; /**< Не используется текущей реализацией */
|
||||
struct {
|
||||
uint64_t local_routes; /**< Количество локальных маршрутов */
|
||||
uint64_t learned_routes; /**< Количество изученных маршрутов */
|
||||
@@ -80,10 +82,12 @@ struct ROUTE_TABLE *route_table_create(uint64_t my_node_id);
|
||||
void route_table_destroy(struct ROUTE_TABLE *table);
|
||||
|
||||
/**
|
||||
* @brief Вставляет в таблицу маршрутизации все подсети для указанного узла
|
||||
* @brief Атомарно вставляет все IPv4-подсети узла; /0..32, адреса нормализуются по маске
|
||||
* @param table Таблица без пересечений; перед заменой маршрутов узла вызвать route_delete
|
||||
* @param node узел, все маршруты которого нужно добавить
|
||||
*
|
||||
* @return true если подсети добавлены; false при отсутствии подсетей, пересечении или ошибке выделения памяти
|
||||
* @return true если весь набор добавлен; false при неверных аргументах/префиксе, отсутствии подсетей,
|
||||
* пересечении с таблицей/внутри набора или нехватке памяти. При false таблица не меняется.
|
||||
*/
|
||||
bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node);
|
||||
|
||||
@@ -112,7 +116,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip);
|
||||
void route_table_print(const struct ROUTE_TABLE *table);
|
||||
|
||||
/**
|
||||
* @brief Парсит строку подсети в сетевой адрес и длину префикса
|
||||
* @brief Разбирает IPv4/префикс; host-биты адреса сохраняются, префикс читается через atoi
|
||||
*
|
||||
* @param subnet_str Строка подсети (например, "192.168.1.0/24")
|
||||
* @param network Указатель для сохранения адреса в host order
|
||||
@@ -121,8 +125,6 @@ void route_table_print(const struct ROUTE_TABLE *table);
|
||||
*/
|
||||
int parse_subnet(const char *subnet_str, uint32_t *network, uint8_t *prefix_length);
|
||||
|
||||
bool route_add_local_subnet(struct ROUTE_TABLE *table, uint32_t network, uint8_t prefix_length);
|
||||
|
||||
/**
|
||||
* @brief Проверяет что IP адрес НЕ является публичным (роутится в интернете)
|
||||
*
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
В таблице роутинга маршруты не пересекаются. Т.е. не может быть одновременно 192.168.1.1/24 и 192.168.1.100/30
|
||||
|
||||
Как работает роутинг:
|
||||
1. типов маршрута бывает два: learned (роутятся в ETCP instance) и local (роутятся в TUN).
|
||||
local - локальные маршруты из конфига (опция конфига my_subnet=IP/Mask)
|
||||
при инициализации local сразу добавляются в роутинг таблицу (ипользуя запись BGP_NODEINFO_PACKET* my_nodeinfo).
|
||||
|
||||
BGP_NODEINFO собирается на узле владельце node, далее распространяется по остальным узлам.
|
||||
|
||||
2. При установке подключения к новому узлу мы отправляем этому узлу все nodeinfo. Если несколько доступных линков - отправляем только один - preferred_conn
|
||||
3. При изменении preferred_conn рассылаем reroute (например старый preferred_conn удален)
|
||||
3. При удалении всех подключений рассылаем withdraw.
|
||||
|
||||
3. При получении BGP_NODEINFO_PACKET мы смотрим есть ли такой узел уже в таблице. Если узла нет - добавляем, если есть - обновляем информацию о узле включая его маршруты и список next_hop через которые доступен узел (nb_routes).
|
||||
Добавляем так:
|
||||
инкрементируем hop_count
|
||||
устанавливая etcp линк с которого приняли как next_hop, добавляем его в hop_list.
|
||||
рассылаем по всем активным линкам кроме линка с которого получили (обязательно)
|
||||
Обновление: обновляем hop_list если поменялся
|
||||
|
||||
4. При отключении от узла (если у node не осталось nb_routes) мы:
|
||||
Удаляем node и вложенные структуры: все маршруты узла
|
||||
Рассылаем withdraw для узла hop_id.
|
||||
|
||||
Логика рассылки withdraw:
|
||||
Если получен withdraw - удаляем этот маршрут и распространяем withdraw или reroute по всем линкам кроме того с которого получили. В зависимости от изменений (остались ли резервные линки или изменился preferred_conn).
|
||||
|
||||
Метрик маршрута пока нет. используется первый доступный.
|
||||
|
||||
Как работаеут маршрутизация:
|
||||
destination IP -> route table lookup -> route entry (ip/mask, node_info*) -> выбор лучшего next_hop -> отправка в next_hop
|
||||
route_lib — локальная таблица IPv4-подсетей UTUN
|
||||
================================================
|
||||
|
||||
Подсети в таблице не пересекаются. Запрещены одинаковые и вложенные диапазоны,
|
||||
как между узлами, так и внутри одного переданного набора. Например,
|
||||
10.0.0.0/8 исключает 10.1.0.0/24; соседние 10.1.0.0/24 и 10.1.1.0/24 допустимы.
|
||||
Маршрут /0 занимает всё адресное пространство и может быть только единственным.
|
||||
|
||||
route_insert(table, node) добавляет все node->subnets->v4_subnets атомарно.
|
||||
Префиксы должны быть в диапазоне 0..32; адреса нормализуются маской перед хранением.
|
||||
При неверных аргументах, отсутствии подсетей, пересечении или OOM возвращает false,
|
||||
не меняя таблицу и её статистику. Перед заменой маршрутов узла вызывают route_delete.
|
||||
|
||||
Таблица — отсортированный массив. route_lookup получает IPv4 в host byte order
|
||||
и бинарным поиском возвращает единственный подходящий диапазон либо NULL.
|
||||
Это не LPM-таблица: вложенные префиксы исключены при вставке.
|
||||
Указатель на ROUTE_ENTRY действителен только до следующего изменения таблицы.
|
||||
Адреса TOPO_SUBNET4 хранятся в сетевых байтах, parse_subnet возвращает host order,
|
||||
is_local_subnet принимает network order. parse_subnet сохраняет host-биты и читает
|
||||
префикс через atoi; это не строгий валидатор всей строки CIDR.
|
||||
|
||||
TOPO_GROUP_NODE заимствован, в том числе для локальных маршрутов.
|
||||
Маршруты удаляют до освобождения узла. destroy освобождает массив, но не узлы.
|
||||
Все вызовы выполняются в одном потоке ядра.
|
||||
|
||||
Наполнение таблицы и поиск следующего узла — разные задачи:
|
||||
IPv4 destination -> route_lookup -> TOPO_GROUP_NODE -> etcp_route_send
|
||||
-> выбор живого next hop через CM/BGP -> ETCP.
|
||||
Для локального владельца routing доставляет пакет в TUN после firewall.
|
||||
Групповые пути, WITHDRAW, выбор по числу переходов и READY описаны в topo_group.h;
|
||||
общая подписанная запись узла — в topo_node.h и doc/node_snapshot.md.
|
||||
|
||||
IPv6-таблица route6_lib использует radix и разрешает вложенные префиксы.
|
||||
Основной routing.c пока доставляет только IPv4.
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Прямые UDP-наблюдения с указанного рабочего сокета. Управление помощниками —
|
||||
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. */
|
||||
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет.
|
||||
* Вызовы с ETCP_SOCKET выполняются в потоке его ядра; буферы адресов/тегов предоставляет вызывающий. */
|
||||
#ifndef ROUTE_PING_H
|
||||
#define ROUTE_PING_H
|
||||
|
||||
@@ -14,16 +15,25 @@ extern "C" {
|
||||
#define ROUTE_ADDR_WIRE_SIZE 19
|
||||
#define ROUTE_OBSERVE_TAG_SIZE 33
|
||||
|
||||
/* family(1:4/6), IP(16: IPv4 + 12 нулей либо IPv6), port(2 BE), без IPv6 scope_id.
|
||||
* encode/decode: 0/-1; decode также отвергает нулевой порт и ненулевой IPv4-padding.
|
||||
* При ошибке выход может быть частично заполнен. encode сам по себе нулевой порт не запрещает. */
|
||||
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address);
|
||||
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address);
|
||||
/* 1 — совпадают семейство/IP и (при include_port!=0) порт; 0 — иначе. IPv6 scope_id не сравнивается. */
|
||||
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port);
|
||||
|
||||
/* Ответ: address(19) || tag(33); tag связывает socket/generation/round/group. Числа BE, IP — сетевые байты. */
|
||||
/* Отправить OBSERVE PING: group/round и generation сокета ненулевые, timeout_ms — мс.
|
||||
* peer_key — 32 байта ключа наблюдателя, peer_address — его UDP endpoint.
|
||||
* Возвращает 0 при запуске, отрицательный код ошибки ETCP при отказе; callback/arg — контракт ETCP.
|
||||
* Ответ: address(19) || tag(33); профиль сокета здесь не изменяется. */
|
||||
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE],
|
||||
const struct sockaddr_storage *peer_address, uint64_t group, uint64_t round, int timeout_ms,
|
||||
etcp_ping_callback_t callback, void *arg);
|
||||
/* Проверить точные размер/тег и декодировать observed; 0/-1. Подлинность ответа проверяет ETCP. */
|
||||
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket,
|
||||
uint64_t group, uint64_t round, struct sockaddr_storage *observed);
|
||||
/* NAT4UDP!(8) || socket_id(1) || generation(8 BE) || round(8 BE) || group(8 BE); out/socket обязательны. */
|
||||
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t group, uint64_t round);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
`ETCP_SOCKET`. Выбор B/C, пересылка управляющих сообщений и классификация
|
||||
профиля принадлежат `nat_detection`; алгоритм — в `doc/nat_detection.txt`.
|
||||
|
||||
`route_ping_observe(socket, peer_key, peer_address, round, timeout_ms, callback, arg)`
|
||||
`route_ping_observe(socket, peer_key, peer_address, group, round, timeout_ms, callback, arg)`
|
||||
передаёт PING с `ETCP_PING_FLAG_OBSERVE`. PONG содержит фактически наблюдаемый
|
||||
source IP:port и исходный тег. Профиль не меняется от единственного ответа.
|
||||
`route_ping_observation` проверяет размер, ID/поколение сокета и round.
|
||||
`route_ping_observation` проверяет размер, ID/поколение сокета, round и group.
|
||||
|
||||
Адрес занимает 19 байт: family(1: 4/6), IP(16: IPv4 + 12 нулей либо IPv6),
|
||||
port(2, BE). Тег занимает 25 байт: `NAT2UDP!`(8), socket_id(1),
|
||||
generation(8, BE), round(8, BE). Неправильные family, скрытые IPv4-байты
|
||||
и нулевой порт отвергаются. Новый wire не использует native packed integers.
|
||||
port(2, BE). Тег занимает 33 байта: `NAT4UDP!`(8), socket_id(1),
|
||||
generation(8, BE), round(8, BE), group(8, BE). Декодирование отвергает неправильные
|
||||
family, скрытые IPv4-байты и нулевой порт. IPv6 scope_id в wire не передаётся.
|
||||
Кодирование нулевой порт не запрещает. Числа тега передаются в BE.
|
||||
|
||||
В транспорте nonce генерируется CSPRNG; PONG должен соответствовать ожидаемому
|
||||
ключу, source IP:port и принимающему socket generation. Pending снимается
|
||||
@@ -20,8 +21,8 @@ generation(8, BE), round(8, BE). Неправильные family, скрытые
|
||||
совпадающие запросы без callback; arg остаётся собственностью вызывающего.
|
||||
|
||||
`ETCP_PING_FLAG_ADDRESS_CHECK` обозначает прямое входящее испытание. Его
|
||||
receipt фиксируется на рабочем сокете владельца до отправки PONG. Потеря
|
||||
обратного PONG поэтому не превращает полученный ingress в отрицательный результат.
|
||||
приём передаётся `nat_detection_receive_ingress` без UDP-ответа: ответ мог бы
|
||||
испортить историю контактов. Результат передаётся по защищённому управляющему каналу.
|
||||
UDP-результаты не используются для TCP listener и SOCKS5 endpoint.
|
||||
|
||||
Проверки: `tests/test_route_ping.c`, `tests/test_nat_detection.c`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Учёт первой доставки payload логического канала. Один uasync-поток.
|
||||
* Десять завершённых интервалов по 50мс и текущий интервал; reset начинает новую историю.
|
||||
* Повторы учитывает вызывающий: add вызывается только при снятии подтверждённого inflight. */
|
||||
* Повторы учитывает вызывающий: ack вызывается только при снятии подтверждённого inflight.
|
||||
* Время — монотонные микросекунды; память всех структур принадлежит вызывающему. */
|
||||
#ifndef ROUTE_TX_STATS_H
|
||||
#define ROUTE_TX_STATS_H
|
||||
|
||||
@@ -14,16 +15,21 @@ extern "C" {
|
||||
#define ROUTE_TX_WINDOW_US 500000u
|
||||
#define ROUTE_TX_BUCKETS 11
|
||||
|
||||
/* tick_plus_one=0 означает пустую ячейку; иначе это номер 50-мс интервала + 1. */
|
||||
struct route_tx_bucket { uint64_t tick_plus_one, bytes, packets; };
|
||||
struct route_tx_meter {
|
||||
struct route_tx_bucket buckets[ROUTE_TX_BUCKETS];
|
||||
uint64_t started_us, last_us, bytes, packets;
|
||||
uint64_t started_us, last_us, bytes, packets; /* reset/последний ACK и накопленные итоги эпохи */
|
||||
};
|
||||
/* end_us — правая граница окна; valid=1 после накопления полных 500 мс с момента reset. */
|
||||
struct route_tx_window { uint64_t end_us, bytes, packets; int valid; };
|
||||
|
||||
/* Обнулить историю и начать эпоху в now_us; meter обязателен. */
|
||||
void route_tx_meter_reset(struct route_tx_meter *meter, uint64_t now_us);
|
||||
/* Учесть один впервые подтверждённый payload (bytes>0); 0/-1, при ошибке история не меняется. */
|
||||
int route_tx_meter_ack(struct route_tx_meter *meter, uint64_t now_us, uint64_t bytes);
|
||||
/* Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */
|
||||
/* Заполнить out суммой завершённых ячеек; 0/-1 (при ошибке out не меняется).
|
||||
* Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */
|
||||
int route_tx_meter_window(const struct route_tx_meter *meter, uint64_t now_us, struct route_tx_window *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -33,7 +33,7 @@ int routing_create(struct UTUN_INSTANCE* instance);
|
||||
int routing_bind(struct UTUN_INSTANCE* instance);
|
||||
|
||||
/**
|
||||
* @brief Destroy routing module for instance
|
||||
* @brief Снять DATA binding и освободить IPv4-таблицу; callback TUN снимается при остановке самого TUN
|
||||
* @param instance UTUN instance
|
||||
*/
|
||||
void routing_destroy(struct UTUN_INSTANCE* instance);
|
||||
@@ -58,7 +58,9 @@ void routing_del_conn(struct ETCP_CONN* etcp);
|
||||
void routing_set_tun(struct UTUN_INSTANCE* instance);
|
||||
|
||||
/**
|
||||
* @brief Route a single packet (exposed for NAT module interception)
|
||||
* @brief Маршрутизировать [cmd:1][IPv4] в TUN либо через TOPO_GROUP_UTUN; требует готовых instance->rt/tun
|
||||
* @param entry Передаваемые entry и dgram: модуль забирает владение при любом исходе
|
||||
* @param src_node_id Исходный узел для диагностики, для выхода из TUN — instance->node_id
|
||||
*/
|
||||
void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id);
|
||||
|
||||
|
||||
@@ -20,8 +20,13 @@ IPv6-пакеты на данный момент дропаются на эта
|
||||
**Ключевые нюансы:**
|
||||
- `routing_add_conn` / `routing_del_conn` — **deprecated** (no-op), маршруты теперь управляются через BGP и таблицу `route_lib`
|
||||
- Все не-IPv4 пакеты (IPv6, unknown) дропаются в `extract_dst_ip`
|
||||
- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) дропаются без логирования
|
||||
- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) отбрасываются с DEBUG-логом routing
|
||||
- Пакеты без маршрута дропаются с инкрементом `instance->dropped_packets`
|
||||
- `route_pkt` забирает entry и dgram при любом исходе; ожидает `[cmd:1][IPv4]` и готовые `rt`/`tun`.
|
||||
- Удалённая доставка идёт через `TOPO_GROUP_UTUN`; локальная — в `tun->input_queue`.
|
||||
Firewall проверяется перед маршрутизацией выхода из TUN и перед локальной доставкой в TUN.
|
||||
- Все вызовы выполняются в потоке ядра. `routing_destroy` снимает DATA binding и освобождает таблицу;
|
||||
callback очереди TUN снимается при остановке самого TUN/UTUN-сервиса.
|
||||
|
||||
## 3. API
|
||||
|
||||
|
||||
@@ -21,17 +21,22 @@ struct ETCP_SOCKET;
|
||||
#define SOCK_TC_PROBE 4
|
||||
|
||||
struct sock_view {
|
||||
struct ETCP_SOCKET *sock;
|
||||
uint8_t role, has_inet, has_priv, family, is_tcp;
|
||||
uint32_t local_ip4, public_ip4;
|
||||
struct address_profile address_profile;
|
||||
struct ETCP_SOCKET *sock; /* заимствован; не переживает удаление сокета */
|
||||
uint8_t role, has_inet, has_priv, family, is_tcp; /* SOCK_ROLE_*, доступ к Internet/LAN, AF_INET, TCP-флаг */
|
||||
uint32_t local_ip4, public_ip4; /* network order; public_ip4=0, если публичный адрес неизвестен */
|
||||
struct address_profile address_profile; /* копия текущего профиля */
|
||||
};
|
||||
|
||||
/* public_address — классификация IP в соответствующем семействе, не доказательство входа. */
|
||||
/* Возвращает SOCK_TC_*; address_type=TOPO_ADDR_*, config_type=CFG_SERVER_TYPE_*.
|
||||
* public_address — классификация IP в соответствующем семействе, не доказательство входа. */
|
||||
uint8_t sock_target_class(uint8_t address_type, uint8_t config_type, const struct address_profile *profile,
|
||||
int public_address);
|
||||
/* 1 — разрешена попытка по target_ip4 (network order), 0 — нет; проверяет has_priv/has_inet.
|
||||
* Семейство/протокол и выбор маршрута проверяет вызывающий; profile может быть NULL. */
|
||||
int sock_match(const struct sock_view *view, uint8_t address_type, uint8_t config_type,
|
||||
const struct address_profile *profile, uint32_t target_ip4);
|
||||
/* В потоке ядра заполнить до max views обычных IPv4-сокетов, включая TCP; probe_only исключены.
|
||||
* Возвращает число заполненных (0 также при неверных аргументах); специальной сортировки нет. */
|
||||
int sock_collect_views(struct UTUN_INSTANCE *instance, struct sock_view *out, int max);
|
||||
|
||||
#endif
|
||||
+110
-88
@@ -52,10 +52,11 @@ static int topo_tx_control(struct TOPO_GROUP_CONN_ITEM* peer, const void* data,
|
||||
static int topo_tx_node(struct TOPO_GROUP_CONN_ITEM* peer, uint64_t node_id);
|
||||
static int topo_tx_table(struct TOPO_GROUP_CONN_ITEM* peer);
|
||||
static int topo_control_wait(struct ETCP_CONN* conn, const void* data, size_t size);
|
||||
static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, int event);
|
||||
static int topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, int event);
|
||||
static void topo_group_prune_paths(struct TOPO_GROUP_CONN_ITEM* peer, int all);
|
||||
static void topo_exchange_watch(void* arg);
|
||||
static void topo_tx_fail(struct TOPO_GROUP_CONN_ITEM* peer);
|
||||
static void topo_group_destroy(struct TOPO_GROUP* group);
|
||||
|
||||
static void topo_exchange_cancel(struct TOPO_GROUP_CONN_ITEM* peer) {
|
||||
if (peer->exchange_timer) uasync_cancel_timeout(peer->group->instance->ua, peer->exchange_timer);
|
||||
@@ -156,7 +157,8 @@ static void topo_group_peer_event(struct NODE_CONN_DIRECT* handle, enum ncd_even
|
||||
|
||||
int topo_group_peer_open(struct TOPO_GROUP* group, uint64_t node_id, struct TOPO_PEER_REQUEST** out) {
|
||||
if (out) *out = NULL;
|
||||
if (!group || !out || !node_id || !group->senders_list || group->stopping || !topo_group_peer_allowed(group, node_id)) {
|
||||
if (!group || !group->instance || !out || !node_id || node_id == group->instance->node_id ||
|
||||
!group->senders_list || group->stopping || !topo_group_peer_allowed(group, node_id)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "cannot request group peer=%016llx: invalid arguments, stopped group or unknown member",
|
||||
(unsigned long long)node_id); return -1;
|
||||
}
|
||||
@@ -351,38 +353,6 @@ static void nodeinfo_dump_log(const uint8_t* data, size_t len) {
|
||||
ni->local_v4_sockets, ni->local_v4_addrs, ni->hop_count);
|
||||
}
|
||||
|
||||
/* Форматирует NODEINFO в читаемую строку для лога (имя, подсети, адреса). */
|
||||
static char* nodeinfo_format(const uint8_t* data, size_t len) {
|
||||
if (!data || len < sizeof(struct TOPOMSG_NODEINFO_PKT)) return NULL;
|
||||
struct TOPOMSG_NODEINFO_PKT* pkt = (struct TOPOMSG_NODEINFO_PKT*)data;
|
||||
struct TOPOMSG_NODE* ni = &pkt->node;
|
||||
uint64_t node_id = ni->node_id;
|
||||
const uint8_t* dyn = data + sizeof(struct TOPOMSG_NODEINFO_PKT);
|
||||
size_t off = 0;
|
||||
char name_buf[64] = {0};
|
||||
size_t nl = ni->node_name_len;
|
||||
if (nl > 0 && nl < 63 && off + nl < len - sizeof(struct TOPOMSG_NODEINFO_PKT)) {
|
||||
memcpy(name_buf, dyn + off, nl); off += nl;
|
||||
}
|
||||
off += ni->local_v4_sockets * sizeof(struct TOPOMSG_SOCKMETA4);
|
||||
off += ni->local_v4_addrs * sizeof(struct TOPOMSG_ADDR4);
|
||||
off += ni->local_v6_sockets * sizeof(struct TOPOMSG_SOCKMETA6);
|
||||
off += ni->local_v6_addrs * sizeof(struct TOPOMSG_ADDR6);
|
||||
char subs_buf[512] = {0};
|
||||
if (ni->local_v4_subnets > 0) {
|
||||
const struct TOPOMSG_SUBNET4* subs = (const struct TOPOMSG_SUBNET4*)(dyn + off);
|
||||
size_t sl = 0;
|
||||
for (uint8_t i = 0; i < ni->local_v4_subnets && sl < 400; i++) {
|
||||
uint32_t addr = (subs[i].addr[0]<<24)|(subs[i].addr[1]<<16)|(subs[i].addr[2]<<8)|subs[i].addr[3];
|
||||
char tmp[64]; snprintf(tmp,sizeof(tmp),"%d.%d.%d.%d/%u ",(addr>>24)&255,(addr>>16)&255,(addr>>8)&255,addr&255,subs[i].prefix_length);
|
||||
strcat(subs_buf,tmp); sl += strlen(tmp);
|
||||
}
|
||||
}
|
||||
int need = snprintf(NULL,0,"NODEINFO nid=%016llx ver=%u name=\"%s\" v4subs=\"%s\" v4s=%u v4a=%u hop=%u", (unsigned long long)node_id,(unsigned)ni->ver,name_buf,subs_buf,(unsigned)ni->local_v4_sockets,(unsigned)ni->local_v4_addrs,(unsigned)ni->hop_count);
|
||||
char* buf = u_malloc(need+1); if (!buf) return NULL;
|
||||
snprintf(buf,need+1,"NODEINFO nid=%016llx ver=%u name=\"%s\" v4subs=\"%s\" v4s=%u v4a=%u hop=%u", (unsigned long long)node_id,(unsigned)ni->ver,name_buf,subs_buf,(unsigned)ni->local_v4_sockets,(unsigned)ni->local_v4_addrs,(unsigned)ni->hop_count);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Читаемое имя sub-команды BGP для логов. */
|
||||
static const char* group_subcmd_name(uint8_t subcmd) {
|
||||
@@ -436,7 +406,7 @@ static int topo_group_has_sender(struct TOPO_GROUP* group, struct ETCP_CONN* con
|
||||
static int topo_group_peer_allowed(struct TOPO_GROUP* group, uint64_t node_id) {
|
||||
if (group->group_type != TOPO_GROUP_TYPE_CHAT) return 1;
|
||||
sqlite3* db = group->instance->topo_sqlite_db;
|
||||
return db && group->channel_id[0] && topo_node_sqlite_member_in_channel(db, group->channel_id, node_id);
|
||||
return db && group->channel_id[0] && topo_node_sqlite_member_allowed(db, group->channel_id, node_id);
|
||||
}
|
||||
|
||||
/* Приёмник BGP-пакетов от ETCP: диспетчер по subcmd (NODEINFO/WITHDRAW/...). */
|
||||
@@ -602,7 +572,10 @@ static struct TOPO_GROUP* topo_group_create(struct UTUN_INSTANCE* instance, uint
|
||||
group->group_type = group_type;
|
||||
group->instance = instance;
|
||||
|
||||
sc_derive_ed25519_pubkey(instance->my_keys.private_key, group->ed25519_public_key);
|
||||
if (sc_derive_ed25519_pubkey(instance->my_keys.private_key, group->ed25519_public_key) != SC_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot derive group identity group=%016llx", (unsigned long long)group_id);
|
||||
queue_entry_free(qe); return NULL;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Ed25519 pubkey derived from X25519 private key");
|
||||
|
||||
group->nodes = queue_new(instance->ua, BGP_NODES_HASH_SIZE, offsetof(struct TOPO_GROUP_NODE, node_id) - sizeof(struct ll_entry), 8, "group_nodes");
|
||||
@@ -612,15 +585,20 @@ static struct TOPO_GROUP* topo_group_create(struct UTUN_INSTANCE* instance, uint
|
||||
if (!group->senders_list) { queue_free(group->nodes); queue_entry_free(qe); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "senders_list queue creation failed"); return NULL; }
|
||||
|
||||
group->local_node = NULL;
|
||||
topo_group_update_my_nodeinfo(instance, group);
|
||||
if (topo_group_update_my_nodeinfo(instance, group) < 0) goto fail;
|
||||
|
||||
group->conn_mgr = conn_mgr_init(group);
|
||||
if (!group->conn_mgr) goto fail;
|
||||
|
||||
broadcast_init(group);
|
||||
radio_init(group);
|
||||
if (broadcast_init(group) < 0 || radio_init(group) < 0) goto fail;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TOPO group created: group_id=%016llx", (unsigned long long)group_id);
|
||||
return group;
|
||||
fail:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group creation rolled back group=%016llx local_node=%p conn_mgr=%p broadcast=%p radio=%p",
|
||||
(unsigned long long)group_id, (void*)group->local_node, (void*)group->conn_mgr,
|
||||
(void*)group->broadcast, (void*)group->radio);
|
||||
topo_group_destroy(group); queue_entry_free(qe); return NULL;
|
||||
}
|
||||
|
||||
/* Полностью разрушает группу: recovery, connect, broadcast, conn_mgr, узлы. */
|
||||
@@ -713,9 +691,11 @@ void topo_groups_remove_group(struct TOPO_GROUPS* g, uint64_t group_id) {
|
||||
queue_entry_free(&group->ll);
|
||||
}
|
||||
|
||||
/* Инициализация контейнера групп: реестр, пулы, SQLite, default-группа, подписки. */
|
||||
/* Инициализация контейнера групп: реестр, пулы, SQLite и подписки; группы создают сервисы. */
|
||||
struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
|
||||
if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "instance is NULL"); return NULL; }
|
||||
if (!instance || instance->topo_groups || instance->topo_sqlite_db) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid or already initialized topology/SQLite instance=%p", (void*)instance); return NULL;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_id=%016llx", (unsigned long long)instance->node_id);
|
||||
|
||||
struct TOPO_GROUPS* g = u_calloc(1, sizeof(struct TOPO_GROUPS));
|
||||
@@ -727,6 +707,7 @@ struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
|
||||
|
||||
g->node_registry = queue_new(instance->ua, TOPO_NODE_REGISTRY_HASH_SIZE, 0, 8, "node_registry");
|
||||
if (!g->node_registry) { queue_free(g->group_list); u_free(g); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node_registry queue creation failed"); return NULL; }
|
||||
instance->topo_groups = g;
|
||||
|
||||
g->v4_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA4), "to_v4sock");
|
||||
g->v4_addr_pool = memory_pool_init(sizeof(struct TOPO_ADDR4), "to_v4addr");
|
||||
@@ -734,6 +715,10 @@ struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
|
||||
g->v6_addr_pool = memory_pool_init(sizeof(struct TOPO_ADDR6), "to_v6addr");
|
||||
g->v4_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET4), "to_v4sub");
|
||||
g->v6_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET6), "to_v6sub");
|
||||
if (!g->v4_sock_meta_pool || !g->v4_addr_pool || !g->v6_sock_meta_pool || !g->v6_addr_pool ||
|
||||
!g->v4_subnet_pool || !g->v6_subnet_pool) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot initialize topology memory pools"); goto fail;
|
||||
}
|
||||
|
||||
{
|
||||
char db_file[512] = ":memory:";
|
||||
@@ -742,25 +727,26 @@ struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
|
||||
int rc = sqlite3_open_v2(db_file, &instance->topo_sqlite_db,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
|
||||
if (rc == SQLITE_OK && instance->topo_sqlite_db) {
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA wal_autocheckpoint=10000", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA cache_size=-32768", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA mmap_size=134217728", NULL, NULL, NULL);
|
||||
sqlite3_exec(instance->topo_sqlite_db, "PRAGMA temp_store=MEMORY", NULL, NULL, NULL);
|
||||
topo_node_sqlite_init(instance->topo_sqlite_db);
|
||||
rc = sqlite3_exec(instance->topo_sqlite_db, "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;"
|
||||
" PRAGMA synchronous=NORMAL; PRAGMA wal_autocheckpoint=10000; PRAGMA cache_size=-32768;"
|
||||
" PRAGMA mmap_size=134217728; PRAGMA temp_store=MEMORY", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot configure SQLite %s: rc=%d %s",
|
||||
db_file, rc, sqlite3_errmsg(instance->topo_sqlite_db)); goto fail;
|
||||
}
|
||||
if (topo_node_sqlite_init(instance->topo_sqlite_db) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot initialize topology schema in %s: %s",
|
||||
db_file, sqlite3_errmsg(instance->topo_sqlite_db)); goto fail;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "SQLite opened: %s rc=%d db=%p", db_file, rc, (void*)instance->topo_sqlite_db);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "sqlite3_open(%s) failed: %s",
|
||||
db_file, instance->topo_sqlite_db ? sqlite3_errmsg(instance->topo_sqlite_db) : "null db");
|
||||
if (instance->topo_sqlite_db) { sqlite3_close(instance->topo_sqlite_db); instance->topo_sqlite_db = NULL; }
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
instance->topo_groups = g;
|
||||
|
||||
if (topo_node_init_self(instance) < 0) { topo_groups_destroy(instance); return NULL; }
|
||||
if (topo_node_init_self(instance) < 0) goto fail;
|
||||
|
||||
etcp_bind(instance, ETCP_ID_TOPO_ENTRY, topo_group_receive_cbk);
|
||||
etcp_add_conn_status_cbk(instance, topo_group_conn_status, g);
|
||||
@@ -770,6 +756,10 @@ struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TOPO registry initialized (services create their own groups)");
|
||||
return g;
|
||||
fail:
|
||||
topo_groups_destroy(instance);
|
||||
if (instance->topo_sqlite_db) { sqlite3_close(instance->topo_sqlite_db); instance->topo_sqlite_db = NULL; }
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Callback смены active/standby: обновляет свой nodeinfo и рассылает его пирам. */
|
||||
@@ -920,14 +910,16 @@ void topo_group_set_radio(struct TOPO_GROUP* group, int on) {
|
||||
(unsigned long long)group->group_id, group->channel_id);
|
||||
return;
|
||||
}
|
||||
uint8_t previous = group->radio_active;
|
||||
group->radio_active = v;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "set_radio: %s grp=%016llx ch=%s",
|
||||
v ? "ON" : "OFF", (unsigned long long)group->group_id, group->channel_id);
|
||||
if (topo_group_update_my_nodeinfo(group->instance, group) < 0) {
|
||||
group->radio_active = previous;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "set_radio: cannot update local NODEINFO grp=%016llx ch=%s radio=%u",
|
||||
(unsigned long long)group->group_id, group->channel_id, v);
|
||||
return;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "set_radio: %s grp=%016llx ch=%s",
|
||||
v ? "ON" : "OFF", (unsigned long long)group->group_id, group->channel_id);
|
||||
/* Свой мембер обновляется через те же подписки, что и удалённые BGP-узлы. */
|
||||
for (struct topo_node_cbk_entry* cb = group->node_cbks; cb; cb = cb->next)
|
||||
cb->fn(group, group->instance->node_id, TOPO_NODE_EVENT_UPDATE, cb->arg);
|
||||
@@ -963,7 +955,10 @@ void topo_groups_set_node_updated_cb(struct TOPO_GROUPS* groups, topo_node_updat
|
||||
static int topo_group_begin_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn, int retain) {
|
||||
if (!group || !conn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return -1; }
|
||||
if (group->stopping) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "join on stopped group"); return -1; }
|
||||
if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return -1; }
|
||||
if (conn->instance != group->instance || conn->close_requested) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "group join needs an open transport of the same instance peer=%016llx group=%016llx",
|
||||
(unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id); return -1;
|
||||
}
|
||||
if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return -1; }
|
||||
if (!topo_group_peer_allowed(group, conn->peer_node_id)) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "group connect deferred: member unknown peer=%016llx group=%016llx",
|
||||
@@ -1039,7 +1034,10 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn, en
|
||||
bool found_in_list = false;
|
||||
struct ll_entry* e = group->senders_list->head;
|
||||
while (e) { struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; if (item->conn == conn) { found_in_list = true; break; } e = e->next; }
|
||||
if (!found_in_list) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "topo_group_remove_conn: conn=%p peer=%016llx state=%d NOT in senders_list (already removed?), skip grp=%016llx ch=%s", (void*)conn, (unsigned long long)conn->peer_node_id, conn->state, (unsigned long long)group->group_id, group->channel_id); return; }
|
||||
if (!found_in_list) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "ignore transport removal outside group: peer=%016llx group=%016llx reason=%s",
|
||||
(unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, reason_text); return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "BGP peer session lost; removing its routes: peer=%016llx group=%016llx channel=%s "
|
||||
"reason=%s links_up=%u close_requested=%u reinit_pending=%u",
|
||||
@@ -1172,8 +1170,9 @@ int topo_group_remove_path(struct TOPO_GROUP_NODE* nq, struct ETCP_CONN* conn) {
|
||||
}
|
||||
|
||||
/* Одна точка обновления доступности, потребителей и анонсов после изменения путей. */
|
||||
static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, int event) {
|
||||
static int topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, int event) {
|
||||
uint64_t id = node->node_id;
|
||||
int result = 0;
|
||||
struct TOPO_NODEPATH* best = topo_node_best_path(node);
|
||||
node->conn_presence &= ~(NCONN_BGP | NCONN_DIRECT);
|
||||
node->conn_up &= ~(NCONN_BGP | NCONN_DIRECT);
|
||||
@@ -1191,8 +1190,12 @@ static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROU
|
||||
node->paths ? queue_entry_count(node->paths) : 0,
|
||||
(unsigned long long)(best ? best->conn->peer_node_id : 0), best ? best->hop_count : 0);
|
||||
if (group->group_type != TOPO_GROUP_TYPE_CHAT && group->instance->rt) {
|
||||
if (best && node->subnets) route_insert(group->instance->rt, node);
|
||||
else route_delete(group->instance->rt, node);
|
||||
route_delete(group->instance->rt, node);
|
||||
if (best && node->subnets && node->subnets->v4_subnets && !route_insert(group->instance->rt, node)) {
|
||||
result = -1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "BGP IPv4 routes rejected: group=%016llx node=%016llx; previous routes removed",
|
||||
(unsigned long long)group->group_id, (unsigned long long)id);
|
||||
}
|
||||
}
|
||||
topo_fire_nodeinfo_cbk(group->instance, group, node);
|
||||
if (!node->paths) {
|
||||
@@ -1205,6 +1208,7 @@ static void topo_group_routes_changed(struct TOPO_GROUP* group, struct TOPO_GROU
|
||||
topo_recovery_changed(group);
|
||||
conn_mgr_routes_changed(group->conn_mgr);
|
||||
multicast_routes_changed(group->instance);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* TABLE_COMPLETE подтверждает только NODEINFO текущего обмена. Сбой обмена
|
||||
@@ -1244,7 +1248,10 @@ static void topo_group_send_err_group_mismatch(struct TOPO_GROUP* group, struct
|
||||
|
||||
/* Обрабатывает NODEINFO: проверка подписи/версии, обновление узла, пути, роутинг, форвард. */
|
||||
int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from, const uint8_t* data, size_t len) {
|
||||
if (!group || !from || len < sizeof(struct TOPOMSG_NODEINFO_PKT)) return -1;
|
||||
if (!group || !group->instance || !from || from->instance != group->instance || !data ||
|
||||
len < sizeof(struct TOPOMSG_NODEINFO_PKT)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid NODEINFO context or length=%zu", len); return -1;
|
||||
}
|
||||
|
||||
struct TOPOMSG_NODEINFO_PKT* pkt = (struct TOPOMSG_NODEINFO_PKT*)data;
|
||||
struct TOPOMSG_NODE* ni = &pkt->node;
|
||||
@@ -1265,7 +1272,12 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
}
|
||||
|
||||
int dyn = topo_node_dyn_size(ni);
|
||||
if ((size_t)dyn + sizeof(struct TOPOMSG_NODEINFO_PKT) != len) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "Incorrect packet size (%s)", from->log_name); return -1; }
|
||||
if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size.
|
||||
if ((size_t)dyn != len - sizeof(struct TOPOMSG_NODEINFO_PKT)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO size mismatch from=%s node=%016llx length=%zu expected=%zu",
|
||||
from->log_name, (unsigned long long)ni->node_id, len, sizeof(struct TOPOMSG_NODEINFO_PKT) + (size_t)dyn);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t node_id = ni->node_id;
|
||||
if (ni->hop_count >= MAX_HOPS) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO from %s dropped: too many hops (%d)", from->log_name, ni->hop_count); return -1; }
|
||||
@@ -1348,9 +1360,9 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
if (result < 0) return -1;
|
||||
struct TOPO_GROUP_CONN_ITEM* peer = topo_group_peer(group, from->peer_node_id);
|
||||
((struct TOPO_NODEPATH*)nodeinfo1->paths->tail)->exchange = peer ? peer->local_epoch : 0;
|
||||
topo_group_routes_changed(group, nodeinfo1, TOPO_NODE_EVENT_UPDATE);
|
||||
result = topo_group_routes_changed(group, nodeinfo1, TOPO_NODE_EVENT_UPDATE);
|
||||
topo_group_peer_progressed(group, from->peer_node_id);
|
||||
return 0;
|
||||
return result;
|
||||
}
|
||||
uint64_t incoming_timestamp = new_ni->timestamp;
|
||||
if (!nodeinfo1) {
|
||||
@@ -1365,9 +1377,22 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
nodeinfo1 = (struct TOPO_GROUP_NODE*)qe;
|
||||
memset((uint8_t*)nodeinfo1 + sizeof(struct ll_entry), 0, sizeof(*nodeinfo1) - sizeof(struct ll_entry));
|
||||
}
|
||||
/* Подготовить новый путь до изменения записи/подсетей. OOM сохраняет прежний узел целиком. */
|
||||
uint64_t hop_list[MAX_HOPS];
|
||||
if (new_hop_count) memcpy(hop_list, new_hop_list, new_hop_count * sizeof(*hop_list));
|
||||
hop_list[new_hop_count] = from->peer_node_id;
|
||||
struct TOPO_GROUP_NODE prepared = {0};
|
||||
if (topo_group_add_path(&prepared, from, hop_list, new_hop_count + 1, incoming_cumulative_rtt) < 0) {
|
||||
topo_node_destroy(group->instance->topo_groups, new_ni);
|
||||
struct TOPO_GROUP_NODE rejected = { .subnets = new_subnets };
|
||||
topo_nodeq_free_group_fields(group->instance->topo_groups, &rejected);
|
||||
if (is_new_node) queue_entry_free(&nodeinfo1->ll);
|
||||
u_free(new_hop_list); return -1;
|
||||
}
|
||||
struct TOPO_NODE* stored = topo_node_registry_store(group->instance->topo_groups, new_ni);
|
||||
if (!stored) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node registry update failed");
|
||||
topo_nodeq_clear_paths(&prepared);
|
||||
topo_node_destroy(group->instance->topo_groups, new_ni);
|
||||
struct TOPO_GROUP_NODE rejected = { .subnets = new_subnets };
|
||||
topo_nodeq_free_group_fields(group->instance->topo_groups, &rejected);
|
||||
@@ -1383,24 +1408,24 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node added to group: node=%016llx grp=%016llx type=%u ch=%s (bgp)",
|
||||
(unsigned long long)node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id);
|
||||
|
||||
nodeinfo1->paths = paths; nodeinfo1->last_ver = new_ver;
|
||||
/* Остальные соседи сохраняются; после store больше не нужны выделения памяти. */
|
||||
struct TOPO_NODEPATH* incoming = (struct TOPO_NODEPATH*)prepared.paths->head;
|
||||
struct TOPO_GROUP_CONN_ITEM* peer = topo_group_peer(group, from->peer_node_id);
|
||||
incoming->exchange = peer ? peer->local_epoch : 0;
|
||||
if (paths) {
|
||||
struct ll_entry* e;
|
||||
while ((e = queue_data_get(paths))) {
|
||||
if (((struct TOPO_NODEPATH*)e)->conn == from) queue_entry_free(e);
|
||||
else queue_data_put(prepared.paths, e);
|
||||
}
|
||||
queue_free(paths);
|
||||
}
|
||||
nodeinfo1->paths = prepared.paths; nodeinfo1->last_ver = new_ver;
|
||||
nodeinfo1->last_timestamp = incoming_timestamp;
|
||||
nodeinfo1->radio = (ni->flags & TOPO_FLAG_RADIO) ? 1 : 0;
|
||||
nodeinfo1->phone_status = ni->flags & (TOPO_FLAG_PHONE_KNOWN | TOPO_FLAG_PHONE_BUSY);
|
||||
|
||||
uint64_t hop_list[MAX_HOPS];
|
||||
if (new_hop_count > 0) memcpy(hop_list, new_hop_list, new_hop_count * 8);
|
||||
hop_list[new_hop_count] = from->peer_node_id;
|
||||
uint8_t extended_count = new_hop_count + 1;
|
||||
|
||||
u_free(new_hop_list);
|
||||
|
||||
if (topo_group_add_path(nodeinfo1, from, hop_list, extended_count, incoming_cumulative_rtt) < 0) {
|
||||
if (is_new_node) topo_nodeq_remove_node(group, nodeinfo1);
|
||||
return -1;
|
||||
}
|
||||
struct TOPO_GROUP_CONN_ITEM* peer = topo_group_peer(group, from->peer_node_id);
|
||||
((struct TOPO_NODEPATH*)nodeinfo1->paths->tail)->exchange = peer ? peer->local_epoch : 0;
|
||||
if (node_id != group->instance->node_id) {
|
||||
sqlite3* sdb = group->instance->topo_sqlite_db;
|
||||
if (sdb) {
|
||||
@@ -1422,12 +1447,9 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
if (group->instance->topo_groups->node_updated_cb)
|
||||
group->instance->topo_groups->node_updated_cb(group->instance, node_id, ni->public_key, ni->ed25519_public_key);
|
||||
|
||||
topo_group_routes_changed(group, nodeinfo1, is_new_node ? TOPO_NODE_EVENT_NEW : TOPO_NODE_EVENT_UPDATE);
|
||||
int result = topo_group_routes_changed(group, nodeinfo1, is_new_node ? TOPO_NODE_EVENT_NEW : TOPO_NODE_EVENT_UPDATE);
|
||||
|
||||
int socks_changed = 1;
|
||||
if (!is_new_node) socks_changed = 1;
|
||||
|
||||
if (node_id != group->instance->node_id && (is_new_node || socks_changed)) {
|
||||
if (node_id != group->instance->node_id) {
|
||||
struct TOPO_NODE* tni = topo_node_registry_find(group->instance->topo_groups, nodeinfo1->node_id);
|
||||
int v4a = tni ? topo_list_count((struct _topo_head*)tni->v4_addrs) : 0;
|
||||
int v6a = tni ? topo_list_count((struct _topo_head*)tni->v6_addrs) : 0;
|
||||
@@ -1438,12 +1460,12 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
|
||||
topo_group_peer_progressed(group, from->peer_node_id);
|
||||
node_conn_direct_update_node(group->instance, node_id);
|
||||
|
||||
return 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Обрабатывает WITHDRAW: удаляет узел/пути, чистит роутинг, форвардит withdraw. */
|
||||
int topo_group_process_withdraw(struct TOPO_GROUP* group, struct ETCP_CONN* sender, const uint8_t* data, size_t len) {
|
||||
if (!group || !sender || len != sizeof(struct TOPOMSG_WITHDRAW_PKT)) {
|
||||
if (!group || !group->instance || !sender || sender->instance != group->instance || !data || len != sizeof(struct TOPOMSG_WITHDRAW_PKT)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid WITHDRAW len=%zu", len); return -1;
|
||||
}
|
||||
struct TOPOMSG_WITHDRAW_PKT* wp = (struct TOPOMSG_WITHDRAW_PKT*)data;
|
||||
@@ -1914,9 +1936,9 @@ void topo_group_remove_peer_ready_cbk(struct TOPO_GROUP* group, topo_peer_ready_
|
||||
}
|
||||
|
||||
void topo_group_add_node_cbk(struct TOPO_GROUP* group, topo_node_event_fn fn, void* arg) {
|
||||
if (!group || !fn) return;
|
||||
if (!group || !fn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node subscription: invalid arguments"); return; }
|
||||
struct topo_node_cbk_entry* e = u_malloc(sizeof(*e));
|
||||
if (!e) return;
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node subscription allocation failed"); return; }
|
||||
e->fn = fn; e->arg = arg;
|
||||
e->next = group->node_cbks;
|
||||
group->node_cbks = e;
|
||||
@@ -1940,9 +1962,9 @@ void topo_group_remove_node_cbk(struct TOPO_GROUP* group, topo_node_event_fn fn,
|
||||
|
||||
/* Регистрирует глобальный колбэк изменения nodeinfo (уровень инстанса). */
|
||||
void utun_add_nodeinfo_cbk(struct UTUN_INSTANCE* instance, nodeinfo_cbk_fn fn, void* arg) {
|
||||
if (!instance || !fn) return;
|
||||
if (!instance || !fn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "nodeinfo subscription: invalid arguments"); return; }
|
||||
struct nodeinfo_cbk_entry* e = u_malloc(sizeof(*e));
|
||||
if (!e) return;
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "nodeinfo subscription allocation failed"); return; }
|
||||
e->fn = fn; e->arg = arg;
|
||||
e->next = instance->nodeinfo_cbks;
|
||||
instance->nodeinfo_cbks = e;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @brief Групповые сессии и обмен маршрутами поверх общего транспорта NCD.
|
||||
*
|
||||
* У каждой группы своя топология; идентичности и адреса узлов общие (topo_node.h).
|
||||
* UTUN обменивается также подсетями, CHAT допускает только локально известных участников.
|
||||
* UTUN обменивается также подсетями, CHAT допускает только полных неудалённых участников из локальной БД.
|
||||
* Для подключения используйте peer_open/close: группа владеет NCD с начала попытки.
|
||||
* JOIN/ACCEPT согласует сессию, NODEINFO/WITHDRAW поддерживают её маршруты.
|
||||
* Потерю путей обрабатывает topo_recovery, выбор CHAT-пиров — topo_group_connect.
|
||||
@@ -76,6 +76,11 @@ typedef void (*topo_node_updated_fn)(struct UTUN_INSTANCE* inst, uint64_t node_i
|
||||
typedef void (*topo_node_event_fn)(struct TOPO_GROUP* group, uint64_t node_id,
|
||||
int event, void* arg);
|
||||
|
||||
/* Подписки принадлежат группе/instance; fn и arg заимствованы до remove/destroy.
|
||||
* Вызов синхронный, в uasync. Callback не удаляет группу/узел и не меняет список
|
||||
* подписчиков во время обхода; такие действия нужно отложить через uasync_call_soon.
|
||||
* Повторная регистрация создаёт ещё одну подписку; remove снимает первое совпадение. */
|
||||
|
||||
struct topo_node_cbk_entry {
|
||||
topo_node_event_fn fn;
|
||||
void* arg;
|
||||
@@ -111,7 +116,7 @@ void topo_fire_nodeinfo_cbk(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* g
|
||||
// ETCP ID для пакетов топологии: ETCP_ID_TOPO_ENTRY (см. etcp_api.h)
|
||||
|
||||
/* Групповая сессия принадлежит паре (group, peer), транспорт разделяется через NCD.
|
||||
* JOIN_GROUP не добавляет мембера: CHAT проверяет локальную таблицу участников
|
||||
* JOIN_GROUP не добавляет мембера: CHAT требует source=0 и deleted=0 в локальной таблице участников
|
||||
* (chat_join.h / member_sync). Поздний commit мембера повторяет JOIN на готовом NCD.
|
||||
*
|
||||
* Каждая сторона создаёт случайный ненулевой local_epoch. JOIN несёт src_epoch,
|
||||
@@ -266,7 +271,11 @@ struct TOPO_GROUPS {
|
||||
* @brief Создаёт реестр и локальную идентичность; сервисных групп пока нет.
|
||||
* Открывает общую SQLite: db_path/chats.db, без db_path — :memory:.
|
||||
*
|
||||
* @param instance экземпляр utun с node_id и конфигом
|
||||
* При ошибке выделения пулов, открытия/инициализации SQLite или локальной записи
|
||||
* освобождает созданные ресурсы; instance->topo_groups/topo_sqlite_db остаются NULL.
|
||||
* Повторная инициализация уже имеющихся topology/SQLite запрещена.
|
||||
*
|
||||
* @param instance экземпляр utun с uasync, node_id и конфигом; topology/SQLite пока NULL
|
||||
* @return TOPO_GROUPS или NULL при ошибке
|
||||
*/
|
||||
struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance);
|
||||
@@ -296,13 +305,16 @@ struct TOPO_GROUP* topo_groups_get_default(struct TOPO_GROUPS* g);
|
||||
struct TOPO_GROUP* topo_groups_find(struct TOPO_GROUPS* g, uint64_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Создаёт новую группу с указанным типом.
|
||||
* @brief Создаёт и регистрирует новую группу; существующий group_id — ошибка.
|
||||
* После регистрации присоединяет уже поднятые транспорты: CHAT — локальных мемберов,
|
||||
* UTUN — пиров, которыми владеет конфигурация. Для CHAT по group_autoconnect
|
||||
* (по умолчанию 1) включает подбор пиров. Ошибки этих попыток логируются отдельно.
|
||||
*
|
||||
* @param g контейнер групп
|
||||
* @param group_id уникальный идентификатор
|
||||
* @param group_type TOPO_GROUP_TYPE_UTUN или TOPO_GROUP_TYPE_CHAT
|
||||
* @param channel_id идентификатор канала для групп типа CHAT (может быть NULL)
|
||||
* @return TOPO_GROUP или NULL при ошибке
|
||||
* @param channel_id локальный ключ таблиц CHAT; копируется до 63 байт, NULL оставляет пустой ключ
|
||||
* @return TOPO_GROUP с local_node, conn_mgr, broadcast и radio или NULL с полным откатом создания
|
||||
*/
|
||||
struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type, const char* channel_id);
|
||||
|
||||
@@ -316,7 +328,8 @@ struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t grou
|
||||
void topo_groups_remove_group(struct TOPO_GROUPS* g, uint64_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Присоединяет существующий транспорт к группе и начинает JOIN/ACCEPT.
|
||||
* @brief Присоединяет открытый транспорт того же instance к группе и начинает JOIN/ACCEPT.
|
||||
* Допускает постановку обмена до UP; READY требует UP и завершённой синхронизации.
|
||||
* Группа приобретает собственный NCD handle; повтор не сбрасывает сессию. 0/-1.
|
||||
* Для обычного инициатора используйте отменяемый topo_group_peer_open().
|
||||
*/
|
||||
@@ -341,7 +354,7 @@ enum topo_peer_phase { TOPO_PEER_FAILED, TOPO_PEER_CONNECTING, TOPO_PEER_SYNCING
|
||||
* Запрос остаётся валиден после удаления группы (phase=FAILED), caller обязан
|
||||
* закрыть его. Все операции выполняются в потоке единственного uasync.
|
||||
* progress — timebase последнего принятого NODEINFO/шага обмена, не polling time.
|
||||
* open: 0 = запрос создан в *out, -1 = ошибка. */
|
||||
* open: node_id ненулевой и не self; 0 = запрос создан в *out, -1 = ошибка (*out=NULL). */
|
||||
int topo_group_peer_open(struct TOPO_GROUP* group, uint64_t node_id, struct TOPO_PEER_REQUEST** out);
|
||||
/* Освободить свой запрос; после вызова request недействителен. NULL допустим. */
|
||||
void topo_group_peer_close(struct TOPO_PEER_REQUEST* request);
|
||||
@@ -369,10 +382,18 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn, en
|
||||
/**
|
||||
* @brief Обрабатывает пакет NODEINFO.
|
||||
*
|
||||
* Проверка подписи и timestamp, обновление TOPO_GROUP_NODE, добавление пути,
|
||||
* вставка в роутинг, broadcast если не max hops.
|
||||
* data/len — полный TOPOMSG_NODEINFO_PKT с динамическими секциями; память заимствована.
|
||||
* Диспетчер заранее проверяет поколения/членство сессии. Здесь проверяются длина,
|
||||
* group_id тела, тип, подпись, конфликт timestamp и цепочка; добавляется ID from.
|
||||
* Старый timestamp обновляет только путь. Для новой записи путь готовится до commit:
|
||||
* отказ памяти сохраняет прежние запись/подсети/пути. Входная цепочка < MAX_HOPS,
|
||||
* сохранённая после добавления соседа — 1..MAX_HOPS.
|
||||
* Изменения распространяются из актуального состояния с backpressure.
|
||||
* Для UTUN прежние IPv4-маршруты узла удаляются перед установкой нового набора.
|
||||
* Пересечение/OOM маршрутов оставляет новый набор неустановленным, возвращает -1
|
||||
* и запрещает READY; подписанная запись и групповые пути уже могут быть приняты.
|
||||
*
|
||||
* @return 0 при успехе
|
||||
* @return 0 при применении/игнорировании собственного объявления, -1 при ошибке
|
||||
*/
|
||||
int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from, const uint8_t* data, size_t len);
|
||||
/* Присоединить UP-транспорт; переданный handle остаётся у вызывающего. 0/-1. */
|
||||
@@ -383,15 +404,20 @@ int topo_group_leave_peer(struct TOPO_GROUP* group, struct NODE_CONN_DIRECT* han
|
||||
/**
|
||||
* @brief Обрабатывает WITHDRAW.
|
||||
*
|
||||
* Удаляет затронутые пути; узел удаляется только без оставшихся путей.
|
||||
* data/len — заимствованный полный TOPOMSG_WITHDRAW_PKT; поколения/членство
|
||||
* проверяет диспетчер. Удаляет только путь sender; узел удаляется без оставшихся путей.
|
||||
* Неизвестный узел/отсутствующий путь sender — успешный no-op.
|
||||
*
|
||||
* @return 0 при успехе
|
||||
* @return 0 при успехе, -1 при неверных аргументах/длине
|
||||
*/
|
||||
int topo_group_process_withdraw(struct TOPO_GROUP* group, struct ETCP_CONN* sender, const uint8_t* data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Планирует NODEINFO узла одному пиру с backpressure.
|
||||
* group/node/conn заимствованы на время вызова; очередь хранит только node_id.
|
||||
* Запись и RTT читаются при отправке; повторные изменения узла объединяются.
|
||||
* 0 — запланировано либо сессия ещё не ACCEPT, -1 — ошибка. Ошибка самой отложенной
|
||||
* отправки сбрасывает READY и перезапускает обмен по watchdog.
|
||||
*/
|
||||
int topo_group_send_nodeinfo(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, struct ETCP_CONN* conn);
|
||||
|
||||
@@ -410,7 +436,9 @@ struct ETCP_CONN* topo_group_find_conn_for_node(struct TOPO_GROUP* group, uint64
|
||||
/**
|
||||
* @brief Атомарно заменяет путь через conn; остальные соседи не затрагиваются.
|
||||
*
|
||||
* @return 0 при успехе
|
||||
* nq/conn/hop_list заимствованы; цепочка 1..MAX_HOPS копируется. Семантику и петли
|
||||
* проверяет вызывающий; функция проверяет размер и атомарно заменяет путь conn.
|
||||
* @return 0 при успехе, -1 при неверных аргументах/OOM
|
||||
*/
|
||||
int topo_group_add_path(struct TOPO_GROUP_NODE* nq, struct ETCP_CONN* conn, uint64_t* hop_list, uint8_t hop_count, uint16_t cumulative_rtt);
|
||||
|
||||
@@ -428,8 +456,10 @@ void topo_group_set_nat_check_local(struct TOPO_GROUP* group, int allow);
|
||||
|
||||
/* ── Рация (PTT walkie-talkie, per-CHAT-группа) ── */
|
||||
|
||||
/* Включить/выключить прослушивание рации канала: выставляет local_node->radio и
|
||||
* рассылает NODEINFO (TOPO_FLAG_RADIO) через BGP. Работает только для CHAT-групп. */
|
||||
/* Включить/выключить прослушивание рации канала: публикует local_node->radio и
|
||||
* рассылает NODEINFO (TOPO_FLAG_RADIO) через BGP. Только CHAT-группы.
|
||||
* Ошибка подготовки/подписи сохраняет прежние radio_active/local_node и логируется;
|
||||
* повтор такой же команды снова пробует публикацию. */
|
||||
void topo_group_set_radio(struct TOPO_GROUP* group, int on);
|
||||
|
||||
/* Число узлов группы, слушающих рацию (radio==1), без учёта себя. */
|
||||
|
||||
@@ -28,6 +28,7 @@ static const char* const tgc_names[] = { "unknown", "supernode", "desktop", "mob
|
||||
|
||||
struct tgc_candidate {
|
||||
uint64_t node_id, started, retry_at, retry_delay;
|
||||
size_t priority; // текущая позиция в SQL-порядке connected/RTT/node_id
|
||||
struct TOPO_PEER_REQUEST* request;
|
||||
uint8_t kind, tested, manual, listed;
|
||||
};
|
||||
@@ -85,7 +86,7 @@ static struct tgc_candidate* tgc_add(struct TOPO_GROUP_CONNECT* gc, uint64_t id)
|
||||
struct tgc_candidate* list = u_realloc(gc->candidates, (gc->count + 1) * sizeof(*list));
|
||||
if (!list) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate allocation failed"); return NULL; }
|
||||
gc->candidates = list;
|
||||
list[gc->count] = (struct tgc_candidate){ .node_id = id, .retry_delay = TGC_PAUSE_TB };
|
||||
list[gc->count] = (struct tgc_candidate){ .node_id = id, .retry_delay = TGC_PAUSE_TB, .priority = SIZE_MAX };
|
||||
return &list[gc->count++];
|
||||
}
|
||||
|
||||
@@ -101,6 +102,10 @@ static int tgc_load(struct TOPO_GROUP_CONNECT* gc) {
|
||||
size_t previous_count = gc->count;
|
||||
struct tgc_candidate* c = tgc_add(gc, peers[i].node_id);
|
||||
if (!c) { u_free(peers); return -1; }
|
||||
if (c->priority != i)
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "group candidate priority: group=%016llx node=%016llx position=%zu",
|
||||
(unsigned long long)group->group_id, (unsigned long long)c->node_id, i);
|
||||
c->priority = i;
|
||||
uint8_t device = peers[i].client_type;
|
||||
struct TOPO_NODE* node = topo_node_registry_find(group->instance->topo_groups, c->node_id);
|
||||
if (node && node->timestamp) device = node->client_type;
|
||||
@@ -283,7 +288,8 @@ static void tgc_step(void* arg) {
|
||||
for (size_t i = 0; i < gc->count; i++) {
|
||||
struct tgc_candidate* c = &gc->candidates[i];
|
||||
if (!c->listed || c->kind != kind || c->request || c->retry_at > now || tgc_peer_up(gc->group, c->node_id)) continue;
|
||||
if (!best || c->tested < best->tested || (c->tested == best->tested && c->retry_at < best->retry_at)) best = c;
|
||||
if (!best || c->tested < best->tested || (c->tested == best->tested &&
|
||||
(c->retry_at < best->retry_at || (c->retry_at == best->retry_at && c->priority < best->priority)))) best = c;
|
||||
}
|
||||
if (!best) break;
|
||||
if (tgc_open(gc, best) == 0) pending[kind]++;
|
||||
|
||||
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user