Когда пет-проект выходит из-под контроля: пишем свой tun2socks и закрываем дыры в Android VPN

от автора

Всё начиналось по приколу. Недавно в сети поднялась шумиха вокруг уязвимости 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. В приложении хватает мелких багов и это скорее альфа\бета версия. Как думаете, в какую сторону развивать проект дальше?

Всем добра, и спасибо, что дочитали!

ссылка на оригинал статьи https://habr.com/ru/articles/1030930/