Showing Posts From

Cloudflare

Cloudflare 525 SSL handshake failed 排查全流程

某天访问站点,Cloudflare 直接抛了个 525: { "title": "Error 525: SSL handshake failed", "status": 525, "detail": "The SSL/TLS handshake between Cloudflare and the origin server failed." }意思就是——Cloudflare 收到了请求,但它和你的源站之间的 TLS 握手没建起来。不是 Cloudflare 挂了,是源站 SSL 有问题。 常见 8 种原因 1. 源站证书失效 在源站本机测: echo | openssl s_client -servername example.com -connect example.com:443看输出里有没有: Verify return code: 0 (ok)如果出现 certificate has expired,证书过期了,续签或换证书。 2. Nginx 证书和私钥不匹配 分别算 MD5,两边必须一致: openssl x509 -noout -modulus -in cert.pem | md5sum openssl rsa -noout -modulus -in key.pem | md5sum不一致就是配错了。 3. TLS 版本太老 Cloudflare 现在只接受 TLS 1.2 / 1.3。Nginx 配置里明确写清: ssl_protocols TLSv1.2 TLSv1.3;4. Cipher Suite 不兼容 某些老配置写得太激进(HIGH:!aNULL:!MD5 只留少数几个),Cloudflare 找不到共同 cipher。用 nmap 探一下源站支持哪些: nmap --script ssl-enum-ciphers -p 443 <origin-ip>5. Cloudflare 用了 Full (strict) 模式 Dashboard → SSL/TLS → Overview 里检查模式:Flexible:Cloudflare 到源站走 HTTP Full:源站要有证书(不校验) Full (strict):源站必须有效证书、链完整、域名匹配如果是 strict,用 Cloudflare Origin CA 签的证书最省事。 6. 证书链不完整(最常见的坑) 如果 openssl s_client 里出现: verify error:num=20:unable to get local issuer certificate多半是 Nginx 只配了 cert.pem 而不是 fullchain.pem。 正确写法: ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;7. 源站 443 没监听 直接测: curl -vk https://<origin-ip>如果 Connection refused,Nginx 没在这个 IP 上监听 443。防火墙、云安全组同样要放行。 8. IPv6 配置错误 Cloudflare 可能优先走 IPv6,如果 Nginx 没监听或者防火墙漏放,就 525。 dig AAAA example.com出现 AAAA 记录时,Nginx 记得写: listen 443 ssl; listen [::]:443 ssl;快速定位思路 先在源站本机执行: openssl s_client -connect localhost:443 -servername example.com本机就 handshake failure:问题在 Nginx 或证书 本机 OK 但外网 525:问题在 IP 路由 / 防火墙 / IPv6 / Cloudflare 连的不是这台机器再看 Cloudflare 实际解析到哪个 IP: dig A example.com dig AAAA example.com用 --resolve 强制走特定 IP 复现: curl -I https://example.com --resolve example.com:443:<origin-ip>一句话总结 525 = Cloudflare 和源站的 TLS 握手失败。八成落在 证书链不完整 或 Full (strict) 模式下证书不合规,用 openssl s_client 从源站本机开始逐层排查。

用 Cloudflare Worker 做多域名反代 + Error 1003 的解法

