用阿里云 ESA Pages + Functions 做一个IP 检测网页

这篇文章记录一次很“工程味”的小实践:在 阿里云 ESA 上,用 Pages 静态站点 + Functions 做一个极简的 IP 检测页面。

目标不是做复杂的“探针矩阵”,而是更务实地告诉访问者:

  • 本次访问更偏向 IPv4 还是 IPv6
  • 如果是 IPv6:提示「恭喜,你能访问纯 IPv6 网站」
  • 同时展示 IP 归属地,并按归属地打出一条热情招呼

这非常适合放在 test-ipv6.jsw.ac.cn 这种测试站点上,用来快速判断访客的网络环境。

一、准备:你需要什么

  • 一个 ESA 的站点(Pages 项目)
  • 域名已在 ESA 里做 NS 接入(托管 DNS)
  • 一个绑定到 Pages 项目的域名,例如:test-ipv6.jsw.ac.cn

为什么推荐 NS 接入?
因为后续你在 ESA 控制台里改 DNS、开代理加速、绑域名,同时便于ESA能够自动申请SSL证书。

二、先弄清楚:域名绑定 vs 路由

在 ESA 里,把请求导向 Pages/Functions,常见有两种入口:

1)域名绑定:一个域名整体交给 Pages,Functions

适合这种“纯 Pages/Functions 站点”:

  • / 是静态页面
  • /api/ip 是函数接口
  • 其他路径要么静态资源,要么 404

优点:配置简单、心智负担小
缺点:它是“整站接管”,不适合你还要回源跑传统网站的场景

2)路由:只接管某些路径,其余仍走回源

适合混合架构:

  • /api/* 交给 Functions
  • 其他路径继续走源站

坑点:如果你在 NS 托管下使用“函数路由”,你依然需要:

  • 为那个域名补齐 DNS 记录
  • 并开启 代理/加速(否则可能根本不会进入 ESA 的边缘链路)

本项目搭建用的是路由,因为能够让跟随站点管理同步启用全球ipv6

三、在 ESA 上创建 Pages 项目

你可以直接复用现有 Pages 项目,只要满足:

  • 能部署 public/index.html
  • 能部署 Functions(例如 src/index.js
  • Functions 能响应 /api/ip

四、绑定路由:test-ipv6.jsw.ac.cn

在 ESA 控制台中找到你的 Pages 项目,进行 绑定路由

  • 添加 test-ipv6.jsw.ac.cn并启用

如果你用的是 NS 托管,一般 ESA 会给你 DNS 编辑入口。此处重点是

  • 只需要确保 test-ipv6.jsw.ac.cn 有解析记录(A / AAAA / CNAME 其一),这边建议可以填A 记录 192.0.2.1
  • 并且 代理已启用

五、实现 Functions:/api/ip

我们需要的能力很少:

  1. 从 ESA 的请求对象里拿到“本次连接的客户端 IP”
  2. 用一个简单的函数判断 IPv4/IPv6
  3. request.info 中的归属地信息透传给前端
  4. 响应禁用缓存,避免边缘缓存造成“别人看到你的 IP”

下面是一个可直接用的 src/index.js

// src/index.js

function familyOf(ip) {
  if (!ip) return "unknown";
  if (ip.includes(":")) return "ipv6";
  if (ip.includes(".")) return "ipv4";
  return "unknown";
}

function baseHeaders() {
  return {
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-store, max-age=0",
    "x-content-type-options": "nosniff",
    "referrer-policy": "no-referrer",
    "permissions-policy": "geolocation=()",
  };
}

function json(data, status = 200) {
  return new Response(JSON.stringify(data, null, 2), {
    status,
    headers: baseHeaders(),
  });
}

export default {
  async fetch(request) {
    const url = new URL(request.url);

    // 只开放 /api/ip
    if (request.method !== "GET" || url.pathname !== "/api/ip") {
      return new Response("Not Found", { status: 404, headers: { "cache-control": "no-store" } });
    }

    // ESA 边缘运行时提供的 info 对象(重点:remote_addr)
    const info = request.info || {};
    const ip = info.remote_addr || "";
    const family = familyOf(ip);

    return json({
      ok: true,
      host: url.hostname,
      time: new Date().toISOString(),
      ip,
      family,
      // 透传:常见字段如 ip_city_en / ip_region_en / ip_country_en / ip_isp_en 等
      info,
    });
  },
};

关于 request.info:为什么它比 X-Forwarded-For 更“稳”

很多人第一反应是读请求头 X-Forwarded-For,但在边缘/代理链路里它可能包含多个地址,甚至混入节点地址,容易误判。

ESA 运行时提供的 request.info 通常包含:

  • remote_addr:本次连接看到的客户端地址(最有用)
  • remote_port
  • ip_city_en / ip_region_en / ip_country_en / ip_isp_en 等归属地信息

因此这次实践的策略是:前端只相信 /api/ip 输出的 ip + family + info

六、前端:展示

前端采用三文件分离:

  • public/index.html:页面骨架
  • public/app.css:布局样式
  • public/app.js:请求 /api/ip,按浏览器语言自动展示(不提供手动切换)

你可以直接复用我之前给你的三文件版本。这里再强调一下前端逻辑要点:

  • 只请求当前域名:fetch('/api/ip')

  • 语言只跟随浏览器 navigator.language(s),不提供 UI 切换

  • 展示内容建议包含:

    • 结论:当前使用 IPv4/IPv6
    • 一句短提示:IPv6 则“恭喜可以访问纯 IPv6”
    • IP:本次访问的公网地址
    • 归属地 + ISP:来自 info 字段
    • 一句按归属地定制的招呼(让页面“有人味”)

七、验证:怎么判断是否成功?

部署完成后,打开:

  • https://test-ipv6.jsw.ac.cn/(页面)
  • https://test-ipv6.jsw.ac.cn/api/ip(接口)

接口返回中关键字段:

  • ip:如 36.251.161.352408:...
  • familyipv4 / ipv6
  • info:归属地信息

你会看到几种典型结果

  • family: ipv6
    ✅ 恭喜:你的环境可访问纯 IPv6 网站
  • family: ipv4
    说明本次连接偏向 IPv4(可能是 IPv6 未开、不可用,或浏览器/网络更倾向 IPv4)

八、这次实践的复盘:为什么最后选“单域名单函数”的方案?

一开始我也试过更像 test-ipv6.com 的思路:分别用 “A-only / AAAA-only” 域名做探针,但现实约束是:

  • 因为阿里云ESA不像腾讯云EO支持可为域名单独ipv6设置是否开启

于是最终目标调整为更务实的版本:

不强行判断“你是否具备某一栈”,只告诉用户“本次访问更偏向哪一栈”,并给出可理解的提示。

对于一个放在 jsw.ac.cn 体系下的工具页来说,这个版本更稳定、更易维护,也更符合“白底黑字、无多余解释”的产品风格。

九、常见问题

Q1:为什么我的 x-forwarded-for 里有 IPv6,但 family 还是 ipv4?

因为我们判断的是 request.info.remote_addr,不是代理链里的“可能值”。

Q2:能不能强制让浏览器走 IPv6?

浏览器会根据网络/解析/延迟自动选择(Happy Eyeballs)

Q3:为什么要加 cache-control: no-store

否则边缘缓存可能把某个访客的 IP 结果缓存下来,导致下一位访客看到错误的 IP。