Назад в Центр знаний
Начало работы

Прокси Playwright: маршруты и аутентификация

Диагностика маршрутов, исключений контекста и SOCKS5 в Playwright: как отличить два реальных выхода от неизменных сигналов профиля браузера.

BotBrowser Team

Документация

Нужна структурированная документация по теме Начало работы?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

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

TL;DR

  • Докажите маршрут запроса с помощью собственного сервера-источника или наблюдателя до изменения настроек браузера. Успешная загрузка страницы сама по себе не показывает, какой прокси её обработал.
  • Отделяйте настройки запуска по умолчанию от явного прокси контекста. Исключения маршрутизации выбирают прямое соединение, а не переход к другому прокси при ошибке.
  • Документированные HTTP-учётные данные Playwright не добавляют Chromium нативную поддержку паролей SOCKS5. Проверьте границу протокола до диагностики страницы входа.
  • Сравнивайте побайтово одинаковые копии одного закрытого профиля через два проверенных публичных выхода. Стабильные значения браузера представляют собой наблюдения, а не доказательство решения сайта об идентичности.

Содержание

Начните с операции, которая завершилась ошибкой

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

НаблюдениеКакие свидетельства собратьПервое контролируемое изменение
Создание контекста отклоняет учётные данные SOCKS5Ошибка API до навигации; ни одного обращения к listenerПроверить документированную поддержку протокола
HTTP-прокси запрашивает аутентификацию с 407Запрос аутентификации прокси и счётчики принятых пересылокПроверить HTTP-учётные данные этого прокси
Сервер-источник возвращает 401Ответ источника отдельно от запроса аутентификации проксиИзучить аутентификацию этого источника
Страница загружается через неожиданный маршрутСчётчики прокси запуска и контекста, маркер источникаИсправить область конфигурации или исключение
Оба маршрута показывают одинаковый публичный адресОтветы наблюдателя, временные метки и метки маршрутов провайдераПроверить вышестоящую маршрутизацию до сравнения сигналов
Выходы различаются, но значения браузера совпадаютСнимок профиля, исполняемый файл и отдельные показания страницыИзучить параметр браузера, относящийся к требованию

Тайм-аут навигации не является диагнозом. Увеличение тайм-аута page.goto() может помочь с медленной разрешённой конечной точкой, но не добавит способ аутентификации транспорта и не изменит маршрут контекста. Аналогично, ответ 403 от сайта назначения не указывает на неисправный компонент без дополнительных свидетельств. Сохраните ответ и остановитесь в пределах разрешённой диагностики, вместо того чтобы многократно менять несвязанные настройки при обращениях к стороннему ресурсу.

URL прокси и URL назначения отвечают на разные вопросы. http://proxy.test:8080 обозначает подключение к HTTP-прокси. Назначение с https:// может использовать его туннель CONNECT, не превращая сам прокси в HTTPS-прокси. Записывайте обе схемы. Принятый туннель также не подтверждает, какой выход выбрал вышестоящий провайдер. Различия ошибок HTTP, HTTPS и SOCKS5 в Puppeteer объясняют эту терминологию протоколов; отдельные настройки запуска и контекста Playwright добавляют описанную ниже проблему области действия.

Используйте источник, который возвращает однозначный маркер, с лимитом запросов и счётчиком. Источник доказывает получение нужного ответа; счётчик прокси доказывает, какой локальный маршрут его переслал. IP-наблюдатель отвечает на другой вопрос: какой публичный адрес был виден для этого запроса. Раздельный учёт этих трёх показателей не позволяет принять снимок страницы за сетевое доказательство. И прямые, и пересланные запросы могут отобразить один и тот же неизменный HTML источника.

Разделяйте маршруты запуска, контекста и исключения с прямым подключением

