Движок
Наш браузерный движок
- ARMANOS Browser 153.0.7978.0
- Движок
- 26 файлов Chromium, применяются по порядку
- Набор правок
- 9, каждое доказано признаком в правленом файле
- Объявленных умений
- macOS Apple Silicon и Intel, Windows, Linux
- Сборки
- 25, прямо в программе
- Замеров у профиля
Движок это наша сборка Chromium, а не настройка поверх чужой
ARMANOS Browser 153.0.7978.0 собирается из исходников Chromium с нашими правками, наложенными при сборке. Это не дополнение, не расширение и не ключ запуска. Правки меняют тот самый код на C++, который отвечает странице, поэтому ответ выходит из браузера уже нужным.
Версия живёт в одном месте: ARMANOS_BROWSER_VERSION в packages/shared/src/index.js. Её читает программа, её читает эта страница, и профиль называет ровно ту версию, которая его рисует. Назови он другую, сайт проверил бы это одной строкой, сверив подпись браузера с возможностями, которых у той вехи нет.
Сборок четыре, по одной на систему, и в каждом установщике движок лежит внутри. Упаковка отказывается идти, если ячейка нужной архитектуры пуста. Установщик без движка тихо отдал бы покупателю запасной движок и его настоящую машину, и на экране об этом не было бы ни слова.
Сборка под Windows правит файл BRANDING в дереве до настройки сборки, поэтому chrome.exe и chrome.dll несут имя продукта в собственных сведениях о версии. Сборка под macOS пока брендируется после компиляции, и это более слабый порядок: следующая сборка под macOS должна делать так же, как виндовая. Обе сегодня отвечают ARMANOS Browser 153.0.7978.0, и проверка читает это прямо из того двоичного файла, что лежит в ячейке.
| Система | Куда кладётся сборка | Что программа находит при запуске |
|---|---|---|
| macOS, Apple Silicon | apps/desktop/engines/mac-arm64/ARMANOS Browser.app | Contents/MacOS/ARMANOS Browser внутри пакета |
| macOS, Intel | apps/desktop/engines/mac-x64/ARMANOS Browser.app | тот же путь внутри сборки для Intel |
| Windows 10 и 11, 64 бита | apps/desktop/engines/win/ вместе с сопутствующими файлами | resources/engines/armanos-browser/armanos-browser.exe либо chrome.exe |
| Ubuntu 22.04 и новее, 64 бита | apps/desktop/engines/linux/ вместе с сопутствующими файлами | resources/engines/armanos-browser/chrome |
Значения ставятся в C++, до первой строки скрипта страницы
Страница здесь не может быть первой. Когда её скрипт спрашивает navigator.platform, отвечающий геттер уже заменён внутри движка, и более раннего мгновения просто нет. В этом и вся разница между движком и скриптом.
Семя это один объект, который программа собирает и передаёт один раз, в командной строке. Оно выводится из шаблона, номера профиля и страны выхода прокси, поэтому тот же профиль получает ту же машину и при следующем запуске, и на другом компьютере.
Когда ключа нет, все правки бездействуют и двоичный файл ведёт себя как обычный Chromium. Это не мелочь: именно так работают приёмочные стенды, ведь они сличают один и тот же движок с семенем и без него.
У каждого профиля свой --user-data-dir, поэтому куки, хранилище, история и собственные данные сайта лежат в каталоге, которого не видит ни один другой профиль.
- 1
Программа собирает семя
buildFingerprint(шаблон, номер профиля, страна выхода) в packages/shared/src/index.js возвращает один объект: машина, экран, строки видеокарты, шрифты, часовой пояс, языки, число приборов, числа сети и два семени шума.
- 2
Семя уходит в командную строку один раз
forkEngine.buildArgs пишет --armanos-fingerprint=<base64 JSON>, а рядом --user-data-dir этого профиля, прокси, список языков и подпись браузера.
- 3
Главный процесс разбирает его однажды
base::Base64Decode и base::JSONReader в third_party/blink/renderer/core/armanos/armanos_fp.cc, результат живёт одним объектом на весь процесс.
- 4
Каждый отрисовщик получает то же семя
Ключ внесён в список передаваемых в content/browser/renderer_host/render_process_host_impl.cc, поэтому отрисовщик, поднятый через десять минут, несёт то же самое.
- 5
Геттеры Blink отвечают из семени
navigator_id.cc, screen.cc, font_cache.cc, timezone_controller.cc, network_information.cc и прочие возвращают засеянное там, где раньше возвращали хозяйское.
- 6
Страница выполняется потом
Переопределять нечего. Ни один прототип не подменён, ни у одной функции не переписан toString, а рабочий поток, запущенный со своего адреса, читает те же значения, что и главный.
Поверхность за поверхностью: что читает сайт
Набор правок трогает 26 файлов дерева Chromium плюс архив новых файлов с нашим собственным кодом. Ниже то, что отвечает каждая поверхность сегодня. Здесь нет разрядов: в каждой строке назван файл, где лежит правка.
Через всё это проходят два правила. Первое: шум постоянен у профиля и никогда не свежий на каждый вызов, потому что значение, изменившееся между двумя чтениями за один сеанс, само по себе примета. Второе: числа выводятся по правилам самого браузера, а не по правдоподобию. Предел кучи кладётся в одну из ста ступеней; задержка огрубляется до кратного 50 мс, а скорость режется потолком 10 Мбит/с, ровно как в network_state_notifier.cc; вид связи выводится из этих чисел по порогам из network_quality_estimator_params.h, а не тянется жребием.
Приборов никогда не ноль. Движок умеет спрятать камеру, которая на машине есть, но отнять её не умеет: профиль, заявивший ноль камер, ответил бы «камеры нет» на enumerateDevices и тут же выдал бы её на getUserMedia, а два ответа подряд, противоречащих друг другу, заметнее любого числа.
| Поверхность | Где лежит правка | Что получает страница |
|---|---|---|
| Чтение холста | armanos_canvas_noise.cc, base_rendering_context_2d.cc, html_canvas_element.cc, canvas_async_blob_creator.cc | постоянная поправка на getImageData, toDataURL и toBlob, привязанная к координате, поэтому два чтения одного прямоугольника совпадают |
| Звук | audio_buffer.cc, realtime_analyser.cc | постоянная поправка на буфер и на спектр; повторные чтения её не накапливают |
| Строки WebGL | webgl_rendering_context_base.cc | видеокарта профиля, например Google Inc. (AMD) и строка ANGLE с картой, которая существует |
| Пиксели WebGL | тот же файл, сразу после ReadPixelsHelper | readPixels получает ту же поправку, что и холст, с перевёрнутым порядком строк, поэтому две дороги чтения сходятся |
| WebGPU | gpu_buffer.cc, gpu_command_encoder.cc, gpu_queue.cc | шум ложится только на буферы, в которые снимали картинку; обычные вычисления не трогаются |
| Шрифты | armanos_font_gate.cc, font_cache.cc | отзываются только семейства профиля; остальное подменяется запасным, как будто его не установлено |
| Часовой пояс | timezone_controller.cc | пояс страны, в которой выходит прокси; без страны выхода ничего не навязывается |
| Опознание navigator | navigator_id.cc, navigator_base.cc, navigator_concurrent_hardware.cc, navigator_device_memory.cc | система, подпись браузера, поставщик, число ядер, память, число касаний |
| Подсказки клиента | user_agent_utils.cc, navigator_ua_data.cc | заголовки Sec-CH-UA и getHighEntropyValues() берутся из того же семени, что и подпись, поэтому заголовок и скрипт сходятся |
| Экран | screen.cc, local_dom_window.cc | размер, рабочая область, глубина цвета и плотность точек, согласованные между собой |
| Приборы записи и вывода | media_devices.cc | число микрофонов, выходов звука и камер у этого профиля |
| Голоса синтеза речи | speech_synthesis.cc | список голосов урезается до языка профиля, поэтому бразильский профиль не отдаёт японские голоса |
| Сведения о сети | network_information.cc | скорость, задержка и вид связи, огрублённые так же, как их огрубляет Chromium |
| Предел памяти | memory_info.cc | предел кучи приводится к заявленной памяти, а когда они и так сходятся, значение не меняется вовсе |
Скрипт так не умеет, и разрыв между ответами ищут первым делом
Проверялка отпечатка не просто читает значения. Она сличает заявленное с измеренным и показывает расстояние между ними. Это расстояние и создаёт подмена на уровне страницы.
Возьмите шрифты. Скрипт может переписать measureText и document.fonts.check, но не может изменить, какие очертания система рисует на самом деле, и не дотягивается ни до разбора CSS, ни до рабочего потока. Возьмите холст. Каждую дорогу чтения приходится закрывать отдельно, и одна пропущенная вернёт настоящую картинку, пока остальные возвращают изменённую: это доказательство подмены, а не сокрытие. Возьмите рабочий поток: его скрипт живёт по собственному адресу, который страница переписать не может, поэтому обёртка накроет только источники Blob и data.
Мы измерили все три случая на обоих движках. Таблица ниже это измерение, а не довод. Страница, которая видит засеянную машину в главном потоке и настоящую в рабочем, получает доказательство вмешательства, а это хуже честного несовпадения.
По той же причине мы сняли подмену списка языков в странице, как только движок начал отдавать его сам. Замаскированный геттер это поверхность, на которой ловят. Правда движка ею не является.
| Подмена в странице | ARMANOS Browser | |
|---|---|---|
| Откуда берётся значение | из подменённого геттера прототипа | из самого геттера |
| Рабочий поток со своим адресом | настоящая машина | засеянная машина |
| Кадр, созданный и прочитанный в один такт | настоящая машина | засеянная машина |
| Разрешение шрифта | переписан замер, отрисовка нет | хранилище шрифтов отдаёт только семейства профиля |
| Две дороги чтения одного рисунка | каждую надо закрывать руками, пропущенная расходится с прочими | обе идут из одного места в движке |
Движок сам говорит, что закрывает, и слой в странице отходит
Не всё пока в C++. Что движок не закрывает, программа закрывает из страницы, до первого чужого скрипта. Это честная мера, и она записана, но в ней ловушка: в день, когда новый движок доедет до человека, оба слоя легли бы друг на друга. Движок добавил бы шум чтению пикселей, страница добавила бы свой поверх, и тот же рисунок через обычный холст пришёл бы другим. Расхождение двух дорог чтения это ровно та примета, ради устранения которой всё и делалось, только заведённая нами самими.
Поэтому сборка кладёт рядом с движком опись умений, armanos-engine.json, а программа читает её при запуске и гасит ровно те подмены в странице, которые движок теперь делает сам. Спрашивать у двоичного файла версию бесполезно: форк называет ровно ту же версию, что обычный Chromium той же вехи.
Опись выводится из дерева, а не переписывается со списка. Каждое умение доказывается признаком, который обязан найтись в правленом файле, например armanos::ShouldHideSystemFont( в font_cache.cc. Раньше сборка объявляла все умения, какие перечислены в коде, независимо от того, легли ли заплаты: это заявление, а не измерение. Цена такой лжи вполне осязаема: три поля связи Android программа открывает только движку, объявившему net-mobile-fields, и на движке без подмены значений эти поля отдали бы хозяйское «wifi» и бесконечную предельную скорость.
webgl-readpixels
шум чтения пикселей WebGL внутри движка
intl
часовой пояс, язык, календарь и разделители
fonts
список шрифтов по профилю, а не по машине
devices
число приборов записи и вывода
voices
голоса синтеза речи по заявленному языку
net
скорость, задержка и вид связи
memory
предел кучи сходится с заявленной памятью
webgpu
шум снимка картинки через WebGPU
net-mobile-fields
три поля связи Android: type, downlinkMax, ontypechange
Встроенный движок это честный запасной, и он слабее
В программе есть второй движок: настоящий Chromium через Electron, сейчас веха 130, с защитой отпечатка, внедрённой скриптом. Он работает на любой системе, и на него профиль откатывается, когда ARMANOS Browser не смог подняться. Поскольку теперь движок лежит внутри каждого установщика, свежая установка сразу работает на ARMANOS Browser.
Откат никогда не бывает молчаливым. Если форк не поднялся, профиль всё равно откроется, но уведомление назовёт профиль и причину. Для такого продукта тихая смена движка, несущего личность, это единственный откат, о котором обязательно надо сказать: на экране движка по-прежнему стояло бы ARMANOS Browser, а профиль работал бы на подмене в странице и на другой версии Chromium.
Одну разницу надо назвать прямо, потому что она выглядит работающей и не работает. Блокирующее расширение из каталога на встроенном движке загружается, числится включённым и не блокирует ничего: Electron не исполняет правила declarativeNetRequest. Мы измерили это одним расширением на обоих движках, правилом против страницы, которая сама запрашивает /blockme. На встроенном движке сервер запрос увидел. На ARMANOS Browser не увидел. Пока это так, в разделе расширений стоит предупреждение, а проверка потребует его убрать, если Electron однажды это починит.
Наша собственная блокировка трекеров и чистка ссылок на встроенном движке работают, потому что правила исполняет сама программа, а не браузер. RPA Plus и Синхронизатор работают на обоих движках.
| Встроенный движок, Chromium 130 | ARMANOS Browser, Chromium 153 | |
|---|---|---|
| Где ставится отпечаток | в странице, внедрённым скриптом | в C++, до первого скрипта |
| Рабочий поток совпадает с главным | – | Да |
| Кадр совпадает с родителем | – | Да |
| Блокирующее расширение из каталога действительно блокирует | – | Да |
| Наша блокировка трекеров и чистка ссылок | Да | Да |
| RPA Plus и Синхронизатор | Да | Да |
| Есть на любой системе | Да | Да |

Что едет вместе с движком
Форк это отдельный процесс Chromium без сеанса Electron и без предзагрузки, поэтому защиты, написанные под Electron, сами по себе туда не переезжают. Их доставляет небольшое расширение, которое собирается для каждого профиля при запуске и грузится ключом --load-extension вместе с --disable-extensions-except, чтобы рядом не загрузилось ничего постороннего.
Блокировка идёт внутри сетевого слоя через declarativeNetRequest. Ни крючка, видимого странице, ни обращения к нашему процессу на каждый запрос, а значит заминка в программе не может подвесить чужую страницу. Правила собираются из того же списка, которым пользуется встроенный движок, поэтому два движка не разойдутся.
Блокировка трекеров
Три набора правил: список узлов, собственная слежка известных площадок и метки в ссылках. Служебный поток расширения сообщает о срабатываниях, и из этого складывается экран защиты.
Чистка ссылок
40 отслеживающих меток срезаются на переходе, до выхода запроса в сеть, и адресная строка показывает уже чистый адрес.
WebRTC через прокси
--force-webrtc-ip-handling-policy=disable_non_proxied_udp, поэтому местные кандидаты не предлагаются в обход прокси.
Весь трафик на прокси
--proxy-server покрывает все схемы, а не только https. Неизвестный вид прокси приводит к отказу в запуске, а не к тихому выходу напрямую.
Пароль прокси мимо командной строки
Chromium отвергает пароль внутри --proxy-server, да и в списке процессов он был бы виден всем. Пароль идёт через мост на петле, а браузер предъявляет мосту пропуск из расширения.
Автоматизация без приметы
Порт отладки слушает петлю, а номер выбирает система. Его открытие ставит navigator.webdriver, поэтому признак автоматизации гасится ключом запуска; через этот же порт работают RPA Plus и Синхронизатор.
Свой ключ шифрования
--use-mock-keychain и --password-store=basic держат браузер вне хранилища паролей системы, а заодно оставляют профиль переносимым между машинами.
Сборка не уезжает, пока её не приняли стенды
Приёмку запускает одна команда: один отчёт и один код возврата. Стенд, который не удалось запустить, считается проваленным, а не пропущенным: молчаливый пропуск это ровно то, из-за чего непринятая сборка однажды уехала людям с чужим именем в меню.
Стенды меряют готовый двоичный файл, а не исходники. Они запускают его, читают то, что видит страница, и сличают с тем же файлом без семени, поэтому чистым образцом служит сам движок, а не Chrome другой вехи.
Исходов три, а не два. «Принято» значит, что прошли все стенды. «Принято с оговоркой» значит, что не провален ни один, но проверено не всё: обычно на машине нет настоящего Chrome той же вехи. «Не принято» значит, что провален хотя бы один стенд, и такая сборка не выкладывается.
Сам набор правок стережёт отдельная проверка, и появилась она из настоящей беды: десять пронумерованных файлов в папке заплат выглядели готовой серией и все были заглушками. Сборщик пропускал каждую с предупреждением и выдавал обычный Chromium, пока настоящая правка лежала рядом и в список не входила. Теперь проверка требует, чтобы в списке не было заглушек, чтобы каждая названная правка действительно меняла исходники, чтобы у шума WebGL был вызов после чтения пикселей с передачей высоты буфера и чтобы каждое объявленное умение подтверждалось своим признаком.
- 1
Засеянные значения доезжают
accept-engine.js запускает движок со страной выхода и читает то, что видит страница, а затем сличает с тем же движком без семени.
- 2
Инварианты держатся на нескольких семенах
engine-invariants.js гоняет по умолчанию четыре семени. Дефект холста зависит от семени, и одно удачное пропустило бы негодную сборку.
- 3
Форма нашего движка снимается в файл
compare-shape.js записывает всю поверхность движка в файл, а не печатает строку, которую никто не сличает.
- 4
Форма настоящего Chrome снимается тоже
тот же сценарий идёт по обычному Chrome, без семени, во второй файл.
- 5
Две формы сличаются
сравнить-форму.js их сравнивает. Код 3 значит «сличено частично», код 4 значит «нашли лишнее и объяснить нечем», и это отказ: такую разницу сайт находит одной строкой.
Как проверить самому
Каждый профиль открывается на стартовой странице, где перечислены проверялки, которыми эта работа и так пользуется: BrowserScan, BrowserLeaks, CreepJS, Pixelscan и определение адреса. Страницу отдаёт сервер на петле внутри программы, поэтому об открытии профиля не узнаёт никто посторонний. Единственный запрос наружу это определение адреса, и оно идёт через прокси самого профиля, так что вы видите тот адрес, который увидит сайт.
Внутри программы есть самопроверка: она снимает 25 значений с живой страницы профиля и сверяет каждое с тем, что просило семя. Она смотрит признак автоматизации, систему и подпись браузера, строки видеокарты, экран, ядра и память, постоянство холста и три его инварианта, чтение пикселей WebGL, постоянство звука, часовой пояс, весь список языков, дополнения, кадр и рабочий поток, шрифты и поля подсказок клиента. У телефонных профилей добавляются касания, поворот экрана и размеры окна.
Самопроверка отказывается мерить окно без защиты, а не считает его. Профиль без подмены провалил бы замеры, которых настоящий профиль не проваливает никогда, а красный экран, ничего не значащий, приучает не смотреть на красные экраны.
Ниже значения одного профиля, полученные той же функцией, которую отдают движку. Для своего профиля вы получите такой же вид на стартовой странице, ещё не открыв ни одного сайта.
- navigator.platform
- Win32
- navigator.userAgent
- Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
- Ядра и память
- 12 логических ядер, 8 ГБ
- Экран
- 1920 x 1080, рабочая высота 1040, плотность 1
- Видеокарта, поставщик
- Google Inc. (AMD)
- Видеокарта, отрисовщик
- ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)
- Часовой пояс
- America/New_York
- Языки
- en-US, en
- Шрифты
- 25 семейств, все виндовые
- Приборы
- 1 микрофон, 3 выхода звука, 1 камера
- Сеть
- 4g, 10 Мбит/с, задержка 200 мс
- Голоса синтеза речи
- только английские
- maxTouchPoints
- 0
Один профиль на шаблоне windows-chrome, прокси выходит в США. Все значения выводятся из номера профиля, поэтому при следующем запуске и на другом компьютере они те же. Число приборов это потолок: движок умеет спрятать прибор, который на машине есть, но не умеет выдумать тот, которого нет.
Чего это не делает
- Чтение пикселей WebGL закрыто для восьмибитного RGBA, той дороги, которой ходят проверялки. Три дороги остаются открытыми, и они честно названы открытыми в engine/patches/PENDING.md: RGB по три байта на пиксель, вещественные типы float и half-float, и чтение через буфер упаковки.
- Настоящая опись шрифтов машины по-прежнему утекает через запрос local(). Замер урезан списком профиля, но поиск по уникальному имени отдаёт то, что на машине есть, и закрыть это можно только правкой в local_font_face_source.cc.
- На macOS язык ICU следует за хозяйской машиной, поэтому Intl может форматировать числа и даты на языке хозяина, пока профиль заявляет другой. Обычный Chromium с теми же ключами ведёт себя так же. Пока движок не ставит язык ICU из семени, пояс и язык закрепляет слой в странице.
- Профилю, чей шаблон не совпадает с системой хозяина, дописываются хозяйские базовые шрифты, иначе текст не рисуется вовсе. Эти имена странице видны, и починить это можно только в движке, разделив список на отрисовку и на ответы.
- Признак автоматизации гасится открытым ключом Chromium, а не правкой. Ключ работает и сторожится проверкой, которая поднимает настоящий движок и читает значение с настоящей страницы, но сам ключ виден в командной строке процесса любому, кто на этой машине умеет её прочесть.
- Мы продаём разделение и постоянную машину, а не невидимость. Сайт по-прежнему видит ваш адрес, ваше поведение и историю того аккаунта, в который вы входите, и движок об этом ничего не решает.
Как проверить
За каждым пунктом файл, который можно открыть, и стенд, который валит сборку, когда обещание перестаёт быть правдой.
- Отпечаток подменяется в исходниках Chromium, правки применяются при сборке
- engine/patches/series · apps/desktop/test/engine-patches-real.js
- Шум чтения пикселей WebGL действительно вызывается, а не лежит в заплате мёртвым кодом
- engine/patches/armanos-fp.cumulative.diff · apps/desktop/test/engine-patches-real.js
- Каждое объявленное умение доказано признаком в правленом файле, а не списком
- packages/shared/src/index.js · engine/scripts/опись-умений.js
- Слой в странице отходит ровно там, где движок берёт работу на себя
- apps/desktop/src/lib/дыры-страницы.js · apps/desktop/test/страница-отходит-когда-движок-умеет.js
- Сборку принимают стенды, и неполная проверка не выдаётся за полную
- engine/scripts/приёмка.sh · apps/desktop/test/сборочные-скрипты.js
- Установщик не уезжает без движка и с движком чужой версии
- apps/desktop/build/before-pack.js · apps/desktop/test/установщик-без-движка.js
Читать дальше
Как работает ARMANOS
Путь от профиля до открытого окна браузера, по шагам.
Что умеет программа
Профили, группы, команда, автоматизация и остальное, с файлами за ними.
Проверка отпечатка
Посмотрите, что прямо сейчас отдаёт ваш браузер в этой вкладке.
Прокси
Как выходит трафик профиля и что движок делает со страной выхода.
Запустите на своей машине
Поставьте ARMANOS, откройте профиль и прочитайте стартовую страницу до того, как открывать сайты. Движок едет внутри программы.