L2TP + FreeRadius + MySQL对接双因子认证
安装
- 安装epel
wget --no-check-certificate https://mirrors.ustc.edu.cn/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm
yum --disablerepo=epel -y update ca-certificates
- 安装 openssl
yum -y install openssl openssl-devel
yum -y install mysql-devel
- 安装mysql
yum -y install mysql-server
修改mysql 的root密码
mysql -u root -p
> use mysql
> update user set password=password('root');
4.安装FreeRadius
wget ftp://ftp.freeradius.org/pub/freeradius/freeradius-server-2.2.9.tar.gz
tar zxvf freeradius-server-2.2.9.tar.gz
cd freeradius-server-2.2.9
./configure
make
make install
测试可用:
radiusd -X
配置文件
/usr/local/etc/raddb/
日志文件:
/usr/local/var/log
配置:
配置mysql
- 连接mysql 创建数据库 radius
create database radius;
- 创建数据库表结构
mysql -u root -p radius < /usr/local/etc/raddb/sql/mysql/schema.sql
mysql -u root -p radius < /usr/local/etc/raddb/sql/mysql/nas.sql
表结构信息:
radcheck 用户检查信息表
radreply 用户回复信息表
radgroupcheck 用户组检查信息表
radgroupreply 用户组检查信息表
radusergroup 用户和组关系表
radacct 计费情况表
radpostauth 认证后处理信息,可以包括认证请求成功和拒绝的记录。
创建一个用户名和密码均为test
mysql> use radius;
建立组信息:(这些命令不用做任何修改,直接ctrl+c→ctrl+v就好了)
mysql> insert into radgroupreply (groupname,attribute,op,value) values ('user','Auth-Type',':=','Local');
mysql> insert into radgroupreply (groupname,attribute,op,value) values ('user','Service-Type',':=','Framed-User');
建立用户信息:
mysql> insert into radcheck (username,attribute,op,value) values ('test','User-Password',':=','test');
将用户加入组中:
mysql> insert into radusergroup (username,groupname) values ('test','user');
mysql>exit;退出数据库
配置 radius:
修改与mysql数据库连接的配置文件/usr/local/etc/raddb/sql.conf,
database = "mysql"
driver = "rlm_sql_${database}"
server = "localhost"
login = "root"
password = "root"
radius_db = "radius"
修改radius的配置文件,在目录/usr/local/etc/raddb/radiusd.conf中
一定要取消这一行的注释: $INCLUDE sql.conf
chap 认证
[root@localhost freeradius-server-2.2.9]# radtest -t chap test test localhost 0 testing123
Sending Access-Request of id 101 to 127.0.0.1 port 1812
User-Name = "test"
CHAP-Password = 0x653a2eef4db1398a628f1a1bd62432f2b5
NAS-IP-Address = 127.0.0.1
NAS-Port = 0
Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=101, length=26
Service-Type = Framed-User
使用chap 进行校验,发现整个过程中,没有办法获得密码明文,
在radius debug模式下,使用l2tp进行认证,通过调试信息,定位到对用户认证的代码在:
\src\main\auth.c 的rad_check_password函数中,其中关键代码如下:
rad_chap_encode(request->packet, my_chap,
auth_item->vp_octets[0], password_pair);
/*
* Compare them
*/
if (memcmp(my_chap + 1, auth_item->vp_strvalue + 1,
CHAP_VALUE_LENGTH) != 0) {
RDEBUG2("CHAP-Password is incorrect.");
return -1;
}
RDEBUG2("CHAP-Password is correct.");
其中 rad_chap_encode 是关键函数,根据mysql中保存的明文密码、客户端发送的challenge随机数、客户端发送的id,计算md5值,最终与客户端发送过来的md5值(16位)进行比较,如果相同验证通过,否则验证失败。
该函数实现在\src\lib\radius.c中,具体代码如下:
/**
输入参数: packet: 客户端发送的challenge信息
output:最终生成的16位md5值
id: 客户端发送的id值
password: mysql保存的用户明文密码
具体实现: md5(id+password+challenge)
**/
int rad_chap_encode(RADIUS_PACKET *packet, uint8_t *output, int id,
VALUE_PAIR *password)
{
int i;
uint8_t *ptr;
uint8_t string[MAX_STRING_LEN * 2 + 1];
VALUE_PAIR *challenge;
/*
* Sanity check the input parameters
*/
if ((packet == NULL) || (password == NULL)) {
return -1;
}
/*
* Note that the password VP can be EITHER
* a User-Password attribute (from a check-item list),
* or a CHAP-Password attribute (the client asking
* the library to encode it).
*/
i = 0;
ptr = string;
*ptr++ = id;
i++;
memcpy(ptr, password->vp_strvalue, password->length);
ptr += password->length;
i += password->length;
/*
* Use Chap-Challenge pair if present,
* Request Authenticator otherwise.
*/
challenge = pairfind(packet->vps, PW_CHAP_CHALLENGE);
if (challenge) {
memcpy(ptr, challenge->vp_strvalue, challenge->length);
i += challenge->length;
} else {
memcpy(ptr, packet->vector, AUTH_VECTOR_LEN);
i += AUTH_VECTOR_LEN;
}
*output = id;
fr_md5_calc((uint8_t *)output + 1, (uint8_t *)string, i);
return 0;
}
因此,最终实现,将
id,password(明文),challenge,secret(密码密文),username 发送到双因子认证服务进行校验。但实际测试时,发现secret 和challenge等字段都是二进制,非可打印字符,没有办法通过网络直传,因此将其做base64编码
详细代码如下:
找到 src\main\auth.c 注释掉 line: 361 - 374 行
/*
rad_chap_encode(request->packet, my_chap,
auth_item->vp_octets[0], password_pair);
*/
/*
* Compare them
*/
/*
if (memcmp(my_chap + 1, auth_item->vp_strvalue + 1,
CHAP_VALUE_LENGTH) != 0) {
RDEBUG2("CHAP-Password is incorrect.");
return -1;
}
*/
替换为
char cChallengeBase64[MAX_STRING_LEN];
char cSecretBase64[MAX_STRING_LEN];
VALUE_PAIR *challenge;
challenge = pairfind(request->packet->vps, PW_CHAP_CHALLENGE);
if(challenge)
fr_base64_encode( challenge->vp_strvalue, challenge->length, cChallengeBase64, MAX_STRING_LEN);
else
fr_base64_encode( request->packet->vector, AUTH_VECTOR_LEN, cChallengeBase64, MAX_STRING_LEN);
fr_base64_encode( auth_item->vp_strvalue, 17, cSecretBase64, MAX_STRING_LEN); //认证密码前是id,因此是17位
// id: auth_item->vp_octets[0], password: password_pair->vp_strvalue, username: request->username->vp_strvalue
//
RDEBUG2("id:[%d] username:[%s] challenge:[%s] secret:[%s] password:[%s]" , auth_item->vp_octets[0],request->username->vp_strvalue,cChallengeBase64,cSecretBase64,password_pair->vp_strvalue);
if(google_authenticate(request->username->vp_strvalue, password_pair->vp_strvalue, auth_item->vp_octets[0],cChallengeBase64,cSecretBase64) == 2)
return -1;
同时,在该文件的 line:36 添加调用google authenticate认证服务
#include <freeradius-devel/base64.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <netdb.h>
#include <fcntl.h>
#include <string.h>
#define HOST "vpn.website80.com"
#define PORT 80
#define USERAGENT "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.114 Safari/537.36"
#define ACCEPTLANGUAGE "zh-CN,zh;q=0.8,en;q=0.6,en-US;q=0.4,en-GB;q=0.2"
#define MAX_LEN 50
//#define ACCEPTENCODING "gzip,deflate,sdch"
static int http_Get(char *data);
static int google_authenticate(char *username, char*password, int id, char *challenge, char *secret);
static int URLEncode(const char* str, const int strSize, char* result, const int resultSize);
static int URLEncode(const char* str, const int strSize, char* result, const int resultSize)
{
int i;
int j = 0;//for result index
char ch;
if ((str==NULL) || (result==NULL) || (strSize<=0) || (resultSize<=0)) {
return 0;
}
for ( i=0; (i<strSize)&&(j<resultSize); ++i) {
ch = str[i];
if (((ch>='A') && (ch<'Z')) ||
((ch>='a') && (ch<'z')) ||
((ch>='0') && (ch<'9'))) {
result[j++] = ch;
} else if (ch == ' ') {
result[j++] = '+';
} else if (ch == '.' || ch == '-' || ch == '_' || ch == '*') {
result[j++] = ch;
} else {
if (j+3 < resultSize) {
sprintf(result+j, "%%%02X", (unsigned char)ch);
j += 3;
} else {
return 0;
}
}
}
result[j] = '\0';
return j;
}
static int http_Get(char *data){
int sock;
if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))<0){
DEBUG("main_auth.c:Can't create TCP socket!\n");
return 0;
}
struct timeval timeout = {2,0};
char *get;
char *tpl="POST /vpn/ HTTP/1.1\r\nHost:%s\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nUser-Agent:%s\r\nAccept-Language:%s\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: %d\r\n\r\n%s";
struct hostent *hent;
int iplen=15;
int tmpres;
struct sockaddr_in *remote;
char *ip=(char *)malloc(iplen+1);
if(ip == NULL)
return 0;
setsockopt(sock,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(struct timeval));
setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval));
memset(ip,0,iplen+1);
if((hent=gethostbyname(HOST))==NULL){
DEBUG("main_auth.c:Can't get ip from %s\n", HOST);
if(ip) free(ip);
return 0;
}
if(inet_ntop(AF_INET,(void *)hent->h_addr_list[0],ip,iplen)==NULL){
DEBUG("main_auth.c:Can't resolve host! %s\n", HOST);
if(ip) free(ip);
return 0;
}
remote=(struct sockaddr_in *)malloc(sizeof(struct sockaddr_in*));
if(remote == NULL)
return 0;
remote->sin_family=AF_INET;
tmpres=inet_pton(AF_INET,ip,(void *)(&(remote->sin_addr.s_addr)));
if(tmpres<0){
DEBUG("main_auth.c:Can't set remote->sin_addr.s_addr\n");
if(ip) free(ip);
if(remote) free(remote);
return 0;
}else if(tmpres==0){
DEBUG("main_auth.c:%s is not a valid IP address\n",ip);
if(ip) free(ip);
if(remote) free(remote);
return 0;
}
remote->sin_port=htons(PORT);
if(connect(sock,(struct sockaddr *)remote,sizeof(struct sockaddr))<0){
if(ip) free(ip);
if(remote) free(remote);
DEBUG("main_auth.c:Could not connect %s!\n",HOST);
return 0;
}
get=(char *)malloc(strlen(HOST)+strlen(data)+strlen(USERAGENT)+strlen(tpl)+strlen(ACCEPTLANGUAGE));
memset(get,0,sizeof(get));
if(get == NULL)
return 0;
sprintf(get,tpl,HOST,USERAGENT,ACCEPTLANGUAGE,strlen(data),data);
int sent=0;
while(sent<strlen(get)){
tmpres=send(sock,get+sent,strlen(get)-sent,0);
if(tmpres==-1){
DEBUG("main_auth.c:Can't send query!\n");
if(ip) free(ip);
if(get) free(get);
if(remote) free(remote);
return 0;
}
sent+=tmpres;
}
char buf[BUFSIZ+1];
memset(buf,0,sizeof(buf));
int htmlstart=0;
char *htmlcontent;
while((tmpres=recv(sock,buf,BUFSIZ,0))>0){
if(htmlstart==0){
htmlcontent=strstr(buf,"\r\n\r\n");
if(htmlcontent!=NULL){
htmlstart=1;
htmlcontent+=4;
}
}else{
htmlcontent=buf;
}
if(htmlstart){
//fprintf(stdout,htmlcontent);
break;
}
memset(buf,0,tmpres);
}
if(get)
free(get);
if(remote)
free(remote);
if(ip)
free(ip);
close(sock);
DEBUG("url:[%s],http_get %s \n", data,htmlcontent);
if(strlen(htmlcontent) >0 && strstr(htmlcontent,"\"errcode\": 0,") != NULL)
return 1;
else
return 2;
}
static int google_authenticate(char *req_username, char* req_password,int id, char *challenge, char *secret){
char cUname[MAX_LEN];
char cPass[MAX_LEN];
char cChallenge[MAX_LEN*2];
char cSecret[MAX_LEN*2];
int iret = 0;
if(strlen(req_password) > MAX_LEN)
{
DEBUG("main_auth.c:google_authenticate the password is too long!");
return iret;
}
if(strlen(req_username) > MAX_LEN)
{
DEBUG("main_auth.c:google_authenticate the username is too long!\n");
return iret;
}
URLEncode(req_username, strlen(req_username), cUname, MAX_LEN);
URLEncode(req_password, strlen(req_password), cPass, MAX_LEN);
URLEncode(challenge, strlen(challenge), cChallenge, MAX_LEN*2);
URLEncode(secret, strlen(secret), cSecret, MAX_LEN*2);
char *tpl="username=%s&password=%s&id=%d&challenge=%s&secret=%s";
char *cData ;
cData=(char *)malloc(strlen(tpl) + strlen(cUname) + strlen(cPass) + strlen(cChallenge) + strlen(cSecret) + 4);
if(cData == NULL)
return iret;
memset(cData,0,sizeof(cData));
sprintf(cData,tpl,cUname,cPass, id, cChallenge,cSecret);
int count = 0;
while(1){
int iTmp = http_Get(cData);
count++;
if(1 == iTmp){
iret =1;
break;
}else if(count > 3){
iret = 3;
break;
}else if(2 == iTmp){
iret = 2;
break;
}
}
if(cData)
free(cData);
return iret;
}
对应python代码校验chap
import os
import hashlib
import base64
def vpn_validate_test(pincode,upass,umsgid,uchallenge,usecret):
try:
token = chr(umsgid) + upass + pincode + base64.decodestring(uchallenge)
m2 = hashlib.md5()
m2.update(token)
if base64.encodestring(chr(umsgid) + m2.digest()).strip() == usecret.strip():
return True
except Exception,e:
print str(e)
return False
def vpn_validate_request(uname,upass,umsgid,uchallenge,usecret):
pincode = getpin(uname)
return vpn_validate_test(pincode,upass,umsgid,uchallenge,usecret)
参考文献:
radius MySQL
http://laibulai.iteye.com/blog/941662
l2tp
http://zlyang.blog.51cto.com/1196234/1873868