真实客户端 IP
CF-Connecting-IP 头本来就送到了 nginx(它就是真实客户端 IP)。要把它变成 $remote_addr, 靠的是 nginx 自带的 ngx_http_realip_module(Debian 官方 nginx 与本模块的构建都编了它):
nginx
http {
cfd_tunnel_token eyJhIjoi...;
cfd_tunnel_inject_source_addr 192.0.2.1; # 0.13.0
server {
listen 127.0.0.1:8080;
set_real_ip_from 192.0.2.1;
real_ip_header CF-Connecting-IP;
# 业务照旧
}
}为什么值得配这一条
ngx_http_realip_module 判断「信不信这条连接带来的头」的唯一依据是 ngx_cidr_match(c->sockaddr, set_real_ip_from 的集合) —— 它只看连接的对端地址,不管端口, 也不管这个地址在本机存不存在。而虚拟连接的对端地址正是本模块填的:
cfd_tunnel_inject_source_addr | 注入连接的 $remote_addr(realip 之前) | set_real_ip_from 必须写 | 谁能伪造 CF-Connecting-IP |
|---|---|---|---|
| 不写(默认) | 127.0.0.1 | 127.0.0.1(= 整个回环) | 云端访客不能;本机任意能连到注入端口的进程都能 |
192.0.2.1 | 192.0.2.1 | 192.0.2.1(只这一个地址) | 云端访客不能(RFC 5737 地址公网路由不到);本机非特权进程也不能(bind() 不属于本机的源地址会 EADDRNOTAVAIL) |
不配 realip 时,注入请求的 $remote_addr 就是那个假地址本身 —— 日志里所有隧道请求都来自 同一个地址,这是能一眼看出来的地方。
要点与坑
- 默认不配 = 与 cloudflared 对齐。 真 cloudflared 直面本机源站时,源站看到的就是
127.0.0.1,所以默认值刻意保持它(老配置的set_real_ip_from 127.0.0.1;照旧管用)。 - 配了只影响对端地址。
c->local_sockaddr没动 ——$server_addr/$server_port/ngx_connection_local_sockaddr()仍是真实监听地址,listen与server_name匹配、 静态文件、proxy_pass全都不受影响。 - 先检查按
127.0.0.1写的规则。allow/deny、limit_req_zone/limit_conn_zone用$binary_remote_addr做键的,配了这条之后都不再命中隧道流量。要么按新地址再写一份, 要么别配这条。 $remote_port恒为空串。 虚拟连接没有真实端口(端口恒为 0),realip 采纳头部之后也一样空 —— 头部里没有端口信息。- 只收单个地址。 不写网段(网段是
set_real_ip_from那边的事)、不写端口;IPv4 / IPv6 都行 (IPv6 例如 RFC 3849 的2001:db8::1)。写错(网段 / 带端口 /0.0.0.0/::/ 拼错 / 同一处写两遍)都是nginx -t直接报错,不会静默退回默认值。 - 只挡得住非特权进程。 本机 root / 带
CAP_NET_ADMIN的进程仍可用IP_FREEBIND/IP_TRANSPARENT从任意源地址连进来伪造。这条指令收窄的是「本机任何用户都能冒充实访客 IP」, 不是「本机已被攻破也安全」。 - 块内可以一条隧道一个地址(多条隧道指向不同源站、各自
set_real_ip_from时用得上):
nginx
cfd_tunnel { name a; token_file /etc/nginx/cloudflared/a.token; inject_listen 127.0.0.1:8080; inject_source_addr 192.0.2.1; }
cfd_tunnel { name b; token_file /etc/nginx/cloudflared/b.token; inject_listen 127.0.0.1:8081; inject_source_addr 198.51.100.7; }配了就会在 error_log 里留一条启动 NOTICE(地址 + 配套的 set_real_ip_from 写法)。