feat: implement PrefixService for cached prefix lookup and enhance ASN list handling across modules

This commit is contained in:
Mikhail Movchan 2026-06-09 19:18:46 +03:00
parent a579f691f8
commit 8077ae14c0
7 changed files with 154 additions and 44 deletions

View file

@ -16,6 +16,9 @@ public class BgpDbContext : DbContext
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
Database.EnsureCreated();
Peers.Where(p => p.Status == "active").ExecuteUpdate(
s => s.SetProperty(p => p.Status, "inactive"));
}
protected override void OnConfiguring(DbContextOptionsBuilder options)

View file

@ -3,6 +3,7 @@ using System.Text;
using System.Text.Json;
using BGPLite.Api.Entities;
using BGPLite.Configuration;
using BGPLite.Providers;
using BGPLite.Routing;
using BGPLite.Server;
using Microsoft.Extensions.Hosting;
@ -16,6 +17,7 @@ public sealed class ManagementApi : IHostedService, IDisposable
private readonly RouteTable _routeTable;
private readonly AppConfig _config;
private readonly BgpMetrics _metrics;
private readonly PrefixService? _prefixService;
private readonly ILogger<ManagementApi> _logger;
private HttpListener? _listener;
private Task? _listenTask;
@ -26,12 +28,14 @@ public sealed class ManagementApi : IHostedService, IDisposable
RouteTable routeTable,
AppConfig config,
BgpMetrics metrics,
ILogger<ManagementApi> logger)
ILogger<ManagementApi> logger,
PrefixService? prefixService = null)
{
_store = store;
_routeTable = routeTable;
_config = config;
_metrics = metrics;
_prefixService = prefixService;
_logger = logger;
}
@ -89,7 +93,7 @@ public sealed class ManagementApi : IHostedService, IDisposable
response = ApiResponse.Ok(new { ip = clientIp });
}
else if (method == "GET" && path == "/api/asn-lists")
response = HandleGetAsnLists();
response = await HandleGetAsnListsAsync();
else if (method == "GET" && path == "/api/sessions")
response = HandleGetSessions();
else if (method == "POST" && path == "/api/peers")
@ -117,17 +121,35 @@ public sealed class ManagementApi : IHostedService, IDisposable
}
}
private ApiResponse HandleGetAsnLists()
private async Task<ApiResponse> HandleGetAsnListsAsync()
{
var lists = _config.RipeStat?.AsnLists ?? [];
return ApiResponse.Ok(lists.Select(l => new
var result = new List<object>();
foreach (var l in lists)
{
l.Name,
l.Description,
l.Country,
asns = l.Asns,
type = l.Country is not null ? "country" : "asn"
}));
int prefixCount = 0;
if (_prefixService is not null)
{
foreach (var asn in l.Asns)
{
try { prefixCount += await _prefixService.GetPrefixCountAsync(asn); }
catch { }
}
}
result.Add(new
{
id = l.Id,
l.Name,
l.Description,
l.Country,
prefixCount,
type = l.Country is not null ? "country" : "asn"
});
}
return ApiResponse.Ok(result);
}
private ApiResponse HandleGetSessions()

View file

@ -10,6 +10,8 @@ public sealed class RipeStatConfig
public sealed class AsnList
{
public string Id { get; init; } = Guid.NewGuid().ToString();
[YamlMember(Alias = "Name")]
public string Name { get; init; } = "";

View file

@ -0,0 +1,44 @@
using System.Collections.Concurrent;
namespace BGPLite.Providers;
public sealed class PrefixService
{
private readonly RipeStatProvider _ripeStat;
private readonly ConcurrentDictionary<uint, (IReadOnlyList<(uint Prefix, byte Length)> Data, DateTime CachedAt)> _cache = new();
private readonly TimeSpan _cacheTtl;
public PrefixService(RipeStatProvider ripeStat, TimeSpan? cacheTtl = null)
{
_ripeStat = ripeStat;
_cacheTtl = cacheTtl ?? TimeSpan.FromHours(1);
}
public async Task<IReadOnlyList<(uint Prefix, byte Length)>> GetPrefixesAsync(uint asn)
{
if (_cache.TryGetValue(asn, out var cached) && DateTime.UtcNow - cached.CachedAt < _cacheTtl)
return cached.Data;
var prefixes = await _ripeStat.GetPrefixesAsync(asn);
_cache[asn] = (prefixes, DateTime.UtcNow);
return prefixes;
}
public async Task<List<(uint Prefix, byte Length, uint Asn)>> GetPrefixesForAsns(IEnumerable<uint> asns)
{
var result = new List<(uint Prefix, byte Length, uint Asn)>();
foreach (var asn in asns)
{
var prefixes = await GetPrefixesAsync(asn);
foreach (var (prefix, length) in prefixes)
result.Add((prefix, length, asn));
}
return result;
}
public async Task<int> GetPrefixCountAsync(uint asn)
{
var prefixes = await GetPrefixesAsync(asn);
return prefixes.Count;
}
}

View file

@ -21,7 +21,7 @@ public sealed class BgpServer : IHostedService, IDisposable
private readonly ILogger<BgpServer> _logger;
private readonly Action<string, uint>? _onPeerIdentified;
private readonly PeerStore? _peerStore;
private readonly RipeStatProvider? _ripeStatProvider;
private readonly PrefixService? _prefixService;
private readonly ConcurrentDictionary<string, BgpSession> _sessions = new();
private readonly CancellationTokenSource _cts = new();
private Socket? _listener;
@ -41,7 +41,7 @@ public sealed class BgpServer : IHostedService, IDisposable
ILogger<BgpServer> logger,
Action<string, uint>? onPeerIdentified = null,
PeerStore? peerStore = null,
RipeStatProvider? ripeStatProvider = null)
PrefixService? prefixService = null)
{
_config = config;
_routeTable = routeTable;
@ -51,7 +51,7 @@ public sealed class BgpServer : IHostedService, IDisposable
_logger = logger;
_onPeerIdentified = onPeerIdentified;
_peerStore = peerStore;
_ripeStatProvider = ripeStatProvider;
_prefixService = prefixService;
}
public Task StartAsync(CancellationToken cancellationToken)
@ -119,7 +119,7 @@ public sealed class BgpServer : IHostedService, IDisposable
socket, peerConfig, _config.Bgp, _routeTable,
_routeFilter, _metrics, _sessionLogger,
_onPeerIdentified,
_peerStore, _ripeStatProvider, _config);
_peerStore, _prefixService, _config);
_sessions[peerAddress] = session;

View file