Документированный API принимает прокси при запуске браузера и при создании его контекста. Постоянный запуск также принимает настройки прокси вместе с userDataDir. У этих настроек разные сроки действия. Обычный browser.newContext() изолирован и не записывает состояние просмотра в этот каталог. Постоянный контекст использует состояние на диске; его закрытие закрывает и связанный браузер. Выбор варианта только потому, что он встречается в примере, может изменить условия хранения состояния в эксперименте.

Явный прокси контекста означает новый выбор маршрута, а не указание сначала попробовать прокси запуска и использовать прокси контекста при неудаче. Рассматривайте всю конфигурацию контекста как единое целое: сервер, поддерживаемые учётные данные и список исключений. Проверяйте каждое поле в месте создания контекста. Маршрут, собранный в launch(), не определяет содержимое последующего newContext({ proxy }). Решения о настройке браузера Playwright объясняют владение браузером и срок жизни контекста до начала сетевого сравнения.

Точный член proxy.bypass обозначает документированный список исключений маршрутизации, разделённых запятыми. Здесь исключение означает, что подходящий запрос соединяется напрямую, а не через этот настроенный прокси. Оно не означает разрешение доступа со стороны назначения и не определяет идентичность браузера. Исключение запроса из прокси контекста C не доказывает, что его обработал прокси запуска L. Контролируемой матрице нужны отдельные счётчики L и C, чтобы отличить прямое обращение к источнику от каждой пересланной ветви.

Счётчики маршрутов подтверждают выбранный сетевой путь; показания неизменяемого профиля подтверждают только измеренные значения страницы.

В конечном счёте правила маршрутизации интерпретирует браузер, поэтому проверяйте правило с точной формой URL из рабочей нагрузки. Chromium различает простое имя хоста, шаблон домена и URL с IP-адресом в явном виде. Правило <local> относится к простым именам хостов без точки; оно не обозначает любой адрес, который в итоге разрешается в локальную машину. Правило CIDR применяется к явному IP-адресу в URL, а не к имени хоста только потому, что DNS вернул адрес из этой подсети. Сохраняйте написание хоста и порт в записи об ошибке.

Поведение loopback заслуживает отдельного локального случая. У Chromium есть неявные исключения локальных адресов, а Playwright дополнительно строит правила маршрутизации loopback. Реализация 1.63.0 проверяет явные записи loopback перед решением о добавлении своего модификатора. Нельзя переносить старый результат только из-за неизменного написания опции. Ни общее утверждение о том, что localhost всегда подключается напрямую, ни неизмеренное предположение об области действия не являются достаточным свидетельством. Проверьте счётчики пересылки для 127.0.0.1, указав установленную версию; недокументированные переменные среды и дополнительные флаги резолвера не нужны.

Справочник настройки прокси BotBrowser определяет сетевые параметры продукта. Эти параметры не заменяют область действия объектов и правила объединения самого Playwright. При смене библиотеки автоматизации осознанно переносите модель конфигурации, вместо того чтобы копировать внешне похожий объект в предположении, что семантика маршрутизации сохранится.

Воспроизведите маршрут до изменения браузера

Локальный fixture ошибки исключает поведение сайта назначения из диагностики. Запустите собственный HTTP-источник на динамически назначенном порту loopback и прокси, пересылающий запросы только этому источнику. Используйте уникальный маркер ответа и учитывайте запросы аутентификации, принятые пересылки и прямые обращения к источнику. Все остальные назначения прокси должен отклонять. Открытый универсальный пересыльщик не нужен для воспроизведения ошибки HTTP-учётных данных.

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

У SOCKS5 есть собственная граница. Зафиксированная реализация Playwright 1.63.0 отклоняет опции имени пользователя и пароля прокси для SOCKS5-пути Chromium. Ошибка в этом случае может произойти ещё до получения соединения SOCKS-listener. Это не ошибка входа на сайт, а перенос учётных данных в HTTP-заголовки источника не добавляет отсутствующую процедуру SOCKS. Listener SOCKS без аутентификации и попытка настройки с учётными данными проверяют разные утверждения. Локальный адаптер, который аутентифицируется перед вышестоящим сервисом SOCKS, представляет другую архитектуру, и её следует обозначать соответствующим образом.

