feat: implement PrefixService for cached prefix lookup and enhance ASN list handling across modules
This commit is contained in:
parent
a579f691f8
commit
8077ae14c0
7 changed files with 154 additions and 44 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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; } = "";
|
||||
|
||||
|
|
|
|||
44
BGPLite.Providers/PrefixService.cs
Normal file
44
BGPLite.Providers/PrefixService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue