网络生活

用网络记录生活!

« 系统管理员工具包: 使用 SNMP 数据radius属性 »

未命名文章

Radius系统安装和开发指南

1、在Linux系统上安装OpenRadius
从获得的openradius源代码编译OpenRadius,可以从http://www.xs4all.nl/~evbergen/openradius/download/或者www.openradius.org网站下载最新的源代码。以下步骤也是根据撰写该文档时其提供的文档,如果有更新,以网站上提供的文档为准。

提供的源代码包的名称为openradius-x.y.z.tar.gz,其中x.y.z是版本号。在linux下可以使用命令tar zxvf openradius-x.y.z.tar.gz展开,其生成一个目录openradius-x.y.z,其中包含源代码以及一些相关文件。

进入到该目录中,编辑所使用的平台(这里是linux)的Makefile,指定安装目录,模块和编译器需要的参数。根据我们使用的平台是linux,编译器是gcc,应该选择的Makefile是Makefile.gnu。缺省情况下有一个Makefile文件链接指向Makefile.gnu,也可以使用,如果你知道什么是文件链接的话。

一般情况下,我们需要注意的参数主要是安装目录,在Makefile里是这样定义的:
DIR_BIN = /usr/local/bin
DIR_SBIN = /usr/local/sbin
DIR_ETC = /usr/local/etc/openradius
DIR_LIB = /usr/local/lib/openradius
FILE_LOG = /var/log/openradius/radiusd.log
这些目录的配置是按照一般unix/linux系统目录配置习惯来的,正常情况推荐使用上述的配置。其中DIR_BIN表示是一般执行程序安装的目录,就是除了服务程序外的一些辅助工具程序。DIR_SBIN表示的是系统程序,服务程序或者需要特殊权限执行的程序的安装目录。DIR_ETC表示的是配置文件放置的目录。DIR_LIB表示的是库文件的安装目录,FILE_LOG表示的是日志文件的路径。另外为了避免初学者的误会,下面在涉及到这 其中的文件时,会使用这几个宏来表示你的本地路径。

然后我们就可以运行make命令开始编译程序,如果Makefile名字不是Makefile而是上面说的有后缀的,则使用make -f [Makefile]来指定。如果编译中有错误发生,则请参考网站的说明,或请教对Unix/Linux系统比较精通的人士帮助解决。

如果编译没有问题,则运行make -f [Makefile] install命令安装,安装路径在上面已经作了解释。注意在做这一步的时候,一般需要(根据安装路径)超级用户(root)权限。

安装完毕后即可以通过运行位于DIR_SBIN目录下radiusd命令启动radius服务,但为了特定的需求,我们还有许多的工作需要准备。

如果需要把radiusd作为系统服务启动而不仅仅是测试一下,也就是把它设定成每次系统启动的时候都会自动运行,这在linux下面一般是通过脚本的方式实现的。每次系统启动或重新启动时都会根据/etc/init.d/目录下的脚本定义来启动或停止某些(服务)进程。以下是根据另一个开源的radius服务器freeradius(www.freeradius.org)提供的脚本略加修改形成了的redhat linux风格服务脚本:
#!/bin/sh
#
# chkconfig: - 88 10
# description: Start/Stop the RADIUS server daemon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Copyright (C) 2001 The FreeRADIUS Project http://www.freeradius.org
#

# Source function library.
. /etc/rc.d/init.d/functions

RADIUSD=/usr/local/sbin/radiusd
LOCKF=/var/lock/subsys/radiusd
CONFIG=/usr/local/etc/openradius/configuration

[ -f $RADIUSD ] || exit 0
[ -f $CONFIG ] || exit 0

RETVAL=0

case "$1" in
start)
echo -n $"Starting RADIUS server: "
daemon $RADIUSD -y
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch $LOCKF &&
ln -s /var/run/radiusd/radiusd.pid /var/run/radiusd.pid 2>/dev/null
;;
stop)
echo -n $"Stopping RADIUS server: "
killproc $RADIUSD
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f $LOCKF
;;
status)
status radiusd
RETVAL=$?
;;
reload)
echo -n $"Reloading RADIUS server: "
killproc $RADIUSD -HUP
RETVAL=$?
echo
;;
restart)
$0 stop
sleep 3
$0 start
RETVAL=$?
;;
condrestart)
if [ -f $LOCKF ]; then
$0 stop
sleep 3
$0 start
RETVAL=$?
fi
;;
*)
echo $"Usage: $0 {start|stop|status|restart|reload|condrestart}"
exit 1
esac

exit $RETVAL

把上述脚本存成一个文件radiusd,放到/etc/init.d目录下,然后你就可以通过service radiusd start/stop命令启动和停止radius服务。最后在redhat linux下面通过chkconfig --add radiusd命令就可以把radius作为系统服务启停了,在redhat中可以通过setup命令设置服务缺省的运行状态,以在系统启动的时候自动启动radius服务。上述步骤中出现任何无法解决的问题,请求教对linux熟悉的人。

2、基本Radius服务配置
安装好了radius服务,可以运行之后,就需要对radius服务做一些配置和测试,以确保安装正确和的确能够使用。在以下步骤中对配置的缘由存有疑问的可以参考radius协议RFC请求标准。OpenRadius需要配置的内容主要包括:

1)服务器的地址和端口配置。此配置在OpenRadius中被称为源,表示提供服务的源信息。考虑到安全性,Radius服务器的地址一般不是公开的,甚至位于防火墙保护的内网中。运行在具有多个IP地址机器上时一般最好是仅打开真正提供服务的那个,当然这个也不是强制的。另外尽管RFC标准中的端口已经指定为1812和1813(分别提供认证和计费服务),但和其他Internet服务一样,这也可以是改变的,特别是常常需要适应客户端的需要,因为早期采用的radius接入设备往往使用的是非标准化的1645和1646端口。在Openradius中,这个配置项在DIR_ETC/openradius/configuration,目前缺省配置是:
#
# SOURCES
#

source (port=1812,
port=1813),

#source (port=1645, addr=127.0.0.1,
# port=1646, addr=127.0.0.1),
提供了所有地址端口1812、1813的服务绑定。下面被注释的行(在脚本和配置文件中,以#开始的行是注释行),表示的是提供本机地址(127.0.0.1)上,端口1645、1646的服务绑定。源的配置一般来说是非常简单的,更多的内容请参考自带的更新文档。

2)接口配置。和通常的radius配置方式所不同的是,OpenRadius提供了一种更加灵活,可方便定制化和变化的配置脚本的方式,统称为接口。一个接口配置包括以下信息:

name: 定义可以在行为定义(behaviour)文件中使用的名称。必须有一个定义。

sendattr: 如果没有指定的话,在Radius请求报文中的所有属性都被发送到该接口被调用的模块中。否则仅包含指定的一个列表。

prog: 定义这个接口启动时运行的一个或者多个子进程和命令行参数。至少定义一个进程,如果指定了多个子进程,服务器将循环查找到空闲的那个,如果所有子进程都忙,等到第一个变成空闲的那个来调用。

recvattr: 如果没有指定的话,所有出现在模块回应中的属性都被加入到回应报文中,否则仅仅包换那些列表指定的。

timeout: 定义接口子进程的超时时间。

flags: 有一些数字值组合而成,以下是定义好的一些属性(参考具体的模块约定)。

Ascii: 接口使用ASCII编码消息
Add-Tab: 在属性对前添加Tab键(ASCII)
Add-Spaces: 在等于号两边添加空格 (ASCII)
Add-Type: 在值之前添加属性类型用逗号分格(ASCII)
Hex-Value: 使用16进制发送和接收值(对字符串使用两位十六进制值) (ASCII)
Double-Backslash: 对转移字符发送两个反斜杠而不是一个(ASCII)
Named-Const: 如果在字典中有定义的话,(ASCII)
Short-Attr: 忽略空格-和厂商定义的名字(ASCII)

考虑到Radius本身应用的的需求,仍然按照缺省的传统配置方式把内容分成以下几个方面,注意这些虽然通常都是一个Radius服务器所必需的,但在接口的实现上并不强制分开。传统配置是一种文本格式,这也是一些厂商提供的服务器和freeradius缺省经常采用的方式,因为他们都是基于早期的Livingston服务器。

a) 客户端配置。同样是出于安全以及握手的需要,必须在服务端配置好客户端的一些信息,包括地址,加密用的密钥等等。可以指定多个客户端,每个客户端使用不同的配置信息。缺省情况下在DIR_ETC/openradius/configuration中的配置是:
interface (name="Clientsfile",
sendattr="str",
prog="ascfile -d legacy/clients",
recvattr="int",
recvattr="str",
flags=Ascii + Short-Attr,
timeout=15),
这表示客户端配置信息存放在标准的ascII编码的文本文件DIR_ETC/openradius/legacy/clients中。clients的内容例示如下:
127.0.0.1 h1dd3n
192.168.5.1 ohyessir
192.168.5.7 testn4s
每一行分别由IP地址和共享(由服务器和客户端共享)密钥组成。这表示相应地址的客户端可以以这个密钥和radius服务器通讯。而没有配置或者正确配置的客户端则无法和服务器进行通讯。

除了通过clients方式配置客户端,还有一种配置接入服务器的方式,因为接入服务器往往都是radius的客户端,这里我们就略过。

b) 域配置。对于较大的或者比较复杂的环境而言,存在一个服务器为多个客户提供服务,也可能存在多个radius服务器同时提供服务,这就需要对服务对象进一步的细分,对应的我们一般用域来表示这种概念。可以把域看作是物理上的不同用户群体,或者逻辑上提供不同服务模式的用户群体。在认证中,我们可以让用户通过添加类似Email地址的“@域名”的方式对此作出辨别。缺省配置为:
localtest local-realm=1

striptest strip-realm=1
Target-Server="127.0.0.1/h1dd3n"

remotetest Target-Server="192.168.9.99/g$bbl*dyg00k741"
这其中配置了三个域,每个域有一些自己的属性,用于在认证过程中采取动作时的依据。

c) 用户配置。对radius服务器管理来说,最主要的部分就是用户配置了,对一个用户的描述是通过属性实现的,认证时,这些属性将被用于对一个用户的鉴别以及下发到接入服务器通知其所应该采取的动作。在缺省的传统方式下,用户配置的内容为:
evbergen clear-password = "welcome1"
Service-Type = Framed,
Framed-Protocol = PPP
这配置了一个evbergen用户,记录了其明文密码,以及接入类型等信息。通常一个配置需要的属性可参考RFC或者参考接入服务器厂商的资料和ISP的服务的具体要求。实际应用中,用户信息可能并不是存放在这种简单的文本文件中,可以由interface来描述来自不同源的属性

d) 其他配置,在正式使用中可能还有一些其他需要配置的,比如代理认证服务器等,这里不作详细介绍,请参考相关文档,实现起来不是很难。

以上所有的基于文本方式配置信息都可以藉由其他方式实现。所需要的做的工作就是改变interface的配置,以及实现相应的信息(属性对)访问程序。不过OpenRadius已经内置了很多类型的配置文件访问方式,比如通过数据库或者LDAP目录,并且提供了相应的缺省配置文件,比如configuration.sample-mysql,configuration.sample-ldap等等很多。也同时提供了这些接口所对应的子进程,这些子进程一般都是使用perl脚本实现的,非常简单,可以方便修改以适应灵活多变的需求。你可以在modules目录里找到这些进程,以及他们需要使用的数据库或者目录等用户密码存放的模式文件。大部分情况下,这些接口都可以直接拿过来使用。比如我们直接拿来使用的mysql接口配置:
#
# SOURCES
#

source (port=1812, port=1813),

#
# INTERFACES
#

interface (name="Sql",
prog="radsql -d mysql: openradius:localhost radiusd radiusd",
flags=Ascii + Short-Attr + Named-Const,
timeout=5),


3)行为文件
具体对一个Radius请求的响应定义在行为文件(behaviour)中,实际上就是OpenRadius提供的一种脚本,适合于Radius这种采用属性对的交互协议。在行为文件中你可以具体的定义对Radius客户端和用户认证属性的鉴别,以及所应采取的行为。属性是通过脚本和接口交互取得的,同时也可以提交到比如日志这样的接口中来记录行为中的一些属性变更情况。
因为行为脚本比较复杂,这里就不详细解释了。OpenRadius也提供了很多对应接口的标准行为文件供参考,比如behaviour.sample-mysql等等。如果我们使用的Radius协议和RFC标准的基本内容相符的话,那么一般直接使用就可以了,如果是具体厂商相关的Radius协议,也往往只需要做简单的扩充就可以工作。例如,我们使用的behaviour文件修改如下(使用radsql模块配合mysql配置模式的接口):

# Init reply list

RAD-Code = Access-Reject,
RAD-Identifier = RAD-Identifier,
RAD-Authenticator = RAD-Authenticator,
RAD-Length = 20,

moveall Proxy-State,


# Look up secret and other attributes associated with the client

Sql(str = "select attribute, value from data where space=? and name=?",
str = "str,IP-Source",
str = "clients"), delall str, delall REP:int,

REP:Secret || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "IP-Source,str",
str = "未知接入服务器发来的请求?" .
(NAS-Identifier || NAS-IP-Address) . ",用户名为" . User-Name),

abort
),


# Uncomment this if you want data from the groups space based on the
# distinct values of the groupname column for this client. MySQL doesn't have
# subqueries or CONNECT BY clauses, so we need to do it this way.

Sql(str = "select distinct g.attribute,g.value " .
" from data c,data g" .
" where c.space='clients' and c.name=? " .
" and g.space='groups' and g.name=c.groupname ",
str = "IP-Source"),
delall str, delall REP:int,


# Do accounting for stop records with on-line duplicate detection

RAD-Code == Accounting-Request && (

RAD-Code := Accounting-Response,

# check signature (crudely)

RAD-Authenticator == md5 (RAD-Code toraw . RAD-Identifier toraw . RAD-Length toraw . "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" . RAD-Attributes . REP:Secret) || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "IP-Source,str",
str = "无效签名的计费请求来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) .
",用户名为" . User-Name),
abort
),

# check accounting type

Acct-Status-Type == Stop || halt,

# Do the transaction. Note 1: if you use multiple prog= lines for
# connection pooling and/or load sharing, you *must* define a separate,
# *single* interface for accounting, because the next group of statements
# must use the same session!

Sql(str = "lock tables accounting write"), del str,

Sql(str = "select count(acct_id) as 'int' from accounting " .
" where acct_nas = ? " .
" and acct_session_id = ? " .
" and unix_timestamp(acct_timestamp)+120 >= unix_timestamp()",
str = "str,Acct-Session-Id",
str = (NAS-Identifier || NAS-IP-Address)), delall str, del REP:int,

int || (
Sql(str = "insert into accounting (acct_nas, acct_session_id, acct_timestamp, user_name, nas_ip_address, nas_port, service_type, framed_protocol, framed_ip_address, framed_ip_netmask, login_ip_host, login_service, login_tcp_port, class, called_station_id, calling_station_id, nas_identifier, nas_port_type, port_limit, acct_status_type, acct_input_octets, acct_output_octets, acct_session_time, acct_input_packets, acct_output_packets, acct_terminate_cause, acct_multi_session_id, acct_link_count) values (?, ?, now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
str = "str,Acct-Session-Id,User-Name,NAS-IP-Address,NAS-Port,Service-Type,Framed-Protocol,Framed-IP-Address,Framed-IP-Netmask,Login-IP-Host,Login-Service,Login-TCP-Port,Class,Called-Station-Id,Calling-Station-Id,NAS-Identifier,NAS-Port-Type,Port-Limit,Acct-Status-Type,Acct-Input-Octets,Acct-Output-Octets,Acct-Session-Time,Acct-Input-Packets,Acct-Output-Packets,Acct-Terminate-Cause,Acct-Multi-Session-Id,Acct-Link-Count",
str = (NAS-Identifier || NAS-IP-Address)), delall str, delall int
),

Sql(str = "unlock tables"), del str, del REP:int,

# test the last int returned by the previous module call; drop packet if 0

int || abort,
halt
),


# Get data from database, both for reply items and authentication
#
# uncomment if you want a global default set of attributes
#
Sql(str = "select attribute, value from data where space=? and name=?",
str = "str,str"
str = "DEFAULT"), delall str, del REP:int,
#
# uncomment if you want per-nas values
#
#Sql(str = "select attribute, value from data where space=? and name=?",
# str = "str,str",
# str = "nases",
# str = (NAS-Identifier || NAS-IP-Address)), delall str, del REP:int,
#
# uncomment if you want per-realm values, incl. the strip-realm option
#
Sql(str = "select attribute, value from data where space=? and name=?",
str = "str,str",
str = "realms",
str = (User-Name beforefirst "/" || User-Name afterlast "@")),
delall str, del REP:int,

strip-realm && REQ:User-Name := (User-Name afterfirst "/" ||
User-Name beforelast "@"),
#
# uncomment if you want per-user values
#
Sql(str = "select attribute, value from data where space=? and name=?",
str = "str,User-Name",
str = "users"), delall str, del REP:int,

# Perform database-defined checks based on username on the data we have now.
# Uncomment the Sqlcheck interface in the behaviour file as well if you need
# this.
#
#Sqlcheck(str = "select attribute, op, value from radcheck " .
# "where space=? and name=?",
# str = "str,User-Name",
# str = "users"), delall str, int || halt,

# If the data we obtained has auth-type set to Reject or Accept, act now

auth-type == Reject && halt,
auth-type == Accept && (RAD-Code := Access-Accept, halt),

Calling-Station-Id && (
# Sql(str = "insert into log (log_when, log_who, log_what) " .
# "values (now(), ?, ?)",
# str = "User-Name,str",
# str = "无效认证请求: 收到数据中无IMIS号码,请求来自接入服务器" .
# (NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
# delall str, del REP:int,
# abort

imis || (
Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证失败:数据库中尚未给用户“" .
User-Name . "”配置IMIS号码,收到号码为“" .
Calling-Station-Id . "”,请求来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),

Calling-Station-Id == imis || (
Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证失败: 数据库中用户“" .
User-Name . "”的IMIS号码“" . imis . "”和收到的号码“" .
Calling-Station-Id . "”不同,请求来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),
del REP:int
),

# If we have a User-Password, perform PAP using cleartext or md5-hex hash

User-Password && (

# Decrypt it

REQ:User-Password := (User-Password ^ md5 (REP:Secret . RAD-Authenticator) . "\x00") beforefirst "\x00",

# If we have a clear-password, compare and we're done

clear-password && (
User-Password == clear-password || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证失败: 收到密码“" .
User-Password . "”和明文密码“" . clear-password . "”不同,请求来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证成功: 收到密码“" . User-Password . "”,请求来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
RAD-Code := Access-Accept, halt
),

# If we have a md5-hex-password, hash User-Password with salt and compare

REP:md5-hex-password && (

32 lastof REP:md5-hex-password == hex md5 (User-Password . 4 firstof REP:md5-hex-password) || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证失败: 收到密码“" .
User-Password . "” 和MD5算法散列的密码不符“" .
REP:md5-hex-password . "”,来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证成功: 收到密码“" . User-Password .
"”与MD5算法散列的密码“" . REP:md5-hex-password .
"”相同,来自接入服务器," .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
RAD-Code := Access-Accept, halt
),

# If we have nothing to compare to, reject

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "认证失败: 数据库没有可以核实密码的数据“" .
User-Password . "”,来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt

),


# If we have a CHAP-Password, perform CHAP

CHAP-Password && (

# Set challenge based on authenticator if it's missing from request

CHAP-Challenge || REQ:CHAP-Challenge := RAD-Authenticator,

# If we have nothing to compare to, reject

clear-password || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "CHAP认证失败,数据库中没有对应的数据核实密码“" .
hex CHAP-Password . "”,challenge '" . hex CHAP-Challenge . "',来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),

16 lastof CHAP-Password == md5 (1 firstof CHAP-Password . clear-password . CHAP-Challenge) || (

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "CHAP认证失败: response '" .
hex CHAP-Password . "' to challenge '" . hex CHAP-Challenge .
" does not match '" . clear-password . "',来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
halt
),

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "CHAP认证成功: using response '" .
hex CHAP-Password . "' to challenge '" . hex CHAP-Challenge .
"' which matches '" . clear-password . "', 来自接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,
RAD-Code := Access-Accept, halt
),


# No valid credentials, so reject

Sql(str = "insert into log (log_when, log_who, log_what) " .
"values (now(), ?, ?)",
str = "User-Name,str",
str = "用户没有提供有效的凭证给接入服务器" .
(NAS-Identifier || NAS-IP-Address) . ",IP地址为" . IP-Source),
delall str, del REP:int,

halt

4)字典(dictionary)
字典也是Radius服务器通常配置的一部分,方便对属性对的定义和实现做一些调整,适应一些厂商相关的,和定制化的需求。字典以行的方式分隔,每一行表示一个属性的定义,包括名称,类型和值等等。

比如我们对缺省的字典中位于subdicts的dict.internal添加了一条定制的属性:
$add field 46 imis
表示在我们使用的radius协议中,使用了一条名叫imis的属性,把它的字段ID值定义为未使用过的46。这样在上述行为脚本中就可以访问到这条属性了。

3、Radius测试
安装并配置好或者不修改任何配置(使用缺省的简单配置),运行以后,就可以做一些测试来判断是否可以正常工作了。大多数radius服务器都提供一个radtest的测试程序,可以很方便的对服务器的运行情况进行测试和诊断。它实际上就是模仿一个真实的radius客户端对服务器发送指定的radius协议报文进行测试。如果可以在本地的话,还可以在调试模式下运行服务器,通过观测服务器的反应,可以方便诊断一些问题。

开发进阶

1)模式
除了对radius服务器做一些修改,以满足定制,灵活的认证模式之外,在商业运行中更主要的需求是在管理维护上。我们可以通过数据库,ldap目录等存储结构把维护管理和服务器解耦分离。在我们的实现中,是直接使用OpenRadius提供的mysql数据库模式。这个模式在modules/radsql目录下的schema.mysql文件中,其中是mysql数据库使用的DDL文本。你可以直接导入到数据库中,也可以根据需求做一些调整,如果这些调整不会影响到radsql接口本身(比如仅仅增加一些字段,并且不被radius协议使用的话),仍然可以直接使用radsql模块。因为这个schema是适应了radius协议所使用的属性对的方式,所以如果仅仅是修改定制属性的话,不需要修改模式文件,比如添加imis属性。以下是模式文件内容:
# Example OpenRADIUS schema for MySQL
#
# 2003/04/28 - Created, EvB
# 2003/05/02 - Added groupname and comment columns, EvB

create database openradius;

use openradius;


# Owner of the database

grant all on openradius.* to openradius@localhost identified by 'openradius';


# Data table containing clients, users, groups, realms, hints, huntgroups,
# whatnot; everything that adds attributes, whether for use in subsequent
# queries, for checking or for use in the reply.

create table data (
id int not null auto_increment,
space varchar(8) not null,
name varchar(64) not null,

attribute varchar(64) not null,
value varchar(255),

groupname varchar(64),
comment varchar(255),

primary key (id),
index space_name (space, name)
);


# Logging table

create table log (
log_id int not null auto_increment,

log_when datetime,
log_who varchar(64),
log_what varchar(255),

primary key (log_id)
);


# Accounting table
#
# 1. To detect duplicates in Stop records (the only ones you need for metered
# billing) while recording them, lock the accounting table and check that the
# query
#
# select acct_id
# from accounting
# where acct_nas = ?
# and acct_sessionid = ?
# and acct_timestamp + <dupinterval> >= now()
#
# does not return any records before inserting. The theory behind this is that
# although Acct-Session-Id may collide among NASes and a single NAS may reuse
# the same values quickly, a single NAS' values should be unique over the
# period during which duplicates can be expected.
#
# This period is at most max_retrans_count * timeout, which is typically
# below one minute. Any simplistic NAS' values for Acct-Session-Id should be
# unique within such a short timespan. Even a simple counter that's reset at
# reboot will do as session id that way.
#
# So, we can safely conclude a record is a duplicate -- erring on the safe
# side, i.e. in favour of dropping the record -- if the same NAS generated the
# same Acct-Session-Id in the last minute or two. False negatives are only
# possible if a NAS uses different Session-Ids for the same session, which is
# forbidden by RFC2866.
#
# 2. To match Stop records to Start records (not very useful in most cases, as
# only few broken NASes send information in the Start record that is not
# repeated in the Stop record, and for 'sessions in progress' it's probably
# better to use a separate session table; see below), I'd advise to lock the
# table and use the following statement; do the (standard) insert only if the
# update affected no rows.
#
# update accounting
# set acct_status_type = 'Stop',
# acct_session_time = ?,
# acct_input_octets = ?,
# acct_output_octets = ?,
# acct_input_packets = ?,
# acct_output_packets = ?,
# where acct_nas = ?
# and acct_sessionid = ?
# and acct_timestamp + ? + 600 >= now() # use acct_session_time as bind var
# and acct_timestamp + ? <= now() + 600 # dito
# and acct_status_type = 'Start'
# and user_name = ? # optional
# and nas_port = ? # optional
#
# The idea is to look back in time, 10 minutes on either side around now() -
# session time, for a start record from the same NAS with the same session id.
# A few extra safety checks prevent overeager matching for NASes that are
# broken enough to reuse the same session id for a start record within 10
# minutes. Be sure to only use information for this that is known in both the
# Start- and Stop records; some NASes may not send eg. Framed-IP-Address in
# start records, so don't use that.

