注入与原生 HTTP 链
模块最核心的一句话:边缘送来的请求就是 nginx 的普通请求。
text
Cloudflare 边缘
│ 一条 h2 流(请求头、请求体 DATA)
▼
本模块:造一条虚拟连接 → ngx_http_init_connection()
│
▼
nginx 原生 HTTP 处理链
server_name 匹配 → location 匹配 → rewrite → proxy_pass / return / 变量 / access_log
│
▼
本模块:把响应(含 chunked 解块、trailer)写回那条 h2 流因此业务配置照旧写 —— 你在 cloudflared 时代为 127.0.0.1:8080 写的 server {}, 现在原样有效,只是「谁把请求送进来」从 cloudflared 变成了模块自己。
注入目标怎么定
| 写法 | 含义 |
|---|---|
cfd_tunnel_inject_listen 127.0.0.1:8080; | 请求送进这个 listen |
不配 inject_listen(扁平) | 由注入器在已有监听器里自动挑一个(地址族/本地地址合适的那个) |
不配 + 开了 remote_config | 由命中规则的 service 端口决定(见 远程配置下发) |
块内 inject_listen 8090; | 只写端口:在该端口的监听地址里找唯一的那个,有歧义就跳过这条隧道 |
注入的虚拟连接是一条真正由 nginx 分配的连接:连接槽来自 ngx_get_connection()、连接池来自 ngx_create_pool()、c->fd 是 socketpair(AF_UNIX) 的真实 fd(只作连接身份载体,数据一个字节 都不经过它),本地地址借自监听器(c->local_sockaddr)。请求字节由模块通过 c->recv 喂进 nginx 的 HTTP 解析器 —— 所以仍然不需要 UDS、不需要额外进程、不需要共享内存。
详细机制(含「为什么绝不能碰 socketpair 的对端」这条不变式)见 注入层。
哪些东西是原生的
| 能力 | 是否原生 | 说明 |
|---|---|---|
server_name / location / if / rewrite | ✅ | 按注入请求的 Host 与 path 正常匹配 |
proxy_pass(含 upstream、keepalive、buffering) | ✅ | 模块只是请求的来源,不是代理的替代 |
变量($host、$request_uri、$http_*…) | ✅ | 由 nginx 求值 |
access_log / error_log | ✅ | 与普通请求同形 |
| gzip / gunzip 过滤器 | ✅ | 含上游 gzip 的透明处理 |
| 请求体流式上传 | ✅ | content-length 已知与未知(chunked)都支持 |
| 响应体流式下载 + 背压 | ✅ | 高水位 CFD_RESP_HWM = 256 KiB |
| WebSocket / 升级类流 | ✅ | 升级头由模块合成,101 之后双向裸字节转发 |
set_real_ip_from / realip | ✅ | 配合 inject_source_addr |
| 裸 TCP / ssh / rdp / smb / bastion | ❌ | 传输层直接 RST_STREAM(REFUSED_STREAM),不进 nginx |
两条必须记住的例外
1. 裸 TCP 隧道流:直接丢弃
判据与 cloudflared 一致:流头里 Cf-Cloudflared-Proxy-Src 非空 ⇒ 这条流是 TypeTCP(非 HTTP 服务)。本模块的注入点在 nginx 的 HTTP 链上,承载不了裸 TCP,于是:
- 回
RST_STREAM(REFUSED_STREAM)(0x7),不回 502、不进任何 location; - 立刻释放流,留一条 ERR 日志。
这是用户定案的规则:宁可响亮失败,也绝不静默降级成 HTTP。
2. 大请求要自己放开 nginx 的门槛
| 指令 | 默认 | 影响 |
|---|---|---|
client_max_body_size | 1m | 超了 nginx 自己回 413 |
client_body_timeout | 60s | 上传太慢会断 |
这是 nginx 对普通请求的正常行为 —— 从 cloudflared 换过来时,原来能传的 1 MiB 以上文件 要显式放开:
nginx
server {
listen 127.0.0.1:8080;
client_max_body_size 512m;
client_body_timeout 300s;
}WebSocket
nginx
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 空闲长连接
}升级由模块完成:请求侧合成 Upgrade / Connection,响应 101 被翻译成 h2 的 200 + cf-cloudflared-response-headers,之后两个方向都是裸字节转发(含 4 MiB 级别背压路径)。
一条流是怎么收尾的
| 场景 | 行为 |
|---|---|
| 正常结束 | 响应写完后流关闭,记账释放(连接级配额一并归还) |
| 隧道中途被掐断 | 请求置错并摇醒写侧,避免请求永不收尾 |
| 源站返回错误 | 按 nginx 的语义回 h2 状态码(不是伪装成 200) |
半路收到 RST_STREAM | 放弃该请求,不污染同一连接上的其他流 |