Всё начиналось по приколу. Недавно в сети поднялась шумиха вокруг уязвимости VLESS-клиентов: оказалось, что даже при использовании сплит-туннелирования (когда VPN включен только для избранных приложений), любое "шпионское" приложение на телефоне может узнать IP-адрес вашего VPN-сервера.
Уязвимость была тривиальной - ядро клиента открывает локальный SOCKS-прокси, который никак не защищен. Любая софтина на устройстве может постучаться в этот локальный порт и отправить пакет наружу. Ради академического интереса я написал Android-приложение TeapodStream, под капотом которого связал xray-core и tun2socks. Локальный прокси я посадил на случайный порт и закрыл динамическим паролем (подробнее об этом писал в прошлой статье, пиво кстати выдохлось)) ).
Пост получил огромный отклик у комьюнити, породил живое обсуждение и разлетелся по закладкам. Я думал, что на этом мой эксперимент закончен.
Но... я сам не заметил, как меня затянуло.
Дыра, о которой не принято говорить
После публикации поста в комментариях всплыл еще один баг безопасности. Куда более противный, и присутствует он чуть ли не во всех популярных VPN-клиентах под Android.
Симптом выглядит так: шпионское приложение, которое не добавлено в список туннелируемых, просто выполняет системный вызов, эквивалентный команде curl --interface tun0https://checkip.amazonaws.com - и пакет благополучно улетает на ваш сервер, раскрывая его IP.
Как это вообще возможно, если работает сплит-туннелинг?
Давайте заглянем под капот Android. При поднятии VPN ваше приложение через VpnService API создает сетевой интерфейс tun0. Дальше операционная система использует Policy-Based Routing (маршрутизацию на основе политик). Когда обычное приложение открывает сокет и пытается выйти в сеть, Android смотрит на его UID. Если UID есть в списке разрешенных для VPN - пакет летит в tun0, если нет - летит через дефолтный маршрут (сотовую сеть или Wi-Fi).
Но есть один нюанс. tun0 - это сетевое устройство уровня ядра системы. А ядру Linux плевать на высокоуровневые политики и Android UID. Если приложение принудительно укажет сокету параметр SO_BINDTODEVICE с именем интерфейса tun0, ядро послушно отправит пакет именно туда, в обход всех правил Android.
Раньше такой фокус требовал root-прав. Но, начиная с ядра Linux версии 5.7, этот вызов разрешили делать обычным пользовательским приложениям. Бинго!
Пишем свой tun2socks (закрываем дыры Android?)
С этой дырой невозможно бороться, просто подставив хитрые параметры запуска для обычного tun2socks или xray-core. Пакет уже провалился в туннель, его нужно отлавливать и фильтровать изнутри.
Поняв это, я принял радикальное решение: написать собственное ядро tun2socks с нуля на Go, используя gVisor (userspace сетевой стек).
Логика работы моего кастомного движка: после получения пакета из tun0, мы собираем из него 5-tuple (протокол, IP источника, порт источника, IP назначения, порт назначения). Затем мы стучимся в нативный API Android (ConnectivityManager.getConnectionOwnerUid) и спрашиваем: "А кому, собственно, принадлежит этот сокет?".
Получив UID, мы сверяем его со списками сплит-туннелинга. Любой пакет, чей UID нам не подошел или который мы вообще не смогли определить, безжалостно дропается.
На уровне протоколов это выглядит так:
-
С TCP всё просто. Если в туннель падает
SYN-пакет (попытка открыть соединение), проверяем UID. Если приложению нельзя в VPN - отправляем в ответRST(сброс соединения). Если можно - заворачиваем трафик в наш запароленный SOCKS. -
С UDP всё намного сложнее. Соединения как такового нет, это просто поток датаграмм (нет
SYN-пакетов). Нам приходится реагировать на каждый пакет. Мы сверяем UID и кэшируем связку портов, чтобы не дергать тяжелый Android API на каждую датаграмму. Плюс на этом этапе приходится делать ребинд сокетов (Strict Source Binding), чтобы UDP-пакеты не застревали в петле маршрутизации внутри самого туннеля и не утекали. -
С ICMP - сплошная засада. В ядре определить, от какого именно приложения идет ICMP-пакет (тот же пинг), настолько сложно и ресурсоемко, что это просто теряет смысл для мобилки. Поэтому было принято волевое решение: дать пользователю галочку "Заблокировать весь ICMP в туннеле" от греха подальше.
И оно заработало? Короткий ответ: да. Кастомный tun2socks на Go оказался очень гибким и непробиваемым для SO_BINDTODEVICE.
От пет-проекта к полноценному приложению (боли и радости)
Я был в шоке от фидбэка комьюнити. Столько слов благодарности, столько пожеланий и... столько баг-репортов.
Каждый день я старался выделять время и допиливать проект. Функционал рос: появилась маршрутизация трафика по GeoIP/GeoSite, тайл в шторке быстрых настроек, экспорт интентов (для автоматизаций через Tasker или Macrodroid), профили настроек (чтобы можно было пошарить свой конфиг близким), поддержка Always-On VPN и многое другое.
Драма с редизайном В какой-то момент я решил, что UI приложения выглядит слишком скучно, и выкатил стильное (на мой взгляд) обновление интерфейса. Реакция была мгновенной и беспощадной. В issues, в tg, на почту посыпались жалобы: "моим клиентам стало неудобно", "вынужден отказаться от вашего приложения, если не вернете старый дизайн" и всё в таком духе. В этот момент я осознал: то, что я пилил "по приколу для души", люди уже вовсю используют, в том числе в коммерческом бизнесе для своих клиентов. Местами пришлось искать компромиссы.
Битва за стабильность Но самая большая боль, работа над которой продолжается до сих пор - это стабильность vpn-соединения. Android - суровая среда. Нужно корректно обрабатывать переключения сети (Wi-Fi <-> LTE), уход телефона в глубокий сон (Doze mode) и пробуждение, запуск и остановку туннеля из шторки или автоматизаций (когда UI приложения вообще не загружается в память), поддержку heartbeats и кучу других нюансов.
Я напомню: TeapodStream не является коммерческим проектом. Занимаюсь я им в свободное от работы время. Его код открыт, я не планирую вводить монетизацию и не собираю донаты. Это просто мой способ размять , повеселиться и (надеюсь) сделать интернет чуточку свободнее и безопаснее.
Исходники TeapodStream и моего кастомного tun2socks лежат на GitHub. В приложении хватает мелких багов и это скорее альфабета версия. Как думаете, в какую сторону развивать проект дальше?
Всем добра, и спасибо, что дочитали!
Автор: Wendor