Области маршрутов тоже нужны независимые случаи. Назначьте разные счётчики прокси запуска L и прокси контекста C. Сначала установите маршрут запуска по умолчанию, затем создайте контекст с явным маршрутом C, после чего проверьте исключение с прямым подключением в этом контексте C. Маркер источника оставляйте неизменным. Иначе изменённая страница может скрыть изменённый маршрут, и будет неясно, какая правка вызвала кажущееся исправление. Один лишь счётчик источника не различает L и C: оба прокси могут переслать одинаковое содержимое.

Задачи браузера должны использовать ограниченный пул, а не последовательно выполняться в меняющихся условиях машины. Fixture принимает --pool и --workers, по умолчанию запрашивает 20 и измеряет доступные CPU и память перед выбором фактического параллелизма. Большее число запрошенных мест не создаёт дополнительных независимых случаев. Если внешнее сравнение не может поддерживать два одновременных запуска браузера, остановитесь до запросов к наблюдателю; запуск A сейчас, а B значительно позже ослабляет контролируемое сравнение.

Наблюдаемые локальные результаты и исполняемый fixture

Контролируемый запуск использовал Playwright 1.63.0, стандартный Chrome 154.0.8037.57, Node 20.20.2 и Ubuntu 24.04.5 LTS 11 ноября 2026 года по времени Торонто. Публичный HTTP-fixture выполнил шесть задач одновременно: оценка в 31.88 свободного ядра CPU и 121726328832 байта доступной памяти позволяла выполнить все шесть. Запрошенный пул составлял 20 и ограничивался числом случаев, а не произвольным последовательным лимитом. Три отдельных случая SOCKS тоже выполнялись одновременно. Эта изолированная среда с собственным источником, запущенная от root, использовала значение Playwright по умолчанию chromiumSandbox: false; это условие не является рекомендацией для промышленного развёртывания.

Контролируемый случайНаблюдаемый результатСчётчик, подтверждающий результат
HTTP при запуске, правильные учётные данныеМаркер источника через LПересылки L 1; пересылки C 0
HTTP при запуске, неправильные учётные данныеНет маркера источникаЗапросы аутентификации L 2; пересылки 0
HTTP при запуске, учётные данные отсутствуютНет маркера источникаЗапросы аутентификации L 1; пересылки 0
Запуск явно исключает 127.0.0.1Маркер прямого обращения к источникуПрямые обращения 1; пересылки L/C 0
Контекст C заменяет исключённый маршрут запуска LМаркер источника через CПересылки C 1; пересылки L 0
Контекст C явно исключает 127.0.0.1Маркер прямого обращения к источникуПрямые обращения 1; пересылки L/C 0
SOCKS5 без учётных данныхМаркер источникаМаршрутизировано 1; обращений к источнику 1
Учётные данные SOCKS5 при запускеОтказ APIСоединений с listener 0; обращений к источнику 0
Учётные данные SOCKS5 в контекстеОтказ APIСоединений с listener 0; обращений к источнику 0

Это измеренные счётчики, а не универсальное число повторных попыток. Неправильные HTTP-учётные данные вызвали net::ERR_HTTP_RESPONSE_CODE_FAILURE; отсутствующие данные вызвали net::ERR_INVALID_AUTH_CREDENTIALS. Оба случая с учётными данными SOCKS вернули Browser does not support socks5 proxy authentication. Локальные прокси также отклоняли посторонние фоновые попытки подключения. Успешной пересылкой считается только запрос приложения с уникальным маркером. Результат loopback в 1.63.0 соответствует зафиксированной реализации; более ранний исследовательский результат 1.49.0 отличался и не подставляется вместо этого запуска.

