Мне давно было интересно, можно ли легко добавить проксирование через тор в Android приложение. Вроде бы довольно очевидная задача, плюс тор браузеры уже под эту платформу давно есть… Но есть много задач, которые сложнее, чем кажутся. Для нетерпеливых сразу скажу — да, можно, и получается довольно легко, быстро и классно. В особенности если не копать с нуля, а воспользоваться моими наработками.

Для примера я буду использовать приложение для работы с рутрекером — никто не любит код, который работает со сферический конём в вакууме. Раньше это приложение обходило блокировку при помощи Google Compression Proxy — но увы — то ли рутрекер, то ли гугл выпилил возможность авторизации с этой проксёй. Сразу скажу, что, конечно, есть всякие впны и прочее, что вы используете для лёгкого обхода блокировки и просмотра сериальчиков. Но речь здесь идёт не про это. Как вы понимаете, тор можно использовать в мобильном приложении для огромного количества вещей — например, для доступа к веб сайтам в .onion или для реализации особо безопасного мессенджера.
Если вас не интересует сборка с нуля, то сразу перейдите к следующему заголовку.
Итак, что у нас есть на эту тему из готового инструментария. Есть особый репозиторий от неких ребят под предводительством Microsoft (ссылка в подвале). Вроде бы у них всё работало — но качество и механизм сборки просто ужасают. А ещё репозиторий устарел на два года. И скомпилированный версии библиотеки там нет, есть только довольно стрёмные инструкции по тому, как собрать её самостоятельно (в стиле — “я делал так, не знаю почему, но без этого ничего не работало”). Впрочем, имеющихся инструкций вполне достаточно для того, чтобы обновить код до актуального состояния и исправить все странные косяки.
Пункты 1-6 я уже сделал за вас, так что просто соберите библиотеку из моего репозитория, или скачайте её в секции релизов. Ссылка будет в “подвале” поста. Однако обращаю внимание, что правильным будет проверить код и библиотеки на соответствие оригинальным и отсутствие закладок. Не стоит такие вещи добавлять вслепую в свои приложения.
Сначала нужно включить тор:
Затем подождать, пока он подцепится:
Если всё прошло успешно — ура, он слушает у нас localhost на каком-то случайном порту:
Но это пока не всё. У нас теперь есть тор, который слушает порт в качестве Socks4a прокси. Однако далеко не все стандартные библиотеки умеют работать с Socks4a. Там из соображений анонимности требуется, чтобы резолв хоста происходил на прокси, а не ранее. Не знаю, какие из стандартных библиотек это умеют, и у меня был код, написанный с Apache HttpComponents. Я уже писал ранее, почему их можно использовать, да и данный пост не про то. Если вы хотите, то можете реализовать то же самое на любой другой библиотеке.
Итак, для использования httpComponents нам нужно переписать ConnectionSocketFactory и SSLConnectionSocketFactory.
Использовать эти фабрики легко и просто. Для этого нужно создать HttpClient, который использует эти библиотеки:
И указать ему наш прокси сервер:
Всё, теперь мы можем использовать тор так же, как если бы делали обыкновенные запросы. Более того, мы можем так же обращаться и к веб сайтам .onion.
Получившийся код я использовал в своём приложении для рутрекера. Да, инициализация тора занимает около 20 секунд, и страницы грузятся не так быстро — но зато мы гарантированно проходим блокировку. А все ресурсы, которые не блокированы, подгружаются через обычное соединение. Можно было бы остальные ресурсы пропускать через Google Compression Proxy, но многие жаловались, что у них заблокирован этот прокси — так что я не стал этого делать. Конечно, в приложении можно было бы ещё много всего сделать — например, кэшировать статику на телефоне для экономии трафика и более быстрой работы — но это не столь критично, да и приложение я писал скорее для примера.
Тор на андроиде — классная и удобная штука, которая достаточно работает, и её действительно можно использовать в своих приложениях. Кстати да, есть гораздо более лёгкий способ это делать — просто требовать установки Orbot, который сам поднимет вам тор. Но мне не нравятся зависимости одних приложений от других, да и 3 лишних мегабайта не так критичны в размере приложения. Так что если кому понравилось моё решение — используйте, делайте пулл реквесты, и да пребудет с вами свобода.


