From 5f4abf79277e7d3409139a7544bdb1ee9328bacc Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Sun, 1 Oct 2017 06:27:09 +0000 Subject: [PATCH 01/33] Added index keys to accounting_summary table to speedup lookups --- database/core.tsql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/database/core.tsql b/database/core.tsql index 42713de..b6478aa 100644 --- a/database/core.tsql +++ b/database/core.tsql @@ -269,6 +269,9 @@ CREATE TABLE @PREFIX@accounting_summary ( TotalOutput @INT_UNSIGNED@ ) @CREATE_TABLE_SUFFIX@; +CREATE INDEX @PREFIX@accounting_summary_idx1 ON @PREFIX@accounting_summary (Username); +CREATE INDEX @PREFIX@accounting_summary_idx2 ON @PREFIX@accounting_summary (PeriodKey); +CREATE INDEX @PREFIX@accounting_summary_idx3 ON @PREFIX@accounting_summary (Username,PeriodKey); /* Users data */ -- GitLab From 5e853fcc75767fc94e3d29876e3cfbd2074996c5 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 06:40:03 +0000 Subject: [PATCH 02/33] Renamed POD to COA and added backwards compatibility --- lib/smradius/daemon.pm | 56 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index e3486fd..a3118d6 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -931,64 +931,76 @@ sub process_request { # Grab packet my $response = auth_resp($resp->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")); - # Check for POD Servers and send disconnect + my $coaServer; + + # Check for old POD server attribute if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'})) { $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is defined"); + $coaServer = $user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}; + } + + # Check for new CoA server attribute + if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'})) { + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is defined"); + $coaServer = $user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'}; + } + # Check for CoA servers + if (defined($coaServer)) { # Check address format - foreach my $podServerAttribute (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}}) { + foreach my $coaServerAttribute (@{$coaServer}) { # Check for valid IP - if ($podServerAttribute =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/) { - my $podServer = $1; + if ($coaServerAttribute =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/) { + my $coaServer = $1; # If we have a port, use it, otherwise use default 1700 - my $podServerPort; - if ($podServerAttribute =~ /:([0-9]+)$/) { - $podServerPort = $1; + my $coaServerPort; + if ($coaServerAttribute =~ /:([0-9]+)$/) { + $coaServerPort = $1; } else { - $podServerPort = 1700; + $coaServerPort = 1700; } - $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying PODServer => IP: '".$podServer."' Port: '".$podServerPort."'"); + $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying CoAServer => IP: '".$coaServer."' Port: '".$coaServerPort."'"); # Create socket to send packet out on - my $podServerTimeout = "10"; # 10 second timeout - my $podSock = IO::Socket::INET->new( - PeerAddr => $podServer, - PeerPort => $podServerPort, + my $coaServerTimeout = "10"; # 10 second timeout + my $coaSock = IO::Socket::INET->new( + PeerAddr => $coaServer, + PeerPort => $coaServerPort, Type => SOCK_DGRAM, Proto => 'udp', - TimeOut => $podServerTimeout, + TimeOut => $coaServerTimeout, ); - if (!$podSock) { + if (!$coaSock) { $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to create socket to send POD on"); next; } # Check if we sent the packet... - if (!$podSock->send($response)) { + if (!$coaSock->send($response)) { $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to send data on socket"); next; } # Once sent, we need to get a response back - my $sh = IO::Select->new($podSock); + my $sh = IO::Select->new($coaSock); if (!$sh) { $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to select data on socket"); next; } - if (!$sh->can_read($podServerTimeout)) { + if (!$sh->can_read($coaServerTimeout)) { $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to receive data on socket"); next; } my $data; - $podSock->recv($data, 65536); + $coaSock->recv($data, 65536); if (!$data) { $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Receive data failed"); - $logReason = "POD Failure"; + $logReason = "CoA Failure"; } else { $logReason = "User POD"; } @@ -996,11 +1008,11 @@ sub process_request { #my @stuff = unpack('C C n a16 a*', $data); #$self->log(LOG_DEBUG,"STUFF: ".Dumper(\@stuff)); } else { - $self->log(LOG_DEBUG,"[SMRADIUS] Invalid POD Server value: '".$podServerAttribute."'"); + $self->log(LOG_DEBUG,"[SMRADIUS] Invalid CoA Server value: '".$coaServerAttribute."'"); } } } else { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is not defined"); + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is not defined"); } } -- GitLab From 157ce22333cdf4698f28cf494092fc561b63b76f Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 06:40:23 +0000 Subject: [PATCH 03/33] Removed space where it should not be --- FEATURES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FEATURES b/FEATURES index 126acc7..8c7a2ef 100644 --- a/FEATURES +++ b/FEATURES @@ -27,7 +27,7 @@ Enhanced features: * Plugin: Topups * Plugin: Auto-topups * Plugin: Usage/Time caps -* Plugin: Prepaid accounting based on usage/time +* Plugin: Prepaid accounting based on usage/time * Plugin: Creation of accounting START records when no START record has been received but an interim update has - helps on slow/lossly links * Plugin: Notifications, % based or approximate time based * Plugin: User blacklists -- GitLab From cea79eef61b0aa4495c00224beb46fcb722f6291 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 06:40:36 +0000 Subject: [PATCH 04/33] Updated copyright --- lib/smradius/daemon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index a3118d6..73b0b79 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -1,5 +1,5 @@ # Radius daemon -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -- GitLab From 64a0f66c37e4fce42bf9b6fd3c6f2b673b66d434 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 10:26:23 +0000 Subject: [PATCH 05/33] Fixed spelling --- lib/smradius/attributes.pm | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index f72fb50..717a93f 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -342,7 +342,7 @@ sub checkAuthAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($operator eq '+=') { @@ -439,7 +439,7 @@ sub checkAcctAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. if ($operator eq '+=') { @@ -546,7 +546,7 @@ sub setReplyAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { # Overwrite @@ -561,7 +561,7 @@ sub setReplyAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($attribute->{'Operator'} eq '+=') { @@ -640,7 +640,7 @@ sub setReplyVAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { # Overwrite @@ -655,7 +655,7 @@ sub setReplyVAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($attribute->{'Operator'} eq '+=') { @@ -707,7 +707,7 @@ sub processConfigAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. if ($attribute->{'Operator'} eq '+=') { @@ -720,7 +720,7 @@ sub processConfigAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { @{$configAttributes->{$attribute->{'Name'}}} = @attrValues; -- GitLab From 411585c39f68b8dd5c865ffb8beaf68c2da81bf4 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 12:48:27 +0000 Subject: [PATCH 06/33] Changed wording from Client to User --- .../modules/features/mod_feature_capping.pm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/smradius/modules/features/mod_feature_capping.pm b/lib/smradius/modules/features/mod_feature_capping.pm index ee25a55..b1d1a28 100644 --- a/lib/smradius/modules/features/mod_feature_capping.pm +++ b/lib/smradius/modules/features/mod_feature_capping.pm @@ -1,5 +1,5 @@ # Capping support -# Copyright (C) 2007-2017, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -219,7 +219,7 @@ sub post_auth_hook # - # Allow for capping overrides by client attribute + # Allow for capping overrides by attribute # if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) { @@ -231,7 +231,7 @@ sub post_auth_hook $uptimeLimitWithTopups = $newLimit; $accountingUsage->{'TotalSessionTime'} = $newSessionTime; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client uptime multiplier '$multiplier' changes ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User uptime multiplier '$multiplier' changes ". "uptime limit ('$uptimeLimitWithTopups' => '$newLimit'), ". "uptime usage ('".$accountingUsage->{'TotalSessionTime'}."' => '$newSessionTime')" ); @@ -245,7 +245,7 @@ sub post_auth_hook $trafficLimitWithTopups = $newLimit; $accountingUsage->{'TotalDataUsage'} = $newDataUsage; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client traffic multiplier '$multiplier' changes ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User traffic multiplier '$multiplier' changes ". "traffic limit ('$trafficLimitWithTopups' => '$newLimit'), ". "traffic usage ('".$accountingUsage->{'TotalDataUsage'}."' => '$newDataUsage')" ); @@ -346,7 +346,7 @@ sub post_acct_hook # Skip MAC authentication return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); - # Exceeding maximum, must be disconnected + # User is either connecting 'START' or disconnecting 'STOP' return MOD_RES_SKIP if ($packet->rawattr('Acct-Status-Type') ne "1" && $packet->rawattr('Acct-Status-Type') ne "3"); $server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] POST ACCT HOOK"); @@ -440,20 +440,20 @@ sub post_acct_hook # - # Allow for capping overrides by client attribute + # Allow for capping overrides by user attribute # if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) { my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'}}); my $newLimit = $uptimeLimitWithTopups * $multiplier; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap uptime multiplier '$multiplier' changes limit ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User cap uptime multiplier '$multiplier' changes limit ". "from '$uptimeLimitWithTopups' to '$newLimit'"); $uptimeLimitWithTopups = $newLimit; } if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'})) { my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'}}); my $newLimit = $trafficLimitWithTopups * $multiplier; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap traffic multiplier '$multiplier' changes limit ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User cap traffic multiplier '$multiplier' changes limit ". "from '$trafficLimitWithTopups' to '$newLimit'"); $trafficLimitWithTopups = $newLimit; } @@ -491,7 +491,7 @@ sub post_acct_hook ## @internal -# Code snippet to grab the current uptime limit by processing the user attributes +# Code snippet to grab the current attribute key limit by processing the user attributes sub _getAttributeKeyLimit { my ($server,$user,$attributeKey) = @_; -- GitLab From 5899c683c80417c24807868d57c1e6aa5e9b6141 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 12:48:59 +0000 Subject: [PATCH 07/33] Added additional indexing for the accounting table --- database/core.tsql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/database/core.tsql b/database/core.tsql index b6478aa..68b74ab 100644 --- a/database/core.tsql +++ b/database/core.tsql @@ -251,6 +251,9 @@ CREATE INDEX @PREFIX@accounting_idx2 ON @PREFIX@accounting (PeriodKey); CREATE INDEX @PREFIX@accounting_idx4 ON @PREFIX@accounting (Username,AcctSessionID,NASIPAddress,NASPort); /* accounting_update_query */ CREATE INDEX @PREFIX@accounting_idx5 ON @PREFIX@accounting (Username,AcctSessionID,NASIPAddress,NASPort,PeriodKey); +/* Index for the EventTimestamp */ +CREATE INDEX @PREFIX@accounting_idx7 ON @PREFIX@accounting (EventTimestamp); +CREATE INDEX @PREFIX@accounting_idx8 ON @PREFIX@accounting (Username,EventTimestamp); -- GitLab From 4e729270f0e4984dfcabaa0a3b44534d64afdd10 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 12:49:28 +0000 Subject: [PATCH 08/33] Added the new SMRadius attributes to the ignore list for replies --- lib/smradius/attributes.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index 717a93f..b17ad29 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -81,6 +81,8 @@ my @attributeReplyIgnoreList = ( 'SMRadius-AutoTopup-Uptime-Notify', 'SMRadius-AutoTopup-Uptime-NotifyTemplate', 'SMRadius-AutoTopup-Uptime-Threshold', + 'SMRadius-FUP-Period', + 'SMRadius-FUP-Traffic-Threshold', ); my @attributeVReplyIgnoreList = ( ); -- GitLab From 01dd85b5eb7adb4a4a660f21b078560f28ac0b6a Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 12:50:35 +0000 Subject: [PATCH 09/33] Move the processing of reply attributes into its own internal function --- lib/smradius/daemon.pm | 250 ++++++++++++++++++++--------------------- 1 file changed, 123 insertions(+), 127 deletions(-) diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index 73b0b79..cd95ae2 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -1166,133 +1166,8 @@ sub process_request { $resp->set_identifier($pkt->identifier); $resp->set_authenticator($pkt->authenticator); - # Loop with attributes we got from the getReplyAttributes function, its a hash of arrays which are the values - my %replyAttributes = %{ $user->{'ReplyAttributes'} }; - foreach my $attrName (keys %{$user->{'Attributes'}}) { - # Loop with operators - foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { - # Grab attribute - my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; - # Add this to the reply attribute? - setReplyAttribute($self,\%replyAttributes,$attr); - } - } - # Loop with reply attributes - $request->addLogLine(". RFILTER => "); - foreach my $attrName (keys %replyAttributes) { - # Loop with values - foreach my $value (@{$replyAttributes{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-Attribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # Add each value - $resp->set_attr($attrName,$value); - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - # Loop with vendor reply attributes - $request->addLogLine(". RVFILTER => "); - my %replyVAttributes = (); - # Process reply vattributes already added - foreach my $vendor (keys %{ $user->{'ReplyVAttributes'} }) { - # Loop with operators - foreach my $attrName (keys %{$user->{'ReplyVAttributes'}->{$vendor}}) { - # Add each value - foreach my $value (@{$user->{'ReplyVAttributes'}{$vendor}->{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # This attribute is not excluded, so its ok - $replyVAttributes{$vendor}->{$attrName} = $user->{'ReplyVAttributes'}->{$vendor}->{$attrName}; - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - } - # Process VAttributes - foreach my $attrName (keys %{$user->{'VAttributes'}}) { - # Loop with operators - foreach my $attrOp (keys %{$user->{'VAttributes'}->{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # Grab attribute - my $attr = $user->{'VAttributes'}->{$attrName}->{$attrOp}; - # Add this to the reply attribute? - setReplyVAttribute($self,\%replyVAttributes,$attr); - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - foreach my $vendor (keys %replyVAttributes) { - # Loop with operators - foreach my $attrName (keys %{$replyVAttributes{$vendor}}) { - # Add each value - foreach my $value (@{$replyVAttributes{$vendor}->{$attrName}}) { - $resp->set_vsattr($vendor,$attrName,$value); - } - } - } - - # Add attributes onto logline - $request->addLogLine(". REPLY => "); - foreach my $attrName ($resp->attributes) { - $request->addLogLine( - "%s: '%s", - $attrName, - $resp->rawattr($attrName) - ); - } - - # Add vattributes onto logline - $request->addLogLine(". VREPLY => "); - # Loop with vendors - foreach my $vendor ($resp->vendors()) { - # Loop with attributes - foreach my $attrName ($resp->vsattributes($vendor)) { - # Grab the value - my @attrRawVal = ( $resp->vsattr($vendor,$attrName) ); - my $attrVal = $attrRawVal[0][0]; - # Sanatize it a bit - if ($attrVal =~ /[[:cntrl:]]/) { - $attrVal = "-nonprint-"; - } else { - $attrVal = "'$attrVal'"; - } - - $request->addLogLine( - "%s/%s: %s", - $vendor, - $attrName, - $attrVal - ); - } - } + # Process the reply attributes + $self->_processReplyAttributes($request,$user,$resp); $server->{'client'}->send( auth_resp($resp->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")) @@ -1424,6 +1299,127 @@ EOF +# +# Internal functions +# + + +# Process reply attributes +sub _processReplyAttributes +{ + my ($self,$request,$user,$pkt) = @_; + + # Add attributes we got from plugins and process attributes attached to the user + my %replyAttributes = %{ $user->{'ReplyAttributes'} }; + foreach my $attrName (keys %{$user->{'Attributes'}}) { + # Loop with operators + foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { + # Grab attribute + my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; + # Add this to the reply attribute? + setReplyAttribute($self,\%replyAttributes,$attr); + } + } + # Add vendor attributes we got from plugins and process attributes attached to the user + my %replyVAttributes = %{ $user->{'ReplyVAttributes'} }; + foreach my $attrName (keys %{$user->{'VAttributes'}}) { + # Loop with operators + foreach my $attrOp (keys %{$user->{'VAttributes'}->{$attrName}}) { + # Grab attribute + my $attr = $user->{'VAttributes'}->{$attrName}->{$attrOp}; + # Add this to the reply attribute? + setReplyVAttribute($self,\%replyVAttributes,$attr); + } + } + + # Loop with reply attributes add them to our response, or output them to log if they were excluded + $request->addLogLine("RFILTER => "); + foreach my $attrName (keys %replyAttributes) { + # Loop with values + foreach my $value (@{$replyAttributes{$attrName}}) { + # Check for filter matches + my $excluded = 0; + foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-Attribute'}}) { + my @attrList = split(/[;,]/,$item); + foreach my $aItem (@attrList) { + $excluded = 1 if (lc($attrName) eq lc($aItem)); + } + } + # If we must be filtered, just exclude it then + if (!$excluded) { + # Add each value + $pkt->set_attr($attrName,$value); + } else { + $request->addLogLine("%s ",$attrName); + } + } + } + + # Loop with reply vendor attributes add them to our response, or output them to log if they were excluded + $request->addLogLine(". RVFILTER => "); + # Process reply vattributes already added + foreach my $vendor (keys %replyVAttributes) { + # Loop with operators + foreach my $attrName (keys %{$replyVAttributes{$vendor}}) { + # Add each value + foreach my $value (@{$replyVAttributes{$vendor}->{$attrName}}) { + # Check for filter matches + my $excluded = 0; + foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { + my @attrList = split(/[;,]/,$item); + foreach my $aItem (@attrList) { + $excluded = 1 if (lc($attrName) eq lc($aItem)); + } + } + # If we must be filtered, just exclude it then + if (!$excluded) { + # This attribute is not excluded, so its ok + $pkt->set_vsattr($vendor,$attrName,$value); + } else { + $request->addLogLine("%s ",$attrName); + } + } + } + } + + # Add attributes onto logline + $request->addLogLine(". REPLY => "); + foreach my $attrName ($pkt->attributes) { + $request->addLogLine( + "%s: '%s", + $attrName, + $pkt->rawattr($attrName) + ); + } + # Add vattributes onto logline + $request->addLogLine(". VREPLY => "); + # Loop with vendors + foreach my $vendor ($pkt->vendors()) { + # Loop with attributes + foreach my $attrName ($pkt->vsattributes($vendor)) { + # Grab the value + my @attrRawVal = ( $pkt->vsattr($vendor,$attrName) ); + my $attrVal = $attrRawVal[0][0]; + # Sanatize it a bit + if ($attrVal =~ /[[:cntrl:]]/) { + $attrVal = "-nonprint-"; + } else { + $attrVal = "'$attrVal'"; + } + $request->addLogLine( + "%s/%s: %s", + $vendor, + $attrName, + $attrVal + ); + } + } + + return $self; +}; + + + 1; # vim: ts=4 -- GitLab From 62f57daeafaf84ec1df57b209c86346793d609a8 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 12:51:45 +0000 Subject: [PATCH 10/33] Added variable period usage accounting --- .../modules/accounting/mod_accounting_sql.pm | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/lib/smradius/modules/accounting/mod_accounting_sql.pm b/lib/smradius/modules/accounting/mod_accounting_sql.pm index 98976aa..b9aa890 100644 --- a/lib/smradius/modules/accounting/mod_accounting_sql.pm +++ b/lib/smradius/modules/accounting/mod_accounting_sql.pm @@ -1,5 +1,5 @@ # SQL accounting database -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -206,6 +206,20 @@ sub init AND PeriodKey = %{query.PeriodKey} '; + $config->{'accounting_usage_query_period'} = ' + SELECT + SUM(AcctInputOctets) AS AcctInputOctets, + SUM(AcctOutputOctets) AS AcctOutputOctets, + SUM(AcctInputGigawords) AS AcctInputGigawords, + SUM(AcctOutputGigawords) AS AcctOutputGigawords, + SUM(AcctSessionTime) AS AcctSessionTime + FROM + @TP@accounting + WHERE + Username = %{user.Username} + AND EventTimestamp > %{query.PeriodKey} + '; + $config->{'accounting_select_duplicates_query'} = ' SELECT ID @@ -280,6 +294,15 @@ sub init $config->{'accounting_usage_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_usage_query'}; } } + if (defined($scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}) && + $scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'} ne "") { + if (ref($scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}) eq "ARRAY") { + $config->{'accounting_usage_query_period'} = join(' ', + @{$scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}}); + } else { + $config->{'accounting_usage_query_period'} = $scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}; + } + } if (defined($scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'}) && $scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'} ne "") { if (ref($scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'}) eq "ARRAY") { @@ -327,9 +350,10 @@ sub init # Function to get radius user data usage +# The 'period' parameter is optional and is the number of days to return usage for sub getUsage { - my ($server,$user,$packet) = @_; + my ($server,$user,$packet,$period) = @_; # Build template my $template; @@ -341,9 +365,27 @@ sub getUsage $template->{'user'}->{'ID'} = $user->{'ID'}; $template->{'user'}->{'Username'} = $user->{'Username'}; - # Current PeriodKey + # Current PeriodKey, this is used for non-$period queries my $now = DateTime->now->set_time_zone($server->{'smradius'}->{'event_timezone'}); - $template->{'query'}->{'PeriodKey'} = $now->strftime("%Y-%m"); + + # Query template to use below + my $queryTemplate; + # If we're doing a query for a specific period + if (defined($period)) { + # We need to switch out the query to the period query + $queryTemplate = "accounting_usage_query_period"; + # Grab a clone of now, and create the start date DateTime object + my $startDate = $now->clone->subtract( 'days' => $period ); + # And we add the start date + $template->{'query'}->{'PeriodKey'} = $startDate->ymd(); + + # If not, we just use PeriodKey as normal... + } else { + # Set the normal PeriodKey query template to use + $queryTemplate = "accounting_usage_query"; + # And set the period key to this month + $template->{'query'}->{'PeriodKey'} = $now->strftime("%Y-%m"); + } # If we using caching, check how old the result is if (defined($config->{'accounting_usage_cache_time'})) { @@ -355,7 +397,7 @@ sub getUsage } # Replace template entries - my (@dbDoParams) = templateReplace($config->{'accounting_usage_query'},$template); + my (@dbDoParams) = templateReplace($config->{$queryTemplate},$template); # Fetch data my $sth = DBSelect(@dbDoParams); -- GitLab From 93071e93be6c010f26c837339715109425a95302 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 13:01:36 +0000 Subject: [PATCH 11/33] Fixed deprecated warning Unescaped left brace in regex is deprecated, passed through in regex; marked by <-- HERE in m/%{ <-- HERE query.Value}/ at xxx/lib/smradius/util.pm line 90. --- lib/smradius/util.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/smradius/util.pm b/lib/smradius/util.pm index 2c500ee..fa6724e 100644 --- a/lib/smradius/util.pm +++ b/lib/smradius/util.pm @@ -85,7 +85,7 @@ sub templateReplace $placeholder //= '?'; # Replace blanks - while (my ($entireMacro,$section,$item,$default) = ($string =~ /(\%{([a-z]+)\.([a-z0-9\-]+)(?:=([^}]*))?})/i )) { + while (my ($entireMacro,$section,$item,$default) = ($string =~ /(\%\{([a-z]+)\.([a-z0-9\-]+)(?:=([^\}]*))?\})/i )) { # Replace macro with ? $string =~ s/$entireMacro/$placeholder/; -- GitLab From 010577159234668efac0efc552db3eba4f5e28e3 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 13:17:07 +0000 Subject: [PATCH 12/33] Initialize BigInt with 0 to get around the uninitialized warnings Use of uninitialized value in new at xxx/lib/smradius/modules/accounting/mod_accounting_sql.pm line 411. --- .../modules/accounting/mod_accounting_sql.pm | 24 +++++++++---------- .../modules/system/mod_config_sql_topups.pm | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/smradius/modules/accounting/mod_accounting_sql.pm b/lib/smradius/modules/accounting/mod_accounting_sql.pm index b9aa890..8ca7b8a 100644 --- a/lib/smradius/modules/accounting/mod_accounting_sql.pm +++ b/lib/smradius/modules/accounting/mod_accounting_sql.pm @@ -408,9 +408,9 @@ sub getUsage # Our usage hash my %usageTotals; - $usageTotals{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{'TotalDataInput'} = Math::BigInt->new(); - $usageTotals{'TotalDataOutput'} = Math::BigInt->new(); + $usageTotals{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{'TotalDataInput'} = Math::BigInt->new(0); + $usageTotals{'TotalDataOutput'} = Math::BigInt->new(0); # Pull in usage and add up while (my $row = hashifyLCtoMC($sth->fetchrow_hashref(), @@ -443,9 +443,9 @@ sub getUsage DBFreeRes($sth); # Convert to bigfloat for accuracy - my $totalData = Math::BigFloat->new(); + my $totalData = Math::BigFloat->new(0); $totalData->badd($usageTotals{'TotalDataOutput'})->badd($usageTotals{'TotalDataInput'}); - my $totalTime = Math::BigFloat->new(); + my $totalTime = Math::BigFloat->new(0); $totalTime->badd($usageTotals{'TotalSessionTime'}); # Rounding up @@ -515,10 +515,10 @@ sub acct_log } # Convert session total gigawords/octets into bytes - my $totalInputBytes = Math::BigInt->new(); + my $totalInputBytes = Math::BigInt->new(0); $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Gigawords'})->bmul(UINT_MAX); $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Octets'}); - my $totalOutputBytes = Math::BigInt->new(); + my $totalOutputBytes = Math::BigInt->new(0); $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Gigawords'})->bmul(UINT_MAX); $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Octets'}); # Packets, no conversion @@ -534,10 +534,10 @@ sub acct_log )) { # Convert this session usage to bytes - my $sessionInputBytes = Math::BigInt->new(); + my $sessionInputBytes = Math::BigInt->new(0); $sessionInputBytes->badd($sessionPart->{'AcctInputGigawods'})->bmul(UINT_MAX); $sessionInputBytes->badd($sessionPart->{'AcctInputOctets'}); - my $sessionOutputBytes = Math::BigInt->new(); + my $sessionOutputBytes = Math::BigInt->new(0); $sessionOutputBytes->badd($sessionPart->{'AcctOutputGigawods'})->bmul(UINT_MAX); $sessionOutputBytes->badd($sessionPart->{'AcctOutputOctets'}); # And packets @@ -815,9 +815,9 @@ sub cleanup } else { # Make BigInts for this user - $usageTotals{$row->{'Username'}}{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{$row->{'Username'}}{'TotalDataInput'} = Math::BigInt->new(); - $usageTotals{$row->{'Username'}}{'TotalDataOutput'} = Math::BigInt->new(); + $usageTotals{$row->{'Username'}}{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{$row->{'Username'}}{'TotalDataInput'} = Math::BigInt->new(0); + $usageTotals{$row->{'Username'}}{'TotalDataOutput'} = Math::BigInt->new(0); # Look for session time if (defined($row->{'AcctSessionTime'}) && $row->{'AcctSessionTime'} > 0) { diff --git a/lib/smradius/modules/system/mod_config_sql_topups.pm b/lib/smradius/modules/system/mod_config_sql_topups.pm index 3309c98..28f3b3b 100644 --- a/lib/smradius/modules/system/mod_config_sql_topups.pm +++ b/lib/smradius/modules/system/mod_config_sql_topups.pm @@ -383,8 +383,8 @@ sub cleanup # Our usage hash my %usageTotals; - $usageTotals{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{'TotalDataUsage'} = Math::BigInt->new(); + $usageTotals{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{'TotalDataUsage'} = Math::BigInt->new(0); # Pull in usage and add up if (my $row = hashifyLCtoMC($sth->fetchrow_hashref(), -- GitLab From 8accbb9d4cac7b73cb7dc5b1adaf9babdd0e8075 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 14:17:04 +0000 Subject: [PATCH 13/33] Quote the variable we're interpolating into the regex --- lib/smradius/util.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/smradius/util.pm b/lib/smradius/util.pm index fa6724e..749dd35 100644 --- a/lib/smradius/util.pm +++ b/lib/smradius/util.pm @@ -86,8 +86,9 @@ sub templateReplace # Replace blanks while (my ($entireMacro,$section,$item,$default) = ($string =~ /(\%\{([a-z]+)\.([a-z0-9\-]+)(?:=([^\}]*))?\})/i )) { - # Replace macro with ? - $string =~ s/$entireMacro/$placeholder/; + # Replace macro with ? or the placeholder if specified + # We also quote the entireMacro + $string =~ s/\Q$entireMacro\E/$placeholder/; # Get value to substitute my $value = (defined($hashref->{$section}) && defined($hashref->{$section}->{$item})) ? -- GitLab From 893bc2f4c79da1c5c10f34d6ce4765e70fddbc1f Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 14:28:12 +0000 Subject: [PATCH 14/33] Fixed whitespaces --- database/core.tsql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/core.tsql b/database/core.tsql index 68b74ab..f5222e4 100644 --- a/database/core.tsql +++ b/database/core.tsql @@ -199,13 +199,13 @@ CREATE TABLE @PREFIX@accounting ( ServiceType @INT_UNSIGNED@, - FramedProtocol @INT_UNSIGNED@, + FramedProtocol @INT_UNSIGNED@, NASPort VARCHAR(255), NASPortType @INT_UNSIGNED@, - CallingStationID VARCHAR(255), + CallingStationID VARCHAR(255), CalledStationID VARCHAR(255), @@ -279,7 +279,7 @@ CREATE INDEX @PREFIX@accounting_summary_idx3 ON @PREFIX@accounting_summary (User /* Users data */ CREATE TABLE @PREFIX@users_data ( - ID @SERIAL_TYPE@, + ID @SERIAL_TYPE@, UserID @INT_UNSIGNED@, @@ -290,4 +290,4 @@ CREATE TABLE @PREFIX@users_data ( Value VARCHAR(255), UNIQUE (UserID,Name) -) @CREATE_TABLE_SUFFIX@; +) @CREATE_TABLE_SUFFIX@; -- GitLab From 6a79baab22cdd49c0f0988f4e97fe8a157b53420 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 14:38:24 +0000 Subject: [PATCH 15/33] Added unit test for reply attributes --- t/200-dbtests.t | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index a1ef087..6ec4c6c 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -181,6 +181,11 @@ if ($child = fork()) { $user1_ID,'User-Password','==','test123' ); + my $user1attr2_ID = testDBInsert("Create user 'testuser1' attribute 'Framed-IP-Address'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'Framed-IP-Address',':=','10.0.0.1' + ); + $res = smradius::client->run( "--raddb","dicts", "127.0.0.1", @@ -191,6 +196,7 @@ if ($child = fork()) { ); is(ref($res),"HASH","smradclient should return a HASH"); is($res->{'response'}->{'code'},"Access-Accept","Check our return is 'Access-Accept' for bare user blank '' realm"); + is($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that an attribute is replied with if setup"); # -- GitLab From 0868c9bc34c4ef776c55cc8a620621827db42c88 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 14:59:12 +0000 Subject: [PATCH 16/33] Handle NULL's received from the DB a bit more sanely with the newer Math::BigInt --- lib/smradius/modules/accounting/mod_accounting_sql.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/smradius/modules/accounting/mod_accounting_sql.pm b/lib/smradius/modules/accounting/mod_accounting_sql.pm index 8ca7b8a..76e9999 100644 --- a/lib/smradius/modules/accounting/mod_accounting_sql.pm +++ b/lib/smradius/modules/accounting/mod_accounting_sql.pm @@ -532,6 +532,14 @@ sub acct_log qw(AcctInputOctets AcctInputPackets AcctOutputOctets AcctOutputPackets AcctInputGigawords AcctOutputGigawords SessionTime PeriodKey) )) { + # Make sure we treat undef values sort of sanely + $sessionPart->{'AcctInputGigawords'} //= 0; + $sessionPart->{'AcctInputOctets'} //= 0; + $sessionPart->{'AcctOutputGigawords'} //= 0; + $sessionPart->{'AcctOutputOctets'} //= 0; + $sessionPart->{'AcctInputPackets'} //= 0; + $sessionPart->{'AcctOutputPackets'} //= 0; + $sessionPart->{'AcctSessionTime'} //= 0; # Convert this session usage to bytes my $sessionInputBytes = Math::BigInt->new(0); -- GitLab From 62b4c89723bb35c3d5e5e3aa3c2f18ea6c853756 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 15:41:39 +0000 Subject: [PATCH 17/33] Added unit tests to test the attribute filtering --- t/200-dbtests.t | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 6ec4c6c..d447c77 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -186,6 +186,11 @@ if ($child = fork()) { $user1_ID,'Framed-IP-Address',':=','10.0.0.1' ); + my $user1attr3_ID = testDBInsert("Create user 'testuser1' vendor attribute '[14988:Mikrotik-Rate-Limit]'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'[14988:Mikrotik-Rate-Limit]',':=','1024k/512k' + ); + $res = smradius::client->run( "--raddb","dicts", "127.0.0.1", @@ -196,8 +201,34 @@ if ($child = fork()) { ); is(ref($res),"HASH","smradclient should return a HASH"); is($res->{'response'}->{'code'},"Access-Accept","Check our return is 'Access-Accept' for bare user blank '' realm"); - is($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that an attribute is replied with if setup"); + # Test the normal attribute and vendor attribute + is($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that attribute 'Framed-IP-Address' is". + " returned"); + is($res->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1024k/512k","Check that the vendor attribute". + "'14988:Mikrotik-Rate-Limit' is returned"); + + # Add filter attributes + my $user1attr4_ID = testDBInsert("Create user 'testuser1' filter attribute for 'Framed-IP-Address'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'SMRadius-Config-Filter-Reply-Attribute',':=','Framed-IP-Address' + ); + my $user1attr5_ID = testDBInsert("Create user 'testuser1' filter vattribute for 'Mikrotik-Rate-Limit'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'SMRadius-Config-Filter-Reply-VAttribute',':=','Mikrotik-Rate-Limit' + ); + $res = smradius::client->run( + "--raddb","dicts", + "127.0.0.1", + "auth", + "secret123", + 'User-Name=testuser1', + 'User-Password=test123', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + # We shouldn't.... + isnt($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that attribute 'Framed-IP-Address' is". + " returned"); # # Modify data for the default realm -- GitLab From 819476fca17a11f4709e82acc04334666e95ef7e Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 16:29:43 +0000 Subject: [PATCH 18/33] Fixed debugging output --- lib/smradius/attributes.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index b17ad29..5abebcf 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -612,7 +612,7 @@ sub setReplyVAttribute @attrValues = ( $attribute->{'Value'} ); } - $server->log(LOG_DEBUG,"[VATTRIBUTES] Processing REPLY attribute: '". + $server->log(LOG_DEBUG,"[VATTRIBUTES] Processing REPLY vattribute: '". $attribute->{'Name'}."' ".$attribute->{'Operator'}." '".join("','",@attrValues)."'"); -- GitLab From 8f47efb59d1ce61e576a68692c9c8e5063b857dc Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 17:06:24 +0000 Subject: [PATCH 19/33] Call the VERSION function and check value of $VERSION so we can increase our test coverage --- lib/smradius/client.pm | 4 ++-- t/200-dbtests.t | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/smradius/client.pm b/lib/smradius/client.pm index 8248ff6..429c260 100644 --- a/lib/smradius/client.pm +++ b/lib/smradius/client.pm @@ -1,5 +1,5 @@ # Radius client -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -48,7 +48,7 @@ sub run $self = $self->new() if (!ref($self)); - print(STDERR "SMRadClient v$VERSION - Copyright (c) 2007-2016, AllWorldIT\n"); + print(STDERR "SMRadClient v".VERSION." - Copyright (c) 2007-2016, AllWorldIT\n"); print(STDERR "\n"); diff --git a/t/200-dbtests.t b/t/200-dbtests.t index d447c77..09ed1a0 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -126,7 +126,6 @@ if ($child = fork()) { my $res; - # # Make sure basic test without any config does not authenticate users # -- GitLab From 2dc0563dd804dea1f84d416e9f0a9464b301ab19 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 17:21:07 +0000 Subject: [PATCH 20/33] Add reply filters to attribute reply ignore list --- lib/smradius/attributes.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index 5abebcf..93b9256 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -81,6 +81,8 @@ my @attributeReplyIgnoreList = ( 'SMRadius-AutoTopup-Uptime-Notify', 'SMRadius-AutoTopup-Uptime-NotifyTemplate', 'SMRadius-AutoTopup-Uptime-Threshold', + 'SMRadius-Config-Filter-Reply-Attribute', + 'SMRadius-Config-Filter-Reply-VAttribute', 'SMRadius-FUP-Period', 'SMRadius-FUP-Traffic-Threshold', ); -- GitLab From bcaa1e811fae80625522b745613c386264f7b98b Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 17:36:43 +0000 Subject: [PATCH 21/33] FEATURE: FUP --- lib/smradius/daemon.pm | 7 + .../modules/features/mod_feature_fup.pm | 355 ++++++++++++++++++ smradiusd.conf | 1 + t/200-dbtests.t | 96 ++++- 4 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 lib/smradius/modules/features/mod_feature_fup.pm diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index cd95ae2..fd6c1f3 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -903,6 +903,13 @@ sub process_request { # We don't care if it fails } } +# TEST START + my $coaReq = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); + + # Process the reply attributes + $self->_processReplyAttributes($request,$user,$coaReq); +# TEST END + # Check if we must POD the user if ($PODUser) { diff --git a/lib/smradius/modules/features/mod_feature_fup.pm b/lib/smradius/modules/features/mod_feature_fup.pm new file mode 100644 index 0000000..0f9c91b --- /dev/null +++ b/lib/smradius/modules/features/mod_feature_fup.pm @@ -0,0 +1,355 @@ +# FUP support +# Copyright (C) 2007-2019, AllWorldIT +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package smradius::modules::features::mod_feature_fup; + +use strict; +use warnings; + +# Modules we need +use smradius::attributes; +use smradius::constants; +use smradius::logging; +use smradius::util; + +use AWITPT::Util; +use List::Util qw( min ); +use MIME::Lite; +use POSIX qw( floor ); + + +# Set our version +our $VERSION = "0.0.1"; + + +# Load exporter +use base qw(Exporter); +our @EXPORT = qw( +); +our @EXPORT_OK = qw( +); + + + +# Plugin info +our $pluginInfo = { + Name => "User FUP Feature", + Init => \&init, + + # Authentication hook + 'Feature_Post-Authentication_hook' => \&post_auth_hook, + + # Accounting hook + 'Feature_Post-Accounting_hook' => \&post_acct_hook, +}; + + +# Some constants +my $FUP_PERIOD_ATTRIBUTE = 'SMRadius-FUP-Period'; +my $FUP_TRAFFIC_THRESHOLD_ATTRIBUTE = 'SMRadius-FUP-Traffic-Threshold'; + +my $config; + + + +## @internal +# Initialize module +sub init +{ + my $server = shift; + my $scfg = $server->{'inifile'}; + + + # Defaults + $config->{'enable_mikrotik'} = 0; + + # Setup SQL queries + if (defined($scfg->{'mod_feature_fup'})) { + # Check if option exists + if (defined($scfg->{'mod_feature_fup'}{'enable_mikrotik'})) { + # Pull in config + if (defined(my $val = isBoolean($scfg->{'mod_feature_fup'}{'enable_mikrotik'}))) { + if ($val) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Mikrotik-specific vendor return attributes ENABLED"); + $config->{'enable_mikrotik'} = $val; + } + } else { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Value for 'enable_mikrotik' is invalid"); + } + } + } + + return; +} + + + +## @post_auth_hook($server,$user,$packet) +# Post authentication hook +# +# @param server Server object +# @param user User data +# @param packet Radius packet +# +# @return Result +sub post_auth_hook +{ + my ($server,$user,$packet) = @_; + + + # Skip MAC authentication + return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] POST AUTH HOOK"); + + + # + # Get threshold from attributes + # + + my $fupPeriod = _getAttributeKeyNumeric($server,$user,$FUP_PERIOD_ATTRIBUTE); + my $trafficThreshold = _getAttributeKeyNumeric($server,$user,$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE); + + # If we have no FUP period, skip + if (!defined($fupPeriod)) { + return MOD_RES_SKIP; + }; + + # If we have no traffic threshold, display an info message and skip + if (!defined($trafficThreshold)) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] User has a '$FUP_PERIOD_ATTRIBUTE' defined, but NOT a ". + "'$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE' attribute, aborting FUP checks."); + return MOD_RES_SKIP; + }; + + + # + # Get current traffic and uptime usage + # + + my $accountingUsage = _getAccountingUsage($server,$user,$packet,$fupPeriod); + if (!defined($accountingUsage)) { + return MOD_RES_SKIP; + } + + + # + # Display our FUP info + # + + _logUsage($server,$fupPeriod,$accountingUsage->{'TotalDataUsage'},$trafficThreshold); + + + # + # Check if the user has exceeded the FUP + # + + my $fupExceeded = ($accountingUsage->{'TotalDataUsage'} > $trafficThreshold) ? 1 : 0; + + # + # Add conditional variables + # + + addAttributeConditionalVariable($user,"SMRadius_FUP",$fupExceeded); + + + return MOD_RES_ACK; +} + + + +## @post_acct_hook($server,$user,$packet) +# Post authentication hook +# +# @param server Server object +# @param user User data +# @param packet Radius packet +# +# @return Result +sub post_acct_hook +{ + my ($server,$user,$packet) = @_; + + + # We cannot cap a user if we don't have a UserDB module can we? no userdb, no cap? + return MOD_RES_SKIP if (!defined($user->{'_UserDB'}->{'Name'})); + + # Skip MAC authentication + return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); + + # User is either connecting 'START' or disconnecting 'STOP' + return MOD_RES_SKIP if ($packet->rawattr('Acct-Status-Type') ne "1" && $packet->rawattr('Acct-Status-Type') ne "3"); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] POST ACCT HOOK"); + + + # + # Get threshold from attributes + # + + my $fupPeriod = _getAttributeKeyNumeric($server,$user,$FUP_PERIOD_ATTRIBUTE); + my $trafficThreshold = _getAttributeKeyNumeric($server,$user,$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE); + + # If we have no FUP period, skip + if (!defined($fupPeriod)) { + return MOD_RES_SKIP; + }; + + # If we have no traffic threshold, display an info message and skip + if (!defined($trafficThreshold)) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] User has a '$FUP_PERIOD_ATTRIBUTE' defined, but NOT a ". + "'$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE' attribute, aborting FUP checks."); + return MOD_RES_SKIP; + }; + + + # + # Get current traffic and uptime usage + # + + my $accountingUsage = _getAccountingUsage($server,$user,$packet,$fupPeriod); + if (!defined($accountingUsage)) { + return MOD_RES_SKIP; + } + + + # + # Display our FUP info + # + + _logUsage($server,$fupPeriod,$accountingUsage->{'TotalDataUsage'},$trafficThreshold); + + + # + # Check if the user has exceeded the FUP + # + + my $fupExceeded = ($accountingUsage->{'TotalDataUsage'} > $trafficThreshold) ? 1 : 0; + + # + # Add conditional variables + # + + addAttributeConditionalVariable($user,"SMRadius_FUP",$fupExceeded); + + + return MOD_RES_ACK; +} + + + +## @internal +# Code snippet to grab the current uptime limit by processing the user attributes +sub _getAttributeKeyNumeric +{ + my ($server,$user,$attributeKey) = @_; + + + # Short circuit return if we don't have the uptime key set + return if (!defined($user->{'Attributes'}->{$attributeKey})); + + # Short circuit if we do not have a valid attribute operator: ':=' + if (!defined($user->{'Attributes'}->{$attributeKey}->{':='})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] No valid operators for attribute '". + $user->{'Attributes'}->{$attributeKey}."'"); + return; + } + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] Attribute '".$attributeKey."' is defined"); + + # Check for valid attribute value + if (!defined($user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}) || + $user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'} !~ /^\d+$/) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Attribute '".$user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}. + "' is NOT a numeric value"); + return; + } + + return $user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}; +} + + + +## @internal +# Code snippet to grab the accounting usage of a user for a specific period +sub _getAccountingUsage +{ + my ($server,$user,$packet,$period) = @_; + + + foreach my $module (@{$server->{'module_list'}}) { + # Do we have the correct plugin? + if (defined($module->{'Accounting_getUsage'})) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] Found plugin: '".$module->{'Name'}."'"); + # Fetch users session uptime & bandwidth used for a specific period + if (my $res = $module->{'Accounting_getUsage'}($server,$user,$packet,$period)) { + return $res; + } + $server->log(LOG_ERR,"[MOD_FEATURE_FUP] No usage data found for user '".$user->{'Username'}."'"); + } + } + + return; +} + + + +## @internal +# Code snippet to log our FUP information +sub _logUsage +{ + my ($server,$period,$total,$threshold) = @_; + + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] FUP information [period: %s days, total: %s, threshold: %s]", + $period,$total,$threshold); + + return; +} + + + +## @internal +# Function snippet to return a attribute +sub _getAttribute +{ + my ($server,$user,$attributeName) = @_; + + + # Check the attribute exists + return if (!defined($user->{'Attributes'}->{$attributeName})); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' is defined"); + + # Check the required operator is present in this case := + if (!defined($user->{'Attributes'}->{$attributeName}->{':='})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' has no ':=' operator"); + return; + } + + # Check the operator value is defined... + if (!defined($user->{'Attributes'}->{$attributeName}->{':='}->{'Value'})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' has no value"); + return; + } + + return $user->{'Attributes'}->{$attributeName}->{':='}->{'Value'}; +} + + + +1; +# vim: ts=4 diff --git a/smradiusd.conf b/smradiusd.conf index 638df3a..8a48538 100644 --- a/smradiusd.conf +++ b/smradiusd.conf @@ -163,6 +163,7 @@ mod_feature_capping mod_feature_user_stats mod_feature_update_user_stats_sql mod_feature_validity +mod_feature_fup EOT diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 09ed1a0..1754078 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -752,8 +752,6 @@ if ($child = fork()) { my $session3_ID = 9858240; my $session3_Timestamp = time(); - my $session3_Timestamp_str = DateTime->from_epoch(epoch => $session3_Timestamp,time_zone => 'UTC') - ->strftime('%Y-%m-%d %H:%M:%S'); $res = smradius::client->run( "--raddb","dicts", @@ -811,6 +809,100 @@ if ($child = fork()) { ); + # + # Check that if we send an accounting ALIVE we trigger the FUP + # + + my $user5_ID = testDBInsert("Create user 'testuser5'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser5',0)" + ); + + my $user5attr1_ID = testDBInsert("Create user 'testuser5' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'User-Password','==','test456' + ); + + my $user5attr2_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-FUP-Period'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-FUP-Period',':=','1' + ); + + my $user5attr3_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-FUP-Traffic-Threshold'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-FUP-Traffic-Threshold',':=',800 + ); + + # Add an attribute so we can check the FUP match results + my $user5attr4_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-Evaluate'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m" + ); + + my $session4_ID = 9858240; + my $session4_Timestamp = time() - 3600; + + $res = smradius::client->run( + "--raddb","dicts", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser5', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=12', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=786933', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=708163705', + 'Acct-Input-Packets=670235', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=102600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session4_Timestamp, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session4_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + + my $session4_Timestamp2 = time(); + + $res = smradius::client->run( + "--raddb","dicts", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser5', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=8', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=700000', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=850000000', + 'Acct-Input-Packets=100000', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=100000000', + 'Acct-Session-Time=1000', + 'Event-Timestamp='.$session4_Timestamp2, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session4_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + sleep(5); -- GitLab From 8c539166f092e231e1609e081e135b9075faac14 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 18:44:06 +0000 Subject: [PATCH 22/33] Changed gigaword constant name --- lib/smradius/constants.pm | 7 ++-- .../modules/accounting/mod_accounting_sql.pm | 38 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/smradius/constants.pm b/lib/smradius/constants.pm index b02f2f8..b23ddf9 100644 --- a/lib/smradius/constants.pm +++ b/lib/smradius/constants.pm @@ -20,14 +20,13 @@ ## @class smradius::constants # SMRadius constants package package smradius::constants; +use base qw(Exporter); use strict; use warnings; -# Exporter stuff -use base qw(Exporter); our (@EXPORT,@EXPORT_OK); @EXPORT = qw( RES_OK @@ -37,7 +36,7 @@ our (@EXPORT,@EXPORT_OK); MOD_RES_NACK MOD_RES_SKIP - UINT_MAX + GIGAWORD_VALUE ); @EXPORT_OK = (); @@ -50,7 +49,7 @@ use constant { MOD_RES_ACK => 1, MOD_RES_NACK => 2, - UINT_MAX => 2**32 + GIGAWORD_VALUE => 2**32, }; diff --git a/lib/smradius/modules/accounting/mod_accounting_sql.pm b/lib/smradius/modules/accounting/mod_accounting_sql.pm index 76e9999..d3265d1 100644 --- a/lib/smradius/modules/accounting/mod_accounting_sql.pm +++ b/lib/smradius/modules/accounting/mod_accounting_sql.pm @@ -427,7 +427,7 @@ sub getUsage } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -436,7 +436,7 @@ sub getUsage } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{'TotalDataOutput'}->badd($outputGigawords); } } @@ -514,12 +514,13 @@ sub acct_log return; } - # Convert session total gigawords/octets into bytes - my $totalInputBytes = Math::BigInt->new(0); - $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Gigawords'})->bmul(UINT_MAX); + # Convert session total gigawords into bytes + my $totalInputBytes = Math::BigInt->new($template->{'request'}->{'Acct-Input-Gigawords'}); + my $totalOutputBytes = Math::BigInt->new($template->{'request'}->{'Acct-Output-Gigawords'}); + $totalInputBytes->bmul(GIGAWORD_VALUE); + $totalOutputBytes->bmul(GIGAWORD_VALUE); + # Add byte counters $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Octets'}); - my $totalOutputBytes = Math::BigInt->new(0); - $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Gigawords'})->bmul(UINT_MAX); $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Octets'}); # Packets, no conversion my $totalInputPackets = Math::BigInt->new($template->{'request'}->{'Acct-Input-Packets'}); @@ -541,12 +542,13 @@ sub acct_log $sessionPart->{'AcctOutputPackets'} //= 0; $sessionPart->{'AcctSessionTime'} //= 0; - # Convert this session usage to bytes - my $sessionInputBytes = Math::BigInt->new(0); - $sessionInputBytes->badd($sessionPart->{'AcctInputGigawods'})->bmul(UINT_MAX); + # Convert the gigawords into bytes + my $sessionInputBytes = Math::BigInt->new($sessionPart->{'AcctInputGigawords'}); + my $sessionOutputBytes = Math::BigInt->new($sessionPart->{'AcctOutputGigawords'}); + $sessionInputBytes->bmul(GIGAWORD_VALUE); + $sessionOutputBytes->bmul(GIGAWORD_VALUE); + # Add the byte counters $sessionInputBytes->badd($sessionPart->{'AcctInputOctets'}); - my $sessionOutputBytes = Math::BigInt->new(0); - $sessionOutputBytes->badd($sessionPart->{'AcctOutputGigawods'})->bmul(UINT_MAX); $sessionOutputBytes->badd($sessionPart->{'AcctOutputOctets'}); # And packets my $sessionInputPackets = Math::BigInt->new($sessionPart->{'AcctInputPackets'}); @@ -588,8 +590,8 @@ sub acct_log } # Re-calculate - my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(UINT_MAX); - my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(UINT_MAX); + my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(GIGAWORD_VALUE); + my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(GIGAWORD_VALUE); # Conversion to strings $template->{'query'}->{'Acct-Input-Gigawords'} = $inputGigawordsStr->bstr(); @@ -806,7 +808,7 @@ sub cleanup } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -815,7 +817,7 @@ sub cleanup } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataOutput'}->badd($outputGigawords); } @@ -837,7 +839,7 @@ sub cleanup } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -846,7 +848,7 @@ sub cleanup } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataOutput'}->badd($outputGigawords); } -- GitLab From 0343eaeb4168676c49cd1cb371fef2f81b38ba3b Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 19:01:05 +0000 Subject: [PATCH 23/33] Fixed session ID's in tests --- t/200-dbtests.t | 63 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 1754078..f4e74eb 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -500,7 +500,7 @@ if ($child = fork()) { # Test missing accounting START packet # - my $session2_ID = 81700217; + my $session2_ID = "817a0f1b"; my $session2_Timestamp = time(); my $session2_Timestamp_str = DateTime->from_epoch(epoch => $session2_Timestamp,time_zone => 'UTC') ->strftime('%Y-%m-%d %H:%M:%S'); @@ -750,7 +750,7 @@ if ($child = fork()) { ); - my $session3_ID = 9858240; + my $session3_ID = "9c5f24a"; my $session3_Timestamp = time(); $res = smradius::client->run( @@ -810,7 +810,7 @@ if ($child = fork()) { # - # Check that if we send an accounting ALIVE we trigger the FUP + # Check that if we send an accounting ALIVE we do not trigger FUP # my $user5_ID = testDBInsert("Create user 'testuser5'", @@ -838,7 +838,7 @@ if ($child = fork()) { $user5_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m" ); - my $session4_ID = 9858240; + my $session4_ID = "a8abc40"; my $session4_Timestamp = time() - 3600; $res = smradius::client->run( @@ -870,29 +870,61 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'response'}->{'vattributes'},undef,"Check that the vendor attributes are not defined"); - my $session4_Timestamp2 = time(); + + # + # Check that if we send an accounting ALIVE with a usage amount that exceeds FUP, that we trigger it + # + + my $user6_ID = testDBInsert("Create user 'testuser6'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser6',0)" + ); + + my $user6attr1_ID = testDBInsert("Create user 'testuser6' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'User-Password','==','test456' + ); + + my $user6attr2_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-FUP-Period'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-FUP-Period',':=','1' + ); + + my $user6attr3_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-FUP-Traffic-Threshold'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-FUP-Traffic-Threshold',':=',800 + ); + + # Add an attribute so we can check the FUP match results + my $user6attr4_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-Evaluate'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m" + ); + + my $session5_ID = "582dc00"; + my $session5_Timestamp = time() - 3600; $res = smradius::client->run( "--raddb","dicts", "127.0.0.1", "acct", "secret123", - 'User-Name=testuser5', + 'User-Name=testuser6', 'NAS-IP-Address=10.0.0.1', - 'Acct-Delay-Time=8', + 'Acct-Delay-Time=12', 'NAS-Identifier=Test-NAS2', 'Acct-Status-Type=Interim-Update', - 'Acct-Output-Packets=700000', + 'Acct-Output-Packets=786933', 'Acct-Output-Gigawords=0', - 'Acct-Output-Octets=850000000', - 'Acct-Input-Packets=100000', + 'Acct-Output-Octets=808163705', + 'Acct-Input-Packets=670235', 'Acct-Input-Gigawords=0', - 'Acct-Input-Octets=100000000', - 'Acct-Session-Time=1000', - 'Event-Timestamp='.$session4_Timestamp2, + 'Acct-Input-Octets=202600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session5_Timestamp, 'Framed-IP-Address=10.0.1.1', - 'Acct-Session-Id='.$session4_ID, + 'Acct-Session-Id='.$session5_ID, 'NAS-Port-Id=wlan1', 'Called-Station-Id=testservice2', 'Calling-Station-Id=00:00:0C:EE:47:BF', @@ -902,6 +934,9 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the vendor attribute". + "'14988:Mikrotik-Rate-Limit' is returned"); + sleep(5); -- GitLab From e26801c68c1712c2d7d6a905b9660c030aece2a4 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 19:31:49 +0000 Subject: [PATCH 24/33] Added support to smradclient to listen on an additional port --- lib/smradius/client.pm | 75 ++++++++++++++++++++++++++++++++++++++---- t/200-dbtests.t | 6 ++-- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/lib/smradius/client.pm b/lib/smradius/client.pm index 429c260..ba10ff8 100644 --- a/lib/smradius/client.pm +++ b/lib/smradius/client.pm @@ -44,11 +44,15 @@ if (!eval {require Config::IniFiles; 1;}) { sub run { my ($self,@methodArgs) = @_; + + # Instantiate if we're not already instantiated $self = $self->new() if (!ref($self)); + # The hash we're going to return + my $ret = { }; - print(STDERR "SMRadClient v".VERSION." - Copyright (c) 2007-2016, AllWorldIT\n"); + print(STDERR "SMRadClient v".VERSION." - Copyright (c) 2007-2019, AllWorldIT\n"); print(STDERR "\n"); @@ -67,6 +71,7 @@ sub run \%{$cmdline}, "config:s", "raddb:s", + "listen:s", "help", )) { print(STDERR "ERROR: Error parsing commandline arguments"); @@ -190,13 +195,38 @@ sub run return 1; } + my $sock2; + # Check if we must listen on another IP/port + if (defined($cmdline->{'listen'}) && $cmdline->{'listen'} ne "") { + print(STDERR "Creating second socket\n"); + + # Check the details we were provided + my ($localAddr,$localPort) = split(/:/,$cmdline->{'listen'}); + if (!defined($localPort)) { + print(STDERR "ERROR: The format for --listen is IP:Port\n"); + return 1; + } + + $sock2 = IO::Socket::INET->new( + LocalAddr => $localAddr, + LocalPort => $localPort, + Type => SOCK_DGRAM, + Proto => 'udp', + Timeout => $sockTimeout, + ); + + if (!$sock2) { + print(STDERR "ERROR: Failed to create second socket\n"); + return 1; + } + } + # Check if we sent the packet... if (!$sock->send($udp_packet)) { print(STDERR "ERROR: Failed to send data on socket\n"); return 1; } - # And time for the response print(STDERR "\nResponse:\n"); @@ -216,7 +246,7 @@ sub run # Read packet $sock->recv($udp_packet, 65536); if (!$udp_packet) { - print(STDERR "ERROR: Receive response data failed: $!\n"); + print(STDERR "ERROR: Receive response data failed on socket: $!\n"); return 1; } @@ -225,13 +255,44 @@ sub run print(STDERR " > Authenticated: ". (defined(auth_req_verify($udp_packet,$self->{'secret'},$authen)) ? "yes" : "no") ."\n"); print(STDERR $pkt->str_dump()); + # Setup response + $ret->{'request'} = $self->hashedPacket($self->{'packet'}); + $ret->{'response'} = $self->hashedPacket($pkt); + + + my $udp_packet2; + if (defined($sock2)) { + my $rsock2 = IO::Select->new($sock2); + if (!$rsock2) { + print(STDERR "ERROR: Failed to select response data on socket2\n"); + return 1; + } + + # Check if we can read a response after the select() + if (!$rsock2->can_read($sockTimeout)) { + print(STDERR "ERROR: Failed to receive response data on socket2\n"); + return 1; + } + + # Read packet + my $udp_packet2; + $sock2->recv($udp_packet2, 65536); + if (!$udp_packet2) { + print(STDERR "ERROR: Receive response data failed on socket2: $!\n"); + return 1; + } + + my $pkt2 = smradius::Radius::Packet->new($raddb,$udp_packet2); + print(STDERR $pkt2->str_dump()); + + # Save the packet we got + $ret->{'listen'}->{'response'} = $self->hashedPacket($pkt2); + } + # If we were called as a function, return hashed version of the response packet if (@methodArgs) { - return { - 'request' => $self->hashedPacket($self->{'packet'}), - 'response' => $self->hashedPacket($pkt), - }; + return $ret; } return 0; diff --git a/t/200-dbtests.t b/t/200-dbtests.t index f4e74eb..7f1ebd4 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -839,10 +839,11 @@ if ($child = fork()) { ); my $session4_ID = "a8abc40"; - my $session4_Timestamp = time() - 3600; + my $session4_Timestamp = time(); $res = smradius::client->run( "--raddb","dicts", + "--listen","127.0.0.1:1700", "127.0.0.1", "acct", "secret123", @@ -903,10 +904,11 @@ if ($child = fork()) { ); my $session5_ID = "582dc00"; - my $session5_Timestamp = time() - 3600; + my $session5_Timestamp = time(); $res = smradius::client->run( "--raddb","dicts", + "--listen","127.0.0.1:1700", "127.0.0.1", "acct", "secret123", -- GitLab From c615c7eaa005aa88458284493568a511fd16d516 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 21:31:06 +0000 Subject: [PATCH 25/33] Fixed whitespaces --- lib/smradius/client.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/smradius/client.pm b/lib/smradius/client.pm index ba10ff8..0ea5a26 100644 --- a/lib/smradius/client.pm +++ b/lib/smradius/client.pm @@ -74,8 +74,8 @@ sub run "listen:s", "help", )) { - print(STDERR "ERROR: Error parsing commandline arguments"); - return 1; + print(STDERR "ERROR: Error parsing commandline arguments"); + return 1; } # Check for some args @@ -322,7 +322,7 @@ sub hashedPacket foreach my $attrName ($pkt->vsattributes($attrVendor)) { $res->{'vattributes'}->{$attrVendor}->{$attrName} = $pkt->vsattr($attrVendor,$attrName); } - } + } return $res; } -- GitLab From 035c70fa98421565702506cc5598f2e1b2ca7752 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Wed, 15 May 2019 22:41:45 +0000 Subject: [PATCH 26/33] FEATURE: CoA --- lib/smradius/daemon.pm | 221 ++++++++++++++++++++++------------------- t/200-dbtests.t | 9 +- 2 files changed, 124 insertions(+), 106 deletions(-) diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index fd6c1f3..7775751 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -898,128 +898,145 @@ sub process_request { foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { # Grab attribute my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; - # Check attribute against accounting attributes attributes + # Check attribute against accounting attributes my $res = checkAcctAttribute($self,$user,$acctAttributes,$attr); # We don't care if it fails } } -# TEST START - my $coaReq = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); - # Process the reply attributes - $self->_processReplyAttributes($request,$user,$coaReq); -# TEST END + # The coaReq may be either POD or CoA + my $coaReq = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); + # Set packet identifier + $coaReq->set_identifier( $$ & 0xff ); - # Check if we must POD the user + # Check if we must POD the user, if so we set the code to disconnect if ($PODUser) { $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying to disconnect user..."); + $coaReq->set_code('Disconnect-Request'); + } else { + # If this is *not* a POD, we need to process reply attributes + $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Sending CoA..."); + $coaReq->set_code('CoA-Request'); + # Process the reply attributes + $self->_processReplyAttributes($request,$user,$coaReq); + } - my $resp = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); + # NAS identification + $coaReq->set_attr('NAS-IP-Address',$pkt->attr('NAS-IP-Address')); + # Session identification + $coaReq->set_attr('User-Name',$pkt->attr('User-Name')); + $coaReq->set_attr('NAS-Port',$pkt->attr('NAS-Port')); + $coaReq->set_attr('Acct-Session-Id',$pkt->attr('Acct-Session-Id')); - $resp->set_code('Disconnect-Request'); - my $id = $$ & 0xff; - $resp->set_identifier( $id ); - - $resp->set_attr('User-Name',$pkt->attr('User-Name')); - $resp->set_attr('Framed-IP-Address',$pkt->attr('Framed-IP-Address')); - $resp->set_attr('NAS-IP-Address',$pkt->attr('NAS-IP-Address')); - - # Add onto logline - $request->addLogLine(". REPLY => "); - foreach my $attrName ($resp->attributes) { - $request->addLogLine( - "%s: '%s'", - $attrName, - $resp->rawattr($attrName) - ); + # Add onto logline + $request->addLogLine(". REPLY => "); + foreach my $attrName ($coaReq->attributes) { + $request->addLogLine( + "%s: '%s'", + $attrName, + $coaReq->rawattr($attrName) + ); + } + + # Generate coaReq packet + my $coaReq_packet = auth_resp($coaReq->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")); + + # Array CoA servers to contact + my @coaServers; + + # Check for old POD server attribute + if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'})) { + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is defined"); + @coaServers = @{$user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}}; + } + + # Check for new CoA server attribute + if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'})) { + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is defined"); + @coaServers = @{$user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'}}; + } + + # If we didn't get provided a CoA server, use the peer address + if (!@coaServers) { + push(@coaServers,$server->{'peeraddr'}); + } + + # Check address format + foreach my $coaServer (@coaServers) { + # Remove IPv6 portion for now... + $coaServer =~ s/^::ffff://; + # Check for valid IP + my ($coaServerIP,$coaServerPort) = ($coaServer =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(?::([0-9]+))?/); + + if (!defined($coaServerIP)) { + $self->log(LOG_NOTICE,"[SMRADIUS] POST-ACCT: CoAServer '$coaServer' looks incorrect"); + next; + } + + # Set default CoA server port + $coaServerPort //= 1700; + + $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying CoAServer => IP: '".$coaServer."' Port: '".$coaServerPort."'"); + + # Create socket to send packet out on + my $coaServerTimeout = "2"; # 2 second timeout + my $coaSock = IO::Socket::INET->new( + PeerAddr => $coaServerIP, + PeerPort => $coaServerPort, + Type => SOCK_DGRAM, + Proto => 'udp', + TimeOut => $coaServerTimeout, + ); + + if (!$coaSock) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to create socket to send CoA on: $!"); + next; } - # Grab packet - my $response = auth_resp($resp->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")); + # Check if we sent the packet... + if (!$coaSock->send($coaReq_packet)) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to send data on CoA socket: $!"); + next; + } - my $coaServer; + # Once sent, we need to get a response back + my $select = IO::Select->new($coaSock); + if (!$select) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to select data on socket: $!"); + next; + } - # Check for old POD server attribute - if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'})) { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is defined"); - $coaServer = $user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}; + if (!$select->can_read($coaServerTimeout)) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to receive data on socket: $!"); + next; } - # Check for new CoA server attribute - if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'})) { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is defined"); - $coaServer = $user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'}; + # Grab CoA response + my $coaRes_packet; + $coaSock->recv($coaRes_packet, 65536); + if (!$coaRes_packet) { + $self->log(LOG_INFO,"[SMRADIUS] POST-ACCT: No data received in response to our request to '$coaServerIP:$coaServerPort': $!"); + $logReason = "No Response"; + next; } - # Check for CoA servers - if (defined($coaServer)) { - # Check address format - foreach my $coaServerAttribute (@{$coaServer}) { - # Check for valid IP - if ($coaServerAttribute =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/) { - my $coaServer = $1; - - # If we have a port, use it, otherwise use default 1700 - my $coaServerPort; - if ($coaServerAttribute =~ /:([0-9]+)$/) { - $coaServerPort = $1; - } else { - $coaServerPort = 1700; - } - - $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying CoAServer => IP: '".$coaServer."' Port: '".$coaServerPort."'"); - - # Create socket to send packet out on - my $coaServerTimeout = "10"; # 10 second timeout - my $coaSock = IO::Socket::INET->new( - PeerAddr => $coaServer, - PeerPort => $coaServerPort, - Type => SOCK_DGRAM, - Proto => 'udp', - TimeOut => $coaServerTimeout, - ); - - if (!$coaSock) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to create socket to send POD on"); - next; - } - - # Check if we sent the packet... - if (!$coaSock->send($response)) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to send data on socket"); - next; - } - - # Once sent, we need to get a response back - my $sh = IO::Select->new($coaSock); - if (!$sh) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to select data on socket"); - next; - } - - if (!$sh->can_read($coaServerTimeout)) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to receive data on socket"); - next; - } - - my $data; - $coaSock->recv($data, 65536); - if (!$data) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Receive data failed"); - $logReason = "CoA Failure"; - } else { - $logReason = "User POD"; - } - - #my @stuff = unpack('C C n a16 a*', $data); - #$self->log(LOG_DEBUG,"STUFF: ".Dumper(\@stuff)); - } else { - $self->log(LOG_DEBUG,"[SMRADIUS] Invalid CoA Server value: '".$coaServerAttribute."'"); - } - } + # Parse the radius packet + my $coaRes = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'},$coaRes_packet); + + # Check status + if ($coaRes->code eq "CoA-ACK") { + $logReason = "CoA Success"; + last; + } elsif ($coaRes->code eq "CoA-NACK") { + $logReason = "CoA Fail"; + } elsif ($coaRes->code eq "Disconnect-ACK") { + $logReason = "POD Success"; + last; + } elsif ($coaRes->code eq "Disconnect-NACK") { + $logReason = "POD Fail"; } else { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is not defined"); + $logReason = "CoA/POD Fail"; } } diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 7f1ebd4..90a528c 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -835,7 +835,7 @@ if ($child = fork()) { # Add an attribute so we can check the FUP match results my $user5attr4_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-Evaluate'", "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", - $user5_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m" + $user5_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m : [14988:Mikrotik-Rate-Limit] = 1k/1m" ); my $session4_ID = "a8abc40"; @@ -871,7 +871,8 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); - is($res->{'response'}->{'vattributes'},undef,"Check that the vendor attributes are not defined"); + is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1k/1m","Check that the vendor attribute". + "'14988:Mikrotik-Rate-Limit' is returned on the negative side of the IF"); # @@ -900,7 +901,7 @@ if ($child = fork()) { # Add an attribute so we can check the FUP match results my $user6attr4_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-Evaluate'", "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", - $user6_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m" + $user6_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m : [14988:Mikrotik-Rate-Limit] = 1k/1m" ); my $session5_ID = "582dc00"; @@ -936,7 +937,7 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); - is($res->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the vendor attribute". + is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the vendor attribute". "'14988:Mikrotik-Rate-Limit' is returned"); -- GitLab From 9ff2149e1b7d5da991c1c317a12360f2567e2ef9 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 09:25:40 +0000 Subject: [PATCH 27/33] Updated todo --- TODO | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TODO b/TODO index 15e95f7..2031e38 100644 --- a/TODO +++ b/TODO @@ -5,8 +5,7 @@ smradiusd: * Create a raddbpath config option which is prepended to dict paths -usage related queries: -* Use Math module to perform calculations +* Configurable 'use defaults for POD/CoA' we may not want to send these smadmin: * Ability to run smadmin before the end of current month and updating the records as necessary at a later stage -- GitLab From ceb2e82a9fb1f10f2886ad5a8a085749aeb84173 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 10:55:57 +0000 Subject: [PATCH 28/33] Remove spaces from the end of attribute values when using conditionals --- lib/smradius/attributes.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index 93b9256..f1a8b7e 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -794,7 +794,8 @@ sub processConditional # Split off expression - my ($condition,$onTrue,$onFalse) = ($attrVal =~ /^([^\?]*)(?:\?\s*((?:\S+)?[^:]*)(?:\s*\:\s*(.*))?)?$/); + # NK: This probably needs a bit of work + my ($condition,$onTrue,$onFalse) = ($attrVal =~ /^([^\?]*)(?:\?\s*((?:\S+)?[^:]*)(?:\:\s*(.*))?)?$/); # If there is no condition we cannot really continue? if (!defined($condition)) { @@ -842,6 +843,10 @@ sub processConditional $res = 1; } + # Sanitize the output + $attribStr =~ s/^\s*//; + $attribStr =~ s/\s*$//; + $server->log(LOG_DEBUG,"[ATTRIBUTES] - Evaluated to '$res' returning '".(defined($attribStr) ? $attribStr : "-undef-")."'"); # Loop with attributes: -- GitLab From eb648befe3a2e3339800c918da9c873c373a627b Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 11:25:35 +0000 Subject: [PATCH 29/33] Added capping test for POD being sent --- t/200-dbtests.t | 65 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 90a528c..c21c3a4 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -871,6 +871,9 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); + warn "DUMP: ".Dumper($res); + is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". + "CoA-Request"); is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1k/1m","Check that the vendor attribute". "'14988:Mikrotik-Rate-Limit' is returned on the negative side of the IF"); @@ -937,9 +940,67 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); - is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the vendor attribute". - "'14988:Mikrotik-Rate-Limit' is returned"); + is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". + "CoA-Request"); + is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the ". + "vendor attribute '14988:Mikrotik-Rate-Limit' is returned"); + + + # + # Check that if we send an accounting ALIVE with a usage amount that exceeds capping, that we trigger a POD + # + my $user7_ID = testDBInsert("Create user 'testuser7'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser7',0)" + ); + + my $user7attr1_ID = testDBInsert("Create user 'testuser7' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user7_ID,'User-Password','==','test456' + ); + + # Add an attribute so we can check the capping does a POD + my $user7attr4_ID = testDBInsert("Create user 'testuser7' attribute 'SMRadius-Capping-Traffic-Limit'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user7_ID,'SMRadius-Capping-Traffic-Limit',':=','1' + ); + + + my $session6_ID = "5209dac0"; + my $session6_Timestamp = time(); + + $res = smradius::client->run( + "--raddb","dicts", + "--listen","127.0.0.1:1700", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser7', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=12', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=786933', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=808163705', + 'Acct-Input-Packets=670235', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=202600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session6_Timestamp, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session6_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'listen'}->{'response'}->{'code'},"Disconnect-Request","Check that the packet we got back is infact a ". + "Disconnect-Request"); sleep(5); -- GitLab From 692c9c1f9bf3ee022e56d4c9b517f48dbfd07e6d Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 12:43:01 +0000 Subject: [PATCH 30/33] Added FUP state to mod_feature_user_stats --- .../features/mod_feature_user_stats.pm | 16 +++++++++ t/200-dbtests.t | 35 ++++++++++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/lib/smradius/modules/features/mod_feature_user_stats.pm b/lib/smradius/modules/features/mod_feature_user_stats.pm index cce0eb8..111902f 100644 --- a/lib/smradius/modules/features/mod_feature_user_stats.pm +++ b/lib/smradius/modules/features/mod_feature_user_stats.pm @@ -147,6 +147,22 @@ sub updateUserStats return MOD_RES_SKIP; } } + + # Set user FUP state + my $fupState = $user->{'AttributeConditionalVariables'}->{"SMRadius_FUP"}; + if (defined($fupState)) { + $fupState = $fupState->[0]; + } else { + $fupState = "-1"; + } + $res = $user->{'_UserDB'}->{'Users_data_set'}($server,$user, + 'mod_feature_user_stats','FUPState', + $fupState + ); + if (!defined($res)) { + $server->log(LOG_ERR,"[MOD_USERS_DATA] Failed to store FUP state for user '".$user->{'Username'}."'"); + return MOD_RES_SKIP; + } } return MOD_RES_ACK; diff --git a/t/200-dbtests.t b/t/200-dbtests.t index c21c3a4..264e303 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -877,6 +877,12 @@ if ($child = fork()) { is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1k/1m","Check that the vendor attribute". "'14988:Mikrotik-Rate-Limit' is returned on the negative side of the IF"); + testDBResults("Check FUP state was added to the user stats table as 0",'users_data', + {'UserID' => $user5_ID, 'Name' => "FUPState"}, + {'Value' => 0}, + 1, # Disable order + ); + # # Check that if we send an accounting ALIVE with a usage amount that exceeds FUP, that we trigger it @@ -943,7 +949,13 @@ if ($child = fork()) { is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". "CoA-Request"); is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the ". - "vendor attribute '14988:Mikrotik-Rate-Limit' is returned"); + "vendor attribute '14988:Mikrotik-Rate-Limit' is returned on the success side of the FUP check"); + + testDBResults("Check FUP state was added to the user stats table as 1",'users_data', + {'UserID' => $user6_ID, 'Name' => "FUPState"}, + {'Value' => 1}, + 1, # Disable order + ); # @@ -1084,7 +1096,7 @@ sub testDBDelete # Test DB select results sub testDBResults { - my ($name,$table,$where,$resultCheck) = @_; + my ($name,$table,$where,$resultCheck,$disableOrder) = @_; # Build column list @@ -1101,20 +1113,27 @@ sub testDBResults } my $whereLines_str = join(',',@whereLines); - # Do select - my $sth = DBSelect(" + # Check if we're not disabling ordering + my $extraSQL = ""; + if (!defined($disableOrder)) { + $extraSQL = "ORDER BY ID DESC"; + } + + my $sqlQuery = " SELECT $columnList_str FROM $table WHERE $whereLines_str - ORDER BY - ID DESC - ",@whereData); + $extraSQL + "; + + # Do select + my $sth = DBSelect($sqlQuery,@whereData); # Make sure we got no error - is(AWITPT::DB::DBLayer::error(),"","Errors on DBSelect: $name"); + is(AWITPT::DB::DBLayer::error(),"","Errors on DBSelect ($sqlQuery interpolated with ".join(', ',$extraSQL).": $name"); # We should get one result... my $row = hashifyLCtoMC($sth->fetchrow_hashref(),keys %{$resultCheck}); -- GitLab From c203ca1d411d04f82a5b8858eb7a66dbc806e031 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 13:17:57 +0000 Subject: [PATCH 31/33] Fixed DB test where statement creation --- t/200-dbtests.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 264e303..25a7b39 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -1111,7 +1111,7 @@ sub testDBResults # Add data for template placeholders push(@whereData,$where->{$columnName}); } - my $whereLines_str = join(',',@whereLines); + my $whereLines_str = join(' AND ',@whereLines); # Check if we're not disabling ordering my $extraSQL = ""; -- GitLab From 39c09cec1c962e0a4164a7ffd841b9f09b967f71 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 13:20:20 +0000 Subject: [PATCH 32/33] Remove variable dump --- t/200-dbtests.t | 1 - 1 file changed, 1 deletion(-) diff --git a/t/200-dbtests.t b/t/200-dbtests.t index 25a7b39..b07ecec 100644 --- a/t/200-dbtests.t +++ b/t/200-dbtests.t @@ -871,7 +871,6 @@ if ($child = fork()) { 'Service-Type=Framed-User', ); is(ref($res),"HASH","smradclient should return a HASH"); - warn "DUMP: ".Dumper($res); is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". "CoA-Request"); is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1k/1m","Check that the vendor attribute". -- GitLab From f24d760c0ca17c78ae27a5ed20cbec88bb2409a9 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Thu, 16 May 2019 13:25:03 +0000 Subject: [PATCH 33/33] Stats should come after FUP so we can pickup FUP state --- lib/smradius/modules/features/mod_feature_user_stats.pm | 3 ++- smradiusd.conf | 4 ++-- t/200-dbtests.t | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/smradius/modules/features/mod_feature_user_stats.pm b/lib/smradius/modules/features/mod_feature_user_stats.pm index 111902f..6fab000 100644 --- a/lib/smradius/modules/features/mod_feature_user_stats.pm +++ b/lib/smradius/modules/features/mod_feature_user_stats.pm @@ -149,6 +149,7 @@ sub updateUserStats } # Set user FUP state + # NK: Perhaps this should be moved to the mod_feature_fup module? my $fupState = $user->{'AttributeConditionalVariables'}->{"SMRadius_FUP"}; if (defined($fupState)) { $fupState = $fupState->[0]; @@ -156,7 +157,7 @@ sub updateUserStats $fupState = "-1"; } $res = $user->{'_UserDB'}->{'Users_data_set'}($server,$user, - 'mod_feature_user_stats','FUPState', + 'mod_feature_fup','State', $fupState ); if (!defined($res)) { diff --git a/smradiusd.conf b/smradiusd.conf index 8a48538..b54a5a2 100644 --- a/smradiusd.conf +++ b/smradiusd.conf @@ -160,10 +160,10 @@ EOT [features] modules=< $user5_ID, 'Name' => "FUPState"}, - {'Value' => 0}, + {'UserID' => $user5_ID, 'Name' => "mod_feature_fup/State"}, + {'Value' => "0"}, 1, # Disable order ); @@ -951,8 +951,8 @@ if ($child = fork()) { "vendor attribute '14988:Mikrotik-Rate-Limit' is returned on the success side of the FUP check"); testDBResults("Check FUP state was added to the user stats table as 1",'users_data', - {'UserID' => $user6_ID, 'Name' => "FUPState"}, - {'Value' => 1}, + {'UserID' => $user6_ID, 'Name' => "mod_feature_fup/State"}, + {'Value' => "1"}, 1, # Disable order ); -- GitLab