Сохраните следующий полный, исключительно локальный пример как playwright-local-proxy.mjs рядом с установленным пакетом [email protected]. Задайте в CHROME путь к исполняемому файлу стандартного Chrome. Проверенный Linux-хост предоставлял DISPLAY=:88; браузер работал в режиме headless. Вышестоящие учётные данные и внешняя цель не нужны. Скрипт отклоняет посторонние назначения прокси, освобождает ресурсы, проверяет CPU и память и завершается с ошибкой при нарушении проверок или доступности менее двух мест.

CHROME=/opt/google/chrome/chrome node playwright-local-proxy.mjs --pool 20
import assert from 'node:assert/strict';
import { randomBytes } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';

const require = createRequire(process.env.PLAYWRIGHT_RESOLVE_FROM || import.meta.url);
const { chromium } = require('playwright');
const playwrightVersion = require('playwright/package.json').version;
const expectedVersion = process.env.EXPECTED_PLAYWRIGHT_VERSION || '1.63.0';
assert.equal(playwrightVersion, expectedVersion, `Install playwright@${expectedVersion} beside this script`);
const CHROME = process.env.CHROME;
const flags = [
  '--password-store=basic',
  '--use-mock-keychain',
  '--disable-background-networking',
  '--disable-component-update',
  '--disable-sync',
  '--no-first-run',
  '--no-default-browser-check',
];
const safeError = error => ({
  name: error.name,
  networkCode: String(error.message).match(/net::ERR_[A-Z_]+/)?.[0] || null,
});
export async function resources() {
  const sample = () =>
    os.cpus().reduce(
      (sum, cpu) => ({
        idle: sum.idle + cpu.times.idle,
        total: sum.total + Object.values(cpu.times).reduce((a, b) => a + b, 0),
      }),
      { idle: 0, total: 0 }
    );
  const before = sample();
  await new Promise(resolve => setTimeout(resolve, 250));
  const after = sample();
  const unusedCpuCores = (os.cpus().length * (after.idle - before.idle)) / Math.max(1, after.total - before.total);
  let availableMemoryBytes = os.freemem();
  if (process.platform === 'darwin') {
    const vm = execFileSync('vm_stat', { encoding: 'utf8' });
    const pageSize = Number(vm.match(/page size of (\d+) bytes/)[1]);
    availableMemoryBytes = [...vm.matchAll(/^(?:Pages free|Pages inactive|Pages speculative):\s+(\d+)\./gm)].reduce(
      (sum, match) => sum + Number(match[1]) * pageSize,
      0
    );
  } else if (process.platform === 'linux') {
    availableMemoryBytes =
      Number((await fs.readFile('/proc/meminfo', 'utf8')).match(/^MemAvailable:\s+(\d+)/m)[1]) * 1024;
  }
  return {
    capturedAt: new Date().toISOString(),
    cpuCount: os.cpus().length,
    unusedCpuCores: Number(unusedCpuCores.toFixed(2)),
    availableMemoryBytes,
  };
}
export function requestedPool(argv = process.argv.slice(2)) {
  const index = argv.findIndex(value => /^--?(pool|workers)$/i.test(value));
  const requested = index < 0 ? 20 : Number(argv[index + 1]);
  assert.ok(Number.isInteger(requested) && requested > 0, 'Pool must be a positive integer');
  return requested;
}
export function capacity(snapshot, requested, jobs) {
  const cpuSlots = Math.floor(snapshot.unusedCpuCores);
  const memorySlots = Math.floor(snapshot.availableMemoryBytes / 1024 ** 3);
  return {
    requested,
    jobs,
    effective: Math.min(requested, jobs, Math.max(0, cpuSlots), Math.max(0, memorySlots)),
    cpuSlots,
    memorySlots,
    reason: 'One estimated idle CPU core and 1 GiB per browser; at least two concurrent slots required',
  };
}
export async function pool(jobs, size) {
  let next = 0;
  const results = [];
  await Promise.all(
    Array.from({ length: size }, async () => {
      while (next < jobs.length) {
        const index = next++;
        results[index] = await jobs[index]();
      }
    })
  );
  return results;
}
export async function listen(server) {
  const sockets = new Set();
  server.on('connection', socket => {
    sockets.add(socket);
    socket.on('close', () => sockets.delete(socket));
    socket.on('error', () => {});
  });
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', resolve);
  });
  return {
    port: server.address().port,
    async close() {
      for (const socket of sockets) socket.destroy();
      await new Promise(resolve => server.close(resolve));
    },
  };
}
export async function httpCase(spec) {
  let origin, left, right, browser;
  const marker = randomBytes(12).toString('hex');
  const counts = { origin: { direct: 0, L: 0, C: 0 }, originOtherRequests: 0 };
  const result = { case: spec.name, startedAt: new Date().toISOString(), expectedRoute: spec.expected };
  try {
    origin = await listen(
      http.createServer((req, res) => {
        if (req.url !== '/') {
          counts.originOtherRequests++;
          res.writeHead(404).end();
          return;
        }
        const route = req.headers['x-local-route'] || 'direct';
        counts.origin[route]++;
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(`<p id="marker">${marker}</p><p id="route">${route}</p>`);
      })
    );
    const target = `http://127.0.0.1:${origin.port}/`;
    async function proxyServer(label) {
      const counter = (counts[label] = { requests: 0, challenges: 0, accepted: 0, forwarded: 0, denied: 0 });
      const expected = `Basic ${Buffer.from('local-user:local-password').toString('base64')}`;
      const server = http.createServer((req, res) => {
        if (req.url !== target) {
          counter.denied++;
          res.writeHead(403).end();
          return;
        }
        counter.requests++;
        if (req.headers['proxy-authorization'] !== expected) {
          counter.challenges++;
          res.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="owned-local-test"' }).end();
          return;
        }
        counter.accepted++;
        counter.forwarded++;
        const upstream = http.request(
          {
            host: '127.0.0.1',
            port: origin.port,
            path: '/',
            headers: { Host: `127.0.0.1:${origin.port}`, 'X-Local-Route': label },
          },
          remote => {
            res.writeHead(remote.statusCode, remote.headers);
            remote.pipe(res);
          }
        );
        upstream.on('error', () => {
          if (!res.headersSent) res.writeHead(502);
          res.end();
        });
        req.pipe(upstream);
      });
      server.on('connect', (req, socket) => {
        counter.denied++;
        socket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
      });
      return listen(server);
    }
    left = await proxyServer('L');
    right = await proxyServer('C');
    function settings(port, excluded, credential = 'correct') {
      const proxy = { server: `http://127.0.0.1:${port}` };
      if (credential !== 'absent') {
        proxy.username = 'local-user';
        proxy.password = credential === 'wrong' ? 'incorrect' : 'local-password';
      }
      if (excluded) proxy.bypass = '127.0.0.1';
      return proxy;
    }
    browser = await chromium.launch({
      executablePath: CHROME,
      headless: true,
      proxy: settings(left.port, spec.launchExcluded, spec.credential),
      args: flags,
      timeout: 25000,
    });
    result.browserVersion = browser.version();
    const context = await browser.newContext(
      spec.contextProxy ? { proxy: settings(right.port, spec.contextExcluded) } : {}
    );
    await context.route('**/*', route => (route.request().url() === target ? route.continue() : route.abort()));
    const page = await context.newPage();
    let response = null,
      navigationError = null;
    try {
      response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 12000 });
    } catch (error) {
      navigationError = safeError(error);
    }
    let content = '',
      contentReadError = null;
    try {
      content = await page.content();
    } catch (error) {
      contentReadError = {
        ...safeError(error),
        pageChangingAfterFailure: /Unable to retrieve content/.test(String(error.message)),
      };
    }
    const markerObserved = content.includes(marker);
    const observedRoute = markerObserved ? await page.locator('#route').textContent() : null;
    Object.assign(result, {
      navigationStatus: response?.status() ?? null,
      navigationError,
      contentReadError,
      markerObserved,
      observedRoute,
    });
    assert.equal(observedRoute, spec.expected);
    for (const label of ['direct', 'L', 'C']) assert.equal(counts.origin[label], label === spec.expected ? 1 : 0);
    for (const label of ['L', 'C']) assert.equal(counts[label].forwarded, label === spec.expected ? 1 : 0);
    if (spec.expected) assert.equal(response.status(), 200);
    else {
      assert.ok(counts.L.challenges > 0);
      assert.equal(counts.L.accepted, 0);
    }
    return { ...result, status: 'passed', counts };
  } catch (error) {
    return { ...result, status: 'failed', error: safeError(error), counts };
  } finally {
    if (browser) await browser.close();
    if (left) await left.close();
    if (right) await right.close();
    if (origin) await origin.close();
  }
}
export const cases = [
  { name: 'launch-http-correct', expected: 'L' },
  { name: 'launch-http-wrong', credential: 'wrong', expected: null },
  { name: 'launch-http-absent', credential: 'absent', expected: null },
  { name: 'launch-loopback-exclusion', launchExcluded: true, expected: 'direct' },
  { name: 'context-overrides-launch-exclusion', launchExcluded: true, contextProxy: true, expected: 'C' },
  {
    name: 'context-loopback-exclusion-no-launch-fallback',
    contextProxy: true,
    contextExcluded: true,
    expected: 'direct',
  },
];
export async function main() {
  assert.ok(CHROME, 'Set CHROME to the browser executable');
  assert.ok(
    !process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK,
    'Use Playwright default loopback handling'
  );
  const snapshot = await resources(),
    execution = capacity(snapshot, requestedPool(), cases.length);
  if (execution.effective < 2) {
    const output = {
      passed: false,
      status: 'resource-preflight-stopped',
      playwrightVersion,
      resources: snapshot,
      execution,
      cases: [],
      externalRequests: 0,
    };
    console.log(JSON.stringify(output, null, 2));
    process.exitCode = 1;
    return output;
  }
  let active = 0,
    maximumActive = 0;
  const results = await pool(
    cases.map(spec => async () => {
      maximumActive = Math.max(maximumActive, ++active);
      try {
        return { ...(await httpCase(spec)), finishedAt: new Date().toISOString() };
      } finally {
        active--;
      }
    }),
    execution.effective
  );
  assert.ok(maximumActive >= 2, 'The local matrix must run concurrently');
  const output = {
    passed: results.every(result => result.status === 'passed'),
    playwrightVersion,
    expectedVersion,
    display: process.env.DISPLAY || null,
    sandbox: 'Playwright default chromiumSandbox:false; isolated owned-origin test',
    forcedLoopbackHandlingDisabled: false,
    resources: snapshot,
    execution,
    maximumActive,
    cases: results,
    scope:
      'Loopback HTTP authentication and observed launch/context routing in this pinned runtime; no external request',
  };
  console.log(JSON.stringify(output, null, 2));
  if (!output.passed) process.exitCode = 1;
  return output;
}
if (process.argv[1] && import.meta.url === pathToFileURL(await fs.realpath(process.argv[1])).href) {
  main().catch(error => {
    console.error(JSON.stringify({ passed: false, error: safeError(error) }));
    process.exitCode = 1;
  });
}

