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:
Mikhail Movchan 2026-06-05 19:30:18 +03:00
commit 9a6c0af00b
49 changed files with 3097 additions and 0 deletions

98
.gitignore vendored Normal file
View 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/

View 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>

View 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
View 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; } = [];
}

View 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; } = [];
}

View 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>

View 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);
}

View 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);
}

View 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);
}

View 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
};
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View 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);
}

View 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]);
}

View file

@ -0,0 +1,10 @@
namespace BGPLite.Protocol;
public enum BgpFsmState
{
Idle,
Connect,
OpenSent,
OpenConfirm,
Established
}

View 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();
}

View file

@ -0,0 +1,6 @@
namespace BGPLite.Protocol;
public abstract class BgpMessage
{
public abstract BgpMessageType Type { get; }
}

View 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) { }
}

View file

@ -0,0 +1,9 @@
namespace BGPLite.Protocol;
public enum BgpMessageType : byte
{
Open = 1,
Update = 2,
Notification = 3,
Keepalive = 4
}

View 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
}

View 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; }
}

View 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; } = [];
}

View file

@ -0,0 +1,8 @@
namespace BGPLite.Protocol;
public enum BgpOrigin : byte
{
Igp = 0,
Egp = 1,
Incomplete = 2
}

View 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; } = [];
}

View 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;
}
}

View 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}";
}

View 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;
}

View 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;
}
}

View 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;
}

View 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>

View 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);
}

View 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
View 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);
}

View 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();
}

View 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>

View 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
View 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();
}
}

View 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();
}
}

View 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);
}

View 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>

View 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));
}
}

View 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);
}
}

View 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);
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}