前言
单位里有10来个网件的AP(WNAP210),需要对接入端(主要是手机)进行MAC认证,原来采用AP本地MAC认证,但是人员经常变动(离职),另外人员的岗位(流水线)也经常调整,这样就需在变动后,将员工手机的MAC地址添加到对应AP的数据库里,AP一多就变的相当麻烦,后来发现网件的AP支持Radius认证,于是就着手进行,先是使用FreeRadius的windows版,整了半天死活搞不定将用户存储在mysql中,而且运行时一个命令行窗口不能关掉,用着相当别扭,后来采用WinRadius,但是经常出现ODBC重连对话框(图1),
图1
另外使用winRadius时,远程桌面登陆服务器后,还不能点注销,否则直接关闭winRadius.在痛苦的使用WinRadius小半年后打算直接自己写个。
1.Radius认证的一些内容(网件AP为客户端)
AP接收到用户手机连接请求后,会发送一个Raidus认证数据包(通过UDP)给Radius服务器,Radius服务从数据包中提取用户名跟密码后到数据库里做比对,如果存在就发送“接受回应”否则发送“拒绝回应”。AP发送过来的用户名跟密码其实就是接入手机的mac地址(如:54271eacab03),具体格式可以参考http://www.freeradius.org 上的说明。
主要的几组代码,主要是密码加密,与签名生成
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NGRadius.Core;
using System.Security.Cryptography; namespace NGRadius.Core
{
public class RadiusPaket
{
private RadiusPaket()
{
Attributes = new List<RadiusAttribute>();
}
public byte Code { get; set; }
public byte Id { get; set; }
public UInt16 Length { get; set; }
public byte[] Authenticator{get;set;}
public byte[] Paket { get; set; }
public List<RadiusAttribute> Attributes { get; set; }
public static RadiusPaket Parser(byte[] receiveData)
{
var paket = new RadiusPaket(); if (receiveData.Length < ) throw new Exception("包长度小于20!");
byte code = receiveData[];
byte id = receiveData[]; UInt16 len = BitConverter.ToUInt16(new byte[] { receiveData[], receiveData[] }, );
if (len != receiveData.Length) throw new Exception("包长度异常!"); var authenticator = new byte[];
Array.Copy(receiveData, , authenticator, , ); paket.Code = code;
paket.Id = id;
paket.Length = len;
paket.Authenticator = authenticator;
paket.Paket = new byte[receiveData.Length];
Array.Copy(receiveData, paket.Paket, paket.Length);
//提取属性
var index = ; while (index < len)
{
var attrType = receiveData[index];
var attrLen = receiveData[index + ];
var attrValue = new byte[attrLen - ];
Array.Copy(receiveData, index + , attrValue, , attrValue.Length);
var attr = new RadiusAttribute(attrType, attrValue);
index += attrLen;
paket.Attributes.Add(attr); } return paket;
} /// <summary>
///
/// </summary>
/// <param name="sharedSecret"></param>
/// <param name="requestAuthernticator"></param>
/// <param name="code">2:accept,3:reject</param>
/// <param name="id"></param>
/// <returns></returns>
public static RadiusPaket Build(string sharedSecret,byte[] requestAuthernticator,byte code, byte id)
{
var attributes = new List<RadiusAttribute>(); var sessionTimeoutAttr = new RadiusAttribute(, new byte[] { , 0x98, 0x96, 0x7F });
attributes.Add(sessionTimeoutAttr); var msgAuth= GenMessageAuthenticator(sharedSecret, requestAuthernticator, code, id, attributes);
attributes.Add(msgAuth); //20个字节加,属性长度,加MessageAuthenticator 18字节
UInt16 len = (ushort)( + attributes.Sum(ent => ent.Paket.Length));
var lenBytes = BitConverter.GetBytes((ushort)len).Reverse(); #region 计算Response Authernticator,
var authRaw = new List<byte>();
authRaw.Add(code);
authRaw.Add(id);
authRaw.AddRange(lenBytes);
authRaw.AddRange(requestAuthernticator);
foreach (var a in attributes)
{
authRaw.AddRange(a.Paket);
}
authRaw.AddRange(Encoding.Default.GetBytes(sharedSecret));
var authernticator =MD5.Create("MD5").ComputeHash(authRaw.ToArray());
#endregion var paketBytes = new List<byte>();
paketBytes.Add(code);
paketBytes.Add(id);
paketBytes.AddRange(lenBytes);
paketBytes.AddRange(authernticator);
foreach (var a in attributes)
{
paketBytes.AddRange(a.Paket); } var paket = new RadiusPaket();
paket.Attributes = attributes;
paket.Authenticator = authernticator;
paket.Code = code;
paket.Id = id;
paket.Length = len;
paket.Paket = paketBytes.ToArray();
return paket;
}
public static string ToHexStr(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", "");
}
public static String ToHexStr(byte b)
{
return BitConverter.ToString(new byte[] { b }).Replace("-", "");
} #region Util
/// <summary>
///
/// </summary>
/// <param name="pwdAttrPaket">User-Password段,包括type跟length+x...</param>
/// <param name="SharedSecret"></param>
/// <param name="RequestAuthenticator"></param>
/// <returns></returns>
public static byte[] EncodePAPPwd(String pwdStr, string SharedSecret, byte[] RequestAuthenticator)
{ var pwdBytes = Encoding.Default.GetBytes(pwdStr);
var dataLen = pwdBytes.Length / ;
var r = pwdBytes.Length % ;
if (r != )
{
dataLen++;
} var pArr = new byte[dataLen * ];
Array.Copy(pwdBytes, pArr, pwdBytes.Length); //补0字节处理
if (r != )
{
for (int i = pwdBytes.Length; i < pArr.Length; i++)
{
pArr[i] = ;
}
} var bi = new byte[];
var ciArr = new byte[pArr.Length]; var shareSecretBytes = Encoding.Default.GetBytes(SharedSecret); var tmp = new byte[shareSecretBytes.Length + ];
Array.Copy(shareSecretBytes, tmp, shareSecretBytes.Length);
Array.Copy(RequestAuthenticator, , tmp, shareSecretBytes.Length, );
Array.Copy(MD5.Create("MD5").ComputeHash(tmp), bi, ); for (int i = ; i < dataLen; i++)
{
for (int bIndex = ; bIndex < ; bIndex++)
{
ciArr[i * + bIndex] = (byte)(bi[bIndex] ^ pArr[i * + bIndex]);
} Array.Copy(ciArr, i * , tmp, shareSecretBytes.Length, );
Array.Copy(MD5.Create("MD5").ComputeHash(tmp), bi, ); }
return ciArr;
}
/// <summary>
///
/// </summary>
/// <param name="pwdAttrPaket">User-Password段,包括type跟length+x...</param>
/// <param name="SharedSecret"></param>
/// <param name="RequestAuthenticator"></param>
/// <returns></returns>
public static byte[] DecodePAPPwd(byte[] pwdAttrPaket, string SharedSecret, byte[] RequestAuthenticator)
{
var chunksCount = (pwdAttrPaket.Length - ) / ;
var biArr = new byte[pwdAttrPaket.Length - ]; var shareSecretBytes = Encoding.Default.GetBytes(SharedSecret);
var tmp = new byte[shareSecretBytes.Length + ];
Array.Copy(shareSecretBytes, tmp, shareSecretBytes.Length);
Array.Copy(RequestAuthenticator, , tmp, shareSecretBytes.Length, );
Array.Copy(MD5.Create("MD5").ComputeHash(tmp), biArr, ); for (int i = ; i < chunksCount; i++)
{ Array.Copy(pwdAttrPaket, ((i - ) * ) + , tmp, shareSecretBytes.Length, );
Array.Copy(MD5.Create("MD5").ComputeHash(tmp), , biArr, i * , );
} for (int i = ; i < biArr.Length; i++)
{
biArr[i] = (byte)(biArr[i] ^ pwdAttrPaket[ + i]);
} return biArr;
} public static RadiusAttribute GenMessageAuthenticator(string sharedSecret, byte[] requestAuthenticator, byte code, byte id, List<RadiusAttribute> attributes)
{ if (attributes == null) attributes = new List<RadiusAttribute>(); var attr = new RadiusAttribute(, new byte[] {,,,,,,,,,,,,,,,}); var msgAuthRaw = new List<byte>();
msgAuthRaw.Add(code);
msgAuthRaw.Add(id);
//20个字节加,属性长度,加MessageAuthenticator 18字节
UInt16 len = (ushort)( + attributes.Sum(ent => ent.Paket.Length) + );
msgAuthRaw.AddRange(BitConverter.GetBytes((ushort)len).Reverse() );
msgAuthRaw.AddRange(requestAuthenticator);
foreach (var a in attributes)
{
msgAuthRaw.AddRange(a.Paket);
}
msgAuthRaw.AddRange(attr.Paket); var hmacMD5 = HMACMD5.Create("HMACMD5"); hmacMD5.Key = Encoding.Default.GetBytes(sharedSecret);
var hmacBytes = hmacMD5.ComputeHash(msgAuthRaw.ToArray());
//更新属性内容
for (int i = ; i < ; i++)
{
attr.Value[i] = hmacBytes[i];
attr.Paket[i + ] = hmacBytes[i];
}
return attr;
} #endregion
}
}
2.网件AP的MAC认证配置
登录每个AP,将wifi名称设置成一样,并设置同样的加密方式与wifi密码
设置Radius Server如图2,注意Shared Secret,这个跟下面SConfig.txt文件中配置要一致
图2
设置MAC认证为远程数据库,参考图3
图3
3.安装与配置NGRadius服务
需要.net4.0框架,在win2003,win7,win2008上测试过正常
3.1.先远行脚本安装数据库
3.2.调整配置文件NGRadius.WinServer.exe.config 中的连接字符串
3.3.设置配置文件SConfig.txt, 主要是SharedSecret参数
3.4.运行NGRadius.Setup.exe 安装windows服务
图4
使用与测试:
将允许接入的手机MAC地址添加到表tbUsers中即可(需要小写,并去除":"符号)。
性能方面模拟30个客户端,各发起100个请求,等待返回,整个过程结束需要165毫秒,也就是说165毫秒至少可以处理3000个请求。
稳定性一周7*24小时运行,一切正常。
5.代码与可执行包
代码在:https://github.com/doomguards/NGRadiusServer
可执行包:点击下载