<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Opt-In Software Blog</title>
  <icon>https://optinsoft.net/blog/icon.png</icon>
  <subtitle>Software, dvelopment, and practical insights</subtitle>
  <link href="https://optinsoft.net/blog/atom.xml" rel="self"/>
  
  <link href="https://optinsoft.net/blog/"/>
  <updated>2026-10-07T05:51:02.096Z</updated>
  <id>https://optinsoft.net/blog/</id>
  
  <author>
    <name>Opt-In Software</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Использование OpenRouter в VS Code Agent без GitHub Copilot</title>
    <link href="https://optinsoft.net/blog/ru/2026/10/07/Using-OpenRouter-in-the-VS-Code-Agent-without-GitHub-Copilot/"/>
    <id>https://optinsoft.net/blog/ru/2026/10/07/Using-OpenRouter-in-the-VS-Code-Agent-without-GitHub-Copilot/</id>
    <published>2026-10-07T05:12:52.000Z</published>
    <updated>2026-10-07T05:51:02.096Z</updated>
    
    <content type="html"><![CDATA[<p><em>Проверено на VS Code 1.140 (Stable) и VS Code Insiders 1.141, октябрь 2026.</em></p><p>Я хотел использовать модели OpenRouter в VS Code Agent без входа в GitHub Copilot. VS Code умеет это с помощью пользовательских провайдеров языковых моделей, но на моём компьютере всё заработало только в <strong>VS Code Insiders</strong>. В этом посте описана настройка VS Code, а также способ понять, нужен ли вам Insiders, и nginx в роли обратного прокси для сетей, где OpenRouter недоступен.</p><h2 id="Кратко"><a href="#Кратко" class="headerlink" title="Кратко"></a>Кратко</h2><ol><li>Сначала попробуйте обычный VS Code. Если увидите описанные ниже симптомы, переходите на <strong>VS Code Insiders</strong>.</li><li>Создайте <code>chatLanguageModels.json</code> с провайдером OpenRouter (<code>vendor: &quot;customendpoint&quot;</code>) и своими моделями. Используйте точный ID модели с openrouter.ai.</li><li>Задайте API-ключ через <strong>Manage Models → Update API Key</strong>. Не оставляйте ключ в JSON в открытом виде: у меня это не сработало.</li><li>Выберите модель в <strong>Models</strong> и переключитесь на <strong>Agent</strong>.</li><li>Если OpenRouter заблокирован в вашей стране, см. раздел про прокси ниже.</li></ol><h2 id="Что-понадобится"><a href="#Что-понадобится" class="headerlink" title="Что понадобится"></a>Что понадобится</h2><ul><li>Аккаунт OpenRouter с API-ключом и некоторой суммой на балансе. Бесплатные модели приходят и уходят (см. Troubleshooting).</li><li>Модель с поддержкой <strong>tool calling</strong>. Для режима Agent это необходимо.</li><li>VS Code Insiders, если обычный VS Code у вас не работает (см. следующий раздел).</li></ul><h2 id="Нужен-ли-вам-VS-Code-Insiders"><a href="#Нужен-ли-вам-VS-Code-Insiders" class="headerlink" title="Нужен ли вам VS Code Insiders?"></a>Нужен ли вам VS Code Insiders?</h2><p>Возможно, нет, поэтому сначала попробуйте обычный VS Code. На моём компьютере (Stable 1.140) он не заработал:</p><ul><li>С пустым <code>chatLanguageModels.json</code> не было ни кнопки <strong>Manage Models</strong>, ни команды <strong>Chat: Manage Language Models</strong>.</li><li>Когда я вручную добавил провайдера в файл и перезапустил VS Code, кнопка и команда появились, но в диалоге Language Models были только модели самого Copilot. Моих моделей OpenRouter там не было, и выбрать их в чате было нельзя.</li><li><strong>Add Models → OpenRouter</strong> принимал API-ключ и записывал группу в <code>chatLanguageModels.json</code>, но эта группа не появлялась ни в диалоге, ни в списке моделей, даже после перезапуска.</li></ul><p>Почему так, я разбираться не стал. <a href="https://code.visualstudio.com/docs/agent-customization/language-models">Официальная документация</a> сейчас помечает провайдер Custom endpoint как функцию Insiders, и в Insiders всё описанное ниже работало. Если вы видите те же симптомы, установите VS Code Insiders. Он ставится рядом с обычным VS Code и хранит собственные настройки.</p><h2 id="Где-находится-chatLanguageModels-json"><a href="#Где-находится-chatLanguageModels-json" class="headerlink" title="Где находится chatLanguageModels.json"></a>Где находится <code>chatLanguageModels.json</code></h2><p>Файл лежит в папке пользовательских данных VS Code Insiders, которая отделена от обычного VS Code (<code>Code - Insiders</code>, а не <code>Code</code>):</p><ul><li><strong>Windows:</strong> нажмите <code>Win + R</code>, введите <code>%APPDATA%\Code - Insiders\User</code> и нажмите Enter.</li><li><strong>macOS:</strong> в Finder нажмите <code>Cmd + Shift + G</code> и перейдите в <code>~/Library/Application Support/Code - Insiders/User</code>.</li><li><strong>Linux:</strong> перейдите в <code>~/.config/Code - Insiders/User</code>.</li></ul><blockquote><p><strong>Примечание:</strong> в обычном VS Code в этих путях вместо <code>Code - Insiders</code> используется <code>Code</code>. Файл там можно редактировать, но если обычный VS Code не показывает ваши модели, правка не поможет. Редактируйте копию из Insiders.</p></blockquote><h2 id="Создаём-провайдера-в-chatLanguageModels-json"><a href="#Создаём-провайдера-в-chatLanguageModels-json" class="headerlink" title="Создаём провайдера в chatLanguageModels.json"></a>Создаём провайдера в <code>chatLanguageModels.json</code></h2><p>Я не добавлял провайдера через диалог <strong>Manage Models</strong>. Я написал его в <code>chatLanguageModels.json</code> вручную, и VS Code его подхватил. Вот файл, который я использую:</p><pre><code class="hljs json"><span class="hljs-punctuation">[</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;OpenRouter&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vendor&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;customendpoint&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;$&#123;input:chat.lm.secret.-32b9a01a&#125;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;apiType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;chat-completions&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;models&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;qwen/qwen3.8-27b&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Qwen3.8 27B&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://openrouter.ai/api/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;toolCalling&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxInputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">262000</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxOutputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">16000</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;deepseek/deepseek-r1&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DeepSeek R1&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://openrouter.ai/api/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;toolCalling&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxInputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">64000</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxOutputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">16000</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">]</span></code></pre><p>Значение <code>apiKey</code> VS Code запишет сам после следующего шага. До тех пор о нём можно не беспокоиться.</p><p>Что означают поля:</p><ul><li><code>vendor: &quot;customendpoint&quot;</code> помечает провайдера как пользовательского.</li><li><code>apiType: &quot;chat-completions&quot;</code> заставляет VS Code использовать Chat Completions API.</li><li><code>id</code> это ID модели на OpenRouter. Копируйте его со страницы модели.</li><li><code>url</code> это базовый URL. VS Code строит из него запрос Chat Completions (запрос в итоге уходит на <code>/v1/chat/completions</code>), поэтому здесь <code>.../api/</code>, а не полный адрес эндпоинта.</li><li><code>toolCalling</code> включает вызов инструментов. Без него Agent работать не может.</li><li><code>vision</code> указывает, принимает ли модель изображения.</li><li><code>maxInputTokens</code> и <code>maxOutputTokens</code> это лимиты, которые использует VS Code.</li></ul><h3 id="Как-выбрать-значения-для-модели"><a href="#Как-выбрать-значения-для-модели" class="headerlink" title="Как выбрать значения для модели"></a>Как выбрать значения для модели</h3><p><code>toolCalling</code> и <code>vision</code> берите со страницы модели на OpenRouter. Например, Qwen3.8 27B это vision-language модель с поддержкой инструментов, а DeepSeek R1 только текстовая, поэтому у неё <code>vision</code> равен <code>false</code>.</p><p>С лимитами всё менее однозначно. Размер контекста может отличаться у разных провайдеров и источников: для R1 я видел 64K на странице модели и 131K-164K в других местах, для Qwen3.8 27B 262K и 1M. Я беру меньшее значение, оно безопасно с любым провайдером. Не занижайте <code>maxOutputTokens</code> для reasoning-моделей, потому что токены рассуждений тоже считаются выходными. 4096 слишком мало: ответы будут обрезаться.</p><p>Если нужна модель Claude, подойдёт тот же блок со slug со страницы модели на OpenRouter (для Sonnet 5 это <code>anthropic/claude-sonnet-5</code>, с <code>vision: true</code> и <code>toolCalling: true</code>).</p><h2 id="Задаём-API-ключ-этот-шаг-обязателен"><a href="#Задаём-API-ключ-этот-шаг-обязателен" class="headerlink" title="Задаём API-ключ (этот шаг обязателен)"></a>Задаём API-ключ (этот шаг обязателен)</h2><p>Кажется естественным записать ключ OpenRouter прямо в <code>apiKey</code>:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sk-or-v1-...&quot;</span></code></pre><p>Так я и сделал сначала, и это не сработало. OpenRouter отклонял каждый запрос с ошибкой 403. В логе nginx я увидел, что заголовок <code>Authorization</code> содержит только <code>Bearer</code>, без токена после него.</p><p>Исправило это следующее: в <strong>Manage Models</strong> я выбрал провайдера OpenRouter, нажал <strong>Update API Key</strong> и ввёл ключ там. VS Code заменил значение в файле на ссылку на секрет, который хранит сам:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;$&#123;input:chat.lm.secret.-32b9a01a&#125;&quot;</span></code></pre><p>После этого заголовок стал передаваться правильно, и запросы заработали. Бонусом ключ больше не лежит в файле, который может попасть в синхронизацию настроек или в git. Ссылка работает только на том компьютере, где вы выполнили <strong>Update API Key</strong>, поэтому не копируйте её на другой компьютер. Выполните <strong>Update API Key</strong> и там.</p><p>VS Code сам заменил сырой ключ в файле этой ссылкой, так что ничего дополнительно чистить не нужно.</p><h2 id="Используем-модель"><a href="#Используем-модель" class="headerlink" title="Используем модель"></a>Используем модель</h2><p>Вернитесь в Chat view, нажмите <strong>Models</strong>, выберите модель OpenRouter и переключитесь на <strong>Agent</strong>. VS Code остаётся интерфейсом и агентом, а OpenRouter поставляет модель, так что модель можно менять правкой одной записи в JSON.</p><h2 id="Если-OpenRouter-недоступен-из-вашей-страны"><a href="#Если-OpenRouter-недоступен-из-вашей-страны" class="headerlink" title="Если OpenRouter недоступен из вашей страны"></a>Если OpenRouter недоступен из вашей страны</h2><p>Если OpenRouter у вас работает, пропустите этот раздел. Иначе запросы можно направлять через обратный прокси nginx на VPS, откуда OpenRouter доступен:</p><pre><code class="hljs text">VS Code → https://example.com/openrouter/api/ → nginx → https://openrouter.ai/api/v1/chat/completions</code></pre><p>В конфигурации модели замените URL:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://example.com/openrouter/api/&quot;</span></code></pre><h3 id="Конфигурация-nginx"><a href="#Конфигурация-nginx" class="headerlink" title="Конфигурация nginx"></a>Конфигурация nginx</h3><p>Вот конфигурация, которая работает у меня (предполагается, что TLS для <code>example.com</code> уже настроен):</p><pre><code class="hljs nginx"><span class="hljs-section">location</span> /openrouter/api/v1/chat/completions &#123;    <span class="hljs-attribute">proxy_pass</span> https://openrouter.ai/api/v1/chat/completions;    <span class="hljs-attribute">proxy_ssl_server_name</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_ssl_name</span> openrouter.ai;    <span class="hljs-attribute">proxy_ssl_protocols</span> TLSv1.<span class="hljs-number">2</span> TLSv1.<span class="hljs-number">3</span>;    <span class="hljs-attribute">proxy_ssl_session_reuse</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_set_header</span> Host openrouter.ai;    <span class="hljs-attribute">proxy_set_header</span> Authorization <span class="hljs-variable">$http_authorization</span>;    <span class="hljs-attribute">proxy_buffering</span> <span class="hljs-literal">off</span>;    <span class="hljs-attribute">proxy_cache</span> <span class="hljs-literal">off</span>;    <span class="hljs-attribute">chunked_transfer_encoding</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_read_timeout</span> <span class="hljs-number">300s</span>;    <span class="hljs-attribute">proxy_connect_timeout</span> <span class="hljs-number">75s</span>;    <span class="hljs-attribute">proxy_send_timeout</span> <span class="hljs-number">300s</span>;    <span class="hljs-attribute">proxy_buffer_size</span> <span class="hljs-number">32k</span>;    <span class="hljs-attribute">proxy_buffers</span> <span class="hljs-number">8</span> <span class="hljs-number">32k</span>;    <span class="hljs-attribute">proxy_busy_buffers_size</span> <span class="hljs-number">64k</span>;&#125;</code></pre><p>Ваш ключ OpenRouter передаётся из заголовка <code>Authorization</code>, который присылает VS Code, поэтому на сервере он не хранится. Location покрывает только путь Chat Completions, поэтому другие эндпоинты, например <code>/v1/models</code>, не проксируются. Здесь это нормально, потому что модели перечислены вручную в JSON.</p><h3 id="Замечания-по-безопасности"><a href="#Замечания-по-безопасности" class="headerlink" title="Замечания по безопасности"></a>Замечания по безопасности</h3><ul><li>Прокси видит ваш API-ключ и каждый промпт, включая код вашего проекта. Запускайте его только на сервере, который вы контролируете.</li><li>Публичным прокси может воспользоваться любой, кто его найдёт. Ограничьте доступ, например через <code>allow</code>&#x2F;<code>deny</code> для ваших IP-адресов и <code>limit_req</code> для ограничения частоты запросов. Basic auth здесь не подойдёт, потому что использует тот же заголовок <code>Authorization</code>, в котором передаётся ваш ключ OpenRouter.</li><li>Прежде чем обходить сетевые блокировки, ознакомьтесь с условиями использования OpenRouter и региональными ограничениями, которые к вам относятся.</li></ul><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><p><strong>Модель не появляется в списке.</strong> Сначала убедитесь, что вы в VS Code Insiders, а не в обычном VS Code, и что правили файл в папке <code>Code - Insiders</code>. Затем проверьте, что JSON корректен и у провайдера задан API-ключ.</p><p><strong>403, а заголовок <code>Authorization</code> содержит только <code>Bearer</code>.</strong> У меня так было, когда я записал сырой ключ в <code>apiKey</code>. Выполните <strong>Update API Key</strong> (см. выше), чтобы VS Code сохранил ключ как секрет. Если вы используете прокси nginx, посмотреть, что реально приходит, можно, временно добавив <code>$http_authorization</code> в формат лога. Потом уберите его: он записывает ваш ключ в лог.</p><p><strong><code>404 — This model is unavailable for free</code>.</strong> Это я получил, когда попробовал <code>:free</code>-модель:</p><pre><code class="hljs text">&#123;&quot;message&quot;:&quot;This model is unavailable for free. The paid version is available now - use this slug instead: deepseek/deepseek-r1&quot;,&quot;code&quot;:404&#125;</code></pre><p>Бесплатные варианты моделей убирают. В сообщении указан платный slug, подставьте его в <code>id</code>. Эта ошибка также показывает, что запрос дошёл до OpenRouter и ключ принят, так что конфигурация и прокси в порядке.</p><p><strong>401.</strong> API-ключ отсутствует или неверен. Выполните <strong>Update API Key</strong> ещё раз.</p><p><strong>Agent игнорирует инструменты или падает.</strong> Модель или конкретный провайдер, на который OpenRouter направляет запрос, могут не поддерживать tool calling. Проверьте страницу модели и попробуйте другую.</p><p><strong>Ответы зависают или приходят сразу целиком (при использовании прокси).</strong> Обычно это буферизация. Убедитесь, что задано <code>proxy_buffering off;</code>.</p><h2 id="О-приватности"><a href="#О-приватности" class="headerlink" title="О приватности"></a>О приватности</h2><p>В режиме Agent части вашего проекта отправляются провайдеру модели в виде промптов. OpenRouter пересылает их провайдеру, который обслуживает модель, поэтому изучите политику обработки данных у моделей, которые используете, особенно у бесплатных.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;em&gt;Проверено на VS Code 1.140 (Stable) и VS Code Insiders 1.141, октябрь 2026.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Я хотел использовать модели OpenRouter в VS</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="vscode" scheme="https://optinsoft.net/blog/tags/vscode/"/>
    
    <category term="vscode-insiders" scheme="https://optinsoft.net/blog/tags/vscode-insiders/"/>
    
    <category term="openrouter" scheme="https://optinsoft.net/blog/tags/openrouter/"/>
    
    <category term="copilot" scheme="https://optinsoft.net/blog/tags/copilot/"/>
    
    <category term="llm" scheme="https://optinsoft.net/blog/tags/llm/"/>
    
    <category term="ai-agents" scheme="https://optinsoft.net/blog/tags/ai-agents/"/>
    
    <category term="nginx" scheme="https://optinsoft.net/blog/tags/nginx/"/>
    
    <category term="reverse-proxy" scheme="https://optinsoft.net/blog/tags/reverse-proxy/"/>
    
  </entry>
  
  <entry>
    <title>Using OpenRouter in the VS Code Agent without GitHub Copilot</title>
    <link href="https://optinsoft.net/blog/2026/10/07/Using-OpenRouter-in-the-VS-Code-Agent-without-GitHub-Copilot/"/>
    <id>https://optinsoft.net/blog/2026/10/07/Using-OpenRouter-in-the-VS-Code-Agent-without-GitHub-Copilot/</id>
    <published>2026-10-07T05:12:52.000Z</published>
    <updated>2026-10-07T05:18:20.028Z</updated>
    
    <content type="html"><![CDATA[<p><em>Tested on VS Code 1.140 (Stable) and VS Code Insiders 1.141, October 2026.</em></p><p>I wanted to use OpenRouter models in the VS Code Agent without signing in to GitHub Copilot. VS Code can do this with custom language model providers, but on my machine it only worked in <strong>VS Code Insiders</strong>. This post shows the setup, how to tell whether you need Insiders, and an nginx reverse proxy for networks where OpenRouter is not reachable.</p><h2 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h2><ol><li>Try the regular VS Code first. If you see the symptoms described below, switch to <strong>VS Code Insiders</strong>.</li><li>Create <code>chatLanguageModels.json</code> with an OpenRouter provider (<code>vendor: &quot;customendpoint&quot;</code>) and your models. Use the exact model ID from openrouter.ai.</li><li>Set the API key with <strong>Manage Models → Update API Key</strong>. Don’t leave the raw key in the JSON: it didn’t work for me.</li><li>Select the model in <strong>Models</strong> and switch to <strong>Agent</strong>.</li><li>If OpenRouter is blocked in your country, see the proxy section below.</li></ol><h2 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h2><ul><li>An OpenRouter account with an API key and some credit. Free models come and go (see Troubleshooting).</li><li>A model that supports <strong>tool calling</strong>. Agent mode needs it.</li><li>VS Code Insiders, if the regular VS Code doesn’t work for you (see the next section).</li></ul><h2 id="Do-you-need-VS-Code-Insiders"><a href="#Do-you-need-VS-Code-Insiders" class="headerlink" title="Do you need VS Code Insiders?"></a>Do you need VS Code Insiders?</h2><p>Maybe not, so try the regular VS Code first. On my machine (Stable 1.140), it didn’t work:</p><ul><li>With an empty <code>chatLanguageModels.json</code> there was no <strong>Manage Models</strong> button and no <strong>Chat: Manage Language Models</strong> command.</li><li>After I added a provider to the file by hand and restarted, the button and the command appeared, but the Language Models dialog showed only Copilot’s own models. My OpenRouter models were nowhere to be found, and I couldn’t select them in the chat.</li><li><strong>Add Models → OpenRouter</strong> accepted an API key and wrote a group to <code>chatLanguageModels.json</code>, but the group never showed up in the dialog or in the model picker, even after a restart.</li></ul><p>I didn’t dig into why. The <a href="https://code.visualstudio.com/docs/agent-customization/language-models">official docs</a> currently list the Custom endpoint provider as an Insiders feature, and in Insiders everything below worked. So if you see the same symptoms, install VS Code Insiders. It installs side by side with the regular VS Code and keeps its own settings.</p><h2 id="Where-is-chatLanguageModels-json"><a href="#Where-is-chatLanguageModels-json" class="headerlink" title="Where is chatLanguageModels.json"></a>Where is <code>chatLanguageModels.json</code></h2><p>The file lives in the user data folder of VS Code Insiders, which is separate from the regular VS Code (<code>Code - Insiders</code>, not <code>Code</code>):</p><ul><li><strong>Windows:</strong> press <code>Win + R</code>, enter <code>%APPDATA%\Code - Insiders\User</code> and press Enter.</li><li><strong>macOS:</strong> in Finder press <code>Cmd + Shift + G</code> and go to <code>~/Library/Application Support/Code - Insiders/User</code>.</li><li><strong>Linux:</strong> go to <code>~/.config/Code - Insiders/User</code>.</li></ul><blockquote><p><strong>Note:</strong> the regular VS Code uses <code>Code</code> instead of <code>Code - Insiders</code> in these paths. You can edit the file there, but if the regular VS Code doesn’t show your models, editing it won’t help. Edit the Insiders copy.</p></blockquote><h2 id="Create-the-provider-in-chatLanguageModels-json"><a href="#Create-the-provider-in-chatLanguageModels-json" class="headerlink" title="Create the provider in chatLanguageModels.json"></a>Create the provider in <code>chatLanguageModels.json</code></h2><p>I didn’t add the provider through the <strong>Manage Models</strong> dialog. I wrote it in <code>chatLanguageModels.json</code> by hand and VS Code picked it up. This is the file I use:</p><pre><code class="hljs json"><span class="hljs-punctuation">[</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;OpenRouter&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vendor&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;customendpoint&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;$&#123;input:chat.lm.secret.-32b9a01a&#125;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;apiType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;chat-completions&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;models&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;qwen/qwen3.8-27b&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Qwen3.8 27B&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://openrouter.ai/api/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;toolCalling&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxInputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">262000</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxOutputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">16000</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;deepseek/deepseek-r1&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DeepSeek R1&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://openrouter.ai/api/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;toolCalling&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;vision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxInputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">64000</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;maxOutputTokens&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">16000</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">]</span></code></pre><p>The <code>apiKey</code> value is what VS Code writes after the next step. Until then, don’t worry about it.</p><p>What the fields mean:</p><ul><li><code>vendor: &quot;customendpoint&quot;</code> marks this as a custom model provider.</li><li><code>apiType: &quot;chat-completions&quot;</code> makes VS Code use the Chat Completions API.</li><li><code>id</code> is the model ID on OpenRouter. Copy it from the model’s page.</li><li><code>url</code> is the base URL. VS Code builds the Chat Completions request from it (the request ends up at <code>/v1/chat/completions</code>), which is why it is <code>.../api/</code> and not the full endpoint.</li><li><code>toolCalling</code> enables tool calling. Without it the Agent can’t work.</li><li><code>vision</code> says whether the model accepts images.</li><li><code>maxInputTokens</code> and <code>maxOutputTokens</code> are the limits VS Code uses.</li></ul><h3 id="Choosing-values-for-a-model"><a href="#Choosing-values-for-a-model" class="headerlink" title="Choosing values for a model"></a>Choosing values for a model</h3><p>Take <code>toolCalling</code> and <code>vision</code> from the model’s page on OpenRouter. For example, Qwen3.8 27B is a vision-language model that accepts tools, while DeepSeek R1 is text-only, so <code>vision</code> is <code>false</code> there.</p><p>Limits are less clear-cut. The context size can differ between providers and sources: for R1 I’ve seen 64K on the model page and 131K-164K elsewhere, and for Qwen3.8 27B 262K and 1M. I use the smaller figure, which is safe with any provider. Don’t set <code>maxOutputTokens</code> too low for reasoning models, because reasoning tokens count as output. 4096 is too small, since answers get cut off.</p><p>If you want a Claude model, the same block works with the slug from its OpenRouter page (for Sonnet 5 it is <code>anthropic/claude-sonnet-5</code>, with <code>vision: true</code> and <code>toolCalling: true</code>).</p><h2 id="Set-the-API-key-this-step-is-not-optional"><a href="#Set-the-API-key-this-step-is-not-optional" class="headerlink" title="Set the API key (this step is not optional)"></a>Set the API key (this step is not optional)</h2><p>It might seem natural to write your OpenRouter key directly into <code>apiKey</code>:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sk-or-v1-...&quot;</span></code></pre><p>That is what I did first, and it didn’t work. OpenRouter rejected every request with a 403. I checked the nginx log and found that the <code>Authorization</code> header contained just <code>Bearer</code>, with no token after it.</p><p>What fixed it: in <strong>Manage Models</strong> I selected the OpenRouter provider and chose <strong>Update API Key</strong>, then entered the key there. VS Code replaced the value in the file with a reference to a secret it stores itself:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;$&#123;input:chat.lm.secret.-32b9a01a&#125;&quot;</span></code></pre><p>After that the header was passed correctly and requests went through. As a bonus, the key no longer sits in a file. The reference only works on the machine where you ran <strong>Update API Key</strong>, so don’t copy it to another machine. Run <strong>Update API Key</strong> there too.</p><p>VS Code replaced the raw key in the file with that reference on its own, so nothing else needs cleaning up.</p><h2 id="Using-the-model"><a href="#Using-the-model" class="headerlink" title="Using the model"></a>Using the model</h2><p>Go back to the Chat view, click <strong>Models</strong>, pick the OpenRouter model and switch to <strong>Agent</strong>. VS Code stays the interface and the agent, while OpenRouter provides the model, so you can swap models by editing one entry in the JSON.</p><h2 id="If-OpenRouter-is-not-reachable-from-your-country"><a href="#If-OpenRouter-is-not-reachable-from-your-country" class="headerlink" title="If OpenRouter is not reachable from your country"></a>If OpenRouter is not reachable from your country</h2><p>If OpenRouter works for you, skip this section. Otherwise, you can route requests through an nginx reverse proxy on a VPS that can reach it:</p><pre><code class="hljs text">VS Code → https://example.com/openrouter/api/ → nginx → https://openrouter.ai/api/v1/chat/completions</code></pre><p>In the model config, change the URL:</p><pre><code class="hljs json"><span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://example.com/openrouter/api/&quot;</span></code></pre><h3 id="nginx-configuration"><a href="#nginx-configuration" class="headerlink" title="nginx configuration"></a>nginx configuration</h3><p>This is the configuration that works for me (TLS for <code>example.com</code> is assumed to be configured already):</p><pre><code class="hljs nginx"><span class="hljs-section">location</span> /openrouter/api/v1/chat/completions &#123;    <span class="hljs-attribute">proxy_pass</span> https://openrouter.ai/api/v1/chat/completions;    <span class="hljs-attribute">proxy_ssl_server_name</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_ssl_name</span> openrouter.ai;    <span class="hljs-attribute">proxy_ssl_protocols</span> TLSv1.<span class="hljs-number">2</span> TLSv1.<span class="hljs-number">3</span>;    <span class="hljs-attribute">proxy_ssl_session_reuse</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_set_header</span> Host openrouter.ai;    <span class="hljs-attribute">proxy_set_header</span> Authorization <span class="hljs-variable">$http_authorization</span>;    <span class="hljs-attribute">proxy_buffering</span> <span class="hljs-literal">off</span>;    <span class="hljs-attribute">proxy_cache</span> <span class="hljs-literal">off</span>;    <span class="hljs-attribute">chunked_transfer_encoding</span> <span class="hljs-literal">on</span>;    <span class="hljs-attribute">proxy_read_timeout</span> <span class="hljs-number">300s</span>;    <span class="hljs-attribute">proxy_connect_timeout</span> <span class="hljs-number">75s</span>;    <span class="hljs-attribute">proxy_send_timeout</span> <span class="hljs-number">300s</span>;    <span class="hljs-attribute">proxy_buffer_size</span> <span class="hljs-number">32k</span>;    <span class="hljs-attribute">proxy_buffers</span> <span class="hljs-number">8</span> <span class="hljs-number">32k</span>;    <span class="hljs-attribute">proxy_busy_buffers_size</span> <span class="hljs-number">64k</span>;&#125;</code></pre><p>Your OpenRouter key is passed through from the <code>Authorization</code> header sent by VS Code, so it is never stored on the server. The location covers only the Chat Completions path, so other endpoints, such as <code>/v1/models</code>, are not proxied. That’s fine here because the models are listed manually in the JSON.</p><h3 id="Security-notes"><a href="#Security-notes" class="headerlink" title="Security notes"></a>Security notes</h3><ul><li>The proxy sees your API key and every prompt, including your project’s code. Only run it on a server you control.</li><li>A public proxy can be used by anyone who finds it. Restrict access, for example with <code>allow</code>&#x2F;<code>deny</code> for your IP addresses and <code>limit_req</code> for rate limiting. Basic auth won’t work here, because it uses the same <code>Authorization</code> header that carries your OpenRouter key.</li><li>Check OpenRouter’s terms of service and any regional restrictions that apply to you before routing around network blocks.</li></ul><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><p><strong>The model doesn’t appear in the picker.</strong> First check that you are in VS Code Insiders and not in the regular VS Code, and that you edited the file in the <code>Code - Insiders</code> folder. Then check that the JSON is valid and that the provider has an API key set.</p><p><strong>403, and the <code>Authorization</code> header contains only <code>Bearer</code>.</strong> I got this when I wrote the raw key into <code>apiKey</code>. Run <strong>Update API Key</strong> (see above) so that VS Code stores the key as a secret. If you use the nginx proxy, you can see what actually arrives by temporarily logging <code>$http_authorization</code> in your log format. Remove it afterwards, because it puts your key into the log.</p><p><strong><code>404 — This model is unavailable for free</code>.</strong> This is what I got when I tried a <code>:free</code> model:</p><pre><code class="hljs text">&#123;&quot;message&quot;:&quot;This model is unavailable for free. The paid version is available now - use this slug instead: deepseek/deepseek-r1&quot;,&quot;code&quot;:404&#125;</code></pre><p>Free variants of models get removed. The message contains the paid slug, so put it into <code>id</code>. The error also tells you that the request reached OpenRouter and the key was accepted, so the config and the proxy are fine.</p><p><strong>401.</strong> The API key is missing or wrong. Run <strong>Update API Key</strong> again.</p><p><strong>The Agent ignores tools or fails.</strong> The model, or the specific provider OpenRouter routes you to, may not support tool calling. Check the model’s page and try another model.</p><p><strong>Responses hang or arrive all at once (when using the proxy).</strong> This is usually buffering. Make sure <code>proxy_buffering off;</code> is set.</p><h2 id="A-note-on-privacy"><a href="#A-note-on-privacy" class="headerlink" title="A note on privacy"></a>A note on privacy</h2><p>In Agent mode, parts of your project are sent to the model provider as prompts. OpenRouter forwards them to the provider that serves the model, so check the data policy of the models you use, especially the free ones.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;em&gt;Tested on VS Code 1.140 (Stable) and VS Code Insiders 1.141, October 2026.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wanted to use OpenRouter models in the VS</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="vscode" scheme="https://optinsoft.net/blog/tags/vscode/"/>
    
    <category term="vscode-insiders" scheme="https://optinsoft.net/blog/tags/vscode-insiders/"/>
    
    <category term="openrouter" scheme="https://optinsoft.net/blog/tags/openrouter/"/>
    
    <category term="copilot" scheme="https://optinsoft.net/blog/tags/copilot/"/>
    
    <category term="llm" scheme="https://optinsoft.net/blog/tags/llm/"/>
    
    <category term="ai-agents" scheme="https://optinsoft.net/blog/tags/ai-agents/"/>
    
    <category term="nginx" scheme="https://optinsoft.net/blog/tags/nginx/"/>
    
    <category term="reverse-proxy" scheme="https://optinsoft.net/blog/tags/reverse-proxy/"/>
    
  </entry>
  
  <entry>
    <title>Integrating Developer PowerShell into Visual Studio Code</title>
    <link href="https://optinsoft.net/blog/2026/10/04/Integrating-Developer-PowerShell-x64-and-x86-into-Visual-Studio-Code/"/>
    <id>https://optinsoft.net/blog/2026/10/04/Integrating-Developer-PowerShell-x64-and-x86-into-Visual-Studio-Code/</id>
    <published>2026-10-04T16:00:04.000Z</published>
    <updated>2026-10-04T16:09:06.426Z</updated>
    
    <content type="html"><![CDATA[<p>To build projects using the MSVC compiler (cl.exe), CMake, and Ninja directly from the Visual Studio Code interface, you need to add the Visual Studio environment profiles to the built-in terminal.</p><h2 id="Step-by-Step-Instructions"><a href="#Step-by-Step-Instructions" class="headerlink" title="Step-by-Step Instructions"></a>Step-by-Step Instructions</h2><ol><li>Open the settings.json configuration file:<ul><li>Press Ctrl + Shift + P.</li><li>Type Preferences: Open User Settings (JSON) into the search bar and press Enter.</li></ul></li><li>Add the profile configuration:<br>   Insert the following block of settings inside the main curly braces { … } of your settings.json file:</li></ol>   <pre><code class="hljs json"><span class="hljs-attr">&quot;terminal.integrated.profiles.windows&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Developer PowerShell (x64)&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PowerShell&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;overrideName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>            <span class="hljs-string">&quot;-NoExit&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;-Command&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;&amp; &#123; Import-Module &#x27;C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll&#x27;; Enter-VsDevShell 43240475 -SkipAutomaticLocation -DevCmdArguments &#x27;-arch=x64&#x27; &#125;&quot;</span>        <span class="hljs-punctuation">]</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Developer PowerShell (x86)&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;C:\\Windows\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;overrideName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>            <span class="hljs-string">&quot;-NoExit&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;-Command&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;&amp; &#123; Import-Module &#x27;C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll&#x27;; Enter-VsDevShell 43240475 -SkipAutomaticLocation &#125;&quot;</span>        <span class="hljs-punctuation">]</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>   (Note: The paths are specified for a standard Visual Studio 2022 Community installation. If you are using Professional or Enterprise editions, replace Community in the paths with the corresponding name).</p><ol start="3"><li>Apply the settings:<ul><li>Save the settings.json file.</li><li>Close active terminal sessions in VS Code using the trash can icon (Kill Terminal) in the terminal panel to refresh the profile list.</li></ul></li><li>Launch the terminals:<ul><li>Click the arrow next to the “plus” sign in the upper-right corner of the terminal panel.</li><li>Select the required architecture from the dropdown list: Developer PowerShell (x64) or Developer PowerShell (x86).</li></ul></li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;To build projects using the MSVC compiler (cl.exe), CMake, and Ninja directly from the Visual Studio Code interface, you need to add the</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="powershell" scheme="https://optinsoft.net/blog/tags/powershell/"/>
    
    <category term="vscode" scheme="https://optinsoft.net/blog/tags/vscode/"/>
    
    <category term="msvc" scheme="https://optinsoft.net/blog/tags/msvc/"/>
    
  </entry>
  
  <entry>
    <title>Интеграция Developer PowerShell в Visual Studio Code</title>
    <link href="https://optinsoft.net/blog/ru/2026/10/04/Integrating-Developer-PowerShell-x64-and-x86-into-Visual-Studio-Code/"/>
    <id>https://optinsoft.net/blog/ru/2026/10/04/Integrating-Developer-PowerShell-x64-and-x86-into-Visual-Studio-Code/</id>
    <published>2026-10-04T16:00:04.000Z</published>
    <updated>2026-10-04T16:15:01.494Z</updated>
    
    <content type="html"><![CDATA[<p>Для сборки проектов с использованием компилятора MSVC (cl.exe), CMake и Ninja непосредственно из интерфейса Visual Studio Code, необходимо добавить профили окружения Visual Studio во встроенный терминал.</p><h2 id="Пошаговая-инструкция"><a href="#Пошаговая-инструкция" class="headerlink" title="Пошаговая инструкция"></a>Пошаговая инструкция</h2><ol><li>Откройте конфигурационный файл settings.json:<ul><li>Нажмите комбинацию клавиш Ctrl + Shift + P.</li><li>Введите в поисковую строку Preferences: Open User Settings (JSON) и нажмите Enter.</li></ul></li><li>Добавьте конфигурацию профилей:<br>   Вставьте следующий блок настроек внутрь основных фигурных скобок { … } вашего файла settings.json:</li></ol><pre><code class="hljs json"><span class="hljs-attr">&quot;terminal.integrated.profiles.windows&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Developer PowerShell (x64)&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PowerShell&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;overrideName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>            <span class="hljs-string">&quot;-NoExit&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;-Command&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;&amp; &#123; Import-Module &#x27;C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll&#x27;; Enter-VsDevShell 43240475 -SkipAutomaticLocation -DevCmdArguments &#x27;-arch=x64&#x27; &#125;&quot;</span>        <span class="hljs-punctuation">]</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Developer PowerShell (x86)&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;C:\\Windows\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;overrideName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>            <span class="hljs-string">&quot;-NoExit&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;-Command&quot;</span><span class="hljs-punctuation">,</span>            <span class="hljs-string">&quot;&amp; &#123; Import-Module &#x27;C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\Microsoft.VisualStudio.DevShell.dll&#x27;; Enter-VsDevShell 43240475 -SkipAutomaticLocation &#125;&quot;</span>        <span class="hljs-punctuation">]</span>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>   (Примечание: Пути указаны для стандартной установки Visual Studio 2022 Community. При использовании редакций Professional или Enterprise замените Community в путях на соответствующее наименование).</p><ol start="3"><li>Применение настроек:<ul><li>Сохраните файл settings.json.</li><li>Закройте активные сессии терминала в VS Code с помощью иконки корзины (Kill Terminal) на панели терминала для обновления списка профилей.</li></ul></li><li>Запуск терминалов:<ul><li>Нажмите на стрелку рядом со знаком «плюс» в верхнем правом углу панели терминала.</li><li>Выберите необходимую архитектуру из выпадающего списка: Developer PowerShell (x64) или Developer PowerShell (x86).</li></ul></li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Для сборки проектов с использованием компилятора MSVC (cl.exe), CMake и Ninja непосредственно из интерфейса Visual Studio Code,</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="powershell" scheme="https://optinsoft.net/blog/tags/powershell/"/>
    
    <category term="vscode" scheme="https://optinsoft.net/blog/tags/vscode/"/>
    
    <category term="msvc" scheme="https://optinsoft.net/blog/tags/msvc/"/>
    
  </entry>
  
  <entry>
    <title>BoringSSL on Top of Any Stream in .NET</title>
    <link href="https://optinsoft.net/blog/2026/10/01/BoringSSL-on-Top-of-Any-Stream-in-NET/"/>
    <id>https://optinsoft.net/blog/2026/10/01/BoringSSL-on-Top-of-Any-Stream-in-NET/</id>
    <published>2026-10-01T15:07:00.000Z</published>
    <updated>2026-10-01T15:13:41.728Z</updated>
    
    <content type="html"><![CDATA[<p>The standard <code>SslStream</code> in .NET is good in every way except one: it doesn’t let you control what your ClientHello looks like. The set of ciphers, the order of extensions, GREASE, ALPS, post-quantum groups are determined by the platform (SChannel, OpenSSL) and are almost non-configurable. Usually this isn’t a problem. But if a client needs to look like Chrome in TLS fingerprints (JA3&#x2F;JA4), <code>SslStream</code> won’t do.</p><p>Chrome has its own TLS stack, BoringSSL, Google’s fork of OpenSSL. I built a proof-of-concept <strong><a href="https://github.com/optinsoft/BoringSslConsole">BoringSslConsole</a></strong>: a .NET 8 console application and a <code>BoringSslStream</code> wrapper that exposes BoringSSL as an ordinary <code>Stream</code>.</p><p>Sources: <a href="https://github.com/optinsoft/BoringSslConsole">https://github.com/optinsoft/BoringSslConsole</a></p><p>The main architectural requirement for the wrapper is this: <strong>BoringSSL must not perform any network operations</strong>. All I&#x2F;O goes through the managed <code>innerStream</code>. Below I’ll explain why this is needed and how it’s structured.</p><h2 id="Why-a-“networkless”-BoringSSL"><a href="#Why-a-“networkless”-BoringSSL" class="headerlink" title="Why a “networkless” BoringSSL"></a>Why a “networkless” BoringSSL</h2><p>BoringSSL can work with the network itself: it has socket and file BIOs (<code>BIO_new_socket</code>, <code>BIO_new_fd</code>). If I had used them, the library would drive data through the socket itself, and there would be no way to implement asynchrony or substitute my own transport.</p><p>I needed something different: for the TLS layer to work on top of <strong>any</strong> <code>Stream</code>. This could be a <code>NetworkStream</code>, a tunnel through a proxy (CONNECT, SOCKS), a <code>MemoryStream</code> in tests, or another layer on top of TLS. Therefore the native part is responsible only for TLS state: handshake, encryption, and decryption of records. And C# moves the bytes to and from the network.</p><h2 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h2><pre><code class="hljs text">TcpClient   |NetworkStream  (innerStream)   |BoringSslStream (C#): pumps bytes between the stream and the buffers   |BoringSSL, memory BIO (pure TLS state, no network)   |HTTP/1.1 or HTTP/2</code></pre><p>Between C# and BoringSSL there are two in-memory buffers (<code>BIO_s_mem</code>):</p><ul><li><strong>input</strong>: C# puts bytes read from <code>innerStream</code> here (<code>pms_ssl_feed_read</code>);</li><li><strong>output</strong>: C# takes bytes from here that need to be sent to <code>innerStream</code> (<code>pms_ssl_take_write</code>, the size can be queried via <code>pms_ssl_pending_write</code>).</li></ul><p>The native DLL (<code>proxymap_boringssl.dll</code>) exports a small flat C API: <code>pms_ssl_create</code>, <code>pms_ssl_connect</code>, <code>pms_ssl_read</code>, <code>pms_ssl_write</code>, plus buffer-pumping functions and a few getters (protocol version, cipher, selected ALPN, server DER certificate, last error). All ClientHello parameters are passed from C# as strings and numbers.</p><p><code>BoringSslStream</code> inherits from <code>Stream</code>, so it can be substituted anywhere an ordinary stream is expected. Everything is fully asynchronous, with <code>CancellationToken</code>.</p><h2 id="The-main-loop-pumping-bytes"><a href="#The-main-loop-pumping-bytes" class="headerlink" title="The main loop: pumping bytes"></a>The main loop: pumping bytes</h2><p>The whole mechanism fits into a single pattern. We call a BoringSSL operation, flush the accumulated output to the network, and if the library asks for data, we read it from the network and feed it back. Here is the handshake:</p><pre><code class="hljs csharp"><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>)&#123;    <span class="hljs-built_in">int</span> result = Native.pms_ssl_connect(_connection);    <span class="hljs-keyword">await</span> FlushWriteBioAsync(cancellationToken);    <span class="hljs-keyword">switch</span> (result)    &#123;        <span class="hljs-keyword">case</span> Native.PMS_SSL_OK:            ValidateServerCertificate(_hostname);            _authenticated = <span class="hljs-literal">true</span>;            <span class="hljs-keyword">return</span>;        <span class="hljs-keyword">case</span> Native.PMS_SSL_WANT_READ:            <span class="hljs-keyword">await</span> ReadFromNetworkAsync(cancellationToken);            <span class="hljs-keyword">break</span>;        <span class="hljs-keyword">case</span> Native.PMS_SSL_WANT_WRITE:            <span class="hljs-keyword">break</span>; <span class="hljs-comment">// output has already been flushed to the network</span>    &#125;&#125;</code></pre><p><code>FlushWriteBioAsync</code> takes everything accumulated in the output buffer and writes it to <code>innerStream</code>. <code>ReadFromNetworkAsync</code> reads from <code>innerStream</code> and feeds the bytes to the input buffer. The same loop is repeated in <code>ReadAsync</code> and <code>WriteAsync</code>.</p><p>The output must be flushed after <em>every</em> BoringSSL call, not just during the handshake: even <code>SSL_read</code> can itself generate TLS records (for example, KeyUpdate or alerts).</p><p>Buffers are taken from <code>ArrayPool&lt;byte&gt;</code>, and pointers are passed to native code via <code>Memory&lt;T&gt;.Pin()</code>, without unnecessary copies.</p><h2 id="Synchronization"><a href="#Synchronization" class="headerlink" title="Synchronization"></a>Synchronization</h2><p>The class has four <code>SemaphoreSlim</code>s: <code>_readLock</code> and <code>_writeLock</code> serialize reads and writes respectively, while <code>_networkReadLock</code> and <code>_networkWriteLock</code> protect the exchange with <code>innerStream</code>. Thanks to this, concurrent <code>ReadAsync</code> and <code>WriteAsync</code> calls don’t break each other’s work with the stream and buffers. The handshake takes both outer locks at once.</p><p>A caveat: this is sufficient for a PoC, but a full-fledged full-duplex over a single <code>SSL*</code> requires care on the native library side as well, not just in C#. I’ll mention this again in the limitations.</p><h2 id="The-“like-Chrome”-preset"><a href="#The-“like-Chrome”-preset" class="headerlink" title="The “like Chrome” preset"></a>The “like Chrome” preset</h2><p>The <code>BoringSslStream</code> constructor accepts a set of parameters, filled by default with Chrome’s values:</p><ul><li>cipher list: ECDHE variants of AES-GCM and ChaCha20-Poly1305, plus a legacy tail;</li><li>GREASE and ECH GREASE;</li><li>ALPN <code>h2:http/1.1</code> and ALPS <code>h2</code>;</li><li>signature algorithms, including post-quantum <code>ML-DSA-44/65/87</code>;</li><li>SCT, <code>status_request</code>, Brotli certificate compression;</li><li><code>trust_anchors</code> as a hex string.</li></ul><p>The fine-grained work with extensions (reordering, dynamic formation of ALPN&#x2F;ALPS, padding of <code>status_request</code>) is done on the C++ side. Brotli is linked statically so that everything builds into a single self-contained DLL and there’s no <code>DllNotFoundException</code> at runtime.</p><p>On <code>tls.peet.ws</code> the <strong>JA4</strong> fingerprint matched Chrome. But <strong>JA3</strong> does not match, and that’s how it should be. Starting with Chrome 110, the browser randomly shuffles the order of TLS extensions in ClientHello on every connection. JA3 is built from the lists of ciphers, extensions, groups, and point formats in the order they arrived, so a real Chrome’s JA3 hash is different every time, and comparing it to some “reference” is meaningless. Our client does the same: the native layer shuffles extensions, and JA3 changes from connection to connection, just like in the browser.</p><p>JA4 is structured differently: before hashing, it sorts ciphers and extensions (and discards GREASE), so shuffling doesn’t affect it. That’s exactly why JA4 is stable and suitable for verification: if it matches, then the set of ciphers, extensions, signature algorithms, and ALPN is the same as Chrome’s.</p><h2 id="Certificate-verification"><a href="#Certificate-verification" class="headerlink" title="Certificate verification"></a>Certificate verification</h2><p>Here BoringSSL only negotiates the channel, while the trust decision is made by .NET. Immediately after a successful handshake, the native layer returns the server’s DER certificate, and then:</p><pre><code class="hljs csharp"><span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> certificate = <span class="hljs-keyword">new</span> X509Certificate2(memory.Span);<span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> chain = <span class="hljs-keyword">new</span> X509Chain();chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;chain.ChainPolicy.ApplicationPolicy.Add(<span class="hljs-keyword">new</span> Oid(<span class="hljs-string">&quot;1.3.6.1.5.5.7.3.1&quot;</span>)); <span class="hljs-comment">// Server Auth</span><span class="hljs-keyword">if</span> (!chain.Build(certificate))    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AuthenticationException(...);<span class="hljs-keyword">if</span> (!certificate.MatchesHostname(hostname))    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AuthenticationException(...);</code></pre><p>The system root certificate store is used, online revocation checking is enabled, and the hostname is verified via <code>MatchesHostname</code> (available in .NET 8, takes SAN and wildcard rules into account).</p><p>It’s easy to test on badssl.com: <code>expired.badssl.com</code> fails on chain building, <code>untrusted-root.badssl.com</code> on an unknown root, <code>revoked.badssl.com</code> on revocation checking. The relevant hosts are commented out in <code>Program.cs</code>; just change one constant.</p><h2 id="HTTP-2-by-hand"><a href="#HTTP-2-by-hand" class="headerlink" title="HTTP&#x2F;2 by hand"></a>HTTP&#x2F;2 by hand</h2><p>Since ALPN negotiates <code>h2</code>, I had to write a minimal HTTP&#x2F;2 client right in <code>Program.cs</code>, without third-party libraries.</p><p><strong>Sending.</strong> First a 24-byte connection preface, then an empty <code>SETTINGS</code>. Then the headers: a tiny <code>HpackEncoder</code> writes indexed fields from the static table (<code>:method: GET</code> is index 2, <code>:scheme: https</code> is 7) and literals without indexing for <code>:authority</code>, <code>:path</code>, and the rest. The block is wrapped in a 9-byte <code>HEADERS</code> frame header with flags <code>END_STREAM | END_HEADERS</code> on stream 1.</p><p><strong>Receiving.</strong> We read strictly 9 bytes of frame header, parse the length (24 bits), type, flags, and <code>stream id</code>, then read exactly that many bytes of payload. Then we parse by type:</p><ul><li><code>DATA</code> is written to a file and to the console, and on <code>END_STREAM</code> we exit;</li><li><code>HEADERS</code> prints the status (see below) and also checks for <code>END_STREAM</code>;</li><li><code>SETTINGS</code> without the ACK flag is acknowledged with <code>SETTINGS ACK</code>, as RFC requires;</li><li><code>RST_STREAM</code> and <code>GOAWAY</code> are printed with a decoded error code;</li><li><code>WINDOW_UPDATE</code> is ignored.</li></ul><p>There’s no full HPACK decoder, so the response status is determined by a heuristic: we look in the block for bytes <code>0x88</code>–<code>0x8E</code>, corresponding to indexed statuses from the static table (<code>0x88</code> is 200, <code>0x8D</code> is 404). For a demo this is enough, but it’s precisely a hack.</p><p>If ALPN returns <code>http/1.1</code>, everything is simpler: an ordinary text request with <code>Connection: close</code> goes out, and the response is read until the stream closes.</p><h2 id="Running"><a href="#Running" class="headerlink" title="Running"></a>Running</h2><pre><code class="hljs powershell">git clone <span class="hljs-literal">--recursive</span> https://github.com/optinsoft/BoringSslConsole.git<span class="hljs-built_in">cd</span> Nativecmake <span class="hljs-literal">-G</span> Ninja <span class="hljs-literal">-B</span> build <span class="hljs-literal">-DCMAKE_BUILD_TYPE</span>=Releasecmake <span class="hljs-literal">--build</span> build<span class="hljs-built_in">cd</span> ..dotnet run <span class="hljs-literal">--project</span> BoringSslConsole</code></pre><p>To build on Windows you need the .NET 8 SDK, Visual Studio 2022 with C++ tools, CMake 3.22+, Ninja, Git, NASM, and Go (the last two are required by BoringSSL itself). BoringSSL and Brotli are included as git submodules; after building, the DLL is copied to the .NET output directory. The response is printed to the console and saved to <code>./output/&lt;host&gt;.txt</code>.</p><h2 id="Results-and-limitations"><a href="#Results-and-limitations" class="headerlink" title="Results and limitations"></a>Results and limitations</h2><p>The result is a compact asynchronous <code>Stream</code> with a Chrome-like TLS stack inside, with real certificate verification, while the network is entirely in .NET’s hands. On top of it you can build proxies, an HTTP client, or testing tools, and the transport can be swapped without touching the TLS layer.</p><p>An honest list of what’s missing here:</p><ul><li><strong>HTTP&#x2F;2 is minimal:</strong> no HPACK decoder, no <code>CONTINUATION</code>, no flow control, no multiplexing.</li><li><strong>The TLS fingerprint matches, not the whole client.</strong> HTTP&#x2F;2 has its own fingerprint (<code>SETTINGS</code> values, <code>WINDOW_UPDATE</code>, pseudo-header order), and here it differs from Chrome’s.</li><li><strong>Thread safety needs separate verification.</strong> The locks in C# separate reads, writes, and network operations, but the safety of concurrent <code>SSL_read</code> and <code>SSL_write</code> on a single <code>SSL*</code> also depends on the native layer. Freeing the <code>SSL*</code> in <code>Dispose</code> should also be synchronized with in-flight operations.</li><li><strong>Chain validation:</strong> only the leaf certificate is taken from the native layer, so chain building may depend on AIA fetching of intermediates, and online revocation checking blocks the thread inside <code>async</code> code.</li><li><strong>Platform:</strong> building requires a heavy toolchain, and the native part is currently Windows x64 only.</li></ul><p>This is a PoC, but the architectural idea — “BoringSSL as a pure TLS machine, with I&#x2F;O outside” — turned out well, in my opinion, and easily transfers to other transports.</p><p>The project on GitHub: <a href="https://github.com/optinsoft/BoringSslConsole">optinsoft&#x2F;BoringSslConsole</a></p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;The standard &lt;code&gt;SslStream&lt;/code&gt; in .NET is good in every way except one: it doesn’t let you control what your ClientHello looks</summary>
        
      
    
    
    
    <category term="GitHub" scheme="https://optinsoft.net/blog/categories/GitHub/"/>
    
    
    <category term="dotnet" scheme="https://optinsoft.net/blog/tags/dotnet/"/>
    
    <category term="tls" scheme="https://optinsoft.net/blog/tags/tls/"/>
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="boringssl" scheme="https://optinsoft.net/blog/tags/boringssl/"/>
    
    <category term="csharp" scheme="https://optinsoft.net/blog/tags/csharp/"/>
    
    <category term="footprint" scheme="https://optinsoft.net/blog/tags/footprint/"/>
    
    <category term="fingerprint" scheme="https://optinsoft.net/blog/tags/fingerprint/"/>
    
    <category term="JA4" scheme="https://optinsoft.net/blog/tags/JA4/"/>
    
  </entry>
  
  <entry>
    <title>BoringSSL поверх любого Stream в .NET</title>
    <link href="https://optinsoft.net/blog/ru/2026/10/01/BoringSSL-on-Top-of-Any-Stream-in-NET/"/>
    <id>https://optinsoft.net/blog/ru/2026/10/01/BoringSSL-on-Top-of-Any-Stream-in-NET/</id>
    <published>2026-10-01T15:07:00.000Z</published>
    <updated>2026-10-01T15:18:35.217Z</updated>
    
    <content type="html"><![CDATA[<p>Стандартный <code>SslStream</code> в .NET хорош всем, кроме одного: он не даёт управлять тем, как выглядит ваш ClientHello. Набор шифров, порядок расширений, GREASE, ALPS, постквантовые группы определяются платформой (SChannel, OpenSSL) и почти не настраиваются. Обычно это не проблема. Но если клиент должен выглядеть в TLS-отпечатках (JA3&#x2F;JA4) как Chrome, <code>SslStream</code> не подходит.</p><p>У Chrome свой TLS-стек, BoringSSL, форк OpenSSL от Google. Я собрал proof-of-concept <strong><a href="https://github.com/optinsoft/BoringSslConsole">BoringSslConsole</a></strong>: консольное приложение на .NET 8 и обёртку <code>BoringSslStream</code>, которая даёт доступ к BoringSSL как к обычному <code>Stream</code>.</p><p>Исходники: <a href="https://github.com/optinsoft/BoringSslConsole">https://github.com/optinsoft/BoringSslConsole</a></p><p>Главное архитектурное требование к обёртке такое: <strong>BoringSSL не должен выполнять никаких сетевых операций</strong>. Весь ввод-вывод идёт через управляемый <code>innerStream</code>. Ниже объясню, зачем это нужно и как устроено.</p><h2 id="Зачем-нужен-«бессетевой»-BoringSSL"><a href="#Зачем-нужен-«бессетевой»-BoringSSL" class="headerlink" title="Зачем нужен «бессетевой» BoringSSL"></a>Зачем нужен «бессетевой» BoringSSL</h2><p>BoringSSL умеет сам работать с сетью: у него есть сокетные и файловые BIO (<code>BIO_new_socket</code>, <code>BIO_new_fd</code>). Если бы я их использовал, библиотека сама бы гоняла данные через сокет, и не было бы возможности реализовать асинхронность, а также подставить свой транспорт.</p><p>Мне нужно было другое: чтобы TLS-слой работал поверх <strong>любого</strong> <code>Stream</code>. Это может быть <code>NetworkStream</code>, туннель через прокси (CONNECT, SOCKS), <code>MemoryStream</code> в тестах или другой слой поверх TLS. Поэтому нативная часть отвечает только за состояние TLS: рукопожатие, шифрование и расшифровку записей. А байты в сеть и из сети гоняет C#.</p><h2 id="Архитектура"><a href="#Архитектура" class="headerlink" title="Архитектура"></a>Архитектура</h2><pre><code class="hljs text">TcpClient   |NetworkStream  (innerStream)   |BoringSslStream (C#): качает байты между потоком и буферами   |BoringSSL, memory BIO (чистое TLS-состояние, без сети)   |HTTP/1.1 или HTTP/2</code></pre><p>Между C# и BoringSSL стоят два in-memory буфера (<code>BIO_s_mem</code>):</p><ul><li><strong>входной</strong>: сюда C# кладёт байты, прочитанные из <code>innerStream</code> (<code>pms_ssl_feed_read</code>);</li><li><strong>выходной</strong>: отсюда C# забирает байты, которые нужно отправить в <code>innerStream</code> (<code>pms_ssl_take_write</code>, размер можно узнать через <code>pms_ssl_pending_write</code>).</li></ul><p>Нативная DLL (<code>proxymap_boringssl.dll</code>) экспортирует небольшой плоский C-API: <code>pms_ssl_create</code>, <code>pms_ssl_connect</code>, <code>pms_ssl_read</code>, <code>pms_ssl_write</code>, плюс функции прокачки буферов и несколько геттеров (версия протокола, шифр, выбранный ALPN, DER-сертификат сервера, последняя ошибка). Все параметры ClientHello передаются из C# строками и числами.</p><p><code>BoringSslStream</code> наследуется от <code>Stream</code>, поэтому подставляется везде, где ждут обычный поток. Всё полностью асинхронно, с <code>CancellationToken</code>.</p><h2 id="Главный-цикл-прокачка-байтов"><a href="#Главный-цикл-прокачка-байтов" class="headerlink" title="Главный цикл: прокачка байтов"></a>Главный цикл: прокачка байтов</h2><p>Весь механизм умещается в один паттерн. Вызываем операцию BoringSSL, сбрасываем накопившийся выход в сеть, а если библиотека просит данных, читаем их из сети и отдаём обратно. Вот рукопожатие:</p><pre><code class="hljs csharp"><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>)&#123;    <span class="hljs-built_in">int</span> result = Native.pms_ssl_connect(_connection);    <span class="hljs-keyword">await</span> FlushWriteBioAsync(cancellationToken);    <span class="hljs-keyword">switch</span> (result)    &#123;        <span class="hljs-keyword">case</span> Native.PMS_SSL_OK:            ValidateServerCertificate(_hostname);            _authenticated = <span class="hljs-literal">true</span>;            <span class="hljs-keyword">return</span>;        <span class="hljs-keyword">case</span> Native.PMS_SSL_WANT_READ:            <span class="hljs-keyword">await</span> ReadFromNetworkAsync(cancellationToken);            <span class="hljs-keyword">break</span>;        <span class="hljs-keyword">case</span> Native.PMS_SSL_WANT_WRITE:            <span class="hljs-keyword">break</span>; <span class="hljs-comment">// выход уже сброшен в сеть</span>    &#125;&#125;</code></pre><p><code>FlushWriteBioAsync</code> забирает из выходного буфера всё накопленное и пишет в <code>innerStream</code>. <code>ReadFromNetworkAsync</code> читает из <code>innerStream</code> и скармливает байты входному буферу. Тот же цикл повторяется в <code>ReadAsync</code> и <code>WriteAsync</code>.</p><p>Сбрасывать выход нужно после <em>каждого</em> вызова BoringSSL, а не только во время рукопожатия: даже <code>SSL_read</code> может сам порождать TLS-записи (например, KeyUpdate или алерты).</p><p>Буферы берутся из <code>ArrayPool&lt;byte&gt;</code>, а указатели передаются в нативный код через <code>Memory&lt;T&gt;.Pin()</code>, без лишних копирований.</p><h2 id="Синхронизация"><a href="#Синхронизация" class="headerlink" title="Синхронизация"></a>Синхронизация</h2><p>В классе четыре <code>SemaphoreSlim</code>: <code>_readLock</code> и <code>_writeLock</code> сериализуют соответственно чтения и записи, а <code>_networkReadLock</code> и <code>_networkWriteLock</code> защищают обмен с <code>innerStream</code>. Благодаря этому одновременные вызовы <code>ReadAsync</code> и <code>WriteAsync</code> не ломают друг другу работу с потоком и буферами. Рукопожатие берёт сразу оба внешних лока.</p><p>Оговорка: этого достаточно для PoC, но полноценный full-duplex поверх одного <code>SSL*</code> требует аккуратности и на стороне нативной библиотеки, а не только в C#. Об этом ещё скажу в ограничениях.</p><h2 id="Пресет-«как-Chrome»"><a href="#Пресет-«как-Chrome»" class="headerlink" title="Пресет «как Chrome»"></a>Пресет «как Chrome»</h2><p>Конструктор <code>BoringSslStream</code> принимает набор параметров, по умолчанию заполненных значениями Chrome:</p><ul><li>cipher list: ECDHE-варианты AES-GCM и ChaCha20-Poly1305, плюс легаси-хвост;</li><li>GREASE и ECH GREASE;</li><li>ALPN <code>h2:http/1.1</code> и ALPS <code>h2</code>;</li><li>алгоритмы подписи, включая постквантовые <code>ML-DSA-44/65/87</code>;</li><li>SCT, <code>status_request</code>, сжатие сертификатов Brotli;</li><li><code>trust_anchors</code> в виде hex-строки.</li></ul><p>Тонкая работа с расширениями (перестановка, динамическое формирование ALPN&#x2F;ALPS, паддинг <code>status_request</code>) выполнена на C++ стороне. Brotli подключён статически, чтобы всё собиралось в одну автономную DLL и не было <code>DllNotFoundException</code> в рантайме.</p><p>На <code>tls.peet.ws</code> совпал <strong>JA4</strong>-отпечаток Chrome. А вот <strong>JA3</strong> не совпадает, и так и должно быть. Начиная с Chrome 110 браузер случайно перемешивает порядок TLS-расширений в ClientHello при каждом соединении. JA3 строится из списков шифров, расширений, групп и форматов точек в том порядке, в котором они пришли, поэтому у настоящего Chrome JA3-хеш каждый раз новый, и сравнивать его с каким-то «эталоном» бессмысленно. Наш клиент делает то же самое: нативный слой переставляет расширения, и JA3 меняется от соединения к соединению, как у браузера.</p><p>JA4 устроен иначе: перед хешированием он сортирует шифры и расширения (и отбрасывает GREASE), так что перестановка на него не влияет. Именно поэтому JA4 стабилен и годится для проверки: если он совпал, набор шифров, расширений, алгоритмов подписи и ALPN такой же, как у Chrome.</p><h2 id="Проверка-сертификата"><a href="#Проверка-сертификата" class="headerlink" title="Проверка сертификата"></a>Проверка сертификата</h2><p>BoringSSL здесь только согласует канал, а решение о доверии принимает .NET. Сразу после успешного рукопожатия нативный слой отдаёт DER-сертификат сервера, и дальше:</p><pre><code class="hljs csharp"><span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> certificate = <span class="hljs-keyword">new</span> X509Certificate2(memory.Span);<span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> chain = <span class="hljs-keyword">new</span> X509Chain();chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;chain.ChainPolicy.ApplicationPolicy.Add(<span class="hljs-keyword">new</span> Oid(<span class="hljs-string">&quot;1.3.6.1.5.5.7.3.1&quot;</span>)); <span class="hljs-comment">// Server Auth</span><span class="hljs-keyword">if</span> (!chain.Build(certificate))    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AuthenticationException(...);<span class="hljs-keyword">if</span> (!certificate.MatchesHostname(hostname))    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AuthenticationException(...);</code></pre><p>Используется системное хранилище корневых сертификатов, включена онлайн-проверка отзыва, имя хоста проверяется через <code>MatchesHostname</code> (доступен в .NET 8, учитывает SAN и правила для wildcard).</p><p>Легко проверить на badssl.com: <code>expired.badssl.com</code> падает на построении цепочки, <code>untrusted-root.badssl.com</code> на неизвестном корне, <code>revoked.badssl.com</code> на проверке отзыва. Нужные хосты закомментированы в <code>Program.cs</code>, достаточно сменить одну константу.</p><h2 id="HTTP-2-руками"><a href="#HTTP-2-руками" class="headerlink" title="HTTP&#x2F;2 руками"></a>HTTP&#x2F;2 руками</h2><p>Раз ALPN договаривается о <code>h2</code>, пришлось написать минимальный HTTP&#x2F;2-клиент прямо в <code>Program.cs</code>, без сторонних библиотек.</p><p><strong>Отправка.</strong> Сначала 24-байтный connection preface, затем пустой <code>SETTINGS</code>. Потом заголовки: крошечный <code>HpackEncoder</code> пишет индексированные поля из статической таблицы (<code>:method: GET</code> это индекс 2, <code>:scheme: https</code> это 7) и литералы без индексирования для <code>:authority</code>, <code>:path</code> и остального. Блок оборачивается в 9-байтный заголовок кадра <code>HEADERS</code> с флагами <code>END_STREAM | END_HEADERS</code> на стриме 1.</p><p><strong>Приём.</strong> Читаем строго по 9 байт заголовка кадра, парсим длину (24 бита), тип, флаги и <code>stream id</code>, затем читаем ровно столько байт payload. Дальше разбираем по типу:</p><ul><li><code>DATA</code> пишем в файл и в консоль, на <code>END_STREAM</code> выходим;</li><li><code>HEADERS</code> выводим статус (см. ниже) и тоже смотрим на <code>END_STREAM</code>;</li><li><code>SETTINGS</code> без флага ACK подтверждаем <code>SETTINGS ACK</code>, как требует RFC;</li><li><code>RST_STREAM</code> и <code>GOAWAY</code> печатаем с расшифровкой кода ошибки;</li><li><code>WINDOW_UPDATE</code> игнорируем.</li></ul><p>Полноценного HPACK-декодера нет, поэтому статус ответа определяется эвристикой: ищем в блоке байты <code>0x88</code>–<code>0x8E</code>, соответствующие индексированным статусам из статической таблицы (<code>0x88</code> это 200, <code>0x8D</code> это 404). Для демонстрации хватает, но это именно хак.</p><p>Если ALPN вернул <code>http/1.1</code>, всё проще: уходит обычный текстовый запрос с <code>Connection: close</code>, ответ читается до закрытия потока.</p><h2 id="Запуск"><a href="#Запуск" class="headerlink" title="Запуск"></a>Запуск</h2><pre><code class="hljs powershell">git clone <span class="hljs-literal">--recursive</span> https://github.com/optinsoft/BoringSslConsole.git<span class="hljs-built_in">cd</span> Nativecmake <span class="hljs-literal">-G</span> Ninja <span class="hljs-literal">-B</span> build <span class="hljs-literal">-DCMAKE_BUILD_TYPE</span>=Releasecmake <span class="hljs-literal">--build</span> build<span class="hljs-built_in">cd</span> ..dotnet run <span class="hljs-literal">--project</span> BoringSslConsole</code></pre><p>Для сборки на Windows нужны .NET 8 SDK, Visual Studio 2022 с C++ tools, CMake 3.22+, Ninja, Git, NASM и Go (последние два требуются самому BoringSSL). BoringSSL и Brotli подключены как git-submodule, DLL после сборки копируется в выходной каталог .NET. Ответ выводится в консоль и сохраняется в <code>./output/&lt;host&gt;.txt</code>.</p><h2 id="Итоги-и-ограничения"><a href="#Итоги-и-ограничения" class="headerlink" title="Итоги и ограничения"></a>Итоги и ограничения</h2><p>Получился компактный асинхронный <code>Stream</code>, внутри которого Chrome-подобный TLS-стек с настоящей проверкой сертификата, а сеть полностью в руках .NET. Поверх него можно строить прокси, HTTP-клиент или тестовые инструменты, и транспорт при этом можно менять, не трогая TLS-слой.</p><p>Честный список того, чего здесь нет:</p><ul><li><strong>HTTP&#x2F;2 минимальный:</strong> нет HPACK-декодера, <code>CONTINUATION</code>, управления окнами и мультиплексирования.</li><li><strong>Совпадает TLS-отпечаток, а не весь клиент.</strong> У HTTP&#x2F;2 есть собственный отпечаток (значения <code>SETTINGS</code>, <code>WINDOW_UPDATE</code>, порядок псевдозаголовков), и здесь он отличается от Chrome.</li><li><strong>Потокобезопасность требует отдельной проверки.</strong> Блокировки в C# разводят чтение, запись и сетевые операции, но безопасность одновременных <code>SSL_read</code> и <code>SSL_write</code> на одном <code>SSL*</code> зависит и от нативного слоя. Освобождение <code>SSL*</code> в <code>Dispose</code> также стоит синхронизировать с идущими операциями.</li><li><strong>Валидация цепочки:</strong> из нативного слоя берётся только листовой сертификат, поэтому построение цепочки может зависеть от AIA-загрузки промежуточных, а онлайн-проверка отзыва блокирует поток внутри <code>async</code>-кода.</li><li><strong>Платформа:</strong> сборка требует тяжёлого тулчейна, а нативная часть пока только под Windows x64.</li></ul><p>Это PoC, но архитектурная идея, то есть «BoringSSL как чистая TLS-машина, а ввод-вывод снаружи», на мой взгляд, получилась удачной и легко переносится на другие транспорты.</p><p>Проект на GitHub: <a href="https://github.com/optinsoft/BoringSslConsole">optinsoft&#x2F;BoringSslConsole</a></p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Стандартный &lt;code&gt;SslStream&lt;/code&gt; в .NET хорош всем, кроме одного: он не даёт управлять тем, как выглядит ваш ClientHello. Набор</summary>
        
      
    
    
    
    <category term="GitHub" scheme="https://optinsoft.net/blog/categories/GitHub/"/>
    
    
    <category term="dotnet" scheme="https://optinsoft.net/blog/tags/dotnet/"/>
    
    <category term="tls" scheme="https://optinsoft.net/blog/tags/tls/"/>
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="boringssl" scheme="https://optinsoft.net/blog/tags/boringssl/"/>
    
    <category term="csharp" scheme="https://optinsoft.net/blog/tags/csharp/"/>
    
    <category term="footprint" scheme="https://optinsoft.net/blog/tags/footprint/"/>
    
    <category term="fingerprint" scheme="https://optinsoft.net/blog/tags/fingerprint/"/>
    
    <category term="JA4" scheme="https://optinsoft.net/blog/tags/JA4/"/>
    
  </entry>
  
  <entry>
    <title>Configure SPF, DKIM, and DMARC for a Linux Mail Server</title>
    <link href="https://optinsoft.net/blog/2026/09/17/Configure-SPF-DKIM-and-DMARC-for-a-Linux-Mail-Server/"/>
    <id>https://optinsoft.net/blog/2026/09/17/Configure-SPF-DKIM-and-DMARC-for-a-Linux-Mail-Server/</id>
    <published>2026-09-17T15:30:41.000Z</published>
    <updated>2026-09-18T04:31:59.182Z</updated>
    
    <content type="html"><![CDATA[<p>If you need to send email from your own domain without using third-party SMTP services, you can rent a Linux VPS. Postfix or Exim are commonly used as the mail transfer agent (MTA).</p><p>Before buying a VPS, contact the hosting provider and ask: “Is outbound SMTP port 25 open?” Many hosting providers block it by default to prevent spam. If the port is blocked, sending email directly to remote mail servers may not be possible.</p><hr><h2 id="1-Preparation-Network-and-DNS-Settings"><a href="#1-Preparation-Network-and-DNS-Settings" class="headerlink" title="1. Preparation: Network and DNS Settings"></a>1. Preparation: Network and DNS Settings</h2><p>To make sure your mail is delivered correctly, you need to configure several basic DNS records for your domain:</p><ol><li><p><strong>Hostname.</strong> Set the server hostname to something like <code>mail.yourdomain.com</code>.</p></li><li><p><strong>A record.</strong> Point <code>mail.yourdomain.com</code> to the IP address of your VPS.</p></li><li><p><strong>MX record.</strong> Specify which server handles email for your domain. The record looks like this:</p><pre><code class="hljs text">@ MX 10 mail.yourdomain.com.</code></pre></li><li><p><strong>PTR record (Reverse DNS).</strong> Maps the server’s IP address back to its hostname. It is configured in your VPS provider’s control panel rather than at your domain registrar.</p><p>A PTR record is important for email delivery. Many mail servers check the reverse DNS record of the sending IP address, and a missing or inconsistent PTR record can result in messages being rejected or sent to spam.</p><p>It is recommended to keep the forward and reverse DNS records consistent:</p><pre><code class="hljs text">1.2.3.4 → mail.yourdomain.commail.yourdomain.com → 1.2.3.4</code></pre></li></ol><hr><h2 id="2-Configuring-SPF"><a href="#2-Configuring-SPF" class="headerlink" title="2. Configuring SPF"></a>2. Configuring SPF</h2><p>SPF is a DNS record that specifies which servers are authorized to send email on behalf of your domain.</p><p>Create a TXT record:</p><ul><li><p><strong>Record type:</strong> TXT</p></li><li><p><strong>Host&#x2F;Name:</strong> <code>@</code> (or leave it empty, depending on your DNS provider)</p></li><li><p><strong>Value:</strong></p><pre><code class="hljs text">v=spf1 ip4:1.2.3.4 ~all</code></pre><p>Replace <code>1.2.3.4</code> with the IP address of your server.</p></li></ul><h3 id="What-does-all-mean"><a href="#What-does-all-mean" class="headerlink" title="What does ~all mean?"></a>What does <code>~all</code> mean?</h3><p><code>~all</code> means SoftFail: messages sent from IP addresses not listed in the SPF record receive a soft-failure result. The receiving server decides how to handle such messages.</p><p>After testing your configuration, you can use <code>-all</code> if all legitimate sources that send email for your domain are listed in the SPF record.</p><hr><h2 id="3-Generating-DKIM-Keys-with-OpenSSL"><a href="#3-Generating-DKIM-Keys-with-OpenSSL" class="headerlink" title="3. Generating DKIM Keys with OpenSSL"></a>3. Generating DKIM Keys with OpenSSL</h2><p>DKIM is a digital signature mechanism that helps verify the authenticity of the sender and the integrity of an email. The server signs the message with a private key, and the recipient verifies the signature using the public key published in DNS.</p><p>To generate a key pair, connect to the server over SSH and run the following commands.</p><h3 id="3-1-Generate-a-2048-bit-private-key"><a href="#3-1-Generate-a-2048-bit-private-key" class="headerlink" title="3.1. Generate a 2048-bit private key"></a>3.1. Generate a 2048-bit private key</h3><pre><code class="hljs bash">openssl genrsa -out dkim.private 2048</code></pre><h3 id="3-2-Generate-the-public-key"><a href="#3-2-Generate-the-public-key" class="headerlink" title="3.2. Generate the public key"></a>3.2. Generate the public key</h3><pre><code class="hljs bash">openssl rsa -<span class="hljs-keyword">in</span> dkim.private -pubout -out dkim.public</code></pre><h3 id="3-3-Display-the-public-key"><a href="#3-3-Display-the-public-key" class="headerlink" title="3.3. Display the public key"></a>3.3. Display the public key</h3><pre><code class="hljs bash"><span class="hljs-built_in">cat</span> dkim.public</code></pre><p>The terminal will display something like:</p><pre><code class="hljs text">-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...-----END PUBLIC KEY-----</code></pre><p>For the DNS record, take only the Base64-encoded content between the headers and combine it into a single line without spaces or line breaks.</p><p>Then create a TXT record named <code>dkim._domainkey</code> (<code>dkim</code> is the selector name).</p><ul><li><p><strong>Record type:</strong> TXT</p></li><li><p><strong>Host&#x2F;Name:</strong> <code>dkim._domainkey</code></p></li><li><p><strong>Value:</strong></p><pre><code class="hljs text">v=DKIM1; k=rsa; p=YOUR_COPIED_PUBLIC_KEY</code></pre></li></ul><p>If your DNS provider supports splitting long TXT records into multiple strings, use that option for keys that do not fit into a single string.</p><hr><h2 id="4-Connecting-DKIM-to-the-Mail-Server"><a href="#4-Connecting-DKIM-to-the-Mail-Server" class="headerlink" title="4. Connecting DKIM to the Mail Server"></a>4. Connecting DKIM to the Mail Server</h2><h2 id="4-1-Option-A-If-You-Use-Exim"><a href="#4-1-Option-A-If-You-Use-Exim" class="headerlink" title="4.1 Option A: If You Use Exim"></a>4.1 Option A: If You Use Exim</h2><p>In the Exim configuration file (the exact path depends on the distribution and installation method), find the transport responsible for sending mail to external servers. It is usually called <code>remote_smtp</code>.</p><p>Example configuration:</p><pre><code class="hljs text">remote_smtp:  driver = smtp  dkim_domain = yourdomain.com  dkim_selector = dkim  dkim_private_key = /etc/exim/dkim.private  dkim_canon = relaxed</code></pre><p>Copy the private key to the location specified in the Exim configuration:</p><pre><code class="hljs bash"><span class="hljs-built_in">cp</span> dkim.private /etc/exim/dkim.private</code></pre><p>Give the user running Exim permission to read the private key. The username depends on the distribution. For example, some systems use <code>exim</code>, while Debian may use <code>Debian-exim</code>.</p><p>Example for a system using the <code>exim</code> user:</p><pre><code class="hljs bash"><span class="hljs-built_in">chown</span> exim:exim /etc/exim/dkim.private<span class="hljs-built_in">chmod</span> 600 /etc/exim/dkim.private</code></pre><p>Restart the service:</p><pre><code class="hljs bash">systemctl restart exim4<span class="hljs-comment"># or</span>systemctl restart exim</code></pre><p>Make sure that the Exim user actually has access to the private key.</p><hr><h2 id="4-2-Option-B-If-You-Use-Postfix"><a href="#4-2-Option-B-If-You-Use-Postfix" class="headerlink" title="4.2 Option B: If You Use Postfix"></a>4.2 Option B: If You Use Postfix</h2><p>Postfix uses the external OpenDKIM utility to add DKIM signatures to outgoing messages.</p><h3 id="4-2-1-Install-the-packages"><a href="#4-2-1-Install-the-packages" class="headerlink" title="4.2.1. Install the packages"></a>4.2.1. Install the packages</h3><pre><code class="hljs bash">apt install opendkim opendkim-tools</code></pre><h3 id="4-2-2-Configure-OpenDKIM"><a href="#4-2-2-Configure-OpenDKIM" class="headerlink" title="4.2.2. Configure OpenDKIM"></a>4.2.2. Configure OpenDKIM</h3><p>Open <code>/etc/opendkim.conf</code> and add or configure the following settings:</p><pre><code class="hljs text">Syslog                 yesRequiredHeaders        yesMode                   svSubDomains             noSocket                 inet:8891@localhostKeyTable               /etc/opendkim/KeyTableSigningTable           /etc/opendkim/SigningTableExternalIgnoreList     /etc/opendkim/TrustedHostsInternalHosts          /etc/opendkim/TrustedHosts</code></pre><h3 id="4-2-3-Connect-OpenDKIM-to-Postfix"><a href="#4-2-3-Connect-OpenDKIM-to-Postfix" class="headerlink" title="4.2.3. Connect OpenDKIM to Postfix"></a>4.2.3. Connect OpenDKIM to Postfix</h3><p>Add the following lines to the end of the main Postfix configuration file, <code>/etc/postfix/main.cf</code>:</p><pre><code class="hljs text">milter_protocol = 6milter_default_action = acceptsmtpd_milters = inet:localhost:8891non_smtpd_milters = inet:localhost:8891</code></pre><h3 id="4-2-4-Configure-the-OpenDKIM-tables"><a href="#4-2-4-Configure-the-OpenDKIM-tables" class="headerlink" title="4.2.4. Configure the OpenDKIM tables"></a>4.2.4. Configure the OpenDKIM tables</h3><p>This example uses a single domain and a single DKIM key. The key name in <code>KeyTable</code> is <code>dkim._domainkey.yourdomain.com</code>.</p><p><strong><code>/etc/opendkim/KeyTable</code></strong></p><p>This file maps the key name to the domain, selector, and private key path:</p><pre><code class="hljs text">dkim._domainkey.yourdomain.com yourdomain.com:dkim:/etc/opendkim/keys/dkim.private</code></pre><p>Here:</p><ul><li><code>dkim._domainkey.yourdomain.com</code> — the key name in the table.</li><li><code>yourdomain.com</code> — the domain that will appear in the DKIM signature (<code>d=</code>).</li><li><code>dkim</code> — the selector (<code>s=</code>).</li><li><code>/etc/opendkim/keys/dkim.private</code> — the path to the private key.</li></ul><p><strong><code>/etc/opendkim/SigningTable</code></strong></p><p>This file maps the sender domain to the key name from <code>KeyTable</code>:</p><pre><code class="hljs text">yourdomain.com dkim._domainkey.yourdomain.com</code></pre><p>If you need to use a pattern for sender addresses from the domain, you can specify:</p><pre><code class="hljs text">*@yourdomain.com dkim._domainkey.yourdomain.com</code></pre><p>The wildcard form requires the corresponding <code>refile:</code> table type to be configured in the <code>SigningTable</code> directive. This guide uses the simple domain-based mapping without <code>refile</code>.</p><p><strong><code>/etc/opendkim/TrustedHosts</code></strong></p><p>Add localhost and the IP address of your server:</p><pre><code class="hljs text">127.0.0.1localhost1.2.3.4</code></pre><h3 id="4-2-5-Move-the-private-key-and-restart-the-services"><a href="#4-2-5-Move-the-private-key-and-restart-the-services" class="headerlink" title="4.2.5. Move the private key and restart the services"></a>4.2.5. Move the private key and restart the services</h3><p>Create the directory for the keys:</p><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p /etc/opendkim/keys/</code></pre><p>Move the private key:</p><pre><code class="hljs bash"><span class="hljs-built_in">mv</span> dkim.private /etc/opendkim/keys/</code></pre><p>Set the owner and permissions:</p><pre><code class="hljs bash"><span class="hljs-built_in">chown</span> opendkim:opendkim /etc/opendkim/keys<span class="hljs-built_in">chmod</span> 700 /etc/opendkim/keys<span class="hljs-built_in">chown</span> opendkim:opendkim /etc/opendkim/keys/dkim.private<span class="hljs-built_in">chmod</span> 600 /etc/opendkim/keys/dkim.private</code></pre><p>Restart the services:</p><pre><code class="hljs bash">systemctl restart opendkim postfix</code></pre><p>Check their status:</p><pre><code class="hljs bash">systemctl status opendkimsystemctl status postfix</code></pre><p>If necessary, check the OpenDKIM log:</p><pre><code class="hljs bash">journalctl -u opendkim -n 50 --no-pager</code></pre><hr><h2 id="5-Configuring-DMARC"><a href="#5-Configuring-DMARC" class="headerlink" title="5. Configuring DMARC"></a>5. Configuring DMARC</h2><p>DMARC is a mechanism that allows a domain to tell receiving mail servers how to handle messages that fail the required SPF and DKIM checks, including the alignment of the domains involved with the domain in the <code>From</code> header.</p><p>Create a TXT record:</p><ul><li><p><strong>Record type:</strong> TXT</p></li><li><p><strong>Host&#x2F;Name:</strong> <code>_dmarc</code></p></li><li><p><strong>Initial value:</strong></p><pre><code class="hljs text">v=DMARC1; p=none; rua=mailto:admin@yourdomain.com</code></pre></li></ul><h3 id="What-does-this-mean"><a href="#What-does-this-mean" class="headerlink" title="What does this mean?"></a>What does this mean?</h3><ul><li><strong><code>p=none</code></strong> — monitoring mode. The domain asks the receiving server not to apply a special action to messages that fail DMARC. You can monitor the results and collect reports.</li><li><strong><code>rua=mailto:...</code></strong> — the address to which mail services can send aggregate reports about authentication checks.</li><li><strong><code>p=quarantine</code></strong> — asks the receiving server to treat messages that fail DMARC as suspicious, for example by placing them in the spam folder.</li><li><strong><code>p=reject</code></strong> — asks the receiving server to reject messages that fail DMARC. The actual action is determined by the receiving server.</li></ul><p>After testing and fixing the configuration, you can change the policy from <code>p=none</code> to <code>p=quarantine</code> or <code>p=reject</code>.</p><hr><h2 id="6-How-to-Check-DNS-Records-from-the-Terminal"><a href="#6-How-to-Check-DNS-Records-from-the-Terminal" class="headerlink" title="6. How to Check DNS Records from the Terminal"></a>6. How to Check DNS Records from the Terminal</h2><p>You can check whether the DNS settings have been updated using <code>dig</code> or <code>nslookup</code>.</p><h3 id="Check-SPF"><a href="#Check-SPF" class="headerlink" title="Check SPF"></a>Check SPF</h3><pre><code class="hljs bash">dig yourdomain.com TXT +short</code></pre><p>Or:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT yourdomain.com</code></pre><h3 id="Check-DKIM"><a href="#Check-DKIM" class="headerlink" title="Check DKIM"></a>Check DKIM</h3><p>The query uses the name of your selector:</p><pre><code class="hljs bash">dig dkim._domainkey.yourdomain.com TXT +short</code></pre><p>Or:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT dkim._domainkey.yourdomain.com</code></pre><h3 id="Check-DMARC"><a href="#Check-DMARC" class="headerlink" title="Check DMARC"></a>Check DMARC</h3><pre><code class="hljs bash">dig _dmarc.yourdomain.com TXT +short</code></pre><p>Or:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT _dmarc.yourdomain.com</code></pre><h3 id="Check-PTR-Reverse-DNS"><a href="#Check-PTR-Reverse-DNS" class="headerlink" title="Check PTR (Reverse DNS)"></a>Check PTR (Reverse DNS)</h3><pre><code class="hljs bash">dig -x 1.2.3.4 +short</code></pre><p>Or:</p><pre><code class="hljs bash">nslookup 1.2.3.4</code></pre><h3 id="Check-the-server-hostname"><a href="#Check-the-server-hostname" class="headerlink" title="Check the server hostname:"></a>Check the server hostname:</h3><pre><code class="hljs bash">hostname -f</code></pre><p>Make sure that the hostname is consistent with the A and PTR records.</p><p>The recommended setup is:</p><pre><code class="hljs text">Hostname: mail.yourdomain.comA:        mail.yourdomain.com → 1.2.3.4PTR:      1.2.3.4 → mail.yourdomain.com</code></pre><p>The hostname used by the server and the reverse DNS record should be configured consistently.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;If you need to send email from your own domain without using third-party SMTP services, you can rent a Linux VPS. Postfix or Exim are</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="linux" scheme="https://optinsoft.net/blog/tags/linux/"/>
    
    <category term="mail-server" scheme="https://optinsoft.net/blog/tags/mail-server/"/>
    
    <category term="postfix" scheme="https://optinsoft.net/blog/tags/postfix/"/>
    
    <category term="exim" scheme="https://optinsoft.net/blog/tags/exim/"/>
    
    <category term="spf" scheme="https://optinsoft.net/blog/tags/spf/"/>
    
    <category term="dkim" scheme="https://optinsoft.net/blog/tags/dkim/"/>
    
    <category term="dmarc" scheme="https://optinsoft.net/blog/tags/dmarc/"/>
    
    <category term="dns" scheme="https://optinsoft.net/blog/tags/dns/"/>
    
  </entry>
  
  <entry>
    <title>Настройка SPF, DKIM и DMARC для почтового сервера на Linux</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/17/Configure-SPF-DKIM-and-DMARC-for-a-Linux-Mail-Server/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/17/Configure-SPF-DKIM-and-DMARC-for-a-Linux-Mail-Server/</id>
    <published>2026-09-17T15:30:41.000Z</published>
    <updated>2026-09-18T04:41:17.446Z</updated>
    
    <content type="html"><![CDATA[<p>Если вам нужно самостоятельно отправлять почту со своего домена без использования сторонних SMTP-сервисов, можно арендовать Linux VPS. В качестве почтового сервера (MTA) обычно используют Postfix или Exim.</p><p>Перед покупкой VPS обязательно напишите в поддержку хостинга и спросите: «Открыт ли у вас исходящий 25 порт SMTP?». Многие хостеры закрывают его по умолчанию, чтобы защититься от спамеров. Если порт закрыт, отправка почты напрямую на почтовые серверы может быть невозможна.</p><hr><h2 id="1-Подготовка-настройки-сети-и-DNS"><a href="#1-Подготовка-настройки-сети-и-DNS" class="headerlink" title="1. Подготовка: настройки сети и DNS"></a>1. Подготовка: настройки сети и DNS</h2><p>Чтобы почта корректно доставлялась, нужно настроить несколько базовых параметров DNS вашего домена:</p><ol><li><p><strong>Имя хоста (Hostname).</strong> Задайте серверу имя в системе, например <code>mail.yourdomain.com</code>.</p></li><li><p><strong>A-запись.</strong> Направьте поддомен <code>mail.yourdomain.com</code> на IP-адрес вашей VPS.</p></li><li><p><strong>MX-запись.</strong> Укажите, какой сервер принимает почту для домена. Запись выглядит так:</p><pre><code class="hljs text">@ MX 10 mail.yourdomain.com.</code></pre></li><li><p><strong>PTR-запись (Reverse DNS).</strong> Связывает IP-адрес сервера обратно с его именем. Она настраивается в панели хостинга VPS, а не у регистратора домена.</p><p>PTR-запись важна для доставки почты. Многие почтовые серверы проверяют обратную DNS-запись IP-адреса, а её отсутствие или несоответствие может привести к отклонению писем или попаданию в спам.</p><p>Рекомендуется согласовать прямую и обратную DNS-записи:</p><pre><code class="hljs text">1.2.3.4 → mail.yourdomain.commail.yourdomain.com → 1.2.3.4</code></pre></li></ol><hr><h2 id="2-Настройка-SPF"><a href="#2-Настройка-SPF" class="headerlink" title="2. Настройка SPF"></a>2. Настройка SPF</h2><p>SPF — это запись в DNS, которая указывает, каким серверам разрешено отправлять почту от имени вашего домена.</p><p>Создайте TXT-запись:</p><ul><li><p><strong>Тип записи:</strong> TXT</p></li><li><p><strong>Хост&#x2F;Имя:</strong> <code>@</code> (или пустое поле, в зависимости от DNS-панели)</p></li><li><p><strong>Значение:</strong></p><pre><code class="hljs text">v=spf1 ip4:1.2.3.4 ~all</code></pre><p>Вместо <code>1.2.3.4</code> подставьте IP-адрес своего сервера.</p></li></ul><h3 id="Что-значит-all"><a href="#Что-значит-all" class="headerlink" title="Что значит ~all"></a>Что значит <code>~all</code></h3><p><code>~all</code> означает SoftFail: сообщения с IP-адресов, не перечисленных в SPF, не проходят проверку с мягким результатом. Принимающий сервер самостоятельно решает, как обработать такое письмо.</p><p>После проверки конфигурации можно использовать <code>-all</code>, если все легитимные источники отправки перечислены в SPF.</p><hr><h2 id="3-Генерация-ключей-DKIM-через-OpenSSL"><a href="#3-Генерация-ключей-DKIM-через-OpenSSL" class="headerlink" title="3. Генерация ключей DKIM через OpenSSL"></a>3. Генерация ключей DKIM через OpenSSL</h2><p>DKIM — это цифровая подпись, которая помогает проверять подлинность отправителя и целостность письма. Сервер подписывает письмо приватным ключом, а получатель проверяет подпись с помощью публичного ключа из DNS.</p><p>Для генерации пары ключей зайдите на сервер по SSH и выполните следующие команды.</p><h3 id="3-1-Создаём-приватный-ключ-длиной-2048-бит"><a href="#3-1-Создаём-приватный-ключ-длиной-2048-бит" class="headerlink" title="3.1. Создаём приватный ключ длиной 2048 бит"></a>3.1. Создаём приватный ключ длиной 2048 бит</h3><pre><code class="hljs bash">openssl genrsa -out dkim.private 2048</code></pre><h3 id="3-2-Генерируем-публичный-ключ"><a href="#3-2-Генерируем-публичный-ключ" class="headerlink" title="3.2. Генерируем публичный ключ"></a>3.2. Генерируем публичный ключ</h3><pre><code class="hljs bash">openssl rsa -<span class="hljs-keyword">in</span> dkim.private -pubout -out dkim.public</code></pre><h3 id="3-3-Выводим-публичный-ключ-на-экран"><a href="#3-3-Выводим-публичный-ключ-на-экран" class="headerlink" title="3.3. Выводим публичный ключ на экран"></a>3.3. Выводим публичный ключ на экран</h3><pre><code class="hljs bash"><span class="hljs-built_in">cat</span> dkim.public</code></pre><p>В терминале появится текст примерно такого вида:</p><pre><code class="hljs text">-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...-----END PUBLIC KEY-----</code></pre><p>Для DNS-записи нужно взять только Base64-содержимое ключа между заголовками и объединить его в одну строку без пробелов и переносов.</p><p>Затем создайте TXT-запись <code>dkim._domainkey</code> (где <code>dkim</code> — имя вашего селектора).</p><ul><li><p><strong>Тип записи:</strong> TXT</p></li><li><p><strong>Хост&#x2F;Имя:</strong> <code>dkim._domainkey</code></p></li><li><p><strong>Значение:</strong></p><pre><code class="hljs text">v=DKIM1; k=rsa; p=ТУТ_ВАШ_СКОПИРОВАННЫЙ_КЛЮЧ</code></pre></li></ul><p>Если DNS-провайдер поддерживает разбиение длинных TXT-записей на несколько строк, используйте эту возможность для ключей, которые не помещаются в одну строку.</p><hr><h2 id="4-Подключение-DKIM-к-почтовому-серверу"><a href="#4-Подключение-DKIM-к-почтовому-серверу" class="headerlink" title="4. Подключение DKIM к почтовому серверу"></a>4. Подключение DKIM к почтовому серверу</h2><h2 id="4-1-Вариант-А-если-у-вас-Exim"><a href="#4-1-Вариант-А-если-у-вас-Exim" class="headerlink" title="4.1 Вариант А: если у вас Exim"></a>4.1 Вариант А: если у вас Exim</h2><p>В конфигурационном файле Exim (точный путь зависит от дистрибутива и способа установки) найдите транспорт, который отвечает за отправку почты наружу. Обычно он называется <code>remote_smtp</code>.</p><p>Пример конфигурации:</p><pre><code class="hljs text">remote_smtp:  driver = smtp  dkim_domain = yourdomain.com  dkim_selector = dkim  dkim_private_key = /etc/exim/dkim.private  dkim_canon = relaxed</code></pre><p>Скопируйте закрытый ключ в место, указанное в конфигурации Exim:</p><pre><code class="hljs bash"><span class="hljs-built_in">cp</span> dkim.private /etc/exim/dkim.private</code></pre><p>Выдайте права на чтение файла приватного ключа пользователю, от имени которого работает Exim. Имя пользователя зависит от дистрибутива. Например, в некоторых системах это <code>exim</code>, а в Debian может использоваться <code>Debian-exim</code>.</p><p>Пример для системы с пользователем <code>exim</code>:</p><pre><code class="hljs bash"><span class="hljs-built_in">chown</span> exim:exim /etc/exim/dkim.private<span class="hljs-built_in">chmod</span> 600 /etc/exim/dkim.private</code></pre><p>Перезапустите сервис:</p><pre><code class="hljs bash">systemctl restart exim4<span class="hljs-comment"># или</span>systemctl restart exim</code></pre><p>Проверьте, что используемый пользователь Exim действительно имеет доступ к приватному ключу.</p><hr><h2 id="4-2-Вариант-Б-если-у-вас-Postfix"><a href="#4-2-Вариант-Б-если-у-вас-Postfix" class="headerlink" title="4.2 Вариант Б: если у вас Postfix"></a>4.2 Вариант Б: если у вас Postfix</h2><p>Postfix использует внешнюю утилиту OpenDKIM для добавления DKIM-подписей.</p><h3 id="4-2-1-Установите-пакеты"><a href="#4-2-1-Установите-пакеты" class="headerlink" title="4.2.1. Установите пакеты"></a>4.2.1. Установите пакеты</h3><pre><code class="hljs bash">apt install opendkim opendkim-tools</code></pre><h3 id="4-2-2-Настройте-OpenDKIM"><a href="#4-2-2-Настройте-OpenDKIM" class="headerlink" title="4.2.2. Настройте OpenDKIM"></a>4.2.2. Настройте OpenDKIM</h3><p>Откройте конфигурационный файл <code>/etc/opendkim.conf</code> и добавьте или настройте следующие параметры:</p><pre><code class="hljs text">Syslog                 yesRequiredHeaders        yesMode                   svSubDomains             noSocket                 inet:8891@localhostKeyTable               /etc/opendkim/KeyTableSigningTable           /etc/opendkim/SigningTableExternalIgnoreList     /etc/opendkim/TrustedHostsInternalHosts          /etc/opendkim/TrustedHosts</code></pre><h3 id="4-2-3-Подключите-OpenDKIM-к-Postfix"><a href="#4-2-3-Подключите-OpenDKIM-к-Postfix" class="headerlink" title="4.2.3. Подключите OpenDKIM к Postfix"></a>4.2.3. Подключите OpenDKIM к Postfix</h3><p>В конец основного конфига Postfix <code>/etc/postfix/main.cf</code> добавьте:</p><pre><code class="hljs text">milter_protocol = 6milter_default_action = acceptsmtpd_milters = inet:localhost:8891non_smtpd_milters = inet:localhost:8891</code></pre><h3 id="4-2-4-Настройте-таблицы-OpenDKIM"><a href="#4-2-4-Настройте-таблицы-OpenDKIM" class="headerlink" title="4.2.4. Настройте таблицы OpenDKIM"></a>4.2.4. Настройте таблицы OpenDKIM</h3><p>В этом примере используется один домен и один ключ DKIM. Имя ключа в <code>KeyTable</code> — <code>dkim._domainkey.yourdomain.com</code>.</p><p><strong><code>/etc/opendkim/KeyTable</code></strong></p><p>Этот файл связывает имя ключа с доменом, селектором и путём к приватному ключу:</p><pre><code class="hljs text">dkim._domainkey.yourdomain.com yourdomain.com:dkim:/etc/opendkim/keys/dkim.private</code></pre><p>Здесь:</p><ul><li><code>dkim._domainkey.yourdomain.com</code> — имя ключа в таблице.</li><li><code>yourdomain.com</code> — домен, который попадёт в DKIM-подпись (<code>d=</code>).</li><li><code>dkim</code> — селектор (<code>s=</code>).</li><li><code>/etc/opendkim/keys/dkim.private</code> — путь к приватному ключу.</li></ul><p><strong><code>/etc/opendkim/SigningTable</code></strong></p><p>Этот файл связывает домен отправителя с именем ключа из KeyTable:</p><pre><code class="hljs text">yourdomain.com dkim._domainkey.yourdomain.com</code></pre><p>Если необходимо использовать шаблон для адресов отправителей домена, можно указать:</p><pre><code class="hljs text">*@yourdomain.com dkim._domainkey.yourdomain.com</code></pre><p>Для wildcard-варианта необходимо настроить соответствующий тип таблицы <code>refile:</code> в директиве <code>SigningTable</code>. В данной инструкции используется простой вариант сопоставления по домену без <code>refile</code>.</p><p><strong><code>/etc/opendkim/TrustedHosts</code></strong></p><p>Внесите localhost и IP-адрес своего сервера:</p><pre><code class="hljs text">127.0.0.1localhost1.2.3.4</code></pre><h3 id="4-2-5-Перенесите-приватный-ключ-и-перезапустите-службы"><a href="#4-2-5-Перенесите-приватный-ключ-и-перезапустите-службы" class="headerlink" title="4.2.5. Перенесите приватный ключ и перезапустите службы"></a>4.2.5. Перенесите приватный ключ и перезапустите службы</h3><p>Создайте каталог для ключей:</p><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> -p /etc/opendkim/keys/</code></pre><p>Переместите приватный ключ:</p><pre><code class="hljs bash"><span class="hljs-built_in">mv</span> dkim.private /etc/opendkim/keys/</code></pre><p>Назначьте владельца и права:</p><pre><code class="hljs bash"><span class="hljs-built_in">chown</span> opendkim:opendkim /etc/opendkim/keys<span class="hljs-built_in">chmod</span> 700 /etc/opendkim/keys<span class="hljs-built_in">chown</span> opendkim:opendkim /etc/opendkim/keys/dkim.private<span class="hljs-built_in">chmod</span> 600 /etc/opendkim/keys/dkim.private</code></pre><p>Перезапустите службы:</p><pre><code class="hljs bash">systemctl restart opendkim postfix</code></pre><p>Проверьте статус:</p><pre><code class="hljs bash">systemctl status opendkimsystemctl status postfix</code></pre><p>При необходимости изучите журнал OpenDKIM:</p><pre><code class="hljs bash">journalctl -u opendkim -n 50 --no-pager</code></pre><hr><h2 id="5-Настройка-DMARC"><a href="#5-Настройка-DMARC" class="headerlink" title="5. Настройка DMARC"></a>5. Настройка DMARC</h2><p>DMARC — это механизм, который позволяет домену сообщить принимающим почтовым серверам, как обрабатывать сообщения, не прошедшие необходимые проверки SPF и DKIM с учётом соответствия домена в поле From.</p><p>Создайте TXT-запись:</p><ul><li><p><strong>Тип записи:</strong> TXT</p></li><li><p><strong>Хост&#x2F;Имя:</strong> <code>_dmarc</code></p></li><li><p><strong>Значение для начала:</strong></p><pre><code class="hljs text">v=DMARC1; p=none; rua=mailto:admin@yourdomain.com</code></pre></li></ul><h3 id="Что-это-значит"><a href="#Что-это-значит" class="headerlink" title="Что это значит"></a>Что это значит</h3><ul><li><strong><code>p=none</code></strong> — режим мониторинга. Домен просит не применять специальное действие к сообщениям, не прошедшим DMARC. Можно наблюдать за результатами проверок и собирать отчёты.</li><li><strong><code>rua=mailto:...</code></strong> — адрес, на который почтовые сервисы могут отправлять агрегированные отчёты о проверках.</li><li><strong><code>p=quarantine</code></strong> — просит принимающий сервер обрабатывать сообщения, не прошедшие DMARC, как подозрительные, например помещать их в спам.</li><li><strong><code>p=reject</code></strong> — просит отклонять сообщения, не прошедшие DMARC. Реальное действие определяется принимающим сервером.</li></ul><p>После проверки и исправления конфигурации можно изменить политику с <code>p=none</code> на <code>p=quarantine</code> или <code>p=reject</code>.</p><hr><h2 id="6-Как-проверить-DNS-записи-через-терминал"><a href="#6-Как-проверить-DNS-записи-через-терминал" class="headerlink" title="6. Как проверить DNS-записи через терминал"></a>6. Как проверить DNS-записи через терминал</h2><p>Проверить, что настройки обновились в DNS, можно из консоли с помощью утилит <code>dig</code> или <code>nslookup</code>.</p><h3 id="Проверка-SPF"><a href="#Проверка-SPF" class="headerlink" title="Проверка SPF"></a>Проверка SPF</h3><pre><code class="hljs bash">dig yourdomain.com TXT +short</code></pre><p>Или:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT yourdomain.com</code></pre><h3 id="Проверка-DKIM"><a href="#Проверка-DKIM" class="headerlink" title="Проверка DKIM"></a>Проверка DKIM</h3><p>Запрос выполняется по имени вашего селектора:</p><pre><code class="hljs bash">dig dkim._domainkey.yourdomain.com TXT +short</code></pre><p>Или:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT dkim._domainkey.yourdomain.com</code></pre><h3 id="Проверка-DMARC"><a href="#Проверка-DMARC" class="headerlink" title="Проверка DMARC"></a>Проверка DMARC</h3><pre><code class="hljs bash">dig _dmarc.yourdomain.com TXT +short</code></pre><p>Или:</p><pre><code class="hljs bash">nslookup -<span class="hljs-built_in">type</span>=TXT _dmarc.yourdomain.com</code></pre><h3 id="Проверка-PTR-обратной-записи"><a href="#Проверка-PTR-обратной-записи" class="headerlink" title="Проверка PTR (обратной записи)"></a>Проверка PTR (обратной записи)</h3><pre><code class="hljs bash">dig -x 1.2.3.4 +short</code></pre><p>Или:</p><pre><code class="hljs bash">nslookup 1.2.3.4</code></pre><hr><h3 id="Проверка-hostname"><a href="#Проверка-hostname" class="headerlink" title="Проверка hostname"></a>Проверка hostname</h3><pre><code class="hljs bash">hostname -f</code></pre><p>Убедитесь, что имя согласовано с A- и PTR-записями.</p><p>Рекомендуемая схема:</p><pre><code class="hljs text">Hostname: mail.yourdomain.comA:        mail.yourdomain.com → 1.2.3.4PTR:      1.2.3.4 → mail.yourdomain.com</code></pre><p>Имя хоста, используемое сервером, и обратная DNS-запись должны быть настроены согласованно.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Если вам нужно самостоятельно отправлять почту со своего домена без использования сторонних SMTP-сервисов, можно арендовать Linux VPS. В</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="linux" scheme="https://optinsoft.net/blog/tags/linux/"/>
    
    <category term="mail-server" scheme="https://optinsoft.net/blog/tags/mail-server/"/>
    
    <category term="postfix" scheme="https://optinsoft.net/blog/tags/postfix/"/>
    
    <category term="exim" scheme="https://optinsoft.net/blog/tags/exim/"/>
    
    <category term="spf" scheme="https://optinsoft.net/blog/tags/spf/"/>
    
    <category term="dkim" scheme="https://optinsoft.net/blog/tags/dkim/"/>
    
    <category term="dmarc" scheme="https://optinsoft.net/blog/tags/dmarc/"/>
    
    <category term="dns" scheme="https://optinsoft.net/blog/tags/dns/"/>
    
  </entry>
  
  <entry>
    <title>How Windows OpenSSH Broke My Brain</title>
    <link href="https://optinsoft.net/blog/2026/09/12/How-Windows-OpenSSH-Broke-My-Brain/"/>
    <id>https://optinsoft.net/blog/2026/09/12/How-Windows-OpenSSH-Broke-My-Brain/</id>
    <published>2026-09-12T06:04:08.000Z</published>
    <updated>2026-09-12T06:43:40.303Z</updated>
    
    <content type="html"><![CDATA[<p>There are errors you can fix with a two-second Google search, and then there are those that drain your time. A prime example is an SSH connection failure to a Windows Server, where the OpenSSH service drops the following logs into the Event Viewer:</p><pre><code class="hljs plaintext">sshd: error: get_user_token - unable to generate token on 2nd attempt for user administratorsshd: fatal: ga_init, unable to resolve user administrator</code></pre><p>These messages indicate that the Windows authentication subsystem either cannot recognize the user at the OS level or is unable to generate a security token for them. Everything seems properly configured, yet the server persistently drops the connection.</p><h2 id="Where-the-Time-Goes-False-Trails"><a href="#Where-the-Time-Goes-False-Trails" class="headerlink" title="Where the Time Goes (False Trails)"></a>Where the Time Goes (False Trails)</h2><p>If you start googling this error, 90% of the online advice will lead you down two main paths:</p><ol><li>Key File Permissions: You will be told to configure ACLs via icacls for the administrators_authorized_keys file located in C:\ProgramData\ssh, disable inheritance, and restrict access exclusively to SYSTEM and Administrators.</li><li>The sshd_config File: You will be advised to comment out the Match Group administrators block at the very bottom of the config to force OpenSSH to read keys from the standard user profile (.ssh&#x2F;authorized_keys).</li></ol><p>I tried everything on that list. Permissions and configs were verified, services were restarted—yet the result was zero. The server stubbornly terminated the session immediately after successfully validating the key.</p><h2 id="The-Unobvious-Answer"><a href="#The-Unobvious-Answer" class="headerlink" title="The Unobvious Answer"></a>The Unobvious Answer</h2><p>The root cause turned out to be trivial, yet hidden behind the specifics of how OpenSSH operates on Windows.<br>How does SSH work in Linux? If the key matches, you are in.<br>How does SSH work in Windows? Even if the key matches, the OpenSSH service (running under the SYSTEM account) needs to establish a fully functional Windows workspace session and generate a user security token using the system API (LsaLogonUser).<br>And that is exactly where Windows denies the request.<br>I ran a quick check in the server console:</p><pre><code class="hljs plaintext">net user Administrator</code></pre><p>And there it was: Account Active: Locked.<br>As it turned out, the Administrator account was locked out! This was likely caused by routine brute-force attempts—bots kept hammering port 22, triggering the Windows password lockout policy, which “froze” the account. Meanwhile, the SSH server successfully verified and confirmed the key (since the file exists on the disk), but the Windows OS refused to generate a token for a locked user. Hence the completely cryptic log about being unable to generate token.</p><h2 id="The-5-Second-Fix"><a href="#The-5-Second-Fix" class="headerlink" title="The 5-Second Fix"></a>The 5-Second Fix</h2><p>If you have run into the same issue, the solution is straightforward. Log into the server using another admin account (or log in locally) and unlock the Administrator.</p><p>Via PowerShell:</p><pre><code class="hljs powershell"><span class="hljs-built_in">Unlock-LocalUser</span> <span class="hljs-literal">-Name</span> <span class="hljs-string">&quot;Administrator&quot;</span></code></pre><p>Via the good old CMD:</p><pre><code class="hljs plaintext">net user Administrator /active:yes</code></pre><p>Immediately after, the connection issues disappear, and the client successfully gains SSH access to the server.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;There are errors you can fix with a two-second Google search, and then there are those that drain your time. A prime example is an SSH</summary>
        
      
    
    
    
    <category term="Troubleshooting" scheme="https://optinsoft.net/blog/categories/Troubleshooting/"/>
    
    
    <category term="windows" scheme="https://optinsoft.net/blog/tags/windows/"/>
    
    <category term="openssh" scheme="https://optinsoft.net/blog/tags/openssh/"/>
    
    <category term="ssh" scheme="https://optinsoft.net/blog/tags/ssh/"/>
    
    <category term="devops" scheme="https://optinsoft.net/blog/tags/devops/"/>
    
    <category term="troubleshooting" scheme="https://optinsoft.net/blog/tags/troubleshooting/"/>
    
    <category term="security" scheme="https://optinsoft.net/blog/tags/security/"/>
    
  </entry>
  
  <entry>
    <title>Как Windows OpenSSH сломал мне мозг</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/12/How-Windows-OpenSSH-Broke-My-Brain/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/12/How-Windows-OpenSSH-Broke-My-Brain/</id>
    <published>2026-09-12T06:04:08.000Z</published>
    <updated>2026-09-12T10:47:38.517Z</updated>
    
    <content type="html"><![CDATA[<p>Бывают ошибки, которые гуглятся за две секунды, а бывают те, из-за которых теряешь много времени. К последним как раз относится сбой подключения по SSH к Windows Server, когда служба OpenSSH выдает в журнал событий сообщения:</p><pre><code class="hljs plaintext">sshd: error: get_user_token - unable to generate token on 2nd attempt for user administratorsshd: fatal: ga_init, unable to resolve user administrator</code></pre><p>Эти сообщения говорят о том, что подсистема аутентификации Windows вообще не может распознать пользователя на уровне ОС или не способна создать для него маркер безопасности (security token). Вроде бы всё настроено правильно, а сервер упорно сбрасывает соединение.</p><h2 id="Куда-уходит-время-Ложные-следы"><a href="#Куда-уходит-время-Ложные-следы" class="headerlink" title="Куда уходит время (Ложные следы)"></a>Куда уходит время (Ложные следы)</h2><p>Если вы начнете гуглить эту ошибку, 90% советов в интернетах поведут вас по двум путям:</p><ol><li>Права на файлы ключей. Вам предложат настраивать ACL (права доступа) через icacls для файла administrators_authorized_keys в C:\ProgramData\ssh, убирать наследование и оставлять доступ только для SYSTEM и Administrators.</li><li>Конфиг sshd_config. Посоветуют закомментировать блок Match Group administrators в самом низу конфига, чтобы заставить OpenSSH читать ключи из стандартного профиля пользователя (.ssh&#x2F;authorized_keys).</li></ol><p>Я перепробовал всё из этого списка. Права и конфиги проверены, службы перезапущены. Результат — ноль. Сервер упорно сбрасывал сессию сразу после успешной проверки ключа.</p><h2 id="Неочевидная-разгадка"><a href="#Неочевидная-разгадка" class="headerlink" title="Неочевидная разгадка"></a>Неочевидная разгадка</h2><p>Причина оказалась банальной, но скрытой за спецификой работы OpenSSH в Windows.<br>Как работает SSH в Linux? Если ключ подошел — ты внутри.<br>Как работает SSH в Windows? Даже если ключ подошел, службе OpenSSH (которая работает под учеткой SYSTEM) нужно создать полноценную рабочую сессию для Windows и сгенерировать токен безопасности пользователя через системное API (LsaLogonUser).<br>И вот в этот момент Windows “дает отлуп”.<br>Вбиваю в консоль сервера проверку:</p><pre><code class="hljs plaintext">net user Administrator</code></pre><p>И вижу строчку: Account Active: Locked (или Учетная запись заблокирована).<br>Оказывается, аккаунт Администратора был заблокирован! Скорее всего, из-за банального брутфорса (роботы ломились на 22 порт, сработала Windows-политика блокировки паролей, и учетка «заморозилась»). При этом SSH-сервер ключ-то проверил и подтвердил (ведь файл на диске лежит), а вот сама ОС Windows создать токен для заблокированного юзера не дала. Отсюда и абсолютно невнятный лог про unable to generate token.</p><h2 id="Как-это-лечится-за-5-секунд"><a href="#Как-это-лечится-за-5-секунд" class="headerlink" title="Как это лечится за 5 секунд"></a>Как это лечится за 5 секунд</h2><p>Если вы наступили на те же грабли, решение простое. Заходим на сервер под другой админской учеткой (или локально) и разблокируем Администратора.</p><p>Через PowerShell:</p><pre><code class="hljs powershell"><span class="hljs-built_in">Unlock-LocalUser</span> <span class="hljs-literal">-Name</span> <span class="hljs-string">&quot;Administrator&quot;</span></code></pre><p>Через старый добрый CMD:</p><pre><code class="hljs plaintext">net user Administrator /active:yes</code></pre><p>После этого чудеса прекращаются, и клиент получает доступ к серверу по SSH.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Бывают ошибки, которые гуглятся за две секунды, а бывают те, из-за которых теряешь много времени. К последним как раз относится сбой</summary>
        
      
    
    
    
    <category term="Troubleshooting" scheme="https://optinsoft.net/blog/categories/Troubleshooting/"/>
    
    
    <category term="windows" scheme="https://optinsoft.net/blog/tags/windows/"/>
    
    <category term="openssh" scheme="https://optinsoft.net/blog/tags/openssh/"/>
    
    <category term="ssh" scheme="https://optinsoft.net/blog/tags/ssh/"/>
    
    <category term="devops" scheme="https://optinsoft.net/blog/tags/devops/"/>
    
    <category term="troubleshooting" scheme="https://optinsoft.net/blog/tags/troubleshooting/"/>
    
    <category term="security" scheme="https://optinsoft.net/blog/tags/security/"/>
    
  </entry>
  
  <entry>
    <title>How to Quickly Get the Direct URL from a Tracking Link</title>
    <link href="https://optinsoft.net/blog/2026/09/04/How-to-Quickly-Get-the-Direct-URL-from-a-Tracking-Link/"/>
    <id>https://optinsoft.net/blog/2026/09/04/How-to-Quickly-Get-the-Direct-URL-from-a-Tracking-Link/</id>
    <published>2026-09-04T13:33:37.000Z</published>
    <updated>2026-09-09T16:00:22.388Z</updated>
    
    <content type="html"><![CDATA[<p>Services, online stores, and other websites often use tracking links instead of direct links.</p><p>This is especially noticeable in emails: a link to a question, order, product, or another page first goes through an intermediate service that tracks the click and then redirects you to the actual URL.</p><p>But tracking links are not limited to email. You can also encounter them on forums, in comments, communities, and other services — basically anywhere the site owner wants to track clicks on external links.</p><p>Sometimes the actual URL is already embedded directly in one of the tracking link’s parameters. For example:</p><pre><code class="hljs text">https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&amp;campaign=email</code></pre><p>Here, the <code>u</code> parameter contains the destination URL:</p><pre><code class="hljs text">https%3A%2F%2Fexample.com%2Forders%2F12345</code></pre><p>You don’t have to copy and decode it manually. You can quickly extract the direct URL right in DevTools.</p><h2 id="How-to-do-it"><a href="#How-to-do-it" class="headerlink" title="How to do it"></a>How to do it</h2><ol><li>Copy the tracking link.</li><li>Open DevTools by pressing <code>F12</code>.</li><li>Go to the <strong>Console</strong> tab.</li><li>Enter:</li></ol><pre><code class="hljs js"><span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(<span class="hljs-string">&quot;https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&amp;campaign=email&quot;</span>);</code></pre><ol start="5"><li>Then retrieve the value of the parameter:</li></ol><pre><code class="hljs js">url.<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&quot;u&quot;</span>);</code></pre><p>The Console will display the already decoded direct URL:</p><pre><code class="hljs text">https://example.com/orders/12345</code></pre><p>You can even click the link directly in the Console.</p><p>The <code>URL</code> object provides access to query parameters through <code>searchParams</code>, and <code>URLSearchParams.get()</code> returns the value of a specific parameter. Query parameter values are decoded automatically, so there is no need to call <code>decodeURIComponent()</code> separately.</p><p>Of course, the parameter name will not always be <code>u</code>. Depending on the service, it could be <code>url</code>, <code>target</code>, <code>redirect</code>, <code>redirect_url</code>, or something else.</p><p>The main thing is to inspect the tracking link and look for a parameter that contains the encoded destination URL.</p><p>It’s a simple way to get the direct link without opening the tracking URL and going through the intermediate service.</p><h2 id="How-to-get-the-URL-from-a-reddit-email"><a href="#How-to-get-the-URL-from-a-reddit-email" class="headerlink" title="How to get the URL from a reddit email"></a>How to get the URL from a reddit email</h2><p>Links in emails from reddit look something like this:</p><pre><code class="hljs text">https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a...</code></pre><p>To get the direct link:</p><ol><li>In DevTools, enter the following (replace it with the link from your email):</li></ol><pre><code class="hljs js"><span class="hljs-keyword">const</span> sourceUrl=<span class="hljs-string">&quot;https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a...&quot;</span></code></pre><ol start="2"><li>Run the following to extract the direct link:</li></ol><pre><code class="hljs js"><span class="hljs-built_in">decodeURIComponent</span>(sourceUrl.<span class="hljs-title function_">match</span>(<span class="hljs-regexp">/(?&lt;=\/)https?:.*?(?=\/\d\/|$)/</span>)[<span class="hljs-number">0</span>])</code></pre>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Services, online stores, and other websites often use tracking links instead of direct links.&lt;/p&gt;
&lt;p&gt;This is especially noticeable in</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="url" scheme="https://optinsoft.net/blog/tags/url/"/>
    
    <category term="tracking" scheme="https://optinsoft.net/blog/tags/tracking/"/>
    
    <category term="privacy" scheme="https://optinsoft.net/blog/tags/privacy/"/>
    
    <category term="web" scheme="https://optinsoft.net/blog/tags/web/"/>
    
    <category term="devtools" scheme="https://optinsoft.net/blog/tags/devtools/"/>
    
    <category term="tips" scheme="https://optinsoft.net/blog/tags/tips/"/>
    
    <category term="javascript" scheme="https://optinsoft.net/blog/tags/javascript/"/>
    
    <category term="reddit" scheme="https://optinsoft.net/blog/tags/reddit/"/>
    
  </entry>
  
  <entry>
    <title>Как быстро получить прямую ссылку из tracking URL</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/04/How-to-Quickly-Get-the-Direct-URL-from-a-Tracking-Link/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/04/How-to-Quickly-Get-the-Direct-URL-from-a-Tracking-Link/</id>
    <published>2026-09-04T13:33:37.000Z</published>
    <updated>2026-09-09T16:00:32.001Z</updated>
    
    <content type="html"><![CDATA[<p>Сервисы, интернет-магазины и другие сайты часто используют tracking-ссылки вместо прямых ссылок.</p><p>Это особенно заметно в письмах: ссылка на вопрос, заказ, товар или другую страницу сначала ведёт через промежуточный сервис, который фиксирует переход, а затем перенаправляет на настоящий URL.</p><p>Но tracking-ссылки встречаются не только в email. Их можно увидеть на форумах, в комментариях, сообществах и других сервисах — везде, где владельцу сайта нужно учитывать переходы по внешним ссылкам.</p><p>Иногда настоящий URL уже находится прямо внутри tracking-ссылки, в одном из её параметров. Например:</p><pre><code class="hljs text">https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&amp;campaign=email</code></pre><p>Здесь параметр <code>u</code> содержит URL назначения:</p><pre><code class="hljs text">https%3A%2F%2Fexample.com%2Forders%2F12345</code></pre><p>Его не обязательно вручную копировать и декодировать. Можно быстро извлечь прямую ссылку прямо в DevTools.</p><h2 id="Как-это-сделать"><a href="#Как-это-сделать" class="headerlink" title="Как это сделать"></a>Как это сделать</h2><ol><li>Скопируйте tracking-ссылку.</li><li>Откройте DevTools клавишей <code>F12</code>.</li><li>Перейдите на вкладку <strong>Console</strong>.</li><li>Введите:</li></ol><pre><code class="hljs js"><span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(<span class="hljs-string">&quot;https://tracking.example.com/click?u=https%3A%2F%2Fexample.com%2Forders%2F12345&amp;campaign=email&quot;</span>)</code></pre><ol start="5"><li>Затем получите значение нужного параметра:</li></ol><pre><code class="hljs js">url.<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&quot;u&quot;</span>)</code></pre><p>В консоли будет выведена уже декодированная прямая ссылка:</p><pre><code class="hljs text">https://example.com/orders/12345</code></pre><p>На неё можно сразу кликнуть прямо в консоли.</p><p><code>URL</code> предоставляет доступ к параметрам строки запроса через <code>searchParams</code>, а <code>URLSearchParams.get()</code> возвращает значение указанного параметра. При чтении query-параметров значение декодируется автоматически, поэтому отдельный вызов <code>decodeURIComponent()</code> здесь не требуется.</p><p>Конечно, имя параметра не всегда будет <code>u</code>. В зависимости от сервиса это может быть <code>url</code>, <code>target</code>, <code>redirect</code>, <code>redirect_url</code> и т. п.</p><p>Главное — посмотреть на tracking-ссылку и найти параметр, в котором содержится закодированный URL назначения.</p><p>Это простой способ получить прямую ссылку, не открывая tracking URL и не проходя через промежуточный сервис.</p><h2 id="Как-получить-URL-из-письма-от-reddit"><a href="#Как-получить-URL-из-письма-от-reddit" class="headerlink" title="Как получить URL из письма от reddit"></a>Как получить URL из письма от reddit</h2><p>Ссылки в письмах от reddit выглядят примерно так:</p><pre><code class="hljs text">https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a...</code></pre><p>Чтобы получить прямую ссылку:</p><ol><li>В DevTools введите (замените своей ссылкой из письма):</li></ol><pre><code class="hljs js"><span class="hljs-keyword">const</span> sourceUrl=<span class="hljs-string">&quot;https://click.redditmail.com/CL0/https:%2F%2Fwww.reddit.com%2Fr%2Frust%2F%3F%2524deep_link=true%26correlation_id=70d9a...&quot;</span></code></pre><ol start="2"><li>Введите для получения прямой ссылки:</li></ol><pre><code class="hljs js"><span class="hljs-built_in">decodeURIComponent</span>(sourceUrl.<span class="hljs-title function_">match</span>(<span class="hljs-regexp">/(?&lt;=\/)https?:.*?(?=\/\d\/|$)/</span>)[<span class="hljs-number">0</span>])</code></pre>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Сервисы, интернет-магазины и другие сайты часто используют tracking-ссылки вместо прямых ссылок.&lt;/p&gt;
&lt;p&gt;Это особенно заметно в письмах:</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="url" scheme="https://optinsoft.net/blog/tags/url/"/>
    
    <category term="tracking" scheme="https://optinsoft.net/blog/tags/tracking/"/>
    
    <category term="privacy" scheme="https://optinsoft.net/blog/tags/privacy/"/>
    
    <category term="web" scheme="https://optinsoft.net/blog/tags/web/"/>
    
    <category term="devtools" scheme="https://optinsoft.net/blog/tags/devtools/"/>
    
    <category term="tips" scheme="https://optinsoft.net/blog/tags/tips/"/>
    
    <category term="javascript" scheme="https://optinsoft.net/blog/tags/javascript/"/>
    
    <category term="reddit" scheme="https://optinsoft.net/blog/tags/reddit/"/>
    
  </entry>
  
  <entry>
    <title>Tools for Digital Footprint Auditing</title>
    <link href="https://optinsoft.net/blog/2026/09/03/Tools-for-Digital-Footprint-Auditing/"/>
    <id>https://optinsoft.net/blog/2026/09/03/Tools-for-Digital-Footprint-Auditing/</id>
    <published>2026-09-03T08:42:31.000Z</published>
    <updated>2026-10-01T15:12:53.475Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Tools-for-Digital-Footprint-Auditing"><a href="#Tools-for-Digital-Footprint-Auditing" class="headerlink" title="Tools for Digital Footprint Auditing"></a>Tools for Digital Footprint Auditing</h2><p>Modern anti-fraud systems (Akamai, Cloudflare, PerimeterX) have long moved past analyzing just IPs and Cookies. Today, identification relies on Browser Fingerprinting — evaluating canvas&#x2F;WebGL rendering artifacts, audio subsystem entropy, JA3&#x2F;JA4 network stack hashes, and JS prototype behaviors. </p><h2 id="Top-9-Browser-Fingerprint-Checkers"><a href="#Top-9-Browser-Fingerprint-Checkers" class="headerlink" title="Top 9 Browser Fingerprint Checkers"></a>Top 9 Browser Fingerprint Checkers</h2><ul><li><strong>BrowserScan</strong> — A comprehensive scanner tracking over 50 parameters. It detects automation frameworks (Puppeteer&#x2F;Selenium) and scores overall profile consistency (Human Score).</li><li><strong>BrowserLeaks</strong> — The go-to tool for low-level analysis. It verifies JA3&#x2F;JA4 TLS fingerprints, HTTP&#x2F;2 characteristics, and provides raw Canvas&#x2F;WebGL hashes.</li><li><strong>CreepJS</strong> — An open-source checker designed to expose spoofing. It analyzes Prototype Pollution, hidden anomalies in JS objects, and calculates a baseline Trust Score.</li><li><strong>Pixelscan</strong> — A tool focused on logical data consistency. It cross-references environment metrics like system language, browser timezone, and proxy geolocation to catch mismatches.</li><li><strong>Iphey</strong> — A simulator modeled after fintech and ad-tech verification engines. It evaluates profiles based on Autonomous System (ASN) reputation and hardware parity.</li><li><strong>Cover Your Tracks (EFF)</strong> — A project by the Electronic Frontier Foundation. It measures browser uniqueness in bits of entropy against a live database of real users.</li><li><strong>AmIUnique</strong> — A statistics-driven utility. It displays the exact percentage of global users sharing your specific font lists or screen resolution.</li><li><strong>Scrapfly Browser Fingerprint Test</strong> — A verification tool developed by a web scraping platform. It simulates popular bot-detection challenges and monitors runtime reactions to script injections.</li><li><strong>Whoer.net</strong> — A quick network perimeter assessment tool. It identifies WebRTC IP leaks, references DNSBL blocklists, and detects potential proxy&#x2F;VPN tunnels using MTU analysis.</li></ul><h2 id="Comparative-Analysis"><a href="#Comparative-Analysis" class="headerlink" title="Comparative Analysis"></a>Comparative Analysis</h2><table><thead><tr><th>Tool</th><th>Focus</th><th>JS &#x2F; API Analysis Depth</th><th>Network Stack Inspection</th><th>Key Metric &#x2F; Feature</th></tr></thead><tbody><tr><td><strong><strong>BrowserScan</strong></strong></td><td>Full profile auditing</td><td>High</td><td>Medium</td><td>Automated consistency scoring</td></tr><tr><td><strong><strong>BrowserLeaks</strong></strong></td><td>Low-level APIs</td><td>High</td><td>High (JA3&#x2F;JA4)</td><td>Aspect-by-aspect vector testing</td></tr><tr><td><strong><strong>CreepJS</strong></strong></td><td>Spoof &amp; mask detection</td><td>Extreme</td><td>Minimum</td><td>In-depth JS prototype tracking</td></tr><tr><td><strong><strong>Pixelscan</strong></strong> Profile data integrity</td><td>Medium</td><td>Medium</td><td>System vs. IP anomaly detection</td><td></td></tr><tr><td><strong><strong>Iphey</strong></strong></td><td>Reputation scoring</td><td>Medium</td><td>High (ASN, Proxy)</td><td>ASN trust and behavior validation</td></tr><tr><td><strong><strong>Cover Your Tracks</strong></strong></td><td>Privacy &amp; tracking defense</td><td>Medium</td><td>Minimum</td><td>Entropy measurements in bits</td></tr><tr><td><strong><strong>AmIUnique</strong></strong></td><td>Global metadata trends</td><td>Medium</td><td>Minimum</td><td>Percentage comparison with real devices</td></tr><tr><td><strong><strong>Scrapfly Test</strong></strong></td><td>Anti-bot mitigation</td><td>High</td><td>High (HTTP&#x2F;2)</td><td>Cloudflare &#x2F; Akamai engine simulation</td></tr><tr><td><strong><strong>Whoer.net</strong></strong></td><td>Network perimeter</td><td>Basic</td><td>High (MTU, DNSBL)</td><td>Rapid real IP exposure testing</td></tr></tbody></table>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;Tools-for-Digital-Footprint-Auditing&quot;&gt;&lt;a href=&quot;#Tools-for-Digital-Footprint-Auditing&quot; class=&quot;headerlink&quot; title=&quot;Tools for Digital</summary>
        
      
    
    
    
    <category term="General" scheme="https://optinsoft.net/blog/categories/General/"/>
    
    
    <category term="footprint" scheme="https://optinsoft.net/blog/tags/footprint/"/>
    
    <category term="fingerprint" scheme="https://optinsoft.net/blog/tags/fingerprint/"/>
    
    <category term="web" scheme="https://optinsoft.net/blog/tags/web/"/>
    
    <category term="browser" scheme="https://optinsoft.net/blog/tags/browser/"/>
    
    <category term="scrapping" scheme="https://optinsoft.net/blog/tags/scrapping/"/>
    
    <category term="checker" scheme="https://optinsoft.net/blog/tags/checker/"/>
    
  </entry>
  
  <entry>
    <title>Инструменты для аудита цифрового следа</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/03/Tools-for-Digital-Footprint-Auditing/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/03/Tools-for-Digital-Footprint-Auditing/</id>
    <published>2026-09-03T08:42:31.000Z</published>
    <updated>2026-10-01T15:13:03.107Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Инструменты-для-аудита-цифрового-следа"><a href="#Инструменты-для-аудита-цифрового-следа" class="headerlink" title="Инструменты для аудита цифрового следа"></a>Инструменты для аудита цифрового следа</h2><p>Современный антифрод (Akamai, Cloudflare, PerimeterX) давно не смотрит на IP и Cookie. Идентификация строится на базе Browser Fingerprinting — анализе особенностей рендеринга (Canvas&#x2F;WebGL), энтропии аудиосистемы, JA3&#x2F;JA4-хэшей сетевого стека и JS-прототипов. </p><h2 id="Топ-9-чекеров-цифрового-отпечатка"><a href="#Топ-9-чекеров-цифрового-отпечатка" class="headerlink" title="Топ-9 чекеров цифрового отпечатка"></a>Топ-9 чекеров цифрового отпечатка</h2><ul><li><strong>BrowserScan</strong> — комплексный сканер (50+ параметров). Находит следы автоматизации (Puppeteer&#x2F;Selenium) и оценивает консистентность профиля (Human Score).</li><li><strong>BrowserLeaks</strong> — база для низкоуровневого анализа. Проверяет JA3&#x2F;JA4 отпечатки TLS, особенности HTTP&#x2F;2 и выдает чистые хэши Canvas&#x2F;WebGL.</li><li><strong>CreepJS</strong> — open-source чекер для выявления подмен. Проверяет Prototype Pollution, аномалии JS-объектов и вычисляет Trust Score.</li><li><strong>Pixelscan</strong> — инструмент для проверки логической согласованности данных. Сопоставляет параметры окружения (соответствие языка ОС, таймзоны браузера и геопозиции прокси).</li><li><strong>Iphey</strong> — симулятор проверок финтех- и рекламных платформ. Оценивает профиль на основе репутации автономной системы (ASN) провайдера и аппаратных соответствий.</li><li><strong>Cover Your Tracks (EFF)</strong> — проект Electronic Frontier Foundation. Измеряет уникальность браузера в битах энтропии относительно базы данных реальных пользователей.</li><li><strong>AmIUnique</strong> — инструмент для сбора статистики. Показывает точный процент пользователей с аналогичным набором шрифтов или расширением экрана.</li><li><strong>Scrapfly Browser Fingerprint Test</strong> — чекер от платформы веб-скрапинга. Эмулирует проверки антибот-систем и тестирует реакцию браузера на инъекции скриптов.</li><li><strong>Whoer.net</strong> — экспресс-тест сетевого периметра. Находит утечки IP через WebRTC, проверяет DNSBL (черные списки) и определяет туннели по значению MTU.</li></ul><h2 id="Сравнительный-анализ"><a href="#Сравнительный-анализ" class="headerlink" title="Сравнительный анализ"></a>Сравнительный анализ</h2><table><thead><tr><th>Инструмент</th><th>Фокус проверки</th><th>Глубина анализа JS &#x2F; API</th><th>Проверка сетевого стека</th><th>Главная фишка</th></tr></thead><tbody><tr><td><strong><strong>BrowserScan</strong></strong></td><td>Комплексный аудит</td><td>Высокая</td><td>Средняя</td><td>Автоматический скоринг консистентности</td></tr><tr><td><strong><strong>BrowserLeaks</strong></strong></td><td>Низкоуровневые API</td><td>Высокая</td><td>Высокая (JA3&#x2F;JA4)</td><td>Поаспектный анализ каждого вектора уязвимости</td></tr><tr><td><strong><strong>CreepJS</strong></strong></td><td>Поиск подмен и маскировок</td><td>Экстремальная</td><td>Минимальная</td><td>Глубокий анализ прототипов JS-объектов</td></tr><tr><td><strong><strong>Pixelscan</strong></strong></td><td>Логика данных профиля</td><td>Средняя</td><td>Средняя</td><td>Проверка логических аномалий системы и IP</td></tr><tr><td><strong><strong>Iphey</strong></strong></td><td>Репутационный скоринг</td><td>Средняя</td><td>Высокая (ASN, Proxy)</td><td>Оценка траста автономной системы (ASN)</td></tr><tr><td><strong><strong>Cover Your Tracks</strong></strong></td><td>Анонимность и защита</td><td>Средняя</td><td>Минимальная</td><td>Измерение уникальности в битах энтропии</td></tr><tr><td><strong><strong>AmIUnique</strong></strong></td><td>Глобальная статистика</td><td>Средняя</td><td>Минимальная</td><td>Процентное сравнение параметров с мировой базой</td></tr><tr><td><strong><strong>Scrapfly Test</strong></strong></td><td>Обход антибот-систем</td><td>Высокая</td><td>Высокая (HTTP&#x2F;2)</td><td>Эмуляция проверок систем Cloudflare &#x2F; Akamai</td></tr><tr><td><strong><strong>Whoer.net</strong></strong></td><td>Сетевой периметр</td><td>Базовая</td><td>Высокая (MTU, DNSBL)</td><td>Быстрый экспресс-тест на утечку реального IP</td></tr></tbody></table>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;Инструменты-для-аудита-цифрового-следа&quot;&gt;&lt;a href=&quot;#Инструменты-для-аудита-цифрового-следа&quot; class=&quot;headerlink&quot; title=&quot;Инструменты для</summary>
        
      
    
    
    
    <category term="General" scheme="https://optinsoft.net/blog/categories/General/"/>
    
    
    <category term="footprint" scheme="https://optinsoft.net/blog/tags/footprint/"/>
    
    <category term="fingerprint" scheme="https://optinsoft.net/blog/tags/fingerprint/"/>
    
    <category term="web" scheme="https://optinsoft.net/blog/tags/web/"/>
    
    <category term="browser" scheme="https://optinsoft.net/blog/tags/browser/"/>
    
    <category term="scrapping" scheme="https://optinsoft.net/blog/tags/scrapping/"/>
    
    <category term="checker" scheme="https://optinsoft.net/blog/tags/checker/"/>
    
  </entry>
  
  <entry>
    <title>WebSocket Session API in ProxyMapService</title>
    <link href="https://optinsoft.net/blog/2026/09/02/WebSocket-Session-API-in-ProxyMapService/"/>
    <id>https://optinsoft.net/blog/2026/09/02/WebSocket-Session-API-in-ProxyMapService/</id>
    <published>2026-09-02T10:29:08.000Z</published>
    <updated>2026-09-02T11:38:34.604Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Real-Time-Traffic-Monitoring-Implementing-a-WebSocket-Subscription-API-in-ProxyMapService"><a href="#Real-Time-Traffic-Monitoring-Implementing-a-WebSocket-Subscription-API-in-ProxyMapService" class="headerlink" title="Real-Time Traffic Monitoring: Implementing a WebSocket Subscription API in ProxyMapService"></a>Real-Time Traffic Monitoring: Implementing a WebSocket Subscription API in ProxyMapService</h2><p>Web scraping applications frequently require real-time tracking of all currently downloading URLs.</p><p>However, achieving this in practice is far from trivial. For instance, if the target application is a web browser, capturing network activity typically requires globally overriding the built-in fetch and XHR (XMLHttpRequest) methods. The situation becomes significantly more complex if the web page contains iframe elements: you must dynamically inject fetch and XHR overrides into them as well, adding a heavy layer of complexity and fragility to the system. Custom injection scripts can easily conflict with Content Security Policies (CSP) or simply miss requests originating from isolated contexts. </p><p>Instead of breaking frontend logic inside the browser and relying on complex script injections, a much more reliable approach is to leverage the proxy layer. To achieve this, an optional <strong>WebSocket Session API</strong> was implemented in ProxyMapService. It allows external applications to connect directly to the proxy, supply a URL filter, and instantly receive push notifications for all requested resources. </p><h2 id="Configuration-and-Connection-Logistics"><a href="#Configuration-and-Connection-Logistics" class="headerlink" title="Configuration and Connection Logistics"></a>Configuration and Connection Logistics</h2><p>The entire mechanism is managed by the SessionAPI section in the appsettings.json configuration file. Here is how it looks by default: </p><pre><code class="hljs json"><span class="hljs-attr">&quot;SessionAPI&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;WebSocketsEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>As you can see, by default, the entire SessionAPI infrastructure, including WebSockets, is turned off, and the Domain property is left empty. </p><h2 id="Option-1-Working-via-Local-Port-Empty-Domain"><a href="#Option-1-Working-via-Local-Port-Empty-Domain" class="headerlink" title="Option 1. Working via Local Port (Empty Domain)"></a>Option 1. Working via Local Port (Empty Domain)</h2><p>When Domain is empty, the control endpoints are accessed like a regular web server on one of the ports that ProxyMapService listens to for incoming connections. For example, if the proxy is running on port 5000: </p><ul><li>HTTP API Access: <a href="http://127.0.0.1:5000/session/">http://127.0.0.1:5000/session/</a></li><li>Browser Configuration: 127.0.0.1:5000 is specified as the HTTP or SOCKS5 proxy.</li></ul><h2 id="Option-2-Working-via-a-Dedicated-Pseudo-Domain-Recommended"><a href="#Option-2-Working-via-a-Dedicated-Pseudo-Domain-Recommended" class="headerlink" title="Option 2. Working via a Dedicated Pseudo-Domain (Recommended)"></a>Option 2. Working via a Dedicated Pseudo-Domain (Recommended)</h2><p>To fully isolate control traffic from proxied traffic, it is recommended to enable the API and define a special internal domain: </p><pre><code class="hljs json"><span class="hljs-attr">&quot;SessionAPI&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;WebSocketsEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;proxymapper&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>In this mode, proxymapper acts as a dedicated system domain. When ProxyMapService detects that a client is trying to access the proxymapper host, it intercepts the request locally and handles it as a call to the SessionAPI instead of routing it out to the external network. </p><p><strong>Crucial Nuance for Command-Line Utilities (e.g., cURL):</strong></p><p>If you are using curl to route traffic through the proxy, the socks4 protocol will not allow you to call the internal API at all. Furthermore, the socks5:&#x2F;&#x2F; prefix must be replaced with socks5h:&#x2F;&#x2F; (e.g., socks5h:&#x2F;&#x2F;127.0.0.1:5000). This ensures that curl does not attempt to resolve the proxymapper domain locally via your OS DNS, but instead delegates the name resolution directly to the proxy server itself. </p><p>With this configuration, the WebSocket Session API endpoint will be accessible via: <code>ws://proxymapper/session/ws</code></p><h2 id="How-It-Works-in-Practice"><a href="#How-It-Works-in-Practice" class="headerlink" title="How It Works in Practice"></a>How It Works in Practice</h2><p>Using the subscription mechanism is straightforward for a developer: </p><ol><li><strong>Establishing a Connection</strong>: The client opens a persistent WebSocket connection to ws:&#x2F;&#x2F;proxymapper&#x2F;session&#x2F;ws.</li><li><strong>Sending a Filter</strong>: Immediately after a successful connection, the client sends a text JSON message containing a subscribe action and a regular expression (Regex) matching the target URLs:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;subscribe&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;pattern&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;api\\.example\\.com/v1/.*&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><ol start="3"><li><strong>Subscription Confirmation and session_id</strong>: The server replies with a confirmation message acknowledging the registered filter, which includes vital execution context:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;subscribed&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;session_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;:5000&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p><strong>What does session_id mean and how does traffic isolation work?</strong> </p><ul><li>session_id represents a unique session identifier. If your environment utilizes authentication via a <strong>Sticky Proxy</strong>, this string identifier is explicitly bound to the user upon authentication.</li><li>If a <strong>Sticky Proxy is not used</strong>, the session_id returns a colon followed by the incoming port number (e.g., :5000). This means that notifications are strictly isolated to the specific port through which the subscription was created.</li><li><strong>Lifecycle Rule</strong>: The user is guaranteed to receive notifications for URLs downloaded <em>strictly within the session that was active at the moment of subscription</em>. If the proxy session changes, notifications for the old subscription will cease. Similarly, under port-based isolation (without sticky routing), you will never see traffic passing through another proxy port inside the WebSocket channel for port :5000.</li></ul><ol start="4"><li><strong>Receiving Notifications</strong>: When a downloaded URL matches your pattern, the proxy server dispatches a flat, structured event optimized for fast reading:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;event_type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;url_matched&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;timestamp&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-09-02T10:20:45.0328755Z&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;session_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;:5000&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://api.example.com/v1/users/profile&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><h2 id="Testing-Environment-Proxy-WebSocket-Control-Panel"><a href="#Testing-Environment-Proxy-WebSocket-Control-Panel" class="headerlink" title="Testing Environment: Proxy WebSocket Control Panel"></a>Testing Environment: Proxy WebSocket Control Panel</h2><p>The test web panel is hosted in a dedicated project subdirectory: <code>tests\test-websocket</code>. </p><p>Inside the folder lies a self-contained HTML file, index.html, which implements the diagnostic interface: </p><pre><code class="hljs html"><span class="hljs-comment">&lt;!-- Located at: \tests\test-websocket\index.html --&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;card&quot;</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Proxy WebSocket Control Panel<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;form-group&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;wsUrl&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;ws://proxymapper/session/ws&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;toggleConnection()&quot;</span>&gt;</span>Connect<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;form-group&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;filterPattern&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;(google\.com|yandex\.ru)/.*&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;sendSubscription()&quot;</span>&gt;</span>Set Filter<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></code></pre><h2 id="How-to-Run-the-Test-and-Verify-Monitoring"><a href="#How-to-Run-the-Test-and-Verify-Monitoring" class="headerlink" title="How to Run the Test and Verify Monitoring:"></a>How to Run the Test and Verify Monitoring:</h2><ol><li>Launch the configured ProxyMapService with WebSockets enabled and the domain set to proxymapper.</li><li>Open the tests\test-websocket\index.html file in any modern browser whose traffic is being routed through your proxy.</li><li>Click the <strong>Connect</strong> button, set your regex filter pattern, and watch live notifications populate the dark console UI with full request details instantly.</li></ol><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Shifting URL tracing logic out of fragile browser scripts and into the ProxyMapService layer provides 100% visibility into network requests. It no longer matters where a request originates—be it the main window, a heavy background iframe, or a hidden service worker—the proxy server reliably intercepts, filters based on the active session (or port), and safely delivers the event to the client via a WebSocket subscription without modifying a single line of original page code.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;Real-Time-Traffic-Monitoring-Implementing-a-WebSocket-Subscription-API-in-ProxyMapService&quot;&gt;&lt;a</summary>
        
      
    
    
    
    <category term="ProxyMapService" scheme="https://optinsoft.net/blog/categories/ProxyMapService/"/>
    
    
    <category term="proxy" scheme="https://optinsoft.net/blog/tags/proxy/"/>
    
    <category term="session" scheme="https://optinsoft.net/blog/tags/session/"/>
    
    <category term="api" scheme="https://optinsoft.net/blog/tags/api/"/>
    
    <category term="websocket" scheme="https://optinsoft.net/blog/tags/websocket/"/>
    
  </entry>
  
  <entry>
    <title>WebSocket Session API в ProxyMapService</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/02/WebSocket-Session-API-in-ProxyMapService/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/02/WebSocket-Session-API-in-ProxyMapService/</id>
    <published>2026-09-02T10:29:08.000Z</published>
    <updated>2026-10-04T16:16:19.410Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Мониторинг-трафика-в-реальном-времени-реализация-WebSocket-API-для-подписок-в-ProxyMapService"><a href="#Мониторинг-трафика-в-реальном-времени-реализация-WebSocket-API-для-подписок-в-ProxyMapService" class="headerlink" title="Мониторинг трафика в реальном времени: реализация WebSocket API для подписок в ProxyMapService"></a>Мониторинг трафика в реальном времени: реализация WebSocket API для подписок в ProxyMapService</h2><p>Часто в приложениях веб-скрапинга необходимо оперативно получать информацию обо всех URL-адресах, которые скачиваются в данный момент. </p><p>Однако на практике это может оказаться далеко не тривиальной задачей. Например, если целевое приложение — браузер, для перехвата сетевой активности обычно требуется глобально переопределить встроенные методы fetch и XHR (XMLHttpRequest). Ситуация сильно усложняется, если на веб-странице присутствуют элементы iframe: в них также необходимо динамически внедрять переопределения для fetch и XHR, что добавляет дополнительный уровень сложности и хрупкости всей системе. Кастомные скрипты могут конфликтовать с политиками безопасности (Content Security Policy) или просто пропускать запросы из изолированных контекстов. </p><p>Вместо того чтобы ломать логику внутри самого браузера и внедрять сложные инъекции, гораздо надежнее использовать возможности прокси-слоя. Для этого в ProxyMapService было реализовано опциональное <strong>WebSocket Session API</strong>. Оно позволяет внешним приложениям подключаться напрямую к прокси, передавать нужный фильтр URL и мгновенно получать push-сообщения обо всех запрашиваемых ресурсах. </p><h2 id="Конфигурация-и-особенности-подключения"><a href="#Конфигурация-и-особенности-подключения" class="headerlink" title="Конфигурация и особенности подключения"></a>Конфигурация и особенности подключения</h2><p>За работу данного механизма отвечает раздел SessionAPI в файле конфигурации appsettings.json. Вот как он выглядит по умолчанию: </p><pre><code class="hljs json"><span class="hljs-attr">&quot;SessionAPI&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;WebSocketsEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>Как можно видеть, по умолчанию вся инфраструктура SessionAPI, включая веб-сокеты, отключена, а свойство Domain оставлено пустым. </p><h2 id="Вариант-1-Работа-через-локальный-порт-Пустой-домен"><a href="#Вариант-1-Работа-через-локальный-порт-Пустой-домен" class="headerlink" title="Вариант 1. Работа через локальный порт (Пустой домен)"></a>Вариант 1. Работа через локальный порт (Пустой домен)</h2><p>Если Domain пустой, доступ к управляющим эндпоинтам осуществляется как к обычному веб-серверу на одном из портов, которые ProxyMapService слушает для входящих соединений. Например, если прокси запущен на порту 5000: </p><ul><li>Доступ к HTTP API: <a href="http://127.0.0.1:5000/session/">http://127.0.0.1:5000/session/</a></li><li>Настройка браузера: в качестве HTTP или SOCKS5 прокси указывается 127.0.0.1:5000.</li></ul><h2 id="Вариант-2-Работа-через-выделенный-псевдо-домен-Рекомендуемый"><a href="#Вариант-2-Работа-через-выделенный-псевдо-домен-Рекомендуемый" class="headerlink" title="Вариант 2. Работа через выделенный псевдо-домен (Рекомендуемый)"></a>Вариант 2. Работа через выделенный псевдо-домен (Рекомендуемый)</h2><p>Для полноценной изоляции управляющего трафика от проксируемого рекомендуется включить API и задать специальный внутренний домен: </p><pre><code class="hljs json"><span class="hljs-attr">&quot;SessionAPI&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;Enabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;WebSocketsEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>    <span class="hljs-attr">&quot;Domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;proxymapper&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>В этом режиме proxymapper становится выделенным системным доменом. Когда ProxyMapService видит, что клиент обращается к хосту proxymapper, он перехватывает этот запрос локально и обрабатывает его как вызов к SessionAPI, вместо того чтобы проксировать его во внешнюю сеть. </p><p><strong>Важный нюанс для консольных утилит (например, cURL):</strong></p><p>Если вы используете curl для работы с прокси, протокол socks4 вообще не позволит вызвать внутреннее API. Более того, вместо стандартного префикса socks5:&#x2F;&#x2F; нужно использовать socks5h:&#x2F;&#x2F; (например, socks5h:&#x2F;&#x2F;127.0.0.1:5000). Это необходимо для того, чтобы curl не пытался самостоятельно отресолвить домен proxymapper через DNS вашей операционной системы, а делегировал разрешение этого имени самому прокси-серверу. </p><p>При такой конфигурации вызов WebSocket Session API будет осуществляться через веб-сокет по адресу: <code>ws://proxymapper/session/ws</code></p><h2 id="Как-это-работает-на-практике"><a href="#Как-это-работает-на-практике" class="headerlink" title="Как это работает на практике"></a>Как это работает на практике</h2><p>Использование механизма подписок выглядит так: </p><ol><li><strong>Установка соединения</strong>: Клиент открывает постоянное WebSocket-соединение по адресу ws:&#x2F;&#x2F;proxymapper&#x2F;session&#x2F;ws.</li><li><strong>Передача фильтра</strong>: Сразу после успешного подключения клиент отправляет текстовое JSON-сообщение с действием subscribe и регулярным выражением (Regex) для интересующих его URL:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;subscribe&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;pattern&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;api\\.example\\.com/v1/.*&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><ol start="3"><li><strong>Подтверждение подписки и session_id</strong>: В ответ сервер присылает подтверждение успешной регистрации фильтра, которое содержит важный контекст:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;subscribed&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;session_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;:5000&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><p><strong>Что означает session_id и как работает изоляция трафика?</strong> </p><ul><li>session_id — это уникальный идентификатор сессии. Если в вашем окружении используется авторизация через <strong>Sticky Proxy</strong>, этот строковый идентификатор явно привязывается к пользователю в момент аутентификации.</li><li>Если же <strong>Sticky Proxy не используется</strong>, то в качестве session_id возвращается двоеточие, за которым следует номер входящего порта (например, :5000). Это означает, что уведомления жестко изолированы конкретным портом, через который была оформлена подписка.</li><li><strong>Важное правило жизненного цикла</strong>: Пользователь гарантированно получает уведомления об URL-адресах, скачиваемых <em>строго в рамках той сессии, которая была активна на момент подписки</em>. Если сессия на прокси сменилась, уведомления по старой подписке приходить перестанут. Точно так же, при изоляции портами (без sticky), вы никогда не увидите в веб-сокете порта :5000 трафик, проходящий параллельно через другой настроенный порт прокси.</li></ul><ol start="4"><li><strong>Получение уведомлений</strong>: При совпадении скачиваемого URL с вашей маской прокси-сервер отправляет плоское структурированное событие, оптимизированное для быстрого чтения:</li></ol><p> </p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;event_type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;url_matched&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;timestamp&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-09-02T10:20:45.0328755Z&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;session_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;:5000&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://api.example.com/v1/users/profile&quot;</span><span class="hljs-punctuation">&#125;</span></code></pre><h2 id="Окружение-для-тестирования-Proxy-WebSocket-Control-Panel"><a href="#Окружение-для-тестирования-Proxy-WebSocket-Control-Panel" class="headerlink" title="Окружение для тестирования: Proxy WebSocket Control Panel"></a>Окружение для тестирования: Proxy WebSocket Control Panel</h2><p>Тестовая веб-панель размещена в выделенной поддиректории проекта: <code>tests\test-websocket</code>.</p><p>Внутри папки находится автономный HTML-файл index.html, реализующий диагностический интерфейс: </p><pre><code class="hljs html"><span class="hljs-comment">&lt;!-- Located at: \tests\test-websocket\index.html --&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;card&quot;</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Proxy WebSocket Control Panel<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;form-group&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;wsUrl&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;ws://proxymapper/session/ws&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;toggleConnection()&quot;</span>&gt;</span>Connect<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;form-group&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;filterPattern&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;(google\.com|yandex\.ru)/.*&quot;</span>&gt;</span>        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;sendSubscription()&quot;</span>&gt;</span>Set Filter<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></code></pre><h2 id="Как-запустить-тест-и-проверить-мониторинг"><a href="#Как-запустить-тест-и-проверить-мониторинг" class="headerlink" title="Как запустить тест и проверить мониторинг:"></a>Как запустить тест и проверить мониторинг:</h2><ol><li>Запустите настроенный ProxyMapService с включенными веб-сокетами и доменом proxymapper.</li><li>Откройте файл tests\test-websocket\index.html в любом современном браузере, трафик которого идет через ваш прокси.</li><li>Нажмите кнопку <strong>Connect</strong>, задайте регулярное выражение для фильтрации и наблюдайте, как в темную консоль интерфейса мгновенно прилетают уведомления со всеми деталями перехваченных запросов.</li></ol><h2 id="Заключение"><a href="#Заключение" class="headerlink" title="Заключение"></a>Заключение</h2><p>Вынесение логики трейсинга URL из кастомных скриптов браузера на уровень ProxyMapService позволило увидеть все сетевые запросы. Теперь не важно, откуда уходит запрос — из основного окна, тяжелого iframe или вообще из фонового сервис-воркера — прокси-сервер гарантированно перехватит его, отфильтрует с учетом активной сессии (или порта) и безопасно доставит клиенту через WebSocket-подписку без изменения оригинального кода страниц.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;Мониторинг-трафика-в-реальном-времени-реализация-WebSocket-API-для-подписок-в-ProxyMapService&quot;&gt;&lt;a</summary>
        
      
    
    
    
    <category term="ProxyMapService" scheme="https://optinsoft.net/blog/categories/ProxyMapService/"/>
    
    
    <category term="proxy" scheme="https://optinsoft.net/blog/tags/proxy/"/>
    
    <category term="session" scheme="https://optinsoft.net/blog/tags/session/"/>
    
    <category term="api" scheme="https://optinsoft.net/blog/tags/api/"/>
    
    <category term="websocket" scheme="https://optinsoft.net/blog/tags/websocket/"/>
    
  </entry>
  
  <entry>
    <title>Kinescope JSON LD Extractor</title>
    <link href="https://optinsoft.net/blog/2026/09/01/Kinescope-JSON-LD-Extractor/"/>
    <id>https://optinsoft.net/blog/2026/09/01/Kinescope-JSON-LD-Extractor/</id>
    <published>2026-09-01T13:20:47.000Z</published>
    <updated>2026-09-01T13:33:33.888Z</updated>
    
    <content type="html"><![CDATA[<h2 id="How-I-Built-a-Browser-Extension-to-Download-Videos-from-Kinescope-for-Offline-Learning"><a href="#How-I-Built-a-Browser-Extension-to-Download-Videos-from-Kinescope-for-Offline-Learning" class="headerlink" title="How I Built a Browser Extension to Download Videos from Kinescope for Offline Learning"></a>How I Built a Browser Extension to Download Videos from Kinescope for Offline Learning</h2><p>I travel a lot — trains, planes, long drives out of town. And I always want to use that time productively: read, listen to podcasts, or learn something new.</p><p>The problem is, internet connectivity on the road is rarely reliable. And many educational platforms use the Kinescope player, which doesn’t offer a built-in way to download videos for offline viewing.</p><p>Sure, you could open DevTools, find the <code>.m3u8</code> playlist in the network requests, copy the link, fire up FFmpeg in the terminal… But doing that every single time — especially when there are dozens of videos — is tedious and eats into time better spent actually learning.</p><p>I wanted a simple solution: open the video page, click a button, and have everything ready for offline watching.</p><hr><h2 id="Why-a-Browser-Extension"><a href="#Why-a-Browser-Extension" class="headerlink" title="Why a Browser Extension?"></a>Why a Browser Extension?</h2><p>I considered a few options: writing a parser script, using a download manager, or building a browser extension.</p><p>I went with an extension because it’s the <strong>most natural and convenient</strong> approach:</p><ul><li>It works right on the video page — no context switching.</li><li>It can intercept the player’s network requests in real time.</li><li>Everything is controlled with a single button. No extra steps.</li></ul><p>The workflow is dead simple: open the lecture, click “Save,” move on to the next one. Repeat for all the materials I plan to watch on the road.</p><hr><h2 id="What-I-Built"><a href="#What-I-Built" class="headerlink" title="What I Built"></a>What I Built</h2><p>The extension does two things:</p><ol><li><strong>Intercepts all links</strong> to video and audio streams requested by the Kinescope player.</li><li><strong>Saves them</strong> to <code>Downloads/kinescope/[Video_Title]/</code> along with metadata (title, duration, description).</li></ol><p>A floating button appears on the page showing how many streams have been found. One click — and all the links are saved to your folder, neatly sorted by video and audio.</p><p>After that, I run a Python script that automatically stitches everything together using FFmpeg into a ready-to-watch <code>.mp4</code> file. And just like that — a library of lectures ready for offline viewing.</p><hr><h2 id="The-Result"><a href="#The-Result" class="headerlink" title="The Result"></a>The Result</h2><p>Now preparing for a trip takes just a few minutes:</p><ol><li>Open all the videos I need → click the button on each page.</li><li>Run one script → it processes everything automatically.</li><li>Copy the finished files to my laptop or tablet and hit the road.</li></ol><p>No DevTools, no manual copying of links. Just time well spent.</p><hr><h2 id="Project-Link"><a href="#Project-Link" class="headerlink" title="Project Link"></a>Project Link</h2><p>The extension is open source and can be installed manually via developer mode in Chrome. All you need are four files from the repository:</p><p>👉 <strong><a href="https://github.com/optinsoft/KinescopeJSONLDExtractor">Kinescope JSON-LD Extractor PRO on GitHub</a></strong> 👈</p><p>Hope you find it useful.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;How-I-Built-a-Browser-Extension-to-Download-Videos-from-Kinescope-for-Offline-Learning&quot;&gt;&lt;a</summary>
        
      
    
    
    
    <category term="GitHub" scheme="https://optinsoft.net/blog/categories/GitHub/"/>
    
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="extension" scheme="https://optinsoft.net/blog/tags/extension/"/>
    
    <category term="kinescope" scheme="https://optinsoft.net/blog/tags/kinescope/"/>
    
  </entry>
  
  <entry>
    <title>Kinescope JSON LD Extractor</title>
    <link href="https://optinsoft.net/blog/ru/2026/09/01/Kinescope-JSON-LD-Extractor/"/>
    <id>https://optinsoft.net/blog/ru/2026/09/01/Kinescope-JSON-LD-Extractor/</id>
    <published>2026-09-01T13:20:47.000Z</published>
    <updated>2026-09-01T13:38:22.159Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Как-я-сделал-расширение-для-скачивания-видео-с-Kinescope-чтобы-учиться-в-дороге"><a href="#Как-я-сделал-расширение-для-скачивания-видео-с-Kinescope-чтобы-учиться-в-дороге" class="headerlink" title="Как я сделал расширение для скачивания видео с Kinescope, чтобы учиться в дороге"></a>Как я сделал расширение для скачивания видео с Kinescope, чтобы учиться в дороге</h2><p>Я часто путешествую — поезда, самолёты, длинные переезды за город. И всегда хочется использовать это время с пользой: почитать, послушать подкасты или позаниматься самообразованием.</p><p>Проблема в том, что в дороге интернет — штука нестабильная. А многие образовательные платформы используют плеер Kinescope, который не позволяет просто скачать видео для офлайн-просмотра.</p><p>Конечно, можно открыть DevTools, найти в сетевых запросах <code>.m3u8</code> плейлист, скопировать ссылку, запустить FFmpeg в консоли… Но делать это каждый раз, особенно если материалов много — утомительно и отнимает время, которое лучше потратить на учёбу.</p><p>Мне хотелось простого решения: открыл страницу с видео, нажал кнопку — и всё готово для просмотра офлайн.</p><hr><h2 id="Почему-расширение"><a href="#Почему-расширение" class="headerlink" title="Почему расширение?"></a>Почему расширение?</h2><p>Вариантов было несколько: написать парсер, использовать готовый download-менеджер или сделать браузерное расширение.</p><p>Я выбрал расширение, потому что это <strong>самый естественный и удобный способ</strong>:</p><ul><li>Оно работает прямо на странице с видео, не нужно никуда переключаться.</li><li>Может перехватывать сетевые запросы плеера в реальном времени.</li><li>Всё управление — одной кнопкой. Никаких лишних действий.</li></ul><p>В итоге процесс максимально простой: зашёл на страницу лекции, нажал «Сохранить», пошёл дальше. И так для всех материалов, которые планирую посмотреть в дороге.</p><hr><h2 id="Что-получилось"><a href="#Что-получилось" class="headerlink" title="Что получилось"></a>Что получилось</h2><p>Расширение делает две вещи:</p><ol><li><strong>Перехватывает ссылки</strong> на видео- и аудиопотоки, которые запрашивает плеер Kinescope.</li><li><strong>Сохраняет их</strong> в папку <code>Загрузки/kinescope/[Название_видео]/</code> вместе с метаданными (название, длительность, описание).</li></ol><p>На странице появляется плавающая кнопка, которая показывает, сколько потоков найдено. Нажал — и все ссылки уже в папке, аккуратно отсортированные по видео и аудио.</p><p>Дальше я запускаю Python-скрипт, который автоматически склеивает всё через FFmpeg в готовый <code>.mp4</code> файл. И вуаля — библиотека лекций готова к просмотру без интернета.</p><hr><h2 id="Итог"><a href="#Итог" class="headerlink" title="Итог"></a>Итог</h2><p>Теперь подготовка к поездкам занимает пару минут:</p><ol><li>Открываю все нужные видео → на каждой странице нажимаю кнопку.</li><li>Запускаю один скрипт → он обрабатывает всё автоматически.</li><li>Копирую готовые файлы на ноутбук&#x2F;планшет и в путь.</li></ol><p>Никаких DevTools, никакой рутины. Только время, потраченное с пользой.</p><hr><h2 id="Ссылка-на-проект"><a href="#Ссылка-на-проект" class="headerlink" title="Ссылка на проект"></a>Ссылка на проект</h2><p>Расширение открытое, устанавливается вручную через режим разработчика в Chrome. Всё, что нужно — четыре файла из репозитория.</p><p>👉 <strong><a href="https://github.com/optinsoft/KinescopeJSONLDExtractor">Kinescope JSON-LD Extractor PRO на GitHub</a></strong> 👈</p><p>Буду рад, если пригодится и вам.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h2 id=&quot;Как-я-сделал-расширение-для-скачивания-видео-с-Kinescope-чтобы-учиться-в-дороге&quot;&gt;&lt;a</summary>
        
      
    
    
    
    <category term="GitHub" scheme="https://optinsoft.net/blog/categories/GitHub/"/>
    
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="extension" scheme="https://optinsoft.net/blog/tags/extension/"/>
    
    <category term="kinescope" scheme="https://optinsoft.net/blog/tags/kinescope/"/>
    
  </entry>
  
  <entry>
    <title>How to Download a Specific ChromeDriver Version</title>
    <link href="https://optinsoft.net/blog/2026/08/21/How-to-Download-a-Specific-ChromeDriver-Version/"/>
    <id>https://optinsoft.net/blog/2026/08/21/How-to-Download-a-Specific-ChromeDriver-Version/</id>
    <published>2026-08-21T08:44:18.000Z</published>
    <updated>2026-08-21T09:00:42.782Z</updated>
    
    <content type="html"><![CDATA[<p>Sometimes you need a specific version of ChromeDriver rather than the latest one — for example, to match a particular version of Google Chrome.</p><p>For this, you can use <a href="https://googlechromelabs.github.io/chrome-for-testing/">Chrome for Testing</a>, a Chrome flavor specifically designed for web testing and automation.</p><p>The project provides a JSON file containing available versions and download links:</p><p><a href="https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json">known-good-versions-with-downloads.json</a></p><p>For example, let’s say you need ChromeDriver version <code>151.0.7922.138</code>. The JSON contains an entry for this version with download URLs for different platforms:</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;151.0.7922.138&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;revision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1654411&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;downloads&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;chromedriver&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>      <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;platform&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;linux64&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/linux64/chromedriver-linux64.zip&quot;</span>      <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>      <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;platform&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;win64&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/win64/chromedriver-win64.zip&quot;</span>      <span class="hljs-punctuation">&#125;</span>    <span class="hljs-punctuation">]</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>The <code>downloads.chromedriver</code> section contains download links for different platforms: <code>linux64</code>, <code>mac-arm64</code>, <code>mac-x64</code>, <code>win32</code>, and <code>win64</code>.</p><h3 id="Finding-the-URL-using-Chrome-DevTools"><a href="#Finding-the-URL-using-Chrome-DevTools" class="headerlink" title="Finding the URL using Chrome DevTools"></a>Finding the URL using Chrome DevTools</h3><p>The JSON file is quite large, but you don’t need to browse it manually. You can use Chrome DevTools to find the exact download URL.</p><p>Open the JSON file in Chrome, press <code>F12</code>, switch to the <strong>Console</strong> tab, and run:</p><pre><code class="hljs js"><span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-variable language_">document</span>.<span class="hljs-property">body</span>.<span class="hljs-property">innerText</span>).<span class="hljs-property">versions</span>  .<span class="hljs-title function_">find</span>(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x.<span class="hljs-property">version</span> === <span class="hljs-string">&#x27;151.0.7922.138&#x27;</span>)  ?.<span class="hljs-property">downloads</span>.<span class="hljs-property">chromedriver</span>  .<span class="hljs-title function_">find</span>(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x.<span class="hljs-property">platform</span> === <span class="hljs-string">&#x27;win64&#x27;</span>)  ?.<span class="hljs-property">url</span></code></pre><p>The console will return the direct download URL:</p><pre><code class="hljs text">https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/win64/chromedriver-win64.zip</code></pre><p>Simply change the version and platform in the command to get the ChromeDriver you need.</p><p>This approach requires no additional software or online JSON tools — everything can be done directly in Chrome DevTools.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Sometimes you need a specific version of ChromeDriver rather than the latest one — for example, to match a particular version of Google</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="version" scheme="https://optinsoft.net/blog/tags/version/"/>
    
    <category term="download" scheme="https://optinsoft.net/blog/tags/download/"/>
    
    <category term="howto" scheme="https://optinsoft.net/blog/tags/howto/"/>
    
    <category term="chromedriver" scheme="https://optinsoft.net/blog/tags/chromedriver/"/>
    
  </entry>
  
  <entry>
    <title>Как скачать нужную версию ChromeDriver</title>
    <link href="https://optinsoft.net/blog/ru/2026/08/21/How-to-Download-a-Specific-ChromeDriver-Version/"/>
    <id>https://optinsoft.net/blog/ru/2026/08/21/How-to-Download-a-Specific-ChromeDriver-Version/</id>
    <published>2026-08-21T08:44:18.000Z</published>
    <updated>2026-08-24T04:46:48.212Z</updated>
    
    <content type="html"><![CDATA[<p>Иногда требуется не последняя версия ChromeDriver, а конкретная — например, соответствующая определённой версии Google Chrome.</p><p>Для этого можно использовать <a href="https://googlechromelabs.github.io/chrome-for-testing/">Chrome for Testing</a> — специальную версию Chrome, предназначенную для тестирования и автоматизации.</p><p>Проект предоставляет JSON-файл со списком доступных версий и ссылками для загрузки:</p><p><a href="https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json">known-good-versions-with-downloads.json</a></p><p>Например, нам нужна версия ChromeDriver <code>151.0.7922.138</code>. В JSON есть запись для этой версии со ссылками на загрузку для разных платформ:</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span>  <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;151.0.7922.138&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;revision&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1654411&quot;</span><span class="hljs-punctuation">,</span>  <span class="hljs-attr">&quot;downloads&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span>    <span class="hljs-attr">&quot;chromedriver&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>      <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;platform&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;linux64&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/linux64/chromedriver-linux64.zip&quot;</span>      <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span>      <span class="hljs-punctuation">&#123;</span>        <span class="hljs-attr">&quot;platform&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;win64&quot;</span><span class="hljs-punctuation">,</span>        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/win64/chromedriver-win64.zip&quot;</span>      <span class="hljs-punctuation">&#125;</span>    <span class="hljs-punctuation">]</span>  <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">&#125;</span></code></pre><p>В разделе <code>downloads.chromedriver</code> находятся ссылки для разных платформ: <code>linux64</code>, <code>mac-arm64</code>, <code>mac-x64</code>, <code>win32</code> и <code>win64</code>.</p><h3 id="Поиск-URL-с-помощью-Chrome-DevTools"><a href="#Поиск-URL-с-помощью-Chrome-DevTools" class="headerlink" title="Поиск URL с помощью Chrome DevTools"></a>Поиск URL с помощью Chrome DevTools</h3><p>JSON-файл довольно большой, но вручную просматривать его не нужно. Можно воспользоваться Chrome DevTools и сразу получить нужную ссылку.</p><p>Откройте JSON-файл в Chrome, нажмите <code>F12</code>, перейдите на вкладку <strong>Console</strong> и выполните:</p><pre><code class="hljs js"><span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-variable language_">document</span>.<span class="hljs-property">body</span>.<span class="hljs-property">innerText</span>).<span class="hljs-property">versions</span>  .<span class="hljs-title function_">find</span>(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x.<span class="hljs-property">version</span> === <span class="hljs-string">&#x27;151.0.7922.138&#x27;</span>)  ?.<span class="hljs-property">downloads</span>.<span class="hljs-property">chromedriver</span>  .<span class="hljs-title function_">find</span>(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x.<span class="hljs-property">platform</span> === <span class="hljs-string">&#x27;win64&#x27;</span>)  ?.<span class="hljs-property">url</span></code></pre><p>В консоли будет выведен прямой URL для загрузки:</p><pre><code class="hljs text">https://storage.googleapis.com/chrome-for-testing-public/151.0.7922.138/win64/chromedriver-win64.zip</code></pre><p>Чтобы получить другую версию или версию для другой платформы, просто измените соответствующие значения в команде.</p><p>Таким образом, не требуется устанавливать дополнительное программное обеспечение или использовать сторонние онлайн-сервисы — всё можно сделать непосредственно в Chrome DevTools.</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Иногда требуется не последняя версия ChromeDriver, а конкретная — например, соответствующая определённой версии Google</summary>
        
      
    
    
    
    <category term="HowTo" scheme="https://optinsoft.net/blog/categories/HowTo/"/>
    
    
    <category term="chrome" scheme="https://optinsoft.net/blog/tags/chrome/"/>
    
    <category term="version" scheme="https://optinsoft.net/blog/tags/version/"/>
    
    <category term="download" scheme="https://optinsoft.net/blog/tags/download/"/>
    
    <category term="howto" scheme="https://optinsoft.net/blog/tags/howto/"/>
    
    <category term="chromedriver" scheme="https://optinsoft.net/blog/tags/chromedriver/"/>
    
  </entry>
  
</feed>
