@ -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 ) && !is TemporaryV6 ( 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 = Th readLocalR andom. 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 loadServers ( ) : List < ServerEntry > = runBlocking {
val json = context . dataStore . data . first ( ) [ serversJson ] ?: " [] "
try {
gson . fromJson ( json , object : TypeToken < List < ServerEntry > > ( ) { } . type ) ?: emptyList ( )
} catch ( e : Exception ) {
emptyList ( )
}
fun configFilePath ( ) : String {
val dir = getString ( " db.path " ) ?: context . filesDir . absolutePath
return " $dir /utun.cfg "
}
/** Return all currently available network interface names (for dropdown) */
fun getAvailableInterfaces ( ) : List < Pair < String , String > > = runBlocking {
val list = mutableListOf < Pair < String , String > > ( )
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 {
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 )
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 "
}
}
} catch ( _ : Exception ) { }
if ( list . isEmpty ( ) ) list . add ( " lo " to " 127.0.0.1 " )
list
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 )
}
}
/** Build full INI config text for NativeLib.start() */
fun buildConfigText ( ) : String {
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 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 ) && !is TemporaryV6 ( 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 )
}
}