WIP DHCP server

dhcp-server
Pat Hartl 2023-09-13 18:26:06 -05:00
parent 4476fbed5f
commit 78dd2796eb
14 changed files with 2023 additions and 1 deletions

View File

@ -0,0 +1,70 @@
@using System.Net.NetworkInformation
<RadioGroup @bind-Value="Value" Class="network-interfaces" OnChange="ValueChanged">
<Collapse Accordion="true">
@foreach (var nic in NetworkInterfaces)
{
<Panel Key="@nic.Id" ShowArrow="false">
<HeaderTemplate>
<Space>
<SpaceItem>
<Radio Value="@nic.Id" Checked="Value == nic.Id" />
</SpaceItem>
<SpaceItem>
@switch (nic.NetworkInterfaceType)
{
case NetworkInterfaceType.Wireless80211:
case NetworkInterfaceType.Wwanpp:
case NetworkInterfaceType.Wwanpp2:
case NetworkInterfaceType.Wman:
<Icon Type="@IconType.Outline.Wifi" />
break;
case NetworkInterfaceType.Loopback:
<Icon Type="@IconType.Outline.Api" />
break;
default:
<Icon Type="@IconType.Outline.Partition" />
break;
}
</SpaceItem>
<SpaceItem>
<Text Strong>@nic.Name</Text>
<br />
<Text>@nic.Description</Text>
</SpaceItem>
</Space>
</HeaderTemplate>
<ChildContent>
<div class="vertical-table">
<div class="vertical-table-row">
<div class="vertical-table-th">Media state:</div>
<div class="vertical-table-td">@nic.OperationalStatus</div>
</div>
<div class="vertical-table-row">
<div class="vertical-table-th">Bytes sent:</div>
<div class="vertical-table-td">@ByteSizeLib.ByteSize.FromBytes(nic.GetIPStatistics().BytesSent).ToString()</div>
</div>
<div class="vertical-table-row">
<div class="vertical-table-th">Bytes received:</div>
<div class="vertical-table-td">@ByteSizeLib.ByteSize.FromBytes(nic.GetIPStatistics().BytesReceived).ToString()</div>
</div>
<div class="vertical-table-row">
<div class="vertical-table-th">Link speed:</div>
<div class="vertical-table-td">@(nic.Speed / 1000000) (Mbps)</div>
</div>
</div>
</ChildContent>
</Panel>
}
</Collapse>
</RadioGroup>
@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
IEnumerable<NetworkInterface> NetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
}

View File

@ -123,5 +123,7 @@ namespace LANCommander.Data
public DbSet<Server>? Servers { get; set; }
public DbSet<ServerConsole>? ServerConsoles { get; set; }
public DbSet<DHCPLease>? DHCPLeases { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LANCommander.Data.Models
{
[Table("DHCPLeases")]
public class DHCPLease : BaseModel
{
[Display(Name = "MAC Address")]
public byte[] MACAddress { get; set; }
[Display(Name = "IP Address")]
public byte[] IPAddress { get; set; }
}
}

View File

@ -28,6 +28,7 @@
<PackageReference Include="BlazorMonaco" Version="3.1.0" />
<PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="CoreRCON" Version="5.0.5" />
<PackageReference Include="DotNetProjects.DhcpServer" Version="2.0.26" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.5" />
<PackageReference Include="Hangfire.Core" Version="1.8.5" />
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
@ -51,6 +52,7 @@
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.9" />
<PackageReference Include="NetworkPrimitives" Version="1.1.0" />
<PackageReference Include="NLog" Version="5.2.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.3" />
<PackageReference Include="rix0rrr.BeaconLib" Version="1.0.2" />

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddDHCPLeases : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DHCPLeases",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
MACAddress = table.Column<byte[]>(type: "BLOB", nullable: false),
IPAddress = table.Column<byte[]>(type: "BLOB", nullable: false),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedById = table.Column<Guid>(type: "TEXT", nullable: true),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedById = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DHCPLeases", x => x.Id);
table.ForeignKey(
name: "FK_DHCPLeases_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_DHCPLeases_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_DHCPLeases_CreatedById",
table: "DHCPLeases",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_DHCPLeases_UpdatedById",
table: "DHCPLeases",
column: "UpdatedById");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DHCPLeases");
}
}
}

View File

@ -268,6 +268,41 @@ namespace LANCommander.Migrations
b.ToTable("Companies");
});
modelBuilder.Entity("LANCommander.Data.Models.DHCPLease", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<byte[]>("IPAddress")
.IsRequired()
.HasColumnType("BLOB");
b.Property<byte[]>("MACAddress")
.IsRequired()
.HasColumnType("BLOB");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("UpdatedById");
b.ToTable("DHCPLeases");
});
modelBuilder.Entity("LANCommander.Data.Models.Game", b =>
{
b.Property<Guid>("Id")
@ -1120,6 +1155,21 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.DHCPLease", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.Game", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")

View File

@ -1,4 +1,6 @@
namespace LANCommander.Models
using DotNetProjects.DhcpServer;
namespace LANCommander.Models
{
public enum LANCommanderTheme
{
@ -18,6 +20,7 @@
public LANCommanderAuthenticationSettings Authentication { get; set; } = new LANCommanderAuthenticationSettings();
public LANCommanderArchiveSettings Archives { get; set; } = new LANCommanderArchiveSettings();
public LANCommanderIPXRelaySettings IPXRelay { get; set; } = new LANCommanderIPXRelaySettings();
public LANCommanderDHCPServerSettings DHCPServer { get; set; } = new LANCommanderDHCPServerSettings();
}
public class LANCommanderAuthenticationSettings
@ -44,4 +47,19 @@
public int Port { get; set; } = 213;
public bool Logging { get; set; } = false;
}
public class LANCommanderDHCPServerSettings
{
public bool Enabled { get; set; } = false;
public string NetworkInterface { get; set; }
public string SubnetMask { get; set; } = "255.255.255.0";
public string Network { get; set; } = "10.0.0.0";
public string RangeStart { get; set; } = "10.0.0.30";
public string RangeEnd { get; set; } = "10.0.0.254";
public string Domain { get; set; } = "LANCommander";
public string ServerIdentifier { get; set; } = "10.0.0.1";
public string DefaultGateway = "10.0.0.1";
public string[] DnsServers = new string[] { "10.0.0.1" };
public NetworkRoute[] StaticRoutes;
}
}

View File

@ -0,0 +1,90 @@
@page "/Settings/DHCP"
@page "/Settings/DHCP/Configuration"
@using LANCommander.Models;
@layout SettingsLayout
@inject SettingService SettingService
@inject DHCPService DHCPService
@inject IMessageService MessageService
@attribute [Authorize(Roles = "Administrator")]
<PageHeader Title="DHCP Server" />
<div style="padding: 0 24px;">
<Form @ref="Form" Model="Settings" Layout="@FormLayout.Vertical" ValidateMode="FormValidateMode.Rules" ValidateOnChange="true">
<FormItem Label="Enable">
<Switch @bind-Checked="context.DHCPServer.Enabled" />
</FormItem>
<FormItem Label="Network Interface">
<NetworkInterfacePicker @bind-Value="context.DHCPServer.NetworkInterface" />
</FormItem>
<FormItem Label="Subnet Mask" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.SubnetMask" />
</FormItem>
<FormItem Label="Range Start" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.Network" />
</FormItem>
<FormItem Label="Range Start" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.RangeStart" />
</FormItem>
<FormItem Label="Range End" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.RangeEnd" />
</FormItem>
<FormItem Label="Default Gateway" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.DefaultGateway" />
</FormItem>
<FormItem Label="Server Identifier" Rules="@(new FormValidationRule[] { IPAddressValidationRule })">
<Input @bind-Value="Settings.DHCPServer.ServerIdentifier" />
</FormItem>
<FormItem Label="Domain">
<Input @bind-Value="Settings.DHCPServer.Domain" />
</FormItem>
<FormItem>
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
</FormItem>
</Form>
</div>
@code {
LANCommanderSettings Settings;
Form<LANCommanderSettings> Form;
FormValidationRule IPAddressValidationRule = new FormValidationRule
{
Type = FormFieldType.Regexp,
Pattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
Message = "Not a valid IP address"
};
protected override async Task OnInitializedAsync()
{
Settings = SettingService.GetSettings();
}
private void Save()
{
try
{
if (Form.Validate())
{
SettingService.SaveSettings(Settings);
MessageService.Success("Settings saved!");
DHCPService.Init();
}
}
catch
{
MessageService.Error("An unknown error occurred.");
}
}
}

View File

@ -0,0 +1,43 @@
@page "/Settings/DHCP/Leases"
@using LANCommander.Models;
@using System.Net;
@layout SettingsLayout
@inject SettingService SettingService
@inject DHCPService DHCPService
@inject IMessageService MessageService
@attribute [Authorize(Roles = "Administrator")]
<PageHeader Title="DHCP Leases" />
<div style="padding: 0 24px;">
<Table TItem="DHCPLease" DataSource="@DHCPLeases" Loading="@Loading" PageSize="50">
<PropertyColumn Property="l => l.IPAddress">
@(new IPAddress(context.IPAddress))
</PropertyColumn>
<PropertyColumn Property="l => l.MACAddress">
@Convert.ToHexString(context.MACAddress)
</PropertyColumn>
<PropertyColumn Property="l => l.CreatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable />
</Table>
</div>
@code {
ICollection<DHCPLease> DHCPLeases;
bool Loading = true;
protected override async Task OnInitializedAsync()
{
//DHCPLeases = await DHCPService.Get();
Loading = false;
}
async Task Release(DHCPLease lease)
{
//await DHCPService.Delete(lease);
DHCPLeases.Remove(lease);
StateHasChanged();
}
}

View File

@ -10,6 +10,10 @@
<MenuItem RouterLink="/Settings/Authentication">Authentication</MenuItem>
<MenuItem RouterLink="/Settings/Archives">Archives</MenuItem>
<MenuItem RouterLink="/Settings/IPXRelay">IPX Relay</MenuItem>
<SubMenu Key="DHCP" Title="DHCP">
<MenuItem RouterLink="/Settings/DHCP/Configuration">Configuration</MenuItem>
<MenuItem RouterLink="/Settings/DHCP/Leases">Leases</MenuItem>
</SubMenu>
<MenuItem RouterLink="/Settings/Tools" RouterMatch="@NavLinkMatch.Prefix">Tools</MenuItem>
</Menu>
</Sider>

View File

@ -127,6 +127,7 @@ builder.Services.AddScoped<GameSaveService>();
builder.Services.AddSingleton<ServerProcessService>();
builder.Services.AddSingleton<IPXRelayService>();
builder.Services.AddSingleton<DHCPService>();
if (settings.Beacon)
{

View File

@ -0,0 +1,157 @@
using DotNetProjects.DhcpServer;
using LANCommander.Data;
using LANCommander.Data.Models;
using NetworkPrimitives.Ipv4;
using System.Collections.Concurrent;
using System.Net;
using System.Net.NetworkInformation;
namespace LANCommander.Services
{
public class DHCPService
{
private DHCPServer Server { get; set; }
private Dictionary<string, IPAddress> Leases { get; set; } = new Dictionary<string, IPAddress>();
private string NetworkInterfaceId { get; set; }
private IPAddress SubnetMask { get; set; }
private IPAddress Network { get; set; }
private IPAddress RangeStart { get; set; }
private IPAddress RangeEnd { get; set; }
private string Domain { get; set; }
private IPAddress ServerIdentifier { get; set; }
private IPAddress DefaultGateway { get; set; }
private IEnumerable<IPAddress> DnsServers { get; set; }
public DHCPService()
{
Init();
}
public void Init()
{
var settings = SettingService.GetSettings();
NetworkInterfaceId = settings.DHCPServer.NetworkInterface;
SubnetMask = IPAddress.Parse(settings.DHCPServer.SubnetMask);
Network = IPAddress.Parse(settings.DHCPServer.Network);
RangeStart = IPAddress.Parse(settings.DHCPServer.RangeStart);
RangeEnd = IPAddress.Parse(settings.DHCPServer.RangeEnd);
Domain = settings.DHCPServer.Domain;
ServerIdentifier = IPAddress.Parse(settings.DHCPServer.ServerIdentifier);
DefaultGateway = IPAddress.Parse(settings.DHCPServer.DefaultGateway);
DnsServers = settings.DHCPServer.DnsServers.Select(IPAddress.Parse);
// Load stored leases
// Leases = new ConcurrentBag<DHCPLease>(await Get());
if (Server != null)
Server.Dispose();
if (settings.DHCPServer.Enabled)
Start();
}
public void Start()
{
Server = new DHCPServer();
Server.ServerName = "LANCommander";
Server.OnDataReceived += OnRequest;
Server.BroadcastAddress = IPAddress.Broadcast;
Server.SendDhcpAnswerNetworkInterface = GetNetworkInterfaces().FirstOrDefault(nic => nic.Id == NetworkInterfaceId);
Server.Start();
}
private void OnRequest(DHCPRequest request)
{
var type = request.GetMsgType();
var mac = GetMacAddress(request.GetChaddr());
IPAddress ip;
if (!Leases.TryGetValue(mac, out ip))
{
ip = GetNextAddress();
Leases[mac] = ip;
}
var reply = new DHCPReplyOptions
{
SubnetMask = SubnetMask,
DomainName = Domain,
ServerIdentifier = ServerIdentifier,
RouterIP = DefaultGateway,
DomainNameServers = DnsServers.ToArray()
};
switch (type)
{
case DHCPMsgType.DHCPDISCOVER:
request.SendDHCPReply(DHCPMsgType.DHCPOFFER, ip, reply);
break;
case DHCPMsgType.DHCPREQUEST:
request.SendDHCPReply(DHCPMsgType.DHCPACK, ip, reply);
break;
}
}
private IPAddress GetNextAddress()
{
var subnet = Ipv4Subnet.Parse($"{Network}/{GetBitmask(SubnetMask)}");
foreach (var address in subnet.GetUsableAddresses())
{
var ip = address.ToIpAddress();
if (ip.Address >= RangeStart.Address && ip.Address <= RangeEnd.Address && !Leases.Values.Any(l => l.Address == ip.Address))
{
return ip;
}
}
return null;
}
private ushort GetBitmask(IPAddress subnet)
{
var bytes = subnet.GetAddressBytes();
ushort bitmask = 0;
foreach (var b in bytes)
{
for (int i = 7; i >= 0; i--)
{
if ((b & (1 << i)) != 0)
bitmask++;
else
break;
}
}
return bitmask;
}
private string GetMacAddress(byte[] bytes)
{
return String.Join(':', bytes.Select(b => b.ToString("X2")));
}
public IEnumerable<NetworkInterface> GetNetworkInterfaces()
{
return NetworkInterface.GetAllNetworkInterfaces();
}
public NetworkInterface GetActiveNetworkInterface()
{
var settings = SettingService.GetSettings();
return GetNetworkInterfaces().FirstOrDefault(nic => nic.Id == settings.DHCPServer.NetworkInterface);
}
}
}

View File

@ -173,6 +173,33 @@
background: rgba(255, 255, 255, 0.03);
}
.network-interfaces.ant-radio-group {
width: 100%;
}
.network-interfaces .ant-collapse-header .anticon {
font-size: 32px;
margin-right: 8px;
}
.vertical-table {
display: table;
}
.vertical-table-row {
display: table-row;
}
.vertical-table-th {
display: table-cell;
font-weight: bold;
padding-right: 16px;
}
.vertical-table-td {
display: table-cell;
}
@media screen and (min-width: 768px) {
.mobile-menu {
display: none;