Proxy Playwright: rutas, autenticación y señales
Diagnostica rutas, exclusiones de contexto y autenticación SOCKS5 en Playwright; distingue dos salidas reales de las señales del perfil que permanecen iguales.
BotBrowser Team
Quieres la documentación estructurada de Primeros pasos?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
Un proxy de Playwright puede enviar una petición por la salida prevista mientras la zona horaria, el idioma y el canvas permanecen iguales. Comprueba por separado la ruta y el entorno del navegador: un proxy operativo no define un perfil nuevo. BotBrowser admite entradas declaradas de perfil, proxy, zona horaria, configuración regional e idioma, pero no puede hacer que una ruta ascendente acepte credenciales ni garantizar cómo interpretará esas entradas el destino.
TL;DR
- Demuestra la ruta de la petición con un origen u observador propio antes de cambiar los ajustes del navegador. Que una página cargue correctamente no determina qué proxy la gestionó.
- Separa los valores predeterminados del lanzamiento del proxy explícito de un contexto. Las exclusiones de ruta seleccionan una conexión directa; no recurren a otro proxy como alternativa.
- Las credenciales HTTP documentadas por Playwright no proporcionan a Chromium compatibilidad nativa con contraseñas SOCKS5. Comprueba el límite del protocolo antes de investigar una página de inicio de sesión.
- Compara copias idénticas byte a byte de un perfil cerrado a través de dos salidas públicas validadas. Los valores estables del navegador son observaciones, no pruebas de cómo decide el destino la identidad.
Contenido
- Empieza por la operación que falló
- Separa las rutas de lanzamiento, de contexto y las exclusiones directas
- Reproduce la ruta antes de cambiar el navegador
- Compara dos salidas reales desde un perfil cerrado
- Interpreta cada valor del navegador según su alcance
- Conclusión práctica
Empieza por la operación que falló
Conserva la primera operación fallida y su respuesta. Un fallo al crear un contexto no es lo mismo que un proxy rechazando una conexión; ambos ocurren antes de que un sitio web rechace una acción de cuenta. Para diagnosticar playwright proxy, el registro inicial más útil contiene el esquema del proxy, el ámbito de la configuración, el esquema del destino y el primer error. Sustituye las credenciales por una etiqueta antes de compartir registros: un mensaje de error puede incluir la dirección del servidor o una URL con secretos incorporados.
| Observación | Evidencia que recopilar | Primer cambio controlado |
|---|---|---|
| La creación del contexto rechaza las credenciales SOCKS5 | Error de API antes de navegar; cero conexiones al listener | Comprobar la compatibilidad documentada del protocolo |
El proxy HTTP solicita autenticación con 407 | Contadores de solicitudes de autenticación y reenvíos aceptados | Comprobar las credenciales HTTP de ese proxy |
Un origen devuelve 401 | Respuesta del origen, separada del desafío del proxy | Examinar la autenticación de ese origen |
| La página carga por una ruta inesperada | Contadores de los proxies de lanzamiento y contexto, y marcador del origen | Corregir el ámbito de configuración o la exclusión |
| Ambas rutas muestran la misma dirección pública | Respuestas del observador, marcas de tiempo y etiquetas de ruta del proveedor | Validar la ruta ascendente antes de comparar señales |
| Las salidas difieren, pero los valores del navegador coinciden | Instantánea del perfil, binario y lecturas individuales de la página | Examinar la entrada del navegador relevante para el requisito |
Un tiempo de espera agotado al navegar no constituye un diagnóstico. Aumentar el tiempo de espera de page.goto() puede ayudar con un endpoint autorizado lento, pero no puede añadir un método de autenticación de transporte ni cambiar la ruta de un contexto. Del mismo modo, un 403 del destino no identifica el componente que falló sin más evidencia. Conserva la respuesta y respeta el límite autorizado del diagnóstico, en lugar de cambiar repetidamente ajustes sin relación contra un tercero.
La URL del proxy y la URL del destino responden a preguntas distintas. http://proxy.test:8080 designa una conexión a un proxy HTTP. Un destino https:// puede viajar por su túnel CONNECT sin convertir al propio proxy en un proxy HTTPS. Registra ambos esquemas. La aceptación de un túnel tampoco certifica qué salida seleccionó el proveedor ascendente. Las diferencias entre fallos HTTP, HTTPS y SOCKS5 en Puppeteer cubren ese vocabulario de protocolos; la configuración separada de lanzamiento y contexto de Playwright añade el problema de ámbito siguiente.
Utiliza un origen que devuelva un marcador inequívoco, con un presupuesto de peticiones y un contador. El origen demuestra que llegó la respuesta prevista; el contador del proxy demuestra qué ruta local la reenvió. Un observador de IP responde a otra pregunta: la dirección pública visible en esa petición. Mantener separados estos tres registros evita tratar una captura de página como evidencia de red. Tanto las peticiones directas como las reenviadas pueden mostrar el mismo HTML del origen.
Separa las rutas de lanzamiento, de contexto y las exclusiones directas
La API documentada acepta un proxy al lanzar el navegador y al crear un contexto. Un lanzamiento persistente también acepta ajustes de proxy junto con su userDataDir. Estas configuraciones tienen duraciones distintas. Un browser.newContext() normal está aislado y no escribe su estado de navegación en ese directorio. Un contexto persistente utiliza estado almacenado en disco; cerrarlo cierra también su navegador asociado. Elegir uno porque un ejemplo lo utiliza puede cambiar las condiciones de almacenamiento del experimento.
Un proxy explícito de contexto es una nueva elección de ruta, no una orden de probar primero el proxy de lanzamiento y usar el del contexto si falla. Trata toda la configuración del contexto como una unidad: servidor, credenciales compatibles y lista de exclusiones. Comprueba cada campo donde se crea ese contexto. Una ruta montada en launch() no determina lo que suministró un newContext({ proxy }) posterior. Las decisiones de configuración del navegador con Playwright explican la propiedad del navegador y la duración del contexto antes de iniciar la comparación de red.
El miembro exacto proxy.bypass designa la lista documentada de exclusiones de ruta separadas por comas. Aquí, una exclusión significa que una petición coincidente se conecta directamente en lugar de pasar por ese proxy configurado. No significa que el destino conceda acceso ni define una identidad del navegador. Que una petición quede excluida del proxy de contexto C no demuestra que la gestionara el proxy de lanzamiento L. La matriz controlada necesita contadores L y C separados para distinguir una llegada directa al origen de cualquiera de las rutas reenviadas.
El navegador interpreta finalmente sus reglas de ruta, así que comprueba cada regla con la forma exacta de URL que utiliza la carga de trabajo. Chromium distingue un nombre de host simple, un patrón de dominio y una URL con IP literal. Su regla <local> se refiere a nombres de host simples sin punto; no es sinónimo de toda dirección que termina resolviéndose a una máquina local. Una regla CIDR se aplica a una IP literal en la URL, no a un nombre de host solo porque el DNS devuelva una dirección de esa subred. Conserva la escritura del host y el puerto en el registro del fallo.
El comportamiento de loopback merece un caso local explícito. Chromium tiene exclusiones implícitas para direcciones locales, mientras Playwright también construye reglas de ruta de loopback. La implementación 1.63.0 comprueba las entradas explícitas de loopback antes de decidir si añade su modificador. Un resultado anterior no puede trasladarse solo porque la opción conserve el mismo nombre. Ni afirmar de forma general que localhost siempre conecta directamente ni suponer un ámbito sin medirlo constituye evidencia suficiente. Comprueba los contadores de reenvío en 127.0.0.1, registrando la versión instalada; no hacen falta variables de entorno no documentadas ni opciones adicionales del resolvedor.
La referencia de configuración de proxy de BotBrowser define las entradas de red del producto. Esas entradas no sustituyen el ámbito de objetos ni las reglas de combinación de Playwright. Al cambiar de biblioteca de automatización, adapta deliberadamente el modelo de configuración en lugar de copiar un objeto de aspecto parecido y suponer que conserva la misma semántica de ruta.
Reproduce la ruta antes de cambiar el navegador
Un fixture local del fallo elimina el comportamiento del destino del diagnóstico. Inicia un origen HTTP propio en un puerto de loopback asignado dinámicamente y un proxy que solo reenvíe a ese origen. Utiliza un marcador único de respuesta y registra los desafíos de autenticación, los reenvíos aceptados y las peticiones directas al origen. El proxy debe rechazar todos los demás destinos. Un reenviador abierto de uso general no es necesario para reproducir un error de credenciales HTTP.
Para la autenticación HTTP, ejecuta como casos separados credenciales correctas, ausentes e incorrectas. El caso satisfactorio debe demostrar tanto el reenvío como el marcador del origen. Los casos rechazados deben demostrar que no se alcanzó el origen, en lugar de limitarse a coincidir con una cadena de error conveniente. Los reintentos del navegador pueden producir más de un desafío, por lo que conviene afirmar la propiedad relevante, como cero reenvíos aceptados, y no suponer un número universal de intentos de socket. No trates estos resultados HTTP locales como una prueba de la política de autenticación de un proveedor ascendente.
SOCKS5 necesita su propio límite. La implementación fijada de Playwright 1.63.0 rechaza las opciones de usuario y contraseña del proxy para la ruta SOCKS5 de Chromium. En ese caso, el fallo puede producirse antes de que el listener SOCKS reciba una conexión. No es un fallo de inicio de sesión del sitio web, y mover las credenciales a cabeceras HTTP del origen no añade la negociación SOCKS ausente. Un listener SOCKS sin autenticación y un intento de configuración con credenciales comprueban afirmaciones distintas. Un adaptador local que se autentica ante un servicio SOCKS ascendente es otra arquitectura y debe etiquetarse como tal.
El ámbito de la ruta también necesita casos independientes. Asigna contadores distintos al proxy de lanzamiento L y al proxy de contexto C. Establece primero el valor predeterminado de lanzamiento, después crea un contexto con una ruta C explícita y, por último, comprueba una exclusión directa en ese contexto C. Mantén el mismo marcador del origen. De lo contrario, una página distinta puede ocultar una ruta distinta y dejar sin aclarar qué modificación causó la aparente solución. El contador del origen por sí solo no distingue L de C, porque ambos proxies pueden reenviar el mismo contenido.
Los trabajos del navegador deben utilizar un pool acotado, en lugar de ejecutarse en serie mientras cambian las condiciones de la máquina. El fixture acepta --pool y --workers, solicita 20 por defecto y mide la CPU y la memoria disponibles antes de elegir el paralelismo real. Solicitar más plazas no crea más casos independientes. Si una comparación externa no puede sostener dos ejecuciones simultáneas del navegador, detente antes de hacer las peticiones al observador; ejecutar A ahora y B mucho después debilita la comparación controlada.
Resultados locales observados y un fixture ejecutable
La ejecución controlada utilizó Playwright 1.63.0, Chrome estándar 154.0.8037.57, Node 20.20.2 y Ubuntu 24.04.5 LTS el 11 de noviembre de 2026, hora de Toronto. El fixture HTTP público ejecutó seis trabajos simultáneos: 31.88 núcleos de CPU estimados como inactivos y 121726328832 bytes de memoria disponible permitieron los seis. El pool solicitado fue 20, limitado por el número de casos y no por un límite serial arbitrario. Los tres casos SOCKS separados también se ejecutaron simultáneamente. Este entorno aislado con origen propio, ejecutado como root, usó el valor predeterminado de Playwright chromiumSandbox: false; esa condición no es una recomendación para desplegar en producción.
| Caso controlado | Resultado observado | Contador que demuestra el resultado |
|---|---|---|
| HTTP de lanzamiento, credenciales correctas | Marcador del origen a través de L | L reenviadas 1; C reenviadas 0 |
| HTTP de lanzamiento, credenciales incorrectas | Sin marcador del origen | L desafíos 2; reenviadas 0 |
| HTTP de lanzamiento, credenciales ausentes | Sin marcador del origen | L desafíos 1; reenviadas 0 |
El lanzamiento excluye explícitamente 127.0.0.1 | Marcador directo del origen | Origen directo 1; L/C reenviadas 0 |
| El contexto C sustituye al lanzamiento L excluido | Marcador del origen a través de C | C reenviadas 1; L reenviadas 0 |
El contexto C excluye explícitamente 127.0.0.1 | Marcador directo del origen | Origen directo 1; L/C reenviadas 0 |
| SOCKS5 sin credenciales | Marcador del origen | Enrutadas 1; llegadas al origen 1 |
| Credenciales SOCKS5 en el lanzamiento | Rechazo de la API | Conexiones al listener 0; llegadas al origen 0 |
| Credenciales SOCKS5 en el ámbito del contexto | Rechazo de la API | Conexiones al listener 0; llegadas al origen 0 |
Son recuentos medidos, no números universales de reintentos. Las credenciales HTTP incorrectas produjeron net::ERR_HTTP_RESPONSE_CODE_FAILURE; las ausentes produjeron net::ERR_INVALID_AUTH_CREDENTIALS. Ambos casos de credenciales SOCKS devolvieron Browser does not support socks5 proxy authentication. Los proxies locales también rechazaron intentos de conexión de fondo sin relación. Solo la petición de aplicación con el marcador único cuenta como reenvío satisfactorio. El resultado de loopback de 1.63.0 coincide con su implementación fijada; un resultado exploratorio anterior de 1.49.0 fue distinto y no sustituye esta ejecución.
Guarda el siguiente ejemplo completo, exclusivamente local, como playwright-local-proxy.mjs junto a un paquete [email protected] instalado. Asigna a CHROME un ejecutable de Chrome estándar. El host Linux probado proporcionaba DISPLAY=:88; su navegador se ejecutó en modo headless. No hacen falta credenciales ascendentes ni un destino externo. El script rechaza destinos de proxy sin relación, gestiona la limpieza, comprueba CPU y memoria, y termina con error si fallan las aserciones o hay menos de dos plazas disponibles.
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;
});
}
Compara dos salidas reales desde un perfil cerrado
Un par de puertos de proxy no constituye un experimento con dos salidas. Distintos adaptadores locales pueden alcanzar la misma dirección pública ascendente. Antes de comparar valores del navegador, exige que cada ruta autorizada devuelva una dirección pública válida desde un observador propio y que ambas direcciones sean distintas. Si alguna respuesta es una página de autenticación, una traza malformada, una redirección inesperada o un error de red, la comparación está incompleta. No sustituyas esa evidencia por un número de puerto local o una etiqueta de país del proveedor.
Utiliza un perfil ordinario que se haya creado, cerrado correctamente y mantenido después inmutable. Crea dos copias independientes y escribibles a partir de ese origen y compara sus resúmenes del árbol antes de iniciar cualquiera de los navegadores. Las copias deben incluir los mismos archivos seleccionados y aplicar la misma política de exclusión de bloqueos transitorios. Tras la comparación, vuelve a calcular el hash del origen. Que coincida solo el nombre del directorio del perfil no demuestra nada sobre su contenido; compartir un directorio escribible activo entre dos navegadores también introduce bloqueos y condiciones de carrera en el estado.
Mantén constantes el ejecutable del navegador, el sistema operativo, el modo de lanzamiento, el viewport y cualquier configuración regional explícita. Cambia únicamente la ruta autorizada entre los contextos persistentes A y B, creados con launchPersistentContext(). Inicia ambas ejecuciones a la vez, navega una vez con cada una al observador propio y lee los valores del navegador en esa misma página. Esto establece el alcance de la observación: esas dos peticiones, esas dos entradas clonadas y ese entorno. No determina todas las rutas que el navegador puede utilizar durante su vida.
Cloudflare documenta el endpoint /cdn-cgi/trace de cada dominio como recurso de diagnóstico. El observador utilizado aquí está en botbrowser.io, con límites explícitos de peticiones; no hace falta ningún detector, buscador o sitio de destino ajeno. La traza completa puede contener una dirección sin anonimizar, así que compárala y valídala en memoria. Conserva en la evidencia un resumen con clave efímera y específico de la ejecución, en lugar de publicar direcciones o una consulta reutilizable de direcciones. Una etiqueta de ruta describe la configuración autorizada, no garantiza una ubicación física.
Los servicios SOCKS ascendentes autenticados requieren una denominación precisa. Un adaptador HTTP CONNECT local restringido puede autenticarse ante el servicio ascendente mientras el navegador utiliza el protocolo de proxy HTTP ordinario con ese adaptador. Una petición satisfactoria a través de él demuestra que la ruta compuesta funcionó para el destino permitido. No elimina la limitación de contraseñas SOCKS5 nativas de Chromium, y nunca debe describirse como si Playwright aceptara un par de usuario y contraseña SOCKS5. Registra los contadores de túneles del adaptador por separado de los valores de página y las respuestas del observador.
Comparación medida de dos salidas
El mismo entorno Ubuntu fijado ejecutó simultáneamente ambas ejecuciones externas del navegador. Sus navegaciones al observador comenzaron en 2026-11-12T00:01:38.546Z, todavía el 11 de noviembre en Toronto. Cada una hizo una petición aceptada al endpoint de traza propio, devolvió HTTP 200 y superó la validación de dirección pública. Las dos direcciones aceptadas fueron distintas. Los adaptadores restringidos al destino permitieron un túnel ascendente cada uno; los intentos adicionales de conexión se rechazaron localmente. No hubo reintentos externos. Antes de esas peticiones, cuatro comprobaciones locales del cliente del adaptador cubrieron autenticación correcta, credenciales rechazadas, negociación malformada y un destino no permitido; todas pasaron, con cero reenvíos en los tres casos negativos.
| Magnitud registrada | Ruta A | Ruta B |
|---|---|---|
| Estado HTTP del observador / peticiones aceptadas | 200 / 1 | 200 / 1 |
| HMAC de la dirección pública, primeros 12 caracteres | bf448a3168c1 | 2ef6c2495efc |
| Entradas del perfil cerrado / prefijo del resumen inicial | 134 / cb216f5343b0 | 134 / cb216f5343b0 |
| Zona horaria | America/Toronto | America/Toronto |
| Configuración regional Intl resuelta | en-US | en-US |
navigator.languages | en-US, en | en-US, en |
| SHA-256 del canvas, primeros 12 caracteres | a37a4010499a | a37a4010499a |
| Comparación del agente de usuario | Iguales | Iguales |
Candidatos ICE host con iceServers: [] | 1; recopilación completa | 1; recopilación completa |
El agente de usuario completo de ambas ejecuciones fue Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36. La comparación de canvas dibujó un canvas de 160 × 50, fondo #e5ebf3, color de texto #17243a, fuente 16px Arial y Proxy fixture 2026 en (5, 30), y después aplicó SHA-256 a canvas.toDataURL(). Esas entradas exactas definen el resumen; no es una puntuación general de huella digital.
Ambas copias de 134 entradas coincidieron con la entrada cerrada antes del lanzamiento, y el resumen de la entrada original permaneció igual después. La igualdad tras el cambio de salida validado demuestra que esos valores de página registrados no siguieron automáticamente las rutas en esta ejecución. No demuestra que se inspeccionara todo el estado del navegador. Las cookies, los permisos, los service workers, el almacenamiento y la actividad de red posterior pueden diferir sin aparecer en esas lecturas. Los recuentos ICE limitados a host no miden la igualdad de direcciones de candidatos ni el tráfico STUN/TURN. Ni las señales iguales ni las salidas distintas determinan la decisión del destino de vincular cuentas.
Interpreta cada valor del navegador según su alcance
La zona horaria procede del entorno del navegador expuesto por Intl.DateTimeFormat().resolvedOptions(), no directamente de la dirección de la respuesta de traza. Una ruta etiquetada para un país puede coexistir con otra zona horaria porque la entrada del navegador y la ruta ascendente son independientes. Un requisito legítimo de prueba puede ser mantener una zona horaria fija en todas las rutas o exigir un entorno regional declarado. Define primero ese requisito; coincidir con una base de datos de IP no es una condición universal de corrección.
navigator.languages, la configuración regional resuelta de Intl y una cabecera de idioma HTTP son observaciones relacionadas, pero distintas. Registra lo que devuelve realmente la página y, si las cabeceras importan, regístralas por separado en un servidor propio. No deduzcas las tres de una sola opción regional. Un ajuste de contexto es una entrada experimental: cambiarlo junto con el proxy impide atribuir una diferencia exclusivamente a la ruta. Esa entrada puede ser útil en una prueba posterior, después de completar la comparación de una sola variable.
La salida del canvas corresponde a un dibujo, una compilación del navegador y un entorno de renderizado concretos. Calcular el hash de un dibujo fijo facilita comparar esa salida; no define un identificador universal del navegador. Conserva las instrucciones de dibujo y el método de comparación. No sorprende que dos rutas produzcan el mismo resumen cuando se mantienen fijas las entradas del renderizador y del perfil subyacentes. Resúmenes distintos requieren investigar las entradas de renderizado antes de atribuirles una interpretación geográfica o relacionada con una cuenta.
WebRTC exige una afirmación especialmente acotada. Una conexión entre pares con iceServers: [] observa la recopilación de candidatos host locales bajo esa configuración. Es posible conservar tipos y recuentos de candidatos sin exponer direcciones. Esto no ejercita STUN o TURN, no demuestra que UDP siga un proxy HTTP ni establece la ausencia de una fuga. No recopilar un candidato tampoco es un veredicto general sobre la red. Comprueba cualquier requisito más amplio mediante un endpoint autorizado por separado y un plan de observación específico del protocolo.
El agente de usuario es otra entrada que registrar junto con el binario exacto. Una UA coincidente no demuestra que ClientHello, HTTP/2 o todas las API de página también coincidan. A la inversa, una diferencia de UA no debe atribuirse a la rotación de proxies sin comprobar la versión del navegador y los ajustes de lanzamiento. Mantén las comparaciones al nivel que sustenta la medición. Un destino puede combinar señales que la página local nunca midió; ningún resultado de aquí certifica su decisión de vinculación de cuentas o de riesgo.
La referencia de alineación de proxy y geolocalización de BotBrowser documenta las entradas de entorno compatibles. Puede ayudar a reproducir una configuración de prueba declarada, pero no elige la política regional correcta para una aplicación ni valida credenciales ascendentes. Corrige un fallo de autenticación HTTP en la capa del proxy; corrige una ruta incorrecta de contexto donde se configura ese contexto; cambia valores del navegador solo cuando un requisito explícito de prueba los exija. Así se conserva el valor causal de la siguiente comparación.
Fuentes
- Documentación de proxies de red de Playwright
- Opciones de proxy de lanzamiento de BrowserType
- Opciones de proxy de contexto del navegador
- Implementación de contexto fijada de Playwright 1.63.0
- Construcción fijada de rutas de lanzamiento de Chromium
- Construcción fijada de rutas de loopback de contexto
- Especificación de protocolos de proxy y rutas de Chromium
- Endpoints de diagnóstico de Cloudflare
- Recomendación WebRTC del W3C
- Configuración de proxy de BotBrowser
- Entradas de proxy y geolocalización de BotBrowser
Conclusión práctica
Empieza con un marcador de origen propio y un contador de ruta. Una vez comprendido ese trayecto local, valida dos salidas públicas autorizadas desde entradas de perfil cerrado idénticas byte a byte y compara los valores concretos del navegador que mencione el requisito. Un proxy de Playwright operativo no altera automáticamente esos valores. El siguiente cambio útil es el mínimo en la capa que falló, seguido de la misma comprobación controlada, en lugar de un nuevo conjunto de ajustes de proxy, perfil y renderizado.
Preguntas frecuentes
¿Puede un contexto heredar estado del navegador solo por usar el proxy de lanzamiento?
La herencia de ruta y la persistencia de estado son propiedades independientes. Un newContext() normal crea un contexto aislado y no persistente aunque utilice el valor predeterminado del lanzamiento. Un lanzamiento persistente vincula un contexto a un directorio en disco. Para un experimento con la misma entrada, utiliza una instantánea cerrada y copias escribibles separadas; compartir la dirección del proxy no demuestra que se compartieran cookies o almacenamiento.
¿Una exclusión directa envía una petición fallida al proxy de lanzamiento como alternativa?
Una exclusión selecciona la ruta directa para un destino coincidente, en lugar de una cadena de alternativas. Confírmalo con contadores independientes del proxy de lanzamiento, del proxy de contexto y del origen. Si la propia ruta directa no está disponible, ese fallo no demuestra que se consultara ninguno de los proxies. Mantén constantes la escritura del destino y el esquema al comprobar la regla.
¿Ocultar las direcciones en el informe hace que la medición sea menos útil?
La validez de las direcciones públicas y la comprobación de desigualdad A/B pueden afirmarse antes de descartar los valores originales. Un resumen temporal con clave por ejecución permite al informe mostrar que las respuestas aceptadas difirieron sin almacenar una dirección. La clave secreta no se publica ni reutiliza, y el registro no puede determinar por sí solo el país físico ni las rutas posteriores de ninguna de las salidas del proveedor.
Este artículo forma parte de la guía: Primeros pasos con automatización de navegador: mapa inicial
Artículos Relacionados
Proxy en Puppeteer: autenticación, SOCKS5 y señales
RedConfiguración de proxy del navegador: guía de SOCKS5, HTTP y HTTPS
Primeros pasosScraping con Playwright: navegador y perfil
RedProxy por contexto: propiedad de ruta para trabajo separado
IdentidadZona horaria, locale e idioma en las API del navegador
Primeros pasosRecetas CLI para automatización de navegador: ejemplos de comandos
Ejecuta la misma comprobación en BotBrowser
Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.