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.
This commit is contained in:
Narasimha-sc committed 2026-10-05 20:57:31 +00:00
1 parent 680790695e
commit 8c11f93bb7
5 files changed
+32 -5

No files matched your search

@@ -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)
}
+4 -1
View File
@@ -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 {
+5 -1
View File
@@ -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)
}
@@ -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)
}
}
@@ -61,14 +61,17 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -
val networkTCPKeepIdle: MutableState<Int>
val networkTCPKeepIntvl: MutableState<Int>
val networkTCPKeepCnt: MutableState<Int>
val networkTCPUserTimeout: MutableState<Boolean>
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<Int>,
networkTCPKeepIntvl: MutableState<Int>,
networkTCPKeepCnt: MutableState<Int>,
networkTCPUserTimeout: MutableState<Boolean>,
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 = {},