Skip to content

Commit 8077ae1

Browse files
committed
feat: implement PrefixService for cached prefix lookup and enhance ASN list handling across modules
1 parent a579f69 commit 8077ae1

7 files changed

Lines changed: 154 additions & 44 deletions

File tree

BGPLite.Api/BgpDbContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public BgpDbContext(string dbPath)
1616
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
1717
Directory.CreateDirectory(dir);
1818
Database.EnsureCreated();
19+
20+
Peers.Where(p => p.Status == "active").ExecuteUpdate(
21+
s => s.SetProperty(p => p.Status, "inactive"));
1922
}
2023

2124
protected override void OnConfiguring(DbContextOptionsBuilder options)

BGPLite.Api/ManagementApi.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Text.Json;
44
using BGPLite.Api.Entities;
55
using BGPLite.Configuration;
6+
using BGPLite.Providers;
67
using BGPLite.Routing;
78
using BGPLite.Server;
89
using Microsoft.Extensions.Hosting;
@@ -16,6 +17,7 @@ public sealed class ManagementApi : IHostedService, IDisposable
1617
private readonly RouteTable _routeTable;
1718
private readonly AppConfig _config;
1819
private readonly BgpMetrics _metrics;
20+
private readonly PrefixService? _prefixService;
1921
private readonly ILogger<ManagementApi> _logger;
2022
private HttpListener? _listener;
2123
private Task? _listenTask;
@@ -26,12 +28,14 @@ public ManagementApi(
2628
RouteTable routeTable,
2729
AppConfig config,
2830
BgpMetrics metrics,
29-
ILogger<ManagementApi> logger)
31+
ILogger<ManagementApi> logger,
32+
PrefixService? prefixService = null)
3033
{
3134
_store = store;
3235
_routeTable = routeTable;
3336
_config = config;
3437
_metrics = metrics;
38+
_prefixService = prefixService;
3539
_logger = logger;
3640
}
3741

@@ -89,7 +93,7 @@ private async Task HandleAsync(HttpListenerContext ctx)
8993
response = ApiResponse.Ok(new { ip = clientIp });
9094
}
9195
else if (method == "GET" && path == "/api/asn-lists")
92-
response = HandleGetAsnLists();
96+
response = await HandleGetAsnListsAsync();
9397
else if (method == "GET" && path == "/api/sessions")
9498
response = HandleGetSessions();
9599
else if (method == "POST" && path == "/api/peers")
@@ -117,17 +121,35 @@ private async Task HandleAsync(HttpListenerContext ctx)
117121
}
118122
}
119123

120-
private ApiResponse HandleGetAsnLists()
124+
private async Task<ApiResponse> HandleGetAsnListsAsync()
121125
{
122126
var lists = _config.RipeStat?.AsnLists ?? [];
123-
return ApiResponse.Ok(lists.Select(l => new
127+
var result = new List<object>();
128+
129+
foreach (var l in lists)
124130
{
125-
l.Name,
126-
l.Description,
127-
l.Country,
128-
asns = l.Asns,
129-
type = l.Country is not null ? "country" : "asn"
130-
}));
131+
int prefixCount = 0;
132+
if (_prefixService is not null)
133+
{
134+
foreach (var asn in l.Asns)
135+
{
136+
try { prefixCount += await _prefixService.GetPrefixCountAsync(asn); }
137+
catch { }
138+
}
139+
}
140+
141+
result.Add(new
142+
{
143+
id = l.Id,
144+
l.Name,
145+
l.Description,
146+
l.Country,
147+
prefixCount,
148+
type = l.Country is not null ? "country" : "asn"
149+
});
150+
}
151+
152+
return ApiResponse.Ok(result);
131153
}
132154

133155
private ApiResponse HandleGetSessions()

BGPLite.Configuration/RipeStatConfig.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public sealed class RipeStatConfig
1010

