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 入门:从 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 项目。
