认证门户(Captive Portal)是生活中非常常见的上网准入方式。当我们连上一个开放式(不需要密码即可连接)的 Wi-Fi 接入点以后,几乎都不能直接访问互联网,而是需要在一个网页上进行一些认证后才可以访问,这就是认证门户。
从这种 Wi-Fi 接入点的准入方式来说,我们很容易就发现一个问题:需要访问网页才能让用户开始准入认证,但怎么才能让用户去访问呢?目前通行的一种做法是直接劫持 DNS 请求或者 HTTP 请求,重定向到认证页面。这种做法在 HTTPS 时代之前很容易做到,但是 HTTPS 时代之后就遇到了困难:由于认证门户并没有用户访问的网站的证书,要劫持用户就势必要用假的证书来进行,这样一来二去也会给用户带来信任不安全证书的坏习惯。
当然,现在的系统也都有自己的做法。比如 Windows 就会尝试用 http 连接 www.msftconnecttest.com
,如果请求被重定向了,那么就会弹出页面让用户进行登录。这虽然解决了一部分问题,但是怎么看怎么不清真对吧,所以在 2015 年,RFC 7710 规定了一种方式:如果 DHCP 或 RA 在指定的 option 中发送了一个 URL,那么就引导用户前往该页面进行认证。
RFC 7710 看起来确实解决了一些问题,但是如果用户此前已经登录,其实是没必要再次登录的。这样系统还是绕不开一次互联网连通性的测试,当然 DHCP 服务器可以在通过认证的情况下不发送有关 option,然而 DHCP 服务器并不一定和计费系统是耦合的,做这种耦合也是复杂且不合适的。所以 capport 工作组设计了一套认证门户的标准,也就是 RFC 8908 和 RFC 8910(代替 RFC 7710)。
我了解到 capport 工作组的这些内容,是因为前两天名为菜色狼的群友去了澳门,在澳门线下赌场看完性感荷官在线发牌之后回到了他下榻的喜来登酒店,连上 Wi-Fi 后发现了名为“到期时间”的内容。
一开始我猜测是 DHCP 租约到期时间,但是菜色狼说之前在家里和别的地方都没见过,于是猜测是某种标准,我便顺藤摸瓜找到了上面提到的两个 RFC。
RFC 8908 规定了认证门户对客户端提供一些信息的 API 格式,API 返回格式为 JSON 对象,IANA 也维护了一个注册表来登记可用的字段。目前只有随着 RFC 发布的这几个,其中 captive
字段是必须包含的:
captive: boolean
表示用户是否需要登录。user-portal-url: string
描述了用户完成准入所需访问的页面,也是之前 RFC 7710 中规定的 URL。venue-info-url: string
描述了网络运营者想向用户展示的内容(例如地图、航班信息等)。can-extend-session: boolean
表示用户当前的会话是否可以被延长(时间、流量等),这个字段可以让系统在时间或流量将要耗尽的时候让用户访问user-portal-url
重新认证。seconds-remaining: number
描述了用户当前会话的剩余时间。bytes-remaining: number
描述了用户当前会话的剩余流量(字节数)。
其中菜色狼看到的“到期时间”就是 seconds-remaining
带来的。
RFC 8910 则是替代了之前的 RFC 7710,将 URL 的定义改成了 RFC 8908 中提到的 API 端点,同样也有 3 种通告方式:
- DHCPv4 Option 114
- DHCPv6 Option 103
- IPv6 Router Advertisement Option 37
有了这两个 RFC,认证门户上网准入的流程就是这样的了:
- 连上网络、从 DHCP/RA 获得地址的同时知道认证门户 API 的地址。
- 系统请求 API,得知设备当前的准入状态,如果需要认证则让用户访问
user-portal-url
。 - 设备已经准入,根据 API 返回的信息选择性地进行一些信息展示。
相比劫持用户的连接、系统自己探测这些做法之外,就清晰和明确很多,也安全很多了。
而且这样一来,只需要提供一个 API 地址并返回规定的内容,并不需要一个真实的认证门户就能复现,折腾的劲儿也就上来了。接着我创建了这样一个 PHP 文件:
<?php
header('Content-Type: application/captive+json');
echo json_encode([
'captive' => false,
'venue-info-url' => 'https://jin.sh',
'seconds-remaining' => 4555739950 - time(),
'bytes-remaining' => 114514000,
'can-extend-session' => false
]);
按照 RFC 8908 所描述的,这样应该会在 Android 上显示出过期时间为 2114-05-14 19:19:10(即 4555739950 时间戳),并展示一个前往“信息页” https://jin.sh
的链接。接着只要把代码挂上线,在 DHCP 服务器上根据 RFC 8910 中所描述的 DHCP Option 指向线上的 URL。对于我的 Juniper 上只需要一句:
set access address-assignment pool soha-home_4 family inet dhcp-attributes option 114 string "https://soha.moe/rfc8908.php"
OpenWrt 也可以在 DHCP 服务器设置中的高级设置选项卡里用类似 114,https://soha.moe/rfc8908.php
的方式来设置这个 DHCP option,但我没有可以测试的设备,没法确定这里的写法是否正确。另外 Android 不支持 DHCPv6,我这里也就没设置 DHCPv6 对应的 DHCP option。
然而手机连接后并没有任何动静,服务器没看到任何请求。根据 commit 历史,这个功能早在 2020 年已经进入 Android 主线,Android 13 应当是支持的。但是考虑到我的设备是运行 Oxygen OS 的 1+ 10T,1+ 魔改的系统可能并没有合并这个功能。因此我斥资 1500 元在某宝购买了一个全新 Pixel 6a,在原生 Android 14 上进行体验。
两天后手机到货,连上 Wi-Fi 以后如愿看到了过期时间的信息、以及一个跳转到我的个人主页的按钮“Open site”和通知信息。
RFC 对于安全的考虑还是很充分的,比如强制要求 API 和认证页面是通过 https 请求的,还考虑了 DNS 安全等方面的问题,包括但不限于这些内容在 RFC 原文中都有提到。
我在最初测试的时候并不是将 API 放在 https://soha.moe/rfc8908.php
,而是直接在本地用 php -S
开了一个服务器(所以没有 https)。配上 DHCP option 以后并没有看到有任何请求,我当时以为是 Oxygen OS 完全不支持。为了测试这个功能,以及之前用来收短信的 1+ 3T 的电池已经把屏幕都彻底顶开该换了,才直接购买了一部 Pixel 6a。但是到货以后还是不行,重新阅读 RFC 才发现了有强制 https 的内容,所以 Android 不请求我的 API。重新配置才发现其实 Oxygen OS 也是会请求 API 的,只是 OnePlus 的设置 UI 并没有 merge 主线上的这一部分,也就没有展示出来,打开 venue-info-url
的通知信息也没有弹出来。
当前这个 RFC 似乎也只有 Android 实现了,虽然 Apple 员工是 RFC 8908 的一作,但是 iOS 并不支持这个功能。Windows 也似乎没有相关的跟进。
iOS 18 已经支持显示 venue-info-url
,但是没有理会 bytes-remaining
和 seconds-remaining
: