воскресенье, 26 августа 2018 г.

Каким должен быть протокол для мессенджера

В последнее время пришлось неоднократно сталкиваться с протоколами мессенджеров изнутри и участвовать в обсуждениях, а каким, собственно, должен быть безопасный, удобный, нецензурируемый протокол для массового мессенджера.


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

Давайте определим, какие, собственно, требования мы хотим видеть выполненными в протоколе и реализации нашего идеального мессенджера. Дабы кто-нибудь вменяемый мог учесть эти пожелания и реализовать, наконец, в софте то, что никто не сделал.
  1. Мессенджер должен без всяких дополнительных телодвижений самостоятельно выбираться из-за NAT любой сложности, а также самостоятельно траверсить любые виды прокси-серверов. Данным свойством в настоящее время не обладает почти ни один мессенджер, за исключением Tox.
  2. Бутстрап и начальная идентификация должны быть цензуроустойчивыми, защищенными от блокирования, проходить по E2E каналу, поддерживать торификацию и вход посредством любого прокси. Идентификация не должна привязываться к мобильному или иному номеру в принципе. Такая возможность должна быть опциональной и она не должна быть основной или заданной по умолчанию.
  3. В идеале протокол мессенджера должен быть децентрализованным, по образу P2P. В случае, если протокол не P2P, сервера должны быть размазаны по множеству сетей IP, не должны тривиально идентифицироваться, и не должны видеть какие угодно данные пользователей. Собственно, я предполагал некие генерируемые FQDN во множестве доменов второго и первого уровня, например, с хэш-функцией в имени хоста сервера, однако не выглядящие как хакерские визуально. Только IP-адреса серверов - скверная идея, сети элементарно идентифицируются и накрываются. Также точки входа палятся по отсутствию SNI (правда, в TLS 1.3 SNI обязателен, так что Телеграм и дальше будет палиться своими точками входа без SNI и легитимных сертификатов в connection probing), поэтому SNI должен присутствовать.
  4. Полный E2E. Протокол физически не должен иметь никаких третьих сторон, которые могут видеть передаваемые данные. Причем E2E должен распространяться также на медийный и файловый контент, передаваемый по каналам связи - говоря простым языком, никаких CDN с TLS для контента быть не должно. Все передается исключительно в защищенном E2E канале.
  5. Обновления проверяются и передаются по защищенному E2E-каналу.
  6. Протокол не должен иметь фиксированных портов, что облегчает блокировку до предела. В идеале протокол должен иметь возможность работать по любому свободному порту.
  7. Протокол не должен иметь идентифицируемой сигнатуры. То есть должна поддерживаться обфускация или полная мимикрия под известный широкораспространенный протокол, например, HTTPS. Мимикрия должна быть совершенной - то есть соединение должно быть устойчиво к connection probing. Иначе говоря, если это мимикрия под TLS - сервер должен корректно устанавливать TLS-сессию, отвечать легитимными (подписанными честными рутами) FQDN, а сертификаты и их подписи должны быть вне каких бы то ни было подозрений.
  8. Протокол не должен использовать DNS-over-HTTPS. Как я писал ранее, HTTPS не является E2E, и может быть использован для идентификации протокола в случае использования MiTM. Протокол вообще не должен зависеть каким либо образом от DNS либо основываться на DNSSEC - и это должно быть обязательное требование. Некоторые айтишники справедливо указывали на DANE.
  9. Handover - в случае использования серверов, при отказе connection к текущей группе серверов (не одиночного сервера!) соединение должно переходить на новые точки автоматически, подгружая их, скажем, с серверов authority/guards/directory - как это делает Тор. Это обеспечивает, с одной стороны, отказоустойчивость, а с другой - устойчивость к цензурированию в реальном времени. С другой стороны, конечное число точек входа позволяет цензурировать бутстрап. В этой связи handover должен поддерживаться серверами на основе, скажем, постоянного keep alive к клиентам, что позволит обнаруживать потерю соединений в результате нарушения связности сети и восстанавливаеть его. В идеале реализована идея группы супернод (как в первоскайпе), когда нет фиксированных конечных authority/guards/directory серверов и любой сервер точки входа может выполнять такую роль с handover функций между ними.
  10. Финальное требование таково. В идеале, протокол работает по 443 порту, использует как наружную обертку абсолютно легитимное HTTPS-соединение, с честными сертификатами на стороне сервера, честными SNI, и, в случае MiTM, протокол неотличим от честного HTTPS (обфускация под настоящий HTTPS,  с заголовками и содержимым). Обфусцированный под HTTPS туннель на самом деле является честным E2E и не поддается атаке MiTM ни при каких обстоятельствах. Протокол не имеет конечных и однозначно идентифицируемых IP-сетей/диапазонов либо однозначно идентифицируемых FQDN (при использовании MiTM). Bootstrap распределен и устойчив к площадным блокировкам. Протокол не поддается классификации и не имеет четких сигнатур на транспортном уровне и фиксированных портов.
