基于用户名/密码认证和流量控制的OpenVPN系统的实现

8月 14th, 2010 | Filed under GNU/Linux, VPS相关

前一阵子,弄到了一个非常便宜的VPS服务器,位置在美国亚利桑那州的凤凰城,线路可以,速度不错,于是乎在上面架起了OpenVPN服务器,以便在“特殊条件”下“更加安全地访问网络”。由于每个月1TB的流量无论如何是用不完的,打算把它分享给朋友们一起用。为了防止被滥用,需要实现OpenVPN系统的用户认证和流量控制。考虑到方便性,需要UDP、TCP协议同时支持。经过一番研究,将其实现方法记录如下。

一、OpenVPN的基本安装与配置

以Debian 5.0系统为例。主要包括OpenVPN服务器程序的安装和证书的生成。

1、下载安装OpenVPN

apt-get install openvpn

2、生成证书

复制生成证书的脚本:

cp -R /usr/share/doc/openvpn/examples/easy-rsa/ /etc/openvpn/

修改证书的变量:

cd /etc/openvpn/easy-rsa/2.0/
nano vars

编辑该文件,将最后几行的变量改成自己的,例如

export KEY_COUNTRY="CN"
export KEY_PROVINCE="BJ"
export KEY_CITY="Beijing"
export KEY_ORG="XX"
export KEY_EMAIL="xxx@xxx.com"

保存退出后,运行脚本设置变量,并清理:

source ./vars
./clean-all

之后就可以生成公钥和私钥证书了,一路回车默认值或yes即可:

./build-ca
./build-key-server server
./build-key client1
./build-dh

实际上,对于用户名/密码认证机制来说,client1可以省略掉。

二、基于MySQL的用户名/密码认证实现

1、安装MySQL Server

apt-get install mysql-server

已安装的可以略过。

2、建立数据库

以管理员身份登录MySQL:

mysql -uroot -p

运行以下SQL命令:

-- 创建数据库
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_mysql模块

apt-get install libpam-mysql

4、配置OpenVPN的PAM

nano /etc/pam.d/openvpn

输入以下内容:

auth            sufficient      pam_mysql.so \
user=openvpn passwd=openvpn host=localhost db=openvpn \
table=user usercolumn=username passwdcolumn=password \
where=active=1 sqllog=0 crypt=1
 
account         required        pam_mysql.so \
user=openvpn passwd=openvpn host=localhost db=openvpn \
table=user usercolumn=username passwdcolumn=password \
where=active=1 sqllog=0 crypt=1

其中数据库、用户名、密码按照自己的实际情况设置。
crypt表示密码在数据库中加密存储的方式,含义如下:

0 (or “plain”):不加密,明文存储。不推荐使用。
1 (or “Y”):使用crypt(3)函数,相当于MySQL中的ENCRYPT()函数。
2 (or “mysql”):使用MySQL的PASSWORD()函数。PAM可能与MySQL的函数不同,不推荐使用。
3 (or “md5″):使用MD5。
4 (or “sha1″):使用SHA1。

MD5我试用过有些问题。最后我使用的是1。
之后重启saslauthd:

/etc/init.d/saslauthd restart

如果出现以下提示:

To enable saslauthd, edit /etc/default/saslauthd and set START=yes (warning).

说明saslauthd未配置成自动启动,则需修改/etc/default/saslauthd文件,将START=no改为START=yes,再重启服务即可。

5、测试saslauthd是否配置成功

登入MySQL数据库:

mysql -uopenvpn -p

执行以下命令:

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

退出后,运行以下命令:

testsaslauthd -u test -p 123456 -s openvpn

如果显示

0: OK "Success."

则说明配置成功。否则,请根据/var/log/auth.log日志查找原因。

6、复制OpenVPN PAM认证模块

cp /usr/lib/openvpn/openvpn-auth-pam.so /etc/openvpn/

7、编写OpenVPN配置文件。

OpenVPN服务启动时,会扫描/etc/openvpn目录中的.conf文件,对于每个文件,启动一个daemon。本系统要实现UDP、TCP登录的同时支持,我的做法是写两份配置文件,即启动两个daemon,分别负责UDP和TCP协议。

nano /etc/openvpn/

输入以下内容

dev tun
proto udp
port 1194
 
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.0.0 255.255.255.0
 
keepalive 20 120
persist-key
persist-tun
 
# user/pass auth from mysql
plugin ./openvpn-auth-pam.so openvpn
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/udp.log
log-append /var/log/openvpn/udp.log
verb 3
mute 5

