导读:本期聚焦于冷风创作的《如何使用Ruby实现一个简单的RADIUS服务器处理AAA认证与记账报文?》,敬请观看详情。RADIUS协议在网络认证领域应用广泛,但动手实现一个RADIUS服务器的人却不多。本文用Ruby从零搭建一个可运行的简易RADIUS服务,内容涵盖RADIUS协议基础、UDP报文结构与PAP、CHAP两种认证方式的处理流程,以及Accounting-Request记账报文的解析思路。文中给出完整的代码实现,包括属性解析、User-Password加解密、报文校验与响应构造等核心环节,并分析常见调试问题与扩展方向,帮助读者真正理解AAA框架下认证、授权、记账三个环节的协作机制。

RADIUS(Remote Authentication Dial In User Service)是一种基于UDP的客户端/服务器协议,广泛用于拨号接入、Wi-Fi认证(802.1X)、VPN网关等场景,是AAA框架——即认证(Authentication)、授权(Authorization)和记账(Accounting)——最经典的落地协议之一。虽然生产环境通常使用FreeRADIUS这样的成熟软件,但用Ruby亲手实现一个简易的RADIUS服务器,是理解协议细节最直接的方式。本文将从协议结构讲起,逐步完成报文解析、PAP与CHAP认证、记账处理以及响应构造的完整实现。

如何使用Ruby实现一个简单的RADIUS服务器处理AAA认证与记账报文?

一、RADIUS协议基础与报文结构

RADIUS报文封装在UDP数据报中,官方推荐认证端口为1812,记账端口为1813。整个报文由固定长度的20字节头部和若干属性(Attribute)组成。头部包含以下字段:1字节Code(报文类型)、1字节Identifier(报文标识,用于配对请求与响应)、2字节Length(报文总长度)、16字节Authenticator(认证器)。

Code字段决定了报文的用途,常见的取值包括:1表示Access-Request(接入请求)、2表示Access-Accept(认证通过)、3表示Access-Reject(认证拒绝)、11表示Access-Challenge(质询)、4表示Accounting-Request(记账请求)、5表示Accounting-Response(记账响应)。头部之后是TLV格式的属性列表,每个属性由1字节Type、1字节Length和Length-2字节的数据构成。

Authenticator在请求和响应中的含义不同。对于Access-Request,它是16字节的随机数(Request Authenticator);对于响应报文,该字段被替换为Response Authenticator,其值是对响应报文与请求Authenticator以及共享密钥拼接后做MD5运算的结果,客户端据此校验响应的合法性。对于记账请求,Authenticator则是报文内容与密钥拼接后的MD5摘要。理解这一点是实现校验和构造响应的关键。

二、用Ruby解析请求报文

首先需要一个UDP服务器来接收报文,Ruby标准库中的socket就能胜任。收到数据后,用String#unpack按格式拆出头部字段,再循环解析TLV属性,把结果存入哈希表方便后续查询。

require 'socket'

class RadiusServer
  CODE_ACCESS_REQUEST = 1
  CODE_ACCOUNTING_REQUEST = 4

  def initialize(port = 1812, secret = 'testing123')
    @secret = secret
    @socket = UDPSocket.new
    @socket.bind('0.0.0.0', port)
  end

  def parse_packet(data)
    code, id, length, auth = data.unpack('CCna16')
    attrs = {}
    offset = 20
    while offset < length
      type, len = data[offset, 2].unpack('CC')
      break if len.nil? || len < 2
      value = data[offset + 2, len - 2]
      attrs[type] = value
      offset += len
    end
    { code: code, id: id, auth: auth, attrs: attrs }
  end

  def run
    loop do
      data, addr = @socket.recvfrom(4096)
      pkt = parse_packet(data)
      handle(pkt, data, addr)
    end
  end
end

上面的代码完成了报文骨架的解析。注意unpack('CCna16')中,C对应1字节无符号整数,n对应16位网络字节序整数,a16对应16字节的原始二进制串。属性解析时务必检查Length字段是否小于2,否则畸形报文会导致死循环,这是处理不可信网络输入时的基本防御手段。

常用的属性类型可以定义常量映射,例如Type 1是User-Name,Type 2是User-Password,Type 4是NAS-IP-Address,Type 5是NAS-Port,Type 60是CHAP-Password。为了调试方便,建议把属性类型号翻译成可读名称后打印日志,这样在排查问题时能快速定位是哪个属性解析出了偏差。

三、PAP与CHAP认证的实现

PAP(Password Authentication Protocol)认证中,客户端把用户密码加密后放在User-Password属性里传输。加密算法是:先将密码补零到16字节的整数倍并按16字节分块,第一块与Request Authenticenticator做异或后对共享密钥做MD5,后续每一块与前一块的密文异或后再做MD5。解密即是逆过程。Ruby的digest库提供了MD5实现,代码非常简洁。

require 'digest'

def decrypt_user_password(encrypted, request_auth)
  blocks = encrypted.unpack('a16' * (encrypted.bytesize / 16))
  result = ''
  previous = request_auth
  blocks.each do |block|
    md5 = Digest::MD5.digest(@secret + previous)
    plain = block.bytes.zip(md5.bytes).map { |b, m| b ^ m }.pack('C*')
    result << plain.gsub(/\x00+\z/, '')
    previous = block
  end
  result
end

CHAP认证则不同,客户端发送的CHAP-Password属性包含1字节标识符加16字节的MD5摘要,摘要的计算方式是MD5(标识符 + 明文密码 + CHAP-Challenge)。CHAP-Challenge通常作为独立属性携带,若无此属性则直接使用Request Authenticator。服务器端只需按同样规则计算摘要再比对即可,密码本身不以任何形式在线路上传输,安全性优于PAP。

认证逻辑的核心就是取出用户名和密码凭据,与本地用户库比对。认证通过返回Access-Accept,失败返回Access-Reject。如果想实现二次验证(如短信验证码),可以返回Access-Challenge并在State属性中携带会话状态,客户端会带着State再次发起请求。一个简单的用户库可以用哈希表示,生产环境则应对接数据库或LDAP目录服务。

四、记账报文处理与响应构造

记账请求的Code为4,通过Acct-Status-Type属性(Type 40)区分Start(1)、Stop(2)、Interim-Update(3)等事件。NAS设备在用户上线时发送Start,下线时发送Stop,服务器据此可以统计在线时长、流量等信息。记账请求的Authenticator必须校验:将报文中Authenticator字段替换为16个零字节后,对整个报文与密钥的拼接结果做MD5,结果应与原始Authenticator一致,否则说明报文被篡改或密钥不匹配,应当丢弃。

响应报文的构造需要注意Identifier必须与请求一致,Response Authenticator的计算方式前文已述。下面给出完整的响应构造与主处理逻辑:

def build_response(code, id, request_auth, attrs = '')
  header = [code, id, 20 + attrs.bytesize].pack('CCn')
  resp_without_auth = header + "\x00" * 16 + attrs
  digest = Digest::MD5.digest(resp_without_auth + request_auth + @secret)
  header + digest + attrs
end

USERS = { 'alice' => 'password123' }

def handle(pkt, raw, addr)
  case pkt[:code]
  when 1 # Access-Request
    user = pkt[:attrs][1]
    pass = decrypt_user_password(pkt[:attrs][2].to_s, pkt[:auth])
    ok = USERS[user] == pass
    code = ok ? 2 : 3
    puts "Auth #{user}: #{ok ? 'ACCEPT' : 'REJECT'}"
    resp = build_response(code, pkt[:id], pkt[:auth])
  when 4 # Accounting-Request
    status = pkt[:attrs][40].to_s.unpack1('C')
    puts "Accounting event: status=#{status}"
    resp = build_response(5, pkt[:id], pkt[:auth])
  else
    return
  end
  @socket.send(resp, 0, addr[3], addr[1])
end

记账数据建议落库保存,至少记录用户名、事件类型、会话ID(属性44)、会话时长(属性46)和上下行流量(属性42与43),这样就能完整还原用户的会话轨迹。对于授权环节,可以在Access-Accept中附带Reply-Message、Session-Timeout等属性,向NAS下发策略,这就是AAA中Authorization的具体体现。

五、测试验证与常见问题排查

测试可以使用FreeRADIUS自带的radclient工具。例如发送一个PAP认证请求:把User-Name和User-Password写成字典格式通过管道传给radclient,目标指向127.0.0.1的auth服务并指定共享密钥testing123,如果一切正常,会收到Access-Accept响应。也可以用scapy构造原始报文做更精细的边界测试,例如发送畸形Length字段的报文,验证服务器的容错能力。

常见问题有几个:一是密钥不一致导致响应被客户端静默丢弃,此时服务器看似正常但客户端始终报超时,抓包对比Authenticator即可确认;二是忘记校验Length导致解析越界;三是认证端口与记账端口分开监听却共用同一套处理逻辑,建议分别创建两个UDP socket并用线程或IO.select多路复用。此外,真实环境还需考虑报文重复(RADIUS靠UDP重传,Identifier相同的请求应做幂等处理)、字典文件支持以及EAP透传等扩展。完成这个实现之后,AAA三要素在协议层面的协作就不再神秘:认证体现在Code 1到3的交互流程,授权落实为Accept报文中携带的属性,记账则由Code 4和5独立闭环完成。

RubyRADIUS服务器AAA认证修改时间:2026-09-01 13:16:54

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。