Приветствую, Хабр!

Я не претендую на срывание покров или какой-то революционный способ, но данный метод позволит как минимум сохранить ту часть трафика, так преданного вашему проекту/сайту/блогу, и немного вернуть справедливость со всеми этими перипетиями с массовыми блокировками.

TL;DR


Суть способа в обыгрывании возможности Service Worker'ов проверять контент на подконтрольных ему страницам. Если воркер не находит определённого текста на странице — происходит редирект. Таким образом вместо заглушки провайдера о том, что сайт заблокирован пользователь переходит на незаблокированный домен.

Этап 1


Итак, для приготовления нам понадобится всего ничего:

  • сайт, который (пока ещё) не заблокирован;
  • источник, который при запросе на него будет выдавать URL на новый, незаблокированный ресурс (о них немного позже);
  • JS файл — сервис-воркер, который мы будем использовать по прямому назначению, а именно, если руководствоваться статьёй:
    Одной из важнейших проблем, от которой страдали пользователи веб-приложений, была работа в условиях потери связи

Начнём, пожалуй, с основы нашего воркера — переменных и констант:

// DEBUG_MODE - при true будет выводить в console log некоторые результаты выполнения наших функций
const DEBUG_MODE = false;
const DNS_RESOLVER_URL = "https://dns.google.com/resolve?type=TXT&name=";

var settings = {
    enabled: 1,
    block_id: "<!-- RKN-BLOCK-URANUS-PLS -->", // Часть контента, при отсутствии которого наш воркер будет считать, что страница заблокирована
    redirect_url: "//google.com", // Fallback URL, если не нашли настроек для текущего домена
    dns_domains: ["subdomain.somesite.com", "subdomain.somesite.ru"] // Наши домены, в DNS ТХТ-записях у которых хранятся наши настройки. Если что-то случится с одним - воркер проверит на другом и далее по списку
};

var redirect_params = {
    utm_term: self.location.hostname+'_swredir' // Исключительно для удобства добавляем ко всем редиректам utm_term, чтобы было понятно откуда и сколько мы спасли людей
};

Установим event'ы fetch и install. Очевидно, это та «база» которая будет выполнять необходимые действия при установке воркера и каждом отдельном запросе к подконтрольным сервис воркеру ресурсам:

self.addEventListener("install", function () {
    self.skipWaiting();
    checkSettings();
    log("Install event");
});

self.addEventListener("fetch", function (event) {
    if (event.request.redirect === "manual" && navigator.onLine === true) {
        event.respondWith(async function() {
            await checkSettings();
            return fetch(event.request)
                .then(function (response) {
                    return process(response, event.request.url);
                })
                .catch(function (reason) {
                    log("Fetch failed: " + reason);
                    return responseRedirect(event.request.url);
                });
        }());
    }
});

Как вы заметили, в этой части мы используем функцию checkSettings(), с помощью которой мы и получаем набор настроек для домена, которые мы будем хранить в DNS TXT-записи того же или любого другого домена.

Конкретно в моём варианте используется текстовая версия DNS-резолвера от Google, но, возможно, вы сможете придумать что-то лучше. Пишите в комментарии.

function checkSettings(i = 0) {
    return fetch(DNS_RESOLVER_URL + settings.dns_domains[i], {cache: 'no-cache'})
        .then(function (response) {
            return response.clone().json();
        })
        .then(function (data) {
            return JSON.parse(data['Answer'][0]['data']);
        })
        .then(function (data) {
            settings.enabled = data[1];
            settings.block_id = (data[2]) ? data[2] : settings.block_id;
            settings.redirect_url = (data[3]) ? data[3] : settings.redirect_url;
            settings.last_update = Date.now();
            log("Settings updated: " + JSON.stringify(settings));
            return true;
        })
        .catch(function (reason) {
            if (settings.dns_domains.length - 1 > i) {
                log("Check settings on other domains DNS TXT: " + reason);
                return checkSettings(++i);
            } else {
                settings.enabled = 0;
                log("Settings error: " + reason);
                return false;
            }
        });
}

Как видно из функции checkSettings — мы обращаемся непосредственно к API DNS-резолвера гугла, дабы получить наш набор настроек. Что же наш воркер ожидает увидеть?
Набор параметров в виде JSON:
{"1": 1, "2": "<!-- RKN-BLOCK-URANUS-PLS -->", "3": "https://notblocked.ru"}
, где 1 — это параметр «enabled», которым мы указываем редиректить или нет в случае недоступности искомого контента на странице, 2 — собственно, сам искомый текст, 3 — домен, на который будем перенаправлять пользователя в случае отсутствия текста.

Осталось дело за малым — подключить наш воркер на всех страницах нашего сайта:

