Validate saved Android socket ports before reuse
This commit is contained in:
1 parent
080c7a5fd5
commit
7f0c080492
3 files changed
+149
-40
No files matched your search
@@ -20,7 +20,6 @@ import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
private val Context.dataStore by preferencesDataStore("utun_config")
|
||||
|
||||
@@ -633,54 +632,28 @@ class ConfigProvider(private val context: Context) {
|
||||
return "$dir/utun.cfg"
|
||||
}
|
||||
|
||||
/** Читает проверенные listen-секции; IP берёт с текущего интерфейса либо оставляет wildcard для отсутствующего. */
|
||||
fun loadServersFromConfig(includeCommented: Boolean = false): 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 {
|
||||
var ifName = ""; var family = ""; var addrPort = 0; var proto = "tcp"
|
||||
var sectionOpen = false; var sectionCommented = false
|
||||
for (line in file.readLines()) {
|
||||
val trimmed = line.trim()
|
||||
val commented = trimmed.startsWith("#")
|
||||
val body = if (commented) trimmed.removePrefix("#").trim() else trimmed
|
||||
if (body.startsWith("[") && body.endsWith("]")) {
|
||||
if (sectionOpen && ifName.isNotEmpty() && addrPort > 0 && (!sectionCommented || includeCommented))
|
||||
raw.add(RawSrv(ifName, family, addrPort, proto))
|
||||
sectionOpen = false; sectionCommented = commented; addrPort = 0; proto = "tcp"; ifName = ""; family = ""
|
||||
if (!body.startsWith("[server:")) continue
|
||||
if (commented && !includeCommented) continue
|
||||
val section = body.removeSurrounding("[", "]").removePrefix("server:").removeSuffix("_tcp").removeSuffix("_udp")
|
||||
val lastUnderscore = section.lastIndexOf('_')
|
||||
if (lastUnderscore > 0) {
|
||||
ifName = section.substring(0, lastUnderscore)
|
||||
family = section.substring(lastUnderscore + 1)
|
||||
}
|
||||
sectionOpen = true
|
||||
} else if (sectionOpen && body.startsWith("addr=")) {
|
||||
val addr = body.removePrefix("addr=")
|
||||
val colonIdx = addr.lastIndexOf(':')
|
||||
if (colonIdx >= 0) addrPort = addr.substring(colonIdx + 1).toIntOrNull() ?: 0
|
||||
} else if (sectionOpen && body.startsWith("transport=")) {
|
||||
proto = if (body.removePrefix("transport=") == "udp") "udp" else "tcp"
|
||||
val saved = try {
|
||||
file.bufferedReader().use { reader ->
|
||||
ServerConfig.parse(reader.lineSequence(), includeCommented) { warning ->
|
||||
LogManager.addLog("WARN", "CFG", warning)
|
||||
}
|
||||
}
|
||||
if (sectionOpen && ifName.isNotEmpty() && addrPort > 0 && (!sectionCommented || includeCommented))
|
||||
raw.add(RawSrv(ifName, family, addrPort, proto))
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CFG", "loadServersFromConfig failed path=${file.path}: ${e.message}")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
if (saved.isEmpty()) return emptyList()
|
||||
val liveIps = getAvailableInterfaces()
|
||||
return raw.map { rs ->
|
||||
val isV6 = rs.family == "v6"
|
||||
return saved.map { server ->
|
||||
val isV6 = server.bindIp.contains(':')
|
||||
val realIp = liveIps.firstOrNull { (ifn, ip) ->
|
||||
ifn == rs.ifName && (if (isV6) ip.contains(":") else !ip.contains(":"))
|
||||
ifn == server.ifName && ip.contains(':') == isV6
|
||||
}?.second
|
||||
ServerEntry(UUID.randomUUID().toString(), rs.ifName,
|
||||
realIp ?: if (isV6) "::" else "0.0.0.0", rs.protocol, rs.port)
|
||||
server.copy(bindIp = realIp ?: server.bindIp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +776,7 @@ class ConfigProvider(private val context: Context) {
|
||||
sb.appendLine()
|
||||
}
|
||||
|
||||
/** Return all currently available network interface names (for dropdown) */
|
||||
/** Синхронный список (интерфейс, IP) для UI/конфига; ошибка и fallback на loopback видны в CFG-логе. */
|
||||
fun getAvailableInterfaces(): List<Pair<String, String>> = runBlocking {
|
||||
val list = mutableListOf<Pair<String, String>>()
|
||||
try {
|
||||
@@ -821,8 +794,13 @@ class ConfigProvider(private val context: Context) {
|
||||
list.add(ni.name to ip)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
if (list.isEmpty()) list.add("lo" to "127.0.0.1")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CFG", "getAvailableInterfaces failed: ${e.javaClass.simpleName}: ${e.message}")
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
LogManager.addLog("WARN", "CFG", "getAvailableInterfaces: no usable addresses, using lo=127.0.0.1")
|
||||
list.add("lo" to "127.0.0.1")
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
/** Кодек listen-секций ConfigProvider, без файловой системы/Android. Полученные wildcard IP вызывающий код заменяет текущими. */
|
||||
internal object ServerConfig {
|
||||
private val sectionName = Regex("(.+)_(v4|v6)(?:_(tcp|udp))?")
|
||||
|
||||
/**
|
||||
* Читает [server:interface_v4/v6_tcp/udp]; без суффикса протокола по умолчанию TCP.
|
||||
* transport имеет приоритет над суффиксом; порт 1..65535. Остальные секции/поля не относятся к этому кодеку.
|
||||
* includeCommented восстанавливает полностью закомментированные секции. Неверная секция пропускается с warn.
|
||||
*/
|
||||
fun parse(lines: Sequence<String>, includeCommented: Boolean, warn: (String) -> Unit): List<ServerEntry> {
|
||||
val result = mutableListOf<ServerEntry>()
|
||||
val fields = mutableMapOf<String, String>()
|
||||
var name: String? = null
|
||||
var sectionCommented = false
|
||||
|
||||
fun finishSection() {
|
||||
val current = name ?: return
|
||||
val match = sectionName.matchEntire(current)
|
||||
if (match == null) { warn("saved server '$current': invalid interface/family/protocol section name"); return }
|
||||
val protocol = fields["transport"] ?: match.groupValues[3].ifEmpty { "tcp" }
|
||||
if (protocol != "tcp" && protocol != "udp") { warn("saved server '$current': unsupported transport"); return }
|
||||
val port = fields["addr"]?.substringAfterLast(':', "")?.toIntOrNull()
|
||||
if (port == null || port !in 1..65535) { warn("saved server '$current': invalid listen port parsed=$port, expected 1..65535"); return }
|
||||
val ip = if (match.groupValues[2] == "v6") "::" else "0.0.0.0"
|
||||
result.add(ServerEntry(UUID.randomUUID().toString(), match.groupValues[1], ip, protocol, port))
|
||||
}
|
||||
|
||||
for (line in lines) {
|
||||
val trimmed = line.trim()
|
||||
val commented = trimmed.startsWith('#')
|
||||
val body = if (commented) trimmed.removePrefix("#").trim() else trimmed
|
||||
if (body.startsWith('[') && body.endsWith(']')) {
|
||||
finishSection()
|
||||
fields.clear()
|
||||
sectionCommented = commented
|
||||
name = if (body.startsWith("[server:") && (!commented || includeCommented))
|
||||
body.removeSurrounding("[", "]").removePrefix("server:") else null
|
||||
} else if (name != null && commented == sectionCommented) {
|
||||
val equals = body.indexOf('=')
|
||||
if (equals > 0) fields[body.substring(0, equals).trim()] = body.substring(equals + 1).trim()
|
||||
}
|
||||
}
|
||||
finishSection()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** Сохранённые порты проверяются до повторного использования в автосокетах; ошибки сохраняют остальные секции. */
|
||||
class ServerConfigTest {
|
||||
private fun section(name: String, port: String, transport: String = "tcp") =
|
||||
"[server:$name]\naddr=0.0.0.0:$port\ntransport=$transport\n"
|
||||
|
||||
@Test fun roundTripIdentityUsesInterfaceFamilyAndTransport() {
|
||||
val config = section("wlan_0_v4_tcp", "2001") + section("wlan_0_v4_udp", "2002", "udp") +
|
||||
"[server:wlan_0_v6_tcp]\naddr=[::]:2003\ntransport=tcp\n"
|
||||
val servers = ServerConfig.parse(config.lineSequence(), false) { fail(it) }
|
||||
assertEquals(listOf(2001, 2002, 2003), servers.map { it.port })
|
||||
assertEquals(listOf("wlan_0_v4_tcp", "wlan_0_v4_udp", "wlan_0_v6_tcp"), servers.map { it.sectionName })
|
||||
assertEquals(listOf("0.0.0.0", "0.0.0.0", "::"), servers.map { it.bindIp })
|
||||
assertEquals(listOf(2001, 2002), AutoSocketProtocols.BOTH.createServers("wlan_0", "192.0.2.1", servers).map { it.port })
|
||||
}
|
||||
|
||||
@Test fun invalidSavedPortsAreSkippedBeforeAutoReuse() {
|
||||
for (port in listOf("0", "-1", "65536", "2147483648", "invalid", "")) {
|
||||
val warnings = mutableListOf<String>()
|
||||
val config = section("wlan0_v4_tcp", port) + section("other_v4_tcp", "3456")
|
||||
val servers = ServerConfig.parse(config.lineSequence(), false, warnings::add)
|
||||
assertEquals(listOf("other"), servers.map { it.ifName })
|
||||
assertEquals(listOf(3456), servers.map { it.port })
|
||||
assertEquals(1, warnings.size)
|
||||
assertTrue(warnings.single(), warnings.single().contains("wlan0_v4_tcp"))
|
||||
val recovered = AutoSocketProtocols.TCP.createServers("wlan0", "192.0.2.1", servers).single()
|
||||
assertTrue(recovered.port in 2000..9999)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun bothPortBoundariesArePreserved() {
|
||||
val config = section("first_v4_tcp", "1") + section("last_v4_udp", "65535", "udp")
|
||||
assertEquals(listOf(1, 65535), ServerConfig.parse(config.lineSequence(), false) { fail(it) }.map { it.port })
|
||||
}
|
||||
|
||||
@Test fun commentedSectionsAreOnlyRestoredWhenRequested() {
|
||||
val inactive = section("offline_v6_udp", "4567", "udp").lineSequence().filter { it.isNotEmpty() }.joinToString("\n") { "#$it" }
|
||||
val config = section("live_v4_tcp", "2345") + inactive
|
||||
val active = ServerConfig.parse(config.lineSequence(), false) { fail(it) }
|
||||
val all = ServerConfig.parse(config.lineSequence(), true) { fail(it) }
|
||||
assertEquals(listOf("live"), active.map { it.ifName })
|
||||
assertEquals(listOf("live", "offline"), all.map { it.ifName })
|
||||
assertEquals("::", all.last().bindIp)
|
||||
assertEquals("udp", all.last().protocol)
|
||||
}
|
||||
|
||||
@Test fun commentedFieldsDoNotOverrideActiveFields() {
|
||||
val config = section("wlan0_v4_tcp", "2345") + "#addr=0.0.0.0:9999\n#transport=udp\n"
|
||||
for (includeCommented in listOf(false, true)) {
|
||||
val server = ServerConfig.parse(config.lineSequence(), includeCommented) { fail(it) }.single()
|
||||
assertEquals(2345, server.port)
|
||||
assertEquals("tcp", server.protocol)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun unknownSectionsDoNotLeakFieldsIntoServer() {
|
||||
val config = "[global]\naddr=0.0.0.0:9999\n" + section("wlan0_v4_tcp", "2345") + "[debug]\ntransport=udp\n"
|
||||
val server = ServerConfig.parse(config.lineSequence(), false) { fail(it) }.single()
|
||||
assertEquals(2345, server.port)
|
||||
assertEquals("tcp", server.protocol)
|
||||
}
|
||||
|
||||
@Test fun invalidSectionAndTransportAreDiagnosed() {
|
||||
val warnings = mutableListOf<String>()
|
||||
val config = section("wlan0_unknown_tcp", "2345") + section("wlan0_v4_tcp", "2345", "unknown") +
|
||||
"[server:missing_v4_tcp]\ntransport=tcp\n"
|
||||
assertTrue(ServerConfig.parse(config.lineSequence(), false, warnings::add).isEmpty())
|
||||
assertEquals(3, warnings.size)
|
||||
}
|
||||
|
||||
@Test fun transportFieldWinsAndSuffixSuppliesDefault() {
|
||||
val config = section("wlan0_v4_tcp", "2345", "udp") + "[server:wlan0_v6_udp]\naddr=[::]:3456\n" +
|
||||
"[server:old_v4]\naddr=0.0.0.0:4567\n"
|
||||
val servers = ServerConfig.parse(config.lineSequence(), false) { fail(it) }
|
||||
assertEquals(listOf("udp", "udp", "tcp"), servers.map { it.protocol })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user