古老的 RADIUS 协议

古老的 RADIUS 协议

•约 11.4 千字

第一次听到 /'reɪdiəs/ 联想到的单词肯定是 Radius,但是如果听到的是 RADIUS 协议,就不得不怀疑自己的耳朵了,半径协议是什么?当然,如果是看到 RADIUS 这个词就不会有这个疑惑,全大写意味着是首字母的缩写形式,就像 HTTP 协议一样。

但是和 HTTP 协议一样,RADIUS 的全称 Remote Authentication Dial-In User Service 理解起来一样不怎么直观。远程用户拨号认证服务,这个短语看起来就有点儿古老,名称里面居然还有拨号,这协议没和 Modem、电话线一起退休吗?

事实恰好相反。你连星巴克、酒店、校园网的 Wi-Fi,交换机检查电脑能不能接入,运营商给 PPPoE 用户分配地址,VPN 网关验证账号,可能都还有 RADIUS 参与。设备早已从拨号服务器变成无线控制器、交换机、VPN 网关和宽带接入服务器,协议的骨架却没有发生根本变化。

这是一件很有意思的事。一份诞生于拨号上网时代、报文最大只有 4096 字节、核心安全设计依赖 MD5 和共享密钥的协议,为什么还能出现在今天的网络里?更奇怪的是,当你决定自己写一个 RADIUS 服务端,最后往往会发现:RFC 当然要读,各个厂商的文档也得读,但是你最好将 FreeRADIUS 这个 C 写的开源实现作为唯一参考;很多问题搜半天文档,不如直接去看它的源码、字典和测试。

原因不是 FreeRADIUS 写得像教科书,也不是 RADIUS 高深到无法复刻。恰恰相反,RADIUS 的第一层非常简单。麻烦都藏在简单报文背后的三十年兼容性里。

一、RADIUS 到底是什么

RADIUS 是一套集中处理网络接入控制的分布式 C/S 架构的协议。它通常承担三类工作,合起来叫 AAA:

  • Authentication 认证:你是谁,提交的凭据是否可信
  • Authorization 授权:你可以使用什么网络和服务
  • Accounting 计费:你何时上下线,上网时长和流量是多少

这里的 C/S 架构比较容易误解为客户端就是手机、电脑。但在 RADIUS 语境中是 NAS,这个 NAS 可不是群晖、懒猫微服这种 NAS。它的全称是 Network Access Server 网络接入服务器,它可以是 Wi-Fi 接入点、无线控制器、交换机、VPN 网关、PPPoE 接入设备,也可以真的是二十多年前的拨号服务器。设备会先和 NAS 交互,NAS 再把认证相关信息放进 RADIUS 请求,发给服务器。

从运营商的角度 NAS 就是 BRAS 或者是 vBRAS,那 BRAS 是什么?它可以是华为的控制网关,例如 ME60 等。对于普通人呢?是否曾经在连接某些 Wi-Fi 时,连上了之后没网,且会自动弹出 Portal 网页,要求手机验证码进行验证,它的背后就有 NAS。

802.1X、PPP、VPN 登录等Access-Request查询Accept / Reject / Challenge实际放行、限速、分配 VLAN用户 / 终端NASRADIUS 客户端RADIUS 服务器用户库、LDAP、AD、SQL、证书系统

RADIUS 服务器并不直接和终端打交道,它只会回答 NAS:认证通过,设置 VLAN 20,无流量下线设置为 600 秒,限速 10 Mbps。真正执行这些策略的是 NAS,也就是接入设备,而不是 RADIUS 服务器。

也正因为如此,RADIUS 并不是一个身份目录,更不是 LDAP 的替代品。它可以查询本地文件、SQL、LDAP、Active Directory 或外部脚本,但这些系统存放身份和凭据,RADIUS 把它们组织成一次网络接入决策。把 FreeRADIUS 装起来不等于自动得到一套用户管理系统,正如装一个 HTTP 服务器不等于自动得到商城。

二、它解决的不是登录问题

想象一间有 200 台交换机和 1000 个无线接入点的公司。如果每个终端都有一个账户及权限信息,新入职一位员工,就得远程那 1200 台设备做配置更新,离职就再来一遍;忘改一台,旧账号就可能继续生效。设备型号不一,密码格式、权限表达和审计能力还各不相同。

RADIUS 把这个问题拆成两半:

  1. 接入设备只负责收集凭据、描述接入现场,并执行返回的策略
  2. 中央服务器负责验证身份、计算权限、记录会话

以后新增或禁用用户,直接在中央系统上做修改。NAS 不需要知道 LDAP 查询怎么写,也不需要保存全部用户密码,它只需要认识 RADIUS,把 User-Name、端口等属性交给服务器,再理解服务器返回的属性。

这听起来已经很像现代的策略服务,但 RADIUS 还有一个特别现实的设计:它从来没有假设世界里只有一家设备厂商。厂商有一个厂商特定属性 VSA (Vendor-Specific Attribute) 可以存放自己的参数。思科可以表达思科的授权命令,宽带设备可以表达自己的限速模板,无线设备可以带上自家定义的终端信息。

这个扩展一方面让生态长得极快,另一方面也埋下了后来的大麻烦:大家说自己支持 RADIUS,但是大家的属性恨不得都放进 VSA 里。协议负责把数据送到,语义却可能散落在 RFC、厂商手册、设备固件和 FreeRADIUS 字典中。

三、从拨号池里长出来的 AAA

RADIUS 的名字没有骗人,最早面对的就是远程拨号接入:用户通过电话线拨到 ISP 的 Modem 池,接入服务器需要判断账号能否登录、给他什么地址、允许使用哪种服务,并记录在线时长以便计费。

这类系统有几个鲜明约束:接入设备很多,用户库应该集中;链路不总可靠,但用户不会愿意为一次认证等上几分钟;认证请求很小,没必要为每次请求维持一条昂贵的长连接;不同运营商或机构之间还可能转发请求。

1997 年的 RFC 2058 和随后替代它的 RFC 2138 已经定义了 RADIUS,2000 年发布的 RFC 2865 成为今天最常引用的基础认证规范;同年的 RFC 2866 定义计费。它们没有凭空设计一套宏大的身份平台,而是把当时已经运行的拨号业务整理成一个足够小、足够容易塞进设备固件的协议。

后来接入媒介一直变:电话拨号退场,DSL 和 PPPoE 兴起,企业网络普及 802.1X,无线局域网、VPN 和运营商网络继续扩张。RADIUS 没有绑定某种物理介质。只要系统仍然存在「边缘设备询问中央策略点能不能让某个主体接入」这个问题,它就有容身之处。

所以 RADIUS 的长寿不是因为拨号长寿,而是因为它抽出来的那个问题长寿。

四、一次最普通的认证发生了什么

先看最简单的用户名密码认证,不考虑 EAP、代理和二次验证。

  1. 用户向 NAS 提交用户名和密码;
  2. NAS 构造 Access-Request,加入用户名、密码、设备 IP、MAC 以及自己的地址或标识、接入端口和服务类型;
  3. RADIUS 服务器验证这个 NAS 是否是已配置的客户端,再验证用户凭据;
  4. 服务器返回 Access-Accept、Access-Reject 或 Access-Challenge;
  5. NAS 根据结果放行、拒绝,或者继续向用户索取一次输入。

Access-Challenge 让一次认证不必在一问一答中结束。服务器可以返回 State 属性,NAS 在下一次 Access-Request 中原样带回,服务器据此找回上下文。一次性验证码、EAP 会话和其他多轮认证都依赖类似机制。

身份数据源RADIUS 服务器NAS用户身份数据源RADIUS 服务器NAS用户提交身份与凭据Access-Request (ID=37)验证用户 / 读取策略需要第二步验证Access-Challenge + State请求验证码提交验证码新 Access-Request + 原 State完成验证通过,返回用户策略Access-Accept + VLAN / 超时等放行并执行策略

这里的授权没有独立的 Authorization-Request。认证和授权结果通常一起回在 Access-Accept 中。服务器可以返回 Framed-IP-Address、Session-Timeout、Filter-Id、隧道参数或厂商属性。NAS 是否理解、怎样执行这些属性,是另一回事。

这也是 RADIUS 经常让开发者不舒服的地方。Web API 通常会把请求、响应和错误语义写得很完整;RADIUS 更像一次设备间协商。服务器发送的数据包含属性值对 AVP (Attribute-Value Pair),设备挑自己支持的执行。某些属性在某型号上有效,换一台同厂商设备也可能要改字典或值格式。

如果认证完成之后想要改变终端的权限呢?直到 RFC 3576 才新增了 Change of Authorization 用于授权变更,同时也有了 Disconnect Message 用于断开连接,它后面被 RFC 5176 所取代。

凭据不是换一种算法就能通用

认证方法还会反过来限制后端能保存什么。PAP 场景中,NAS 把经过 RADIUS 加密处理过的密码交给服务器,服务器解开后就拿到明文密码了。虽然这里的明文密码只存在于受控处理边界内,但是真的符合安全规范吗?

CHAP 就不同,服务器收到的是挑战与响应,需要掌握用户原始密码或某种可用于重新计算响应的等价秘密。数据库里若只存了 bcrypt、scrypt 或 Argon2 这类不可逆密码哈希,服务器无法把哈希值「解密」回密码,也不能用它复算 CHAP。MS-CHAPv2 常依赖 NT 哈希,又形成另一种后端约束。EAP-TLS 则把凭据变成证书和私钥体系,验证路径完全不同。

因此 RADIUS 所谓「支持多种认证方法」不意味着任意用户库能无损切换方法。NAS 提交什么凭据、服务器能取得哪种密码表示、内外层 EAP 怎样组合,必须成套设计。为了兼容旧设备而保留可逆密码或旧式哈希,会把协议兼容债直接变成身份库的安全债。更合理的方案通常是优先选择不要求服务器恢复用户原始密码的方法,并明确淘汰旧方法的时间表;但现网设备支持到哪一步,就看命了。

五、20 字节头部:简单得很有欺骗性

传统 RADIUS 报文的固定头部只有 20 字节:

