#!/usr/bin/perl -w # Author: Nigel Kukard <nkukard@lbsd.net> # Date: 12/04/2007 # Desc: Authentication filter for GNU Radius # License: GPL use strict; use Benchmark; use Getopt::Long; use DateTime; use Time::HiRes qw( gettimeofday tv_interval ); # Set library directory use lib qw(../../); use sm::config; use sm::dblayer; require("common.pm"); my %optctl = (); GetOptions(\%optctl, "help"); # Check if user wants usage if (defined($optctl{'help'})) { displayUsage(); } # Open up logfile my $logfile = "/var/log/radius/auth-filter"; open(FH,">> $logfile") or die "Failed to open '$logfile': $!"; # Databases my $dbh; # Authentication my $dbh_log; # Logs # Get db handle $dbh = sm::dbilayer->new($cfg_db_DSN, $cfg_db_Username, $cfg_db_Password); if (!$dbh) { print(STDERR "Error creating database object: ".sm::dbilayer->internalErr()); exit 1; } # Connect to database if ($dbh->connect() != 0) { print(STDERR "Error connecting to database: ".$dbh->err); exit 1; } # Check if we must use split db's if (defined($cfg_radiuslog_db_DSN)) { # Get log db handle $dbh_log = sm::dbilayer->new($cfg_radiuslog_db_DSN, $cfg_radiuslog_db_Username, $cfg_radiuslog_db_Password); if (!$dbh_log) { print(STDERR "Error creating database object: ".sm::dbilayer->internalErr()); exit 1; } # Connect to database if ($dbh_log->connect() != 0) { print(STDERR "Error connecting to database: ".$dbh_log->err); exit 1; } # If not use the main DB } else { $dbh_log = $dbh; } # No buffering select((select(FH), $| = 1)[0]); select((select(STDOUT), $| = 1)[0]); # Loop with input while (my $line = <STDIN>) { my %request; my @request; my %accessAttribs; my %replyAttribs; # Munch off \n chomp($line); # Check number of results if ((@request = split /:/, $line) < 6) { print(FH "ERROR: Number of params from radiusd: ".(@request)."/$line\n"); print(STDOUT "1\n"); next; } # Pull in request ($request{'User-Name'},$request{'NAS-IP-Address'},$request{'NAS-Port-Type'},$request{'NAS-Port'},$request{'Connect-Info'}, $request{'Service-Type'}) = @request; my $dt = DateTime->from_epoch( epoch => time() ); $request{'Timestamp'} = $dt->strftime('%Y-%m-%d %H:%M:%S'); # If this is a auth mechanism that called us, allow if ($request{'NAS-Port-Type'} eq "0" && $request{'NAS-Port'} eq "0" && $request{'Service-Type'} eq "0") { print(STDOUT "0\n"); next; } my $timer0 = [gettimeofday]; # Grab user details my $userData = getUser($dbh,$request{'User-Name'}); if (ref $userData ne "HASH") { print(FH "ERROR: $userData\n"); print(STDOUT "1\n"); next; } printf(FH 'INFO: User-Name: %s, Timestamp: %s, NAS-IP-Address: %s, NAS-Port-Type: %s, NAS-Port: %s, Connect-Info: %s, Service-Type: %s, CappingType: %s, UsageCap: %s, AgentDisabled: %s'."\n", $request{'User-Name'}, $request{'Timestamp'}, $request{'NAS-IP-Address'}, $request{'NAS-Port-Type'}, $request{'NAS-Port'}, $request{'Connect-Info'}, $request{'Service-Type'}, $userData->{'CappingType'}, defined($userData->{'UsageCap'}) ? $userData->{'UsageCap'} : "uncapped", $userData->{'AgentDisabled'} ); # Check user active, else insert into auth fail if ($userData->{'AgentDisabled'} eq "1") { print(FH " - Agent disabled\n"); print(STDOUT "1 Reply-Message = \"Your account is currently deactivated. Please, contact your ISP.\"\n"); authFail(\%request,1); next; } # Pull in class attribs & check my $sth = $dbh->select(" SELECT Attr, OP, Value FROM radiusClassAttribs WHERE RadiusClassID = ".$dbh->quote($userData->{'RadiusClassID'})." AND OP IS NOT NULL "); if (!$sth) { print(FH "ERROR: Selecting class attributes: ".$dbh->err."\n"); print(STDOUT "1\n"); next; } # Loop with class attribs and push while (my $item = $sth->fetchrow_hashref()) { push(@{$accessAttribs{$item->{'Attr'}}{$item->{'OP'}}},$item->{'Value'}); } $sth->finish(); # Pull in user attribs & check $sth = $dbh->select(" SELECT Attr, OP, Value FROM radiusAttribs WHERE RadiusUserID = ".$dbh->quote($userData->{'ID'})." AND OP IS NOT NULL "); if (!$sth) { print(FH "ERROR: Selecting class attributes: ".$dbh->err."\n"); print(STDOUT "1\n"); next; } # Loop with class attribs and push while (my $item = $sth->fetchrow_hashref()) { push(@{$accessAttribs{$item->{'Attr'}}{$item->{'OP'}}},$item->{'Value'}); } $sth->finish(); # Loop with access attribs and push my $rejectAttrs = 0; foreach my $attr (keys %accessAttribs) { my $ok = 0; # Check if we missing something in the request if (!defined($request{$attr})) { printf(FH " - WARNING: Attribute '$attr' was in accessAttribs, but not request\n"); next; } # Loop with attrib op's and check them out foreach my $op (keys %{$accessAttribs{$attr}}) { printf(FH ' - Checking %s, request="%s", op="%s", attr="%s": ', $attr, $request{$attr}, $op, join(',',@{$accessAttribs{$attr}{$op}}) ); # Check value against operator foreach my $val (@{$accessAttribs{$attr}{$op}}) { # Equal if ($op eq "=") { if ($request{$attr} eq $val) { print(FH "matched '$val'\n"); $ok = 1; last; } } } # Check if we ok, if not continue if ($ok == 0) { print(FH "no match\n"); } else { last } } # Check if we ok, if not we've been violated if ($ok == 0) { print(FH " - Class attribute violation: '$attr'\n"); $rejectAttrs = 1; last; } } # Check if something didn't match up if ($rejectAttrs == 1) { print(STDOUT "1 Reply-Message = \"Connection attribute mismatch. Please, contact your ISP.\"\n"); authFail(\%request,5); next; } # Check user type vs. adsl & analogue/isdn # IF ADSL if ($request{'NAS-Port-Type'} eq "5") { # Calculate dates my $date = DateTime->from_epoch( epoch => time() ); my $today = $date->ymd(); $date->set_day(1); my $thismonth = $date->ymd(); $date->add( months => 1); my $nextmonth = $date->ymd(); # Extra query for selects my $extraQuery = ""; # Check port locking, else insert into auth fail $sth = $dbh->select(" SELECT NASPort FROM radiusPortLocks WHERE RadiusUserID = ".$dbh->quote($userData->{'ID'})." AND AgentDisabled = 0 "); if (!$sth) { print(FH "ERROR: Selecting NAS ports: ".$dbh->err."\n"); print(STDOUT "1\n"); next; } # Check rows if ($sth->rows > 0) { my $found = 0; # Loop with port locks while (my $portLock = $sth->fetchrow_hashref()) { # Check if we found port locking if ($request{'NAS-Port'} eq $portLock->{'NASPort'}) { $found = 1; last; } } # Check if we found it if ($found == 0) { print(FH " - Port locked\n"); print(STDOUT "1 Reply-Message = \"Connection from unauthorized port. Please, contact your ISP.\"\n"); authFail(\%request,4); $sth->finish(); next; } } $sth->finish(); # NULL - uncapped, no limits if (!defined($userData->{'UsageCap'})) { $extraQuery = "AND Timestamp > ".$dbh->quote($thismonth)." AND Timestamp < ".$dbh->quote($nextmonth); # > 0 - normal cap, check usage for this month, check topups for this month # Calculate cap user has (acctinputoctets + (2^32 * gigawords)) /1024 / 1024 # Calculate usage user has } elsif ($userData->{'UsageCap'} > 0) { $extraQuery = "AND Timestamp > ".$dbh->quote($thismonth)." AND Timestamp < ".$dbh->quote($nextmonth); # 0 - topup account } elsif ($userData->{'UsageCap'} == 0) { } # Grab users usage my $usageData = getUsage($dbh_log,$request{'User-Name'},$extraQuery); if (ref $usageData ne "HASH") { print(FH "ERROR: $usageData\n"); print(STDOUT "1\n"); next; } my $totalUsage = $usageData->{'Total'}; # If we a normal or topup, check topups if (defined($userData->{'UsageCap'}) && ($userData->{'UsageCap'} > 0 || $userData->{'UsageCap'} == 0)) { # Prepare $extraQuery = ""; # Only total up this month if ($userData->{'UsageCap'} > 0) { $extraQuery = "AND ValidFrom <= ".$dbh->quote($today)." AND ValidTo >= ".$dbh->quote($today); } # Get how much we've been topped up my $topupData = getTopups($dbh,$request{'User-Name'},$extraQuery); if (ref $topupData ne "HASH") { print(FH "ERROR: $topupData\n"); print(STDOUT "1\n"); next; } my $topupBw = $topupData->{'Total'}; # Check capping, else insert into auth fail print(FH " - Usage $totalUsage (Cap:".$userData->{'UsageCap'}."+Topup:$topupBw)\n"); # Check capping if ($userData->{'CappingType'} == 1 && ($userData->{'UsageCap'} + $topupBw) <= $totalUsage) { print(FH " - User capped\n"); print(STDOUT "1 Reply-Message = \"Your account is has been capped. Please, contact your ISP.\"\n"); authFail(\%request,2); next; } } # IF ANALOGUE } elsif ($request{'NAS-Port-Type'} eq "0") { # IF ISDN } elsif ($request{'NAS-Port-Type'} eq "2") { } else { print(FH "ERROR: Unknown NAS-Port-Type: ".$request{'NAS-Port-Type'}."\n"); } # Pull in class attribs $sth = $dbh->select(" SELECT Attr, Value FROM radiusClassAttribs WHERE RadiusClassID = ".$dbh->quote($userData->{'RadiusClassID'})." AND OP IS NULL "); if (!$sth) { print(FH "ERROR: Selecting class attributes: ".$dbh->err."\n"); print(STDOUT "1\n"); next; } # Loop with class attribs while (my $item = $sth->fetchrow_hashref()) { $replyAttribs{$item->{'Attr'}} = $item->{'Value'}; } $sth->finish(); # Pull in user attribs $sth = $dbh->select(" SELECT Attr, Value FROM radiusAttribs WHERE RadiusUserID = ".$dbh->quote($userData->{'ID'})." AND OP IS NULL "); if (!$sth) { print(FH "ERROR: Selecting user attributes: ".$dbh->err."\n"); print(STDOUT "1\n"); next; } # Loop with user attribs while (my $item = $sth->fetchrow_hashref()) { $replyAttribs{$item->{'Attr'}} = $item->{'Value'}; } $sth->finish(); # Build up our attrib pairs my @replyAttribs; foreach my $key (keys %replyAttribs) { push(@replyAttribs,sprintf('%s = %s',$key,$replyAttribs{$key})); } my $timer1 = [gettimeofday]; my $timediff = tv_interval($timer0,$timer1); printf(FH ' - Attributes => %s'."\n",join(", ",@replyAttribs)); print(FH "Code execution took: ${timediff}s\n"); # Reply with positive status plus , separated list of attribs printf(STDOUT '0 %s'."\n",join(", ",@replyAttribs)); } close(FH); # Log failed authentication sub authFail { my ($request,$reason) = @_; # Insert entry into auth fail table my $sth = $dbh_log->do(" INSERT INTO radiusAuthFail ( Username, Timestamp, NASIPAddress, NASPortType, NASPort, ConnectInfo, ServiceType, Reason ) VALUES ( ".$dbh->quote($request->{'User-Name'}).", ".$dbh->quote($request->{'Timestamp'}).", ".$dbh->quote($request->{'NAS-IP-Address'}).", ".$dbh->quote($request->{'NAS-Port-Type'}).", ".$dbh->quote($request->{'NAS-Port'}).", ".$dbh->quote($request->{'Connect-Info'}).", ".$dbh->quote($request->{'Service-Type'}).", ".$dbh->quote($request->{'Reason'})." ) "); if (!$sth) { print(FH "ERROR: Failed to insert radius auth fail data: ".$dbh_log->err."\n"); } } # Display usage sub displayUsage { print("Usage: $0 [--quiet]\n"); exit 0; } # vim: ts=4