120 lines
4.1 KiB
C#
120 lines
4.1 KiB
C#
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
using radiuscontroller.Data;
|
||
|
|
using radiuscontroller.Models;
|
||
|
|
|
||
|
|
namespace radiuscontroller.Services;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Background service that periodically checks all active guest accounts
|
||
|
|
/// and disables them if their time has expired or data cap has been reached.
|
||
|
|
/// Time starts from the moment the user was created (guest_profiles.created_at).
|
||
|
|
/// </summary>
|
||
|
|
public class AccountEnforcementService : BackgroundService
|
||
|
|
{
|
||
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
||
|
|
private readonly ILogger<AccountEnforcementService> _logger;
|
||
|
|
private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(30);
|
||
|
|
|
||
|
|
public AccountEnforcementService(IServiceScopeFactory scopeFactory, ILogger<AccountEnforcementService> logger)
|
||
|
|
{
|
||
|
|
_scopeFactory = scopeFactory;
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
|
|
{
|
||
|
|
_logger.LogInformation("AccountEnforcementService started. Checking every {Interval}s.", CheckInterval.TotalSeconds);
|
||
|
|
|
||
|
|
// Wait a bit for the app to fully start
|
||
|
|
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||
|
|
|
||
|
|
while (!stoppingToken.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await EnforceAccountLimitsAsync(stoppingToken);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error during account enforcement check.");
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.Delay(CheckInterval, stoppingToken);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task EnforceAccountLimitsAsync(CancellationToken ct)
|
||
|
|
{
|
||
|
|
using var scope = _scopeFactory.CreateScope();
|
||
|
|
var db = scope.ServiceProvider.GetRequiredService<RadiusDbContext>();
|
||
|
|
|
||
|
|
var activeProfiles = await db.GuestProfiles
|
||
|
|
.Where(p => p.Status == "Active")
|
||
|
|
.ToListAsync(ct);
|
||
|
|
|
||
|
|
if (activeProfiles.Count == 0) return;
|
||
|
|
|
||
|
|
var now = DateTime.UtcNow;
|
||
|
|
var disabledCount = 0;
|
||
|
|
|
||
|
|
foreach (var profile in activeProfiles)
|
||
|
|
{
|
||
|
|
string? reason = null;
|
||
|
|
|
||
|
|
// 1. Check time expiration: created_at + session_time_minutes
|
||
|
|
var expiresAt = profile.CreatedAt.AddMinutes(profile.SessionTimeMinutes);
|
||
|
|
if (now >= expiresAt)
|
||
|
|
{
|
||
|
|
reason = "Time expired";
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Check data cap: sum acctinputoctets + acctoutputoctets from radacct
|
||
|
|
if (reason == null && profile.DataLimitMb > 0)
|
||
|
|
{
|
||
|
|
var totalOctets = await db.RadAcct
|
||
|
|
.Where(ra => ra.Username == profile.Username)
|
||
|
|
.SumAsync(ra => (ra.AcctInputOctets ?? 0) + (ra.AcctOutputOctets ?? 0), ct);
|
||
|
|
|
||
|
|
long dataLimitBytes = profile.DataLimitMb * 1024 * 1024;
|
||
|
|
if (totalOctets >= dataLimitBytes)
|
||
|
|
{
|
||
|
|
reason = $"Data cap reached ({totalOctets / (1024 * 1024)} MB / {profile.DataLimitMb} MB)";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (reason != null)
|
||
|
|
{
|
||
|
|
// Disable the account
|
||
|
|
profile.Status = "Expired";
|
||
|
|
|
||
|
|
// Remove Cleartext-Password from radcheck so FreeRADIUS rejects future auth
|
||
|
|
var checks = await db.RadCheck
|
||
|
|
.Where(rc => rc.Username == profile.Username)
|
||
|
|
.ToListAsync(ct);
|
||
|
|
db.RadCheck.RemoveRange(checks);
|
||
|
|
|
||
|
|
// Insert Auth-Type := Reject to explicitly deny
|
||
|
|
db.RadCheck.Add(new RadCheck
|
||
|
|
{
|
||
|
|
Username = profile.Username,
|
||
|
|
Attribute = "Auth-Type",
|
||
|
|
Op = ":=",
|
||
|
|
Value = "Reject"
|
||
|
|
});
|
||
|
|
|
||
|
|
_logger.LogInformation(
|
||
|
|
"Account '{Username}' disabled. Reason: {Reason}",
|
||
|
|
profile.Username, reason);
|
||
|
|
|
||
|
|
disabledCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (disabledCount > 0)
|
||
|
|
{
|
||
|
|
await db.SaveChangesAsync(ct);
|
||
|
|
_logger.LogInformation("Enforcement check complete. {Count} account(s) disabled.", disabledCount);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|