Per-Context Proxy:为每个浏览器上下文配置独立的网络身份
为每个 BrowserContext 配置独立的代理和地理身份。在单个浏览器实例中运行多个区域,自动对齐时区、地区和语言设置。
简介
在浏览器中运行多个身份或区域工作流时,整个会话使用单一代理是不够的。如果每个上下文共享同一个 IP 地址,不同身份的流量可以通过这个共享的网络路径被关联。追踪系统会观察 IP 模式,两个在相似时间从同一地址访问的账户很容易被关联起来。
Per-context proxy 通过为每个 BrowserContext 分配专用代理来解决这个问题。每个上下文通过不同的代理服务器路由,获得自己的公网 IP,并根据代理出口位置自动获取地理元数据(时区、地区、语言)。结合 per-context 指纹隔离,每个上下文成为一个完全独立的身份,没有共享的网络或指纹信号。
本文介绍了 BotBrowser 中 per-context proxy 的工作原理,如何在 Puppeteer 和 Playwright 中进行配置,以及如何优化多区域部署的性能。
隐私影响
单一代理在所有上下文之间创建了共享的网络身份。即使使用不同的指纹配置文件,共享的 IP 也会成为关联点。考虑以下风险:
- 基于 IP 的关联:两个从同一 IP 访问同一服务的账户可以被关联,无论指纹差异如何
- 地理不一致:配置了德国指纹配置文件但通过美国代理路由的上下文,会在声明的位置和网络来源之间产生明显的不匹配
- 时序分析:共享一个代理的多个身份可以通过流量时序模式被关联,因为所有请求都来自同一个网络端点
Per-context proxy 消除了这些风险。每个上下文有自己的代理、自己的公网 IP 和自己的地理元数据。上下文之间没有共享的网络信号。
技术背景
BrowserContext 级别的网络隔离
在 Chromium 中,BrowserContext 是一个隔离的浏览环境。每个上下文有自己的 cookie jar、localStorage、sessionStorage、IndexedDB 和缓存。标准 Chromium 开箱即提供这种存储隔离。
BotBrowser 将此隔离扩展到网络层。当 BrowserContext 被分配了自己的代理时,该上下文的所有 HTTP、HTTPS 和 WebSocket 流量都通过指定的代理服务器路由。同一浏览器实例中的其他上下文不受影响。这是上下文级别的真正网络隔离,而不是可能遗漏早期请求或 WebSocket 连接的页面级拦截器。
自动地理位置检测
当上下文通过代理连接时,BotBrowser 检测代理的出口 IP 并自动为该特定上下文推导地理设置:
- 时区:根据代理 IP 的地理位置推导(例如,美国东部代理对应
America/New_York) - 地区:匹配代理所在国家(例如
en-US) - 语言:根据代理所在区域设置(例如
en-US,en) - 地理坐标:根据 IP 近似计算
这对每个上下文独立进行。美国代理上下文获得美国地理设置,而同一浏览器中的德国代理上下文获得德国设置。无需手动配置时区、地区或语言。
与 Per-Context 指纹的配合
Per-context proxy 与 BotBrowser 的 per-context 指纹隔离(ENT Tier3)协同工作。每个上下文可以接收:
- 通过
--bot-profile设置的唯一指纹配置文件 - 独立的代理和公网 IP
- 从代理推导的匹配地理元数据
- 隔离的存储(cookie、localStorage、IndexedDB)
这意味着每个上下文都是完全独立的身份。上下文之间没有共享的指纹信号、网络路径或存储。
配置
Puppeteer:多区域设置
在 Puppeteer 中,per-context proxy 通过 CDP(Chrome DevTools Protocol)配置。你创建一个 BrowserContext,通过 BotBrowser.setBrowserContextFlags 分配代理设置和指纹配置文件,然后在该上下文中创建页面。
const puppeteer = require('puppeteer-core');
const browser = await puppeteer.launch({
executablePath: process.env.BOTBROWSER_EXEC_PATH,
headless: true,
defaultViewport: null,
args: ['--bot-profile=/path/to/default-profile.enc'],
});
const client = await browser.target().createCDPSession();
// 美国上下文 + 美国代理
const usCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: usCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/us-profile.enc',
'--proxy-ip=203.0.113.1',
],
});
const usPage = await usCtx.newPage();
// 英国上下文 + 英国代理
const ukCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@uk-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ukCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/uk-profile.enc',
'--proxy-ip=198.51.100.1',
],
});
const ukPage = await ukCtx.newPage();
// 德国上下文 + 德国代理
const deCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@de-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: deCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/de-profile.enc',
'--proxy-ip=192.0.2.1',
],
});
const dePage = await deCtx.newPage();
// 每个上下文使用自己的代理和地理身份进行导航
await Promise.all([
usPage.goto('https://example.com'),
ukPage.goto('https://example.com'),
dePage.goto('https://example.com'),
]);
await browser.close();
重要提示:BotBrowser.setBrowserContextFlags 必须在该上下文中创建任何页面之前调用。渲染器进程在启动时读取其标志。如果页面已经存在,新标志将不会生效。
Puppeteer:仅通过 botbrowserFlags 配置代理
你也可以完全通过 botbrowserFlags 配置代理,无需在 createBrowserContext 中传递 proxyServer:
const ctx = await browser.createBrowserContext();
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-ip=203.0.113.1',
'--proxy-bypass-list=localhost;127.0.0.1',
],
});
const page = await ctx.newPage();
这种方式将所有代理参数(服务器、IP、绕过规则)配置在一个地方。
Playwright:Per-Context 代理
Playwright 通过 browser.newContext() 提供原生的 per-context 代理支持:
const { chromium } = require('playwright-core');
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: ['--bot-profile=/path/to/profile.enc'],
headless: true,
});
// 美国上下文
const usContext = await browser.newContext({
proxy: { server: 'socks5://us-proxy:1080', username: 'user', password: 'pass' },
});
// 德国上下文
const deContext = await browser.newContext({
proxy: { server: 'socks5://de-proxy:1080', username: 'user', password: 'pass' },
});
// 每个上下文自动获取匹配其代理的地理设置
const usPage = await usContext.newPage();
const dePage = await deContext.newPage();
await usPage.goto('https://example.com');
await dePage.goto('https://example.com');
BotBrowser 根据每个上下文的代理出口 IP 自动推导时区、地区和语言。
使用 --proxy-ip 跳过 IP 检测
每个上下文的代理会触发一个 IP 检测请求来确定地理设置。如果你已经知道代理的出口 IP,可以使用 --proxy-ip 跳过这个检测步骤:
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-ip=203.0.113.1', // 跳过 IP 查询,使用此 IP 进行地理检测
],
});
这消除了每个上下文的 IP 查询延迟,在连续创建多个上下文时尤其有益。
使用 proxy-bypass-rgx 进行选择性路由
使用 --proxy-bypass-rgx 将特定 URL 直接路由而不通过代理。这可以减少静态资源或内部服务的代理带宽消耗:
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-bypass-list=localhost;127.0.0.1',
'--proxy-bypass-rgx=\\.(js|css|png|jpg|svg)(\\?|$)',
],
});
--proxy-bypass-list 使用标准 Chromium 分号分隔的主机列表。--proxy-bypass-rgx 使用 RE2 正则语法,匹配主机名和 URL 路径。
常见场景
单一浏览器中的多区域测试
在单个浏览器实例中同时运行美国、英国和德国上下文。每个上下文获得不同的代理、指纹配置文件和地理身份:
const regions = [
{
name: 'US',
proxy: 'socks5://user:pass@us.proxy.example.com:1080',
ip: '203.0.113.1',
profile: '/path/to/us-profile.enc',
},
{
name: 'UK',
proxy: 'socks5://user:pass@uk.proxy.example.com:1080',
ip: '198.51.100.1',
profile: '/path/to/uk-profile.enc',
},
{
name: 'DE',
proxy: 'socks5://user:pass@de.proxy.example.com:1080',
ip: '192.0.2.1',
profile: '/path/to/de-profile.enc',
},
];
const client = await browser.target().createCDPSession();
for (const region of regions) {
const ctx = await browser.createBrowserContext({
proxyServer: region.proxy,
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
`--bot-profile=${region.profile}`,
`--proxy-ip=${region.ip}`,
],
});
const page = await ctx.newPage();
await page.goto('https://example.com');
// 验证地理身份
const tz = await page.evaluate(() =>
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const lang = await page.evaluate(() => navigator.language);
console.log(`${region.name}: timezone=${tz}, language=${lang}`);
}
Per-Context:不同配置文件 + 不同代理
每个上下文可以使用完全不同的指纹配置文件。Windows 配置文件搭配美国代理,macOS 配置文件搭配英国代理,全在一个浏览器中:
// Windows 身份 + 美国代理
const winCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: winCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/windows-profile.enc',
'--proxy-ip=203.0.113.1',
],
});
// macOS 身份 + 英国代理
const macCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@uk-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: macCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/macos-profile.enc',
'--proxy-ip=198.51.100.1',
],
});
运行时代理切换
对于需要在创建后更改上下文代理的场景(例如会话内的地理轮换),使用 BotBrowser.setBrowserContextProxy(ENT Tier3):
const ctx = await browser.createBrowserContext();
const page = await ctx.newPage();
const client = await page.createCDPSession();
// 从美国代理开始
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
proxyIp: '203.0.113.1',
});
await page.goto('https://example.com');
// 运行时切换到英国代理
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5h://user:pass@uk-proxy.example.com:1080',
proxyIp: '198.51.100.1',
proxyBypassList: 'localhost;127.0.0.1',
proxyBypassRgx: 'cdn\\.example\\.com|/static/',
});
await page.goto('https://example.co.uk');
// 切换到日本代理
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5://user:pass@jp-proxy.example.com:1080',
proxyIp: '192.0.2.1',
});
await page.goto('https://example.jp');
每次切换后,BotBrowser 会重新检测地理设置并应用到上下文。地理更新在下一次主框架导航时生效。
性能优化
使用 --proxy-ip 减少查询开销
当你知道每个代理的出口 IP 时,始终传递 --proxy-ip。如果不传递,BotBrowser 会在首次导航时为每个上下文执行 IP 检测请求。有 10 个以上的上下文时,这些查询会累积:
// 不使用 --proxy-ip:每个上下文都要发起 IP 检测请求
// 使用 --proxy-ip:地理检测即时完成,无需网络请求
const contexts = proxies.map(async (proxy) => {
const ctx = await browser.createBrowserContext({ proxyServer: proxy.server });
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
`--proxy-ip=${proxy.knownIp}`,
],
});
return ctx;
});
共享进程节省资源
单个浏览器实例内的 per-context proxy 共享 GPU 进程、浏览器进程和实用程序进程。与为每个代理启动单独的浏览器实例相比,这种方法节省了:
| 资源 | 独立实例(10 个代理) | Per-Context(10 个上下文) |
|---|---|---|
| 浏览器进程 | 10 | 1 |
| GPU 进程 | 10 | 1 |
| 网络进程 | 10 | 1 |
| 基础内存开销 | ~500 MB | ~50 MB |
| 上下文创建时间 | 每个 1-3 秒 | 每个毫秒级 |
结合 Per-Context 指纹
当你为每个上下文同时分配代理和指纹配置文件时,每个上下文成为完全独立的身份,而无需单独浏览器实例的开销:
// 单个浏览器实例,3 个完整身份
const identities = [
{ profile: '/profiles/win-us.enc', proxy: 'socks5://us:1080', ip: '203.0.113.1' },
{ profile: '/profiles/mac-uk.enc', proxy: 'socks5://uk:1080', ip: '198.51.100.1' },
{ profile: '/profiles/linux-de.enc', proxy: 'socks5://de:1080', ip: '192.0.2.1' },
];
for (const id of identities) {
const ctx = await browser.createBrowserContext({ proxyServer: id.proxy });
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
`--bot-profile=${id.profile}`,
`--proxy-ip=${id.ip}`,
],
});
// 每个上下文:唯一指纹 + 唯一代理 + 唯一地理 = 完全独立
}
验证
设置 per-context proxy 后,独立验证每个上下文:
async function verifyContext(context, label) {
const page = await context.newPage();
// 检查公网 IP
await page.goto('https://httpbin.org/ip');
const ipData = await page.evaluate(() => document.body.textContent);
console.log(`[${label}] IP: ${ipData.trim()}`);
// 检查时区
const tz = await page.evaluate(() =>
Intl.DateTimeFormat().resolvedOptions().timeZone
);
console.log(`[${label}] 时区: ${tz}`);
// 检查语言
const lang = await page.evaluate(() => navigator.language);
console.log(`[${label}] 语言: ${lang}`);
// 检查地区
const locale = await page.evaluate(() =>
Intl.NumberFormat().resolvedOptions().locale
);
console.log(`[${label}] 地区: ${locale}`);
await page.close();
}
await verifyContext(usCtx, 'US');
await verifyContext(ukCtx, 'UK');
await verifyContext(deCtx, 'DE');
确认每个上下文显示不同的 IP、时区和地区,且与代理的地理位置匹配。
常见问题
需要什么许可证级别才能使用 per-context proxy?
Per-context proxy 配合指纹隔离需要 ENT Tier3。通过 BotBrowser.setBrowserContextProxy 进行运行时代理切换也需要 ENT Tier3。
可以在 Playwright 中使用 per-context proxy 吗?
可以。Playwright 原生支持通过 browser.newContext({ proxy: ... }) 进行 per-context 代理配置。BotBrowser 会为每个 Playwright 上下文自动推导地理设置。
setBrowserContextFlags 必须在创建页面之前调用吗?
是的。渲染器进程在启动时读取其标志。如果上下文中已存在页面,新标志将不会生效。正确顺序是:createBrowserContext -> setBrowserContextFlags -> newPage。
创建后可以更改上下文的代理吗?
可以,通过 BotBrowser.setBrowserContextProxy(ENT Tier3)。这允许在不重新创建上下文的情况下进行运行时代理切换。地理设置在下一次主框架导航时重新推导。
支持哪些代理协议?
--proxy-server 支持的所有协议都可用于 per-context:socks5://、socks5h://、http://、https://。所有协议都支持内嵌认证(user:pass@host:port)。
可以同时运行多少个上下文? 没有硬性限制。每个上下文消耗的内存与其打开的页面和缓存资源成正比。实际上,在有足够 RAM 的机器上,可以在单个浏览器实例中运行数十或数百个上下文。
BotBrowser 会为每个上下文自动检测地理信息吗? 会。每个具有不同代理的上下文都会获得独立的地理检测。BotBrowser 检测出口 IP 并为该特定上下文配置时区、地区和语言。
如果不设置 --proxy-ip 会怎样?
BotBrowser 会在每个上下文的首次导航时执行自动 IP 检测请求。这可以正常工作,但会增加少量延迟。设置 --proxy-ip 可以消除这个查询开销。
每个上下文可以使用不同的绕过规则吗?
可以。--proxy-bypass-list 和 --proxy-bypass-rgx 都可以通过 botbrowserFlags 为每个上下文单独设置。
总结
Per-context proxy 为单个浏览器实例中的每个 BrowserContext 提供完整的网络隔离。每个上下文通过自己的代理路由,获得独立的公网 IP,并根据代理出口位置自动推导地理元数据(时区、地区、语言)。结合 per-context 指纹配置文件,每个上下文都作为完全独立的身份运行。
有关代理基础知识和协议详情,请参阅代理配置。有关在现有上下文中进行运行时代理切换,请参阅动态代理切换。有关完整的多身份隔离,请参阅多账户浏览器隔离。有关所有上下文的 DNS 和 WebRTC 泄漏防护,请结合DNS 泄漏防护和 WebRTC 泄漏防护。