Сравните два реальных выхода из одного закрытого профиля

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

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

Сохраняйте неизменными исполняемый файл браузера, операционную систему, режим запуска, viewport и любые явные настройки локали. Меняйте только разрешённый маршрут между постоянными контекстами A и B, созданными через launchPersistentContext(). Запускайте обе ветви вместе, один раз переходите каждой к собственному наблюдателю и считывайте значения браузера на той же странице. Это задаёт область наблюдения: эти два запроса, эти два клонированных входа и эта среда. Это не определяет все маршруты, которые браузер может использовать за время работы.

Cloudflare документирует конечную точку /cdn-cgi/trace для каждого домена как средство диагностики. Используемый здесь наблюдатель находится на botbrowser.io с явными лимитами запросов; посторонний детектор, поисковая система или целевой сайт не нужны. Полная трассировка может содержать исходный адрес, поэтому сравнивайте и проверяйте его в памяти. Сохраняйте в свидетельствах хеш с временным ключом, уникальным для запуска, вместо публикации адресов или средства их повторного поиска. Метка маршрута описывает разрешённую конфигурацию, а не гарантирует физическое местоположение.

Аутентифицированные вышестоящие SOCKS-сервисы требуют точного обозначения. Ограниченный локальный адаптер HTTP CONNECT может аутентифицироваться в вышестоящем сервисе, пока браузер общается с адаптером по обычному протоколу HTTP-прокси. Успешный запрос через него доказывает, что составной путь сработал для разрешённого назначения. Он не отменяет ограничение нативных паролей SOCKS5 в Chromium, и его нельзя описывать как принятие Playwright пары имени пользователя и пароля SOCKS5. Записывайте счётчики туннелей адаптера отдельно от значений страницы и ответов наблюдателя.

