Radius - 概述

RADIUS(Remote Authentication Dial In User Service,远程用户拨号认证)协议是一种分布式的、客户端/服务器结构的信息交互协议,能保护网络不受未授权访问的干扰,常应用在既要求较高安全性、又允许远程用户访问的各种网络环境中。RADIUS协议为标准协议,基本所有主流设备均支持,在实际网络中应用最多。RADIUS使用UDP(User Datagram Protocol,用户数据报协议)作为传输协议,具有良好的实时性;同时也支持重传机制和备用服务器机制,具有较好的可靠性;实现较为简单,适用于大用户量时服务器端的多线程结构

 

参考资料:

https://blog.csdn.net/m0_49864110/article/details/129584307

https://tonydeng.github.io/sdn-handbook/sdn/aaa/radius.html

 

抓包测试

请求报文:04 00 00 21 1C 6F 61 B9 FC 60 26 53 00 F4 AE BD 18 90 62 21 28 06 00 00 00 01 2C 07 31 36 32 34 30 

通过报文:05 00 00 26 B4 9C 6E 23 71 F4 C5 D3 35 BB 68 B9 7F 67 CD 63 50 12 9A AB 65 EE DE 81 5A DF 4E 1C 37 CB D1 C0 03 06

报文数据:04 01 00 5E 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 01 02 1E 0A 38 34 30 39 30 32 31 35 1F 0A 36 32 37 35 30 31 31 34 28 06 00 00 00 01 06 06 00 00 00 01 08 06 FF 01 02 03 2A 06 00 00 00 64 2B 06 00 00 03 E8 2C 04 69 64 50 12 B8 38 33 DE C6 FC 34 4F 0E 6B 48 29 2A D0 E5 6E 

通过报文:05 01 00 26 F3 0F BE 8A 6D 04 3F 20 33 48 F7 A1 DA BA 2A C0 50 12 F4 55 7C D4 20 C7 2B B1 DA 18 57 BE C8 AF 1C 56 

Message-Authenticator校验规则

 

linux下抓包

sudo tcpdump -i any port 1812 or port 1813

sudo tcpdump -i any -w radius_packets.pcap port 1812 or port 1813

交互解读

 Raduis服务器和NAS服务器通过UDP协议进行通信,Raduis服务器的1812端口负责认证,1813端口负责计费工作。

 

Identifier 包标识

1、用来匹配请求和响应报文

Radius客户端发送请求报文后,Radius服务器返回的响应报文中的Identifier值与请求报文中的Identifier值相同,将相同Identifier值的请求和响应报文进行匹配

2、用来检测在一段时间内重发的请求报文

如果在一个很短的时间内收到相同的源IP地址、源UDP端口号以及Identifier值的请求报文,Radius服务器就认为是重复的请求报文

Length 包长度

指定Radius报文的长度(Code、Identifier、Length、Authenticator、Avps五个字段)

接收到的报文的实际长度大于Length的取值,超过的字节将作为填充字符而忽略

接收到的报文的实际长度小于Length的取值,该报文会被丢弃

Authenticator 包验证字(对用户进行认证授权) 

Authenticator=MD5(Code+Identifier+Length+请求验证字+Avps+共享密钥) 

AVPs(Attributes Value Pairs)(Radius属性,可变字段)

携带认证、授权和记账信息(通过TLV结构表示)

 报文数据:04ae01320ec270f4b1fd56d1b1c9e5a6e9fbe29c04060a1400911e0f676c6f62616c73696d2e63786e0706000000073d06000000051f1134363731393030313736383637393428060000000108060a6000282c12424335434138413342333436424536442e0600000000370667bd2207290600000000322236374244323230373030303030303638323430303830303137353533343530461a17000028af01113234303038303031373535333435301a0c000028af0306000000001a0c000028af0606dcceb5431a0d000028af080732343030381a09000028af0a03381a09000028af0c03301a0c000028af0d06306630301a18000028af1412333532393736313131363539343534321a09000028af1503061a15000028af160f8264f0105b5964f010081fc8031a0a000028af17042300

 

rcf2866计费只有密钥,没用户名密码

 

开源库

golang:github.com/bronze1man/radius
net:Flexinets.Radius.Core
 
