Il binario ausiliario di OpenVPN non funziona su Android 10: permesso negato
Android 10 rifiuta di eseguire un ausiliario che l’app ha scritto nel proprio archivio. Leggi logcat, distribuisci l’ausiliario come libreria nativa e correggi le build bundle.
L’app funzionava su Android 9 e muore sullo stesso telefono dopo l’aggiornamento ad Android 10. Il tuo profilo è intatto, i certificati sono gli stessi e il crash arriva prima di qualsiasi traffico di autenticazione. Logcat mostra prima un puntatore nullo, e qualche riga sopra c’è Cannot run program ...: error=13, Permission denied. Quel messaggio riguarda l’esecuzione di un file, non la lettura della tua configurazione.
Che cosa rifiuta davvero l’errore 13
Android 10, livello API 29, ha impedito alle app di eseguire file che scrivevano nella propria directory di dati. La regola colpisce l’archiviazione prima scrivibile e poi eseguibile, che è esattamente ciò che crea un launcher che copia dagli assets. Le build più vecchie del core ics-openvpn estraggono un eseguibile openvpn dall’APK al primo avvio e lo avviano con ProcessBuilder. L’estrazione riesce ancora; a essere rifiutata è l’esecuzione. Un’app che punta all’API 29 eredita la regola anche quando gira su un dispositivo uscito con una versione precedente, ed è per questo che un aggiornamento può rompere una build che nessuno ha toccato.
Niente del rifiuto è specifico di un nome di cartella. Un file che l’app ha creato sotto la propria directory di dati viene trattato come non eseguibile qualunque sia il percorso, quindi getFilesDir(), getCacheDir() e una sottodirectory personalizzata falliscono tutti allo stesso modo.
Separa il crash dell’ausiliario da un problema di profilo
Leggi il log intorno all’avvio del processo invece di scorrere dall’inizio. La firma da cercare è la riga error=13, e la traccia dello stack sopra di essa di solito nomina il metodo del launcher che ha chiamato ProcessBuilder:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"Se l’ausiliario non è mai partito, non vedrai alcun output di log di OpenVPN: nessuna negoziazione dei cifrari, nessuna rotta spinta, nessuna configurazione DNS. Se invece quelle righe compaiono e il guasto arriva più tardi, il binario ha girato e il problema è nel profilo, nel server o nello strato di routing. Questa sola distinzione risparmia molto tempo che altrimenti se ne andrebbe a modificare file .ovpn che non avevano mai colpa. Ti dice anche quale dei due rimedi qui sotto ti serve davvero.
Controlla anche il livello API del dispositivo che stai provando, non quello nella tua configurazione di build. Un’immagine di emulatore su API 28 non riproduce nulla, perché la restrizione morde solo dal 29 in su. Controlla allo stesso tempo l’SDK di destinazione nel tuo file di build: è il suo innalzamento che tira dentro la regola.
adb shell getprop ro.build.version.sdkPerché copiare il binario altrove non aiuta
La soluzione ovvia, scrivere l’ausiliario in un’altra directory scrivibile, fallisce per lo stesso motivo per cui è fallita l’originale. Android permette ancora di eseguire le librerie condivise installate nella directory delle librerie native dell’app, perché le impacchetta l’installer invece di scriverle a runtime. Quella directory è l’unico posto verso cui il launcher dovrebbe puntare. Qualunque cosa estragga tu stesso a runtime, comunque la chiami, resta nella categoria rifiutata.
L’archiviazione esterna è peggio: è montata con noexec, e un ausiliario messo lì non può essere avviato da nessuna app. Cancellare i dati dell’app non ripristina nemmeno il vecchio comportamento, perché l’avvio successivo ricrea semplicemente la stessa copia non eseguibile.
Punta il launcher a una libreria impacchettata
Rinomina l’ausiliario con un nome di libreria condivisa e distribuiscilo sotto jniLibs per ogni ABI che supporti, poi fai restituire al launcher il percorso della libreria installata dall’API 28 in poi:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}La libreria deve essere estratta al momento dell’installazione perché quel percorso contenga un file reale, quindi imposta android:extractNativeLibs="true" sull’elemento <application>. Se un merger di manifest porta un valore diverso, aggiungi tools:replace="android:extractNativeLibs" accanto. La guida all’elemento application documenta che cosa controlla l’attributo.
Attenzione a una variabile locale duplicata quando correggi il launcher a mano. Le fonti più vecchie dichiarano nativeAPI più avanti nello stesso metodo, e una seconda dichiarazione è un errore di compilazione, non di esecuzione, quindi la build fallisce subito e l’errore è facile da vedere. Tieni l’elenco delle ABI nel blocco Gradle allineato alle cartelle che aggiungi.
Fai coincidere l’artefatto di release con la build di test
Un APK di debug può alzare il tunnel mentre un bundle di release va ancora in crash, perché un bundle impacchetta le librerie native in una forma diversa e può servirle compresse dall’archivio. Se falliscono solo le build derivate dal bundle, aggiungi questo a gradle.properties e ricompila:
android.bundle.enableUncompressedNativeLibs=falsePoi conferma che il pacchetto installato dichiari l’ABI che ti aspetti, e ripeti il controllo su un dispositivo a 32 bit e uno a 64 bit se li distribuisci entrambi. Il gestore dei pacchetti nomina l’ABI che ha scelto, quindi un dispositivo ricaduto a 32 bit compare qui invece che in un rapporto di crash:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiUna libreria mancante per un’ABI riproduce l’errore di permesso originale solo su quel dispositivo, ed è per questo che un solo telefono di test funzionante non è una prova. Distribuisci insieme il flag del manifest, il nome della libreria e l’impostazione del bundle; cambiare uno e dimenticare gli altri ti riporta allo stesso crash. La correzione duratura è passare a una release del core mantenuta invece di portarti dietro la patch per sempre.