Лёгкая интеграция tor в android приложение на примере клиента для рутрекера

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

    Для примера я буду использовать приложение для работы с рутрекером — никто не любит код, который работает со сферический конём в вакууме. Раньше это приложение обходило блокировку при помощи Google Compression Proxy — но увы — то ли рутрекер, то ли гугл выпилил возможность авторизации с этой проксёй. Сразу скажу, что, конечно, есть всякие впны и прочее, что вы используете для лёгкого обхода блокировки и просмотра сериальчиков. Но речь здесь идёт не про это. Как вы понимаете, тор можно использовать в мобильном приложении для огромного количества вещей — например, для доступа к веб сайтам в .onion или для реализации особо безопасного мессенджера.

    Как подключить библиотеку для работы с Тором


    Как собрать с нуля


    Если вас не интересует сборка с нуля, то сразу перейдите к следующему заголовку.

    Итак, что у нас есть на эту тему из готового инструментария. Есть особый репозиторий от неких ребят под предводительством Microsoft (ссылка в подвале). Вроде бы у них всё работало — но качество и механизм сборки просто ужасают. А ещё репозиторий устарел на два года. И скомпилированный версии библиотеки там нет, есть только довольно стрёмные инструкции по тому, как собрать её самостоятельно (в стиле — “я делал так, не знаю почему, но без этого ничего не работало”). Впрочем, имеющихся инструкций вполне достаточно для того, чтобы обновить код до актуального состояния и исправить все странные косяки.

    1. Клонируем себе этот репозиторий.

    2. Обновляем там компонент, который отвечает за управление тором — jtorctl. Они использовали форк основного репозитория с правками от briar, но эти правки уже включены в основной репозиторий, так что лучше взять с основного. Можно подключать из maven репозитория, но я такие вещи обычно забираю исходниками — можно сразу посмотреть, прогнать анализ и править на лету баги — проект-то довольно сырой, несмотря на возраст.

    3. Обновляем geoip и geoip6 — базы данных блоков IP-адресов с привязкой к географическому положению каждого блока для версий IPv4 и IPv6 соответственно. Для этого скачиваем на сайте тора windows expert bundle.

    4. Обновляем сам тор (то есть нативную библиотеку). Стандартной общедоступной нет (ну или я плохо искал) — так что идём к ребятам, которые разрабатывают тор и тор браузер под андроид (Orbot и Orfox), берём последний релиз их Orbot и вынимаем оттуда библиотеку. Тор там довольно свежий, что приятно.

    5. Правим руками всё, что перестало компилироваться в нашем проекте. Несколько функций в зависимых библиотеках изменились, но в целом всё интуитивно понятно и поправимо за 5 минут.

    6. Следуя рекомендациям ридми нашего проекта, создаём локальные мавен репозитории и строим из кучи кусков наш проект. Кстати, обратите внимание, что билд скрипт настолько кривой, что в одном месте включает в себя предыдущий релиз себя же. Жуть. Так что рекомендую переписать его заново, простым и понятным языком, чтобы получить на выходе обыкновенную библиотеку 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 лишних мегабайта не так критичны в размере приложения. Так что если кому понравилось моё решение — используйте, делайте пулл реквесты, и да пребудет с вами свобода.

    Ссылки:


    1. Исходная библиотека;
    2. Моя сборка библиотеки;
    3. Приложение для рутрекера;
    4. Guardian Project — ребята, которым мы обязаны наличием нативной тор библиотеки.

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      0
      while (!onionProxyManager.isRunning())
      Thread.sleep(90);


      А по-другому никак нельзя сделать? Listener какой-нибудь повесить и ловить callback?
        0
        Конечно, можно, и, конечно, так будет правильнее. Я просто не занимался переработкой исходной библиотеки, мне было интересно — взлетит или нет. Если библиотека будет востребована, то можно будет переписать по человечески. Пока что реакции не наблюдается. Вообще довольно мало вопросов и кода по этому вопросу — видимо, пока что никому не нужно.
          0
          Может быть, в статье не хватает ответа на вопрос: «Зачем это надо?». На мой взгляд тор нужен для решения других задач.
            0
            Таки тор решает огромное количество разных задач. Самое очевидное употребление это злоупотребление, и я не стал подробно на этом останавливаться. Да и у меня было ощущение, что на хабре все и так знают, зачем можно использовать тор.
        0
        Раньше это приложение обходило блокировку при помощи Google Compression Proxy — но увы — то ли рутрекер, то ли гугл выпилил возможность авторизации с этой проксёй

        Странно, после вашей статьи о Google Compression Proxy добавил его в своё приложение как один из вариантов, и у меня до сих пор оно отлично работает.
        Единственное отличие, я использую OkHttpClient3 для сетевых запросов, и не заморачивался с SSL.
          0
          Предполагаю, что вы не с рутрекером работаете. У меня перестала работать конкретно авторизация именно на рутрекере (причём даже через GCP на десктопе) — возможно, из статьи это не совсем понятно. Теперь у вас есть ещё один вариант на будущее.
            0
            Как раз с рутрекером и работаю =)
            Да, теперь есть ещё один отличный вариант, спасибо! Сам давно думал как безболезненно и без особых заморочек это сделать.
              0

              А, судя по логину, RuTracker.Поиск это ваше приложение. Классно. Могу предположить, что блокировка зависит от суровости провайдера или от точки выхода проксирования, определённой гуглом.


              У меня просто отваливается POST запрос авторизации к http://rutracker.org/forum/login.php — при этом через GET эта же страница отлично открывается. Думаю, что проблема именно в точке выхода, которая где-то в РФ расположена и не может получить доступ к ресурсу. Ведь я общаюсь с прокси через SSL, и провайдер не может знать, что я запросил...

                0
                при этом через GET эта же страница отлично открывается

                Тогда почему вы решили что проблема с блокированием запроса?

                И что именно отваливается при POST запросе?
                  0

                  При POST запросе приходит тупо ERR_CONNECTION_REFUSED. А если через приложение, то это выглядит как 502 Bad Gateway.
                  Возможно, это рутрекер не любит и блокирует конкретно мою выходную точку Google Compression Proxy.
                  При этом тот же код с тором отлично работает.

          0
          -
            0
            А почему вы не используете pure-java клиент для тор, например Orchid?
              0

              Нашёл только этот самый Orchid. При этом на его официальном сайте нет обновлений с 2013 года, документации не нашёл вообще никакой, релизы датируются тем же 2013 годом, на issues и pull requests никто не реагирует, есть только невнятные коммиты полгода назад. В общем, выглядит как плевок в душу. Впрочем, большая часть документации по тору выглядит примерно так. По сравнению с этими ребятами, моя исходная библиотека выглядела гораздо более внятно. Кроме того, не вижу причин использовать чистую Java вместо Native Library. Есть ли заметный прирост в производительности — вопрос спорный. Но наверняка не медленнее.


              Ну и непонятно было, насколько Orchid заведётся в Android. Java-то java, но есть нюансы. Было проще взять решение, которое заведомо будет работать.

            Only users with full accounts can post comments. Log in, please.