diff --git a/src/MikrotikInstaller.App/App.xaml b/src/MikrotikInstaller.App/App.xaml
index b06a565..ee6bbc3 100644
--- a/src/MikrotikInstaller.App/App.xaml
+++ b/src/MikrotikInstaller.App/App.xaml
@@ -30,6 +30,9 @@
+
+
+
diff --git a/src/MikrotikInstaller.App/MainWindow.xaml b/src/MikrotikInstaller.App/MainWindow.xaml
index 3ca8802..33bec91 100644
--- a/src/MikrotikInstaller.App/MainWindow.xaml
+++ b/src/MikrotikInstaller.App/MainWindow.xaml
@@ -147,7 +147,10 @@
-
+
+
diff --git a/src/MikrotikInstaller.App/MainWindow.xaml.cs b/src/MikrotikInstaller.App/MainWindow.xaml.cs
index cca8f2d..62ff983 100644
--- a/src/MikrotikInstaller.App/MainWindow.xaml.cs
+++ b/src/MikrotikInstaller.App/MainWindow.xaml.cs
@@ -11,6 +11,11 @@ public partial class MainWindow : FluentWindow
InitializeComponent();
Closing += OnClosing;
+ if (DataContext is MainViewModel viewModel)
+ {
+ viewModel.CloseRequested += (_, _) => Close();
+ }
+
// Folgt Hell/Dunkel-Wechseln des Windows-Systemthemes zur Laufzeit;
// updateAccents:false, damit unser MikroTik-Grün-Akzent dabei erhalten bleibt.
SystemThemeWatcher.Watch(this, WindowBackdropType.Mica, updateAccents: false);
diff --git a/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs b/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
index e7211dd..7981b93 100644
--- a/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/MainViewModel.cs
@@ -18,6 +18,7 @@ public partial class MainViewModel : ObservableObject
[
new ConnectionStepViewModel(_session),
new DeviceOverviewStepViewModel(_session),
+ new ModeSelectionStepViewModel(_session),
new WanStepViewModel(_session),
new LanStepViewModel(_session),
new VlanStepViewModel(_session),
@@ -87,6 +88,12 @@ public partial class MainViewModel : ObservableObject
private bool CanGoNext() => !IsLastStep && CurrentStep.CanGoNext;
+ /// Wird ausgelöst, wenn der Nutzer auf dem letzten Schritt „Beenden" klickt.
+ public event EventHandler? CloseRequested;
+
+ [RelayCommand]
+ private void Finish() => CloseRequested?.Invoke(this, EventArgs.Empty);
+
/// Wird beim Schließen des Hauptfensters aufgerufen, um die Geräteverbindung sauber zu beenden.
public Task DisposeSessionAsync() => _session.DisposeClientAsync();
}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/FirewallStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/FirewallStepViewModel.cs
index 0116e4e..7d4a21c 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/FirewallStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/FirewallStepViewModel.cs
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MikrotikInstaller.Core.Configuration;
@@ -24,10 +25,17 @@ public partial class FirewallStepViewModel : WizardStepViewModelBase
public IReadOnlyList Protocols { get; } = ["tcp", "udp"];
+ // Experten-Feld — nur im Experten-Modus sichtbar, leer bedeutet: keine Sperrliste (wie bisher).
+ [ObservableProperty]
+ private string _blockedAddressesText = string.Empty;
+
+ public bool IsExpertMode => _session.IsExpertMode;
+
public override bool CanGoNext => PortForwards.All(IsValid);
public override Task OnActivatedAsync()
{
+ OnPropertyChanged(nameof(IsExpertMode));
SaveToSession();
return Task.CompletedTask;
}
@@ -48,6 +56,8 @@ public partial class FirewallStepViewModel : WizardStepViewModelBase
SaveToSession();
}
+ partial void OnBlockedAddressesTextChanged(string value) => SaveToSession();
+
private static bool IsValid(PortForwardEntry entry) =>
!string.IsNullOrWhiteSpace(entry.Name)
&& !string.IsNullOrWhiteSpace(entry.TargetAddress)
@@ -66,10 +76,21 @@ public partial class FirewallStepViewModel : WizardStepViewModelBase
}
var forwards = PortForwards
- .Select(e => new PortForward(e.Name, e.Protocol, int.Parse(e.ExternalPort), e.TargetAddress, int.Parse(e.InternalPort)))
+ .Select(e => new PortForward(
+ e.Name, e.Protocol, int.Parse(e.ExternalPort), e.TargetAddress, int.Parse(e.InternalPort),
+ string.IsNullOrWhiteSpace(e.AllowedSourceAddress) ? null : e.AllowedSourceAddress,
+ e.LogRule,
+ int.TryParse(e.ExternalPortRangeEnd, out var extEnd) ? extEnd : null,
+ int.TryParse(e.InternalPortRangeEnd, out var intEnd) ? intEnd : null))
.ToList();
- var settings = new FirewallSettings(_session.WanInterfaceName, _session.LanBridgeName, forwards);
+ var blockedAddresses = BlockedAddressesText
+ .Split([',', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .ToList();
+
+ var settings = new FirewallSettings(
+ _session.WanInterfaceName, _session.LanBridgeName, forwards,
+ blockedAddresses.Count > 0 ? blockedAddresses : null);
_session.FirewallSettings = settings;
_session.FirewallActions = FirewallConfigurator.BuildActions(settings);
}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs
index 307e488..1442b43 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/LanStepViewModel.cs
@@ -44,6 +44,18 @@ public partial class LanStepViewModel : WizardStepViewModelBase
[ObservableProperty]
private string _secondaryDns = "8.8.8.8";
+ // Experten-Felder — nur im Experten-Modus sichtbar, leer bedeutet: RouterOS-Standardverhalten.
+ [ObservableProperty]
+ private string _dhcpLeaseTime = string.Empty;
+
+ [ObservableProperty]
+ private string _domainName = string.Empty;
+
+ [ObservableProperty]
+ private string _ntpServer = string.Empty;
+
+ public bool IsExpertMode => _session.IsExpertMode;
+
public override bool CanGoNext =>
AvailableInterfaces.Any(i => i.IsSelected)
&& !string.IsNullOrWhiteSpace(BridgeName)
@@ -54,6 +66,8 @@ public partial class LanStepViewModel : WizardStepViewModelBase
public override Task OnActivatedAsync()
{
+ OnPropertyChanged(nameof(IsExpertMode));
+
AvailableInterfaces.Clear();
var candidates = (_session.Device?.Interfaces ?? []).Where(
i => i.Kind == InterfaceKind.Ethernet && i.Name != _session.WanInterfaceName);
@@ -83,6 +97,12 @@ public partial class LanStepViewModel : WizardStepViewModelBase
partial void OnSecondaryDnsChanged(string value) => SaveToSession();
+ partial void OnDhcpLeaseTimeChanged(string value) => SaveToSession();
+
+ partial void OnDomainNameChanged(string value) => SaveToSession();
+
+ partial void OnNtpServerChanged(string value) => SaveToSession();
+
private void SaveToSession()
{
OnPropertyChanged(nameof(CanGoNext));
@@ -104,7 +124,10 @@ public partial class LanStepViewModel : WizardStepViewModelBase
DhcpPoolStart,
DhcpPoolEnd,
PrimaryDns,
- string.IsNullOrWhiteSpace(SecondaryDns) ? null : SecondaryDns);
+ string.IsNullOrWhiteSpace(SecondaryDns) ? null : SecondaryDns,
+ string.IsNullOrWhiteSpace(DhcpLeaseTime) ? null : DhcpLeaseTime,
+ string.IsNullOrWhiteSpace(DomainName) ? null : DomainName,
+ string.IsNullOrWhiteSpace(NtpServer) ? null : NtpServer);
_session.LanSettings = settings;
_session.LanActions = LanConfigurator.BuildActions(settings);
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs
new file mode 100644
index 0000000..7aa2b06
--- /dev/null
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs
@@ -0,0 +1,34 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace MikrotikInstaller.App.ViewModels.Steps;
+
+///
+/// Dritter Schritt (direkt nach der Geräte-Erkennung): Wahl zwischen dem geführten Assistenten
+/// (Standard, für die meisten Nutzer passend) und dem Experten-Modus, der in den folgenden
+/// Schritten zusätzliche, technischere RouterOS-Optionen einblendet. Die Wahl blockiert nie das
+/// Weitergehen — der geführte Assistent ist vorausgewählt.
+///
+public partial class ModeSelectionStepViewModel : WizardStepViewModelBase
+{
+ private readonly WizardSession _session;
+
+ public ModeSelectionStepViewModel(WizardSession session)
+ : base(
+ title: "Modus wählen",
+ description: "Der geführte Assistent passt für die meisten Haushalte. Kennst du dich mit RouterOS aus, kannst du den Experten-Modus wählen — die folgenden Schritte zeigen dann zusätzliche, technischere Einstellungen.")
+ {
+ _session = session;
+ _isExpertMode = session.IsExpertMode;
+ }
+
+ [ObservableProperty]
+ private bool _isExpertMode;
+
+ public override Task OnActivatedAsync()
+ {
+ IsExpertMode = _session.IsExpertMode;
+ return Task.CompletedTask;
+ }
+
+ partial void OnIsExpertModeChanged(bool value) => _session.IsExpertMode = value;
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/PortForwardEntry.cs b/src/MikrotikInstaller.App/ViewModels/Steps/PortForwardEntry.cs
index 9cf9d8a..cbef93a 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/PortForwardEntry.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/PortForwardEntry.cs
@@ -19,4 +19,17 @@ public partial class PortForwardEntry : ObservableObject
[ObservableProperty]
private string _internalPort = string.Empty;
+
+ // Experten-Felder — nur im Experten-Modus sichtbar.
+ [ObservableProperty]
+ private string _allowedSourceAddress = string.Empty;
+
+ [ObservableProperty]
+ private bool _logRule;
+
+ [ObservableProperty]
+ private string _externalPortRangeEnd = string.Empty;
+
+ [ObservableProperty]
+ private string _internalPortRangeEnd = string.Empty;
}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/VlanEntry.cs b/src/MikrotikInstaller.App/ViewModels/Steps/VlanEntry.cs
index ae52c0b..ea9e940 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/VlanEntry.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/VlanEntry.cs
@@ -19,4 +19,17 @@ public partial class VlanEntry : ObservableObject
[ObservableProperty]
private string _poolEnd = string.Empty;
+
+ // Experten-Felder — nur im Experten-Modus sichtbar.
+ [ObservableProperty]
+ private int _prefixLength = 24;
+
+ [ObservableProperty]
+ private bool _isolateFromLan;
+
+ [ObservableProperty]
+ private string _dnsServerOverride = string.Empty;
+
+ [ObservableProperty]
+ private string _dhcpLeaseTime = string.Empty;
}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/VlanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/VlanStepViewModel.cs
index b4d793b..c5ece60 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/VlanStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/VlanStepViewModel.cs
@@ -23,10 +23,13 @@ public partial class VlanStepViewModel : WizardStepViewModelBase
public ObservableCollection Vlans { get; } = [];
+ public bool IsExpertMode => _session.IsExpertMode;
+
public override bool CanGoNext => Vlans.All(IsValid);
public override Task OnActivatedAsync()
{
+ OnPropertyChanged(nameof(IsExpertMode));
SaveToSession();
return Task.CompletedTask;
}
@@ -76,7 +79,15 @@ public partial class VlanStepViewModel : WizardStepViewModelBase
}
var definitions = Vlans
- .Select(v => new VlanDefinition(int.Parse(v.VlanId), v.Name, $"{v.RouterAddress}/24", v.PoolStart, v.PoolEnd))
+ .Select(v => new VlanDefinition(
+ int.Parse(v.VlanId),
+ v.Name,
+ $"{v.RouterAddress}/{v.PrefixLength}",
+ v.PoolStart,
+ v.PoolEnd,
+ v.IsolateFromLan,
+ string.IsNullOrWhiteSpace(v.DnsServerOverride) ? null : v.DnsServerOverride,
+ string.IsNullOrWhiteSpace(v.DhcpLeaseTime) ? null : v.DhcpLeaseTime))
.ToList();
_session.VlanDefinitions = definitions;
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs
index 2ff0d92..4c181f2 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/WanStepViewModel.cs
@@ -37,12 +37,29 @@ public partial class WanStepViewModel : WizardStepViewModelBase
[ObservableProperty]
private string _staticGateway = string.Empty;
+ // Experten-Felder — nur im Experten-Modus sichtbar, Defaults entsprechen dem bisherigen Verhalten.
+ [ObservableProperty]
+ private bool _useDhcpDefaultRoute = true;
+
+ [ObservableProperty]
+ private bool _useDhcpDns = true;
+
+ [ObservableProperty]
+ private string _mtu = string.Empty;
+
+ [ObservableProperty]
+ private string _natSourceAddress = string.Empty;
+
+ public bool IsExpertMode => _session.IsExpertMode;
+
public override bool CanGoNext =>
!string.IsNullOrWhiteSpace(SelectedInterface)
&& (!UseStaticAddress || (!string.IsNullOrWhiteSpace(StaticAddress) && !string.IsNullOrWhiteSpace(StaticGateway)));
public override Task OnActivatedAsync()
{
+ OnPropertyChanged(nameof(IsExpertMode));
+
AvailableInterfaces.Clear();
foreach (var iface in _session.Device?.Interfaces.Where(i => i.Kind == InterfaceKind.Ethernet) ?? [])
{
@@ -62,6 +79,14 @@ public partial class WanStepViewModel : WizardStepViewModelBase
partial void OnStaticGatewayChanged(string value) => SaveToSession();
+ partial void OnUseDhcpDefaultRouteChanged(bool value) => SaveToSession();
+
+ partial void OnUseDhcpDnsChanged(bool value) => SaveToSession();
+
+ partial void OnMtuChanged(string value) => SaveToSession();
+
+ partial void OnNatSourceAddressChanged(string value) => SaveToSession();
+
private void SaveToSession()
{
OnPropertyChanged(nameof(CanGoNext));
@@ -75,9 +100,12 @@ public partial class WanStepViewModel : WizardStepViewModelBase
return;
}
+ var mtu = int.TryParse(Mtu, out var mtuValue) ? mtuValue : (int?)null;
+ var natSourceAddress = string.IsNullOrWhiteSpace(NatSourceAddress) ? null : NatSourceAddress;
+
var settings = UseStaticAddress
- ? new WanSettings(SelectedInterface!, WanAddressMode.Static, StaticAddress, StaticGateway)
- : new WanSettings(SelectedInterface!, WanAddressMode.Dhcp);
+ ? new WanSettings(SelectedInterface!, WanAddressMode.Static, StaticAddress, StaticGateway, UseDhcpDefaultRoute, UseDhcpDns, mtu, natSourceAddress)
+ : new WanSettings(SelectedInterface!, WanAddressMode.Dhcp, UseDhcpDefaultRoute: UseDhcpDefaultRoute, UseDhcpDns: UseDhcpDns, Mtu: mtu, NatSourceAddress: natSourceAddress);
_session.WanSettings = settings;
_session.WanActions = WanConfigurator.BuildActions(settings);
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/WlanStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/WlanStepViewModel.cs
index 9aac5b5..479a6d5 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/WlanStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/WlanStepViewModel.cs
@@ -36,6 +36,20 @@ public partial class WlanStepViewModel : WizardStepViewModelBase
[ObservableProperty]
private bool _skipWireless;
+ // Experten-Felder — nur im Experten-Modus sichtbar, Defaults entsprechen dem bisherigen Verhalten.
+ [ObservableProperty]
+ private WirelessSecurityMode _securityMode = WirelessSecurityMode.Wpa2;
+
+ [ObservableProperty]
+ private bool _hideSsid;
+
+ [ObservableProperty]
+ private string _band = string.Empty;
+
+ public IReadOnlyList SecurityModes { get; } = Enum.GetValues();
+
+ public bool IsExpertMode => _session.IsExpertMode;
+
public override bool ShouldSkip => _session.Device?.HasWireless != true;
public override bool CanGoNext =>
@@ -44,6 +58,8 @@ public partial class WlanStepViewModel : WizardStepViewModelBase
public override Task OnActivatedAsync()
{
+ OnPropertyChanged(nameof(IsExpertMode));
+
AvailableInterfaces.Clear();
foreach (var iface in _session.Device?.Interfaces.Where(i => i.Kind == InterfaceKind.Wireless) ?? [])
{
@@ -63,6 +79,12 @@ public partial class WlanStepViewModel : WizardStepViewModelBase
partial void OnSkipWirelessChanged(bool value) => SaveToSession();
+ partial void OnSecurityModeChanged(WirelessSecurityMode value) => SaveToSession();
+
+ partial void OnHideSsidChanged(bool value) => SaveToSession();
+
+ partial void OnBandChanged(string value) => SaveToSession();
+
private void SaveToSession()
{
OnPropertyChanged(nameof(CanGoNext));
@@ -74,7 +96,9 @@ public partial class WlanStepViewModel : WizardStepViewModelBase
return;
}
- var settings = new WirelessSettings(SelectedInterface!, Ssid, Password);
+ var settings = new WirelessSettings(
+ SelectedInterface!, Ssid, Password, SecurityMode, HideSsid,
+ string.IsNullOrWhiteSpace(Band) ? null : Band);
_session.WirelessSettings = settings;
_session.WirelessActions = WirelessConfigurator.BuildActions(settings);
}
diff --git a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
index 2858944..4a7a7aa 100644
--- a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
+++ b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
@@ -16,6 +16,12 @@ public sealed class WizardSession
public RouterDevice? Device { get; set; }
+ ///
+ /// Ob der Nutzer im Moduswahl-Schritt den Experten-Modus gewählt hat. Steuert, ob die
+ /// nachfolgenden Schritte zusätzliche, technischere Felder anzeigen. Standard: Laien-Modus (false).
+ ///
+ public bool IsExpertMode { get; set; }
+
/// Vom WAN-Schritt gewählte Internet-Schnittstelle, damit spätere Schritte sie referenzieren können.
public string? WanInterfaceName { get; set; }
diff --git a/src/MikrotikInstaller.App/Views/Steps/FirewallStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/FirewallStepView.xaml
index 9778b65..6a7177a 100644
--- a/src/MikrotikInstaller.App/Views/Steps/FirewallStepView.xaml
+++ b/src/MikrotikInstaller.App/Views/Steps/FirewallStepView.xaml
@@ -75,13 +75,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml
index 64f6513..af54840 100644
--- a/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml
+++ b/src/MikrotikInstaller.App/Views/Steps/LanStepView.xaml
@@ -87,5 +87,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml
new file mode 100644
index 0000000..422112a
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml.cs b/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml.cs
new file mode 100644
index 0000000..49f85d8
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/Steps/ModeSelectionStepView.xaml.cs
@@ -0,0 +1,11 @@
+using System.Windows.Controls;
+
+namespace MikrotikInstaller.App.Views.Steps;
+
+public partial class ModeSelectionStepView : UserControl
+{
+ public ModeSelectionStepView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/MikrotikInstaller.App/Views/Steps/VlanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/VlanStepView.xaml
index 4cd3829..c2ed645 100644
--- a/src/MikrotikInstaller.App/Views/Steps/VlanStepView.xaml
+++ b/src/MikrotikInstaller.App/Views/Steps/VlanStepView.xaml
@@ -63,6 +63,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml
index df0af09..710b04f 100644
--- a/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml
+++ b/src/MikrotikInstaller.App/Views/Steps/WanStepView.xaml
@@ -35,5 +35,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/Steps/WlanStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/WlanStepView.xaml
index 8831050..148a651 100644
--- a/src/MikrotikInstaller.App/Views/Steps/WlanStepView.xaml
+++ b/src/MikrotikInstaller.App/Views/Steps/WlanStepView.xaml
@@ -32,6 +32,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/FirewallConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/FirewallConfiguratorTests.cs
index 1e2052d..f402514 100644
--- a/src/MikrotikInstaller.Core.Tests/Configuration/FirewallConfiguratorTests.cs
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/FirewallConfiguratorTests.cs
@@ -47,6 +47,44 @@ public class FirewallConfiguratorTests
Assert.True(allowIndex >= 0 && dropIndex >= 0 && allowIndex < dropIndex);
}
+ [Fact]
+ public async Task BuildActions_ExpertOptions_AppliesSourceAddressLogAndPortRange()
+ {
+ var settings = new FirewallSettings("ether1", "bridge-lan",
+ [new PortForward("Spieleserver", "udp", 25565, "192.168.88.50", 25565,
+ AllowedSourceAddress: "203.0.113.0/24", LogRule: true, ExternalPortRangeEnd: 25575, InternalPortRangeEnd: 25575)]);
+ var actions = FirewallConfigurator.BuildActions(settings);
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/firewall/nat"
+ && c.Parameters["src-address"] == "203.0.113.0/24" && c.Parameters["log"] == "yes"
+ && c.Parameters["dst-port"] == "25565-25575" && c.Parameters["to-ports"] == "25565-25575");
+ }
+
+ [Fact]
+ public async Task BuildActions_WithBlockedAddresses_AddsAddressListAndDropRules()
+ {
+ var settings = new FirewallSettings("ether1", "bridge-lan", [], BlockedAddresses: ["203.0.113.5", "198.51.100.0/24"]);
+ var actions = FirewallConfigurator.BuildActions(settings);
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Equal(2, client.AddCalls.Count(c => c.Path == "/ip/firewall/address-list" && c.Parameters["list"] == "blocked"));
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/firewall/filter"
+ && c.Parameters["chain"] == "input" && c.Parameters.GetValueOrDefault("src-address-list") == "blocked" && c.Parameters["action"] == "drop");
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/firewall/filter"
+ && c.Parameters["chain"] == "forward" && c.Parameters.GetValueOrDefault("src-address-list") == "blocked" && c.Parameters["action"] == "drop");
+ }
+
[Fact]
public void BuildFriendlySummary_WithoutPortForwards_OnlyMentionsProtection()
{
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs
index c4b5dcd..1c8701e 100644
--- a/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/LanConfiguratorTests.cs
@@ -47,6 +47,24 @@ public class LanConfiguratorTests
&& c.Parameters!["servers"] == "1.1.1.1,8.8.8.8" && c.Parameters["allow-remote-requests"] == "yes");
}
+ [Fact]
+ public async Task BuildActions_ExpertOptions_AppliesLeaseTimeDomainNameAndNtpServer()
+ {
+ var settings = DefaultSettings() with { DhcpLeaseTime = "1d", DomainName = "heim.lan", NtpServer = "pool.ntp.org" };
+ var actions = LanConfigurator.BuildActions(settings);
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server" && c.Parameters["lease-time"] == "1d");
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server/network" && c.Parameters["domain-name"] == "heim.lan");
+ Assert.Contains(client.ExecuteCalls, c => c.Path == "/system/ntp/client/set"
+ && c.Parameters!["enabled"] == "yes" && c.Parameters["servers"] == "pool.ntp.org");
+ }
+
[Fact]
public void BuildFriendlySummary_MentionsNetworkAndMemberInterfaces()
{
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/VlanConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/VlanConfiguratorTests.cs
index ccb37ee..5af4950 100644
--- a/src/MikrotikInstaller.Core.Tests/Configuration/VlanConfiguratorTests.cs
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/VlanConfiguratorTests.cs
@@ -27,6 +27,30 @@ public class VlanConfiguratorTests
Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server/network" && c.Parameters["address"] == "192.168.20.0/24");
}
+ [Fact]
+ public async Task BuildActions_ExpertOptions_AppliesDnsOverrideLeaseTimeAndIsolation()
+ {
+ var vlans = new List
+ {
+ new(VlanId: 20, Name: "Gäste", RouterAddressCidr: "192.168.20.1/24", DhcpPoolStart: "192.168.20.10", DhcpPoolEnd: "192.168.20.254",
+ IsolateFromLan: true, DnsServerOverride: "9.9.9.9", DhcpLeaseTime: "12h"),
+ };
+ var actions = VlanConfigurator.BuildActions("bridge-lan", vlans);
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server" && c.Parameters["lease-time"] == "12h");
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-server/network" && c.Parameters["dns-server"] == "9.9.9.9");
+
+ var isolationDrops = client.AddCalls.Where(c => c.Path == "/ip/firewall/filter" && c.Parameters["action"] == "drop").ToList();
+ Assert.Contains(isolationDrops, c => c.Parameters["in-interface"] == "vlan20" && c.Parameters["out-interface"] == "bridge-lan");
+ Assert.Contains(isolationDrops, c => c.Parameters["in-interface"] == "bridge-lan" && c.Parameters["out-interface"] == "vlan20");
+ }
+
[Fact]
public void BuildActions_NoVlans_ReturnsEmptyList()
{
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs
index 6234fa0..b378fe4 100644
--- a/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/WanConfiguratorTests.cs
@@ -46,6 +46,34 @@ public class WanConfiguratorTests
&& c.Parameters["action"] == "masquerade" && c.Parameters["out-interface"] == "ether1");
}
+ [Fact]
+ public async Task BuildActions_ExpertOptions_AppliesDhcpFlagsMtuAndNatSourceAddress()
+ {
+ var actions = WanConfigurator.BuildActions(new WanSettings(
+ "ether1", WanAddressMode.Dhcp,
+ UseDhcpDefaultRoute: false, UseDhcpDns: false, Mtu: 1492, NatSourceAddress: "192.168.88.0/24"));
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/dhcp-client"
+ && c.Parameters["add-default-route"] == "no" && c.Parameters["use-peer-dns"] == "no");
+ Assert.Contains(client.AddCalls, c => c.Path == "/ip/firewall/nat" && c.Parameters["src-address"] == "192.168.88.0/24");
+ Assert.Contains(client.ExecuteCalls, c => c.Path == "/interface/ethernet/set"
+ && c.Parameters!["numbers"] == "ether1" && c.Parameters["mtu"] == "1492");
+ }
+
+ [Fact]
+ public void BuildActions_WithoutExpertOptions_MatchesPreviousBehavior()
+ {
+ var actions = WanConfigurator.BuildActions(new WanSettings("ether1", WanAddressMode.Dhcp));
+
+ Assert.Equal(2, actions.Count);
+ }
+
[Fact]
public void BuildFriendlySummary_ContainsNoRouterOsJargon()
{
diff --git a/src/MikrotikInstaller.Core.Tests/Configuration/WirelessConfiguratorTests.cs b/src/MikrotikInstaller.Core.Tests/Configuration/WirelessConfiguratorTests.cs
index 0a1ee41..d3f23ea 100644
--- a/src/MikrotikInstaller.Core.Tests/Configuration/WirelessConfiguratorTests.cs
+++ b/src/MikrotikInstaller.Core.Tests/Configuration/WirelessConfiguratorTests.cs
@@ -21,6 +21,24 @@ public class WirelessConfiguratorTests
&& c.Parameters!["numbers"] == "wlan1" && c.Parameters["ssid"] == "Mein-WLAN" && c.Parameters["disabled"] == "no");
}
+ [Fact]
+ public async Task BuildActions_ExpertOptions_AppliesWpa3HideSsidAndBand()
+ {
+ var actions = WirelessConfigurator.BuildActions(new WirelessSettings(
+ "wlan1", "Mein-WLAN", "sicheres-passwort", WirelessSecurityMode.Wpa3, HideSsid: true, Band: "5ghz-a/n/ac"));
+ var client = new RecordingRouterOsClient();
+
+ foreach (var action in actions)
+ {
+ await action.ApplyAsync(client, CancellationToken.None);
+ }
+
+ Assert.Contains(client.AddCalls, c => c.Path == "/interface/wireless/security-profiles"
+ && c.Parameters["authentication-types"] == "wpa3-psk" && c.Parameters["wpa3-pre-shared-key"] == "sicheres-passwort");
+ Assert.Contains(client.ExecuteCalls, c => c.Path == "/interface/wireless/set"
+ && c.Parameters!["hide-ssid"] == "yes" && c.Parameters["band"] == "5ghz-a/n/ac");
+ }
+
[Fact]
public void BuildFriendlySummary_MentionsSsidWithoutExposingPassword()
{
diff --git a/src/MikrotikInstaller.Core/Configuration/FirewallConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/FirewallConfigurator.cs
index d1d4ff8..4074408 100644
--- a/src/MikrotikInstaller.Core/Configuration/FirewallConfigurator.cs
+++ b/src/MikrotikInstaller.Core/Configuration/FirewallConfigurator.cs
@@ -25,32 +25,75 @@ public static class FirewallConfigurator
new Dictionary { ["in-interface"] = settings.LanBridgeName, ["action"] = "accept" }),
};
+ if (settings.BlockedAddresses is { Count: > 0 } blockedAddresses)
+ {
+ const string blockListName = "blocked";
+
+ foreach (var address in blockedAddresses)
+ {
+ actions.Add(new ConfigurationAction(
+ $"„{address}\" zur Sperrliste hinzufügen",
+ (client, ct) => client.AddAsync("/ip/firewall/address-list", new Dictionary
+ {
+ ["list"] = blockListName,
+ ["address"] = address,
+ }, ct)));
+ }
+
+ actions.Add(InputRule(
+ "Zugriff auf die Router-Einstellungen von gesperrten Adressen blockieren",
+ new Dictionary { ["src-address-list"] = blockListName, ["action"] = "drop" }));
+ actions.Add(ForwardRule(
+ "Datenverkehr von gesperrten Adressen blockieren",
+ new Dictionary { ["src-address-list"] = blockListName, ["action"] = "drop" }));
+ }
+
// Portfreigaben müssen vor der abschließenden Sperrregel stehen, damit sie greifen.
foreach (var forward in settings.PortForwards)
{
+ var externalPort = forward.ExternalPortRangeEnd is int externalEnd
+ ? $"{forward.ExternalPort}-{externalEnd}"
+ : forward.ExternalPort.ToString();
+ var internalPort = forward.InternalPortRangeEnd is int internalEnd
+ ? $"{forward.InternalPort}-{internalEnd}"
+ : forward.InternalPort.ToString();
+
+ var natParameters = new Dictionary
+ {
+ ["chain"] = "dstnat",
+ ["protocol"] = forward.Protocol,
+ ["dst-port"] = externalPort,
+ ["in-interface"] = settings.WanInterfaceName,
+ ["action"] = "dst-nat",
+ ["to-addresses"] = forward.TargetAddress,
+ ["to-ports"] = internalPort,
+ };
+ var filterParameters = new Dictionary
+ {
+ ["chain"] = "forward",
+ ["protocol"] = forward.Protocol,
+ ["dst-address"] = forward.TargetAddress,
+ ["dst-port"] = internalPort,
+ ["action"] = "accept",
+ };
+ if (!string.IsNullOrWhiteSpace(forward.AllowedSourceAddress))
+ {
+ natParameters["src-address"] = forward.AllowedSourceAddress;
+ filterParameters["src-address"] = forward.AllowedSourceAddress;
+ }
+ if (forward.LogRule)
+ {
+ natParameters["log"] = "yes";
+ filterParameters["log"] = "yes";
+ }
+
actions.Add(new ConfigurationAction(
- $"Port {forward.ExternalPort}/{forward.Protocol.ToUpperInvariant()} von außen zu {forward.TargetAddress}:{forward.InternalPort} weiterleiten („{forward.Name}\")",
- (client, ct) => client.AddAsync("/ip/firewall/nat", new Dictionary
- {
- ["chain"] = "dstnat",
- ["protocol"] = forward.Protocol,
- ["dst-port"] = forward.ExternalPort.ToString(),
- ["in-interface"] = settings.WanInterfaceName,
- ["action"] = "dst-nat",
- ["to-addresses"] = forward.TargetAddress,
- ["to-ports"] = forward.InternalPort.ToString(),
- }, ct)));
+ $"Port {externalPort}/{forward.Protocol.ToUpperInvariant()} von außen zu {forward.TargetAddress}:{internalPort} weiterleiten („{forward.Name}\")",
+ (client, ct) => client.AddAsync("/ip/firewall/nat", natParameters, ct)));
actions.Add(new ConfigurationAction(
$"Weitergeleiteten Datenverkehr für „{forward.Name}\" durch die Firewall lassen",
- (client, ct) => client.AddAsync("/ip/firewall/filter", new Dictionary
- {
- ["chain"] = "forward",
- ["protocol"] = forward.Protocol,
- ["dst-address"] = forward.TargetAddress,
- ["dst-port"] = forward.InternalPort.ToString(),
- ["action"] = "accept",
- }, ct)));
+ (client, ct) => client.AddAsync("/ip/firewall/filter", filterParameters, ct)));
}
actions.Add(ForwardRule(
diff --git a/src/MikrotikInstaller.Core/Configuration/FirewallSettings.cs b/src/MikrotikInstaller.Core/Configuration/FirewallSettings.cs
index a85bf9e..b284b59 100644
--- a/src/MikrotikInstaller.Core/Configuration/FirewallSettings.cs
+++ b/src/MikrotikInstaller.Core/Configuration/FirewallSettings.cs
@@ -1,9 +1,21 @@
namespace MikrotikInstaller.Core.Configuration;
/// Eine Portfreigabe: leitet einen Port von außen zu einem bestimmten Gerät im Heimnetz weiter.
-public sealed record PortForward(string Name, string Protocol, int ExternalPort, string TargetAddress, int InternalPort);
+public sealed record PortForward(
+ string Name,
+ string Protocol,
+ int ExternalPort,
+ string TargetAddress,
+ int InternalPort,
+ // Experten-Optionen — null/false bedeutet: bisheriges Verhalten (von überall erreichbar, keine Log-Einträge).
+ string? AllowedSourceAddress = null,
+ bool LogRule = false,
+ int? ExternalPortRangeEnd = null,
+ int? InternalPortRangeEnd = null);
public sealed record FirewallSettings(
string WanInterfaceName,
string LanBridgeName,
- IReadOnlyList PortForwards);
+ IReadOnlyList PortForwards,
+ // Experten-Option: CIDR-Adressen/-Bereiche, die grundsätzlich blockiert werden (leer = kein Sperrlisten-Eintrag, wie bisher).
+ IReadOnlyList? BlockedAddresses = null);
diff --git a/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs
index ae17ab1..93d53ea 100644
--- a/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs
+++ b/src/MikrotikInstaller.Core/Configuration/LanConfigurator.cs
@@ -48,24 +48,36 @@ public static class LanConfigurator
["ranges"] = $"{settings.DhcpPoolStart}-{settings.DhcpPoolEnd}",
}, ct)));
+ var dhcpServerParameters = new Dictionary
+ {
+ ["name"] = dhcpServerName,
+ ["interface"] = settings.BridgeName,
+ ["address-pool"] = poolName,
+ ["disabled"] = "no",
+ };
+ if (!string.IsNullOrWhiteSpace(settings.DhcpLeaseTime))
+ {
+ dhcpServerParameters["lease-time"] = settings.DhcpLeaseTime;
+ }
+
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)));
+ (client, ct) => client.AddAsync("/ip/dhcp-server", dhcpServerParameters, ct)));
+
+ var networkParameters = new Dictionary
+ {
+ ["address"] = networkCidr,
+ ["gateway"] = routerAddress,
+ ["dns-server"] = routerAddress,
+ };
+ if (!string.IsNullOrWhiteSpace(settings.DomainName))
+ {
+ networkParameters["domain-name"] = settings.DomainName;
+ }
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)));
+ (client, ct) => client.AddAsync("/ip/dhcp-server/network", networkParameters, ct)));
var upstreamDns = settings.SecondaryDnsServer is null
? settings.PrimaryDnsServer
@@ -79,6 +91,17 @@ public static class LanConfigurator
["allow-remote-requests"] = "yes",
}, ct)));
+ if (!string.IsNullOrWhiteSpace(settings.NtpServer))
+ {
+ actions.Add(new ConfigurationAction(
+ $"Zeitserver (NTP) {settings.NtpServer} einrichten",
+ (client, ct) => client.ExecuteAsync("/system/ntp/client/set", new Dictionary
+ {
+ ["enabled"] = "yes",
+ ["servers"] = settings.NtpServer,
+ }, ct)));
+ }
+
return actions;
}
diff --git a/src/MikrotikInstaller.Core/Configuration/LanSettings.cs b/src/MikrotikInstaller.Core/Configuration/LanSettings.cs
index 57f34b9..f6d901d 100644
--- a/src/MikrotikInstaller.Core/Configuration/LanSettings.cs
+++ b/src/MikrotikInstaller.Core/Configuration/LanSettings.cs
@@ -7,4 +7,8 @@ public sealed record LanSettings(
string DhcpPoolStart,
string DhcpPoolEnd,
string PrimaryDnsServer,
- string? SecondaryDnsServer);
+ string? SecondaryDnsServer,
+ // Experten-Optionen — null/leer bedeutet: RouterOS-Standardverhalten, wie bisher.
+ string? DhcpLeaseTime = null,
+ string? DomainName = null,
+ string? NtpServer = null);
diff --git a/src/MikrotikInstaller.Core/Configuration/VlanConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/VlanConfigurator.cs
index 27f85ee..5bf047a 100644
--- a/src/MikrotikInstaller.Core/Configuration/VlanConfigurator.cs
+++ b/src/MikrotikInstaller.Core/Configuration/VlanConfigurator.cs
@@ -43,15 +43,21 @@ public static class VlanConfigurator
["ranges"] = $"{vlan.DhcpPoolStart}-{vlan.DhcpPoolEnd}",
}, ct)));
+ var dhcpServerParameters = new Dictionary
+ {
+ ["name"] = dhcpServerName,
+ ["interface"] = interfaceName,
+ ["address-pool"] = poolName,
+ ["disabled"] = "no",
+ };
+ if (!string.IsNullOrWhiteSpace(vlan.DhcpLeaseTime))
+ {
+ dhcpServerParameters["lease-time"] = vlan.DhcpLeaseTime;
+ }
+
actions.Add(new ConfigurationAction(
$"DHCP-Server für VLAN „{vlan.Name}\" aktivieren",
- (client, ct) => client.AddAsync("/ip/dhcp-server", new Dictionary
- {
- ["name"] = dhcpServerName,
- ["interface"] = interfaceName,
- ["address-pool"] = poolName,
- ["disabled"] = "no",
- }, ct)));
+ (client, ct) => client.AddAsync("/ip/dhcp-server", dhcpServerParameters, ct)));
actions.Add(new ConfigurationAction(
$"Netzwerk-Einstellungen (Gateway {routerAddress}) für VLAN „{vlan.Name}\" hinterlegen",
@@ -59,8 +65,31 @@ public static class VlanConfigurator
{
["address"] = networkCidr,
["gateway"] = routerAddress,
- ["dns-server"] = routerAddress,
+ ["dns-server"] = string.IsNullOrWhiteSpace(vlan.DnsServerOverride) ? routerAddress : vlan.DnsServerOverride,
}, ct)));
+
+ if (vlan.IsolateFromLan)
+ {
+ actions.Add(new ConfigurationAction(
+ $"VLAN „{vlan.Name}\" vom Heimnetz „{parentBridgeName}\" isolieren",
+ (client, ct) => client.AddAsync("/ip/firewall/filter", new Dictionary
+ {
+ ["chain"] = "forward",
+ ["in-interface"] = interfaceName,
+ ["out-interface"] = parentBridgeName,
+ ["action"] = "drop",
+ }, ct)));
+
+ actions.Add(new ConfigurationAction(
+ $"Heimnetz „{parentBridgeName}\" vor Zugriff aus VLAN „{vlan.Name}\" schützen",
+ (client, ct) => client.AddAsync("/ip/firewall/filter", new Dictionary
+ {
+ ["chain"] = "forward",
+ ["in-interface"] = parentBridgeName,
+ ["out-interface"] = interfaceName,
+ ["action"] = "drop",
+ }, ct)));
+ }
}
return actions;
diff --git a/src/MikrotikInstaller.Core/Configuration/VlanSettings.cs b/src/MikrotikInstaller.Core/Configuration/VlanSettings.cs
index 40244ba..32bf7d0 100644
--- a/src/MikrotikInstaller.Core/Configuration/VlanSettings.cs
+++ b/src/MikrotikInstaller.Core/Configuration/VlanSettings.cs
@@ -6,4 +6,8 @@ public sealed record VlanDefinition(
string Name,
string RouterAddressCidr,
string DhcpPoolStart,
- string DhcpPoolEnd);
+ string DhcpPoolEnd,
+ // Experten-Optionen — Defaults entsprechen dem bisherigen (Laien-)Verhalten.
+ bool IsolateFromLan = false,
+ string? DnsServerOverride = null,
+ string? DhcpLeaseTime = null);
diff --git a/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs
index ac7a99e..b517d4d 100644
--- a/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs
+++ b/src/MikrotikInstaller.Core/Configuration/WanConfigurator.cs
@@ -15,6 +15,8 @@ public static class WanConfigurator
{
["interface"] = settings.InterfaceName,
["disabled"] = "no",
+ ["add-default-route"] = settings.UseDhcpDefaultRoute ? "yes" : "no",
+ ["use-peer-dns"] = settings.UseDhcpDns ? "yes" : "no",
}, ct)));
}
else
@@ -36,14 +38,33 @@ public static class WanConfigurator
}, ct)));
}
+ var natParameters = new Dictionary
+ {
+ ["chain"] = "srcnat",
+ ["out-interface"] = settings.InterfaceName,
+ ["action"] = "masquerade",
+ };
+ if (settings.NatSourceAddress is not null)
+ {
+ natParameters["src-address"] = settings.NatSourceAddress;
+ }
+
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)));
+ settings.NatSourceAddress is null
+ ? $"Datenverkehr aus deinem Heimnetz über „{settings.InterfaceName}\" ins Internet weiterleiten (NAT)"
+ : $"Datenverkehr aus {settings.NatSourceAddress} über „{settings.InterfaceName}\" ins Internet weiterleiten (NAT)",
+ (client, ct) => client.AddAsync("/ip/firewall/nat", natParameters, ct)));
+
+ if (settings.Mtu is not null)
+ {
+ actions.Add(new ConfigurationAction(
+ $"MTU auf „{settings.InterfaceName}\" auf {settings.Mtu} setzen",
+ (client, ct) => client.ExecuteAsync("/interface/ethernet/set", new Dictionary
+ {
+ ["numbers"] = settings.InterfaceName,
+ ["mtu"] = settings.Mtu.Value.ToString(),
+ }, ct)));
+ }
return actions;
}
diff --git a/src/MikrotikInstaller.Core/Configuration/WanSettings.cs b/src/MikrotikInstaller.Core/Configuration/WanSettings.cs
index c5dbf8e..871124b 100644
--- a/src/MikrotikInstaller.Core/Configuration/WanSettings.cs
+++ b/src/MikrotikInstaller.Core/Configuration/WanSettings.cs
@@ -13,4 +13,9 @@ public sealed record WanSettings(
string InterfaceName,
WanAddressMode Mode,
string? StaticAddressCidr = null,
- string? StaticGateway = null);
+ string? StaticGateway = null,
+ // Experten-Optionen — Defaults entsprechen dem bisherigen (Laien-)Verhalten.
+ bool UseDhcpDefaultRoute = true,
+ bool UseDhcpDns = true,
+ int? Mtu = null,
+ string? NatSourceAddress = null);
diff --git a/src/MikrotikInstaller.Core/Configuration/WirelessConfigurator.cs b/src/MikrotikInstaller.Core/Configuration/WirelessConfigurator.cs
index 3e83bb0..bba1f44 100644
--- a/src/MikrotikInstaller.Core/Configuration/WirelessConfigurator.cs
+++ b/src/MikrotikInstaller.Core/Configuration/WirelessConfigurator.cs
@@ -6,34 +6,57 @@ public static class WirelessConfigurator
public static IReadOnlyList BuildActions(WirelessSettings settings)
{
var profileName = $"profile-{settings.InterfaceName}";
+ var authenticationTypes = settings.SecurityMode switch
+ {
+ WirelessSecurityMode.Wpa3 => "wpa3-psk",
+ WirelessSecurityMode.Wpa2Wpa3Mixed => "wpa2-psk,wpa3-psk",
+ _ => "wpa2-psk",
+ };
+
+ var setParameters = new Dictionary
+ {
+ ["numbers"] = settings.InterfaceName,
+ ["ssid"] = settings.Ssid,
+ ["security-profile"] = profileName,
+ ["disabled"] = "no",
+ ["hide-ssid"] = settings.HideSsid ? "yes" : "no",
+ };
+ if (!string.IsNullOrWhiteSpace(settings.Band))
+ {
+ setParameters["band"] = settings.Band;
+ }
+
+ var securityProfileParameters = new Dictionary
+ {
+ ["name"] = profileName,
+ ["mode"] = "dynamic-keys",
+ ["authentication-types"] = authenticationTypes,
+ ["wpa2-pre-shared-key"] = settings.Password,
+ };
+ if (settings.SecurityMode is WirelessSecurityMode.Wpa3 or WirelessSecurityMode.Wpa2Wpa3Mixed)
+ {
+ securityProfileParameters["wpa3-pre-shared-key"] = settings.Password;
+ }
return
[
new ConfigurationAction(
- $"WLAN-Sicherheitsprofil für „{settings.Ssid}\" anlegen (WPA2)",
- (client, ct) => client.AddAsync("/interface/wireless/security-profiles", new Dictionary
- {
- ["name"] = profileName,
- ["mode"] = "dynamic-keys",
- ["authentication-types"] = "wpa2-psk",
- ["wpa2-pre-shared-key"] = settings.Password,
- }, ct)),
+ $"WLAN-Sicherheitsprofil für „{settings.Ssid}\" anlegen ({authenticationTypes})",
+ (client, ct) => client.AddAsync("/interface/wireless/security-profiles", securityProfileParameters, ct)),
// "numbers" ist die RouterOS-API-Konvention, um einen Eintrag anhand seines Namens statt
// seiner internen .id zu adressieren — vermeidet einen zusätzlichen Nachschlage-Aufruf.
new ConfigurationAction(
$"WLAN „{settings.Ssid}\" auf „{settings.InterfaceName}\" aktivieren",
- (client, ct) => client.ExecuteAsync("/interface/wireless/set", new Dictionary
- {
- ["numbers"] = settings.InterfaceName,
- ["ssid"] = settings.Ssid,
- ["security-profile"] = profileName,
- ["disabled"] = "no",
- }, ct)),
+ (client, ct) => client.ExecuteAsync("/interface/wireless/set", setParameters, ct)),
];
}
/// Kurze, laientaugliche Zusammenfassung für den Zusammenfassungs-Schritt (keine RouterOS-Fachbegriffe).
public static IReadOnlyList BuildFriendlySummary(WirelessSettings settings) =>
- [$"WLAN „{settings.Ssid}\" ist eingerichtet und mit einem Passwort geschützt."];
+ [
+ settings.HideSsid
+ ? $"WLAN „{settings.Ssid}\" ist eingerichtet, mit einem Passwort geschützt und in der Netzwerkliste unsichtbar (muss manuell eingegeben werden)."
+ : $"WLAN „{settings.Ssid}\" ist eingerichtet und mit einem Passwort geschützt.",
+ ];
}
diff --git a/src/MikrotikInstaller.Core/Configuration/WirelessSecurityMode.cs b/src/MikrotikInstaller.Core/Configuration/WirelessSecurityMode.cs
new file mode 100644
index 0000000..833211c
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Configuration/WirelessSecurityMode.cs
@@ -0,0 +1,14 @@
+namespace MikrotikInstaller.Core.Configuration;
+
+/// WLAN-Sicherheitsstandard für das erzeugte Sicherheitsprofil.
+public enum WirelessSecurityMode
+{
+ /// WPA2-PSK — kompatibel mit praktisch jedem Gerät, bisheriger Standardwert.
+ Wpa2,
+
+ /// WPA3-PSK — moderner, sicherer, aber nicht von jedem älteren Gerät unterstützt.
+ Wpa3,
+
+ /// WPA2/WPA3 gemischt — ältere und neuere Geräte können sich gleichzeitig verbinden.
+ Wpa2Wpa3Mixed,
+}
diff --git a/src/MikrotikInstaller.Core/Configuration/WirelessSettings.cs b/src/MikrotikInstaller.Core/Configuration/WirelessSettings.cs
index 5a86ea5..b389e6d 100644
--- a/src/MikrotikInstaller.Core/Configuration/WirelessSettings.cs
+++ b/src/MikrotikInstaller.Core/Configuration/WirelessSettings.cs
@@ -1,3 +1,10 @@
namespace MikrotikInstaller.Core.Configuration;
-public sealed record WirelessSettings(string InterfaceName, string Ssid, string Password);
+public sealed record WirelessSettings(
+ string InterfaceName,
+ string Ssid,
+ string Password,
+ // Experten-Optionen — Defaults entsprechen dem bisherigen (Laien-)Verhalten (WPA2, sichtbare SSID, Auto-Band).
+ WirelessSecurityMode SecurityMode = WirelessSecurityMode.Wpa2,
+ bool HideSsid = false,
+ string? Band = null);