Proxy Playwright : routes, authentification et signaux
Diagnostiquer les routes, exclusions de contexte et accès SOCKS5 avec Playwright, puis distinguer deux sorties réelles des signaux de profil inchangés.
BotBrowser Team
Vous voulez la documentation structurée pour Démarrage ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
Un proxy Playwright peut envoyer une requête par la sortie prévue alors que le fuseau horaire, la langue et le canvas restent inchangés. Vérifiez séparément la route et l’environnement du navigateur : un proxy fonctionnel ne définit pas un nouveau profil. BotBrowser prend en charge des entrées déclarées pour le profil, le proxy, le fuseau horaire, la locale et la langue, mais ne peut pas faire accepter des identifiants par une route amont et ne garantit pas la façon dont une destination interprète ces entrées.
TL;DR
- Prouvez la route de la requête avec une origine ou un observateur sous votre contrôle avant de modifier le navigateur. Le seul chargement réussi d’une page ne détermine pas quel proxy l’a traitée.
- Distinguez les valeurs par défaut du lancement du proxy explicite d’un contexte. Les exclusions de routage choisissent une connexion directe ; elles ne constituent pas un repli vers un autre proxy.
- Les identifiants HTTP documentés par Playwright ne donnent pas à Chromium une prise en charge native des mots de passe SOCKS5. Vérifiez la limite du protocole avant de diagnostiquer une page de connexion.
- Comparez des copies identiques octet par octet d’un profil fermé sur deux sorties publiques validées. Des valeurs stables du navigateur sont des observations, pas une preuve de la décision d’identité prise par la destination.
Sommaire
- Partir de la première opération en échec
- Distinguer les routes de lancement, de contexte et les exclusions directes
- Reproduire la route avant de modifier le navigateur
- Comparer deux sorties réelles depuis un profil fermé
- Interpréter chaque valeur du navigateur selon sa portée
- Conclusion pratique
Partir de la première opération en échec
Conservez la première opération qui échoue et sa réponse. Un échec de création de contexte diffère d’un refus de connexion par un proxy ; les deux précèdent le rejet d’une action de compte par un site. Pour diagnostiquer playwright proxy, le premier relevé le plus utile contient le schéma du proxy, la portée de configuration, le schéma de destination et la première erreur. Remplacez les identifiants par une étiquette avant de partager les journaux : un message d’erreur peut contenir une adresse de serveur ou une URL avec des secrets intégrés.
| Observation | Éléments à recueillir | Première modification contrôlée |
|---|---|---|
| La création du contexte refuse les identifiants SOCKS5 | Erreur API avant la navigation ; aucune connexion au listener | Vérifier les protocoles pris en charge dans la documentation |
Le proxy HTTP demande une authentification avec 407 | Défi du proxy et compteurs de transmissions acceptées | Vérifier les identifiants HTTP de ce proxy |
Une origine renvoie 401 | Réponse de l’origine, distincte du défi du proxy | Examiner l’authentification de cette origine |
| La page charge par une route inattendue | Compteurs des proxies de lancement et de contexte, marqueur de l’origine | Corriger la portée de configuration ou l’exclusion |
| Les deux routes présentent la même adresse publique | Réponses de l’observateur, horodatages et étiquettes de route du fournisseur | Valider le routage amont avant de comparer les signaux |
| Les sorties diffèrent mais les valeurs du navigateur coïncident | Instantané du profil, binaire et relevés individuels de la page | Examiner l’entrée du navigateur liée à l’exigence |
Un délai de navigation dépassé ne constitue pas un diagnostic. Augmenter le délai de page.goto() peut aider avec un endpoint autorisé lent, mais ne peut ajouter une méthode d’authentification de transport ni modifier la route d’un contexte. De même, un 403 de la destination n’identifie pas le composant défaillant sans éléments supplémentaires. Conservez la réponse et respectez le périmètre de diagnostic autorisé, plutôt que de modifier à répétition des paramètres sans rapport face à un tiers.
L’URL du proxy et celle de la destination répondent à des questions différentes. http://proxy.test:8080 désigne une connexion à un proxy HTTP. Une destination https:// peut emprunter son tunnel CONNECT sans que le proxy lui-même soit un proxy HTTPS. Notez les deux schémas. Un tunnel accepté ne certifie pas non plus la sortie choisie par le fournisseur amont. Les différences entre échecs HTTP, HTTPS et SOCKS5 dans Puppeteer couvrent ce vocabulaire de protocoles ; les réglages distincts de lancement et de contexte de Playwright ajoutent le problème de portée ci-dessous.
Utilisez une origine qui renvoie un marqueur sans ambiguïté, avec un budget de requêtes et un compteur. L’origine prouve que la réponse attendue est arrivée ; le compteur du proxy prouve quelle route locale l’a transmise. Un observateur IP répond à une autre question : l’adresse publique visible pour cette requête. Séparer ces trois relevés évite de prendre une capture de page pour une preuve réseau. Les requêtes directes et transmises peuvent toutes deux afficher le même HTML inchangé de l’origine.
Distinguer les routes de lancement, de contexte et les exclusions directes
L’API documentée accepte un proxy au lancement du navigateur et à la création d’un contexte. Un lancement persistant accepte aussi les paramètres de proxy avec son userDataDir. Ces réglages ont des durées de vie différentes. Un browser.newContext() ordinaire est isolé et n’écrit pas son état de navigation dans ce répertoire. Un contexte persistant utilise un état stocké sur disque ; sa fermeture ferme le navigateur associé. Choisir l’un parce qu’un exemple l’utilise peut modifier les conditions de stockage de l’expérience.
Un proxy de contexte explicite représente un nouveau choix de route, pas une demande d’essayer le proxy de lancement puis celui du contexte en cas d’échec. Traitez la configuration complète du contexte comme un ensemble : serveur, identifiants pris en charge et liste d’exclusions. Vérifiez chaque champ à l’endroit où le contexte est créé. Une route assemblée dans launch() n’établit pas ce qu’un appel ultérieur à newContext({ proxy }) a fourni. Les choix de configuration du navigateur avec Playwright expliquent la propriété du navigateur et la durée de vie du contexte avant la comparaison réseau.
Le membre exact proxy.bypass désigne la liste documentée d’exclusions de routage séparées par des virgules. Ici, une exclusion signifie qu’une requête correspondante se connecte directement au lieu de passer par ce proxy configuré. Elle ne signifie pas que la destination autorise un accès et ne définit pas une identité de navigateur. Une requête exclue du proxy de contexte C ne prouve pas que le proxy de lancement L l’a traitée. La matrice contrôlée nécessite des compteurs L et C distincts pour différencier un accès direct à l’origine de chacune des routes transmises.
Le navigateur interprète finalement ses règles de routage : testez donc une règle avec la forme exacte d’URL utilisée par la charge de travail. Chromium distingue un nom d’hôte simple, un motif de domaine et une URL contenant une IP littérale. Sa règle <local> concerne les noms d’hôte simples sans point ; elle ne désigne pas toute adresse qui finit par se résoudre vers une machine locale. Une règle CIDR s’applique à une IP littérale dans l’URL, pas à un nom d’hôte au seul motif que le DNS renvoie une adresse de ce sous-réseau. Conservez l’écriture de l’hôte et le port dans le relevé d’échec.
Le comportement de loopback mérite un cas local explicite. Chromium possède des exclusions implicites pour les adresses locales, tandis que Playwright construit aussi des règles de routage loopback. L’implémentation 1.63.0 vérifie les entrées loopback explicites avant de décider d’ajouter son modificateur. Un résultat plus ancien ne peut être transposé au seul motif que le nom de l’option reste identique. Ni une affirmation générale selon laquelle localhost se connecte toujours directement, ni une hypothèse non mesurée sur la portée ne suffisent. Vérifiez les compteurs de transmission sur 127.0.0.1 en notant la version installée ; des variables d’environnement non documentées ou des options supplémentaires de résolution sont inutiles.
La référence de configuration proxy de BotBrowser définit les entrées réseau du produit. Ces entrées ne remplacent pas la portée des objets ni les règles de fusion propres à Playwright. Lors d’un changement de bibliothèque d’automatisation, transposez délibérément le modèle de configuration au lieu de copier un objet superficiellement similaire en supposant qu’il garde la même sémantique de routage.
Reproduire la route avant de modifier le navigateur
Un fixture local reproduisant l’échec retire le comportement de la destination du diagnostic. Démarrez une origine HTTP sous votre contrôle sur un port loopback attribué dynamiquement, et un proxy qui ne transmet qu’à cette origine. Utilisez un marqueur de réponse unique et relevez les défis, les transmissions acceptées et les requêtes directes à l’origine. Le proxy doit refuser toute autre destination. Un relais ouvert généraliste n’est pas nécessaire pour reproduire une erreur d’identifiants HTTP.
Pour l’authentification HTTP, exécutez séparément les cas avec identifiants corrects, absents et incorrects. Le cas réussi doit prouver à la fois la transmission et le marqueur de l’origine. Les cas refusés doivent prouver que l’origine n’a pas été atteinte, plutôt que de simplement reconnaître une chaîne d’erreur commode. Les nouvelles tentatives du navigateur peuvent produire plusieurs défis : vérifiez la propriété pertinente, telle que zéro transmission acceptée, au lieu de supposer un nombre universel de tentatives de socket. Ne prenez pas ces résultats HTTP locaux pour un test de la politique d’authentification d’un fournisseur amont.
SOCKS5 a sa propre limite. L’implémentation figée de Playwright 1.63.0 refuse les options de nom d’utilisateur et de mot de passe du proxy pour le chemin SOCKS5 de Chromium. L’échec peut alors survenir avant que le listener SOCKS reçoive une connexion. Ce n’est pas un échec de connexion au site, et placer les identifiants dans les en-têtes HTTP de l’origine n’ajoute pas la négociation SOCKS manquante. Un listener SOCKS sans authentification et une tentative de configuration avec identifiants testent des affirmations distinctes. Un adaptateur local qui s’authentifie auprès d’un service SOCKS amont constitue une autre architecture et doit être nommé comme tel.
La portée de route demande aussi des cas indépendants. Donnez des compteurs différents au proxy de lancement L et au proxy de contexte C. Établissez d’abord la valeur par défaut du lancement, puis créez un contexte avec une route C explicite, et testez enfin une exclusion directe sur ce contexte C. Gardez le marqueur de l’origine inchangé. Sinon, une page modifiée peut masquer une route modifiée et laisser incertaine la modification responsable de la réparation apparente. Le seul compteur d’origine ne distingue pas L de C, car les deux proxies peuvent transmettre le même contenu.
Les tâches navigateur doivent utiliser un pool borné plutôt que se succéder pendant que les conditions de la machine évoluent. Le fixture accepte --pool et --workers, demande 20 par défaut et mesure le processeur et la mémoire disponibles avant de choisir le parallélisme réel. Demander davantage de places ne crée pas plus de cas indépendants. Si une comparaison externe ne peut soutenir deux exécutions simultanées du navigateur, arrêtez avant les requêtes à l’observateur ; exécuter A maintenant et B bien plus tard affaiblit la comparaison contrôlée.
Résultats locaux observés et fixture exécutable
L’exécution contrôlée a utilisé Playwright 1.63.0, Chrome standard 154.0.8037.57, Node 20.20.2 et Ubuntu 24.04.5 LTS le 11 novembre 2026, heure de Toronto. Le fixture HTTP public a exécuté six tâches simultanément : 31.88 cœurs de processeur estimés inactifs et 121726328832 octets de mémoire disponible permettaient les six. Le pool demandé était de 20, limité par le nombre de cas plutôt que par une limite séquentielle arbitraire. Les trois cas SOCKS distincts ont également été exécutés simultanément. Cet environnement isolé avec origine propre, exécuté en root, utilisait la valeur par défaut de Playwright chromiumSandbox: false ; cette condition n’est pas une recommandation de déploiement en production.
| Cas contrôlé | Résultat observé | Compteur établissant le résultat |
|---|---|---|
| HTTP au lancement, identifiants corrects | Marqueur de l’origine via L | L transmis 1 ; C transmis 0 |
| HTTP au lancement, identifiants incorrects | Aucun marqueur de l’origine | L défis 2 ; transmis 0 |
| HTTP au lancement, identifiants absents | Aucun marqueur de l’origine | L défis 1 ; transmis 0 |
Le lancement exclut explicitement 127.0.0.1 | Marqueur direct de l’origine | Origine directe 1 ; L/C transmis 0 |
| Le contexte C remplace le lancement L exclu | Marqueur de l’origine via C | C transmis 1 ; L transmis 0 |
Le contexte C exclut explicitement 127.0.0.1 | Marqueur direct de l’origine | Origine directe 1 ; L/C transmis 0 |
| SOCKS5 sans identifiants | Marqueur de l’origine | Routés 1 ; accès à l’origine 1 |
| Identifiants SOCKS5 au lancement | Refus de l’API | Connexions au listener 0 ; accès à l’origine 0 |
| Identifiants SOCKS5 à la portée du contexte | Refus de l’API | Connexions au listener 0 ; accès à l’origine 0 |
Il s’agit de comptes mesurés, pas de nombres universels de nouvelles tentatives. Les identifiants HTTP incorrects ont produit net::ERR_HTTP_RESPONSE_CODE_FAILURE ; les identifiants absents ont produit net::ERR_INVALID_AUTH_CREDENTIALS. Les deux cas d’identifiants SOCKS ont renvoyé Browser does not support socks5 proxy authentication. Les proxies locaux ont aussi refusé les tentatives de connexion en arrière-plan sans rapport. Seule la requête applicative portant le marqueur unique compte comme transmission réussie. Le résultat loopback de 1.63.0 concorde avec son implémentation figée ; un résultat exploratoire antérieur de 1.49.0 différait et ne remplace pas cette exécution.
Enregistrez l’exemple complet exclusivement local suivant dans playwright-local-proxy.mjs, à côté d’un paquet [email protected] installé. Définissez CHROME sur un exécutable Chrome standard. L’hôte Linux testé fournissait DISPLAY=:88 ; son navigateur fonctionnait en mode headless. Aucun identifiant amont ni aucune cible externe ne sont nécessaires. Le script refuse les destinations proxy sans rapport, gère le nettoyage, vérifie le processeur et la mémoire, et se termine en échec si les assertions échouent ou si moins de deux places sont 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;
});
}
Comparer deux sorties réelles depuis un profil fermé
Deux ports de proxy ne constituent pas une expérience à deux sorties. Des adaptateurs locaux distincts peuvent encore atteindre la même adresse publique amont. Avant de comparer les valeurs du navigateur, exigez que chaque route autorisée renvoie une adresse publique valide depuis un observateur sous votre contrôle, et que les adresses diffèrent. Si une réponse est une page d’authentification, une trace mal formée, une redirection inattendue ou une erreur réseau, la comparaison est incomplète. Un numéro de port local ou une étiquette de pays du fournisseur ne remplace pas cette preuve.
Utilisez un profil ordinaire créé, fermé proprement puis maintenu immuable. Faites deux copies indépendantes et modifiables de cette source, et comparez les empreintes de leurs arborescences avant le lancement des navigateurs. Les copies doivent inclure les mêmes fichiers sélectionnés et appliquer la même politique d’exclusion des verrous transitoires. Après la comparaison, recalculez le hachage de la source d’origine. Le seul nom identique d’un répertoire de profil ne prouve rien sur son contenu ; partager un répertoire actif modifiable entre deux navigateurs introduit aussi des verrous et des courses sur l’état.
Gardez constants l’exécutable du navigateur, le système d’exploitation, le mode de lancement, le viewport et tout réglage explicite de locale. Ne changez que la route autorisée entre les contextes persistants A et B, créés avec launchPersistentContext(). Lancez les deux exécutions ensemble, naviguez une fois vers l’observateur avec chacune et relevez les valeurs du navigateur dans cette même page. Cela fixe la portée de l’observation : ces deux requêtes, ces deux entrées copiées et cet environnement. Cela ne détermine pas toutes les routes utilisables par le navigateur au cours de sa vie.
Cloudflare documente l’endpoint /cdn-cgi/trace de chaque domaine comme outil de diagnostic. L’observateur utilisé ici se trouve sur botbrowser.io, avec des limites explicites de requêtes ; aucun détecteur, moteur de recherche ou site cible tiers n’est nécessaire. La trace complète peut contenir une adresse brute : comparez-la et validez-la donc en mémoire. Conservez dans les preuves une empreinte à clé éphémère propre à l’exécution, au lieu de publier les adresses ou un moyen réutilisable de les retrouver. Une étiquette de route décrit la configuration autorisée, sans garantir un emplacement physique.
Les services SOCKS amont authentifiés exigent une dénomination précise. Un adaptateur HTTP CONNECT local restreint peut s’authentifier en amont tandis que le navigateur lui parle avec le protocole proxy HTTP ordinaire. Une requête réussie par cet intermédiaire prouve que le chemin composé a fonctionné pour la destination autorisée. Elle ne supprime pas la limite native des mots de passe SOCKS5 de Chromium et ne doit jamais être décrite comme l’acceptation par Playwright d’un couple utilisateur/mot de passe SOCKS5. Relevez séparément les compteurs de tunnels de l’adaptateur, les valeurs de page et les réponses de l’observateur.
Comparaison mesurée de deux sorties
Le même environnement Ubuntu figé a exécuté simultanément les deux navigateurs externes. Leurs navigations vers l’observateur ont commencé à 2026-11-12T00:01:38.546Z, encore le 11 novembre à Toronto. Chacune a effectué une requête acceptée vers l’endpoint de trace propre, a reçu HTTP 200 et a passé la validation d’adresse publique. Les deux adresses acceptées différaient. Les adaptateurs limités à la cible ont autorisé chacun un tunnel amont ; les tentatives de connexion supplémentaires ont été refusées localement. Il n’y a eu aucune nouvelle tentative externe. Avant ces requêtes, quatre contrôles locaux du client de l’adaptateur ont couvert une authentification réussie, des identifiants refusés, une négociation mal formée et une cible interdite ; tous ont réussi, avec zéro transmission pour les trois cas négatifs.
| Quantité relevée | Route A | Route B |
|---|---|---|
| Statut HTTP de l’observateur / requêtes acceptées | 200 / 1 | 200 / 1 |
| HMAC de l’adresse publique, 12 premiers caractères | bf448a3168c1 | 2ef6c2495efc |
| Entrées du profil fermé / préfixe de l’empreinte initiale | 134 / cb216f5343b0 | 134 / cb216f5343b0 |
| Fuseau horaire | America/Toronto | America/Toronto |
| Locale Intl résolue | en-US | en-US |
navigator.languages | en-US, en | en-US, en |
| SHA-256 du canvas, 12 premiers caractères | a37a4010499a | a37a4010499a |
| Comparaison du user agent | Identique | Identique |
Candidats ICE host avec iceServers: [] | 1 ; collecte terminée | 1 ; collecte terminée |
Le user agent complet des deux exécutions était Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36. La comparaison du canvas a dessiné un canvas de 160 × 50, avec le fond #e5ebf3, la couleur de texte #17243a, la police 16px Arial et Proxy fixture 2026 à (5, 30), puis a appliqué SHA-256 à canvas.toDataURL(). Ces entrées exactes définissent l’empreinte ; ce n’est pas un score général de fingerprint.
Les deux copies de 134 entrées correspondaient à l’entrée fermée avant le lancement, et l’empreinte de la source d’origine est restée inchangée ensuite. L’égalité après le changement validé de sortie montre que ces valeurs de page relevées n’ont pas automatiquement suivi les routes dans cette exécution. Elle ne montre pas que tout l’état du navigateur a été inspecté. Cookies, permissions, service workers, stockage et activité réseau ultérieure peuvent différer sans apparaître dans ces relevés. Les seuls comptes ICE host ne mesurent ni l’égalité des adresses des candidats ni le trafic STUN/TURN. Ni des signaux égaux ni des sorties différentes n’établissent la décision d’association de comptes prise par une destination.
Interpréter chaque valeur du navigateur selon sa portée
Le fuseau horaire provient de l’environnement du navigateur exposé par Intl.DateTimeFormat().resolvedOptions(), pas directement de l’adresse dans la réponse de trace. Une route étiquetée pour un pays peut coexister avec un autre fuseau horaire, car l’entrée du navigateur et la route amont sont distinctes. Une exigence de test légitime peut imposer un fuseau fixe pour toutes les routes, ou un environnement régional déclaré. Écrivez d’abord cette exigence ; correspondre à une base IP n’est pas une condition universelle de validité.
navigator.languages, la locale résolue par Intl et un en-tête HTTP de langue sont des observations liées mais distinctes. Relevez ce que la page renvoie réellement et, si les en-têtes importent, consignez-les séparément sur un serveur sous votre contrôle. Ne déduisez pas les trois d’une seule option de locale. Un réglage de contexte est une entrée expérimentale : le changer en même temps que le proxy empêche d’attribuer une différence à la seule route. Une telle entrée peut être utile dans un test ultérieur, après la comparaison à variable unique.
La sortie canvas appartient à un dessin, une version du navigateur et un environnement de rendu spécifiés. Hacher un dessin fixe facilite la comparaison de cette sortie ; cela ne définit pas un identifiant universel du navigateur. Conservez les instructions de dessin et la méthode de comparaison. Obtenir la même empreinte par deux routes n’est pas surprenant lorsque les entrées sous-jacentes du moteur de rendu et du profil restent fixes. Des empreintes différentes demandent d’examiner les entrées de rendu avant de leur attribuer une signification géographique ou liée au compte.
WebRTC exige une formulation particulièrement limitée. Une connexion pair à pair avec iceServers: [] observe la collecte locale de candidats host dans cette configuration. Les types et comptes de candidats peuvent être conservés sans exposer leurs adresses. Cela ne sollicite pas STUN ou TURN, ne prouve pas qu’UDP suit un proxy HTTP et n’établit pas l’absence de fuite. L’absence de candidat collecté n’est pas non plus un verdict général sur le réseau. Testez toute exigence plus forte au moyen d’un endpoint autorisé séparément et d’un plan d’observation propre au protocole.
Le user agent est une autre entrée à relever avec le binaire exact. Une UA identique ne prouve pas que ClientHello, HTTP/2 ou toutes les API de page correspondent aussi. Inversement, une différence d’UA ne doit pas être attribuée à la rotation de proxy sans vérifier la version du navigateur et les paramètres de lancement. Gardez les comparaisons au niveau étayé par la mesure. Une destination peut combiner des signaux que la page locale n’a jamais mesurés ; aucun résultat ici ne certifie sa décision de risque ou d’association de comptes.
La référence BotBrowser sur l’alignement proxy et géolocalisation documente les entrées d’environnement prises en charge. Elle peut aider à reproduire une configuration de test déclarée, mais ne choisit pas la bonne politique régionale d’une application et ne valide pas les identifiants amont. Corrigez un échec d’authentification HTTP à la couche proxy ; corrigez une mauvaise route de contexte à l’endroit où ce contexte est configuré ; changez les valeurs du navigateur uniquement lorsqu’une exigence explicite les demande. La prochaine comparaison conserve ainsi sa valeur causale.
Sources
- Documentation des proxies réseau de Playwright
- Options de proxy au lancement de BrowserType
- Options de proxy de contexte du navigateur
- Implémentation de contexte figée de Playwright 1.63.0
- Construction figée des routes de lancement Chromium
- Construction figée des routes loopback de contexte
- Spécification des protocoles proxy et du routage de Chromium
- Endpoints de diagnostic de Cloudflare
- Recommandation WebRTC du W3C
- Configuration proxy de BotBrowser
- Entrées de proxy et géolocalisation de BotBrowser
Conclusion pratique
Commencez par un marqueur d’origine sous votre contrôle et un compteur de route. Une fois ce chemin local compris, validez deux sorties publiques autorisées à partir de profils fermés identiques octet par octet, puis comparez les valeurs précises du navigateur nommées par l’exigence. Un proxy Playwright fonctionnel ne modifie pas automatiquement ces valeurs. La prochaine modification utile est la plus petite à la couche défaillante, suivie du même contrôle, plutôt qu’un nouvel ensemble de réglages de proxy, profil et rendu.
Questions fréquentes
Un contexte peut-il hériter de l’état du navigateur simplement en utilisant le proxy de lancement ?
L’héritage de route et la persistance de l’état sont deux propriétés distinctes. Un newContext() ordinaire crée un contexte isolé et non persistant même lorsqu’il utilise la valeur par défaut du lancement. Un lancement persistant lie un contexte à un répertoire sur disque. Pour une expérience avec la même entrée, utilisez un instantané fermé et des copies modifiables séparées ; une adresse proxy commune ne prouve pas que les cookies ou le stockage étaient partagés.
Une exclusion directe envoie-t-elle une requête échouée vers le proxy de lancement en repli ?
Une exclusion choisit le routage direct pour une destination correspondante, pas une chaîne de repli. Confirmez-le avec des compteurs indépendants pour le proxy de lancement, le proxy de contexte et l’origine. Si la route directe est elle-même indisponible, cet échec ne prouve pas que l’un des proxies a été consulté. Gardez constants l’écriture de la destination et son schéma pendant la vérification de la règle.
Masquer les adresses dans le rapport réduit-il l’utilité de la mesure ?
La validité des adresses publiques et leur inégalité A/B peuvent être vérifiées avant de supprimer les valeurs brutes. Une empreinte temporaire à clé propre à l’exécution permet de montrer que les réponses acceptées différaient sans stocker une adresse. La clé secrète n’est ni publiée ni réutilisée, et le relevé ne peut pas établir indépendamment le pays physique ou le routage ultérieur de l’une des sorties du fournisseur.
Cet article fait partie du guide : Débuter avec l'automatisation du navigateur : carte initiale
Articles Connexes
Proxy Puppeteer : authentification, SOCKS5 et signaux
RéseauConfiguration proxy du navigateur : guide SOCKS5, HTTP et HTTPS
DémarrageScraping avec Playwright : navigateur et profil
RéseauProxy par contexte : attribution des routes de travail
IdentitéFuseau horaire, locale et langue dans les API du navigateur
DémarrageRecettes CLI du navigateur automatisé : exemples de commandes
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.