一台 VPS 上跑了七八个服务,每个都占一个端口。想用一个域名的多个子域直接映射过去,还不想暴露 IP。上 Nginx 有点重——一个 Cloudflare Worker 就够了。 Worker 代码 思路:读请求的 Host,从 map 里查目标 IP:端口,重新 fetch 转发过去。 export default { async fetch(request) { const host = request.headers.get("host"); const map = { "app.example.com": "http://origin.example.com:51118", "api.example.com": "http://origin.example.com:51117", "admin.example.com":"http://origin.example.com:51116", "shop.example.com": "http://origin.example.com:51115", }; const target = map[host]; if (!target) return new Response("Host not found", { status: 404 }); const url = new URL(request.url); const proxyUrl = target + url.pathname + url.search; const proxied = new Request(proxyUrl, { method: request.method, headers: request.headers, body: (request.method === "GET" || request.method === "HEAD") ? undefined : request.body, redirect: "manual", }); // 把 Host 改成目标域,避免源站按 Host 路由时错乱 proxied.headers.set("host", new URL(target).host); return fetch(proxied); }, };部署路径Cloudflare Dashboard → Workers & Pages → Create Worker 粘代码,Deploy Worker 详情 → Settings → Triggers → Routes 添加路由:app.example.com/*、api.example.com/* 等 对应的 DNS 记录必须 开小黄云(Proxied),否则 Worker 拦不到动态版本(不用维护 map) 如果子域名和端口有规律(比如 app → 51118、api → 51117),可以从子域直接算端口: const portMap = { app: 51118, api: 51117, admin: 51116, shop: 51115 };const sub = host.split(".")[0]; const port = portMap[sub]; if (!port) return new Response("Not found", { status: 404 });const target = `http://origin.example.com:${port}`;以后加子域只改 map 一处。 Error 1003:direct IP access not allowed 第一次绑域名很容易撞到: error code: 1003含义是"直接用 IP 访问 Cloudflare 不允许"。多半是因为你把域名 CNAME 到了 xxx.workers.dev,Cloudflare 就把这当成"客户端在裸 IP 访问"。 正解:不要 CNAME 到 workers.dev,走 Worker 路由。 DNS 那边:加一条 A 记录,IP 随便填(比如 192.0.2.1),一定要开小黄云:类型 名称 内容 代理A app 192.0.2.1 ProxiedA api 192.0.2.1 ProxiedWorker 那边:Settings → Triggers → Routes,加上 app.example.com/* 等。 之后请求走的是 Worker,A 记录里那个 IP 只是 Cloudflare 需要一条 DNS 记录而已,不会真的连过去。 需要 WebSocket / SSE Workers 支持 WebSocket 升级,fetch 里带上 Upgrade 头就能透传: if (request.headers.get("Upgrade") === "websocket") { return fetch(target, request); }SSE (text/event-stream) 靠 Response.body 流式返回就行,Worker 会自动保持长连接。 顺带的好处源站真实 IP 不会暴露给客户端 免费额度撑得住中小站(10 万请求/天) 加 WAF、限流、缓存全在 Cloudflare 控制台一键切换一句话总结 一个 Worker + 一张 host→port 表 = 多域名反代。踩到 Error 1003 别慌,DNS 走 A 记录 + 小黄云 + Worker Route,不要 CNAME 到 workers.dev。

Cloudflare Workers 开发指南:路由、反向代理、KV 存储和 TCP Socket

Cloudflare Workers 是部署在 Cloudflare 全球边缘节点上的无服务器运行环境,写 JavaScript/TypeScript 即可,无需管理服务器。 快速开始 npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地调试 npm run deploy # 部署基础结构 export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };路由处理 export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "soulock" }); } return new Response("404", { status: 404 }); }, };获取 query 参数: const name = url.searchParams.get("name");获取请求体: const data = await request.json();反向代理 export default { async fetch(request) { const url = new URL(request.url); const target = "https://example.com"; const targetUrl = new URL(url.pathname + url.search, target); const headers = new Headers(request.headers); headers.set("Host", targetUrl.host); const resp = await fetch(targetUrl.toString(), { method: request.method, headers, body: request.body, redirect: "follow", }); return new Response(resp.body, { status: resp.status, headers: resp.headers, }); }, };流式返回(AI 接口必须用这种写法): // 正确:保持流式 return new Response(resp.body, resp);// 错误:会阻塞到完整响应 const text = await resp.text();CORS 处理 function corsHeaders() { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", }; }export default { async fetch(request) { if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders() }); } const resp = await fetch("https://example.com"); return new Response(resp.body, { status: resp.status, headers: { ...Object.fromEntries(resp.headers), ...corsHeaders() }, }); }, };环境变量 # wrangler.toml [vars] API_KEY = "xxx"代码里用 env.API_KEY。生产环境推荐用 secret(不写入代码): wrangler secret put API_KEYKV 存储 [[kv_namespaces]] binding = "MY_KV" id = "xxxx"await env.MY_KV.put("key", "value"); const value = await env.MY_KV.get("key");D1 数据库(SQLite) wrangler d1 create mydb[[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxx"const result = await env.DB.prepare("SELECT * FROM users").all();定时任务 export default { async scheduled(event, env, ctx) { // 定时执行的逻辑 }, };[triggers] crons = ["*/5 * * * *"]TCP Socket Workers 支持主动发起 TCP 连接(不能监听 TCP): import { connect } from "cloudflare:sockets";const socket = connect({ hostname: "example.com", port: 80 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); writer.releaseLock(); return new Response(socket.readable);常见用途:WebSocket → TCP 隧道,代理 Redis、PostgreSQL 等 TCP 协议。 Workers 不能替代大流量 TCP 服务(有连接数和执行时间限制),适合轻量中转。 推荐:Hono 框架 npm i honoimport { Hono } from "hono";const app = new Hono();app.get("/", (c) => c.text("hello")); app.get("/user", (c) => c.json({ name: "soulock" }));// 反向代理 app.all("*", async (c) => { const url = new URL(c.req.url); const target = "https://example.com" + url.pathname + url.search; const resp = await fetch(target, { method: c.req.method, headers: c.req.raw.headers, body: c.req.raw.body }); return new Response(resp.body, resp); });export default app;适合与不适合的场景适合 不适合API 网关 / 鉴权 大型 CPU 运算AI 接口中转 ffmpeg / 视频编码Webhook / Bot Puppeteer反向代理 本地文件读写短链接 / 边缘缓存 高并发大流量 VPNWorkers 没有 fs、child_process、net 等 Node.js 原生模块,运行在沙盒环境中。

Cloudflare Workers 入门:从 hello world 到 KV / D1

Cloudflare Workers 本质上是"边缘 JS/TS 函数"——写一段代码,部署到全球 300 多个节点,用户就近访问。适合做 API 代理、Webhook、AI 接口中转、鉴权网关,比自建 Nginx 或买 VPS 省事。 项目起步 一个命令拉起脚手架: npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地起 wrangler dev npm run deploy # 部署到生产第一次部署会让你登录: npx wrangler login最小骨架 新版模块化写法: export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };访问 https://<name>.<account>.workers.dev 就能看到。 路由分发 原生没有路由框架,URL 手动分: export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "alice", age: 18 }); } if (url.pathname === "/health") { return new Response("ok"); } return new Response("Not Found", { status: 404 }); }, };要更完善的路由体验就上 Hono: import { Hono } from "hono"; const app = new Hono(); app.get("/api/user", (c) => c.json({ name: "alice" })); export default app;读取请求 // query string const url = new URL(request.url); const name = url.searchParams.get("name");// JSON body const data = await request.json();// 表单 const form = await request.formData();转发外部 API(最常见用法) export default { async fetch(request, env) { const resp = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` }, }); return new Response(resp.body, resp); }, };常见用途:藏 API Key、破跨域、加限流、加缓存、AI 接口中转。Key 千万别写死在代码里,走 wrangler secret。 KV:类 Redis 键值 wrangler.toml: [[kv_namespaces]] binding = "MY_KV" id = "xxxxxxxx"代码: await env.MY_KV.put("username", "alice"); const v = await env.MY_KV.get("username"); await env.MY_KV.delete("username");KV 是最终一致(全球复制有几秒延迟),做缓存 / 配置 / session 都很合适。 D1:SQLite [[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxxxxxx"用法: const rows = await env.DB .prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .all();参数化查询是标配,别拼字符串——同样有 SQL 注入。 环境变量 vs Secret 普通变量写 wrangler.toml: [vars] APP_NAME = "my-app"敏感 key 用 secret,不进代码库: npx wrangler secret put OPENAI_API_KEY两种都通过 env.XXX 访问。 免费额度和局限免费 10 万次请求/天,付费 10 美元/月起 单请求 CPU 时间限制(免费 10ms,付费 50ms+,最高 5 分钟) 不能开 socket、不能持久保存本地文件 Node.js 内置模块要靠 nodejs_compat 兼容标志一句话总结 Workers = 全球边缘 JavaScript。npm create cloudflare@latest 起步,fetch 转发是 90% 用例,配上 KV/D1 就能扛住一个中等 API 项目。