How to manage internet clients of an ISP with PPPoE and MikroTik. For
centralized AAA (Authentication, Authorization and Accounting), freeRadius is used.
1. Managing Internet Connections
With
PPPoE, MikroTik and Radius
Dashamir Hoxha <dashohoxha@gmail.com>
Artur Nurja <tatanka@albaniaonline.net>
Copyright (C) 2009 Dashamir Hoxha, Artur Nurja. Permission is granted to copy, distribute and/or modify
this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version
published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and
with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free
Documentation License."
1 Abstract
This article will describe how to manage internet clients of an ISP with PPPoE and MikroTik. For
centralized AAA (Authentication, Authorization and Accounting), freeRadius is used. This article is
based on the work that we have done at AlbaniaOnline ISP (now called Primo).
2 Introduction
The aim of the project was to give PPPoE service to the internet clients, through MikroTik. This is not
difficult and can be implemented easily, however we would like to use freeRadius for AAA, so that we
could keep the data of the clients and their settings in a database. MikroTik should be just a dummy box
with a simple configuration, so that we can throw it away and replace it by another one at anytime. All the
client data (username, password, connection settings etc.) should be stored in the freeRadius database. If
they are in a database the client data can be managed easily by an external application as well (by
integrating the freeRadius database with the application).
2.1 Observations About Implementation Possibilities
Digging into the manual pages of MikroTik, freeRadius, etc. and making experiments, we made these
observations about the different ways a PPPoE service can be implemented with MikroTik and Radius:
● There can be more than one MikroTik servers in a LAN.
● In a MikroTik server we can run more than one PPPoE service (with different names and
profiles).
● Each of the PPPoE services can authenticate the internet users using one or more Radius servers.
● The MikroTik consults the radius server about authenticating a PPPoE user only if this user is not
in his local database.
● A freeRadius server can use one or more MySQL databases for authenticating a user. If it cannot
2. connect to one database, then it switches to another.
● A freeRadius server can act as proxy, getting the answer from another radius server. It can use
more than one radius server for getting the answer and can do even loadbalancing between them,
in case that they are the same (are used for the same thing).
● The attributes of the Internet connection of a client (IP, GW, DNS, bandwidth, etc.) can be defined
in the radius database or in the profile of the client in MikroTik.
● In the freeRadius database, the connection attributes can be set for each client or for the group to
which the client belongs.
● IPs that are assigned to the clients can be random (from an IP pool) or each client can be assigned
a fixed IP, always the same IP no matter to which MikroTik it is connected.
● The MikroTik gateway server can connect the clients to the Internet using IPproxy (NAT) or
using ARPproxy.
2.2 The Architecture of The PPPoE Service
Based on the above observations and on the project goals, we decided to construct the PPPoE service
infrastructure like this:
● For client authentication we should use 2 freeRadius servers. Each of them will query its own
MySQL database and these databases will replicate with eachother (one of them as primary and
the other one as secondary).
The advantages of using two radius servers instead of one, are these:
● Half of the requests will be handled by one and half will be handled by the other, so,
we will have loadbalancing.
● If one of them is not working properly, the other one will handle all the requests and
the service will not be interrupted. This provides high availability of the service (the
service is more stable).
● They also serve as a backup of eachother. If the database of one of them is damaged,
the data can be restored from the other.
● The external application that keeps the client data will be connected to the radius database (only
to the primary one) and will manage the client data there.
● To manage the client connections, 34 or more MikroTik servers can be used. Each of them has
the same configuration and they are connected in the same hub/switch with the clients. Since the
clients connect randomly to each of them, we are going to have loadbalancing between the
MikroTik servers. In general, each of the MikroTik servers can serve up to 400500 clients
simultaneously.
● MikroTik gateways will authenticate the clients by asking the radius servers. Each of them will
have two radius servers in its configuration. Half of them ask first radius1 and if they get no
answer they ask radius2. The other half ask first radius2. This provides loadbalancing for the
radius servers.
3. ● The clients are connected to the Internet through IPproxy (NAT).
● The IP that is assigned to the clients are random, taken from an IP pool (they are not fixed IPs).
3 Installing And Configuring freeRadius, MySQL and MikroTik
3.1 Installing freeRadius
We installed freeRadius on Fedora7. First I installed the packages freeradius and freeradiusmysql :
bash# yum install freeradius freeradius-mysql
Then I enabled the service radiusd and started it:
bash# /sbin/chkconfig --list radiusd
bash# /sbin/chkconfig radiusd on
bash# /sbin/chkconfig --list radiusd
bash# /sbin/service radiusd start
Since freeradius uses the ports 1812 and 1813 (see e.g. the file /etc/services ), I had to open these
ports in the firewall, both for tcp and udp . In order to do this, I edited the file
/etc/sysconfig/iptables and added there these lines:
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 1812 -j ACCEPT
-A RH-Firewall-1-INPUT -m udp -p udp --dport 1812 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 1813 -j ACCEPT
-A RH-Firewall-1-INPUT -m udp -p udp --dport 1813 -j ACCEPT
To apply these modifications in firewall, I restarted the service iptables:
bash# /sbin/service iptables restart
Tip: To check that the ports 1812 and 1813 are open in the firewall, we can use one
of these commands:
bash# /sbin/service iptables status | grep 1812
bash# /sbin/iptables-save | grep 1812
3.2 Testing freeRadius Installation
Just to test that freeRadius is correctly installed and works, we can make a simple configuration using the
standard text files, like this:
Edit the file /etc/raddb/clients.conf . At the section client 127.0.0.1 modify the value
of secret , for example make it local1 . The entry
client 127.0.0.1
4. {
. . .
}
will allow the localhost to use the radius service.
Edit the file /etc/raddb/users . Uncomment there the test user steve (or create another
user with similar details). It should look like this:
steve Cleartext-Password := "testing"
Service-Type = Framed-User,
Framed-Protocol = PPP,
Framed-IP-Address = 172.16.3.33,
Framed-IP-Netmask = 255.255.255.0,
Framed-Routing = Broadcast-Listen,
Framed-Filter-Id = "std.ppp",
Framed-MTU = 1500,
Framed-Compression = Van-Jacobsen-TCP-IP
Edit /etc/raddb/radiusd.conf and make sure that authorization using files is enabled. (It
should be enabled by default, so in general you don't need to modify anything.)
Now we can use the command radtest to request access for user steve with password testing :
bash# radtest --help
bash# radtest steve testing 127.0.0.1 10 local1
bash# radtest steve testing localhost 10 local1
bash# radtest steve testingX 127.0.0.1 10 local1
bash# radtest steve testing 127.0.0.1 10 local1X
In the first and second tests you should get the answer 'AccessAccept'. In the last two tests you should
get the answer 'AccessReject'.
Tip: In order to get more details about what happens in the server, run radiusd in debug mode.
First stop the service: /sbin/service radiusd stop , then run it like this: /usr/sbin/radiusd x or
/usr/sbin/radiusd X .
Note: If you have Windows, you may also wish to use NTradPing (downloadable from
MasterSoft ) instead of radtest. If you do this, or test from any other machine, remember to put
your PC (or the other machine) in your NAS list in the file /etc/raddb/clients.conf .
3.3 Set Up freeRadius to Use a MySQL Database
Now that radius is installed and we have tested that it works correctly, we can create a mysql database for
it and configure radius to use this database.
First let's create a new database and a new database user:
bash$ mysql -p -u root
mysql> CREATE DATABASE radiusdb;
mysql> GRANT ALL ON radiusdb.* TO raduser@localhost IDENTIFIED BY "radpass";
mysql> exit;
5. Now lets create the tables of the database by running the SQL script file that is in the directory
freeradius/doc/examples/:
bash$ mysql -p -u root -D radiusdb < /usr/share/doc/freeradius-
1.1.7/examples/mysql.sql
We should modify now /etc/raddb/sql.conf by setting there the database, the username
and the password that are needed to connect to the mysql server:
# Connect info
server = "localhost"
login = "raduser"
password = "radpass"
# Database table configuration
radius_db = "radiusdb"
Note: For testing/debug purposes, change sqltrace to yes. Then, freeradius will dump all
SQL commands to the debug output.
Note: You may also need to modify the line about sql_user_name in this file.
Edit the file /etc/raddb/radiusd.conf and make there these modifications:
Uncomment the line saying 'sql' in the authorize{} section and comment the line saying
'files'.
Also uncomment the line saying 'sql' to the accounting{} section to tell FreeRADIUS to
store accounting records in SQL as well. This file should then look something like this:
authorise {
preprocess
chap
mschap
suffix
eap
# files
sql
pap
}
accounting {
# We leave "detail" enabled to _additionally_ log accounting to
/var/log/radius/radacct
detail
sql
}
3.4 Testing MySQL
Enter some data into the database:
bash$ mysql -u raduser -p radpassw
mysql> USE radiusdb;
mysql> SHOW TABLES;
6. mysql> INSERT INTO usergroup (UserName, GroupName)
--> VALUES ("radiustest", "testgroup");
mysql> SELECT * FROM usergroup;
mysql> INSERT INTO radcheck (UserName, Attribute, Value)
--> VALUES ("radiustest", "Password", "testpassword");
mysql> SELECT * FROM radcheck;
mysql> INSERT INTO radgroupreply (GroupName, Attribute, op, Value)
--> VALUES ("testgroup","Framed-Compression","==","Van-Jacobsen-TCP-IP");
mysql> INSERT INTO radgroupreply (GroupName, Attribute, op, Value)
--> VALUES ("testgroup","Framed-Protocol","==","PPP");
mysql> INSERT INTO radgroupreply (GroupName, Attribute, op, Value)
--> VALUES ("testgroup","Framed-MTU","==","1500");
mysql> INSERT INTO radgroupreply (GroupName, Attribute, op, Value)
--> VALUES ("testgroup","Service-Type","==","Framed-User");
mysql> quit;
Then stop the service /sbin/service radiusd stop and run radiusd in debug mode: /usr/sbin/radiusd x
or /usr/sbin/radiusd X .
Now check access for the user radiustest with password testpassword :
bash# radtest radiustest testpassword localhost 10 local1
Sending Access-Request of id 224 to 127.0.0.1 port 1812
User-Name = "radiustest"
User-Password = "testpassword"
NAS-IP-Address = 255.255.255.255
NAS-Port = 10
rad_recv: Access-Accept packet from host 127.0.0.1:1812, id=224, length=44
Framed-Compression = Van-Jacobson-TCP-IP
Framed-Protocol = PPP
Framed-MTU = 1500
Service-Type = Framed-User
3.5 Configure MikroTik for Being a PPPoE Server
First, the package PPP needs to be installed in MikroTik.
Now let us suppose that ether1 is connected to WAN and ether2 is connected to LAN. Then add
an IP address to ether1, add a gateway, DNS etc. so that the MikroTik box can connect to internet.
Important: Do not add an IP address to the internal interface (ether2).
If NAT is used, ensure that srcnat/masquerade firewall rule has been added ( /ip firewall nat ... )
and it is working properly. It can be added like this:
> /ip firewall nat add chain=srcnat out-interface=ether1 action=masquerade
Test the connection of the MikroTik server to internet:
> /ping www.google.com
Once you have verified the server’s connectivity, create PPP profiles:
7. > /ppp profile add name="pppoe-128k" local-address=10.10.1.1 dns-
server="192.168.25.101" rate-limit=128k/128k
> /ppp profile add name="pppoe-256k" local-address=10.10.1.1 dns-
server="192.168.25.101" rate-limit=256k/256k
These two profiles have different connection speeds.
Now create a PPPoE server instance (service) and enable it:
> /interface pppoe-server server add service-name="pppoe1"
interface=ether2
one-session-per-host=yes default-profile="pppoe-128k"
> /interface pppoe-server server print
> /interface pppoe-server enable 0
Finally create user accounts:
> /ppp secret add name="test128" password="test128" service=pppoe
profile="pppoe-128k" remote-address=10.10.1.111
> /ppp secret add name="test256" password="test256" service=pppoe
profile="pppoe-256k" remote-address=10.10.1.112
> ppp secret print
Flags: X - disabled
# NAME SERVICE CALLER-ID PASSWORD PROFILE REMOTE-
ADDRESS
0 test128 pppoe test128 pppoe-128k 10.10.1.111
1 test256 pppoe test256 pppoe-256k 10.10.1.112
Now the PPPoE server is ready to answer PPPoE requests and to authenticate PPPoE clients.
Important: We don't need to give an IP address to ether2 (the interface that is on the clients' side) for
PPPoE to work. The PPPoE will assign automatically to the interface a new IP (which is like 10.10.1.1/32
in our example). So, a new virtual IP will be assigned to the interface for each client that is connected to
the server. If we assign an IP address to ether2, then the clients can connect to the internet using ethernet
instead of using pppoe. In general this is not what we want, because the ethernet connections do not
require a username and password and their bandwidth cannot be limited as easily as pppoe connections.
3.6 Testing the PPPoE Service
To configure a windows computer to connect as a PPPoE client, do these:
Open the "New Connection Wizard" (from Network Connections).
In the next window (Network Connection Type) choose "Connect to the Internet".
In the next window (Getting Ready) select the choice "Set up my connection manually".
In the next step of the wizard (Internet Connection), select "Connect using a broadband connection
that requires a username and password".
Then, in the next window (Connection Name), write a name for the connection, something like
"PPPoE".
Next, give the username and password, for example username: test256 , password: test256 .
8. Finally finish the wizard. Now you should have a new connection at the Network Connections.
To configure a Fedora7 computer to connect as a PPPoE client, follow these steps:
From the menus, open System > Administration > Network . You have to give the root password
in order to access this menu.
Create a new connection and select connection type xDSL.
Give username, password, etc.
Activate the new connection.
3.7 Getting MikroTik to Work with RADIUS
Right now we have a MikroTik that is configured as a PPPoE server and we have a freeRadius server that
is configured to check the data in a MySQL database. Now we need to configure MikroTik to use the
radius server for authenticating users. We also need to enter in the database of radius the data of the
clients (username, password and connection properties). Let's see how these can be done.
In MikroTik, add a radius server for the service ppp :
> /radius add service=ppp address=192.168.25.101 secret=mikro1
The IP of the radius server is 192.168.25.101 and it will be used for authenticating PPPoE clients
(users of service ppp ). The secret is like a password that the MikroTik and radius servers use to
verify eachother.
Tell the ppp service to use radius for AAA (Authentication, Authorization and Accounting):
> /ppp aaa set use-radius=yes
> /ppp aaa print
In the radius server, make sure that the configuration file /etc/raddb/clients.conf
contains a section like this:
client 192.168.25.1 {
secret = mikro1
shortname = MikroTik1
}
Then restart the radius service.
Make sure that the user test256 is not authenticated in the ppp service of MikroTik (or remove it if
it is there):
> /ppp secret print
> /ppp secret remove 1
Check and make sure that the client test256 cannot connect to internet using the PPPoE
9. connection.
Now lets add some data about the user test256 in the mysql database of radius:
bash$ mysql -p -u root
mysql> show databases;
mysql> use radius;
mysql> show tables;
mysql> insert into usergroup (Username, GroupName) values ('test256',
'static256');
mysql> select * from usergroup where username='test256';
+------------+-----------+----------+
| UserName | GroupName | priority |
+------------+-----------+----------+
| test256 | static256 | 1 |
+------------+-----------+----------+
1 row in set (0.02 sec)
mysql> insert into radcheck (Username, Attribute, Value, Op)
--> values ('test256', 'Cleartext-Password', 'test256', ":=");
mysql> select * from radcheck where username='test256';
+----+----------+--------------------+----+----------+
| id | UserName | Attribute | op | Value |
+----+----------+--------------------+----+----------+
| 2 | test256 | Cleartext-Password | := | test256 |
+----+----------+--------------------+----+----------+
1 row in set (0.00 sec)
mysql> insert into radreply (UserName, Attribute, Value, Op)
--> values ('test256', 'Framed-IP-Address', '192.168.10.101', ':=');
mysql> select * from radreply where username='test256';
+----+----------+-------------------+----+----------------+
| id | UserName | Attribute | op | Value |
+----+----------+-------------------+----+----------------+
| 1 | test256 | Framed-IP-Address | := | 192.168.10.101 |
+----+----------+-------------------+----+----------------+
1 row in set (0.02 sec)
mysql> insert into radgroupreply (GroupName, Attribute, Value, Op)
--> values ('static256', 'Framed-Protocol', 'PPP', ':=');
mysql> insert into radgroupreply (GroupName, Attribute, Value, Op)
--> values ('static256', 'Service-Type', 'Framed-User', ':=');
mysql> insert into radgroupreply (GroupName, Attribute, Value, Op)
--> values ('static256', 'Framed-Compression', 'Van-Jacobsen-TCP-IP',
':=');
mysql> select * from radgroupreply where groupname='static256';
+----+-----------+--------------------+----+---------------------+
| id | GroupName | Attribute | op | Value |
+----+-----------+--------------------+----+---------------------+
| 5 | static256 | Framed-Protocol | := | PPP |
| 6 | static256 | Service-Type | := | Framed-User |
| 7 | static256 | Framed-Compression | := | Van-Jacobsen-TCP-IP |
+----+-----------+--------------------+----+---------------------+
3 rows in set (0.00 sec)
If you try now to connect to internet as a client with username 'test256' and password 'test256', using
PPPoE, it should work. However, to make sure that it really works through radius authentication, which
gets the authentication data from the database, you can stop the radiusd service (in the radius server) and
run it in debug mode: /usr/sbin/radiusd x .
10. 4 Using Two Radius Servers
4.1 Adding a Second Radius Server in MikroTik
MikroTik can be configured to use more than one radius server for the authentication of the users. It is
done simply by adding additional radius servers:
> /radius add service=ppp address=192.168.25.101 secret=mikro1
> /radius add service=ppp address=192.168.25.102 secret=mikro1
> /radius print
Flags: X - disabled
# SERVICE CALLED-ID DOMAIN ADDRESS SECRET
0 ppp 192.168.25.101 mikro1
1 ppp 192.168.25.102 mikro1
In this case, MikroTik tries to use the first server for authentication, and if it cannot be connected, tries
the second one. This make the service of Radius more robust (if one server is down, there is still the other
one).
The configuration of the second radius server is the same as the first one. Both of their MySQL databases
replicate with eachother (twoway, bidirectional replication), so that both of them can be used at the
same time and they can synchronize the data automatically.
Since both of the servers can be used at the same time (and they synchronize with eachother), then we
can have a kind of load balancing by configuring half of the MikroTik servers to have one radius server as
primary, and the other half to have the other server as primary in the list.
So, this redundancy of the radius servers ensures both highavailability and load balancing.
4.2 Replicating MySQL Databases of Radius
In order to ensure service backup, high availability and load balancing, we replicate the databases of the
radius servers. We do it a twoway replication, so that both of them can be used for reading and writing,
and so that in case that one of them goes off, we don't need to do any manual configuration.
The twoway replication can be done like this:
1. In both servers modify the section [mysqld] in the configuration file /etc/my.cnf and add
these lines:
### configuration as a master for replication
server-id=1
log-bin=mysql_bin
innodb_flush_log_at_trx_commit=1
sync_binlog=1
slave-skip-errors=all
auto_increment_offset=1
auto_increment_increment=10
Make sure the servers have unique (different) server IDs, that skipnetworking is not set, and that
binary logging is enabled. Also make sure that auto_increment_offset is different for each server.
(For the meaning of these options check the manual of mysql.)
11. 2. In the first server create a user that has permission to do replication:
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'192.168.25.%'
--> IDENTIFIED BY 'replpassw';
mysql> FLUSH PRIVILEGES;
3. Make a full backup of the mysql database in the first server:
/usr/bin/mysqldump --user=root --password --all-databases
--lock-all-tables --flush-logs --flush-privileges
--add-drop-database --add-drop-table
--force --master-data > backup.sql
4. Copy it to the other server and restore it:
bash# scp root@192.168.25.101:backup.sql .
bash# /sbin/service mysqld start
bash# mysql -p -u root < backup.sql
5. Setup the second server as slave and start the replication in it:
mysql> CHANGE MASTER TO MASTER_HOST='192.168.25.101',
MASTER_USER='repluser',
MASTER_PASSWORD='replpassw';
mysql> START SLAVE;
mysql> show slave statusG
The options MASTER_LOG_FILE and MASTER_LOG_POS can be given to CHANGE
MASTER TO as well, however they are restored from the backup file (since mysqldump was
called with the option masterdata). If you want to add them manually, then read their values
from the backup file backup.sql .
6. Setup also the first server as a slave of the first master.
a. In the second server (that will be master of the first one), get the status of the master:
mysql> show master status;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql_bin.000001 | 430585 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
b. Set the values of File and Position that are displayed above to the slave (first server), along
with the host, username and password:
12. mysql> stop slave;
mysql> CHANGE MASTER TO MASTER_HOST='192.168.25.102',
MASTER_USER='repluser',
MASTER_PASSWORD='replpassw',
MASTER_LOG_FILE='mysql_bin.000001',
MASTER_LOG_POS=430585;
mysql> start slave;
mysql> show slave statusG
The user 'repluser' has to be created in 192.168.25.102 and it should have replication
permissions. However, since this user was created in 192.168.255.101 and a full backup of
the database was done, this user should have been created when we restored the database to
it.
In the output of the command show slave statusG , make sure that both
Slave_IO_Running and Slave_SQL_Running are Yes , otherwise find out what is the
problem.
4.3 Checking Replicated Databases
It can happen that the replicated databases become inconsistent (especially since we have used the option
slaveskiperrors=all in /etc/my.cnf ). So, it is important to check and make sure that both databases
have exactly the same data. This can be done using the mysql external tools in
http://maatkit.sourceforge.net/ . They can be installed with:
bash$ perl Makefile.PL
bash# make install
Checking that the replicated databases are the same can be done with this command:
bash$ mysql-table-checksum --replicate=replcheck.checksum --replcheck --user=root
--askpass 192.168.25.121
The server that is given in the command should be the master, however, since we use a twoway
replication, it doesn't matter which one we give.
The database replcheck and the table checksum must be created in the database, before we run this
command. The table checksum can be created like this:
CREATE TABLE checksum (
db char(64) NOT NULL,
tbl char(64) NOT NULL,
chunk int NOT NULL,
boundaries char(64) NOT NULL,
this_crc char(40) NOT NULL,
this_cnt int NOT NULL,
master_crc char(40) NULL,
master_cnt int NULL,
ts timestamp NOT NULL,
PRIMARY KEY (db, tbl, chunk)
);
14. Returns the data of a given user. Gets the username of the user as a parameter (type: varchar(32)), and
returns one or more records with the data of the users who match the data of the username. Matching is
done with LIKE. It may return nothing if such a user does not exist. The record that is returned has these
fields: username, srvid, srvname, expiration, enabled, nr_conn
> call radius.user_get('%');
-- returns the list of all the users
> call radius.user_get('%d%');
-- returns all the users with a 'd' in username
> call radius.user_get('xyz');
-- returns nothing because such a user does not exist
> call radius.user_get('test');
-- returns the data of the user 'test':
+----------+-------+----------------+------------+---------+---------+
| username | srvid | srvname | expiration | enabled | nr_conn |
+----------+-------+----------------+------------+---------+---------+
| test | 2 | 256/128 Biznes | 2007-12-30 | 1 | 1 |
+----------+-------+----------------+------------+---------+---------+
1 row in set (0.00 sec)
● user_add
procedure user_add(user varchar(32),
passwd varchar(32),
service_id int(11),
nr_conn varchar(5),
expxiration_date date )
Add a user in the database. It deletes first this user, in case that such a user already exists. It is the
responsibility of the application to check and make sure that the users that are added are unique (no two
users with the same username are added), otherwise the second user will overwrite the first one.
> call radius.user_add('xyz', 'passwd', 3, 2, 2007-12-15);
-- inserts a new user with 2 simultaneous connections
> call radius.user_add('abc', 'passwd1', '4', '1', '2007-12-15');
-- inserts another user with only 1 connection
> call radius.user_add('xyz', 'passwd', 3, 2, '2007-12-15');
-- gives error because the user 'xyz' already exists
● user_update
procedure user_update(user varchar(32),
service_id int(11),
nr_conn varchar(253),
expiration_date date )
Update the data of the given user in the database.
> call radius.user_update('xyz', 3, 2, '2008-1-30');
-- update the data of the user 'xyz'
(for example expiration date is changed)
15. ● user_set_password
procedure user_set_password(user varchar(32),
passwd varchar(32) )
Set a new password to the given user.
> call radius.user_set_password('xyz', 'passwd-1');
● user_change_service
procedure user_change_service(old_srvid varchar(100),
new_srvid varchar(100))
Change an old service to a new one for all the users. This can be used in case that a service becomes
obsolete and all the users of this service should be upgraded to another service.
> call radius.user_change_service('3', '7');
● user_del
procedure user_del(user varchar(32))
Delete the given user.
> call radius.user_del('xyz');
● service_get
procedure service_get(service_id varchar(100),
service_name varchar(40) )
Return a list of services that match the given service id and name. Matching is done with LIKE, so the
service name and id may contain wildcards and the procedure may return more than one record (or none
if nothing matches). The result that is returned contains these fields:
srvid, srvname, download_rate, upload_rate, enabled
Fields download_rate and upload_rate are in Kbps, integers. The field enabled can be 1 or 0. If it is 0,
then this service should not be used for the new clients and after some time may be deleted from the table.
> call radius.service_get('%', '%');
-- returns a list of all the services
> call radius.service_get('%', '%256%');
-- returns a list of the services that contain '256' in the name
> call radius.service_get('%', '256');
16. -- returns nothing because no service has the name '256'
> call radius.service_get('2', '%');
-- returns the service with id=2:
+-------+----------------+---------------+-------------+---------+
| srvid | srvname | download_rate | upload_rate | enabled |
+-------+----------------+---------------+-------------+---------+
| 2 | 256/128 Biznes | 256 | 128 | 1 |
+-------+----------------+---------------+-------------+---------+
1 row in set (0.00 sec)
● service_add
procedure service_add(service_name varchar(40),
download_rate int(11),
upload_rate int(11) )
Add a new service. Download and upload rates are in Kbps. In case that a service with the same id
already exists, it is deleted first and then the new service is added. So, it is the responsibility of the
application to make sure that it does not overwrite existing services.
> call radius.service_add('256/128 Vetem 1 PC', 256, 128);
> call radius.service_add('512/256 Vetem 1 PC', '512', '256');
● service_update
procedure service_update(service_id int(11),
service_name varchar(40),
download_rate int(11),
upload_rate int(11),
enabled int(11) )
Update the attributes of a service.
+ call radius.service_update(2, '256/128 Biznes', 256, 128, 0);
+ call radius.service_update('3', '256/128 Familje', '256', '128', '0');
-- disable these services
● service_del
procedure service_del(service_id int(11))
Delete the service with the given id. It is deleted only if there are no users having this service. See also
user_change_service().
+ call radius.service_del('2');
+ call radius.service_del(3);
-- delete these services
17. 5.3 Implementation
The MySQL procedures can be declared in a file, for example rm_api.sql and then they can be loaded
in the MySQL server like this:
bash$ mysql -p -u root
mysql> ?
mysql . rm_api.sql
or like this:
bash$ mysql -p -u root < rm_api.sql
The declaration of the procedures is like this:
/**
* Set the delimiter of the SQL commands to double semicolon,
* because semicolon needs to be used inside the procedure declaration
* to separate the statements.
*/
DELIMITER ;;
/**
* Select the database that will be used by the procedures and functions.
* The procedures and functions will be attached to this database.
*/
USE radius ;;
/** procedure user_get
* Returns the data of a given user.
* Gets the username of the user as a parameter (type: varchar(32)),
* and returns one or more records with the data of the users who
* match the data of the username. Matching is done with LIKE.
* It may return nothing if such a user does not exist.
* The record that is returned has these fields:
* username, srvid, srvname, expiration, enabled, nr_conn
*/
DROP PROCEDURE IF EXISTS user_get ;;
CREATE PROCEDURE user_get(user varchar(32))
BEGIN
SELECT rm_users.username, rm_services.srvid, srvname, expiration,
enableuser AS enabled, radcheck.Value AS nr_conn
FROM rm_users
LEFT JOIN rm_services USING (srvid)
LEFT JOIN radcheck ON ( rm_users.username = radcheck.UserName
AND radcheck.Attribute = 'Simultaneous-Use' )
WHERE rm_users.username LIKE user;
END
;;
/** procedure user_add
* Add a user in the database.
* Takes these parameters:
* username, password, service_id, nr_conn, expiration_date
*/
DROP PROCEDURE IF EXISTS user_add ;;
CREATE PROCEDURE user_add(user varchar(32),
passwd varchar(32),
service_id int(11),
nr_conn varchar(5),
18. expiration_date date)
BEGIN
### first delete it, in case that such a user exists
CALL user_del(user);
### insert a record into the table rm_users
INSERT INTO rm_users
SET
username = user,
password = MD5(passwd),
srvid = service_id,
expiration = expiration_date,
enableuser = '1',
createdon = NOW(),
createdby = 'admin';
### insert two records into the table radcheck
INSERT INTO radcheck (UserName, Attribute, op, Value)
VALUES
(user, 'Simultaneous-Use', ':=', nr_conn),
(user, 'User-Password', ':=', passwd);
END
;;
/** procedure user_update
* Update the data of the given user in the database.
* Takes these parameters:
* username, service_id, nr_conn, expiration_date
*/
DROP PROCEDURE IF EXISTS user_update ;;
CREATE PROCEDURE user_update(user varchar(32),
service_id int(11),
nr_conn varchar(253),
expiration_date date)
BEGIN
UPDATE rm_users
SET srvid = service_id,
expiration = expiration_date,
enableuser = '1',
createdon = NOW(),
createdby = 'admin'
WHERE username = user;
UPDATE radcheck
SET Value = nr_conn
WHERE UserName = user
AND Attribute = 'Simultaneous-Use';
END
;;
/** procedure user_set_password
* Set a new password to the given user.
* Gets the parameters: user, passwd
*/
DROP PROCEDURE IF EXISTS user_set_password ;;
CREATE PROCEDURE user_set_password(user varchar(32), passwd varchar(32))
BEGIN
UPDATE rm_users
SET password = MD5(passwd)
WHERE username = user;
END
;;
19. /** procedure user_change_service
* Change an old service to a new one for all the users.
* Gets the parameters: old_srvid, new_srvid
*/
DROP PROCEDURE IF EXISTS user_change_service ;;
CREATE PROCEDURE user_change_service(old_srvid varchar(100),
new_srvid varchar(100))
BEGIN
UPDATE rm_users
SET srvid = new_srvid
WHERE srvid = old_srvid;
END
;;
/** procedure user_del
* Delete the given user.
*/
DROP PROCEDURE IF EXISTS user_del ;;
CREATE PROCEDURE user_del(user varchar(32))
BEGIN
DELETE FROM rm_users WHERE username = user;
DELETE FROM radcheck WHERE UserName = user;
END
;;
/** procedure service_get
* Return a list of services that match the given service id and name.
* Matching is done with LIKE, so the service name and id may contain
* wildcards and the procedure may return more than one records
* (or none if nothing matches).
* The result that is returned contains these fields:
* srvid, srvname, download_rate, upload_rate, enabled
* 'download_rate' and 'upload_rate' are in Kbps, integers
* 'enabled' can be 1 or 0. If it is 0, then this service
* should not be used for the new clients and after some
* time may be deleted from the table.
*/
DROP PROCEDURE IF EXISTS service_get ;;
CREATE PROCEDURE service_get(service_id varchar(100),
service_name varchar(40) )
BEGIN
SELECT srvid, srvname, (downrate DIV 1024) AS download_rate,
(uprate DIV 1024) AS upload_rate, enableservice AS enabled
FROM rm_services
WHERE srvid LIKE service_id
AND srvname LIKE service_name;
END
;;
/** procedure service_add
* Add a new service. Parameters that are given to this procedure
* are these: service_name, download_rate, upload_rate
* Download and upload rates are integers in Kbps.
*/
DROP PROCEDURE IF EXISTS service_add ;;
CREATE PROCEDURE service_add(service_id varchar(100),
service_name varchar(40),
download_rate int(11),
upload_rate int(11))
BEGIN
### if such a service exists, delete it first
CALL service_del(service_id);
20. ### insert the new service
INSERT INTO rm_services
SET
srvid = service_id,
srvname = service_name,
downrate = (download_rate * 1024),
uprate = (upload_rate * 1024),
enableservice = '1',
limitexpiration = '1',
poolname = 'pool0';
END
;;
/** procedure service_update
* Update the attributes of a service.
* The parameters of the procedure are:
* service_id, service_name, download_rate, upload_rate, enabled
* Download and upload rates are integers in Kbps.
*/
DROP PROCEDURE IF EXISTS service_update ;;
CREATE PROCEDURE service_update(service_id int(11),
service_name varchar(40),
download_rate int(11),
upload_rate int(11),
enabled int(11))
BEGIN
UPDATE rm_services
SET srvname = service_name,
downrate = (download_rate * 1024),
uprate = (upload_rate * 1024),
enableservice = enabled
WHERE srvid = service_id;
END
;;
/** procedure service_del
* Delete the service with the given id.
* It is deleted only if there are no users having this service.
*/
DROP PROCEDURE IF EXISTS service_del ;;
CREATE PROCEDURE service_del(service_id int(11))
BEGIN
### get the number of the users which have this service
DECLARE cnt INT DEFAULT 0;
SELECT COUNT(*) INTO cnt FROM rm_users WHERE srvid = service_id;
### delete the service only if there are no users having it
IF cnt = 0 THEN
DELETE FROM rm_services WHERE srvid = service_id;
END IF;
END
;;
/** Set the delimiter of the SQL commands back to semicolon. */
DELIMITER ;
5.4 User Access Rights
Access rights can be arranged to the user used by the program in such a way that it is able to execute only
these procedures. It can be done as follows:
21. /**
* Create a user and assign privileges to be able to execute
* the functions and procedures in this file.
*/
USE radius;
DROP USER 'prog'@'192.168.25.%';
CREATE USER 'prog'@'192.168.25.%' IDENTIFIED BY 'progpassw';
GRANT EXECUTE ON PROCEDURE user_get TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE user_add TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE user_update TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE user_set_password TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE user_change_service TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE user_del TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE service_get TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE service_add TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE service_update TO 'prog'@'192.168.25.%';
GRANT EXECUTE ON PROCEDURE service_del TO 'prog'@'192.168.25.%';
/**
* Create a user for phpMyAdmin, which can see only certain tables.
*/
USE radius;
DROP USER 'prog'@'localhost';
CREATE USER 'prog'@'localhost' IDENTIFIED BY 'progpassw';
GRANT SELECT ON rm_users TO 'prog'@'localhost';
GRANT SELECT ON rm_services TO 'prog'@'localhost';
GRANT SELECT ON radcheck TO 'prog'@'localhost';
In order to check what is happening in the database with phpMyAdmin, another local user has been
created, which has only SELECT access.
5.5 Using It On SugarCRM
SugarCRM has some logic hooks where you can call custom code. These logic hooks are placed before
saving a record, before deleting it etc. In these logic hooks we have placed our code which integrates
SugarCRM with the database of freeRadius. So, the clients are managed on SugarCRM, however some
relevant data (like username, password, etc.) are replicated on the freeRadius database as well.
The custom code that does it, looks like this:
SugarCRM/custom/modules/Accounts/UpdateRadiusClients.php:
<?php
/**
* This file contains hooks of the module Account.
*/
require_once('data/SugarBean.php');
require_once('modules/Accounts/Account.php');
require_once('custom/include/MysqlDB.php');
/**
* The class UpdateRadiusClients contains some hooks
* for the module Accounts, which keep updated the data
* of the clients in the radius manager.
*/
class UpdateRadiusClients
{
22. /** connection to the radius database */
var $radb;
var $default_email = 'reception@albaniaonline.net';
function __construct()
{
$this->radb = new MysqlDB('192.168.25.101', 'usr', 'pass', 'radius');
}
function debug($msg)
{
$GLOBALS['log']->fatal($msg);
}
/** Fired before a record is deleted. */
function before_delete(&$bean, $event, $arguments)
{
//delete the user from the radius manager as well
$username = $bean->username_c;
try
{
$this->radb->query("call user_del('$username');");
}
catch (Exception $e)
{
$msg = 'UpdateRadiusClients::before_delete(): ' . $e->getMessage();
$GLOBALS['log']->fatal($msg);
print $e->getMessage();
exit(1);
}
}
/** Fired before a record is saved. */
function before_save(&$bean, $event, $arguments)
{
try
{
//get the old (unmodified yet) values of the data
$old_bean = new Account();
$old_bean->retrieve($bean->id);
//check whether this is a new record or an existing one
if ($old_bean->id=='')
{
//this is a new record, add a new user
$this->add_radius_user($bean);
}
else
{
//update the user data in radius manager
$this->update_radius_user($bean, $old_bean);
}
}
catch (Exception $e)
{
$msg = 'UpdateRadiusClients::before_save(): ' . $e->getMessage();
$GLOBALS['log']->fatal($msg);
print $e->getMessage();
exit(1);
}
}
/** add/insert a new user in the database of radius manager */
function add_radius_user(&$bean)
23. {
//get variables that will be stored in radius
$username = $bean->username_c;
$password = $bean->password_c;
$service = $bean->service_c;
$nr_conn = $bean->simultaneous_connections_c;
$mac = $bean->mac_address_c;
$expiration_date = $bean->expiration_date_c;
$fullname = $bean->name;
if ($fullname=='') $fullname = $username;
//$email = $bean->email1;
$email = $_REQUEST[$_REQUEST['emailAddressPrimaryFlag']];
if ($email=='') $email = $this->default_email;
//make sure that the username is not already in use
if ($this->username_exists_in_sugar($username))
{
$msg = "<strong>The username '$username' is already used "
. "for another client.<br/>n"
. "Please try again with another username.</strong>";
print $msg;
exit(1);
}
if ($this->username_exists_in_radius($username))
{
$msg = "<strong>The username '$username' is already used in RADIUS"
. "for another client.<br/>n"
. "Please try again with another username.</strong>";
print $msg;
exit(1);
}
//build the field comment of radius manager
$comment = $bean->shipping_address_street . ' -- ' . $bean->description;
//add a new user in radius
$query = ("call user_add('$username', '$password', '$service', '$nr_conn',"
. " '$expiration_date', '$mac', '$fullname', "
. "'$email', '$comment');");
$this->radb->query($query);
}
/** update user in the database of radius manager */
function update_radius_user(&$bean, &$old_bean)
{
//get the fields of the client
$username = $bean->username_c;
$password = $bean->password_c;
$service = $bean->service_c;
$nr_conn = $bean->simultaneous_connections_c;
$mac = $bean->mac_address_c;
$expiration_date = $bean->expiration_date_c;
$fullname = $bean->name;
if ($fullname=='') $fullname = $username;
//$email = $bean->email1;
$email = $_REQUEST[$_REQUEST['emailAddressPrimaryFlag']];
if ($email=='') $email = $this->default_email;
//if the old username does not exist in radius, then create it
$old_username = $old_bean->username_c;
if (!$this->username_exists_in_radius($old_username))
{
$query = "call user_add('$username','$password','$service','$nr_conn',"
. " '$expiration_date','$mac','$fullname','$email');";
24. $this->radb->query($query);
}
//update the username if it is changed
if ($username != $old_username)
{
//make sure that the new username is not already in use
if ( $this->username_exists_in_sugar($username) or
$this->username_exists_in_radius($username) )
{
//use the old username
$username = $old_username;
$bean->username_c = $old_username;
//todo: display a notification message about this problem
}
else
{
//change the username
$query = "call user_change_username('$old_username', '$username')";
$this->radb->query($query);
}
}
//update the password if it is changed
$old_password = $old_bean->password_c;
$password = $bean->password_c;
if ($password != $old_password)
{
$query = "call user_set_password('$username', '$password')";
$this->radb->query($query);
}
//build the field comment of radius manager
$comment = $bean->shipping_address_street . ' -- ' . $bean->description;
//update the rest of the fields
$query = ( "call user_update('$username', '$service', '$nr_conn', "
. "'$expiration_date', '$mac', '$fullname', "
. "'$email', '$comment');" );
$this->radb->query($query);
}
/**
* Return true if the given username already exists
* in the sugar database. Otherwise return false.
*/
function username_exists_in_sugar($username)
{
//check in the sugar database
$client = new Account();
$client->retrieve_by_string_fields(array('username_c'=>$username));
if ($client->username_c == $username) return true;
//not found
return false;
}
/**
* Return true if the given username already exists
* in the radius database. Otherwise return false.
*/
function username_exists_in_radius($username)
{
25. //check in the radius database
$query = "select user_check('$username') as username";
$result = $this->radb->query($query);
$record = $result->fetch_object();
if ($record->username == $username) return true;
//not found
return false;
}
}
?>
SugarCRM/custom/include/MysqlDB.php:
<?php
/**
* This class handles the queries to a mysql database.
*/
class MysqlDB
{
var $conn = false; //connection to the database
/** constructor */
function __construct($db_host, $db_user, $db_pass, $db_name)
{
//create a new connection to the database
$this->conn = new mysqli($db_host, $db_user, $db_pass, $db_name);
//check for errors
if (!$this->conn)
{
$errno = mysqli_connect_errno();
$error = mysqli_connect_error();
throw new Exception("Can't connect to DB: $errno : $error");
}
}
function __destruct()
{
$this->conn->close();
}
/** run a mysql query and return the result */
function query($query)
{
//if there is no db connection, cannot run the query
if (!$this->conn) return;
//debug
//$GLOBALS['log']->fatal('MysqlDB::query(): '.$query);
//run the query and get the result
$result = $this->conn->query($query);
//check for errors
if (!$result)
{
$errno = mysqli_errno($this->conn);
$error = mysqli_error($this->conn);
throw new Exception("Query failed: $errno : $error : $queryn");
}
26. //return the result
return $result;
}
}
?>
6 Almost Automated Configuration of MikroTik
After a MikroTik server has been installed, it can be configured quickly by doing copy/paste of the
configuration commands. This can be done easily if we login to MikroTik using a linux terminal.
Note: In order to login to MikroTik through a linux terminal, the network configuration has to be
done manually first, right after the installation. It can be done by adding an IP address. A
gateway may be needed as well.
We assume here that the MikroTik servers are named: MikroTik1, MikroTik2, etc. Initially we have some
configuration files in cfg/ , named MikroTik1.cfg, MikroTik2.cfg, etc. After running the
script generate-mt-setup.sh, the files MikroTik1.mt, MikroTik2.mt, etc. will be
generated in setup/.
6.1 generatemtsetup.sh
The script generate-mt-setup.sh is used to (re)generate the setup configuration scripts for all the
MikroTik servers:
#!/bin/bash
### generate the setup configuration scripts for all the MikroTik servers
./mikromik-setup.sh cfg/MikroTik1.cfg > setup/MikroTik1.mt
./mikrotik-setup.sh cfg/MikroTik2.cfg > setup/MikroTik2.mt
./mikrotik-setup.sh cfg/MikroTik3.cfg > setup/MikroTik3.mt
./mikrotik-setup.sh cfg/MikroTik4.cfg > setup/MikroTik4.mt
./mikrotik-setup.sh cfg/MikroTik5.cfg > setup/MikroTik5.mt
6.2 mikrotiksetup.sh
The script mikrotik-setup.sh is used to generate and output the MikroTik commands that are used
to configure a MikroTik server with PPPoE service. The commands that are generated can be executed on
the MikroTik server with copy/paste (all of them or many of them at once). The script should get an
argument, which is the name of a file which contains configuration parameters that are specific for each
server (for example IP, gateway, etc.).
#!/bin/bash
### This shell script generates and output the commands that are used
### to configure a MikroTik server with PPPoE service.
### The commands that are generated can be executed on the MikroTik server
### with copy/paste (all of them at once).
### get the configuration file as the first parameter
cfgfile=${1:-MikroTik.cfg}
27. ### include the configuration file
. $cfgfile
### generate and output the MikroTik commands
cat <<EOF
### add an address on the outside (WAN) interface of the MikroTik
# / ip address add address=$ADDRESS interface=$WAN_IF
### add a gateway
# / ip route add gateway=$GATEWAY
### set a password
# / password
#
# mt-$ID
# mt-$ID
## change the name of the server
/ system identity set name=MikroTik$ID
### set the DNS servers
/ ip dns set primary-dns=$DNS1 secondary-dns=$DNS2
### setup NAT on the outside interface of the MikroTik
/ ip firewall nat add chain=srcnat out-interface=$WAN_IF action=masquerade
### create a PPPoE service on the inner (LAN) interface of the MikroTik
/ ppp profile add name=pppoe_profile local-address=10.0.0.0
/ interface pppoe-server server add service-name=pppoe_service
default-profile=pppoe_profile interface=$PPPOE_IF
authentication=pap,chap max-sessions=450
one-session-per-host=no disabled=no
### add another address for connecting to the radius server
/ ip address add address=192.168.25.$ID/24 interface=$RADIUS_IF
### disable masquerading for the radius LAN (192.168.25.0/24)
/ ip firewall nat add chain=srcnat out-interface=$WAN_IF
src-address=192.168.25.0/24 action=return
/ ip firewall nat move 1 0
### add radius servers for any PPP service on MikroTik
/ radius add service=ppp address=192.168.25.101 secret=passw
timeout=2000ms called-id=pppoe_service
/ radius add service=ppp address=192.168.25.102 secret=passw
timeout=2000ms called-id=pppoe_service
### tell MikroTik to use the radius servers for authentication and accounting
/ ppp aaa set use-radius=yes
/ ppp aaa set accounting=yes
/ ppp aaa set interim-update=60
/ radius incoming set accept=yes
### add some pools of about 500 IPs each
/ ip pool add name=pool0 ranges=10.0.0.0/23
/ ip pool add name=pool1 ranges=10.0.2.0/23
/ ip pool add name=pool2 ranges=10.0.4.0/23
/ ip pool add name=pool3 ranges=10.0.6.0/23
/ ip pool add name=pool4 ranges=10.0.8.0/23
/ ip pool add name=pool5 ranges=10.0.10.0/23
/ ip pool add name=pool6 ranges=10.0.12.0/23
/ ip pool add name=pool7 ranges=10.0.14.0/23
/ ip pool add name=pool8 ranges=10.0.16.0/23
/ ip pool add name=pool9 ranges=10.0.18.0/23
28. ### logout
/ quit
EOF
6.3 cfg/MikroTik1.cfg
The file cfg/MikroTik1.cfg contains some configuration variables for the values that are different
for each MikroTik that is configured.
### These configuration variables are included by the script
### that generates the MikroTik setup commands. Their values
### should be set according to the server where MikroTik is
### installed. The ID should be a different number from the
### other MikroTiks that connect to the same server radius.
ID=1
WAN_IF=ether1
ADDRESS=192.168.25.21/24
GATEWAY=192.168.25.1
DNS1=192.168.25.11
DNS2=4.2.2.2
PPPOE_IF=ether2
RADIUS_IF=ether2
6.4 setup/MikroTik1.mt
These are the configuration commands that are generated for a MikroTik server by the script
mikrotik-setup.sh , which takes as input the configuration variables in the file
cfg/MikroTik1.cfg . These commands can be copy/pasted to the MikroTik prompt from a terminal.
The commands that are commented should be given manually right after the installation of the MikroTik.
### add an address on the outside (WAN) interface of the MikroTik
# / ip address add address=192.168.25.21/24 interface=ether1
### add a gateway
# / ip route add gateway=192.168.25.1
### set a password
# / password
#
# mt-1
# mt-1
## change the name of the server
/ system identity set name=MikroTik1
### set the DNS servers
/ ip dns set primary-dns=192.168.25.11 secondary-dns=4.2.2.2
### setup NAT on the outside interface of the MikroTik
/ ip firewall nat add chain=srcnat out-interface=ether1 action=masquerade
29. ### create a PPPoE service on the inner (LAN) interface of the MikroTik
/ ppp profile add name=pppoe_profile local-address=10.0.0.0
/ interface pppoe-server server add service-name=pppoe_service
default-profile=pppoe_profile interface=ether2
authentication=pap,chap max-sessions=450
one-session-per-host=no disabled=no
### add another address for connecting to the radius server
/ ip address add address=192.168.25.1/24 interface=ether2
### disable masquerading for the radius LAN (192.168.25.0/24)
/ ip firewall nat add chain=srcnat out-interface=ether1
src-address=192.168.25.0/24 action=return
/ ip firewall nat move 1 0
### add radius servers for any PPP service on MikroTik
/ radius add service=ppp address=192.168.25.101 secret=radiuspassw
timeout=2000ms called-id=pppoe_service
/ radius add service=ppp address=192.168.25.102 secret=radiuspassw
timeout=2000ms called-id=pppoe_service
### tell MikroTik to use the radius servers for authentication and accounting
/ ppp aaa set use-radius=yes
/ ppp aaa set accounting=yes
/ ppp aaa set interim-update=60
/ radius incoming set accept=yes
### add some pools of about 500 IPs each
/ ip pool add name=pool0 ranges=10.0.0.0/23
/ ip pool add name=pool1 ranges=10.0.2.0/23
/ ip pool add name=pool2 ranges=10.0.4.0/23
/ ip pool add name=pool3 ranges=10.0.6.0/23
/ ip pool add name=pool4 ranges=10.0.8.0/23
/ ip pool add name=pool5 ranges=10.0.10.0/23
/ ip pool add name=pool6 ranges=10.0.12.0/23
/ ip pool add name=pool7 ranges=10.0.14.0/23
/ ip pool add name=pool8 ranges=10.0.16.0/23
/ ip pool add name=pool9 ranges=10.0.18.0/23
### logout
/ quit
7 Optimize and Test the Performance
We need to know (or estimate) how much load can keep the server, how many clients it can serve without
creating problems. For this reason, we simulate a high load by sending too many requests to the server, in
order to see up to what point the server can bear the load.
However, before testing the server we should optimize it so that it can have the best performance with the
resources that are available (RAM etc.).
7.1 Optimize RAM
The usage of RAM by the server can be seen at the statistics generated by MRTG. It can be seen also
using linux commands such as top , free, ps and pstree.
● The command top shows the most active processes. The command pstree displays a graphical
tree of processes and their children.
30. ● Using the command ps I can find out the processes that consume the most amount of RAM and
how much uses each of them. I do it like this:
bash# ps -e -o vsize,comm | sort -k1 -n -r | head -n 20
146896 mysqld
123064 radiusd
36412 httpd
36392 httpd
36380 httpd
36380 httpd
36376 httpd
36128 httpd
36128 httpd
36120 httpd
30200 httpd
29600 sort
13072 dbus-daemon
12800 pcscd
11040 snmpd
10720 restorecond
9156 sendmail
8252 sshd
8252 sshd
7996 sendmail
● The command free shows the usage of memory, how much is free etc:
bash# free
total used free shared buffers cached
Mem: 1026844 449416 577428 0 141572 178628
-/+ buffers/cache: 129216 897628
Swap: 1052248 0 1052248
From the graph of the free memory we can notice that gradually the server uses all of the free
RAM that is available. Initially I thought that maybe there is some problem with memory leaking
from some program (e.g. mysqld). However, after googling for this problem, I found out that this
is something normal for a linux server:
The kernel will attempt to use any available memory for buffering and
cacheing, as this makes things run faster. When applications need more
memory, buffer and cache space is released. So the figure you need to
look at in the "free" output is the one in the row marked "/+
buffers/cache:", which shows memory usage not in buffers and cache; that
represents how much memory you're "really" using.
See also these discussions: http://linux.derkeiler.com/MailingLists/Fedora/200502/3149.html
and http://linux.derkeiler.com/Newsgroups/linux.redhat.misc/200401/0220.html .
● In order to optimize the usage of RAM, stop any services that use a lot of memory and are not
necessary. I stopped ConsoleKit and yumupdatesd like this:
service ConsoleKit stop
service ConsoleKit status
chkconfig ConsoleKit off
chkconfig --list ConsoleKit
service yum-updatesd stop
service yum-updatesd status
31. chkconfig yum-updatesd off
chkconfig --list yum-updatesd
7.2 Optimize MySQL
In order to check the performance of the mysql server and to discover any problems, we can use the tools
mysqlreport and mysqlsla , as described in this guide: Nontechnical Guide to Isolating Slow MySQL
Queries .
● First download mysqlreport and mysqlsla from http://hackmysql.com/ and place them at /usr/
local/bin/ .
● Then create the cron job /etc/cron.daily/mysqlreport.sh (which will send a daily
report by email):
#!/bin/bash
### send a report to root by email, about the performance of the mysql
server
/usr/local/bin/mysqlreport --all --flush-status
--user root --password sqladmin --email root
Alternatively, this command can be executed periodically from the command line:
mysqlreport --all --user root --password
● Add these lines in /etc/my.cnf , in order to log the slow queries:
### log slow queries
log-slow-queries
long_query_time=1
Then restart mysql: service mysql restart .
● Finally, wait for the report (which should come daily) and check the values of Read ratio , Slow ,
Waited , etc. (as described in http://hackmysql.com/nontech ). If there are slow queries, find out
which are these queries, using a command like this:
mysqlsla --slow /var/lib/mysql/slow_queries.log > top_10_slow_queries
In order to optimize mysql we can do these things:
● Modify the file /etc/my.cnf similarly to the following lines and then restart mysqld:
[mysqld]
. . . . .
### options for increasing performance
skip-name-resolve
max_connections=500
32. key_buffer=128M
table_cache=256
read_buffer_size=1M
read_rnd_buffer_size=4M
myisam_sort_buffer_size=64M
thread_cache_size=8
query_cache_size=16M
thread_concurrency=2
. . . . .
[mysqldump]
quick
max_allowed_packet = 16M
[isamchk]
key_buffer = 128M
sort_buffer_size = 128M
read_buffer = 2M
write_buffer = 2M
[myisamchk]
key_buffer = 128M
sort_buffer_size = 128M
read_buffer = 2M
write_buffer = 2M
These values are mainly copied from the file /usr/share/mysql/my-large.cnf . See
also the articles in http://www.databasejournal.com/features/mysql/article.php/3367871 and http://
www.debianhelp.co.uk/mysqlperformance.htm .
● As advised in /usr/local/share/doc/freeradius/tuning_guide , we convert to
the engine innodb the table radacct :
mysql> ALTER TABLE radacct ENGINE=INNODB;
● Then, to increase the performance of the innodb engine, we also add these parameters at
/etc/my.cnf :
### innodb options for incleasing performance
innodb-buffer_pool_size=256M
innodb_additional_mem_pool_size=20M
innodb_log_file_size=64M
innodb_log_buffer_size=8M
● We create a multi column index for the (UserName,AcctStopTime) attributes, as advised in
/usr/local/share/doc/freeradius/tuning_guide :
mysql> EXPLAIN SELECT UserName, AcctStopTime FROM radacct;
mysql> ALTER TABLE radacct ADD INDEX username_acctsstoptime
--> (UserName, AcctStopTime);
mysql> EXPLAIN SELECT UserName, AcctStopTime FROM radacct;
Read also this article:
http://www.databasejournal.com/features/mysql/article.php/10897_1382791_1
33. 7.3 Optimize FreeRADIUS
The optimization of FreeRADIUS is based on the suggestions given in the file
/usr/local/share/doc/freeradius/tuning_guide .
● First of all we add the noatime attribute to the log files:
chattr -R -A /usr/local/var/log/radius/
lsattr /usr/local/var/log/radius/
This will decrease the number of accesses to the disk because access time of the log files will not
be recorded.
● Disable at all detailed logs in the configuration file
/usr/local/etc/raddb/radiusd.conf by commenting them out (they can be found by
searching for the word detail ):
# Log authentication requests to the log file.
#
# allowed values: {no, yes}
#
#log_auth = yes
log_auth = no
# Write a detailed log of all accounting records received.
#
# detail {
# detailfile = ${radacctdir}/%{Client-IP-Address}/detail-%Y%m%d
# detailperm = 0600
# }
# detail auth_log {
# detailfile = ${radacctdir}/%{Client-IP-Address}/auth-detail-%Y%m%d
# detailperm = 0600
# }
# detail reply_log {
# detailfile = ${radacctdir}/%{Client-IP-Address}/reply-detail-%Y%m
%d
# detailperm = 0600
# }
# detail pre_proxy_log {
# detailfile = ${radacctdir}/%{Client-IP-Address}/pre-proxy-detail-
%Y%m%d
# detailperm = 0600
# }
# detail post_proxy_log {
# detailfile = ${radacctdir}/%{Client-IP-Address}/post-proxy-detail-
%Y%m%
# detailperm = 0600
# }
authorize {
# If you want to have a log of authentication requests,
# un-comment the following line, and the 'detail auth_log'
# section, above.
34. # auth_log
}
accounting {
#
# Create a 'detail'ed log of the packets.
# Note that accounting requests which are proxied
# are also logged in the detail file.
# detail
# daily
}
post-auth {
#
# If you want to have a log of authentication replies,
# un-comment the following line, and the 'detail reply_log'
# section, above.
# reply_log
}
pre-proxy {
# If you want to have a log of packets proxied to a home
# server, un-comment the following line, and the
# 'detail pre_proxy_log' section, above.
# pre_proxy_log
}
post-proxy {
# If you want to have a log of replies from a home server,
# un-comment the following line, and the 'detail post_proxy_log'
# section, above.
# post_proxy_log
}
● We can also comment the lines related to unix , radwtmp and radutmp in radiusd.conf .
● Increase the value of cleanup_delay and max_requests :
# cleanup_delay: The time to wait (in seconds) before cleaning up
# a reply which was sent to the NAS.
#
# The RADIUS request is normally cached internally for a short period
# of time, after the reply is sent to the NAS. The reply packet may be
# lost in the network, and the NAS will not see it. The NAS will then
# re-send the request, and the server will respond quickly with the
# cached reply.
#
# If this value is set too low, then duplicate requests from the NAS
# MAY NOT be detected, and will instead be handled as seperate requests.
#
# If this value is set too high, then the server will cache too many
# requests, and some new requests may get blocked. (See 'max_requests'.)
#
# Useful range of values: 2 to 10
#
#cleanup_delay = 5
cleanup_delay = 10
# max_requests: The maximum number of requests which the server keeps
# track of. This should be 256 multiplied by the number of clients.
# e.g. With 4 clients, this number should be 1024.
35. #
# If this number is too low, then when the server becomes busy,
# it will not respond to any new requests, until the 'cleanup_delay'
# time has passed, and it has removed the old requests.
#
# If this number is set too high, then the server will use a bit more
# memory for no real benefit.
#
# If you aren't sure what it should be set to, it's better to set it
# too high than too low. Setting it to 1000 per client is probably
# the highest it should be.
#
# Useful range of values: 256 to infinity
#
#max_requests = 1024
max_requests = 15000000
● Turn off proxy requests:
#proxy_requests = yes
#$INCLUDE ${confdir}/proxy.conf
proxy_requests = no
● Increase some values at thread pool :
thread pool {
#max_servers = 32
max_servers = 128
#min_spare_servers = 3
#max_spare_servers = 10
min_spare_servers = 5
max_spare_servers = 16
}
● Modify /usr/local/etc/raddb/sql.conf and increase the number of sql connections. It
should be greater than the number of the radius servers:
# number of sql connections to make to server
#num_sql_socks = 5
### this value is greater than max_servers (=128)
num_sql_socks = 130
# number of seconds to dely retrying on a failed database
# connection (per_socket)
#connect_failure_retry_delay = 60
connect_failure_retry_delay = 10
● Finally restart radiusd : /usr/local/sbin/rc.radiusd restart
37. acct_pkg_update="
User-Name=$user,
Acct-Session-Id=$id,
Acct-Status-Type=Interim-Update,
Service-Type=Framed-User,
Framed-Protocol=PPP,
User-Name=$user ,
Calling-Station-Id=00:11:43:9f:90:53,
Called-Station-Id=Test,
NAS-Port-Id=ether2,
Framed-IP-Address="10.2.255.255",
Acct-Authentic=RADIUS,
Acct-Session-Time=5461,
Acct-Input-Octets=974290,
Acct-Input-Gigawords=0,
Acct-Input-Packets=6458,
Acct-Output-Octets=3578820,
Acct-Output-Gigawords=0,
Acct-Output-Packets=6338,
NAS-Identifier=TEST,
Acct-Delay-Time=0,
Client-IP-Address=192.168.25.22"
### start the connection
#echo $acct_pkg_stop | $radclient $radius_server acct $secret
echo $auth_pkg | $radclient $radius_server auth $secret
echo $acct_pkg_start | $radclient $radius_server acct $secret
### send update account packages periodically until stop time
curr_time_sec=$(date +%s)
#echo $curr_time_sec ## debug
while [ $curr_time_sec -lt $stop_time_sec ]
do
echo $acct_pkg_update | $radclient $radius_server acct $secret
sleep $interval
curr_time_sec=$(date +%s)
#echo $curr_time_sec ## debug
done
### stop the connection
echo $acct_pkg_stop | $radclient $radius_server acct $secret
● testradius.sh
#!/bin/bash
### test the resources of the radius server by starting
### many clients and sending a lot of request packages to it
### go to this directory
cd $(dirname $0)
### get the parameters of the script
if [ "$2" = "" ]
then
echo "Usage: $0 nr_clients stop_time"
exit
fi
nr_clients=$1
stop_time=$2
for (( i=0 ; i < $nr_clients ; i++ ))
do
id=$(expr $i + 1)
echo ./radius-client.sh $id $stop_time
38. ./radius-client.sh $id $stop_time &
sleep 1
done
● stoptest.sh
#!/bin/bash
### stop the test by killing the processes
pslist=$(ps ax | grep radius-client.sh | gawk '{print $1}')
for ps in $pslist
do
kill -9 $ps
done
8 Making Backups
Backups are stored in the directory /backup/ on the radius server. Some of them are generated
periodically by cron jobs, some of them manually. They can be downloaded from
http://192.168.25.101/backup/ in order to write them in a CD or DVD. They are made accessible via http
by the configuration file /etc/httpd/conf.d/backup.conf, which has this content:
Alias /backup /backup
<Directory "/backup">
Options +Indexes
order deny,allow
deny from all
allow from 127.0.0.1
allow from 192.168.25.0/24
</Directory>
8.1 MikroTik
Binary backups of the MikroTik servers can be done by the script
/backup/mikrotik/backup.sh . It takes automatically backups from the MikroTik servers. In
case that a MikroTik server needs to be reinstalled for some reason, its configuration can be restored
easily from the backup. First the backup is uploaded by FTP on the MikroTik server, then it is restored
using the command " / system backup save name=filename.backup " .
#!/bin/bash
### Get binary backups of all the MikroTik servers.
### This script can be executed manually or periodicly (by a cron job).
### However, since the configuration of the MikroTik servers is standard
### and does not change over the time (because all the data of the clients
### are stored in radius), there is no need to make the backup periodically.
### mtexec.exp is a script that can execute a MikroTik command remotely
### it requires expect (install it with `yum install expect`)
exec_mt_cmd=/usr/local/bin/mtexec.exp
### Make a backup of the MikroTik router and get it here.
### Gets these parameters: server_ip passwd filename
function get_backup
{
# get the parameters
39. server_ip=$1
passwd=$2
filename=$3
# make a binary backup of the MikroTik server
$exec_mt_cmd "$passwd" "$server_ip" "/ system backup save name=MikroTik.backup"
# get it here
wget "ftp://admin:$passwd@$server_ip/MikroTik.backup"
# copy it to the given destination filename
mv MikroTik.backup $filename
}
### get the backup of all the MikroTik servers
get_backup 192.168.25.101 "mt-1" backups/MikroTik1.backup
get_backup 192.168.25.102 "mt-2" backups/MikroTik2.backup
get_backup 192.168.25.103 "mt-3" backups/MikroTik3.backup
#get_backup 192.168.25.104 "mt-4" backups/MikroTik4.backup
#get_backup 192.168.25.105 "mt-5" backups/MikroTik5.backup
The script /usr/local/bin/mtexec.exp is used to execute a MikroTik command remotely (see
the section Executing MikroTik Commands From a Script below).
8.2 Database
A backup of the database of radius is made each night, however the backups older than a week are deleted
automatically in order to free disk space. They can be downloaded from
http://192.168.25.101/backup/radius_db/backups/ (by a script that uses wget, or manually) and then
optionally can be written to another backup media (such as CD or DVD).
The cron script that does this is /etc/cron.daily/backup-db.cron which is actually a symbolic
link to /backup/radius_db/backup-db.cron . It just calls the script that makes the backup:
#!/bin/bash
### Make a backup of the database daily.
### It should be linked to cron.daily like this:
### ln -s /backup/radius_db/backup-db.cron /etc/cron.daily/
/backup/radius_db/backup-db.sh
The script /backup/radius_db/backup-db.sh makes a full backup of the database and cleans
any backups that are older than a week:
#!/bin/bash
### This script makes a full backup of the database.
### It should be called periodically from a cron job.
###
### To restore, first unzip the backup file, and then use the command:
### bash$ mysql -p -u root < radiusdb.2007-10-27.sql
### go to this directory
cd $(dirname $0)
### create a filename for the backup, which contains the date
filename="backups/radiusdb.$(/bin/date +%Y-%m-%d).sql"
40. ### backup all the databases
/usr/bin/mysqldump --user=root --password=sqladmin --all-databases --flush-logs
--lock-all-tables --force --master-data=2 > $filename
gzip -f $filename
### find and clean any backup files older than a week
find backups/ -ctime +7 | xargs rm -f
To restore a backup file, first unzip it, and then use the command:
bash$ mysql -p -u root < radiusdb.2007-10-27.sql
8.3 Server
A backup of the data and configurations of the radius server can be done by the script
/backup/radius_server/backup.sh . There is no need to do this backup periodically or
automatically, it can be done manually whenever any modifications on the configuration are done. The
files and directories that are backuped by the script are those that are listed in the file
/backup/backup_server/backup-list.txt .
The created backup archive can be retrieved remotely from http://192.168.25.101/backup/radius_server/
in order to store it to another backup media (such as CD, DVD, etc.). This backup can be used to build
quickly another radius server that is almost identical as the original one (or to rebuild it in case of failure).
● backup/radius_server/backup_server.sh
The script /backup/radius_server/backup_server.sh is used to backup the data and
configurations of the server:
#!/bin/bash
### Backup the data and configurations in the radius server.
### In case of failure, we should be able to setup the server by just
### installing fedora7 and restoring the backup.
### go to this directory
cd $(dirname $0)
### get a current list of the installed packages
#rpm -qa | sort > pkglist.txt
#rpm -qa | sed 's/-[0-9].*//' | sort > pkglist_1.txt
yum list installed > pkglist.txt
yum list installed | gawk '{print $1}' | sed 's/.[^.]*$//' > pkglist_1.txt
### create a backup of the config files that are listed in 'backup-list.txt'
echo radius-server-config.tgz
tar --create --gzip --preserve
--files-from="backup-list.txt" --file radius-server-config.tgz
### create a backup of the directories /var/www/html/, /usr/local/, etc.
echo radius-server-html.tgz
tar cz --preserve --file radius-server-html.tgz /var/www/html/
echo radius-server-local.tgz
41. tar cz --preserve --file radius-server-local.tgz /usr/local/
● backup/backup_server/backuplist.txt
The file /backup/backup_server/backup-list.txt is a list of the files and directories that
should be backuped:
/etc/radiusmanager.cfg
/var/www/html/radiusmanager/lic.txt
/var/www/html/radiusmanager/config/config.php
/etc/phpMyAdmin/config.inc.php
/etc/my.cnf
/etc/crontab
/etc/cron.daily/backup-db.cron
/etc/cron.daily/check-files.sh
/etc/cron.monthly/backup-server.sh
/etc/sysconfig/iptables
/etc/resolv.conf
/etc/aliases
/home/dhoxha/.procmailrc
/etc/rc.d/rc.local
/etc/php.ini
/etc/mail/sendmail.mc
/etc/mail/sendmail.cf
/etc/ntp.conf
/etc/httpd/conf/httpd.conf
/etc/httpd/conf.d/backup.conf
/etc/httpd/conf.d/phpMyAdmin.conf
/etc/httpd/conf.d/radiusmanager.conf
/backup/mikrotik/backup.sh
/backup/radius_db/backup-db.sh
/backup/radius_db/backup-db.cron
/backup/radius_server/backup-list.txt
/backup/radius_server/pkglist.txt
/backup/radius_server/pkglist_1.txt
/backup/radius_server/backup-server.sh
/usr/local/config/
/usr/local/etc/raddb/
/usr/local/bin/mtexec.exp
/usr/local/scripts/
8.4 Rebuild the Server From Backup
The backup radius_server.tgz can be used to build quickly another radius server that is almost
identical as the original one. It can be done like this:
1. Initially install Fedora7 from CD/DVD. Install a system that is as small as possible (a minimal
system, no need for GUI etc., just the base system).
2. Transfer the backup file on the new system, using scp , wget or anything else.
3. Make sure that in this system are installed almost the same packages as in the original server. It
can be done like this:
a. First update the packages that are already installed:
42. bash# yum update
b. Then extract the pkglist_1.txt from the backup archive:
bash# tar xfz radius-server.tgz backup/radius_server/pkglist_1.txt
c. Next, make sure that all packages in pkglist_1.txt are installed in the server:
bash# cat backup/radius_server/pkglist_1.txt | xargs yum install
d. If you want, you can also make sure that no extra packages (more than those on the
original server) are installed. It can be done by generating a list of the installed packages
(using yum list installed ), comparing it to the file pkglist_1.txt (using diff ),
finding out which are the extra packages and removing them:
bash# yum list installed | gawk '{print $1}' | sed 's/. [^.] *$//'
>package_list.txt
bash# diff -u package_list.txt pkglist_1.txt > pkg.diff
bash# yum remove ...
However, extra packages in general should not harm anything.
4. Now restore the backup file:
bash# cd /
bash# tar xvz --preserve --file radius_server.tgz
5. Don't forget to restore the database of radius manager.
6. Also make sure that the needed services are ON (like httpd, mysqld, etc.), and those that are not
needed are OFF. This can be done using the commands /sbin/chkconfig and /sbin/service .
9 Misc
9.1 Keeping the Time Correct
It is important that the time of the server be correct, since radius manager checks the expiration times of
the clients. In order to make sure that the time of the server is correct, we can use ntp. It can be installed
and configured like this:
1. Install the package ntp :
bash# yum install ntp
2. Modify /etc/ntp.conf by adding time servers: