Wizard-Schritte für Internet-Zugang (WAN) und Heimnetzwerk (DHCP)

ConfigurationAction kapselt eine Änderung als Klartext-Beschreibung plus
Anwendungslogik über IRouterOsClient — WanConfigurator und
LanConfigurator bauen daraus die konkreten RouterOS-Befehle:

- WAN: DHCP-Client oder feste IP/Gateway, plus NAT-Masquerade.
- LAN: Bridge mit gewählten Schnittstellen, IP-Adresse, DHCP-Pool,
  DHCP-Server, Netzwerk-Eintrag (Gateway/DNS) und Router als
  DNS-Weiterleitung fürs Heimnetz.

Beide Wizard-Schritte sammeln die geplanten Änderungen nur in der
WizardSession (WanActions/LanActions) — am Gerät wird weiterhin nichts
verändert, bis der noch ausstehende Zusammenfassungs-Schritt es tut.
Der LAN-Schritt schließt die im WAN-Schritt gewählte Schnittstelle aus
der Anschluss-Auswahl aus und liefert sinnvolle Vorbelegungen
(192.168.88.0/24 u. Ä.), damit man sich weitgehend durchklicken kann.

Unit-Tests prüfen sowohl die erzeugten Beschreibungstexte als auch die
tatsächlich angewendeten RouterOS-Pfade/Parameter über ein
aufzeichnendes Test-Double, ohne echtes Gerät.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-11 19:19:07 +02:00
1 parent 9232a0d065
commit ca21d7b8a0
19 files changed
+683 -4

No files matched your search

