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