Cloudflare Workers VPC 连接实战:基于 TCP Sockets API 打通私有网络
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
导读
本文以 Cloudflare Workers 的TCP Sockets API(cloudflare:sockets)为核心,系统讲解如何让 Worker 通过出站 TCP 连接访问 AWS、Azure、GCP、本地数据中心等私有网络中的资源,并联合 Cloudflare Tunnel、Hyperdrive、Smart Placement 组成完整的私有网络接入方案。读完本文,你将掌握connect()的完整 API 用法、TLS/StartTLS 三种加密模式、Wrangler 配置与多环境部署、Worker + Tunnel 的架构搭建,以及重试、超时、SSRF 防护等生产级实战模式。
说明:本文对应仓库 workers-vpc 参考文档 系列(含 api.md、configuration.md、patterns.md、gotchas.md)。该文档体系隶属于本仓库
cloudflare-deployskill(见 SKILL.md 中 "Networking & Connectivity" 产品索引)。注意:本文档讲述的是 TCP Sockets API;更新的 Workers VPC Services 产品(仅支持 HTTP 的 service binding,内置 SSRF 防护)目前处于 beta 阶段(2025+),属另一套独立文档。
技术选型:先回答"该用哪个"
面对"Worker 需要访问私有网络资源"的需求,第一件事不是写代码,而是按需选型。下面是文档给出的快速决策表:
| 需求 | 使用 | 原因 |
|---|---|---|
| 私有网络中的 HTTP/HTTPS API | VPC Services(beta,独立文档) | SSRF 安全、声明式绑定 |
| PostgreSQL/MySQL 数据库 | Hyperdrive | 连接池、缓存、链路优化 |
| 自定义 TCP 协议(SSH、MQTT、私有协议) | TCP Sockets(本文主题) | 完全掌控线上协议 |
| 简单 HTTP、追求最低延迟 | TCP Sockets + Smart Placement | 手动优化链路 |
| 把内网服务暴露到公网(入站方向) | Cloudflare Tunnel | 非 Worker 专属能力 |
何时该用 TCP Sockets
适合使用 TCP Sockets 的场景:
- ✅ 需要直接控制线上协议(如 Postgres 线上协议、SSH、Redis RESP)
- ✅ 非 HTTP 协议(MQTT、SMTP、自定义二进制协议)
- ✅ 需要 StartTLS 或自定义 TLS 协商
- ✅ 需要通过 TCP 流式传输二进制数据
不要使用 TCP Sockets 的场景:
- ❌ 只是需要 HTTP/HTTPS(请用
fetch()或 VPC Services) - ❌ 需要 PostgreSQL/MySQL(请用 Hyperdrive 做连接池)
- ❌ 需要 WebSocket(请用 Workers 原生 WebSocket 能力)
这个取舍背后是明确的工程理由:TCP Sockets 给你的是"裸线上协议"级别的控制力,代价是你要自己处理协议细节、连接生命周期与安全边界。仓库的 gotchas.md 中"何时使用替代方案"一节进一步印证了这一原则:数据库场景推荐 Hyperdrive(自带连接池与缓存)、HTTP 场景推荐fetch()(更简单、内置)、需要 SSRF 保护的 HTTP 场景则等待 VPC Services(beta)提供声明式绑定。
快速开始:一个最小可运行的示例
在 Worker 中发起 TCP 连接,核心只有一个函数connect(),从cloudflare:sockets模块导入:
import { connect } from 'cloudflare:sockets'; export default { async fetch(req: Request): Promise<Response> { // 连接私有服务 const socket = connect( { hostname: "db.internal.company.net", port: 5432 }, { secureTransport: "on" } ); try { await socket.opened; // 等待连接建立 const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("QUERY\r\n")); await writer.close(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); } finally { await socket.close(); // 始终关闭 socket } } };注意三个关键点:await socket.opened确保连接成功后才读写;写入与读取分别通过writable/readable两个 Web Stream;finally中await socket.close()保证无论成功失败都释放连接。try/finally关闭模式是本 API 最核心的规范(详见下文"最佳实践")。
TCP Sockets API 详解
connect()函数签名
function connect( address: SocketAddress, options?: SocketOptions ): Socket创建一条到指定地址的出站 TCP 连接。
SocketAddress:目标地址
interface SocketAddress { hostname: string; // DNS 主机名或 IP 地址 port: number; // TCP 端口(1-65535,排除被屏蔽端口) }| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
hostname | string | 目标主机名或 IP | "db.internal.net"、"10.0.1.50" |
port | number | TCP 端口号 | 5432、443、22 |
DNS 名称在连接时解析。支持 IPv4、IPv6 以及私有 IP(10.x、172.16.x、192.168.x)。
SocketOptions:连接选项
interface SocketOptions { secureTransport?: "off" | "on" | "starttls"; allowHalfOpen?: boolean; }| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
secureTransport | "off" \| "on" \| "starttls" | "off" | TLS 模式 |
allowHalfOpen | boolean | false | 是否允许半开连接 |
secureTransport三种模式的适用场景:
| 模式 | 行为 | 适用场景 |
|---|---|---|
"off" | 纯 TCP,不加密 | 测试、内部可信网络 |
"on" | 立即进行 TLS 握手 | HTTPS、安全数据库、SSH |
"starttls" | 先明文连接,之后用startTls()升级 | Postgres、SMTP、IMAP |
allowHalfOpen:默认false时,关闭读流会自动关闭写流;设为true后读写流相互独立,适合需要半开连接语义的自定义协议。
Socket 接口
interface Socket { // 数据流 readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array>; // 连接状态 opened: Promise<SocketInfo>; closed: Promise<void>; // 方法 close(): Promise<void>; startTls(): Socket; }readable:用于读取数据的流,通过getReader()消费。const { done, value } = await reader.read();每次读取一个 chunk。writable:用于写入数据的流,通过getWriter()发送。写入后建议await writer.close()通知对端写入结束。opened:连接成功时 resolve、失败时 reject 的 Promise,可拿到SocketInfo(remoteAddress、localAddress,均可能为undefined):
interface SocketInfo { remoteAddress?: string; // 可能为 undefined localAddress?: string; // 可能为 undefined } try { const info = await socket.opened; } catch (error) { // 连接失败 }closed:socket 完全关闭(双向)时 resolve 的 Promise。close():优雅关闭连接,会等待待写入数据完成。务必在finally中调用:
const socket = connect({ hostname: "api.internal", port: 443 }); try { // 使用 socket } finally { await socket.close(); }startTls():将连接升级为 TLS,仅当创建时指定了secureTransport: "starttls"才可用。升级后必须使用返回的新 socket,而不是原 socket:
const socket = connect( { hostname: "db.internal", port: 5432 }, { secureTransport: "starttls" } ); // 先发送协议专用的 StartTLS 命令 const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("STARTTLS\r\n")); // 升级为 TLS - 使用返回的 socket,而不是原来的 const secureSocket = socket.startTls(); const secureWriter = secureSocket.writable.getWriter();StartTLS 的典型协议是 SMTP、IMAP、Postgres:这些协议先以明文建立会话,客户端发送STARTTLS命令、等待服务端确认 OK 后,再调用startTls()升级为加密通道(时机错误会触发 TLS 错误,详见后文"常见坑")。
完整示例与快速参考
import { connect } from 'cloudflare:sockets'; export default { async fetch(req: Request): Promise<Response> { const socket = connect({ hostname: "echo.example.com", port: 7 }, { secureTransport: "on" }); try { await socket.opened; const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("Hello, TCP!\n")); await writer.close(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new Response(value); } finally { await socket.close(); } } };常用操作速查表:
| 任务 | 代码 |
|---|---|
| 导入 | import { connect } from 'cloudflare:sockets'; |
| 连接 | connect({ hostname: "host", port: 443 }) |
| 带 TLS | connect(addr, { secureTransport: "on" }) |
| StartTLS | 握手后socket.startTls() |
| 写入 | await writer.write(data); await writer.close(); |
| 读取 | const { value } = await reader.read(); |
| 错误处理 | try { await socket.opened; } catch { } |
| 始终关闭 | try { } finally { await socket.close(); } |
Wrangler 配置与多环境部署
基础配置
TCP Sockets 在 Workers 运行时默认可用,无需任何特殊配置。一个最简wrangler.jsonc:
{ "name": "private-network-worker", "main": "src/index.ts", "compatibility_date": "2025-01-01" }环境变量:连接信息不写死在代码里
{ "vars": { "DB_HOST": "10.0.1.50", "DB_PORT": "5432" } }interface Env { DB_HOST: string; DB_PORT: string; } export default { async fetch(req: Request, env: Env): Promise<Response> { const socket = connect({ hostname: env.DB_HOST, port: parseInt(env.DB_PORT) }); } };注意:vars中的值都是字符串,port需要parseInt()转换。
多环境(staging / production)配置
{ "vars": { "DB_HOST": "localhost" }, "env": { "staging": { "vars": { "DB_HOST": "staging-db.internal.net" } }, "production": { "vars": { "DB_HOST": "prod-db.internal.net" } } } }部署时用--env指定环境:wrangler deploy --env staging或wrangler deploy --env production。这让你可以用同一份代码、不同的目标地址做隔离发布。
密钥管理:敏感凭据用 Secret
数据库口令、TLS 私钥等敏感信息不要放进wrangler.jsonc,应使用wrangler secret:
wrangler secret put DB_PASSWORD # 按提示输入值在 Worker 中通过env.DB_PASSWORD读取,用于协议握手或身份认证。这是文档明确强调的安全边界:vars会随代码进入版本库,secrets只存在于运行时。
本地开发
使用wrangler dev调试。注意:本地模式可能无法访问私有网络,开发阶段应使用公网端点或 mock 服务器:
const config = process.env.NODE_ENV === 'dev' ? { hostname: 'localhost', port: 5432 } // Mock : { hostname: 'db.internal.example.com', port: 5432 }; // Production连接串解析
很多配置系统以连接串形式给出数据库地址,可解析出 host 与 port:
function parseConnectionString(connStr: string): SocketAddress { const url = new URL(connStr); // e.g., "postgres://10.0.1.50:5432/mydb" return { hostname: url.hostname, port: parseInt(url.port) || 5432 }; }兼容性说明
TCP Sockets 在所有现代 Workers 运行时均可用,设置当前日期(如"compatibility_date": "2025-01-01")即可,无需任何特殊 compatibility flags。部署前可先按 SKILL.md 的要求执行npx wrangler whoami确认已认证,CI/CD 场景设置CLOUDFLARE_API_TOKEN环境变量。
架构模式:Worker + Tunnel 打通私有网络
TCP Sockets 本身只解决了"出站 TCP"问题,而私有网络通常没有公网入口。文档给出的标准架构是Worker + Cloudflare Tunnel组合:
┌─────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Worker │────▶│ TCP Socket │────▶│ Tunnel │────▶│ Private │ │ │ │ (this API) │ │ (cloudflared)│ │ Network │ └─────────┘ └─────────────┘ └──────────────┘ └─────────────┘- Worker 向 Tunnel 主机名发起 TCP socket 连接
- Tunnel 端点将流量路由到私有 IP
- 响应沿 Tunnel 原路返回 Worker
数据链路为:Worker (TCP Socket) → Tunnel hostname → cloudflared → Private Network。
快速搭建步骤
- 在私有网络内的服务器上安装 cloudflared(详见 Tunnel 参考文档)
- 创建隧道:
cloudflared tunnel create my-private-network - 在
config.yml中配置路由:
tunnel: <TUNNEL_ID> credentials-file: /path/to/<TUNNEL_ID>.json ingress: - hostname: db.internal.example.com service: tcp://10.0.1.50:5432 - service: http_status:404 # 必需的兜底规则- 运行隧道:
cloudflared tunnel run my-private-network - 从 Worker 连接:
const socket = connect( { hostname: "db.internal.example.com", port: 5432 }, // Tunnel 主机名 { secureTransport: "on" } );从仓库的 Tunnel 配置参考 可以补充几个深化点:
- ingress 规则自上而下匹配,第一条命中生效,支持精确主机名、通配符主机名(
"*.example.com")、路径正则(path: \.(jpg|png|css|js)$),service: http_status:404兜底规则必须有; - service 类型支持
tcp://localhost:2222、ssh://localhost:22、rdp://localhost:3389、unix:/path/to/socket等,本文的数据库场景走tcp://; - 配置后可用
cloudflared tunnel ingress validate校验配置、cloudflared tunnel ingress rule https://foo.example.com模拟匹配; - 生产环境更推荐token 化隧道(
cloudflared tunnel --no-autoupdate run --token <TOKEN>),路由在 Cloudflare Dashboard(Zero Trust > Networks > Tunnels)集中管理,无需分发配置文件、改动即时生效。
配合 Smart Placement 降低延迟
在 Worker 多次访问私有后端时,可开启 Smart Placement 让 Worker 自动迁移到离后端更近的位置执行:
{ "placement": { "mode": "smart" } }Workers 会观察与 TCP socket 目标之间的连接延迟,自动选择更优的执行位置。详见 Smart Placement 参考。
数据库场景优先 Hyperdrive
如果目标是 PostgreSQL/MySQL,强烈建议优先使用 Hyperdrive而非裸 TCP socket,它自带连接池(省去每次请求的 TCP/TLS/auth 握手往返)、边缘建连与查询缓存(非变更查询默认 60s TTL):
{ "hyperdrive": [{ "binding": "DB", "id": "<HYPERDRIVE_ID>" }] }创建配置:npx wrangler hyperdrive create my-db --connection-string="postgres://user:pass@host:5432/db"。完整配置见 Hyperdrive 参考。这也与主文档"最佳实践"第 3 条"数据库用 Hyperdrive"一致:原始 Postgres 线上协议复杂度高(启动、认证、查询消息),生产环境不应在裸 socket 上重造轮子。
实战模式:从协议到生产级错误处理
仓库的 patterns.md 提供了大量可直接落地的模式。
读取全部数据
TCP 是流式传输,一次read()拿不到完整报文,需要循环累积:
async function readAll(socket: Socket): Promise<Uint8Array> { const reader = socket.readable.getReader(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const total = chunks.reduce((sum, c) => sum + c.length, 0); const result = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; }流式响应:socket 直接接 HTTP Response
// 将 socket 数据直接流式写入 HTTP 响应 const socket = connect({ hostname: "stream.internal", port: 9000 }, { secureTransport: "on" }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("STREAM\n")); await writer.close(); return new Response(socket.readable);协议示例:Redis RESP 与 MQTT
Redis RESP(发送*2\r\n$3\r\nGET\r\n$<keylen>\r\n<key>\r\n,接收$<len>\r\n<data>\r\n或$-1\r\n表示 null):
const socket = connect({ hostname: "redis.internal", port: 6379 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode(`*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n`));MQTT(CONNECT 报文0x10 <len> 0x00 0x04 "MQTT" 0x04 <flags> ...,PUBLISH 报文0x30 <len> <topic_len> <topic> <message>):
const socket = connect({ hostname: "mqtt.broker", port: 1883 }); const writer = socket.writable.getWriter(); // CONNECT: 0x10 <len> 0x00 0x04 "MQTT" 0x04 <flags> ... // PUBLISH: 0x30 <len> <topic_len> <topic> <message>PostgreSQL:文档明确提示生产环境用 Hyperdrive,因为裸 Postgres 协议非常复杂(startup、auth、query 消息)。
错误处理三件套
指数退避重试:
async function connectWithRetry(addr: SocketAddress, opts: SocketOptions, maxRetries = 3): Promise<Socket> { for (let i = 1; i <= maxRetries; i++) { try { const socket = connect(addr, opts); await socket.opened; return socket; } catch (error) { if (i === maxRetries) throw error; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i - 1))); // 指数退避 } } throw new Error('Unreachable'); }超时控制(API 没有内置超时,必须用Promise.race()自行实现):
async function connectWithTimeout(addr: SocketAddress, opts: SocketOptions, ms = 5000): Promise<Socket> { const socket = connect(addr, opts); const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms)); await Promise.race([socket.opened, timeout]); return socket; }主备降级:
async function connectWithFallback(primary: string, fallback: string, port: number): Promise<Socket> { try { const socket = connect({ hostname: primary, port }, { secureTransport: "on" }); await socket.opened; return socket; } catch { return connect({ hostname: fallback, port }, { secureTransport: "on" }); } }安全模式:SSRF 防护
TCP Sockets 给了"任意出站连接"的能力,必须防 SSRF——不能让用户可控的目标直达内部服务。标准做法是严格白名单:
const ALLOWED_HOSTS = ['db.internal.company.net', 'api.internal.company.net', /^10\.0\.1\.\d+$/]; function isAllowed(hostname: string): boolean { return ALLOWED_HOSTS.some(p => p instanceof RegExp ? p.test(hostname) : p === hostname); } export default { async fetch(req: Request): Promise<Response> { const target = new URL(req.url).searchParams.get('host'); if (!target || !isAllowed(target)) return new Response('Forbidden', { status: 403 }); const socket = connect({ hostname: target, port: 443 }); // Use socket... } };白名单同时支持精确字符串与正则两种匹配。
连接池
每次请求新建 TCP 连接开销很大,可以自建小型连接池复用(注意池大小受平台并发限制约束):
class SocketPool { private pool = new Map<string, Socket[]>(); async acquire(hostname: string, port: number): Promise<Socket> { const key = `${hostname}:${port}`; const sockets = this.pool.get(key) || []; if (sockets.length > 0) return sockets.pop()!; const socket = connect({ hostname, port }, { secureTransport: "on" }); await socket.opened; return socket; } release(hostname: string, port: number, socket: Socket): void { const key = `${hostname}:${port}`; const sockets = this.pool.get(key) || []; if (sockets.length < 3) { sockets.push(socket); this.pool.set(key, sockets); } else socket.close(); } }多协议网关
用同一个 Worker 暴露多种协议的连通性探测能力(以 Redis PING 为例):
interface Protocol { name: string; defaultPort: number; test(host: string, port: number): Promise<string>; } const PROTOCOLS: Record<string, Protocol> = { redis: { name: 'redis', defaultPort: 6379, async test(host, port) { const socket = connect({ hostname: host, port }); try { const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode('*1\r\n$4\r\nPING\r\n')); writer.releaseLock(); const reader = socket.readable.getReader(); const { value } = await reader.read(); return new TextDecoder().decode(value || new Uint8Array()); } finally { await socket.close(); } } } }; export default { async fetch(req: Request): Promise<Response> { const url = new URL(req.url); const proto = url.pathname.slice(1); // /redis const host = url.searchParams.get('host'); if (!host || !PROTOCOLS[proto]) return new Response('Invalid', { status: 400 }); const result = await PROTOCOLS[proto].test(host, parseInt(url.searchParams.get('port') || '') || PROTOCOLS[proto].defaultPort); return new Response(result); } };常见坑与排查指南
仓库的 gotchas.md 汇总了平台限制与高频错误,这里全部列出。
平台限制
| 限制项 | 值 |
|---|---|
| 每个请求的最大并发 socket 数 | 6(硬限制) |
| socket 生命周期 | 与请求同生命周期 |
| 连接超时 | 平台决定,无配置项 |
- 超出 6 个连接会直接抛错,解决方式是分批处理(每批 ≤6):
for (let i = 0; i < hosts.length; i += 6) { const batch = hosts.slice(i, i + 6).map(h => connect({ hostname: h, port: 443 })); await Promise.all(batch.map(async s => { /* use */ await s.close(); })); }- 被屏蔽的目标:Cloudflare 自身 IP(如 1.1.1.1)、localhost(127.0.0.1)、端口 25(SMTP)、Worker 自己的 URL,均因安全原因被禁止。解决办法是使用公网 IP 或 Tunnel 主机名。
- 作用域限制:在全局作用域创建的 socket 会失败,因为 socket 与请求生命周期绑定,必须在 handler 内创建(
export default { async fetch() { const socket = connect(...); } })。
常见错误对照
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
proxy request failed | 目标被屏蔽(Cloudflare IP / localhost / 25 端口)、DNS 失败、网络不可达 | 校验目标、使用 Tunnel 主机名、try/catch 兜底 |
TCP Loop detected | Worker 连接到了自身 | 连接外部服务,不要连 Worker 自己的主机名 |
Port 25 prohibited | SMTP 端口被屏蔽 | 改用 Email Workers API 处理邮件 |
socket is not open | 关闭后还在读写 | 始终用 try/finally 保证关闭顺序 |
| 连接超时 | API 无内置超时 | 用Promise.race()自行实现(见上文超时模式) |
TLS/SSL 陷阱
- StartTLS 时机:必须先发送协议专用 STARTTLS 命令、等待服务端 OK,再调用
socket.startTls(),过早升级会失败。 - 证书校验:自签名证书会失败。要么使用正规证书,要么走 Cloudflare Tunnel(由 Tunnel 处理 TLS 终止)。
性能与数据处理的坑
- 不建连接池:每次请求新建连接开销大。数据库场景直接换 Hyperdrive(内置池化)。
- 不用 Smart Placement:后端延迟高时,在
wrangler.jsonc开启{ "placement": { "mode": "smart" } }。 - 忘记关闭 socket:资源泄漏,务必
try/finally { await socket.close(); }。 - 假设一次读完:TCP 分块到达,必须循环
reader.read()直到done === true。 - 编码错误:明确指定编码,如
new TextDecoder('iso-8859-1').decode(data)。
调试技巧
- 记录连接信息:
const info = await socket.opened; console.log(info.remoteAddress); - 先用公网服务验证:可先用 tcpbin.com:4242 之类的 echo 服务器测试基本连通性
- 验证 Tunnel:
cloudflared tunnel info <name>与cloudflared tunnel route ip list(私有网络路由场景)
最佳实践清单
综合 README.md 与全系列文档,落地时遵循以下原则:
- 始终关闭 socket——用
try/finally包裹,finally中await socket.close(); - 校验目标地址——用白名单防 SSRF,绝不让用户输入直接决定连接目标;
- 数据库用 Hyperdrive——连接池与缓存远胜裸 TCP,且省去实现 Postgres/MySQL 线上协议的复杂度;
- HTTP 优先
fetch()——只有非 HTTP 或需要线上协议控制时才用 TCP; - 配合 Smart Placement——降低到私有网络的端到端延迟;
- 分批处理并发——单请求内并发 socket 不超过 6 个;
- 密钥走
wrangler secret,地址等非敏感配置走vars,并用env区分 staging/production。
相关技术导航
- Hyperdrive:PostgreSQL/MySQL 连接池化与缓存
- Cloudflare Tunnel:安全访问私有网络(含 configuration.md 的 ingress 规则与 networking.md 网络预检)
- Smart Placement:让 Worker 自动就近后端
- Workers:承载本文所有代码的运行时
- 同目录配套阅读顺序:api.md(接口与类型)→ configuration.md(Wrangler 配置与 Tunnel 集成)→ patterns.md(实战模式)→ gotchas.md(限制与排查)
- VPC Services(beta, 2025+):仅 HTTP 的 service binding、内置 SSRF 保护,属独立文档体系,后续可关注
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考