ocserv 配合 FreeRADIUS 实现用户流量统计是一种常见的配置方式,通过 FreeRADIUS 的 radacct 表记录用户流量数据(包括上行流量、下行流量和总流量),并可进一步扩展为流量限制。
1. 安装和配置 FreeRADIUS
1.1 安装 FreeRADIUS
在服务器上安装 FreeRADIUS 和数据库支持(这里全部都是自己编译,因为需要支持MySQL):
pacman -U libmysqlclient* mysql-clients* freeradius* radcli* ocserv-allinone*
1.2 配置数据库支持
1.2.0. 初始化数据库:
mkdir -p /srv/mysql/{data,inno}
chown -R mysql:mysql /srv/mysql
nano -w /etc/mysql/my.cnf
innodb_data_home_dir = /srv/mysql/inno
innodb_log_group_home_dir = /srv/mysql/inno
basedir = /usr
datadir = /srv/mysql/data
mysqld --initialize --user=mysql --basedir=/usr --datadir=/srv/mysql/data --innodb_data_home_dir=/srv/mysql/inno --innodb_log_group_home_dir=/srv/mysql/inno
ALTER USER USER() IDENTIFIED BY 'password';
CREATE USER 'root'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
1.2.1. 创建数据库:
登录数据库并创建 FreeRADIUS 的数据库和用户:
CREATE DATABASE radius;
CREATE USER 'radius'@'localhost' IDENTIFIED BY 'radius';
GRANT ALL PRIVILEGES ON radius.* TO 'radius'@'localhost';
FLUSH PRIVILEGES;
1.2.2. 导入 FreeRADIUS 数据库结构:
mysql -u radius -p radius < /etc/raddb/mods-config/sql/main/mysql/schema.sql
修改表结构,方便统计操作
在 radcheck 和 radreply 表中添加 Domain 字段:
ALTER TABLE radcheck ADD COLUMN domain VARCHAR(64);
ALTER TABLE radreply ADD COLUMN domain VARCHAR(64);
创建用户
INSERT INTO radcheck (username, attribute, op, value, domain)
VALUES ('test@domain.com', 'Cleartext-Password', ':=', 'password', 'domain.com');
- username:用户名, @后的是主机名。
- attribute:认证属性,Cleartext-Password 用于明文密码。
- op:操作符,:= 表示赋值。
- value:密码。
- domain: 服务器的域名, 用于分开不同的服务器
添加应答属性(可选)
在 radreply 表中为用户配置会话的应答属性。
示例:VIP用户可以自定义路由
INSERT INTO radreply (username, attribute, op, value)
VALUES ('user@domain.com', 'Framed-Route', ':=', '41.57.120.0/22');
- attribute:应答属性,Framed-Route 表示分配的 IP 地址。
- value:具体的值,这里是 41.57.120.0/22。
创建用户组
INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES ('free', 'Auth-Type', ':=', 'Accept');
INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES ('user', 'Auth-Type', ':=', 'Accept');
INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES ('vip', 'Auth-Type', ':=', 'Accept');
INSERT INTO radgroupcheck (groupname, attribute, op, value) VALUES ('admin', 'Auth-Type', ':=', 'Accept');
- groupname:组的名称。
- attribute:检查条件属性(如 Auth-Type)。
- op:操作符,通常为 :=。
- value:属性值(如 Accept)。
上面的配置了会导致计费功能失效,请不要设置,这里只是演示而已
将用户分配到组
在 radusergroup 表中,将用户加入指定组。
示例:将用户加入user 组, 注意,这里只是中间组, 由radius管理的, 实际的组需要在响应里面配置
INSERT INTO radusergroup (username, groupname, priority)
VALUES ('test@domain.com', 'vip', 1);
- username:用户名。
- groupname:组名(如 admin、user)。
- priority:优先级,数值越小优先级越高。
配置组的返回属性
在 radgroupreply 表中,为组配置应答属性。
基础组匹配信息
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('free', 'Class', '=', 'optimize');
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('user', 'Class', '=', 'optimize');
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('user', 'Class', '=', 'cnnoroute');
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('vip', 'Class', '=', 'optimize');
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('vip', 'Class', '=', 'cnnoroute');
INSERT INTO radgroupreply (groupname, attribute, op, value)
VALUES ('vip', 'Class', '=', 'transmitted');
- groupname:组的名称。
- attribute:返回的 RADIUS 属性(如 Class 表示组名)。
- op:操作符, =为追加,:=为替换。
- value:返回值(如组的标识 transmitted)。
1.2.3. 配置 FreeRADIUS 使用数据库:
nano -w /etc/raddb/mods-available/sql
dialect = "mysql"
driver = "rlm_sql_${dialect}"
mysql {
# If any of the files below are set, TLS encryption is enabled
tls {
#ca_file = "/etc/ssl/certs/my_ca.crt"
#ca_path = "/etc/ssl/certs/"
#certificate_file = "/etc/ssl/certs/private/client.crt"
#private_key_file = "/etc/ssl/certs/private/client.key"
#cipher = "DHE-RSA-AES256-SHA:AES128-SHA"
tls_required = no
tls_check_cert = no
tls_check_cert_cn = no
}
# If yes, (or auto and libmysqlclient reports warnings are
# available), will retrieve and log additional warnings from
# the server if an error has occured. Defaults to 'auto'
warnings = auto
}
server = "localhost"
port = 3306
login = "radius"
password = "radius"
radius_db = "radius"
read_clients = yes
read_groups = yes # 启用组功能
启用 SQL 模块:
ln -s /etc/raddb/mods-available/sql /etc/raddb/mods-enabled/
1.2.4. 在 FreeRADIUS 服务器中配置
nano -w /etc/raddb/clients.conf , 也可在localhost里面直接改密码
client localhost {
ipaddr = 127.0.0.1 # ocserv 的服务器地址
secret = testing123 # 共享密钥
shortname = ocserv # 客户端的名称
require_message_authenticator = yes
# 发送包含 Message-Authenticator 的请求
nas_type = other # NAS 类型,可选值:other、cisco、nokia 等
}
- ipaddr:客户端的 IP 地址(可以是单个 IP 或子网)
- secret:共享密钥,需与客户端配置一致
- shortname:客户端的标识名称(可选,用于日志和管理)
1.2.5 生成自签名证书
nano -w /etc/raddb/certs/ca.cnf
nano -w /etc/raddb/certs/server.cnf
[ req_distinguished_name ]
commonName = radius.example.com # 将 "radius.example.com" 替换为主机名
cd /etc/raddb/certs
nano -w client.cnf 替换为主机名
./bootstrap
该脚本会生成以下文件:
- server.pem:服务器证书
- server.key:服务器私钥
- ca.pem:CA 证书(客户端需要)
2. 配置 ocserv 使用 FreeRADIUS
2.1 安装 FreeRADIUS 客户端工具
先测试一下
[root@ocserv certs]# radtest test@domain.com password 127.0.0.1 0 testing123
Sent Access-Request Id 58 from 0.0.0.0:44492 to 127.0.0.1:1812 length 74
User-Name = "test"
User-Password = "password"
NAS-IP-Address = 172.16.1.187
NAS-Port = 0
Message-Authenticator = 0x00
Cleartext-Password = "password"
Received Access-Accept Id 34 from 127.0.0.1:1812 to 127.0.0.1:44805 length 83
Message-Authenticator = 0x52136e587e33a7d07e8ee9c2a620ca1a
Framed-IP-Address = 172.16.200.100
Class = 0x6f7074696d697a65
Framed-Route = "41.57.120.0/22 172.16.200.1"
2.2 配置 ocserv
nano -w /etc/ocserv/ocserv.conf
#auth = "radius[config=/etc/radiusclient.default/radiusclient.conf]"
auth = "radius[config=/etc/radcli/radiusclient.conf]"
注意不要覆盖本地组配置的组配置属性
2.3 配置 radiusclient.conf
#nano -w /etc/radiusclient.default/radiusclient.conf
nano -w /etc/radcli/radiusclient.conf
authserver 127.0.0.1
acctserver 127.0.0.1:1813
#servers /etc/radiusclient/servers
servers /etc/radiusclient.default/servers
#dictionary /etc/radiusclient/dictionary
#dictionary /etc/radiusclient.default/dictionary
dictionary /usr/share/radcli/dictionary
#nano -w /etc/radiusclient.default/servers
nano -w /etc/radcli/servers
localhost/localhost testing123
3. 配置 FreeRADIUS 记录用户流量
3.1 启用会计记录
确保 FreeRADIUS 的 acct 模块启用,记录用户会话流量。
nano -w /etc/raddb/sites-available/default
authorize {
...
-sql
# 检查用户是否超出流量限制
if ("%{sql:SELECT disabled FROM user_limits WHERE username = SUBSTRING_INDEX('%{SQL-User-Name}', '@', 1)}" == 1) {
reject # 立即拒绝认证
update reply {
Reply-Message := "Your account has been disabled due to exceeding the traffic limit."
}
}
...
}
accounting {
...
-sql
...
}
session {
...
sql
...
}
3.2 检查会计记录
• 用户连接后,FreeRADIUS 会在 radacct 表中记录用户流量信息。
• 可以通过以下 SQL 查询查看用户流量:
SELECT UserName, AcctInputOctets, AcctOutputOctets, AcctSessionTime FROM radacct;
4. 扩展:流量限制,自动禁用超出的用户
4.1 流量限制表
创建一个用于存储用户流量限制和状态的表:
CREATE TABLE user_limits (
username VARCHAR(64) PRIMARY KEY, — 用户名,统一限制流量,domain可以不同
maxtraffic BIGINT NOT NULL, — 最大流量限制(字节)
currenttraffic BIGINT DEFAULT 0, — 当前已用流量(字节)
disabled TINYINT(1) DEFAULT 0 — 用户是否被禁用(0: 启用, 1: 禁用)
);
4.2 为用户添加初始限额
连接后,可以通过以下 SQL 查询验证用户流量是否被正确记录:
INSERT INTO user_limits (username, maxtraffic) VALUES
('test', 21474836480); -- 20 GB
5.3 显示用户每个月的流量
当用户流量超出 Max-Monthly-Traffic 值时,FreeRADIUS 会自动断开用户的 VPN 连接。
CREATE VIEW `radius`.`radtraffic` AS SELECT username,
SUM(acctinputoctets + acctoutputoctets) AS totaltraffic
FROM radacct
WHERE acctstarttime >= DATE_FORMAT(NOW() ,'%Y-%m-01 00:00:00')
AND acctstarttime < DATE_FORMAT(NOW() + INTERVAL 1 MONTH,'%Y-%m-01 00:00:00')
GROUP BY username;
5.4 通过 SQL 更新用户的流量数据到 user_limits 表
UPDATE user_limits ul
JOIN (
SELECT
username,
SUM(acctinputoctets) +
SUM(acctoutputoctets) AS totaltraffic
FROM radacct
GROUP BY username
) rt ON ul.username = rt.username
SET ul.currenttraffic = rt.totaltraffic;
5.5 禁用超出流量限制的用户
UPDATE user_limits
SET disabled = 1
WHERE currenttraffic > maxtraffic;
5.6 禁用超出流量限制的用户
创建时间15分钟执行事件
CREATE EVENT `radius`.`radlimit`
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL '15' MINUTE
DO UPDATE user_limits ul
JOIN (
SELECT
username,
SUM(acctinputoctets) +
SUM(acctoutputoctets) AS totaltraffic
FROM radacct
GROUP BY username
) rt ON ul.username = rt.username
SET ul.currenttraffic = rt.totaltraffic;
UPDATE user_limits
SET disabled = 1
WHERE currenttraffic > maxtraffic;