radius-packet.txt
text
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Code      |  Identifier   |            Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                    Authenticator (16 字节)                     |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Attributes ...                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Code 占 1 字节,表示 Access-Request、Access-Accept、Accounting-Request 等报文类型
  • Identifier 占 1 字节,用于把响应和请求配对
  • Length 占 2 字节,但传统规范把合法报文上限限制在 4096 字节
  • Authenticator 固定 16 字节,用于响应认证、请求认证或密码隐藏,具体算法取决于报文类型
  • 后面的就是真正的数据了,里面就包括上文提到的 AVP 和 VSA

数据的格式一样很简单,一条属性简称 TLV 代表着 Type Length Value,类型和长度都只占据 1 字节,但是反直觉的是这里的长度是包含了类型和长度的。一个属性最大 255 字节,扣掉类型和长度后,值最多 253 字节。解析器读出长度,向前跳到下一个属性,直到报文结束。

这个格式有两个巨大优点。第一,硬件实现很容易:不用处理 JSON,不需要 ASN.1 编译器,也没有复杂协商。第二,未知属性可以跳过,老设备仍能转发或忽略新属性。RADIUS 从一开始就把可扩展性放进了报文结构,RFC 2865 也把「不打扰现有实现地增加新属性」列为设计特点。

欺骗性在于,能拆出 TLV 不等于能正确理解 RADIUS。Authenticator 不是一个统一字段:Access-Request 通常放随机值,Accounting-Request 则要按另一套规则计算;响应又必须带上原请求的 Authenticator 参与摘要。某些属性允许重复,重复后的顺序有意义;某些必须唯一;某些字符串没有尾随 NUL;某些整数固定为网络字节序;User-Password 还不是通常意义上的密文,而是用共享密钥和 Request Authenticator 生成 MD5 链,再逐块异或进行隐藏。

十分钟可以写出一个能解析示例包的程序。要写出一个不会在畸形报文、代理转发、EAP 分片和厂商属性前翻车的实现,是另一项工作。

六、Identifier 只有 8 位,UDP 也不给你兜底

经典 RADIUS 通常使用 UDP。认证与授权使用 1812 端口,计费使用 1813;早期部署也可能见到历史上的 1645/1646。UDP 让 NAS 发一个小请求就走,不必维护连接,切换备用服务器也很直接。这很适合当年的设备与业务。

代价是丢包、乱序、重复和重传全部留给应用层。基础 RFC 刻意没有规定一套精确的重试与故障转移算法。后来专门总结实现问题的 RFC 5080 甚至提醒,过于激进的固定重传会在大量 NAS 同时恢复时造成拥塞崩溃。

8 位 Identifier 又把问题往前推了一步。同一条通信关系中最多只有 256 个 ID。如果请求速度很高、往返时间变长、发生重传或后端处理阻塞,ID 会重新使用。此时不能只拿「源地址 + ID」判断响应属于谁,还要结合端口、请求 Authenticator、报文类型和未完成请求表。否则一个迟到的旧响应可能被认作新请求的响应。

服务端也要识别重复请求。NAS 没收到响应会重发,重传通常保持相同的 Identifier 和 Request Authenticator。服务端如果把每个重传都当作全新事务,可能重复扣费、重复调用外部系统,或者为同一认证创建多份状态;但如果缓存键选得太粗,又会把合法的新请求误判为重复。

这就是 RADIUS 的典型气质:协议省下了传输层的复杂度,却没有消灭复杂度,只是把它分摊给每个实现。

特别是在 NAS 是 vBRAS 的情况下,多数 BRAS 都是华为、华三、思科等大厂的产品,即使协议没有正确实现,在大量客户场景的落地中也都会慢慢修正;vBRAS 就麻烦了,你不知道对接的 NAS 是否正确实现了 RADIUS 协议。

起码,Identifier 的问题就是遇到过的。如果 NAS 设计之初使用的是固定的单一端口进行发送,短时间内请求数量超过 256 时,Identifier 就会重复。

七、认证、授权和计费并没有想象中整齐

AAA 三个字母排得很整齐,实际报文却是三套不同脾气的流程。

认证与授权主要围绕 Access-Request 展开。成功响应既说明身份验证通过,也携带授权参数。服务器能做出策略,但无法保证 NAS 真按策略执行。比如返回 Session-Timeout = 3600,最后是否一小时断线,取决于接入设备。

计费的基础由 RFC 2866 定义,后来的 RFC 2869 又补充了中间更新。NAS 会发送 Accounting-Start、Accounting-Interim-Update 和 Accounting-Stop 一类记录,携带会话 ID、在线时长、输入输出字节数、终止原因等信息。服务器返回 Accounting-Response 表示收到了。

但这不是金融数据库事务。设备断电时可能来不及发 Stop;UDP 可能丢包;请求重传可能产生重复记录;32 位流量计数器会回绕,于是又有 Gigawords 属性补高位;集群中的不同服务器可能先后看到同一会话的更新。若要拿 RADIUS Accounting 算钱,必须自己处理去重、乱序、缺失、延迟和会话对账,不能把每一条报文直接等同于一条准确账目。