手撸编解码
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace RadiusNet
{
    class RadiusPacket
    {
        public byte Code { get; set; }
        public byte Identifier { get; set; }
        public ushort Length { get; set; }
        public byte[] Authenticator { get; set; }
        public RadiusAttribute[] Attributes { get; set; } 
    }

    public class RadiusAttribute
    {
        public byte Type { get; set; }
        public byte Length { get; set; }
        public byte[] Value { get; set; }
    }

    class RadiusUtil
    {
        /// <summary>
        /// 生成请求报文
        /// </summary>
        public static byte[] CreateRadiusAccountRequest(byte[] secretBytes)
        {
            var attributes = WithAccountingRequestAttributes().ToArray();

            byte code = 0x04;
            byte identifier = 0x00;
            ushort length = Convert.ToUInt16(4 + 16 + attributes.Length); 

            byte[] request = new byte[length]; 
            request[0] = code; 
            request[1] = identifier;  
            request[2] = (byte)((length >> 8) & 0xFF); // 包总长度,低字节
            request[3] = (byte)(length & 0xFF); // 包总长度,高字节

            Array.Copy(attributes, 0, request, 20, attributes.Length); // 拷贝属性  
             
            byte[] hash = CreateAuthenticator(code, identifier, length, attributes, secretBytes, new byte[16]);// 16字节零填充 
            Array.Copy(hash, 0, request, 4, hash.Length); // 拷贝Authenticator

            return request;
        }

        /// <summary>
        /// 请求属性
        /// </summary>
        static List<byte> WithAccountingRequestAttributes()
        {
            // BitConverter.GetBytes(1).Reverse()
            // Encoding.UTF8.GetBytes(value);
            // StringToByteArray("9aab65eede815adf4e1c37cbd1c00306");
            //
            List<byte> attrs = new();

            // Acct-Status-Type (必须字段)
            var status = BitConverter.GetBytes(1).Reverse();
            attrs.Add(Convert.ToByte(40)); // Type=40 
            attrs.Add(Convert.ToByte(2 + 4)); // Length=6 
            attrs.AddRange(status);

            // Acct-Session-Id
            string str = "16240";
            attrs.Add(Convert.ToByte(44));
            attrs.Add(Convert.ToByte(2 + str.Length));// Length:2 + 5
            attrs.AddRange(Encoding.UTF8.GetBytes(str));

            return attrs;
        }

        /// <summary>
        /// 生成回复报文
        /// </summary>
        public static byte[] CreateRadiusAccountResponse(byte[] request, byte[] secretBytes)
        { 
            byte[] requestAuthenticator =  new byte[16];
            Array.Copy(request, 4, requestAuthenticator, 0, requestAuthenticator.Length);

            var attributes = WithAccountingResponseAttributes().ToArray(); var b1 = BitConverter.ToString(attributes).Replace("-", " ");

            byte code = 0x05;
            byte identifier = 0x00;
            ushort length = Convert.ToUInt16(4 + 16 + attributes.Length);

            byte[] response = new byte[length]; // RADIUS报文最大128字节  
            response[0] = code;
            response[1] = identifier;
            response[2] = (byte)((length >> 8) & 0xFF); // 包总长度,低字节
            response[3] = (byte)(length & 0xFF); // 包总长度,高字节

            Array.Copy(attributes, 0, response, 20, attributes.Length); // 拷贝属性  

            // 计算Message-Authenticator 
            // Authenticator:请求中的Authenticator
            // Message-Authenticator:值临时置为 16 字节零填充,HMAC-MD5(共享密钥, 完整报文数据)
            Array.Copy(requestAuthenticator, 0, response, 4, requestAuthenticator.Length);// 响应Message-Authenticator,用的是老的Authenticator
            var strMessageAuth = BitConverter.ToString(response).Replace("-", "");

            byte[] messageAuth = CreateMessageAuthenticator(response, secretBytes); var strMessageMD5 = BitConverter.ToString(messageAuth).Replace("-", "");// 9AAB65EEDE815ADF4E1C37CBD1C00306
            Array.Copy(messageAuth, 0, response, response.Length - 16, messageAuth.Length);// 更新最后的16字节

            byte[] hash = CreateAuthenticator(code, identifier, length, attributes, secretBytes, requestAuthenticator);// requestAuthenticator 
            Array.Copy(hash, 0, response, 4, hash.Length); // 更新Authenticator

            return response;
        }

        /// <summary>
        /// 回复属性
        /// </summary>
        static List<byte> WithAccountingResponseAttributes()
        {
            // Message-Authenticator
            List<byte> attrs = new();
            attrs.Add(Convert.ToByte(80));
            //
            //attrs.Add(Convert.ToByte(18));
            //attrs.AddRange(StringToByteArray("9aab65eede815adf4e1c37cbd1c00306"));
            //
            attrs.Add(Convert.ToByte(18)); // Length=18(2字节头+16字节值)
            attrs.AddRange(new byte[16]);// 计算前:临时置为16字节零填充后再计算;计算后:9aab65eede815adf4e1c37cbd1c00306

            return attrs;
        }

        /// <summary>
        /// 生成Authenticator
        /// </summary>
        static byte[] CreateAuthenticator(byte code, byte identifier, ushort length, byte[] attributes, byte[] secretBytes, byte[] padding)
        { 
            using (MD5 md5 = MD5.Create())
            {
                byte[] input = new[] { code, identifier }
                    .Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)length)))
                    .Concat(padding)
                    .Concat(attributes)
                    .Concat(secretBytes)
                    .ToArray();

                return md5.ComputeHash(input);
            }
        }

        /// <summary>
        /// 生成MessageAuthenticator
        /// </summary>
        static byte[] CreateMessageAuthenticator(byte[] data, byte[] secret)
        {
            var b1 = BitConverter.ToString(data).Replace("-", "");
             
            using (var hmac = new HMACMD5(secret))
            {  
                byte[] hmacResult = hmac.ComputeHash(data);
                return hmacResult;
            }
        }

        /// <summary>
        /// 校验MessageAuthenticator
        /// </summary>
        public static bool IsMessageAuthenticator(byte[] response, byte[] request, byte[] secret)
        {
            bool result = false;

            var b1 = BitConverter.ToString(response).Replace("-", " ");

            // 1. 定位并置零 Message-Authenticator 属性(Type=80)
            //int offset = 20; // 跳过头部 
            //while (offset < response.Length)
            //{
            //    byte type = response[offset];
            //    byte length = response[offset + 1];
            //    if (type == 80) // 找到 Message-Authenticator 
            //    {
            //        // 将 Value 置零(位置:offset+2 至 offset+2+16)
            //        Array.Clear(response, offset + 2, 16);
            //        break;
            //    }
            //    offset += length;
            //}

            var messageAuth = new byte[16];
            Array.Copy(response, response.Length - 16, messageAuth, 0, 16);// 取出校验值MessageAuthenticator

            Array.Copy(request, 4, response, 4, 16);// 用请求中的Authenticator替换
            Array.Clear(response, response.Length - 16, 16);// MessageAuthenticator置空

            var b2 = BitConverter.ToString(response).Replace("-", "");

            // 2. 计算 HMAC-MD5 
            using (var hmac = new HMACMD5(secret))
            {
                var md5 = hmac.ComputeHash(response);

                result = md5.SequenceEqual(messageAuth);
            }

            return result;
        }

        /// <summary>
        /// 校验请求的Authentic
        /// </summary>
        public static bool IsAuthenticRequest(byte[] request, byte[] secret)
        {
            bool result = false;

            List<byte> auth = new List<byte>(request.Length + 16 + secret.Length);

            // Identifier + Length 4 个字节
            for (int i = 0; i < 4; i++)
            {
                auth.Add(request[i]);
            }

            // Authenticator 置0 16字节
            for (int i = 0; i < 16; i++)
            {
                auth.Add(0);
            }

            // AVPs
            for (int i = 20; i < request.Length; i++)
            {
                auth.Add(request[i]);
            }

            // secret
            auth.AddRange(secret);

            // 计算 MD5 哈希
            using (MD5 md5 = MD5.Create())
            {
                byte[] hash = md5.ComputeHash(auth.ToArray()); Console.WriteLine("MD5 Hash: " +  BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant());

                byte[] requestAuth = new byte[16]; // request报文中Authenticator数据
                Array.Copy(request, 4, requestAuth, 0, requestAuth.Length); Console.WriteLine("MD5 Request: " +  BitConverter.ToString(requestAuth).Replace("-", "").ToLowerInvariant());

                // 对比  
                result = hash.SequenceEqual(requestAuth);
                Console.WriteLine("Request Result:" + result);
            }

            return result;
        }

        /// <summary>
        /// 校验回复的Authentic
        /// </summary>
        public static bool IsAuthenticResponse(byte[] response, byte[] request, byte[] secret)
        {
            bool result = false;

            if (response.Length < 20 || request.Length < 20 || secret.Length == 0)
            {
                return false;
            }

            using (MD5 md5 = MD5.Create())
            {
                using (var memoryStream = new System.IO.MemoryStream())
                {
                    memoryStream.Write(response, 0, 4);
                    memoryStream.Write(request, 4, 16);
                    memoryStream.Write(response, 20, response.Length - 20);
                    memoryStream.Write(secret, 0, secret.Length);
                    byte[] hash = md5.ComputeHash(memoryStream.ToArray()); Console.WriteLine("MD5 Hash: " +  BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant());
                    byte[] expectedHash = new byte[16];
                    Array.Copy(response, 4, expectedHash, 0, 16); Console.WriteLine("MD5 Response: " +  BitConverter.ToString(expectedHash).Replace("-", "").ToLowerInvariant());

                    // 对比  
                    result = hash.SequenceEqual(expectedHash);
                    Console.WriteLine("Response Result:" + result);
                }
            }

            return result;
        }

        public static RadiusAttribute[] ParseAttributes(byte[] data)
        {
            var attributes = new System.Collections.Generic.List<RadiusAttribute>();
            int index = 0;

            while (index < data.Length)
            {
                byte type = data[index];
                byte length = data[index + 1];
                byte[] value = data.Skip(index + 2).Take(length - 2).ToArray();

                attributes.Add(new RadiusAttribute
                {
                    Type = type,
                    Length = length,
                    Value = value
                });

                // 更新索引  
                index += length;
            }

            return attributes.ToArray();
        }

        public static byte[] StringToByteArray(string hex)
        {
            hex = hex.Replace(" ", "");

            int numberChars = hex.Length;
            byte[] bytes = new byte[numberChars / 2];
            for (int i = 0; i < numberChars; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }
            return bytes;
        }
    }

    class RadiusParse
    {
        // 共享密钥 
        static byte[] secret = Encoding.UTF8.GetBytes("R3QlUmFkaXVzMjQjNjU4YzRmYmNkY2Y0Mzc2NDY3MGNiYWM3MGQ2ODk4ZmY=");

        public static void Account()
        {
            // 请求报文
            //RADIUS Protocol
            //Code: Accounting-Request(4)
            //Packet identifier: 0x0(0)
            //Length: 33
            //Authenticator: 1c6f61b9fc60265300f4aebd18906221
            //Attribute Value Pairs
            //    AVP: t=Acct-Status-Type(40) l=6 val=Start(1)
            //    AVP: t=Acct-Session-Id(44) l=7 val=16240 
            string strRequest = "04 00 00 21 1C 6F 61 B9 FC 60 26 53 00 F4 AE BD 18 90 62 21 28 06 00 00 00 01 2C 07 31 36 32 34 30";

            // 响应报文  
            //RADIUS Protocol
            //    Code: Accounting-Response(5)
            //    Packet identifier: 0x0 (0)
            //    Length: 38
            //    Authenticator: b49c6e2371f4c5d335bb68b97f67cd63
            //    Attribute Value Pairs
            //        AVP: t=Message-Authenticator(80) l=18 val=9aab65eede815adf4e1c37cbd1c00306

            string strResponse = "05 00 00 26 B4 9C 6E 23 71 F4 C5 D3 35 BB 68 B9 7F 67 CD 63 50 12 9A AB 65 EE DE 81 5A DF 4E 1C 37 CB D1 C0 03 06";

            //Authenticator 计算规则
            //请求报文:使用 MD5(Code +Identifier + Length + 16字节零填充 + 属性 + 共享密钥)
            //响应报文:使用 MD5(Code +Identifier + Length + 请求报文Authenticator + 属性 + 共享密钥) 

            byte[] request = RadiusUtil.StringToByteArray(strRequest);
            byte[] response = RadiusUtil.StringToByteArray(strResponse);

            var c1 = RadiusUtil.IsAuthenticRequest(request, secret);
            var c2 = RadiusUtil.IsAuthenticResponse(response, request, secret);

            // 请求包
            var p1 = RadiusUtil.CreateRadiusAccountRequest(secret);
            var b1 = BitConverter.ToString(p1).Replace("-", " "); 

            // 响应包
            var p2 = RadiusUtil.CreateRadiusAccountResponse(p1, secret);
            var b2 = BitConverter.ToString(p2).Replace("-", " ");

            // 校验Message-Authenticator
            var b3 = RadiusUtil.IsMessageAuthenticator(p2, request, secret);

            // 报文解析
            Console.WriteLine("【报文解析】");
            RadiusPacket packet = new RadiusPacket
            {
                Code = request[0],
                Identifier = request[1],
                Length = BitConverter.ToUInt16(request, 2),
                Authenticator = request.Skip(4).Take(16).ToArray(),
                Attributes = RadiusUtil.ParseAttributes(request.Skip(20).ToArray())
            };

            // 输出解析结果  
            Console.WriteLine($"Code: {packet.Code}");
            Console.WriteLine($"Identifier: {packet.Identifier}");
            Console.WriteLine($"Length: {packet.Length}");
            Console.WriteLine($"Authenticator: {BitConverter.ToString(packet.Authenticator).Replace("-", "")}");

            foreach (var attr in packet.Attributes)
            {
                Console.WriteLine($"Attribute Type: {attr.Type}, Length: {attr.Length}, Value: {BitConverter.ToString(attr.Value)}");
            }

            Console.WriteLine("OK");
        }
    }
}

 

 
posted @ 2025-02-27 08:54  CHHC  阅读(2)  评论(0)    收藏  举报