Changing IP Pool for Expired Username
Expiration After Certain Connection Time
User Disconnection from RADIUS
pfSense PPPoE Server MPD Bandwidth Rate Limiting
Accepting All Passwords For a Username
Transforming Data Using Regular Expression Sub-Capture
This document presents the configurations related to some advanced FreeRADIUS use cases.
Install MySQL
apt-get install -y mysql-server
Install FreeRADIUS
apt-get install freeradius freeradius-mysql freeradius-utils
Setup MySQL
Open MySQL:
mysql
Enter the following commands:
create database radius;
CREATE USER '<username>'@'localhost' IDENTIFIED BY '<password>';
GRANT ALL PRIVILEGES ON radius.* TO '<username>'@'localhost';
FLUSH PRIVILEGES;
exit
Import MySQL Schema
mysql radius < /etc/freeradius/3.0/mods-config/sql/main/mysql/schema.sql
Setup FreeRADIUS-MySQL Integration
Open SQL configuration file:
nano /etc/freeradius/3.0/mods-available/sql
Set the following options:
dialect = "mysql"
Comment:
# driver = "rlm_sql_null"
Uncomment:
driver = "rlm_sql_${dialect}"
Comment out tls
section in mysql
section:
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 = yes
# tls_check_cert = no
# tls_check_cert_cn = no
# }
Uncomment and add login credentials created earlier:
server = "localhost"
port = 3306
login = "<username>"
password = "<password>"
Uncomment:
read_clients = yes
Save and exit.
Enable SQL Module
ln -s /etc/freeradius/3.0/mods-available/sql /etc/freeradius/3.0/mods-enabled/
Set Permissions
chgrp -h freerad /etc/freeradius/3.0/mods-available/sql
chown -R freerad:freerad /etc/freeradius/3.0/mods-enabled/sql
Restart FreeRADIUS Service
systemctl restart freeradius
Create Dummy User
Open MySQL:
mysql
Add user:
use radius;
insert into radcheck (username,attribute,op,value) values("fredf", "Cleartext-Password", ":=", "wilma");
Authentication Test Using radtest
radtest fredf wilma localhost 0 testing123
You should receive Access-Accept
in response.
The Expiration attribute is entered in radcheck
table as follows:
This means that this username will expire on 25 Apr 2020 at 10:42:00. Session-Timeout is given to user based on this.
Note: expiration
keyword needs to be present in authorize section in
/etc/freeradius/3.0/sites-enabled/default
(it is present by default in
FreeRADIUS 3.x).
Add the following code in authorize section of
/etc/freeradius/3.0/sites-enabled/default
:
expiration{
userlock = 1
}
if(userlock){
# Let him connect with EXPIRED pool in reply
ok
update reply {
Reply-Message := "Your account has expired, %{User-Name} / Reason: DATE LIMIT REACHED"
Framed-Pool := "Expired"
}
}
Testing results:
Suppose a user has paid for a certain amount of time and their username
needs to be expired after that much time online. We can use the
Expire-After
attribute for that:
Enter the following code in Post-Auth-Type REJECT
section of
/etc/freeradius/3.0/sites-enabled/default
:
update reply {
Reply-Message = 'Wrong Password'
}
A new table is created to store username to MAC address bindings:
CREATE TABLE IF NOT EXISTS `macs` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`callingstationid` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'na',
PRIMARY KEY (`id`)
);
The following code is added to authorize
section of /etc/freeradius/3.0/sites-enabled/default
for MAC addition and
authorization. When a username is initially created it has the MAC
address set to na
. Upon first connection the MAC address is saved in
macs
table, and subsequently it is checked.
update control{
#check if callingstationid does not exist in mac_limit Table
Tmp-String-0 = "%{sql: SELECT callingstationid FROM macs WHERE username = '%{User-Name}'}"
#check if mac is available or not in mac_limit table
Tmp-Integer-2 = "%{sql: SELECT count(*) FROM macs WHERE username = '%{User-Name}' AND callingstationid='%{Calling-Station-Id}'}"
}
if(&control:Tmp-String-0 == "na"){
"%{sql: UPDATE macs SET callingstationid = '%{Calling-Station-Id}' WHERE Username = '%{User-Name}'}"
update reply {
Reply-Message := "New mac Added"
}
}
elsif(&control:Tmp-Integer-2 == 0){
update reply {
Reply-Message += "MAC auth Failed"
}
reject
}
A new table is created to store huntgroups:
CREATE TABLE radhuntgroup (
id int(11) unsigned NOT NULL auto_increment,
groupname varchar(64) NOT NULL default '',
nasipaddress varchar(15) NOT NULL default '',
nasportid varchar(15) default NULL,
PRIMARY KEY (id),
KEY nasipaddress (nasipaddress)
) ;
NAS IP addresses are assigned to various huntgroups:
Users are assigned to usergroups:
The following code is added in authorize section after preprocess. This
code adds the Huntgroup-Name attribute to RADIUS Access Request. Then,
it allows users of usergroup hg1users
to only be connected on NASs
belonging to hg1
. Similarly, users of usergroup usergroup2
are only
allowed to be connected to NASs belonging to hg2
.
update request{
Huntgroup-Name := "%{sql:SELECT groupname FROM radhuntgroup WHERE nasipaddress='%{NAS-IP-Address}'}"
}
# bind hg1users usergroup to hg1 huntgroup
if (SQL-Group == "hg1users") {
if (Huntgroup-Name == "hg1") {
#ok
}
else {
update reply {
Reply-Message += "Error: User not allowed connection on this device"
}
reject
}
}
# bind hg2users usergroup to hg2 huntgroup
elsif (SQL-Group == "hg2users") {
if (Huntgroup-Name == "hg2") {
#ok
}
else {
update reply {
Reply-Message += "Error: User not allowed connection on this device"
}
reject
}
}
Suppose we only want to allow requests to RADIUS of the following Service-Type:
- Framed
- Login
And we want to reject all other requests. The following code should be added to authorize section:
#Only allow service types "Framed-User" or "Login-User"; reject all others
if((&request:Service-Type=="Framed-User") || (&request:Service-Type=="Login-User")){
#ok
}
else{
update reply {
Reply-Message += "Wrong service type"
}
reject
}
Time Based Quota
We can use Session-Timeout
attribute to limit session time of a user.
For example, if we wanted to limit a user to only 30 minutes of network
access daily we could set Session-Timeout to 1800 (1800 s = 30 min).
This will ensure that the user automatically gets disconnected after 30
min. But the problem with this approach is that the user can connect
again to get 30 more minutes. To solve this problem, FreeRADIUS has some
pre-defined counters that can be used to assign time-based session
limits (like daily, monthly, etc).
The file mods-enabled/sqlcounter
contains several default counters. This
is the configuration of daily counter:
sqlcounter dailycounter {
sql_module_instance = sql
dialect = mysql
counter_name = Daily-Session-Time
check_name = Max-Daily-Session
reply_name = Session-Timeout
key = User-Name
reset = daily
$INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf
}
This counter checks for an internal attribute Max-Daily-Session
and uses
the session data in DB to calculate the remaining session time of the
user for that day. It then adds that remaining Session-Timeout
to RADIUS
Access Accept packet.
We need to add dailycounter
to sites-available/default
in authorize
section. In DB add the Max-Daily-Session
attribute to radcheck table:
Enable the sqlcounter
module:
cd /etc/freeradius/3.0/mods-enabled
ln -s ../mods-available/sqlcounter
Volume Based Quota
We can use sqlcounter
module to query accounting data in DB and impose
volume-based limits. Suppose we want to allow a user to have 25 MB of
volume daily.
In the file mods-enabled/sqlcounter
create a new counter:
sqlcounter total_volume {
sql_module_instance = sql
dialect = sql
counter_name = Max-Total-Volume
check_name = Max-Volume
key = User-Name
reset = daily
query = "SELECT SUM(acctinputoctets) + SUM(acctoutputoctets) FROM radacct WHERE UserName='%{${key}}'"
}
Register the counter total\_volume
in authorize section in
sites-available/default
. Add the Max-Volume
attribute for that user in
bytes in radcheck
table:
Once the user has exceeded their volume limit they will not be authorized on their next connection attempt. Please note that users who are already connected will not be affected by this restriction. To perform any action on such already-connected sessions that have gone over their volume limit some script can be written that periodically queries the DB and if it finds some user has used up their data limit it performs some action on them (like disconnection, rate-limiting etc).
To disconnect a user from RADIUS we use the user’s username and IP address and secret of NAS in radclient to disconnect that user:
root@ubuntu:~# echo 'User-Name = test1' | radclient -x 192.168.100.35:3799 disconnect ''1234''
Sent Disconnect-Request Id 28 from 0.0.0.0:35685 to 192.168.100.35:3799 length 27
User-Name = "test1"
Received Disconnect-ACK Id 28 from 192.168.100.35:3799 to 0.0.0.0:0 length 30
NAS-Identifier = "MikroTik"
Enabling incoming messages in Mikrotik NAS
- Create
dictionary.mpd
in/usr/share/freeradius/
:
# dictionary.mpd
VENDOR mpd 12341
BEGIN-VENDOR mpd
ATTRIBUTE mpd-rule 1 string
ATTRIBUTE mpd-pipe 2 string
ATTRIBUTE mpd-queue 3 string
ATTRIBUTE mpd-table 4 string
ATTRIBUTE mpd-table-static 5 string
ATTRIBUTE mpd-filter 6 string
ATTRIBUTE mpd-limit 7 string
ATTRIBUTE mpd-input-octets 8 string
ATTRIBUTE mpd-input-packets 9 string
ATTRIBUTE mpd-output-octets 10 string
ATTRIBUTE mpd-output-packets 11 string
ATTRIBUTE mpd-link 12 string
ATTRIBUTE mpd-bundle 13 string
ATTRIBUTE mpd-iface 14 string
ATTRIBUTE mpd-iface-index 15 integer
ATTRIBUTE mpd-input-acct 16 string
ATTRIBUTE mpd-output-acct 17 string
ATTRIBUTE mpd-action 18 string
ATTRIBUTE mpd-peer-ident 19 string
ATTRIBUTE mpd-iface-name 20 string
ATTRIBUTE mpd-iface-descr 21 string
ATTRIBUTE mpd-iface-group 22 string
ATTRIBUTE mpd-drop-user 154 integer
END-VENDOR mpd
- Add link to
dictionary.mpd
in/usr/share/freeradius/dictionary
:
$INCLUDE dictionary.mpd
- Add rate limit to user using
mpd-limit
attribute in the following way:
steve Cleartext-Password := "testing"
mpd-limit = "in#1=all rate-limit 3000000",
mpd-limit = "out#1=all rate-limit 3000000"
When using users file, add the following on top of /etc/freeradius/3.0/users
:
DEFAULT Auth-Type := Accept
When using MySQL, add username with Auth-Type:= Accept
in radcheck
like this:
+----+----------+------------+----+----------------------+
| id | username | attribute | op | value |
+----+----------+------------+----+----------------------+
| 1 | testuser | Auth-Type | := | Accept |
+----+----------+------------+----+----------------------+
Let's say we have an attribute of this form Cisco-AVPair = "client-mac-address=70a7.4130.6438"
as shown below:
User-Name = "user1"
User-Password = "pass1"
Cisco-AVPair = "client-mac-address=70a7.4130.6438"
Suppose we wanted to transform it into this form: Cisco-AVPair = "70:A7:41:30:64:38"
.
We can add the following code in the relevant configration section (such as authorize
section of /etc/freeradius/3.0/sites-enabled/default
):
if (Cisco-AVPair =~ /^client-mac-address=(..)(..).(..)(..).(..)(..)$/){
update request {
Tmp-String-0 = "%{1}:%{2}:%{3}:%{4}:%{5}:%{6}"
Tmp-String-1 = "%{toupper:%{Tmp-String-0}}"
Cisco-AVPair := "%{Tmp-String-1}"
}
}
I haven't seen this issue.