Помощният бинарник на OpenVPN не работи на Android 10: достъпът е отказан
Android 10 отказва да изпълни помощен файл, който приложението е записало в собственото си хранилище. Прочетете logcat, доставяйте помощника като нативна библиотека и поправете bundle сборките.
Приложението работеше на Android 9 и умира на същия телефон след актуализацията до Android 10. Профилът ви е непокътнат, сертификатите са същите, а сривът идва преди какъвто и да е трафик за удостоверяване. Logcat първо показва нулев указател, а няколко реда по-горе стои Cannot run program ...: error=13, Permission denied. Това съобщение е за изпълнение на файл, не за четене на конфигурацията ви.
Какво всъщност отказва грешка 13
Android 10, ниво на API 29, спря приложенията да изпълняват файлове, които са записали в своята директория с данни. Правилото е насочено към хранилище, което първо е записваемо, а после изпълнимо — точно това, което създава стартер, който копира от assets. По-стари сборки на ядрото ics-openvpn разопаковат изпълним файл openvpn от APK при първото стартиране и го пускат с ProcessBuilder. Разопаковането все още успява; отказано е именно изпълнението. Приложение, насочено към API 29, наследява правилото дори когато работи на устройство, излязло с по-стара версия, затова една актуализация може да счупи сборка, която никой не е пипал.
В отказа няма нищо специфично за едно име на папка. Файл, създаден от приложението в директорията му с данни, се третира като неизпълним независимо от пътя, така че getFilesDir(), getCacheDir() и собствена поддиректория се провалят по един и същ начин.
Отделете срива на помощника от проблем с профила
Четете лога около стартирането на процеса, вместо да скролирате отгоре. Подписът, който търсите, е редът error=13, а трасировката на стека над него обикновено посочва метода на стартера, който е извикал ProcessBuilder:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"Ако помощникът никога не е стартирал, няма да видите никакъв изход от лога на OpenVPN: нито договаряне на шифри, нито изтласкани маршрути, нито DNS конфигурация. Ако тези редове се появят, а провалът дойде по-късно, бинарникът е работил и проблемът е в профила, сървъра или слоя за маршрутизация. Това единствено разграничение спестява много време, изразходвано за редактиране на файлове .ovpn, които никога не са били виновни. То ви казва и коя от двете поправки по-долу ви е наистина нужна.
Проверете също нивото на API на устройството, което тествате, а не това във вашата конфигурация за сборка. Емулаторен образ на API 28 не възпроизвежда нищо, защото ограничението хапе само от 29 нагоре. Проверете едновременно целевия SDK във файла за сборка: повишаването му е това, което въвежда правилото.
adb shell getprop ro.build.version.sdkЗащо копирането на бинарника другаде не помага
Очевидното заобикаляне — записване на помощника в друга записваема директория — се проваля по същата причина, по която се провали оригиналът. Android все още разрешава изпълнение на споделени библиотеки, инсталирани в директорията с нативни библиотеки на приложението, защото те се пакетират от инсталатора, а не се записват по време на изпълнение. Тази директория е единственото място, към което стартерът трябва да сочи. Всичко, което разопаковате сами по време на изпълнение, както и да го наречете, остава в отказаната категория.
Външното хранилище е по-лошо: то е монтирано с noexec и помощник, поставен там, не може да бъде стартиран от никое приложение. Изчистването на данните на приложението също не връща старото поведение, защото следващото стартиране просто пресъздава същото неизпълнимо копие.
Насочете стартера към пакетирана библиотека
Преименувайте помощника на име на споделена библиотека и го доставяйте под jniLibs за всеки ABI, който поддържате, после накарайте стартера да връща пътя на инсталираната библиотека от API 28 нататък:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}Библиотеката трябва да бъде разопакована при инсталиране, за да съдържа този път реален файл, затова задайте android:extractNativeLibs="true" на елемента <application>. Ако обединител на манифеста внесе друга стойност, добавете tools:replace="android:extractNativeLibs" до него. справката за елемента application документира какво контролира атрибутът.
Внимавайте за дублирана локална променлива, когато кърпите стартера на ръка. По-стари източници декларират nativeAPI по-късно в същия метод, а второ деклариране е грешка при компилация, не при изпълнение, така че сборката се проваля бързо и грешката се забелязва лесно. Поддържайте списъка с ABI в Gradle блока в синхрон с папките, които добавяте.
Направете артефакта за release да съвпада с тестовата сборка
Debug APK може да вдигне тунела, докато release bundle все още се срива, защото bundle пакетира нативните библиотеки в различна форма и може да ги сервира компресирани от архива. Ако само сборките от bundle се провалят, добавете това в gradle.properties и преизградете:
android.bundle.enableUncompressedNativeLibs=falseПосле потвърдете, че инсталираният пакет съобщава ABI, който очаквате, и повторете проверката на 32-битово и 64-битово устройство, ако доставяте и двете. Мениджърът на пакети посочва избрания ABI, така че устройство, върнало се на 32 бита, се показва тук, а не в доклад за срив:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiЛипсваща библиотека за един ABI възпроизвежда оригиналната грешка за достъп само на това устройство, затова един успешен тестов телефон не е доказателство. Доставяйте флага на манифеста, името на библиотеката и настройката на bundle заедно; смяната на едно и забравата на другите ви връща към същия срив. Трайното решение е да преминете към поддържано издание на ядрото, вместо да носите кръпката вечно.