返回博客
网络

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 和自己的地理元数据。上下文之间没有共享的网络信号。

Per-Context Proxy 架构 单个 BotBrowser 实例 上下文 A 配置文件: Windows US 时区: America/New_York 地区: en-US Cookie、存储 (隔离) 上下文 B 配置文件: macOS UK 时区: Europe/London 地区: en-GB Cookie、存储 (隔离) 上下文 C 配置文件: Linux DE 时区: Europe/Berlin 地区: de-DE Cookie、存储 (隔离) 美国代理 203.0.113.1 英国代理 198.51.100.1 德国代理 192.0.2.1 每个上下文通过自己的 代理路由,并匹配 对应的地理元数据

技术背景

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 个上下文)
浏览器进程101
GPU 进程101
网络进程101
基础内存开销~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 泄漏防护

#proxy#per-context#network#isolation#geographic-identity#multi-region