Безусловно, мессенджер должен поддерживать внешнюю проксификацию/соксификацию, чтобы его можно было направить в любой внешний по отношению к приложению туннель.  Но это уже пожелание к реализации собственно мессенджера, нежели собственно протокола. Как я писал ранее, весьма немногие мессенджеры (включая заявленные как безопасные) имеют такую возможность в принципе.

Понятно, что некоторые требования из списка выше труднореализуемы в одном флаконе, либо требуют значительных инфраструктурных затрат. Однако эти требования обобщены многолетним наблюдением на прокси за существующими мессенджерами, сетью Тор и опытом реальной эксплуатации мессенджеров в проксированных инфраструктурах.

Прошу заметить вот что. Частичная реализация не имеет смысла. Реализовывать такое надо полностью - это раз, и два - это должна быть массовая реализация (пепел Matrix/Riot/Tox/Bitmessage стучит в наши сердца), а три - HTTPS/TLS должен использоваться исключительно как уровень прикрытия и ничего более.

среда, 15 августа 2018 г.

Squid 5: make things faster - part 1

В этой статье я хочу поделиться некоторым опытом промышленной эксплуатации Squid 5 и его оптимизационных трюков. Частично это касается и предыдущих веток.


Я хочу прежде всего рассмотреть некоторые аспекты масштабирования, производительность тут лишь небольшая часть имеющихся проблем.

Память и IO

Вообще, сквид исторически имеет очень неважные дефолтные параметры. Произошедшие из 90х - начале 2000х. В силу этого ванильный сквид очень плохо масштабируется. Не все можно изменить штатными конфигурационными параметрами.

Одна из скверно написанных подсистем - управление памятью и ввод-вывод.

Я уже писал про память в Squid. Проблема в том, что управление памятью в сквиде - лоскутное одеяло, очень фрагментарное, без единой архитектуры. Используются и пулы (местами и очень скверно), и страничное выделение - очень маленькими страницами (в 2018 году страница 4096 байт для сервиса, предполагающего неограниченное масштабирование), и общие области памяти, ну и, вишенка на торте - память SSL управляется openssl (вообще без всякой связи с общей идеологией управления памятью). Накладывается на все это тот факт, что в коде посейчас раскидана масса int, с присущим этому типу ограничением, что, в случае 64битный сборки приводит к неожиданным 32битным лимитам в самых неожиданных местах (вплоть до антипереполнения при подсчете статистики). Есть до сих пор и неочевидные места утечек, и внезапные segfaults из-за повреждений памяти. Начисто отсутствует такая вещь, как GC. То есть память, единожды выделенная, не освобождается уже никогда - даже если она не нужна.

Все вышеописанное приводит к высокой фрагментации оперативной памяти при мало-мальски длительной работе сервиса (что, в свою очередь, как следствие приводит к безумным величинам потребления памяти процессом Squid), впрямую бьет по производительности, и, по факту, требует серьезного рефакторинга (как минимум).

Причем, что самое характерное, конфигурационными параметрами это не лечится.

Также это не лечится простой линковкой какого-нибудь стороннего аллокатора, наподобие tcmalloc.

Вторая сторона медали - IO. Внутренний и внешний ввод-вывод в сквиде выполняется через буферы heap (что само по себе достаточно дорого), но проблема в том, что эти буферы крошечные по умолчанию.

Как следствие, мы имеем высочайшие показатели физических операций ввода-вывода - на всех уровнях. И, прежде всего, на уровне дискового IO.

Некоторые коллеги пытаются залить проблему деньгами, покупая под cache_dir SSD. Однако следует понимать, что SSD смертны. Самое неприятное - они внезапно смертны. И, хотя потеря кэша - это не потеря БД, время восстановления - скверная штука. Да и стоимость таких решений - гм......

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

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

Так как нам до смерти надоело иметь перманентные проблемы с оперативной памятью, мы с коллегой подошли к решению вопроса радикально.

Во-первых, мы нашли хороший аллокатор пулов с алгоритмической сложностью O(1). Мы встроили его в пятый сквид и подчистили подавляющее большинство вызовов xalloc(), приведя управление памятью к чанкам размерами 64K по умолчанию. Также мы задействовали GC данного аллокатора, и каждые несколько секунд собираем мусор. Это практически решило проблему неограниченно растущей памяти (с фрагментацией отдельная песня, полностью простой заменой аллокатора эта проблема не решается).

Во-вторых, мы полностью отключили нативные пулы Сквид:
 memory_pools off  

Да, мы абсолютно уверены, что наша malloc library outperforms Squid routines.

