RADIUS计费协议基础与报文结构
RADIUS计费功能由RFC 2866规范定义,核心作用是让网络接入服务器(NAS)向计费服务器上报用户会话的相关信息,包括会话开始、会话进行中、会话结束三个阶段的事件。其中计费开始(Accounting-Start)报文用于告知服务器用户会话已建立,计费停止(Accounting-Stop)报文用于告知服务器用户会话已终止,这两个报文是完成会话时长与流量统计的基础。
每个RADIUS计费报文都包含固定的头部和若干属性字段。头部由编码(Code)、标识符(Identifier)、长度(Length)、认证字(Authenticator)、属性(Attributes)五部分组成,其中Code字段用于区分报文类型:计费开始的Code值为4,计费停止的Code值为5。属性字段采用TLV(Type-Length-Value)结构,每个属性包含类型、长度、值三个部分,常用的计费相关属性包括用户名称(User-Name,类型1)、纳斯IP地址(NAS-IP-Address,类型4)、会话ID(Acct-Session-Id,类型44)、会话开始时间(Acct-Start-Time,类型40)、会话停止时间(Acct-Stop-Time,类型41)、输入流量(Acct-Input-Octets,类型42)、输出流量(Acct-Output-Octets,类型43)等。
在构造报文时需要注意认证字的计算规则,计费报文的Authenticator字段是16字节的随机值,用于后续报文校验时的哈希计算。如果是计费请求报文,Authenticator需要由请求端随机生成;如果是计费响应报文,Authenticator是请求报文的Authenticator和响应报文内容拼接后,用共享密钥计算MD5哈希得到的结果。这个规则在Ruby实现时需要严格遵循,否则计费服务器会认为报文不合法直接丢弃。
Ruby实现RADIUS计费开始报文构造
要构造符合规范的计费开始报文,首先需要封装基础的工具方法,处理TLV属性的编码和报文头部的组装。Ruby中没有原生的RADIUS协议支持库,我们可以手动实现属性的编码逻辑,核心是将属性的类型、长度、值按照规范拼接成二进制数据。对于字符串类型的属性值,直接转换为二进制即可;对于IP地址类型的属性值,需要先将点分十进制的IP转换为4字节的二进制;对于整数类型的属性值,需要按照网络字节序转换为4字节二进制。
以下是一个基础的工具模块实现,包含属性编码和报文头部构造的方法:
require 'socket'
require 'openssl'
require 'time'
module RadiusAccounting
# RADIUS属性类型常量
ATTR_USER_NAME = 1
ATTR_NAS_IP_ADDRESS = 4
ATTR_ACCT_STATUS_TYPE = 40
ATTR_ACCT_DELAY_TIME = 41
ATTR_ACCT_INPUT_OCTETS = 42
ATTR_ACCT_OUTPUT_OCTETS = 43
ATTR_ACCT_SESSION_ID = 44
ATTR_ACCT_SESSION_TIME = 46
ATTR_ACCT_START_TIME = 47
ATTR_ACCT_STOP_TIME = 48
# 计费状态类型常量
ACCT_STATUS_START = 1
ACCT_STATUS_STOP = 2
# 编码单个RADIUS属性
def self.encode_attribute(type, value)
# 根据值类型处理编码逻辑
case type
when ATTR_NAS_IP_ADDRESS
# IP地址转换为4字节二进制
ip_parts = value.split('.').map(&:to_i)
value_bin = ip_parts.pack('C4')
when ATTR_ACCT_INPUT_OCTETS, ATTR_ACCT_OUTPUT_OCTETS, ATTR_ACCT_SESSION_TIME, ATTR_ACCT_DELAY_TIME
# 整数按网络字节序转为4字节
value_bin = [value].pack('N')
else
# 字符串类型直接转二进制
value_bin = value.to_s
end
# 拼接类型、长度、值
type_bin = [type].pack('C')
length_bin = [value_bin.bytesize + 2].pack('C')
type_bin + length_bin + value_bin
end
# 构造RADIUS计费报文
def self.build_accounting_packet(code, secret, attributes)
identifier = rand(0..255) # 随机生成标识符
authenticator = OpenSSL::Random.random_bytes(16) # 随机生成16字节认证字
# 拼接属性部分
attrs_bin = attributes.map { |k, v| encode_attribute(k, v) }.join
# 计算总长度:头部8字节 + 属性长度
length = 8 + attrs_bin.bytesize
# 拼接报文头部
header = [code, identifier, length].pack('C*')
# 拼接认证字和属性
packet = header + authenticator + attrs_bin
# 计算响应认证字(如果是请求报文不需要,这里先预留)
packet
end
end
有了基础工具方法后,构造计费开始报文只需要传入对应的属性即可。计费开始报文需要包含用户名称、NAS IP地址、会话ID、计费状态类型(值为1表示开始)、会话开始时间等必要属性。会话开始时间通常使用Unix时间戳,格式为字符串类型的RFC 3164时间格式,或者直接用整数时间戳表示,具体需要和计费服务器的要求匹配。
以下是构造计费开始报文的示例代码:
# 构造计费开始报文
secret = 'radius_secret' # 与计费服务器约定的共享密钥
attrs = {
RadiusAccounting::ATTR_USER_NAME => 'test_user_001',
RadiusAccounting::ATTR_NAS_IP_ADDRESS => '192.168.1.1',
RadiusAccounting::ATTR_ACCT_SESSION_ID => 'session_20240501_001',
RadiusAccounting::ATTR_ACCT_STATUS_TYPE => RadiusAccounting::ACCT_STATUS_START,
RadiusAccounting::ATTR_ACCT_START_TIME => Time.now.to_i.to_s,
RadiusAccounting::ATTR_ACCT_DELAY_TIME => 0
}
start_packet = RadiusAccounting.build_accounting_packet(4, secret, attrs)
puts "计费开始报文长度:#{start_packet.bytesize} 字节"
puts "报文十六进制表示:#{start_packet.unpack('H*').first}"
Ruby实现RADIUS计费停止报文与会话统计
计费停止报文的构造逻辑和开始报文类似,只是Code值改为5,计费状态类型改为2,同时需要新增会话时长、上下行流量、会话停止时间等属性。会话时长的计算逻辑是会话停止时间减去会话开始时间,单位为秒,存储在Acct-Session-Time(类型46)属性中。上下行流量分别用Acct-Input-Octets(类型42)和Acct-Output-Octets(类型43)表示,单位是字节,统计的是用户会话期间NAS接收到的上行流量和发送给用户设备的下行流量。
为了保证统计的准确性,会话ID需要和计费开始报文中的会话ID完全一致,这样计费服务器才能将两个报文关联到同一个用户会话。以下是一个构造计费停止报文的示例,假设我们已经记录了会话开始时间、上下行流量等信息:
# 假设会话相关信息
session_start_time = Time.now - 3600 # 会话开始于1小时前
session_end_time = Time.now
session_duration = session_end_time.to_i - session_start_time.to_i # 会话时长3600秒
input_octets = 1024 * 1024 * 50 # 上行流量50MB
output_octets = 1024 * 1024 * 200 # 下行流量200MB
# 构造计费停止报文属性
stop_attrs = {
RadiusAccounting::ATTR_USER_NAME => 'test_user_001',
RadiusAccounting::ATTR_NAS_IP_ADDRESS => '192.168.1.1',
RadiusAccounting::ATTR_ACCT_SESSION_ID => 'session_20240501_001',
RadiusAccounting::ATTR_ACCT_STATUS_TYPE => RadiusAccounting::ACCT_STATUS_STOP,
RadiusAccounting::ATTR_ACCT_SESSION_TIME => session_duration,
RadiusAccounting::ATTR_ACCT_START_TIME => session_start_time.to_i.to_s,
RadiusAccounting::ATTR_ACCT_STOP_TIME => session_end_time.to_i.to_s,
RadiusAccounting::ATTR_ACCT_INPUT_OCTETS => input_octets,
RadiusAccounting::ATTR_ACCT_OUTPUT_OCTETS => output_octets,
RadiusAccounting::ATTR_ACCT_DELAY_TIME => 0
}
stop_packet = RadiusAccounting.build_accounting_packet(5, secret, stop_attrs)
puts "计费停止报文长度:#{stop_packet.bytesize} 字节"
报文构造完成后,需要将其发送到计费服务器。RADIUS协议使用UDP传输,默认端口是1813,所以我们需要用Ruby的UDPSocket发送报文,并等待服务器的响应。发送逻辑需要注意设置超时时间,避免无响应时一直阻塞。以下是发送报文并处理响应的完整代码示例:
def send_radius_packet(packet, secret, server_ip='127.0.0.1', server_port=1813)
socket = UDPSocket.new
socket.send(packet, 0, server_ip, server_port)
# 等待响应,超时时间3秒
begin
socket.settimeout(3)
response, addr = socket.recvfrom(1024)
puts "收到计费服务器响应,长度:#{response.bytesize} 字节"
# 简单校验响应:响应的认证字需要符合规范
resp_code = response.unpack('C').first
if resp_code == 5 # 计费响应Code为5
puts "计费报文处理成功"
else
puts "计费报文处理失败,响应Code:#{resp_code}"
end
rescue Timeout::Error
puts "等待计费服务器响应超时"
ensure
socket.close
end
end
# 发送计费开始报文
send_radius_packet(start_packet, secret)
# 发送计费停止报文
send_radius_packet(stop_packet, secret)
在实际生产环境中,还需要处理更多的边界情况,比如报文重传机制、大流量场景下的流量计数溢出问题(RADIUS的Acct-Input-Octets等属性是32位整数,最大表示4GB流量,超过后需要配合Acct-Input-Gigawords等属性扩展计数)、不同时区的时间处理问题等。此外,如果需要支持计费更新报文(Code值为5,计费状态类型为3),只需要在会话过程中定期发送包含最新流量和时长的报文即可,构造逻辑和停止报文类似,只是不需要包含会话停止时间属性。
常见问题与调试方法
开发过程中最常见的问题是计费服务器无法识别报文,通常原因是属性编码不符合规范。比如IP地址属性没有转换为4字节二进制,而是直接传入字符串,或者整数属性的长度计算错误,导致属性TLV结构不符合要求。调试时可以将构造的报文转换为十六进制字符串,对照RFC 2866的报文结构逐字节校验,也可以用Wireshark抓包查看发送的报文内容,确认每个字段的数值是否正确。
另一个常见问题是会话时长统计不准确,通常是因为会话开始时间和停止时间的时间戳来源不一致,比如开始时间用的是NAS本地时间,停止时间用的是计费服务器本地时间,两者存在时区差或者时钟偏差。解决方法是在所有涉及时间的地方统一使用Unix时间戳(UTC时间),避免时区转换带来的误差,同时可以在NAS和计费服务器之间部署NTP服务,保证两者的时钟同步。
流量统计偏差的问题通常是因为上下行流量的统计口径和计费服务器不一致,比如有些设备将输入流量统计为NAS接收的流量,有些则统计为用户发送的流量,需要在开发前和计费服务器的运维人员确认属性的定义。另外,需要注意流量统计的单位是字节,不要误写成比特,否则统计结果会相差8倍。如果计费服务器要求流量以千字节为单位,需要在构造报文前将字节数除以1024后再传入属性值。
