feat: initial commit — BGPLite BGP route server
Lightweight BGP route server with TCP session management, route table, community-based filtering, management HTTP API, and SQLite peer store. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
9a6c0af00b
49 changed files with 3097 additions and 0 deletions
98
.gitignore
vendored
Normal file
98
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
## Build output
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
## NuGet
|
||||
packages/
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
.nuget/
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
## .NET
|
||||
*.dotCover
|
||||
*.dotTrace
|
||||
*.swp
|
||||
*~
|
||||
|
||||
## Visual Studio / Rider
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
riderModule.iml
|
||||
_ReSharper.Caches/
|
||||
*.DotSettings.user
|
||||
*.sln.DotSettings.user
|
||||
|
||||
## User-specific settings
|
||||
appsettings.yml
|
||||
appsettings.*.yml
|
||||
appsettings.*.json
|
||||
!appsettings.Example.yml
|
||||
|
||||
## Data files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
nets.txt
|
||||
|
||||
## Docker
|
||||
.docker/
|
||||
|
||||
## OS files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
## AI / LLM / Neural networks
|
||||
.claude/
|
||||
.aider*
|
||||
.copilot/
|
||||
.cursor/
|
||||
Continue/
|
||||
.codeium/
|
||||
.sourcegraph/
|
||||
.tabby/
|
||||
.ollama/
|
||||
models/
|
||||
weights/
|
||||
checkpoints/
|
||||
*.safetensors
|
||||
*.gguf
|
||||
*.bin
|
||||
*.pt
|
||||
*.pth
|
||||
*.onnx
|
||||
*.h5
|
||||
|
||||
## Python (if any scripts)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
## Node (if any tooling)
|
||||
node_modules/
|
||||
|
||||
## Logs
|
||||
*.log
|
||||
logs/
|
||||
18
BGPLite.Api/BGPLite.Api.csproj
Normal file
18
BGPLite.Api/BGPLite.Api.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BGPLite.Routing\BGPLite.Routing.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Server\BGPLite.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
230
BGPLite.Api/ManagementApi.cs
Normal file
230
BGPLite.Api/ManagementApi.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using BGPLite.Routing;
|
||||
using BGPLite.Server;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BGPLite.Api;
|
||||
|
||||
public sealed class ManagementApi : IHostedService, IDisposable
|
||||
{
|
||||
private readonly PeerStore _store;
|
||||
private readonly RouteTable _routeTable;
|
||||
private readonly ILogger<ManagementApi> _logger;
|
||||
private HttpListener? _listener;
|
||||
private Task? _listenTask;
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
public ManagementApi(
|
||||
PeerStore store,
|
||||
RouteTable routeTable,
|
||||
ILogger<ManagementApi> logger)
|
||||
{
|
||||
_store = store;
|
||||
_routeTable = routeTable;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add("http://+:5000/");
|
||||
_listener.Start();
|
||||
|
||||
_logger.LogInformation("Management API listening on http://+:5000/");
|
||||
_listenTask = ListenAsync(_cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts.Cancel();
|
||||
_listener?.Stop();
|
||||
if (_listenTask is not null)
|
||||
{
|
||||
try { await _listenTask; } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListenAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ctx = await _listener!.GetContextAsync();
|
||||
if (ct.IsCancellationRequested) break;
|
||||
_ = HandleAsync(ctx);
|
||||
}
|
||||
catch (HttpListenerException) when (ct.IsCancellationRequested) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Management API error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(HttpListenerContext ctx)
|
||||
{
|
||||
var path = ctx.Request.Url!.AbsolutePath;
|
||||
var method = ctx.Request.HttpMethod;
|
||||
|
||||
try
|
||||
{
|
||||
ApiResponse response;
|
||||
|
||||
if (method == "GET" && path == "/api/peers")
|
||||
response = HandleGetPeers();
|
||||
else if (method == "GET" && path == "/api/routes/count")
|
||||
response = HandleGetRouteCount();
|
||||
else if (method == "GET" && path.StartsWith("/api/peer/") && path.EndsWith("/communities"))
|
||||
response = HandleGetPeerCommunities(ExtractPeerIp(path));
|
||||
else if (method == "PUT" && path.StartsWith("/api/peer/") && path.EndsWith("/communities"))
|
||||
response = await HandleSetPeerCommunities(ExtractPeerIp(path), ctx);
|
||||
else if (method == "DELETE" && path.StartsWith("/api/peer/") && path.EndsWith("/communities"))
|
||||
response = HandleDeletePeerCommunities(ExtractPeerIp(path));
|
||||
else if (method == "PUT" && path.StartsWith("/api/peer/") && path.EndsWith("/description"))
|
||||
response = await HandleSetPeerDescription(ExtractPeerIp(path), ctx);
|
||||
else
|
||||
response = ApiResponse.Error("Not found", 404);
|
||||
|
||||
await WriteResponse(ctx, response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await WriteResponse(ctx, ApiResponse.Error(ex.Message, 500));
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractPeerIp(string path)
|
||||
{
|
||||
// /api/peer/{ip}/communities → segments: ["", "api", "peer", "{ip}", ...]
|
||||
return path.Split('/')[3];
|
||||
}
|
||||
|
||||
private ApiResponse HandleGetPeers()
|
||||
{
|
||||
var peers = _store.GetAllPeers();
|
||||
return ApiResponse.Ok(peers.Select(p => new
|
||||
{
|
||||
ip = p.Ip,
|
||||
asn = p.Asn,
|
||||
description = p.Description,
|
||||
connectedAt = p.ConnectedAt,
|
||||
communities = p.Communities.Select(CommunityToString),
|
||||
allRoutes = p.Communities.Count == 0
|
||||
}));
|
||||
}
|
||||
|
||||
private ApiResponse HandleGetPeerCommunities(string peerIp)
|
||||
{
|
||||
var communities = _store.GetCommunities(peerIp);
|
||||
return ApiResponse.Ok(new
|
||||
{
|
||||
ip = peerIp,
|
||||
communities = communities.Select(CommunityToString),
|
||||
allRoutes = communities.Count == 0
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<ApiResponse> HandleSetPeerCommunities(string peerIp, HttpListenerContext ctx)
|
||||
{
|
||||
using var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<SetCommunitiesRequest>(body);
|
||||
|
||||
if (data?.Communities is null)
|
||||
return ApiResponse.Error("Invalid request body", 400);
|
||||
|
||||
var communities = new HashSet<uint>();
|
||||
foreach (var c in data.Communities)
|
||||
communities.Add(ParseCommunity(c));
|
||||
|
||||
_store.SetCommunities(peerIp, communities);
|
||||
|
||||
_logger.LogInformation("Updated communities for {Peer}: {Communities}",
|
||||
peerIp, string.Join(", ", communities.Select(CommunityToString)));
|
||||
|
||||
return ApiResponse.Ok(new { ip = peerIp, communities = communities.Select(CommunityToString) });
|
||||
}
|
||||
|
||||
private async Task<ApiResponse> HandleSetPeerDescription(string peerIp, HttpListenerContext ctx)
|
||||
{
|
||||
using var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<SetDescriptionRequest>(body);
|
||||
|
||||
if (data?.Description is null)
|
||||
return ApiResponse.Error("Invalid request body", 400);
|
||||
|
||||
_store.SetDescription(peerIp, data.Description);
|
||||
|
||||
_logger.LogInformation("Updated description for {Peer}: {Desc}", peerIp, data.Description);
|
||||
|
||||
return ApiResponse.Ok(new { ip = peerIp, description = data.Description });
|
||||
}
|
||||
|
||||
private ApiResponse HandleDeletePeerCommunities(string peerIp)
|
||||
{
|
||||
_store.ClearCommunities(peerIp);
|
||||
|
||||
_logger.LogInformation("Removed community filter for {Peer}", peerIp);
|
||||
return ApiResponse.Ok(new { ip = peerIp, allRoutes = true });
|
||||
}
|
||||
|
||||
private ApiResponse HandleGetRouteCount()
|
||||
{
|
||||
var routes = _routeTable.GetAll();
|
||||
var byCommunity = routes
|
||||
.SelectMany(r => r.Communities.Length == 0
|
||||
? [(community: 0u, route: r)]
|
||||
: r.Communities.Select(c => (community: c, route: r)))
|
||||
.GroupBy(x => x.community)
|
||||
.ToDictionary(g => g.Key == 0 ? "default" : CommunityToString(g.Key), g => g.Count());
|
||||
|
||||
return ApiResponse.Ok(new { total = routes.Count, byCommunity });
|
||||
}
|
||||
|
||||
private static uint ParseCommunity(string community)
|
||||
{
|
||||
var colon = community.IndexOf(':');
|
||||
var asn = uint.Parse(community[..colon]);
|
||||
var value = uint.Parse(community[(colon + 1)..]);
|
||||
return (asn << 16) | (value & 0xFFFF);
|
||||
}
|
||||
|
||||
private static string CommunityToString(uint community)
|
||||
{
|
||||
var asn = community >> 16;
|
||||
var value = community & 0xFFFF;
|
||||
return $"{asn}:{value}";
|
||||
}
|
||||
|
||||
private static async Task WriteResponse(HttpListenerContext ctx, ApiResponse response)
|
||||
{
|
||||
ctx.Response.StatusCode = response.StatusCode;
|
||||
ctx.Response.ContentType = "application/json";
|
||||
var json = JsonSerializer.Serialize(response.Body);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await ctx.Response.OutputStream.WriteAsync(bytes);
|
||||
ctx.Response.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
_listener?.Close();
|
||||
}
|
||||
|
||||
private record SetCommunitiesRequest(List<string> Communities);
|
||||
private record SetDescriptionRequest(string Description);
|
||||
|
||||
private record ApiResponse(object? Body, int StatusCode = 200)
|
||||
{
|
||||
public static ApiResponse Ok(object data) => new(data);
|
||||
public static ApiResponse Error(string message, int code) => new(new { error = message }, code);
|
||||
}
|
||||
}
|
||||
167
BGPLite.Api/PeerStore.cs
Normal file
167
BGPLite.Api/PeerStore.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace BGPLite.Api;
|
||||
|
||||
public sealed class PeerStore : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
|
||||
public PeerStore(string dbPath)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
_connection = new SqliteConnection($"Data Source={dbPath}");
|
||||
_connection.Open();
|
||||
InitSchema();
|
||||
}
|
||||
|
||||
private void InitSchema()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
CREATE TABLE IF NOT EXISTS peers (
|
||||
ip TEXT PRIMARY KEY,
|
||||
asn INTEGER,
|
||||
description TEXT,
|
||||
connected_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS peer_communities (
|
||||
peer_ip TEXT NOT NULL,
|
||||
community INTEGER NOT NULL,
|
||||
PRIMARY KEY (peer_ip, community),
|
||||
FOREIGN KEY (peer_ip) REFERENCES peers(ip) ON DELETE CASCADE
|
||||
)
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void UpsertPeer(string ip, uint asn)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO peers (ip, asn, connected_at)
|
||||
VALUES ($ip, $asn, $now)
|
||||
ON CONFLICT(ip) DO UPDATE SET asn=$asn, connected_at=$now
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$ip", ip);
|
||||
cmd.Parameters.AddWithValue("$asn", (long)asn);
|
||||
cmd.Parameters.AddWithValue("$now", DateTime.UtcNow.ToString("O"));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public List<PeerInfo> GetAllPeers()
|
||||
{
|
||||
var peers = new List<PeerInfo>();
|
||||
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT ip, asn, description, connected_at FROM peers";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
peers.Add(new PeerInfo
|
||||
{
|
||||
Ip = reader.GetString(0),
|
||||
Asn = reader.IsDBNull(1) ? null : (uint?)reader.GetInt64(1),
|
||||
Description = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
ConnectedAt = reader.IsDBNull(3) ? null : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var peer in peers)
|
||||
peer.Communities = GetCommunities(peer.Ip);
|
||||
|
||||
return peers;
|
||||
}
|
||||
|
||||
public PeerInfo? GetPeer(string ip)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT ip, asn, description, connected_at FROM peers WHERE ip=$ip";
|
||||
cmd.Parameters.AddWithValue("$ip", ip);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
if (!reader.Read()) return null;
|
||||
|
||||
var peer = new PeerInfo
|
||||
{
|
||||
Ip = reader.GetString(0),
|
||||
Asn = reader.IsDBNull(1) ? null : (uint?)reader.GetInt64(1),
|
||||
Description = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
ConnectedAt = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
Communities = GetCommunities(ip)
|
||||
};
|
||||
return peer;
|
||||
}
|
||||
|
||||
public HashSet<uint> GetCommunities(string ip)
|
||||
{
|
||||
var communities = new HashSet<uint>();
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT community FROM peer_communities WHERE peer_ip=$ip";
|
||||
cmd.Parameters.AddWithValue("$ip", ip);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
communities.Add((uint)reader.GetInt64(0));
|
||||
return communities;
|
||||
}
|
||||
|
||||
public void SetCommunities(string ip, HashSet<uint> communities)
|
||||
{
|
||||
using var tx = _connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
using (var del = _connection.CreateCommand())
|
||||
{
|
||||
del.CommandText = "DELETE FROM peer_communities WHERE peer_ip=$ip";
|
||||
del.Parameters.AddWithValue("$ip", ip);
|
||||
del.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
foreach (var c in communities)
|
||||
{
|
||||
using var ins = _connection.CreateCommand();
|
||||
ins.CommandText = "INSERT INTO peer_communities (peer_ip, community) VALUES ($ip, $c)";
|
||||
ins.Parameters.AddWithValue("$ip", ip);
|
||||
ins.Parameters.AddWithValue("$c", (long)c);
|
||||
ins.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
tx.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCommunities(string ip)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM peer_communities WHERE peer_ip=$ip";
|
||||
cmd.Parameters.AddWithValue("$ip", ip);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void SetDescription(string ip, string description)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "UPDATE peers SET description=$desc WHERE ip=$ip";
|
||||
cmd.Parameters.AddWithValue("$desc", description);
|
||||
cmd.Parameters.AddWithValue("$ip", ip);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void Dispose() => _connection.Dispose();
|
||||
}
|
||||
|
||||
public class PeerInfo
|
||||
{
|
||||
public string Ip { get; init; } = "";
|
||||
public uint? Asn { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? ConnectedAt { get; init; }
|
||||
public HashSet<uint> Communities { get; set; } = [];
|
||||
}
|
||||
12
BGPLite.Configuration/AppConfig.cs
Normal file
12
BGPLite.Configuration/AppConfig.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace BGPLite.Configuration;
|
||||
|
||||
public sealed class AppConfig
|
||||
{
|
||||
[YamlMember(Alias = "Bgp")]
|
||||
public BgpConfig Bgp { get; init; } = new();
|
||||
|
||||
[YamlMember(Alias = "Peers")]
|
||||
public List<PeerConfig> Peers { get; init; } = [];
|
||||
}
|
||||
13
BGPLite.Configuration/BGPLite.Configuration.csproj
Normal file
13
BGPLite.Configuration/BGPLite.Configuration.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
BGPLite.Configuration/BgpConfig.cs
Normal file
21
BGPLite.Configuration/BgpConfig.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.Net;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace BGPLite.Configuration;
|
||||
|
||||
public sealed class BgpConfig
|
||||
{
|
||||
[YamlMember(Alias = "Asn")]
|
||||
public uint Asn { get; init; }
|
||||
|
||||
[YamlMember(Alias = "RouterId")]
|
||||
public string RouterId { get; init; } = "0.0.0.0";
|
||||
|
||||
[YamlMember(Alias = "KeepAlive")]
|
||||
public int KeepAlive { get; init; } = 60;
|
||||
|
||||
[YamlMember(Alias = "HoldTime")]
|
||||
public int HoldTime { get; init; } = 180;
|
||||
|
||||
public IPAddress GetRouterIdAddress() => IPAddress.Parse(RouterId);
|
||||
}
|
||||
25
BGPLite.Configuration/ConfigLoader.cs
Normal file
25
BGPLite.Configuration/ConfigLoader.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace BGPLite.Configuration;
|
||||
|
||||
public static class ConfigLoader
|
||||
{
|
||||
private static readonly IDeserializer Deserializer = new DeserializerBuilder()
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
|
||||
public static AppConfig Load(string path)
|
||||
{
|
||||
var yaml = File.ReadAllText(path);
|
||||
return Deserializer.Deserialize<AppConfig>(yaml);
|
||||
}
|
||||
|
||||
public static AppConfig LoadFromText(string yaml) =>
|
||||
Deserializer.Deserialize<AppConfig>(yaml);
|
||||
|
||||
private static readonly ISerializer Serializer = new SerializerBuilder()
|
||||
.Build();
|
||||
|
||||
public static string Save(AppConfig config) =>
|
||||
Serializer.Serialize(config);
|
||||
}
|
||||
18
BGPLite.Configuration/PeerConfig.cs
Normal file
18
BGPLite.Configuration/PeerConfig.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.Net;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace BGPLite.Configuration;
|
||||
|
||||
public sealed class PeerConfig
|
||||
{
|
||||
[YamlMember(Alias = "Address")]
|
||||
public string Address { get; init; } = "0.0.0.0";
|
||||
|
||||
[YamlMember(Alias = "RemoteAsn")]
|
||||
public uint? RemoteAsn { get; init; }
|
||||
|
||||
[YamlMember(Alias = "Description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
public IPAddress GetAddress() => IPAddress.Parse(Address);
|
||||
}
|
||||
130
BGPLite.Protocol/AttributeHelper.cs
Normal file
130
BGPLite.Protocol/AttributeHelper.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class AttributeHelper
|
||||
{
|
||||
public static BgpOrigin ReadOrigin(PathAttribute attr)
|
||||
{
|
||||
return (BgpOrigin)attr.Data[0];
|
||||
}
|
||||
|
||||
public static PathAttribute WriteOrigin(BgpOrigin origin)
|
||||
{
|
||||
return new PathAttribute
|
||||
{
|
||||
Flags = BgpConstants.Attribute.FlagTransitive,
|
||||
TypeCode = BgpConstants.Attribute.Origin,
|
||||
Data = [(byte)origin]
|
||||
};
|
||||
}
|
||||
|
||||
public static uint[] ReadAsPath(PathAttribute attr, bool fourByteAsn)
|
||||
{
|
||||
var ases = new List<uint>();
|
||||
var offset = 0;
|
||||
while (offset < attr.Data.Length)
|
||||
{
|
||||
var segmentType = attr.Data[offset++];
|
||||
var segmentLength = attr.Data[offset++];
|
||||
|
||||
for (var i = 0; i < segmentLength; i++)
|
||||
{
|
||||
if (fourByteAsn && offset + 4 <= attr.Data.Length)
|
||||
{
|
||||
ases.Add(BinaryPrimitives.ReadUInt32BigEndian(attr.Data.AsSpan(offset)));
|
||||
offset += 4;
|
||||
}
|
||||
else if (offset + 2 <= attr.Data.Length)
|
||||
{
|
||||
ases.Add(BinaryPrimitives.ReadUInt16BigEndian(attr.Data.AsSpan(offset)));
|
||||
offset += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ases.ToArray();
|
||||
}
|
||||
|
||||
public static PathAttribute WriteAsPath(uint[] ases, bool fourByteAsn)
|
||||
{
|
||||
var asSize = fourByteAsn ? 4 : 2;
|
||||
var data = new byte[2 + ases.Length * asSize];
|
||||
data[0] = BgpConstants.AsPath.AsSequence;
|
||||
data[1] = (byte)ases.Length;
|
||||
|
||||
var offset = 2;
|
||||
foreach (var asn in ases)
|
||||
{
|
||||
if (fourByteAsn)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), asn);
|
||||
offset += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(data.AsSpan(offset), (ushort)asn);
|
||||
offset += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return new PathAttribute
|
||||
{
|
||||
Flags = BgpConstants.Attribute.FlagTransitive,
|
||||
TypeCode = BgpConstants.Attribute.AsPath,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static uint ReadNextHop(PathAttribute attr)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(attr.Data);
|
||||
}
|
||||
|
||||
public static PathAttribute WriteNextHop(uint nextHop)
|
||||
{
|
||||
var data = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data, nextHop);
|
||||
return new PathAttribute
|
||||
{
|
||||
Flags = BgpConstants.Attribute.FlagTransitive,
|
||||
TypeCode = BgpConstants.Attribute.NextHop,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static uint[] ReadCommunities(PathAttribute attr)
|
||||
{
|
||||
var count = attr.Data.Length / 4;
|
||||
var communities = new uint[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
communities[i] = BinaryPrimitives.ReadUInt32BigEndian(attr.Data.AsSpan(i * 4));
|
||||
return communities;
|
||||
}
|
||||
|
||||
public static PathAttribute WriteCommunities(uint[] communities)
|
||||
{
|
||||
var data = new byte[communities.Length * 4];
|
||||
for (var i = 0; i < communities.Length; i++)
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(i * 4), communities[i]);
|
||||
return new PathAttribute
|
||||
{
|
||||
Flags = BgpConstants.Attribute.FlagOptional | BgpConstants.Attribute.FlagTransitive,
|
||||
TypeCode = BgpConstants.Attribute.Community,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsKnownAttribute(byte typeCode) => typeCode switch
|
||||
{
|
||||
BgpConstants.Attribute.Origin => true,
|
||||
BgpConstants.Attribute.AsPath => true,
|
||||
BgpConstants.Attribute.NextHop => true,
|
||||
BgpConstants.Attribute.Community => true,
|
||||
BgpConstants.Attribute.Med => true,
|
||||
BgpConstants.Attribute.LocalPref => true,
|
||||
BgpConstants.Attribute.AtomicAggregate => true,
|
||||
BgpConstants.Attribute.Aggregator => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
9
BGPLite.Protocol/BGPLite.Protocol.csproj
Normal file
9
BGPLite.Protocol/BGPLite.Protocol.csproj
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
29
BGPLite.Protocol/BgpCapabilityInfo.cs
Normal file
29
BGPLite.Protocol/BgpCapabilityInfo.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class BgpCapabilityInfo
|
||||
{
|
||||
public byte Code { get; init; }
|
||||
public byte[] Data { get; init; } = [];
|
||||
|
||||
public static BgpCapabilityInfo FourOctetAsn(uint asn)
|
||||
{
|
||||
var data = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data, asn);
|
||||
return new BgpCapabilityInfo { Code = BgpConstants.Capability.FourOctetAsn, Data = data };
|
||||
}
|
||||
|
||||
public static BgpCapabilityInfo RouteRefresh() => new()
|
||||
{
|
||||
Code = BgpConstants.Capability.RouteRefresh
|
||||
};
|
||||
|
||||
public static BgpCapabilityInfo MultiprotocolIpv4Unicast() => new()
|
||||
{
|
||||
Code = BgpConstants.Capability.Multiprotocol,
|
||||
Data = [(byte)(BgpConstants.Afi.IPv4 >> 8), (byte)BgpConstants.Afi.IPv4, 0x00, BgpConstants.Safi.Unicast]
|
||||
};
|
||||
|
||||
public uint ReadAsn() => BinaryPrimitives.ReadUInt32BigEndian(Data);
|
||||
}
|
||||
94
BGPLite.Protocol/BgpConstants.cs
Normal file
94
BGPLite.Protocol/BgpConstants.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
using System.Net;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class BgpConstants
|
||||
{
|
||||
public const int BgpVersion = 4;
|
||||
public const int BgpPort = 179;
|
||||
public const int MarkerSize = 16;
|
||||
public const int MessageHeaderSize = 19; // 16 marker + 2 length + 1 type
|
||||
public const int MinMessageSize = 19;
|
||||
public const int MaxMessageSize = 4096;
|
||||
public const int MinOpenMessageSize = 29;
|
||||
|
||||
public const ushort DefaultKeepAlive = 60;
|
||||
public const ushort DefaultHoldTime = 180;
|
||||
public const int ConnectRetryDelay = 5;
|
||||
|
||||
public static ReadOnlySpan<byte> Marker =>
|
||||
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
|
||||
|
||||
public static class Error
|
||||
{
|
||||
public const byte MessageHeaderError = 1;
|
||||
public const byte OpenMessageError = 2;
|
||||
public const byte UpdateMessageError = 3;
|
||||
public const byte HoldTimerExpired = 4;
|
||||
public const byte FiniteStateMachineError = 5;
|
||||
public const byte Cease = 6;
|
||||
}
|
||||
|
||||
public static class SubError
|
||||
{
|
||||
public const byte Unspecific = 0;
|
||||
public const byte UnsupportedVersion = 1;
|
||||
public const byte BadPeerAs = 2;
|
||||
public const byte BadBgpIdentifier = 3;
|
||||
public const byte UnacceptableHoldTime = 6;
|
||||
}
|
||||
|
||||
public static class Attribute
|
||||
{
|
||||
public const byte Origin = 1;
|
||||
public const byte AsPath = 2;
|
||||
public const byte NextHop = 3;
|
||||
public const byte Med = 4;
|
||||
public const byte LocalPref = 5;
|
||||
public const byte AtomicAggregate = 6;
|
||||
public const byte Aggregator = 7;
|
||||
public const byte Community = 8;
|
||||
public const byte OriginatorId = 9;
|
||||
public const byte ClusterList = 10;
|
||||
public const byte ExtendedCommunity = 16;
|
||||
public const byte LargeCommunity = 32;
|
||||
|
||||
public const byte FlagOptional = 0x80;
|
||||
public const byte FlagTransitive = 0x40;
|
||||
public const byte FlagPartial = 0x20;
|
||||
public const byte FlagExtendedLength = 0x10;
|
||||
}
|
||||
|
||||
public static class AsPath
|
||||
{
|
||||
public const byte AsSet = 1;
|
||||
public const byte AsSequence = 2;
|
||||
}
|
||||
|
||||
public static class Capability
|
||||
{
|
||||
public const byte Multiprotocol = 1;
|
||||
public const byte RouteRefresh = 2;
|
||||
public const byte FourOctetAsn = 65;
|
||||
}
|
||||
|
||||
public static class Afi
|
||||
{
|
||||
public const ushort IPv4 = 1;
|
||||
}
|
||||
|
||||
public static class Safi
|
||||
{
|
||||
public const byte Unicast = 1;
|
||||
}
|
||||
|
||||
public static uint IPAddressToUint(IPAddress address)
|
||||
{
|
||||
var bytes = address.GetAddressBytes();
|
||||
return (uint)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]);
|
||||
}
|
||||
|
||||
public static IPAddress UintToIPAddress(uint address) =>
|
||||
new([(byte)(address >> 24), (byte)(address >> 16), (byte)(address >> 8), (byte)address]);
|
||||
}
|
||||
10
BGPLite.Protocol/BgpFsmState.cs
Normal file
10
BGPLite.Protocol/BgpFsmState.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public enum BgpFsmState
|
||||
{
|
||||
Idle,
|
||||
Connect,
|
||||
OpenSent,
|
||||
OpenConfirm,
|
||||
Established
|
||||
}
|
||||
7
BGPLite.Protocol/BgpKeepaliveMessage.cs
Normal file
7
BGPLite.Protocol/BgpKeepaliveMessage.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class BgpKeepaliveMessage : BgpMessage
|
||||
{
|
||||
public override BgpMessageType Type => BgpMessageType.Keepalive;
|
||||
public static BgpKeepaliveMessage Instance { get; } = new();
|
||||
}
|
||||
6
BGPLite.Protocol/BgpMessage.cs
Normal file
6
BGPLite.Protocol/BgpMessage.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public abstract class BgpMessage
|
||||
{
|
||||
public abstract BgpMessageType Type { get; }
|
||||
}
|
||||
232
BGPLite.Protocol/BgpMessageReader.cs
Normal file
232
BGPLite.Protocol/BgpMessageReader.cs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class BgpMessageReader
|
||||
{
|
||||
public static BgpMessage ReadMessage(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length < BgpConstants.MinMessageSize)
|
||||
throw new BgpParseException($"Message too short: {buffer.Length}");
|
||||
|
||||
ValidateMarker(buffer[..BgpConstants.MarkerSize]);
|
||||
|
||||
var length = BinaryPrimitives.ReadUInt16BigEndian(buffer[16..]);
|
||||
if (length < BgpConstants.MinMessageSize || length > BgpConstants.MaxMessageSize)
|
||||
throw new BgpParseException($"Invalid message length: {length}");
|
||||
|
||||
if (buffer.Length < length)
|
||||
throw new BgpParseException($"Incomplete message: have {buffer.Length}, need {length}");
|
||||
|
||||
var type = (BgpMessageType)buffer[18];
|
||||
var payload = buffer[BgpConstants.MessageHeaderSize..length];
|
||||
|
||||
return type switch
|
||||
{
|
||||
BgpMessageType.Open => ParseOpen(payload),
|
||||
BgpMessageType.Keepalive => BgpKeepaliveMessage.Instance,
|
||||
BgpMessageType.Update => ParseUpdate(payload),
|
||||
BgpMessageType.Notification => ParseNotification(payload),
|
||||
_ => throw new BgpParseException($"Unknown message type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
public static int GetMessageLength(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length < BgpConstants.MessageHeaderSize)
|
||||
return -1;
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(buffer[16..]);
|
||||
}
|
||||
|
||||
private static void ValidateMarker(ReadOnlySpan<byte> marker)
|
||||
{
|
||||
var expected = BgpConstants.Marker;
|
||||
for (var i = 0; i < BgpConstants.MarkerSize; i++)
|
||||
{
|
||||
if (marker[i] != expected[i])
|
||||
throw new BgpParseException("Invalid BGP marker");
|
||||
}
|
||||
}
|
||||
|
||||
#region OPEN
|
||||
|
||||
private static BgpOpenMessage ParseOpen(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 10)
|
||||
throw new BgpParseException($"OPEN message too short: {payload.Length}");
|
||||
|
||||
var version = payload[0];
|
||||
if (version != BgpConstants.BgpVersion)
|
||||
throw new BgpParseException($"Unsupported BGP version: {version}");
|
||||
|
||||
var asn = BinaryPrimitives.ReadUInt16BigEndian(payload[1..]);
|
||||
var holdTime = BinaryPrimitives.ReadUInt16BigEndian(payload[3..]);
|
||||
var routerId = BinaryPrimitives.ReadUInt32BigEndian(payload[5..]);
|
||||
var optParamsLen = payload[9];
|
||||
|
||||
var capabilities = new List<BgpCapabilityInfo>();
|
||||
if (optParamsLen > 0 && payload.Length >= 10 + optParamsLen)
|
||||
ParseOptParameters(payload[10..][..optParamsLen], capabilities);
|
||||
|
||||
return new BgpOpenMessage
|
||||
{
|
||||
Version = version,
|
||||
Asn = asn,
|
||||
HoldTime = holdTime,
|
||||
RouterId = routerId,
|
||||
Capabilities = capabilities
|
||||
};
|
||||
}
|
||||
|
||||
private static void ParseOptParameters(ReadOnlySpan<byte> data, List<BgpCapabilityInfo> capabilities)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < data.Length)
|
||||
{
|
||||
if (offset + 2 > data.Length) break;
|
||||
var paramType = data[offset++];
|
||||
var paramLen = data[offset++];
|
||||
|
||||
if (offset + paramLen > data.Length) break;
|
||||
|
||||
if (paramType == 2) // Capability
|
||||
ParseCapabilities(data[offset..][..paramLen], capabilities);
|
||||
|
||||
offset += paramLen;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseCapabilities(ReadOnlySpan<byte> data, List<BgpCapabilityInfo> capabilities)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset + 2 <= data.Length)
|
||||
{
|
||||
var code = data[offset++];
|
||||
var len = data[offset++];
|
||||
|
||||
if (offset + len > data.Length) break;
|
||||
|
||||
var capData = new byte[len];
|
||||
data.Slice(offset, len).CopyTo(capData);
|
||||
capabilities.Add(new BgpCapabilityInfo { Code = code, Data = capData });
|
||||
|
||||
offset += len;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UPDATE
|
||||
|
||||
private static BgpUpdateMessage ParseUpdate(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4)
|
||||
throw new BgpParseException($"UPDATE message too short: {payload.Length}");
|
||||
|
||||
var offset = 0;
|
||||
|
||||
var withdrawnLen = BinaryPrimitives.ReadUInt16BigEndian(payload[offset..]);
|
||||
offset += 2;
|
||||
|
||||
var withdrawn = new List<IpPrefix>();
|
||||
if (withdrawnLen > 0)
|
||||
{
|
||||
var withdrawnEnd = offset + withdrawnLen;
|
||||
while (offset < withdrawnEnd)
|
||||
{
|
||||
var (prefix, consumed) = PrefixCodec.Decode(payload[offset..]);
|
||||
withdrawn.Add(prefix);
|
||||
offset += consumed;
|
||||
}
|
||||
}
|
||||
|
||||
var attrsLen = BinaryPrimitives.ReadUInt16BigEndian(payload[offset..]);
|
||||
offset += 2;
|
||||
|
||||
var attributes = new List<PathAttribute>();
|
||||
if (attrsLen > 0)
|
||||
{
|
||||
var attrsEnd = offset + attrsLen;
|
||||
while (offset < attrsEnd)
|
||||
{
|
||||
var (attr, consumed) = ParseAttribute(payload[offset..]);
|
||||
attributes.Add(attr);
|
||||
offset += consumed;
|
||||
}
|
||||
}
|
||||
|
||||
var nlri = new List<IpPrefix>();
|
||||
while (offset < payload.Length)
|
||||
{
|
||||
var (prefix, consumed) = PrefixCodec.Decode(payload[offset..]);
|
||||
nlri.Add(prefix);
|
||||
offset += consumed;
|
||||
}
|
||||
|
||||
return new BgpUpdateMessage
|
||||
{
|
||||
WithdrawnRoutes = withdrawn,
|
||||
PathAttributes = attributes,
|
||||
Nlri = nlri
|
||||
};
|
||||
}
|
||||
|
||||
private static (PathAttribute attr, int consumed) ParseAttribute(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var flags = data[0];
|
||||
var typeCode = data[1];
|
||||
var offset = 2;
|
||||
|
||||
int length;
|
||||
if ((flags & BgpConstants.Attribute.FlagExtendedLength) != 0)
|
||||
{
|
||||
length = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
|
||||
offset += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
length = data[offset];
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
var attrData = new byte[length];
|
||||
data.Slice(offset, length).CopyTo(attrData);
|
||||
|
||||
return (new PathAttribute { Flags = flags, TypeCode = typeCode, Data = attrData }, offset + length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NOTIFICATION
|
||||
|
||||
private static BgpNotificationMessage ParseNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 2)
|
||||
throw new BgpParseException($"NOTIFICATION message too short: {payload.Length}");
|
||||
|
||||
var errorCode = payload[0];
|
||||
var subErrorCode = payload[1];
|
||||
byte[]? data = null;
|
||||
|
||||
if (payload.Length > 2)
|
||||
{
|
||||
data = new byte[payload.Length - 2];
|
||||
payload[2..].CopyTo(data);
|
||||
}
|
||||
|
||||
return new BgpNotificationMessage
|
||||
{
|
||||
ErrorCode = errorCode,
|
||||
SubErrorCode = subErrorCode,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public sealed class BgpParseException : Exception
|
||||
{
|
||||
public BgpParseException(string message) : base(message) { }
|
||||
public BgpParseException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
9
BGPLite.Protocol/BgpMessageType.cs
Normal file
9
BGPLite.Protocol/BgpMessageType.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public enum BgpMessageType : byte
|
||||
{
|
||||
Open = 1,
|
||||
Update = 2,
|
||||
Notification = 3,
|
||||
Keepalive = 4
|
||||
}
|
||||
212
BGPLite.Protocol/BgpMessageWriter.cs
Normal file
212
BGPLite.Protocol/BgpMessageWriter.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class BgpMessageWriter
|
||||
{
|
||||
public static int WriteMessage(BgpMessage message, Span<byte> buffer)
|
||||
{
|
||||
return message switch
|
||||
{
|
||||
BgpOpenMessage open => WriteOpen(open, buffer),
|
||||
BgpKeepaliveMessage => WriteKeepalive(buffer),
|
||||
BgpUpdateMessage update => WriteUpdate(update, buffer),
|
||||
BgpNotificationMessage notification => WriteNotification(notification, buffer),
|
||||
_ => throw new ArgumentException($"Unknown message type: {message.Type}")
|
||||
};
|
||||
}
|
||||
|
||||
public static int GetBufferSize(BgpMessage message)
|
||||
{
|
||||
return message switch
|
||||
{
|
||||
BgpOpenMessage open => BgpConstants.MessageHeaderSize + GetOpenPayloadSize(open),
|
||||
BgpKeepaliveMessage => BgpConstants.MessageHeaderSize,
|
||||
BgpUpdateMessage update => BgpConstants.MessageHeaderSize + GetUpdatePayloadSize(update),
|
||||
BgpNotificationMessage n => BgpConstants.MessageHeaderSize + 2 + (n.Data?.Length ?? 0),
|
||||
_ => throw new ArgumentException($"Unknown message type: {message.Type}")
|
||||
};
|
||||
}
|
||||
|
||||
private static void WriteHeader(BgpMessageType type, int totalLength, Span<byte> buffer)
|
||||
{
|
||||
BgpConstants.Marker.CopyTo(buffer);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[16..], (ushort)totalLength);
|
||||
buffer[18] = (byte)type;
|
||||
}
|
||||
|
||||
#region OPEN
|
||||
|
||||
private static int WriteOpen(BgpOpenMessage msg, Span<byte> buffer)
|
||||
{
|
||||
var payloadSize = GetOpenPayloadSize(msg);
|
||||
var totalLength = BgpConstants.MessageHeaderSize + payloadSize;
|
||||
|
||||
WriteHeader(BgpMessageType.Open, totalLength, buffer);
|
||||
|
||||
var p = BgpConstants.MessageHeaderSize;
|
||||
buffer[p++] = msg.Version;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[p..], msg.Asn);
|
||||
p += 2;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[p..], msg.HoldTime);
|
||||
p += 2;
|
||||
BinaryPrimitives.WriteUInt32BigEndian(buffer[p..], msg.RouterId);
|
||||
p += 4;
|
||||
|
||||
var optParamsLen = GetOptParamsLength(msg.Capabilities);
|
||||
buffer[p++] = (byte)optParamsLen;
|
||||
|
||||
WriteCapabilities(msg.Capabilities, buffer[p..]);
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
private static int GetOpenPayloadSize(BgpOpenMessage msg) =>
|
||||
9 + 1 + GetOptParamsLength(msg.Capabilities);
|
||||
|
||||
private static int GetOptParamsLength(List<BgpCapabilityInfo> capabilities)
|
||||
{
|
||||
if (capabilities.Count == 0) return 0;
|
||||
|
||||
var capDataLen = 0;
|
||||
foreach (var cap in capabilities)
|
||||
capDataLen += 2 + cap.Data.Length;
|
||||
|
||||
return 2 + capDataLen;
|
||||
}
|
||||
|
||||
private static void WriteCapabilities(List<BgpCapabilityInfo> capabilities, Span<byte> buffer)
|
||||
{
|
||||
if (capabilities.Count == 0) return;
|
||||
|
||||
var capDataLen = 0;
|
||||
foreach (var cap in capabilities)
|
||||
capDataLen += 2 + cap.Data.Length;
|
||||
|
||||
var p = 0;
|
||||
buffer[p++] = 2; // Type: Capabilities
|
||||
buffer[p++] = (byte)capDataLen;
|
||||
|
||||
foreach (var cap in capabilities)
|
||||
{
|
||||
buffer[p++] = cap.Code;
|
||||
buffer[p++] = (byte)cap.Data.Length;
|
||||
cap.Data.AsSpan().CopyTo(buffer[p..]);
|
||||
p += cap.Data.Length;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region KEEPALIVE
|
||||
|
||||
private static int WriteKeepalive(Span<byte> buffer)
|
||||
{
|
||||
WriteHeader(BgpMessageType.Keepalive, BgpConstants.MessageHeaderSize, buffer);
|
||||
return BgpConstants.MessageHeaderSize;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UPDATE
|
||||
|
||||
private static int WriteUpdate(BgpUpdateMessage msg, Span<byte> buffer)
|
||||
{
|
||||
var payloadSize = GetUpdatePayloadSize(msg);
|
||||
var totalLength = BgpConstants.MessageHeaderSize + payloadSize;
|
||||
|
||||
WriteHeader(BgpMessageType.Update, totalLength, buffer);
|
||||
|
||||
var p = BgpConstants.MessageHeaderSize;
|
||||
|
||||
// Withdrawn routes
|
||||
var withdrawnLen = GetNlriLength(msg.WithdrawnRoutes);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[p..], (ushort)withdrawnLen);
|
||||
p += 2;
|
||||
foreach (var w in msg.WithdrawnRoutes)
|
||||
p += PrefixCodec.Encode(w, buffer[p..]);
|
||||
|
||||
// Path attributes
|
||||
var attrsLen = GetAttributesLength(msg.PathAttributes);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[p..], (ushort)attrsLen);
|
||||
p += 2;
|
||||
foreach (var attr in msg.PathAttributes)
|
||||
p += WriteAttribute(attr, buffer[p..]);
|
||||
|
||||
// NLRI
|
||||
foreach (var nlri in msg.Nlri)
|
||||
p += PrefixCodec.Encode(nlri, buffer[p..]);
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
private static int GetUpdatePayloadSize(BgpUpdateMessage msg) =>
|
||||
2 + GetNlriLength(msg.WithdrawnRoutes) + 2 + GetAttributesLength(msg.PathAttributes) + GetNlriLength(msg.Nlri);
|
||||
|
||||
private static int GetNlriLength(List<IpPrefix> prefixes)
|
||||
{
|
||||
var len = 0;
|
||||
foreach (var p in prefixes)
|
||||
len += 1 + (p.Length + 7) / 8;
|
||||
return len;
|
||||
}
|
||||
|
||||
private static int GetAttributesLength(List<PathAttribute> attributes)
|
||||
{
|
||||
var len = 0;
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
len += 2; // flags + type code
|
||||
if (attr.Data.Length > 255)
|
||||
len += 2; // extended length
|
||||
else
|
||||
len += 1; // single byte length
|
||||
len += attr.Data.Length;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
private static int WriteAttribute(PathAttribute attr, Span<byte> buffer)
|
||||
{
|
||||
var p = 0;
|
||||
buffer[p++] = attr.Flags;
|
||||
buffer[p++] = attr.TypeCode;
|
||||
|
||||
if (attr.Data.Length > 255)
|
||||
{
|
||||
buffer[p - 2] |= BgpConstants.Attribute.FlagExtendedLength;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer[p..], (ushort)attr.Data.Length);
|
||||
p += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[p++] = (byte)attr.Data.Length;
|
||||
}
|
||||
|
||||
attr.Data.AsSpan().CopyTo(buffer[p..]);
|
||||
return p + attr.Data.Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NOTIFICATION
|
||||
|
||||
private static int WriteNotification(BgpNotificationMessage msg, Span<byte> buffer)
|
||||
{
|
||||
var totalLength = BgpConstants.MessageHeaderSize + 2 + (msg.Data?.Length ?? 0);
|
||||
WriteHeader(BgpMessageType.Notification, totalLength, buffer);
|
||||
|
||||
var p = BgpConstants.MessageHeaderSize;
|
||||
buffer[p++] = msg.ErrorCode;
|
||||
buffer[p++] = msg.SubErrorCode;
|
||||
|
||||
if (msg.Data is { Length: > 0 })
|
||||
{
|
||||
msg.Data.AsSpan().CopyTo(buffer[p..]);
|
||||
}
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
9
BGPLite.Protocol/BgpNotificationMessage.cs
Normal file
9
BGPLite.Protocol/BgpNotificationMessage.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class BgpNotificationMessage : BgpMessage
|
||||
{
|
||||
public override BgpMessageType Type => BgpMessageType.Notification;
|
||||
public byte ErrorCode { get; init; }
|
||||
public byte SubErrorCode { get; init; }
|
||||
public byte[]? Data { get; init; }
|
||||
}
|
||||
11
BGPLite.Protocol/BgpOpenMessage.cs
Normal file
11
BGPLite.Protocol/BgpOpenMessage.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class BgpOpenMessage : BgpMessage
|
||||
{
|
||||
public override BgpMessageType Type => BgpMessageType.Open;
|
||||
public byte Version { get; init; } = BgpConstants.BgpVersion;
|
||||
public ushort Asn { get; init; }
|
||||
public ushort HoldTime { get; init; }
|
||||
public uint RouterId { get; init; }
|
||||
public List<BgpCapabilityInfo> Capabilities { get; init; } = [];
|
||||
}
|
||||
8
BGPLite.Protocol/BgpOrigin.cs
Normal file
8
BGPLite.Protocol/BgpOrigin.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public enum BgpOrigin : byte
|
||||
{
|
||||
Igp = 0,
|
||||
Egp = 1,
|
||||
Incomplete = 2
|
||||
}
|
||||
9
BGPLite.Protocol/BgpUpdateMessage.cs
Normal file
9
BGPLite.Protocol/BgpUpdateMessage.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class BgpUpdateMessage : BgpMessage
|
||||
{
|
||||
public override BgpMessageType Type => BgpMessageType.Update;
|
||||
public List<IpPrefix> WithdrawnRoutes { get; init; } = [];
|
||||
public List<PathAttribute> PathAttributes { get; init; } = [];
|
||||
public List<IpPrefix> Nlri { get; init; } = [];
|
||||
}
|
||||
44
BGPLite.Protocol/CapabilityHelper.cs
Normal file
44
BGPLite.Protocol/CapabilityHelper.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class CapabilityHelper
|
||||
{
|
||||
public static uint? GetRemoteAsn(BgpOpenMessage open)
|
||||
{
|
||||
foreach (var cap in open.Capabilities)
|
||||
{
|
||||
if (cap.Code == BgpConstants.Capability.FourOctetAsn && cap.Data.Length >= 4)
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(cap.Data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool SupportsRouteRefresh(BgpOpenMessage open)
|
||||
{
|
||||
foreach (var cap in open.Capabilities)
|
||||
{
|
||||
if (cap.Code == BgpConstants.Capability.RouteRefresh)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool SupportsMultiprotocolIpv4Unicast(BgpOpenMessage open)
|
||||
{
|
||||
foreach (var cap in open.Capabilities)
|
||||
{
|
||||
if (cap.Code == BgpConstants.Capability.Multiprotocol &&
|
||||
cap.Data.Length >= 4 &&
|
||||
BinaryPrimitives.ReadUInt16BigEndian(cap.Data) == BgpConstants.Afi.IPv4 &&
|
||||
cap.Data[3] == BgpConstants.Safi.Unicast)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static uint GetEffectiveAsn(BgpOpenMessage open)
|
||||
{
|
||||
return GetRemoteAsn(open) ?? open.Asn;
|
||||
}
|
||||
}
|
||||
6
BGPLite.Protocol/IpPrefix.cs
Normal file
6
BGPLite.Protocol/IpPrefix.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public readonly record struct IpPrefix(uint Address, byte Length)
|
||||
{
|
||||
public override string ToString() => $"{BgpConstants.UintToIPAddress(Address)}/{Length}";
|
||||
}
|
||||
12
BGPLite.Protocol/PathAttribute.cs
Normal file
12
BGPLite.Protocol/PathAttribute.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace BGPLite.Protocol;
|
||||
|
||||
public sealed class PathAttribute
|
||||
{
|
||||
public byte Flags { get; init; }
|
||||
public byte TypeCode { get; init; }
|
||||
public byte[] Data { get; init; } = [];
|
||||
|
||||
public bool Optional => (Flags & BgpConstants.Attribute.FlagOptional) != 0;
|
||||
public bool Transitive => (Flags & BgpConstants.Attribute.FlagTransitive) != 0;
|
||||
public bool Partial => (Flags & BgpConstants.Attribute.FlagPartial) != 0;
|
||||
}
|
||||
61
BGPLite.Protocol/PrefixCodec.cs
Normal file
61
BGPLite.Protocol/PrefixCodec.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace BGPLite.Protocol;
|
||||
|
||||
public static class PrefixCodec
|
||||
{
|
||||
public static int Encode(IpPrefix prefix, Span<byte> buffer)
|
||||
{
|
||||
var length = prefix.Length;
|
||||
if (length == 0)
|
||||
{
|
||||
buffer[0] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
var byteCount = (length + 7) / 8;
|
||||
buffer[0] = length;
|
||||
|
||||
var addr = prefix.Address;
|
||||
for (var i = 0; i < byteCount; i++)
|
||||
buffer[1 + i] = (byte)(addr >> (24 - i * 8));
|
||||
|
||||
return 1 + byteCount;
|
||||
}
|
||||
|
||||
public static (IpPrefix prefix, int bytesConsumed) Decode(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
var length = buffer[0];
|
||||
if (length == 0)
|
||||
return (new IpPrefix(0, 0), 1);
|
||||
|
||||
var byteCount = (length + 7) / 8;
|
||||
uint addr = 0;
|
||||
for (var i = 0; i < byteCount; i++)
|
||||
addr |= (uint)buffer[1 + i] << (24 - i * 8);
|
||||
|
||||
addr &= 0xFFFFFFFF << (32 - length);
|
||||
return (new IpPrefix(addr, length), 1 + byteCount);
|
||||
}
|
||||
|
||||
public static int EncodeList(ReadOnlySpan<IpPrefix> prefixes, Span<byte> buffer)
|
||||
{
|
||||
var offset = 0;
|
||||
for (var i = 0; i < prefixes.Length; i++)
|
||||
offset += Encode(prefixes[i], buffer[offset..]);
|
||||
return offset;
|
||||
}
|
||||
|
||||
public static int DecodeList(ReadOnlySpan<byte> buffer, int length, Span<IpPrefix> prefixes)
|
||||
{
|
||||
var offset = 0;
|
||||
var count = 0;
|
||||
while (offset < length)
|
||||
{
|
||||
var (prefix, consumed) = Decode(buffer[offset..]);
|
||||
prefixes[count++] = prefix;
|
||||
offset += consumed;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
11
BGPLite.Routing/AllowAllFilter.cs
Normal file
11
BGPLite.Routing/AllowAllFilter.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using BGPLite.Configuration;
|
||||
|
||||
namespace BGPLite.Routing;
|
||||
|
||||
public sealed class AllowAllFilter : IRouteFilter
|
||||
{
|
||||
public static AllowAllFilter Instance { get; } = new();
|
||||
|
||||
public bool AcceptIncoming(Route route, PeerConfig peer) => true;
|
||||
public bool AcceptOutgoing(Route route, PeerConfig peer) => true;
|
||||
}
|
||||
13
BGPLite.Routing/BGPLite.Routing.csproj
Normal file
13
BGPLite.Routing/BGPLite.Routing.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BGPLite.Configuration\BGPLite.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
9
BGPLite.Routing/IRouteFilter.cs
Normal file
9
BGPLite.Routing/IRouteFilter.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using BGPLite.Configuration;
|
||||
|
||||
namespace BGPLite.Routing;
|
||||
|
||||
public interface IRouteFilter
|
||||
{
|
||||
bool AcceptIncoming(Route route, PeerConfig peer);
|
||||
bool AcceptOutgoing(Route route, PeerConfig peer);
|
||||
}
|
||||
33
BGPLite.Routing/PeerCommunityFilter.cs
Normal file
33
BGPLite.Routing/PeerCommunityFilter.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using BGPLite.Configuration;
|
||||
|
||||
namespace BGPLite.Routing;
|
||||
|
||||
public sealed class PeerCommunityFilter : IRouteFilter
|
||||
{
|
||||
private readonly Func<string, HashSet<uint>> _getCommunities;
|
||||
|
||||
public PeerCommunityFilter(Func<string, HashSet<uint>> getCommunities)
|
||||
{
|
||||
_getCommunities = getCommunities;
|
||||
}
|
||||
|
||||
public bool AcceptIncoming(Route route, PeerConfig peer) => true;
|
||||
|
||||
public bool AcceptOutgoing(Route route, PeerConfig peer)
|
||||
{
|
||||
var allowed = _getCommunities(peer.Address);
|
||||
if (allowed.Count == 0)
|
||||
return true; // no filter = all routes
|
||||
|
||||
if (route.Communities.Length == 0)
|
||||
return true; // routes without community always pass
|
||||
|
||||
foreach (var c in route.Communities)
|
||||
{
|
||||
if (allowed.Contains(c))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
13
BGPLite.Routing/Route.cs
Normal file
13
BGPLite.Routing/Route.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace BGPLite.Routing;
|
||||
|
||||
public sealed class Route
|
||||
{
|
||||
public required uint Prefix { get; init; }
|
||||
public required byte PrefixLength { get; init; }
|
||||
public required uint NextHop { get; init; }
|
||||
public uint[] AsPath { get; init; } = [];
|
||||
public uint[] Communities { get; init; } = [];
|
||||
public DateTime UpdatedAt { get; init; } = DateTime.UtcNow;
|
||||
|
||||
public (uint Prefix, byte Length) Key => (Prefix, PrefixLength);
|
||||
}
|
||||
31
BGPLite.Routing/RouteTable.cs
Normal file
31
BGPLite.Routing/RouteTable.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BGPLite.Routing;
|
||||
|
||||
public sealed class RouteTable
|
||||
{
|
||||
private readonly ConcurrentDictionary<(uint Prefix, byte Length), Route> _routes = new();
|
||||
|
||||
public int Count => _routes.Count;
|
||||
|
||||
public bool AddOrUpdate(Route route)
|
||||
{
|
||||
var added = false;
|
||||
_routes.AddOrUpdate(route.Key,
|
||||
_ => { added = true; return route; },
|
||||
(_, _) => route);
|
||||
return added;
|
||||
}
|
||||
|
||||
public bool Remove(uint prefix, byte length) =>
|
||||
_routes.TryRemove((prefix, length), out _);
|
||||
|
||||
public Route? Get(uint prefix, byte length) =>
|
||||
_routes.TryGetValue((prefix, length), out var route) ? route : null;
|
||||
|
||||
public IReadOnlyList<Route> GetAll() =>
|
||||
_routes.Values.ToList();
|
||||
|
||||
public void Clear() =>
|
||||
_routes.Clear();
|
||||
}
|
||||
20
BGPLite.Server/BGPLite.Server.csproj
Normal file
20
BGPLite.Server/BGPLite.Server.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BGPLite.Protocol\BGPLite.Protocol.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Routing\BGPLite.Routing.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Configuration\BGPLite.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
26
BGPLite.Server/BgpMetrics.cs
Normal file
26
BGPLite.Server/BgpMetrics.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Threading;
|
||||
|
||||
namespace BGPLite.Server;
|
||||
|
||||
public sealed class BgpMetrics
|
||||
{
|
||||
private int _peerCount;
|
||||
private int _routeCount;
|
||||
private long _updatesReceived;
|
||||
private long _updatesSent;
|
||||
private int _activeSessions;
|
||||
|
||||
public int PeerCount => Volatile.Read(ref _peerCount);
|
||||
public int RouteCount => Volatile.Read(ref _routeCount);
|
||||
public long UpdatesReceived => Interlocked.Read(ref _updatesReceived);
|
||||
public long UpdatesSent => Interlocked.Read(ref _updatesSent);
|
||||
public int ActiveSessions => Volatile.Read(ref _activeSessions);
|
||||
|
||||
public void PeerConnected() => Interlocked.Increment(ref _peerCount);
|
||||
public void PeerDisconnected() => Interlocked.Decrement(ref _peerCount);
|
||||
public void UpdateReceived() => Interlocked.Increment(ref _updatesReceived);
|
||||
public void UpdateSent() => Interlocked.Increment(ref _updatesSent);
|
||||
public void SessionEstablished() => Interlocked.Increment(ref _activeSessions);
|
||||
public void SessionClosed() => Interlocked.Decrement(ref _activeSessions);
|
||||
public void SetRouteCount(int count) => Volatile.Write(ref _routeCount, count);
|
||||
}
|
||||
157
BGPLite.Server/BgpServer.cs
Normal file
157
BGPLite.Server/BgpServer.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using BGPLite.Configuration;
|
||||
using BGPLite.Protocol;
|
||||
using BGPLite.Routing;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BGPLite.Server;
|
||||
|
||||
public sealed class BgpServer : IHostedService, IDisposable
|
||||
{
|
||||
private readonly AppConfig _config;
|
||||
private readonly RouteTable _routeTable;
|
||||
private readonly IRouteFilter _routeFilter;
|
||||
private readonly BgpMetrics _metrics;
|
||||
private readonly ILogger<BgpSession> _sessionLogger;
|
||||
private readonly ILogger<BgpServer> _logger;
|
||||
private readonly Action<string, uint>? _onPeerIdentified;
|
||||
private readonly ConcurrentDictionary<string, BgpSession> _sessions = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private Socket? _listener;
|
||||
private Task? _acceptTask;
|
||||
private PeriodicTimer? _statusTimer;
|
||||
private Task? _statusTask;
|
||||
|
||||
public BgpMetrics Metrics => _metrics;
|
||||
public RouteTable Routes => _routeTable;
|
||||
|
||||
public BgpServer(
|
||||
AppConfig config,
|
||||
RouteTable routeTable,
|
||||
IRouteFilter routeFilter,
|
||||
BgpMetrics metrics,
|
||||
ILogger<BgpSession> sessionLogger,
|
||||
ILogger<BgpServer> logger,
|
||||
Action<string, uint>? onPeerIdentified = null)
|
||||
{
|
||||
_config = config;
|
||||
_routeTable = routeTable;
|
||||
_routeFilter = routeFilter;
|
||||
_metrics = metrics;
|
||||
_sessionLogger = sessionLogger;
|
||||
_logger = logger;
|
||||
_onPeerIdentified = onPeerIdentified;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
_listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_listener.Bind(new IPEndPoint(IPAddress.Any, BgpConstants.BgpPort));
|
||||
_listener.Listen(16);
|
||||
|
||||
_logger.LogInformation("BGP server listening on port {Port}", BgpConstants.BgpPort);
|
||||
_logger.LogInformation("Local ASN={Asn}, RouterId={RouterId}", _config.Bgp.Asn, _config.Bgp.RouterId);
|
||||
|
||||
_acceptTask = AcceptLoopAsync(_cts.Token);
|
||||
|
||||
_statusTimer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
||||
_statusTask = LogStatusLoopAsync(_cts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("BGP server shutting down");
|
||||
_cts.Cancel();
|
||||
|
||||
if (_listener is not null)
|
||||
{
|
||||
_listener.Close();
|
||||
_listener = null;
|
||||
}
|
||||
|
||||
foreach (var session in _sessions.Values)
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
_sessions.Clear();
|
||||
|
||||
if (_acceptTask is not null)
|
||||
{
|
||||
try { await _acceptTask; } catch { }
|
||||
}
|
||||
|
||||
_statusTimer?.Dispose();
|
||||
if (_statusTask is not null)
|
||||
{
|
||||
try { await _statusTask; } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var socket = await _listener!.AcceptAsync(cancellationToken);
|
||||
var remoteEndpoint = (IPEndPoint)socket.RemoteEndPoint!;
|
||||
var peerAddress = remoteEndpoint.Address.ToString();
|
||||
|
||||
_logger.LogInformation("Incoming connection from {Address}", peerAddress);
|
||||
|
||||
var peerConfig = new PeerConfig { Address = peerAddress };
|
||||
|
||||
var session = new BgpSession(
|
||||
socket, peerConfig, _config.Bgp, _routeTable,
|
||||
_routeFilter, _metrics, _sessionLogger,
|
||||
_onPeerIdentified);
|
||||
|
||||
_sessions[peerAddress] = session;
|
||||
|
||||
_ = RunSessionAsync(peerAddress, session, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error accepting connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSessionAsync(string peerAddress, BgpSession session, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await session.RunAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessions.TryRemove(peerAddress, out _);
|
||||
session.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogStatusLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _statusTimer!.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
var peers = string.Join(", ", _sessions.Keys);
|
||||
_logger.LogInformation("Active sessions: {Count} [{Peers}]", _sessions.Count, peers);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
_listener?.Dispose();
|
||||
foreach (var session in _sessions.Values)
|
||||
session.Dispose();
|
||||
}
|
||||
}
|
||||
496
BGPLite.Server/BgpSession.cs
Normal file
496
BGPLite.Server/BgpSession.cs
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using BGPLite.Configuration;
|
||||
using BGPLite.Protocol;
|
||||
using BGPLite.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BGPLite.Server;
|
||||
|
||||
public sealed class BgpSession : IDisposable
|
||||
{
|
||||
private readonly Socket _socket;
|
||||
private readonly NetworkStream _stream;
|
||||
private readonly PeerConfig _peerConfig;
|
||||
private readonly BgpConfig _bgpConfig;
|
||||
private readonly RouteTable _routeTable;
|
||||
private readonly IRouteFilter _routeFilter;
|
||||
private readonly BgpMetrics _metrics;
|
||||
private readonly ILogger<BgpSession> _logger;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Action<string, uint>? _onPeerIdentified;
|
||||
|
||||
private BgpFsmState _state = BgpFsmState.Idle;
|
||||
private uint _remoteAsn;
|
||||
private bool _remoteFourByteAsn;
|
||||
private bool _localFourByteAsn = true;
|
||||
private ushort _negotiatedHoldTime;
|
||||
private TimeSpan _keepAliveInterval;
|
||||
|
||||
public BgpFsmState State => _state;
|
||||
public PeerConfig Peer => _peerConfig;
|
||||
public bool IsEstablished => _state == BgpFsmState.Established;
|
||||
|
||||
public BgpSession(
|
||||
Socket socket,
|
||||
PeerConfig peerConfig,
|
||||
BgpConfig bgpConfig,
|
||||
RouteTable routeTable,
|
||||
IRouteFilter routeFilter,
|
||||
BgpMetrics metrics,
|
||||
ILogger<BgpSession> logger,
|
||||
Action<string, uint>? onPeerIdentified = null)
|
||||
{
|
||||
_socket = socket;
|
||||
_stream = new NetworkStream(socket, ownsSocket: true);
|
||||
_peerConfig = peerConfig;
|
||||
_bgpConfig = bgpConfig;
|
||||
_routeTable = routeTable;
|
||||
_routeFilter = routeFilter;
|
||||
_metrics = metrics;
|
||||
_logger = logger;
|
||||
_onPeerIdentified = onPeerIdentified;
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
TransitionTo(BgpFsmState.Connect);
|
||||
_metrics.PeerConnected();
|
||||
_logger.LogInformation("PeerConnected {Peer}", _peerConfig.Address);
|
||||
|
||||
// Receive OPEN
|
||||
var openMessage = await ReceiveMessageAsync(linkedCts.Token);
|
||||
if (openMessage is not BgpOpenMessage remoteOpen)
|
||||
{
|
||||
await SendNotificationAsync(BgpConstants.Error.OpenMessageError, BgpConstants.SubError.Unspecific);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("OpenReceived from {Peer} ASN={Asn} Capabilities=[{Caps}]",
|
||||
_peerConfig.Address, remoteOpen.Asn,
|
||||
string.Join(", ", remoteOpen.Capabilities.Select(c => c.Data.Length > 0
|
||||
? $"{c.Code}[{Convert.ToHexString(c.Data)}]"
|
||||
: $"{c.Code}")));
|
||||
|
||||
ValidateOpen(remoteOpen);
|
||||
|
||||
TransitionTo(BgpFsmState.OpenSent);
|
||||
|
||||
// Send our OPEN — adapt capabilities to peer
|
||||
await SendOpenAsync(remoteOpen);
|
||||
_logger.LogInformation("OpenSent to {Peer}", _peerConfig.Address);
|
||||
|
||||
// Send KEEPALIVE (acknowledge OPEN)
|
||||
await SendKeepaliveAsync();
|
||||
_logger.LogDebug("KeepAliveSent to {Peer} (OPEN confirm)", _peerConfig.Address);
|
||||
|
||||
TransitionTo(BgpFsmState.OpenConfirm);
|
||||
|
||||
// Receive KEEPALIVE
|
||||
var response = await ReceiveMessageAsync(linkedCts.Token);
|
||||
_logger.LogInformation("Received {Type} from {Peer} in OpenConfirm", response.Type, _peerConfig.Address);
|
||||
|
||||
switch (response)
|
||||
{
|
||||
case BgpKeepaliveMessage:
|
||||
break;
|
||||
case BgpNotificationMessage notif:
|
||||
var dataHex = notif.Data is { Length: > 0 }
|
||||
? Convert.ToHexString(notif.Data)
|
||||
: "(no data)";
|
||||
_logger.LogWarning(
|
||||
"Peer {Peer} sent NOTIFICATION Error={Error} SubError={SubError} Data={Data}",
|
||||
_peerConfig.Address, notif.ErrorCode, notif.SubErrorCode, dataHex);
|
||||
return;
|
||||
default:
|
||||
_logger.LogError("Unexpected message {Type} from {Peer} in OpenConfirm", response.Type, _peerConfig.Address);
|
||||
await SendNotificationAsync(BgpConstants.Error.FiniteStateMachineError, BgpConstants.SubError.Unspecific);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("KeepAliveReceived from {Peer}", _peerConfig.Address);
|
||||
|
||||
TransitionTo(BgpFsmState.Established);
|
||||
_metrics.SessionEstablished();
|
||||
_logger.LogInformation("SessionEstablished with {Peer} ASN={Asn}", _peerConfig.Address, _remoteAsn);
|
||||
|
||||
// Send initial routes
|
||||
await SendAllRoutesAsync();
|
||||
|
||||
// Run main loop: read messages + send keepalives
|
||||
await RunEstablishedAsync(linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("SessionClosed (cancelled) with {Peer}", _peerConfig.Address);
|
||||
}
|
||||
catch (BgpParseException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Parse error from {Peer}", _peerConfig.Address);
|
||||
await SendNotificationAsync(BgpConstants.Error.MessageHeaderError, BgpConstants.SubError.Unspecific);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Session error with {Peer}", _peerConfig.Address);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TransitionTo(BgpFsmState.Idle);
|
||||
_metrics.SessionClosed();
|
||||
_metrics.PeerDisconnected();
|
||||
_logger.LogInformation("SessionClosed with {Peer}", _peerConfig.Address);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunEstablishedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var keepaliveTimer = new PeriodicTimer(_keepAliveInterval);
|
||||
|
||||
var readTask = ReadLoopAsync(cancellationToken);
|
||||
var keepaliveTask = KeepAliveLoopAsync(keepaliveTimer, cancellationToken);
|
||||
|
||||
await Task.WhenAny(readTask, keepaliveTask);
|
||||
_cts.Cancel();
|
||||
|
||||
try { await readTask; } catch { }
|
||||
try { await keepaliveTask; } catch { }
|
||||
}
|
||||
|
||||
private async Task ReadLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var message = await ReceiveMessageAsync(cancellationToken);
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case BgpUpdateMessage update:
|
||||
_metrics.UpdateReceived();
|
||||
await HandleUpdateAsync(update);
|
||||
break;
|
||||
case BgpKeepaliveMessage:
|
||||
_logger.LogDebug("KeepAliveReceived from {Peer}", _peerConfig.Address);
|
||||
break;
|
||||
case BgpNotificationMessage notif:
|
||||
_logger.LogWarning("NotificationReceived from {Peer}: {Error}/{SubError}",
|
||||
_peerConfig.Address, notif.ErrorCode, notif.SubErrorCode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task KeepAliveLoopAsync(PeriodicTimer timer, CancellationToken cancellationToken)
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
await SendKeepaliveAsync();
|
||||
_logger.LogDebug("KeepAliveSent to {Peer}", _peerConfig.Address);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(BgpUpdateMessage update)
|
||||
{
|
||||
_logger.LogInformation("UpdateReceived from {Peer}: {Withdrawn} withdrawn, {Nlri} announced",
|
||||
_peerConfig.Address, update.WithdrawnRoutes.Count, update.Nlri.Count);
|
||||
|
||||
// Process withdrawals
|
||||
foreach (var w in update.WithdrawnRoutes)
|
||||
{
|
||||
_routeTable.Remove(w.Address, w.Length);
|
||||
_logger.LogDebug("Route withdrawn: {Prefix}", w);
|
||||
}
|
||||
|
||||
// Process announcements
|
||||
if (update.Nlri.Count > 0)
|
||||
{
|
||||
var origin = BgpOrigin.Incomplete;
|
||||
uint nextHop = 0;
|
||||
uint[] asPath = [];
|
||||
uint[] communities = [];
|
||||
|
||||
foreach (var attr in update.PathAttributes)
|
||||
{
|
||||
switch (attr.TypeCode)
|
||||
{
|
||||
case BgpConstants.Attribute.Origin:
|
||||
origin = AttributeHelper.ReadOrigin(attr);
|
||||
break;
|
||||
case BgpConstants.Attribute.AsPath:
|
||||
asPath = AttributeHelper.ReadAsPath(attr, _remoteFourByteAsn);
|
||||
break;
|
||||
case BgpConstants.Attribute.NextHop:
|
||||
nextHop = AttributeHelper.ReadNextHop(attr);
|
||||
break;
|
||||
case BgpConstants.Attribute.Community:
|
||||
communities = AttributeHelper.ReadCommunities(attr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var nlri in update.Nlri)
|
||||
{
|
||||
var route = new Route
|
||||
{
|
||||
Prefix = nlri.Address,
|
||||
PrefixLength = nlri.Length,
|
||||
NextHop = nextHop,
|
||||
AsPath = asPath,
|
||||
Communities = communities
|
||||
};
|
||||
|
||||
if (_routeFilter.AcceptIncoming(route, _peerConfig))
|
||||
{
|
||||
_routeTable.AddOrUpdate(route);
|
||||
_logger.LogDebug("Route added: {Prefix} via {NextHop}", nlri, BgpConstants.UintToIPAddress(nextHop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_metrics.SetRouteCount(_routeTable.Count);
|
||||
}
|
||||
|
||||
private async Task SendAllRoutesAsync()
|
||||
{
|
||||
var routes = _routeTable.GetAll();
|
||||
if (routes.Count == 0) return;
|
||||
|
||||
var nextHop = BgpConstants.IPAddressToUint(_bgpConfig.GetRouterIdAddress());
|
||||
|
||||
const int maxNlriPerUpdate = 100;
|
||||
var sent = 0;
|
||||
var batchRoutes = new List<Route>(maxNlriPerUpdate);
|
||||
|
||||
foreach (var route in routes)
|
||||
{
|
||||
if (!_routeFilter.AcceptOutgoing(route, _peerConfig)) continue;
|
||||
|
||||
batchRoutes.Add(route);
|
||||
|
||||
if (batchRoutes.Count >= maxNlriPerUpdate)
|
||||
{
|
||||
await SendRouteBatchAsync(nextHop, batchRoutes);
|
||||
sent += batchRoutes.Count;
|
||||
batchRoutes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (batchRoutes.Count > 0)
|
||||
{
|
||||
await SendRouteBatchAsync(nextHop, batchRoutes);
|
||||
sent += batchRoutes.Count;
|
||||
}
|
||||
|
||||
_logger.LogInformation("UpdateSent {Count} routes to {Peer}", sent, _peerConfig.Address);
|
||||
}
|
||||
|
||||
private async Task SendRouteBatchAsync(uint nextHop, List<Route> routes)
|
||||
{
|
||||
// Collect all unique communities from batch
|
||||
var allCommunities = routes
|
||||
.SelectMany(r => r.Communities)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var attrs = new List<PathAttribute>
|
||||
{
|
||||
AttributeHelper.WriteOrigin(BgpOrigin.Igp),
|
||||
AttributeHelper.WriteAsPath([_bgpConfig.Asn], _localFourByteAsn),
|
||||
AttributeHelper.WriteNextHop(nextHop)
|
||||
};
|
||||
|
||||
if (allCommunities.Length > 0)
|
||||
attrs.Add(AttributeHelper.WriteCommunities(allCommunities));
|
||||
|
||||
var update = new BgpUpdateMessage
|
||||
{
|
||||
PathAttributes = attrs,
|
||||
Nlri = routes.Select(r => new IpPrefix(r.Prefix, r.PrefixLength)).ToList()
|
||||
};
|
||||
|
||||
await SendMessageAsync(update);
|
||||
_metrics.UpdateSent();
|
||||
}
|
||||
|
||||
private async Task SendUpdateBatchAsync(List<PathAttribute> attrs, List<IpPrefix> nlri)
|
||||
{
|
||||
var update = new BgpUpdateMessage
|
||||
{
|
||||
PathAttributes = attrs,
|
||||
Nlri = nlri
|
||||
};
|
||||
|
||||
await SendMessageAsync(update);
|
||||
_metrics.UpdateSent();
|
||||
}
|
||||
|
||||
#region Message I/O
|
||||
|
||||
private async Task<BgpMessage> ReceiveMessageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var headerBuffer = ArrayPool<byte>.Shared.Rent(BgpConstants.MessageHeaderSize);
|
||||
try
|
||||
{
|
||||
await ReadExactAsync(headerBuffer.AsMemory(0, BgpConstants.MessageHeaderSize), cancellationToken);
|
||||
|
||||
var length = BgpMessageReader.GetMessageLength(headerBuffer);
|
||||
if (length < 0)
|
||||
throw new BgpParseException("Incomplete message header");
|
||||
|
||||
var payloadSize = length - BgpConstants.MessageHeaderSize;
|
||||
var messageBuffer = ArrayPool<byte>.Shared.Rent(length);
|
||||
try
|
||||
{
|
||||
Array.Copy(headerBuffer, messageBuffer, BgpConstants.MessageHeaderSize);
|
||||
|
||||
if (payloadSize > 0)
|
||||
await ReadExactAsync(messageBuffer.AsMemory(BgpConstants.MessageHeaderSize, payloadSize), cancellationToken);
|
||||
|
||||
return BgpMessageReader.ReadMessage(messageBuffer.AsSpan(0, length));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(messageBuffer);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(headerBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(BgpMessage message)
|
||||
{
|
||||
var bufferSize = BgpMessageWriter.GetBufferSize(message);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
try
|
||||
{
|
||||
var written = BgpMessageWriter.WriteMessage(message, buffer);
|
||||
await _stream.WriteAsync(buffer.AsMemory(0, written));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
var totalRead = 0;
|
||||
while (totalRead < buffer.Length)
|
||||
{
|
||||
var read = await _stream.ReadAsync(buffer[totalRead..], cancellationToken);
|
||||
if (read == 0)
|
||||
throw new IOException("Connection closed by peer");
|
||||
totalRead += read;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BGP Messages
|
||||
|
||||
private async Task SendOpenAsync(BgpOpenMessage remoteOpen)
|
||||
{
|
||||
var capabilities = new List<BgpCapabilityInfo>
|
||||
{
|
||||
BgpCapabilityInfo.FourOctetAsn(_bgpConfig.Asn)
|
||||
};
|
||||
|
||||
// Only advertise MP IPv4/Unicast if peer specifically supports IPv4/Unicast
|
||||
if (PeerHasMpIpv4Unicast(remoteOpen.Capabilities))
|
||||
capabilities.Add(BgpCapabilityInfo.MultiprotocolIpv4Unicast());
|
||||
|
||||
// Only advertise Route Refresh if peer also supports it
|
||||
if (remoteOpen.Capabilities.Any(c => c.Code == BgpConstants.Capability.RouteRefresh))
|
||||
capabilities.Add(BgpCapabilityInfo.RouteRefresh());
|
||||
|
||||
var asn16 = _bgpConfig.Asn > ushort.MaxValue ? (ushort)23456 : (ushort)_bgpConfig.Asn;
|
||||
var routerId = BgpConstants.IPAddressToUint(_bgpConfig.GetRouterIdAddress());
|
||||
|
||||
_logger.LogInformation(
|
||||
"Sending OPEN: ASN={Asn} RouterId={RouterId} Capabilities=[{Caps}]",
|
||||
asn16, BgpConstants.UintToIPAddress(routerId),
|
||||
string.Join(", ", capabilities.Select(c => $"Code={c.Code}")));
|
||||
|
||||
var open = new BgpOpenMessage
|
||||
{
|
||||
Version = BgpConstants.BgpVersion,
|
||||
Asn = asn16,
|
||||
HoldTime = (ushort)_bgpConfig.HoldTime,
|
||||
RouterId = routerId,
|
||||
Capabilities = capabilities
|
||||
};
|
||||
|
||||
await SendMessageAsync(open);
|
||||
}
|
||||
|
||||
private static bool PeerHasMpIpv4Unicast(List<BgpCapabilityInfo> caps)
|
||||
{
|
||||
foreach (var cap in caps)
|
||||
{
|
||||
if (cap.Code != BgpConstants.Capability.Multiprotocol || cap.Data.Length < 4) continue;
|
||||
var afi = (ushort)((cap.Data[0] << 8) | cap.Data[1]);
|
||||
var safi = cap.Data[3];
|
||||
if (afi == BgpConstants.Afi.IPv4 && safi == BgpConstants.Safi.Unicast)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Task SendKeepaliveAsync() => SendMessageAsync(BgpKeepaliveMessage.Instance);
|
||||
|
||||
private async Task SendNotificationAsync(byte errorCode, byte subErrorCode)
|
||||
{
|
||||
var notification = new BgpNotificationMessage { ErrorCode = errorCode, SubErrorCode = subErrorCode };
|
||||
await SendMessageAsync(notification);
|
||||
_logger.LogInformation("NotificationSent to {Peer}: {Error}/{SubError}", _peerConfig.Address, errorCode, subErrorCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation
|
||||
|
||||
private void ValidateOpen(BgpOpenMessage open)
|
||||
{
|
||||
if (open.Version != BgpConstants.BgpVersion)
|
||||
throw new BgpParseException($"Unsupported BGP version: {open.Version}");
|
||||
|
||||
_remoteFourByteAsn = CapabilityHelper.GetRemoteAsn(open).HasValue;
|
||||
_remoteAsn = CapabilityHelper.GetEffectiveAsn(open);
|
||||
|
||||
_onPeerIdentified?.Invoke(_peerConfig.Address, _remoteAsn);
|
||||
|
||||
if (_peerConfig.RemoteAsn.HasValue && _remoteAsn != _peerConfig.RemoteAsn.Value)
|
||||
throw new BgpParseException($"Unexpected ASN: expected {_peerConfig.RemoteAsn}, got {_remoteAsn}");
|
||||
|
||||
var holdTime = open.HoldTime;
|
||||
if (holdTime != 0 && holdTime < 3)
|
||||
throw new BgpParseException($"Unacceptable hold time: {holdTime}");
|
||||
|
||||
_negotiatedHoldTime = holdTime;
|
||||
_keepAliveInterval = holdTime == 0
|
||||
? TimeSpan.FromSeconds(_bgpConfig.KeepAlive)
|
||||
: TimeSpan.FromSeconds(Math.Max(holdTime / 3, 1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void TransitionTo(BgpFsmState newState)
|
||||
{
|
||||
_logger.LogDebug("FSM: {Old} → {New} for {Peer}", _state, newState, _peerConfig.Address);
|
||||
_state = newState;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_stream.Dispose();
|
||||
_socket.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
10
BGPLite.Server/BgpTimers.cs
Normal file
10
BGPLite.Server/BgpTimers.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using BGPLite.Protocol;
|
||||
|
||||
namespace BGPLite.Server;
|
||||
|
||||
public sealed class BgpTimers
|
||||
{
|
||||
public TimeSpan KeepAliveInterval { get; init; } = TimeSpan.FromSeconds(BgpConstants.DefaultKeepAlive);
|
||||
public TimeSpan HoldTime { get; init; } = TimeSpan.FromSeconds(BgpConstants.DefaultHoldTime);
|
||||
public TimeSpan ConnectRetryInterval { get; init; } = TimeSpan.FromSeconds(BgpConstants.ConnectRetryDelay);
|
||||
}
|
||||
29
BGPLite.Tests/BGPLite.Tests.csproj
Normal file
29
BGPLite.Tests/BGPLite.Tests.csproj
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BGPLite.Protocol\BGPLite.Protocol.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Server\BGPLite.Server.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Routing\BGPLite.Routing.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Configuration\BGPLite.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
225
BGPLite.Tests/BgpMessageTests.cs
Normal file
225
BGPLite.Tests/BgpMessageTests.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
using System.Buffers.Binary;
|
||||
using BGPLite.Protocol;
|
||||
|
||||
namespace BGPLite.Tests;
|
||||
|
||||
public class BgpMessageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Keepalive_WriteThenRead_Roundtrip()
|
||||
{
|
||||
var buffer = new byte[64];
|
||||
var written = BgpMessageWriter.WriteMessage(BgpKeepaliveMessage.Instance, buffer);
|
||||
|
||||
Assert.Equal(BgpConstants.MessageHeaderSize, written);
|
||||
|
||||
var message = BgpMessageReader.ReadMessage(buffer.AsSpan(0, written));
|
||||
Assert.IsType<BgpKeepaliveMessage>(message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_WriteThenRead_Roundtrip()
|
||||
{
|
||||
var open = new BgpOpenMessage
|
||||
{
|
||||
Version = 4,
|
||||
Asn = 65444,
|
||||
HoldTime = 180,
|
||||
RouterId = 0x334B4214,
|
||||
Capabilities =
|
||||
[
|
||||
BgpCapabilityInfo.FourOctetAsn(65444),
|
||||
BgpCapabilityInfo.RouteRefresh(),
|
||||
BgpCapabilityInfo.MultiprotocolIpv4Unicast()
|
||||
]
|
||||
};
|
||||
|
||||
var buffer = new byte[512];
|
||||
var written = BgpMessageWriter.WriteMessage(open, buffer);
|
||||
|
||||
var message = BgpMessageReader.ReadMessage(buffer.AsSpan(0, written));
|
||||
var readOpen = Assert.IsType<BgpOpenMessage>(message);
|
||||
|
||||
Assert.Equal((byte)4, readOpen.Version);
|
||||
Assert.Equal((ushort)65444, readOpen.Asn);
|
||||
Assert.Equal((ushort)180, readOpen.HoldTime);
|
||||
Assert.Equal((uint)0x334B4214, readOpen.RouterId);
|
||||
Assert.Equal(3, readOpen.Capabilities.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_FourOctetAsn_CapabilityRoundtrip()
|
||||
{
|
||||
var asn = 200000u;
|
||||
var open = new BgpOpenMessage
|
||||
{
|
||||
Asn = 23456, // AS_TRANS
|
||||
HoldTime = 60,
|
||||
RouterId = 0x01020304,
|
||||
Capabilities = [BgpCapabilityInfo.FourOctetAsn(asn)]
|
||||
};
|
||||
|
||||
var buffer = new byte[256];
|
||||
var written = BgpMessageWriter.WriteMessage(open, buffer);
|
||||
var readOpen = Assert.IsType<BgpOpenMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
var effectiveAsn = CapabilityHelper.GetEffectiveAsn(readOpen);
|
||||
Assert.Equal(asn, effectiveAsn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notification_WriteThenRead_Roundtrip()
|
||||
{
|
||||
var notif = new BgpNotificationMessage
|
||||
{
|
||||
ErrorCode = 2,
|
||||
SubErrorCode = 2
|
||||
};
|
||||
|
||||
var buffer = new byte[64];
|
||||
var written = BgpMessageWriter.WriteMessage(notif, buffer);
|
||||
var readNotif = Assert.IsType<BgpNotificationMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
Assert.Equal((byte)2, readNotif.ErrorCode);
|
||||
Assert.Equal((byte)2, readNotif.SubErrorCode);
|
||||
Assert.Null(readNotif.Data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Notification_WithData_Roundtrip()
|
||||
{
|
||||
var notif = new BgpNotificationMessage
|
||||
{
|
||||
ErrorCode = 2,
|
||||
SubErrorCode = 1,
|
||||
Data = [4]
|
||||
};
|
||||
|
||||
var buffer = new byte[64];
|
||||
var written = BgpMessageWriter.WriteMessage(notif, buffer);
|
||||
var readNotif = Assert.IsType<BgpNotificationMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
Assert.Equal((byte)2, readNotif.ErrorCode);
|
||||
Assert.Equal((byte)1, readNotif.SubErrorCode);
|
||||
Assert.Single(readNotif.Data!);
|
||||
Assert.Equal((byte)4, readNotif.Data[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Empty_Roundtrip()
|
||||
{
|
||||
var update = new BgpUpdateMessage();
|
||||
|
||||
var buffer = new byte[64];
|
||||
var written = BgpMessageWriter.WriteMessage(update, buffer);
|
||||
var readUpdate = Assert.IsType<BgpUpdateMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
Assert.Empty(readUpdate.WithdrawnRoutes);
|
||||
Assert.Empty(readUpdate.PathAttributes);
|
||||
Assert.Empty(readUpdate.Nlri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithRoutesAndAttributes_Roundtrip()
|
||||
{
|
||||
var update = new BgpUpdateMessage
|
||||
{
|
||||
PathAttributes =
|
||||
[
|
||||
AttributeHelper.WriteOrigin(BgpOrigin.Igp),
|
||||
AttributeHelper.WriteAsPath([65444u, 65001u], fourByteAsn: true),
|
||||
AttributeHelper.WriteNextHop(0xC0A80101),
|
||||
AttributeHelper.WriteCommunities([0x0000FF01])
|
||||
],
|
||||
Nlri =
|
||||
[
|
||||
new IpPrefix(0xC0A80000, 24), // 192.168.0.0/24
|
||||
new IpPrefix(0x0A000000, 8) // 10.0.0.0/8
|
||||
]
|
||||
};
|
||||
|
||||
var buffer = new byte[512];
|
||||
var written = BgpMessageWriter.WriteMessage(update, buffer);
|
||||
var readUpdate = Assert.IsType<BgpUpdateMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
Assert.Equal(2, readUpdate.Nlri.Count);
|
||||
Assert.Equal(0xC0A80000u, readUpdate.Nlri[0].Address);
|
||||
Assert.Equal((byte)24, readUpdate.Nlri[0].Length);
|
||||
Assert.Equal(0x0A000000u, readUpdate.Nlri[1].Address);
|
||||
Assert.Equal((byte)8, readUpdate.Nlri[1].Length);
|
||||
|
||||
Assert.Equal(4, readUpdate.PathAttributes.Count);
|
||||
|
||||
// Verify AS_PATH with 4-byte ASN
|
||||
var asPathAttr = readUpdate.PathAttributes.First(a => a.TypeCode == BgpConstants.Attribute.AsPath);
|
||||
var ases = AttributeHelper.ReadAsPath(asPathAttr, fourByteAsn: true);
|
||||
Assert.Equal([65444u, 65001u], ases);
|
||||
|
||||
// Verify NEXT_HOP
|
||||
var nextHopAttr = readUpdate.PathAttributes.First(a => a.TypeCode == BgpConstants.Attribute.NextHop);
|
||||
Assert.Equal(0xC0A80101u, AttributeHelper.ReadNextHop(nextHopAttr));
|
||||
|
||||
// Verify COMMUNITY
|
||||
var commAttr = readUpdate.PathAttributes.First(a => a.TypeCode == BgpConstants.Attribute.Community);
|
||||
var communities = AttributeHelper.ReadCommunities(commAttr);
|
||||
Assert.Single(communities);
|
||||
Assert.Equal(0x0000FF01u, communities[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithWithdrawals_Roundtrip()
|
||||
{
|
||||
var update = new BgpUpdateMessage
|
||||
{
|
||||
WithdrawnRoutes = [new IpPrefix(0xC0A80000, 24)]
|
||||
};
|
||||
|
||||
var buffer = new byte[256];
|
||||
var written = BgpMessageWriter.WriteMessage(update, buffer);
|
||||
var readUpdate = Assert.IsType<BgpUpdateMessage>(BgpMessageReader.ReadMessage(buffer.AsSpan(0, written)));
|
||||
|
||||
Assert.Single(readUpdate.WithdrawnRoutes);
|
||||
Assert.Equal(0xC0A80000u, readUpdate.WithdrawnRoutes[0].Address);
|
||||
Assert.Equal((byte)24, readUpdate.WithdrawnRoutes[0].Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Marker_IsAllOnes()
|
||||
{
|
||||
for (var i = 0; i < 16; i++)
|
||||
Assert.Equal(0xFF, BgpConstants.Marker[i]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBufferSize_MatchesWriteSize()
|
||||
{
|
||||
var open = new BgpOpenMessage
|
||||
{
|
||||
Asn = 100,
|
||||
HoldTime = 60,
|
||||
RouterId = 0x01020304,
|
||||
Capabilities = [BgpCapabilityInfo.FourOctetAsn(100)]
|
||||
};
|
||||
|
||||
var expectedSize = BgpMessageWriter.GetBufferSize(open);
|
||||
var buffer = new byte[expectedSize];
|
||||
var actualSize = BgpMessageWriter.WriteMessage(open, buffer);
|
||||
|
||||
Assert.Equal(expectedSize, actualSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadMessage_InvalidMarker_Throws()
|
||||
{
|
||||
var buffer = new byte[19];
|
||||
// All zeros — invalid marker
|
||||
Assert.Throws<BgpParseException>(() => BgpMessageReader.ReadMessage(buffer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadMessage_TooShort_Throws()
|
||||
{
|
||||
var buffer = new byte[10];
|
||||
Assert.Throws<BgpParseException>(() => BgpMessageReader.ReadMessage(buffer));
|
||||
}
|
||||
}
|
||||
70
BGPLite.Tests/ConfigurationTests.cs
Normal file
70
BGPLite.Tests/ConfigurationTests.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using BGPLite.Configuration;
|
||||
|
||||
namespace BGPLite.Tests;
|
||||
|
||||
public class ConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void LoadFromText_ParsesValidYaml()
|
||||
{
|
||||
var yaml = """
|
||||
Bgp:
|
||||
Asn: 65444
|
||||
RouterId: 10.0.0.1
|
||||
KeepAlive: 60
|
||||
HoldTime: 180
|
||||
|
||||
Peers:
|
||||
- Address: 10.0.0.2
|
||||
RemoteAsn: 65001
|
||||
Description: "upstream"
|
||||
""";
|
||||
|
||||
var config = ConfigLoader.LoadFromText(yaml);
|
||||
|
||||
Assert.Equal(65444u, config.Bgp.Asn);
|
||||
Assert.Equal("10.0.0.1", config.Bgp.RouterId);
|
||||
Assert.Equal(60, config.Bgp.KeepAlive);
|
||||
Assert.Equal(180, config.Bgp.HoldTime);
|
||||
|
||||
Assert.Single(config.Peers);
|
||||
Assert.Equal("10.0.0.2", config.Peers[0].Address);
|
||||
Assert.Equal(65001u, config.Peers[0].RemoteAsn);
|
||||
Assert.Equal("upstream", config.Peers[0].Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromText_MultiplePeers()
|
||||
{
|
||||
var yaml = """
|
||||
Bgp:
|
||||
Asn: 65444
|
||||
RouterId: 10.0.0.1
|
||||
|
||||
Peers:
|
||||
- Address: 10.0.0.2
|
||||
RemoteAsn: 65001
|
||||
- Address: 10.0.0.3
|
||||
RemoteAsn: 65002
|
||||
""";
|
||||
|
||||
var config = ConfigLoader.LoadFromText(yaml);
|
||||
Assert.Equal(2, config.Peers.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromText_DefaultValues()
|
||||
{
|
||||
var yaml = """
|
||||
Bgp:
|
||||
Asn: 65444
|
||||
RouterId: 10.0.0.1
|
||||
""";
|
||||
|
||||
var config = ConfigLoader.LoadFromText(yaml);
|
||||
|
||||
Assert.Equal(60, config.Bgp.KeepAlive);
|
||||
Assert.Equal(180, config.Bgp.HoldTime);
|
||||
Assert.Empty(config.Peers);
|
||||
}
|
||||
}
|
||||
84
BGPLite.Tests/PrefixCodecTests.cs
Normal file
84
BGPLite.Tests/PrefixCodecTests.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using BGPLite.Protocol;
|
||||
|
||||
namespace BGPLite.Tests;
|
||||
|
||||
public class PrefixCodecTests
|
||||
{
|
||||
[Fact]
|
||||
public void Encode_24bitPrefix_3bytes()
|
||||
{
|
||||
var prefix = new IpPrefix(0xC0A80000, 24); // 192.168.0.0/24
|
||||
var buffer = new byte[8];
|
||||
var written = PrefixCodec.Encode(prefix, buffer);
|
||||
|
||||
Assert.Equal(4, written);
|
||||
Assert.Equal((byte)24, buffer[0]);
|
||||
Assert.Equal(0xC0, buffer[1]);
|
||||
Assert.Equal(0xA8, buffer[2]);
|
||||
Assert.Equal(0x00, buffer[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encode_8bitPrefix_2bytes()
|
||||
{
|
||||
var prefix = new IpPrefix(0x0A000000, 8); // 10.0.0.0/8
|
||||
var buffer = new byte[8];
|
||||
var written = PrefixCodec.Encode(prefix, buffer);
|
||||
|
||||
Assert.Equal(2, written);
|
||||
Assert.Equal((byte)8, buffer[0]);
|
||||
Assert.Equal(0x0A, buffer[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encode_32bitPrefix_5bytes()
|
||||
{
|
||||
var prefix = new IpPrefix(0x01020304, 32); // 1.2.3.4/32
|
||||
var buffer = new byte[8];
|
||||
var written = PrefixCodec.Encode(prefix, buffer);
|
||||
|
||||
Assert.Equal(5, written);
|
||||
Assert.Equal((byte)32, buffer[0]);
|
||||
Assert.Equal(0x01, buffer[1]);
|
||||
Assert.Equal(0x02, buffer[2]);
|
||||
Assert.Equal(0x03, buffer[3]);
|
||||
Assert.Equal(0x04, buffer[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encode_DefaultRoute_1byte()
|
||||
{
|
||||
var prefix = new IpPrefix(0, 0); // 0.0.0.0/0
|
||||
var buffer = new byte[8];
|
||||
var written = PrefixCodec.Encode(prefix, buffer);
|
||||
|
||||
Assert.Equal(1, written);
|
||||
Assert.Equal((byte)0, buffer[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_VariousPrefixes()
|
||||
{
|
||||
var prefixes = new[]
|
||||
{
|
||||
new IpPrefix(0xC0A80000, 24),
|
||||
new IpPrefix(0x0A000000, 8),
|
||||
new IpPrefix(0x01020304, 32),
|
||||
new IpPrefix(0, 0),
|
||||
new IpPrefix(0xAC100000, 20)
|
||||
};
|
||||
|
||||
var buffer = new byte[64];
|
||||
var written = PrefixCodec.EncodeList(prefixes, buffer);
|
||||
|
||||
var decoded = new IpPrefix[prefixes.Length];
|
||||
var count = PrefixCodec.DecodeList(buffer, written, decoded);
|
||||
|
||||
Assert.Equal(prefixes.Length, count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(prefixes[i].Address, decoded[i].Address);
|
||||
Assert.Equal(prefixes[i].Length, decoded[i].Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
BGPLite.Tests/RouteTableTests.cs
Normal file
68
BGPLite.Tests/RouteTableTests.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using BGPLite.Routing;
|
||||
|
||||
namespace BGPLite.Tests;
|
||||
|
||||
public class RouteTableTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddOrUpdate_NewRoute_ReturnsTrue()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
var route = new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x01020304 };
|
||||
|
||||
Assert.True(table.AddOrUpdate(route));
|
||||
Assert.Equal(1, table.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_ExistingRoute_ReturnsFalse()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
var route1 = new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x01020304 };
|
||||
var route2 = new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x05060708 };
|
||||
|
||||
table.AddOrUpdate(route1);
|
||||
Assert.False(table.AddOrUpdate(route2));
|
||||
Assert.Equal(1, table.Count);
|
||||
|
||||
var stored = table.Get(0xC0A80000, 24);
|
||||
Assert.Equal(0x05060708u, stored!.NextHop);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_ExistingRoute_ReturnsTrue()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
table.AddOrUpdate(new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x01020304 });
|
||||
|
||||
Assert.True(table.Remove(0xC0A80000, 24));
|
||||
Assert.Equal(0, table.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_NonExistingRoute_ReturnsFalse()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
Assert.False(table.Remove(0xC0A80000, 24));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ReturnsAllRoutes()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
table.AddOrUpdate(new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x01020304 });
|
||||
table.AddOrUpdate(new Route { Prefix = 0x0A000000, PrefixLength = 8, NextHop = 0x05060708 });
|
||||
|
||||
var routes = table.GetAll();
|
||||
Assert.Equal(2, routes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllRoutes()
|
||||
{
|
||||
var table = new RouteTable();
|
||||
table.AddOrUpdate(new Route { Prefix = 0xC0A80000, PrefixLength = 24, NextHop = 0x01020304 });
|
||||
table.Clear();
|
||||
Assert.Equal(0, table.Count);
|
||||
}
|
||||
}
|
||||
118
BGPLite.sln
Normal file
118
BGPLite.sln
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite", "BGPLite\BGPLite.csproj", "{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Protocol", "BGPLite.Protocol\BGPLite.Protocol.csproj", "{693CF970-8957-442A-8F21-F15DEDDA6D97}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Server", "BGPLite.Server\BGPLite.Server.csproj", "{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Routing", "BGPLite.Routing\BGPLite.Routing.csproj", "{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Configuration", "BGPLite.Configuration\BGPLite.Configuration.csproj", "{4889B8D6-8090-4866-AD15-285AE46A2DA9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Tests", "BGPLite.Tests\BGPLite.Tests.csproj", "{62464139-51DE-49A4-AF93-B354CC0A7CE2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGPLite.Api", "BGPLite.Api\BGPLite.Api.csproj", "{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3D444769-0A70-4C53-86A5-AEB2BDAAAA1B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|x64.Build.0 = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{693CF970-8957-442A-8F21-F15DEDDA6D97}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9B7F52CE-ECBA-4942-A7E5-CFCD1D9C5B5E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5857DD14-B9BD-4D3B-8ADD-08F2978BA53B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4889B8D6-8090-4866-AD15-285AE46A2DA9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{62464139-51DE-49A4-AF93-B354CC0A7CE2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{27ECD1EF-8683-4E79-BC83-92C9D9DEFD7B}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
27
BGPLite/BGPLite.csproj
Normal file
27
BGPLite/BGPLite.csproj
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BGPLite.Protocol\BGPLite.Protocol.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Server\BGPLite.Server.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Routing\BGPLite.Routing.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Configuration\BGPLite.Configuration.csproj" />
|
||||
<ProjectReference Include="..\BGPLite.Api\BGPLite.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\appsettings.yml" Link="appsettings.yml" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="..\nets.txt" Link="nets.txt" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
114
BGPLite/Program.cs
Normal file
114
BGPLite/Program.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System.Net;
|
||||
using BGPLite.Api;
|
||||
using BGPLite.Configuration;
|
||||
using BGPLite.Protocol;
|
||||
using BGPLite.Routing;
|
||||
using BGPLite.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var dataDir = Environment.GetEnvironmentVariable("BGPLITE_DATA") ?? Path.Combine(baseDir, "data");
|
||||
|
||||
var configPath = args.Length > 0 ? Path.GetFullPath(args[0]) : Path.Combine(baseDir, "appsettings.yml");
|
||||
var config = ConfigLoader.Load(configPath);
|
||||
|
||||
var routeTable = new RouteTable();
|
||||
var nextHop = BgpConstants.IPAddressToUint(config.Bgp.GetRouterIdAddress());
|
||||
|
||||
// Load default nets.txt (no community)
|
||||
var netsPath = Path.Combine(baseDir, "nets.txt");
|
||||
if (File.Exists(netsPath))
|
||||
{
|
||||
var count = LoadPrefixes(netsPath, nextHop, [], routeTable);
|
||||
Console.WriteLine($"Loaded {count} prefixes from nets.txt");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"nets.txt not found at {netsPath}");
|
||||
}
|
||||
|
||||
// Load community files from communities/ directory
|
||||
var communitiesDir = Path.Combine(baseDir, "communities");
|
||||
if (Directory.Exists(communitiesDir))
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(communitiesDir, "*.txt"))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
var underscore = fileName.IndexOf('_');
|
||||
if (underscore < 0) continue;
|
||||
|
||||
var asn = uint.Parse(fileName[..underscore]);
|
||||
var value = uint.Parse(fileName[(underscore + 1)..]);
|
||||
var community = (asn << 16) | (value & 0xFFFF);
|
||||
var communities = new uint[] { community };
|
||||
|
||||
var count = LoadPrefixes(file, nextHop, communities, routeTable);
|
||||
Console.WriteLine($"Loaded {count} prefixes with community {asn}:{value} from {fileName}.txt");
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite peer store
|
||||
var dbPath = Path.Combine(dataDir, "bgplite.db");
|
||||
var peerStore = new PeerStore(dbPath);
|
||||
Console.WriteLine($"Peer database: {dbPath}");
|
||||
|
||||
// Route filter — reads communities from PeerStore
|
||||
var peerFilter = new PeerCommunityFilter(ip => peerStore.GetCommunities(ip));
|
||||
|
||||
builder.Services.AddSingleton(config);
|
||||
builder.Services.AddSingleton(config.Bgp);
|
||||
builder.Services.AddSingleton(routeTable);
|
||||
builder.Services.AddSingleton<IRouteFilter>(peerFilter);
|
||||
builder.Services.AddSingleton(peerStore);
|
||||
builder.Services.AddSingleton(new BgpMetrics());
|
||||
builder.Services.AddHostedService(sp =>
|
||||
{
|
||||
var store = sp.GetRequiredService<PeerStore>();
|
||||
return new BgpServer(
|
||||
sp.GetRequiredService<AppConfig>(),
|
||||
sp.GetRequiredService<RouteTable>(),
|
||||
sp.GetRequiredService<IRouteFilter>(),
|
||||
sp.GetRequiredService<BgpMetrics>(),
|
||||
sp.GetRequiredService<ILogger<BgpSession>>(),
|
||||
sp.GetRequiredService<ILogger<BgpServer>>(),
|
||||
(ip, asn) => store.UpsertPeer(ip, asn));
|
||||
});
|
||||
builder.Services.AddHostedService<ManagementApi>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogInformation("BGPLite starting — ASN={Asn}, RouterId={RouterId}", config.Bgp.Asn, config.Bgp.RouterId);
|
||||
logger.LogInformation("Loaded routes: {RouteCount}", routeTable.Count);
|
||||
|
||||
await host.RunAsync();
|
||||
return;
|
||||
|
||||
static int LoadPrefixes(string path, uint nextHop, uint[] communities, RouteTable routeTable)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue;
|
||||
|
||||
var slash = trimmed.IndexOf('/');
|
||||
var ip = IPAddress.Parse(trimmed[..slash]);
|
||||
var length = byte.Parse(trimmed[(slash + 1)..]);
|
||||
var prefix = BgpConstants.IPAddressToUint(ip);
|
||||
|
||||
routeTable.AddOrUpdate(new Route
|
||||
{
|
||||
Prefix = prefix,
|
||||
PrefixLength = length,
|
||||
NextHop = nextHop,
|
||||
Communities = communities
|
||||
});
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY BGPLite.sln .
|
||||
COPY global.json .
|
||||
COPY BGPLite/BGPLite.csproj BGPLite/
|
||||
COPY BGPLite.Protocol/BGPLite.Protocol.csproj BGPLite.Protocol/
|
||||
COPY BGPLite.Server/BGPLite.Server.csproj BGPLite.Server/
|
||||
COPY BGPLite.Routing/BGPLite.Routing.csproj BGPLite.Routing/
|
||||
COPY BGPLite.Configuration/BGPLite.Configuration.csproj BGPLite.Configuration/
|
||||
COPY BGPLite.Api/BGPLite.Api.csproj BGPLite.Api/
|
||||
COPY BGPLite.Tests/BGPLite.Tests.csproj BGPLite.Tests/
|
||||
RUN dotnet restore
|
||||
|
||||
COPY . .
|
||||
RUN dotnet publish BGPLite/BGPLite.csproj -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 179/tcp 5000/tcp
|
||||
|
||||
ENTRYPOINT ["dotnet", "BGPLite.dll"]
|
||||
10
appsettings.Example.yml
Normal file
10
appsettings.Example.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Bgp:
|
||||
Asn: 65444
|
||||
RouterId: 1.2.3.4
|
||||
KeepAlive: 60
|
||||
HoldTime: 180
|
||||
|
||||
Peers:
|
||||
- Address: 10.0.0.1
|
||||
RemoteAsn: 65001
|
||||
Description: "example-peer"
|
||||
7
global.json
Normal file
7
global.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "10.0.0",
|
||||
"rollForward": "latestMajor",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue