212012
 

VPN的流量自己用不完,就想分给同学们用。又怕流量超了,起码得弄个流量的统计吧。老规矩,网上Google一下,果然已经有人做好了。本文的主要参考了这篇文章,首先对原作者表示感谢。

主机为CentOS 5.x,已经安装好了OpenVPN 2.1.1,MySQL 5.1.60,还缺少关键部件libpam-mysql。

1. 先来安装它:

wget http://prdownloads.sourceforge.net/pam-mysql/pam_mysql-0.7RC1.tar.gz
tar zxvf pam_mysql-0.7RC1.tar.gz
cd pam_mysql-0.7RC1
./configure && make && make install

2. 配置MySQL:

     先用MySQL管理员账号登陆数据库,添加用于VPN操作的账号和记录用的数据库、数据表。

mysql -uroot -p
-- 创建数据库
CREATE DATABASE openvpn;
 
-- 切换数据库
USE openvpn;
 
-- 创建用户,用户名openvpn,密码openvpn(可自行设定)
GRANT ALL ON openvpn.* TO 'openvpn'@'localhost' IDENTIFIED BY 'openvpn';
 
-- 创建用户数据表
CREATE TABLE IF NOT EXISTS `user` (
  `username` char(32) COLLATE utf8_unicode_ci NOT NULL,
  `password` char(128) COLLATE utf8_unicode_ci DEFAULT NULL,
  `active` int(10) NOT NULL DEFAULT '1',
  `creation` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `email` char(128) COLLATE utf8_unicode_ci DEFAULT NULL,
  `note` text COLLATE utf8_unicode_ci,
  `quota_cycle` int(10) NOT NULL DEFAULT '30',
  `quota_bytes` bigint(20) NOT NULL DEFAULT '10737418240',
  `enabled` int(10) NOT NULL DEFAULT '1',
  PRIMARY KEY (`username`),
  KEY `idx_active` (`active`),
  KEY `idx_enabled` (`enabled`)
) DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 
-- 创建日志数据表
CREATE TABLE IF NOT EXISTS `log` (
  `username` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `trusted_ip` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `trusted_port` int(10) DEFAULT NULL,
  `protocol` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `remote_ip` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `remote_netmask` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `bytes_received` bigint(20) DEFAULT '0',
  `bytes_sent` bigint(20) DEFAULT '0',
  `status` int(10) NOT NULL DEFAULT '1',
  KEY `idx_username` (`username`),
  KEY `idx_start_time` (`start_time`),
  KEY `idx_end_time` (`end_time`)
) DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

3. 配置PAM for MySQL:

vim /etc/pam.d/openvpn

        进入输入模式,输入以下配置并保存。

auth            sufficient      pam_mysql.so \
user=用户名 passwd=数据库密码 host=localhost db=数据库名 \
table=user usercolumn=username passwdcolumn=password \
where=active=1 sqllog=0 crypt=1
 
account         required        pam_mysql.so \
user=用户名 passwd=数据库密码 host=localhost db=数据库名 \
table=user usercolumn=username passwdcolumn=password \
where=active=1 sqllog=0 crypt=1

crypt的含义:
        0 (plain);明文存储。
        1 (Y):使用crypt(3)函数,相当于MySQL中的ENCRYPT()函数。
        2 (mysql):使用MySQL的PASSWORD()函数。
        3 (md5):使用MD5。
        4 (sha1):使用SHA1。

        本例中用户名、密码和数据库名都是openvpn
        重启saslauthd:/etc/init.d/saslauthd restart
        在数据库的用户表新加一个用户:mysql -uopenvpn -p

USE openvpn;
INSERT INTO user(username, password) VALUES('test', ENCRYPT('123456'));

        退出mysql,执行:testsaslauthd -u test -p 123456 -s openvpn,看返回值可知是否成功。如果返回值为OK “Success.” 则表明通过libpam-mysql,PAM已经可以操作MySQL数据库了。

4.  安装配置PAM Plugin for OpenVPN:

        由于不知道什么原因,OpenVPN 2.1.1的PAM插件有Bug,不能正常工作,必须使用2.0.9版本的才可以。所以要用2.0.9版本的源码编译出openvpn-auth-pam.so供2.1.1版本使用才行。
        自行下载OpenVPN 2.0.9的source,解压,

./configure && make
#不要 make install 不然就把2.1.1弄乱了
cd plugin/auth
make

把生成的openvpn-auth-pam.so复制到某个位置,比如 /etc/openvpn/2.0/conf 下面。

5.  写一个OpenVPN的配置文件,和上一篇中直接用证书认证的略有不同:

dev tun
proto udp6
port 9998
 
ca /etc/openvpn/easy-rsa/2.0/keys/ca.crt
cert /etc/openvpn/easy-rsa/2.0/keys/server.crt
key /etc/openvpn/easy-rsa/2.0/keys/server.key
dh /etc/openvpn/easy-rsa/2.0/keys/dh1024.pem
 
user nobody
group nogroup
server 10.8.3.0 255.255.255.0
 
keepalive 10 120
persist-key
persist-tun
 
# user/pass auth from mysql
plugin /etc/openvpn/2.0/conf/openvpn-auth-pam.so openvpn
# absolute path is better here
client-cert-not-required
username-as-common-name
 
client-to-client
 
push "redirect-gateway def1"
push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS 8.8.4.4"
 
comp-lzo
 
max-clients 15
 
status status/udp6.log
log-append /var/log/openvpn/udp6.log
verb 3
mute 5

        当然,其中涉及到的日志文件存放的目录要建立好,不然就不能正常记录了,不便于debug。
        老规矩,添加iptables规则:

iptables -t nat -A POSTROUTING -s 10.8.3.0/24 -o venet0 -j SNAT --to-source 外网地址
service iptables save
service iptables restart

        开启OpenVPN:openvpn –config /etc/openvpn/2.0/conf/openvpn-udp6.conf &

6.  客户端设置:

        把之前生成的ca.crt放置到客户端的config文件夹中,再放置如下内容的配置文件进去:

client
dev tun
proto udp6
float
remote 【服务器IPv6地址】 9998
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
auth-user-pass
comp-lzo
verb 3

        按照DozView的说法不加”float”在我这里是不行的,会报错,具体原因可以看OpenVPN关于float的文档

现在,基于用户名/密码的OpenVPN认证方式算是配置完成了,可以用刚刚的test/123456账户登录试一下是否成功。

================ 我是分割线,,下面配置流量控制 =================

利用client-connect和client-disconnect这两个OpenVPN的预设的回调命令,加上它向回调脚本传递的一堆有用的参数(Common Name啦,Bytes received啦,Bytes sent啦什么的),可以实现记录每次连接发起时和断开时的数据,以此统计此次连接的流量,然后根据User表里的配额看是否超标。

connect.sh

#!/bin/bash
 
DB='openvpn'
DBADMIN='openvpn'
DBPASSWD='openvpn'
 
mysql -u$DBADMIN -p$DBPASSWD -e "INSERT INTO log(username,start_time,trusted_ip,trusted_port,protocol,remote_ip,remote_netmask,status) VALUES('$common_name',now(),'$trusted_ip',$trusted_port,'$proto_1','$ifconfig_pool_remote_ip','$route_netmask_1',1)" $DB

disconnect.sh:

#!/bin/bash
 
DB='openvpn'
DBADMIN='openvpn'
DBPASSWD='openvpn'
 
mysql -u$DBADMIN -p$DBPASSWD -e "UPDATE log SET end_time=now(),bytes_received=$bytes_received,bytes_sent=$bytes_sent,status=0 WHERE trusted_ip='$trusted_ip' AND trusted_port=$trusted_port AND remote_ip='$ifconfig_pool_remote_ip' AND username='$common_name' AND status=1" $DB
 
mysql -u$DBADMIN -p$DBPASSWD -e "UPDATE user SET active=0 WHERE user.username IN (SELECT username FROM (SELECT log.username AS username, quota_bytes FROM user, log WHERE log.username='$common_name' AND log.username=user.username AND log.status=0 AND TO_DAYS(NOW())-TO_DAYS(start_time)< =quota_cycle GROUP BY log.username HAVING SUM(bytes_received)+SUM(bytes_sent)>=quota_bytes) AS u);" $DB

第9行代码的意思是检索某个用户在据今天quota_cycle天以内的所有连接记录,并把这些记录中所有出入站流量求和,如果超过quota_bytes这个配额,就锁定这个账户(active=0)。也就是说这个使用量是每日更新的,而不是按自然月计算。

更改为可执行的:chmod +x 上面两个文件。
然后添加到openvpn-udp6.conf配置文件里如下内容:

# record in database
script-security 2 system
client-connect ./connect.sh
client-disconnect ./disconnect.sh

此处坑了我很长时间,刚开始我写的是script-security 2,没加system在后面。结果一直认证失败。看看Log是说这个脚本执行不了:Could not execute external program.
开始以为是权限问题,去掉了服务器conf里user nobody和group nobody,依然不奏效,即使脚本乃至整个conf/文件夹都改成777的权限也不行。直到后来看到一篇博文和论坛里一个话题才知道原因:   

在OpenVPN 2.1_rc9.以后的版本中配置文件中要使用如下语句
script-security 2 system(不保存密码在环境变量中)或script-security 3 system(保存密码在环境变量中)
当使用auth-user-pass-verify  cmd via_env验证密码时,
如果script-security 2时就不能验证密码了,需要使用script-security 3
后面SYSTEM参数的作用是,
The method parameter indicates how OpenVPN should call external commands and scripts. Settings for method:
execve – (default) Use execve() function on Unix family OSes and CreateProcess() on Windows. 
system – Use system() function (deprecated and less safe since the external program command line is subject to shell expansion).
如果不加这个参数使用默认的(execve),那在执行脚本时一般情况下只能使用内置命令.(当然了这个环境也可以说比较安全).可以通过指定路径来运行外部程序,但不一定都是可以运行成功的.

这样,流量控制基本就搞定了。

上面这样还有个问题:如果用户被锁定,第二天也就不能上线和下线了,也就永不能更新被锁定这一状态了。必须有个cron每日更新数据库,刷新流量和解锁用户。所用的脚本和disconnect很像,先恢复所有用户,再锁定依然超标的用户,新建 /etc/cron.daily/openvpn,写入如下内容并添加执行权限:

#!/bin/bash
 
DB='openvpn'
DBADMIN='openvpn'
DBPASSWD='openvpn'
 
mysql -u$DBADMIN -p$DBPASSWD -e "UPDATE user SET active=1" $DB
 
mysql -u$DBADMIN -p$DBPASSWD -e "UPDATE user SET active=0 WHERE user.username IN (SELECT username FROM (SELECT log.username AS username, quota_bytes FROM user, log WHERE log.username=user.username AND log.status=0 AND TO_DAYS(NOW())-TO_DAYS(start_time)< =quota_cycle GROUP BY log.username HAVING SUM(bytes_received)+SUM(bytes_sent)>=quota_bytes) AS u);" $DB
 
mysql -u$DBADMIN -p$DBPASSWD -e "UPDATE user SET active=0 WHERE enabled=0" $DB

最后一行供管理员手动设置enabled=0来手动锁定用户。

此外,我觉得还有个问题,就是如果某个用户一直不下线,也就一直无法登记此次所用流量,而且log.status一直是1,cron也不能拿他怎么样。回头研究一下怎么样让所有用户最长每24小时重新登录一次就好了吧。

 Leave a Reply

(必须填写)

(必须填写,邮件地址不会被泄露)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>