1111
public sealed class AsnList
1212
{
13+
public string Id { get; init; } = Guid.NewGuid().ToString();
14+
1315
[YamlMember(Alias = "Name")]
1416
public string Name { get; init; } = "";
1517

BGPLite.Providers/PrefixService.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace BGPLite.Providers;
4+
5+
public sealed class PrefixService
6+
{
7+
private readonly RipeStatProvider _ripeStat;
8+
private readonly ConcurrentDictionary<uint, (IReadOnlyList<(uint Prefix, byte Length)> Data, DateTime CachedAt)> _cache = new();
9+
private readonly TimeSpan _cacheTtl;
10+
11+
public PrefixService(RipeStatProvider ripeStat, TimeSpan? cacheTtl = null)
12+
{
13+
_ripeStat = ripeStat;
14+
_cacheTtl = cacheTtl ?? TimeSpan.FromHours(1);
15+
}
16+
17+
public async Task<IReadOnlyList<(uint Prefix, byte Length)>> GetPrefixesAsync(uint asn)
18+
{
19+
if (_cache.TryGetValue(asn, out var cached) && DateTime.UtcNow - cached.CachedAt < _cacheTtl)
20+
return cached.Data;
21+
22+
var prefixes = await _ripeStat.GetPrefixesAsync(asn);
23+
_cache[asn] = (prefixes, DateTime.UtcNow);
24+
return prefixes;
25+
}
26+
27+
public async Task<List<(uint Prefix, byte Length, uint Asn)>> GetPrefixesForAsns(IEnumerable<uint> asns)
28+
{
29+
var result = new List<(uint Prefix, byte Length, uint Asn)>();
30+
foreach (var asn in asns)
31+
{
32+
var prefixes = await GetPrefixesAsync(asn);
33+
foreach (var (prefix, length) in prefixes)
34+
result.Add((prefix, length, asn));
35+
}
36+
return result;
37+
}
38+
39+
public async Task<int> GetPrefixCountAsync(uint asn)
40+
{
41+
var prefixes = await GetPrefixesAsync(asn);
42+
return prefixes.Count;
43+
}
44+
}

BGPLite.Server/BgpServer.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class BgpServer : IHostedService, IDisposable
2121
private readonly ILogger<BgpServer> _logger;
2222
private readonly Action<string, uint>? _onPeerIdentified;
2323
private readonly PeerStore? _peerStore;
24-
private readonly RipeStatProvider? _ripeStatProvider;
24+
private readonly PrefixService? _prefixService;
2525
private readonly ConcurrentDictionary<string, BgpSession> _sessions = new();
2626
private readonly CancellationTokenSource _cts = new();
2727
private Socket? _listener;
@@ -41,7 +41,7 @@ public BgpServer(
4141
ILogger<BgpServer> logger,
4242
Action<string, uint>? onPeerIdentified = null,
4343
PeerStore? peerStore = null,
44-
RipeStatProvider? ripeStatProvider = null)
44+
PrefixService? prefixService = null)
4545
{
4646
_config = config;
4747
_routeTable = routeTable;
@@ -51,7 +51,7 @@ public BgpServer(
5151
_logger = logger;
5252
_onPeerIdentified = onPeerIdentified;
5353
_peerStore = peerStore;
54-
_ripeStatProvider = ripeStatProvider;
54+
_prefixService = prefixService;
5555
}
5656

5757
public Task StartAsync(CancellationToken cancellationToken)
@@ -119,7 +119,7 @@ private async Task AcceptLoopAsync(CancellationToken cancellationToken)
119119
socket, peerConfig, _config.Bgp, _routeTable,
120120
_routeFilter, _metrics, _sessionLogger,
121121
_onPeerIdentified,
122-
_peerStore, _ripeStatProvider, _config);
122+
_peerStore, _prefixService, _config);
123123

124124
_sessions[peerAddress] = session;
125125

BGPLite.Server/BgpSession.cs

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public sealed class BgpSession : IDisposable
2323
private readonly CancellationTokenSource _cts = new();
2424
private readonly Action<string, uint>? _onPeerIdentified;
2525
private readonly PeerStore? _peerStore;
26-
private readonly RipeStatProvider? _ripeStatProvider;
26+
private readonly PrefixService? _prefixService;
2727
private readonly AppConfig? _appConfig;
2828