Пулы не нужны - используемый нами аллокатор сам по себе pool-based, причем делает это единообразно и значительно лучше, чем ванильный Сквид.

В-третьих, мы прошлись по коду и заменили все опасные int, связанные с памятью (и вообще с объемами) на long - мы работаем в 64 битах и не хотим налетать на внезапные дампы, связанные с переполнением int.

В-четвертых, мы задали достаточный размер кластера на файловых системах - кто, скажите на милость, в 2018м на терабайтных дисках использует кластер 4к?


Понятное дело, что, в случае использования hardware RAID придется плясать от параметров RAID, но их тоже можно и нужно настраивать (сюрприз! правда, не в таких широких пределах, как ZFS).

В-пятых (но не в последних), мы полезли в defines.h и нашли вот такой параметр:


Этот параметр достаточно серьезно влияет на большинство операций IO (и затрагивает массу модулей) и он привязан к размеру страницы памяти аллокатора сквида. Так как мы уже заменили аллокатор, мы можем смело увеличить этот параметр до величины, равной размеру нашего чанка памяти:


И пересоберем код, в процессе удивившись, как достаточно много модулей затрагивает этот параметр (это и модули дискового IO, и управление памятью, и много чего еще).

Результаты наших каторжных интеллектуальных трудов вот:


Значительно (почти вдвое) уменьшилась общая память процесса за счет уменьшения фрагментации.


Заметно (в среднем на 30%) упало использование CPU. Уменьшилась латентность. И, самое главное, сильно снизилось число дисковых IO.

Q.E.D.

Другие пожиратели памяти

Это еще не все. Есть несколько неочевидных tunables, которые включены по умолчанию, расходуют ресурсы (память и циклы CPU - иногда весьма значительно), но в которые никто не желает вникать.

Это ошибка.

Если мы говорим о масштабировании, то дьявол часто кроется именно в таких деталях.
  1. Сиблинги. Они весьма часто используются при построении масштабируемых конфигураций, однако мало кто знает, что периодическое построение дайджестов - тяжелый по CPU и памяти процесс, который включен по умолчанию даже тогда, когда вы его не используете:
  2.  # Default is on  
     digest_generation off
    
  3.  client db. Это пожиратель памяти, которая не будет освобождаться, и который, по большому счету, вам не нужен - вы и так используете статистические отчеты по access.log. Он может быть вам полезен, если вы олдфаг с непрозрачным транспарентным прокси, авторизациями, всякими протухшими обвязками по контролю и управлению пользователями и так далее.  По умолчанию он включен - а что, экономить память в XXI веке больше не нужно? Процессорные циклы тоже? Итак, сделаем это:
  4.  # TAG: client_db     on|off  
     #     If you want to disable collecting per-client statistics,  
     #     turn off client_db here.  
     #Default:  
     # client_db on  
       
     # Turn off collect per-client statistics  
     client_db off  
    

Как минимум, эти два параметра позволяют выиграть несколько сотен мегабайт (и это минимум на крупных инсталляциях) и снизить CPU на величину до 40%.

Еще одна деталь. Если вы используете рекурсивный кэширующий DNS (а вы наверняка его используете в масштабируемой конфигурации), вам не надо увеличивать от дефолта такой параметр, как ipcache_size (более того, его имеет смысл уменьшить):

 # TAG: ipcache_size     (number of entries)  
 #     Maximum number of DNS IP cache entries.  
 #Default:  
 # ipcache_size 1024    

Это забавно, но хороший тюнинг совсем не предполагает гигантских конфигов с non-default параметрами.

понедельник, 13 августа 2018 г.

Оптимизация регулярных выражений ECMAScript

Обычно и сами-то регулярные выражения - проблема для среднего айтишника (знаю не понаслышке, сам таким был когда-то ;)). Что уж говорить об их оптимизации - удалось написать чтобы работало и делало что задумано - и слава подземным богам, остальное уже не колышет. :)))))))))
Однако, когда от скорости обработки регэкспов зависит латентность или, того круче, масштабирование - приходится тягостно задумываться. Я уже не говорю о такой мерзкой вещи, как catastrophic backtrace, чтобы словить которую достаточно скормить относительно много текста относительно расширенному регулярному выражению с кучей жадных подвыражений.

Принято считать, что обработка регулярных выражений - штука тяжелая, медленная, и оптимизации не поддается.
Для того, чтобы поговорить предметно, давайте зайдем на regex101 и, для примера, рассмотрим три регулярных выражения:
 \:\/\/(.+?)\/.*\?[\w]+\=(.+?\.exe)  
 \:\/\/(.+?)\/[\w\/\-\.]+\?[\w]+\=(.+?\.exe)  
 \:\/\/([\w\-\.]+)\/[\w\/\-\.]+\?[\w]+\=(.+?\.exe)  