+6
View File
@@ -24,6 +24,12 @@
<DataTemplate DataType="{x:Type steps:DeviceOverviewStepViewModel}">
<stepViews:DeviceOverviewStepView />
</DataTemplate>
<DataTemplate DataType="{x:Type steps:WanStepViewModel}">
<stepViews:WanStepView />
</DataTemplate>
<DataTemplate DataType="{x:Type steps:LanStepViewModel}">
<stepViews:LanStepView />
</DataTemplate>
<DataTemplate DataType="{x:Type steps:PlaceholderStepViewModel}">
<stepViews:PlaceholderStepView />
</DataTemplate>
@@ -18,8 +18,8 @@ public partial class MainViewModel : ObservableObject
[
new ConnectionStepViewModel(_session),
new DeviceOverviewStepViewModel(_session),
new PlaceholderStepViewModel("Internet-Zugang", "WAN-Schnittstelle und Internetverbindung einrichten."),
new PlaceholderStepViewModel("Heimnetzwerk (DHCP)", "IP-Adressbereich und DHCP-Server für dein Netzwerk festlegen."),
new WanStepViewModel(_session),
new LanStepViewModel(_session),
new PlaceholderStepViewModel("VLANs", "Netzwerke logisch voneinander trennen."),
new PlaceholderStepViewModel("WLAN", "WLAN-Name und Passwort festlegen."),
new PlaceholderStepViewModel("Firewall", "Sichere Basis-Firewallregeln auswählen."),
@@ -0,0 +1,107 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using MikrotikInstaller.Core.Configuration;
using MikrotikInstaller.Core.Devices;
namespace MikrotikInstaller.App.ViewModels.Steps;
/// <summary>
/// Vierter Schritt: Zusammenstellung des Heimnetzwerks (Bridge, Mitglieds-Schnittstellen, IP-Bereich,
/// DHCP-Server, DNS). Wie beim WAN-Schritt wird hier nur der Änderungsplan in der Session gesammelt.
/// </summary>
public partial class LanStepViewModel : WizardStepViewModelBase
{
private readonly WizardSession _session;
public LanStepViewModel(WizardSession session)
: base(
title: "Heimnetzwerk (DHCP)",
description: "Wähle, welche Anschlüsse zu deinem Heimnetzwerk gehören sollen, und lege den IP-Adressbereich fest. Die Vorgaben passen für die meisten Haushalte.")
{
_session = session;
}
public ObservableCollection<SelectableInterface> AvailableInterfaces { get; } = [];
[ObservableProperty]
private string _bridgeName = "bridge-lan";
[ObservableProperty]
private string _routerAddress = "192.168.88.1";
[ObservableProperty]
private int _prefixLength = 24;
[ObservableProperty]
private string _dhcpPoolStart = "192.168.88.10";
[ObservableProperty]
private string _dhcpPoolEnd = "192.168.88.254";
[ObservableProperty]
private string _primaryDns = "1.1.1.1";
[ObservableProperty]
private string _secondaryDns = "8.8.8.8";
public override bool CanGoNext =>
AvailableInterfaces.Any(i => i.IsSelected)
&& !string.IsNullOrWhiteSpace(BridgeName)
&& !string.IsNullOrWhiteSpace(RouterAddress)
&& !string.IsNullOrWhiteSpace(DhcpPoolStart)
&& !string.IsNullOrWhiteSpace(DhcpPoolEnd)
&& !string.IsNullOrWhiteSpace(PrimaryDns);
public override Task OnActivatedAsync()
{
AvailableInterfaces.Clear();
var candidates = (_session.Device?.Interfaces ?? []).Where(
i => i.Kind == InterfaceKind.Ethernet && i.Name != _session.WanInterfaceName);
foreach (var iface in candidates)
{
var item = new SelectableInterface(iface.Name, isSelected: true);
item.PropertyChanged += (_, _) => SaveToSession();
AvailableInterfaces.Add(item);
}
SaveToSession();
return Task.CompletedTask;
}
partial void OnBridgeNameChanged(string value) => SaveToSession();
partial void OnRouterAddressChanged(string value) => SaveToSession();
partial void OnPrefixLengthChanged(int value) => SaveToSession();
partial void OnDhcpPoolStartChanged(string value) => SaveToSession();
partial void OnDhcpPoolEndChanged(string value) => SaveToSession();
partial void OnPrimaryDnsChanged(string value) => SaveToSession();
partial void OnSecondaryDnsChanged(string value) => SaveToSession();
private void SaveToSession()
{
OnPropertyChanged(nameof(CanGoNext));
if (!CanGoNext)
{
_session.LanActions = null;
return;
}
var settings = new LanSettings(
BridgeName,
AvailableInterfaces.Where(i => i.IsSelected).Select(i => i.Name).ToList(),
$"{RouterAddress}/{PrefixLength}",
DhcpPoolStart,
DhcpPoolEnd,
PrimaryDns,
string.IsNullOrWhiteSpace(SecondaryDns) ? null : SecondaryDns);
_session.LanActions = LanConfigurator.BuildActions(settings);
}
}
@@ -0,0 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace MikrotikInstaller.App.ViewModels.Steps;
/// <summary>Eine Schnittstelle mit an-/abwählbarem Kontrollkästchen, z. B. für die Heimnetz-Zuordnung.</summary>
public partial class SelectableInterface(string name, bool isSelected) : ObservableObject
{
public string Name { get; } = name;
[ObservableProperty]
private bool _isSelected = isSelected;
}
@@ -0,0 +1,83 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using MikrotikInstaller.Core.Configuration;
using MikrotikInstaller.Core.Devices;
namespace MikrotikInstaller.App.ViewModels.Steps;
/// <summary>
/// Dritter Schritt: Wahl der Internet-Schnittstelle (WAN) und ob die Adresse automatisch (DHCP)
/// oder fest vergeben werden soll. Es wird hier noch nichts am Gerät verändert — die daraus
/// resultierenden Änderungen werden nur in der Session gesammelt und erst im
/// Zusammenfassungs-Schritt angewendet.
/// </summary>
public partial class WanStepViewModel : WizardStepViewModelBase
{
private readonly WizardSession _session;
public WanStepViewModel(WizardSession session)
: base(
title: "Internet-Zugang",
description: "Wähle die Schnittstelle, an der dein Internet-Kabel (vom Modem/Router deines Anbieters) angeschlossen ist.")
{
_session = session;
}
public ObservableCollection<string> AvailableInterfaces { get; } = [];
[ObservableProperty]
private string? _selectedInterface;
[ObservableProperty]
private bool _useStaticAddress;
[ObservableProperty]
private string _staticAddress = string.Empty;
[ObservableProperty]
private string _staticGateway = string.Empty;
public override bool CanGoNext =>
!string.IsNullOrWhiteSpace(SelectedInterface)
&& (!UseStaticAddress || (!string.IsNullOrWhiteSpace(StaticAddress) && !string.IsNullOrWhiteSpace(StaticGateway)));
public override Task OnActivatedAsync()
{
AvailableInterfaces.Clear();
foreach (var iface in _session.Device?.Interfaces.Where(i => i.Kind == InterfaceKind.Ethernet) ?? [])
{
AvailableInterfaces.Add(iface.Name);
}
SelectedInterface ??= AvailableInterfaces.FirstOrDefault();
SaveToSession();
return Task.CompletedTask;
}
partial void OnSelectedInterfaceChanged(string? value) => SaveToSession();
partial void OnUseStaticAddressChanged(bool value) => SaveToSession();
partial void OnStaticAddressChanged(string value) => SaveToSession();
partial void OnStaticGatewayChanged(string value) => SaveToSession();
private void SaveToSession()
{
OnPropertyChanged(nameof(CanGoNext));
_session.WanInterfaceName = SelectedInterface;
if (!CanGoNext)
{
_session.WanActions = null;
return;
}
var settings = UseStaticAddress
? new WanSettings(SelectedInterface!, WanAddressMode.Static, StaticAddress, StaticGateway)
: new WanSettings(SelectedInterface!, WanAddressMode.Dhcp);
_session.WanActions = WanConfigurator.BuildActions(settings);
}
}
@@ -1,11 +1,13 @@
using MikrotikInstaller.Core.Configuration;
using MikrotikInstaller.Core.Connectivity;
using MikrotikInstaller.Core.Devices;
namespace MikrotikInstaller.App.ViewModels;
/// <summary>
/// Über den gesamten Assistenten geteilter Zustand: die einmal hergestellte Geräteverbindung
/// und die daraus abgeleiteten Erkenntnisse, damit spätere Schritte nicht erneut verbinden müssen.
/// Über den gesamten Assistenten geteilter Zustand: die einmal hergestellte Geräteverbindung,
/// die daraus abgeleiteten Erkenntnisse und die von jedem Schritt geplanten Änderungen.
/// Nichts davon wird angewendet, bevor der Zusammenfassungs-Schritt es tut.
/// </summary>
public sealed class WizardSession
{
@@ -13,6 +15,13 @@ public sealed class WizardSession
public RouterDevice? Device { get; set; }
/// <summary>Vom WAN-Schritt gewählte Internet-Schnittstelle, damit der LAN-Schritt sie ausschließen kann.</summary>
public string? WanInterfaceName { get; set; }
public IReadOnlyList<ConfigurationAction>? WanActions { get; set; }
public IReadOnlyList<ConfigurationAction>? LanActions { get; set; }
public async Task DisposeClientAsync()
{
if (Client is not null)
@@ -0,0 +1,74 @@
<UserControl x:Class="MikrotikInstaller.App.Views.Steps.LanStepView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="600">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="0,16,0,0">
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0,0,0,20" Opacity="0.8" />
<TextBlock Text="Anschlüsse in deinem Heimnetzwerk" Margin="0,0,0,4" />
<ItemsControl ItemsSource="{Binding AvailableInterfaces}" Margin="0,0,0,20">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected}" Margin="0,2,0,2" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Name des Heimnetzwerks (intern)" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding BridgeName, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,16" HorizontalAlignment="Left" MinWidth="200" />
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Router-Adresse in deinem Netzwerk" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding RouterAddress, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Präfix" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding PrefixLength, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Adressbereich von" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding DhcpPoolStart, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="bis" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding DhcpPoolEnd, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="DNS-Server (bevorzugt)" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding PrimaryDns, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="DNS-Server (alternativ, optional)" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding SecondaryDns, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace MikrotikInstaller.App.Views.Steps;
public partial class LanStepView : UserControl
{
public LanStepView()
{
InitializeComponent();
}
}
@@ -0,0 +1,27 @@
<UserControl x:Class="MikrotikInstaller.App.Views.Steps.WanStepView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="600">
<StackPanel Margin="0,16,0,0">
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0,0,0,24" Opacity="0.8" />
<TextBlock Text="Internet-Schnittstelle" Margin="0,0,0,4" />
<ComboBox ItemsSource="{Binding AvailableInterfaces}" SelectedItem="{Binding SelectedInterface}"
Margin="0,0,0,20" HorizontalAlignment="Left" MinWidth="220" />
<CheckBox Content="Feste IP-Adresse statt automatischem Bezug (DHCP) verwenden"
IsChecked="{Binding UseStaticAddress}" Margin="0,0,0,16" />
<StackPanel Visibility="{Binding UseStaticAddress, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="Feste IP-Adresse (mit Präfix, z. B. 203.0.113.5/24)" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding StaticAddress, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,16" />
<TextBlock Text="Gateway (Adresse deines Internetanbieters)" Margin="0,0,0,4" />
<ui:TextBox Text="{Binding StaticGateway, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,16" />
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace MikrotikInstaller.App.Views.Steps;
public partial class WanStepView : UserControl
{
public WanStepView()
{
InitializeComponent();
}
}
@@ -0,0 +1,49 @@
using MikrotikInstaller.Core.Configuration;
namespace MikrotikInstaller.Core.Tests.Configuration;
public class LanConfiguratorTests
{
private static LanSettings DefaultSettings() => new(
BridgeName: "bridge-lan",
MemberInterfaces: ["ether2", "ether3"],
RouterAddressCidr: "192.168.88.1/24",
DhcpPoolStart: "192.168.88.10",
DhcpPoolEnd: "192.168.88.254",
PrimaryDnsServer: "1.1.1.1",
SecondaryDnsServer: "8.8.8.8");
[Fact]
public void BuildActions_OneActionPerBridgePortPlusFixedSteps()
{
var actions = LanConfigurator.BuildActions(DefaultSettings());
// Bridge anlegen + 2 Ports + IP-Adresse + Pool + DHCP-Server + Netzwerk + DNS = 8
Assert.Equal(8, actions.Count);
}
[Fact]
public async Task BuildActions_AppliesExpectedRouterOsCommands()
{
var actions = LanConfigurator.BuildActions(DefaultSettings());
var client = new RecordingRouterOsClient();
foreach (var action in actions)
{
await action.ApplyAsync(client, CancellationToken.None);
}
Assert.Contains(client.AddCalls, c => c.Path == "/interface/bridge" && c.Parameters["name"] == "bridge-lan");
Assert.Contains(client.AddCalls, c => c.Path == "/interface/bridge/port"
&& c.Parameters["bridge"] == "bridge-lan" && c.Parameters["interface"] == "ether2");
Assert.Contains(client.AddCalls, c => c.Path == "/interface/bridge/port"
&& c.Parameters["bridge"] == "bridge-lan" && c.Parameters["interface"] == "ether3");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/address" && c.Parameters["address"] == "192.168.88.1/24");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/pool" && c.Parameters["ranges"] == "192.168.88.10-192.168.88.254");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server" && c.Parameters["interface"] == "bridge-lan");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server/network"
&& c.Parameters["address"] == "192.168.88.0/24" && c.Parameters["gateway"] == "192.168.88.1");
Assert.Contains(client.ExecuteCalls, c => c.Path == "/ip/dns/set"
&& c.Parameters!["servers"] == "1.1.1.1,8.8.8.8" && c.Parameters["allow-remote-requests"] == "yes");
}
}
@@ -0,0 +1,36 @@
using MikrotikInstaller.Core.Connectivity;
namespace MikrotikInstaller.Core.Tests.Configuration;
/// <summary>Test-Double, das aufgezeichnete Add/Execute-Aufrufe zur Prüfung bereitstellt.</summary>
internal sealed class RecordingRouterOsClient : IRouterOsClient
{
public List<(string Path, IReadOnlyDictionary<string, string> Parameters)> AddCalls { get; } = [];
public List<(string Path, IReadOnlyDictionary<string, string>? Parameters)> ExecuteCalls { get; } = [];
public RouterOsProtocol Protocol => RouterOsProtocol.Rest;
public Task<IReadOnlyList<IReadOnlyDictionary<string, string>>> GetAsync(string path, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<IReadOnlyDictionary<string, string>>>([]);
public Task<string> AddAsync(string path, IReadOnlyDictionary<string, string> parameters, CancellationToken cancellationToken = default)
{
AddCalls.Add((path, parameters));
return Task.FromResult("*1");
}
public Task SetAsync(string path, string id, IReadOnlyDictionary<string, string> parameters, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public Task RemoveAsync(string path, string id, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public Task ExecuteAsync(string path, IReadOnlyDictionary<string, string>? parameters = null, CancellationToken cancellationToken = default)
{
ExecuteCalls.Add((path, parameters));
return Task.CompletedTask;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
@@ -0,0 +1,48 @@
using MikrotikInstaller.Core.Configuration;
namespace MikrotikInstaller.Core.Tests.Configuration;
public class WanConfiguratorTests
{
[Fact]
public void BuildActions_DhcpMode_ProducesDhcpClientAndNat()
{
var actions = WanConfigurator.BuildActions(new WanSettings("ether1", WanAddressMode.Dhcp));
Assert.Equal(2, actions.Count);
Assert.Contains(actions, a => a.Description.Contains("DHCP") && a.Description.Contains("ether1"));
Assert.Contains(actions, a => a.Description.Contains("NAT"));
}
[Fact]
public void BuildActions_StaticMode_ProducesAddressRouteAndNat()
{
var actions = WanConfigurator.BuildActions(new WanSettings(
"ether1", WanAddressMode.Static, StaticAddressCidr: "203.0.113.5/24", StaticGateway: "203.0.113.1"));
Assert.Equal(3, actions.Count);
Assert.Contains(actions, a => a.Description.Contains("203.0.113.5/24"));
Assert.Contains(actions, a => a.Description.Contains("203.0.113.1"));
Assert.Contains(actions, a => a.Description.Contains("NAT"));
}
[Fact]
public async Task BuildActions_StaticMode_AppliesExpectedRouterOsCommands()
{
var actions = WanConfigurator.BuildActions(new WanSettings(
"ether1", WanAddressMode.Static, StaticAddressCidr: "203.0.113.5/24", StaticGateway: "203.0.113.1"));
var client = new RecordingRouterOsClient();
foreach (var action in actions)
{
await action.ApplyAsync(client, CancellationToken.None);
}
Assert.Contains(client.AddCalls, c => c.Path == "/ip/address"
&& c.Parameters["address"] == "203.0.113.5/24" && c.Parameters["interface"] == "ether1");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/route"
&& c.Parameters["gateway"] == "203.0.113.1" && c.Parameters["dst-address"] == "0.0.0.0/0");
Assert.Contains(client.AddCalls, c => c.Path == "/ip/firewall/nat"
&& c.Parameters["action"] == "masquerade" && c.Parameters["out-interface"] == "ether1");
}
}
@@ -0,0 +1,10 @@
using MikrotikInstaller.Core.Connectivity;
namespace MikrotikInstaller.Core.Configuration;
/// <summary>
/// Eine einzelne, für sich verständliche Änderung am Gerät: ein laientauglicher Beschreibungstext
/// für die Zusammenfassung im Assistenten, und die Aktion, die sie tatsächlich anwendet.
/// Nichts wird ausgeführt, bevor der Nutzer im Zusammenfassungs-Schritt zugestimmt hat.
/// </summary>
public sealed record ConfigurationAction(string Description, Func<IRouterOsClient, CancellationToken, Task> ApplyAsync);
@@ -0,0 +1,36 @@
using System.Net;
namespace MikrotikInstaller.Core.Configuration;
internal static class IpNetworkHelper
{
/// <summary>Berechnet aus "192.168.88.1/24" die Netzwerkadresse "192.168.88.0/24".</summary>
public static string GetNetworkCidr(string addressCidr)
{
var parts = addressCidr.Split('/');
var address = IPAddress.Parse(parts[0]);
var prefixLength = int.Parse(parts[1]);
var addressBytes = address.GetAddressBytes();
var mask = CreateMask(prefixLength);
var networkBytes = new byte[4];
for (var i = 0; i < 4; i++)
{
networkBytes[i] = (byte)(addressBytes[i] & mask[i]);
}
return $"{new IPAddress(networkBytes)}/{prefixLength}";
}
private static byte[] CreateMask(int prefixLength)
{
var maskValue = prefixLength == 0 ? 0u : 0xFFFFFFFFu << (32 - prefixLength);
return
[
(byte)((maskValue >> 24) & 0xFF),
(byte)((maskValue >> 16) & 0xFF),
(byte)((maskValue >> 8) & 0xFF),
(byte)(maskValue & 0xFF),
];
}
}
@@ -0,0 +1,84 @@
namespace MikrotikInstaller.Core.Configuration;
/// <summary>
/// Erzeugt aus den Wizard-Eingaben zum Heimnetzwerk die anzuwendenden RouterOS-Änderungen:
/// Bridge mit den gewählten Schnittstellen, IP-Adresse, DHCP-Server und DNS-Weiterleitung.
/// </summary>
public static class LanConfigurator
{
public static IReadOnlyList<ConfigurationAction> BuildActions(LanSettings settings)
{
var actions = new List<ConfigurationAction>();
var routerAddress = settings.RouterAddressCidr.Split('/')[0];
var networkCidr = IpNetworkHelper.GetNetworkCidr(settings.RouterAddressCidr);
var poolName = $"pool-{settings.BridgeName}";
var dhcpServerName = $"dhcp-{settings.BridgeName}";
actions.Add(new ConfigurationAction(
$"Netzwerk-Bridge „{settings.BridgeName}\" für dein Heimnetzwerk anlegen",
(client, ct) => client.AddAsync("/interface/bridge", new Dictionary<string, string>
{
["name"] = settings.BridgeName,
}, ct)));
foreach (var interfaceName in settings.MemberInterfaces)
{
actions.Add(new ConfigurationAction(
$"Schnittstelle „{interfaceName}\" dem Heimnetzwerk hinzufügen",
(client, ct) => client.AddAsync("/interface/bridge/port", new Dictionary<string, string>
{
["bridge"] = settings.BridgeName,
["interface"] = interfaceName,
}, ct)));
}
actions.Add(new ConfigurationAction(
$"IP-Adresse {settings.RouterAddressCidr} auf „{settings.BridgeName}\" einrichten",
(client, ct) => client.AddAsync("/ip/address", new Dictionary<string, string>
{
["address"] = settings.RouterAddressCidr,
["interface"] = settings.BridgeName,
}, ct)));
actions.Add(new ConfigurationAction(
$"IP-Adressbereich {settings.DhcpPoolStart}-{settings.DhcpPoolEnd} für Geräte in deinem Netzwerk reservieren",
(client, ct) => client.AddAsync("/ip/pool", new Dictionary<string, string>
{
["name"] = poolName,
["ranges"] = $"{settings.DhcpPoolStart}-{settings.DhcpPoolEnd}",
}, ct)));
actions.Add(new ConfigurationAction(
$"DHCP-Server auf „{settings.BridgeName}\" aktivieren, damit Geräte automatisch eine Adresse bekommen",
(client, ct) => client.AddAsync("/ip/dhcp-server", new Dictionary<string, string>
{
["name"] = dhcpServerName,
["interface"] = settings.BridgeName,
["address-pool"] = poolName,
["disabled"] = "no",
}, ct)));
actions.Add(new ConfigurationAction(
$"Netzwerk-Einstellungen (Gateway {routerAddress}, DNS) für das Heimnetzwerk hinterlegen",
(client, ct) => client.AddAsync("/ip/dhcp-server/network", new Dictionary<string, string>
{
["address"] = networkCidr,
["gateway"] = routerAddress,
["dns-server"] = routerAddress,
}, ct)));
var upstreamDns = settings.SecondaryDnsServer is null
? settings.PrimaryDnsServer
: $"{settings.PrimaryDnsServer},{settings.SecondaryDnsServer}";
actions.Add(new ConfigurationAction(
$"Router als DNS-Server für dein Heimnetz einrichten (leitet an {upstreamDns} weiter)",
(client, ct) => client.ExecuteAsync("/ip/dns/set", new Dictionary<string, string>
{
["servers"] = upstreamDns,
["allow-remote-requests"] = "yes",
}, ct)));
return actions;
}
}
@@ -0,0 +1,10 @@
namespace MikrotikInstaller.Core.Configuration;
public sealed record LanSettings(
string BridgeName,
IReadOnlyList<string> MemberInterfaces,
string RouterAddressCidr,
string DhcpPoolStart,
string DhcpPoolEnd,
string PrimaryDnsServer,
string? SecondaryDnsServer);
@@ -0,0 +1,50 @@
namespace MikrotikInstaller.Core.Configuration;
/// <summary>Erzeugt aus den Wizard-Eingaben zum Internet-Zugang die anzuwendenden RouterOS-Änderungen.</summary>
public static class WanConfigurator
{
public static IReadOnlyList<ConfigurationAction> BuildActions(WanSettings settings)
{
var actions = new List<ConfigurationAction>();
if (settings.Mode == WanAddressMode.Dhcp)
{
actions.Add(new ConfigurationAction(
$"Internetadresse automatisch über DHCP auf „{settings.InterfaceName}\" beziehen",
(client, ct) => client.AddAsync("/ip/dhcp-client", new Dictionary<string, string>
{
["interface"] = settings.InterfaceName,
["disabled"] = "no",
}, ct)));
}
else
{
actions.Add(new ConfigurationAction(
$"Feste IP-Adresse {settings.StaticAddressCidr} auf „{settings.InterfaceName}\" einrichten",
(client, ct) => client.AddAsync("/ip/address", new Dictionary<string, string>
{
["address"] = settings.StaticAddressCidr!,
["interface"] = settings.InterfaceName,
}, ct)));
actions.Add(new ConfigurationAction(
$"Standard-Internetroute über {settings.StaticGateway} eintragen",
(client, ct) => client.AddAsync("/ip/route", new Dictionary<string, string>
{
["dst-address"] = "0.0.0.0/0",
["gateway"] = settings.StaticGateway!,
}, ct)));
}
actions.Add(new ConfigurationAction(
$"Datenverkehr aus deinem Heimnetz über „{settings.InterfaceName}\" ins Internet weiterleiten (NAT)",
(client, ct) => client.AddAsync("/ip/firewall/nat", new Dictionary<string, string>
{
["chain"] = "srcnat",
["out-interface"] = settings.InterfaceName,
["action"] = "masquerade",
}, ct)));
return actions;
}
}
@@ -0,0 +1,16 @@
namespace MikrotikInstaller.Core.Configuration;
public enum WanAddressMode
{
/// <summary>Adresse automatisch vom Internetanbieter per DHCP beziehen (häufigster Fall).</summary>
Dhcp,
/// <summary>Feste, vom Nutzer vorgegebene IP-Adresse verwenden.</summary>
Static,
}
public sealed record WanSettings(
string InterfaceName,
WanAddressMode Mode,
string? StaticAddressCidr = null,
string? StaticGateway = null);