Измеренное сравнение двух выходов

Одна и та же зафиксированная среда Ubuntu одновременно выполнила обе внешние ветви браузера. Их переходы к наблюдателю начались в 2026-11-12T00:01:38.546Z, когда в Торонто ещё было 11 ноября. Каждая сделала один принятый запрос к собственной конечной точке трассировки, получила HTTP 200 и прошла проверку публичного адреса. Два принятых адреса различались. Ограниченные назначением адаптеры разрешили по одному вышестоящему туннелю; дополнительные попытки подключения отклонялись локально. Внешних повторных запросов не было. До этих запросов четыре локальные проверки клиента адаптера охватили успешную аутентификацию, отклонённые учётные данные, некорректную процедуру согласования и запрещённое назначение; все прошли, а в трёх отрицательных случаях пересылок было ноль.

Записанная величинаМаршрут AМаршрут B
HTTP-статус наблюдателя / принятые запросы200 / 1200 / 1
HMAC публичного адреса, первые 12 символовbf448a3168c12ef6c2495efc
Записи закрытого профиля / префикс начального хеша134 / cb216f5343b0134 / cb216f5343b0
Часовой поясAmerica/TorontoAmerica/Toronto
Разрешённая локаль Intlen-USen-US
navigator.languagesen-US, enen-US, en
SHA-256 canvas, первые 12 символовa37a4010499aa37a4010499a
Сравнение user agentОдинаковыеОдинаковые
Кандидаты ICE типа host при iceServers: []1; сбор завершён1; сбор завершён

Полный user agent обеих ветвей был Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36. Для сравнения canvas рисовался canvas размером 160 × 50 с фоном #e5ebf3, цветом текста #17243a, шрифтом 16px Arial и строкой Proxy fixture 2026 в точке (5, 30), затем к canvas.toDataURL() применялся SHA-256. Именно эти входные данные определяют хеш; он не является общей оценкой отпечатка.

Обе копии из 134 записей совпадали с закрытым входом до запуска, а хеш исходного входа после него остался неизменным. Равенство после проверенной смены выхода показывает, что эти записанные значения страницы не последовали автоматически за маршрутами в данном запуске. Оно не доказывает проверку всего состояния браузера. Cookies, разрешения, service workers, хранилища и последующая сетевая активность могут различаться, не отражаясь в этих показаниях. Счётчики ICE только типа host не измеряют равенство адресов кандидатов или трафик STUN/TURN. Ни одинаковые сигналы, ни разные выходы не устанавливают решение сайта о связывании учётных записей.

Оценивайте каждое значение браузера в пределах его смысла

Часовой пояс поступает из среды браузера, доступной через Intl.DateTimeFormat().resolvedOptions(), а не напрямую из адреса в ответе трассировки. Маршрут с меткой одной страны может сосуществовать с другим часовым поясом, потому что параметры браузера и вышестоящий маршрут независимы. Законное требование теста может задавать одинаковый часовой пояс для всех маршрутов или объявленную региональную среду. Сначала запишите это требование: совпадение с базой IP не является универсальным условием корректности.