Все три выражения делают одно и то же. Они применяются к URL, содержащим исполняемый файл (обновление), и на выходе дают две группы - FQDN сайта и имя файла.

Можете выбрать URL, дающий match по данной регулярке, а потом посмотреть, сколько степов будет выполнено. Да, это достаточно условно, однако число шагов, определенных regex101, достаточно четко коррелирует с реальным числом шагов вашего движка (библиотеки) регулярных выражений.

Что вы увидите?

Что данные регулярные выражения имеют уменьшающееся число шагов сверху вниз (примерно в три раза - от 66 шагов до 22-23, в зависимости от обрабатываемого текста).

Если немного поднапрячься, и написать программку с таймингом, вы увидите заметное уменьшение времени выполнения по мере усложнения регулярного выражения.

Что это означает на пальцах?

На пальцах это означает следующее. Жадные выражения вида .*, применяемые к неопределенному тексту, дают бОльшее число шагов, так как проверить надо все. Выражения вида [\w]+, по логике эквивалентные выражениям .*, имеют мЕньшее число шагов просто за счет того, что на тот же самый объем текста накладывается мЕньшее число проверок. Соответственно, это выливается в более быстрое вычисление регулярного выражения.

Означает ли это, что надо всюду и везде заменять .* на \w+ ?

Нет, не означает.

Если вы поиграетесь еще немного, то обнаружите, что далеко не всегда такая замена означает выигрыш в числе шагов при обработке регэкспов. Нужно трассировать логику выполнения и смотреть, где у вас образуются тяжелые шаги. Зачастую проще выяснить это опытным путем, чем делать предположения об эффективности регулярных выражений применительно к заданному классу задач.

Более того, возможно, вы сильно удивитесь, однако POSIX regex как правило имеют прямую зависимость скорости от сложности - то есть, чем проще регэксп - тем быстрее и в меньшее число шагов он выполняется. А вот ECMAScript - сюрприз! - наоборот. Более простые регэкспы весьма склонны к разрастанию числа шагов по мере упрощения регэкспа и зачастую число шагов прямо пропорционально числу жадных подвыражений.

Это довольно логично, учитывая внутренности POSIX regex engines и ECMAScript. Но не слишком известно. ;)

Вы можете в этом месте хмыкнуть и заметить - "Преждевременная оптимизация - корень всех зол, пусть сервер пашет - он железянный, возьмем побольше процессоров и делов-то", НО.

Меньшее число steps при обработке регулярных выражений в критичных местах - это меньшее число циклов CPU. Меньше миллисекунд. А миллисекунды складываются в секунды, а секунды - это боль.

И, когда речь идет о выполняющихся с высокой частотой проверках (например, ACLs и тому подобное) - эти самые миллисекунды очень существенны. И иногда от них здорово зависит и латентность и масштабирование. Потому что далеко не всегда мы можем отказаться от использования регэкспов.

Finally. Да, регэкспы это боль и проблема сами по себе. Однако, если вы все же озаботитесь их оптимизацией - боли будет несколько меньше. ;)

вторник, 7 августа 2018 г.

Как засунуть верблюда в ангулярное отверстие

Двести метров джаваскрипта
Грузит текста триста байт
Я — элита программистов,
Не какой-то разъе*ай


Я уж думал, никогда этого не дождусь.

Ребята, вы еще access logs на прокси никогда не видели! Там ежедневный ужас.

Я никогда не забуду того дня, когда со Steam прилетел JS (один) размером 46 мегабайт. 46 мегабайт, Карл!

Первая мысль - самая правильная. Kill they with fire!

Но, так как мы не в состоянии дотянуться до худеньких горлышек ангулярщиков, все, что мы можем сделать - это постараться утоптать передаваемый клиенту объем текстообразного буллшита.


Вы не поверите, насколько можно улучшить латентность, просто используя правильный прокси, который умеет компрессию и кэширование компрессированного текстоподобного контента (да, я говорил - сжимать медиа не надо, "так верстают только мудаки").

Лирическое отступление. Только не говорите мне, пожалуйста, что прокси в 2018 году-де неактуальны, нинужны, айфоны рулят, и безлимитный интернет по оптике 1 гбит минимум есть в каждой африканской хижине. В вашем манямирке, это, может быть, и так. Но вы - это не весь мир.

Так что - пусть ваш интернет будет быстрым настолько, насколько это зависит от вас. Декомпрессия сжатого контента, по опыту, занимает меньше времени, чем передача несжатого барахла.

суббота, 4 августа 2018 г.

C++: Гадкий я и причуды memmove()

Компиляторы отражают придури их создателей. Усугубляемых придурями геевпрограммистов.


Нет, я серьезно.

Причуды GCC вкупе с одним legacy недавно отняли у меня неделю времени на разборки с Мамбуду.

История совершенно апокалиптическая.

Есть некий legacy - код. Изначально написанный на C. И, в настоящее время, переписываемый разработчиками противоестественной ориентации на C++11. В стиле C.

Внимательно следите за руками:
     newsize = fetch->bufofs - retsize;  
   
     memmove(fetch->buf, fetch->buf + retsize, fetch->bufofs - newsize);  
   
     fetch->bufofs = newsize;  
   
   } while (cbdataReferenceValid(fetch) && prevstate != fetch->state && fetch->bufofs > 0);  
   

Милый кусочек, вычерпывающий буфер обмена при помощи memmove(). Memmove использована по той причине, что адреса могут (иногда) перекрываться (И, если это C++11, то я - Мессалина, не меньше).

Компилятор GCC, однако, считает, что тут вместо memmove() должна быть _memcpy(). При любом уровне оптимизации. _memcpy() и баста.

С виду кажется, что тут и думать не о чем - аргументы memmove() таковы, что всегда dst < src и тут поведение компилятора стопроцентно соответствует стандарту (а тогда какого дьявола вообще memmove() написали? Тут вообще должен быть конструктор копирования, как выяснилось далее).

И вот этот фрагмент падает через нерегулярные промежутки времени в SEGFAULT прямо на _memcpy(). В зависимости от данных в буфере.

Вы думаете, надо написать свою собственную реализацию memmove(), которую компилятор не исковеркает в _memcpy() и выяснить, что вообще происходит?

Нет, технически это, конечно, можно попробовать, например, вот так (украдено в Бристоле; коряво, конечно, но работает после незначительных косметических правок; для опытов сойдет):

 void* Memmove(void* dst, const void* src, std::size_t cnt)  
 {  
      char *tmp = nullptr;  
      char *pszDest = (char *)dst;  
      const char *pszSource = (const char*)src;  
      // allocate memory for tmp array  
      tmp = (char *)malloc(sizeof(char) * cnt);  
      if (nullptr == tmp) {  
           return nullptr;  
      } else {  
           // copy src to tmp array  
           for (std::size_t uiLoop = 0; uiLoop < cnt; ++uiLoop) {  
                *(tmp + uiLoop) = *(pszSource + uiLoop);  
           }  
           // copy tmp to dst  
           for (std::size_t uiLoop = 0; uiLoop < cnt; ++uiLoop) {  
                *(pszDest + uiLoop) = *(tmp + uiLoop);  
           }  
           free(tmp); // free allocated memory  
      }  
      return dst;  
 }     

Только вот это ничего не изменило. То есть, все честно, dst < src, и данные корректны на первый взгляд - только вот дебаг дампа показывает, что memmove() двигает структуру, которая совсем не trivially copyable (Написавший этот код не мог не знать, что он по факту перемещает совсем не массив char по указателям) и падение происходит на строке

*(tmp + uiLoop) = *(pszSource + uiLoop)

  ---- called from signal handler with signal 11 (SIGSEGV) ------  
  [10] Memmove(dst = 0xfffffd7fc735ec78, src = 0xfffffd7fc735fafb, cnt = 3715U), line 417 in "peer_digest.cc"  
  [11] peerDigestHandleReply(data = 0xfffffd7fc735ec18, receivedData = CLASS), line 510 in "peer_digest.cc"  
   

Замечание: Не-а, std::memmove тут не катит - она реализована точно так же. Догадываетесь, почему хочется насрать в руки всем стандартоклепателям от C++ и программистам нетрадиционной ориентации, не видящим разницы между C и C++? Как быть вот с такими легаси? Переписывать до основания от начала до конца?

На самом деле вместо memmove() здесь должно быть что-то вроде:

 std::copy_backward(fetch->buf, fetch->buf + fetch->bufofs - newsize, fetch->buf + retsize);  

Или даже вообще вот так.

Это означает, что рукосуи, накорябавшие этот код, по факту набив его рудиментами C, сделали его беспредельно хрупким и зависящим от поведения других частей кода в других местах. Хотелось бы мне посмотреть в бесстыжие шары того геяпрограммиста, который написал такое. И в бесстыжие шары другого геяпрограммиста, который называет себя экспертом в C++ и пишет вот такой вот мутный ужас. Я все понимаю - пианист играет, как умеет. Но это уже за гранью добра и зла. На ровном месте неопределенное поведение целой фичи, и поведение программы в целом - хочу работаю, хочу в корку свалюсь на всем скаку. Она же, зараза, целиком валится!

То есть вместо того, чтобы по-человечески, сказав А - сказать Б, переписать типы, сделать нормальный ООП - а он тут действительно необходим, типу нужны конструкторы copy и move - они оставили все, как есть - авось, проканает.

А умный компилятор, которому non-POD тип пихнули по ссылке, естественно, позволил прострелить ногу к чертовой матери и запихнул type-unsafe memcpy(). Все путем. То, что данные могут поплыть в рантайме, что они вообще не тривиально копируемые - всем пофигу, а компилятору тем более. Компилятор здесь в упор не видит структурного типа, и не может его видеть, ему ссылку передают в memmove(). Он умный - но не настолько умный.

За такое написание кода....

Smear in your hands...

Нет, ребята. Вы либо трусы наденьте, либо крестик снимите. Не надо миксовать C с C++ - получается хреновый бармен. От которого тошнит.

А к вам, программистам нетрадиционной ориентации, хочется прийти с факелами, вилами, и в белых балахонах с прорезями для глаз.

PS. В финале пришлось использовать вот такую конструкцию:

 std::move_backward(fetch->buf, fetch->buf + fetch->bufofs - newsize, fetch->buf + retsize);  

конкретно в моей реализации GCC std::copy_backward дает сильную просадку по перфомансу, с выраженными пиками на ядрах процессора (дополнительный issue), что приводило к отдельным проблемам. std::move_backward решает их все.

пятница, 3 августа 2018 г.

Интернет-цензура: назад, в будущее


Disclamer
Целью статьи является по-возможности объективное рассмотрение интернет-цензуры с точки зрения технических возможностей, в том числе малоизвестных. Незнание данных возможностей приводит к распространенным заблуждениям среди IT-специалистов, разработчиков, пользователей, а также к ошибочным техническим и нетехническим решениям при противодействии цензуре, многие из которых уже невозможно легко исправить. В этой связи считаю необходимым озвучить принципы, подходы и решения, большинство из которых широко применяются на практике.

Прошлое

Вообще говоря, по большому счету цензура как таковая - если мы определяем ее как принудительное ограничение доступа к ресурсу - появилось еще во времена FIDO. Термин "бан" восходит еще к тем самым временам, а интернет его всего лишь унаследовал.

Не вдаваясь в совсем уж далекие подробности, в ФИДО был даже термин "экскоммуникация" - то бишь исключение нарушителя из сети обмена.

По существу, праматерь всех цензур появилась еще во времена ARPANET, на заре его существования, в виде ограничения доступа по IP для uucp-клиентов. :)

Если говорить совсем по большому счету, то цензура (а точнее, ограничение доступа в том или ином виде) существует с двух трех сторон:

  • Со стороны серверов - для предотвращения доступа, например, из сети Тор (блокировка выходных нод), для отдельных стран, для отдельных - и хорошо известных - сетей или фиксированных адресов, для предотвращения атак или несанкционированного доступа; делается это средствами firewalls или htaccess - хорошо знакомый веб-мастерам метод
  • Со стороны инфраструктуры локальных сетей - файрволлы применяются для этого с незапамятных времен
  • Со стороны клиентов - торрентщикам хорошо известна фильтрация по IP
То есть пока что мы не говорим о цензуре, как таковой, верно? Обычные админские дела, связанные с безопасностью и так далее.

В случае статичной сети, со статичными адресами, все достаточно просто и хорошо известно. "Я вычислю тебя по IP..... и забаню!" :)

Практически сразу выяснилось, что интернет полон не только полезных вещей, а также практически сразу обнаружилась способность злонамеренных сайтов менять адреса - и появились прокси-сервера.

Помимо того факта, что интернет, оказывается, тоже имеет физические пределы (и законы природы на него распространяются), оказалось, что IP-адрес вовсе не такой уж статичный, это не почтовый адрес с пропиской. И блокировка по IP хороша лишь поскольку-постольку.

Следует учитывать тот факт, что обычно firewalls располагались на сетевом оборудовании и не имели (да и сейчас не имеют) функции DNS resolving при каждом обращении к правилу. Что логично - firewall работает на уровне пакетов (это важно, мы к этому вернемся, когда будет рассматривать DPI), что требует, в свою очередь, высокой скорости обработки. А DNS-resolve не обязан быть быстрым, более того, он не обязан быть оперативным. TTL мешает. Самая, однако, важная проблема firewall заключается в том, что ресолв он делает один раз (когда и если мы задаем доменные имена, а не IP-адреса). При загрузке правил. То есть - сменился у жертвы цензуры IP-адрес - все, мимо денег. Файрвол об этом не знает до следующей загрузки конфигурации.

Нет, конечно, сейчас уже есть навороченные файры, которые ресолвят имена, кэшируют DNS-ответы, считают TTL, обновляют FQDN- и IP-кэши, но это уже в большей степени прокси-сервера, а не файрволы в классическом исполнении.