动态授权在上面说了,是后来在 RFC 5176 补上的:服务器可以主动要求 NAS 踢掉用户,或修改正在进行的会话。方向从「NAS 请求服务器」反过来变成「动态授权客户端请求 NAS」,端口、密钥配置、防火墙和设备能力也随之多出一套。

于是一个真正可用的 RADIUS 系统至少包含:

  • 接入认证、多轮挑战
  • 响应中的授权属性
  • 开始、更新、停止等计费报文
  • 重传、重复检测和主备切换
  • 可能存在的代理和漫游
  • 可能存在的 CoA 与 DM
  • 数据库、证书和策略逻辑

如果只实现 Access-Request 到 Access-Accept,得到的是一个 Demo 而不是 RADIUS 服务器。

八、EAP 和 802.1X:RADIUS 为什么又复杂了一层

企业 Wi-Fi 中常见的 WPA2-Enterprise、WPA3-Enterprise,以及有线网络的 802.1X,常把可扩展认证协议 EAP (Extensible Authentication Protocol) 装进 RADIUS 属性里传输。

此时 RADIUS 并不直接理解用户输入的密码。终端和认证服务器进行 EAP 对话,NAS 多数时候扮演透传者:从链路侧收到 EAP 消息,把它放进一个或多个 EAP-Message 属性送给 RADIUS;拿到 Access-Challenge 后,再把其中的 EAP 内容交还终端。

一条 EAP 消息可能超过普通 RADIUS 属性的 253 字节值上限,因此要拆成多个连续的 EAP-Message 属性。接收端必须按顺序拼回去。含有 EAP 的报文还涉及 Message-Authenticator,也就是基于共享密钥的 HMAC-MD5 完整性校验。计算时需要把该属性的值暂时视为全零,完成报文编码和长度计算,再填入结果;验证时同样要注意使用原始字节,而不是把属性解码、重排、重新编码以后再验。

如果继续进入 EAP-TLS、PEAP 或 EAP-TTLS,TLS 握手消息还会跨多轮 Access-Challenge 分片传递。服务器必须维持状态、处理超时和重传,并把外层身份、内层身份、证书结果和授权策略对应到同一会话。RADIUS 本身的小报文没有变,里面承载的状态机却已经不是校验一下密码那么简单。

很多所谓的 RADIUS 语言库只完成了报文编解码和基本 Authenticator 计算。它们能帮你写测试客户端,甚至能跑通 PAP,却没有替你实现 EAP 状态机、证书校验、代理、策略与计费。

而且 EAP 中涉及到 TLS 的情况下,FreeRADIUS 自己是不会实现 TLS 的,而是将 Payload 读写驱动 OpenSSL 完成握手。如果你是 Go 来实现会不会很懵呢?在 Go 中你得实现一个 net.Conn 才能拿到 tls.Conn 完成后面的握手、读写数据,Payload 数据自然就得从专用的 net.Conn 中做读写了。

九、共享密钥、MD5,以及那个叫 BlastRADIUS 的警报

按今天的眼光看,传统 RADIUS 的安全设计很难让人安心。

NAS 与服务器预先配置一个共享密钥。这个密钥不直接上网,但会参与响应 Authenticator、请求 Authenticator 或 Message-Authenticator 的计算。User-Password 属性只是被隐藏:密码填充到 16 字节的倍数,再与 MD5(secret + Request-Authenticator) 及后续链式结果异或。这不等于现代的端到端加密,也没有前向保密。

更麻烦的是,共享密钥传统上常按客户端源 IP 查找。NAT、动态地址、负载均衡和容器化会让这个模型变得别扭;跨不可信网络直接跑 RADIUS/UDP,则会把本来只适合受控网络的假设暴露出去。

2024 年公开的 BlastRADIUS 攻击 把这个老问题重新推到台前。它利用 MD5 选择前缀碰撞影响没有受到 Message-Authenticator 充分保护的 RADIUS 响应,可把某些拒绝结果篡改成接受结果。FreeRADIUS 随后发布修复,并推动客户端与服务器更严格地要求 Message-Authenticator。这件事说明,「共享密钥没有泄露」并不自动等于传统 RADIUS 交换安全;包认证方式、属性是否存在、代理链是否完整校验都很关键。

现代缓解方向主要有三层:

  1. 在受控网络中隔离传统 RADIUS 流量,使用足够长且唯一的共享密钥,不在大量客户端间复用
  2. 对相关报文要求并严格验证 Message-Authenticator,部署供应商修复
  3. 跨不可信网络时使用 TLS 或 DTLS 保护传输,即通常所说的 RadSec,而不是把裸 UDP 暴露在公网

RADIUS/TLS 最早由 RFC 6614 描述,RADIUS/DTLS 由 RFC 7360 描述。2025 年发布的实验性 RFC 9765 定义 RADIUS/1.1,通过 ALPN 协商新的传输配置,在 TLS/DTLS 已经提供保护的前提下移除共享密钥和所有基于 MD5 的报文认证、属性混淆。它没有修改 RADIUS/UDP 或 RADIUS/TCP,也不意味着三十年的设备一夜之间全部升级。

这正是现状的缩影:新方案已经知道旧地基哪里不安全,却仍必须和海量旧设备共存。目前国内主流 BRAS 只有部分新机器支持这些新标准。

