- PVSM.RU - https://www.pvsm.ru -
Всем привет! Поводом к написанию данной статьи стало осознание того факта, что при наличии большого количества статей и обзоров про приложения VPN-клиенты [1] для Android, нет ни одной нормальной статьи описывающей проблемы разработки с использованием VpnService [2] API. Причём, в большинстве случаев вы, как разработчик приложения, не сможете ничего сделать с этими проблемами.
Начнём с самого начала. Некоторое время назад наша компания, на основе проводимых исследований в области безопасности мобильных устройств, решила выпустить небольшое приложение (WebGuard [3]) под Android для блокировки всем уже надоевшей рекламы, а так же защиты от слежки и вирусов при работе из любого браузера. Чтобы реализовать данный функционал нам потребовалось решить множество задач, самой трудоёмкой из которых оказалась задача по перехвату и обработке трафика приложений. Для перехвата и фильтрации соединений браузеров было решено использовать VpnService API, которое появилось в Android с версии 4.0.3 и предоставляет всю нужную функциональность (правда, временами эта функциональность просто не работает по куче разных причин, но выяснилось это несколько позже).
Тут стоит рассказать подробнее, почему было выбрано данное API и какие вообще способы существуют для перехвата сетевого трафика [4] приложений другим приложением на «нерутованном» android-устройстве. Собственно способов всего три (не считая различных уязвимостей) и знать о них будет полезно разработчикам приложений с различными механизмами внутриигровых покупок. Т. к. многие из них считают, что подмена сетевого трафика приложения возможно только на «рутованном» устройстве и «не заморачиваются» с защитой передаваемых данных, хотя есть вероятность (и довольно большая) появления «читерских» приложений вносящих изменения в передаваемые данные.
Итак, первый способ — это установка локального прокси-сервера [5]. Этим способом пользуется большинство антивирусов под Android. Приложению для перехвата трафика достаточно реализовать поддержку HTTP-прокси описанную в стандарте RFC 2068 [6] и установить в настройках WiFi сети прокси-сервер или APN [7] в настройках мобильной сети. В этом случае передача данных будет происходить как на схеме ниже.
Но у этого способа есть куча серьёзных проблем:
Эти все проблемы, кроме последней, успешно решаются вторым способом — написанием приложения VPN-клиента использующего классы VpnService и VpnService.Builder. Приложению достаточно вызвать пару функций (в коде опущены обязательные проверки):
// показываем Activity для запроса прав у пользователя
Intent intent = VpnService.prepare(PromptActivity.this);
startActivityForResult(intent, VPN_REQUEST_CODE); // запрос прав
// и в onActivityResult если нам выдали права
// requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK
VpnService.Builder vpnBuilder = new VpnService.Builder();
vpnBuilder.addAddress(options.address, options.maskBits);
vpnBuilder.addRoute("0.0.0.0", 0);
ParcelFileDescriptor pfd = vpnBuilder.establish();
FileInputStream in = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
FileOutputStream out = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
, и весь TCP/IP [10] трафик всех приложений (даже запущенных под root'ом) будет перенаправлен на TUN интерфейс [11], который создаст Android, а вашему приложению будет доступен на чтение/запись файл устройства /dev/tun (или /dev/tun0, /dev/tun1 и т. п.), откуда можно вычитывать исходящие сетевые пакеты, передавать их на обработку на удалённый VPN-сервер (обычно через шифрованное соединение) и затем записывать входящие сетевые пакеты. Для того, что бы соединения самого VPN-клиента не «заворачивались» в TUN, используется метод VpnService.protect на TCP или UDP сокетах созданных приложением.
Схема, представленная выше, в данном случае будет выглядеть следующим образом:
У этого способа есть две особенности:
Причём, Android запоминает, что выдал права приложению, только до перезагрузки устройства, поэтому после перезагрузки права нужно запрашивать снова. Как выяснилось (мы этого даже не ожидали) есть пользователи («да их тут сотни» ©), которые любят перезагружать свой телефон каждые 10 минут и им это окно мешает, за что они в маркете могут оценить приложение в 1 балл;
Тут недовольство пользователей было ожидаемо, т. к. многие нынешние приложения просто обожают «вешать» уведомления (иногда сразу по несколько) и статус-бар превращается в новогоднюю гирлянду:
Так же, если пользователь нажмёт кнопку «Разъединить» Android отключит VPN и заберёт права у приложения, после чего для активации VPN придётся заново просить права через вызов startActivityForResult.
Есть в этом способе и небольшая дополнительная проблема (не считая «фич» [12] о которых ниже) — для его работы нужен удалённый сервер.
Проблема с наличием сервера решается третьим способом (несколько изменённый способ №2) — приложение VPN-клиент содержит ещё и стек TCP/IP (можно взять готовый или написать самому из-за наличия недостатков [13] в готовых) для разбора трафика из приложений и обработки соединений почти как прокси-сервер. Тогда схема обработки трафика приложений несколько изменится и будет выглядеть следующим образом:
Именно этот способ мы используем в WebGuard. Из недостатков, по сравнению с предыдущим способом, можно отметить только один — это невозможность нормальной обработки протоколов отличных от TCP или UDP (или протоколов «поверх» них), потому что приложению нужно будет создавать «сырые» сокеты [14] для чего обычно нужны права root'а. Чтобы было понятно, о чём идёт речь, возьмём простой пример: пользователь запускает шелл через ADB и выполняет команду «ping www.ya.ru [15]», которая отправляет ICMP [16] эхо-запрос. Далее, приложение VPN-клиент читает из /dev/tun IP пакет, разбирает его, и выясняет что пакет содержит ICMP эхо-запрос к некоему серверу. А так как приложение не может передать запрос далее в сеть, то вариантов у него всего два: игнорировать пакет или эмулировать ping попытавшись установить соединение с нужным сервером и в случае успеха записать поддельный ICMP эхо-ответ в /dev/tun.
В процессе разработки приложения, тестирования и использования первых версий пользователями, мы столкнулись с большим количеством ошибок или недоработок связанных с VpnService API. Часть из них удалось исправить, т. к. по сути это были недоработки наших программистов (о чем мы честно написали и попали [17] на bash.org.ru), а с оставшейся частью сделать что либо довольно сложно или невозможно:
Вот так прочитаешь этот список и подумаешь — а может ну его этот VPN? К сожалению, другого способа перехватывать весь трафик на «нерутованном» телефоне с Android пока что нет.
В список выше, не попала ещё одна проблема, последствия решения которой были для нас весьма неожиданны (и не только для нас). Поэтому мы решили рассказать о последствиях поподробнее. Началось все с того, что в какой то момент нужно было сделать самую важную часть приложения — нескучную иконку. Сказано — сделано. Красивая круглая иконка нарисована и мы радостно тестируем предрелизную версию, как вдруг:
Выяснилось, что на части телефонов иконка приложения в уведомлении о VPN может очень странно отображаться (неправильная цветность, размеры или ещё что-нибудь). После экспериментов и недолгого, но жаркого обсуждения
, было приказанорешено сделать иконку приложения в формате nine-patch, благо официальная документация по этому поводу ничего не говорит (т. е. не запрещает) и яйцеобразная проблема решается. Но, после выхода релиза очень быстро появились «пострадавшие». Это были как приложения под Android, так и различные онлайн сервисы работающие с apk-файлами, и не ожидающие получить иконку приложения в формате nine-patch. Самых примечательных мы решили расположить на пьедестале из трёх мест:
    3. Различные лаунчеры, которые падали при попытке отобразить иконку (например LauncherPro).
    2. Магазины Android приложений от Samsung и Yandex. При попытке загрузить приложение в Yandex.Store выдавалось вполне понятное описание ошибки: «Не удалось извлечь из APK иконку приложения». C Samsung Apps оказалось веселее. Так как компания высокотехнологичная, то и результаты проверки при добавлении приложения приходят в соответствующем виде — письмо с ссылкой на видео в котором записан процесс тестирования приложения и должно быть видно (по идее) ошибку. Получилось, правда, все как обычно [28], пришло письмо с ссылкой по которой видео не было.
    1. Ну а почётное первое место, по праву, занимает компания Sony с телефоном Xperia. Через какое то время после выхода релиза, владельцы Xperia L начали присылать сообщения о том, что WebGuard «убил» им телефон (пример [29]). Оказалось, что падает PackageManagerService при попытке обработать иконку приложения во время установки, после чего телефон автоматически перезагружается и идёт бесконечная загрузка:
E/AndroidRuntime( 790): *** FATAL EXCEPTION IN SYSTEM PROCESS: Thread-123<br /> E/AndroidRuntime( 790): java.lang.ClassCastException: android.graphics.drawable.NinePatchDrawable cannot be cast to android.graphics.drawable.BitmapDrawable<br /> E/AndroidRuntime( 790): at com.android.server.pm.PackageManagerService$SetIconCacheThread.run(PackageManagerService.java:3672)<br /> ...<br /> E/AndroidRuntime( 2981): FATAL EXCEPTION: ApplicationsProviderUpdater<br /> E/AndroidRuntime( 2981): java.lang.RuntimeException: Package manager has died<br /> E/AndroidRuntime( 2981): at android.app.ApplicationPackageManager.queryIntentActivitiesAsUser(ApplicationPackageManager.java:487)<br /> E/AndroidRuntime( 2981): at android.app.ApplicationPackageManager.queryIntentActivities(ApplicationPackageManager.java:473)<br /> E/AndroidRuntime( 2981): at com.android.providers.applications.ApplicationsProvider.updateApplicationsList(ApplicationsProvider.java:518)<br /> E/AndroidRuntime( 2981): at com.android.providers.applications.ApplicationsProvider.access$300(ApplicationsProvider.java:69)<br /> E/AndroidRuntime( 2981): at com.android.providers.applications.ApplicationsProvider$UpdateHandler.handleMessage(ApplicationsProvider.java:206)<br /> E/AndroidRuntime( 2981): at android.os.Handler.dispatchMessage(Handler.java:99)<br /> E/AndroidRuntime( 2981): at android.os.Looper.loop(Looper.java:137)<br /> E/AndroidRuntime( 2981): at android.os.HandlerThread.run(HandlerThread.java:60)<br /> E/AndroidRuntime( 2981): Caused by: android.os.DeadObjectException<br /> E/AndroidRuntime( 2981): at android.os.BinderProxy.transact(Native Method)<br /> E/AndroidRuntime( 2981): at android.content.pm.IPackageManager$Stub$Proxy.queryIntentActivities(IPackageManager.java:2027)<br /> E/AndroidRuntime( 2981): at android.app.ApplicationPackageManager.queryIntentActivitiesAsUser(ApplicationPackageManager.java:481)<br /> E/AndroidRuntime( 2981): ... 7 more<br />
В итоге, с версии 1.3 было решено использовать иконку без nine-patch, тем более что его использование всех проблем с отображением иконки не решило:
Нам не хотелось делать слишком большой пост, поэтому было решено разбить статью на несколько частей. В следующей части мы расскажем почему приложения под Android, фильтрующие трафик, потребляют так много заряда аккумулятора (по мнению андроида) и напомним, на примере одного теста производительности, что DalvikVM != JavaVM.
Надеемся наша статья кому-нибудь поможет в написании интересного приложения под Android. Удачной разработки!
Автор: MobisoftSupport
Источник [30]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/yumor/66518
Ссылки в тексте:
[1] VPN-клиенты: http://technet.microsoft.com/ru-ru/library/cc731954(v=ws.10).aspx
[2] VpnService: http://developer.android.com/reference/android/net/VpnService.html
[3] WebGuard: https://play.google.com/store/apps/details?id=com.mobisoft.webguard
[4] сетевого трафика: http://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%82%D0%B5%D0%B2%D0%BE%D0%B9_%D1%82%D1%80%D0%B0%D1%84%D0%B8%D0%BA
[5] прокси-сервера: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%BA%D1%81%D0%B8-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80
[6] RFC 2068: http://www.ietf.org/rfc/rfc2068.txt
[7] APN: http://ru.wikipedia.org/wiki/APN
[8] Http(s)URLConnection: https://android.googlesource.com/platform/libcore/+/android-4.0.3_r1.1/luni/src/main/java/libcore/net/http/HttpConnection.java#62
[9] например: http://dmdoka.com/2014/05/01/kaspersky-internet-security-net-wifi-na-android/
[10] TCP/IP: http://ru.wikipedia.org/wiki/TCP/IP
[11] TUN интерфейс: http://ru.wikipedia.org/wiki/TUN/TAP
[12] «фич»: http://ru.wiktionary.org/wiki/%D0%B1%D0%B0%D0%B3
[13] недостатков: http://lurkmore.to/%D0%A4%D0%B0%D1%82%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BD%D0%B5%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BE%D0%BA
[14] «сырые» сокеты: http://ru.wikipedia.org/wiki/%D0%A1%D1%8B%D1%80%D0%BE%D0%B9_%D1%81%D0%BE%D0%BA%D0%B5%D1%82
[15] www.ya.ru: http://www.ya.ru
[16] ICMP: http://ru.wikipedia.org/wiki/ICMP
[17] попали: http://bestbash.org/b_154671
[18] ядре linux: http://ru.wikipedia.org/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux
[19] том числе: http://code.google.com/p/android/issues/detail?id=21030
[20] вот этому: https://android.googlesource.com/platform/frameworks/base/+/56480ef
[21] не сможет: http://code.google.com/p/ics-openvpn/issues/detail?id=185
[22] OOM Killer: http://catap.ru/blog/2009/05/03/about-memory-oom-killer/
[23] маршруты передачи пакетов: http://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%80%D1%88%D1%80%D1%83%D1%82%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F
[24] /dev/null: http://ru.wikipedia.org/?title=/dev/null
[25] здесь: http://code.google.com/p/android/issues/detail?id=62714
[26] здесь: https://code.google.com/p/android/issues/detail?id=62588
[27] здесь: https://code.google.com/p/android/issues/detail?id=61948
[28] как обычно: http://lurkmore.to/%D0%A4%D1%8D%D0%B9%D0%BB
[29] пример: http://4pda.ru/forum/index.php?showtopic=446014&st=10120#entry28922407
[30] Источник: http://habrahabr.ru/post/231827/
Нажмите здесь для печати.