其中,# user/pass auth from mysql下面的几行是该认证设置的关键所在。
同理,如果想支持TCP,建立一个openvpn-tcp.conf文件,内容跟上面相同,仅仅把

proto udp
server 10.8.0.0 255.255.255.0
status status/udp.log
log-append /var/log/openvpn/udp.log

改为

proto tcp
server 10.10.0.0 255.255.255.0
status status/tcp.log
log-append /var/log/openvpn/tcp.log

即可。
同时,为日志和状态文件建立目录:

mkdir /etc/openvpn/status
mkdir /var/log/openvpn

重启OpenVPN服务:

/etc/init.d/openvpn restart

8、设置iptables

nano /etc/rc.local

exit 0之前添加以下几行:

# iptables for OpenVPN
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o venet0 -j SNAT --to xxx.xxx.xxx.xxx
iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o venet0 -j SNAT --to xxx.xxx.xxx.xxx

其中xxx.xxx.xxx.xxx是你的服务器的IP地址。
然后让其生效:

/etc/rc.local

至此,一个用户名/密码认证的OpenVPN系统就配置完成了。客户端下载使用/etc/openvpn/easy-rsa/2.0/keys/ca.crt作为证书文件,用用户名、密码认证,即可连接。一个典型的客户端配置文件如下:

client
dev tun
proto udp
remote xxx.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
auth-user-pass
comp-lzo
verb 3

其中xxx.com替换为服务器的域名或IP地址。
用刚才建立的test/123456用户连接一下试试吧!

9、“PAM unable to dlopen(/lib/security/pam_mysql.so): /lib/security/pam_mysql.so: undefined symbol: pam_get_item”错误的解决办法

我在Debian 5中,遇到了如上的错误(/var/log/auth.log),导致OpenVPN提示认证失败。解决方法如下:

echo "/lib/libpam.so.0" >> /etc/ld.so.preload
/etc/init.d/saslauthd restart
/etc/init.d/openvpn restart

三、流量控制的实现

总体思路:利用OpenVPN程序在连接、断开时的脚本钩子,将用户的使用信息记录到数据库。根据数据库中的日志,判断用户是否超过流量配额,如果超过,则将用户锁定。

1、连接、断开时的脚本

建立文件/etc/openvpn/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

建立文件/etc/openvpn/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

将文件改为可执行属性:

chmod +x /etc/openvpn/connect.sh
chmod +x /etc/openvpn/disconnect.sh

修改OpenVPN配置文件openvpn-udp.confopenvpn-tcp.conf,添加以下几行:

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

其主要作用是:在用户连接时,在数据库log表中新建一条记录,记录用户的IP地址、端口号、连接时间等信息。在用户断开连接时,更新刚才添加的记录,记下用户的断开连接时间、发送数据量、接收数据量等。然后,对用户的流量进行判断,若超过配额,则将用户锁定(active=0)。
user表中的quota_cycle是用户的流量计算周期,quota_bytes是用户每个周期内最多允许的流量。
connect.shdisconnect.sh脚本文件中调用了OpenVPN的环境变量。OpenVPN在执行脚本时,自动各种设置了环境变量,供脚本使用。具体的环境变量可以查看这里

2、使用cron每天对用户进行检查

以上操作在用户超过流量时自动将用户锁定。每天还应该执行一次检查,把已经恢复流量的用户解锁。可以通过cron实现此功能。
建立文件/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

其思路是:先默认将所有用户解锁,然后将超过流量的用户锁定。同时,管理员可以通过user表中的enabled字段手工禁用用户。
然后给文件可执行权限:

chmod +x /etc/cron.daily/openvpn

3、修改saslauthd的缓存时间

saslauthd默认有一段较长的缓存时间,在用户通过认证后的一段时间里,可以再次通过认证而不需要重新查询数据库。这样不利于实现对超流量用户的立即锁定。
saslauthd启动时有一个-t参数,可以设置其超时时间。修改/etc/default/saslauthd文件,将

OPTIONS="..."

一行,引号最后添上-t 60,可将缓存时间设置为60秒。当然,也可直接将其设置为0,即不缓存。
重启saslauthd服务和OpenVPN,使设置生效:

/etc/init.d/saslauthd restart
/etc/init.d/openvpn restart

综上所述,一个简单的基于用户名/密码认证和流量控制的OpenVPN系统就初步形成了。沿用这个思路,我们还可以通过对连接、断开时间的统计,很容易地实现对用户在线时间的控制。另外,我们还可以借助tc命令,实现对用户连接速率的控制。更进一步,可以做一个Web界面,使用户能够修改密码、查询历史记录、查询流量等。

  1. nick
    9月 1st, 201002:19

    能写一个ssh平台的方案吗? :)

    • DozView
      9月 1st, 201012:16

      ssh的没做过,pam认证的话好弄,主要是流量控制,不知道openssh有没有类似的机制,如果没有就要另想办法了

  2. nick
    9月 5th, 201018:08

    最近一有时间又过来欣赏这篇文章,精神要吃透啊,:D 可惜我这智商太低,还要一段时间啊……

  3. sadotmd
    9月 6th, 201015:30

    Thanks! It worked for me

    • DozView
      9月 10th, 201010:11

      I’m glad it helps. I’ve now implemented a web front-end control panel with it.

  4. jiehanzheng
    10月 22nd, 201019:35

    非常有用,多谢!尤其是用 MySQL 做流量记录的部分~

  5. schemacs
    11月 2nd, 201014:38

    There is one extra space causing error in the second SQL statement near “cron TO_DAYS(NOW())-TO_DAYS(start_time)< =quota_cycle".Just "< =" should be "<=".I found this after I received error mail from my OpenVPN configured based on you post(I just cut/paste the script code somehow).

    • DozView
      11月 3rd, 201010:58

      I believe this is issue with WordPress editor. I tried to fix it but failed. I’ll leave it as it is though.

  6. Dozview
    11月 6th, 201020:05
    #9
  7. OpenSalon
    12月 7th, 201015:31
    #10
  8. jeff
    12月 9th, 201005:05

    hi you have a good tutorial on implemention of bandwith qouta for openvpn,,im done with your great tutorial but if you dont mind,can you share your web front end panel..please

  9. besthd
    1月 7th, 201108:38

    202618:~# /etc/init.d/openvpn restart
    Stopping virtual private network daemon:.
    Starting virtual private network daemon: openvpn-tcp failed!

  10. besthd
    1月 7th, 201108:39

    怎么启动不了啊?

    • DozView
      1月 7th, 201110:01

      大概是TUN设备没开或者配置写错了吧。看一下tail /var/log/syslog的错误信息

  11. besthd
    1月 7th, 201116:56

    TUN设备没开Fri Jan 07 16:51:53 2011 OpenVPN 2.1_rc15 i686-pc-mingw32 [SSL] [LZO2] [PKCS11] built on May 5 2009
    Fri Jan 07 16:52:03 2011 ERROR: could not read Auth username from stdin
    Fri Jan 07 16:52:03 2011 Exiting

  12. besthd
    1月 7th, 201116:57

    Fri Jan 07 16:51:53 2011 OpenVPN 2.1_rc15 i686-pc-mingw32 [SSL] [LZO2] [PKCS11] built on May 5 2009
    Fri Jan 07 16:52:03 2011 ERROR: could not read Auth username from stdin
    Fri Jan 07 16:52:03 2011 Exi
    现在连接的时候出现这个问题,请问怎么解决呀?

  13. besthd
    1月 7th, 201117:23

    解决了 谢谢你的教程哦 因为我的.conf 更改过了 所以有些东西漏掉了

  14. 聊城SEO
    1月 9th, 201101:02

    实在太棒了,学习了 刚做完OPENVPN 打算做流量控制呢,谢谢分享了

  15. besthd
    1月 10th, 201109:55

    还是有点小问题,流量表内无记录,在学习学习楼主的思想吧

    • DozView
      1月 10th, 201110:08

      看一下OpenVPN的日志(在配置文件里设置的路径),八成是SQL语句的问题,可能从网页复制时候变样了。有几个地方多了空格,Wordpress编辑器的bug,楼上有人提到了,你注意改一下再试试。

  16. besthd
    1月 10th, 201112:36

    搞定 果然SQL语句的问题 一个少空格 一个多空格 谢了~

  17. besthd
    1月 11th, 201113:11

    想和其他整合在一起 用MD5认证 确实一直不能成功

    • DozView
      1月 11th, 201113:14

      貌似是pam_mysql的问题,没仔细研究。要把多个整合在一起最省事的还是明文了,虽然有点不安全。

  18. besthd
    1月 11th, 201116:55

    MD5搞定了 主要是apt-get install没把MD5编译进去 手工安装pam_mysql 即可

  19. PH_Kevin
    1月 22nd, 201118:32

    How about in CentOS and for Ubuntu??
    can you post it? TIA

*