十、代理与漫游:一个小协议怎么组成大网络

RADIUS 服务器收到请求后,不一定亲自认证。它可以根据用户名中的 realm(常见形式类似 user@example.org)把请求转发给上游服务器,自己同时扮演下游的服务器和上游的客户端。

代理会在请求中加入 Proxy-State,上游返回时必须原样、按顺序带回。每一跳与相邻节点共享自己的密钥,因此代理不能把收到的 UDP 包原封不动丢给下一跳:它可能需要解开并重新隐藏 User-Password,修改代理状态,用下一跳密钥重新签名;响应回来后又做一遍相反的工作。

这种逐跳信任不是现代意义上的端到端安全,但它非常适合联盟式运营。不同机构不用共享完整用户库,只要知道某个 realm 的请求该交给谁。eduroam 一类漫游体系就是这个思路的典型应用:访问地网络把认证请求沿 RADIUS 层级送回用户所属机构,所属机构验证身份,访问地执行接入策略。

代理也是实现者的噩梦之一。你需要处理路由环路、上游故障、主备切换、Proxy-State 顺序、不同跳的共享密钥、密码重新隐藏、响应对应关系和超时预算。如果中间某一跳把同类型属性重排,或者重新编码时改变了原始字节,后面的校验就可能失败。RFC 能告诉你每一步应该怎样,现实设备则会告诉你它们实际怎样。

特别是需要代理 EAP 请求的情况下,很可能会发现自己粗陋实现下隐藏的惊喜,而且偷偷告诉你,不会有具备价值的错误信息,NAS 也不会告诉你哪里错了。

十一、RADIUS 今天到底算不算过时

如果过时指协议保留大量历史包袱,答案是算。它的名字、UDP 模型、8 位 ID、4096 字节报文、MD5 依赖、按 IP 配共享密钥,都带着九十年代的印记。

如果过时指已经没人使用、没有维护、随时会被替换,答案又是否定的。企业 Wi-Fi、802.1X、有线准入、VPN、PPPoE 和电信接入仍广泛使用它;FreeRADIUS 3.2 系列仍在维护;IETF 的 RADIUS Extensions 工作组 仍是活跃状态,工作内容正包括淘汰不安全传输、推进基于 TLS/DTLS 的现代配置,以及改善长期存在的限制。

Diameter 曾被设计为更现代的 AAA 协议:使用可靠传输,更大的标识空间,更明确的能力协商和故障处理,名字甚至直白地比 Radius 直径更大。它在移动核心网等领域有自己的位置,却没有把接入侧的 RADIUS 连根拔起。原因并不神秘:协议替换从来不只比较报文设计。

一台已经稳定工作的交换机不会因为新协议在理论上更漂亮就增加支持。企业现有的无线控制器、VPN、身份目录、审计流程、运维脚本、厂商支持合同和工程师经验都围绕 RADIUS。替换它需要两端同时升级,还要重新验证所有授权属性。若旧系统已经够用,新系统带来的收益很难覆盖迁移风险。

RADIUS 的现状更像 IPv4,而不是 Telnet:大家清楚它的缺陷,也在给它加安全外层、扩展属性和部署约束,但庞大的存量与足够好的核心抽象让它继续活着。

十二、为什么它至今没有被淘汰

RADIUS 的优点不在密码学,而在工程边界。

1. 它足够小

固定头部 20 字节,属性也是简单 TLV。对资源有限的交换机、接入点和嵌入式网关来说,实现一个基础客户端并不昂贵。NAS 不需要运行复杂的身份系统,只要构造请求、校验响应、执行属性。

2. 它把身份系统与网络设备解耦

设备无需分别集成 LDAP、SQL、Active Directory、令牌服务和证书数据库。RADIUS 服务器吸收后端差异,设备只面对一种协议。修改用户数据时可以改服务器侧,成百上千台 NAS 不必跟着重写。

3. 它把决策和执行放在正确的位置

中央服务器知道用户和组织策略,边缘设备知道端口、无线电、隧道和当前会话。服务器返回授权意图,NAS 就地执行。既能集中控制,又不要求所有流量经过中央服务器。

4. 属性模型给了它很强的适应力

标准属性、厂商属性、扩展属性和 TLV 让它可以逐渐承载新的接入类型与策略。老设备遇到不认识的属性通常可以忽略,新设备继续增加能力。RFC 6929 甚至在不改变旧属性基本布局的前提下扩展类型空间、长属性、TLV 和 64 位整数;这种近乎固执的向后兼容,丑归丑,却很实用。

5. 代理天然支持跨域

realm 路由和逐跳代理让组织之间能够建立认证联盟。各家保留自己的身份库,只交换完成接入所需的信息。这个能力在漫游、运营商和教育网络里不是锦上添花,而是系统能否落地的关键。

6. 生态已经进入设备固件

几乎所有企业级网络设备都认识 RADIUS。协议支持、厂商字典、排障工具、抓包解析、运维经验和认证流程经过多年积累。标准的价值不只来自纸面设计,也来自「两边设备插上以后大概率能说话」。

