导读:本期聚焦于李修然创作的《如何使用Node.js实现RADIUS服务器完成网络接入控制?》,敬请观看详情。RADIUS协议通常被理解成简单的登录校验,但它的实际工作方式更接近一套基于UDP的二进制请求应答模型,由NAS设备把用户凭证和接入参数封装成属性列表后发送给认证服务器。用Node.js实现RADIUS服务端时,除了处理Access-Request和Access-Accept,还需要解决共享密钥下的密码隐藏、请求认证子随机性、属性字典兼容等问题。文章从20字节报文头入手,展示如何用Buffer解析Code、Identifier、Length、Authenticator和TLV属性,再给出构造响应包的完整逻辑。随后说明交换机、无线控制器和VPN网关如何通过1812端口接入服务,并讨论EAP中继、计费端口、超时重传以及防重放策略。读完可以掌握搭建一个最小可用的网络准入控制服务所需的核心步骤。

网络准入控制里的RADIUS协议并不等同于普通的Web登录接口,它是运行在UDP 1812和1813端口上的一套二进制请求响应机制。NAS设备,例如交换机、无线控制器或VPN网关,只负责采集用户身份和接入参数,随后把这些信息按TLV格式打包成若干属性,交给中心化认证服务器判断是否放行。Node.js的事件驱动模型适合处理大量并发短连接,内置的Buffer能力又可以方便地读取和写入固定位长协议头,因此用它来实现一个RADIUS服务端是可行的。

如何使用Node.js实现RADIUS服务器完成网络接入控制?

实现过程中最关键的并不是HTTP路由设计,而是先理解协议报文,再严格控制字节偏移、属性长度和认证子计算。下面从Access-Request的解析开始,逐步搭建一个可以接收NAS请求并返回Access-Accept的UDP服务。

一、RADIUS报文结构与TLV属性解析

RADIUS报文头固定为20字节,依次为Code、Identifier、Length和Authenticator。Code表示报文类型,1对应Access-Request,2对应Access-Accept,3对应Access-Reject,4和5分别对应计费请求与响应,11则是Access-Challenge。Identifier用于匹配请求和响应,Length表示整个报文的总字节数,Authenticator在请求中通常是16字节随机数,在响应中则要根据请求认证子、响应属性和共享密钥计算出来。

头部之后紧跟若干TLV属性,每个属性包含Type、Length和Value三部分。Type占用1字节,Length占用1字节,Value长度由Length减2决定。常见属性包括User-Name对应类型1,User-Password对应类型2,NAS-IP-Address对应类型4,Reply-Message对应类型18。解析时必须严格检查每个属性的边界,否则一个异常报文就可能让服务崩溃。

const crypto = require('crypto');

function parseRadiusPacket(packet) {
  if (packet.length < 20) {
    throw new Error('Invalid RADIUS packet length');
  }
  const code = packet.readUInt8(0);
  const identifier = packet.readUInt8(1);
  const length = packet.readUInt16BE(2);
  const authenticator = packet.slice(4, 20);
  if (packet.length !== length) {
    throw new Error('Packet length mismatch');
  }
  const attributes = [];
  let offset = 20;
  while (offset < length) {
    const attrType = packet.readUInt8(offset);
    const attrLength = packet.readUInt8(offset + 1);
    if (attrLength < 2 || offset + attrLength > length) {
      throw new Error('Invalid attribute length');
    }
    const attrValue = packet.slice(offset + 2, offset + attrLength);
    attributes.push({ type: attrType, value: attrValue });
    offset += attrLength;
  }
  return { code, identifier, length, authenticator, attributes };
}

这段解析逻辑先把头部四个字段读出来,再从第20字节开始循环读取属性。读取属性时不能只读Type和Value,还必须用Length控制步进,因为不同属性的长度差异很大。如果某个属性的Length小于2,或者偏移越过报文总长度,就说明报文损坏,应立即抛错并丢弃。

解析完成后得到的是一个结构清晰的对象,后续认证逻辑只需要遍历attributes数组,按type找到用户名和密码属性即可。这样做的好处是解析层与业务层分离,新增属性类型时不必改动整体结构,只需要在业务层增加对应类型的处理分支。

二、构造响应报文与密码隐藏算法

构造响应包同样从20字节头部开始。Code按认证结果填2或3,Identifier必须与请求一致,Length根据头部和属性总长度计算,Authenticator则不能随便填随机数。RADIUS规定响应认证子的计算公式为MD5(Code+Identifier+Length+RequestAuthenticator+Attributes+Secret),其中RequestAuthenticator来自原始请求,Secret是NAS与服务器共享的密钥。很多实现在这一步出错,把随机数直接填入响应,导致NAS端校验失败。

function buildRadiusPacket(code, identifier, authenticator, attributes) {
  const header = Buffer.alloc(20);
  header.writeUInt8(code, 0);
  header.writeUInt8(identifier, 1);
  header.writeUInt16BE(0, 2);
  authenticator.copy(header, 4);
  const bodyParts = [header];
  let totalLength = 20;
  for (const attr of attributes) {
    const attrBuffer = Buffer.alloc(2 + attr.value.length);
    attrBuffer.writeUInt8(attr.type, 0);
    attrBuffer.writeUInt8(2 + attr.value.length, 1);
    attr.value.copy(attrBuffer, 2);
    bodyParts.push(attrBuffer);
    totalLength += attrBuffer.length;
  }
  const response = Buffer.concat(bodyParts);
  response.writeUInt16BE(totalLength, 2);
  return response;
}

function calculateResponseAuthenticator(code, identifier, requestAuthenticator, attributes, secret) {
  const attrBuffers = [];
  for (const attr of attributes) {
    const attrBuffer = Buffer.alloc(2 + attr.value.length);
    attrBuffer.writeUInt8(attr.type, 0);
    attrBuffer.writeUInt8(2 + attr.value.length, 1);
    attr.value.copy(attrBuffer, 2);
    attrBuffers.push(attrBuffer);
  }
  const totalLength = 20 + attrBuffers.reduce(function (sum, buf) {
    return sum + buf.length;
  }, 0);
  const header = Buffer.alloc(20);
  header.writeUInt8(code, 0);
  header.writeUInt8(identifier, 1);
  header.writeUInt16BE(totalLength, 2);
  requestAuthenticator.copy(header, 4);
  const hashInput = Buffer.concat([header, Buffer.concat(attrBuffers), Buffer.from(secret, 'utf8')]);
  return crypto.createHash('md5').update(hashInput).digest();
}

User-Password属性不能明文传输,而要通过共享密钥和请求认证子做异或加密。算法先把密码按16字节分块,不足部分补零,然后第一块与MD5(Secret+RequestAuthenticator)做异或,后续每块与MD5(Secret+前一块密文)做异或。解密时使用同样流程反向操作。这个算法的安全性依赖共享密钥的保密性,前提是NAS和服务器都必须安全存储密钥。

响应包中的User-Password一般不需要写入,Access-Accept更常返回Session-Timeout、Idle-Timeout或Filter-Id等授权属性。只有在测试环境才建议用明文用户名密码直接比对,生产环境应接入LDAP、Active Directory或证书认证。

三、最小可运行的Node.js UDP服务端

先搭建一个监听1812端口的UDP服务,收到Access-Request后提取用户名和密码属性,进行简单校验,并根据结果返回Access-Accept或Access-Reject。这里使用Node.js内置的dgram模块,不需要额外依赖,适合理解协议过程。实际生产中可以引入radius模块或自己封装更多的属性处理。

const dgram = require('dgram');
const crypto = require('crypto');
const SHARED_SECRET = 'replace-with-a-long-shared-secret';

const server = dgram.createSocket('udp4');

server.on('message', function (msg, rinfo) {
  const request = parseRadiusPacket(msg);
  if (request.code !== 1) {
    return;
  }
  const usernameAttr = request.attributes.find(function (attr) {
    return attr.type === 1;
  });
  const passwordAttr = request.attributes.find(function (attr) {
    return attr.type === 2;
  });
  const username = usernameAttr ? usernameAttr.value.toString('utf8') : '';
  const accepted = username === 'admin' && passwordAttr;

  const responseCode = accepted ? 2 : 3;
  const responseAttrs = [];
  responseAttrs.push({ type: 1, value: Buffer.from(username, 'utf8') });
  if (!accepted) {
    responseAttrs.push({ type: 18, value: Buffer.from('Authentication failed', 'utf8') });
  }
  const responseAuthenticator = calculateResponseAuthenticator(
    responseCode,
    request.identifier,
    request.authenticator,
    responseAttrs,
    SHARED_SECRET
  );
  const response = buildRadiusPacket(
    responseCode,
    request.identifier,
    responseAuthenticator,
    responseAttrs
  );
  server.send(response, rinfo.port, rinfo.address, function (err) {
    if (err) {
      console.error('send failed:', err);
    }
  });
});

server.bind(1812, function () {
  console.log('RADIUS server listening on 1812');
});

这个示例实际上只做了用户名是否存在和密码属性是否存在的校验,并没有真正解密User-Password。原因是很多NAS设备会使用CHAP或EAP方式,密码可能不在User-Password属性中,而是在CHAP-Password或EAP-Message中。如果只处理本地测试,可以进一步实现解密函数,把密码与共享密钥还原后再比对。

事件回调中的parseRadiusPacket如果抛出异常,会导致整个Node.js进程退出,因此必须在message回调内部做try catch保护,或者把解析函数设计成返回错误而不是直接抛异常。UDP数据包来源不可控,攻击者可以发送畸形包,服务端健壮性必须放在第一位。

四、对接NAS设备与EAP中继

真实网络准入控制通常不会让用户在登录页里提交密码后由Node.js直接校验,而是由交换机或无线控制器发起802.1X认证,NAS与RADIUS服务器之间使用EAP协议中继。此时Access-Request里会包含EAP-Message属性,类型79,服务器需要解析EAP包,并根据EAP类型返回Access-Challenge或Access-Accept。Message-Authenticator属性,类型80,用来保护EAP消息不被篡改。

对接时,需要在交换机或无线控制器上配置RADIUS服务器的IP地址、认证端口1812、计费端口1813以及共享密钥。共享密钥必须两端完全一致,任何一侧多出空格或换行都会导致认证失败。服务器端还要识别NAS-IP-Address、Called-Station-ID和Calling-Station-ID等属性,这些信息常用于记录用户从哪个设备和哪个端口接入。

EAP中继比简单密码认证复杂,因为请求和响应会经过多轮交互。Access-Challenge里携带EAP请求,NAS将其转发给客户端,客户端响应后再由NAS封装成新的Access-Request。Node.js服务端需要维护会话状态,把Identifier、请求认证子和EAP消息串联起来。可以使用内存Map保存状态,但多实例部署时建议改用Redis等共享存储。

五、生产环境注意事项与安全加固

RADIUS基于UDP,本身没有连接状态,也没有可靠传输保障。认证请求可能在网络拥塞时丢失,NAS会按配置重传。服务端必须把认证设计成幂等操作,同一Identifier和请求认证子的重复请求不应产生副作用。计费请求更要注意去重,尤其是计费开始和计费结束报文可能因为网络重传而重复到达,需要根据Acct-Session-Id做幂等判断。

安全方面,共享密钥不能使用短字符串或默认值。请求认证子必须由高质量的随机数生成器产生,否则攻击者可以构造重放包。生产环境应尽量启用Message-Authenticator属性,并对来自NAS的请求做来源IP校验。日志中不要记录明文密码,必要时只记录用户名、NAS标识和认证结果。

如果部署规模较大,可以考虑使用RadSec,也就是基于TCP和TLS的RADIUS扩展。Node.js可以通过TLS socket承载RADIUS报文,这样传输过程具备加密和可靠传输能力。不过很多传统NAS设备仍只支持UDP,因此在兼容现有网络设备时,1812和1813端口仍然是绕不开的基础。

Node.jsRadius协议网络接入控制修改时间:2026-09-22 01:01:23

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