解决问题
目前这个博客套了阿里云和腾讯云的 CDN,忽然意识到,如果不设置请求头,Typecho 的后端可能没办法获取到正确的 IP,都是阿里云或者腾讯云的回源节点
我用的是 Caddy,官方文档看着有点懵,不如直接参考 Cloudflare 官方给的示例:Restoring original visitor IPs · Cloudflare Support docs
配置文件大概是这样的:
https://example.com {
reverse_proxy localhost:8080 {
# Sets X-Forwarded-For as the value Cloudflare gives us for CF-Connecting-IP.
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
}
}
CF-Connecting-IP
是 Cloudflare 他们自己弄的头,我用的是阿里云 ESA,这里可以自定义标头,默认为 ali-real-client-ip
,然后我自定义为 X-Forwarded-For
,因为腾讯云默认里面为 X-Forwarded-For
之后我的 Caddy 配置文件为:
www.juniortree.com {
root * /var/www/typecho
encode gzip zstd
php_fastcgi unix//run/php/php8.2-fpm.sock {
env REMOTE_ADDR {http.request.header.x-forwarded-for}
}
file_server
}
但是这样还是不能解决问题,你现在测试还是会发现,访客的 IP 还是 CDN 回源节点的 IP,此时需要对 Typecho 的配置文件做些许修改
我找了几个比较热门的,都是说在 config.inc.php
最后添加:
//防止 CDN 造成无法获取客户真实 IP 地址
if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $list[0];
}
但是别人用了都说好,我用就是不舒服,所以重新找了个大佬的教程:【原创】Typecho 在使用 CDN 的情况下获取真实客户端 IP(非覆盖 $\_SERVER) - 青石坞
在配置文件 (config.inc.php
) 中加入如下配置:
// 定义 IP 来源
define('__TYPECHO_IP_SOURCE__', 'HTTP_X_FORWARDED_FOR');
此时就能获得正确 IP 了
延伸
但,目前这样是有问题的
任何客户端都可以自定义 X-Forwarded-For
头部,例如通过 curl:
curl -H "X-Forwarded-For: 1.2.3.4" https://www.juniortree.com
所以最好的方法是你去定义一个自定义的,或者使用 CDN 服务商默认提供给你的,CDN 在收到用户请求后,会把真实用户 IP 提取出来(从 TCP 层),然后由系统在请求头中插入如 Ali-CDN-Real-IP
、CF-Connecting-IP
等字段,并传给你的源站服务器
比如说,我前面用的是阿里云和腾讯云的 CDN,他们都提供了自定义的方法,我就自定义一个我自己的请求头:
之后我写了一个 php 页面,尝试来打印一下请求头+服务器变量
<?php
header('Content-Type: text/plain');
// 打印 $_SERVER 中的和 IP、请求头相关的信息
echo "==== _SERVER ====\n";
$ip_keys = [
'REMOTE_ADDR',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CF_CONNECTING_IP',
'HTTP_ALI_CDN_REAL_IP',
'HTTP_X_CLIENT_IP',
];
foreach ($ip_keys as $key) {
if (isset($_SERVER[$key])) {
echo "$key: {$_SERVER[$key]}\n";
}
}
// 打印全部请求头(仅 PHP 7.3+)
echo "\n==== getallheaders() ====\n";
if (function_exists('getallheaders')) {
foreach (getallheaders() as $name => $value) {
echo "$name: $value\n";
}
} else {
echo "getallheaders() 不可用\n";
}
?>
第一次在 Caddy 里面我们使用 X-Forward-For
,然后来尝试伪造:
liueic@HUAWEI-MateBook-Go-ARM-Version ~ % curl -s https://www.juniortree.com/test.php \
-H "X-Forwarded-For: 1.2.3.4" \
-H "X-Real-IP: 5.6.7.8" \
-H "CF-Connecting-IP: 9.9.9.9"
==== _SERVER ====
REMOTE_ADDR: 1.2.3.4, 124.127.xxx.xx
HTTP_X_FORWARDED_FOR: 121.89.xx.xxx
HTTP_X_REAL_IP: 5.6.7.8
HTTP_CF_CONNECTING_IP: 9.9.9.9
==== getallheaders() ====
X-Real-Ip: 5.6.7.8
Accept: */*
X-Forwarded-Proto: https
Ali-Real-Client-Ip: 124.127.xxx.xx
Cdn-Loop: esa;loop=1
Ali-Ip-Country: CN
Content-Type:
Eagleeye-Traceid:
X-Forwarded-For: 121.89.xx.xxx
Ali-Ip-City: Beijing
User-Agent: curl/8.7.1
Content-Length: 0
Via: 2.0 Caddy
Host: www.juniortree.com
X-Forwarded-Host: www.juniortree.com
返回结果 REMOTE_ADDR: 1.2.3.4, 124.127.203.125
,其中带有 1.2.3.4
,说明被成功伪造了
之后在 Caddy 里面使用我们在 CDN 中自定义的头部:
liueic@HUAWEI-MateBook-Go-ARM-Version ~ % curl -s https://www.juniortree.com/test.php \
-H "X-Forwarded-For: 1.2.3.4" \
-H "Tree-Real-Client-Ip: 5.6.7.8"
==== _SERVER ====
REMOTE_ADDR: 124.127.xxx.xx
HTTP_X_FORWARDED_FOR: 121.89.xxx.xx
==== getallheaders() ====
X-Forwarded-Host: www.juniortree.com
Eagleeye-Traceid:
Accept: */*
Via: 2.0 Caddy
Content-Type:
X-Forwarded-Proto: https
Content-Length: 0
X-Forwarded-For: 121.89.xxx.xx
Host: www.juniortree.com
Ali-Ip-City: Beijing
Tree-Real-Client-Ip: 124.127.xxx.xx
Ali-Ip-Country: CN
Cdn-Loop: esa;loop=1
User-Agent: curl/8.7.1
此时并没有被成功伪造,而是被覆写了,这样的方式更安全,尤其是在你有审计或者 WAF 的情况下