Первыми цензорами - и на весьма долгое время - таким образом, стали администраторы прокси-серверов, подводящих под свои действия сперва собственный здравый смысл и физические ограничения Интернета, а потом бизнес-правила организаций.

Однако об этом позже, а сейчас вернемся в девяностые.

В те времена такая блокировка была достаточно эффективна и даже действовала. Если пакеты просто втихоря сбрасывались, то можно было достаточно долго не знать о факте бана. Каменные лица были хорошим ответом на вопрос - а есть ли блокировка? "Ничего не знаем, оно само сломалось, претензии предъявляйте к сервису, идите лесом".

Это одинаково эффективно работало в обе стороны - и со стороны серверов, и со стороны клиентов, идущих через трансграничный файрвол.

Однако, появились CDN. Как я уже говорил выше, Интернет подчиняется физическим законам, поэтому - сперва раздача обновлений Windows и антивирусов, а затем и все остальные, потянулись в CDN. Практически сразу начало подпирать исчерпание адресного пространства IPv4, началась масштабная и непрерывная ротация адресного пространства, повсеместное применение NAT.

Что привело к тому, что прежняя блокировка по IP перестала быть эффективной. Случилось это примерно в начале 2000х (чувствуете дух старины? Некоторые просто любители сушеных мумий). И с этих пор прогрессивное человечество, для организации ограничений доступа в энтерпрайзе, да и просто в Интернете, в основном использует проксирование.

Настоящее

Проксирование как таковое является экзотикой только в странах СНГ, причем не всех, а некоторых особо зажравшихся тех, где существует какое-то подобие безлимита и быстрых каналов (причем это не затянется навсегда, как вы понимаете. Было это сделано, чтобы набрать абонентскую базу. И только). Но почти нигде в мире нет безлимитного и копеечного интернета.

Энтерпрайзы мы не рассматриваем, они, если хоть капельку беспокоятся о безопасности (или продуктивности) проксируются в обязательном порядке и на предрассудки среднего админа локалхоста болт кладут.

Однако, вернемся к цензуре.

Посмотрите вот на этот ресурс, если вы еще не заглядывали в него.

Вы увидите вот такую картинку:


Посмотрите на красный значок завода, покликайте по карте и посмотрите этого самого вендора, который identified.

Эти прокси стоят у провайдеров 1,2 и 3 уровней.

В основном они используются для кэширования, но практически все также используются и для ограничения доступа, согласно местным законодательствам (какими бы ублюдочными, с точки зрения граждан, они не были).

Как это работает?

Канонически.

Целевой сайт/мессенджер банится по hostname или SNI (в случае HTTPS. А вы думали, HTTPS это панацея? Черта с два. Это вы еще про SSL Bump никогда не слышали, а также плохо себе представляете реверсивные прокси для HTTPS).

В этом случае, не имеет никакого значения, какие IP он использует, на каких хостингах соседствует с другими сайтами, как бегает по адресам, спасаясь от блокирования и так далее.

У малолетних апологетов digital resistance вот тут, в этом месте, нешуточно пригорает задница и они начинают, брызгая слюной, визжать, что-де HTTPS везде и всюду спасет, что-де прокси невозможно масштабировать до уровня провайдера, что-де существуют сервисы, которые только connection probing может выловить и пресечь, а средний-де провайдер состоит из одних даунов и никогда не справится с такой технически сложной задачей, ну и так далее.

Давайте по-порядку.

  1. HTTPS (он же TLS) не является E2E и неспособен обеспечить анонимность (В силу органических пороков своего предка, HTTP) и/или безопасность (см. Google Global Cache, Oracle Web Cache, Nginx, Squid, Blue Coat, Riverbed, etc.)
  2.  "Вы просто их готовить не умеете". То, что лично вы не умеете масштабировать прокси до уровня ISP - не означает, что этого никто не умеет.
  3. Connection probing обеспечивается, например, вот таким вот опенсурсным решением. Которое, к слову, активно используется в ISP и великолепно масштабируется. TCP-probing элементарно пишется на C и спокойно распараллеливается по ядрам процессоров. Совершенно никакого труда не составляет выполнять фоновую проверку сессий и - шаг влево-шаг в право-попытка к бегству-прыжок на месте-провокация-TCP RST!
  4. Средний провайдер, может, и состоит из одних даунов, но он для решения таких задач нанимает, например, Squid Foundation.
