refactor: TCP socket addresses — sync to chat DB, topo_node, config file single source
- chat: collect_my_addrs extends to TCP + IPv6, chat_core_sync_my_addresses writes TCP addrs - topo_node: replace CFG_SERVER with TCP_SOCKET for TCP addresses, simplify NAT logic - etcp_connections: add IPv6 interface addr resolution for TCP sockets - Android: INI config file as single source of truth for servers, remove JSON/Gson - Android: filter temporary IPv6 addresses, fix restart with socket cleanup - instance_lite: sync addresses after socket creation, proper restart cleanup
This commit is contained in:
1 parent
740ed2a93a
commit
ddf696bcf6
9 files changed
+338
-161
No files matched your search
@@ -35,10 +35,8 @@ class ChatApplication : Application() {
|
||||
/* First launch config */
|
||||
if (configProvider.isFirstLaunch()) {
|
||||
kotlinx.coroutines.runBlocking {
|
||||
LogManager.addLog("INFO", "APP", "First launch — configuring")
|
||||
configProvider.detectInterfaces()
|
||||
LogManager.addLog("INFO", "APP", "First launch — marking launched")
|
||||
configProvider.markLaunched()
|
||||
LogManager.addLog("INFO", "APP", "First launch done, ${configProvider.loadServers().size} interfaces detected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -19,6 +17,7 @@ import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
private val Context.dataStore by preferencesDataStore("utun_config")
|
||||
|
||||
@@ -35,7 +34,6 @@ class ConfigProvider(private val context: Context) {
|
||||
private val debugCategories = stringPreferencesKey("debug_categories")
|
||||
private val logUdpIp = stringPreferencesKey("log_udp_ip")
|
||||
private val logUdpPort = intPreferencesKey("log_udp_port")
|
||||
private val serversJson = stringPreferencesKey("servers")
|
||||
private val chatStorageAutoload = intPreferencesKey("chat_storage_autoload")
|
||||
private val chatStorageAutoloadMaxsizeMb = intPreferencesKey("chat_storage_autoload_maxsize_mb")
|
||||
private val chatStorageMaxsizeGb = intPreferencesKey("chat_storage_maxsize_gb")
|
||||
@@ -46,9 +44,6 @@ class ConfigProvider(private val context: Context) {
|
||||
private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers")
|
||||
private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
|
||||
|
||||
private val gson = Gson()
|
||||
private val random = java.util.concurrent.ThreadLocalRandom.current()
|
||||
|
||||
private fun keysFilePath(): String {
|
||||
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
|
||||
return "$dir/keys.conf"
|
||||
@@ -95,19 +90,6 @@ class ConfigProvider(private val context: Context) {
|
||||
|
||||
fun getString(key: String): String? = runBlocking {
|
||||
when {
|
||||
key.startsWith("servers.") -> {
|
||||
val parts = key.removePrefix("servers.").split(".", limit = 2)
|
||||
val idx = parts[0].toIntOrNull() ?: return@runBlocking null
|
||||
val field = parts.getOrNull(1) ?: return@runBlocking null
|
||||
val list = loadServers()
|
||||
val srv = list.getOrNull(idx) ?: return@runBlocking null
|
||||
when (field) {
|
||||
"bindIp" -> srv.bindIp
|
||||
"protocol" -> srv.protocol
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
key == "servers.count" -> loadServers().size.toString()
|
||||
key == "node.private_key" -> readKeysFromFile()?.second ?: context.dataStore.data.first()[nodePrivateKey]
|
||||
key == "node.public_key" -> readKeysFromFile()?.first ?: context.dataStore.data.first()[nodePublicKey]
|
||||
key == "node.name" -> context.dataStore.data.first()[nodeName] ?: getDeviceName()
|
||||
@@ -123,18 +105,6 @@ class ConfigProvider(private val context: Context) {
|
||||
|
||||
fun getInt(key: String): Int = runBlocking {
|
||||
when {
|
||||
key.startsWith("servers.") -> {
|
||||
val parts = key.removePrefix("servers.").split(".", limit = 2)
|
||||
val idx = parts[0].toIntOrNull() ?: return@runBlocking 0
|
||||
val field = parts.getOrNull(1) ?: return@runBlocking 0
|
||||
val list = loadServers()
|
||||
val srv = list.getOrNull(idx) ?: return@runBlocking 0
|
||||
when (field) {
|
||||
"port" -> srv.port
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
key == "servers.count" -> loadServers().size
|
||||
key == "node.listen_port" -> context.dataStore.data.first()[nodeListenPort] ?: 12345
|
||||
key == "control.port" -> context.dataStore.data.first()[controlPort] ?: 9999
|
||||
key == "log_udp.port" -> context.dataStore.data.first()[logUdpPort] ?: 9999
|
||||
@@ -191,6 +161,20 @@ class ConfigProvider(private val context: Context) {
|
||||
// fc00::/7 → fc00-fdff, 2000::/3 → 2000-3fff
|
||||
return hi in 0xFC00..0xFDFF || hi in 0x2000..0x3FFF
|
||||
}
|
||||
|
||||
// 0x20 = IFA_F_TEMPORARY в /proc/net/if_inet6
|
||||
fun isTemporaryV6(ifName: String, ip: String): Boolean {
|
||||
val hexAddr = ip.split("%")[0].replace(":", "").uppercase().padStart(32, '0')
|
||||
return try {
|
||||
File("/proc/net/if_inet6").useLines { lines ->
|
||||
lines.any { line ->
|
||||
val parts = line.trim().split("\\s+".toRegex(), limit = 6)
|
||||
parts.size >= 6 && parts[0] == hexAddr && parts[5] == ifName &&
|
||||
parts[4].toIntOrNull(16)?.let { (it and 0x20) != 0 } == true
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── First launch ── */
|
||||
@@ -207,6 +191,7 @@ class ConfigProvider(private val context: Context) {
|
||||
|
||||
suspend fun detectInterfaces(): List<ServerEntry> {
|
||||
val servers = mutableListOf<ServerEntry>()
|
||||
val seen = mutableSetOf<String>()
|
||||
try {
|
||||
val nis = NetworkInterface.getNetworkInterfaces()
|
||||
while (nis.hasMoreElements()) {
|
||||
@@ -216,70 +201,90 @@ class ConfigProvider(private val context: Context) {
|
||||
val addr = a.address
|
||||
val ip = when {
|
||||
addr is Inet4Address -> addr.hostAddress
|
||||
addr is Inet6Address && isUsableV6(addr) -> addr.hostAddress?.let { it.split("%")[0] }
|
||||
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
|
||||
else -> null
|
||||
} ?: continue
|
||||
val key = "${ni.name}/${if (ip.contains(":")) "v6" else "v4"}"
|
||||
if (!seen.add(key)) continue
|
||||
servers.add(ServerEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
ifName = ni.name,
|
||||
bindIp = ip,
|
||||
protocol = "tcp",
|
||||
port = random.nextInt(2000, 10000) // 2000-9999
|
||||
port = ThreadLocalRandom.current().nextInt(2000, 10000)
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CFG", "detectInterfaces failed: ${e.message}")
|
||||
}
|
||||
saveServers(servers)
|
||||
return servers
|
||||
}
|
||||
|
||||
suspend fun saveServers(servers: List<ServerEntry>) {
|
||||
val json = gson.toJson(servers)
|
||||
context.dataStore.edit { prefs -> prefs[serversJson] = json }
|
||||
/* ── Config file as single source of truth for servers ── */
|
||||
|
||||
fun configFilePath(): String {
|
||||
val dir = getString("db.path") ?: context.filesDir.absolutePath
|
||||
return "$dir/utun.cfg"
|
||||
}
|
||||
|
||||
fun loadServers(): List<ServerEntry> = runBlocking {
|
||||
val json = context.dataStore.data.first()[serversJson] ?: "[]"
|
||||
fun loadServersFromConfig(): List<ServerEntry> {
|
||||
val file = File(configFilePath())
|
||||
if (!file.exists()) return emptyList()
|
||||
data class RawSrv(val ifName: String, val family: String, val port: Int, val protocol: String)
|
||||
val raw = mutableListOf<RawSrv>()
|
||||
try {
|
||||
gson.fromJson(json, object : TypeToken<List<ServerEntry>>() {}.type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
val lines = file.readLines()
|
||||
var ifName = ""; var family = ""; var addrPort = 0; var proto = "tcp"; var sectionOpen = false
|
||||
for (line in lines) {
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.startsWith("[server:") && trimmed.endsWith("]")) {
|
||||
if (sectionOpen && ifName.isNotEmpty() && addrPort > 0)
|
||||
raw.add(RawSrv(ifName, family, addrPort, proto))
|
||||
val section = trimmed.removeSurrounding("[", "]").removePrefix("server:")
|
||||
val lastUnderscore = section.lastIndexOf('_')
|
||||
if (lastUnderscore > 0) {
|
||||
ifName = section.substring(0, lastUnderscore)
|
||||
family = section.substring(lastUnderscore + 1)
|
||||
} else { ifName = ""; family = "" }
|
||||
addrPort = 0; proto = "tcp"; sectionOpen = true
|
||||
} else if (trimmed.startsWith("addr=")) {
|
||||
val addr = trimmed.removePrefix("addr=")
|
||||
val colonIdx = addr.lastIndexOf(':')
|
||||
if (colonIdx >= 0) addrPort = addr.substring(colonIdx + 1).toIntOrNull() ?: 0
|
||||
} else if (trimmed.startsWith("transport=")) {
|
||||
proto = if (trimmed.removePrefix("transport=") == "udp") "udp" else "tcp"
|
||||
}
|
||||
}
|
||||
if (sectionOpen && ifName.isNotEmpty() && addrPort > 0)
|
||||
raw.add(RawSrv(ifName, family, addrPort, proto))
|
||||
} catch (_: Exception) { return emptyList() }
|
||||
|
||||
val liveIps = getAvailableInterfaces()
|
||||
return raw.map { rs ->
|
||||
val isV6 = rs.family == "v6"
|
||||
val realIp = liveIps.firstOrNull { (ifn, ip) ->
|
||||
ifn == rs.ifName && (if (isV6) ip.contains(":") else !ip.contains(":"))
|
||||
}?.second
|
||||
ServerEntry(UUID.randomUUID().toString(), rs.ifName,
|
||||
realIp ?: if (isV6) "::" else "0.0.0.0", rs.protocol, rs.port)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return all currently available network interface names (for dropdown) */
|
||||
fun getAvailableInterfaces(): List<Pair<String, String>> = runBlocking {
|
||||
val list = mutableListOf<Pair<String, String>>()
|
||||
try {
|
||||
val nis = NetworkInterface.getNetworkInterfaces()
|
||||
while (nis.hasMoreElements()) {
|
||||
val ni = nis.nextElement()
|
||||
if (!ni.isUp || ni.isLoopback) continue
|
||||
for (a in ni.interfaceAddresses) {
|
||||
val addr = a.address
|
||||
val ip = when {
|
||||
addr is Inet4Address -> addr.hostAddress
|
||||
addr is Inet6Address && isUsableV6(addr) -> addr.hostAddress?.let { it.split("%")[0] }
|
||||
else -> null
|
||||
} ?: continue
|
||||
list.add(ni.name to ip)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
if (list.isEmpty()) list.add("lo" to "127.0.0.1")
|
||||
list
|
||||
fun saveServersToConfig(servers: List<ServerEntry>) {
|
||||
val text = buildConfigFromServers(servers)
|
||||
File(configFilePath()).writeText(text)
|
||||
LogManager.addLog("INFO", "CFG", "saveServersToConfig: ${servers.size} servers written" +
|
||||
servers.joinToString(", ") { "${it.ifName}=${it.protocol}" })
|
||||
}
|
||||
|
||||
/** Build full INI config text for NativeLib.start() */
|
||||
fun buildConfigText(): String {
|
||||
/** Build complete INI text from server list (without touching config file) */
|
||||
private fun buildConfigFromServers(servers: List<ServerEntry>): String {
|
||||
val pubKey = getString("node.public_key") ?: ""
|
||||
val privKey = getString("node.private_key") ?: ""
|
||||
val nodeName = getString("node.name") ?: getDeviceName()
|
||||
val listenPort = getInt("node.listen_port")
|
||||
val dbDir = getString("db.path") ?: context.filesDir.absolutePath
|
||||
return buildString {
|
||||
val dbDir = getString("db.path") ?: context.filesDir.absolutePath
|
||||
return buildString {
|
||||
appendLine("[global]")
|
||||
if (nodeName.isNotBlank()) appendLine("my_node_name=$nodeName")
|
||||
if (pubKey.length == 64) appendLine("my_public_key=$pubKey")
|
||||
@@ -287,12 +292,15 @@ class ConfigProvider(private val context: Context) {
|
||||
appendLine("db_path=$dbDir")
|
||||
appendLine("db_sync_enabled=1")
|
||||
appendLine()
|
||||
appendLine("[server:main]")
|
||||
appendLine("addr=0.0.0.0:$listenPort")
|
||||
appendLine()
|
||||
appendLine("[server:main6]")
|
||||
appendLine("addr=[::]:$listenPort")
|
||||
appendLine()
|
||||
for (srv in servers) {
|
||||
val family = if (srv.bindIp.contains(":")) "v6" else "v4"
|
||||
appendLine("[server:${srv.ifName}_$family]")
|
||||
appendLine("addr=${if (family == "v6") "[::]" else "0.0.0.0"}:${srv.port}")
|
||||
appendLine("type=public")
|
||||
appendLine("transport=${srv.protocol}")
|
||||
if (family == "v6") appendLine("netif=${srv.ifName}")
|
||||
appendLine()
|
||||
}
|
||||
appendLine("[allowed_keys]")
|
||||
appendLine("allow_all=yes")
|
||||
appendLine()
|
||||
@@ -329,4 +337,42 @@ class ConfigProvider(private val context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return all currently available network interface names (for dropdown) */
|
||||
fun getAvailableInterfaces(): List<Pair<String, String>> = runBlocking {
|
||||
val list = mutableListOf<Pair<String, String>>()
|
||||
try {
|
||||
val nis = NetworkInterface.getNetworkInterfaces()
|
||||
while (nis.hasMoreElements()) {
|
||||
val ni = nis.nextElement()
|
||||
if (!ni.isUp || ni.isLoopback) continue
|
||||
for (a in ni.interfaceAddresses) {
|
||||
val addr = a.address
|
||||
val ip = when {
|
||||
addr is Inet4Address -> addr.hostAddress
|
||||
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
|
||||
else -> null
|
||||
} ?: continue
|
||||
list.add(ni.name to ip)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
if (list.isEmpty()) list.add("lo" to "127.0.0.1")
|
||||
list
|
||||
}
|
||||
|
||||
/** Build full INI config text for NativeLib.start() */
|
||||
fun buildConfigText(): String {
|
||||
val cfgFile = File(configFilePath())
|
||||
val existing = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
|
||||
LogManager.addLog("INFO", "CFG", "buildConfigText: exists=${cfgFile.exists()} srv=${existing.size}" +
|
||||
existing.joinToString(", ") { "${it.ifName}=${it.protocol}" })
|
||||
if (!cfgFile.exists() || existing.isEmpty()) {
|
||||
val detected = runBlocking { detectInterfaces() }
|
||||
LogManager.addLog("WARN", "CFG", "buildConfigText: auto-detecting ${detected.size} interfaces")
|
||||
saveServersToConfig(detected)
|
||||
return buildConfigFromServers(detected)
|
||||
}
|
||||
return buildConfigFromServers(existing)
|
||||
}
|
||||
}
|
||||
@@ -123,11 +123,11 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
if (logUdpIp.isNotEmpty() && port > 0) NativeLib.setUdpLogTarget(logUdpIp, port)
|
||||
val stored = provider.getString("debug.categories") ?: ""
|
||||
catLevels = if (stored.isNotEmpty()) stored.split(",").associate { it.split("=", limit = 2).let { (k, v) -> k to v } } else emptyMap()
|
||||
servers = provider.loadServers()
|
||||
servers = provider.loadServersFromConfig()
|
||||
availableIfaces = provider.getAvailableInterfaces()
|
||||
}
|
||||
|
||||
val save = { s: List<ServerEntry> -> onDirty(true); servers = s; scope.launch { provider.saveServers(s) } }
|
||||
val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) }
|
||||
val setDebug = { level: String ->
|
||||
onDirty(true); NativeLib.setDebugLevel("", level)
|
||||
scope.launch { provider.setValue("debug.console_level", level) }; debugLevel = level
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "sqlite3.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include "transport_layer/etcp_connections.h"
|
||||
#include "transport_layer/stcp_link.h"
|
||||
#include "../jni_bridge/android_udp_log.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
@@ -207,6 +209,9 @@ static void* instance_thread(void* arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* sockets are now created — sync my addresses to DB */
|
||||
chat_core_sync_my_addresses();
|
||||
|
||||
/* Bind chat sync via etcp_router */
|
||||
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
|
||||
|
||||
@@ -347,6 +352,14 @@ static void restart_trampoline(void* arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clean up old sockets so init_connections reads new config */
|
||||
while (g_inst->etcp_sockets) etcp_socket_remove(g_inst->etcp_sockets);
|
||||
g_inst->etcp_sockets = NULL;
|
||||
while (g_inst->tcp_sockets) tcp_socket_remove(g_inst->tcp_sockets);
|
||||
g_inst->tcp_sockets = NULL;
|
||||
if (g_inst->stcp_server) { stcp_link_server_destroy(g_inst->stcp_server); g_inst->stcp_server = NULL; }
|
||||
g_inst->socket_init_status = 0;
|
||||
|
||||
if (utun_instance_init(g_inst) != 0) {
|
||||
IL_LOGE("restart: utun_instance_init failed");
|
||||
g_running = 0;
|
||||
@@ -354,6 +367,8 @@ static void restart_trampoline(void* arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
chat_core_sync_my_addresses();
|
||||
|
||||
if (g_inst->config->global.name[0]) {
|
||||
chat_core_update_my_name(g_inst->config->global.name);
|
||||
IL_LOGI("restart: my_name set to '%s'", g_inst->config->global.name);
|
||||
|
||||
Reference in new issue
Block a user