feat: tunnel WireGuard vers le serveur familyd et mode propriétaire de l'appareil
- Firebase et le pare-feu local (trou noir, liste blanche par application, coupure cyclique locale) sont retirés - Client WireGuard officiel (GoBackend) : tout le trafic passe par le serveur familyd, qui filtre - Appairage par code ou lien familyd:// ; clé privée WireGuard et jeton chiffrés par l'Android Keystore - Remontées par le tunnel : état, temps d'écran, applications installées, évènements (file persistante) - Mode propriétaire (Device Owner) : VPN toujours actif avec blocage hors VPN, réglages verrouillés, garde-fou si le serveur ne répond plus pendant 15 min - Plan de la refonte : docs/PLAN-wireguard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
This commit is contained in:
1 parent
f382eeb4fe
commit
33ea4ff947
40 files changed
+1680
-1635
No files matched your search
@@ -0,0 +1,106 @@
|
||||
package com.jttools.datacontrol.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.net.VpnService
|
||||
import com.jttools.datacontrol.log.AppLog
|
||||
import com.jttools.datacontrol.remote.FamilyLink
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.config.Config
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Tunnel WireGuard vers le serveur familyd, par la bibliothèque officielle (wireguard-go embarqué).
|
||||
* Un seul VPN sur l'appareil : tout le trafic du téléphone passe par le serveur, qui le filtre.
|
||||
*/
|
||||
@Singleton
|
||||
class TunnelManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private val backend by lazy { GoBackend(context) }
|
||||
private val mutex = Mutex()
|
||||
private var appliedConfig: String? = null
|
||||
|
||||
private val _up = MutableStateFlow(false)
|
||||
/** Vrai tant que le tunnel est monté (faux après un retrait du VPN par l'utilisateur ou un autre VPN). */
|
||||
val up: StateFlow<Boolean> = _up.asStateFlow()
|
||||
|
||||
@Volatile var lastError: String? = null
|
||||
private set
|
||||
|
||||
private val tunnel = object : Tunnel {
|
||||
override fun getName() = "familyd"
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
val isUp = newState == Tunnel.State.UP
|
||||
if (!isUp && _up.value) AppLog.w("Tunnel", "Tunnel arrêté (VPN retiré ou autre VPN prioritaire)")
|
||||
_up.value = isUp
|
||||
if (!isUp) appliedConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
/** Monte (ou reconfigure) le tunnel. Sans effet si la configuration n'a pas changé et que le tunnel tourne. */
|
||||
suspend fun ensureUp(link: FamilyLink, privateKey: String): Boolean = withContext(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
val text = link.wgQuickConfig(privateKey)
|
||||
if (text == appliedConfig && backend.getState(tunnel) == Tunnel.State.UP) return@withLock true
|
||||
try {
|
||||
val config = Config.parse(text.byteInputStream())
|
||||
backend.setState(tunnel, Tunnel.State.UP, config)
|
||||
appliedConfig = text
|
||||
lastError = null
|
||||
_up.value = true
|
||||
AppLog.i("Tunnel", "Tunnel monté vers ${link.endpoint} (adresse ${link.tunnelIp})")
|
||||
true
|
||||
} catch (e: BackendException) {
|
||||
lastError = describe(e.reason)
|
||||
AppLog.e("Tunnel", "Tunnel non monté : $lastError")
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
lastError = e.message ?: e.javaClass.simpleName
|
||||
AppLog.e("Tunnel", "Tunnel non monté", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun down() = withContext(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
runCatching { backend.setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
.onFailure { AppLog.w("Tunnel", "Arrêt du tunnel : ${it.message}") }
|
||||
appliedConfig = null
|
||||
_up.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Dernière poignée de main avec le serveur (ms), ou null si aucune. */
|
||||
fun lastHandshakeMillis(): Long? = runCatching {
|
||||
val stats = backend.getStatistics(tunnel)
|
||||
stats.peers().firstOrNull()?.let { stats.peer(it)?.latestHandshakeEpochMillis() }?.takeIf { it > 0 }
|
||||
}.getOrNull()
|
||||
|
||||
private fun describe(reason: BackendException.Reason): String = when (reason) {
|
||||
BackendException.Reason.VPN_NOT_AUTHORIZED -> "autorisation VPN absente (à redonner dans DataControl)"
|
||||
BackendException.Reason.DNS_RESOLUTION_FAILURE -> "adresse du serveur introuvable (DNS)"
|
||||
BackendException.Reason.UNABLE_TO_START_VPN -> "Android refuse de démarrer le VPN"
|
||||
BackendException.Reason.TUN_CREATION_ERROR -> "création de l'interface VPN impossible"
|
||||
else -> reason.name
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isPrepared(context: Context): Boolean = VpnService.prepare(context) == null
|
||||
|
||||
/** Nouvelle paire de clés WireGuard : (clé privée, clé publique) en base64. */
|
||||
fun newKeyPair(): Pair<String, String> = KeyPair().let { it.privateKey.toBase64() to it.publicKey.toBase64() }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user