前言
單位里有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 < 20) throw new Exception("包長度小於20!"); byte code = receiveData[0]; byte id = receiveData[1]; UInt16 len = BitConverter.ToUInt16(new byte[] { receiveData[3], receiveData[2] }, 0); if (len != receiveData.Length) throw new Exception("包長度異常!"); var authenticator = new byte[16]; Array.Copy(receiveData, 4, authenticator, 0, 16); 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 = 20; while (index < len) { var attrType = receiveData[index]; var attrLen = receiveData[index + 1]; var attrValue = new byte[attrLen - 2]; Array.Copy(receiveData, index + 2, attrValue, 0, 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(27, new byte[] { 00, 0x98, 0x96, 0x7F }); attributes.Add(sessionTimeoutAttr); var msgAuth= GenMessageAuthenticator(sharedSecret, requestAuthernticator, code, id, attributes); attributes.Add(msgAuth); //20個字節加,屬性長度,加MessageAuthenticator 18字節 UInt16 len = (ushort)(20 + 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 / 16; var r = pwdBytes.Length % 16; if (r != 0) { dataLen++; } var pArr = new byte[dataLen * 16]; Array.Copy(pwdBytes, pArr, pwdBytes.Length); //補0字節處理 if (r != 0) { for (int i = pwdBytes.Length; i < pArr.Length; i++) { pArr[i] = 0; } } var bi = new byte[16]; var ciArr = new byte[pArr.Length]; var shareSecretBytes = Encoding.Default.GetBytes(SharedSecret); var tmp = new byte[shareSecretBytes.Length + 16]; Array.Copy(shareSecretBytes, tmp, shareSecretBytes.Length); Array.Copy(RequestAuthenticator, 0, tmp, shareSecretBytes.Length, 16); Array.Copy(MD5.Create("MD5").ComputeHash(tmp), bi, 16); for (int i = 0; i < dataLen; i++) { for (int bIndex = 0; bIndex < 16; bIndex++) { ciArr[i * 16 + bIndex] = (byte)(bi[bIndex] ^ pArr[i * 16 + bIndex]); } Array.Copy(ciArr, i * 16, tmp, shareSecretBytes.Length, 16); Array.Copy(MD5.Create("MD5").ComputeHash(tmp), bi, 16); } 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 - 2) / 16; var biArr = new byte[pwdAttrPaket.Length - 2]; var shareSecretBytes = Encoding.Default.GetBytes(SharedSecret); var tmp = new byte[shareSecretBytes.Length + 16]; Array.Copy(shareSecretBytes, tmp, shareSecretBytes.Length); Array.Copy(RequestAuthenticator, 0, tmp, shareSecretBytes.Length, 16); Array.Copy(MD5.Create("MD5").ComputeHash(tmp), biArr, 16); for (int i = 1; i < chunksCount; i++) { Array.Copy(pwdAttrPaket, ((i - 1) * 16) + 2, tmp, shareSecretBytes.Length, 16); Array.Copy(MD5.Create("MD5").ComputeHash(tmp), 0, biArr, i * 16, 16); } for (int i = 0; i < biArr.Length; i++) { biArr[i] = (byte)(biArr[i] ^ pwdAttrPaket[2 + 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(80, new byte[16] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}); var msgAuthRaw = new List<byte>(); msgAuthRaw.Add(code); msgAuthRaw.Add(id); //20個字節加,屬性長度,加MessageAuthenticator 18字節 UInt16 len = (ushort)(20 + attributes.Sum(ent => ent.Paket.Length) + 18); 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 = 0; i < 16; i++) { attr.Value[i] = hmacBytes[i]; attr.Paket[i + 2] = 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
可執行包:點擊下載