Для примера я буду использовать приложение для работы с рутрекером — никто не любит код, который работает со сферический конём в вакууме. Раньше это приложение обходило блокировку при помощи Google Compression Proxy — но увы — то ли рутрекер, то ли гугл выпилил возможность авторизации с этой проксёй. Сразу скажу, что, конечно, есть всякие впны и прочее, что вы используете для лёгкого обхода блокировки и просмотра сериальчиков. Но речь здесь идёт не про это. Как вы понимаете, тор можно использовать в мобильном приложении для огромного количества вещей — например, для доступа к веб сайтам в .onion или для реализации особо безопасного мессенджера.
Как подключить библиотеку для работы с Тором
Как собрать с нуля
Если вас не интересует сборка с нуля, то сразу перейдите к следующему заголовку.
Итак, что у нас есть на эту тему из готового инструментария. Есть особый репозиторий от неких ребят под предводительством Microsoft (ссылка в подвале). Вроде бы у них всё работало — но качество и механизм сборки просто ужасают. А ещё репозиторий устарел на два года. И скомпилированный версии библиотеки там нет, есть только довольно стрёмные инструкции по тому, как собрать её самостоятельно (в стиле — “я делал так, не знаю почему, но без этого ничего не работало”). Впрочем, имеющихся инструкций вполне достаточно для того, чтобы обновить код до актуального состояния и исправить все странные косяки.
- Клонируем себе этот репозиторий.
- Обновляем там компонент, который отвечает за управление тором — jtorctl. Они использовали форк основного репозитория с правками от briar, но эти правки уже включены в основной репозиторий, так что лучше взять с основного. Можно подключать из maven репозитория, но я такие вещи обычно забираю исходниками — можно сразу посмотреть, прогнать анализ и править на лету баги — проект-то довольно сырой, несмотря на возраст.
- Обновляем geoip и geoip6 — базы данных блоков IP-адресов с привязкой к географическому положению каждого блока для версий IPv4 и IPv6 соответственно. Для этого скачиваем на сайте тора windows expert bundle.
- Обновляем сам тор (то есть нативную библиотеку). Стандартной общедоступной нет (ну или я плохо искал) — так что идём к ребятам, которые разрабатывают тор и тор браузер под андроид (Orbot и Orfox), берём последний релиз их Orbot и вынимаем оттуда библиотеку. Тор там довольно свежий, что приятно.
- Правим руками всё, что перестало компилироваться в нашем проекте. Несколько функций в зависимых библиотеках изменились, но в целом всё интуитивно понятно и поправимо за 5 минут.
- Следуя рекомендациям ридми нашего проекта, создаём локальные мавен репозитории и строим из кучи кусков наш проект. Кстати, обратите внимание, что билд скрипт настолько кривой, что в одном месте включает в себя предыдущий релиз себя же. Жуть. Так что рекомендую переписать его заново, простым и понятным языком, чтобы получить на выходе обыкновенную библиотеку aar.
Как собрать из моих наработок
Пункты 1-6 я уже сделал за вас, так что просто соберите библиотеку из моего репозитория, или скачайте её в секции релизов. Ссылка будет в “подвале” поста. Однако обращаю внимание, что правильным будет проверить код и библиотеки на соответствие оригинальным и отсутствие закладок. Не стоит такие вещи добавлять вслепую в свои приложения.
Как перестать волноваться и начать проксировать через тор
Сначала нужно включить тор:
int totalSecondsPerTorStartup = 4 * 60; int totalTriesPerTorStartup = 5; try { boolean ok = onionProxyManager.startWithRepeat(totalSecondsPerTorStartup, totalTriesPerTorStartup); if (!ok) Log.e("TorTest", "Couldn't start Tor!"); } catch (InterruptedException | IOException e) { e.printStackTrace(); }
Затем подождать, пока он подцепится:
while (!onionProxyManager.isRunning()) Thread.sleep(90);
Если всё прошло успешно — ура, он слушает у нас localhost на каком-то случайном порту:
Log.v("My App", "Tor initialized on port " + onionProxyManager.getIPv4LocalHostSocksPort());
Но это пока не всё. У нас теперь есть тор, который слушает порт в качестве Socks4a прокси. Однако далеко не все стандартные библиотеки умеют работать с Socks4a. Там из соображений анонимности требуется, чтобы резолв хоста происходил на прокси, а не ранее. Не знаю, какие из стандартных библиотек это умеют, и у меня был код, написанный с Apache HttpComponents. Я уже писал ранее, почему их можно использовать, да и данный пост не про то. Если вы хотите, то можете реализовать то же самое на любой другой библиотеке.
Итак, для использования httpComponents нам нужно переписать ConnectionSocketFactory и SSLConnectionSocketFactory.
SSLConnectionSocketFactory
public class MySSLConnectionSocketFactory extends SSLConnectionSocketFactory { public MySSLConnectionSocketFactory(final SSLContext sslContext) { super(sslContext); } @Override public Socket createSocket(final HttpContext context) throws IOException { return new Socket(); } @Override public Socket connectSocket( int connectTimeout, Socket socket, final HttpHost host, final InetSocketAddress remoteAddress, final InetSocketAddress localAddress, final HttpContext context) throws IOException { Args.notNull(host, "HTTP host"); Args.notNull(remoteAddress, "Remote address"); InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socks.address"); socket = new Socket(); connectTimeout = 100000; socket.setSoTimeout(connectTimeout); socket.connect(new InetSocketAddress(socksaddr.getHostName(), socksaddr.getPort()), connectTimeout); DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); outputStream.write((byte) 0x04); outputStream.write((byte) 0x01); outputStream.writeShort((short) host.getPort()); outputStream.writeInt(0x01); outputStream.write((byte) 0x00); outputStream.write(host.getHostName().getBytes()); outputStream.write((byte) 0x00); DataInputStream inputStream = new DataInputStream(socket.getInputStream()); if (inputStream.readByte() != (byte) 0x00 || inputStream.readByte() != (byte) 0x5a) { throw new IOException("SOCKS4a connect failed"); } else Log.v("SSLConnectionSF", "SOCKS4a connect ok!"); inputStream.readShort(); inputStream.readInt(); SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createLayeredSocket(socket, host.getHostName(), host.getPort(), context); prepareSocket(sslSocket); return sslSocket; } }
ConnectionSocketFactory
public class MyConnectionSocketFactory implements ConnectionSocketFactory { @Override public Socket createSocket(final HttpContext context) throws IOException { return new Socket(); } @Override public Socket connectSocket( int connectTimeout, Socket socket, final HttpHost host, final InetSocketAddress remoteAddress, final InetSocketAddress localAddress, final HttpContext context) throws IOException, ConnectTimeoutException { InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socks.address"); socket = new Socket(); connectTimeout = 100000; socket.setSoTimeout(connectTimeout); socket.connect(new InetSocketAddress(socksaddr.getHostName(), socksaddr.getPort()), connectTimeout); DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); outputStream.write((byte) 0x04); outputStream.write((byte) 0x01); outputStream.writeShort((short) host.getPort()); outputStream.writeInt(0x01); outputStream.write((byte) 0x00); outputStream.write(host.getHostName().getBytes()); outputStream.write((byte) 0x00); DataInputStream inputStream = new DataInputStream(socket.getInputStream()); if (inputStream.readByte() != (byte) 0x00 || inputStream.readByte() != (byte) 0x5a) { throw new IOException("SOCKS4a connect failed"); } else Log.v("SSLConnectionSF", "SOCKS4a connect ok!"); inputStream.readShort(); inputStream.readInt(); return socket; } }
Использовать эти фабрики легко и просто. Для этого нужно создать HttpClient, который использует эти библиотеки:
public HttpClient getNewHttpClient() { Registry<ConnectionSocketFactory> reg = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", new MyConnectionSocketFactory()) .register("https", new MySSLConnectionSocketFactory(SSLContexts.createSystemDefault())) .build(); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg); return HttpClients.custom() .setConnectionManager(cm) .build(); }
И указать ему наш прокси сервер:
HttpClient cli = getNewHttpClient(); int port = onionProxyManager.getIPv4LocalHostSocksPort(); InetSocketAddress socksaddr = new InetSocketAddress("127.0.0.1", port); HttpClientContext context = HttpClientContext.create(); context.setAttribute("socks.address", socksaddr);
Всё, теперь мы можем использовать тор так же, как если бы делали обыкновенные запросы. Более того, мы можем так же обращаться и к веб сайтам .onion.
Результат
Получившийся код я использовал в своём приложении для рутрекера. Да, инициализация тора занимает около 20 секунд, и страницы грузятся не так быстро — но зато мы гарантированно проходим блокировку. А все ресурсы, которые не блокированы, подгружаются через обычное соединение. Можно было бы остальные ресурсы пропускать через Google Compression Proxy, но многие жаловались, что у них заблокирован этот прокси — так что я не стал этого делать. Конечно, в приложении можно было бы ещё много всего сделать — например, кэшировать статику на телефоне для экономии трафика и более быстрой работы — но это не столь критично, да и приложение я писал скорее для примера.
Заключение
Тор на андроиде — классная и удобная штука, которая достаточно работает, и её действительно можно использовать в своих приложениях. Кстати да, есть гораздо более лёгкий способ это делать — просто требовать установки Orbot, который сам поднимет вам тор. Но мне не нравятся зависимости одних приложений от других, да и 3 лишних мегабайта не так критичны в размере приложения. Так что если кому понравилось моё решение — используйте, делайте пулл реквесты, и да пребудет с вами свобода.
Ссылки:
- Исходная библиотека;
- Моя сборка библиотеки;
- Приложение для рутрекера;
- Guardian Project — ребята, которым мы обязаны наличием нативной тор библиотеки.