navigator.languages, разрешённая локаль Intl и языковой HTTP-заголовок связаны, но представляют разные наблюдения. Записывайте фактический ответ страницы, а если важны заголовки, отдельно фиксируйте их на собственном сервере. Не выводите все три значения из одной опции локали. Настройка контекста служит входным параметром эксперимента: её смена вместе с прокси не позволяет приписать различие только маршруту. Такой параметр может пригодиться в следующем тесте после завершения сравнения с одной переменной.

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

WebRTC требует особенно узкой формулировки. Соединение между узлами с iceServers: [] наблюдает сбор локальных кандидатов host в этой конфигурации. Типы и количество кандидатов можно сохранить без раскрытия адресов. Это не задействует STUN или TURN, не доказывает прохождение UDP через HTTP-прокси и не устанавливает отсутствие утечки. Отсутствие собранного кандидата также не является общим вердиктом о сети. Проверяйте любое более сильное требование через отдельно разрешённую конечную точку и план наблюдений для конкретного протокола.

User agent представляет собой ещё один вход, который нужно записать вместе с точным исполняемым файлом. Совпадающая UA не доказывает совпадение ClientHello, HTTP/2 или всех API страницы. И наоборот, различие UA нельзя приписывать ротации прокси без проверки версии браузера и параметров запуска. Сохраняйте сравнения на уровне, который подтверждает измерение. Сайт может объединять сигналы, не измерявшиеся локальной страницей; ни один результат здесь не подтверждает его решение о риске или связывании учётных записей.

Справочник BotBrowser по согласованию прокси и геолокации документирует поддерживаемые параметры среды. Он помогает воспроизвести объявленную тестовую конфигурацию, но не выбирает правильную региональную политику приложения и не проверяет вышестоящие учётные данные. Исправляйте ошибку HTTP-аутентификации на уровне прокси; неверный маршрут контекста исправляйте там, где этот контекст настроен; меняйте значения браузера только при явном требовании теста. Так сохраняется причинная ценность следующего сравнения.

Источники

Практический вывод

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

Вопросы и ответы

Может ли контекст унаследовать состояние браузера только потому, что использует прокси запуска?

Наследование маршрута и сохранение состояния являются разными свойствами. Обычный newContext() создаёт изолированный непостоянный контекст даже при использовании настроек запуска по умолчанию. Постоянный запуск привязывает один контекст к каталогу на диске. Для эксперимента с одинаковым входом используйте закрытый снимок и отдельные записываемые копии; общий адрес прокси не доказывает совместное использование cookies или хранилища.

Отправляет ли прямое исключение неудачный запрос к прокси запуска в качестве запасного пути?

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

Становится ли измерение менее полезным, если скрыть адреса в отчёте?

Корректность публичных адресов и неравенство A/B можно проверить до удаления исходных значений. Временный хеш с отдельным ключом запуска позволяет показать в отчёте различие принятых ответов без сохранения адреса. Секретный ключ не публикуется и не используется повторно, а запись сама по себе не устанавливает физическую страну или последующую маршрутизацию любого выхода провайдера.

#Playwright#Аутентификация Прокси#SOCKS5#Идентичность Браузера

Эта статья входит в руководство: Начало работы с автоматизацией браузера: карта

Выполните ту же проверку в BotBrowser

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