Different rate limits based on login ?
-
Thank you both for responding. I already know I can make 3 portals/vlans. However, this is not very convenient on a customer level. I am right now looking at how to create a single portal that will accept vouchers and offer not only time length but also speed rates.
I have since learned Radius will be the way to go, it supports returning custom attributes back to pfSense and these can be used to regulate speed of a client based on their voucher type.https://docs.netgate.com/pfsense/en/latest/captiveportal/configuration.html#radius-options
-
@Swicago said in Different rate limits based on login ?:
I have since learned Radius will be the way to go
As long as you use "reauthenticate every minute" and do not enable multiuser per account. freeRadius can work.
I assume you are using the default freeRadius installation that enables a SQL Lite DB for time and a flat file system for data quotas (/var/log/radacct), both on the same server as pfSense. I suggest you setup a test using low data and time quotas to ensure you are getting what you want before you do the final configuration. Stick to a single "Tunnel", i.e. a single portal and enable Stop/Start and reauthenticate every minute. In my experience, it is best to rely on the Captive Portal idle timeout and max timeout values if possible. That would require that each account have the same timeout limits independent of access type. Make sure it is freeRadius that is disconnecting your user by making it's timeout smaller during your test. If you want to use vouchers from the portal in freeRadius, export the vouchers, enter the first one manually in freeRadius, then you can backup only package manager in Diagnostics, Backup & Restore. Edit the XML file by finding the entry for the voucher you entered and duplicate it inline in the XML file, then restore the package manager backup after editing it. I found Excel did a nice job of automating it for me. freeRadius will start balking (user manager slow down) at around 2000 users (vouchers). As freeRadius does not inheritantly support vouchers, we use the same voucher value for user name and password to simplify logins.
As you can see, you may find 3 Station IDs (Captive Portals) suddenly look quite good, quick and easy.
-
@EDaleH , great info. Thanks for the tips for testing. Good to know about the 2k limit. I don't I will have that problem at my site, but if it were to become a problem, maybe I'll look at the radius protocol and make my own listener that can respond correctly to pfsense.
I have done similar services in the past for flow control on openwrt type devices. -
So I was able to setup FreeRadius and limit a users bandwidth, however this solution won't work 100% either.
It seems I can only limit the number of users connected in the portal itself and not from FreeRadius.Ideally I would want a Free user that can have as many people connect as they want, and then faster tier user where only one device can be connected at a time.
Any idea if freeradius could be configured to force authentication failure, if the user is already logged in on one device? That would solve my issue and being able to use one SSID/Portal to rule them all.
Thanks and happy new year
-
@Swicago said in Different rate limits based on login ?:
Any idea if freeradius could be configured to force authentication failure, if the user is already logged in on one device?
It appears the answer is use a different Captive Portal with last login set instead of multiple connections per user. That implies a second Station ID. Then you will discover that the Tunnel Attribute in freeRadius is not supported by Captive Portal and that either user can log into either portal. So it gets messy. In that case have the FREE portal authenticate with local database to hopefully avoid this issue.
On a single Station ID, you could look at captiveportal.inc below "/* Authenticate users using Authentication Backend */" (search for that string). You could then use the daily/weekly/monthly/forever settings to distinguish one user from another. You could then test for the user type based on the "Time Period" freeRadius setting. Check to see if that user name has a file in that directory and that will tell you if it is a "faster tier" or not. Then check to see if that user is already logged in and if so I believe you can only reject the current login attempt. That would act like a "first login" setting in Captive Portal.
I know that sounds confusing and it is. Below is a bit of code that does something similar but doesn't check to see if the user is logged in. It might get you started but customizing captiveportal.inc is a slippery slope.
if (!$radmac_error) { if ($authcfg['type'] === 'none') { $result = true; } else { $result = authenticate_user($login, $password, $authcfg, $attributes); } if (!empty($attributes['error_message'])) { $msg = $attributes['error_message']; } if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) { $tmp_user_item_config = getUserEntry($login); if (!userHasPrivilege($tmp_user_item_config['item'], "user-services-captiveportal-login")) { $result = false; $msg = gettext("Access Denied"); } } if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) { $msg = gettext("RADIUS MAC Authentication Failed."); }
/* ----------------------------------------------------- New Code ------------------------------------------- /
/ Check to see if this user has a freeRadius data file /
$user_file = "/var/log/radacct/datacounter/monthly/max-octets-";
$user_file .= $login;
if (file_exists($user_file) && ($cpzone == "FasterTier")) {
} else {
// Check to see if that user is logged in already and if so
// No code written for this yet but put it here and if YES, then fail this login
// do nothing if the user is not logged in yet
$status = "FAILURE";
$result = false;
}
}
/ --------------------------------------------------------------------------------------------------------------- */if (empty($status)) { if ($result === true) { $status = "ACCEPT"; } elseif ($result === null) { $status = "ERROR"; } else { $status = "FAILURE"; } } if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) { $login = "unauthenticated"; }
Good luck. 2 Station ID's looking better yet?
-
Thanks for the reply. I am actually looking at the source code of radius.
I believe it may be easier to modify
/usr/local/share/pear/Auth/RADIUS.php
and add an extra attribute that can return weather or not a user can be logged in more than once "multiple" or if they get booted "last/first".Basically overriding the default setting in the portal page "Concurrent user logins". Then captiveportal.inc could be left alone.The other option is to add support to the voucher system to store bw_up, bw_dn and noconcurrentlogins attributes per voucher roll and have that override portals setting. However this option looks to be far more complex than modifying Radius to support an extra attribute.
-
I have added support for Non-Conccrent-Logins with Radius.
First file is the dictionary. I added new attribute "pfSense-Non-Concurrent-Logins"
/usr/local/share/freeradius/dictionary.pfsense
# -*- text -*- # Copyright (C) 2019 The FreeRADIUS Server project and contributors # # dictionary.pfsense # pfSense Captive Portal Dictionary # # https://github.com/pfsense/pfsense/blob/master/src/usr/share/doc/radius/dictionary.pfsense # VENDOR pfSense 13644 BEGIN-VENDOR pfSense ATTRIBUTE pfSense-Bandwidth-Max-Up 1 integer ATTRIBUTE pfSense-Bandwidth-Max-Down 2 integer ATTRIBUTE pfSense-Max-Total-Octets 3 integer ATTRIBUTE pfSense-Non-Concurrent-Logins 4 string END-VENDOR pfSense
Then I added support for the new attribute in
/usr/local/share/pear/Auth/RADIUS.php} elseif ($vendor == 13644) { /* Netgate */ switch ($attrv) { case 1: /* pfSense-Bandwidth-Max-Up */ $this->attributes['bw_up'] = radius_cvt_int($datav); break; case 2: /* pfSense-Bandwidth-Max-Down */ $this->attributes['bw_down'] = radius_cvt_int($datav); break; case 3: /* pfSense-Max-Total-Octets */ $this->attributes['maxbytes'] = radius_cvt_int($datav); break; case 4: /* pfSense-Non-Concurrent-Logins */ $this->attributes['noconcurrentlogins'] = radius_cvt_string($datav); } }
Last I modified
/etc/inc/captiveportal.inc/* read in client database */ $query = "WHERE ip = '{$clientip}'"; $tmpusername = SQLite3::escapeString(strtolower($username)); //if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null) { if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null /* RADIUS Support for NON CURRENT LOGINS */ || (isset($cpzone_config['radacct_server']) && $cpzone_config['radacct_server'] == 'Radius' && !empty($attributes['noconcurrentlogins'])) ) { $query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')"; } $cpdb = captiveportal_read_db($query); /* Snapshot the timestamp */ $allow_time = time(); if ($existing_sessionid !== null) { // If we received this connection through XMLRPC sync : // we fetch allow_time from the info given by the other node $allow_time = $attributes['allow_time']; } $unsetindexes = array(); $cpzone_config = config_get_path("captiveportal/{$cpzone}", []); /* RADIUS NON CURRENT LOGINS */ if (isset($cpzone_config['radacct_server']) && $cpzone_config['radacct_server'] == 'Radius') { $cpzone_config['noconcurrentlogins'] = !empty($attributes['noconcurrentlogins']) ? $attributes['noconcurrentlogins'] : $cpzone_config['noconcurrentlogins']; }
This allows me now to support a slow free user that can have multiple connections or faster tier users where connection is limited to what ever I set "pfSense-Non-Concurrent-Logins" as in Radius user config. In my case I set it to last. Default portal is set to multiple and Radius overrides it, just as it does bandwidth.
Next I will see how I can make the Radius user valid for a few days after 1st login. That will simulate vouchers that are only valid for a certain number of days. Maybe this is already supported, else I'll just mod captiveportal.inc and RADIUS.php again for support.
My current code changes do not affect portal in default, so it can still run as originally coded.
-
@Swicago said in Different rate limits based on login ?:
modifying Radius to support an extra attribute
I believe that someone at Netgate told me there was no more room to "add" a radius attribute or the variable would overflow (much like data quota does), it is easier to simply place the FREE user in the "daily" and the "FasterTier" user in say "monthly" in the freeRadius user database and check the datacounter filename for a match on the username. If you want to logout the existing user to make it "last login" equivalent, that would require a call to the disconnect routine for the original user and that is less straightforward as the communication between captive portal and freeRadius is asynchronous; tied to the reauthenticate/accounting interval. You could check to see if there is any value in the used-octets file to determine if someone has already logged in but then you would have to do something like create a file on disk with the new username in it and in the reauthenticate routine in captive portal, disconnect the off side user at the next reauthenticate interval. That would be fairly easy to do. The sample code below does something similar:
$d_max_file = "/var/log/radacct/datacounter/monthly/max-octets-" . $cpentry[4];
$d_max = array(); if (file_exists($d_max_file)) { $file_max_OK = "File exists"; $d_max_handle = fopen($d_max_file, "r"); while(!feof($d_max_handle)) { $d_max[] = fgets($d_max_handle); } fclose($d_max_handle); $cpdb_50 = captiveportal_read_db(); $nbr_logins_50 = 0; $user_50 = $cpentry[4]; foreach ($cpdb_50 as $cpentry_50) { if($cpentry_50[4] === $user_50) { $nbr_logins_50 = $nbr_logins_50 +1; $time_used = $time_used + (time() - $cpentry_50[0]); } } unset($cpdb_50);
Add a variable to keep track of the username and you can check the session start ($cpentry_50[0] I believe), choosing to disconnect the oldest session. This would occur on every reauthenticate interval so the two could be both connected for a couple of them.
Something like this (around line 751 in captiveportal.inc 24.11 Stbl) would disconnect them:
captiveportal_disconnect($cpentry, 17);
captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_result['reply_message']);
$unsetindexes[] = $cpentry[5]; -
-
@Swicago said in Different rate limits based on login ?:
Next I will see how I can make the Radius user valid for a few days after 1st login.
It looks like we were replying to each other at the same time and overlapped our replies.
Impressive solution, well done. As far as your timeout requirement, the solution may partially be in the code I sent with my prior reply. If you can get away with it, I believe Captive Portal will disconnect by itself at the hard timeout value anyway. I needed to share time amongst all connected devices to one user account and the solution below includes that consideration.
$cpdb_50 = captiveportal_read_db(); $nbr_logins_50 = 0; $user_50 = $cpentry[4]; foreach ($cpdb_50 as $cpentry_50) { if($cpentry_50[4] === $user_50) { $nbr_logins_50 = $nbr_logins_50 +1; $time_used = $time_used + (time() - $cpentry_50[0]); } } unset($cpdb_50);
The $time_used variable is tracking total connect time. The next piece of code maxes out a data quota and lets freeRadius disconnect (everyone logged into that user) on the next reauthenticate interval for each of them in turn.
if (($auth_result['result'] === false) || (intval($time_used) > $cpentry[7])) { if((intval($time_used) > $cpentry[7]) && ($cpzone == "vlan50")) { $d_max_file = "/var/log/radacct/datacounter/forever/max-octets-" . $cpentry[4]; $d_used_file = "/var/log/radacct/datacounter/forever/used-octets-" . $cpentry[4]; $d_log_file = "/var/log/radlog/forever-used-octets"; $d_max = array(); if (file_exists($d_max_file)) { $file_max_OK = "File exists"; $d_max_handle = fopen($d_max_file, "r"); while(!feof($d_max_handle)) { $d_max[] = fgets($d_max_handle); } fclose($d_max_handle); } $d_used = array(); if (file_exists($d_used_file)) { $d_used_handle = fopen($d_used_file, "r"); while(!feof($d_used_handle)) { $d_used[] = fgets($d_used_handle); } $dused = $d_used[0]; fclose($d_used_handle); $d_used_handle = fopen($d_used_file, "w"); fwrite($d_used_handle,strval($d_max[0] + 1)); fclose($d_used_handle); } if (file_exists($d_log_file)) { $d_log_handle = fopen($d_log_file, "a"); fwrite($d_log_handle,$d_log_file . "," . str_replace("\n","",strval($user_50)) . "," . str_replace("\n","",strval($nbr_logins_50)) . "," . str_replace("\n","",strval($dused)) . "," . str_replace("\n","",strval($d_max[0])) . "," . date("Y-m-d h:i:sa") . "\n"); fclose($d_log_handle); } unset($d_max); unset($d_used); } captiveportal_disconnect($cpentry, 17); captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_result['reply_message']); $unsetindexes[] = $cpentry[5];
Maybe that will stimulate one possibility to consider?
. -
@EDaleH ,
Radius actually already supports expiration date. And portal respects it.
I just wrapped up coding also Voucher based bandwidth limits and no concurrent login control. For vouchers, I hijacked the description field to add extra attributes. For Vouchers, bandwidth limits is global to the roll, like minutes.
If you think anyone would be interested, I can re-upload my full changes of both RADIUS and VOUCHER control with specific bandwidth and login controls. Changes for voucher was a little tougher. Stupid spelling mistakes LOL.
-
@Swicago said in Different rate limits based on login ?:
If you think anyone would be interested, I can re-upload my full changes of both RADIUS and VOUCHER control with specific bandwidth and login controls.
There is always someone interested. Although not immediately applicable to my current setups, I for one... am.
-
@EDaleH
Well all righty then.I will do full post of all code changes for both RADIUS No Conncurrent Login support and Voucher Badwidth & Non Concurrent Login support. Also, corrected some spelling mistakes for the non concurrent attribute.
Here are my diffs for each current file vs their backups
Radius specific
/usr/local/share/freeradius/dictionary.pfsense17d16 < ATTRIBUTE pfSense-No-Concurrent-Logins 4 string
Radius specific
/usr/local/share/pear/Auth/RADIUS.php709,711d708 < case 4: /* pfSense-No-Concurrent-Logins */ < $this->attributes['noconcurrentlogins'] = < radius_cvt_string($datav);
Voucher specific
/etc/inc/voucher.inc226d225 < $config_per_roll = array(); /* Roll configs */ 230,239d228 < /* Voucher roll config override support */ < if(preg_match('/\bnoconcurrentlogins\s?=\s?(multiple|first|last)\b/',$rollent['descr'],$matches)) { < $config_per_roll[$rollent['number']]['noconcurrentlogins'] = $matches[1]; < } < if(preg_match('/\bbw_down\s?=\s?(\d+)\b/',$rollent['descr'],$matches)) { < $config_per_roll[$rollent['number']]['bw_down'] = intval($matches[1]); < } < if(preg_match('/\bbw_up\s?=\s?(\d+)\b/',$rollent['descr'],$matches)) { < $config_per_roll[$rollent['number']]['bw_up'] = intval($matches[1]); < } 249d237 < $config_voucher = array(); /* Voucher Roll ovverride config */ 306,307d293 < /* Store any voucher override configs found */ < $config_voucher = isset($config_per_roll[$roll]) ? $config_per_roll[$roll] : array(); 399,401c385,386 < /* Return minutes left and voucher roll override configs */ < /* Note captiveportal/index.php needs to be modified to check if array returned */ < return array($total_minutes,$config_voucher); --- > > return $total_minutes;
Voucher specific
/usr/local/captiveportal/index.php< $auth_return = voucher_auth($voucher); < //$timecredit = voucher_auth($voucher); < /* Support array or single var return */ < $timecredit = is_array($auth_return) ? $auth_return[0] : $auth_return; < /* Support voucher configuration override */ < $voucher_configuration = is_array($auth_return) ? $auth_return[1] : array(); --- > $timecredit = voucher_auth($voucher); 221,224d215 < /* Copy all voucher configs into attributes */ < foreach($voucher_configuration as $config_key => $config_value) { < $attr[$config_key] = $config_value; < }
Both Radius and Voucher changes
/etc/inc/captiveportal.inc1952,1958c1952 < //if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null) { < if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null < /* RADIUS Support for NO CONCURRENT LOGINS */ < || (isset($cpzone_config['radacct_server']) && $cpzone_config['radacct_server'] == 'Radius' && !empty($attributes['noconcurrentlogins']) < /* VOUCHER Support for NO CONCURRENT LOGINS */ < || ($attributes['voucher'] && !empty($attributes['noconcurrentlogins'])) ) < ) { --- > if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null) { 1974,1983d1967 < < /* RADIUS NO CONCURRENT LOGINS */ < if (isset($cpzone_config['radacct_server']) && $cpzone_config['radacct_server'] == 'Radius' && !empty($attributes['noconcurrentlogins'])) { < $cpzone_config['noconcurrentlogins'] = $attributes['noconcurrentlogins']; < } < /* VOUCHER NO CONCURRENT LOGINS */ < if ($attributes['voucher'] && !empty($attributes['noconcurrentlogins'])) { < $cpzone_config['noconcurrentlogins'] = $attributes['noconcurrentlogins']; < } < 2058a2043 > 2084,2088c2069 < /* VOUCHER Bandwidth control */ < if ($attributes['voucher'] && (!empty($attributes['bw_up']) || !empty($attributes['bw_down']))) { < $bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up']) : $dwfaultbw_up, 0); < $bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down']) : $dwfaultbw_down, 0); < } elseif (isset($cpzone_config['radiusperuserbw'])) { --- > if (isset($cpzone_config['radiusperuserbw'])) {
I'd upload my files, but I didn't see where that can be done.
If using radius, it already supports bandwidth per user, but not device restriction. This adds support for device restrictions. Use extra option "pfSense-No-Concurrent-Logins"
To add bandwidth and device control via Vouchers
The following can be added to the description field of a voucher roll
noconcurrentlogins=last,bw_down=5000,bw_up=500
The above tells pfsense that only the last device can use the voucher and that bandwidth is 5000kbs down and 500kbs up.
You do not need to specify all options and portal defaults will apply if not supplied.Cheers
-
@Swicago said in Different rate limits based on login ?:
I'd upload my files, but I didn't see where that can be done.
I am going to assume you are running the current pfSense Plus 24.11 Stable. The diff files are compact and convenient as long as they are applied to the correct version which will be obsolete in only a few months but the post will survive. Posting the entire file(s) could result in versioning issues if someone uses them with a different version so I think the diff files are perfect. I prefer the old followed by new representation so you can search for the old code in the newly updated files. This is an ongoing challenge with customized code so you will have to re-run a diff on the next version's updated files against the prior version of the same file, and then re-integrate your changes, then produce a new diff, and so on. It is the safest way to "keep up".
@Swicago said in Different rate limits based on login ?:
If using radius, it already supports bandwidth per user,
I found that when there are multiple logins per user that the time accounting is not reliable on either Captive Portal or freeRadius. Captive Portal applies time limits to each session and starts over if there is a disconnect/reconnect or "last login" for authenticated users. Vouchers work for last login though. The next problem is that with multi logins per user, do you want to track each user against the data/time limits or do you want to cumulate users time/data against a single limit for data/time? How do you handle idle time and do you start over if they log out and then log in again? Lastly, if you have multiple portals authenticated by freeRadius, the users can use their freeRadius credentials to log into the other portal (free Radius Tunnel Attribute not supported by Captive Portal). These items do not affect your specific installation but they are hiding in the background.
You could consider raising a Redmine to add your feature to the project. That is where the diff files will assist in the implementation and you can upload your files to a Redmine safely, make sure you include the pfSense version info as well.
-
@EDaleH ,
Good to know about the bug report page. I'll have to see if I have a login there and report/ask for the feature.
I am using 24.11-RELEASE and have backups of my files. I suspected as well that IO may have to re-apply my patches on later versions.
In my case, time constraints is not really a concern. My multi login tier is my slow tier and only uses a single login and never expires.
If a client wants to use faster tier, then taht is voucher based and only the last logged in device can use it. Also these vouchers are good for 7 days.Background: My wife and I bought a campground last year. It uses a 3rd party service to run a portal and the wifi towers. It is costly and the system is dated/sucks. I am in the process of replacing all towers. We don't charge clients for internet, but still need to regulate usage, so that we have enough bandwidth to go around. This is where my so called free tier comes in. Anyone can use the Free slow Tier. 2mbs down. And each campsite that stays with us gets one voucher for one device at 6mbs, free of charge. This will let them watch netflix and such. I never expect too many devices at once to overload the bandwidth, but I will use shaping to always leave a bit for office vlan.
Anyhow, thanks again for responding to my initial post with suggestions. Was hoping I wouldn't have to do custom code, but I guess that was not a choice.
-
@Swicago said in Different rate limits based on login ?:
My wife and I bought a campground last year.
Interesting twist, I am retired and spent much of the last 2 decades running around in a Motor Home (10 yrs full time). We ended up helping sites setup their WiFi, often with pfSense. Our summer home stop was at an RV park (~120 sites) that I still take care of after about 15 years (all on pfSense) as a hobby. About 3 years ago, they brought in Fiber at great expense and we buried fiber in the Park out to all of 20 or so APs. The park's throughput last summer went from a monthly low of 7 TB to a high of 20 TB. Latency is 2 ms from the ISP and 4-6 ms to the end user. The impact on the park has been substantial, fully booked a couple of years in advance and grandkids visit because the gaming is better than at home. Most of the satellite dishes are gone because everyone is streaming instead. There are a lot of clients that come so they can work from their MH/Cottage all summer. The ISP feed is an expensive business account (conditional for bringing in the fiber about 10 Km) and revenue is now covering the entire cost of the internet itself. The wifi infrastructure, much like power/water/sewer upgrades is a capital expense that will have to be written off over time though. It is currently running on an XG-1541 max. I modified the captive portal code about 3 years ago and have ported it through each version up until the current version. Last year it had 0 down time outside of the seasonal APs loosing power during a couple of power outages. Transient stayed up because they are on a UPS. There are no office sales of wifi, no support requests and it is all automated or rented for the season. Single login at the beginning of the season/visit and automated cleanup when time/data expire. Have you implemented RFC8910 (DHCP 114) for the smartphones? Be careful with Kea as it likes to change the IPs frequently and is very different from ISA DHCP.