Skip to content

Instantly share code, notes, and snippets.

@nasirhafeez
Last active November 5, 2024 10:34
Show Gist options
  • Save nasirhafeez/6669b24aab0bda545f60f9da5ed14f25 to your computer and use it in GitHub Desktop.
Save nasirhafeez/6669b24aab0bda545f60f9da5ed14f25 to your computer and use it in GitHub Desktop.
FreeRADIUS Advanced Use Cases

FreeRADIUS Advanced Use Cases

Contents

Introduction

Installation

Expiration

Changing IP Pool for Expired Username

Expiration After Certain Connection Time

Wrong Password Notification

User MAC Binding

User Binding with NAS

Restrict Service Type

SQLCounter

User Disconnection from RADIUS

pfSense PPPoE Server MPD Bandwidth Rate Limiting

Accepting All Passwords For a Username

Transforming Data Using Regular Expression Sub-Capture

Introduction

This document presents the configurations related to some advanced FreeRADIUS use cases.

Installation

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.

Expiration

The Expiration attribute is entered in radcheck table as follows:

image2

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).

Changing IP Pool for Expired Username

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:

image3

image4

Expiration After Certain Connection Time

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:

image5

Wrong Password Notification

Enter the following code in Post-Auth-Type REJECT section of /etc/freeradius/3.0/sites-enabled/default:

update reply {
	Reply-Message = 'Wrong Password'
}

image6

User MAC Binding

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
}

User Binding with NAS

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:

image7

Users are assigned to usergroups:

image8

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
	}
}

Reference

Restrict Service Type

Suppose we only want to allow requests to RADIUS of the following Service-Type:

  1. Framed
  2. 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
}

SQLCounter

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:

image9

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:

image10

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).

User Disconnection from RADIUS

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

image11

pfSense PPPoE Server MPD Bandwidth Rate Limiting

Reference

MPD RADIUS Documentation

  1. 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
  1. Add link to dictionary.mpd in /usr/share/freeradius/dictionary:
$INCLUDE dictionary.mpd
  1. 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"

Accepting All Passwords For a Username

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               |
+----+----------+------------+----+----------------------+

Transforming Data Using Regular Expression Sub-Capture

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}"
	}
}
@nasirhafeez
Copy link
Author

nasirhafeez commented May 15, 2022

In my example the volume is reset on a daily basis (reset = daily). You can update this based on your requirements to weekly, monthly etc. It can also be 'never'. Check this out for further details.

@V1rusFalcon
Copy link

Sorry, i didn't gave enough information. Once the user reach the limit, input and output octets are reset to a random number that is lover than the limit, and the user can use internet again, all in the same day

@nasirhafeez
Copy link
Author

I haven't seen this issue.

@AhmadSudirman7
Copy link

AhmadSudirman7 commented Aug 31, 2022

hi can we set reset = 30 minutes mean I want to reset every 30 minutes

@nasirhafeez
Copy link
Author

Yes we can. The default counters like daily, monthly are predefined in FreeRADIUS. You can define a custom counter and do all the necessary configs to make it work. Check out the counters in /etc/freeradius/3.0/mods-config/sql/counter/mysql for example.

@AhmadSudirman7
Copy link

how to set ?
because i not see minutcounter.conf

@AhmadSudirman7
Copy link

sqlcounter hourlycounter {
sql_module_instance = sql
dialect = mysql

    counter_name = hourly-Session-Time
    check_name = Max-hourly-Session
    reply_name = Session-Timeout

    key = User-Name
    reset = 1hour

    $INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf

}

i use this and this is working but dont know how to set 30 minutes can help how to solved or any ide for this case ?

@gvt2015
Copy link

gvt2015 commented Jan 25, 2023

Hi, how to set one time usage for user with 1gb limit and 1 day validity in free radius with SQL . After limit either any one condition reaches radius will ask for user credentials again Nas is tplink eap225. With External radius authentication

@nasirhafeez
Copy link
Author

Hi, how to set one time usage for user with 1gb limit and 1 day validity in free radius with SQL . After limit either any one condition reaches radius will ask for user credentials again Nas is tplink eap225. With External radius authentication

Use SQL counter with time and volume based quotas

@hidasw
Copy link

hidasw commented Aug 23, 2023

sqlcounter hourlycounter { sql_module_instance = sql dialect = mysql

    counter_name = hourly-Session-Time
    check_name = Max-hourly-Session
    reply_name = Session-Timeout

    key = User-Name
    reset = 1hour

    $INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf

}

i use this and this is working but dont know how to set 30 minutes can help how to solved or any ide for this case ?

1h is minimum. i tried to set 0.1h but in sqllog.sql still 1 hour (timestamp) interval. please coba saja.

@hamzasec
Copy link

there is no rest api for this like create user on radius server and set daily access??

@nasirhafeez
Copy link
Author

nasirhafeez commented Oct 19, 2023 via email

@hamzasec
Copy link

openwisp better than daloradius ??
my use case is to create co-working space and each user need to use ethernet or wifi should buy it daily or weekly after the time is finished she could not connect with that user anymore

@nasirhafeez
Copy link
Author

nasirhafeez commented Oct 19, 2023 via email

@hamzasec
Copy link

But it's diffucult that each new one come to create new user on radius and set password and expiration date like your tutorial tell is to add this information each time on the database
https://gist.github.com/nasirhafeez/6669b24aab0bda545f60f9da5ed14f25#expiration

@nasirhafeez
Copy link
Author

nasirhafeez commented Oct 19, 2023 via email

@hamzasec
Copy link

I have the front end part it is mobile application developed with flutter i need the back-end one to integrate with it so i search for rest api that already created to implement on my application

@JumaBryan
Copy link

I have the front end part it is mobile application developed with flutter i need the back-end one to integrate with it so i search for rest api that already created to implement on my application

Hello, I could help you setup the back-end api

@welangaieric
Copy link

how can i get session timeout that is stored in radgroupcheck for hotspot users

@Pavewleln
Copy link

If I add a link to
cd /etc/freeradius/3.0/ files with
ln -s ../mods-available/sqlcounte mods
, I will get this view
Checking the configuration of the FreeRADIUS daemon...error (duplicate module "sqlcounter dailycounter { ... }", in the file /etc/freeradius/mods-available/sqlcounter:1 and the file /etc/freeradius/mods-enabled/sqlcounter:1).

How can I fix this?

@Toe-Hlaing-Win
Copy link

Toe-Hlaing-Win commented Mar 25, 2024

Dear,

Is there any way to record hourly usage data.

I try with session-timeout : 3600 seconds and Acct-Interim-Interval : 600 seconds.

It update every 10 mintute and session is stop every 1 hour.

so i got, radacct records for hourly usage.

But, user is disconnect every hours.

@nasirhafeez
Copy link
Author

Don't use session-timeout. Simply query radacct for last hour's records based on interim updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment