PPPoE With Mikrotik and Radius

10,949 views

Published on

How to manage internet clients of an ISP with PPPoE and MikroTik. For
centralized AAA (Authentication, Authorization and Accounting), freeRadius is used.

3 Comments
9 Likes
Statistics
Notes
No Downloads
Views
Total views
10,949
On SlideShare
0
From Embeds
0
Number of Embeds
13
Actions
Shares
0
Downloads
712
Comments
3
Likes
9
Embeds 0
No embeds

No notes for slide

PPPoE With Mikrotik and Radius

  1. 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 modifythis document under the terms of the GNU Free Documentation License, Version 1.1 or any later versionpublished by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, andwith no Back-Cover Texts. A copy of the license is included in the section entitled "GNU FreeDocumentation License."1 AbstractThis 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 IntroductionThe 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 PossibilitiesDigging 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. 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 load­balancing 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. ● IP­s 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 IP­proxy (NAT) or  using ARP­proxy. 2.2 The Architecture of The PPPoE ServiceBased 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 each­other (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 load­balancing. ● 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 each­other. 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, 3­4 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 load­balancing between the  MikroTik servers. In general, each of the MikroTik servers can serve up to 400­500 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 load­balancing for the  radius servers.
  3. 3. ● The clients are connected to the Internet through IP­proxy (NAT). ● The IP that is assigned to the clients are random, taken from an IP pool (they are not fixed IP­s).3 Installing And Configuring freeRadius, MySQL and MikroTik 3.1 Installing freeRadiusWe installed freeRadius on Fedora7. First I installed the packages freeradius and freeradius­mysql :  bash# yum install freeradius freeradius-mysqlThen 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 startSince 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 ACCEPTTo 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 InstallationJust 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. 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 dont 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 local1XIn the first and second tests you should get the answer Access­Accept. In the last two tests you should get the answer Access­Reject. 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 DatabaseNow 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 lets 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. 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 MySQLEnter some data into the database:  bash$ mysql -u raduser -p radpassw mysql> USE radiusdb; mysql> SHOW TABLES;
  6. 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 src­nat/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. 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.112Now the PPPoE server is ready to answer PPPoE requests and to authenticate PPPoE clients. Important: We dont 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 ServiceTo 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. 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 RADIUSRight 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). Lets 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 each­other.   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. 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. 10. 4 Using Two Radius Servers 4.1 Adding a Second Radius Server in MikroTikMikroTik 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 mikro1In 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 each­other (two­way, 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 each­other), 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 high­availability and load balancing.  4.2 Replicating MySQL Databases of RadiusIn order to ensure service backup, high availability and load balancing, we replicate the databases of the radius servers. We do it a two­way 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 dont need to do any manual configuration. The two­way 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 skip­networking 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. 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.sql4. 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.sql5. 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 ­­master­data). 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. 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 DatabasesIt can happen that the replicated databases become inconsistent (especially since we have used the option slave­skip­errors=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 installChecking 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.121The server that is given in the command should be the master, however, since we use a two­way replication, it doesnt 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) );
  13. 13. If it turns out that there are differences between the databases, an option for fixing them is the tool mysql­table­sync . 5 SQL API for Radius ManagerIn order to manage the radius, we used Radius Manager (http://www.dmasoftlab.com/), which is an application for managing the database of freeRadius, services, clients, etc. It even had some simple billing functionality. Unfortunately, Radius Manager was not free software (open source), although it was cheap and the support was quite responsive. We needed to integrate Radius Manager with our own client application (built with SugarCRM), so we tried hard to convince the RM guys to sell us the source code as well. However they would not listen at all about it. Anyway, we managed to do some kind of integration by modifying the database of Radius Manager from our client application. In order to do this, we built an external SQL API for Radius Manager.Since Radius Manager is not free software, we are not going to describe here our experience about its installation and configuration. It does not need it and it does not deserve it. Instead, we are going to describe just the SQL API that we built to work around the integration problems. 5.1 Advantages of Using ItThis SQL API helps to access the database of the Radius Manager from an outside program (from the program that manages the services and users). it is a library of MySQL procedures, which can be used to access and modify only those parts of the database that need to be touched. It has these advantages:  ● It encapsulates (hides) the complexity of the database from the outside programmer. The  programmer doesnt have to know what tables or fields are there in the database, but just needs to  know the procedures/functions that are available in the API, their parameters, return values, etc.  ● It makes simpler the code of the outside program, because instead of using complicated SQL  queries, he just needs to call a procedure with the appropriate parameters.  ● The programmer is relived from the fear and the responsibility of touching something  inappropriately in the database. The responsibility of doing things appropriately is taken over by  the API.  ● In case that the structure of the radius manager is modified in the future releases, it is the  responsibility of the API to take them into account, and the code of the external program doesnt  need to change at all.  5.2 Procedure Descriptions and Usage Examples● user_get procedure user_get(user varchar(32))
  14. 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. 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, enabledFields 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. 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. 17. 5.3 ImplementationThe 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.sqlor like this:  bash$ mysql -p -u root < rm_api.sqlThe 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. 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. 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. 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 RightsAccess 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. 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 SugarCRMSugarCRM 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. 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. 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. 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. 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("Cant 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. 26. //return the result return $result; } } ?>6 Almost Automated Configuration of MikroTikAfter 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 generate­mt­setup.shThe 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 mikrotik­setup.shThe 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. 27. ### include the configuration file. $cfgfile### generate and output the MikroTik commandscat <<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. 28. ### logout / quit EOF 6.3 cfg/MikroTik1.cfgThe 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.mtThese 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. 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 / quit7 Optimize and Test the PerformanceWe 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 RAMThe 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. 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 youre "really" using. See also these discussions: http://linux.derkeiler.com/Mailing­Lists/Fedora/2005­02/3149.html  and http://linux.derkeiler.com/Newsgroups/linux.redhat.misc/2004­01/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 yum­updatesd like this:  service ConsoleKit stop service ConsoleKit status chkconfig ConsoleKit off chkconfig --list ConsoleKit service yum-updatesd stop service yum-updatesd status
  31. 31. chkconfig yum-updatesd off chkconfig --list yum-updatesd 7.2 Optimize MySQLIn 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: Non­technical 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_queriesIn 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. 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. 33. 7.3 Optimize FreeRADIUSThe 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. 34. # auth_log}accounting { # # Create a detailed 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 = 5cleanup_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. 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 arent sure what it should be set to, its 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 = 1024max_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
  36. 36. 7.4 Testing the PerformanceWhen a client tries to connect to internet, MikroTik makes an authentication/authorization request to the radius server. Then, after the client is connected, MikroTik sends periodically an accounting update to the radius server, telling how much download/upload traffic the client has done during this interval. We have set this interval to one minute in MikroTik. So, each minute, the radius server gets an accounting request for each connected client, and it writes the data to the MySQL database.To simulate a client, we use the script radius­client.sh . It keeps running and sending update requests to the radius server until the stop time, then it terminates. The interval between the update requests is 5sec, so, the load on the server that is generated by this client will be approximately equal to the load of 10 real clients. Then we use the scrip test­radius.sh which runs many of such clients in parallel, in order to increase the load. The number of the clients that is generated is configurable. The script stop­test.sh can be used to interrupt the simulation at any time. From the tests that we did, we found out that in general the bottleneck of the system is the MySQL database. One way to increase its performance is to build a MySQL cluster, with two or more MySQL servers. However this requires additional resources (additional servers with big amounts of RAM). At that time we could still handle out clients without a MySQL cluster, so we didnt try to build it.● radius­client.sh #!/bin/bash ### Simulate a radius client. ### It will keep running and sending requests to the radius server ### until the stop time, then it will terminate. ### go to this directory cd $(dirname $0) ### get the parameters if [ "$2" = "" ] then echo "Usage: $0 client_id stop_time" exit fi id=$1 stop_time=$2 ### convert the stop time into seconds stop_time_sec=$(date --date="$stop_time" +%s) ### variables radius_server="192.168.25.101" secret="test" user="testing-radius" passw="test" interval=5 #radclient="/usr/local/bin/radclient -x" radclient="/usr/local/bin/radclient" ### simple packages for authentication and accounting auth_pkg="User-Name=$user,User-Password=$passw" acct_pkg_start="User-Name=$user,Acct-Session-Id=$id,Acct-Status-Type=Start" acct_pkg_stop="User-Name=$user,Acct-Session-Id=$id,Acct-Status-Type=Stop"
  37. 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● test­radius.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. 38. ./radius-client.sh $id $stop_time & sleep 1 done● stop­test.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 done8 Making BackupsBackups 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 MikroTikBinary 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. 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.backupThe 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 DatabaseA 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.shThe 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. 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 -fTo restore a backup file, first unzip it, and then use the command:  bash$ mysql -p -u root < radiusdb.2007-10-27.sql 8.3 ServerA 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 backup­ed 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.shThe 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. 41. tar cz --preserve --file radius-server-local.tgz /usr/local/● backup/backup_server/backup­list.txtThe file /backup/backup_server/backup-list.txt is a list of the files and directories that should be backup­ed:  /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 BackupThe 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: 

×