From 5628138d166f6225fc56364d2112f8292531a3ff Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 21 Sep 2026 23:30:39 +0300 Subject: [PATCH] =?UTF-8?q?etcp:=20keepalive=20sleep-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=BE=D0=BB=20=D0=BD=D0=B0=20=D1=84=D0=BB?= =?UTF-8?q?=D0=B0=D0=B3=D0=B0=D1=85=20(KA=5FFLAG=5FSLEEP=20+=20sleep=5Ftim?= =?UTF-8?q?e,=20announce-retry,=20ack/echo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_setting.c | 1 + src/transport_layer/etcp_connections.c | 12 +- src/transport_layer/etcp_connections.h | 10 +- src/transport_layer/etcp_keepalive.c | 327 +++++++++++------- src/transport_layer/etcp_keepalive.h | 56 ++- .../java/com/utun/chat/data/ConfigProvider.kt | 4 + .../utun/chat/ui/screens/SettingsScreen.kt | 7 + tools/chatgui-android/libutun_lite/standby.c | 26 ++ tools/chatgui-android/libutun_lite/standby.h | 13 + 9 files changed, 316 insertions(+), 140 deletions(-) diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c index 7a04e00f..73481726 100644 --- a/src/chat/chat_setting.c +++ b/src/chat/chat_setting.c @@ -42,6 +42,7 @@ static const struct chat_setting_def g_setting_defs[] = { {"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600}, {"standby_sleep_sec", CHAT_SETTING_INT, 60, 1, 86400}, {"standby_min_sleep_sec", CHAT_SETTING_INT, 15, 1, 86400}, + {"standby_keepalive_ms", CHAT_SETTING_INT, 30000, 1000, 600000}, {"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */ {"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1}, {"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0}, diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 1dde2085..4b805cd9 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -1005,7 +1005,13 @@ struct ETCP_LINK* etcp_link_new(struct ETCP_CONN* etcp, struct ETCP_SOCKET* conn link->keepalive_sent_count = 0; link->keepalive_recv_count = 0; link->ka_period_ms = (uint16_t)link->keepalive_interval; - link->ka_pinned = 0; + link->ka_sleep_period_ms = 0; + link->ka_sleeping = 0; + link->ka_sleep_acked = 0; + link->ka_sleep_time = 0; + link->ka_sleep_deadline_tb = 0; + link->ka_sleep_announce_timer = NULL; + link->ka_sleep_announce_end_tb = 0; /* inflight_lim_bytes set above */ link->bandwidth = 10000; // начальная оценка 10 Mbps для шейпера link->burst_id = 0; @@ -1195,6 +1201,7 @@ void etcp_link_enter_ready_tcp(struct ETCP_LINK *link) { if (etcp->state == 2) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "enter_ready_tcp: conn DELETED after conn_ready, link=%p", (void*)link); return; } etcp_link_send_keepalive(link); start_keepalive_timer(link); + etcp_keepalive_announce_mode(link); loadbalancer_link_ready(link); etcp_fire_link_status_cbk(link, old_state, link->link_status); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] TCP link %d UP (mtu=%d init=%d up=%d tcp_links=%d)", @@ -1993,6 +2000,7 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D start_keepalive_timer(link); + etcp_keepalive_announce_mode(link); loadbalancer_link_ready(link); @@ -2500,7 +2508,7 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, char who[256]; snprintf(who, sizeof(who), "%s addr=%s", link->etcp->log_name, sockaddr_storage_to_str(&link->remote_addr).str); - int is_ka = (pkt_code == ETCP_KEEPALIVE || pkt_code == ETCP_KEEPALIVE_REQ || pkt_code == ETCP_KEEPALIVE_RESP); + int is_ka = (pkt_code == ETCP_KEEPALIVE); standby_log_rx(is_ka ? (link->is_tcp ? "tcp keepalive" : "udp keepalive") : (link->is_tcp ? "tcp data" : "udp data"), who); } diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index ff46a994..1bf8563b 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -281,8 +281,14 @@ struct ETCP_LINK { // Keepalive state void* keepalive_timer; // Таймер для отправки keepalive пакетов uint32_t keepalive_timeout; // таймаут (ms) - uint16_t ka_period_ms; // адаптивный период отправки keepalive (200→10000->200) - uint8_t ka_pinned; // 1 = период зафиксирован протоколом (standby-режим мобильного), адаптивный рост отключён + uint16_t ka_period_ms; // текущий период отправки keepalive (адаптивный в normal, фиксированный в спячке) + uint16_t ka_sleep_period_ms; // период keepalive в спячке (мс; настройка клиента / ka_period_ms пира) + uint8_t ka_sleeping; // 1 = линк в режиме спячки + uint8_t ka_sleep_acked; // клиент: получен ack спячки от сервера + uint16_t ka_sleep_time; // дедлайн пробуждения (x0.1с), текущий + uint64_t ka_sleep_deadline_tb; // абсолютный дедлайн пробуждения (timebase 0.1мс) + void* ka_sleep_announce_timer; // таймер ретрая анонса спячки (клиент) + uint64_t ka_sleep_announce_end_tb; // конец окна ретрая анонса (4с) uint8_t pkt_sent_since_keepalive; // Флаг: был ли отправлен пакет с последнего keepalive тика uint32_t keepalive_sent_count; // Счётчик отправленных keepalive uint32_t keepalive_recv_count; // Счётчик полученных keepalive diff --git a/src/transport_layer/etcp_keepalive.c b/src/transport_layer/etcp_keepalive.c index 176503dc..abdc5686 100644 --- a/src/transport_layer/etcp_keepalive.c +++ b/src/transport_layer/etcp_keepalive.c @@ -7,10 +7,17 @@ #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/memory_pool.h" +#ifdef UTUN_HAVE_STANDBY +#include "standby.h" +#endif // forward declarations (static) static void keepalive_timer_cb(void* arg); static int etcp_all_links_down(struct ETCP_CONN* etcp); +static void ka_send_sleep_keepalive(struct ETCP_LINK* link); +static void ka_announce_retry_cb(void* arg); +static void ka_client_enter_sleep(struct ETCP_LINK* link); +static void ka_client_wake(struct ETCP_LINK* link); // === перенесено из etcp_connections.c без изменений === @@ -27,12 +34,12 @@ uint16_t negotiate_keepalive(uint8_t my_type, uint16_t my_ka, return result; } -// Send empty keepalive packet (only timestamp, no sections) +// Send keepalive packet: code(0x08) + flags + ka_period_ms + [sleep_time] void etcp_link_send_keepalive(struct ETCP_LINK* link) { DEBUG_TRACE(DEBUG_CATEGORY_KEEPALIVE, ""); if (!link || !link->etcp || !link->etcp->instance) return; - struct ETCP_DGRAM* dgram = u_malloc(sizeof(struct ETCP_DGRAM) + 4); + struct ETCP_DGRAM* dgram = u_malloc(sizeof(struct ETCP_DGRAM) + 8); if (!dgram) { DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "malloc failed"); return; @@ -40,9 +47,16 @@ void etcp_link_send_keepalive(struct ETCP_LINK* link) { dgram->link = link; dgram->data[0] = ETCP_KEEPALIVE; - dgram->data[1] = link->ka_period_ms & 0xFF; - dgram->data[2] = link->ka_period_ms >> 8; - dgram->data_len = 3; + dgram->data[1] = link->ka_sleeping ? KA_FLAG_SLEEP : 0; + dgram->data[2] = link->ka_period_ms & 0xFF; + dgram->data[3] = link->ka_period_ms >> 8; + if (link->ka_sleeping) { + dgram->data[4] = link->ka_sleep_time & 0xFF; + dgram->data[5] = link->ka_sleep_time >> 8; + dgram->data_len = 6; + } else { + dgram->data_len = 4; + } dgram->noencrypt_len = 0; dgram->timestamp = get_current_timestamp(); dgram->flag_up = link->is_tcp ? 1 : link->recv_keepalive; @@ -88,6 +102,15 @@ void start_keepalive_timer(struct ETCP_LINK* link) { } } +static void restart_keepalive_timer_ms(struct ETCP_LINK* link, uint16_t ms) { + if (!link || !link->etcp || !link->etcp->instance) return; + if (link->keepalive_timer) { + uasync_cancel_timeout(link->etcp->instance->ua, link->keepalive_timer); + link->keepalive_timer = NULL; + } + link->keepalive_timer = uasync_set_timeout(link->etcp->instance->ua, (uint32_t)ms * 10, link, keepalive_timer_cb, "link_keepalive"); +} + // Keepalive timer callback static void keepalive_timer_cb(void* arg) { DEBUG_TRACE(DEBUG_CATEGORY_KEEPALIVE, ""); @@ -150,9 +173,19 @@ static void keepalive_timer_cb(void* arg) { } } - // Adaptive keepalive period (only if adaptive enabled) - if (link->ka_pinned) - link->ka_period_ms = (uint16_t)link->keepalive_interval; + // Сервер: дедлайн спячки пира вышел — пир должен был проснуться/переанонсировать, + // выходим из спячки в обычный режим (базовый период, probe штатным keepalive). + if (link->is_server && link->ka_sleeping && now >= link->ka_sleep_deadline_tb) { + link->ka_sleeping = 0; + link->ka_period_ms = link->keepalive_interval; + link->keepalive_timeout = (uint32_t)link->keepalive_interval * KA_TIMEOUT_MULT; + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] server: sleep deadline expired, back to normal (interval=%dms)", + link->etcp->log_name, link->keepalive_interval); + } + + // Период: спячка — фиксированный sleep-период; normal — адаптивный от базового. + if (link->ka_sleeping) + link->ka_period_ms = link->ka_sleep_period_ms; else if (link->pkt_sent_since_keepalive) link->ka_period_ms = (uint16_t)link->keepalive_interval; else if (link->etcp->instance && link->etcp->instance->config && @@ -162,94 +195,135 @@ static void keepalive_timer_cb(void* arg) { } link->pkt_sent_since_keepalive = 0; - // Send keepalive (server stops if link lost, client always sends) - if (!link->is_server || link->recv_keepalive) + // Отправка keepalive (в спячке — с флагом и свежим дедлайном) + if (link->ka_sleeping) + ka_send_sleep_keepalive(link); + else if (!link->is_server || link->recv_keepalive) etcp_link_send_keepalive(link); restart_timer: link->keepalive_timer = uasync_set_timeout(link->etcp->instance->ua, link->ka_period_ms * 10, link, keepalive_timer_cb, "link_keepalive"); } -// === протокол смены keepalive-режима (standby/normal, p2p) === +// === протокол спячки (sleep) на флагах keepalive-пакета === + +// Период keepalive в спячке (мс): из настройки standby_keepalive_ms (Android). +static uint16_t ka_sleep_period_ms(struct UTUN_INSTANCE* inst) { +#ifdef UTUN_HAVE_STANDBY + int ms = standby_get_keepalive_ms(); + if (ms < 100) ms = 100; + return (uint16_t)ms; +#else + (void)inst; + return 30000; +#endif +} -static uint8_t mobile_desired_ka_mode(struct UTUN_INSTANCE* inst) { - return (inst->client_activity == CLIENT_ACTIVITY_ACTIVE) ? KA_MODE_NORMAL : KA_MODE_STANDBY; +// Дедлайн пробуждения (x0.1с): остаток SLEEP либо остаток ACTIVE + полная SLEEP. +static uint16_t ka_wake_deadline_01s(void) { +#ifdef UTUN_HAVE_STANDBY + int deadline_tb = standby_get_wake_deadline_tb(); + uint32_t v = deadline_tb > 0 ? (uint32_t)(deadline_tb / 1000) : 0; + return v > 65535 ? 65535 : (uint16_t)v; +#else + return 0; +#endif } -static void restart_keepalive_timer_ms(struct ETCP_LINK* link, uint16_t ms) { - if (!link || !link->etcp || !link->etcp->instance) return; - if (link->keepalive_timer) { - uasync_cancel_timeout(link->etcp->instance->ua, link->keepalive_timer); - link->keepalive_timer = NULL; +// Отправить keepalive спячки (обновить дедлайн) + при необходимости заармить ретрай. +static void ka_send_sleep_keepalive(struct ETCP_LINK* link) { + uint16_t sleep_time = ka_wake_deadline_01s(); + if (sleep_time == 0) sleep_time = link->ka_sleep_period_ms / 100; + link->ka_sleep_time = sleep_time; + etcp_link_send_keepalive(link); + if (!link->ka_sleep_acked && link->ka_sleep_announce_timer == NULL) { + link->ka_sleep_announce_timer = uasync_set_timeout(link->etcp->instance->ua, + KA_SLEEP_RETRY_MS * 10, link, ka_announce_retry_cb, "ka_sleep_announce"); } - link->keepalive_timer = uasync_set_timeout(link->etcp->instance->ua, (uint32_t)ms * 10, link, keepalive_timer_cb, "link_keepalive"); } -static void apply_link_ka_mode(struct ETCP_LINK* link, uint8_t mode) { - uint16_t ms = (mode == KA_MODE_STANDBY) ? KA_MOBILE_STANDBY_MS : KA_MOBILE_ACTIVE_MS; - link->keepalive_interval = ms; - link->ka_period_ms = ms; - link->keepalive_timeout = (uint32_t)ms * KA_TIMEOUT_MULT; - link->ka_pinned = (mode == KA_MODE_STANDBY) ? 1 : 0; - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive mode applied: mode=%s interval=%dms timeout=%dms pinned=%d", - link->etcp->log_name, mode == KA_MODE_STANDBY ? "standby" : "normal", - ms, link->keepalive_timeout, link->ka_pinned); - restart_keepalive_timer_ms(link, link->ka_period_ms); +static void ka_announce_retry_cb(void* arg) { + struct ETCP_LINK* link = (struct ETCP_LINK*)arg; + link->ka_sleep_announce_timer = NULL; + if (!link || !link->etcp || !link->etcp->instance) return; + if (!link->ka_sleeping || link->ka_sleep_acked) return; // анонс завершён + if (get_time_tb() >= link->ka_sleep_announce_end_tb) return; // окно 4с вышло + ka_send_sleep_keepalive(link); // ретрай } -static void etcp_keepalive_send_mode_pkt(struct ETCP_LINK* link, uint8_t code, uint8_t mode) { +// Клиент: вход в спячку (анонс с ретраями). +static void ka_client_enter_sleep(struct ETCP_LINK* link) { if (!link || !link->etcp || !link->etcp->instance) return; - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive %s send: mode=%s", - link->etcp->log_name, - code == ETCP_KEEPALIVE_REQ ? "REQ" : "RESP", - mode == KA_MODE_STANDBY ? "standby" : "normal"); - - struct ETCP_DGRAM* dgram = u_malloc(sizeof(struct ETCP_DGRAM) + 4); - if (!dgram) { - DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "malloc failed"); - return; - } + uint16_t period = ka_sleep_period_ms(link->etcp->instance); + link->ka_sleeping = 1; + link->ka_sleep_acked = 0; + link->ka_sleep_period_ms = period; + link->ka_period_ms = period; + link->keepalive_timeout = (uint32_t)period * KA_TIMEOUT_MULT; + link->ka_sleep_announce_end_tb = get_time_tb() + (uint64_t)KA_SLEEP_ANNOUNCE_TIMEOUT_MS * 10; + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] client entering sleep (period=%dms)", link->etcp->log_name, period); + ka_send_sleep_keepalive(link); + restart_keepalive_timer_ms(link, period); +} - dgram->link = link; - dgram->data[0] = code; - dgram->data[1] = mode; - dgram->data_len = 2; - dgram->noencrypt_len = 0; - dgram->timestamp = get_current_timestamp(); - dgram->flag_up = link->is_tcp ? 1 : link->recv_keepalive; +// Клиент: переанонс на входе в очередной цикл спячки (обновить дедлайн). +static void ka_client_reannounce(struct ETCP_LINK* link) { + if (!link->ka_sleeping) return; + link->ka_sleep_acked = 0; + link->ka_sleep_announce_end_tb = get_time_tb() + (uint64_t)KA_SLEEP_ANNOUNCE_TIMEOUT_MS * 10; + ka_send_sleep_keepalive(link); +} - etcp_encrypt_send(dgram); - u_free(dgram); +// Клиент: пробуждение (форграунд) — обычный keepalive, возврат к базовому периоду. +static void ka_client_wake(struct ETCP_LINK* link) { + if (!link || !link->etcp || !link->etcp->instance) return; + if (!link->ka_sleeping) return; + link->ka_sleeping = 0; + link->ka_sleep_acked = 0; + if (link->ka_sleep_announce_timer) { + uasync_cancel_timeout(link->etcp->instance->ua, link->ka_sleep_announce_timer); + link->ka_sleep_announce_timer = NULL; + } + link->ka_period_ms = (uint16_t)link->keepalive_interval; + link->keepalive_timeout = (uint32_t)link->keepalive_interval * KA_TIMEOUT_MULT; + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] client woke up, back to normal (interval=%dms)", + link->etcp->log_name, link->keepalive_interval); + etcp_link_send_keepalive(link); + restart_keepalive_timer_ms(link, link->ka_period_ms); } -static void handle_keepalive_req(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link) { - uint8_t req_mode = (pkt->data_len >= 2) ? pkt->data[1] : KA_MODE_NORMAL; - uint8_t agreed; - if (e_sock->instance->client_type == CLIENT_TYPE_MOBILE) { - uint8_t my_mode = mobile_desired_ka_mode(e_sock->instance); - agreed = (my_mode == KA_MODE_STANDBY || req_mode == KA_MODE_STANDBY) ? KA_MODE_STANDBY : KA_MODE_NORMAL; - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive REQ recv: req=%s my=%s -> agreed=%s", - link->etcp->log_name, - req_mode == KA_MODE_STANDBY ? "standby" : "normal", - my_mode == KA_MODE_STANDBY ? "standby" : "normal", - agreed == KA_MODE_STANDBY ? "standby" : "normal"); - } else { - agreed = req_mode; - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive REQ recv: req=%s (non-mobile) -> agreed=%s", - link->etcp->log_name, - req_mode == KA_MODE_STANDBY ? "standby" : "normal", - agreed == KA_MODE_STANDBY ? "standby" : "normal"); +typedef void (*ka_link_fn)(struct ETCP_LINK* link); + +static void ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) { + if (inst->connections) { + for (struct ll_entry* e = inst->connections->head; e; e = e->next) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (!ce || !ce->conn) continue; + for (struct ETCP_LINK* l = ce->conn->links; l; l = l->next) + if (l->initialized) fn(l); + } + } + if (inst->tcp_connections) { + for (struct ll_entry* e = inst->tcp_connections->head; e; e = e->next) { + struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; + if (!te || !te->etcp_conn) continue; + for (struct ETCP_LINK* l = te->etcp_conn->links; l; l = l->next) + if (l->initialized) fn(l); + } } - apply_link_ka_mode(link, agreed); - etcp_keepalive_send_mode_pkt(link, ETCP_KEEPALIVE_RESP, agreed); } -static void handle_keepalive_resp(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link) { - uint8_t agreed = (pkt->data_len >= 2) ? pkt->data[1] : KA_MODE_NORMAL; - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] keepalive RESP recv: agreed=%s", - link->etcp->log_name, agreed == KA_MODE_STANDBY ? "standby" : "normal"); - apply_link_ka_mode(link, agreed); +#ifdef UTUN_HAVE_STANDBY +// Фаза duty-cycle: вошли в SLEEP → переанонсировать спячку (каждый цикл). +static void ka_phase_cb(int sleeping, void* arg) { + struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; + if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return; + if (!sleeping) return; + ka_for_each_link(inst, ka_client_reannounce); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "keepalive: duty-cycle SLEEP phase, re-announce (node=%016llx)", + (unsigned long long)inst->node_id); } +#endif int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, size_t pkt_len) { @@ -258,30 +332,52 @@ int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, uint8_t code = pkt->data[0]; if (code == ETCP_KEEPALIVE) { - if (pkt->data_len >= 3) { - uint16_t peer_period = pkt->data[1] | ((uint16_t)pkt->data[2] << 8); - link->keepalive_timeout = (uint32_t)peer_period * KA_TIMEOUT_MULT; - } - link->keepalive_recv_count++; - /* Сервер в standby синхронизируется с keepalive клиента: отвечает echo и - * пере-армит свой таймер на период+запас, чтобы собственная проба ушла - * только если клиент опаздывает. */ - if (link->is_server && link->ka_pinned) { - etcp_link_send_keepalive(link); - restart_keepalive_timer_ms(link, (uint16_t)(link->ka_period_ms + KA_SYNC_MARGIN_MS)); + uint16_t peer_period = 0; + if (pkt->data_len >= 4) + peer_period = pkt->data[2] | ((uint16_t)pkt->data[3] << 8); + else if (pkt->data_len >= 3) /* легаси-формат (без флагов) */ + peer_period = pkt->data[1] | ((uint16_t)pkt->data[2] << 8); + if (peer_period) link->keepalive_timeout = (uint32_t)peer_period * KA_TIMEOUT_MULT; + + uint8_t flags = (pkt->data_len >= 2) ? pkt->data[1] : 0; + + if (flags & KA_FLAG_SLEEP) { + uint16_t sleep_time = (pkt->data_len >= 6) + ? (pkt->data[4] | ((uint16_t)pkt->data[5] << 8)) : 0; + if (link->is_server) { + /* Сервер: пир заснул — перейти в спячку и ответить ack-эхо. */ + link->ka_sleeping = 1; + link->ka_sleep_period_ms = peer_period; + link->ka_sleep_time = sleep_time; + link->ka_sleep_deadline_tb = get_time_tb() + (uint64_t)sleep_time * 1000; + link->ka_period_ms = peer_period; + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] server: peer sleeping (period=%dms deadline=%us), echo ack", + link->etcp->log_name, peer_period, (unsigned)(sleep_time / 10)); + etcp_link_send_keepalive(link); + restart_keepalive_timer_ms(link, peer_period); + } else { + /* Клиент: ack/эхо сервера — анонс завершён. */ + link->ka_sleep_acked = 1; + if (link->ka_sleep_announce_timer) { + uasync_cancel_timeout(e_sock->instance->ua, link->ka_sleep_announce_timer); + link->ka_sleep_announce_timer = NULL; + } + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] client: sleep ack received", link->etcp->log_name); + } + } else { + /* Обычный keepalive (без флага). */ + if (link->is_server && link->ka_sleeping) { + /* Пир проснулся (форграунд) — вернуться в обычный режим. */ + link->ka_sleeping = 0; + link->ka_period_ms = (uint16_t)link->keepalive_interval; + link->keepalive_timeout = (uint32_t)link->keepalive_interval * KA_TIMEOUT_MULT; + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[%s] server: peer woke up, back to normal (interval=%dms)", + link->etcp->log_name, link->keepalive_interval); + restart_keepalive_timer_ms(link, link->ka_period_ms); + } } - memory_pool_free(e_sock->instance->pkt_pool, pkt); - return 1; - } - - if (code == ETCP_KEEPALIVE_REQ) { - handle_keepalive_req(e_sock, pkt, link); - memory_pool_free(e_sock->instance->pkt_pool, pkt); - return 1; - } - if (code == ETCP_KEEPALIVE_RESP) { - handle_keepalive_resp(pkt, link); + link->keepalive_recv_count++; memory_pool_free(e_sock->instance->pkt_pool, pkt); return 1; } @@ -290,36 +386,27 @@ int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, } static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, void* arg) { - (void)active; (void)arg; + (void)arg; if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return; - uint8_t mode = mobile_desired_ka_mode(inst); - - int sent = 0; - if (inst->connections) { - for (struct ll_entry* e = inst->connections->head; e; e = e->next) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; - if (!ce || !ce->conn) continue; - for (struct ETCP_LINK* l = ce->conn->links; l; l = l->next) { - if (l->initialized) { etcp_keepalive_send_mode_pkt(l, ETCP_KEEPALIVE_REQ, mode); sent++; } - } - } - } - if (inst->tcp_connections) { - for (struct ll_entry* e = inst->tcp_connections->head; e; e = e->next) { - struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; - if (!te || !te->etcp_conn) continue; - for (struct ETCP_LINK* l = te->etcp_conn->links; l; l = l->next) { - if (l->initialized) { etcp_keepalive_send_mode_pkt(l, ETCP_KEEPALIVE_REQ, mode); sent++; } - } - } - } + ka_for_each_link(inst, active ? ka_client_wake : ka_client_enter_sleep); + DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity change: %s -> applied to links (node=%016llx)", + active ? "ACTIVE(wake)" : "STANDBY(sleep)", (unsigned long long)inst->node_id); +} - DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity change: mode=%s -> REQ sent to %d links (node=%016llx)", - mode == KA_MODE_STANDBY ? "standby" : "normal", sent, (unsigned long long)inst->node_id); +// Переанонс спячки на только что (пере)инициализированном линке (рестарт сервера). +void etcp_keepalive_announce_mode(struct ETCP_LINK* link) { + if (!link || !link->etcp || !link->etcp->instance) return; + struct UTUN_INSTANCE* inst = link->etcp->instance; + if (inst->client_type != CLIENT_TYPE_MOBILE) return; + if (!inst->standby_enabled) return; /* не в standby — обычный режим */ + ka_client_enter_sleep(link); } void etcp_keepalive_register(struct UTUN_INSTANCE* inst) { if (!inst) return; utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL); - DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity callback registered"); +#ifdef UTUN_HAVE_STANDBY + standby_set_phase_callback(ka_phase_cb, inst); +#endif + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity + phase callbacks registered"); } diff --git a/src/transport_layer/etcp_keepalive.h b/src/transport_layer/etcp_keepalive.h index e1966eae..afbfa5ff 100644 --- a/src/transport_layer/etcp_keepalive.h +++ b/src/transport_layer/etcp_keepalive.h @@ -5,25 +5,45 @@ extern "C" { #endif -// подмодуль ETCP: keepalive-пакеты, таймер, адаптивный период и протокол смены -// keepalive-режима (standby/normal) при смене active mode у мобильных узлов. +// подмодуль ETCP: keepalive-пакеты, таймер, адаптивный период и протокол +// спячки (sleep) на флагах самого keepalive-пакета. #include "etcp_connections.h" -// Типы кодограмм keepalive -#define ETCP_KEEPALIVE_REQ 0x09 // запрос смены keepalive-режима (data[1] = режим) -#define ETCP_KEEPALIVE_RESP 0x0A // ответ: согласованный режим (data[1] = режим) +/* ─── Протокол спячки поверх keepalive-пакета (code ETCP_KEEPALIVE = 0x08) ─── + * + * Режим спячки передаётся флагом в самом keepalive (отдельные кодограммы + * REQ/RESP не используются). Формат keepalive-пакета: + * + * data[0] = ETCP_KEEPALIVE (0x08) + * data[1] = flags (bit0 = KA_FLAG_SLEEP — спячка) + * data[2..3] = ka_period_ms (LE 16-bit) — период keepalive в спячке, мс + * data[4..5] = sleep_time (LE 16-bit, x0.1с) — дедлайн пробуждения (при KA_FLAG_SLEEP) + * + * Засыпание (клиент, перед входом в каждый цикл спячки duty-cycle): + * 1) шлёт keepalive с KA_FLAG_SLEEP + sleep_time. sleep_time = остаток ACTIVE + + * полная длительность SLEEP (дедлайн, после которого клиент ТОЧНО проснётся; + * может и раньше). В спячке при ретраях корректируется вниз до остатка SLEEP. + * 2) ретраит каждые KA_SLEEP_RETRY_MS, пока не получит keepalive с KA_FLAG_SLEEP + * (ack сервера); суммарно KA_SLEEP_ANNOUNCE_TIMEOUT_MS, после чего спит anyway. + * 3) сервер отвечает ответным keepalive с KA_FLAG_SLEEP (ack) и переводит период + * keepalive в спячку (ka_period_ms из пакета). + * + * В спячке (включая ACTIVE-фазу duty-cycle) клиент продолжает слать keepalive с + * KA_FLAG_SLEEP. Сервер отвечает echo на входящие keepalive, либо, если дедлайн + * sleep_time вышел — шлёт серию keepalive с обычным таймаутом, пока не получит + * keepalive со sleep_time (клиент переанонсировал) или обычный (клиент проснулся). + * + * Пробуждение: только выход в форграунд — клиент шлёт обычный keepalive (без + * KA_FLAG_SLEEP), сервер возвращается в обычный адаптивный режим с базовым периодом. + */ -// Режимы keepalive (мобильные узлы) -#define KA_MODE_NORMAL 0 // активен: интервал 3с, адаптивный -#define KA_MODE_STANDBY 1 // спит: интервал 30с, пиннится +// Флаги keepalive-пакета (data[1]) +#define KA_FLAG_SLEEP 0x01 // спячка: анонс (клиент) / ack-эхо (сервер) -#define KA_MOBILE_ACTIVE_MS 3000 -#define KA_MOBILE_STANDBY_MS 30000 - -/* Запас (мс) поверх периода keepalive: сервер в standby пере-армит свой таймер - * на период+запас после эхо-ответа, чтобы пробу успеть отправить при опоздании клиента. */ -#define KA_SYNC_MARGIN_MS 2000 +// Параметры анонса спячки +#define KA_SLEEP_RETRY_MS 200 // ретрай анонса спячки (клиент) +#define KA_SLEEP_ANNOUNCE_TIMEOUT_MS 4000 // суммарный таймаут анонса спячки // --- перенесены из etcp_connections.c (имена сохранены) --- uint16_t negotiate_keepalive(uint8_t my_type, uint16_t my_ka, @@ -32,15 +52,19 @@ void etcp_link_send_keepalive(struct ETCP_LINK* link); void start_keepalive_timer(struct ETCP_LINK* link); // --- новое --- -// Обработка keepalive-пакетов (ETCP_KEEPALIVE/REQ/RESP) из etcp_packet_decrypted. +// Обработка keepalive-пакетов (ETCP_KEEPALIVE) из etcp_packet_decrypted. // Возвращает 1 если пакет обработан (и освобождён), 0 — если это не keepalive. int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, size_t pkt_len); -// Подписка на смену active mode (рассылка KEEPALIVE_REQ всем линкам). +// Подписка на смену active mode (foreground/background) и фазы спячки duty-cycle. // Вызывается из init_connections. void etcp_keepalive_register(struct UTUN_INSTANCE* inst); +// Переанонсировать спячку на только что (пере)инициализированном линке. +// Вызывается из etcp_link_enter_ready_tcp / handle_init_response_client. +void etcp_keepalive_announce_mode(struct ETCP_LINK* link); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index f6376708..0fe0d296 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -46,6 +46,7 @@ class ConfigProvider(private val context: Context) { private val chatStandbyActiveSec = intPreferencesKey("chat_standby_active_sec") private val chatStandbySleepSec = intPreferencesKey("chat_standby_sleep_sec") private val chatStandbyMinSleepSec = intPreferencesKey("chat_standby_min_sleep_sec") + private val chatStandbyKeepaliveMs = intPreferencesKey("chat_standby_keepalive_ms") private val chatGroupAutoconnect = intPreferencesKey("chat_group_autoconnect") private val firstLaunchDone = booleanPreferencesKey("first_launch_done") private val connectionModeKey = stringPreferencesKey("connection_mode") @@ -131,6 +132,7 @@ class ConfigProvider(private val context: Context) { key == "chat.standby_active_sec" -> context.dataStore.data.first()[chatStandbyActiveSec] ?: 2 key == "chat.standby_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60 key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15 + key == "chat.standby_keepalive_ms" -> context.dataStore.data.first()[chatStandbyKeepaliveMs] ?: 30000 key == "chat.group_autoconnect" -> context.dataStore.data.first()[chatGroupAutoconnect] ?: 1 else -> 0 } @@ -162,6 +164,7 @@ class ConfigProvider(private val context: Context) { "chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2 "chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60 "chat.standby_min_sleep_sec" -> prefs[chatStandbyMinSleepSec] = value.toIntOrNull() ?: 15 + "chat.standby_keepalive_ms" -> prefs[chatStandbyKeepaliveMs] = value.toIntOrNull() ?: 30000 "chat.group_autoconnect" -> prefs[chatGroupAutoconnect] = value.toIntOrNull() ?: 1 } } @@ -442,6 +445,7 @@ class ConfigProvider(private val context: Context) { appendLine("standby_active_sec=${getInt("chat.standby_active_sec")}") appendLine("standby_sleep_sec=${getInt("chat.standby_sleep_sec")}") appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}") + appendLine("standby_keepalive_ms=${getInt("chat.standby_keepalive_ms")}") appendLine("group_autoconnect=${getInt("chat.group_autoconnect")}") appendLine() appendLine("[debug]") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 6fe4d239..909fb502 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -117,6 +117,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool var standbyActiveSec by remember { mutableStateOf("2") } var standbySleepSec by remember { mutableStateOf("60") } var standbyMinSleepSec by remember { mutableStateOf("15") } + var standbyKeepaliveMs by remember { mutableStateOf("30000") } val pubKey by vm.pubKey.collectAsState() val generatingKeys by vm.generatingKeys.collectAsState() @@ -147,6 +148,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool standbyActiveSec = provider.getInt("chat.standby_active_sec").toString() standbySleepSec = provider.getInt("chat.standby_sleep_sec").toString() standbyMinSleepSec = provider.getInt("chat.standby_min_sleep_sec").toString() + standbyKeepaliveMs = provider.getInt("chat.standby_keepalive_ms").toString() val port = logUdpPort.toIntOrNull() ?: 0 if (logUdpIp.isNotEmpty() && port > 0) NativeLib.setUdpLogTarget(logUdpIp, port) val stored = provider.getString("debug.categories") ?: "" @@ -345,6 +347,11 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool scope.launch { provider.setValue("chat.standby_min_sleep_sec", v) }; NativeLib.setChatSetting("standby_min_sleep_sec", v) } }, label = { Text("Min sleep (sec)") }, modifier = Modifier.heightIn(min = 40.dp), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true) + OutlinedTextField(standbyKeepaliveMs, + { v -> standbyKeepaliveMs = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1000..600000) { + scope.launch { provider.setValue("chat.standby_keepalive_ms", v) }; NativeLib.setChatSetting("standby_keepalive_ms", v) } }, + label = { Text("Keepalive in sleep (ms)") }, modifier = Modifier.heightIn(min = 40.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true) HorizontalDivider(Modifier.padding(vertical = 8.dp)) diff --git a/tools/chatgui-android/libutun_lite/standby.c b/tools/chatgui-android/libutun_lite/standby.c index e066a19c..d1cbd172 100644 --- a/tools/chatgui-android/libutun_lite/standby.c +++ b/tools/chatgui-android/libutun_lite/standby.c @@ -47,6 +47,8 @@ static uint64_t g_min_sleep_end_tb = 0; /* до этой точки спим б static void* g_phase_timer = NULL; static void (*g_switch_cb)(int enabled, void* arg) = NULL; static void* g_switch_arg = NULL; +static void (*g_phase_cb)(int sleeping, void* arg) = NULL; +static void* g_phase_cb_arg = NULL; static struct memory_pool* g_wait_pool = NULL; static struct standby_wait_entry* g_wait_head = NULL; @@ -126,10 +128,12 @@ static void standby_phase_timer_cb(void* arg) { "standby: SLEEP (active done in %d.%ds, sleep=%dms min_sleep=%dms)", (int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000), standby_sleep_tb() / 10, standby_min_sleep_tb() / 10); + if (g_phase_cb) g_phase_cb(1, g_phase_cb_arg); } else { g_cnt_wakes_timer++; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: WAKE (timer), slept %d.%ds", (int)(elapsed / STANDBY_TB_PER_SEC), (int)((elapsed % STANDBY_TB_PER_SEC) / 1000)); + if (g_phase_cb) g_phase_cb(0, g_phase_cb_arg); } standby_arm_phase(); } @@ -286,6 +290,28 @@ int standby_get_active_remaining_tb(void) { return (int)(g_phase_end_tb - now); } +int standby_get_sleep_duration_tb(void) { + return standby_sleep_tb(); +} + +int standby_get_wake_deadline_tb(void) { + if (!g_enabled) return 0; + if (g_phase == PHASE_SLEEP) return standby_get_sleep_tb(); + /* ACTIVE: остаток активного + полная длительность предстоящего сна */ + return standby_get_active_remaining_tb() + standby_sleep_tb(); +} + +int standby_get_keepalive_ms(void) { + int ms = chat_setting_get_int(g_inst, "standby_keepalive_ms", 30000); + if (ms < 100) ms = 100; + return ms; +} + +void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg) { + g_phase_cb = cb; + g_phase_cb_arg = arg; +} + void* standby_wait(void* arg, void (*cb)(void* arg)) { if (!cb) return NULL; if (!g_ua) { diff --git a/tools/chatgui-android/libutun_lite/standby.h b/tools/chatgui-android/libutun_lite/standby.h index 99084ef6..fd5ab241 100644 --- a/tools/chatgui-android/libutun_lite/standby.h +++ b/tools/chatgui-android/libutun_lite/standby.h @@ -39,6 +39,19 @@ int standby_get_sleep_tb(void); /* Сколько TB осталось активного режима (0 = спим или standby выключен). */ int standby_get_active_remaining_tb(void); +/* Полная длительность SLEEP-фазы duty-cycle (timebase 0.1мс). */ +int standby_get_sleep_duration_tb(void); + +/* Время до гарантированного пробуждения (timebase 0.1мс): остаток SLEEP, либо + * остаток ACTIVE + полная длительность SLEEP. 0 = standby выключен. */ +int standby_get_wake_deadline_tb(void); + +/* Период keepalive в спячке (мс), из настройки standby_keepalive_ms. */ +int standby_get_keepalive_ms(void); + +/* Колбэк смены фазы duty-cycle: sleeping=1 — вошли в SLEEP, 0 — вышли в ACTIVE. */ +void standby_set_phase_callback(void (*cb)(int sleeping, void* arg), void* arg); + /* Ждёт до начала следующего активного интервала (как uasync timeout). * При выходе из standby таймер отменяется и cb вызывается немедленно. * Возвращает handle для standby_wait_cancel(). */