Browse Source
Добавлен 64-битный reset_id (эпоха ресета) в INIT-фреймы и STCP-handshake: - генерится один раз при создании conn, меняется только при фатальном ресете (normalizer desync / bad fragment size); - при приёме чужого id slave принимает эпоху пира и ресетится, master держит свою; - первый id принимается без ресета (свежие при init); - «tcp peer clean» реинит гейтится по смене id — нет повторного сброса seq. Это устраняет коллизию seq=1 (RX dup) из-за двойного реинита нескольких TCP-линков, из-за которой NODEINFO пира терялся и member не возвращался online. Также: chat member online теперь из topo_group (без БД/merkle), тест test_etcp_seq_collision.v2
29 changed files with 663 additions and 385 deletions
@ -0,0 +1,89 @@
|
||||
# Задача: BGP NODEINFO теряется при флапающем реконнекте (chatgui member online не возвращается) |
||||
|
||||
## Кратко |
||||
|
||||
После рестарта Android-пира и реконнекта локальный chatgui показывает мембера **offline** и не возвращает **online**. |
||||
Корневая причина — **не в chat/member коде**, а в ETCP: BGP-пакет NODEINFO (347 байт) расшифровывается, но **не доходит до BGP-обработчика** `topo_group_receive_cbk()`. |
||||
|
||||
Нужно: **точно локализовать точку потери NODEINFO в receive-пути ETCP и починить её.** |
||||
|
||||
## Что уже сделано (исправлено и работает) |
||||
|
||||
1. `tools/chatgui/src/mainwindow.cpp` — **исправлен** `onMemberUpdatedCallback`/`onMemberRemovedCallback`: проверка `len < 1+64+1+79` отбрасывала все события с channel_id короче 64 симв. Теперь `len < 1` + точная проверка. (Это была причина «не обновляется вообще».) |
||||
2. `src/chat/chat_member.c/h` — добавлен `chat_core_member_online(ch_id,node_id)` = присутствие в `topo_group` (`topo_node_find_by_id`). Используется в `chat_core_get_member_list`/`get_single_member`. Убран `nodes.online` из SQL и блок `connected→2`. |
||||
3. `src/chat/chat_core.c`, `src/chat/chat_sync.c`, `src/chat/member_sync.c/h` — удалён онлайн-статус из БД + merkle-online gossip (`member_sync_set_online`, `_ms_apply_update`, `push_update`). |
||||
4. `src/chat/chat_status.c` + `tools/chatgui/src/accountlist.cpp` — флаг «bgp present» (0x08) для detail-панели. |
||||
5. `tools/chatgui/src/nodespage.cpp` — убрана колонка Online из диагностики. |
||||
|
||||
Эти правки корректны; статус работает на чистом connect/disconnect/стабильном реконнекте. |
||||
|
||||
## Найденная причина (подтверждено логами) |
||||
|
||||
- Android **шлёт** свой NODEINFO: `send_nodeinfo: node 1657b3ea8281c0a8 ver=2` (bgp-debug на Android). |
||||
- Локально пакет **расшифровывается**: `decrypt: code=01 dlen=347 plen=350` (347-байтный BGP-пакет, `code=0x01` = `ETCP_ID_TOPO_ENTRY`). |
||||
- Но `BGP recv NODEINFO nid=1657` (строка 193 в `topo_group.c`) **не появляется** — до обработчика пакет не доходит. |
||||
- При этом маленький `TABLE_COMPLETE` (10 байт) **доходит** (виден в логе как «mislabeled» `nid=aaaaaaaaaadeadbe` — это строка 193 логирует не-NODEINFO пакет как NODEINFO, garbage-поля). |
||||
- Признак: в стабильных рестартах (18:18/18:20) узел **добавляется** каждый раз, в флапающем (18:07) — нет. Т.е. потеря привязана к флапу (TCP-линки + ETCP reinit). |
||||
|
||||
## Receive-путь ETCP (где искать потерю) |
||||
|
||||
``` |
||||
decrypt (etcp_connections.c:2262, лог "decrypt: code=.. dlen=..") |
||||
→ if (link_state==CONNECTED) etcp_conn_input(pkt) |
||||
else memory_pool_free(pkt) ← добавлен WARN "decrypted pkt DROPPED" |
||||
→ etcp_conn_input (etcp.c:1506, "RX pkt dlen=%d") |
||||
→ нормализатор (reassembly) → int_queue |
||||
→ etcp_int_recv (etcp_api.c) ← добавлен WARN "int_recv BGP id=0x01 len=.." |
||||
→ dispatch по route id (0x01) → topo_group_receive_cbk (topo_group.c:193 "BGP recv NODEINFO") |
||||
``` |
||||
|
||||
## Уже добавленный дебаг (для локализации) |
||||
|
||||
1. `src/transport_layer/etcp_connections.c` (после дешифровки): |
||||
```c |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] decrypted pkt DROPPED: link_state=%d code=0x%02x dlen=%u", |
||||
link->etcp->log_name, link->link_state, pkt_code, pkt->data_len); |
||||
memory_pool_free(e_sock->instance->pkt_pool, pkt); |
||||
} |
||||
``` |
||||
2. `src/transport_layer/etcp_api.c` (`etcp_int_recv`, после чтения `id`): |
||||
```c |
||||
uint8_t id = e->dgram[0]; |
||||
if (id == 0x01) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "int_recv BGP id=0x01 conn=%s len=%zu", conn->log_name, e->len); |
||||
``` |
||||
3. Android: `tools/chatgui-android/libutun_lite/instance_lite.c` добавлено |
||||
`debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);` (пересобрать APK: `cd tools/chatgui-android && ./build.sh`). |
||||
4. chatgui конфиг `tools/chatgui/build/vibechat.cfg` — включены `bgp=debug`, `chat_sync=debug`, `member_sync=debug`, `debug=debug`. |
||||
|
||||
Как читать (по таймстампу флапа): |
||||
- есть `decrypt code=01 dlen=..` + `DROPPED` → потеря в проверке `link_state==CONNECTED`; |
||||
- есть `decrypt` + нет `DROPPED` + нет `int_recv BGP` → потеря в `etcp_conn_input`/нормализаторе; |
||||
- есть `int_recv BGP` + нет `BGP recv NODEINFO` → потеря в BGP-обработчике. |
||||
|
||||
## Задача для агента |
||||
|
||||
1. Пересобрать chatgui (`cd tools/chatgui && cmake --build build -j4`) и Android APK (`cd tools/chatgui-android && ./build.sh`). |
||||
2. Запустить chatgui (DISPLAY=:0, `./vibechat`), воспроизвести флап реконнекта: |
||||
```bash |
||||
adb shell am force-stop com.utun.chat && adb shell am start -n com.utun.chat/.MainActivity |
||||
``` |
||||
(повторить несколько раз; флап ловится не каждый раз). |
||||
3. В логах (`tools/chatgui/build/chatgui.log`) найти потерю NODEINFO и **точно определить точку** по таблице выше. |
||||
4. Починить первопричину (варианты): |
||||
- буферизовать расшифрованный пакет до `LINK_STATE_CONNECTED` вместо `memory_pool_free`; |
||||
- либо гарантированный BGP-retry (после conn UP, если узел пира не в `group->nodes` через N мс — повторно `topo_group_send_table_request`). |
||||
5. Убрать весь диагностический мусор: |
||||
- WARN "DROPPED" (etcp_connections.c), WARN "int_recv BGP" (etcp_api.c); |
||||
- DEBUG-логи в `chat_member.c` (`on_member_props_changed`), `chat_sync.c` (`cs_on_peer_status_changed`), `memberlistmodel.cpp`; |
||||
- `instance_lite.c` BGP-debug; |
||||
- `vibechat.cfg` — вернуть категории в закомментированное состояние. |
||||
6. Прогнать: connect→online, disconnect→offline, реконнект (флап)→online. `./check.sh` (или `cd src && make -j4`). |
||||
|
||||
## Воспроизведение / ключевые артефакты |
||||
|
||||
- Пир: Android SM-A525F (`com.utun.chat`), node_id `0x1657b3ea8281c0a8`. |
||||
- Локальный: chatgui (`tools/chatgui/build/vibechat`), node_id `0x24cd036a6e659b9a`. |
||||
- Канал: `ch_id=7206723622466219923`, `group_id=0x64036cf3a783ef93`. |
||||
- Лог chatgui: `tools/chatgui/build/chatgui.log` (debug_file из конфига). |
||||
- Лог Android: `adb logcat -d | grep -iE "send_nodeinfo|NOT in registry|NODEINFO|DROPPED|int_recv"`. |
||||
@ -0,0 +1,243 @@
|
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include "../lib/platform_compat.h" |
||||
#include "test_utils.h" |
||||
#ifdef _WIN32 |
||||
#include <windows.h> |
||||
#include <direct.h> |
||||
#include <process.h> |
||||
#define getpid _getpid |
||||
#else |
||||
#include <unistd.h> |
||||
#endif |
||||
#include <time.h> |
||||
|
||||
#include "etcp.h" |
||||
#include "etcp_connections.h" |
||||
#include "etcp_api.h" |
||||
#include "../src/config_parser.h" |
||||
#include "../src/utun_instance.h" |
||||
#include "routing.h" |
||||
#include "secure_channel.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/ll_queue.h" |
||||
#include "../lib/debug_config.h" |
||||
|
||||
#define TEST_TIMEOUT_MS 60000 |
||||
#define PACKET_SIZE 100 |
||||
#define PACKETS_FIRST 3 |
||||
|
||||
static struct UTUN_INSTANCE* server_instance = NULL; |
||||
static struct UTUN_INSTANCE* client_instance = NULL; |
||||
static struct UASYNC* ua = NULL; |
||||
|
||||
static char temp_dir[] = "/tmp/utun_col_XXXXXX"; |
||||
static char server_config_path[256]; |
||||
static char client_config_path[256]; |
||||
static int server_port = 0; |
||||
static int client_port = 0; |
||||
|
||||
static int test_completed = 0; |
||||
static void* global_timeout_id = NULL; |
||||
|
||||
static int phase = 0; |
||||
static int packets_sent = 0; |
||||
static int packets_received = 0; |
||||
static uint8_t packet_buffer[PACKET_SIZE]; |
||||
|
||||
// client = 0x1111 (master, smaller node_id), server = 0x2222 (slave)
|
||||
static int create_temp_configs(void) { |
||||
if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "Failed to create temp directory\n"); return -1; } |
||||
int base_port = 45000 + (getpid() % 10000); |
||||
server_port = base_port; |
||||
client_port = base_port + 1; |
||||
|
||||
snprintf(server_config_path, sizeof(server_config_path), "%s/server.conf", temp_dir); |
||||
snprintf(client_config_path, sizeof(client_config_path), "%s/client.conf", temp_dir); |
||||
|
||||
FILE* f = fopen(server_config_path, "w"); |
||||
if (!f) { fprintf(stderr, "Failed to create server config file\n"); return -1; } |
||||
fprintf(f, |
||||
"[global]\n" |
||||
"my_node_id=0x2222222222222222\n" |
||||
"my_private_key=38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68\n" |
||||
"my_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a\n" |
||||
"tun_ip=10.99.0.1/24\n" |
||||
"tun_ifname=tun99\n" |
||||
"\n" |
||||
"[server: test]\n" |
||||
"addr=127.0.0.1:%d\n" |
||||
"type=public\n" |
||||
"\n" |
||||
"[allowed_keys]\n" |
||||
"allow_all=1\n", |
||||
server_port); |
||||
fclose(f); |
||||
|
||||
f = fopen(client_config_path, "w"); |
||||
if (!f) { fprintf(stderr, "Failed to create client config file\n"); test_unlink(server_config_path); return -1; } |
||||
fprintf(f, |
||||
"[global]\n" |
||||
"my_node_id=0x1111111111111111\n" |
||||
"my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" |
||||
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" |
||||
"tun_ip=10.99.0.2/24\n" |
||||
"tun_ifname=tun98\n" |
||||
"\n" |
||||
"[server: test]\n" |
||||
"addr=127.0.0.1:%d\n" |
||||
"type=public\n" |
||||
"\n" |
||||
"[client: test_client]\n" |
||||
"keepalive=1\n" |
||||
"peer_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a\n" |
||||
"link=test:127.0.0.1:%d\n", |
||||
client_port, server_port); |
||||
fclose(f); |
||||
return 0; |
||||
} |
||||
|
||||
static void cleanup_temp_configs(void) { |
||||
if (server_config_path[0]) test_unlink(server_config_path); |
||||
if (client_config_path[0]) test_unlink(client_config_path); |
||||
if (temp_dir[0]) test_rmdir(temp_dir); |
||||
} |
||||
|
||||
static struct ETCP_CONN* server_conn(void) { |
||||
if (!server_instance || !server_instance->connections || !server_instance->connections->head) return NULL; |
||||
return ((struct conn_queue_entry*)server_instance->connections->head->data)->conn; |
||||
} |
||||
|
||||
static struct ETCP_CONN* client_conn(void) { |
||||
if (!client_instance || !client_instance->connections || !client_instance->connections->head) return NULL; |
||||
return ((struct conn_queue_entry*)client_instance->connections->head->data)->conn; |
||||
} |
||||
|
||||
static int is_connected(void) { |
||||
struct ETCP_CONN* c = client_conn(); |
||||
return c && c->initialized && c->links_up; |
||||
} |
||||
|
||||
static void drain_received(int count_flag) { |
||||
struct ETCP_CONN* conn = server_conn(); |
||||
if (!conn || !conn->output_queue) return; |
||||
queue_set_callback(conn->output_queue, NULL, NULL); |
||||
struct ETCP_FRAGMENT* pkt; |
||||
while ((pkt = (struct ETCP_FRAGMENT*)queue_data_get(conn->output_queue)) != NULL) { |
||||
if (count_flag) packets_received++; |
||||
if (pkt->ll.dgram) memory_pool_free(conn->instance->data_pool, pkt->ll.dgram); |
||||
queue_entry_free((struct ll_entry*)pkt); |
||||
} |
||||
} |
||||
|
||||
static void send_packets(int n) { |
||||
struct ETCP_CONN* conn = client_conn(); |
||||
if (!conn || !conn->initialized) return; |
||||
for (int i = 0; i < n; i++) { |
||||
if (queue_entry_count(conn->input_queue) > 10) break; |
||||
for (int j = 0; j < PACKET_SIZE; j++) packet_buffer[j] = (uint8_t)((packets_sent + j) % 256); |
||||
if (etcp_int_send(conn, packet_buffer, PACKET_SIZE) != 0) break; |
||||
packets_sent++; |
||||
} |
||||
} |
||||
|
||||
static void monitor(void* arg) { |
||||
(void)arg; |
||||
if (test_completed) return; |
||||
|
||||
switch (phase) { |
||||
case 0: // wait connect
|
||||
drain_received(0); |
||||
if (is_connected()) { |
||||
printf("=== Phase 0: connected ===\n"); |
||||
phase = 1; |
||||
} |
||||
break; |
||||
|
||||
case 1: // send PACKETS_FIRST
|
||||
send_packets(PACKETS_FIRST - packets_sent); |
||||
drain_received(1); |
||||
if (packets_received >= PACKETS_FIRST) { |
||||
printf("=== Phase 1: %d packets delivered ===\n", packets_received); |
||||
printf(" forcing one-sided reinit on client (master 0x1111)\n"); |
||||
etcp_conn_reinit(client_conn(), "test seq collision"); |
||||
{ struct ETCP_LINK* l = client_conn() ? client_conn()->links : NULL; |
||||
while (l) { etcp_link_enter_reinit(l); l = l->next; } } |
||||
phase = 2; |
||||
} |
||||
break; |
||||
|
||||
case 2: // wait client re-ready, then send 1 packet
|
||||
drain_received(1); |
||||
if (is_connected()) { |
||||
printf(" client reinitialized, sending post-reinit packet\n"); |
||||
send_packets(1); |
||||
phase = 3; |
||||
} |
||||
break; |
||||
|
||||
case 3: // verify delivery
|
||||
drain_received(1); |
||||
if (packets_received >= PACKETS_FIRST + 1) { |
||||
printf("\n[PASS] post-reinit packet delivered (recv=%d)\n", packets_received); |
||||
test_completed = 1; |
||||
return; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
if (!test_completed) uasync_set_timeout(ua, 20, NULL, monitor, "test_monitor"); |
||||
} |
||||
|
||||
static void test_timeout(void* arg) { |
||||
(void)arg; |
||||
if (!test_completed) { |
||||
printf("\n[FAIL] timeout at phase %d: sent=%d recv=%d — post-reinit packet silently dropped\n", |
||||
phase, packets_sent, packets_received); |
||||
test_completed = 2; |
||||
} |
||||
} |
||||
|
||||
int main(void) { |
||||
if (create_temp_configs() != 0) return 1; |
||||
debug_config_init(); |
||||
utun_instance_set_tun_init_enabled(0); |
||||
|
||||
printf("=== ETCP Seq Collision Regression Test ===\n"); |
||||
|
||||
ua = uasync_create(); |
||||
server_instance = utun_instance_create(ua, server_config_path); |
||||
if (!server_instance || utun_instance_init(server_instance) < 0) { |
||||
fprintf(stderr, "Failed to create server\n"); |
||||
if (server_instance) utun_instance_destroy(server_instance); |
||||
uasync_destroy(ua, 0); cleanup_temp_configs(); return 1; |
||||
} |
||||
client_instance = utun_instance_create(ua, client_config_path); |
||||
if (!client_instance || utun_instance_init(client_instance) < 0) { |
||||
fprintf(stderr, "Failed to create client\n"); |
||||
utun_instance_destroy(server_instance); |
||||
if (client_instance) utun_instance_destroy(client_instance); |
||||
uasync_destroy(ua, 0); cleanup_temp_configs(); return 1; |
||||
} |
||||
|
||||
uasync_set_timeout(ua, 20, NULL, monitor, "test_monitor"); |
||||
global_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout, "test_timeout"); |
||||
|
||||
int elapsed = 0; |
||||
int poll_interval = 20; |
||||
while (!test_completed && elapsed < TEST_TIMEOUT_MS * 10 + 5000) { |
||||
uasync_poll(ua, poll_interval); |
||||
elapsed += poll_interval; |
||||
} |
||||
|
||||
if (global_timeout_id) uasync_cancel_timeout(ua, global_timeout_id); |
||||
if (server_instance) { server_instance->running = 0; utun_instance_destroy(server_instance); server_instance = NULL; } |
||||
if (client_instance) { client_instance->running = 0; utun_instance_destroy(client_instance); client_instance = NULL; } |
||||
if (ua) { uasync_destroy(ua, 0); ua = NULL; } |
||||
cleanup_temp_configs(); |
||||
|
||||
if (test_completed == 1) { printf("=== TEST PASSED ===\n"); return 0; } |
||||
printf("=== TEST FAILED ===\n"); |
||||
return 1; |
||||
} |
||||
Loading…
Reference in new issue