Initial v1.1.8 Commits

This commit is contained in:
amurcanov
2026-05-23 22:18:08 +03:00
commit ac86caaf8b
76 changed files with 15693 additions and 0 deletions
@@ -0,0 +1,209 @@
package com.wdtt.client
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.util.Log
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.wireguard.config.Interface
import com.wireguard.config.Peer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
class WireGuardHelper(context: Context) {
private val appContext = context.applicationContext
private val backend = (appContext as WdttApplication).getBackend(context)
private companion object {
val wgMutex = Mutex()
var sharedTunnel: WgTunnel? = null
}
class WgTunnel : Tunnel {
override fun getName() = "wdtt"
override fun onStateChange(newState: Tunnel.State) {}
}
suspend fun startTunnel(configString: String) = wgMutex.withLock {
startTunnelLocked(configString)
}
private suspend fun startTunnelLocked(configString: String) = withContext(Dispatchers.IO) {
try {
if (VpnService.prepare(appContext) != null) {
throw IllegalStateException("VPN-разрешение не выдано")
}
ensureGoBackendServiceStarted()
sharedTunnel?.let { existingTunnel ->
try {
backend.setState(existingTunnel, Tunnel.State.DOWN, null)
} catch (e: Exception) {
Log.w("WG", "Failed to stop previous tunnel before restart: ${e.readableMessage()}")
}
sharedTunnel = null
delay(150)
}
val parsedConfig = Config.parse(ByteArrayInputStream(configString.toByteArray(Charsets.UTF_8)))
val builder = Interface.Builder()
.parseAddresses(parsedConfig.`interface`.addresses.joinToString(", ") { it.toString() })
if (parsedConfig.`interface`.dnsServers.isNotEmpty()) {
builder.parseDnsServers(parsedConfig.`interface`.dnsServers.joinToString(", ") { it.hostAddress ?: "" })
}
if (parsedConfig.`interface`.listenPort.isPresent) {
builder.parseListenPort(parsedConfig.`interface`.listenPort.get().toString())
}
if (parsedConfig.`interface`.mtu.isPresent) {
val serverMtu = parsedConfig.`interface`.mtu.get()
// Используем серверное значение, но не менее 1280 для мобильных сетей
builder.parseMtu(serverMtu.coerceAtLeast(1280).toString())
} else {
builder.parseMtu("1280")
}
builder.parsePrivateKey(parsedConfig.`interface`.keyPair.privateKey.toBase64())
// 1. Пакеты, которые всегда исключаются (наше приложение, ВК)
// 2. Получаю настройки пользователя
val settingsStore = SettingsStore(appContext)
val savedExcluded = settingsStore.excludedApps.first()
val userSelected = savedExcluded.split(",").filter { it.isNotEmpty() }.toSet()
// В обоих режимах (ЧС и БС) мы технически используем Blacklist (Checked = Excluded),
// так как пользователю удобнее логика "снимите галочку, чтобы приложение пошло в туннель".
// Разница только в описании и начальном состоянии списка (пустой/полный).
val excluded = mutableSetOf(appContext.packageName, "com.vkontakte.android", "com.vk.calls")
excluded.addAll(userSelected)
val installedExcluded = excluded.filter { it.isInstalledPackage() }.toSet()
if (installedExcluded.isNotEmpty()) {
builder.excludeApplications(installedExcluded)
}
val newInterface = builder.build()
val peerBuilder = Peer.Builder()
val firstPeer = parsedConfig.peers.firstOrNull()
?: throw IllegalStateException("WireGuard config has no peer")
firstPeer.let { peer ->
peerBuilder.parsePublicKey(peer.publicKey.toBase64())
if (peer.preSharedKey.isPresent) peerBuilder.parsePreSharedKey(peer.preSharedKey.get().toBase64())
if (peer.endpoint.isPresent) peerBuilder.parseEndpoint(peer.endpoint.get().toString())
if (peer.persistentKeepalive.isPresent) peerBuilder.parsePersistentKeepalive(peer.persistentKeepalive.get().toString())
}
// Override AllowedIPs
peerBuilder.parseAllowedIPs("0.0.0.0/0")
val finalConfig = Config.Builder()
.setInterface(newInterface)
.addPeer(peerBuilder.build())
.build()
val nextTunnel = WgTunnel()
setTunnelUpWithRetry(nextTunnel, finalConfig)
sharedTunnel = nextTunnel
Log.d("WG", "WireGuard tunnel started successfully")
} catch (e: Exception) {
val detailed = "WireGuard start failed: ${e.readableMessage()}; ${configString.describeWireGuardConfig()}"
Log.e("WG", detailed)
e.printStackTrace()
throw IllegalStateException(detailed, e)
}
}
suspend fun reloadTunnel() = wgMutex.withLock {
withContext(Dispatchers.IO) {
val currentTunnel = sharedTunnel ?: return@withContext
try {
val configFlow = TunnelManager.config.first() ?: return@withContext
backend.setState(currentTunnel, Tunnel.State.DOWN, null)
sharedTunnel = null
delay(150)
startTunnelLocked(configFlow)
Log.d("WG", "WireGuard tunnel reloaded for new exceptions")
} catch (e: Exception) {
Log.e("WG", "Failed to reload WireGuard: ${e.readableMessage()}")
}
}
}
suspend fun stopTunnel() = wgMutex.withLock {
withContext(Dispatchers.IO) {
try {
sharedTunnel?.let {
backend.setState(it, Tunnel.State.DOWN, null)
sharedTunnel = null
Log.d("WG", "WireGuard tunnel stopped")
}
} catch (e: Exception) {
Log.e("WG", "Failed to stop WireGuard: ${e.readableMessage()}")
}
}
}
private suspend fun ensureGoBackendServiceStarted() {
withContext(Dispatchers.Main) {
runCatching {
val intent = Intent(appContext, GoBackend.VpnService::class.java)
appContext.startService(intent)
}.onFailure {
Log.w("WG", "GoBackend service warmup failed: ${it.readableMessage()}")
}
}
delay(300)
}
private suspend fun setTunnelUpWithRetry(nextTunnel: WgTunnel, finalConfig: Config) {
var lastError: Exception? = null
repeat(3) { attempt ->
try {
backend.setState(nextTunnel, Tunnel.State.UP, finalConfig)
return
} catch (e: Exception) {
lastError = e
Log.w("WG", "WireGuard UP attempt ${attempt + 1}/3 failed: ${e.readableMessage()}")
runCatching { backend.setState(nextTunnel, Tunnel.State.DOWN, null) }
ensureGoBackendServiceStarted()
delay(250L * (attempt + 1))
}
}
throw lastError ?: IllegalStateException("WireGuard UP failed")
}
private fun Throwable.readableMessage(): String {
val text = message ?: localizedMessage
return if (text.isNullOrBlank()) this::class.java.simpleName else "${this::class.java.simpleName}: $text"
}
private fun String.isInstalledPackage(): Boolean {
return runCatching {
appContext.packageManager.getPackageInfo(this, 0)
true
}.getOrDefault(false)
}
private fun String.describeWireGuardConfig(): String {
val lines = lineSequence().map { it.trim() }.filter { it.isNotEmpty() }.toList()
val hasInterface = lines.any { it.equals("[Interface]", ignoreCase = true) }
val hasPeer = lines.any { it.equals("[Peer]", ignoreCase = true) }
val hasPrivateKey = lines.any { it.startsWith("PrivateKey", ignoreCase = true) }
val hasPublicKey = lines.any { it.startsWith("PublicKey", ignoreCase = true) }
val hasAddress = lines.any { it.startsWith("Address", ignoreCase = true) }
val endpoint = lines.firstOrNull { it.startsWith("Endpoint", ignoreCase = true) }
?.substringAfter("=", "")
?.trim()
?.take(80)
?: "none"
return "config lines=${lines.size}, interface=$hasInterface, peer=$hasPeer, privateKey=$hasPrivateKey, publicKey=$hasPublicKey, address=$hasAddress, endpoint=$endpoint"
}
}