2929
private BgpFsmState _state = BgpFsmState.Idle;
@@ -47,7 +47,7 @@ public BgpSession(
4747
ILogger<BgpSession> logger,
4848
Action<string, uint>? onPeerIdentified = null,
4949
PeerStore? peerStore = null,
50-
RipeStatProvider? ripeStatProvider = null,
50+
PrefixService? prefixService = null,
5151
AppConfig? appConfig = null)
5252
{
5353
_socket = socket;
@@ -60,7 +60,7 @@ public BgpSession(
6060
_logger = logger;
6161
_onPeerIdentified = onPeerIdentified;
6262
_peerStore = peerStore;
63-
_ripeStatProvider = ripeStatProvider;
63+
_prefixService = prefixService;
6464
_appConfig = appConfig;
6565
}
6666

@@ -270,44 +270,40 @@ private async Task SendAllRoutesAsync()
270270
var nextHop = BgpConstants.IPAddressToUint(_bgpConfig.GetRouterIdAddress());
271271
var routes = new List<Route>();
272272

273-
// Try dynamic route fetching from peer subscriptions
274-
if (_peerStore is not null && _ripeStatProvider is not null && _appConfig is not null)
273+
if (_peerStore is not null && _prefixService is not null && _appConfig is not null)
275274
{
276275
var peer = _peerStore.GetPeerByIp(_peerConfig.Address);
277276
if (peer is not null)
278277
{
279278
_peerStore.UpdateSessionStatus(_peerConfig.Address, true);
280279

281280
var subscriptionNames = _peerStore.GetSubscriptions(peer.Id);
282-
var asnLists = _appConfig.RipeStat?.AsnLists
281+
var asns = _appConfig.RipeStat?.AsnLists
283282
.Where(l => subscriptionNames.Contains(l.Name))
283+
.SelectMany(l => l.Asns)
284284
.ToList() ?? [];
285285

286-
// Fetch prefixes for each subscribed AS list
287-
foreach (var asnList in asnLists)
286+
if (asns.Count > 0)
288287
{
289-
foreach (var asn in asnList.Asns)
288+
try
290289
{
291-
try
290+
var prefixes = await _prefixService.GetPrefixesForAsns(asns);
291+
foreach (var (prefix, length, asn) in prefixes)
292292
{
293-
var prefixes = await _ripeStatProvider.GetPrefixesAsync(asn);
294-
foreach (var (prefix, length) in prefixes)
293+
routes.Add(new Route
295294
{
296-
routes.Add(new Route
297-
{
298-
Prefix = prefix,
299-
PrefixLength = length,
300-
NextHop = nextHop,
301-
AsPath = [asn]
302-
});
303-
}
304-
_logger.LogInformation("Fetched {Count} prefixes for AS{Asn} ({List}) for {Peer}",
305-
prefixes.Count, asn, asnList.Name, _peerConfig.Address);
306-
}
307-
catch (Exception ex)
308-
{
309-
_logger.LogError(ex, "Failed to fetch prefixes for AS{Asn} for {Peer}", asn, _peerConfig.Address);
295+
Prefix = prefix,
296+
PrefixLength = length,
297+
NextHop = nextHop,
298+
AsPath = [asn]
299+
});
310300
}
301+
_logger.LogInformation("Fetched {Count} prefixes for {Peer} from subscriptions",
302+
routes.Count, _peerConfig.Address);
303+
}
304+
catch (Exception ex)
305+
{
306+
_logger.LogError(ex, "Failed to fetch prefixes for {Peer}", _peerConfig.Address);
311307
}
312308
}
313309

@@ -327,6 +323,46 @@ private async Task SendAllRoutesAsync()
327323
});
328324
}
329325

326+
if (routes.Count > 0)
327+
{
328+
await SendRoutesAsync(nextHop, routes);
329+
return;
330+
}
331+
}
332+
else
333+
{
334+
// Unknown peer — auto-register and send default RU list
335+
_logger.LogInformation("Unknown peer {Ip}, auto-registering with RU defaults", _peerConfig.Address);
336+
337+
_peerStore.CreatePeer(_peerConfig.Address, _remoteAsn, null);
338+
339+
var ruAsns = _appConfig.RipeStat?.AsnLists
340+
.Where(l => l.Country == "RU")
341+
.SelectMany(l => l.Asns)
342+
.ToList();
343+
344+
if (ruAsns is { Count: > 0 })
345+
{
346+
try
347+
{
348+
var prefixes = await _prefixService.GetPrefixesForAsns(ruAsns);
349+
foreach (var (prefix, length, asn) in prefixes)
350+
{
351+
routes.Add(new Route
352+
{
353+
Prefix = prefix,
354+
PrefixLength = length,
355+
NextHop = nextHop,
356+
AsPath = [asn]
357+
});
358+
}
359+
}
360+
catch (Exception ex)
361+
{
362+
_logger.LogError(ex, "Failed to fetch RU prefixes for {Peer}", _peerConfig.Address);
363+
}
364+
}
365+
330366
if (routes.Count > 0)
331367
{
332368
await SendRoutesAsync(nextHop, routes);
@@ -335,7 +371,7 @@ private async Task SendAllRoutesAsync()
335371
}
336372
}
337373

338-
// Fallback: send from shared route table
374+
// Final fallback: send from shared route table
339375
var tableRoutes = _routeTable.GetAll();
340376
if (tableRoutes.Count == 0) return;
341377

BGPLite/Program.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,16 @@
7171

7272
// RIPE Stat prefix provider
7373
if (config.RipeStat is { AsnLists.Count: > 0 })
74+
{
7475
builder.Services.AddHttpClient<RipeStatProvider>(c => c.Timeout = TimeSpan.FromSeconds(30));
76+
builder.Services.AddSingleton<PrefixService>();
77+
}
7578

7679
builder.Services.AddHostedService(sp =>
7780
{
7881
var store = sp.GetRequiredService<PeerStore>();
79-
RipeStatProvider? ripe = null;
80-
try { ripe = sp.GetRequiredService<RipeStatProvider>(); } catch { }
82+
PrefixService? prefixService = null;
83+
try { prefixService = sp.GetRequiredService<PrefixService>(); } catch { }
8184

8285
return new BgpServer(
8386
sp.GetRequiredService<AppConfig>(),
@@ -88,7 +91,7 @@
8891
sp.GetRequiredService<ILogger<BgpServer>>(),
8992
(ip, asn) => store.UpsertPeer(ip, asn),
9093
store,
91-
ripe);
94+
prefixService);
9295
});
9396
builder.Services.AddHostedService<ManagementApi>();
9497

0 commit comments

Comments
 (0)