<script>navigator.serviceWorker.register('/rp-sw.js');</script>

Эпилог


Итак, наш сайт пока не заблокирован, DNS-записи готовы, SW подключен.

Мы в полном обмундировании готовы встречать блокировку.

И, конечно же, выкладываю полный вариант моего воркера:

rp-sw.js
// DEBUG_MODE - при true будет выводить в console log некоторые результаты выполнения наших функций
const DEBUG_MODE = false;
const DNS_RESOLVER_URL = "https://dns.google.com/resolve?type=TXT&name=";

var settings = {
    enabled: 1,
    block_id: "<!-- RKN-BLOCK-URANUS-PLS -->", // Часть контента, при отсутствии которого наш воркер будет считать, что страница заблокирована
    redirect_url: "//google.com", // Fallback URL, если не нашли настроек для текущего домена, то куда будем редиректить если enabled: 1
    dns_domains: ["subdomain.somesite.com", "subdomain.somesite.ru"] // Наши домены, в DNS ТХТ-записях у которых хранятся наши настройки
};

var redirect_params = {
    utm_term: self.location.hostname+'_swredir'
};

function getUrlParams(url, prop) {
    var params = {};
    url = url || '';
    var searchIndex = url.indexOf('?');
    if (-1 === searchIndex || url.length === searchIndex + 1) {
        return {};
    }
    var search = decodeURIComponent( url.slice( searchIndex + 1 ) );
    var definitions = search.split( '&' );

    definitions.forEach( function( val, key ) {
        var parts = val.split( '=', 2 );
        params[ parts[ 0 ] ] = parts[ 1 ];
    } );

    return ( prop && params.hasOwnProperty(prop) ) ? params[ prop ] : params;
}

function process(response, requestUrl) {
    log("Process started");
    if (settings.enabled === 1) {
        return response.clone().text()
            .then(function(body) {
                if (checkBody(body)) {
                    log("Check body success");
                    return true;
                }
            })
            .then(function (result) {
                if (result) {
                    return response;
                } else {
                    log("Check failed. Send redirect to: " + getRedirectUrl(settings.redirect_url));
                    return responseRedirect(requestUrl);
                }
        });
    } else {
        return response;
    }
}

function checkBody(body) {
    return (body.indexOf(settings.block_id) >= 0);
}

function checkSettings(i = 0) {
    return fetch(DNS_RESOLVER_URL + settings.dns_domains[i], {cache: 'no-cache'})
        .then(function (response) {
            return response.clone().json();
        })
        .then(function (data) {
            return JSON.parse(data['Answer'][0]['data']);
        })
        .then(function (data) {
            settings.enabled = data[1];
            settings.block_id = (data[2]) ? data[2] : settings.block_id;
            settings.redirect_url = (data[3]) ? data[3] : settings.redirect_url;
            settings.last_update = Date.now();
            log("Settings updated: " + JSON.stringify(settings));
            return true;
        })
        .catch(function (reason) {
            if (settings.dns_domains.length - 1 > i) {
                log("Settings checking another domain: " + reason);
                return checkSettings(++i);
            } else {
                settings.enabled = 0;
                log("Settings error: " + reason);
                return false;
            }
        });
}

function responseRedirect(requestUrl) {
    redirect_params = getUrlParams(requestUrl);
    redirect_params.utm_term = self.location.hostname+'_swredir';

    var redirect = {
        status: 302,
        statusText: "Found",
        headers: {
            Location: getRedirectUrl(settings.redirect_url)
        }
    };

    return new Response('', redirect);
}

function getRedirectUrl(url) {
    url += (url.indexOf('?') === -1 ? '?' : '&') + queryParams(redirect_params);
    return url;
}

function queryParams(params) {
    return Object.keys(params).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])).join('&');
}

function log(text) {
    if (DEBUG_MODE) {
        console.log(text);
    }
}

self.addEventListener("install", function () {
    self.skipWaiting();
    checkSettings();
    log("Install event");
});

self.addEventListener("fetch", function (event) {
    if (event.request.redirect === "manual" && navigator.onLine === true) {
        event.respondWith(async function() {
            await checkSettings();
            return fetch(event.request)
                .then(function (response) {
                    return process(response, event.request.url);
                })
                .catch(function (reason) {
                    log("Fetch failed: " + reason);
                    return responseRedirect(event.request.url);
                });
        }());
    }
});

Комментарии (33)


  1. Furriest
    16.09.2021 19:47
    +6

    Может я не въезжаю, сорри. Но не понимаю логику работы. Service worker подключен к заблокированному сайту example.com. Пользователь открыл у себя новое окно браузера, набрал example.com, ТСПУ перехватило запрос в DNS и HTTP-реквест и заблокировало их, а в клиента плюнула HTTP 302 на картинку блокировки. Откуда у клиента в его браузере возьмется service worker, чтобы определить, что блокировка произошла, и переадресовать клиента на новый сайт?
    Или все клиенты сначала должны зайти на сайт до блокировки и закэшировать у себя воркер?


    1. xjukebox Автор
      16.09.2021 19:56
      +1

      Да, клиенты должны посетить хотя бы раз сайт до блокировки.


      1. Anrikigai
        16.09.2021 20:24
        +1

        Все-таки можно поподробнее?

        Вот я посетил сайт, закрыл браузер со всеми вкладками, открыл заново, набрал руками адрес...

        Вместо, чтобы полезть на запрошенный сайт с заглушкой, браузер запустит скрипт из кеша?


        1. xjukebox Автор
          16.09.2021 20:36
          +4

          • Посетили сайт

          • В кэш подгружается воркер, который при каждом последующем посещении будет проверять есть ли на странице текст "Х" и если его нет - перенаправлять на другой домен, которого нет в РКН

          • Если при следующем посещении сайта ваш провайдер будет пытаться вас перенаправить или показать заглушку о том, что сайт заблокирован - сработает сценарий из предыдущего пункта


          1. hw_store
            16.09.2021 23:42

            "на другой домен, которого нет в РКН" - на какой такой другой домен?
            Что вообще в данном контексте подразумевается под словом "домен"?
            Если вообще не использовать в браузере доменные имена, а набирать IP-адрес цифрами, то... ?
            ...например, когда я набираю адрес своего сайта, то там открывается сайт
            ...а когда набираю адрес сайта одной организации, не так давно признанной самизнаетечем, то там выскакивает заглушка, видимо, хостера - "direct IP access not allowed"


            1. xjukebox Автор
              17.09.2021 10:27

              Допустим у вас есть site1.com, который пока доступен в РФ.

              Рассмотрим несколько вариантов блокировки и пути их решения с помощью редиректа воркером:

              • РКН может заблокировать отдельную страницу на сайте https://site1.com/index.html (редиректим посетителя на https://site1.com/index_not_blocked.html)

              • Домен/поддомен без блокировки по маске site1.com или sub.site1.com (редиректим на любое другое "зеркало", подойдёт и поддомен notblocked.site1.com)

              • Домен и все поддомены заблокированы *.site1.com (редиректим на site2.com, поддомены site1.com не подойдут)


              1. hw_store
                17.09.2021 13:39

                Я вот чего не понимаю. Блокировка, если я правильно понимаю, происходит на этапе запроса к DNS. Если скрипт, анализируя возвращаемый ответ, обнаруживает блокировку, то он подменяет доменное имя в запросе, так? Однако заблокированным может оказаться любой из доменов перечисленной в Вашем ответе структуры, и даже все домены - ведь они статически прописываются как переменные в скрипте при его первоначальной загрузке с незаблокированного сайта.


                1. grumbler66rus
                  22.09.2021 01:31
                  +1

                  Блокировка, если я правильно понимаю, происходит на этапе запроса к DNS.

                  Вы неправильно понимаете.
                  Подмена DNS на сервере DNS провайдера — это только один из многих, причём устаревший, метод блокировки, который обходится тривиально, использованием публичного или своего собственного сервера DNS. (Да, я знаю про DNS spoofing, это выявляется стандартным образом, браузеры уже умеют с ним бороться.)
                  Все без исключения крупные операторы интернета используют DPI для выявления запросов к запрещённым цензурой ресурсам.


              1. Yuribtr
                18.09.2021 09:18

                На данный момент РКН не может блокировать отдельную страницу на сайте, который работает через https. Поэтому РКН блокирует доступ ко всему сайту.


          1. aamonster
            17.09.2021 08:57
            +1

            А зачем проверять наличие текста? Сервис воркеры ж работают только на https, так что в случае перенаправления/заглушки – сайт будет просто недоступен.

            Осталось от отладки на localhost или готовитесь к MiTM-сертификату от РКН?


            1. xjukebox Автор
              17.09.2021 09:22
              +2

              Воркер «подселяется» в кэш только с HTTPS, но содержание может проверять с любой, подконтрольно ему, страницы.

              Использовал этот метод для своей сети сайтов со спорным содержанием. Фича спасла мне много нервов и времени, поэтому решил поделиться. :)


              1. aamonster
                17.09.2021 09:51

                Ему разве подконтрольны не только страницы с того же хоста и протокола? Хотите сказать, что если подселился с https://example.com, то и для http://example.com будет работать?


                1. xjukebox Автор
                  17.09.2021 10:19

                  Извиняюсь, да, вы правы. При запросе HTTP воркер не работает. Только вот провайдеру переадресацию в случае блокировки нужно будет делать именно с HTTPS, а значит переадресация произойдёт.


                  1. aamonster
                    17.09.2021 11:10

                    Погодите. Как провайдер умудрится сделать переадресацию https? Он же без сертификата не может подменить ответ сервера, будет показываться лишь ошибка в браузере.


                    1. xjukebox Автор
                      17.09.2021 11:16

                      Всё так. Если не установлен сервис воркер провайдер попытается сделать переадресацию и в браузере клиент увидит ошибку "PR CONNECT RESET ERROR", если же воркер установлен - произойдёт редирект на незаблокированный сайт.


      1. ctpayc
        24.09.2021 14:13

        А что если сделать расширение для браузера? Вести базу с заблокированными + альтернативными доменами:

        • перехватываем запрос

        • ищем альтернативный адрес

        • перенаправляем

        • профит


  1. Bonio
    16.09.2021 20:24
    +2

    У меня эта идея давно в голове вертелась. У нее есть минус, это будет работать только для тех, кто уже посещал сайт до блокировки.


  1. Sabin
    16.09.2021 20:43
    +1

    Думаю, что при блокировке хоть раз да проверят, сработала ли она. А тут прямо на тестовом оборудовании сразу редирект и произойдет. +1 адрес в список и смысл этого воркера теряется


    1. xjukebox Автор
      16.09.2021 20:48
      +1

      Это уже нюансы, с которыми нужно работать отдельно.

      Как показывает практика - боты РКН при посещении сайта не подгружают ничего, кроме исходного кода страницы. Соответственно о посещениях таких сайтов с полноценным WebDriver'ом, который ещё и с сервис-воркерами полноценно работает не может идти и речи.


      1. JerleShannara
        17.09.2021 02:04
        +1

        Интересно, а с каким user-agent они по сайтам гуляют, друг очень интересуется…


        1. Saiv46
          17.09.2021 05:29

          Мы не знаем, но сайт Умного Голосования при использовании wget перенаправляет на сюрприз. Вероятно это было для РКН или провайдеров что таки доступность сайтов перед блокировкой.


        1. xjukebox Автор
          17.09.2021 10:36
          +1

          Иногда без явно указанного агента, но в частоте случаев такие варианты:

          • Mozilla/5.0 (Windows NT 5.1; rv:35.0) Gecko/20100101 Firefox/35.0

          • Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0

          Извините, больше расписать не могу по понятным причинам.


  1. Popadanec
    16.09.2021 20:47

    А как чпда работает? Его же до сих пор не разблокировали.


    1. dartraiden
      17.09.2021 04:04

      Они сменили домен с 4pda.ru на 4pda.to, никакой магии.


    1. BigTurtle
      27.09.2021 15:18

      Очень просто - они переехали на другой адрес в домене .to


      1. Popadanec
        28.09.2021 12:10

        Но для пользователей то это произошло прозрачно. Не нужно было искать в поисковике. Даже сейчас идя по ссылке чпдаточкару, попадаешь на новый адрес.


  1. Aleksandr-JS-Developer
    16.09.2021 21:14
    -3

    web он такой web...


  1. anonymous
    00.00.0000 00:00


  1. staticmain
    17.09.2021 00:07

    У всено вышеописанного есть проблемы. У части провайдеров часть сайтов заблокирована по https, из-за чего браузер не показывает заглушку, он говорит, что домен Х, а сертификат от домена провайдера поэтому туда мы не пойдем. И только если разрешить переход на сайт - то будет уже заглушка, но при этом на домене сайта.

    Другие провайдеры делают редирект, поэтому при тексте заглушки сразу будет домен провайдера, домена сайта не будет, поэтому воркер не будет знать куда редиректить.


    1. xjukebox Автор
      17.09.2021 08:11

      Редирект провайдера происходит с заблокированного сайта. Поэтому прежде чем провайдер отправит клиента на свою страницу - на вашей заблокированной будет 307 редирект с пустым ответом. А это значит, что воркер и в этом случае не сможет найти искомый текст и отправит клиента не на страницу с ошибкой и не на страницу с заглушкой, а на тот домен, который будет указан в settings.


  1. Vilgelm
    17.09.2021 03:39

    Может быть тогда не редирект делать, а просто подгружать контент с другого домена в фрейме?


  1. dartraiden
    17.09.2021 04:02
    +2

    Таким образом вместо заглушки провайдера о том, что сайт заблокирован пользователь переходит на незаблокированный домен.
    Зачастую вместо заглушки пользователь получает тупо PR CONNECT RESET ERROR.


    1. xjukebox Автор
      17.09.2021 08:06

      Даже если не будет заглушки - воркер не сможет найти искомый контент на сайте и переадресует клиента на новый домен. Попробуйте :)