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:
1 parent
9232a0d065
commit
ca21d7b8a0
19 files changed
+683
-4
No files matched your search
@@ -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);
|
||||
Reference in new issue
Block a user