有时候,我们需要在网页中显示访问者的 IP 地址。比如,需要获取某客户端的公网 IP,然后添加为域名的解析记录,使域名成为 DDNS 域名。如果是现成的 DDNS 服务,大概能从请求中提取客户端 IP 地址,而倘若是自行请求 DNS 服务商 API 修改记录,那就需要指定 A/AAAA 记录的值为何。
本文假定这样一个需求:直接在浏览器输入某个网址(或者用 curl 请求一个 URL),响应内容为客户端的 IP 地址。其实有很多现成的服务,自己搭一个也很容易。但是公开服务的可靠性存疑,自己搭又需要专门购置域名,配置服务器 vhost,似乎有割鸡用宰牛刀之嫌,因此借助 CDN 服务似乎是一条可行之路。
自有 Nginx 配置
一般地,如果要搞一个显示 IP 地址的网页,倒也不必借助动态语言(如 PHP),Nginx 可以直接返回内容:
location / {
default_type text/html;
return 200 $remote_addr;
}
要套一层 CDN 的话,无法直接从 TCP 报文中拿到真正的客户端 IP 地址,也就不能输出 $remote_addr
变量。CDN 服务商会有文档,指示客户端 IP 地址可以从某个请求头(request header)中获取,常见的如 X-Forwarded-For
、X-Real-IP
。以 X-Real-IP
为例,改写为:
location / {
default_type text/html;
return 200 $http_x_real_ip;
}
然后再套一层 CDN 即可,注意关闭 CDN 的缓存。
但这显然不是本文重点。这么做仍然是拿着宰牛刀,并且自建的服务可用性并不因为套上 CDN 而提高,因为关闭缓存后 CDN 每次都会回源。源服务器宕掉后,通过 CDN 访问也会失败。那么一个常见的想法是,如果 CDN 不回源直接能输出 IP 地址就好了——这就是利用边缘节点响应内容。边缘节点可以理解为用户直接访问到的服务器节点。
Cloudflare Workers
全球最著名的 CDN 服务商 Cloudflare 直接就有边缘计算产品,称作 Cloudflare Workers。免费使用。登录以后,创建 Worker,会有一个默认示例,稍作修改即可。根据帮助文档,Cloudflare 会将用户真实 IP 放入 CF-Connecting-IP
,于是可以这样写:
addEventListener("fetch", (event) => {
event.respondWith(
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});
async function handleRequest(request) {
const { pathname } = new URL(request.url);
if (pathname === "/ip/") {
const ip = request.headers.get("cf-connecting-ip")
return new Response(ip, {
headers: { "access-control-allow-origin": "https://ip.shansing.net" },
status: 200,
});
}
return fetch("https://welcome.developers.workers.dev");
}
这段代码还顺带限定路径为 /ip/
,加上回源需要用到的响应头(response header)。接着 Save and Depoly,等待完毕即可。
注意,根据帮助文档,用户真实 IP 地址也会体现在 X-Forwarded-For
请求头中。然而,这个请求头因为它本身的含义,可能有多个由逗号分隔的 IP 地址。为了避免解析麻烦,直接用 CF-Connecting-IP
就行。
现在,全球大量的边缘节点都能显示你的 IP 地址了。在国外,应该相当快。不太清楚有无国内节点。按道理,其默认提供的域名 workers.dev 没有备案,也就不能使用国内服务器,但 ping 出的延迟却又低到好似在境内。总之,得益于 Cloudflare 的架构,这个服务高可用,访问又快速。
可以点击这里查看示例。
又拍云
因为又拍云赞助本站 CDN,所以我也用又拍云搭了一个。又拍云 CDN 没有高级的边缘计算服务,但有所谓的“边缘规则”。阅读它的边缘规则语法后,可以添加一条优先级最高的边缘规则,在编程模式写入以下规则并开启 break:
$WHEN($EQ($_URI, /ip/))
$ADD_RSP_HEADER(content-type, text/html; charset=UTF-8, 1)
$ADD_RSP_HEADER(access-control-allow-origin, https://ip.shansing.net, 1)
$EXIT(200, $_IP)
“URI字符串提取”应该可以随便填,不知道填什么就写 ^/ip/$
吧。
又拍云 CDN 的优势在于国内加速,对国内访客比较友好。部分地区的移动及小运营商宽带,访问国外服务器与访问国内的网络出口是不同的,造成目标服务器获得的 IP 地址也不同。如果我们在国内,DDNS 客户端也在国内,有时候会不得不像这样用国内服务获取 IP 地址。
示例:https://cdn.shansing.net/edge/ip/
高级用途
现在我们知道,有的用户上网因为出口不同而有多个 IP 地址。而 CDN 也是有很多节点的。假设选取一个联通的节点,访问刚搭建的服务,我就能知道我访问联通内容的出口 IP。选取移动,就能知道访问移动服务器的。更具体地,如果我是湖南移动用户,选取上海电信的 CDN 节点访问,也许会发现我的出口 IP 地址属于江苏移动。这对于小运营商(如长城宽带)非常有用,因为这些宽带访问其他运营商内容时,大多会“流量穿透”到其他运营商。
要完成这个选取-访问的操作,也不难。
首先,查看相应 CDN 节点的 IP 地址。可以用一些“多个地点 Ping 服务器”的在线工具确定,比如这个。输入 CDN 域名,我的是 cdn.shansing.net
,提交,可以看到国内外各地解析出来的 CDN 节点的 IP 地址。选一个想要的,比如选福建厦门联通的 36.248.208.254
。
然后,在自己电脑上指定 CDN 域名解析为这个 IP 地址。可以修改操作系统 hosts 文件来完成,然后在浏览器中输入访问。但我比较喜欢命令行工具 curl:
curl https://cdn.shansing.net/edge/ip/ --resolve cdn.shansing.net:443:36.248.208.254
按回车执行,我就看到当前网络访问厦门联通的内容时,他们接收到的 IP 地址。一查,是湖南长沙移动;而我的宽带不是移动提供的,说明有流量穿透现象。
本文介绍的方法也可以用来输出别的东西,比如当前时间,这样又可以利用 HTTP 网页同步服务器时间啦。