From 8c11f93bb7ce9dfcedb4fdd05e8bda35d0c66062 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:57:31 +0000 Subject: [PATCH] android, ios, desktop: TCP_USER_TIMEOUT setting With the simplexmq change this depends on, a connection with unacknowledged data is failed after the keep-alive detection time instead of when the OS gives up retransmitting (~15 minutes), which is how long delivery keeps failing on a dead client when no network change is reported. It is off unless it is enabled, so that it can be tested before it becomes the default, and it is added to the keep-alive options in advanced network settings, next to the socket options it is derived from - it is removed when keep-alive is disabled, as the other options are. The option is not sent by the clients that do not have it, and the core treats it as off then. --- .../AdvancedNetworkSettings.swift | 5 +++++ apps/ios/SimpleXChat/APITypes.swift | 5 ++++- apps/ios/SimpleXChat/AppGroup.swift | 6 +++++- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 7 +++++-- .../networkAndServers/AdvancedNetworkSettings.kt | 14 +++++++++++++- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift index 74d38b050b..054f7fc6aa 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -235,11 +235,16 @@ struct AdvancedNetworkSettings: View { intSettingPicker("TCP_KEEPIDLE", selection: $keepAliveOpts.keepIdle, values: [15, 30, 60, 120, 180], label: secondsLabel) intSettingPicker("TCP_KEEPINTVL", selection: $keepAliveOpts.keepIntvl, values: [5, 10, 15, 30, 60], label: secondsLabel) intSettingPicker("TCP_KEEPCNT", selection: $keepAliveOpts.keepCnt, values: [1, 2, 4, 6, 8], label: "") + Toggle("TCP_USER_TIMEOUT", isOn: Binding( + get: { keepAliveOpts.unackedDataTimeout == true }, + set: { keepAliveOpts.unackedDataTimeout = $0 } + )) } else { Group { Text("TCP_KEEPIDLE") Text("TCP_KEEPINTVL") Text("TCP_KEEPCNT") + Text("TCP_USER_TIMEOUT") } .foregroundColor(theme.colors.secondary) } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 75d66ebb6f..a09518b860 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -438,8 +438,11 @@ public struct KeepAliveOpts: Codable, Equatable { public var keepIdle: Int // seconds public var keepIntvl: Int // seconds public var keepCnt: Int // times + // a connection with unacknowledged data gets no keep-alive probes, and the OS fails it only when it + // gives up retransmitting - ~15 minutes; when this is on it is failed after the keep-alive detection time + public var unackedDataTimeout: Bool? = nil - public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) + public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4, unackedDataTimeout: nil) } public struct NetworkProxy: Equatable, Codable { diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index d8543735b0..3084efe2c5 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -57,6 +57,7 @@ let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle" let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" +let GROUP_DEFAULT_NETWORK_TCP_UNACKED_DATA_TIMEOUT = "networkTCPUnackedDataTimeout" public let GROUP_DEFAULT_INCOGNITO = "incognito" let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" @@ -90,6 +91,7 @@ public let groupAppDefaults: [String: Any] = [ GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle, GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl, GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt, + GROUP_DEFAULT_NETWORK_TCP_UNACKED_DATA_TIMEOUT: KeepAliveOpts.defaults.unackedDataTimeout == true, GROUP_DEFAULT_INCOGNITO: false, GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, @@ -383,7 +385,8 @@ public func getNetCfg() -> NetCfg { let keepIdle = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE) let keepIntvl = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL) let keepCnt = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT) - tcpKeepAlive = KeepAliveOpts(keepIdle: keepIdle, keepIntvl: keepIntvl, keepCnt: keepCnt) + let unackedDataTimeout = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_TCP_UNACKED_DATA_TIMEOUT) + tcpKeepAlive = KeepAliveOpts(keepIdle: keepIdle, keepIntvl: keepIntvl, keepCnt: keepCnt, unackedDataTimeout: unackedDataTimeout) } else { tcpKeepAlive = nil } @@ -427,6 +430,7 @@ public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) { groupDefaults.set(tcpKeepAlive.keepIdle, forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE) groupDefaults.set(tcpKeepAlive.keepIntvl, forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL) groupDefaults.set(tcpKeepAlive.keepCnt, forKey: GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT) + groupDefaults.set(tcpKeepAlive.unackedDataTimeout == true, forKey: GROUP_DEFAULT_NETWORK_TCP_UNACKED_DATA_TIMEOUT) } else { groupDefaults.set(false, forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e3bea95c03..25285f0970 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -5398,11 +5398,14 @@ enum class TransportSessionMode { data class KeepAliveOpts( val keepIdle: Int, // seconds val keepIntvl: Int, // seconds - val keepCnt: Int // times + val keepCnt: Int, // times + // a connection with unacknowledged data gets no keep-alive probes, and the OS fails it only when it gives + // up retransmitting - ~15 minutes; when this is on it is failed after the keep-alive detection time instead + val unackedDataTimeout: Boolean? = null ) { companion object { val defaults: KeepAliveOpts = - KeepAliveOpts(keepIdle = 30, keepIntvl = 15, keepCnt = 4) + KeepAliveOpts(keepIdle = 30, keepIntvl = 15, keepCnt = 4, unackedDataTimeout = null) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 42746006a3..48d319b4fd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -61,14 +61,17 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - val networkTCPKeepIdle: MutableState val networkTCPKeepIntvl: MutableState val networkTCPKeepCnt: MutableState + val networkTCPUserTimeout: MutableState if (currentCfgVal.tcpKeepAlive != null) { networkTCPKeepIdle = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIdle) } networkTCPKeepIntvl = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIntvl) } networkTCPKeepCnt = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepCnt) } + networkTCPUserTimeout = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.unackedDataTimeout == true) } } else { networkTCPKeepIdle = remember { mutableStateOf(KeepAliveOpts.defaults.keepIdle) } networkTCPKeepIntvl = remember { mutableStateOf(KeepAliveOpts.defaults.keepIntvl) } networkTCPKeepCnt = remember { mutableStateOf(KeepAliveOpts.defaults.keepCnt) } + networkTCPUserTimeout = remember { mutableStateOf(KeepAliveOpts.defaults.unackedDataTimeout == true) } } fun buildCfg(): NetCfg { @@ -77,7 +80,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - val keepIdle = networkTCPKeepIdle.value val keepIntvl = networkTCPKeepIntvl.value val keepCnt = networkTCPKeepCnt.value - KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt) + KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt, unackedDataTimeout = networkTCPUserTimeout.value) } else { null } @@ -123,10 +126,12 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle networkTCPKeepIntvl.value = cfg.tcpKeepAlive.keepIntvl networkTCPKeepCnt.value = cfg.tcpKeepAlive.keepCnt + networkTCPUserTimeout.value = cfg.tcpKeepAlive.unackedDataTimeout == true } else { networkTCPKeepIdle.value = KeepAliveOpts.defaults.keepIdle networkTCPKeepIntvl.value = KeepAliveOpts.defaults.keepIntvl networkTCPKeepCnt.value = KeepAliveOpts.defaults.keepCnt + networkTCPUserTimeout.value = KeepAliveOpts.defaults.unackedDataTimeout == true } } @@ -180,6 +185,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - networkTCPKeepIdle, networkTCPKeepIntvl, networkTCPKeepCnt, + networkTCPUserTimeout, updateSessionMode = { sessionMode.value = it; currentCfg.value = currentCfg.value.copy(sessionMode = it) }, updateSMPProxyMode = { smpProxyMode.value = it; currentCfg.value = currentCfg.value.copy(smpProxyMode = it) }, updateSMPProxyFallback = { smpProxyFallback.value = it; currentCfg.value = currentCfg.value.copy(smpProxyFallback = it) }, @@ -215,6 +221,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - networkTCPKeepIdle: MutableState, networkTCPKeepIntvl: MutableState, networkTCPKeepCnt: MutableState, + networkTCPUserTimeout: MutableState, updateSessionMode: (TransportSessionMode) -> Unit, updateSMPProxyMode: (SMPProxyMode) -> Unit, updateSMPProxyFallback: (SMPProxyFallback) -> Unit, @@ -318,6 +325,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - SectionItemView { IntSettingRow("TCP_KEEPCNT", networkTCPKeepCnt, listOf(1, 2, 4, 6, 8), "") } + PreferenceToggle("TCP_USER_TIMEOUT", checked = networkTCPUserTimeout.value) { networkTCPUserTimeout.value = it } } else { SectionItemView { Text("TCP_KEEPIDLE", color = MaterialTheme.colors.secondary) @@ -328,6 +336,9 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - SectionItemView { Text("TCP_KEEPCNT", color = MaterialTheme.colors.secondary) } + SectionItemView { + Text("TCP_USER_TIMEOUT", color = MaterialTheme.colors.secondary) + } } } } @@ -595,6 +606,7 @@ fun PreviewAdvancedNetworkSettingsLayout() { networkTCPKeepIdle = remember { mutableStateOf(10) }, networkTCPKeepIntvl = remember { mutableStateOf(10) }, networkTCPKeepCnt = remember { mutableStateOf(10) }, + networkTCPUserTimeout = remember { mutableStateOf(false) }, updateSessionMode = {}, updateSMPProxyMode = {}, updateSMPProxyFallback = {},