L2TP + FreeRadius + MySQL对接双因子认证

安装

  1. 安装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
  1. 安装 openssl
yum -y install openssl openssl-devel
yum -y install mysql-devel
  1. 安装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

  1. 连接mysql 创建数据库 radius
   create database radius;
  1. 创建数据库表结构
 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

http://blog.chinaunix.net/uid-9509185-id-3061795.html

https://segmentfault.com/a/1190000008050770

results matching ""

    No results matching ""