chat: BGP-driven address sync, member identity verify, join_sig fix

- Адреса узлов вынесены из wire-протокола member_sync/merkle/join/welcome
  в BGP/topo_group: node_addresses теперь пишет только topo_node_sqlite_addrs_put
  (источник истины ni->v4/v6_addrs), классификация config_type/nat_type в sockmeta
- Жёсткая анти-подделка: node_id=derive(x25519), ed25519 сверяется с доверенной
  таблицей nodes, NODEINFO node_id/pubkey mismatch отклоняется как forgery
- Единые member_sync_build_join_msg (112Б) и member_sync_build_update_msg:
  устранён баг _verify_join_sig (join_ts по смещению 80 вместо 104, длина 104→112),
  который рушил приём мемберов; все 7 мест подписи/проверки join_sig через хелпер
- Единое обновление своего мембера chat_core_update_my_member (вместо sync_my_addresses)
- Android standby: пауза таймеров (broadcast/db_sync/merkle_sync/chat_sync) через standby_wait
- name[16] → name[MAX_CONN_NAME_LEN] с truncation-warning в парсере
- conn_mgr: etcp_connect_cancel_by_node (use-after-free при отмене invite),
  промоушен conn в ncd после успешного invite
- Android GUI: foreground-gating опросов, сохранение портов интерфейсов, фикс udp-log flush
- chatgui: персистентный db_path; мелкие DEBUG-логи epoll/pool
This commit is contained in:
evgeny committed 2026-08-15 12:36:05 +03:00
1 parent c07bd385ca
commit 5c2501b4f3
34 files changed
+808 -882

No files matched your search

@@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import com.utun.chat.data.Channel
import com.utun.chat.data.InviteData
import com.utun.chat.data.LogManager
@@ -36,6 +37,13 @@ class MainActivity : ComponentActivity() {
setContent {
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory())
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded,
refresh statuses on resume. */
LifecycleStartEffect(Unit) {
vm.setForeground(true)
onStopOrDispose { vm.setForeground(false) }
}
var screen by remember { mutableStateOf(Screen.ChannelList) }
var qrResult by remember { mutableStateOf<String?>(null) }
var showJoinDialog by remember { mutableStateOf(false) }
@@ -209,7 +209,7 @@ class ConfigProvider(private val context: Context) {
/* ── Server entries ── */
suspend fun detectInterfaces(): List<ServerEntry> {
suspend fun detectInterfaces(saved: List<ServerEntry> = emptyList()): List<ServerEntry> {
val servers = mutableListOf<ServerEntry>()
val seen = mutableSetOf<String>()
try {
@@ -224,14 +224,18 @@ class ConfigProvider(private val context: Context) {
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"}"
val isV6 = ip.contains(":")
val key = "${ni.name}/${if (isV6) "v6" else "v4"}"
if (!seen.add(key)) continue
val savedPort = saved.firstOrNull {
it.ifName == ni.name && it.bindIp.contains(":") == isV6
}?.port
servers.add(ServerEntry(
id = UUID.randomUUID().toString(),
ifName = ni.name,
bindIp = ip,
protocol = "tcp",
port = ThreadLocalRandom.current().nextInt(2000, 10000)
port = savedPort ?: ThreadLocalRandom.current().nextInt(2000, 10000)
))
}
}
@@ -395,7 +399,9 @@ class ConfigProvider(private val context: Context) {
/** Build full INI config text for NativeLib.start() */
fun buildConfigText(): String {
if (getConnectionMode() == "auto") {
val detected = runBlocking { detectInterfaces() }
val cfgFile = File(configFilePath())
val saved = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
val detected = runBlocking { detectInterfaces(saved) }
LogManager.addLog("INFO", "CFG", "buildConfigText: auto mode, detected ${detected.size} interfaces" +
detected.joinToString(", ") { "${it.ifName}=${it.protocol}" })
saveServersToConfig(detected)
@@ -86,6 +86,20 @@ class ChatViewModel : ViewModel() {
@Volatile private var realPlaybackElapsedMs = 0
private val _foreground = MutableStateFlow(false)
fun setForeground(fg: Boolean) {
_foreground.value = fg
if (fg) {
_isStuck.value = false
if (dbReady && repo != null) {
refreshChannels()
refreshLocalSockets()
_currentChannel.value?.let { refreshMessages(it.id); refreshMembers(it.id) }
}
}
}
init {
viewModelScope.launch {
AppEventHandler.events.collect { (type, data) -> handleEvent(type, data) }
@@ -104,37 +118,43 @@ class ChatViewModel : ViewModel() {
}
}
viewModelScope.launch {
while (isActive) {
delay(3000)
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
NativeLib.ping()
delay(100)
_isStuck.value = !NativeLib.isResponsive()
}
}
viewModelScope.launch {
while (isActive) {
if (_isRecording.value) {
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
_foreground.collectLatest { fg ->
while (fg && isActive) {
delay(3000)
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
NativeLib.ping()
delay(100)
_isStuck.value = !NativeLib.isResponsive()
}
delay(80)
}
}
viewModelScope.launch {
while (isActive) {
delay(50)
val s = _playbackState.value
if (s.playing && !s.paused) {
val real = realPlaybackElapsedMs
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
_foreground.collectLatest { fg ->
while (fg && isActive) {
if (_isRecording.value) {
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
}
delay(80)
}
}
}
viewModelScope.launch {
_foreground.collectLatest { fg ->
while (fg && isActive) {
delay(50)
val s = _playbackState.value
if (s.playing && !s.paused) {
val real = realPlaybackElapsedMs
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
_playbackState.value = PlaybackState()
realPlaybackElapsedMs = 0
} else {
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
_playbackState.value = PlaybackState()
realPlaybackElapsedMs = 0
} else {
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
}
}
}
}