Застосунок працював на Android 9 і помирає на тому самому телефоні після оновлення до Android 10. Ваш профіль недоторканий, сертифікати ті самі, а збій настає до будь-якого трафіку автентифікації. Logcat спершу показує нульовий вказівник, а кількома рядками вище стоїть Cannot run program ...: error=13, Permission denied. Це повідомлення про виконання файлу, а не про читання вашої конфігурації.

Що насправді відхиляє помилка 13

Android 10, рівень API 29, заборонив застосункам виконувати файли, які вони записували у власний каталог даних. Правило націлене на сховище, доступне спершу для запису, а потім для виконання, і саме це створює завантажувач, що копіює з assets. Старіші збірки ядра ics-openvpn під час першого запуску розпаковують з APK виконуваний файл openvpn і запускають його через 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 узгодженим із каталогами, які ви додаєте.

Узгодьте артефакт випуску з тестовою збіркою

Налагоджувальний APK може підняти тунель, тоді як релізний 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 разом; змінюючи одне й забуваючи інші, ви повертаєтеся до того самого збою. Довготривале виправлення — перейти на підтримуваний випуск ядра, а не тягнути латку вічно.

Схожі статті