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,113 @@
|
||||
package com.jttools.datacontrol.remote
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
* Liaison avec le serveur familyd, reçue à l'appairage (sans les secrets : jeton de l'appareil et clé
|
||||
* privée WireGuard sont stockés à part, chiffrés par l'Android Keystore, voir SecretBox).
|
||||
*/
|
||||
data class FamilyLink(
|
||||
val serverUrl: String,
|
||||
val deviceId: String,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val dns: String,
|
||||
val mtu: Int,
|
||||
val serverPublicKey: String,
|
||||
val endpoint: String,
|
||||
val allowedIps: List<String>,
|
||||
val keepalive: Int,
|
||||
val apiBase: String
|
||||
) {
|
||||
/** Adresse du téléphone dans le tunnel, sans le /32. */
|
||||
val tunnelIp: String get() = address.substringBefore('/')
|
||||
|
||||
fun toJson(): String = JSONObject()
|
||||
.put("serverUrl", serverUrl).put("deviceId", deviceId).put("name", name)
|
||||
.put("address", address).put("dns", dns).put("mtu", mtu)
|
||||
.put("serverPublicKey", serverPublicKey).put("endpoint", endpoint)
|
||||
.put("allowedIps", JSONArray(allowedIps)).put("keepalive", keepalive).put("apiBase", apiBase)
|
||||
.toString()
|
||||
|
||||
/** Configuration WireGuard au format wg-quick (lue par la bibliothèque officielle). */
|
||||
fun wgQuickConfig(privateKey: String): String = buildString {
|
||||
appendLine("[Interface]")
|
||||
appendLine("PrivateKey = $privateKey")
|
||||
appendLine("Address = $address")
|
||||
appendLine("DNS = $dns")
|
||||
appendLine("MTU = $mtu")
|
||||
appendLine()
|
||||
appendLine("[Peer]")
|
||||
appendLine("PublicKey = $serverPublicKey")
|
||||
appendLine("Endpoint = $endpoint")
|
||||
appendLine("AllowedIPs = ${allowedIps.joinToString(", ")}")
|
||||
appendLine("PersistentKeepalive = $keepalive")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String?): FamilyLink? {
|
||||
if (json.isNullOrBlank()) return null
|
||||
return runCatching {
|
||||
val o = JSONObject(json)
|
||||
val ips = o.getJSONArray("allowedIps")
|
||||
FamilyLink(
|
||||
serverUrl = o.getString("serverUrl"), deviceId = o.getString("deviceId"),
|
||||
name = o.optString("name"), address = o.getString("address"), dns = o.getString("dns"),
|
||||
mtu = o.optInt("mtu", 1280), serverPublicKey = o.getString("serverPublicKey"),
|
||||
endpoint = o.getString("endpoint"),
|
||||
allowedIps = (0 until ips.length()).map { ips.getString(it) },
|
||||
keepalive = o.optInt("keepalive", 25), apiBase = o.getString("apiBase")
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/** Réponse de POST /api/pair → liaison + jeton de l'appareil. */
|
||||
fun fromPairResponse(serverUrl: String, json: String): Pair<FamilyLink, String> {
|
||||
val o = JSONObject(json)
|
||||
val wg = o.getJSONObject("wireguard")
|
||||
val ips = wg.getJSONArray("allowed_ips")
|
||||
val link = FamilyLink(
|
||||
serverUrl = serverUrl, deviceId = o.getString("device_id"), name = o.optString("name"),
|
||||
address = wg.getString("address"), dns = wg.getString("dns"), mtu = wg.optInt("mtu", 1280),
|
||||
serverPublicKey = wg.getString("server_public_key"), endpoint = wg.getString("endpoint"),
|
||||
allowedIps = (0 until ips.length()).map { ips.getString(it) },
|
||||
keepalive = wg.optInt("persistent_keepalive", 25), apiBase = o.getString("api").trimEnd('/')
|
||||
)
|
||||
require(link.serverPublicKey.isNotBlank()) { "Le serveur n'a pas fourni sa clé WireGuard." }
|
||||
return link to o.getString("token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ce que l'utilisateur saisit pour s'associer : le code seul (« ABCD 2345 »), ou le lien
|
||||
* « familyd://pair?url=https://…&code=… » affiché par l'interface des parents.
|
||||
*/
|
||||
data class PairingInput(val serverUrl: String, val code: String) {
|
||||
companion object {
|
||||
const val DEFAULT_SERVER = "https://family.tuxtech.fr"
|
||||
private val CODE = Regex("^[A-HJ-NP-Z2-9]{8}$") // même alphabet que le serveur (sans I, O, 0, 1)
|
||||
|
||||
fun parse(input: String, defaultServer: String = DEFAULT_SERVER): PairingInput? {
|
||||
val text = input.trim()
|
||||
if (text.startsWith("familyd://", ignoreCase = true)) {
|
||||
val query = runCatching { URI(text).rawQuery }.getOrNull() ?: return null
|
||||
val params = query.split('&').mapNotNull {
|
||||
val (k, v) = it.split('=', limit = 2).takeIf { p -> p.size == 2 } ?: return@mapNotNull null
|
||||
k to URLDecoder.decode(v, "UTF-8")
|
||||
}.toMap()
|
||||
val url = params["url"]?.trimEnd('/') ?: return null
|
||||
val code = normalize(params["code"] ?: return null) ?: return null
|
||||
if (!url.startsWith("https://")) return null
|
||||
return PairingInput(url, code)
|
||||
}
|
||||
return normalize(text)?.let { PairingInput(defaultServer, it) }
|
||||
}
|
||||
|
||||
private fun normalize(code: String): String? =
|
||||
code.uppercase().replace(" ", "").replace("-", "").takeIf { CODE.matches(it) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user