@ -23,7 +23,7 @@ public sealed class BgpSession : IDisposable
private readonly CancellationTokenSource _cts = new();
private readonly Action<string, uint>? _onPeerIdentified;
private readonly PeerStore? _peerStore;
private readonly RipeStatProvider? _ripeStatProvider;
private readonly PrefixService? _prefixService;
private readonly AppConfig? _appConfig;
private BgpFsmState _state = BgpFsmState.Idle;
@ -47,7 +47,7 @@ public sealed class BgpSession : IDisposable
ILogger<BgpSession> logger,
Action<string, uint>? onPeerIdentified = null,
PeerStore? peerStore = null,
RipeStatProvider? ripeStatProvider = null,
PrefixService? prefixService = null,
AppConfig? appConfig = null)
{
_socket = socket;
@ -60,7 +60,7 @@ public sealed class BgpSession : IDisposable
_logger = logger;
_onPeerIdentified = onPeerIdentified;
_peerStore = peerStore;
_ripeStatProvider = ripeStatProvider;
_prefixService = prefixService;
_appConfig = appConfig;
}
@ -270,8 +270,7 @@ public sealed class BgpSession : IDisposable
var nextHop = BgpConstants.IPAddressToUint(_bgpConfig.GetRouterIdAddress());
var routes = new List<Route>();
// Try dynamic route fetching from peer subscriptions
if (_peerStore is not null && _ripeStatProvider is not null && _appConfig is not null)
if (_peerStore is not null && _prefixService is not null && _appConfig is not null)
{
var peer = _peerStore.GetPeerByIp(_peerConfig.Address);
if (peer is not null)
@ -279,35 +278,32 @@ public sealed class BgpSession : IDisposable
_peerStore.UpdateSessionStatus(_peerConfig.Address, true);
var subscriptionNames = _peerStore.GetSubscriptions(peer.Id);
var asnLists = _appConfig.RipeStat?.AsnLists
var asns = _appConfig.RipeStat?.AsnLists
.Where(l => subscriptionNames.Contains(l.Name))
.SelectMany(l => l.Asns)
.ToList() ?? [];
// Fetch prefixes for each subscribed AS list
foreach (var asnList in asnLists)
if (asns.Count > 0)
{
foreach (var asn in asnList.Asns)
try
{
try
var prefixes = await _prefixService.GetPrefixesForAsns(asns);
foreach (var (prefix, length, asn) in prefixes)
{
var prefixes = await _ripeStatProvider.GetPrefixesAsync(asn);
foreach (var (prefix, length) in prefixes)
routes.Add(new Route
{
routes.Add(new Route
{
Prefix = prefix,
PrefixLength = length,
NextHop = nextHop,
AsPath = [asn]
});
}
_logger.LogInformation("Fetched {Count} prefixes for AS{Asn} ({List}) for {Peer}",
prefixes.Count, asn, asnList.Name, _peerConfig.Address);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch prefixes for AS{Asn} for {Peer}", asn, _peerConfig.Address);
Prefix = prefix,
PrefixLength = length,
NextHop = nextHop,
AsPath = [asn]
});
}
_logger.LogInformation("Fetched {Count} prefixes for {Peer} from subscriptions",
routes.Count, _peerConfig.Address);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch prefixes for {Peer}", _peerConfig.Address);
}
}
@ -327,6 +323,46 @@ public sealed class BgpSession : IDisposable
});
}
if (routes.Count > 0)
{
await SendRoutesAsync(nextHop, routes);
return;
}
}
else
{
// Unknown peer — auto-register and send default RU list
_logger.LogInformation("Unknown peer {Ip}, auto-registering with RU defaults", _peerConfig.Address);
_peerStore.CreatePeer(_peerConfig.Address, _remoteAsn, null);
var ruAsns = _appConfig.RipeStat?.AsnLists
.Where(l => l.Country == "RU")
.SelectMany(l => l.Asns)
.ToList();
if (ruAsns is { Count: > 0 })
{
try
{
var prefixes = await _prefixService.GetPrefixesForAsns(ruAsns);
foreach (var (prefix, length, asn) in prefixes)
{
routes.Add(new Route
{
Prefix = prefix,
PrefixLength = length,
NextHop = nextHop,
AsPath = [asn]
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch RU prefixes for {Peer}", _peerConfig.Address);
}
}
if (routes.Count > 0)
{
await SendRoutesAsync(nextHop, routes);
@ -335,7 +371,7 @@ public sealed class BgpSession : IDisposable
}
}
// Fallback: send from shared route table
// Final fallback: send from shared route table
var tableRoutes = _routeTable.GetAll();
if (tableRoutes.Count == 0) return;

View file

@ -71,13 +71,16 @@ builder.Services.AddSingleton(new BgpMetrics());
// RIPE Stat prefix provider
if (config.RipeStat is { AsnLists.Count: > 0 })
{
builder.Services.AddHttpClient<RipeStatProvider>(c => c.Timeout = TimeSpan.FromSeconds(30));
builder.Services.AddSingleton<PrefixService>();
}
builder.Services.AddHostedService(sp =>
{
var store = sp.GetRequiredService<PeerStore>();
RipeStatProvider? ripe = null;
try { ripe = sp.GetRequiredService<RipeStatProvider>(); } catch { }
PrefixService? prefixService = null;
try { prefixService = sp.GetRequiredService<PrefixService>(); } catch { }
return new BgpServer(
sp.GetRequiredService<AppConfig>(),
@ -88,7 +91,7 @@ builder.Services.AddHostedService(sp =>
sp.GetRequiredService<ILogger<BgpServer>>(),
(ip, asn) => store.UpsertPeer(ip, asn),
store,
ripe);
prefixService);
});
builder.Services.AddHostedService<ManagementApi>();