El binario auxiliar de OpenVPN falla en Android 10: permiso denegado
Android 10 se niega a ejecutar un auxiliar que la aplicación escribió en su propio almacenamiento. Lee logcat, distribuye el auxiliar como biblioteca nativa y arregla las compilaciones de bundle.
La aplicación funcionaba en Android 9 y muere en el mismo teléfono tras la actualización a Android 10. Tu perfil está intacto, los certificados son los mismos y el fallo llega antes de cualquier tráfico de autenticación. Logcat muestra primero un puntero nulo, y unas líneas más arriba está Cannot run program ...: error=13, Permission denied. Ese mensaje trata de ejecutar un archivo, no de leer tu configuración.
Qué es lo que realmente rechaza el error 13
Android 10, nivel de API 29, impidió que las aplicaciones ejecutaran archivos que escribían en su propio directorio de datos. La regla apunta al almacenamiento que primero es escribible y luego ejecutable, que es exactamente lo que crea un lanzador que copia desde assets. Las compilaciones antiguas del núcleo ics-openvpn descomprimen un ejecutable openvpn del APK en el primer arranque y lo inician con ProcessBuilder. La descompresión sigue funcionando; lo que se rechaza es la ejecución. Una aplicación que apunta a la API 29 hereda la regla incluso cuando se ejecuta en un dispositivo que salió con una versión anterior, y por eso una actualización puede romper una compilación que nadie tocó.
Nada del rechazo es específico de un nombre de carpeta. Un archivo que la aplicación creó bajo su directorio de datos se trata como no ejecutable sea cual sea la ruta, así que getFilesDir(), getCacheDir() y un subdirectorio propio fallan todos igual.
Separa el fallo del auxiliar de un problema de perfil
Lee el log alrededor del arranque del proceso en lugar de desplazarte desde arriba. La firma que buscas es la línea error=13, y la traza de pila que hay encima suele nombrar el método del lanzador que llamó a ProcessBuilder:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"Si el auxiliar nunca arrancó, no verás ninguna salida del log de OpenVPN: ni negociación de cifrado, ni rutas empujadas, ni configuración de DNS. Si esas líneas sí aparecen y el fallo llega después, el binario se ejecutó y el problema está en el perfil, el servidor o la capa de enrutamiento. Esa única distinción ahorra mucho tiempo que se iría en editar archivos .ovpn que nunca tenían la culpa. También te dice cuál de los dos arreglos de abajo necesitas en realidad.
Comprueba también el nivel de API del dispositivo que pruebas, no el de tu configuración de compilación. Una imagen de emulador en la API 28 no reproduce nada, porque la restricción solo muerde a partir de la 29. Comprueba a la vez el SDK objetivo de tu archivo de compilación: subirlo es lo que arrastra la regla.
adb shell getprop ro.build.version.sdkPor qué copiar el binario a otro sitio no ayuda
El arreglo obvio, escribir el auxiliar en otro directorio escribible, falla por el mismo motivo por el que falló el original. Android sigue permitiendo ejecutar bibliotecas compartidas instaladas en el directorio de bibliotecas nativas de la aplicación, porque las empaqueta el instalador en lugar de escribirse en tiempo de ejecución. Ese directorio es el único sitio al que debería apuntar el lanzador. Todo lo que descomprimas tú mismo en tiempo de ejecución, lo llames como lo llames, se queda en la categoría rechazada.
El almacenamiento externo es peor: está montado con noexec, y un auxiliar colocado ahí no puede iniciarlo ninguna aplicación. Borrar los datos de la aplicación tampoco restaura el comportamiento antiguo, porque el siguiente arranque simplemente vuelve a crear la misma copia no ejecutable.
Apunta el lanzador a una biblioteca empaquetada
Renombra el auxiliar a un nombre de biblioteca compartida y distribúyelo bajo jniLibs para cada ABI que admitas, y luego haz que el lanzador devuelva la ruta de la biblioteca instalada desde la API 28 en adelante:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}La biblioteca tiene que descomprimirse en el momento de instalar para que esa ruta contenga un archivo real, así que define android:extractNativeLibs="true" en el elemento <application>. Si un fusionador de manifiesto introduce otro valor, añade tools:replace="android:extractNativeLibs" al lado. La referencia del elemento application documenta qué controla el atributo.
Vigila una variable local duplicada cuando parchees el lanzador a mano. Las fuentes antiguas declaran nativeAPI más adelante en el mismo método, y una segunda declaración es un error de compilación, no de ejecución, así que la compilación falla rápido y el error es fácil de ver. Mantén la lista de ABI del bloque de Gradle en sintonía con las carpetas que añades.
Haz que el artefacto de versión coincida con la compilación de prueba
Un APK de depuración puede levantar el túnel mientras un bundle de versión sigue fallando, porque un bundle empaqueta las bibliotecas nativas de otra forma y puede servirlas comprimidas desde el archivo. Si solo fallan las compilaciones derivadas del bundle, añade esto a gradle.properties y vuelve a compilar:
android.bundle.enableUncompressedNativeLibs=falseLuego confirma que el paquete instalado informa de la ABI que esperas, y repite la comprobación en un dispositivo de 32 bits y otro de 64 bits si distribuyes ambos. El gestor de paquetes nombra la ABI que eligió, así que un dispositivo que cayó a 32 bits aparece aquí en lugar de en un informe de fallo:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiUna biblioteca que falta para una ABI reproduce el error de permiso original solo en ese dispositivo, y por eso un solo teléfono de prueba que funcione no es prueba. Distribuye la marca del manifiesto, el nombre de la biblioteca y el ajuste del bundle juntos; cambiar uno y olvidar los otros te devuelve al mismo fallo. La solución duradera es pasar a una versión del núcleo mantenida en lugar de arrastrar el parche para siempre.