create table accounting (
acct_id bigint(22) not null auto_increment,

acct_nas varchar(32), # NAS-IP-Address or NAS-Identifier
acct_session_id varchar(32),
acct_timestamp datetime,

user_name varchar(64),
nas_ip_address varchar(16),
nas_port varchar(16),
service_type varchar(16),
framed_protocol varchar(16),
framed_ip_address varchar(16),
framed_ip_netmask varchar(16),
login_ip_host varchar(16),
login_service varchar(16),
login_tcp_port integer,
class varchar(16),
called_station_id varchar(64),
calling_station_id varchar(64),
nas_identifier varchar(64),
nas_port_type varchar(16),
port_limit integer,

acct_status_type varchar(16),
acct_input_octets integer,
acct_output_octets integer,
acct_session_time integer,
acct_input_packets integer,
acct_output_packets integer,
acct_terminate_cause varchar(16),
acct_multi_session_id varchar(64),
acct_link_count integer,

primary key (acct_id),
unique index nassesstime (acct_nas, acct_session_id, acct_timestamp)
);


# Session table
#
# This is useful to track open sessions, for debugging purposes and to limit
# concurrent access for users, calling station, area codes, realms, whatever.
# I think a separate table is cleaner than overloading the accounting table.
# By keeping session data elsewhere, you can use your accounting table for
# purely billing-related information.
#
# Theory: the session_key is something you choose to identify the session or
# resource for which you want to keep a current counter. It can be a hash of
# nas, user, the full username, just the username suffix, or whatever you need,
# as long as the information is repeated in start, status update and stop
# records.
#
# Each session has a 'check' statement associated with it that may be used to
# verify the actual count using an external module. Most likely, this string
# will have to contain the type of the NAS and the NAS IP address in some way,
# so that your checking module may do the right thing.
#
# Each session can have multiple records with the same session_key; therefore
# you must count the total number of open sessions using sum(opencount). This
# is useful, because this way, each NAS (or even port type!) can have its own
# check statement. Even if you're keeping a large total ports used count across
# a large number of NASes, you can still have a 'checkrad'-like module verify
# it at each individual NAS if it looks like the customer has hit the limits.
#
# When a session is closed, you must decrement the session_count if above 0,
# and you may drop the record if the session_count reaches 0.
#
# Some extra fields are provided for informational purposes only. Of course,
# you can add more if you need them; even Acct-Input-Octets may be useful if
# you have long running sessions and NASes that send status updates.

create table sessions (
session_id integer not null auto_increment, # not related to RADIUS

session_key varchar(64),
session_count integer,
session_check varchar(255),

user_name varchar(64),
nas varchar(64),
nas_port varchar(16),
nas_port_type varchar(16),
framed_ip_address varchar(16),
login_ip_host varchar(16),
login_tcp_port integer,
called_station_id varchar(64),
calling_station_id varchar(64),

primary key (session_id),
index (session_key)
);


# Check items table la FreeRADIUS' radcheck table. Not used by default.
#
#create table radcheck (
# radcheck_id integer not null auto_increment,
# space varchar(8) not null,
# name varchar(64) not null,
#
# attribute varchar(64) not null,
# op varchar(2),
# value varchar(255),
#
# primary key (radcheck_id),
# index space_name (space, name)
#);


# Access rights for the server itself

grant usage on openradius.* to radiusd@localhost identified by 'radiusd';

grant select on data to radiusd@localhost;
grant insert on log to radiusd@localhost;
grant select, insert, update on accounting to radiusd@localhost;
grant select, insert, update, delete on sessions to radiusd@localhost;


# Create some test data

insert into data (space, name, attribute, value)
values ('clients', '127.0.0.1', 'Secret', 'h1dd3n');

insert into data (space, name, attribute, value)
values ('users', 'evbergen', 'clear-password', 'welcome1');
insert into data (space, name, attribute, value)
values ('users', 'evbergen', 'Framed-IP-Address', '172.31.1.1');
insert into data (space, name, attribute, value)
values ('users', 'evbergen', 'Service-Type', 'Framed');

2)子进程模块
模式和子进程模块是对应的,或者说是子进程的约定的一部分。如果已有的模式不能满足我们的需求,同时已有的radius接口模块也同样不能满足,我们就需要对已有的模块进行修改,或者书写自己的模块。已有的模块文件都是使用perl脚本书写的,而且可以满足大多数情况下的需求,同样,定制一个模块也不是非常困难的。我们使用的radsql模块脚本如下:
#!/usr/bin/perl -w
#
# RADSQL - OpenRADIUS module that queries any DBI/DBD-supported database
#
# Usage: radsql [-d] [-n] [-c] [-a] database dbuser dbpass
# radsql -h
#
# 'database' is a DBI connect string without the leading 'dbi:' part.
# -d increases verbosity on stderr and allows module to run standalone
# -n removes 'int' attribute containing number of rows affected from output
# -c treats first three returned columns (attribute, value, op) as check items;
# instead of number of rows affected, returns 1 if all checks OK, -1 if no
# rows found, and 0 otherwise in the 'int' attribute.
# -a turns off autocommit; that means you must explicitly commit or rollback
# your transactions using COMMIT and ROLLBACK as SQL statements (which are
# trapped and passed to DBI's commit() and rollback() methods, resp.)
#
# The module uses the value of the first 'str' attribute from incoming requests
# as a SQL query and the value of the second 'str' attribute as a comma-
# separated list of attributes to use as bind variables. Attributes that are
# not present in the request will not be bound, so a NULL value will be used.
# Attributes may be listed multiple times; subsequent instances will be used
# in that case.
#
# If the query is the same as the one in the previous request, the query is
# not reparsed, only re-executed (possibly using new bind variable values).
# This allows you to choose in the behaviour file whether you want to
# put your values in the query, or use a fixed query and bind variables;
# the latter is much more efficient for most databases, especially Oracle.
#
# Each column returned by the query is sent to OpenRADIUS as 'columnname =
# value'. This allows the mapping SQL fields to RADIUS attributes to be done
# using 'AS' clauses in the SQL query instead of a fixed table as with radldap.
#
# There is one exception. If the name of a column is 'attribute', then its
# value will be used as the fieldname for the next column, instead of the
# fieldname as given by the table or 'AS' clause. This allows you to orient
# attribute sets vertically as well as horizontally (select ... as attribute,
# value from ...).
#
# Author: Emile van Bergen, emile@evbergen.xs4all.nl
#
# Permission to redistribute an original or modified version of this program in
# source, intermediate or object code form is hereby granted exclusively under
# the terms of the GNU General Public License, version 2. Please see the file
# COPYING for details, or refer to http://www.gnu.org/copyleft/gpl.html.
#
# History:
# 2003/04/24 - EvB - Started
# 2003/04/28 - EvB - Added vertical attribute sets (select attribute, value...)
# 2003/05/01 - EvB - Added variable bind attribute set. Previously only str and
# int, in fixed order (first all strs, then all ints).
# - Moved check item support from radchecksql to here
# 2004/08/08 - EvB - Added flag to turn autocommit off. Useful for postgres.


########
# USES #
########

use Getopt::Long;
use DBI qw(:sql_types);
use strict qw(vars);


###########
# OPTIONS #
###########

my $usage = 0;
my $debug = 0;
my $noint = 0;
my $check = 0;
my $noautoc = 0;


########
# MAIN #
########

# Get options

Getopt::Long::Configure("bundling");
GetOptions("h" => \$usage,
"d+" => \$debug,
"n" => \$noint,
"a" => \$noautoc,
"c" => \$check);

if ($usage || !$ARGV[2]) {
die("Usage: radsql [-d] [-n] [-a] [-c] database dbuser dbpass\n" .
" radsql -h\n");
}

# Check that we're running under OpenRADIUS, interface version 1

unless ($debug ||
$ENV{'RADIUSINTERFACEVERSION'} &&
$ENV{'RADIUSINTERFACEVERSION'} == 1) {
die "radsql: ERROR: not running under OpenRADIUS, interface v1!\n";
}

# Connect to database

my $dbh = DBI->connect("dbi:" . $ARGV[0], $ARGV[1], $ARGV[2], { AutoCommit => !$noautoc }) or die "ERROR: Could not connect to @ARGV!\n";

# Set record separator to empty line and loop on input.

$/ = "\n\n";
$| = 1; # Important - we're outputting to a pipe

my $sql;
my $lastsql;
my $sth;
my $a;
my $v;
my $t;
my $n;
my $ca;
my $cv;
my $co;
my $r;
my %pairs;
my %types;
my @bindvars;
my $colcnt;
my $colref;
my $valref;

MESG:
while(<STDIN>) {

# get pairs from message as hash of array refs

%pairs = ();
%types = ();
PAIR:
while(s/^\s*
([A-Za-z0-9:-]+) # attribute ($1)
\s*=\s*
(
(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*| # ip ($3)
(\d+).*| # int or date ($4)
"([^"]*)".*| # quoted str ($5)
([^"].*) # bare str ($6)
)
(\n|$)//mx) {

$a = $1;
if (defined $4) {
$v = $4;
$types{$a} = 1;
} else {
if (defined $3) { $v = $3; }
if (defined $5) { $v = $5; }
elsif (defined $6) { $v = $6; }
$v =~ s/\\x([a-fA-F0-9]{1,2})/pack("H2", $1)/ge;
$types{$a} = 0;
}

push @{$pairs{$a}}, $v;
print STDERR "parsing: a=[$a] v=[$v] t=[$types{$a}]\n"
if ($debug > 1);
}

# Get SQL statement and list of attribute names for bind variables

$sql = shift @{$pairs{str}};
$t = shift @{$pairs{str}};
if (defined $t) { @bindvars = split(',', $t); }
else { @bindvars = (); $t = ''; }
print STDERR "statement: [$sql]\nbindlist: [$t]\n" if ($debug);
if ($debug > 1) {
foreach $a (keys %pairs) { print STDERR "pair: a=[$a] v=[@{$pairs{$a}}] isint=$types{$a}\n"; }
}
next MESG unless $sql;

# Trap COMMIT and ROLLBACK; DBI requires us to use its own abstractions

if (uc $sql eq "COMMIT") { $r = $dbh->commit(); next MESG; }
if (uc $sql eq "ROLLBACK") { $r = $dbh->rollback(); next MESG; }

# Prepare statement if not same as last one

if (!$sth || $sql ne $lastsql) {
if ($sth) { $sth->finish; }
$sth = $dbh->prepare($sql) or die "ERROR: Could not parse SQL!\n";
$lastsql = $sql;
}

# Replace attribute names in bind var array with ref to value, type
# to work around the bind_param pass-by-reference misbehaviour

foreach $a (@bindvars) { $a = [$a, shift @{$pairs{$a}}, $types{$a}]; }

# Bind variables

$n = 0;
foreach $a (@bindvars) {
$n++;
if (!defined $a->[2]) {
$debug and print STDERR "bindvar $n: NULL\n";
next;
}
print STDERR "bindvar $n: a=[$a->[0]] v=[$a->[1]] i=[$a->[2]]\n"
if $debug;
$sth->bind_param($n, $a->[1], $a->[2] ? SQL_INTEGER : SQL_VARCHAR)
or die "ERROR: Could not bind variable $n!\n";
}

# Execute statement

$r = $sth->execute or die "ERROR: Could not execute SQL!\n";
$colcnt = $sth->{NUM_OF_FIELDS};

# If no columns returned, we're done

next MESG if ($colcnt == 0);

# If we're not doing the check item thing, return rows of columns

if ($check == 0) {
$colref = $sth->{NAME};
$r = 0;
while($valref = $sth->fetchrow_arrayref) {
COL: for($n = 0; $n < $colcnt; $n++) {
next COL unless defined $valref->[$n];
$a = $colref->[$n];
if ($a eq 'attribute') { $a = $valref->[$n++]; }
$v = $valref->[$n];
$v =~ s/([\\'"\x00-\x1f\x7f-\xff])/"\\x" . unpack('H2', $1)/ge;
print "$a=$v\n";
print STDERR "returning: $a=[$v]\n" if $debug;
}
$r++;
}
next MESG;
}

# Otherwise, do the check item thing

if ($colcnt != 3) {
die "ERROR: Radcheck query returns $colcnt columns instead of 3!\n";
}

$r = -1;
ROW: while(($ca, $co, $cv) = $sth->fetchrow_array) {

$v = ${$pairs{$ca}}[0];
if ($types{$ca}) {
$v = 0 unless defined $v;
OP: {
$r &= $v < $cv, last OP if $co eq '<';
$r &= $v <= $cv, last OP if $co eq '<=';
$r &= $v != $cv, last OP if $co eq '!=';
$r &= $v >= $cv, last OP if $co eq '>=';
$r &= $v > $cv, last OP if $co eq '>';
$r &= $v == $cv;
}
} else {
$v = '' unless defined $v;
OP: {
$r &= $v lt $cv, last OP if $co eq '<';
$r &= $v le $cv, last OP if $co eq '<=';
$r &= $v ne $cv, last OP if $co eq '!=';
$r &= $v ge $cv, last OP if $co eq '>=';
$r &= $v gt $cv, last OP if $co eq '>';
$r &= $v eq $cv;
}
}
print STDERR "a=[$ca] cv=[$cv] co=[$co] v=[$v] r=[$r]\n"
if ($debug > 1);
}
}
continue {

print STDERR "returning: r=[$r]\n" if $debug;
print "int=$r\n" unless $noint;
print "\n";
}

比数据库模式还要小一些。

3)管理界面。有了数据库模式,和对应模块以及上述radius协议配置的理解,我们就不难了解如何管理和维护数据信息了。这虽然是再开发的主要的内容,但已经和radius服务本身可以方便的分离开来。我们可以在管理界面中配置客户端,添加用户,查看用户记录,radius运行日志等等。

 

原文地址:http://bbs.et8.net/bbs/showthread.php?t=882844

 

  • 相关文章:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

日历

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Arwen Build 90619 Code detection by Codefense  theme by BokeZhuti

Copyright;2009-2009 blog.hit.edu.cn All Rights Reserved 哈工大网络与信息中心