diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index 60ce8866..619a3d8f 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -35,6 +35,12 @@ listen-сокетов, мониторинга, TUN и всех `route_subnet` п Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных. +`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует +ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI. +Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему +должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не +по успешному созданию потока; после события передаётся свежий снимок адресов. + | Владелец | Ресурсы | |---|---| | Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг | diff --git a/doc/tasks.md b/doc/tasks.md index 9dfdc82b..406f680d 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -4,6 +4,13 @@ ## Текущие задачи +[ ] **Android: отмена асинхронного запуска до готовности ядра** — при проверке + мониторинга 12.10.2026 обнаружено: `NativeLib.stop()` пропускает native stop, + если `isReady()==false`, а `instance_lite_stop()` возвращается при `g_inst==NULL`. + Поэтому запрос stop до завершения конструктора может не отменить запускаемый + поток. Согласовать отмену с состоянием потока и единым сигналом завершения; + проверить stop до SERVICE_STARTED, отказ конструктора и последующий start. + [ ] **Android: повторные отказы persisted node snapshots** — замечено 12.10.2026 на устройстве D2AR10P3600820 при проверке live media. В сохранённой БД ряд узлов регулярно получает `invalid persisted snapshot` в `topo_node_sqlite_snapshot_load`, diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c index 3175c07e..7053ec33 100644 --- a/src/transport_layer/auto_socket.c +++ b/src/transport_layer/auto_socket.c @@ -1187,8 +1187,7 @@ static void auto_socket_netlink_cb(int fd, void* arg) { static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { as->nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (as->nl_sock < 0) { - DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] netlink socket: %s — relying on JNI/scan_all for updates", strerror(errno)); - return 0; + DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink socket failed: %s", strerror(errno)); return -1; } struct sockaddr_nl nladdr; @@ -1196,12 +1195,15 @@ static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { nladdr.nl_family = AF_NETLINK; nladdr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR | RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE; if (bind(as->nl_sock, (struct sockaddr*)&nladdr, sizeof(nladdr)) < 0) { - DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] netlink bind: %s — relying on JNI/scan_all for updates", strerror(errno)); - close(as->nl_sock); as->nl_sock = -1; return 0; + DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink bind failed: %s", strerror(errno)); + close(as->nl_sock); as->nl_sock = -1; return -1; + } + if (socket_set_nonblocking(as->nl_sock) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink nonblocking mode failed: %s", strerror(errno)); + close(as->nl_sock); as->nl_sock = -1; return -1; } - socket_set_nonblocking(as->nl_sock); as->uasync_handle = uasync_add_socket(as->instance->ua, as->nl_sock, auto_socket_netlink_cb, NULL, NULL, "netlink_autosock", as); - if (!as->uasync_handle) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] uasync_add_socket failed"); close(as->nl_sock); as->nl_sock = -1; return 0; } + if (!as->uasync_handle) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] uasync_add_socket failed"); close(as->nl_sock); as->nl_sock = -1; return -1; } DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] netlink monitor started: fd=%d", (int)as->nl_sock); return 0; } @@ -1303,6 +1305,11 @@ int auto_socket_init(struct UTUN_INSTANCE* inst) { struct AUTO_SOCKET* as = u_calloc(1, sizeof(*as)); if (!as) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] alloc failed"); return -1; } +#ifdef __linux__ + as->nl_sock = -1; +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + as->route_sock = -1; +#endif as->instance = inst; as->mode = (uint8_t)inst->config->global.auto_sockets; inst->auto_socket_state = as; @@ -1343,7 +1350,7 @@ void auto_socket_destroy(struct UTUN_INSTANCE* inst) { struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)inst->auto_socket_state; inst->auto_socket_state = NULL; - auto_socket_destroy_monitor(as); + if (as->mode != CFG_AUTO_SOCKETS_ANDROID) auto_socket_destroy_monitor(as); while (as->ifaces) { struct auto_sock_iface* ifa = as->ifaces; diff --git a/src/transport_layer/socket_monitor.c b/src/transport_layer/socket_monitor.c index 5260ccd8..8f4c72e4 100644 --- a/src/transport_layer/socket_monitor.c +++ b/src/transport_layer/socket_monitor.c @@ -413,6 +413,10 @@ static void WINAPI route_change_cb(PVOID ctx, PMIB_IPFORWARD_ROW2 row, MIB_NOTIF int socket_monitor_init(struct UTUN_INSTANCE* instance) { if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "instance is NULL"); return -1; } if (instance->socket_monitor) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket monitor already initialized"); return -1; } + if (instance->config && instance->config->global.auto_sockets == CFG_AUTO_SOCKETS_ANDROID) { + DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "Socket monitoring delegated to external network callbacks (auto_sockets=android)"); + return 0; + } DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "initializing socket monitor"); #ifdef __linux__ diff --git a/src/transport_layer/socket_monitor.h b/src/transport_layer/socket_monitor.h index cb357566..43280f44 100644 --- a/src/transport_layer/socket_monitor.h +++ b/src/transport_layer/socket_monitor.h @@ -12,6 +12,7 @@ extern "C" { struct UTUN_INSTANCE; struct ETCP_SOCKET; +/* В режиме auto_sockets=android адресами управляет frontend; нативный монитор не создаётся. */ int socket_monitor_init(struct UTUN_INSTANCE* instance); void socket_monitor_destroy(struct UTUN_INSTANCE* instance); diff --git a/tests/Makefile.am b/tests/Makefile.am index 4f142a1a..0987f8ff 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -832,7 +832,8 @@ test_services_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_tun_route_SOURCES = test_tun_route.c test_tun_route_LDADD = $(LIBUTUN) $(COMMON_LIBS) -test_tun_route_LDFLAGS = -Wl,--wrap=sendto -Wl,--wrap=recvfrom -Wl,--wrap=poll -Wl,--wrap=tun_platform_init +test_tun_route_LDFLAGS = -Wl,--wrap=sendto -Wl,--wrap=recvfrom -Wl,--wrap=poll -Wl,--wrap=tun_platform_init \ + -Wl,--wrap=socket -Wl,--wrap=bind test_member_files_SOURCES = test_member_files.c test_member_files_CFLAGS = $(test_services_CFLAGS) diff --git a/tests/test_tun_route.c b/tests/test_tun_route.c index 9621a615..fc8d6471 100644 --- a/tests/test_tun_route.c +++ b/tests/test_tun_route.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -12,6 +14,8 @@ #include "tun_route.h" #include "tun_if.h" #include "chat/chat_core.h" +#include "transport_layer/auto_socket.h" +#include "transport_layer/etcp_connections.h" #include "../lib/debug_config.h" #include "../lib/mem.h" @@ -20,6 +24,19 @@ static uint32_t added[8], removed[8], request_seq; static uint16_t request_cmd; static int ack_error; static int fail_tun; +static int deny_netlink, netlink_calls; + +int __real_socket(int, int, int); +int __wrap_socket(int domain, int type, int protocol) { + if (domain == AF_NETLINK) netlink_calls++; + return __real_socket(domain, type, protocol); +} + +int __real_bind(int, const struct sockaddr*, socklen_t); +int __wrap_bind(int fd, const struct sockaddr* addr, socklen_t len) { + if (deny_netlink && addr->sa_family == AF_NETLINK) { errno = EACCES; return -1; } + return __real_bind(fd, addr, len); +} int __real_tun_platform_init(struct tun_if*, const char*, const char*, int); int __wrap_tun_platform_init(struct tun_if* tun, const char* name, const char* ip, int mtu) { @@ -135,6 +152,7 @@ static void check_mock(void) { /* Ошибка второго listen-сокета отклоняет весь запуск, включая начальный auto_sockets=android. */ static void check_socket_failure(int automatic) { + int stdin_flags = fcntl(STDIN_FILENO, F_GETFD); assert(stdin_flags >= 0); int blocker = socket(AF_INET, SOCK_STREAM, 0); assert(blocker >= 0); struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; assert(bind(blocker, (struct sockaddr*)&address, sizeof(address)) == 0 && listen(blocker, 1) == 0); @@ -147,9 +165,38 @@ static void check_socket_failure(int automatic) { struct UASYNC* ua = uasync_create(); assert(ua); assert(!utun_instance_create_from_str(ua, config)); uasync_drain_immediate(ua); uasync_destroy(ua, 0); close(blocker); + assert(fcntl(STDIN_FILENO, F_GETFD) == stdin_flags); DEBUG_INFO(DEBUG_CATEGORY_TUN, "PASS second socket failure rejects startup and cleans opened sockets automatic=%d", automatic); } +/* Android/JNI backend запускается без разрешений netlink и принимает свежие адреса без рестарта. */ +static void check_external_monitor(void) { + const char* config = "[global]\ntun_enabled=no\nauto_sockets=android\n" + "[server:external]\naddr=127.0.0.1:0\ntransport=udp\ntype=private\n" + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"; + int stdin_flags = fcntl(STDIN_FILENO, F_GETFD); assert(stdin_flags >= 0); + struct UASYNC* ua = uasync_create(); assert(ua); + deny_netlink = 1; netlink_calls = 0; + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); + assert(inst && !inst->socket_monitor && inst->auto_socket_state && !netlink_calls); + inst->config->global.db_sync_enabled = 0; + assert(utun_core_start(inst) == 0 && chat_service_start(inst) == 0); + sqlite3* db = inst->topo_sqlite_db; void* chat = inst->chat_core; + struct utun_config* updated = parse_config_from_buf(config, strlen(config), "external-update"); assert(updated); + ((struct sockaddr_in*)&updated->servers->ip)->sin_addr.s_addr = htonl(0x7f000002); + auto_socket_reload_config(inst, updated); + assert(inst->core_started && inst->chat_core == chat && inst->topo_sqlite_db == db); + assert(((struct sockaddr_in*)&inst->etcp_sockets->local_addr)->sin_addr.s_addr == htonl(0x7f000002)); + assert(!netlink_calls); + destroy(inst, ua); assert(fcntl(STDIN_FILENO, F_GETFD) == stdin_flags); + DEBUG_INFO(DEBUG_CATEGORY_TUN, "PASS external monitoring: no netlink, core/chat ready, address update preserves DB and fd=0"); + const char* native = "[global]\ntun_enabled=no\n[server:local]\naddr=127.0.0.1:0\ntype=private\n"; + ua = uasync_create(); assert(ua); + assert(!utun_instance_create_from_str(ua, native) && netlink_calls > 0); + uasync_drain_immediate(ua); uasync_destroy(ua, 0); deny_netlink = 0; + DEBUG_INFO(DEBUG_CATEGORY_TUN, "PASS native monitoring failure remains fatal"); +} + static void check_control_failure(void) { int blocker = socket(AF_INET, SOCK_STREAM, 0); assert(blocker >= 0); struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; @@ -189,6 +236,6 @@ int main(int argc, char** argv) { utun_instance_set_tun_init_enabled(1); size_t baseline = u_get_allocated_count(); if (argc == 2 && !strcmp(argv[1], "--real")) check_real(); - else { check_mock(); check_socket_failure(0); check_socket_failure(1); check_control_failure(); } + else { check_mock(); check_socket_failure(0); check_socket_failure(1); check_control_failure(); check_external_monitor(); } assert(u_get_allocated_count() == baseline); return 0; } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt index 68bf3e93..112a5aba 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt @@ -31,6 +31,7 @@ object AppEventHandler : ChatEventCallback { val events: SharedFlow> = _events override fun onEvent(type: Int, data: ByteArray?) { + if (type == 12) mainHandler.post { com.utun.chat.headless.HeadlessService.onCoreStarted() } if (type == 13 || type in 49..50) mainHandler.post { ChatApplication.instance.memberFiles.onEvent(type, data) } if (type in listOf(4, 8, 9, 12, 13, 23, 24, 25, 31) || type in 49..53) mainHandler.post { ChatApplication.instance.avatars.onEvent(type, data) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt index 9943e6e8..5834d93a 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NetworkChangeMonitor.kt @@ -7,93 +7,99 @@ import android.os.Handler import android.os.Looper import com.utun.chat.ChatApplication -/** - * Слушатель смены сети (ConnectivityManager.NetworkCallback). - * - * На Android нативный netlink-мониторинг сокетов недоступен (Permission denied), - * поэтому о смене сети сообщает ConnectivityManager. При смене default-сети - * (mobile↔wifi, смена IP) пересобираем конфиг сокетов и перезапускаем C-ядро, - * чтобы оно забиндилось на актуальный интерфейс. - * - * Регистрируется в HeadlessService (foreground) — работает в фоне. - */ +/** ConnectivityManager передаёт актуальный конфиг сокетов в uasync без рестарта ядра. + * Регистрация живёт в HeadlessService; состояние очереди обновлений — на main thread. */ object NetworkChangeMonitor { private var started = false - - /* Пропуск первого onAvailable: он приходит сразу при регистрации с текущим - * состоянием сети, а не как реальное событие смены. */ - private var primed = false - - /* Рестарт в процессе — защита от наложения (C-ядро тоже имеет CAS-гарды). */ - private var restarting = false - - /* Последний известный набор интерфейсов (ifName=ip) для лога «было → стало». */ + private var generation = 0 + private var updating = false + private var pending = false private var lastInterfaces: List>? = null - private const val DEBOUNCE_MS = 500L private val handler = Handler(Looper.getMainLooper()) - private val restartRunnable = Runnable { doRestart() } + private val updateRunnable = Runnable { updateSockets() } private val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - if (!primed) { primed = true; return } - schedule() - } - - override fun onLost(network: Network) { - if (!primed) return - schedule() - } - - override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - if (!primed) return - schedule() - } + override fun onAvailable(network: Network) = schedule() + override fun onLost(network: Network) = schedule() + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) = schedule() } private fun schedule() { - handler.removeCallbacks(restartRunnable) - handler.postDelayed(restartRunnable, DEBOUNCE_MS) + handler.post { + if (!started) return@post + handler.removeCallbacks(updateRunnable) + handler.postDelayed(updateRunnable, DEBOUNCE_MS) + } } - private fun doRestart() { - if (restarting || !NativeLib.isReady()) return - restarting = true + private fun updateSockets() { + if (!started || !NativeLib.isReady()) return + if (updating) { pending = true; return } + updating = true + val token = generation Thread { + var interfaces: List>? = null + var config: String? = null try { val provider = ChatApplication.instance.configProvider - val now = provider.getAvailableInterfaces() - val prev = lastInterfaces - lastInterfaces = now - val prevStr = prev?.joinToString(", ") { (n, ip) -> "$n=$ip" } ?: "n/a" - val nowStr = now.joinToString(", ") { (n, ip) -> "$n=$ip" } - LogManager.addLog("INFO", "NET", "network change: [$prevStr] -> [$nowStr]") - NativeLib.notifyNetworkActive() - NativeLib.updateSockets(provider.buildConfigText()) + interfaces = provider.getAvailableInterfaces() + config = provider.buildConfigText() + } catch (e: Exception) { + LogManager.addLog("ERROR", "NET", "network config update failed: ${e.message}") } finally { - restarting = false + val current = interfaces + val text = config + handler.post { + if (!started || token != generation) return@post + updating = false + if (text != null && current != null && NativeLib.isReady()) { + val previous = lastInterfaces?.joinToString(", ") { (n, ip) -> "$n=$ip" } ?: "n/a" + val next = current.joinToString(", ") { (n, ip) -> "$n=$ip" } + lastInterfaces = current + LogManager.addLog("INFO", "NET", "network change: [$previous] -> [$next]") + NativeLib.notifyNetworkActive() + if (!NativeLib.updateSockets(text)) LogManager.addLog("ERROR", "NET", "socket update rejected") + } + if (pending) { pending = false; schedule() } + } } }.start() } - fun start() { - if (started) return - val ctx = ChatApplication.instance.applicationContext - val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return - /* Если сети ещё нет — первый onAvailable будет реальным событием. */ - primed = cm.activeNetwork == null - cm.registerDefaultNetworkCallback(callback) + // Повторный снимок после SERVICE_STARTED покрывает изменения сети во время инициализации. + fun onCoreStarted() = schedule() + + fun start(): Boolean { + if (started) return true + val cm = ChatApplication.instance.getSystemService(ConnectivityManager::class.java) + if (cm == null) { LogManager.addLog("ERROR", "NET", "ConnectivityManager unavailable"); return false } + try { + cm.registerDefaultNetworkCallback(callback) + } catch (e: Exception) { + LogManager.addLog("ERROR", "NET", "network listener registration failed: ${e.message}") + return false + } + generation++ started = true LogManager.addLog("INFO", "NET", "network change listener started") + return true } fun stop() { if (!started) return - val ctx = ChatApplication.instance.applicationContext - val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return - cm.unregisterNetworkCallback(callback) - handler.removeCallbacks(restartRunnable) started = false + generation++ + pending = false + updating = false + lastInterfaces = null + handler.removeCallbacks(updateRunnable) + val cm = ChatApplication.instance.getSystemService(ConnectivityManager::class.java) + try { + cm?.unregisterNetworkCallback(callback) + } catch (e: Exception) { + LogManager.addLog("WARN", "NET", "network listener removal failed: ${e.message}") + } LogManager.addLog("INFO", "NET", "network change listener stopped") } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt index 3468072c..cd57d895 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt @@ -22,6 +22,13 @@ class HeadlessService : Service() { companion object { private var currentService: HeadlessService? = null + fun onCoreStarted() { + if (currentService == null || !NativeLib.isReady()) return + LogManager.addLog("INFO", "SRV", "uTun core started") + NetworkActivityMonitor.start() + NetworkChangeMonitor.onCoreStarted() + } + fun refreshLanguage() { currentService?.let { service -> service.createNotificationChannel() @@ -45,18 +52,19 @@ class HeadlessService : Service() { val dbDir = provider.getString("db.path") ?: filesDir.absolutePath val listenPort = provider.getInt("node.listen_port") - val configText = provider.buildConfigText() - LogManager.addLog("INFO", "SRV", "uTun service starting, port=$listenPort db=$dbDir") NativeLib.init(dbDir, 9999) - val ok = NativeLib.start(configText) + if (!NetworkChangeMonitor.start()) { + LogManager.addLog("ERROR", "SRV", "uTun startup rejected: network monitoring unavailable") + return + } + val ok = NativeLib.start(provider.buildConfigText()) if (ok) { - LogManager.addLog("INFO", "SRV", "uTun core started") - NetworkActivityMonitor.start() - NetworkChangeMonitor.start() + LogManager.addLog("INFO", "SRV", "uTun startup requested") } else { + NetworkChangeMonitor.stop() LogManager.addLog("ERROR", "SRV", "uTun start failed") } } diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 58204761..6c50ce30 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -284,6 +284,7 @@ static void* instance_thread(void* arg) { my_inst = g_inst; if (!g_inst) { IL_LOGE("utun_instance_create_from_config failed"); + free_config(config); standby_deinit(); udp_log_detach(g_ua); uasync_destroy(g_ua, 0); @@ -384,7 +385,7 @@ static void* instance_thread(void* arg) { g_inst = utun_instance_create_from_config(g_ua, config); my_inst = g_inst; - if (!g_inst) { IL_LOGE("poll exit: create_from_config failed"); break; } + if (!g_inst) { IL_LOGE("poll exit: create_from_config failed"); free_config(config); break; } chat_event_set_handler(g_inst, chat_event_forward); /* Recreate core and chat independently of UTUN. */