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,115 @@
|
||||
package com.jttools.datacontrol.admin
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.UserManager
|
||||
import com.jttools.datacontrol.log.AppLog
|
||||
|
||||
/**
|
||||
* Mode « propriétaire de l'appareil » (Device Owner), provisionné une fois par adb :
|
||||
* adb shell dpm set-device-owner com.jttools.datacontrol/.admin.AppDeviceAdminReceiver
|
||||
*
|
||||
* DataControl impose alors au système : VPN toujours actif (connexions bloquées hors tunnel), réglages
|
||||
* VPN / date et heure / DNS privé verrouillés, pas de réinitialisation, pas de mode sans échec, pas
|
||||
* d'autre utilisateur, désinstallation impossible. Sans ce mode, toutes ces fonctions sont inertes.
|
||||
*/
|
||||
object OwnerPolicy {
|
||||
|
||||
/** Restrictions posées tant que la protection est active (le débogage USB reste permis : maintenance). */
|
||||
private val RESTRICTIONS = buildList {
|
||||
add(UserManager.DISALLOW_CONFIG_VPN)
|
||||
add(UserManager.DISALLOW_CONFIG_DATE_TIME)
|
||||
add(UserManager.DISALLOW_FACTORY_RESET)
|
||||
add(UserManager.DISALLOW_SAFE_BOOT)
|
||||
add(UserManager.DISALLOW_ADD_USER)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) add(UserManager.DISALLOW_CONFIG_PRIVATE_DNS)
|
||||
}
|
||||
|
||||
/** État voulu, appliqué tel quel ; [lockdown] = connexions bloquées hors VPN. */
|
||||
data class Target(val protect: Boolean, val lockdown: Boolean)
|
||||
|
||||
private var applied: Target? = null
|
||||
|
||||
fun isDeviceOwner(context: Context): Boolean = AdminController.isDeviceOwner(context)
|
||||
|
||||
private fun dpm(context: Context) = context.getSystemService(DevicePolicyManager::class.java)
|
||||
|
||||
/** Applique [target] si DataControl est propriétaire et que l'état a changé. Renvoie faux en cas d'échec. */
|
||||
fun sync(context: Context, target: Target, force: Boolean = false): Boolean {
|
||||
if (!isDeviceOwner(context)) {
|
||||
applied = null
|
||||
return true
|
||||
}
|
||||
if (!force && target == applied) return true
|
||||
val dpm = dpm(context)
|
||||
val admin = AdminController.component(context)
|
||||
return try {
|
||||
if (target.protect) {
|
||||
// Le système accorde lui-même l'autorisation VPN au paquet désigné : aucune fenêtre de consentement.
|
||||
dpm.setAlwaysOnVpnPackage(admin, context.packageName, target.lockdown)
|
||||
RESTRICTIONS.forEach { dpm.addUserRestriction(admin, it) }
|
||||
dpm.setUninstallBlocked(admin, context.packageName, true)
|
||||
requireAutoTime(context)
|
||||
} else {
|
||||
dpm.setAlwaysOnVpnPackage(admin, null, false)
|
||||
RESTRICTIONS.forEach { dpm.clearUserRestriction(admin, it) }
|
||||
dpm.setUninstallBlocked(admin, context.packageName, false)
|
||||
}
|
||||
applied = target
|
||||
AppLog.i("Propriétaire", when {
|
||||
!target.protect -> "Protections levées (contrôle désactivé ou appareil non associé)"
|
||||
target.lockdown -> "VPN toujours actif, connexions bloquées hors VPN, réglages verrouillés"
|
||||
else -> "VPN toujours actif SANS blocage hors VPN (serveur injoignable), réglages verrouillés"
|
||||
})
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
AppLog.e("Propriétaire", "Application des protections impossible", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun requireAutoTime(context: Context) {
|
||||
val dpm = dpm(context)
|
||||
val admin = AdminController.component(context)
|
||||
runCatching {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
dpm.setAutoTimeEnabled(admin, true)
|
||||
dpm.setAutoTimeZoneEnabled(admin, true)
|
||||
} else {
|
||||
dpm.setAutoTimeRequired(admin, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** VPN toujours actif imposé par DataControl, et avec quel blocage. */
|
||||
fun alwaysOnState(context: Context): Pair<Boolean, Boolean> {
|
||||
if (!isDeviceOwner(context)) return false to false
|
||||
val dpm = dpm(context)
|
||||
val admin = AdminController.component(context)
|
||||
return runCatching {
|
||||
val pkg = dpm.getAlwaysOnVpnPackage(admin)
|
||||
val lockdown = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && dpm.isAlwaysOnVpnLockdownEnabled(admin)
|
||||
(pkg == context.packageName) to lockdown
|
||||
}.getOrDefault(false to false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortie du mode propriétaire (après vérification du mot de passe) : protections levées, puis
|
||||
* DataControl redevient une application ordinaire (l'administrateur d'appareil simple reste actif).
|
||||
*/
|
||||
fun release(context: Context): Boolean {
|
||||
if (!isDeviceOwner(context)) return true
|
||||
sync(context, Target(protect = false, lockdown = false), force = true)
|
||||
return runCatching {
|
||||
dpm(context).clearDeviceOwnerApp(context.packageName)
|
||||
applied = null
|
||||
AppLog.w("Propriétaire", "Mode propriétaire de l'appareil retiré")
|
||||
true
|
||||
}.getOrElse {
|
||||
AppLog.e("Propriétaire", "Retrait du mode propriétaire impossible", it)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user