_________________________________
Здесь я хочу сделать небольшое лирическое отступление, и вернуться к теме программистов мессенджеров. Так как безграничная самонадеянность и слепая вера в технологии (я не могу здесь подобрать более цензурного определения) присутствует повсеместно, а тестирования (в силу предрассудков в отношении проксирования в современном "безлимитном" интернете) никто не производит, большинство массовых мессенджеров либо завязаны на фиксированные порты (которые тупо банятся на бордере, либо просто не открываются), как, например, Jabber (вы правда думали, что он совершенен?), либо имеют bootstrap по HTTPS/TLS (который элементарно либо требует действий администратора для пропускания через прокси, либо тривиально пресекается по SNI - как это сделал Иран, прихлопнув продвинутый Signal одной левой. Причем Сигнал не додумался до соксификации своей поделки и только рыдает в блоге о том, как их, бедных, пришибли-то по SNI), либо, напротив, палится как три тополя на Плющихе нестандартным применением TLS на 443 порту, которое тривиально проверяется и пресекается либо просто не пропускается через прокси. Я уже об этом писал.

Ребята, вы когда мессенджеры пишете - вы хоть на прокси на них гляньте - как вы выглядите с той стороны? Не палитесь ли? Не палите ли пользователя? Не раздвигаете ли вы булки необходимостью ручных действий администратора прокси для пропускания? Если ручные действия требуются - документируйте их, мать вашу, потому что достало заниматься реверс-инжинирингом какой-нибудь паршивой болталки просто для того, чтобы ее пропустить через прокси!
_________________________________


О соксификации я писал тому же Сигналу (Три открытых тикета. Три, Карл!), но ситуация, разумеется, не изменилась. Даже соксификация оказалась мегапуперпрограммистам Сигнал не по зубам. Не говоря уже о разработке приличного протокола обмена, который..... Впрочем, мы отвлеклись.

Прошу подумать вот о чем.

DPI действительно адово тяжелая и скверно масштабируемая штука. Так как надо обмолачивать либо заголовок TCP-пакета, либо еще и его body. Джамбограммы в интернете не применяются, значит, надо тратить ресурсы процессора на такую обработку и это тяжело на потоках уровня даже средней паршивости ISP.

Прокси прикладного уровня, в отличие от DPI, работает не с пакетами, а с протоколами уровня L4-L7. И их давно научились обрабатывать просто, легко и быстро.

Таким образом, в XXI веке никто из вменяемых людей с DPI не связывается. Слишком накладно. Да и не надо, говоря откровенно. Достаточно проксированием отсечь малообразованных, остальных слишком мало, чтобы иметь сколько-нибудь значимое влияние выше статистической погрешности.

Да, проксирование не обеспечивает 100% блокировки. От силы 98%. Но 100% не обеспечивает ничто, если уж на то пошло. В войне щита и меча обычно побеждает меч. На некоторое время, да.

Будущее

Ванговать трудно, однако, как мне представляется, проксирование никуда не денется. Вместе с цензурой в том или ином виде. Более того, мне представляется, что оно будет набирать обороты, так как имеется такой бонус, как кэширование (я что-то не вижу прорывных сетевых технологий, обеспечивающих неограниченную пропускную способность, а евангелисты TLS/LE об этом, судя по всему, никогда не задумывались).

DPI умрет практически полностью. Слишком ресурсоемко (неважно, при какой реальной эффективности) и слишком дорого.

Что спасет гигантов мысли? (Правильный ответ: Вторая поправка к Конституции).

Мне представляется, что на данный момент это Tor. Что будет дальше - я не знаю, потому что Tor нереально сложен простым смертным, слишком медленный для хипстеров, и достаточно легко завинчивается до такой степени недоступности (даже бриджированный, прошу заметить), что теряет ценность для всех, кроме 1% трахнутых криптоманьяков и своих основных пользователей - диссидентов и резидентуры иностранных разведок, а также особо злобных хакеров. Про VPN/SOCKS стоит забыть, при необходимости они пришибаются легким пинком на бордерах и всё. В смысле - совсем всё.

Нет, я не считаю, что TLS 1.3 (и даже с eSNI, прием которого в стандарт лично мне представляется крайне маловероятным) принципиально изменит ситуацию с HTTPS или как-то помешает проксированию.

Мессенджерам хана. Даже Jabber-based. По факту, подавляющее большинство из них держат за глотку недостатки их собственных протоколов, которые прокси способны пресечь одной левой.

Я не хочу сказать, что мессенджеры вымрут как класс, но им здорово сплохеет, когда (не если) за них возьмутся действительно серьезно. (А не надо было становиться коммерческим предприятием. Богадельням проще живется, да)

Что касается простых пользователей - помните о том, что "Что в интернет попало - то пропало". Не надо помещать в интернет (и вообще в сети - исключительно в холодные хранилища, если уж вам так надо данные в компьютерном виде хранить) чувствительных данных, болтать там лишнего и не стоит слишком уж полагаться на анонимность, защищенность, и, в особенности, постоянную доступность мессенджеров и интернет-сервисов.

Помните, что доступ к ним можно прихлопнуть в любой момент. Вопрос не в технической возможности, а исключительно в силе желания или необходимости.