diff --git a/src/MikrotikInstaller.App/App.xaml b/src/MikrotikInstaller.App/App.xaml
index 6ed8235..65baad0 100644
--- a/src/MikrotikInstaller.App/App.xaml
+++ b/src/MikrotikInstaller.App/App.xaml
@@ -24,6 +24,12 @@
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs b/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
index bd82803..e70bf63 100644
--- a/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
@@ -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."),
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs
new file mode 100644
index 0000000..97988d9
--- /dev/null
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs
@@ -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;
+
+///
+/// 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.
+///
+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 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);
+ }
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/SelectableInterface.cs b/src/MikrotikInstaller.App/ViewModels/Steps/SelectableInterface.cs
new file mode 100644
index 0000000..2c08818
--- /dev/null
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/SelectableInterface.cs
@@ -0,0 +1,12 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace MikrotikInstaller.App.ViewModels.Steps;
+
+/// Eine Schnittstelle mit an-/abwählbarem Kontrollkästchen, z. B. für die Heimnetz-Zuordnung.
+public partial class SelectableInterface(string name, bool isSelected) : ObservableObject
+{
+ public string Name { get; } = name;
+
+ [ObservableProperty]
+ private bool _isSelected = isSelected;
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs
new file mode 100644
index 0000000..a83b46b
--- /dev/null
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs
@@ -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;
+
+///
+/// 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.
+///
+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 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);
+ }
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
index c096b73..19359c7 100644
--- a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
+++ b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
@@ -1,11 +1,13 @@
+using MikrotikInstaller.Core.Configuration;
using MikrotikInstaller.Core.Connectivity;
using MikrotikInstaller.Core.Devices;
namespace MikrotikInstaller.App.ViewModels;
///
-/// Ü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.
///
public sealed class WizardSession
{
@@ -13,6 +15,13 @@ public sealed class WizardSession
public RouterDevice? Device { get; set; }
+ /// Vom WAN-Schritt gewählte Internet-Schnittstelle, damit der LAN-Schritt sie ausschließen kann.
+ public string? WanInterfaceName { get; set; }
+
+ public IReadOnlyList? WanActions { get; set; }
+
+ public IReadOnlyList? LanActions { get; set; }
+
public async Task DisposeClientAsync()
{
if (Client is not null)
diff --git a/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml
new file mode 100644
index 0000000..5b76157
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml.cs b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml.cs
new file mode 100644
index 0000000..9132e90
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml.cs
@@ -0,0 +1,11 @@
+using System.Windows.Controls;
+
+namespace MikrotikInstaller.App.Views.Steps;
+
+public partial class LanStepView : UserControl
+{
+ public LanStepView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml
new file mode 100644
index 0000000..ef58de7
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml.cs b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml.cs
new file mode 100644
index 0000000..97acb1f
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml.cs
@@ -0,0 +1,11 @@
+using System.Windows.Controls;
+
+namespace MikrotikInstaller.App.Views.Steps;
+
+public partial class WanStepView : UserControl
+{
+ public WanStepView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs
new file mode 100644
index 0000000..3eeea3a
--- /dev/null
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs
@@ -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");
+ }
+}
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/RecordingRouterOsClient.cs b/src/MikrotikInstaller.Core.Tests/Configuration/RecordingRouterOsClient.cs
new file mode 100644
index 0000000..25f4e36
--- /dev/null
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/RecordingRouterOsClient.cs
@@ -0,0 +1,36 @@
+using MikrotikInstaller.Core.Connectivity;
+
+namespace MikrotikInstaller.Core.Tests.Configuration;
+
+/// Test-Double, das aufgezeichnete Add/Execute-Aufrufe zur Prüfung bereitstellt.
+internal sealed class RecordingRouterOsClient : IRouterOsClient
+{
+ public List<(string Path, IReadOnlyDictionary Parameters)> AddCalls { get; } = [];
+
+ public List<(string Path, IReadOnlyDictionary? Parameters)> ExecuteCalls { get; } = [];
+
+ public RouterOsProtocol Protocol => RouterOsProtocol.Rest;
+
+ public Task>> GetAsync(string path, CancellationToken cancellationToken = default) =>
+ Task.FromResult>>([]);
+
+ public Task AddAsync(string path, IReadOnlyDictionary parameters, CancellationToken cancellationToken = default)
+ {
+ AddCalls.Add((path, parameters));
+ return Task.FromResult("*1");
+ }
+
+ public Task SetAsync(string path, string id, IReadOnlyDictionary parameters, CancellationToken cancellationToken = default) =>
+ Task.CompletedTask;
+
+ public Task RemoveAsync(string path, string id, CancellationToken cancellationToken = default) =>
+ Task.CompletedTask;
+
+ public Task ExecuteAsync(string path, IReadOnlyDictionary? parameters = null, CancellationToken cancellationToken = default)
+ {
+ ExecuteCalls.Add((path, parameters));
+ return Task.CompletedTask;
+ }
+
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+}
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs
new file mode 100644
index 0000000..aef7ab1
--- /dev/null
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs
@@ -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");
+ }
+}
diff --git a/src/MikrotikInstaller.Core/Configuration/ConfigurationAction.cs b/src/MikrotikInstaller.Core/Configuration/ConfigurationAction.cs
new file mode 100644
index 0000000..2c6ff81
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/ConfigurationAction.cs
@@ -0,0 +1,10 @@
+using MikrotikInstaller.Core.Connectivity;
+
+namespace MikrotikInstaller.Core.Configuration;
+
+///
+/// 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.
+///
+public sealed record ConfigurationAction(string Description, Func ApplyAsync);
diff --git a/src/MikrotikInstaller.Core/Configuration/IpNetworkHelper.cs b/src/MikrotikInstaller.Core/Configuration/IpNetworkHelper.cs
new file mode 100644
index 0000000..c66aa6d
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/IpNetworkHelper.cs
@@ -0,0 +1,36 @@
+using System.Net;
+
+namespace MikrotikInstaller.Core.Configuration;
+
+internal static class IpNetworkHelper
+{
+ /// Berechnet aus "192.168.88.1/24" die Netzwerkadresse "192.168.88.0/24".
+ 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),
+ ];
+ }
+}
diff --git a/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs
new file mode 100644
index 0000000..5b013cc
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs
@@ -0,0 +1,84 @@
+namespace MikrotikInstaller.Core.Configuration;
+
+///
+/// Erzeugt aus den Wizard-Eingaben zum Heimnetzwerk die anzuwendenden RouterOS-Änderungen:
+/// Bridge mit den gewählten Schnittstellen, IP-Adresse, DHCP-Server und DNS-Weiterleitung.
+///
+public static class LanConfigurator
+{
+ public static IReadOnlyList BuildActions(LanSettings settings)
+ {
+ var actions = new List();
+ 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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["servers"] = upstreamDns,
+ ["allow-remote-requests"] = "yes",
+ }, ct)));
+
+ return actions;
+ }
+}
diff --git a/src/MikrotikInstaller.Core/Configuration/LanSettings.cs b/src/MikrotikInstaller.Core/Configuration/LanSettings.cs
new file mode 100644
index 0000000..57f34b9
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/LanSettings.cs
@@ -0,0 +1,10 @@
+namespace MikrotikInstaller.Core.Configuration;
+
+public sealed record LanSettings(
+ string BridgeName,
+ IReadOnlyList MemberInterfaces,
+ string RouterAddressCidr,
+ string DhcpPoolStart,
+ string DhcpPoolEnd,
+ string PrimaryDnsServer,
+ string? SecondaryDnsServer);
diff --git a/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs
new file mode 100644
index 0000000..b6e9537
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs
@@ -0,0 +1,50 @@
+namespace MikrotikInstaller.Core.Configuration;
+
+/// Erzeugt aus den Wizard-Eingaben zum Internet-Zugang die anzuwendenden RouterOS-Änderungen.
+public static class WanConfigurator
+{
+ public static IReadOnlyList BuildActions(WanSettings settings)
+ {
+ var actions = new List();
+
+ 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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["chain"] = "srcnat",
+ ["out-interface"] = settings.InterfaceName,
+ ["action"] = "masquerade",
+ }, ct)));
+
+ return actions;
+ }
+}
diff --git a/src/MikrotikInstaller.Core/Configuration/WanSettings.cs b/src/MikrotikInstaller.Core/Configuration/WanSettings.cs
new file mode 100644
index 0000000..c5dbf8e
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/WanSettings.cs
@@ -0,0 +1,16 @@
+namespace MikrotikInstaller.Core.Configuration;
+
+public enum WanAddressMode
+{
+ /// Adresse automatisch vom Internetanbieter per DHCP beziehen (häufigster Fall).
+ Dhcp,
+
+ /// Feste, vom Nutzer vorgegebene IP-Adresse verwenden.
+ Static,
+}
+
+public sealed record WanSettings(
+ string InterfaceName,
+ WanAddressMode Mode,
+ string? StaticAddressCidr = null,
+ string? StaticGateway = null);