7. 它的失败模型与接入认证相配

一次认证的价值具有很强时效性。丢包后快速重试或切到备用服务器,往往比两分钟后可靠送达更有用。UDP 并非单纯落后,它反映了当时对认证交互的取舍。今天可以在需要时换用 TCP、TLS 或 DTLS,但基础模型仍然成立。

说到底,RADIUS 的优势是它把一个长期存在的问题切在了相对稳定的接口上。接口不完美,却足够窄,足够普及,也足够能扩展。

十三、为什么开源实现几乎只能参考 FreeRADIUS

先叠甲,世界上不是只有 FreeRADIUS 一份能收发 RADIUS 的代码。GNU Radius 是完整服务器项目,radsecproxy 是可靠的 RADIUS/TLS 代理,各种语言也有 RADIUS 编解码库,一些商业网络策略产品更有自己的服务端实现。

但如果问题是「我要实现一个面向真实设备、覆盖认证授权计费、能处理现代扩展的 RADIUS 服务端,我可以参考谁」,答案几乎只剩 FreeRADIUS。

原因不只是它名气大。

第一,它覆盖的范围完整。FreeRADIUS 不只会拆包,还包含 UDP/TCP/TLS 监听、请求跟踪、代理、realm 路由、EAP、计费、CoA、大量后端模块、策略语言和成套字典。你遇到的往往不是某个算法不会写,而是不知道算法应该放在整条处理链的哪一层。完整系统才能回答这种问题。

第二,它经受过足够多的设备。RADIUS 最难的兼容性不在标准属性,而在千奇百怪的 NAS 和 VSA。FreeRADIUS 项目称其部署范围从小型站点到千万级用户站点,并每天为数亿人的网络接入提供认证。宣传数字可以保守看待,但它长期处于运营商、企业 Wi-Fi 和 802.1X 现场这一点很难替代。许多边界行为不是从 RFC 推出来的,而是被真实设备撞出来的。

第三,它维护着极其重要的字典集合。RADIUS 属性只有类型号和字节串,字典决定某个编号叫什么、值是什么数据类型、枚举如何解释、厂商的 VSA 内部怎样布局。缺少字典,你最多得到 Vendor-Id = 9, Vendor-Type = 1, Value = ...;有了经过维护的字典,才能把它变成可配置、可日志化的策略对象。对生产实现来说,这些看似「数据文件」的内容和编码器一样重要。

第四,它的代码同时承担规范勘误簿的角色。RFC 5080 专门用了整篇 RFC 解释常见实现问题和修复建议,RFC 6929 又为类型空间、长属性、TLV 和厂商扩展打补丁。FreeRADIUS 的检查、兼容开关和测试把分散的要求落成了可执行行为。

第五,其他开源项目通常少一块关键拼图。有的多年主要做维护;有的只做客户端库;有的能做基础服务端,却不提供完整 EAP、代理、策略或字典生态;radsecproxy 的长处是代理和安全传输,本来就没有打算成为 FreeRADIUS 那样的通用策略服务器。拿它们学习某个局部完全合理,拿其中任何一个当整套协议的兼容性裁判就不够了。

所以「只能参考 FreeRADIUS」不是在宣布代码世界只剩一个仓库,而是在描述一个令人不太舒服的工程事实:只有它同时具备开源、范围完整、持续维护、生产验证和兼容知识沉淀这几个条件。

那我可以直接用它,而不是自己抄一份吗?自然是可以,FreeRADIUS 之所以能成为标准实现的标杆,相信背后存在着大量的用户都是这样做的。但是这也意味着在不熟悉它的情况下,对项目的掌控度会非常低,更不用提去扩展功能,给某类用户加个限速了。

十四、为什么文档常常不如源码好用

先读文档一般是好建议,在 RADIUS 世界里却需要加一句:读哪份文档?

基础认证看 RFC 2865,计费看 RFC 2866,EAP over RADIUS 看 RFC 3579,动态授权看 RFC 5176,实现陷阱看 RFC 5080,属性设计看 RFC 6158,扩展属性看 RFC 6929,数据类型看 RFC 8044,TLS 和 DTLS 又各有规范。后来的 RFC 会更新前面的要求,安全公告还会改变原本兼容但不再安全的默认行为。只读一份 RFC,得到的必然是不完整切片。

RFC 还故意不规定许多产品策略。重试几次、多久切备用、并发请求如何占用 ID、重复请求缓存多久、未知厂商属性怎样展示、畸形包记什么日志,这些都要实现自己决定。RFC 的任务是规定互操作边界,不是替你设计一台生产服务器。

厂商文档则有另一类问题。它常从配置界面出发,只告诉你在这里填共享密钥、返回这个属性可以下发 VLAN,很少交代字节布局、异常行为和固件差异。博客与问答又容易把某个 FreeRADIUS 2.x 配置抄到 3.x,或者把某台设备的怪癖写成通用规则。FreeRADIUS 官方文档页面干脆提醒,不要照搬多年以前的第三方教程,因为过时配置很可能不能工作甚至不安全。

源码的价值在于它必须作出选择。面对长度小于头部、属性长度为 1、重复的 Message-Authenticator、未知扩展类型、尾部多余字节、响应 Authenticator 不匹配,程序不可能像文章一样说「视情况而定」;它必须接受、拒绝、忽略或记录。FreeRADIUS 的源码把编码、解码、签名、校验和属性处理拆开,fr_radius_ok、fr_radius_verify、fr_radius_sign 一类入口比二手教程更接近真实行为。

测试又比单看实现更进一步。代码可能为了性能写得绕,测试文件会明确给出输入和预期结果。字典文件告诉你实现实际上认识哪些属性;示例配置告诉你请求会经过哪些处理阶段;radiusd -X 的调试输出则把一次请求如何匹配客户端、解码属性、进入策略、调用模块和生成响应摊在屏幕上。

比较有效的阅读顺序不是 RFC 或源码二选一,而是:

  1. 用基础 RFC 建立术语、报文和必须遵守的规则
  2. 顺着 RFC Editor 页面的 Updates/Updated by 找到后续修订
  3. 用 FreeRADIUS 字典、测试和源码确认可执行行为
  4. 最后拿目标 NAS 真机或抓包做互操作测试

源码不能替代规范。FreeRADIUS 也可能有 bug,也包含为了旧设备保留的行为,更可能在版本之间改变内部 API。可是当问题已经具体到「这个字节此刻参与哪一次哈希」或「同类型属性顺序能不能变」时,继续读泛泛的概念文档通常不会得到答案。

十五、自己实现 RADIUS,真正疼在哪里

如果只做一个内部测试客户端,找个成熟库就够了。如果要从头实现生产服务端,痛点会一层层出现。

1. 解析器必须对恶意输入保持无聊

报文来自网络,长度字段不能相信。总长度必须至少 20 字节且不超过实际收到的字节;每个属性至少包含 Type 和 Length;属性长度不能越过报文边界;嵌套 TLV、长扩展属性和厂商属性还要逐层验证。任何先切片再检查都可能变成越界、整数回绕或拒绝服务漏洞。

未知属性也不能一概报错。某些场景允许忽略,某些 Access-Accept 中的未知标准属性可能代表客户端无法执行的服务,RFC 5080 建议更谨慎地按拒绝处理;未知 VSA 又通常应该忽略。正确行为取决于包类型、属性种类和兼容配置。

2. 编码不是解码的逆操作那么简单

多个同类型属性的顺序有时必须保持。Proxy-State 需要逐跳追加和移除;EAP 分片需要连续且有序;字符串、IPv4/IPv6、带标签整数、枚举、时间、接口 ID 和厂商内部 TLV 都有不同编码。把解析结果丢进无序 map 再编码,可能立即破坏协议语义和摘要校验。

3. Authenticator 是一张条件分支表

不同 Code 的请求 Authenticator 规则不同,响应计算又依赖原请求的 16 字节 Authenticator。Message-Authenticator 计算时要处理字段清零与填回,某些报文必须包含它,某些旧设备又可能没有。代理修改包后需要用相邻一跳的密钥重新计算。若抽象成一个看似优雅的 sign(packet),往往只是把条件藏错了位置。

密码隐藏也要保留原始字节语义。PAP 的 User-Password 按 16 字节块链接 MD5 与异或,最大长度、零填充、空密码、共享密钥长度和随机 Authenticator 都有边界。实现能和自己互相解密,不代表能和别人的设备互通;两边犯同一个错也会得到「测试通过」。

4. 请求生命周期比报文复杂

客户端要管理 ID 分配、未完成请求、超时、指数退避、重传和主备服务器。服务端要识别重复包、缓存或重放响应、防止同一请求并行执行多次,还要在后端超时时尽快释放状态。多线程或多实例部署后,请求表放在哪里、同一 NAS 的包落到哪个节点、状态能否恢复,都要重新决定。

Access-Challenge 又引入跨报文会话。State 对 NAS 来说应该是不透明值,对服务器来说却要安全地映射上下文、防篡改、防过期,并在重传时返回一致结果。EAP-TLS 可能让这条链持续许多轮,内存和超时都不能无限增长。

5. 字典不是附属品

实现标准属性只完成了一小半。生产环境很快会要求某厂商限速、某交换机命令授权、某无线控制器返回角色。你需要字典加载器、命名空间、枚举、类型定义、VSA 编码和冲突处理,还要追踪厂商更新。

更棘手的是,历史厂商并不总按标准使用保留编号。RFC 6929 直接讨论了错误占用 241—246 类型与扩展属性冲突的情况,并建议服务器提供按客户端选择解释方式的开关。这不是靠一份「最新标准属性表」能解决的问题。

6. 策略系统很快会吞掉协议系统

真实需求不会停在「账号密码相等」。它会变成:这个用户来自哪个 NAS、接入端口是什么、证书属于哪个组织、当前时间是否允许、已经在线几台设备、应分配什么 VLAN、LDAP 超时时是拒绝还是故障转移、数据库写失败能否放行。

如果把这些条件硬编码在请求处理器里,几个月后就得到一团无法审计的分支。于是你会开始做模块接口、属性列表、处理阶段、条件语言、异步查询、事务和调试追踪。走到这里,已经在重新发明 FreeRADIUS 最庞大的那部分,而不是重新实现那 20 字节头部。

7. 计费需要接受「至少一次,但也可能一次没有」

UDP 重传让同一条计费记录可能到达多次,设备掉电又让 Stop 可能永远不到。会话 ID 未必全局唯一,设备重启后计数器和启动时间也可能变化。要把计费记录写进数据库,就得设计幂等键、更新顺序、过期会话清理、设备重启检测与离线对账。

如果业务要求金额准确,RADIUS 记录只能作为输入证据之一。账务系统还需要自己的不可变流水、补偿和核对。协议名字里的 Accounting 很容易让人高估它提供的保证。

8. 可观测性必须从第一天设计

认证失败可能是密码错、证书链错、NAS 用错共享密钥、响应回到错误端口、属性被代理改了、LDAP 超时、设备不认识 VLAN 属性,或者 MTU 导致 EAP 分片丢失。对 NAS 只回一个 Access-Reject 很简单,对运维解释为什么拒绝才是难题。

日志要能关联请求、重传、代理跳和后端调用,又不能把用户密码、共享密钥、EAP 内层身份等敏感信息打出来。指标要区分拒绝与系统错误、首次请求与重传、后端延迟与总延迟。抓包有帮助,但启用 TLS 或支持 EAP-TLS 时,Wireshark 虽不至于两眼一抹黑,但也看不到加密后的内容。

9. 兼容和安全经常互相扯后腿

严格要求 Message-Authenticator 更安全,却可能让多年没升级的 NAS 全部掉线;接受旧包保证业务连续,又保留攻击面。禁用弱 EAP 方法、提高 TLS 最低版本、轮换共享密钥、切换 RadSec,都需要两端能力和灰度计划。

成熟实现的价值,常常不是帮你走最理想的路径,而是允许你按客户端做兼容,再给出逐步收紧的出口。自己实现时,这些开关和迁移状态很容易被低估。

十六、如果确实要写,边界应该画在哪里

并不是任何场景都必须部署完整 FreeRADIUS。测试工具、协议分析器、嵌入式 NAS 客户端、只在受控环境里使用的最小代理,都可能值得自己写。关键是别把「我能发出一个合法包」错认成「我实现了 RADIUS」。

比较稳妥的做法是先限定角色和范围:

  • 只做客户端还是服务端,是否做代理
  • 只支持认证,还是包括计费和 CoA
  • 支持 PAP、CHAP,还是要承载 EAP
  • 只对接固定的一两种 NAS,还是声称通用
  • 只跑在隔离 UDP 网络,还是需要 TLS/DTLS
  • 是否处理厂商字典,未知属性怎样处置
  • 重传、重复请求和故障转移提供什么保证

然后用三类依据约束实现。第一类是 RFC 的规范要求,尤其是后续更新;第二类是 FreeRADIUS 对应稳定分支的代码和测试;第三类是目标设备的真机互操作结果。三者冲突时,不要静默挑一个看起来顺眼的答案,要把差异记录成兼容策略。

安全边界也要提前写明。传统 RADIUS/UDP 应限制在受控网络,密钥按客户端隔离;解析和校验先于策略执行;默认拒绝比解析失败后继续放行安全;敏感属性不进普通日志;上线前应覆盖畸形长度、重复属性、错误 Authenticator、重放、ID 回绕、EAP 分片与后端超时。

最后保留一组与 FreeRADIUS 交叉验证的测试向量。让自己的客户端打 FreeRADIUS,让 FreeRADIUS 客户端打自己的服务端,再引入真正的交换机或 AP。只做同源实现的自测,很容易让编码器和解码器一起把错误当成约定。

十七、古老不等于简单,更不等于该消失

RADIUS 的报文并不好看,安全地基也显得陈旧。它把传输可靠性留给应用,把扩展语义留给厂商,把许多部署问题留给管理员。站在今天重新设计,我们大概不会选择 8 位 ID、逐跳共享密钥和 MD5 链。

可它解决问题的切口仍然准确:让大量异构网络设备把身份判断交给中央系统,同时保留在边缘执行策略的能力。简单 TLV、低客户端成本、代理式跨域、可扩展属性和无处不在的设备支持,让它熬过了拨号时代,也熬过了好几轮「下一代 AAA」的替代预言。

FreeRADIUS 则像这份协议的活化石与航海日志。RFC 记录设计者希望世界怎样运转,FreeRADIUS 的源码、字典、测试和兼容开关记录世界实际上怎样运转。前者能让你知道规则,后者才能让你看到这三十年中设备、扩展、错误和修补如何叠在一起。

因此,自己实现 RADIUS 最难的从来不是画出 20 字节头部,也不是调用一次 MD5。真正的难点是决定哪些历史行为必须兼容,哪些不安全行为必须拒绝,以及如何在两者之间给现网留出迁移路径。

古老协议最擅长出的题,往往不是「你会不会实现」,而是「你愿意替过去承担多少现在的维护成本」。RADIUS 到今天仍有答案,只是这份答案远比它的报文长。

作者Aoang
发布于2026-09-11
更新于2026-09-11
许可协议CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,并注明出处。